From 942f4ae2c940c36724c75d456ab59fd68d788335 Mon Sep 17 00:00:00 2001 From: Renato Alcara Date: Sun, 26 Apr 2026 15:50:47 -0300 Subject: [PATCH] fix(inbound): recover from doAppStateSync failures + route bg errors MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses two Copilot review comments on PR #392. 1. doAppStateSync stuck-state recovery (pre-existing bug, made more visible by Promise.allSettled silencing the propagation): If `resyncAppState` throws, the lines that set `syncState = Online` and call `ev.flush()` never execute — leaving the event buffer pinned and `syncState` stuck at `Syncing` until the buffer's own safety timeout expires. Wrap `resyncAppState` in try/catch that forces the state machine forward (`syncState = Online`, `ev.flush()`) before re-throwing, so live inbound events can flow even when app-state sync fails. Collections were already cleared, so blocked patches will be retried on the next creds.update tick. 2. Replace `void postUpsertWork` with a defensive `.catch(err => onUnexpectedError(err, ...))`. The previous `void` was clean but exposed the long-running socket to potential process termination from UnhandledPromiseRejection on Node ≥15. The new pattern matches existing background-work handling elsewhere in chats.ts (presence updates, init queries) and routes truly unexpected rejections through the centralised error handler instead of letting them surface as unhandled. --- src/Socket/chats.ts | 35 +++++++++++++++++++++++++++-------- 1 file changed, 27 insertions(+), 8 deletions(-) diff --git a/src/Socket/chats.ts b/src/Socket/chats.ts index 1210f6b6..0a287a63 100644 --- a/src/Socket/chats.ts +++ b/src/Socket/chats.ts @@ -1411,7 +1411,19 @@ export const makeChatsSocket = (config: SocketConfig) => { blockedCollections.clear() logger.info('Doing app state sync') - await resyncAppState(ALL_WA_PATCH_NAMES, true) + try { + await resyncAppState(ALL_WA_PATCH_NAMES, true) + } catch (err) { + // Failure recovery: without this, syncState would stay at Syncing + // and ev.flush() would never run, leaving the event buffer pinned + // until the buffer's own safety timeout expires. Force the state + // machine forward so live inbound events can flow even if the + // app-state resync failed (collections are already cleared, so + // blocked patches will be retried on the next creds.update tick). + syncState = SyncState.Online + ev.flush() + throw err + } // Sync is complete, go online and flush everything syncState = SyncState.Online @@ -1532,13 +1544,20 @@ export const makeChatsSocket = (config: SocketConfig) => { } } } else { - // `postUpsertWork` cannot reject in practice — Promise.allSettled - // inside postUpsertTasks never rejects, and the per-task warnings - // already log any failures. `void` marks the floating promise as - // intentional. If `postUpsertMutex` itself ever throws (e.g. runtime - // corruption), the resulting unhandled rejection is the signal we - // want — it's loud and points at a real bug. - void postUpsertWork + // `postUpsertWork` is not expected to reject — `Promise.allSettled` + // inside `postUpsertTasks` never rejects, and per-task failures are + // already logged inline. The defensive catch routes any truly + // unexpected rejection (e.g. `postUpsertMutex` internal corruption, + // future synchronous throws inside processMessage) through + // `onUnexpectedError` instead of letting it surface as an + // UnhandledPromiseRejection — which on Node ≥15 can terminate the + // long-running socket process. + postUpsertWork.catch(err => + onUnexpectedError( + err, + `processing post-upsert work for message ${msg.key?.id || 'unknown'} on ${msg.key?.remoteJid || 'unknown chat'}` + ) + ) } })