From b90b3838119d8a10200ef4b9d750df0a3bc49612 Mon Sep 17 00:00:00 2001 From: Renato Alcara Date: Sun, 26 Apr 2026 14:50:14 -0300 Subject: [PATCH] fix(inbound): ensure mutex holds until both post-upsert tasks settle Addresses Copilot review on PR #392. A plain `Promise.all([doAppStateSync(), processMessage(...)])` rejects on the first task that fails. When that happens inside the messageMutex callback, the mutex releases as soon as the rejection settles even though the other task is still running. The next message for the same chat can then overtake the in-flight task and break the per-chat ordering guarantee that was the whole point of the inner mutex. Wrap each task in its own `.catch` that logs and resolves. The combined Promise.all now only settles after BOTH operations finish (success or failure), so the mutex is held for the full duration of the work and ordering is preserved. Errors are still surfaced via the per-task warnings. --- src/Socket/chats.ts | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/src/Socket/chats.ts b/src/Socket/chats.ts index ca196855..dd464f2c 100644 --- a/src/Socket/chats.ts +++ b/src/Socket/chats.ts @@ -1416,9 +1416,22 @@ export const makeChatsSocket = (config: SocketConfig) => { // (createBufferedFunction only schedules flush after work() resolves), so // the hot path detaches this work to release the emit on the next debounce // tick. + // + // Each task is wrapped in its own .catch so the combined promise only + // settles after BOTH tasks finish. With a plain Promise.all, an early + // rejection from one task would release the keyed mutex while the other + // task is still mutating chat state — letting the next message of the + // same chat overtake it and break per-chat ordering. const postUpsertTasks = () => Promise.all([ - shouldProcessHistoryMsg ? doAppStateSync() : Promise.resolve(), + shouldProcessHistoryMsg + ? doAppStateSync().catch(err => + logger?.warn( + { err, messageId: msg.key?.id, remoteJid: msg.key?.remoteJid }, + 'history doAppStateSync failed' + ) + ) + : Promise.resolve(), processMessage(msg, { signalRepository, shouldProcessHistoryMsg, @@ -1429,7 +1442,12 @@ export const makeChatsSocket = (config: SocketConfig) => { logger, options: config.options, getMessage - }) + }).catch(err => + logger?.warn( + { err, messageId: msg.key?.id, remoteJid: msg.key?.remoteJid }, + 'processMessage failed' + ) + ) ]) const isKeyShareDuringSync =