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({