From a1393b679ce6cbcd63d4ed7ac1eacd34fb1a8ee7 Mon Sep 17 00:00:00 2001 From: moothz Date: Thu, 30 Oct 2025 16:00:44 -0300 Subject: [PATCH 1/2] fix(OnWhatsappCache): Prevent unique constraint errors and optimize DB writes Refactors the cache-saving logic to prevent `Unique constraint failed` errors. This issue occurs when an item's `remoteJid` is not yet included in the `jidOptions` of the existing record. The database query now uses an `OR` condition to find a matching record by either `jidOptions` (using `contains`) or by the `remoteJid` itself in a single query. Additionally, this commit introduces several performance optimizations: 1. **Skip Unnecessary Updates**: The function now performs a deep comparison between the new payload and the `existingRecord`. An `update` operation is only executed if the data has actually changed, reducing unnecessary database writes. 2. **Parallel Processing**: The sequential `for...of` loop has been replaced with `Promise.allSettled`. This allows all items in the `data` array to be processed concurrently, significantly speeding up execution for batch inputs. 3. **Data Consistency**: The JIDs in `jidOptions` are now sorted alphabetically before being joined into a string. This ensures that the change-detection logic is accurate, regardless of the order in which JIDs were discovered. 4. **Refactor**: Simplified JID unification logic using a `Set` and introduced a `normalizeJid` helper function for cleaner code. TODO: Investigate the root cause of why `remoteJid` is sometimes not present in `jidOptions` upon initial discovery. --- src/utils/onWhatsappCache.ts | 124 ++++++++++++++++++++++------------- 1 file changed, 79 insertions(+), 45 deletions(-) diff --git a/src/utils/onWhatsappCache.ts b/src/utils/onWhatsappCache.ts index 27d170e8b..4ace78b0d 100644 --- a/src/utils/onWhatsappCache.ts +++ b/src/utils/onWhatsappCache.ts @@ -65,78 +65,112 @@ interface ISaveOnWhatsappCacheParams { lid?: 'lid' | undefined; } +function normalizeJid(jid: string | null | undefined): string | null { + if (!jid) return null; + return jid.startsWith('+') ? jid.slice(1) : jid; +} + export async function saveOnWhatsappCache(data: ISaveOnWhatsappCacheParams[]) { - if (configService.get('DATABASE').SAVE_DATA.IS_ON_WHATSAPP) { - for (const item of data) { - const remoteJid = item.remoteJid.startsWith('+') ? item.remoteJid.slice(1) : item.remoteJid; + if (!configService.get('DATABASE').SAVE_DATA.IS_ON_WHATSAPP) { + return; + } - // TODO: Buscar registro existente PRIMEIRO para preservar dados - const allJids = [remoteJid]; + // Processa todos os itens em paralelo para melhor performance + const processingPromises = data.map(async (item) => { + try { + const remoteJid = normalizeJid(item.remoteJid); + if (!remoteJid) { + logger.warn('[saveOnWhatsappCache] Item skipped, missing remoteJid.'); + return; + } - const altJid = - item.remoteJidAlt && item.remoteJidAlt.includes('@lid') - ? item.remoteJidAlt.startsWith('+') - ? item.remoteJidAlt.slice(1) - : item.remoteJidAlt - : null; + const altJidNormalized = normalizeJid(item.remoteJidAlt); + const lidAltJid = (altJidNormalized && altJidNormalized.includes('@lid')) ? altJidNormalized : null; - if (altJid) { - allJids.push(altJid); + const baseJids = [remoteJid]; // Garante que o remoteJid esteja na lista inicial + if (lidAltJid) { + baseJids.push(lidAltJid); } - const expandedJids = allJids.flatMap((jid) => getAvailableNumbers(jid)); + const expandedJids = baseJids.flatMap((jid) => getAvailableNumbers(jid)); + // 1. Busca entrada por jidOptions e também remoteJid + // Às vezes acontece do remoteJid atual NÃO ESTAR no jidOptions ainda, ocasionando o erro: + // 'Unique constraint failed on the fields: (`remoteJid`)' + // Isso acontece principalmente em grupos que possuem o número do criador no ID (ex.: '559911223345-1234567890@g.us') const existingRecord = await prismaRepository.isOnWhatsapp.findFirst({ where: { - OR: expandedJids.map((jid) => ({ jidOptions: { contains: jid } })), + OR: [ + ...expandedJids.map((jid) => ({ jidOptions: { contains: jid } })), + { remoteJid: remoteJid }, // TODO: Descobrir o motivo que causa o remoteJid não estar (às vezes) incluso na lista de jidOptions + ], }, }); - logger.verbose(`Register exists: ${existingRecord ? existingRecord.remoteJid : 'não not found'}`); - - const finalJidOptions = [...expandedJids]; + logger.verbose(`[saveOnWhatsappCache] Register exists for [${expandedJids.join(",")}]? => ${existingRecord ? existingRecord.remoteJid : 'Not found'}`); + + // 2. Unifica todos os JIDs usando um Set para garantir valores únicos + const finalJidOptions = new Set(expandedJids); - if (existingRecord?.jidOptions) { - const existingJids = existingRecord.jidOptions.split(','); - // TODO: Adicionar JIDs existentes que não estão na lista atual - existingJids.forEach((jid) => { - if (!finalJidOptions.includes(jid)) { - finalJidOptions.push(jid); - } - }); + if (lidAltJid) { + finalJidOptions.add(lidAltJid); } - - // TODO: Se tiver remoteJidAlt com @lid novo, adicionar - if (altJid && !finalJidOptions.includes(altJid)) { - finalJidOptions.push(altJid); + + if (existingRecord?.jidOptions) { + existingRecord.jidOptions.split(',').forEach(jid => finalJidOptions.add(jid)); } - const uniqueNumbers = Array.from(new Set(finalJidOptions)); + // 3. Prepara o payload final + // Ordena os JIDs para garantir consistência na string final + const sortedJidOptions = [...finalJidOptions].sort(); + const newJidOptionsString = sortedJidOptions.join(','); + const newLid = item.lid === 'lid' || item.remoteJid?.includes('@lid') ? 'lid' : null; - logger.verbose( - `Saving: remoteJid=${remoteJid}, jidOptions=${uniqueNumbers.join(',')}, lid=${item.lid === 'lid' || item.remoteJid?.includes('@lid') ? 'lid' : null}`, - ); + const dataPayload = { + remoteJid: remoteJid, + jidOptions: newJidOptionsString, + lid: newLid, + }; + // 4. Decide entre Criar ou Atualizar if (existingRecord) { + // Compara a string de JIDs ordenada existente com a nova + const existingJidOptionsString = existingRecord.jidOptions + ? existingRecord.jidOptions.split(',').sort().join(',') + : ''; + + const isDataSame = existingRecord.remoteJid === dataPayload.remoteJid && + existingJidOptionsString === dataPayload.jidOptions && + existingRecord.lid === dataPayload.lid; + + if (isDataSame) { + logger.verbose(`[saveOnWhatsappCache] Data for ${remoteJid} is already up-to-date. Skipping update.`); + return; // Pula para o próximo item + } + + // Os dados são diferentes, então atualiza + logger.verbose(`[saveOnWhatsappCache] Register exists, updating: remoteJid=${remoteJid}, jidOptions=${dataPayload.jidOptions}, lid=${dataPayload.lid}`); await prismaRepository.isOnWhatsapp.update({ where: { id: existingRecord.id }, - data: { - remoteJid: remoteJid, - jidOptions: uniqueNumbers.join(','), - lid: item.lid === 'lid' || item.remoteJid?.includes('@lid') ? 'lid' : null, - }, + data: dataPayload, }); + } else { + // Cria nova entrada + logger.verbose(`[saveOnWhatsappCache] Register does not exist, creating: remoteJid=${remoteJid}, jidOptions=${dataPayload.jidOptions}, lid=${dataPayload.lid}`); await prismaRepository.isOnWhatsapp.create({ - data: { - remoteJid: remoteJid, - jidOptions: uniqueNumbers.join(','), - lid: item.lid === 'lid' || item.remoteJid?.includes('@lid') ? 'lid' : null, - }, + data: dataPayload, }); } + + } catch (e) { + // Loga o erro mas não para a execução dos outros promises + logger.error(`[saveOnWhatsappCache] Error processing item for ${item.remoteJid}: `, e); } - } + }); + + // Espera todas as operações paralelas terminarem + await Promise.allSettled(processingPromises); } export async function getOnWhatsappCache(remoteJids: string[]) { From 8d1151d0a0ba499144fedf268237e05db674547c Mon Sep 17 00:00:00 2001 From: moothz Date: Thu, 30 Oct 2025 16:18:33 -0300 Subject: [PATCH 2/2] fix: lint MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Descartei as mudanças nos arquivos que não me pertencem, dá uma folga aí, botinho --- src/utils/onWhatsappCache.ts | 35 ++++++++++++++++++++--------------- 1 file changed, 20 insertions(+), 15 deletions(-) diff --git a/src/utils/onWhatsappCache.ts b/src/utils/onWhatsappCache.ts index 4ace78b0d..6194ca51b 100644 --- a/src/utils/onWhatsappCache.ts +++ b/src/utils/onWhatsappCache.ts @@ -85,7 +85,7 @@ export async function saveOnWhatsappCache(data: ISaveOnWhatsappCacheParams[]) { } const altJidNormalized = normalizeJid(item.remoteJidAlt); - const lidAltJid = (altJidNormalized && altJidNormalized.includes('@lid')) ? altJidNormalized : null; + const lidAltJid = altJidNormalized && altJidNormalized.includes('@lid') ? altJidNormalized : null; const baseJids = [remoteJid]; // Garante que o remoteJid esteja na lista inicial if (lidAltJid) { @@ -107,17 +107,19 @@ export async function saveOnWhatsappCache(data: ISaveOnWhatsappCacheParams[]) { }, }); - logger.verbose(`[saveOnWhatsappCache] Register exists for [${expandedJids.join(",")}]? => ${existingRecord ? existingRecord.remoteJid : 'Not found'}`); - + logger.verbose( + `[saveOnWhatsappCache] Register exists for [${expandedJids.join(',')}]? => ${existingRecord ? existingRecord.remoteJid : 'Not found'}`, + ); + // 2. Unifica todos os JIDs usando um Set para garantir valores únicos const finalJidOptions = new Set(expandedJids); if (lidAltJid) { finalJidOptions.add(lidAltJid); } - + if (existingRecord?.jidOptions) { - existingRecord.jidOptions.split(',').forEach(jid => finalJidOptions.add(jid)); + existingRecord.jidOptions.split(',').forEach((jid) => finalJidOptions.add(jid)); } // 3. Prepara o payload final @@ -135,34 +137,37 @@ export async function saveOnWhatsappCache(data: ISaveOnWhatsappCacheParams[]) { // 4. Decide entre Criar ou Atualizar if (existingRecord) { // Compara a string de JIDs ordenada existente com a nova - const existingJidOptionsString = existingRecord.jidOptions - ? existingRecord.jidOptions.split(',').sort().join(',') + const existingJidOptionsString = existingRecord.jidOptions + ? existingRecord.jidOptions.split(',').sort().join(',') : ''; - const isDataSame = existingRecord.remoteJid === dataPayload.remoteJid && - existingJidOptionsString === dataPayload.jidOptions && - existingRecord.lid === dataPayload.lid; + const isDataSame = + existingRecord.remoteJid === dataPayload.remoteJid && + existingJidOptionsString === dataPayload.jidOptions && + existingRecord.lid === dataPayload.lid; if (isDataSame) { logger.verbose(`[saveOnWhatsappCache] Data for ${remoteJid} is already up-to-date. Skipping update.`); return; // Pula para o próximo item } - + // Os dados são diferentes, então atualiza - logger.verbose(`[saveOnWhatsappCache] Register exists, updating: remoteJid=${remoteJid}, jidOptions=${dataPayload.jidOptions}, lid=${dataPayload.lid}`); + logger.verbose( + `[saveOnWhatsappCache] Register exists, updating: remoteJid=${remoteJid}, jidOptions=${dataPayload.jidOptions}, lid=${dataPayload.lid}`, + ); await prismaRepository.isOnWhatsapp.update({ where: { id: existingRecord.id }, data: dataPayload, }); - } else { // Cria nova entrada - logger.verbose(`[saveOnWhatsappCache] Register does not exist, creating: remoteJid=${remoteJid}, jidOptions=${dataPayload.jidOptions}, lid=${dataPayload.lid}`); + logger.verbose( + `[saveOnWhatsappCache] Register does not exist, creating: remoteJid=${remoteJid}, jidOptions=${dataPayload.jidOptions}, lid=${dataPayload.lid}`, + ); await prismaRepository.isOnWhatsapp.create({ data: dataPayload, }); } - } catch (e) { // Loga o erro mas não para a execução dos outros promises logger.error(`[saveOnWhatsappCache] Error processing item for ${item.remoteJid}: `, e);