fix(inbound): recover from doAppStateSync failures + route bg errors

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.
This commit is contained in:
Renato Alcara
2026-04-26 15:50:47 -03:00
parent 98d2de2984
commit 942f4ae2c9
+27 -8
View File
@@ -1411,7 +1411,19 @@ export const makeChatsSocket = (config: SocketConfig) => {
blockedCollections.clear() blockedCollections.clear()
logger.info('Doing app state sync') 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 // Sync is complete, go online and flush everything
syncState = SyncState.Online syncState = SyncState.Online
@@ -1532,13 +1544,20 @@ export const makeChatsSocket = (config: SocketConfig) => {
} }
} }
} else { } else {
// `postUpsertWork` cannot reject in practice — Promise.allSettled // `postUpsertWork` is not expected to reject`Promise.allSettled`
// inside postUpsertTasks never rejects, and the per-task warnings // inside `postUpsertTasks` never rejects, and per-task failures are
// already log any failures. `void` marks the floating promise as // already logged inline. The defensive catch routes any truly
// intentional. If `postUpsertMutex` itself ever throws (e.g. runtime // unexpected rejection (e.g. `postUpsertMutex` internal corruption,
// corruption), the resulting unhandled rejection is the signal we // future synchronous throws inside processMessage) through
// want — it's loud and points at a real bug. // `onUnexpectedError` instead of letting it surface as an
void postUpsertWork // 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'}`
)
)
} }
}) })