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()
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'}`
)
)
}
})