From 8ce2fcdfea7929284c71a867f5270780e67b28d9 Mon Sep 17 00:00:00 2001 From: Gustavo Date: Sat, 1 Aug 2026 01:23:31 -0300 Subject: [PATCH] Fix unbounded inbound-message dedup set and un-unref'd timeout timer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit inboundMessageIds was only ever cleared whole (on disconnect), so it grew without bound proportional to total inbound message volume over a connection's lifetime. Now capped at 5000 entries, evicting the oldest (Set preserves insertion order) once exceeded. withTimeout's internal setTimeout wasn't unref'd, unlike every other timer in this file (qrTimer, reconnectTimer, the lease timer) — a pending lookup/send timeout could keep the event loop alive if the process ever tried to exit gracefully without an explicit process.exit(). Found via a full-project audit. --- src/whatsapp/manager.mjs | 14 +++++++++++++- tests/whatsapp-manager.test.mjs | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/src/whatsapp/manager.mjs b/src/whatsapp/manager.mjs index 861277f..f90e250 100644 --- a/src/whatsapp/manager.mjs +++ b/src/whatsapp/manager.mjs @@ -46,6 +46,7 @@ function withTimeout(promise, timeoutMs, createError) { const provider = Promise.resolve(promise); const timeout = new Promise((_, reject) => { timer = setTimeout(() => reject(createError()), timeoutMs); + timer.unref?.(); }); provider.catch(() => {}); return Promise.race([provider, timeout]).finally(() => clearTimeout(timer)); @@ -101,6 +102,17 @@ export function createWhatsAppManager({ const pendingMessageStatuses = new Map(); const messageStatusInFlight = new Map(); const inboundMessageIds = new Set(); + // Set nunca esvaziado individualmente (so no disconnect inteiro): sem um + // teto, cresce sem limite proporcional ao volume total de mensagens + // recebidas ao longo da vida da conexao. Descarta o id mais antigo (Set + // preserva ordem de insercao) ao ultrapassar o teto. + const MAX_INBOUND_DEDUP_IDS = 5000; + const rememberInboundMessageId = (messageId) => { + inboundMessageIds.add(messageId); + if (inboundMessageIds.size > MAX_INBOUND_DEDUP_IDS) { + inboundMessageIds.delete(inboundMessageIds.values().next().value); + } + }; const lockPath = path.join(path.dirname(authDir), `${path.basename(authDir)}.lock`); const recoveryPath = `${lockPath}.recovery`; @@ -359,7 +371,7 @@ export function createWhatsAppManager({ const timestampMs = Number.isFinite(rawTimestamp) ? rawTimestamp * (rawTimestamp < 1e12 ? 1_000 : 1) : Date.now(); - inboundMessageIds.add(String(messageId)); + rememberInboundMessageId(String(messageId)); Promise.resolve().then(() => onInboundMessage({ messageId: String(messageId), jid, diff --git a/tests/whatsapp-manager.test.mjs b/tests/whatsapp-manager.test.mjs index c91b484..83958de 100644 --- a/tests/whatsapp-manager.test.mjs +++ b/tests/whatsapp-manager.test.mjs @@ -349,6 +349,38 @@ test('messages.upsert publica uma vez somente mensagens inbound novas de convers await fs.rm(f.authDir, { recursive: true, force: true }); }); +// O Set de dedup so era esvaziado inteiro no disconnect: sem um teto, +// crescia sem limite proporcional ao volume total de mensagens recebidas +// durante a vida da conexao. Envia 1 mensagem a mais que o teto interno +// (5000) e confirma que a mais antiga foi descartada (tratada como nova de +// novo), provando que o Set nao cresce indefinidamente. +test('dedup de mensagens inbound tem teto e descarta a mais antiga', async () => { + const inbound = []; + const f = await fixture({ onInboundMessage: (event) => inbound.push(event) }); + await f.manager.connect(); + const socket = f.sockets[0]; + socket.ev.emit('connection.update', { connection: 'open' }); + + const TOTAL = 5001; + for (let i = 0; i < TOTAL; i++) { + socket.ev.emit('messages.upsert', { + type: 'notify', + messages: [{ key: { id: `msg-${i}`, remoteJid: '5575999091780@s.whatsapp.net', fromMe: false }, messageTimestamp: 1785326400 }] + }); + } + await waitFor(() => inbound.length === TOTAL, 5000); + + socket.ev.emit('messages.upsert', { + type: 'notify', + messages: [{ key: { id: 'msg-0', remoteJid: '5575999091780@s.whatsapp.net', fromMe: false }, messageTimestamp: 1785326400 }] + }); + await waitFor(() => inbound.length === TOTAL + 1, 2000); + assert.equal(inbound.filter((e) => e.messageId === 'msg-0').length, 2); + + await f.manager.close(); + await fs.rm(f.authDir, { recursive: true, force: true }); +}); + test('falha ao persistir inbound nao escapa do EventEmitter nem gera resposta automatica', async () => { const logs = []; const f = await fixture({