From f906eca1248d985e172fa7d0353c4e51d0aa203b Mon Sep 17 00:00:00 2001 From: Renato Alcara Date: Tue, 24 Feb 2026 07:40:18 -0300 Subject: [PATCH] =?UTF-8?q?perf:=20eliminates=20a=20delay=20in=20message?= =?UTF-8?q?=20delivery=20between=20phone=E2=86=92app=20+=20cache=20device-?= =?UTF-8?q?list?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit perf: eliminates a delay in message delivery between phone→app + cache device-list --- src/Defaults/index.ts | 4 ++-- src/Signal/libsignal.ts | 27 +++++++++++++++++++++++++-- src/Socket/chats.ts | 22 ++++++++++++++++++++++ src/Utils/event-buffer.ts | 6 +++--- 4 files changed, 52 insertions(+), 7 deletions(-) diff --git a/src/Defaults/index.ts b/src/Defaults/index.ts index b8da4c4d..5453c4e5 100644 --- a/src/Defaults/index.ts +++ b/src/Defaults/index.ts @@ -62,10 +62,10 @@ export const DEFAULT_CONNECTION_CONFIG: SocketConfig = { browser: Browsers.macOS('Chrome'), waWebSocketUrl: 'wss://web.whatsapp.com/ws/chat', connectTimeoutMs: 20_000, - keepAliveIntervalMs: 30_000, + keepAliveIntervalMs: 15_000, logger: logger.child({ class: 'baileys' }), emitOwnEvents: true, - defaultQueryTimeoutMs: 60_000, + defaultQueryTimeoutMs: 30_000, customUploadHosts: [], retryRequestDelayMs: 250, maxMsgRetryCount: 5, diff --git a/src/Signal/libsignal.ts b/src/Signal/libsignal.ts index f1d90df0..b846f961 100644 --- a/src/Signal/libsignal.ts +++ b/src/Signal/libsignal.ts @@ -286,6 +286,17 @@ export function makeLibSignalRepository( updateAgeOnGet: true }) + // Cache for device-list DB reads in migrateSession. + // The device list changes rarely (new linked device), so a 5-minute TTL avoids + // a DB round-trip on every incoming LID message without risking stale state: + // new devices are picked up at most 5 minutes late (still migrated via their own + // decrypt transaction when the session is first used). + const deviceListCache = new LRUCache({ + max: 500, + ttl: 5 * 60 * 1000, // 5 minutes + ttlAutopurge: true + }) + const repository: SignalRepositoryWithLIDStore = { decryptGroupMessage({ group, authorJid, msg }) { const senderName = jidToSignalSenderKeyName(group, authorJid) @@ -493,12 +504,24 @@ export function makeLibSignalRepository( logger.debug({ fromJid }, 'bulk device migration - loading all user devices') - // Get user's device list from storage - const { [user]: userDevices } = await parsedKeys.get('device-list', [user]) + // Get user's device list — use in-memory cache to avoid a DB round-trip on + // every incoming LID message. Cache is invalidated after 5 minutes. + let userDevices: string[] | undefined = deviceListCache.get(user) + if (!userDevices) { + const result = await parsedKeys.get('device-list', [user]) + userDevices = result[user] + if (userDevices) { + deviceListCache.set(user, userDevices) + } + } + if (!userDevices) { return { migrated: 0, skipped: 0, total: 0 } } + // Work on a copy so we don't mutate the cached array + userDevices = [...userDevices] + const { device: fromDevice } = decoded1 const fromDeviceStr = fromDevice?.toString() || '0' if (!userDevices.includes(fromDeviceStr)) { diff --git a/src/Socket/chats.ts b/src/Socket/chats.ts index 362e99fb..8ee92f57 100644 --- a/src/Socket/chats.ts +++ b/src/Socket/chats.ts @@ -1330,6 +1330,7 @@ export const makeChatsSocket = (config: SocketConfig) => { if (syncState === SyncState.Syncing) { // All collections will be synced, so clear any blocked ones blockedCollections.clear() + logger.info('Doing app state sync') await resyncAppState(ALL_WA_PATCH_NAMES, true) @@ -1430,6 +1431,27 @@ export const makeChatsSocket = (config: SocketConfig) => { logger.info('Connection is now AwaitingInitialSync, buffering events') ev.buffer() + // On reconnections (accountSyncCounter > 0), app state was already synced in a previous + // session. Skip the 20s AwaitingInitialSync wait and go directly to Online so that + // live incoming messages are not held in the buffer for up to 20-60 seconds. + // History messages that arrive later are still processed via processMessage regardless + // of the state machine phase (see the Syncing → Online path below). + const isReconnection = (authState.creds.accountSyncCounter ?? 0) > 0 + if (isReconnection) { + logger.info('Reconnection detected (accountSyncCounter > 0), skipping AwaitingInitialSync wait. Transitioning to Online immediately.') + blockedCollections.clear() + syncState = SyncState.Online + const accountSyncCounter = (authState.creds.accountSyncCounter || 0) + 1 + ev.emit('creds.update', { accountSyncCounter }) + // Fire-and-forget: pick up patches missed during downtime (mute/archive/pin/read state). + // Runs in background so live incoming messages are not blocked. + resyncAppState(ALL_WA_PATCH_NAMES, true).catch(err => + logger.warn({ err }, 'Background app state resync failed (non-critical on reconnection)') + ) + setTimeout(() => ev.flush(), 0) + return + } + const willSyncHistory = shouldSyncHistoryMessage( proto.Message.HistorySyncNotification.create({ syncType: proto.HistorySync.HistorySyncType.RECENT diff --git a/src/Utils/event-buffer.ts b/src/Utils/event-buffer.ts index 8d893487..f49fb021 100644 --- a/src/Utils/event-buffer.ts +++ b/src/Utils/event-buffer.ts @@ -54,9 +54,9 @@ export interface BufferConfig { */ export function loadBufferConfig(): BufferConfig { return { - bufferTimeoutMs: parseInt(process.env.BAILEYS_BUFFER_TIMEOUT_MS || '30000', 10), - minBufferTimeoutMs: parseInt(process.env.BAILEYS_BUFFER_MIN_TIMEOUT_MS || '5000', 10), - maxBufferTimeoutMs: parseInt(process.env.BAILEYS_BUFFER_MAX_TIMEOUT_MS || '60000', 10), + bufferTimeoutMs: parseInt(process.env.BAILEYS_BUFFER_TIMEOUT_MS || '15000', 10), + minBufferTimeoutMs: parseInt(process.env.BAILEYS_BUFFER_MIN_TIMEOUT_MS || '3000', 10), + maxBufferTimeoutMs: parseInt(process.env.BAILEYS_BUFFER_MAX_TIMEOUT_MS || '20000', 10), maxHistoryCacheSize: parseInt(process.env.BAILEYS_BUFFER_MAX_HISTORY_CACHE || '10000', 10), maxBufferSize: parseInt(process.env.BAILEYS_BUFFER_MAX_SIZE || '5000', 10), flushDebounceMs: parseInt(process.env.BAILEYS_BUFFER_FLUSH_DEBOUNCE_MS || '100', 10),