diff --git a/package.json b/package.json index 2d3f3d3599..d909d2d0e4 100644 --- a/package.json +++ b/package.json @@ -155,7 +155,7 @@ "@react-spring/web": "10.0.3", "@react-types/shared": "3.33.1", "@signalapp/minimask": "1.0.1", - "@signalapp/mock-server": "19.1.0", + "@signalapp/mock-server": "20.0.1", "@signalapp/quill-cjs": "2.1.2", "@storybook/addon-a11y": "8.4.4", "@storybook/addon-actions": "8.4.4", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9416ce4744..91de62c686 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -185,8 +185,8 @@ importers: specifier: 1.0.1 version: 1.0.1 '@signalapp/mock-server': - specifier: 19.1.0 - version: 19.1.0(bufferutil@4.0.9)(utf-8-validate@5.0.10) + specifier: 20.0.1 + version: 20.0.1(bufferutil@4.0.9)(utf-8-validate@5.0.10) '@signalapp/quill-cjs': specifier: 2.1.2 version: 2.1.2 @@ -1692,14 +1692,14 @@ packages: peerDependencies: prettier: ^3.0.0 - '@indutny/protopiler@3.2.1': - resolution: {integrity: sha512-u3ZVLus/9wwkojVSO0es5tppdd+2f9zA6ogax3OytOwGueDWSj+eVU6sSWYiSY/zxMSgdnKxRik0D3D36u3iWw==} - hasBin: true - '@indutny/protopiler@3.2.4': resolution: {integrity: sha512-uBz01wZxVz8+9DeGKN0QPvViDPq0Agcs5pAF8CkivULFP/sFGWpWaC9nPWIuVC9y44RrPQS+xpp1pFYlnX7BVg==} hasBin: true + '@indutny/protopiler@4.0.0': + resolution: {integrity: sha512-QGVXaIBNXb0QwGHeWsbIdhwph72hYpYeE2M753Y91LGUTvBumFUMDEGBT8HQk1y4c2l80nvFAMGFFCWCCtk5gw==} + hasBin: true + '@indutny/range-finder@1.3.4': resolution: {integrity: sha512-UWNRZZCp3f97NJuhP0IgBktob+kPDFsnyKx42PDwPQsAa/ZczyeLdhfz3tD8zK1wVC+OkOXAM6YkBAgEv9Xhaw==} @@ -3787,8 +3787,8 @@ packages: '@signalapp/minimask@1.0.1': resolution: {integrity: sha512-QAwo0joA60urTNbW9RIz6vLKQjy+jdVtH7cvY0wD9PVooD46MAjE40MLssp4xUJrph91n2XvtJ3pbEUDrmT2AA==} - '@signalapp/mock-server@19.1.0': - resolution: {integrity: sha512-NTOLGe/G+ptVd7xUOTJiWMn5CvxoLcalSlt5ePCFwtyDiJ4HFXR6O2d9f42Elm5PpLtbPULre5G1AeqijmyzUA==} + '@signalapp/mock-server@20.0.1': + resolution: {integrity: sha512-hpWuhfl0u1yX2KsJ9wa74tR/P3/eDDfZMRnlczeaty/q6EKxk8GP2HKkI13apj/redw0fJxq/fMdIg0qoBxM0Q==} '@signalapp/parchment-cjs@3.0.1': resolution: {integrity: sha512-hSBMQ1M7wE4GcC8ZeNtvpJF+DAJg3eIRRf1SiHS3I3Algav/sgJJNm6HIYm6muHuK7IJmuEjkL3ILSXgmu0RfQ==} @@ -6874,8 +6874,8 @@ packages: intl-tel-input@26.8.1: resolution: {integrity: sha512-6UBRifs+nGtlS03SZGxxb9uVX7MTo1ePNJ4zIxO+qiwVwJ6fW7gYTcvYVFKjBn7jlbPw2ZqjGeNLaSUEwmDvIA==} - ioredis@5.10.0: - resolution: {integrity: sha512-HVBe9OFuqs+Z6n64q09PQvP1/R4Bm+30PAyyD4wIEqssh3v9L21QjCVk4kRLucMBcDokJTcLjsGeVRlq/nH6DA==} + ioredis@5.10.1: + resolution: {integrity: sha512-HuEDBTI70aYdx1v6U97SbNx9F1+svQKBDo30o0b9fw055LMepzpOOd0Ccg9Q6tbqmBSJaMuY0fB7yw9/vjBYCA==} engines: {node: '>=12.22.0'} ip-address@9.0.5: @@ -11293,10 +11293,10 @@ snapshots: prettier: 3.8.1 rxjs: 6.6.7 - '@indutny/protopiler@3.2.1': {} - '@indutny/protopiler@3.2.4': {} + '@indutny/protopiler@4.0.0': {} + '@indutny/range-finder@1.3.4': dependencies: readable-stream: 4.7.0 @@ -13851,10 +13851,10 @@ snapshots: '@signalapp/minimask@1.0.1': {} - '@signalapp/mock-server@19.1.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)': + '@signalapp/mock-server@20.0.1(bufferutil@4.0.9)(utf-8-validate@5.0.10)': dependencies: '@indutny/parallel-prettier': 3.0.0(prettier@3.8.1) - '@indutny/protopiler': 3.2.1 + '@indutny/protopiler': 4.0.0 '@signalapp/libsignal-client': 0.89.2 '@tus/file-store': 1.5.1 '@tus/server': 1.10.2 @@ -14439,7 +14439,7 @@ snapshots: lodash.throttle: 4.1.1 optionalDependencies: '@redis/client': 1.6.1 - ioredis: 5.10.0 + ioredis: 5.10.1 transitivePeerDependencies: - supports-color @@ -17547,7 +17547,7 @@ snapshots: intl-tel-input@26.8.1: {} - ioredis@5.10.0: + ioredis@5.10.1: dependencies: '@ioredis/commands': 1.5.1 cluster-key-slot: 1.1.2 diff --git a/ts/LibsignalNetRemoteConfig.preload.ts b/ts/LibsignalNetRemoteConfig.preload.ts index 970dbfe1f0..e4b55cf8c8 100644 --- a/ts/LibsignalNetRemoteConfig.preload.ts +++ b/ts/LibsignalNetRemoteConfig.preload.ts @@ -7,6 +7,7 @@ import { } from '@signalapp/libsignal-client/dist/net.js'; import { isProduction } from './util/version.std.ts'; +import { drop } from './util/drop.std.ts'; import * as RemoteConfig from './RemoteConfig.dom.ts'; import type { AddPrefix, ArrayValues } from './types/Util.std.ts'; import { createLogger } from './logging/log.std.ts'; @@ -21,13 +22,9 @@ function convertToDesktopRemoteConfigKey< export function bindRemoteConfigToLibsignalNet( libsignalNet: Net, - appVersion: string + appVersion: string, + reconnect: () => Promise ): void { - // Calls setLibsignalRemoteConfig and is reset when any libsignal remote - // config key changes. Doing that asynchronously allows the callbacks for - // multiple keys that are triggered by the same config fetch from the server - // to be coalesced into a single timeout. - let reloadRemoteConfig: NodeJS.Immediate | undefined; const libsignalBuildVariant = isProduction(appVersion) ? BuildVariant.Production : BuildVariant.Beta; @@ -46,18 +43,24 @@ export function bindRemoteConfigToLibsignalNet( Object.fromEntries(remoteConfigs) ); libsignalNet.setRemoteConfig(remoteConfigs, libsignalBuildVariant); - reloadRemoteConfig = undefined; }; setLibsignalRemoteConfig(); - KeysExpectedByLibsignalNet.map(convertToDesktopRemoteConfigKey).forEach( - key => { - RemoteConfig.onChange(key, () => { - if (reloadRemoteConfig === undefined) { - reloadRemoteConfig = setImmediate(setLibsignalRemoteConfig); - } - }); + RemoteConfig.onChange( + KeysExpectedByLibsignalNet.map(convertToDesktopRemoteConfigKey), + () => { + setLibsignalRemoteConfig(); + + // When linking for the first time in mock tests we start with an empty + // remote config and fetch the latest values only when we connect the + // socket. However, new values won't be applied until we reconnect so + // we need to reconnect immediately to use remote config dependent + // features like gRPC. + if (window.SignalCI) { + log.info('Reconnecting socket after remote config change'); + drop(reconnect()); + } } ); } diff --git a/ts/RemoteConfig.dom.ts b/ts/RemoteConfig.dom.ts index d9456d3cb1..0281fa62e8 100644 --- a/ts/RemoteConfig.dom.ts +++ b/ts/RemoteConfig.dom.ts @@ -135,13 +135,13 @@ type ConfigValueType = { export type ConfigMapType = { [key in ConfigKeyType]?: ConfigValueType; }; -export type ConfigListenerType = (value: ConfigValueType) => unknown; -type ConfigListenersMapType = { - [key: string]: Array; -}; +type ConfigListenerType = Readonly<{ + keys: Set; + callback: () => unknown; +}>; let config: ConfigMapType | undefined; -const listeners: ConfigListenersMapType = {}; +const listeners = new Set(); export type OptionsType = Readonly<{ getConfig: typeof getConfig; @@ -155,17 +155,17 @@ export function restoreRemoteConfigFromStorage({ } export function onChange( - key: ConfigKeyType, - fn: ConfigListenerType + keys: Array, + callback: () => unknown ): () => void { - const keyListeners: Array = get(listeners, key, []); - keyListeners.push(fn); - listeners[key] = keyListeners; + const listener: ConfigListenerType = { + keys: new Set(keys), + callback, + }; + listeners.add(listener); return () => { - if (listeners[key]) { - listeners[key] = listeners[key].filter(l => l !== fn); - } + listeners.delete(listener); }; } @@ -210,6 +210,8 @@ export const _refreshRemoteConfig = async ({ newConfigValues.set(name, value); } + const changedKeys = new Set(); + const oldConfig = config; let semverError = false; config = Array.from(newConfigValues.entries()).reduce( @@ -253,13 +255,8 @@ export const _refreshRemoteConfig = async ({ semverError = true; } - // If enablement changes at all, notify listeners - const currentListeners = listeners[name] || []; if (hasChanged) { - log.info(`Remote Config: Flag ${name} has changed`); - currentListeners.forEach(listener => { - listener(configValue); - }); + changedKeys.add(name); } // Return new configuration object @@ -271,6 +268,19 @@ export const _refreshRemoteConfig = async ({ {} ); + if (changedKeys.size !== 0) { + log.info( + `Remote Config: Flags ${[...changedKeys].join(', ')} have changed` + ); + + // If enablement changes at all, notify listeners + for (const { keys, callback } of listeners) { + if (!keys.isDisjointFrom(changedKeys)) { + callback(); + } + } + } + if (semverError && config['desktop.internalUser']?.enabled) { window.reduxActions.toast.showToast({ toastType: ToastType.Error, diff --git a/ts/background.preload.ts b/ts/background.preload.ts index 1d9f9227b3..835caa9875 100644 --- a/ts/background.preload.ts +++ b/ts/background.preload.ts @@ -248,6 +248,7 @@ import { encryptConversationAttachments } from './util/encryptConversationAttach import { DataReader, DataWriter } from './sql/Client.preload.ts'; import { restoreRemoteConfigFromStorage, + isEnabled as isRemoteConfigValueEnabled, getValue as getRemoteConfigValue, onChange as onRemoteConfigChange, maybeRefreshRemoteConfig, @@ -1557,8 +1558,12 @@ export async function startApp(): Promise { }); // Listen for changes to the `desktop.clientExpiration` remote flag - onRemoteConfigChange('desktop.clientExpiration', ({ enabled, value }) => { - if (!enabled) { + onRemoteConfigChange(['desktop.clientExpiration'], () => { + if (!isRemoteConfigValueEnabled('desktop.clientExpiration')) { + return; + } + const value = getRemoteConfigValue('desktop.clientExpiration'); + if (value == null) { return; } const remoteBuildExpirationTimestamp = parseRemoteClientExpiration(value); diff --git a/ts/services/expiring/chatFolderCleanupService.preload.ts b/ts/services/expiring/chatFolderCleanupService.preload.ts index a0e1fdb0a0..25fd6b7a8e 100644 --- a/ts/services/expiring/chatFolderCleanupService.preload.ts +++ b/ts/services/expiring/chatFolderCleanupService.preload.ts @@ -26,7 +26,7 @@ export const chatFolderCleanupService = createExpiringEntityCleanupService({ }, subscribeToTriggers: trigger => { let prevMessageQueueTime = getMessageQueueTime(); - return RemoteConfig.onChange('global.messageQueueTimeInSeconds', () => { + return RemoteConfig.onChange(['global.messageQueueTimeInSeconds'], () => { const messageQueueTime = getMessageQueueTime(); if (messageQueueTime !== prevMessageQueueTime) { trigger('messageQueueTime changed'); diff --git a/ts/test-mock/bootstrap.node.ts b/ts/test-mock/bootstrap.node.ts index 5579d3253b..a0cfd77621 100644 --- a/ts/test-mock/bootstrap.node.ts +++ b/ts/test-mock/bootstrap.node.ts @@ -181,6 +181,10 @@ const DEFAULT_REMOTE_CONFIG = [ ['global.backups.mediaTierFallbackCdnNumber', { enabled: true, value: '3' }], ['global.groupsv2.groupSizeHardLimit', { enabled: true, value: '64' }], ['global.groupsv2.maxGroupSize', { enabled: true, value: '32' }], + [ + 'desktop.libsignalNet.grpc.MessagesAnonymousSendMultiRecipientMessage.2', + { enabled: true }, + ], ] as const; // diff --git a/ts/test-node/RemoteConfig_test.dom.ts b/ts/test-node/RemoteConfig_test.dom.ts index 84e1a624a0..b95125babb 100644 --- a/ts/test-node/RemoteConfig_test.dom.ts +++ b/ts/test-node/RemoteConfig_test.dom.ts @@ -6,7 +6,7 @@ import * as sinon from 'sinon'; import lodash from 'lodash'; import { normalizeAci } from '../util/normalizeAci.std.ts'; -import type { ConfigKeyType, ConfigListenerType } from '../RemoteConfig.dom.ts'; +import type { ConfigKeyType } from '../RemoteConfig.dom.ts'; import { COUNTRY_CODE_FALLBACK, getCountryCodeValue, @@ -187,8 +187,8 @@ describe('RemoteConfig', () => { await updateRemoteConfig([]); // oxlint-disable-next-line typescript/no-empty-function - const listener = sinon.spy(() => {}); - onChange('desktop.internalUser', listener); + const listener = sinon.spy<() => void>(() => {}); + onChange(['desktop.internalUser', 'desktop.clientExpiration'], listener); await updateRemoteConfig([ { name: 'desktop.internalUser', value: 'yes' }, @@ -198,14 +198,16 @@ describe('RemoteConfig', () => { { name: 'desktop.internalUser', value: 'yes' }, ]); + // should not trigger callback + await updateRemoteConfig([ + { name: 'desktop.internalUser', value: 'yes' }, + { name: 'global.nicknames.max', value: '42' }, + ]); + const calls = listener .getCalls() .map(call => omit(call.firstArg, 'enabledAt')); - assert.deepEqual(calls, [ - { name: 'desktop.internalUser', value: 'yes', enabled: true }, - { name: 'desktop.internalUser', enabled: false }, - { name: 'desktop.internalUser', value: 'yes', enabled: true }, - ]); + assert.deepEqual(calls, [{}, {}, {}]); }); }); }); diff --git a/ts/textsecure/WebAPI.preload.ts b/ts/textsecure/WebAPI.preload.ts index 1e126291e8..e448e7de9a 100644 --- a/ts/textsecure/WebAPI.preload.ts +++ b/ts/textsecure/WebAPI.preload.ts @@ -1803,7 +1803,11 @@ export async function connect({ hasStoriesDisabled, hasBuildExpired, }: WebAPIConnectOptionsType): Promise { - bindRemoteConfigToLibsignalNet(getLibsignalNet(), window.getVersion()); + bindRemoteConfigToLibsignalNet( + getLibsignalNet(), + window.getVersion(), + reconnect + ); username = initialUsername; password = initialPassword;