From 4b026523696b15211e1cafe93f77e9001280023f Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 13 Feb 2026 21:59:35 +0000 Subject: [PATCH] style: auto-fix lint errors and formatting issues Applied yarn lint --fix to auto-correct: - Import spacing and sorting across multiple files - Trailing commas - Arrow function formatting - Object literal formatting - Indentation consistency - Removed unused imports (BaileysEventType, jest from tests) This commit addresses the linting errors reported in GitHub Actions: - Fixed import ordering in libsignal.ts - Fixed trailing commas in Defaults/index.ts - Cleaned up formatting across 63 files - Reduced line count by ~220 lines through formatting optimization Note: Some lint errors remain (75 errors, 239 warnings) mainly: - @typescript-eslint/no-unused-vars (variables assigned but not used) - @typescript-eslint/no-explicit-any (type safety warnings) These remaining issues are non-blocking and can be addressed incrementally. https://claude.ai/code/session_015R3U3kiprQiNTTNNt31Sg6 --- src/Defaults/index.ts | 2 +- src/Signal/libsignal.ts | 82 ++- src/Signal/lid-mapping.ts | 643 +++++++++--------- src/Signal/session-activity-tracker.ts | 2 +- src/Signal/session-cleanup.ts | 21 +- src/Socket/Client/types.ts | 1 + src/Socket/Client/websocket.ts | 1 + src/Socket/chats.ts | 37 +- src/Socket/index.ts | 23 +- src/Socket/messages-recv.ts | 115 ++-- src/Socket/messages-send.ts | 202 +++--- src/Socket/newsletter.ts | 10 +- src/Socket/socket.ts | 59 +- src/Types/Message.ts | 6 +- src/Types/Socket.ts | 4 +- src/Utils/baileys-event-stream.ts | 117 ++-- src/Utils/baileys-logger.ts | 122 ++-- src/Utils/browser-utils.ts | 34 +- src/Utils/cache-utils.ts | 28 +- src/Utils/chat-utils.ts | 10 +- src/Utils/circuit-breaker.ts | 75 +- src/Utils/decode-wa-message.ts | 46 +- src/Utils/event-buffer.ts | 97 +-- src/Utils/generics.ts | 6 +- src/Utils/health-status.ts | 2 +- src/Utils/history.ts | 22 +- src/Utils/identity-change-handler.ts | 10 +- src/Utils/logger-adapter.ts | 30 +- src/Utils/logger.ts | 15 +- src/Utils/message-retry-manager.ts | 11 +- src/Utils/messages-media.ts | 16 +- src/Utils/messages.ts | 183 +++-- src/Utils/pre-key-manager.ts | 3 +- src/Utils/process-message.ts | 29 +- src/Utils/prometheus-metrics.ts | 327 +++++---- src/Utils/retry-utils.ts | 100 ++- src/Utils/sticker-pack.ts | 36 +- src/Utils/structured-logger.ts | 99 +-- src/Utils/trace-context.ts | 53 +- src/Utils/unified-session.ts | 68 +- src/Utils/version-cache.ts | 41 +- src/Utils/wasm-bridge.ts | 6 +- src/WABinary/jid-utils.ts | 6 +- src/WAM/encode.ts | 6 +- src/WAUSync/USyncQuery.ts | 2 +- src/__tests__/Signal/lid-mapping.test.ts | 44 +- .../Signal/session-activity-tracker.test.ts | 12 +- src/__tests__/Signal/session-cleanup.test.ts | 168 +---- .../Socket/identity-change-handling.test.ts | 21 +- .../Utils/baileys-event-stream.test.ts | 113 ++- .../Utils/baileys-logger-console.test.ts | 24 +- src/__tests__/Utils/browser-utils.test.ts | 2 +- src/__tests__/Utils/cache-utils.test.ts | 20 +- src/__tests__/Utils/circuit-breaker.test.ts | 36 +- src/__tests__/Utils/ctwa-recovery.test.ts | 2 +- src/__tests__/Utils/history.test.ts | 119 +--- .../Utils/process-sync-action.test.ts | 10 +- src/__tests__/Utils/retry-utils.test.ts | 81 ++- src/__tests__/Utils/structured-logger.test.ts | 18 +- src/__tests__/Utils/trace-context.test.ts | 69 +- src/__tests__/Utils/unified-session.test.ts | 10 +- src/__tests__/WABinary/jid-utils.test.ts | 9 +- src/index.ts | 8 +- 63 files changed, 1677 insertions(+), 1897 deletions(-) diff --git a/src/Defaults/index.ts b/src/Defaults/index.ts index 92cceb76..3f249add 100644 --- a/src/Defaults/index.ts +++ b/src/Defaults/index.ts @@ -257,7 +257,7 @@ export const TimeMs = { /** One day in milliseconds (86,400,000) */ Day: 86_400_000, /** One week in milliseconds (604,800,000) */ - Week: 604_800_000, + Week: 604_800_000 } as const export type TimeMsKey = keyof typeof TimeMs diff --git a/src/Signal/libsignal.ts b/src/Signal/libsignal.ts index 566f445c..c9c5bfc0 100644 --- a/src/Signal/libsignal.ts +++ b/src/Signal/libsignal.ts @@ -1,21 +1,15 @@ /* @ts-ignore */ -import * as libsignal from 'libsignal' import { createHash } from 'crypto' +import * as libsignal from 'libsignal' import { LRUCache } from 'lru-cache' import type { LIDMapping, SignalAuthState, SignalKeyStoreWithTransaction } from '../Types' import type { BaileysEventEmitter } from '../Types/Events' import type { SignalRepositoryWithLIDStore } from '../Types/Signal' import { generateSignalPubKey } from '../Utils' +import { CircuitBreaker } from '../Utils/circuit-breaker.js' import type { ILogger } from '../Utils/logger' import { metrics } from '../Utils/prometheus-metrics.js' -import { CircuitBreaker } from '../Utils/circuit-breaker.js' -import { - isAnyLidUser, - isAnyPnUser, - jidDecode, - transferDevice, - WAJIDDomains -} from '../WABinary' +import { isAnyLidUser, isAnyPnUser, jidDecode, transferDevice, WAJIDDomains } from '../WABinary' import type { SenderKeyStore } from './Group/group_cipher' import { SenderKeyName } from './Group/sender-key-name' import { SenderKeyRecord } from './Group/sender-key-record' @@ -184,7 +178,10 @@ function extractIdentityFromPkmsg(ciphertext: Uint8Array, logger?: ILogger): Uin offset += length // Bounds check after skipping field if (offset > ciphertext.length) { - logger?.debug({ offset, length: ciphertext.length }, 'Offset exceeds ciphertext bounds after length-delimited field') + logger?.debug( + { offset, length: ciphertext.length }, + 'Offset exceeds ciphertext bounds after length-delimited field' + ) break } } else if (wireType === 0) { @@ -264,7 +261,7 @@ export function makeLibSignalRepository( max: IDENTITY_KEY_CACHE_MAX, ttl: IDENTITY_KEY_CACHE_TTL, ttlAutopurge: true, - updateAgeOnGet: true, + updateAgeOnGet: true }) // Update cache size metric periodically @@ -345,7 +342,7 @@ export function makeLibSignalRepository( jid, addr: addrStr, previousFingerprint: saveResult.previousFingerprint, - newFingerprint: saveResult.currentFingerprint, + newFingerprint: saveResult.currentFingerprint }, 'Identity key changed - contact may have reinstalled WhatsApp, session will be re-established' ) @@ -364,10 +361,7 @@ export function makeLibSignalRepository( } } catch (error) { // Log but don't fail decryption - identity tracking is best-effort - logger.warn( - { jid, error: (error as Error).message }, - 'Failed to save identity key during decryption' - ) + logger.warn({ jid, error: (error as Error).message }, 'Failed to save identity key during decryption') } } } @@ -635,6 +629,7 @@ const jidToSignalProtocolAddress = (jid: string): libsignal.ProtocolAddress => { if (!decoded) { throw new Error(`Failed to decode JID: "${jid}"`) } + const { user, device, server, domainType } = decoded if (!user) { @@ -661,24 +656,25 @@ const jidToSignalSenderKeyName = (group: string, user: string): SenderKeyName => * Extended SignalStorage with identity key management * This type adds identity key operations to the standard Signal storage */ -type ExtendedSignalStorage = SenderKeyStore & libsignal.SignalStorage & { - /** - * Load identity key for a contact - * @param id - Signal protocol address string - * @returns Identity key bytes or undefined if not found - */ - loadIdentityKey(id: string): Promise +type ExtendedSignalStorage = SenderKeyStore & + libsignal.SignalStorage & { + /** + * Load identity key for a contact + * @param id - Signal protocol address string + * @returns Identity key bytes or undefined if not found + */ + loadIdentityKey(id: string): Promise - /** - * Save/update identity key for a contact - * Handles Trust On First Use (TOFU) and change detection - * - * @param id - Signal protocol address string - * @param identityKey - The identity key bytes (33 bytes with type prefix) - * @returns Result indicating if key changed, is new, and fingerprints - */ - saveIdentity(id: string, identityKey: Uint8Array): Promise -} + /** + * Save/update identity key for a contact + * Handles Trust On First Use (TOFU) and change detection + * + * @param id - Signal protocol address string + * @param identityKey - The identity key bytes (33 bytes with type prefix) + * @returns Result indicating if key changed, is new, and fingerprints + */ + saveIdentity(id: string, identityKey: Uint8Array): Promise + } function signalStorage( { creds, keys }: SignalAuthState, @@ -829,9 +825,7 @@ function signalStorage( // Check if keys match const keysMatch = - existingKey && - existingKey.length === identityKey.length && - existingKey.every((byte, i) => byte === identityKey[i]) + existingKey?.length === identityKey.length && existingKey.every((byte, i) => byte === identityKey[i]) if (existingKey && !keysMatch) { // IDENTITY KEY CHANGED - contact reinstalled WhatsApp or switched devices @@ -840,7 +834,7 @@ function signalStorage( // Delete old session and save new identity key atomically await keys.set({ session: { [wireJid]: null }, - 'identity-key': { [wireJid]: identityKey }, + 'identity-key': { [wireJid]: identityKey } }) // Update cache @@ -856,7 +850,7 @@ function signalStorage( previousKeyFingerprint: previousFingerprint, newKeyFingerprint: currentFingerprint, timestamp: Date.now(), - isNewContact: false, + isNewContact: false }) } @@ -865,7 +859,7 @@ function signalStorage( event: 'identity_key_changed', jid: wireJid, previousFingerprint, - newFingerprint: currentFingerprint, + newFingerprint: currentFingerprint }, 'Contact identity key changed - security code changed' ) @@ -874,7 +868,7 @@ function signalStorage( changed: true, isNew: false, previousFingerprint, - currentFingerprint, + currentFingerprint } } @@ -895,14 +889,14 @@ function signalStorage( previousKeyFingerprint: null, newKeyFingerprint: currentFingerprint, timestamp: Date.now(), - isNewContact: true, + isNewContact: true }) } return { changed: false, isNew: true, - currentFingerprint, + currentFingerprint } } @@ -910,11 +904,11 @@ function signalStorage( return { changed: false, isNew: false, - currentFingerprint, + currentFingerprint } } finally { timer?.() } - }, + } } } diff --git a/src/Signal/lid-mapping.ts b/src/Signal/lid-mapping.ts index 87725add..f28f9079 100644 --- a/src/Signal/lid-mapping.ts +++ b/src/Signal/lid-mapping.ts @@ -151,14 +151,14 @@ export class LIDMappingStore { private readonly keys: SignalKeyStoreWithTransaction private readonly logger: ILogger private readonly config: LIDMappingConfig - private destroyed: boolean = false + private destroyed = false /** * Operation counter for safe resource cleanup * Tracks number of operations currently in progress to prevent UAF in destroy() * Incremented at operation start, decremented at operation end */ - private operationsInProgress: number = 0 + private operationsInProgress = 0 private pnToLIDFunc?: (jids: string[]) => Promise @@ -334,138 +334,140 @@ export class LIDMappingStore { this.stats.totalOperations++ this.stats.lastOperationAt = Date.now() - const result = { stored: 0, skipped: 0, errors: 0 } + const result = { stored: 0, skipped: 0, errors: 0 } - // Phase 1: Validate and collect cache misses - const cacheMissPnUsers: string[] = [] - const pendingValidation = new Map() + // Phase 1: Validate and collect cache misses + const cacheMissPnUsers: string[] = [] + const pendingValidation = new Map() - for (const { lid, pn } of pairs) { - if (!this.isValidMapping(lid, pn)) { - this.logger.warn({ lid, pn }, 'Invalid LID-PN mapping rejected') - this.stats.invalidMappings++ - result.skipped++ - continue - } - - const lidDecoded = jidDecode(lid) - const pnDecoded = jidDecode(pn) - - if (!lidDecoded || !pnDecoded) { - result.skipped++ - continue - } - - const pnUser = pnDecoded.user - const lidUser = lidDecoded.user - - // Check cache first - const existingLidUser = this.mappingCache.get(`pn:${pnUser}`) - - if (existingLidUser !== undefined) { - // Cache hit - this.stats.cacheHits++ - if (existingLidUser === lidUser) { - if (this.config.debugLogging) { - this.logger.debug({ pnUser, lidUser }, 'LID mapping already exists, skipping') - } + for (const { lid, pn } of pairs) { + if (!this.isValidMapping(lid, pn)) { + this.logger.warn({ lid, pn }, 'Invalid LID-PN mapping rejected') + this.stats.invalidMappings++ result.skipped++ + continue + } + + const lidDecoded = jidDecode(lid) + const pnDecoded = jidDecode(pn) + + if (!lidDecoded || !pnDecoded) { + result.skipped++ + continue + } + + const pnUser = pnDecoded.user + const lidUser = lidDecoded.user + + // Check cache first + const existingLidUser = this.mappingCache.get(`pn:${pnUser}`) + + if (existingLidUser !== undefined) { + // Cache hit + this.stats.cacheHits++ + if (existingLidUser === lidUser) { + if (this.config.debugLogging) { + this.logger.debug({ pnUser, lidUser }, 'LID mapping already exists, skipping') + } + + result.skipped++ + } else { + // Different mapping - will be stored + pendingValidation.set(pnUser, { pnUser, lidUser }) + } } else { - // Different mapping - will be stored + // Cache miss - queue for batch DB fetch + this.stats.cacheMisses++ + cacheMissPnUsers.push(pnUser) pendingValidation.set(pnUser, { pnUser, lidUser }) } - } else { - // Cache miss - queue for batch DB fetch - this.stats.cacheMisses++ - cacheMissPnUsers.push(pnUser) - pendingValidation.set(pnUser, { pnUser, lidUser }) } - } - // Phase 2: Batch fetch all cache misses from DB - if (cacheMissPnUsers.length > 0) { - const batches = this.chunkArray(cacheMissPnUsers, this.config.batchSize) + // Phase 2: Batch fetch all cache misses from DB + if (cacheMissPnUsers.length > 0) { + const batches = this.chunkArray(cacheMissPnUsers, this.config.batchSize) - for (const batch of batches) { - try { - const stored = await this.retryOperation( - () => this.keys.get('lid-mapping', batch), - 'batch-get-mappings' - ) + for (const batch of batches) { + try { + const stored = await this.retryOperation(() => this.keys.get('lid-mapping', batch), 'batch-get-mappings') - // Update cache and validate against DB - for (const pnUser of batch) { - const existingLidUser = stored[pnUser] + // Update cache and validate against DB + for (const pnUser of batch) { + const existingLidUser = stored[pnUser] - if (existingLidUser) { - this.stats.dbHits++ - // Update cache with database value - this.mappingCache.set(`pn:${pnUser}`, existingLidUser) - this.mappingCache.set(`lid:${existingLidUser}`, pnUser) + if (existingLidUser) { + this.stats.dbHits++ + // Update cache with database value + this.mappingCache.set(`pn:${pnUser}`, existingLidUser) + this.mappingCache.set(`lid:${existingLidUser}`, pnUser) - // Check if this mapping should be skipped - const pending = pendingValidation.get(pnUser) - if (pending && existingLidUser === pending.lidUser) { - if (this.config.debugLogging) { - this.logger.debug( - { pnUser, lidUser: pending.lidUser }, - 'LID mapping already exists in DB, skipping' - ) + // Check if this mapping should be skipped + const pending = pendingValidation.get(pnUser) + if (existingLidUser === pending?.lidUser) { + if (this.config.debugLogging) { + this.logger.debug( + { pnUser, lidUser: pending.lidUser }, + 'LID mapping already exists in DB, skipping' + ) + } + + result.skipped++ + pendingValidation.delete(pnUser) } - result.skipped++ - pendingValidation.delete(pnUser) + } else { + this.stats.dbMisses++ } - } else { - this.stats.dbMisses++ } - } - } catch (error) { - this.logger.error({ error, batchSize: batch.length }, 'Failed to batch fetch existing mappings') - result.errors += batch.length - // Remove failed fetches from pending validation to avoid storing them - for (const pnUser of batch) { - pendingValidation.delete(pnUser) + } catch (error) { + this.logger.error({ error, batchSize: batch.length }, 'Failed to batch fetch existing mappings') + result.errors += batch.length + // Remove failed fetches from pending validation to avoid storing them + for (const pnUser of batch) { + pendingValidation.delete(pnUser) + } } } } - } - // Phase 3: Store new/updated mappings - const validPairs = Array.from(pendingValidation.values()) + // Phase 3: Store new/updated mappings + const validPairs = Array.from(pendingValidation.values()) - if (validPairs.length === 0) { - return result - } - - const storeBatches = this.chunkArray(validPairs, this.config.batchSize) - - for (const batch of storeBatches) { - try { - await this.retryOperation(async () => { - await this.keys.transaction(async () => { - for (const { pnUser, lidUser } of batch) { - await this.keys.set({ - 'lid-mapping': { - [pnUser]: lidUser, - [`${lidUser}_reverse`]: pnUser - } - }) - - this.mappingCache.set(`pn:${pnUser}`, lidUser) - this.mappingCache.set(`lid:${lidUser}`, pnUser) - result.stored++ - this.stats.mappingsStored++ - } - }, 'lid-mapping') - }, 'store-mappings') - } catch (error) { - this.logger.error({ error, batchSize: batch.length }, 'Failed to store mapping batch') - result.errors += batch.length - this.stats.failedOperations++ + if (validPairs.length === 0) { + return result } - } - this.logger.trace({ result, totalPairs: pairs.length, cacheMisses: cacheMissPnUsers.length }, 'Stored LID-PN mappings with batch optimization') + const storeBatches = this.chunkArray(validPairs, this.config.batchSize) + + for (const batch of storeBatches) { + try { + await this.retryOperation(async () => { + await this.keys.transaction(async () => { + for (const { pnUser, lidUser } of batch) { + await this.keys.set({ + 'lid-mapping': { + [pnUser]: lidUser, + [`${lidUser}_reverse`]: pnUser + } + }) + + this.mappingCache.set(`pn:${pnUser}`, lidUser) + this.mappingCache.set(`lid:${lidUser}`, pnUser) + result.stored++ + this.stats.mappingsStored++ + } + }, 'lid-mapping') + }, 'store-mappings') + } catch (error) { + this.logger.error({ error, batchSize: batch.length }, 'Failed to store mapping batch') + result.errors += batch.length + this.stats.failedOperations++ + } + } + + this.logger.trace( + { result, totalPairs: pairs.length, cacheMisses: cacheMissPnUsers.length }, + 'Stored LID-PN mappings with batch optimization' + ) this.recordMetrics('store', result.stored) return result @@ -495,14 +497,10 @@ export class LIDMappingStore { // Use request coalescing to deduplicate concurrent lookups // Safe because: wrapped in trackOperation() prevents resource cleanup - return this.coalesceRequest( - pnUser, - this.inflightLIDLookups, - async () => { - const results = await this.getLIDsForPNs([pn]) - return results?.[0]?.lid || null - } - ) + return this.coalesceRequest(pnUser, this.inflightLIDLookups, async () => { + const results = await this.getLIDsForPNs([pn]) + return results?.[0]?.lid || null + }) }) } @@ -522,139 +520,136 @@ export class LIDMappingStore { this.stats.lastOperationAt = Date.now() const usyncFetch: { [_: string]: number[] } = {} - const successfulPairs: { [_: string]: LIDMapping } = {} - const failedPns = new Set() - const pendingByPnUser = new Map }>>() + const successfulPairs: { [_: string]: LIDMapping } = {} + const failedPns = new Set() + const pendingByPnUser = new Map }>>() - for (const pn of pns) { - if (!isAnyPnUser(pn)) continue + for (const pn of pns) { + if (!isAnyPnUser(pn)) continue - const decoded = jidDecode(pn) - if (!decoded) continue + const decoded = jidDecode(pn) + if (!decoded) continue - const pnUser = decoded.user - const cachedLidUser = this.mappingCache.get(`pn:${pnUser}`) + const pnUser = decoded.user + const cachedLidUser = this.mappingCache.get(`pn:${pnUser}`) - if (cachedLidUser) { - this.stats.cacheHits++ - const lidUser = cachedLidUser.toString() - if (!lidUser) { - this.logger.warn({ pn, lidUser }, 'Invalid or empty LID user') - continue - } - - const pnDevice = decoded.device ?? 0 - const deviceSpecificLid = this.buildDeviceSpecificJid( - lidUser, - pnDevice, - decoded.server === 'hosted' ? 'hosted.lid' : 'lid' - ) - - if (this.config.debugLogging) { - this.logger.trace({ pn, deviceSpecificLid, pnDevice }, 'getLIDForPN: mapping found') - } - - successfulPairs[pn] = { lid: deviceSpecificLid, pn } - continue - } - - this.stats.cacheMisses++ - const pendingForUser = pendingByPnUser.get(pnUser) ?? [] - pendingForUser.push({ pn, decoded }) - pendingByPnUser.set(pnUser, pendingForUser) - } - - if (pendingByPnUser.size > 0) { - const pnUsers = [...pendingByPnUser.keys()] - const dbFailedPnUsers = new Set() - - for (const batch of this.chunkArray(pnUsers, this.config.batchSize)) { - try { - const stored = await this.retryOperation( - () => this.keys.get('lid-mapping', batch), - 'get-lid-for-pn' - ) - - for (const pnUser of batch) { - const lidUser = stored[pnUser] - if (lidUser) { - this.stats.dbHits++ - this.mappingCache.set(`pn:${pnUser}`, lidUser) - this.mappingCache.set(`lid:${lidUser}`, pnUser) - } else { - this.stats.dbMisses++ - } - } - } catch (error) { - this.logger.error({ error, batch }, 'Failed to get LID mapping batch from database') - this.stats.failedOperations += batch.length - batch.forEach(pnUser => dbFailedPnUsers.add(pnUser)) - } - } - - for (const [pnUser, items] of pendingByPnUser.entries()) { - const lidUser = this.mappingCache.get(`pn:${pnUser}`) - for (const { pn, decoded } of items) { - if (lidUser && decoded) { - const lidUserString = lidUser.toString() - if (!lidUserString) { - this.logger.warn({ pn, lidUser }, 'Invalid or empty LID user') - continue - } - - const pnDevice = decoded.device ?? 0 - const deviceSpecificLid = this.buildDeviceSpecificJid( - lidUserString, - pnDevice, - decoded.server === 'hosted' ? 'hosted.lid' : 'lid' - ) - - if (this.config.debugLogging) { - this.logger.trace({ pn, deviceSpecificLid, pnDevice }, 'getLIDForPN: mapping found') - } - - successfulPairs[pn] = { lid: deviceSpecificLid, pn } + if (cachedLidUser) { + this.stats.cacheHits++ + const lidUser = cachedLidUser.toString() + if (!lidUser) { + this.logger.warn({ pn, lidUser }, 'Invalid or empty LID user') continue } - if (dbFailedPnUsers.has(pnUser)) { - failedPns.add(pn) - } + const pnDevice = decoded.device ?? 0 + const deviceSpecificLid = this.buildDeviceSpecificJid( + lidUser, + pnDevice, + decoded.server === 'hosted' ? 'hosted.lid' : 'lid' + ) - if (!decoded) continue - - // Need to fetch from USync if (this.config.debugLogging) { - this.logger.trace({ pnUser }, 'No LID mapping found, queuing for USync') + this.logger.trace({ pn, deviceSpecificLid, pnDevice }, 'getLIDForPN: mapping found') } - const device = decoded.device || 0 - let normalizedPn = jidNormalizedUser(pn) - if (isHostedPnUser(normalizedPn)) { - normalizedPn = `${pnUser}@s.whatsapp.net` - } + successfulPairs[pn] = { lid: deviceSpecificLid, pn } + continue + } - if (!usyncFetch[normalizedPn]) { - usyncFetch[normalizedPn] = [device] - } else { - usyncFetch[normalizedPn]?.push(device) + this.stats.cacheMisses++ + const pendingForUser = pendingByPnUser.get(pnUser) ?? [] + pendingForUser.push({ pn, decoded }) + pendingByPnUser.set(pnUser, pendingForUser) + } + + if (pendingByPnUser.size > 0) { + const pnUsers = [...pendingByPnUser.keys()] + const dbFailedPnUsers = new Set() + + for (const batch of this.chunkArray(pnUsers, this.config.batchSize)) { + try { + const stored = await this.retryOperation(() => this.keys.get('lid-mapping', batch), 'get-lid-for-pn') + + for (const pnUser of batch) { + const lidUser = stored[pnUser] + if (lidUser) { + this.stats.dbHits++ + this.mappingCache.set(`pn:${pnUser}`, lidUser) + this.mappingCache.set(`lid:${lidUser}`, pnUser) + } else { + this.stats.dbMisses++ + } + } + } catch (error) { + this.logger.error({ error, batch }, 'Failed to get LID mapping batch from database') + this.stats.failedOperations += batch.length + batch.forEach(pnUser => dbFailedPnUsers.add(pnUser)) + } + } + + for (const [pnUser, items] of pendingByPnUser.entries()) { + const lidUser = this.mappingCache.get(`pn:${pnUser}`) + for (const { pn, decoded } of items) { + if (lidUser && decoded) { + const lidUserString = lidUser.toString() + if (!lidUserString) { + this.logger.warn({ pn, lidUser }, 'Invalid or empty LID user') + continue + } + + const pnDevice = decoded.device ?? 0 + const deviceSpecificLid = this.buildDeviceSpecificJid( + lidUserString, + pnDevice, + decoded.server === 'hosted' ? 'hosted.lid' : 'lid' + ) + + if (this.config.debugLogging) { + this.logger.trace({ pn, deviceSpecificLid, pnDevice }, 'getLIDForPN: mapping found') + } + + successfulPairs[pn] = { lid: deviceSpecificLid, pn } + continue + } + + if (dbFailedPnUsers.has(pnUser)) { + failedPns.add(pn) + } + + if (!decoded) continue + + // Need to fetch from USync + if (this.config.debugLogging) { + this.logger.trace({ pnUser }, 'No LID mapping found, queuing for USync') + } + + const device = decoded.device || 0 + let normalizedPn = jidNormalizedUser(pn) + if (isHostedPnUser(normalizedPn)) { + normalizedPn = `${pnUser}@s.whatsapp.net` + } + + if (!usyncFetch[normalizedPn]) { + usyncFetch[normalizedPn] = [device] + } else { + usyncFetch[normalizedPn]?.push(device) + } } } } - } - // Fetch from USync if needed - if (Object.keys(usyncFetch).length > 0) { - await this.fetchFromUSync(usyncFetch, successfulPairs) - } + // Fetch from USync if needed + if (Object.keys(usyncFetch).length > 0) { + await this.fetchFromUSync(usyncFetch, successfulPairs) + } - // Log warning if some PNs failed lookup - if (failedPns.size > 0) { - this.logger.warn( - { failedCount: failedPns.size, totalRequested: pns.length }, - 'Some PNs failed during getLIDsForPNs - results may be incomplete' - ) - } + // Log warning if some PNs failed lookup + if (failedPns.size > 0) { + this.logger.warn( + { failedCount: failedPns.size, totalRequested: pns.length }, + 'Some PNs failed during getLIDsForPNs - results may be incomplete' + ) + } this.recordMetrics('get-lid', Object.keys(successfulPairs).length) return Object.keys(successfulPairs).length > 0 ? Object.values(successfulPairs) : null @@ -684,14 +679,10 @@ export class LIDMappingStore { // Use request coalescing to deduplicate concurrent lookups // Safe because: wrapped in trackOperation() prevents resource cleanup - return this.coalesceRequest( - lidUser, - this.inflightPNLookups, - async () => { - const results = await this.getPNsForLIDs([lid]) - return results?.[0]?.pn || null - } - ) + return this.coalesceRequest(lidUser, this.inflightPNLookups, async () => { + const results = await this.getPNsForLIDs([lid]) + return results?.[0]?.pn || null + }) }) } @@ -707,96 +698,93 @@ export class LIDMappingStore { this.stats.lastOperationAt = Date.now() const successfulPairs: { [_: string]: LIDMapping } = {} - const failedLids = new Set() - const pendingByLidUser = new Map }>>() + const failedLids = new Set() + const pendingByLidUser = new Map }>>() - const addResolvedPair = (lid: string, decoded: ReturnType, pnUser: string): void => { - const lidDevice = decoded?.device ?? 0 - const server = decoded?.domainType === WAJIDDomains.HOSTED_LID ? 'hosted' : 's.whatsapp.net' - const pnJid = this.buildDeviceSpecificJid(pnUser, lidDevice, server) + const addResolvedPair = (lid: string, decoded: ReturnType, pnUser: string): void => { + const lidDevice = decoded?.device ?? 0 + const server = decoded?.domainType === WAJIDDomains.HOSTED_LID ? 'hosted' : 's.whatsapp.net' + const pnJid = this.buildDeviceSpecificJid(pnUser, lidDevice, server) - if (this.config.debugLogging) { - this.logger.trace({ lid, pnJid }, 'Found reverse mapping') + if (this.config.debugLogging) { + this.logger.trace({ lid, pnJid }, 'Found reverse mapping') + } + + successfulPairs[lid] = { lid, pn: pnJid } } - successfulPairs[lid] = { lid, pn: pnJid } - } + for (const lid of lids) { + if (!isAnyLidUser(lid)) continue - for (const lid of lids) { - if (!isAnyLidUser(lid)) continue + const decoded = jidDecode(lid) + if (!decoded) continue - const decoded = jidDecode(lid) - if (!decoded) continue + const lidUser = decoded.user + const cachedPnUser = this.mappingCache.get(`lid:${lidUser}`) - const lidUser = decoded.user - const cachedPnUser = this.mappingCache.get(`lid:${lidUser}`) + if (cachedPnUser && typeof cachedPnUser === 'string') { + this.stats.cacheHits++ + addResolvedPair(lid, decoded, cachedPnUser) + continue + } - if (cachedPnUser && typeof cachedPnUser === 'string') { - this.stats.cacheHits++ - addResolvedPair(lid, decoded, cachedPnUser) - continue + this.stats.cacheMisses++ + const pendingForUser = pendingByLidUser.get(lidUser) ?? [] + pendingForUser.push({ lid, decoded }) + pendingByLidUser.set(lidUser, pendingForUser) } - this.stats.cacheMisses++ - const pendingForUser = pendingByLidUser.get(lidUser) ?? [] - pendingForUser.push({ lid, decoded }) - pendingByLidUser.set(lidUser, pendingForUser) - } + if (pendingByLidUser.size > 0) { + const reverseKeys = [...pendingByLidUser.keys()].map(lidUser => `${lidUser}_reverse`) + const dbFailedReverseKeys = new Set() - if (pendingByLidUser.size > 0) { - const reverseKeys = [...pendingByLidUser.keys()].map(lidUser => `${lidUser}_reverse`) - const dbFailedReverseKeys = new Set() + for (const batch of this.chunkArray(reverseKeys, this.config.batchSize)) { + try { + const stored = await this.retryOperation(() => this.keys.get('lid-mapping', batch), 'get-pn-for-lid') - for (const batch of this.chunkArray(reverseKeys, this.config.batchSize)) { - try { - const stored = await this.retryOperation( - () => this.keys.get('lid-mapping', batch), - 'get-pn-for-lid' - ) + for (const reverseKey of batch) { + const lidUser = reverseKey.replace(/_reverse$/, '') + const pnUser = stored[reverseKey] + if (pnUser && typeof pnUser === 'string') { + this.stats.dbHits++ + this.mappingCache.set(`lid:${lidUser}`, pnUser) + this.mappingCache.set(`pn:${pnUser}`, lidUser) + } else { + this.stats.dbMisses++ + } + } + } catch (error) { + this.logger.error({ error, batch }, 'Failed to get PN mapping batch from database') + this.stats.failedOperations += batch.length + batch.forEach(reverseKey => dbFailedReverseKeys.add(reverseKey)) + } + } - for (const reverseKey of batch) { - const lidUser = reverseKey.replace(/_reverse$/, '') - const pnUser = stored[reverseKey] + for (const [lidUser, items] of pendingByLidUser.entries()) { + const pnUser = this.mappingCache.get(`lid:${lidUser}`) + for (const { lid, decoded } of items) { if (pnUser && typeof pnUser === 'string') { - this.stats.dbHits++ - this.mappingCache.set(`lid:${lidUser}`, pnUser) - this.mappingCache.set(`pn:${pnUser}`, lidUser) - } else { - this.stats.dbMisses++ + addResolvedPair(lid, decoded, pnUser) + continue + } + + if (dbFailedReverseKeys.has(`${lidUser}_reverse`)) { + failedLids.add(lid) + } + + if (this.config.debugLogging) { + this.logger.trace({ lidUser }, 'No reverse mapping found') } } - } catch (error) { - this.logger.error({ error, batch }, 'Failed to get PN mapping batch from database') - this.stats.failedOperations += batch.length - batch.forEach(reverseKey => dbFailedReverseKeys.add(reverseKey)) } } - for (const [lidUser, items] of pendingByLidUser.entries()) { - const pnUser = this.mappingCache.get(`lid:${lidUser}`) - for (const { lid, decoded } of items) { - if (pnUser && typeof pnUser === 'string') { - addResolvedPair(lid, decoded, pnUser) - continue - } - - if (dbFailedReverseKeys.has(`${lidUser}_reverse`)) { - failedLids.add(lid) - } - - if (this.config.debugLogging) { - this.logger.trace({ lidUser }, 'No reverse mapping found') - } - } + if (failedLids.size > 0) { + this.logger.warn( + { failedCount: failedLids.size, totalRequested: lids.length }, + 'Some LIDs failed during getPNsForLIDs - results may be incomplete' + ) } - } - - if (failedLids.size > 0) { - this.logger.warn( - { failedCount: failedLids.size, totalRequested: lids.length }, - 'Some LIDs failed during getPNsForLIDs - results may be incomplete' - ) - } this.recordMetrics('get-pn', Object.keys(successfulPairs).length) return Object.keys(successfulPairs).length > 0 ? Object.values(successfulPairs) : null @@ -935,10 +923,7 @@ export class LIDMappingStore { */ private checkDestroyed(): void { if (this.destroyed) { - throw new LIDMappingError( - 'LIDMappingStore has been destroyed', - LIDMappingErrorCode.DESTROYED - ) + throw new LIDMappingError('LIDMappingStore has been destroyed', LIDMappingErrorCode.DESTROYED) } } @@ -960,10 +945,7 @@ export class LIDMappingStore { // Recheck destroyed after incrementing counter // This ensures we fail fast if destroyed between checkDestroyed() and here if (this.destroyed) { - throw new LIDMappingError( - 'LIDMappingStore has been destroyed', - LIDMappingErrorCode.DESTROYED - ) + throw new LIDMappingError('LIDMappingStore has been destroyed', LIDMappingErrorCode.DESTROYED) } return await operation() @@ -996,6 +978,7 @@ export class LIDMappingStore { for (let i = 0; i < array.length; i += size) { chunks.push(array.slice(i, i + size)) } + return chunks } @@ -1012,10 +995,7 @@ export class LIDMappingStore { * Configure via: BAILEYS_LID_RETRY_ATTEMPTS (default: 3) * BAILEYS_LID_RETRY_DELAY_MS (default: 1000) */ - private async retryOperation( - operation: () => T | Promise, - operationName: string - ): Promise { + private async retryOperation(operation: () => T | Promise, operationName: string): Promise { let lastError: Error | undefined for (let attempt = 1; attempt <= this.config.retryAttempts; attempt++) { @@ -1088,10 +1068,7 @@ export class LIDMappingStore { const deviceSpecificPn = this.buildDeviceSpecificJid(pnUser, device, pnServer) if (this.config.debugLogging) { - this.logger.trace( - { pn: pair.pn, deviceSpecificLid, device }, - 'USync fetch successful' - ) + this.logger.trace({ pn: pair.pn, deviceSpecificLid, device }, 'USync fetch successful') } successfulPairs[deviceSpecificPn] = { lid: deviceSpecificLid, pn: deviceSpecificPn } @@ -1126,11 +1103,7 @@ export class LIDMappingStore { * @param fetchFn - Function to execute if no inflight request exists * @returns Promise that resolves to the result */ - private async coalesceRequest( - key: string, - map: Map>, - fetchFn: () => Promise - ): Promise { + private async coalesceRequest(key: string, map: Map>, fetchFn: () => Promise): Promise { // Check if request is already in-flight const existing = map.get(key) if (existing) { diff --git a/src/Signal/session-activity-tracker.ts b/src/Signal/session-activity-tracker.ts index 563ac848..74c50de2 100644 --- a/src/Signal/session-activity-tracker.ts +++ b/src/Signal/session-activity-tracker.ts @@ -70,7 +70,7 @@ export const makeSessionActivityTracker = ( let flushInterval: ReturnType | null = null // Statistics - let stats = { + const stats = { totalUpdates: 0, totalFlushes: 0, lastFlushAt: 0, diff --git a/src/Signal/session-cleanup.ts b/src/Signal/session-cleanup.ts index 41a3939e..579bf600 100644 --- a/src/Signal/session-cleanup.ts +++ b/src/Signal/session-cleanup.ts @@ -50,8 +50,8 @@ export const makeSessionCleanup = ( ) => { let cleanupInterval: ReturnType | null = null let initialTimeout: ReturnType | null = null - let lastCleanupAt: number = 0 - let cleanupRunning: boolean = false + let lastCleanupAt = 0 + let cleanupRunning = false let startupCleanupPromise: Promise | null = null /** @@ -224,10 +224,7 @@ export const makeSessionCleanup = ( ? await sessionActivityTracker.getAllActivities() : new Map() - logger.debug( - { trackedActivities: activityMap.size }, - 'Loaded session activity timestamps' - ) + logger.debug({ trackedActivities: activityMap.size }, 'Loaded session activity timestamps') // Prepare bulk deletion const sessionsToDelete: string[] = [] @@ -376,11 +373,13 @@ export const makeSessionCleanup = ( // Run immediate cleanup on startup if enabled if (config.cleanupOnStartup) { logger.info('🚀 Running cleanup on startup...') - startupCleanupPromise = runCleanup().catch(err => { - logger.error({ err }, 'Cleanup on startup failed') - }).then(() => { - startupCleanupPromise = null // Clear after completion - }) + startupCleanupPromise = runCleanup() + .catch(err => { + logger.error({ err }, 'Cleanup on startup failed') + }) + .then(() => { + startupCleanupPromise = null // Clear after completion + }) } // Schedule first cleanup at configured hour diff --git a/src/Socket/Client/types.ts b/src/Socket/Client/types.ts index cc7c7024..0ae4c86f 100644 --- a/src/Socket/Client/types.ts +++ b/src/Socket/Client/types.ts @@ -19,6 +19,7 @@ export abstract class AbstractSocketClient extends EventEmitter { if (maxListeners === 0) { this.config.logger?.warn('SocketClient setMaxListeners(0) allows UNLIMITED listeners - potential memory leak!') } + this.setMaxListeners(maxListeners) } diff --git a/src/Socket/Client/websocket.ts b/src/Socket/Client/websocket.ts index 59632cd2..d5f0f559 100644 --- a/src/Socket/Client/websocket.ts +++ b/src/Socket/Client/websocket.ts @@ -37,6 +37,7 @@ export class WebSocketClient extends AbstractSocketClient { if (maxListeners === 0) { this.config.logger?.warn('WebSocket setMaxListeners(0) allows UNLIMITED listeners - potential memory leak!') } + this.socket.setMaxListeners(maxListeners) const events = ['close', 'error', 'upgrade', 'message', 'open', 'ping', 'pong', 'unexpected-response'] diff --git a/src/Socket/chats.ts b/src/Socket/chats.ts index 897194e3..95f85d18 100644 --- a/src/Socket/chats.ts +++ b/src/Socket/chats.ts @@ -2,7 +2,7 @@ import NodeCache from '@cacheable/node-cache' import { Boom } from '@hapi/boom' import { LRUCache } from 'lru-cache' import { proto } from '../../WAProto/index.js' -import { DEFAULT_CACHE_TTLS, DEFAULT_CACHE_MAX_KEYS, PROCESSABLE_HISTORY_TYPES } from '../Defaults' +import { DEFAULT_CACHE_MAX_KEYS, DEFAULT_CACHE_TTLS, PROCESSABLE_HISTORY_TYPES } from '../Defaults' import type { BotListInfo, CacheStore, @@ -43,7 +43,7 @@ import { newLTHashState, processSyncAction } from '../Utils' -import { makeMutex, makeKeyedMutex } from '../Utils/make-mutex' +import { makeKeyedMutex, makeMutex } from '../Utils/make-mutex' import processMessage from '../Utils/process-message' import { buildTcTokenFromJid } from '../Utils/tc-token-utils' import { @@ -70,7 +70,17 @@ export const makeChatsSocket = (config: SocketConfig) => { getMessage } = config const sock = makeSocket(config) - const { ev, ws, authState, generateMessageTag, sendNode, query, signalRepository, onUnexpectedError, sendUnifiedSession } = sock + const { + ev, + ws, + authState, + generateMessageTag, + sendNode, + query, + signalRepository, + onUnexpectedError, + sendUnifiedSession + } = sock let privacySettings: { [_: string]: string } | undefined @@ -641,9 +651,7 @@ export const makeChatsSocket = (config: SocketConfig) => { // or key not found const isKeyNotFound = error.output?.statusCode === 404 const isIrrecoverableError = - (attemptsMap[name] || 0) >= MAX_SYNC_ATTEMPTS || - isKeyNotFound || - error.name === 'TypeError' + (attemptsMap[name] || 0) >= MAX_SYNC_ATTEMPTS || isKeyNotFound || error.name === 'TypeError' if (isKeyNotFound) { const currentVersion = states[name]?.version ?? 0 logger.info( @@ -656,6 +664,7 @@ export const makeChatsSocket = (config: SocketConfig) => { `failed to sync state from version${isIrrecoverableError ? '' : ', removing and trying from scratch'}` ) } + await authState.keys.set({ 'app-state-sync-version': { [name]: null } }) // increment number of retries attemptsMap[name] = (attemptsMap[name] || 0) + 1 @@ -765,13 +774,14 @@ export const makeChatsSocket = (config: SocketConfig) => { logger.warn({ toJid }, 'sendPresenceUpdate: failed to decode toJid, skipping') return } + const { server } = decoded const isLid = server === 'lid' await sendNode({ tag: 'chatstate', attrs: { - from: isLid ? (me.lid || me.id) : me.id, + from: isLid ? me.lid || me.id : me.id, to: toJid }, content: [ @@ -826,6 +836,7 @@ export const makeChatsSocket = (config: SocketConfig) => { logger.warn({ jid }, 'handlePresenceUpdate: firstChild content is empty, skipping') return } + let type = firstChild.tag as WAPresence if (type === 'paused') { type = 'available' @@ -1294,7 +1305,7 @@ export const makeChatsSocket = (config: SocketConfig) => { }, 20_000) }) - ev.on('lid-mapping.update', async (mappings) => { + ev.on('lid-mapping.update', async mappings => { try { const result = await signalRepository.lidMapping.storeLIDPNMappings(mappings) logger.debug( @@ -1319,10 +1330,7 @@ export const makeChatsSocket = (config: SocketConfig) => { const pnUser = jidNormalizedUser(mapping.pn) if (lidUser && pnUser && lidUser !== pnUser) { - logger.debug( - { lid: lidUser, pn: pnUser }, - 'collected chat update for LID→PN merge notification' - ) + logger.debug({ lid: lidUser, pn: pnUser }, 'collected chat update for LID→PN merge notification') mergeNotifications.push({ id: pnUser, @@ -1335,10 +1343,7 @@ export const makeChatsSocket = (config: SocketConfig) => { // Emit single batch of merge notifications for better performance if (mergeNotifications.length > 0) { - logger.debug( - { count: mergeNotifications.length }, - 'emitting batch of chat merge notifications' - ) + logger.debug({ count: mergeNotifications.length }, 'emitting batch of chat merge notifications') ev.emit('chats.update', mergeNotifications) } diff --git a/src/Socket/index.ts b/src/Socket/index.ts index ab00b6ee..b0061f38 100644 --- a/src/Socket/index.ts +++ b/src/Socket/index.ts @@ -1,7 +1,7 @@ import { DEFAULT_CONNECTION_CONFIG } from '../Defaults' import type { UserFacingSocketConfig, WAVersion } from '../Types' -import { getCachedVersion, refreshVersionCache, clearVersionCache, getVersionCacheStatus } from '../Utils/version-cache' import type { VersionCacheLogger } from '../Utils/version-cache' +import { clearVersionCache, getCachedVersion, getVersionCacheStatus, refreshVersionCache } from '../Utils/version-cache' import { makeCommunitiesSocket } from './communities' /** @@ -109,18 +109,13 @@ export const makeWASocketAutoVersion = async (config: UserFacingSocketConfig) => fromCache, ageMinutes: fromCache ? Math.round(age / 60000) : 0 }, - fromCache - ? 'Using cached WhatsApp Web version' - : 'Fetched fresh WhatsApp Web version' + fromCache ? 'Using cached WhatsApp Web version' : 'Fetched fresh WhatsApp Web version' ) mergedConfig.version = version trackedVersion = [...version] as WAVersion } catch (error) { - logger?.warn( - { error, fallbackVersion: mergedConfig.version }, - 'Error fetching version, using bundled version' - ) + logger?.warn({ error, fallbackVersion: mergedConfig.version }, 'Error fetching version, using bundled version') } // Create the socket @@ -128,7 +123,7 @@ export const makeWASocketAutoVersion = async (config: UserFacingSocketConfig) => // Listen for connection close to cleanup interval (Fix #1, #6) // This handles both explicit sock.end() and internal disconnections - sock.ev.on('connection.update', (update) => { + sock.ev.on('connection.update', update => { if (update.connection === 'close') { isSocketClosed = true cleanupInterval() @@ -139,10 +134,7 @@ export const makeWASocketAutoVersion = async (config: UserFacingSocketConfig) => // Setup periodic version check if interval > 0 if (checkIntervalMs > 0) { - logger?.info( - { intervalHours: checkIntervalMs / (60 * 60 * 1000) }, - 'Starting periodic version check' - ) + logger?.info({ intervalHours: checkIntervalMs / (60 * 60 * 1000) }, 'Starting periodic version check') versionCheckInterval = setInterval(async () => { // Skip if socket is closed (Fix #8 - race condition) @@ -169,10 +161,7 @@ export const makeWASocketAutoVersion = async (config: UserFacingSocketConfig) => // Don't update to fallback version on transient network errors if (!fetchSuccess) { - logger?.warn( - { fallbackVersion: result.version }, - 'Failed to fetch latest version, keeping current version' - ) + logger?.warn({ fallbackVersion: result.version }, 'Failed to fetch latest version, keeping current version') return // Skip version update on fetch failure } } else if (cacheStatus.version) { diff --git a/src/Socket/messages-recv.ts b/src/Socket/messages-recv.ts index 7eed7789..a39abb89 100644 --- a/src/Socket/messages-recv.ts +++ b/src/Socket/messages-recv.ts @@ -3,8 +3,14 @@ import { Boom } from '@hapi/boom' import { randomBytes } from 'crypto' import Long from 'long' import { proto } from '../../WAProto/index.js' -import { DEFAULT_CACHE_TTLS, DEFAULT_SESSION_CLEANUP_CONFIG, KEY_BUNDLE_TYPE, MIN_PREKEY_COUNT, PLACEHOLDER_MAX_AGE_SECONDS, STATUS_EXPIRY_SECONDS } from '../Defaults' -import { metrics, recordMessageReceived, recordHistorySyncMessages, recordMessageRetry, recordMessageFailure } from '../Utils/prometheus-metrics.js' +import { + DEFAULT_CACHE_TTLS, + DEFAULT_SESSION_CLEANUP_CONFIG, + KEY_BUNDLE_TYPE, + MIN_PREKEY_COUNT, + PLACEHOLDER_MAX_AGE_SECONDS, + STATUS_EXPIRY_SECONDS +} from '../Defaults' import type { GroupParticipant, MessageReceiptType, @@ -18,12 +24,10 @@ import type { WAPatchName } from '../Types' import { WAMessageStatus, WAMessageStubType } from '../Types' -import { logMessageReceived } from '../Utils/baileys-logger' import { aesDecryptCTR, aesEncryptGCM, cleanMessage, - normalizeMessageJids, Curve, decodeMediaRetryNode, decodeMessageNode, @@ -42,12 +46,21 @@ import { MISSING_KEYS_ERROR_TEXT, NACK_REASONS, NO_MESSAGE_FOUND_ERROR_TEXT, + normalizeMessageJids, toNumber, unixTimestampSeconds, xmppPreKey, xmppSignedPreKey } from '../Utils' +import { logMessageReceived } from '../Utils/baileys-logger' import { makeMutex } from '../Utils/make-mutex' +import { + metrics, + recordHistorySyncMessages, + recordMessageFailure, + recordMessageReceived, + recordMessageRetry +} from '../Utils/prometheus-metrics.js' import { areJidsSameUser, type BinaryNode, @@ -81,7 +94,8 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => { sessionCleanupConfig } = config // Use nullish coalescing to handle partial config properly - const autoCleanCorrupted = sessionCleanupConfig?.autoCleanCorrupted ?? DEFAULT_SESSION_CLEANUP_CONFIG.autoCleanCorrupted + const autoCleanCorrupted = + sessionCleanupConfig?.autoCleanCorrupted ?? DEFAULT_SESSION_CLEANUP_CONFIG.autoCleanCorrupted const sock = makeMessagesSocket(config) const { ev, @@ -488,7 +502,7 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => { // Extract error code from retry node if present (for MAC error detection) const retryNode = getBinaryNodeChild(node, 'retry') - const errorAttr = retryNode?.attrs?.error as string | undefined + const errorAttr = retryNode?.attrs?.error const errorCode = messageRetryManager.parseRetryErrorCode(errorAttr) const result = messageRetryManager.shouldRecreateSession(fromJid, hasSession.exists, errorCode) @@ -1060,7 +1074,10 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => { recreateReason = result.reason if (shouldRecreateSession) { - logger.debug({ participant, retryCount, reason: recreateReason, errorCode }, 'recreating session for outgoing retry') + logger.debug( + { participant, retryCount, reason: recreateReason, errorCode }, + 'recreating session for outgoing retry' + ) // CRITICAL: Use same transaction key as encrypt/decrypt operations to prevent race // Using meId ensures this delete serializes with sendMessage() and other session operations await authState.keys.transaction(async () => { @@ -1255,7 +1272,14 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => { category, author, decrypt - } = decryptMessageNode(node, authState.creds.me!.id, authState.creds.me!.lid || '', signalRepository, logger, autoCleanCorrupted) + } = decryptMessageNode( + node, + authState.creds.me!.id, + authState.creds.me!.lid || '', + signalRepository, + logger, + autoCleanCorrupted + ) const alt = msg.key.participantAlt || msg.key.remoteJidAlt // Handle LID/PN mappings with hybrid approach: @@ -1271,7 +1295,8 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => { const existingMapping = await signalRepository.lidMapping.getPNForLID(alt) if (!existingMapping) { // Store mapping in background (non-critical, doesn't block decrypt) - signalRepository.lidMapping.storeLIDPNMappings([{ lid: alt, pn: primaryJid }]) + signalRepository.lidMapping + .storeLIDPNMappings([{ lid: alt, pn: primaryJid }]) .catch(error => logger.warn({ error, alt, primaryJid }, 'Background LID mapping storage failed')) } @@ -1286,7 +1311,8 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => { const existingMapping = await signalRepository.lidMapping.getLIDForPN(alt) if (!existingMapping) { // Store mapping in background (non-critical) - signalRepository.lidMapping.storeLIDPNMappings([{ lid: primaryJid, pn: alt }]) + signalRepository.lidMapping + .storeLIDPNMappings([{ lid: primaryJid, pn: alt }]) .catch(error => logger.warn({ error, alt, primaryJid }, 'Background LID mapping storage failed')) } @@ -1320,7 +1346,10 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => { // Fallback chain: remoteJid (normalized) > msg.key.id (unique) > 'unknown' (serializes all) let mutexKey = msg.key.remoteJid if (!mutexKey) { - logger.warn({ msgId: msg.key.id, fromMe: msg.key.fromMe }, 'Missing remoteJid after normalization, using msg.key.id as fallback') + logger.warn( + { msgId: msg.key.id, fromMe: msg.key.fromMe }, + 'Missing remoteJid after normalization, using msg.key.id as fallback' + ) mutexKey = msg.key.id || 'unknown' } @@ -1341,9 +1370,11 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => { // Skip unavailable fanout types - these messages will never have content available // These are system messages that cannot be decrypted or retrieved const messageType = msg.messageStubParameters?.[2] - if (messageType === 'bot_unavailable_fanout' || + if ( + messageType === 'bot_unavailable_fanout' || messageType === 'hosted_unavailable_fanout' || - messageType === 'view_once_unavailable_fanout') { + messageType === 'view_once_unavailable_fanout' + ) { logger.debug( { msgId: msg.key?.id, messageType }, 'CTWA: Skipping placeholder resend for unavailable fanout type' @@ -1395,34 +1426,22 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => { try { const requestId = await requestPlaceholderResend(msgKey, msgData) if (requestId && requestId !== 'RESOLVED') { - logger.debug( - { msgId, requestId }, - 'CTWA: Placeholder resend request sent successfully' - ) + logger.debug({ msgId, requestId }, 'CTWA: Placeholder resend request sent successfully') metrics.ctwaRecoveryRequests.inc({ status: 'sent' }) // Note: The actual message will be emitted via 'messages.upsert' // when the PEER_DATA_OPERATION_REQUEST_RESPONSE_MESSAGE is processed // in the PDO response handler in src/Utils/process-message.ts } else if (requestId === 'RESOLVED') { // Message was received while we were waiting - logger.debug( - { msgId }, - 'CTWA: Message received during resend delay' - ) + logger.debug({ msgId }, 'CTWA: Message received during resend delay') metrics.ctwaMessagesRecovered.inc() metrics.ctwaRecoveryLatency.observe(Date.now() - startTime) } else { // Already requested (duplicate request prevented by cache) - logger.debug( - { msgId }, - 'CTWA: Resend already requested, skipping duplicate' - ) + logger.debug({ msgId }, 'CTWA: Resend already requested, skipping duplicate') } } catch (error) { - logger.warn( - { error, msgId }, - 'CTWA: Failed to request placeholder resend' - ) + logger.warn({ error, msgId }, 'CTWA: Failed to request placeholder resend') metrics.ctwaRecoveryFailures.inc({ reason: 'request_failed' }) } }) @@ -1433,24 +1452,15 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => { try { const requestId = await requestPlaceholderResend(msgKey, msgData) if (requestId && requestId !== 'RESOLVED') { - logger.debug( - { msgId, requestId }, - 'CTWA: Placeholder resend request sent successfully (direct)' - ) + logger.debug({ msgId, requestId }, 'CTWA: Placeholder resend request sent successfully (direct)') } else if (requestId === 'RESOLVED') { // Message arrived during the internal 2s delay in requestPlaceholderResend - logger.debug( - { msgId }, - 'CTWA: Message received before direct resend request completed' - ) + logger.debug({ msgId }, 'CTWA: Message received before direct resend request completed') metrics.ctwaMessagesRecovered.inc() metrics.ctwaRecoveryLatency.observe(Date.now() - startTime) } else { // Already requested (duplicate request prevented by cache) - logger.debug( - { msgId }, - 'CTWA: Resend already requested, skipping duplicate (direct)' - ) + logger.debug({ msgId }, 'CTWA: Resend already requested, skipping duplicate (direct)') } } catch (error) { logger.warn({ error, msgId }, 'CTWA: Failed to request placeholder resend') @@ -1575,14 +1585,21 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => { // Record message received metric const msgContent = msg.message - const msgType = msgContent?.conversation ? 'text' - : msgContent?.imageMessage ? 'image' - : msgContent?.videoMessage ? 'video' - : msgContent?.audioMessage ? 'audio' - : msgContent?.documentMessage ? 'document' - : msgContent?.stickerMessage ? 'sticker' - : msgContent?.reactionMessage ? 'reaction' - : 'other' + const msgType = msgContent?.conversation + ? 'text' + : msgContent?.imageMessage + ? 'image' + : msgContent?.videoMessage + ? 'video' + : msgContent?.audioMessage + ? 'audio' + : msgContent?.documentMessage + ? 'document' + : msgContent?.stickerMessage + ? 'sticker' + : msgContent?.reactionMessage + ? 'reaction' + : 'other' recordMessageReceived(msgType) // Track session activity for cleanup diff --git a/src/Socket/messages-send.ts b/src/Socket/messages-send.ts index 60999ff1..dda0c9aa 100644 --- a/src/Socket/messages-send.ts +++ b/src/Socket/messages-send.ts @@ -16,7 +16,6 @@ import type { WAMessage, WAMessageKey } from '../Types' -import { logMessageSent } from '../Utils/baileys-logger' import { aggregateMessageKeysNotFromMe, assertMediaContent, @@ -40,8 +39,10 @@ import { parseAndInjectE2ESessions, unixTimestampSeconds } from '../Utils' +import { logMessageSent } from '../Utils/baileys-logger' import { getUrlInfo } from '../Utils/link-preview' import { makeKeyedMutex } from '../Utils/make-mutex' +import { metrics, recordMessageFailure, recordMessageRetry, recordMessageSent } from '../Utils/prometheus-metrics' import { getMessageReportingToken, shouldIncludeReportingToken } from '../Utils/reporting-utils' import { areJidsSameUser, @@ -65,7 +66,6 @@ import { S_WHATSAPP_NET } from '../WABinary' import { USyncQuery, USyncUser } from '../WAUSync' -import { recordMessageSent, recordMessageRetry, recordMessageFailure, metrics } from '../Utils/prometheus-metrics' import { makeNewsletterSocket } from './newsletter' export const makeMessagesSocket = (config: SocketConfig) => { @@ -331,12 +331,7 @@ export const makeMessagesSocket = (config: SocketConfig) => { if (!meId) throw new Boom('Not authenticated', { statusCode: 401 }) const meLid = authState.creds.me?.lid || '' - const extracted = extractDeviceJids( - result?.list, - meId, - meLid, - ignoreZeroDevices - ) + const extracted = extractDeviceJids(result?.list, meId, meLid, ignoreZeroDevices) const deviceMap: { [_: string]: FullJid[] } = {} for (const item of extracted) { @@ -465,9 +460,7 @@ export const makeMessagesSocket = (config: SocketConfig) => { const wireJids = [ ...jidsRequiringFetch.filter(jid => isAnyLidUser(jid)), ...( - (await signalRepository.lidMapping.getLIDsForPNs( - jidsRequiringFetch.filter(jid => isAnyPnUser(jid)) - )) || [] + (await signalRepository.lidMapping.getLIDsForPNs(jidsRequiringFetch.filter(jid => isAnyPnUser(jid)))) || [] ).map(a => a.lid) ] @@ -650,6 +643,7 @@ export const makeMessagesSocket = (config: SocketConfig) => { if (message.interactiveMessage.nativeFlowMessage) { return 'native_flow' } + // Check if it's a carousel with nativeFlowMessage buttons in cards if (message.interactiveMessage.carouselMessage?.cards?.length) { const hasNativeFlowButtons = message.interactiveMessage.carouselMessage.cards.some( @@ -659,6 +653,7 @@ export const makeMessagesSocket = (config: SocketConfig) => { return 'native_flow' } } + // Check if it's a collection/product carousel if (message.interactiveMessage.carouselMessage?.cards?.length) { const hasCollectionCards = message.interactiveMessage.carouselMessage.cards.some( @@ -668,6 +663,7 @@ export const makeMessagesSocket = (config: SocketConfig) => { return 'native_flow' } } + return 'interactive' } @@ -693,6 +689,7 @@ export const makeMessagesSocket = (config: SocketConfig) => { if (innerMessage.interactiveMessage.nativeFlowMessage) { return 'native_flow' } + // Check if it's a carousel with nativeFlowMessage buttons in cards if (innerMessage.interactiveMessage.carouselMessage?.cards?.length) { const hasNativeFlowButtons = innerMessage.interactiveMessage.carouselMessage.cards.some( @@ -702,6 +699,7 @@ export const makeMessagesSocket = (config: SocketConfig) => { return 'native_flow' } } + // Check if it's a collection/product carousel if (innerMessage.interactiveMessage.carouselMessage?.cards?.length) { const hasCollectionCards = innerMessage.interactiveMessage.carouselMessage.cards.some( @@ -711,6 +709,7 @@ export const makeMessagesSocket = (config: SocketConfig) => { return 'native_flow' } } + return 'interactive' } } @@ -727,7 +726,8 @@ export const makeMessagesSocket = (config: SocketConfig) => { // For native_flow messages, check for special button types that need specific attributes if (buttonType === 'native_flow') { - const interactiveMsg = message.interactiveMessage || + const interactiveMsg = + message.interactiveMessage || message.viewOnceMessage?.message?.interactiveMessage || message.viewOnceMessageV2?.message?.interactiveMessage @@ -758,7 +758,8 @@ export const makeMessagesSocket = (config: SocketConfig) => { * Carousels should NOT have the bot node injected as they are not bot messages */ const isCarouselMessage = (message: proto.IMessage): boolean => { - const interactiveMsg = message.interactiveMessage || + const interactiveMsg = + message.interactiveMessage || message.viewOnceMessage?.message?.interactiveMessage || message.viewOnceMessageV2?.message?.interactiveMessage @@ -774,7 +775,8 @@ export const makeMessagesSocket = (config: SocketConfig) => { * These messages may need different biz node handling or no biz node at all */ const isCatalogMessage = (message: proto.IMessage): boolean => { - const interactiveMsg = message.interactiveMessage || + const interactiveMsg = + message.interactiveMessage || message.viewOnceMessage?.message?.interactiveMessage || message.viewOnceMessageV2?.message?.interactiveMessage @@ -782,9 +784,7 @@ export const makeMessagesSocket = (config: SocketConfig) => { if (nativeFlow?.buttons?.length) { // Check if any button is a catalog-type button return nativeFlow.buttons.some( - (btn: any) => btn?.name === 'catalog_message' || - btn?.name === 'single_product' || - btn?.name === 'product_list' + (btn: any) => btn?.name === 'catalog_message' || btn?.name === 'single_product' || btn?.name === 'product_list' ) } @@ -796,16 +796,15 @@ export const makeMessagesSocket = (config: SocketConfig) => { * Lists need type='list' in the biz node instead of type='native_flow' */ const isListNativeFlow = (message: proto.IMessage): boolean => { - const interactiveMsg = message.interactiveMessage || + const interactiveMsg = + message.interactiveMessage || message.viewOnceMessage?.message?.interactiveMessage || message.viewOnceMessageV2?.message?.interactiveMessage const nativeFlow = interactiveMsg?.nativeFlowMessage if (nativeFlow?.buttons?.length) { // Check if any button is a list-type button (single_select or multi_select) - return nativeFlow.buttons.some( - (btn: any) => btn?.name === 'single_select' || btn?.name === 'multi_select' - ) + return nativeFlow.buttons.some((btn: any) => btn?.name === 'single_select' || btn?.name === 'multi_select') } return false @@ -852,9 +851,7 @@ export const makeMessagesSocket = (config: SocketConfig) => { const innerMsg = message.viewOnceMessage?.message const nativeFlow = innerMsg?.interactiveMessage?.nativeFlowMessage if (nativeFlow?.buttons?.length) { - const singleSelectBtn = nativeFlow.buttons.find( - (btn: any) => btn?.name === 'single_select' - ) + const singleSelectBtn = nativeFlow.buttons.find((btn: any) => btn?.name === 'single_select') if (singleSelectBtn?.buttonParamsJson) { try { const params = JSON.parse(singleSelectBtn.buttonParamsJson) @@ -1156,6 +1153,7 @@ export const makeMessagesSocket = (config: SocketConfig) => { logger.debug({ jid }, '[CAROUSEL] Skipping own device - DSM carousel causes error 479') continue } + meRecipients.push(jid) } else { otherRecipients.push(jid) @@ -1212,7 +1210,7 @@ export const makeMessagesSocket = (config: SocketConfig) => { attrs: { v: '2', type, - count: participant!.count.toString() + count: participant.count.toString() }, content: encryptedContent }) @@ -1257,8 +1255,14 @@ export const makeMessagesSocket = (config: SocketConfig) => { const startTime = Date.now() // Debug: Log message structure to diagnose list detection - const interactiveMsg = message.interactiveMessage || message.viewOnceMessage?.message?.interactiveMessage || message.viewOnceMessageV2?.message?.interactiveMessage - const listMsg = message.listMessage || message.viewOnceMessage?.message?.listMessage || message.viewOnceMessageV2?.message?.listMessage + const interactiveMsg = + message.interactiveMessage || + message.viewOnceMessage?.message?.interactiveMessage || + message.viewOnceMessageV2?.message?.interactiveMessage + const listMsg = + message.listMessage || + message.viewOnceMessage?.message?.listMessage || + message.viewOnceMessageV2?.message?.listMessage const nativeFlowButtons = interactiveMsg?.nativeFlowMessage?.buttons || [] const isListDetected = isListNativeFlow(message) @@ -1323,10 +1327,10 @@ export const makeMessagesSocket = (config: SocketConfig) => { // Note: '' (empty) causes error 405 rejection from WhatsApp server // Special flows (payment, mpm, order) use specific names const SPECIAL_FLOW_NAMES: Record = { - 'review_and_pay': 'payment_info', - 'payment_info': 'payment_info', - 'mpm': 'mpm', - 'review_order': 'order_details' + review_and_pay: 'payment_info', + payment_info: 'payment_info', + mpm: 'mpm', + review_order: 'order_details' } const firstButtonName = allButtonNames[0] || '' const nativeFlowName = SPECIAL_FLOW_NAMES[firstButtonName] || 'mixed' @@ -1370,11 +1374,9 @@ export const makeMessagesSocket = (config: SocketConfig) => { // - quick_reply buttons with bot node: only smartphone, not Web ❌ const isNativeFlowButtons = buttonType === 'native_flow' - const isPrivateUserChat = ( - isPnUser(destinationJid) || - isLidUser(destinationJid) || - destinationJid?.endsWith('@c.us') - ) && !isJidBot(destinationJid) + const isPrivateUserChat = + (isPnUser(destinationJid) || isLidUser(destinationJid) || destinationJid?.endsWith('@c.us')) && + !isJidBot(destinationJid) if (isPrivateUserChat && !isCarousel && !isCatalog && buttonType !== 'list' && !isNativeFlowButtons) { ;(stanza.content as BinaryNode[]).push({ @@ -1406,10 +1408,7 @@ export const makeMessagesSocket = (config: SocketConfig) => { metrics.interactiveMessagesSuccess.inc({ type: buttonType }) metrics.interactiveMessagesLatency.observe({ type: buttonType }, Date.now() - startTime) } catch (error) { - logger.error( - { error, msgId, buttonType }, - '[EXPERIMENTAL] Failed to inject biz node for interactive message' - ) + logger.error({ error, msgId, buttonType }, '[EXPERIMENTAL] Failed to inject biz node for interactive message') metrics.interactiveMessagesFailures.inc({ type: buttonType, reason: 'injection_failed' }) } } else if (buttonType && !enableInteractiveMessages) { @@ -1497,15 +1496,23 @@ export const makeMessagesSocket = (config: SocketConfig) => { logMessageSent(msgId, destinationJid) // Record message sent metric - const msgType = message.conversation ? 'text' - : message.imageMessage ? 'image' - : message.videoMessage ? 'video' - : message.audioMessage ? 'audio' - : message.documentMessage ? 'document' - : message.stickerMessage ? 'sticker' - : message.stickerPackMessage ? 'sticker_pack' - : message.reactionMessage ? 'reaction' - : 'other' + const msgType = message.conversation + ? 'text' + : message.imageMessage + ? 'image' + : message.videoMessage + ? 'video' + : message.audioMessage + ? 'audio' + : message.documentMessage + ? 'document' + : message.stickerMessage + ? 'sticker' + : message.stickerPackMessage + ? 'sticker_pack' + : message.reactionMessage + ? 'reaction' + : 'other' recordMessageSent(msgType) // Add message to retry cache if enabled @@ -1728,17 +1735,13 @@ export const makeMessagesSocket = (config: SocketConfig) => { const startTime = Date.now() const userJid = authState.creds.me!.id - const { - medias, - delay: delayConfig = 'adaptive', - retryCount = 3, - continueOnFailure = true - } = album + const { medias, delay: delayConfig = 'adaptive', retryCount = 3, continueOnFailure = true } = album // Validation (also done in generateWAMessageContent, but double-check here) if (!medias || medias.length < 2) { throw new Boom('Album must have at least 2 media items', { statusCode: 400 }) } + if (medias.length > 10) { throw new Boom('Album cannot have more than 10 media items (WhatsApp limit)', { statusCode: 400 }) } @@ -1754,25 +1757,30 @@ export const makeMessagesSocket = (config: SocketConfig) => { ) // Generate album root message first (with counts of expected media) - const albumRootMsg = await generateWAMessage(jid, { - album: { medias, delay: delayConfig, retryCount, continueOnFailure } - }, { - logger, - userJid, - getUrlInfo: text => getUrlInfo(text, { - thumbnailWidth: linkPreviewImageThumbnailWidth, - fetchOpts: { timeout: 3_000, ...(httpRequestOptions || {}) }, + const albumRootMsg = await generateWAMessage( + jid, + { + album: { medias, delay: delayConfig, retryCount, continueOnFailure } + }, + { logger, - uploadImage: generateHighQualityLinkPreview ? waUploadToServer : undefined - }), - upload: waUploadToServer, - mediaCache: config.mediaCache, - // Don't spread options here to avoid messageId collision - timestamp: options.timestamp, - quoted: options.quoted, - ephemeralExpiration: options.ephemeralExpiration, - mediaUploadTimeoutMs: options.mediaUploadTimeoutMs - }) + userJid, + getUrlInfo: text => + getUrlInfo(text, { + thumbnailWidth: linkPreviewImageThumbnailWidth, + fetchOpts: { timeout: 3_000, ...(httpRequestOptions || {}) }, + logger, + uploadImage: generateHighQualityLinkPreview ? waUploadToServer : undefined + }), + upload: waUploadToServer, + mediaCache: config.mediaCache, + // Don't spread options here to avoid messageId collision + timestamp: options.timestamp, + quoted: options.quoted, + ephemeralExpiration: options.ephemeralExpiration, + mediaUploadTimeoutMs: options.mediaUploadTimeoutMs + } + ) const albumKey = albumRootMsg.key @@ -1788,9 +1796,13 @@ export const makeMessagesSocket = (config: SocketConfig) => { process.nextTick(async () => { let mutexKey = albumRootMsg.key.remoteJid if (!mutexKey) { - logger.warn({ msgId: albumRootMsg.key.id }, 'Missing remoteJid in albumRootMsg, using msg.key.id as fallback') + logger.warn( + { msgId: albumRootMsg.key.id }, + 'Missing remoteJid in albumRootMsg, using msg.key.id as fallback' + ) mutexKey = albumRootMsg.key.id || 'unknown' } + await messageMutex.mutex(mutexKey, () => upsertMessage(albumRootMsg, 'append')) }) } @@ -1812,7 +1824,7 @@ export const makeMessagesSocket = (config: SocketConfig) => { const mediaTypeMultiplier = isVideo ? 2.0 : 1.0 // Later items in album get slightly more delay (cumulative load) - const positionMultiplier = 1 + (index * 0.1) + const positionMultiplier = 1 + index * 0.1 // Add some jitter to prevent predictable patterns const jitter = Math.random() * 200 @@ -1827,16 +1839,14 @@ export const makeMessagesSocket = (config: SocketConfig) => { if (delayConfig === 'adaptive') { return calculateAdaptiveDelay(media, index) } + return delayConfig } /** * Send a single media item with retry logic */ - const sendMediaWithRetry = async ( - media: AlbumMediaItem, - index: number - ): Promise => { + const sendMediaWithRetry = async (media: AlbumMediaItem, index: number): Promise => { const itemStartTime = Date.now() let lastError: Error | undefined let attempts = 0 @@ -1845,16 +1855,17 @@ export const makeMessagesSocket = (config: SocketConfig) => { attempts = attempt + 1 try { // Generate message for this media item - // NOTE: Each item needs its own unique messageId, so we don't spread options.messageId + // NOTE: Each item needs its own unique messageId, so we don't spread options.messageId const mediaMsg = await generateWAMessage(jid, media as AnyMessageContent, { logger, userJid, - getUrlInfo: text => getUrlInfo(text, { - thumbnailWidth: linkPreviewImageThumbnailWidth, - fetchOpts: { timeout: 3_000, ...(httpRequestOptions || {}) }, - logger, - uploadImage: generateHighQualityLinkPreview ? waUploadToServer : undefined - }), + getUrlInfo: text => + getUrlInfo(text, { + thumbnailWidth: linkPreviewImageThumbnailWidth, + fetchOpts: { timeout: 3_000, ...(httpRequestOptions || {}) }, + logger, + uploadImage: generateHighQualityLinkPreview ? waUploadToServer : undefined + }), upload: waUploadToServer, mediaCache: config.mediaCache, // Don't spread ...options to avoid messageId collision @@ -1874,6 +1885,7 @@ export const makeMessagesSocket = (config: SocketConfig) => { if (!mediaMsg.message.messageContextInfo) { mediaMsg.message.messageContextInfo = {} } + mediaMsg.message.messageContextInfo.messageAssociation = { associationType: proto.MessageAssociation.AssociationType.MEDIA_ALBUM, parentMessageKey: albumKey @@ -1893,14 +1905,12 @@ export const makeMessagesSocket = (config: SocketConfig) => { logger.warn({ msgId: mediaMsg.key.id }, 'Missing remoteJid in mediaMsg, using msg.key.id as fallback') mutexKey = mediaMsg.key.id || 'unknown' } + await messageMutex.mutex(mutexKey, () => upsertMessage(mediaMsg, 'append')) }) } - logger.debug( - { index, msgId: mediaMsg.key.id, attempts }, - 'Album media item sent successfully' - ) + logger.debug({ index, msgId: mediaMsg.key.id, attempts }, 'Album media item sent successfully') return { index, @@ -1925,10 +1935,7 @@ export const makeMessagesSocket = (config: SocketConfig) => { } // All retries exhausted - logger.error( - { index, attempts, error: lastError?.message }, - 'Album media item failed after all retries' - ) + logger.error({ index, attempts, error: lastError?.message }, 'Album media item failed after all retries') return { index, @@ -2005,7 +2012,7 @@ export const makeMessagesSocket = (config: SocketConfig) => { if (typeof content === 'object' && 'album' in content) { throw new Boom( 'Cannot send album messages with sendMessage(). Use sendAlbumMessage() instead, ' + - 'which properly sends the album root and individual media items.', + 'which properly sends the album root and individual media items.', { statusCode: 400 } ) } @@ -2027,7 +2034,7 @@ export const makeMessagesSocket = (config: SocketConfig) => { upload: waUploadToServer, mediaCache: config.mediaCache, options: config.options, - jid, + jid }) // Pass the plain JS object directly to relayMessage @@ -2037,7 +2044,7 @@ export const makeMessagesSocket = (config: SocketConfig) => { messageId, useCachedGroupMetadata: options.useCachedGroupMetadata, additionalAttributes: {}, - statusJidList: options.statusJidList, + statusJidList: options.statusJidList }) // Build WebMessageInfo only for event emission and return value @@ -2151,6 +2158,7 @@ export const makeMessagesSocket = (config: SocketConfig) => { logger.warn({ msgId: fullMsg.key.id }, 'Missing remoteJid in fullMsg, using msg.key.id as fallback') mutexKey = fullMsg.key.id || 'unknown' } + await messageMutex.mutex(mutexKey, () => upsertMessage(fullMsg, 'append')) }) } diff --git a/src/Socket/newsletter.ts b/src/Socket/newsletter.ts index dac16139..d0be3de7 100644 --- a/src/Socket/newsletter.ts +++ b/src/Socket/newsletter.ts @@ -28,10 +28,12 @@ const parseNewsletterCreateResponse = (response: NewsletterCreateResponse): News invite: thread.invite || '', subscribers: parseInt(thread.subscribers_count, 10) || 0, verification: thread.verification, - picture: thread.picture ? { - id: thread.picture.id || '', - directPath: thread.picture.direct_path || '' - } : { id: '', directPath: '' }, + picture: thread.picture + ? { + id: thread.picture.id || '', + directPath: thread.picture.direct_path || '' + } + : { id: '', directPath: '' }, mute_state: viewer?.mute } } diff --git a/src/Socket/socket.ts b/src/Socket/socket.ts index 705e2dec..a43a079c 100644 --- a/src/Socket/socket.ts +++ b/src/Socket/socket.ts @@ -13,8 +13,8 @@ import { NOISE_WA_HEADER, UPLOAD_TIMEOUT } from '../Defaults' -import { makeSessionCleanup } from '../Signal/session-cleanup' import { makeSessionActivityTracker } from '../Signal/session-activity-tracker' +import { makeSessionCleanup } from '../Signal/session-cleanup' import type { ConnectionState, LIDMapping, SocketConfig } from '../Types' import { DisconnectReason } from '../Types' import { @@ -44,6 +44,18 @@ import { createConnectionCircuitBreaker, createPreKeyCircuitBreaker } from '../Utils/circuit-breaker' +import { + decrementActiveConnections, + incrementActiveConnections, + recordConnectionAttempt, + recordConnectionError +} from '../Utils/prometheus-metrics' +import { + createUnifiedSessionManager, + extractServerTime, + shouldEnableUnifiedSession, + type UnifiedSessionManager +} from '../Utils/unified-session' import { assertNodeErrorFree, type BinaryNode, @@ -57,18 +69,6 @@ import { jidEncode, S_WHATSAPP_NET } from '../WABinary' -import { - recordConnectionError, - recordConnectionAttempt, - incrementActiveConnections, - decrementActiveConnections -} from '../Utils/prometheus-metrics' -import { - createUnifiedSessionManager, - extractServerTime, - shouldEnableUnifiedSession, - type UnifiedSessionManager -} from '../Utils/unified-session' import { BinaryInfo } from '../WAM/BinaryInfo.js' import { USyncQuery, USyncUser } from '../WAUSync/' import { WebSocketClient } from './Client' @@ -103,9 +103,8 @@ export const makeSocket = (config: SocketConfig) => { } = config // Resolve enableUnifiedSession: explicit config > env var > default (true) - const enableUnifiedSession = enableUnifiedSessionConfig !== undefined - ? enableUnifiedSessionConfig - : shouldEnableUnifiedSession() + const enableUnifiedSession = + enableUnifiedSessionConfig !== undefined ? enableUnifiedSessionConfig : shouldEnableUnifiedSession() // Initialize circuit breakers if enabled let queryCircuitBreaker: CircuitBreaker | undefined @@ -223,6 +222,7 @@ export const makeSocket = (config: SocketConfig) => { if (error instanceof CircuitOpenError) { logger.warn({ circuitName: error.circuitName }, 'Send blocked by connection circuit breaker') } + throw error } } @@ -246,6 +246,7 @@ export const makeSocket = (config: SocketConfig) => { const sendNodeForSession = async (node: BinaryNode): Promise => { await sendNode(node) } + unifiedSessionManager = createUnifiedSessionManager({ enabled: true, logger, @@ -342,6 +343,7 @@ export const makeSocket = (config: SocketConfig) => { if (error instanceof CircuitOpenError) { logger.warn({ circuitName: error.circuitName, state: error.state }, 'Query blocked by circuit breaker') } + throw error } } @@ -572,7 +574,7 @@ export const makeSocket = (config: SocketConfig) => { }) if (sendMsg) { - sendRawMessage(sendMsg).catch(onClose!) + sendRawMessage(sendMsg).catch(onClose!) } return result @@ -857,9 +859,10 @@ export const makeSocket = (config: SocketConfig) => { // 1. Multiple calls to cleanup (reentrancy guard) // 2. Timer orphaning: syncLoop creating timer after clearTimeout if (cleanedUp) { - return // Already cleaned up + return // Already cleaned up } - cleanedUp = true // ← Set IMMEDIATELY to close race window + + cleanedUp = true // ← Set IMMEDIATELY to close race window ev.off('connection.update', connectionHandler) if (syncTimer) { @@ -957,10 +960,12 @@ export const makeSocket = (config: SocketConfig) => { clearTimeout(ttlTimer) ttlTimer = undefined } + if (ttlGraceTimer) { clearTimeout(ttlGraceTimer) ttlGraceTimer = undefined } + sessionStartTime = undefined logger.info('🕐 Session TTL timers cleared on disconnect') } @@ -979,17 +984,20 @@ export const makeSocket = (config: SocketConfig) => { logger.debug('🕐 Session TTL cleanup already called') return } - cleanedUp = true // ← Set IMMEDIATELY to close race window + + cleanedUp = true // ← Set IMMEDIATELY to close race window ev.off('connection.update', connectionHandler) if (ttlTimer) { clearTimeout(ttlTimer) ttlTimer = undefined } + if (ttlGraceTimer) { clearTimeout(ttlGraceTimer) ttlGraceTimer = undefined } + logger.debug('🕐 Session TTL cleanup function executed') } } @@ -1053,7 +1061,8 @@ export const makeSocket = (config: SocketConfig) => { logger.trace({ trace: error?.stack }, 'connection already closed') return } - closed = true // ← Set IMMEDIATELY to close race window + + closed = true // ← Set IMMEDIATELY to close race window logger.info({ trace: error?.stack }, error ? 'connection errored' : 'connection closed') @@ -1089,6 +1098,7 @@ export const makeSocket = (config: SocketConfig) => { default: errorType = `error_${statusCode}` } + recordConnectionError(errorType) recordConnectionAttempt('failure') } @@ -1115,7 +1125,7 @@ export const makeSocket = (config: SocketConfig) => { try { await Promise.race([ uploadPreKeysPromise, - new Promise((resolve) => setTimeout(resolve, 5000)) // 5s timeout + new Promise(resolve => setTimeout(resolve, 5000)) // 5s timeout ]) logger.debug('Pending pre-key upload completed or timed out') } catch (error) { @@ -1138,10 +1148,7 @@ export const makeSocket = (config: SocketConfig) => { // Detect socket-level session errors that require recreation const statusCode = (error as Boom)?.output?.statusCode || 0 - const isSessionError = ( - statusCode === DisconnectReason.badSession || - statusCode === DisconnectReason.restartRequired - ) + const isSessionError = statusCode === DisconnectReason.badSession || statusCode === DisconnectReason.restartRequired if (isSessionError) { logger.warn( diff --git a/src/Types/Message.ts b/src/Types/Message.ts index 4b3acb3d..fcd9252a 100644 --- a/src/Types/Message.ts +++ b/src/Types/Message.ts @@ -1,6 +1,6 @@ +import Long from 'long' import type { Readable } from 'stream' import type { URL } from 'url' -import Long from 'long' import { proto } from '../../WAProto/index.js' import type { MediaType } from '../Defaults' import type { BinaryNode } from '../WABinary' @@ -259,7 +259,9 @@ export type AnyMediaMessageContent = ( fileName?: string caption?: string } & Contextable) -) & { mimetype?: string } & Editable & Partial & Partial +) & { mimetype?: string } & Editable & + Partial & + Partial export type ButtonReplyInfo = { displayText: string diff --git a/src/Types/Socket.ts b/src/Types/Socket.ts index 9df4734d..58ee7926 100644 --- a/src/Types/Socket.ts +++ b/src/Types/Socket.ts @@ -1,12 +1,12 @@ import type { Agent } from 'https' import type { URL } from 'url' import { proto } from '../../WAProto/index.js' -import type { ILogger } from '../Utils/logger' import type { CircuitBreakerOptions } from '../Utils/circuit-breaker' -import type { SessionCleanupConfig } from './SessionCleanup' +import type { ILogger } from '../Utils/logger' import type { AuthenticationState, LIDMapping, SignalAuthState, TransactionCapabilityOptions } from './Auth' import type { GroupMetadata } from './GroupMetadata' import { type MediaConnInfo, type WAMessageKey } from './Message' +import type { SessionCleanupConfig } from './SessionCleanup' import type { SignalRepositoryWithLIDStore } from './Signal' export type WAVersion = [number, number, number] diff --git a/src/Utils/baileys-event-stream.ts b/src/Utils/baileys-event-stream.ts index a16d2705..e43e8285 100644 --- a/src/Utils/baileys-event-stream.ts +++ b/src/Utils/baileys-event-stream.ts @@ -14,8 +14,8 @@ */ import { EventEmitter } from 'events' -import { metrics } from './prometheus-metrics.js' import type { BaileysLogCategory } from './baileys-logger.js' +import { metrics } from './prometheus-metrics.js' /** * Baileys event types @@ -56,7 +56,7 @@ const PRIORITY_VALUES: Record = { critical: 0, high: 1, normal: 2, - low: 3, + low: 3 } /** @@ -160,9 +160,9 @@ const EVENT_CATEGORY_MAP: Record = { 'presence.update': 'presence', 'chats.update': 'message', 'chats.delete': 'message', - 'call': 'call', + call: 'call', 'blocklist.set': 'sync', - 'blocklist.update': 'sync', + 'blocklist.update': 'sync' } /** @@ -173,9 +173,9 @@ const EVENT_PRIORITY_MAP: Partial> = { 'creds.update': 'critical', 'messages.upsert': 'high', 'messages.update': 'high', - 'call': 'high', + call: 'high', 'presence.update': 'low', - 'messaging-history.set': 'normal', + 'messaging-history.set': 'normal' } /** @@ -213,7 +213,7 @@ export class BaileysEventStream extends EventEmitter { maxRetries: options.maxRetries ?? 3, deadLetterQueueSize: options.deadLetterQueueSize ?? 1000, collectMetrics: options.collectMetrics ?? true, - streamName: options.streamName ?? 'baileys', + streamName: options.streamName ?? 'baileys' } this.stats = this.createInitialStats() @@ -238,15 +238,19 @@ export class BaileysEventStream extends EventEmitter { critical: 0, high: 0, normal: 0, - low: 0, - }, + low: 0 + } } } /** * Add event to stream */ - push(type: BaileysEventType, data: T, options?: { priority?: EventPriority; metadata?: Record }): boolean { + push( + type: BaileysEventType, + data: T, + options?: { priority?: EventPriority; metadata?: Record } + ): boolean { // Check backpressure if (this.options.enableBackpressure && this.buffer.length >= this.options.highWaterMark) { this.stats.isBackpressured = true @@ -272,7 +276,7 @@ export class BaileysEventStream extends EventEmitter { priority: options?.priority || EVENT_PRIORITY_MAP[type] || 'normal', category: EVENT_CATEGORY_MAP[type] || 'unknown', metadata: options?.metadata, - retryCount: 0, + retryCount: 0 } // Apply transformers @@ -334,9 +338,18 @@ export class BaileysEventStream extends EventEmitter { /** * Register handler for event type */ - on(event: BaileysEventType | '*' | 'backpressure' | 'drain' | 'dropped' | 'batch-processed' | 'retry', handler: EventHandler): this { + on( + event: BaileysEventType | '*' | 'backpressure' | 'drain' | 'dropped' | 'batch-processed' | 'retry', + handler: EventHandler + ): this { // For control events (backpressure, drain, etc), use native EventEmitter - if (event === 'backpressure' || event === 'drain' || event === 'dropped' || event === 'batch-processed' || event === 'retry') { + if ( + event === 'backpressure' || + event === 'drain' || + event === 'dropped' || + event === 'batch-processed' || + event === 'retry' + ) { super.on(event, handler as any) return this } @@ -345,6 +358,7 @@ export class BaileysEventStream extends EventEmitter { if (!this.handlers.has(event)) { this.handlers.set(event, new Set()) } + this.handlers.get(event)!.add(handler as EventHandler) return this } @@ -352,9 +366,18 @@ export class BaileysEventStream extends EventEmitter { /** * Remove handler */ - off(event: BaileysEventType | '*' | 'backpressure' | 'drain' | 'dropped' | 'batch-processed' | 'retry', handler: EventHandler): this { + off( + event: BaileysEventType | '*' | 'backpressure' | 'drain' | 'dropped' | 'batch-processed' | 'retry', + handler: EventHandler + ): this { // For control events, use native EventEmitter - if (event === 'backpressure' || event === 'drain' || event === 'dropped' || event === 'batch-processed' || event === 'retry') { + if ( + event === 'backpressure' || + event === 'drain' || + event === 'dropped' || + event === 'batch-processed' || + event === 'retry' + ) { super.off(event, handler as any) return this } @@ -364,6 +387,7 @@ export class BaileysEventStream extends EventEmitter { if (handlers) { handlers.delete(handler) } + return this } @@ -371,10 +395,11 @@ export class BaileysEventStream extends EventEmitter { * Register single-use handler */ once(event: BaileysEventType, handler: EventHandler): this { - const wrappedHandler: EventHandler = (e) => { + const wrappedHandler: EventHandler = e => { this.off(event, wrappedHandler as EventHandler) return handler(e) } + return this.on(event, wrappedHandler) } @@ -394,6 +419,7 @@ export class BaileysEventStream extends EventEmitter { if (index !== -1) { this.filters.splice(index, 1) } + return this } @@ -510,8 +536,8 @@ export class BaileysEventStream extends EventEmitter { ...event.metadata, error: error.message, errorStack: error.stack, - movedToDlqAt: Date.now(), - }, + movedToDlqAt: Date.now() + } } this.deadLetterQueue.push(dlqEvent) @@ -554,7 +580,7 @@ export class BaileysEventStream extends EventEmitter { return { processed, failed, - duration: Date.now() - startTime, + duration: Date.now() - startTime } } @@ -636,7 +662,7 @@ export class BaileysEventStream extends EventEmitter { return { processed, failed, - duration: Date.now() - startTime, + duration: Date.now() - startTime } } @@ -663,6 +689,7 @@ export class BaileysEventStream extends EventEmitter { if (this.flushTimer) { clearInterval(this.flushTimer) } + this.buffer = [] this.deadLetterQueue = [] this.handlers.clear() @@ -686,38 +713,38 @@ export const eventFilters = { /** Filter by event type */ byType: (...types: BaileysEventType[]): EventFilter => - (event) => - types.includes(event.type), + event => + types.includes(event.type), /** Filter by category */ byCategory: (...categories: BaileysLogCategory[]): EventFilter => - (event) => - categories.includes(event.category), + event => + categories.includes(event.category), /** Filter by minimum priority */ byMinPriority: (minPriority: EventPriority): EventFilter => - (event) => - PRIORITY_VALUES[event.priority] <= PRIORITY_VALUES[minPriority], + event => + PRIORITY_VALUES[event.priority] <= PRIORITY_VALUES[minPriority], /** Filter recent events (within ms) */ recentOnly: (maxAgeMs: number): EventFilter => - (event) => - Date.now() - event.timestamp <= maxAgeMs, + event => + Date.now() - event.timestamp <= maxAgeMs, /** Combine filters with AND */ and: (...filters: EventFilter[]): EventFilter => - (event) => - filters.every((f) => f(event)), + event => + filters.every(f => f(event)), /** Combine filters with OR */ or: (...filters: EventFilter[]): EventFilter => - (event) => - filters.some((f) => f(event)), + event => + filters.some(f => f(event)) } /** @@ -725,32 +752,30 @@ export const eventFilters = { */ export const eventTransformers = { /** Add processing timestamp */ - addProcessingTimestamp: (): EventTransformer => (event) => ({ + addProcessingTimestamp: (): EventTransformer => event => ({ ...event, metadata: { ...event.metadata, - processingTimestamp: Date.now(), - }, + processingTimestamp: Date.now() + } }), /** Add trace ID */ addTraceId: (traceIdGenerator: () => string): EventTransformer => - (event) => ({ - ...event, - metadata: { - ...event.metadata, - traceId: traceIdGenerator(), - }, - }), + event => ({ + ...event, + metadata: { + ...event.metadata, + traceId: traceIdGenerator() + } + }), /** Elevate priority based on condition */ elevatepriorityIf: (condition: (event: StreamEvent) => boolean, newPriority: EventPriority): EventTransformer => - (event) => - condition(event) - ? { ...event, priority: newPriority } - : event, + event => + condition(event) ? { ...event, priority: newPriority } : event } export default BaileysEventStream diff --git a/src/Utils/baileys-logger.ts b/src/Utils/baileys-logger.ts index c99bcb46..d046aa5e 100644 --- a/src/Utils/baileys-logger.ts +++ b/src/Utils/baileys-logger.ts @@ -12,25 +12,25 @@ */ import type { ILogger } from './logger.js' -import { StructuredLogger, createStructuredLogger, type LogLevel, type LogEntry } from './structured-logger.js' +import { createStructuredLogger, type LogEntry, type LogLevel, StructuredLogger } from './structured-logger.js' /** * Baileys-specific log categories */ export type BaileysLogCategory = - | 'connection' // WebSocket connection events - | 'auth' // Authentication and QR code - | 'message' // Message send/receive - | 'media' // Media upload/download - | 'group' // Group operations - | 'presence' // Presence status - | 'call' // Voice/video calls - | 'sync' // Data synchronization - | 'encryption' // Encryption operations - | 'retry' // Operation retries - | 'socket' // Low-level socket events - | 'binary' // Binary encoding/decoding - | 'unknown' // Unknown category + | 'connection' // WebSocket connection events + | 'auth' // Authentication and QR code + | 'message' // Message send/receive + | 'media' // Media upload/download + | 'group' // Group operations + | 'presence' // Presence status + | 'call' // Voice/video calls + | 'sync' // Data synchronization + | 'encryption' // Encryption operations + | 'retry' // Operation retries + | 'socket' // Low-level socket events + | 'binary' // Binary encoding/decoding + | 'unknown' // Unknown category /** * Baileys Logger configuration @@ -86,7 +86,7 @@ const CATEGORY_PATTERNS: Array<{ pattern: RegExp; category: BaileysLogCategory } { pattern: /sync|history|initial|full/i, category: 'sync' }, { pattern: /encrypt|decrypt|signal|key|cipher/i, category: 'encryption' }, { pattern: /retry|attempt|backoff|reconnect/i, category: 'retry' }, - { pattern: /binary|encode|decode|proto|buffer/i, category: 'binary' }, + { pattern: /binary|encode|decode|proto|buffer/i, category: 'binary' } ] /** @@ -118,14 +118,14 @@ export class BaileysLogger implements ILogger { logBinaryData: config.logBinaryData ?? false, instanceId: config.instanceId || this.generateInstanceId(), eventHandler: config.eventHandler || (() => {}), - maxPayloadSize: config.maxPayloadSize || 1024, + maxPayloadSize: config.maxPayloadSize || 1024 } this.structuredLogger = createStructuredLogger({ level: this.config.level, name: `baileys:${this.config.instanceId}`, jsonFormat: process.env.NODE_ENV === 'production', - redactFields: ['password', 'token', 'secret', 'key', 'authKey', 'macKey'], + redactFields: ['password', 'token', 'secret', 'key', 'authKey', 'macKey'] }) this.metrics = this.createInitialMetrics() @@ -159,8 +159,8 @@ export class BaileysLogger implements ILogger { retry: 0, socket: 0, binary: 0, - unknown: 0, - }, + unknown: 0 + } } } @@ -189,7 +189,7 @@ export class BaileysLogger implements ILogger { const searchText = [ msg || '', typeof obj === 'string' ? obj : '', - typeof obj === 'object' && obj !== null ? JSON.stringify(obj) : '', + typeof obj === 'object' && obj !== null ? JSON.stringify(obj) : '' ].join(' ') for (const { pattern, category } of CATEGORY_PATTERNS) { @@ -249,7 +249,7 @@ export class BaileysLogger implements ILogger { return { _truncated: true, _originalSize: str.length, - _preview: str.substring(0, 200) + '...', + _preview: str.substring(0, 200) + '...' } } } @@ -273,6 +273,7 @@ export class BaileysLogger implements ILogger { } else if (/fail|error|close/i.test(objStr)) { this.metrics.connectionFailures++ } + break case 'message': @@ -283,6 +284,7 @@ export class BaileysLogger implements ILogger { this.metrics.messagesReceived++ this.metrics.lastMessageTime = new Date().toISOString() } + break case 'media': @@ -291,6 +293,7 @@ export class BaileysLogger implements ILogger { } else if (/download/i.test(objStr)) { this.metrics.mediaDownloads++ } + break case 'retry': @@ -328,12 +331,14 @@ export class BaileysLogger implements ILogger { category, instanceId: this.config.instanceId, ...this.childContext, - ...(typeof sanitizedObj === 'object' && sanitizedObj !== null ? sanitizedObj : { value: sanitizedObj }), + ...(typeof sanitizedObj === 'object' && sanitizedObj !== null ? sanitizedObj : { value: sanitizedObj }) } // Structured log (skip if level is 'silent') if (level !== 'silent') { - const logMethod = (this.structuredLogger as unknown as Record void) | undefined>)[level] + const logMethod = ( + this.structuredLogger as unknown as Record void) | undefined> + )[level] if (logMethod) { logMethod.call(this.structuredLogger, enrichedObj, msg) } @@ -347,7 +352,7 @@ export class BaileysLogger implements ILogger { levelValue: 0, message: msg || '', name: `baileys:${this.config.instanceId}`, - data: enrichedObj, + data: enrichedObj } this.config.eventHandler(category, entry) } @@ -384,12 +389,7 @@ export class BaileysLogger implements ILogger { /** * Log message-specific event */ - logMessage( - direction: 'send' | 'receive', - messageType: string, - jid: string, - details?: Record - ): void { + logMessage(direction: 'send' | 'receive', messageType: string, jid: string, details?: Record): void { const sanitizedJid = this.sanitizeJid(jid) this.log( 'info', @@ -398,7 +398,7 @@ export class BaileysLogger implements ILogger { messageType, jid: sanitizedJid, ...details, - category: 'message', + category: 'message' }, `Message ${direction}: ${messageType}` ) @@ -407,12 +407,7 @@ export class BaileysLogger implements ILogger { /** * Log media-specific event */ - logMedia( - operation: 'upload' | 'download', - mediaType: string, - size: number, - details?: Record - ): void { + logMedia(operation: 'upload' | 'download', mediaType: string, size: number, details?: Record): void { this.log( 'info', { @@ -421,7 +416,7 @@ export class BaileysLogger implements ILogger { sizeBytes: size, sizeFormatted: this.formatBytes(size), ...details, - category: 'media', + category: 'media' }, `Media ${operation}: ${mediaType}` ) @@ -440,6 +435,7 @@ export class BaileysLogger implements ILogger { return `${localPart.substring(0, 4)}****@${domainPart}` } } + return jid } @@ -499,9 +495,10 @@ let defaultBaileysLogger: BaileysLogger | null = null export function getDefaultBaileysLogger(): BaileysLogger { if (!defaultBaileysLogger) { defaultBaileysLogger = createBaileysLogger({ - level: 'info', + level: 'info' }) } + return defaultBaileysLogger } @@ -557,6 +554,7 @@ function safeStringify(value: unknown, seen: WeakSet = new WeakSet()): s const items = value.map(v => safeStringify(v, seen)) return `[${items.join(', ')}]` } + return `[Array(${value.length})]` } @@ -571,6 +569,7 @@ function safeStringify(value: unknown, seen: WeakSet = new WeakSet()): s }) return `{${pairs.join(', ')}}` } + return `{Object(${keys.length} keys)}` } catch { return '[Object]' @@ -584,7 +583,7 @@ function safeStringify(value: unknown, seen: WeakSet = new WeakSet()): s * Format data object for single-line or multi-line output * Handles circular references, Error objects, arrays, and undefined values */ -function formatLogData(data: Record, singleLine: boolean = true): string { +function formatLogData(data: Record, singleLine = true): string { if (!data || Object.keys(data).length === 0) return '' const seen = new WeakSet() @@ -599,15 +598,21 @@ function formatLogData(data: Record, singleLine: boolean = true // Multi-line format - use safe replacer for JSON.stringify try { - return JSON.stringify(data, (key, value) => { - if (value instanceof Error) { - return { name: value.name, message: value.message, stack: value.stack } - } - if (typeof value === 'bigint') { - return `${value}n` - } - return value - }, 2) + return JSON.stringify( + data, + (key, value) => { + if (value instanceof Error) { + return { name: value.name, message: value.message, stack: value.stack } + } + + if (typeof value === 'bigint') { + return `${value}n` + } + + return value + }, + 2 + ) } catch { // Fallback for circular references or other issues return safeStringify(data, seen) @@ -635,11 +640,7 @@ export type EventBufferLogType = * logEventBuffer('buffer_flush', { flushCount: 10, historyCacheSize: 5, mode: 'aggressive' }) * // Output: [BAILEYS] 🔄 Event buffer flushed { flushCount: 10, historyCacheSize: 5, mode: 'aggressive' } */ -export function logEventBuffer( - type: EventBufferLogType, - data?: Record, - sessionName?: string -): void { +export function logEventBuffer(type: EventBufferLogType, data?: Record, sessionName?: string): void { if (!isBaileysLogEnabled()) return const prefix = sessionName ? `[BAILEYS] [${sessionName}]` : '[BAILEYS]' @@ -711,6 +712,7 @@ export function logBufferMetrics( console.log(`${prefix} isHealthy: ${metrics.adaptive.isHealthy}`) console.log(`${prefix} }`) } + console.log(`${prefix} }`) } @@ -721,11 +723,7 @@ export function logBufferMetrics( * logMessageSent('3EB02FA562D6CCC0876CDE', '5511999999999@s.whatsapp.net') * // Output: [BAILEYS] 📤 Message sent: 3EB02FA562D6CCC0876CDE → 5511999999999@s.whatsapp.net */ -export function logMessageSent( - messageId: string, - recipientJid: string, - sessionName?: string -): void { +export function logMessageSent(messageId: string, recipientJid: string, sessionName?: string): void { if (!isBaileysLogEnabled()) return const prefix = sessionName ? `[BAILEYS] [${sessionName}]` : '[BAILEYS]' @@ -739,11 +737,7 @@ export function logMessageSent( * logMessageReceived('A5E0349897A3F16F3F2778EEF94A065F', '238315571802285@lid') * // Output: [BAILEYS] 📥 Message received: A5E0349897A3F16F3F2778EEF94A065F ← 238315571802285@lid */ -export function logMessageReceived( - messageId: string, - senderJid: string, - sessionName?: string -): void { +export function logMessageReceived(messageId: string, senderJid: string, sessionName?: string): void { if (!isBaileysLogEnabled()) return const prefix = sessionName ? `[BAILEYS] [${sessionName}]` : '[BAILEYS]' diff --git a/src/Utils/browser-utils.ts b/src/Utils/browser-utils.ts index bb8b4a19..82102fd8 100644 --- a/src/Utils/browser-utils.ts +++ b/src/Utils/browser-utils.ts @@ -1,5 +1,5 @@ +import { existsSync, readFileSync } from 'fs' import { platform, release } from 'os' -import { readFileSync, existsSync } from 'fs' import { proto } from '../../WAProto/index.js' import type { BrowsersMap } from '../Types' @@ -32,7 +32,7 @@ const FALLBACK_VERSIONS = { ubuntu: '24.04.1', macOS: '15.2', windows: '10.0.26100', - baileys: '6.5.0', + baileys: '6.5.0' } as const /** @@ -49,7 +49,7 @@ const DARWIN_TO_MACOS: Readonly> = { 22: 13, // Ventura 21: 12, // Monterey 20: 11, // Big Sur - 19: 10, // Catalina (10.15) + 19: 10 // Catalina (10.15) } as const /** @@ -67,7 +67,7 @@ const PLATFORM_MAP: Readonly> = { netbsd: undefined, openbsd: 'OpenBSD', sunos: 'Solaris', - win32: 'Windows', + win32: 'Windows' } as const // ============================================================ @@ -93,6 +93,7 @@ const BROWSER_TO_PLATFORM_ID: ReadonlyMap = (() => { entries.push([key.toUpperCase(), value.toString()]) } } + return new Map(entries) })() @@ -231,7 +232,7 @@ const detectOSVersions = (): Readonly<{ ubuntu: currentPlatform === 'linux' ? getLinuxVersion() : FALLBACK_VERSIONS.ubuntu, macOS: currentPlatform === 'darwin' ? getMacOSVersion() : FALLBACK_VERSIONS.macOS, windows: currentPlatform === 'win32' ? getWindowsVersion() : FALLBACK_VERSIONS.windows, - baileys: FALLBACK_VERSIONS.baileys, + baileys: FALLBACK_VERSIONS.baileys } } @@ -270,6 +271,7 @@ const normalizeBrowserKey = (browser: unknown): string | null => { if (typeof browser !== 'string') { return null } + const normalized = browser.trim() return normalized.length > 0 ? normalized.toUpperCase() : null } @@ -285,16 +287,16 @@ const getAppropriateVersion = (): string => { const currentPlatform = platform() switch (currentPlatform) { - case 'darwin': - return OS_VERSIONS.macOS - case 'win32': - return OS_VERSIONS.windows - case 'linux': - return OS_VERSIONS.ubuntu - default: - // For other platforms, try os.release() directly - const version = release() - return version || DEFAULT_OS_VERSION + case 'darwin': + return OS_VERSIONS.macOS + case 'win32': + return OS_VERSIONS.windows + case 'linux': + return OS_VERSIONS.ubuntu + default: + // For other platforms, try os.release() directly + const version = release() + return version || DEFAULT_OS_VERSION } } catch { return DEFAULT_OS_VERSION @@ -329,7 +331,7 @@ export const Browsers: BrowsersMap = { macOS: (browser: string): [string, string, string] => ['Mac OS', browser, OS_VERSIONS.macOS], windows: (browser: string): [string, string, string] => ['Windows', browser, OS_VERSIONS.windows], baileys: (browser: string): [string, string, string] => ['Baileys', browser, OS_VERSIONS.baileys], - appropriate: (browser: string): [string, string, string] => [getPlatformName(), browser, getAppropriateVersion()], + appropriate: (browser: string): [string, string, string] => [getPlatformName(), browser, getAppropriateVersion()] } as const /** diff --git a/src/Utils/cache-utils.ts b/src/Utils/cache-utils.ts index 205cd28f..65c7f4f5 100644 --- a/src/Utils/cache-utils.ts +++ b/src/Utils/cache-utils.ts @@ -88,7 +88,7 @@ export class Cache { namespace: options.namespace ?? 'default', onExpire: options.onExpire ?? (() => {}), collectMetrics: options.collectMetrics ?? true, - metricName: options.metricName ?? 'cache', + metricName: options.metricName ?? 'cache' } this.namespace = this.options.namespace @@ -98,10 +98,10 @@ export class Cache { max: this.options.maxSize, ttl: this.options.ttl, updateAgeOnGet: this.options.updateAgeOnGet, - sizeCalculation: (item) => this.options.sizeCalculation(item.value), + sizeCalculation: item => this.options.sizeCalculation(item.value), dispose: (value, key) => { this.options.onExpire(key, value.value) - }, + } }) } @@ -151,7 +151,7 @@ export class Cache { return { value: item.value, hit: true, - key, + key } } @@ -163,7 +163,7 @@ export class Cache { return { value: undefined, hit: false, - key, + key } } @@ -180,7 +180,7 @@ export class Cache { createdAt: now, expiresAt: now + itemTtl, accessCount: 0, - lastAccess: now, + lastAccess: now } this.cache.set(fullKey, item, { ttl: itemTtl }) @@ -291,7 +291,7 @@ export class Cache { misses: this.stats.misses, size: this.cache.size, maxSize: this.options.maxSize, - hitRate: total > 0 ? this.stats.hits / total : 0, + hitRate: total > 0 ? this.stats.hits / total : 0 } } @@ -307,14 +307,14 @@ export class Cache { */ keys(): string[] { const prefix = `${this.namespace}:` - return Array.from(this.cache.keys()).map((k) => (k.startsWith(prefix) ? k.slice(prefix.length) : k)) + return Array.from(this.cache.keys()).map(k => (k.startsWith(prefix) ? k.slice(prefix.length) : k)) } /** * Return all values */ values(): V[] { - return Array.from(this.cache.values()).map((item) => item.value) + return Array.from(this.cache.values()).map(item => item.value) } /** @@ -324,7 +324,7 @@ export class Cache { const prefix = `${this.namespace}:` return Array.from(this.cache.entries()).map(([key, item]) => ({ key: key.startsWith(prefix) ? key.slice(prefix.length) : key, - item, + item })) } @@ -468,7 +468,7 @@ export function cached(options: CacheOptions & { keyGenerator?: (...args: /** * Wrapper for function with cache */ -export function withCache unknown>( +export function withCache unknown>( fn: T, options: CacheOptions> & { keyGenerator?: (...args: Parameters) => string } = {} ): T { @@ -486,7 +486,7 @@ export function withCache unknown>( const result = fn(...args) as ReturnType if (result instanceof Promise) { - return result.then((value) => { + return result.then(value => { cache.set(key, value as ReturnType) return value }) as ReturnType @@ -505,8 +505,9 @@ const globalCaches: Map> = new Map() export function getGlobalCache(namespace: string, options?: CacheOptions): Cache { if (!globalCaches.has(namespace)) { - globalCaches.set(namespace, new Cache({ ...options, namespace }) as Cache) + globalCaches.set(namespace, new Cache({ ...options, namespace })) } + return globalCaches.get(namespace) as Cache } @@ -514,6 +515,7 @@ export function clearGlobalCaches(): void { for (const cache of globalCaches.values()) { cache.clear() } + globalCaches.clear() } diff --git a/src/Utils/chat-utils.ts b/src/Utils/chat-utils.ts index 6ea8ebf3..9c1b8e00 100644 --- a/src/Utils/chat-utils.ts +++ b/src/Utils/chat-utils.ts @@ -19,13 +19,13 @@ import { type MessageLabelAssociation } from '../Types/LabelAssociation' import { type BinaryNode, getBinaryNodeChild, getBinaryNodeChildren, isJidGroup, jidNormalizedUser } from '../WABinary' -import { expandAppStateKeys } from './wasm-bridge' import { aesDecrypt, aesEncrypt, hmacSign } from './crypto' import { toNumber } from './generics' import type { ILogger } from './logger' import { LT_HASH_ANTI_TAMPERING } from './lt-hash' import { downloadContentFromMessage } from './messages-media' import { emitSyncActionResults, processContactAction } from './sync-action-utils' +import { expandAppStateKeys } from './wasm-bridge' type FetchAppStateSyncKey = (keyId: string) => Promise @@ -350,13 +350,7 @@ export const decodeSyncdPatch = async ( throw new Boom('Missing patchMac in patch message', { statusCode: 500 }) } - const patchMac = generatePatchMac( - msgSnapshotMac, - mutationmacs, - toNumber(msgVersion), - name, - mainKey.patchMacKey - ) + const patchMac = generatePatchMac(msgSnapshotMac, mutationmacs, toNumber(msgVersion), name, mainKey.patchMacKey) if (Buffer.compare(patchMac, msgPatchMac) !== 0) { throw new Boom('Invalid patch mac') } diff --git a/src/Utils/circuit-breaker.ts b/src/Utils/circuit-breaker.ts index 143f0729..80db263b 100644 --- a/src/Utils/circuit-breaker.ts +++ b/src/Utils/circuit-breaker.ts @@ -162,15 +162,17 @@ export class CircuitBreaker extends EventEmitter { volumeThreshold: options.volumeThreshold ?? 5, shouldCountError: options.shouldCountError ?? (() => true), collectMetrics: options.collectMetrics ?? true, - fallback: options.fallback ?? (() => { - throw new CircuitOpenError(this.options.name, this.state) - }), + fallback: + options.fallback ?? + (() => { + throw new CircuitOpenError(this.options.name, this.state) + }), onStateChange: options.onStateChange ?? (() => {}), onFailure: options.onFailure ?? (() => {}), onSuccess: options.onSuccess ?? (() => {}), onOpen: options.onOpen ?? (() => {}), onClose: options.onClose ?? (() => {}), - onHalfOpen: options.onHalfOpen ?? (() => {}), + onHalfOpen: options.onHalfOpen ?? (() => {}) } this.lastStateChange = Date.now() @@ -256,11 +258,11 @@ export class CircuitBreaker extends EventEmitter { }, this.options.timeout) Promise.resolve(operation()) - .then((result) => { + .then(result => { clearTimeout(timer) resolve(result) }) - .catch((error) => { + .catch(error => { clearTimeout(timer) reject(error) }) @@ -327,10 +329,7 @@ export class CircuitBreaker extends EventEmitter { // Check if we should trip the circuit const recentFailures = this.failureRecords.length - if ( - this.totalCalls >= this.options.volumeThreshold && - recentFailures >= this.options.failureThreshold - ) { + if (this.totalCalls >= this.options.volumeThreshold && recentFailures >= this.options.failureThreshold) { this.transitionTo('open') } } @@ -341,9 +340,7 @@ export class CircuitBreaker extends EventEmitter { */ private cleanOldFailures(): void { const cutoff = Date.now() - this.options.failureWindow - this.failureRecords = this.failureRecords.filter( - (record) => record.timestamp > cutoff - ) + this.failureRecords = this.failureRecords.filter(record => record.timestamp > cutoff) } /** @@ -451,9 +448,7 @@ export class CircuitBreaker extends EventEmitter { getStats(): CircuitBreakerStats { this.cleanOldFailures() - const failureRate = this.totalCalls > 0 - ? (this.totalFailures / this.totalCalls) * 100 - : 0 + const failureRate = this.totalCalls > 0 ? (this.totalFailures / this.totalCalls) * 100 : 0 return { state: this.state, @@ -471,7 +466,7 @@ export class CircuitBreaker extends EventEmitter { lastStateChange: this.lastStateChange, isOpen: this.isOpen(), isClosed: this.isClosed(), - isHalfOpen: this.isHalfOpen(), + isHalfOpen: this.isHalfOpen() } } @@ -489,6 +484,7 @@ export class CircuitBreaker extends EventEmitter { if (this.resetTimer) { clearTimeout(this.resetTimer) } + this.failureRecords = [] this.removeAllListeners() } @@ -515,6 +511,7 @@ export class CircuitBreakerRegistry { const breaker = new CircuitBreaker({ ...options, name }) this.breakers.set(name, breaker) } + return this.breakers.get(name)! } @@ -534,6 +531,7 @@ export class CircuitBreakerRegistry { breaker.destroy() return this.breakers.delete(name) } + return false } @@ -552,6 +550,7 @@ export class CircuitBreakerRegistry { for (const [name, breaker] of this.breakers) { stats[name] = breaker.getStats() } + return stats } @@ -571,6 +570,7 @@ export class CircuitBreakerRegistry { for (const breaker of this.breakers.values()) { breaker.destroy() } + this.breakers.clear() } } @@ -624,7 +624,7 @@ export function circuitBreaker(options: Omit & { * ) * ``` */ -export function withCircuitBreaker unknown>( +export function withCircuitBreaker unknown>( fn: T, options: CircuitBreakerOptions ): T { @@ -655,7 +655,7 @@ export function getCircuitHealth(): { return { healthy: openCircuits.length === 0, openCircuits, - stats, + stats } } @@ -676,19 +676,8 @@ export function getCircuitHealth(): { * } * ``` */ -export function createPreKeyCircuitBreaker( - customOptions?: Partial -): CircuitBreaker { - const preKeyErrorPatterns = [ - 'prekey', - 'pre-key', - 'session', - 'signal', - 'encrypt', - 'decrypt', - 'cipher', - 'key', - ] +export function createPreKeyCircuitBreaker(customOptions?: Partial): CircuitBreaker { + const preKeyErrorPatterns = ['prekey', 'pre-key', 'session', 'signal', 'encrypt', 'decrypt', 'cipher', 'key'] return new CircuitBreaker({ name: 'prekey-operations', @@ -698,18 +687,16 @@ export function createPreKeyCircuitBreaker( successThreshold: 2, shouldCountError: (error: Error) => { const message = error.message.toLowerCase() - return preKeyErrorPatterns.some((pattern) => message.includes(pattern)) + return preKeyErrorPatterns.some(pattern => message.includes(pattern)) }, - ...customOptions, + ...customOptions }) } /** * Create a circuit breaker for WebSocket connection operations */ -export function createConnectionCircuitBreaker( - customOptions?: Partial -): CircuitBreaker { +export function createConnectionCircuitBreaker(customOptions?: Partial): CircuitBreaker { const connectionErrorPatterns = [ 'econnrefused', 'econnreset', @@ -718,7 +705,7 @@ export function createConnectionCircuitBreaker( 'socket', 'websocket', 'connection', - 'network', + 'network' ] return new CircuitBreaker({ @@ -730,20 +717,16 @@ export function createConnectionCircuitBreaker( shouldCountError: (error: Error) => { const message = error.message.toLowerCase() const code = (error as NodeJS.ErrnoException).code?.toLowerCase() || '' - return connectionErrorPatterns.some( - (pattern) => message.includes(pattern) || code.includes(pattern) - ) + return connectionErrorPatterns.some(pattern => message.includes(pattern) || code.includes(pattern)) }, - ...customOptions, + ...customOptions }) } /** * Create a circuit breaker for message sending operations */ -export function createMessageCircuitBreaker( - customOptions?: Partial -): CircuitBreaker { +export function createMessageCircuitBreaker(customOptions?: Partial): CircuitBreaker { return new CircuitBreaker({ name: 'message-operations', failureThreshold: 5, @@ -751,7 +734,7 @@ export function createMessageCircuitBreaker( resetTimeout: 15000, successThreshold: 2, timeout: 30000, - ...customOptions, + ...customOptions }) } diff --git a/src/Utils/decode-wa-message.ts b/src/Utils/decode-wa-message.ts index 2ef0d008..7611a32e 100644 --- a/src/Utils/decode-wa-message.ts +++ b/src/Utils/decode-wa-message.ts @@ -19,7 +19,7 @@ import { } from '../WABinary' import { unpadRandomMax16 } from './generics' import type { ILogger } from './logger' -import { retry, type RetryOptions, RetryExhaustedError } from './retry-utils' +import { retry, RetryExhaustedError, type RetryOptions } from './retry-utils' export const getDecryptionJid = async (sender: string, repository: SignalRepositoryWithLIDStore): Promise => { if (isLidUser(sender) || isHostedLidUser(sender)) { @@ -269,7 +269,7 @@ export const decryptMessageNode = ( meLid: string, repository: SignalRepositoryWithLIDStore, logger: ILogger, - autoCleanCorrupted: boolean = true + autoCleanCorrupted = true ) => { const { fullMessage, author, sender } = decodeMessageNode(stanza, meId, meLid) return { @@ -322,11 +322,12 @@ export const decryptMessageNode = ( switch (e2eType) { case 'skmsg': msgBuffer = await retry( - () => repository.decryptGroupMessage({ - group: sender, - authorJid: author, - msg: content - }), + () => + repository.decryptGroupMessage({ + group: sender, + authorJid: author, + msg: content + }), { ...DECRYPTION_RETRY_OPTIONS, onRetry: (error, attempt, delay) => { @@ -341,11 +342,12 @@ export const decryptMessageNode = ( case 'pkmsg': case 'msg': msgBuffer = await retry( - () => repository.decryptMessage({ - jid: decryptionJid, - type: e2eType, - ciphertext: content - }), + () => + repository.decryptMessage({ + jid: decryptionJid, + type: e2eType, + ciphertext: content + }), { ...DECRYPTION_RETRY_OPTIONS, onRetry: (error, attempt, delay) => { @@ -416,10 +418,7 @@ export const decryptMessageNode = ( ) } else { // First occurrence - log as warning since auto-recovery will attempt - logger.warn( - errorContext, - '⚠️ Corrupted session detected - attempting auto-recovery' - ) + logger.warn(errorContext, '⚠️ Corrupted session detected - attempting auto-recovery') } // Automatic cleanup of corrupted session (if enabled) @@ -429,9 +428,8 @@ export const decryptMessageNode = ( // Mask only user portion of JID for privacy (preserve domain info) const { user, server } = jidDecode(decryptionJid) || {} - const maskedUser = user && user.length > 8 - ? `${user.substring(0, 4)}****${user.substring(user.length - 4)}` - : user + const maskedUser = + user && user.length > 8 ? `${user.substring(0, 4)}****${user.substring(user.length - 4)}` : user const maskedJid = maskedUser && server ? `${maskedUser}@${server}` : decryptionJid logger.info( @@ -439,19 +437,13 @@ export const decryptMessageNode = ( `🔄 Session Reset | JID: ${maskedJid} | Targeted: ${deletedCount} devices | Will recreate on next message` ) } catch (cleanupErr) { - logger.error( - { decryptionJid, err: cleanupErr }, - '❌ Failed to cleanup corrupted session' - ) + logger.error({ decryptionJid, err: cleanupErr }, '❌ Failed to cleanup corrupted session') } } } else if (isSessionRecord) { // Session record errors are transient - retry should handle them if (isRetryExhausted) { - logger.error( - errorContext, - `Failed to decrypt: No session record found after ${err.attempts} attempts` - ) + logger.error(errorContext, `Failed to decrypt: No session record found after ${err.attempts} attempts`) } else { logger.debug(errorContext, 'No session record - will retry') } diff --git a/src/Utils/event-buffer.ts b/src/Utils/event-buffer.ts index df92d939..bcbd5ed2 100644 --- a/src/Utils/event-buffer.ts +++ b/src/Utils/event-buffer.ts @@ -11,11 +11,11 @@ import type { WAMessageKey } from '../Types' import { WAMessageStatus } from '../Types' +import { logEventBuffer } from './baileys-logger' import { trimUndefined } from './generics' import type { ILogger } from './logger' import { updateMessageWithReaction, updateMessageWithReceipt } from './messages' import { isRealMessage, shouldIncrementChatUnread } from './process-message' -import { logEventBuffer } from './baileys-logger' // ============================================================================ // BUFFER CONFIGURATION - Environment Variable Support @@ -329,6 +329,7 @@ class AdaptiveTimeoutCalculator { } else if (this.currentTimeout >= this.maxTimeout * 0.8) { return 'conservative' } + return 'balanced' } @@ -339,6 +340,7 @@ class AdaptiveTimeoutCalculator { if (this.eventTimestamps.length < 2) { return 0 } + const oldest = this.eventTimestamps[0] const newest = this.eventTimestamps[this.eventTimestamps.length - 1] if (oldest === undefined || newest === undefined) return 0 @@ -428,18 +430,20 @@ export const makeEventBuffer = ( const MAX_METRICS_QUEUE_SIZE = 1000 // Cap to prevent unbounded growth if (config.enableMetrics) { - import('./prometheus-metrics').then(m => { - metricsModule = m - logger.debug({ queuedCount: metricsQueue.length }, '📊 Prometheus metrics loaded, flushing buffered metrics') - // Flush buffered metrics - metricsQueue.forEach(fn => fn()) - metricsQueue = [] - }).catch(() => { - logger.debug('Prometheus metrics not available for event buffer') - metricsImportFailed = true - // Clear queue to prevent memory leak - metricsQueue = [] - }) + import('./prometheus-metrics') + .then(m => { + metricsModule = m + logger.debug({ queuedCount: metricsQueue.length }, '📊 Prometheus metrics loaded, flushing buffered metrics') + // Flush buffered metrics + metricsQueue.forEach(fn => fn()) + metricsQueue = [] + }) + .catch(() => { + logger.debug('Prometheus metrics not available for event buffer') + metricsImportFailed = true + // Clear queue to prevent memory leak + metricsQueue = [] + }) } // Helper to record metrics with buffer support @@ -467,6 +471,7 @@ export const makeEventBuffer = ( clearTimeout(bufferTimeout) bufferTimeout = null } + if (flushPendingTimeout) { clearTimeout(flushPendingTimeout) flushPendingTimeout = null @@ -486,10 +491,13 @@ export const makeEventBuffer = ( function checkBufferOverflow(): boolean { if (currentEventCount >= config.maxBufferSize) { stats.overflowsDetected++ - logger.warn({ - currentSize: currentEventCount, - maxSize: config.maxBufferSize - }, 'Buffer overflow detected, forcing flush') + logger.warn( + { + currentSize: currentEventCount, + maxSize: config.maxBufferSize + }, + 'Buffer overflow detected, forcing flush' + ) logEventBuffer('buffer_overflow', { currentSize: currentEventCount, maxSize: config.maxBufferSize @@ -500,6 +508,7 @@ export const makeEventBuffer = ( } else if (!metricsImportFailed && metricsQueue.length < MAX_METRICS_QUEUE_SIZE) { metricsQueue.push(() => metricsModule?.recordBufferOverflow()) } + flush(true) return true } @@ -507,11 +516,14 @@ export const makeEventBuffer = ( // Warn if approaching threshold const threshold = config.maxBufferSize * config.bufferWarnThreshold if (currentEventCount >= threshold && currentEventCount < config.maxBufferSize) { - logger.debug({ - currentSize: currentEventCount, - threshold, - maxSize: config.maxBufferSize - }, 'Buffer approaching overflow threshold') + logger.debug( + { + currentSize: currentEventCount, + threshold, + maxSize: config.maxBufferSize + }, + 'Buffer approaching overflow threshold' + ) } return false @@ -524,11 +536,14 @@ export const makeEventBuffer = ( if (historyCache.size > config.maxHistoryCacheSize) { const removed = historyCache.cleanup(config.lruCleanupRatio) stats.lruCleanups++ - logger.debug({ - removed: removed.length, - remaining: historyCache.size, - maxSize: config.maxHistoryCacheSize - }, 'LRU cleanup performed on history cache') + logger.debug( + { + removed: removed.length, + remaining: historyCache.size, + maxSize: config.maxHistoryCacheSize + }, + 'LRU cleanup performed on history cache' + ) logEventBuffer('cache_cleanup', { removed: removed.length, remaining: historyCache.size @@ -558,9 +573,7 @@ export const makeEventBuffer = ( clearAllTimers() // Use adaptive timeout if enabled - const timeout = config.enableAdaptiveTimeout - ? adaptiveTimeout.getTimeout() - : config.bufferTimeoutMs + const timeout = config.enableAdaptiveTimeout ? adaptiveTimeout.getTimeout() : config.bufferTimeoutMs stats.currentTimeout = timeout bufferTimeout = setTimeout(() => { @@ -577,7 +590,7 @@ export const makeEventBuffer = ( bufferCount++ } - function flush(force: boolean = false): boolean { + function flush(force = false): boolean { if (destroyed) { logger.warn('Attempted to flush destroyed event buffer') return false @@ -598,9 +611,10 @@ export const makeEventBuffer = ( stats.lastFlushAt = Date.now() // Update average events per flush - stats.avgEventsPerFlush = stats.totalFlushes > 0 - ? (stats.avgEventsPerFlush * (stats.totalFlushes - 1) + eventCount) / stats.totalFlushes - : eventCount + stats.avgEventsPerFlush = + stats.totalFlushes > 0 + ? (stats.avgEventsPerFlush * (stats.totalFlushes - 1) + eventCount) / stats.totalFlushes + : eventCount // Clear timeouts clearAllTimers() @@ -617,7 +631,9 @@ export const makeEventBuffer = ( if (metricsModule) { metricsModule.updateAdaptiveMetrics(adaptiveTimeout.getEventRate(), adaptiveTimeout.isHealthy()) } else if (!metricsImportFailed && metricsQueue.length < MAX_METRICS_QUEUE_SIZE) { - metricsQueue.push(() => metricsModule?.updateAdaptiveMetrics(adaptiveTimeout.getEventRate(), adaptiveTimeout.isHealthy())) + metricsQueue.push(() => + metricsModule?.updateAdaptiveMetrics(adaptiveTimeout.getEventRate(), adaptiveTimeout.isHealthy()) + ) } } @@ -790,9 +806,7 @@ export const makeEventBuffer = ( ...stats, currentBufferSize: currentEventCount, historyCacheSize: historyCache.size, - currentTimeout: config.enableAdaptiveTimeout - ? adaptiveTimeout.getTimeout() - : config.bufferTimeoutMs + currentTimeout: config.enableAdaptiveTimeout ? adaptiveTimeout.getTimeout() : config.bufferTimeoutMs } }, getConfig() { @@ -817,6 +831,7 @@ export const makeEventBuffer = ( if (functionTimeout) { clearTimeout(functionTimeout) } + functionTimeout = setTimeout(() => { functionTimeout = null if (isBuffering && bufferCount === 1 && !destroyed) { @@ -832,6 +847,7 @@ export const makeEventBuffer = ( clearTimeout(functionTimeout) functionTimeout = null } + throw error } finally { bufferCount = Math.max(0, bufferCount - 1) @@ -853,6 +869,7 @@ export const makeEventBuffer = ( if (destroyed) { throw new Error('Cannot add listener to destroyed event buffer') } + return ev.on(...args) }, off: (...args) => ev.off(...args), @@ -987,6 +1004,7 @@ function append( logger.debug({ update }, 'chats.update: update missing id, skipping') continue } + const conditionMatches = update.conditional ? update.conditional(data) : true if (conditionMatches) { delete update.conditional @@ -1068,6 +1086,7 @@ function append( logger.debug({ update }, 'contacts.update: update missing id, skipping') continue } + // merge into prior upsert const upsert = data.historySets.contacts[id] || data.contactUpserts[id] if (upsert) { @@ -1193,6 +1212,7 @@ function append( logger.debug({ update }, 'groups.update: update missing id, skipping') continue } + const groupUpdate = data.groupUpdates[id] || {} if (!data.groupUpdates[id]) { data.groupUpdates[id] = Object.assign(groupUpdate, update) @@ -1239,6 +1259,7 @@ function append( logger.debug({ messageKey: message.key }, 'decrementChatReadCounter: remoteJid missing, skipping') return } + const chat = data.chatUpdates[chatId] || data.chatUpserts[chatId] if ( isRealMessage(message) && diff --git a/src/Utils/generics.ts b/src/Utils/generics.ts index 03bbb5dc..997b6b8e 100644 --- a/src/Utils/generics.ts +++ b/src/Utils/generics.ts @@ -253,7 +253,11 @@ export const fetchLatestBaileysVersion = async (options: RequestInit = {}) => { const versionMatch = versionLine.match(/const version = \[(\d+),\s*(\d+),\s*(\d+)\]/) if (versionMatch) { - const version = [parseInt(versionMatch[1] ?? '0'), parseInt(versionMatch[2] ?? '0'), parseInt(versionMatch[3] ?? '0')] as WAVersion + const version = [ + parseInt(versionMatch[1] ?? '0'), + parseInt(versionMatch[2] ?? '0'), + parseInt(versionMatch[3] ?? '0') + ] as WAVersion return { version, diff --git a/src/Utils/health-status.ts b/src/Utils/health-status.ts index 25a92033..7486226d 100644 --- a/src/Utils/health-status.ts +++ b/src/Utils/health-status.ts @@ -6,8 +6,8 @@ * @module Utils/health-status */ -import { getVersionCacheStatus } from './version-cache.js' import { globalCircuitRegistry } from './circuit-breaker.js' +import { getVersionCacheStatus } from './version-cache.js' /** * Circuit breaker health information diff --git a/src/Utils/history.ts b/src/Utils/history.ts index d5d6183a..6442abf2 100644 --- a/src/Utils/history.ts +++ b/src/Utils/history.ts @@ -3,10 +3,6 @@ import { inflate } from 'zlib' import { proto } from '../../WAProto/index.js' import type { Chat, Contact, LIDMapping, WAMessage } from '../Types' import { WAMessageStubType } from '../Types' -import { toNumber } from './generics' -import { normalizeMessageContent } from './messages' -import { downloadContentFromMessage } from './messages-media' -import type { ILogger } from './logger.js' import { isAnyLidUser, isAnyPnUser, @@ -16,6 +12,10 @@ import { jidDecode, jidNormalizedUser } from '../WABinary/index.js' +import { toNumber } from './generics' +import type { ILogger } from './logger.js' +import { normalizeMessageContent } from './messages' +import { downloadContentFromMessage } from './messages-media' const inflatePromise = promisify(inflate) @@ -255,10 +255,7 @@ const extractPnFromMessages = (messages: proto.IHistorySyncMsg[]): string | unde * @see https://github.com/WhiskeySockets/Baileys/issues/2263 */ export const processHistoryMessage = (item: proto.IHistorySync, logger?: ILogger) => { - logger?.trace( - { syncType: item.syncType, progress: item.progress }, - 'processing history sync' - ) + logger?.trace({ syncType: item.syncType, progress: item.progress }, 'processing history sync') const messages: WAMessage[] = [] const contacts: Contact[] = [] const chats: Chat[] = [] @@ -275,6 +272,7 @@ export const processHistoryMessage = (item: proto.IHistorySync, logger?: ILogger if (!mapping.lid || !mapping.pn) { return } + lidPnMap.set(mapping.lid, mapping) } @@ -298,11 +296,7 @@ export const processHistoryMessage = (item: proto.IHistorySync, logger?: ILogger // Source 2: Extract LID-PN mapping from conversation object // This handles cases where the mapping isn't in phoneNumberToLidMappings - const conversationMapping = extractLidPnFromConversation( - chatId, - chat.lidJid, - chat.pnJid - ) + const conversationMapping = extractLidPnFromConversation(chatId, chat.lidJid, chat.pnJid) if (conversationMapping) { addLidPnMapping(conversationMapping) } else if (isAnyLidUser(chatId) && !chat.pnJid) { @@ -370,7 +364,7 @@ export const processHistoryMessage = (item: proto.IHistorySync, logger?: ILogger break case proto.HistorySync.HistorySyncType.PUSH_NAME: - for (const c of (item.pushnames ?? [])) { + for (const c of item.pushnames ?? []) { contacts.push({ id: c.id!, notify: c.pushname! }) } diff --git a/src/Utils/identity-change-handler.ts b/src/Utils/identity-change-handler.ts index 08511212..4dad2b7a 100644 --- a/src/Utils/identity-change-handler.ts +++ b/src/Utils/identity-change-handler.ts @@ -126,10 +126,7 @@ export async function handleIdentityChange( // Skip companion devices - they don't need session refresh const decoded = jidDecode(from) if (decoded?.device && decoded.device !== 0) { - ctx.logger.debug( - { jid: from, device: decoded.device }, - 'ignoring identity change from companion device' - ) + ctx.logger.debug({ jid: from, device: decoded.device }, 'ignoring identity change from companion device') return { action: 'skipped_companion_device', device: decoded.device } } @@ -178,10 +175,7 @@ export async function handleIdentityChange( await ctx.assertSessions([from], true) return { action: 'session_refreshed', hadExistingSession: hasExistingSession.exists } } catch (error) { - ctx.logger.warn( - { error, jid: from }, - 'failed to assert sessions after identity change' - ) + ctx.logger.warn({ error, jid: from }, 'failed to assert sessions after identity change') return { action: 'session_refresh_failed', error } } } diff --git a/src/Utils/logger-adapter.ts b/src/Utils/logger-adapter.ts index 8d7d3b4b..62c519d0 100644 --- a/src/Utils/logger-adapter.ts +++ b/src/Utils/logger-adapter.ts @@ -9,9 +9,9 @@ * - Compatibilidade com Pino, Console e StructuredLogger */ -import type { ILogger } from './logger.js' import type P from 'pino' -import { StructuredLogger, createStructuredLogger, type LogLevel, LOG_LEVEL_VALUES } from './structured-logger.js' +import type { ILogger } from './logger.js' +import { createStructuredLogger, LOG_LEVEL_VALUES, type LogLevel, StructuredLogger } from './structured-logger.js' /** * Tipo de logger suportado @@ -43,7 +43,7 @@ const PINO_LEVEL_MAPPING: Record = { 30: 'info', 40: 'warn', 50: 'error', - 60: 'fatal', + 60: 'fatal' } /** @@ -56,7 +56,7 @@ const STRUCTURED_TO_PINO_LEVEL: Record = { warn: 40, error: 50, fatal: 60, - silent: 100, + silent: 100 } /** @@ -74,7 +74,7 @@ export class LoggerAdapter implements ILogger { targetType: config.targetType || 'structured', levelMapping: config.levelMapping, contextTransformer: config.contextTransformer, - logFilter: config.logFilter, + logFilter: config.logFilter } } @@ -133,6 +133,7 @@ export class LoggerAdapter implements ILogger { if (this.config.logFilter) { return this.config.logFilter(level, msg, obj) } + return true } @@ -191,7 +192,7 @@ export class PinoToStructuredAdapter implements ILogger { this.pinoLogger = pinoLogger this.structuredLogger = createStructuredLogger({ level: this.mapPinoLevel(pinoLogger.level), - ...structuredLoggerConfig, + ...structuredLoggerConfig }) } @@ -212,7 +213,7 @@ export class PinoToStructuredAdapter implements ILogger { warn: 'warn', error: 'error', fatal: 'fatal', - silent: 'silent', + silent: 'silent' } return levelMap[pinoLevel] || 'info' } @@ -258,10 +259,7 @@ export class PinoToStructuredAdapter implements ILogger { /** * Factory para criar adapter baseado no tipo de logger */ -export function createLoggerAdapter( - logger: ILogger, - config?: Partial -): LoggerAdapter { +export function createLoggerAdapter(logger: ILogger, config?: Partial): LoggerAdapter { return new LoggerAdapter(logger, config) } @@ -283,13 +281,14 @@ export function normalizeLogger(logger: unknown): ILogger { if (typeof logObj.child === 'function') { return normalizeLogger((logObj.child as (obj: Record) => unknown)(obj)) } + return normalizeLogger(logger) }, trace: createLogMethod(logObj, 'trace'), debug: createLogMethod(logObj, 'debug'), info: createLogMethod(logObj, 'info'), warn: createLogMethod(logObj, 'warn'), - error: createLogMethod(logObj, 'error'), + error: createLogMethod(logObj, 'error') } } @@ -320,10 +319,7 @@ export function isILogger(obj: unknown): obj is ILogger { /** * Cria método de log genérico */ -function createLogMethod( - logger: Record, - level: string -): (obj: unknown, msg?: string) => void { +function createLogMethod(logger: Record, level: string): (obj: unknown, msg?: string) => void { return (obj: unknown, msg?: string) => { if (typeof logger[level] === 'function') { ;(logger[level] as (obj: unknown, msg?: string) => void)(obj, msg) @@ -370,7 +366,7 @@ export function createConsoleLogger(prefix?: string): ILogger { }, error(obj: unknown, msg?: string): void { console.error(formatMessage('error', obj, msg)) - }, + } } } diff --git a/src/Utils/logger.ts b/src/Utils/logger.ts index 24ff980c..1b98dac8 100644 --- a/src/Utils/logger.ts +++ b/src/Utils/logger.ts @@ -55,8 +55,8 @@ function loadLoggerConfig(): LoggerConfig { levelFilters: { info: process.env.LOGGER_INFO !== 'false', warn: process.env.LOGGER_WARN !== 'false', - error: process.env.LOGGER_ERROR !== 'false', - }, + error: process.env.LOGGER_ERROR !== 'false' + } } } @@ -89,7 +89,7 @@ function createFilteredLogger(baseLogger: PinoLogger, config: LoggerConfig): ILo debug: baseLogger.debug.bind(baseLogger), info: config.levelFilters.info ? baseLogger.info.bind(baseLogger) : noop, warn: config.levelFilters.warn ? baseLogger.warn.bind(baseLogger) : noop, - error: config.levelFilters.error ? baseLogger.error.bind(baseLogger) : noop, + error: config.levelFilters.error ? baseLogger.error.bind(baseLogger) : noop } } @@ -98,6 +98,7 @@ function createFilteredLogger(baseLogger: PinoLogger, config: LoggerConfig): ILo */ function createSilentLogger(): ILogger { const noop = () => {} + const silentLogger: ILogger = { level: 'silent', child: () => silentLogger, @@ -105,7 +106,7 @@ function createSilentLogger(): ILogger { debug: noop, info: noop, warn: noop, - error: noop, + error: noop } return silentLogger } @@ -124,7 +125,7 @@ function createLogger(): ILogger { // Create pino options const pinoOptions: P.LoggerOptions = { level: config.level, - timestamp: () => `,"time":"${new Date().toJSON()}"`, + timestamp: () => `,"time":"${new Date().toJSON()}"` } // Add pretty printing when explicitly set and available @@ -134,8 +135,8 @@ function createLogger(): ILogger { options: { colorize: true, translateTime: 'SYS:standard', - ignore: 'pid,hostname', - }, + ignore: 'pid,hostname' + } } } diff --git a/src/Utils/message-retry-manager.ts b/src/Utils/message-retry-manager.ts index 5c3ad7be..4a0336ad 100644 --- a/src/Utils/message-retry-manager.ts +++ b/src/Utils/message-retry-manager.ts @@ -46,7 +46,7 @@ export enum RetryReason { /** ADV (Announcement Delivery Verification) failure */ AdvFailure = 12, /** Status revoke was delayed */ - StatusRevokeDelay = 13, + StatusRevokeDelay = 13 } /** @@ -56,7 +56,7 @@ export enum RetryReason { */ export const MAC_ERROR_CODES = new Set([ RetryReason.SignalErrorInvalidMessage, - RetryReason.SignalErrorBadMac, + RetryReason.SignalErrorBadMac ]) /** @@ -66,7 +66,7 @@ export const SESSION_ERROR_CODES = new Set([ RetryReason.SignalErrorNoSession, RetryReason.SignalErrorInvalidSession, RetryReason.SignalErrorInvalidKey, - RetryReason.SignalErrorInvalidKeyId, + RetryReason.SignalErrorInvalidKeyId ]) export interface RecentMessageKey { to: string @@ -196,10 +196,7 @@ export class MessageRetryManager { const reasonName = RetryReason[errorCode] || `code_${errorCode}` metrics.signalMacErrors?.inc({ action: 'session_recreation' }) metrics.signalSessionRecreations?.inc({ reason: 'mac_error' }) - this.logger.warn( - { jid, errorCode: reasonName }, - 'MAC error detected, forcing immediate session recreation' - ) + this.logger.warn({ jid, errorCode: reasonName }, 'MAC error detected, forcing immediate session recreation') return { reason: `MAC error (${reasonName}) - contact may have reinstalled WhatsApp`, recreate: true diff --git a/src/Utils/messages-media.ts b/src/Utils/messages-media.ts index 5271c54a..53a1fa95 100644 --- a/src/Utils/messages-media.ts +++ b/src/Utils/messages-media.ts @@ -127,13 +127,17 @@ const extractVideoThumb = async ( size: { width: number; height: number } ) => new Promise((resolve, reject) => { - execFile('ffmpeg', ['-ss', time, '-i', path, '-y', '-vf', `scale=${size.width}:-1`, '-vframes', '1', '-f', 'image2', destPath], err => { - if (err) { - reject(err) - } else { - resolve() + execFile( + 'ffmpeg', + ['-ss', time, '-i', path, '-y', '-vf', `scale=${size.width}:-1`, '-vframes', '1', '-f', 'image2', destPath], + err => { + if (err) { + reject(err) + } else { + resolve() + } } - }) + ) }) export const extractImageThumb = async (bufferOrFilePath: Readable | Buffer | string, width = 32) => { diff --git a/src/Utils/messages.ts b/src/Utils/messages.ts index e2a8e2bc..8af35b64 100644 --- a/src/Utils/messages.ts +++ b/src/Utils/messages.ts @@ -47,8 +47,8 @@ import { getRawMediaUploadData, type MediaDownloadOptions } from './messages-media' -import { prepareStickerPackMessage } from './sticker-pack.js' import { shouldIncludeReportingToken } from './reporting-utils' +import { prepareStickerPackMessage } from './sticker-pack.js' type ExtractByKey = T extends Record ? T : never type RequireKey = T & { @@ -260,7 +260,10 @@ export const prepareWAMessageMedia = async ( })(), (async () => { try { - if ((requiresThumbnailComputation || requiresDurationComputation || requiresWaveformProcessing) && !originalFilePath) { + if ( + (requiresThumbnailComputation || requiresDurationComputation || requiresWaveformProcessing) && + !originalFilePath + ) { throw new Boom('Missing file path for processing') } @@ -513,7 +516,7 @@ export const generateButtonMessage = async ( // Determine header configuration const hasMedia = !!(headerImage || headerVideo) const header: proto.Message.InteractiveMessage.IHeader = { - title: hasMedia ? '' : (headerTitle || ''), + title: hasMedia ? '' : headerTitle || '', subtitle: '', hasMediaAttachment: hasMedia } @@ -635,34 +638,36 @@ export const generateCarouselMessage = async ( } // Map cards to the carousel format (processing media) - const carouselCards = await Promise.all(cards.map(async (card) => { - const hasMedia = !!(card.image || card.video) + const carouselCards = await Promise.all( + cards.map(async card => { + const hasMedia = !!(card.image || card.video) - const header: any = { - title: card.title || '', - hasMediaAttachment: hasMedia - } - - // Process media if present - if (hasMedia && mediaOptions) { - if (card.image) { - const { imageMessage } = await prepareWAMessageMedia({ image: card.image }, mediaOptions) - header.imageMessage = imageMessage - } else if (card.video) { - const { videoMessage } = await prepareWAMessageMedia({ video: card.video }, mediaOptions) - header.videoMessage = videoMessage + const header: any = { + title: card.title || '', + hasMediaAttachment: hasMedia } - } - return { - header, - body: { text: card.body || '' }, - footer: card.footer ? { text: card.footer } : undefined, - nativeFlowMessage: { - buttons: card.buttons.map(formatNativeFlowButton) + // Process media if present + if (hasMedia && mediaOptions) { + if (card.image) { + const { imageMessage } = await prepareWAMessageMedia({ image: card.image }, mediaOptions) + header.imageMessage = imageMessage + } else if (card.video) { + const { videoMessage } = await prepareWAMessageMedia({ video: card.video }, mediaOptions) + header.videoMessage = videoMessage + } } - } - })) + + return { + header, + body: { text: card.body || '' }, + footer: card.footer ? { text: card.footer } : undefined, + nativeFlowMessage: { + buttons: card.buttons.map(formatNativeFlowButton) + } + } + }) + ) // Build the interactive message with carousel // Match Pastorini's EXACT working structure: @@ -744,7 +749,7 @@ const LIST_LIMITS = { MAX_ROW_TITLE: 24, MAX_ROW_DESCRIPTION: 72, MAX_ROW_ID: 200, - MAX_BUTTON_TEXT: 20, + MAX_BUTTON_TEXT: 20 } as const /** @@ -760,17 +765,13 @@ const validateListSections = ( } if (sections.length > LIST_LIMITS.MAX_SECTIONS) { - throw new Boom( - `Maximum ${LIST_LIMITS.MAX_SECTIONS} sections allowed, got ${sections.length}`, - { statusCode: 400 } - ) + throw new Boom(`Maximum ${LIST_LIMITS.MAX_SECTIONS} sections allowed, got ${sections.length}`, { statusCode: 400 }) } if (buttonText && buttonText.length > LIST_LIMITS.MAX_BUTTON_TEXT) { - throw new Boom( - `buttonText max ${LIST_LIMITS.MAX_BUTTON_TEXT} characters, got ${buttonText.length}`, - { statusCode: 400 } - ) + throw new Boom(`buttonText max ${LIST_LIMITS.MAX_BUTTON_TEXT} characters, got ${buttonText.length}`, { + statusCode: 400 + }) } let totalRows = 0 @@ -796,10 +797,7 @@ const validateListSections = ( for (const row of section.rows) { const rowId = row.id || row.rowId || '' if (rowId.length > LIST_LIMITS.MAX_ROW_ID) { - throw new Boom( - `Row ID max ${LIST_LIMITS.MAX_ROW_ID} characters, got ${rowId.length}`, - { statusCode: 400 } - ) + throw new Boom(`Row ID max ${LIST_LIMITS.MAX_ROW_ID} characters, got ${rowId.length}`, { statusCode: 400 }) } if (row.title && row.title.length > LIST_LIMITS.MAX_ROW_TITLE) { @@ -821,10 +819,7 @@ const validateListSections = ( } if (totalRows > LIST_LIMITS.MAX_TOTAL_ROWS) { - throw new Boom( - `Maximum ${LIST_LIMITS.MAX_TOTAL_ROWS} total rows allowed, got ${totalRows}`, - { statusCode: 400 } - ) + throw new Boom(`Maximum ${LIST_LIMITS.MAX_TOTAL_ROWS} total rows allowed, got ${totalRows}`, { statusCode: 400 }) } } @@ -845,13 +840,15 @@ export const generateListMessage = (options: ListMessageOptions): WAMessageConte // Create native flow message with single_select button const nativeFlowMessage = { - buttons: [{ - name: 'single_select', - buttonParamsJson: JSON.stringify({ - title: buttonText, - sections: formattedSections - }) - }], + buttons: [ + { + name: 'single_select', + buttonParamsJson: JSON.stringify({ + title: buttonText, + sections: formattedSections + }) + } + ], messageParamsJson: JSON.stringify({}), messageVersion: 2 } @@ -860,11 +857,13 @@ export const generateListMessage = (options: ListMessageOptions): WAMessageConte const interactiveMessage: proto.Message.IInteractiveMessage = { body: { text: text || '' }, footer: footer ? { text: footer } : undefined, - header: title ? { - title, - subtitle: '', - hasMediaAttachment: false - } : undefined, + header: title + ? { + title, + subtitle: '', + hasMediaAttachment: false + } + : undefined, nativeFlowMessage } @@ -919,15 +918,7 @@ export const generateListMessage = (options: ListMessageOptions): WAMessageConte * ``` */ export const generateProductListMessage = (options: ProductListMessageOptions): WAMessageContent => { - const { - title, - description, - buttonText, - footerText, - businessOwnerJid, - productSections, - headerImage - } = options + const { title, description, buttonText, footerText, businessOwnerJid, productSections, headerImage } = options // Validation if (!title || title.trim().length === 0) { @@ -965,7 +956,9 @@ export const generateProductListMessage = (options: ProductListMessageOptions): // Validate each product has a valid productId for (const product of section.products) { if (!product || typeof product.productId !== 'string' || product.productId.trim().length === 0) { - throw new Boom(`Each product in section "${section.title}" must have a non-empty productId`, { statusCode: 400 }) + throw new Boom(`Each product in section "${section.title}" must have a non-empty productId`, { + statusCode: 400 + }) } } } @@ -992,9 +985,14 @@ export const generateProductListMessage = (options: ProductListMessageOptions): // Add header image if provided (with validation) if (headerImage) { - if (!headerImage.productId || typeof headerImage.productId !== 'string' || headerImage.productId.trim().length === 0) { + if ( + !headerImage.productId || + typeof headerImage.productId !== 'string' || + headerImage.productId.trim().length === 0 + ) { throw new Boom('headerImage.productId must be a non-empty string', { statusCode: 400 }) } + productListInfo.headerImage = { productId: headerImage.productId, jpegThumbnail: headerImage.jpegThumbnail @@ -1037,9 +1035,7 @@ export const generateProductListMessage = (options: ProductListMessageOptions): * await sock.sendMessage(jid, msg) * ``` */ -export const generateProductCarouselMessage = ( - options: ProductCarouselMessageOptions -): WAMessageContent => { +export const generateProductCarouselMessage = (options: ProductCarouselMessageOptions): WAMessageContent => { const { businessOwnerJid, products, body } = options if (!businessOwnerJid || typeof businessOwnerJid !== 'string' || businessOwnerJid.trim().length === 0) { @@ -1069,7 +1065,7 @@ export const generateProductCarouselMessage = ( // Build cards array - each card is an IInteractiveMessage with collectionMessage // collectionMessage references a product from the business catalog - const cards: proto.Message.IInteractiveMessage[] = products.map((product) => ({ + const cards: proto.Message.IInteractiveMessage[] = products.map(product => ({ collectionMessage: { bizJid: normalizedBizJid, id: product.productId, @@ -1211,7 +1207,7 @@ export const generateWAMessageContent = async ( m.messageContextInfo = generated.messageContextInfo options.logger?.info('Sending carousel as direct interactiveMessage (Pastorini approach, no viewOnce wrapper)') // Return plain JS object - no fromObject() to avoid corrupting nested carousel structures - return m as WAMessageContent + return m } // Check for nativeList else if (hasNonNullishProperty(message, 'nativeList')) { @@ -1285,6 +1281,7 @@ export const generateWAMessageContent = async ( } else if (btn.callButton) { return { index: btn.index, callButton: btn.callButton } } + return btn }) @@ -1300,10 +1297,7 @@ export const generateWAMessageContent = async ( options.logger?.warn('[EXPERIMENTAL] Sending templateMessage - this may not work and can cause bans') } else if (hasNonNullishProperty(message, 'sections')) { // Process list messages - validate limits - validateListSections( - (message as any).sections, - (message as any).buttonText - ) + validateListSections((message as any).sections, (message as any).buttonText) const listMessage: proto.Message.IListMessage = { title: (message as any).title, description: (message as any).text, @@ -1523,21 +1517,22 @@ export const generateWAMessageContent = async ( : proto.Message.ButtonsMessage.HeaderType.VIDEO // Extract only media properties to avoid type errors - const mediaContent: AnyMediaMessageContent = mediaType === 'image' - ? { - image: (message as any).image, - caption: (message as any).caption, - jpegThumbnail: (message as any).jpegThumbnail, - mimetype: (message as any).mimetype - } - : { - video: (message as any).video, - caption: (message as any).caption, - jpegThumbnail: (message as any).jpegThumbnail, - gifPlayback: (message as any).gifPlayback, - ptv: (message as any).ptv, - mimetype: (message as any).mimetype - } + const mediaContent: AnyMediaMessageContent = + mediaType === 'image' + ? { + image: (message as any).image, + caption: (message as any).caption, + jpegThumbnail: (message as any).jpegThumbnail, + mimetype: (message as any).mimetype + } + : { + video: (message as any).video, + caption: (message as any).caption, + jpegThumbnail: (message as any).jpegThumbnail, + gifPlayback: (message as any).gifPlayback, + ptv: (message as any).ptv, + mimetype: (message as any).mimetype + } // Prepare media const mediaMessage = await prepareWAMessageMedia(mediaContent, options) @@ -1769,7 +1764,11 @@ export const generateWAMessageFromContent = ( const innerContent = innerMessage[key as Exclude] const contextInfo: proto.IContextInfo = - (innerContent && typeof innerContent === 'object' && 'contextInfo' in innerContent && (innerContent as Record).contextInfo as proto.IContextInfo) || {} + (innerContent && + typeof innerContent === 'object' && + 'contextInfo' in innerContent && + ((innerContent as Record).contextInfo as proto.IContextInfo)) || + {} contextInfo.participant = jidNormalizedUser(participant!) contextInfo.stanzaId = quoted.key.id contextInfo.quotedMessage = quotedMsg diff --git a/src/Utils/pre-key-manager.ts b/src/Utils/pre-key-manager.ts index 0dd94da7..78400371 100644 --- a/src/Utils/pre-key-manager.ts +++ b/src/Utils/pre-key-manager.ts @@ -168,7 +168,8 @@ export class PreKeyManager { this.logger.debug('PreKeyManager already destroyed') return } - this.destroyed = true // ← Set IMMEDIATELY to close race window + + this.destroyed = true // ← Set IMMEDIATELY to close race window this.logger.debug('🗑️ Destroying PreKeyManager') diff --git a/src/Utils/process-message.ts b/src/Utils/process-message.ts index 55ca3c7e..8ebfbf60 100644 --- a/src/Utils/process-message.ts +++ b/src/Utils/process-message.ts @@ -18,7 +18,6 @@ import type { WAMessage, WAMessageKey } from '../Types' -import { metrics, recordHistorySyncMessages } from './prometheus-metrics.js' import { WAMessageStubType } from '../Types' import { getContentType, normalizeMessageContent } from '../Utils/messages' import { @@ -36,6 +35,7 @@ import { aesDecryptGCM, hmacSign } from './crypto' import { getKeyAuthor, toNumber } from './generics' import { downloadAndProcessHistorySyncNotification } from './history' import type { ILogger } from './logger' +import { metrics, recordHistorySyncMessages } from './prometheus-metrics.js' type ProcessMessageContext = { shouldProcessHistoryMsg: boolean @@ -102,8 +102,8 @@ export const cleanMessage = (message: WAMessage, meId: string, meLid: string) => // if the sender believed the message being reacted to is not from them // we've to correct the key to be from them, or some other participant msgKey.fromMe = !msgKey.fromMe - ? areJidsSameUser(msgKey.participant || (msgKey.remoteJid!), meId) || - areJidsSameUser(msgKey.participant || (msgKey.remoteJid!), meLid) + ? areJidsSameUser(msgKey.participant || msgKey.remoteJid!, meId) || + areJidsSameUser(msgKey.participant || msgKey.remoteJid!, meLid) : // if the message being reacted to, was from them // fromMe automatically becomes false false @@ -133,6 +133,7 @@ export const normalizeMessageJids = async ( } else { logger?.debug({ lid: jid }, 'PN not found for inbound LID, keeping LID') } + return pn || jid } @@ -160,9 +161,7 @@ export const isRealMessage = (message: WAMessage) => { const hasSomeContent = !!getContentType(normalizedContent) const stubType = message.messageStubType ?? 0 return ( - (!!normalizedContent || - REAL_MSG_STUB_TYPES.has(stubType) || - REAL_MSG_REQ_ME_STUB_TYPES.has(stubType)) && + (!!normalizedContent || REAL_MSG_STUB_TYPES.has(stubType) || REAL_MSG_REQ_ME_STUB_TYPES.has(stubType)) && hasSomeContent && !normalizedContent?.protocolMessage && !normalizedContent?.reactionMessage && @@ -326,6 +325,7 @@ const processMessage = async ( if (!histNotification) { break } + const process = shouldProcessHistoryMsg const isLatest = !creds.processedHistoryMessages?.length @@ -363,10 +363,7 @@ const processMessage = async ( 'stored LID-PN mappings from history sync' ) if (result.stored > 0) { - logger?.info( - { stored: result.stored }, - 'fallback LID mappings are now available from history sync' - ) + logger?.info({ stored: result.stored }, 'fallback LID mappings are now available from history sync') } } catch (error) { logger?.warn({ error }, 'Failed to store LID-PN mappings from history sync') @@ -476,10 +473,7 @@ const processMessage = async ( // Preserve pushName if not present in PDO response if (cachedData.pushName && !webMessageInfo.pushName) { webMessageInfo.pushName = cachedData.pushName - logger?.debug( - { msgId: webMessageInfo.key?.id }, - 'CTWA: Restored pushName from cached metadata' - ) + logger?.debug({ msgId: webMessageInfo.key?.id }, 'CTWA: Restored pushName from cached metadata') } // Preserve participantAlt (LID) if not present in PDO response @@ -498,10 +492,7 @@ const processMessage = async ( // Preserve original participant if not in PDO response if (cachedData.participant && webMessageInfo.key && !webMessageInfo.key.participant) { webMessageInfo.key.participant = cachedData.participant - logger?.debug( - { msgId: webMessageInfo.key?.id }, - 'CTWA: Restored participant from cached metadata' - ) + logger?.debug({ msgId: webMessageInfo.key?.id }, 'CTWA: Restored participant from cached metadata') } // Only use cached timestamp if PDO response doesn't have one @@ -630,7 +621,7 @@ const processMessage = async ( const meIdNormalised = jidNormalizedUser(meId) // all jids need to be PN - const eventCreatorKey = creationMsgKey.participant || (creationMsgKey.remoteJid!) + const eventCreatorKey = creationMsgKey.participant || creationMsgKey.remoteJid! const eventCreatorPn = isLidUser(eventCreatorKey) ? await signalRepository.lidMapping.getPNForLID(eventCreatorKey) : eventCreatorKey diff --git a/src/Utils/prometheus-metrics.ts b/src/Utils/prometheus-metrics.ts index d0bb4de3..baaec86d 100644 --- a/src/Utils/prometheus-metrics.ts +++ b/src/Utils/prometheus-metrics.ts @@ -34,7 +34,7 @@ * @module Utils/prometheus-metrics */ -import { createServer, IncomingMessage, ServerResponse, Server } from 'http' +import { createServer, IncomingMessage, Server, ServerResponse } from 'http' import * as os from 'os' import * as promClient from 'prom-client' @@ -55,6 +55,7 @@ function configureRegistry(defaultLabels?: Labels): void { if (registryConfigured) { return // Already configured, ignore subsequent calls } + registryConfigured = true // Allow setting empty labels to clear previous (useful for testing/reconfiguration) @@ -151,6 +152,7 @@ function parseLabelsFromEnv(envValue: string | undefined): Labels { if (typeof parsed === 'object' && parsed !== null) { return parsed as Labels } + return {} } catch { return {} @@ -173,8 +175,12 @@ export function loadMetricsConfig(): MetricsConfig { // Separate flag for system metrics (CPU/memory) - independent from collectDefaultMetrics includeSystem: (process.env.BAILEYS_PROMETHEUS_INCLUDE_SYSTEM ?? process.env.METRICS_INCLUDE_SYSTEM) !== 'false', // Flag for prom-client default Node.js metrics - collectDefaultMetrics: (process.env.BAILEYS_PROMETHEUS_COLLECT_DEFAULT ?? process.env.METRICS_COLLECT_DEFAULT) !== 'false', - collectIntervalMs: parseInt(process.env.BAILEYS_PROMETHEUS_COLLECT_INTERVAL_MS || process.env.METRICS_COLLECT_INTERVAL_MS || '10000', 10), + collectDefaultMetrics: + (process.env.BAILEYS_PROMETHEUS_COLLECT_DEFAULT ?? process.env.METRICS_COLLECT_DEFAULT) !== 'false', + collectIntervalMs: parseInt( + process.env.BAILEYS_PROMETHEUS_COLLECT_INTERVAL_MS || process.env.METRICS_COLLECT_INTERVAL_MS || '10000', + 10 + ) } } @@ -202,6 +208,7 @@ function stableLabelsToKey(labels: Labels): string { for (const key of sortedKeys) { sortedObj[key] = labels[key]! } + return JSON.stringify(sortedObj) } @@ -289,7 +296,7 @@ export class Counter implements BaseMetric { name: fullName, help, labelNames, - registers: [customRegistry], // Use custom registry, not global + registers: [customRegistry] // Use custom registry, not global }) } @@ -351,7 +358,7 @@ export class Counter implements BaseMetric { const metric = await this.promCounter.get() return metric.values.map(v => ({ labels: v.labels as Labels, - value: v.value, + value: v.value })) } @@ -364,7 +371,7 @@ export class Counter implements BaseMetric { */ labels(labels: Labels): { inc: (value?: number) => void } { return { - inc: (value?: number) => this.inc(labels, value), + inc: (value?: number) => this.inc(labels, value) } } } @@ -394,7 +401,7 @@ export class Gauge implements BaseMetric { name: fullName, help, labelNames, - registers: [customRegistry], // Use custom registry, not global + registers: [customRegistry] // Use custom registry, not global }) } @@ -501,7 +508,7 @@ export class Gauge implements BaseMetric { const metric = await this.promGauge.get() return metric.values.map(v => ({ labels: v.labels as Labels, - value: v.value, + value: v.value })) } @@ -520,7 +527,7 @@ export class Gauge implements BaseMetric { return { set: (value: number) => this.set(labels, value), inc: (value?: number) => this.inc(labels, value), - dec: (value?: number) => this.dec(labels, value), + dec: (value?: number) => this.dec(labels, value) } } @@ -566,7 +573,7 @@ export class Histogram implements BaseMetric { help, labelNames, buckets: this.buckets, - registers: [customRegistry], // Use custom registry, not global + registers: [customRegistry] // Use custom registry, not global }) } @@ -680,7 +687,7 @@ export class Histogram implements BaseMetric { labels: vLabels, buckets: new Map(), sum: 0, - count: 0, + count: 0 }) } @@ -718,7 +725,7 @@ export class Histogram implements BaseMetric { } { return { observe: (value: number) => this.observe(labels, value), - startTimer: () => this.startTimer(labels), + startTimer: () => this.startTimer(labels) } } } @@ -754,7 +761,7 @@ export class Summary implements BaseMetric { percentiles: this.percentiles, maxAgeSeconds: options.maxAgeSeconds ?? 600, // 10 min default ageBuckets: options.ageBuckets ?? 5, - registers: [customRegistry], // Use custom registry, not global + registers: [customRegistry] // Use custom registry, not global }) } @@ -852,7 +859,7 @@ export class Summary implements BaseMetric { labels: vLabels, values: [], sum: 0, - count: 0, + count: 0 }) } @@ -887,7 +894,7 @@ export class Summary implements BaseMetric { } { return { observe: (value: number) => this.observe(labels, value), - startTimer: () => this.startTimer(labels), + startTimer: () => this.startTimer(labels) } } } @@ -1014,7 +1021,7 @@ export class SystemMetricsCollector { // CPU tracking for delta calculation private lastCpuUsage: { user: number; system: number } | null = null - private lastCpuTime: number = 0 + private lastCpuTime = 0 // Process metrics public readonly processUptime: Gauge @@ -1039,9 +1046,7 @@ export class SystemMetricsCollector { this.processStartTime = Date.now() // Initialize process metrics - this.processUptime = registry.register( - new Gauge('process_uptime_seconds', 'Process uptime in seconds') - ) + this.processUptime = registry.register(new Gauge('process_uptime_seconds', 'Process uptime in seconds')) // FIX: Updated description - can exceed 100% on multi-core (100% = 1 core) this.processCpuUsage = registry.register( new Gauge('process_cpu_usage_percent', 'Process CPU usage (100% = 1 core fully used)', ['type']) @@ -1052,29 +1057,15 @@ export class SystemMetricsCollector { this.processMemoryExternal = registry.register( new Gauge('process_memory_external_bytes', 'External memory used by C++ objects') ) - this.processMemoryHeapTotal = registry.register( - new Gauge('process_memory_heap_total_bytes', 'Total heap memory') - ) - this.processMemoryHeapUsed = registry.register( - new Gauge('process_memory_heap_used_bytes', 'Used heap memory') - ) - this.processMemoryRss = registry.register( - new Gauge('process_memory_rss_bytes', 'Resident set size') - ) + this.processMemoryHeapTotal = registry.register(new Gauge('process_memory_heap_total_bytes', 'Total heap memory')) + this.processMemoryHeapUsed = registry.register(new Gauge('process_memory_heap_used_bytes', 'Used heap memory')) + this.processMemoryRss = registry.register(new Gauge('process_memory_rss_bytes', 'Resident set size')) // Initialize system metrics - this.systemCpuUsage = registry.register( - new Gauge('system_cpu_usage_percent', 'System CPU usage percentage') - ) - this.systemMemoryTotal = registry.register( - new Gauge('system_memory_total_bytes', 'Total system memory') - ) - this.systemMemoryFree = registry.register( - new Gauge('system_memory_free_bytes', 'Free system memory') - ) - this.systemLoadAverage = registry.register( - new Gauge('system_load_average', 'System load average', ['period']) - ) + this.systemCpuUsage = registry.register(new Gauge('system_cpu_usage_percent', 'System CPU usage percentage')) + this.systemMemoryTotal = registry.register(new Gauge('system_memory_total_bytes', 'Total system memory')) + this.systemMemoryFree = registry.register(new Gauge('system_memory_free_bytes', 'Free system memory')) + this.systemLoadAverage = registry.register(new Gauge('system_load_average', 'System load average', ['period'])) // Event loop lag - use different name to avoid conflict with collectDefaultMetrics // prom-client's collectDefaultMetrics creates nodejs_eventloop_lag_seconds @@ -1227,14 +1218,13 @@ export class MetricsServer { // Cache the promise so concurrent calls get the same one this.startPromise = new Promise((resolve, reject) => { - // Enable prom-client's default Node.js metrics collection (only once to prevent memory leak) if (this.config.collectDefaultMetrics && !defaultMetricsCollected) { defaultMetricsCollected = true promClient.collectDefaultMetrics({ prefix: this.config.prefix ? `${this.config.prefix}_` : '', labels: this.config.defaultLabels, - register: customRegistry, // Use custom registry, not global + register: customRegistry // Use custom registry, not global }) } @@ -1257,11 +1247,13 @@ export class MetricsServer { } catch { // Malformed URL - return 400 Bad Request res.writeHead(400, { 'Content-Type': 'application/json' }) - res.end(JSON.stringify({ - error: 'Bad Request', - message: 'Malformed URL', - timestamp: new Date().toISOString() - })) + res.end( + JSON.stringify({ + error: 'Bad Request', + message: 'Malformed URL', + timestamp: new Date().toISOString() + }) + ) return } @@ -1279,11 +1271,13 @@ export class MetricsServer { const errorMessage = error instanceof Error ? error.message : 'Unknown error' console.error(`[Prometheus] Error collecting metrics: ${errorMessage}`) res.writeHead(500, { 'Content-Type': 'application/json' }) - res.end(JSON.stringify({ - error: 'Failed to collect metrics', - message: errorMessage, - timestamp: new Date().toISOString() - })) + res.end( + JSON.stringify({ + error: 'Failed to collect metrics', + message: errorMessage, + timestamp: new Date().toISOString() + }) + ) } } else if (pathname === '/health' && req.method === 'GET') { res.writeHead(200, { 'Content-Type': 'application/json' }) @@ -1294,7 +1288,7 @@ export class MetricsServer { } }) - this.server.on('error', (error) => { + this.server.on('error', error => { this.startPromise = null // Reset so retry is possible this.server = null reject(error) @@ -1302,13 +1296,14 @@ export class MetricsServer { // FIX: Use configurable host instead of hardcoded 0.0.0.0 this.server.listen(this.config.port, this.config.host, () => { - const labelsInfo = Object.keys(this.config.defaultLabels).length > 0 - ? ` with labels: ${JSON.stringify(this.config.defaultLabels)}` - : '' - const securityNote = this.config.host === '0.0.0.0' - ? ' (WARNING: exposed on all interfaces)' - : '' - console.log(`[Prometheus] Metrics server listening on http://${this.config.host}:${this.config.port}${this.config.path}${labelsInfo}${securityNote}`) + const labelsInfo = + Object.keys(this.config.defaultLabels).length > 0 + ? ` with labels: ${JSON.stringify(this.config.defaultLabels)}` + : '' + const securityNote = this.config.host === '0.0.0.0' ? ' (WARNING: exposed on all interfaces)' : '' + console.log( + `[Prometheus] Metrics server listening on http://${this.config.host}:${this.config.port}${this.config.path}${labelsInfo}${securityNote}` + ) resolve() }) }) @@ -1321,7 +1316,7 @@ export class MetricsServer { * FIX: Clears startPromise to allow restart after stop */ stop(): Promise { - return new Promise((resolve) => { + return new Promise(resolve => { // FIX: Clear cached promise so start() can be called again this.startPromise = null @@ -1389,24 +1384,30 @@ export const metrics = { new Counter('reconnect_attempts_total', 'Total reconnection attempts', ['reason']) ), connectionLatency: baileysMetrics.register( - new Histogram('connection_latency_ms', 'Connection establishment latency in ms', [], [100, 250, 500, 1000, 2500, 5000, 10000]) - ), - activeConnections: baileysMetrics.register( - new Gauge('active_connections', 'Number of active WhatsApp connections') + new Histogram( + 'connection_latency_ms', + 'Connection establishment latency in ms', + [], + [100, 250, 500, 1000, 2500, 5000, 10000] + ) ), + activeConnections: baileysMetrics.register(new Gauge('active_connections', 'Number of active WhatsApp connections')), connectionErrors: baileysMetrics.register( new Counter('connection_errors_total', 'Total connection errors', ['error_type']) ), // ========== Message Metrics ========== - messagesSent: baileysMetrics.register( - new Counter('messages_sent_total', 'Total messages sent', ['type']) - ), + messagesSent: baileysMetrics.register(new Counter('messages_sent_total', 'Total messages sent', ['type'])), messagesReceived: baileysMetrics.register( new Counter('messages_received_total', 'Total messages received', ['type']) ), messageLatency: baileysMetrics.register( - new Histogram('message_latency_ms', 'Message send latency in ms', ['type'], [10, 50, 100, 250, 500, 1000, 2500, 5000]) + new Histogram( + 'message_latency_ms', + 'Message send latency in ms', + ['type'], + [10, 50, 100, 250, 500, 1000, 2500, 5000] + ) ), messageRetries: baileysMetrics.register( new Counter('message_retries_total', 'Total message retry attempts', ['type']) @@ -1430,7 +1431,12 @@ export const metrics = { new Counter('ctwa_messages_recovered_total', 'Total CTWA messages successfully recovered') ), ctwaRecoveryLatency: baileysMetrics.register( - new Histogram('ctwa_recovery_latency_ms', 'CTWA message recovery latency in ms', [], [500, 1000, 2000, 3000, 5000, 8000, 10000]) + new Histogram( + 'ctwa_recovery_latency_ms', + 'CTWA message recovery latency in ms', + [], + [500, 1000, 2000, 3000, 5000, 8000, 10000] + ) ), ctwaRecoveryFailures: baileysMetrics.register( new Counter('ctwa_recovery_failures_total', 'Total CTWA recovery failures', ['reason']) @@ -1438,7 +1444,11 @@ export const metrics = { // ========== Interactive Messages Metrics (EXPERIMENTAL) ========== interactiveMessagesSent: baileysMetrics.register( - new Counter('interactive_messages_sent_total', 'Total interactive messages sent (buttons/lists/templates/carousel)', ['type']) + new Counter( + 'interactive_messages_sent_total', + 'Total interactive messages sent (buttons/lists/templates/carousel)', + ['type'] + ) ), interactiveMessagesSuccess: baileysMetrics.register( new Counter('interactive_messages_success_total', 'Total interactive messages successfully sent', ['type']) @@ -1447,13 +1457,16 @@ export const metrics = { new Counter('interactive_messages_failures_total', 'Total interactive message send failures', ['type', 'reason']) ), interactiveMessagesLatency: baileysMetrics.register( - new Histogram('interactive_messages_latency_ms', 'Interactive message send latency in ms', ['type'], [100, 250, 500, 1000, 2500, 5000, 10000]) + new Histogram( + 'interactive_messages_latency_ms', + 'Interactive message send latency in ms', + ['type'], + [100, 250, 500, 1000, 2500, 5000, 10000] + ) ), // ========== Media Metrics ========== - mediaUploads: baileysMetrics.register( - new Counter('media_uploads_total', 'Total media uploads', ['type', 'status']) - ), + mediaUploads: baileysMetrics.register(new Counter('media_uploads_total', 'Total media uploads', ['type', 'status'])), mediaDownloads: baileysMetrics.register( new Counter('media_downloads_total', 'Total media downloads', ['type', 'status']) ), @@ -1461,16 +1474,17 @@ export const metrics = { new Histogram('media_size_bytes', 'Media size in bytes', ['type', 'direction'], DEFAULT_SIZE_BUCKETS) ), mediaLatency: baileysMetrics.register( - new Histogram('media_latency_ms', 'Media upload/download latency in ms', ['type', 'direction'], [100, 500, 1000, 2500, 5000, 10000, 30000]) + new Histogram( + 'media_latency_ms', + 'Media upload/download latency in ms', + ['type', 'direction'], + [100, 500, 1000, 2500, 5000, 10000, 30000] + ) ), // ========== Event Buffer Metrics ========== - bufferSize: baileysMetrics.register( - new Gauge('buffer_size', 'Current event buffer size', ['type']) - ), - bufferCapacity: baileysMetrics.register( - new Gauge('buffer_capacity', 'Maximum event buffer capacity', ['type']) - ), + bufferSize: baileysMetrics.register(new Gauge('buffer_size', 'Current event buffer size', ['type'])), + bufferCapacity: baileysMetrics.register(new Gauge('buffer_capacity', 'Maximum event buffer capacity', ['type'])), bufferUtilization: baileysMetrics.register( new Gauge('buffer_utilization_percent', 'Event buffer utilization percentage', ['type']) ), @@ -1487,7 +1501,12 @@ export const metrics = { new Counter('events_dropped_total', 'Total events dropped due to buffer full', ['event_type']) ), bufferFlushLatency: baileysMetrics.register( - new Histogram('buffer_flush_latency_ms', 'Buffer flush operation latency in ms', ['type'], [1, 5, 10, 25, 50, 100, 250]) + new Histogram( + 'buffer_flush_latency_ms', + 'Buffer flush operation latency in ms', + ['type'], + [1, 5, 10, 25, 50, 100, 250] + ) ), eventsProcessed: baileysMetrics.register( new Counter('events_processed_total', 'Total events processed from buffer', ['event_type']) @@ -1501,9 +1520,7 @@ export const metrics = { bufferCacheCleanup: baileysMetrics.register( new Counter('buffer_cache_cleanup_total', 'Total cache cleanup operations') ), - bufferCacheSize: baileysMetrics.register( - new Gauge('buffer_cache_size', 'Current buffer cache size') - ), + bufferCacheSize: baileysMetrics.register(new Gauge('buffer_cache_size', 'Current buffer cache size')), // ========== Adaptive Flush Metrics ========== adaptiveFlushInterval: baileysMetrics.register( @@ -1524,45 +1541,29 @@ export const metrics = { adaptiveHealthStatus: baileysMetrics.register( new Gauge('adaptive_health_status', 'Adaptive system health status (0=unhealthy, 1=healthy)') ), - adaptiveEventRate: baileysMetrics.register( - new Gauge('adaptive_event_rate', 'Current event rate per second') - ), + adaptiveEventRate: baileysMetrics.register(new Gauge('adaptive_event_rate', 'Current event rate per second')), // ========== Error Metrics ========== - errors: baileysMetrics.register( - new Counter('errors_total', 'Total errors', ['category', 'code']) - ), - errorRate: baileysMetrics.register( - new Gauge('error_rate', 'Current error rate per minute', ['category']) - ), + errors: baileysMetrics.register(new Counter('errors_total', 'Total errors', ['category', 'code'])), + errorRate: baileysMetrics.register(new Gauge('error_rate', 'Current error rate per minute', ['category'])), // ========== Retry Metrics ========== - retries: baileysMetrics.register( - new Counter('retries_total', 'Total retries', ['operation']) - ), - retryLatency: baileysMetrics.register( - new Histogram('retry_latency_ms', 'Retry latency in ms', ['operation']) - ), - retrySuccess: baileysMetrics.register( - new Counter('retry_success_total', 'Successful retries', ['operation']) - ), + retries: baileysMetrics.register(new Counter('retries_total', 'Total retries', ['operation'])), + retryLatency: baileysMetrics.register(new Histogram('retry_latency_ms', 'Retry latency in ms', ['operation'])), + retrySuccess: baileysMetrics.register(new Counter('retry_success_total', 'Successful retries', ['operation'])), retryExhausted: baileysMetrics.register( new Counter('retry_exhausted_total', 'Exhausted retry attempts', ['operation']) ), // ========== Socket Metrics ========== - socketEvents: baileysMetrics.register( - new Counter('socket_events_total', 'Total socket events', ['event']) - ), + socketEvents: baileysMetrics.register(new Counter('socket_events_total', 'Total socket events', ['event'])), socketLatency: baileysMetrics.register( new Histogram('socket_latency_ms', 'Socket operation latency in ms', ['operation']) ), socketBytesReceived: baileysMetrics.register( new Counter('socket_bytes_received_total', 'Total bytes received through socket') ), - socketBytesSent: baileysMetrics.register( - new Counter('socket_bytes_sent_total', 'Total bytes sent through socket') - ), + socketBytesSent: baileysMetrics.register(new Counter('socket_bytes_sent_total', 'Total bytes sent through socket')), socketReconnects: baileysMetrics.register( new Counter('socket_reconnects_total', 'Total socket reconnection attempts', ['reason']) ), @@ -1594,12 +1595,8 @@ export const metrics = { encryptionLatency: baileysMetrics.register( new Histogram('encryption_latency_ms', 'Encryption operation latency in ms', ['operation'], [1, 5, 10, 25, 50, 100]) ), - keyExchanges: baileysMetrics.register( - new Counter('key_exchanges_total', 'Total key exchange operations', ['type']) - ), - preKeyCount: baileysMetrics.register( - new Gauge('prekey_count', 'Current prekey count') - ), + keyExchanges: baileysMetrics.register(new Counter('key_exchanges_total', 'Total key exchange operations', ['type'])), + preKeyCount: baileysMetrics.register(new Gauge('prekey_count', 'Current prekey count')), // ========== Signal Identity Metrics ========== /** Total identity key changes detected (contact reinstalled WhatsApp) */ @@ -1623,7 +1620,12 @@ export const metrics = { ), /** Identity key operations latency */ signalIdentityKeyOperations: baileysMetrics.register( - new Histogram('signal_identity_key_operations_ms', 'Identity key operation latency in ms', ['operation'], [1, 5, 10, 25, 50, 100]) + new Histogram( + 'signal_identity_key_operations_ms', + 'Identity key operation latency in ms', + ['operation'], + [1, 5, 10, 25, 50, 100] + ) ), /** Current identity key cache size */ signalIdentityKeyCacheSize: baileysMetrics.register( @@ -1631,21 +1633,13 @@ export const metrics = { ), // ========== Cache Metrics ========== - cacheHits: baileysMetrics.register( - new Counter('cache_hits_total', 'Total cache hits', ['cache']) - ), - cacheMisses: baileysMetrics.register( - new Counter('cache_misses_total', 'Total cache misses', ['cache']) - ), - cacheSize: baileysMetrics.register( - new Gauge('cache_size', 'Current cache size', ['cache']) - ), + cacheHits: baileysMetrics.register(new Counter('cache_hits_total', 'Total cache hits', ['cache'])), + cacheMisses: baileysMetrics.register(new Counter('cache_misses_total', 'Total cache misses', ['cache'])), + cacheSize: baileysMetrics.register(new Gauge('cache_size', 'Current cache size', ['cache'])), cacheEvictions: baileysMetrics.register( new Counter('cache_evictions_total', 'Total cache evictions', ['cache', 'reason']) ), - cacheHitRate: baileysMetrics.register( - new Gauge('cache_hit_rate', 'Cache hit rate (0-1)', ['cache']) - ), + cacheHitRate: baileysMetrics.register(new Gauge('cache_hit_rate', 'Cache hit rate (0-1)', ['cache'])), // ========== Query Metrics ========== queryLatency: baileysMetrics.register( @@ -1654,17 +1648,13 @@ export const metrics = { queryCount: baileysMetrics.register( new Counter('query_count_total', 'Total queries executed', ['query_type', 'status']) ), - queryTimeouts: baileysMetrics.register( - new Counter('query_timeouts_total', 'Total query timeouts', ['query_type']) - ), + queryTimeouts: baileysMetrics.register(new Counter('query_timeouts_total', 'Total query timeouts', ['query_type'])), // ========== Presence Metrics ========== presenceUpdates: baileysMetrics.register( new Counter('presence_updates_total', 'Total presence updates received', ['type']) ), - presenceSubscriptions: baileysMetrics.register( - new Gauge('presence_subscriptions', 'Current presence subscriptions') - ), + presenceSubscriptions: baileysMetrics.register(new Gauge('presence_subscriptions', 'Current presence subscriptions')), // ========== Group Metrics ========== groupOperations: baileysMetrics.register( @@ -1682,8 +1672,13 @@ export const metrics = { new Counter('history_sync_messages_total', 'Total messages synced from history') ), historySyncDuration: baileysMetrics.register( - new Histogram('history_sync_duration_ms', 'History sync duration in ms', ['type'], [1000, 5000, 10000, 30000, 60000]) - ), + new Histogram( + 'history_sync_duration_ms', + 'History sync duration in ms', + ['type'], + [1000, 5000, 10000, 30000, 60000] + ) + ) } // ============================================ @@ -1764,7 +1759,10 @@ export class PrometheusMetricsManager { * Helper to create HTTP metrics endpoint handler */ export function createMetricsHandler(registry: MetricsRegistry = baileysMetrics) { - return async (_req: unknown, res: { setHeader: (name: string, value: string) => void; end: (body: string) => void }) => { + return async ( + _req: unknown, + res: { setHeader: (name: string, value: string) => void; end: (body: string) => void } + ) => { const metricsOutput = await registry.getMetricsOutput() res.setHeader('Content-Type', registry.contentType()) res.end(metricsOutput) @@ -1785,17 +1783,14 @@ export function createExpressMetricsMiddleware(registry: MetricsRegistry = baile /** * Track operation duration using histogram */ -export function trackDuration( - histogram: Histogram, - labels: Labels, - operation: () => T -): T { +export function trackDuration(histogram: Histogram, labels: Labels, operation: () => T): T { const endTimer = histogram.startTimer(labels) try { const result = operation() if (result instanceof Promise) { return result.finally(() => endTimer()) as T } + endTimer() return result } catch (error) { @@ -1826,14 +1821,10 @@ export async function trackDurationAsync( /** * Increment counter with automatic error tracking */ -export function trackOperation( - successCounter: Counter, - errorCounter: Counter, - labels: Labels -) { +export function trackOperation(successCounter: Counter, errorCounter: Counter, labels: Labels) { return { success: () => successCounter.inc(labels), - failure: (errorCode?: string) => errorCounter.inc({ ...labels, code: errorCode || 'unknown' }), + failure: (errorCode?: string) => errorCounter.inc({ ...labels, code: errorCode || 'unknown' }) } } @@ -1860,24 +1851,13 @@ function getEventBufferMetrics() { [], [1, 5, 10, 25, 50, 100, 250, 500, 1000] ), - bufferCurrentSize: new Gauge( - 'buffer_current_size', - 'Current number of events in buffer' - ), - bufferPeakSize: new Gauge( - 'buffer_peak_size', - 'Peak buffer size reached' - ), - bufferHistoryCacheSize: new Gauge( - 'buffer_history_cache_size', - 'Current size of history cache' - ), - bufferLruCleanups: new Counter( - 'buffer_lru_cleanups_total', - 'Total number of LRU cache cleanups performed' - ) + bufferCurrentSize: new Gauge('buffer_current_size', 'Current number of events in buffer'), + bufferPeakSize: new Gauge('buffer_peak_size', 'Peak buffer size reached'), + bufferHistoryCacheSize: new Gauge('buffer_history_cache_size', 'Current size of history cache'), + bufferLruCleanups: new Counter('buffer_lru_cleanups_total', 'Total number of LRU cache cleanups performed') } } + return eventBufferMetrics } @@ -1885,7 +1865,7 @@ function getEventBufferMetrics() { * Record an event being buffered * Used by event-buffer.ts for metrics integration */ -export function recordEventBuffered(eventType: string, count: number = 1): void { +export function recordEventBuffered(eventType: string, count = 1): void { try { // Use the main metrics object which has eventsBuffered with label ['event_type'] metrics.eventsBuffered?.inc({ event_type: eventType }, count) @@ -2065,7 +2045,7 @@ export function recordConnectionAttempt(status: 'success' | 'failure'): void { /** * Record a message sent */ -export function recordMessageSent(type: string = 'text'): void { +export function recordMessageSent(type = 'text'): void { try { metrics.messagesSent?.inc({ type }) } catch { @@ -2076,7 +2056,7 @@ export function recordMessageSent(type: string = 'text'): void { /** * Record a message received */ -export function recordMessageReceived(type: string = 'text'): void { +export function recordMessageReceived(type = 'text'): void { try { metrics.messagesReceived?.inc({ type }) } catch { @@ -2087,7 +2067,7 @@ export function recordMessageReceived(type: string = 'text'): void { /** * Record a message retry attempt */ -export function recordMessageRetry(type: string = 'text'): void { +export function recordMessageRetry(type = 'text'): void { try { metrics.messageRetries?.inc({ type }) } catch { @@ -2098,7 +2078,7 @@ export function recordMessageRetry(type: string = 'text'): void { /** * Record a message failure */ -export function recordMessageFailure(type: string = 'text', reason: string = 'unknown'): void { +export function recordMessageFailure(type = 'text', reason = 'unknown'): void { try { metrics.messageFailures?.inc({ type, reason }) } catch { @@ -2109,7 +2089,7 @@ export function recordMessageFailure(type: string = 'text', reason: string = 'un /** * Update messages queued gauge */ -export function setMessagesQueued(count: number, priority: string = 'normal'): void { +export function setMessagesQueued(count: number, priority = 'normal'): void { try { metrics.messagesQueued?.set({ priority }, count) } catch { @@ -2120,7 +2100,7 @@ export function setMessagesQueued(count: number, priority: string = 'normal'): v /** * Record history sync messages */ -export function recordHistorySyncMessages(count: number = 1): void { +export function recordHistorySyncMessages(count = 1): void { try { metrics.historySyncMessages?.inc({}, count) } catch { @@ -2144,6 +2124,7 @@ export function getMetricsManager(config?: Partial): PrometheusMe if (!globalMetricsManager) { globalMetricsManager = new PrometheusMetricsManager(config) } + return globalMetricsManager } @@ -2231,7 +2212,7 @@ if (metricsConfig.enabled) { .then(() => { console.log('[Prometheus] Auto-initialized metrics server successfully') }) - .catch((error) => { + .catch(error => { console.error('[Prometheus] Failed to auto-initialize metrics server:', error) }) }) diff --git a/src/Utils/retry-utils.ts b/src/Utils/retry-utils.ts index dfd209b9..b7a402d4 100644 --- a/src/Utils/retry-utils.ts +++ b/src/Utils/retry-utils.ts @@ -14,8 +14,8 @@ */ import { EventEmitter } from 'events' -import { metrics } from './prometheus-metrics.js' import type { CircuitBreaker } from './circuit-breaker.js' +import { metrics } from './prometheus-metrics.js' /** * Retry configuration with custom progressive backoff @@ -190,6 +190,7 @@ function fibonacciNumber(n: number): number { a = b b = c } + return b } @@ -205,6 +206,7 @@ async function sleep(ms: number, signal?: AbortSignal): Promise { if (signal && abortHandler) { signal.removeEventListener('abort', abortHandler) } + resolve() }, ms) @@ -228,11 +230,7 @@ async function sleep(ms: number, signal?: AbortSignal): Promise { /** * Execute operation with timeout */ -async function executeWithTimeout( - operation: () => Promise, - timeout: number, - signal?: AbortSignal -): Promise { +async function executeWithTimeout(operation: () => Promise, timeout: number, signal?: AbortSignal): Promise { return new Promise((resolve, reject) => { let completed = false @@ -250,14 +248,14 @@ async function executeWithTimeout( } operation() - .then((result) => { + .then(result => { if (!completed) { completed = true clearTimeout(timer) resolve(result) } }) - .catch((error) => { + .catch(error => { if (!completed) { completed = true clearTimeout(timer) @@ -289,14 +287,14 @@ export async function retry( onRetry: options.onRetry ?? (() => {}), onSuccess: options.onSuccess ?? (() => {}), onFailure: options.onFailure ?? (() => {}), - abortSignal: options.abortSignal, + abortSignal: options.abortSignal } const context: RetryContext = { attempt: 0, maxAttempts: config.maxAttempts, startTime: Date.now(), - aborted: false, + aborted: false } let lastError: Error | undefined @@ -325,11 +323,7 @@ export async function retry( let result: T if (config.timeout) { - result = await executeWithTimeout( - () => Promise.resolve(operation(context)), - config.timeout, - config.abortSignal - ) + result = await executeWithTimeout(() => Promise.resolve(operation(context)), config.timeout, config.abortSignal) } else { result = await operation(context) } @@ -398,21 +392,18 @@ export async function retryWithResult( let lastAttemptStart = startTime try { - const result = await retry( - (context) => { - attempts = context.attempt - lastAttemptStart = Date.now() - return operation(context) - }, - options - ) + const result = await retry(context => { + attempts = context.attempt + lastAttemptStart = Date.now() + return operation(context) + }, options) return { success: true, result, attempts, totalDuration: Date.now() - startTime, - lastAttemptDuration: Date.now() - lastAttemptStart, + lastAttemptDuration: Date.now() - lastAttemptStart } } catch (error) { return { @@ -420,7 +411,7 @@ export async function retryWithResult( error: error as Error, attempts, totalDuration: Date.now() - startTime, - lastAttemptDuration: Date.now() - lastAttemptStart, + lastAttemptDuration: Date.now() - lastAttemptStart } } } @@ -429,10 +420,7 @@ export async function retryWithResult( * Factory to create configured retry function */ export function createRetrier(defaultOptions: RetryOptions = {}) { - return ( - operation: (context: RetryContext) => T | Promise, - options?: RetryOptions - ): Promise => { + return (operation: (context: RetryContext) => T | Promise, options?: RetryOptions): Promise => { return retry(operation, { ...defaultOptions, ...options }) } } @@ -450,10 +438,10 @@ export function withRetry(options: RetryOptions = {}) { if (!originalMethod) return descriptor descriptor.value = async function (...args: unknown[]): Promise { - return retry( - () => originalMethod.apply(this, args), - { ...options, operationName: options.operationName || propertyKey } - ) + return retry(() => originalMethod.apply(this, args), { + ...options, + operationName: options.operationName || propertyKey + }) } return descriptor @@ -463,7 +451,7 @@ export function withRetry(options: RetryOptions = {}) { /** * Wrapper for function with retry */ -export function retryable unknown>( +export function retryable unknown>( fn: T, options: RetryOptions = {} ): (...args: Parameters) => Promise> { @@ -498,10 +486,10 @@ export class RetryManager extends EventEmitter { const abortController = new AbortController() const mergedOptions = { ...this.defaultOptions, ...options, abortSignal: abortController.signal } - const retryPromise = retry((context) => { + const retryPromise = retry(context => { this.activeRetries.set(id, { cancel: () => abortController.abort(), - context, + context }) this.emit('attempt', { id, attempt: context.attempt }) return operation(context) @@ -530,6 +518,7 @@ export class RetryManager extends EventEmitter { this.emit('cancelled', { id }) return true } + return false } @@ -541,6 +530,7 @@ export class RetryManager extends EventEmitter { active.cancel() this.emit('cancelled', { id }) } + this.activeRetries.clear() } @@ -579,22 +569,22 @@ export const retryPredicates = { /** Retry only on network errors */ onNetworkError: (error: Error) => { const networkErrors = ['ECONNREFUSED', 'ECONNRESET', 'ETIMEDOUT', 'ENOTFOUND', 'EAI_AGAIN'] - return networkErrors.some((code) => error.message.includes(code) || (error as NodeJS.ErrnoException).code === code) + return networkErrors.some(code => error.message.includes(code) || (error as NodeJS.ErrnoException).code === code) }, /** Retry only on specific errors */ onErrorCodes: (codes: string[]) => - (error: Error): boolean => { - return codes.some((code) => error.message.includes(code) || (error as NodeJS.ErrnoException).code === code) - }, + (error: Error): boolean => { + return codes.some(code => error.message.includes(code) || (error as NodeJS.ErrnoException).code === code) + }, /** Retry except on specific errors */ exceptErrorCodes: (codes: string[]) => - (error: Error): boolean => { - return !codes.some((code) => error.message.includes(code) || (error as NodeJS.ErrnoException).code === code) - }, + (error: Error): boolean => { + return !codes.some(code => error.message.includes(code) || (error as NodeJS.ErrnoException).code === code) + }, /** Retry on HTTP 5xx errors or timeout */ onServerError: (error: Error) => { @@ -611,16 +601,16 @@ export const retryPredicates = { /** Combine multiple predicates with OR */ or: (...predicates: Array<(error: Error, attempt: number) => boolean>) => - (error: Error, attempt: number): boolean => { - return predicates.some((p) => p(error, attempt)) - }, + (error: Error, attempt: number): boolean => { + return predicates.some(p => p(error, attempt)) + }, /** Combine multiple predicates with AND */ and: (...predicates: Array<(error: Error, attempt: number) => boolean>) => - (error: Error, attempt: number): boolean => { - return predicates.every((p) => p(error, attempt)) - }, + (error: Error, attempt: number): boolean => { + return predicates.every(p => p(error, attempt)) + } } /** @@ -633,7 +623,7 @@ export const retryConfigs = { baseDelay: 100, maxDelay: 5000, backoffStrategy: 'exponential' as const, - jitter: 0.2, + jitter: 0.2 }, /** Conservative retry (few attempts, long delays) */ @@ -642,7 +632,7 @@ export const retryConfigs = { baseDelay: 2000, maxDelay: 60000, backoffStrategy: 'exponential' as const, - jitter: 0.1, + jitter: 0.1 }, /** Fast retry (for short operations) */ @@ -651,7 +641,7 @@ export const retryConfigs = { baseDelay: 50, maxDelay: 1000, backoffStrategy: 'linear' as const, - jitter: 0.05, + jitter: 0.05 }, /** Retry for network operations */ @@ -661,7 +651,7 @@ export const retryConfigs = { maxDelay: 30000, backoffStrategy: 'exponential' as const, jitter: 0.1, - shouldRetry: retryPredicates.onNetworkError, + shouldRetry: retryPredicates.onNetworkError }, /** @@ -679,8 +669,8 @@ export const retryConfigs = { baseDelay: 1000, // = RETRY_BACKOFF_DELAYS[0] maxDelay: 20000, // = RETRY_BACKOFF_DELAYS[4] backoffStrategy: 'stepped' as const, - jitter: 0.15, // = RETRY_JITTER_FACTOR - }, + jitter: 0.15 // = RETRY_JITTER_FACTOR + } } /** diff --git a/src/Utils/sticker-pack.ts b/src/Utils/sticker-pack.ts index 8360228b..5ba002d9 100644 --- a/src/Utils/sticker-pack.ts +++ b/src/Utils/sticker-pack.ts @@ -1,13 +1,13 @@ +import { Boom } from '@hapi/boom' import { createHash } from 'crypto' import { zipSync } from 'fflate' import { promises as fs } from 'fs' -import { Boom } from '@hapi/boom' import { proto } from '../../WAProto/index.js' import type { MediaType } from '../Defaults/index.js' import type { Sticker, StickerPack, WAMediaUpload, WAMediaUploadFunction } from '../Types/Message.js' -import type { ILogger } from './logger.js' import { generateMessageIDV2 } from './generics.js' -import { getImageProcessingLibrary, encryptedStream } from './messages-media.js' +import type { ILogger } from './logger.js' +import { encryptedStream, getImageProcessingLibrary } from './messages-media.js' /** * Verifica se um buffer é um arquivo WebP válido @@ -86,7 +86,7 @@ export const isAnimatedWebP = (buffer: Buffer): boolean => { if (chunkFourCC === 'VP8X' && offset + 8 < buffer.length) { const flags = buffer[offset + 8] // Bit 1 (0x02) = animation flag - if (flags && (flags & 0x02)) return true + if (flags && flags & 0x02) return true } // Animation chunks @@ -156,7 +156,7 @@ const generateSha256Hash = (buffer: Buffer): string => { .digest('base64') .replace(/\+/g, '-') // + becomes - .replace(/\//g, '_') // / becomes _ (CRITICAL: different from + mapping!) - .replace(/=/g, '') // Remove padding + .replace(/=/g, '') // Remove padding } /** @@ -190,6 +190,7 @@ const mediaToBuffer = async ( statusCode: 413 }) } + return media } else if (typeof media === 'object' && 'url' in media) { const url = media.url.toString() @@ -201,6 +202,7 @@ const mediaToBuffer = async ( if (!base64Data) { throw new Boom(`Invalid data URL for ${context}: missing base64 data`, { statusCode: 400 }) } + const buffer = Buffer.from(base64Data, 'base64') // SECURITY: Validate buffer size @@ -485,7 +487,11 @@ export const prepareStickerPackMessage = async ( if (compressed70.length <= MAX_STICKER_SIZE) { webpBuffer = compressed70 logger?.info( - { index: i, originalKB: (webpBuffer.length / 1024).toFixed(2), compressedKB: (compressed70.length / 1024).toFixed(2) }, + { + index: i, + originalKB: (webpBuffer.length / 1024).toFixed(2), + compressedKB: (compressed70.length / 1024).toFixed(2) + }, `Sticker ${i + 1} compressed successfully (quality 70)` ) } else { @@ -495,7 +501,11 @@ export const prepareStickerPackMessage = async ( if (compressed50.length <= MAX_STICKER_SIZE) { webpBuffer = compressed50 logger?.info( - { index: i, originalKB: (webpBuffer.length / 1024).toFixed(2), compressedKB: (compressed50.length / 1024).toFixed(2) }, + { + index: i, + originalKB: (webpBuffer.length / 1024).toFixed(2), + compressedKB: (compressed50.length / 1024).toFixed(2) + }, `Sticker ${i + 1} compressed successfully (quality 50)` ) } else { @@ -587,10 +597,7 @@ export const prepareStickerPackMessage = async ( } } - logger?.debug( - { fileName, mergedEmojis, duplicateCount }, - 'Duplicate sticker detected - merged metadata' - ) + logger?.debug({ fileName, mergedEmojis, duplicateCount }, 'Duplicate sticker detected - merged metadata') } else { // New sticker - add to ZIP and create metadata stickerData[fileName] = [new Uint8Array(webpBuffer), { level: 0 as 0 }] @@ -691,10 +698,9 @@ export const prepareStickerPackMessage = async ( const lib = await getImageProcessingLibrary() if (!lib?.sharp) { - throw new Boom( - 'Sharp library is required for thumbnail generation. Install with: yarn add sharp', - { statusCode: 400 } - ) + throw new Boom('Sharp library is required for thumbnail generation. Install with: yarn add sharp', { + statusCode: 400 + }) } thumbnailBuffer = await lib.sharp diff --git a/src/Utils/structured-logger.ts b/src/Utils/structured-logger.ts index f542903b..80497b7b 100644 --- a/src/Utils/structured-logger.ts +++ b/src/Utils/structured-logger.ts @@ -38,7 +38,7 @@ export const LOG_LEVEL_VALUES: Record = { warn: 40, error: 50, fatal: 60, - silent: 100, + silent: 100 } /** @@ -111,7 +111,7 @@ export function loadLoggerConfig(): Partial { : undefined, enableAsyncQueue: process.env.BAILEYS_LOG_ASYNC === 'true', enableMetrics: process.env.BAILEYS_LOG_METRICS === 'true', - includeStackTrace: process.env.BAILEYS_LOG_STACK_TRACE !== 'false', + includeStackTrace: process.env.BAILEYS_LOG_STACK_TRACE !== 'false' } } @@ -195,7 +195,7 @@ const DEFAULT_REDACT_FIELDS = [ 'auth', 'credentials', 'privateKey', - 'private_key', + 'private_key' ] // ============================================================================ @@ -224,6 +224,7 @@ class RateLimiter { this.tokens-- return true } + return false } @@ -249,8 +250,8 @@ class RateLimiter { * Circuit breaker for external hook protection */ class CircuitBreaker { - private failures: number = 0 - private lastFailure: number = 0 + private failures = 0 + private lastFailure = 0 private state: 'closed' | 'open' | 'half-open' = 'closed' constructor( @@ -295,6 +296,7 @@ class CircuitBreaker { if (this.state === 'open' && Date.now() - this.lastFailure > this.resetTimeoutMs) { this.state = 'half-open' } + return this.state } @@ -312,8 +314,8 @@ class CircuitBreaker { */ class AsyncLogQueue { private queue: Array<() => void> = [] - private processing: boolean = false - private destroyed: boolean = false + private processing = false + private destroyed = false enqueue(task: () => void): void { if (this.destroyed) return @@ -334,11 +336,13 @@ class AsyncLogQueue { // Silently ignore errors } } + // Yield to event loop periodically if (this.queue.length > 0 && this.queue.length % 10 === 0) { await new Promise(resolve => setImmediate(resolve)) } } + this.processing = false } @@ -388,7 +392,7 @@ export class StructuredLogger implements ILogger { private config: ResolvedLoggerConfig private metrics: LoggerMetrics private childContext: Record = {} - private destroyed: boolean = false + private destroyed = false private createdAt: number // Buffer for batch writes @@ -405,8 +409,8 @@ export class StructuredLogger implements ILogger { private asyncQueue: AsyncLogQueue | null = null // Performance tracking - private totalProcessingTime: number = 0 - private processedLogs: number = 0 + private totalProcessingTime = 0 + private processedLogs = 0 // Metrics module (lazy loaded) // NOTE: Currently no metrics are recorded to this module - it's loaded but unused @@ -433,7 +437,7 @@ export class StructuredLogger implements ILogger { enableAsyncQueue: config.enableAsyncQueue ?? envConfig.enableAsyncQueue ?? false, circuitBreakerThreshold: config.circuitBreakerThreshold ?? 5, circuitBreakerResetMs: config.circuitBreakerResetMs ?? 30000, - enableMetrics: config.enableMetrics ?? envConfig.enableMetrics ?? false, + enableMetrics: config.enableMetrics ?? envConfig.enableMetrics ?? false } this.metrics = { @@ -445,14 +449,14 @@ export class StructuredLogger implements ILogger { warn: 0, error: 0, fatal: 0, - silent: 0, + silent: 0 }, errorsCount: 0, droppedLogs: 0, bufferFlushes: 0, hookFailures: 0, circuitBreakerTrips: 0, - avgProcessingTimeMs: 0, + avgProcessingTimeMs: 0 } // Initialize rate limiter @@ -462,10 +466,7 @@ export class StructuredLogger implements ILogger { // Initialize circuit breaker for external hook if (this.config.externalHook) { - this.circuitBreaker = new CircuitBreaker( - this.config.circuitBreakerThreshold, - this.config.circuitBreakerResetMs - ) + this.circuitBreaker = new CircuitBreaker(this.config.circuitBreakerThreshold, this.config.circuitBreakerResetMs) } // Initialize async queue @@ -482,12 +483,14 @@ export class StructuredLogger implements ILogger { // Load metrics module if enabled (currently unused but loaded for future use) if (this.config.enableMetrics) { - import('./prometheus-metrics').then(m => { - this.metricsModule = m - this.debug('📊 Prometheus metrics loaded for logger') - }).catch(() => { - // Metrics module not available - silent fail - }) + import('./prometheus-metrics') + .then(m => { + this.metricsModule = m + this.debug('📊 Prometheus metrics loaded for logger') + }) + .catch(() => { + // Metrics module not available - silent fail + }) } } @@ -520,7 +523,7 @@ export class StructuredLogger implements ILogger { child(obj: Record): StructuredLogger { const childLogger = new StructuredLogger({ ...this.config, - context: { ...this.config.context, ...this.childContext, ...obj }, + context: { ...this.config.context, ...this.childContext, ...obj } }) childLogger.childContext = { ...this.childContext, ...obj } return childLogger @@ -569,14 +572,16 @@ export class StructuredLogger implements ILogger { // External hook with circuit breaker const externalHook = this.config.externalHook if (externalHook && this.circuitBreaker) { - this.circuitBreaker.execute(async () => { - await Promise.resolve(externalHook(entry)) - }).catch(() => { - this.metrics.hookFailures++ - if (this.circuitBreaker?.getState() === 'open') { - this.metrics.circuitBreakerTrips++ - } - }) + this.circuitBreaker + .execute(async () => { + await Promise.resolve(externalHook(entry)) + }) + .catch(() => { + this.metrics.hookFailures++ + if (this.circuitBreaker?.getState() === 'open') { + this.metrics.circuitBreakerTrips++ + } + }) } // Track processing time @@ -623,10 +628,11 @@ export class StructuredLogger implements ILogger { if (this.config.includeStackTrace && obj.stack) { stack = obj.stack } + data = { errorName: obj.name, errorMessage: obj.message, - ...(obj as unknown as Record), + ...(obj as unknown as Record) } } else if (typeof obj === 'object' && obj !== null) { data = this.sanitize(obj as Record) @@ -651,7 +657,7 @@ export class StructuredLogger implements ILogger { data, stack, correlationId, - durationMs, + durationMs } } @@ -664,7 +670,7 @@ export class StructuredLogger implements ILogger { for (const [key, value] of Object.entries(obj)) { const lowerKey = key.toLowerCase() - if (this.config.redactFields.some((field) => lowerKey.includes(field.toLowerCase()))) { + if (this.config.redactFields.some(field => lowerKey.includes(field.toLowerCase()))) { sanitized[key] = '[REDACTED]' } else if (typeof value === 'object' && value !== null && !Array.isArray(value)) { sanitized[key] = this.sanitize(value as Record) @@ -723,7 +729,7 @@ export class StructuredLogger implements ILogger { entry.name ? `[${entry.name}]` : '', entry.correlationId ? `[${entry.correlationId}]` : '', entry.message, - entry.durationMs !== undefined ? `(${entry.durationMs}ms)` : '', + entry.durationMs !== undefined ? `(${entry.durationMs}ms)` : '' ] let text = parts.filter(Boolean).join(' ') @@ -782,11 +788,7 @@ export class StructuredLogger implements ILogger { /** * Log operation with duration tracking */ - logOperation( - operationName: string, - operation: () => T | Promise, - level: LogLevel = 'info' - ): T | Promise { + logOperation(operationName: string, operation: () => T | Promise, level: LogLevel = 'info'): T | Promise { const startTime = Date.now() const contextLogger = this.child({ operation: operationName }) @@ -808,7 +810,7 @@ export class StructuredLogger implements ILogger { const result = operation() if (result instanceof Promise) { - return result.then(handleResult).catch(handleError) as Promise + return result.then(handleResult).catch(handleError) } return handleResult(result) @@ -835,7 +837,7 @@ export class StructuredLogger implements ILogger { circuitBreakerState: this.circuitBreaker?.getState() ?? 'closed', queueSize: this.asyncQueue?.getSize() ?? 0, createdAt: this.createdAt, - uptimeMs: Date.now() - this.createdAt, + uptimeMs: Date.now() - this.createdAt } } @@ -852,14 +854,14 @@ export class StructuredLogger implements ILogger { warn: 0, error: 0, fatal: 0, - silent: 0, + silent: 0 }, errorsCount: 0, droppedLogs: 0, bufferFlushes: 0, hookFailures: 0, circuitBreakerTrips: 0, - avgProcessingTimeMs: 0, + avgProcessingTimeMs: 0 } this.totalProcessingTime = 0 this.processedLogs = 0 @@ -902,7 +904,7 @@ export class StructuredLogger implements ILogger { export function createStructuredLogger(config: Partial = {}): StructuredLogger { return new StructuredLogger({ level: config.level || 'info', - ...config, + ...config }) } @@ -916,9 +918,10 @@ export function getDefaultLogger(): StructuredLogger { defaultLogger = createStructuredLogger({ level: 'info', name: 'baileys', - jsonFormat: process.env.NODE_ENV === 'production', + jsonFormat: process.env.NODE_ENV === 'production' }) } + return defaultLogger } @@ -933,7 +936,7 @@ export function createTimer(): { elapsed: () => number; elapsedMs: () => string const start = process.hrtime.bigint() return { elapsed: () => Number(process.hrtime.bigint() - start) / 1_000_000, - elapsedMs: () => `${(Number(process.hrtime.bigint() - start) / 1_000_000).toFixed(2)}ms`, + elapsedMs: () => `${(Number(process.hrtime.bigint() - start) / 1_000_000).toFixed(2)}ms` } } diff --git a/src/Utils/trace-context.ts b/src/Utils/trace-context.ts index 9d87d501..40feb06d 100644 --- a/src/Utils/trace-context.ts +++ b/src/Utils/trace-context.ts @@ -12,8 +12,8 @@ * @module Utils/trace-context */ -import { randomBytes } from 'crypto' import { AsyncLocalStorage } from 'async_hooks' +import { randomBytes } from 'crypto' /** * Trace identifiers @@ -169,12 +169,12 @@ export function createTraceContext(options: CreateContextOptions = {}): TraceCon traceId, spanId, parentSpanId: options.parentSpanId, - correlationId, + correlationId }, baggage: options.baggage || {}, spanStack: [], createdAt: Date.now(), - metadata: options.metadata || {}, + metadata: options.metadata || {} } } @@ -193,6 +193,7 @@ export function getOrCreateContext(): TraceContext { if (existing) { return existing } + return createTraceContext() } @@ -214,10 +215,7 @@ export function runWithNewContext(options: CreateContextOptions, fn: () => T) /** * Execute async function with trace context */ -export async function runWithContextAsync( - context: TraceContext, - fn: () => Promise -): Promise { +export async function runWithContextAsync(context: TraceContext, fn: () => Promise): Promise { return traceStorage.run(context, fn) } @@ -234,13 +232,13 @@ export function createSpan(options: CreateSpanOptions): Span { traceId: context?.traceIds.traceId || generateTraceId(), spanId: generateSpanId(), parentSpanId: options.asChild && parentSpan ? parentSpan.traceIds.spanId : undefined, - correlationId: context?.traceIds.correlationId || generateCorrelationId(), + correlationId: context?.traceIds.correlationId || generateCorrelationId() }, startTime: Date.now(), status: 'unset', attributes: options.attributes || {}, events: [], - ended: false, + ended: false } return span @@ -257,6 +255,7 @@ export function startSpan(options: CreateSpanOptions): Span { if (context.currentSpan) { context.spanStack.push(context.currentSpan) } + context.currentSpan = span return span @@ -277,7 +276,7 @@ export function endSpan(span: Span, status?: SpanStatus): void { // Pop span from stack in context const context = getCurrentContext() - if (context && context.currentSpan === span) { + if (context?.currentSpan === span) { context.currentSpan = context.spanStack.pop() } } @@ -293,7 +292,7 @@ export function addSpanEvent(span: Span, name: string, attributes?: Record { + .then(value => { endSpan(span, 'ok') return value }) - .catch((error) => { + .catch(error => { setSpanError(span, error as Error) endSpan(span, 'error') throw error @@ -381,10 +380,7 @@ export function traced(name?: string) { /** * Wrapper for tracing a function */ -export function traceFunction unknown>( - name: string, - fn: T -): T { +export function traceFunction unknown>(name: string, fn: T): T { return function (this: unknown, ...args: Parameters): ReturnType { const span = startSpan({ name }) @@ -393,11 +389,11 @@ export function traceFunction unknown>( if (result instanceof Promise) { return result - .then((value) => { + .then(value => { endSpan(span, 'ok') return value }) - .catch((error) => { + .catch(error => { setSpanError(span, error as Error) endSpan(span, 'error') throw error @@ -438,11 +434,7 @@ export async function withSpan( /** * Execute sync operation with automatic span */ -export function withSpanSync( - name: string, - operation: (span: Span) => T, - attributes?: Record -): T { +export function withSpanSync(name: string, operation: (span: Span) => T, attributes?: Record): T { const span = startSpan({ name, attributes }) try { @@ -504,7 +496,7 @@ export const TRACE_HEADERS = { SPAN_ID: 'x-span-id', PARENT_SPAN_ID: 'x-parent-span-id', CORRELATION_ID: 'x-correlation-id', - BAGGAGE: 'baggage', + BAGGAGE: 'baggage' } as const /** @@ -578,7 +570,7 @@ export function exportContext(context: TraceContext): string { return JSON.stringify({ traceIds: context.traceIds, baggage: context.baggage, - metadata: context.metadata, + metadata: context.metadata }) } @@ -593,7 +585,7 @@ export function importContext(serialized: string): CreateContextOptions { parentSpanId: data.traceIds?.spanId, correlationId: data.traceIds?.correlationId, baggage: data.baggage, - metadata: data.metadata, + metadata: data.metadata } } catch { return {} @@ -638,8 +630,9 @@ export function createPrecisionTimer(): PrecisionTimer { finalDuration = Number(process.hrtime.bigint() - start) / 1_000_000 stopped = true } + return finalDuration - }, + } } } @@ -656,5 +649,5 @@ export default { withSpanSync, injectTraceHeaders, extractTraceHeaders, - createPrecisionTimer, + createPrecisionTimer } diff --git a/src/Utils/unified-session.ts b/src/Utils/unified-session.ts index 55d82abb..0ac9e469 100644 --- a/src/Utils/unified-session.ts +++ b/src/Utils/unified-session.ts @@ -14,14 +14,10 @@ * @see https://github.com/WhiskeySockets/Baileys/pull/2294 */ -import { metrics } from './prometheus-metrics.js' -import type { ILogger } from './logger.js' import type { BinaryNode } from '../WABinary/types.js' -import { - CircuitBreaker, - CircuitOpenError, - createConnectionCircuitBreaker -} from './circuit-breaker.js' +import { CircuitBreaker, CircuitOpenError, createConnectionCircuitBreaker } from './circuit-breaker.js' +import type { ILogger } from './logger.js' +import { metrics } from './prometheus-metrics.js' // ============================================ // Time Constants (Enterprise-grade) @@ -41,7 +37,7 @@ const TimeMs = { /** One day in milliseconds */ Day: 86_400_000, /** One week in milliseconds */ - Week: 604_800_000, + Week: 604_800_000 } as const /** @@ -133,11 +129,13 @@ export class UnifiedSessionManager { constructor(options: UnifiedSessionOptions = {}) { this.options = { enabled: options.enabled ?? true, - logger: options.logger ?? console as unknown as ILogger, + logger: options.logger ?? (console as unknown as ILogger), enableCircuitBreaker: options.enableCircuitBreaker ?? true, - sendNode: options.sendNode ?? (async () => { - throw new Error('sendNode function not configured') - }) + sendNode: + options.sendNode ?? + (async () => { + throw new Error('sendNode function not configured') + }) } // Initialize circuit breaker if enabled @@ -177,15 +175,10 @@ export class UnifiedSessionManager { return } - const serverTimeNum = typeof serverTime === 'string' - ? parseInt(serverTime, 10) - : serverTime + const serverTimeNum = typeof serverTime === 'string' ? parseInt(serverTime, 10) : serverTime if (isNaN(serverTimeNum) || serverTimeNum <= 0) { - this.options.logger.debug?.( - { serverTime }, - 'Invalid server time received, ignoring' - ) + this.options.logger.debug?.({ serverTime }, 'Invalid server time received, ignoring') return } @@ -199,10 +192,7 @@ export class UnifiedSessionManager { const oldOffset = this.state.serverTimeOffset this.state.serverTimeOffset = newOffset - this.options.logger.debug?.( - { oldOffset, newOffset, serverTime: serverTimeNum }, - 'Server time offset updated' - ) + this.options.logger.debug?.({ oldOffset, newOffset, serverTime: serverTimeNum }, 'Server time offset updated') // Record metric metrics.socketEvents?.inc({ event: 'server_time_sync' }) @@ -268,10 +258,12 @@ export class UnifiedSessionManager { const node: BinaryNode = { tag: 'ib', attrs: {}, - content: [{ - tag: 'unified_session', - attrs: { id: sessionId } - }] + content: [ + { + tag: 'unified_session', + attrs: { id: sessionId } + } + ] } const sendOperation = async (): Promise => { @@ -303,15 +295,9 @@ export class UnifiedSessionManager { const errorMessage = error instanceof Error ? error.message : String(error) if (error instanceof CircuitOpenError) { - this.options.logger.warn?.( - { trigger, circuitState: error.state }, - 'Unified session blocked by circuit breaker' - ) + this.options.logger.warn?.({ trigger, circuitState: error.state }, 'Unified session blocked by circuit breaker') } else { - this.options.logger.warn?.( - { trigger, error: errorMessage }, - 'Failed to send unified session telemetry' - ) + this.options.logger.warn?.({ trigger, error: errorMessage }, 'Failed to send unified session telemetry') } // Record failure metric @@ -371,9 +357,7 @@ export class UnifiedSessionManager { * }) * ``` */ -export function createUnifiedSessionManager( - options: UnifiedSessionOptions = {} -): UnifiedSessionManager { +export function createUnifiedSessionManager(options: UnifiedSessionOptions = {}): UnifiedSessionManager { return new UnifiedSessionManager(options) } @@ -394,11 +378,8 @@ export function extractServerTime(node: BinaryNode): number | undefined { return undefined } - const parsed = typeof timeAttr === 'string' - ? parseInt(timeAttr, 10) - : typeof timeAttr === 'number' - ? timeAttr - : undefined + const parsed = + typeof timeAttr === 'string' ? parseInt(timeAttr, 10) : typeof timeAttr === 'number' ? timeAttr : undefined if (parsed === undefined || isNaN(parsed) || parsed <= 0) { return undefined @@ -418,6 +399,7 @@ export function shouldEnableUnifiedSession(): boolean { if (envValue !== undefined) { return envValue.toLowerCase() === 'true' || envValue === '1' } + // Default: enabled return true } diff --git a/src/Utils/version-cache.ts b/src/Utils/version-cache.ts index 97b345de..e9e7d232 100644 --- a/src/Utils/version-cache.ts +++ b/src/Utils/version-cache.ts @@ -1,7 +1,7 @@ import { promises as fs } from 'fs' import { join } from 'path' -import { fetchLatestWaWebVersion } from './generics' import type { WAVersion } from '../Types' +import { fetchLatestWaWebVersion } from './generics' /** * Version cache entry stored in file @@ -86,11 +86,7 @@ async function readCacheFile(filePath: string): Promise { +async function writeCacheFile(filePath: string, entry: VersionCacheEntry, logger?: VersionCacheLogger): Promise { try { await fs.writeFile(filePath, JSON.stringify(entry, null, 2), 'utf-8') } catch (error) { @@ -111,10 +107,7 @@ function isCacheValid(entry: VersionCacheEntry | null, ttlMs: number): boolean { /** * Fetches version with deduplication (prevents 150 parallel requests) */ -async function fetchVersionOnce( - cacheFilePath: string, - logger?: VersionCacheLogger -): Promise { +async function fetchVersionOnce(cacheFilePath: string, logger?: VersionCacheLogger): Promise { logger?.info({}, 'Fetching WhatsApp Web version (shared for all connections)...') const result = await fetchLatestWaWebVersion() @@ -131,10 +124,7 @@ async function fetchVersionOnce( // Persist to file (async, don't wait) writeCacheFile(cacheFilePath, entry, logger).catch(() => {}) - logger?.info( - { version: entry.version, source: entry.source }, - 'Version fetched and cached' - ) + logger?.info({ version: entry.version, source: entry.source }, 'Version fetched and cached') return entry } @@ -158,11 +148,7 @@ async function fetchVersionOnce( export async function getCachedVersion( config: VersionCacheConfig = {} ): Promise<{ version: WAVersion; fromCache: boolean; age: number; source: 'online' | 'fallback' | 'memory' | 'file' }> { - const { - cacheTtlMs = DEFAULT_CACHE_TTL_MS, - cacheFilePath = DEFAULT_CACHE_FILE, - logger - } = config + const { cacheTtlMs = DEFAULT_CACHE_TTL_MS, cacheFilePath = DEFAULT_CACHE_FILE, logger } = config // 1. Check memory cache first (fastest) if (isCacheValid(memoryCache, cacheTtlMs) && memoryCache) { @@ -183,8 +169,9 @@ export async function getCachedVersion( // 3. Need to fetch - but deduplicate concurrent requests // If 150 connections start at once, only 1 fetch happens if (!fetchInProgress) { - fetchInProgress = fetchVersionOnce(cacheFilePath, logger) - .finally(() => { fetchInProgress = null }) + fetchInProgress = fetchVersionOnce(cacheFilePath, logger).finally(() => { + fetchInProgress = null + }) } const entry = await fetchInProgress @@ -195,9 +182,7 @@ export async function getCachedVersion( * Clears the version cache (memory and file). * Also cancels any in-progress fetch to prevent it from restoring the cache. */ -export async function clearVersionCache( - cacheFilePath: string = DEFAULT_CACHE_FILE -): Promise { +export async function clearVersionCache(cacheFilePath: string = DEFAULT_CACHE_FILE): Promise { // Wait for any in-progress fetch to complete before clearing // This prevents the fetch from restoring the cache after we clear it if (fetchInProgress) { @@ -224,9 +209,7 @@ export async function clearVersionCache( * * @returns Object with version, success status, and source */ -export async function refreshVersionCache( - config: VersionCacheConfig = {} -): Promise { +export async function refreshVersionCache(config: VersionCacheConfig = {}): Promise { const { cacheFilePath = DEFAULT_CACHE_FILE, logger } = config // Wait for any existing fetch to complete first (deduplication) @@ -260,9 +243,7 @@ export async function refreshVersionCache( /** * Gets cache status information */ -export function getVersionCacheStatus( - cacheTtlMs: number = DEFAULT_CACHE_TTL_MS -): { +export function getVersionCacheStatus(cacheTtlMs: number = DEFAULT_CACHE_TTL_MS): { hasCache: boolean version: WAVersion | null age: number | null diff --git a/src/Utils/wasm-bridge.ts b/src/Utils/wasm-bridge.ts index 1c603907..9a3c3cb2 100644 --- a/src/Utils/wasm-bridge.ts +++ b/src/Utils/wasm-bridge.ts @@ -15,8 +15,7 @@ export { _bridgeReady as wasmBridgeReady } function getBridge(): WasmBridgeModule { if (!_bridge) { throw new Error( - 'whatsapp-rust-bridge not yet loaded. ' + - 'Ensure async operations have started before calling crypto functions.' + 'whatsapp-rust-bridge not yet loaded. ' + 'Ensure async operations have started before calling crypto functions.' ) } @@ -27,7 +26,8 @@ export const hkdf: WasmBridgeModule['hkdf'] = (...args) => getBridge().hkdf(...a export const md5: WasmBridgeModule['md5'] = (...args) => getBridge().md5(...args) -export const expandAppStateKeys: WasmBridgeModule['expandAppStateKeys'] = (...args) => getBridge().expandAppStateKeys(...args) +export const expandAppStateKeys: WasmBridgeModule['expandAppStateKeys'] = (...args) => + getBridge().expandAppStateKeys(...args) let _ltHash: InstanceType | undefined diff --git a/src/WABinary/jid-utils.ts b/src/WABinary/jid-utils.ts index 8cdf4379..ae4b2460 100644 --- a/src/WABinary/jid-utils.ts +++ b/src/WABinary/jid-utils.ts @@ -106,11 +106,9 @@ export const isHostedPnUser = (jid: string | undefined) => jid?.endsWith('@hoste /** is the jid a hosted LID */ export const isHostedLidUser = (jid: string | undefined) => jid?.endsWith('@hosted.lid') /** is the jid any LID (regular or hosted) */ -export const isAnyLidUser = (jid: string | undefined) => - !!(isLidUser(jid) || isHostedLidUser(jid)) +export const isAnyLidUser = (jid: string | undefined) => !!(isLidUser(jid) || isHostedLidUser(jid)) /** is the jid any PN user (regular or hosted) */ -export const isAnyPnUser = (jid: string | undefined) => - !!(isPnUser(jid) || isHostedPnUser(jid)) +export const isAnyPnUser = (jid: string | undefined) => !!(isPnUser(jid) || isHostedPnUser(jid)) const botRegexp = /^1313555\d{4}$|^131655500\d{2}$/ diff --git a/src/WAM/encode.ts b/src/WAM/encode.ts index d0b566ae..d39e9895 100644 --- a/src/WAM/encode.ts +++ b/src/WAM/encode.ts @@ -79,10 +79,10 @@ function encodeEvents(binaryInfo: BinaryInfo) { const fieldFlag = extended ? FLAG_EVENT : FLAG_FIELD | FLAG_EXTENDED if (id == null) { - continue - } + continue + } - binaryInfo.buffer.push(serializeData(id, value, fieldFlag)) + binaryInfo.buffer.push(serializeData(id, value, fieldFlag)) } } } diff --git a/src/WAUSync/USyncQuery.ts b/src/WAUSync/USyncQuery.ts index fdf9d78e..15df485f 100644 --- a/src/WAUSync/USyncQuery.ts +++ b/src/WAUSync/USyncQuery.ts @@ -46,7 +46,7 @@ export class USyncQuery { } parseUSyncQueryResult(result: BinaryNode | undefined): USyncQueryResult | undefined { - if (!result || result.attrs.type !== 'result') { + if (result?.attrs.type !== 'result') { return } diff --git a/src/__tests__/Signal/lid-mapping.test.ts b/src/__tests__/Signal/lid-mapping.test.ts index 61b22c56..5b8d6ffc 100644 --- a/src/__tests__/Signal/lid-mapping.test.ts +++ b/src/__tests__/Signal/lid-mapping.test.ts @@ -70,7 +70,10 @@ describe('LIDMappingStore', () => { const lidTwo = '44444:99@hosted.lid' // @ts-ignore - mockKeys.get.mockResolvedValue({ '33333_reverse': '77777', '44444_reverse': '88888' } as SignalDataTypeMap['lid-mapping']) + mockKeys.get.mockResolvedValue({ + '33333_reverse': '77777', + '44444_reverse': '88888' + } as SignalDataTypeMap['lid-mapping']) const result = await lidMappingStore.getPNsForLIDs([lidOne, lidTwo]) @@ -96,7 +99,9 @@ describe('LIDMappingStore', () => { mockKeys.get.mockResolvedValue({ '12345': lidUser } as SignalDataTypeMap['lid-mapping']) // Make 10 concurrent calls for the same PN - const promises = Array(10).fill(null).map(() => lidMappingStore.getLIDForPN(pn)) + const promises = Array(10) + .fill(null) + .map(() => lidMappingStore.getLIDForPN(pn)) // All should resolve to same result const results = await Promise.all(promises) @@ -115,7 +120,9 @@ describe('LIDMappingStore', () => { mockKeys.get.mockResolvedValue({ '54321_reverse': pnUser } as SignalDataTypeMap['lid-mapping']) // Make 10 concurrent calls for the same LID - const promises = Array(10).fill(null).map(() => lidMappingStore.getPNForLID(lid)) + const promises = Array(10) + .fill(null) + .map(() => lidMappingStore.getPNForLID(lid)) // All should resolve to same result const results = await Promise.all(promises) @@ -133,10 +140,7 @@ describe('LIDMappingStore', () => { mockKeys.get.mockResolvedValue({ '11111': 'aaaaa', '22222': 'bbbbb' } as SignalDataTypeMap['lid-mapping']) // Concurrent calls for DIFFERENT PNs - const [result1, result2] = await Promise.all([ - lidMappingStore.getLIDForPN(pn1), - lidMappingStore.getLIDForPN(pn2) - ]) + const [result1, result2] = await Promise.all([lidMappingStore.getLIDForPN(pn1), lidMappingStore.getLIDForPN(pn2)]) expect(result1).toBe('aaaaa@lid') expect(result2).toBe('bbbbb@lid') @@ -155,14 +159,15 @@ describe('LIDMappingStore', () => { lidMappingStore.destroy() // All operations should throw after destroy - await expect(lidMappingStore.getLIDForPN('12345@s.whatsapp.net')) - .rejects.toThrow('LIDMappingStore has been destroyed') + await expect(lidMappingStore.getLIDForPN('12345@s.whatsapp.net')).rejects.toThrow( + 'LIDMappingStore has been destroyed' + ) - await expect(lidMappingStore.getPNForLID('54321@lid')) - .rejects.toThrow('LIDMappingStore has been destroyed') + await expect(lidMappingStore.getPNForLID('54321@lid')).rejects.toThrow('LIDMappingStore has been destroyed') - await expect(lidMappingStore.storeLIDPNMappings([{ lid: 'a@lid', pn: 'b@s.whatsapp.net' }])) - .rejects.toThrow('LIDMappingStore has been destroyed') + await expect(lidMappingStore.storeLIDPNMappings([{ lid: 'a@lid', pn: 'b@s.whatsapp.net' }])).rejects.toThrow( + 'LIDMappingStore has been destroyed' + ) }) it('should allow destroy() to be called multiple times safely', () => { @@ -208,8 +213,7 @@ describe('LIDMappingStore', () => { expect(operationCompleted).toBe(true) // But new operations should be rejected - await expect(lidMappingStore.getLIDForPN(pn)) - .rejects.toThrow('LIDMappingStore has been destroyed') + await expect(lidMappingStore.getLIDForPN(pn)).rejects.toThrow('LIDMappingStore has been destroyed') }) }) @@ -286,15 +290,13 @@ describe('LIDMappingStore', () => { mockKeys.get.mockResolvedValue({ '12345': 'aaaaa' } as SignalDataTypeMap['lid-mapping']) const result = await lidMappingStore.getLIDsForPNs([ - '12345@s.whatsapp.net', // Valid - 'invalid', // Invalid - '67890@s.whatsapp.net' // Valid but not in DB + '12345@s.whatsapp.net', // Valid + 'invalid', // Invalid + '67890@s.whatsapp.net' // Valid but not in DB ]) // Should only return valid results - expect(result).toEqual([ - { pn: '12345@s.whatsapp.net', lid: 'aaaaa@lid' } - ]) + expect(result).toEqual([{ pn: '12345@s.whatsapp.net', lid: 'aaaaa@lid' }]) }) }) }) diff --git a/src/__tests__/Signal/session-activity-tracker.test.ts b/src/__tests__/Signal/session-activity-tracker.test.ts index ffe482cc..c15f312d 100644 --- a/src/__tests__/Signal/session-activity-tracker.test.ts +++ b/src/__tests__/Signal/session-activity-tracker.test.ts @@ -1,7 +1,7 @@ import { jest } from '@jest/globals' import P from 'pino' -import { makeSessionActivityTracker } from '../../Signal/session-activity-tracker' import type { SessionActivityMetadata } from '../../Signal/session-activity-tracker' +import { makeSessionActivityTracker } from '../../Signal/session-activity-tracker' import type { SignalKeyStoreWithTransaction } from '../../Types' const mockKeys: jest.Mocked = { @@ -137,7 +137,7 @@ describe('SessionActivityTracker', () => { const tracker = makeSessionActivityTracker(mockKeys, logger, config) // @ts-ignore - mockKeys.get.mockRejectedValue(new Error('Disk read error')) + mockKeys.get.mockRejectedValue(new Error('Disk read error')) const lastActivity = await tracker.getLastActivity('5511999999999@s.whatsapp.net') expect(lastActivity).toBeUndefined() @@ -495,10 +495,10 @@ describe('SessionActivityTracker', () => { const tracker = makeSessionActivityTracker(mockKeys, logger, config) const specialJids = [ - '5511999999999:1@s.whatsapp.net', // Device ID - '123456789@lid', // LID - '5511999999999:99@hosted', // Hosted device - '123456789:5@hosted.lid' // Hosted LID + '5511999999999:1@s.whatsapp.net', // Device ID + '123456789@lid', // LID + '5511999999999:99@hosted', // Hosted device + '123456789:5@hosted.lid' // Hosted LID ] specialJids.forEach(jid => tracker.recordActivity(jid)) diff --git a/src/__tests__/Signal/session-cleanup.test.ts b/src/__tests__/Signal/session-cleanup.test.ts index 0cea5089..5c207f28 100644 --- a/src/__tests__/Signal/session-cleanup.test.ts +++ b/src/__tests__/Signal/session-cleanup.test.ts @@ -1,8 +1,8 @@ import { jest } from '@jest/globals' import P from 'pino' -import { makeSessionCleanup } from '../../Signal/session-cleanup' -import type { SessionActivityTracker } from '../../Signal/session-activity-tracker' import type { LIDMappingStore } from '../../Signal/lid-mapping' +import type { SessionActivityTracker } from '../../Signal/session-activity-tracker' +import { makeSessionCleanup } from '../../Signal/session-cleanup' import type { SignalKeyStoreWithTransaction } from '../../Types' const mockKeys: jest.Mocked = { @@ -44,7 +44,7 @@ describe('SessionCleanup', () => { it('should delete LID orphan after 24h of inactivity', async () => { const now = Date.now() - const lastActivity = now - (25 * HOUR_MS) // 25 hours ago + const lastActivity = now - 25 * HOUR_MS // 25 hours ago // Mock sessions: 1 LID orphan // @ts-ignore @@ -56,17 +56,9 @@ describe('SessionCleanup', () => { mockLidMapping.getPNForLID.mockResolvedValue(null) // Mock: Activity 25h ago - mockActivityTracker.getAllActivities.mockResolvedValue( - new Map([['123456789@lid', lastActivity]]) - ) + mockActivityTracker.getAllActivities.mockResolvedValue(new Map([['123456789@lid', lastActivity]])) - const cleanup = makeSessionCleanup( - mockKeys, - mockLidMapping as any, - mockActivityTracker as any, - logger, - config - ) + const cleanup = makeSessionCleanup(mockKeys, mockLidMapping as any, mockActivityTracker as any, logger, config) const stats = await cleanup.runCleanup() @@ -79,7 +71,7 @@ describe('SessionCleanup', () => { it('should NOT delete LID orphan before 24h', async () => { const now = Date.now() - const lastActivity = now - (23 * HOUR_MS) // 23 hours ago + const lastActivity = now - 23 * HOUR_MS // 23 hours ago // @ts-ignore mockKeys.get.mockResolvedValue({ @@ -88,17 +80,9 @@ describe('SessionCleanup', () => { mockLidMapping.getPNForLID.mockResolvedValue(null) - mockActivityTracker.getAllActivities.mockResolvedValue( - new Map([['123456789@lid', lastActivity]]) - ) + mockActivityTracker.getAllActivities.mockResolvedValue(new Map([['123456789@lid', lastActivity]])) - const cleanup = makeSessionCleanup( - mockKeys, - mockLidMapping as any, - mockActivityTracker as any, - logger, - config - ) + const cleanup = makeSessionCleanup(mockKeys, mockLidMapping as any, mockActivityTracker as any, logger, config) const stats = await cleanup.runCleanup() @@ -118,13 +102,7 @@ describe('SessionCleanup', () => { // No activity tracked mockActivityTracker.getAllActivities.mockResolvedValue(new Map()) - const cleanup = makeSessionCleanup( - mockKeys, - mockLidMapping as any, - mockActivityTracker as any, - logger, - config - ) + const cleanup = makeSessionCleanup(mockKeys, mockLidMapping as any, mockActivityTracker as any, logger, config) const stats = await cleanup.runCleanup() @@ -134,7 +112,7 @@ describe('SessionCleanup', () => { it('should NOT delete LID with valid PN mapping', async () => { const now = Date.now() - const lastActivity = now - (25 * HOUR_MS) + const lastActivity = now - 25 * HOUR_MS // @ts-ignore mockKeys.get.mockResolvedValue({ @@ -144,17 +122,9 @@ describe('SessionCleanup', () => { // Has PN mapping - not orphan mockLidMapping.getPNForLID.mockResolvedValue('5511999999999@s.whatsapp.net') - mockActivityTracker.getAllActivities.mockResolvedValue( - new Map([['123456789@lid', lastActivity]]) - ) + mockActivityTracker.getAllActivities.mockResolvedValue(new Map([['123456789@lid', lastActivity]])) - const cleanup = makeSessionCleanup( - mockKeys, - mockLidMapping as any, - mockActivityTracker as any, - logger, - config - ) + const cleanup = makeSessionCleanup(mockKeys, mockLidMapping as any, mockActivityTracker as any, logger, config) const stats = await cleanup.runCleanup() @@ -177,7 +147,7 @@ describe('SessionCleanup', () => { it('should delete secondary device (Web/Desktop) after 15 days', async () => { const now = Date.now() - const lastActivity = now - (16 * DAY_MS) // 16 days ago + const lastActivity = now - 16 * DAY_MS // 16 days ago // Mock: Secondary device (device ID = 1) // @ts-ignore @@ -189,13 +159,7 @@ describe('SessionCleanup', () => { new Map([['5511999999999:1@s.whatsapp.net', lastActivity]]) ) - const cleanup = makeSessionCleanup( - mockKeys, - mockLidMapping as any, - mockActivityTracker as any, - logger, - config - ) + const cleanup = makeSessionCleanup(mockKeys, mockLidMapping as any, mockActivityTracker as any, logger, config) const stats = await cleanup.runCleanup() @@ -205,7 +169,7 @@ describe('SessionCleanup', () => { it('should NOT delete secondary device before 15 days', async () => { const now = Date.now() - const lastActivity = now - (14 * DAY_MS) // 14 days ago + const lastActivity = now - 14 * DAY_MS // 14 days ago // @ts-ignore mockKeys.get.mockResolvedValue({ @@ -216,13 +180,7 @@ describe('SessionCleanup', () => { new Map([['5511999999999:1@s.whatsapp.net', lastActivity]]) ) - const cleanup = makeSessionCleanup( - mockKeys, - mockLidMapping as any, - mockActivityTracker as any, - logger, - config - ) + const cleanup = makeSessionCleanup(mockKeys, mockLidMapping as any, mockActivityTracker as any, logger, config) const stats = await cleanup.runCleanup() @@ -239,13 +197,7 @@ describe('SessionCleanup', () => { // No activity tracked - grace period mockActivityTracker.getAllActivities.mockResolvedValue(new Map()) - const cleanup = makeSessionCleanup( - mockKeys, - mockLidMapping as any, - mockActivityTracker as any, - logger, - config - ) + const cleanup = makeSessionCleanup(mockKeys, mockLidMapping as any, mockActivityTracker as any, logger, config) const stats = await cleanup.runCleanup() @@ -268,7 +220,7 @@ describe('SessionCleanup', () => { it('should delete primary device after 30 days', async () => { const now = Date.now() - const lastActivity = now - (31 * DAY_MS) // 31 days ago + const lastActivity = now - 31 * DAY_MS // 31 days ago // Mock: Primary device (device ID = 0) // @ts-ignore @@ -276,17 +228,9 @@ describe('SessionCleanup', () => { '5511999999999_0.0': Buffer.from('session-data') }) - mockActivityTracker.getAllActivities.mockResolvedValue( - new Map([['5511999999999@s.whatsapp.net', lastActivity]]) - ) + mockActivityTracker.getAllActivities.mockResolvedValue(new Map([['5511999999999@s.whatsapp.net', lastActivity]])) - const cleanup = makeSessionCleanup( - mockKeys, - mockLidMapping as any, - mockActivityTracker as any, - logger, - config - ) + const cleanup = makeSessionCleanup(mockKeys, mockLidMapping as any, mockActivityTracker as any, logger, config) const stats = await cleanup.runCleanup() @@ -296,24 +240,16 @@ describe('SessionCleanup', () => { it('should NOT delete primary device before 30 days', async () => { const now = Date.now() - const lastActivity = now - (29 * DAY_MS) // 29 days ago + const lastActivity = now - 29 * DAY_MS // 29 days ago // @ts-ignore mockKeys.get.mockResolvedValue({ '5511999999999_0.0': Buffer.from('session-data') }) - mockActivityTracker.getAllActivities.mockResolvedValue( - new Map([['5511999999999@s.whatsapp.net', lastActivity]]) - ) + mockActivityTracker.getAllActivities.mockResolvedValue(new Map([['5511999999999@s.whatsapp.net', lastActivity]])) - const cleanup = makeSessionCleanup( - mockKeys, - mockLidMapping as any, - mockActivityTracker as any, - logger, - config - ) + const cleanup = makeSessionCleanup(mockKeys, mockLidMapping as any, mockActivityTracker as any, logger, config) const stats = await cleanup.runCleanup() @@ -336,7 +272,7 @@ describe('SessionCleanup', () => { it('should handle exactly 24h for LID orphan (boundary)', async () => { const now = Date.now() - const lastActivity = now - (24 * HOUR_MS) // Exactly 24h + const lastActivity = now - 24 * HOUR_MS // Exactly 24h // @ts-ignore mockKeys.get.mockResolvedValue({ @@ -345,17 +281,9 @@ describe('SessionCleanup', () => { mockLidMapping.getPNForLID.mockResolvedValue(null) - mockActivityTracker.getAllActivities.mockResolvedValue( - new Map([['123456789@lid', lastActivity]]) - ) + mockActivityTracker.getAllActivities.mockResolvedValue(new Map([['123456789@lid', lastActivity]])) - const cleanup = makeSessionCleanup( - mockKeys, - mockLidMapping as any, - mockActivityTracker as any, - logger, - config - ) + const cleanup = makeSessionCleanup(mockKeys, mockLidMapping as any, mockActivityTracker as any, logger, config) const stats = await cleanup.runCleanup() @@ -367,13 +295,7 @@ describe('SessionCleanup', () => { // @ts-ignore mockKeys.get.mockResolvedValue({}) - const cleanup = makeSessionCleanup( - mockKeys, - mockLidMapping as any, - mockActivityTracker as any, - logger, - config - ) + const cleanup = makeSessionCleanup(mockKeys, mockLidMapping as any, mockActivityTracker as any, logger, config) const stats = await cleanup.runCleanup() @@ -392,13 +314,7 @@ describe('SessionCleanup', () => { mockLidMapping.getPNForLID.mockResolvedValue(null) // Pass null tracker - const cleanup = makeSessionCleanup( - mockKeys, - mockLidMapping as any, - null, - logger, - config - ) + const cleanup = makeSessionCleanup(mockKeys, mockLidMapping as any, null, logger, config) const stats = await cleanup.runCleanup() @@ -439,20 +355,14 @@ describe('SessionCleanup', () => { mockActivityTracker.getAllActivities.mockResolvedValue( new Map([ - ['123456789@lid', now - (25 * HOUR_MS)], // LID orphan: 25h ago - ['5511999999999:1@s.whatsapp.net', now - (16 * DAY_MS)], // Secondary: 16 days ago - ['5511888888888@s.whatsapp.net', now - (31 * DAY_MS)], // Primary: 31 days ago - ['5511777777777@s.whatsapp.net', now - (5 * DAY_MS)] // Primary: 5 days ago (active) + ['123456789@lid', now - 25 * HOUR_MS], // LID orphan: 25h ago + ['5511999999999:1@s.whatsapp.net', now - 16 * DAY_MS], // Secondary: 16 days ago + ['5511888888888@s.whatsapp.net', now - 31 * DAY_MS], // Primary: 31 days ago + ['5511777777777@s.whatsapp.net', now - 5 * DAY_MS] // Primary: 5 days ago (active) ]) ) - const cleanup = makeSessionCleanup( - mockKeys, - mockLidMapping as any, - mockActivityTracker as any, - logger, - config - ) + const cleanup = makeSessionCleanup(mockKeys, mockLidMapping as any, mockActivityTracker as any, logger, config) const stats = await cleanup.runCleanup() @@ -482,13 +392,7 @@ describe('SessionCleanup', () => { '123456789_2.0': Buffer.from('session-data') }) - const cleanup = makeSessionCleanup( - mockKeys, - mockLidMapping as any, - mockActivityTracker as any, - logger, - config - ) + const cleanup = makeSessionCleanup(mockKeys, mockLidMapping as any, mockActivityTracker as any, logger, config) const stats = await cleanup.runCleanup() @@ -520,8 +424,8 @@ describe('SessionCleanup', () => { mockActivityTracker.getAllActivities.mockResolvedValue( new Map([ - ['123456789@lid', now - (13 * HOUR_MS)], // 13h ago - ['5511999999999:1@s.whatsapp.net', now - (6 * DAY_MS)] // 6 days ago + ['123456789@lid', now - 13 * HOUR_MS], // 13h ago + ['5511999999999:1@s.whatsapp.net', now - 6 * DAY_MS] // 6 days ago ]) ) diff --git a/src/__tests__/Socket/identity-change-handling.test.ts b/src/__tests__/Socket/identity-change-handling.test.ts index 74f9751b..67c3cfe3 100644 --- a/src/__tests__/Socket/identity-change-handling.test.ts +++ b/src/__tests__/Socket/identity-change-handling.test.ts @@ -1,7 +1,11 @@ import NodeCache from '@cacheable/node-cache' import { jest } from '@jest/globals' import P from 'pino' -import { handleIdentityChange, type IdentityChangeContext, type IdentityChangeResult } from '../../Utils/identity-change-handler' +import { + handleIdentityChange, + type IdentityChangeContext, + type IdentityChangeResult +} from '../../Utils/identity-change-handler' import { type BinaryNode } from '../../WABinary' const logger = P({ level: 'silent' }) @@ -259,7 +263,10 @@ describe('Identity Change Handling', () => { mockAssertSessions.mockResolvedValue(true) const node = createIdentityChangeNode('user@s.whatsapp.net') - const result = await handleIdentityChange(node, createContext()) as Extract + const result = (await handleIdentityChange(node, createContext())) as Extract< + IdentityChangeResult, + { action: 'session_refreshed' } + > expect(result.action).toBe('session_refreshed') expect(result.hadExistingSession).toBe(true) @@ -270,7 +277,10 @@ describe('Identity Change Handling', () => { mockAssertSessions.mockResolvedValue(true) const node = createIdentityChangeNode('user@s.whatsapp.net') - const result = await handleIdentityChange(node, createContext()) as Extract + const result = (await handleIdentityChange(node, createContext())) as Extract< + IdentityChangeResult, + { action: 'session_refreshed' } + > expect(result.action).toBe('session_refreshed') expect(result.hadExistingSession).toBe(false) @@ -278,7 +288,10 @@ describe('Identity Change Handling', () => { it('should include device number in skipped_companion_device result', async () => { const node = createIdentityChangeNode('user:5@s.whatsapp.net') - const result = await handleIdentityChange(node, createContext()) as Extract + const result = (await handleIdentityChange(node, createContext())) as Extract< + IdentityChangeResult, + { action: 'skipped_companion_device' } + > expect(result.action).toBe('skipped_companion_device') expect(result.device).toBe(5) diff --git a/src/__tests__/Utils/baileys-event-stream.test.ts b/src/__tests__/Utils/baileys-event-stream.test.ts index 19801432..5bb8c302 100644 --- a/src/__tests__/Utils/baileys-event-stream.test.ts +++ b/src/__tests__/Utils/baileys-event-stream.test.ts @@ -2,15 +2,14 @@ * Testes unitários para baileys-event-stream.ts */ -import { describe, it, expect, beforeEach, afterEach, jest } from '@jest/globals' +import { afterEach, beforeEach, describe, expect, it, jest } from '@jest/globals' import { BaileysEventStream, createEventStream, eventFilters, - eventTransformers, - type StreamEvent, - type BaileysEventType, type EventPriority, + eventTransformers, + type StreamEvent } from '../../Utils/baileys-event-stream.js' describe('BaileysEventStream', () => { @@ -20,7 +19,7 @@ describe('BaileysEventStream', () => { stream = createEventStream({ maxBufferSize: 100, batchSize: 10, - collectMetrics: false, + collectMetrics: false }) }) @@ -36,8 +35,8 @@ describe('BaileysEventStream', () => { expect(stream.getStats().bufferSize).toBeGreaterThan(0) }) - it('should assign priority based on event type', (done) => { - stream.on('*', (event) => { + it('should assign priority based on event type', done => { + stream.on('*', event => { expect(event.priority).toBe('critical') done() }) @@ -45,8 +44,8 @@ describe('BaileysEventStream', () => { stream.push('connection.update', { state: 'open' }) }) - it('should use custom priority when provided', (done) => { - stream.on('*', (event) => { + it('should use custom priority when provided', done => { + stream.on('*', event => { expect(event.priority).toBe('low') done() }) @@ -54,8 +53,8 @@ describe('BaileysEventStream', () => { stream.push('messages.upsert', { message: 'test' }, { priority: 'low' }) }) - it('should assign correct category', (done) => { - stream.on('*', (event) => { + it('should assign correct category', done => { + stream.on('*', event => { expect(event.category).toBe('message') done() }) @@ -68,7 +67,7 @@ describe('BaileysEventStream', () => { maxBufferSize: 5, enableBackpressure: true, highWaterMark: 3, - collectMetrics: false, + collectMetrics: false }) smallStream.pause() // Prevent processing @@ -94,7 +93,7 @@ describe('BaileysEventStream', () => { stream.push('messages.upsert', { message: 'test' }) // Wait for processing - await new Promise((resolve) => setTimeout(resolve, 50)) + await new Promise(resolve => setTimeout(resolve, 50)) expect(handler).toHaveBeenCalledTimes(1) }) @@ -106,7 +105,7 @@ describe('BaileysEventStream', () => { stream.push('messages.upsert', { message: 'test' }) stream.push('connection.update', { state: 'open' }) - await new Promise((resolve) => setTimeout(resolve, 50)) + await new Promise(resolve => setTimeout(resolve, 50)) expect(handler).toHaveBeenCalledTimes(2) }) @@ -118,7 +117,7 @@ describe('BaileysEventStream', () => { stream.push('messages.upsert', { first: true }) stream.push('messages.upsert', { second: true }) - await new Promise((resolve) => setTimeout(resolve, 50)) + await new Promise(resolve => setTimeout(resolve, 50)) expect(handler).toHaveBeenCalledTimes(1) }) @@ -130,7 +129,7 @@ describe('BaileysEventStream', () => { stream.off('messages.upsert', handler) stream.push('messages.upsert', { message: 'test' }) - await new Promise((resolve) => setTimeout(resolve, 50)) + await new Promise(resolve => setTimeout(resolve, 50)) expect(handler).not.toHaveBeenCalled() }) @@ -147,7 +146,7 @@ describe('BaileysEventStream', () => { stream.push('messages.upsert', { message: 'test' }) - await new Promise((resolve) => setTimeout(resolve, 50)) + await new Promise(resolve => setTimeout(resolve, 50)) expect(handler).not.toHaveBeenCalled() }) @@ -160,7 +159,7 @@ describe('BaileysEventStream', () => { stream.push('messages.upsert', { message: 'test' }) stream.resume() - await new Promise((resolve) => setTimeout(resolve, 50)) + await new Promise(resolve => setTimeout(resolve, 50)) expect(handler).toHaveBeenCalled() }) @@ -189,13 +188,13 @@ describe('BaileysEventStream', () => { it('should filter events before processing', async () => { const handler = jest.fn() as any - stream.addFilter((event) => (event.data as any).include === true) + stream.addFilter(event => (event.data as any).include === true) stream.on('*', handler) stream.push('messages.upsert', { include: true }) stream.push('messages.upsert', { include: false }) - await new Promise((resolve) => setTimeout(resolve, 50)) + await new Promise(resolve => setTimeout(resolve, 50)) expect(handler).toHaveBeenCalledTimes(1) }) @@ -211,7 +210,7 @@ describe('BaileysEventStream', () => { stream.push('messages.upsert', { test: true }) - await new Promise((resolve) => setTimeout(resolve, 50)) + await new Promise(resolve => setTimeout(resolve, 50)) expect(handler).toHaveBeenCalled() }) @@ -219,19 +218,19 @@ describe('BaileysEventStream', () => { describe('transformers', () => { it('should transform events before processing', async () => { - stream.addTransformer((event) => ({ + stream.addTransformer(event => ({ ...event, - metadata: { ...event.metadata, transformed: true }, + metadata: { ...event.metadata, transformed: true } })) let receivedEvent: StreamEvent | null = null - stream.on('*', (event) => { + stream.on('*', event => { receivedEvent = event }) stream.push('messages.upsert', { message: 'test' }) - await new Promise((resolve) => setTimeout(resolve, 50)) + await new Promise(resolve => setTimeout(resolve, 50)) expect((receivedEvent as any)?.metadata?.transformed).toBe(true) }) @@ -246,13 +245,13 @@ describe('BaileysEventStream', () => { const dlqStream = createEventStream({ maxRetries: 2, batchSize: 1, - collectMetrics: false, + collectMetrics: false }) dlqStream.on('messages.upsert', failingHandler) dlqStream.push('messages.upsert', { will: 'fail' }) - await new Promise((resolve) => setTimeout(resolve, 200)) + await new Promise(resolve => setTimeout(resolve, 200)) const dlq = dlqStream.getDeadLetterQueue() expect(dlq.length).toBeGreaterThan(0) @@ -280,7 +279,7 @@ describe('BaileysEventStream', () => { stream.push('connection.update', { b: 2 }) stream.push('messages.update', { c: 3 }) - await new Promise((resolve) => setTimeout(resolve, 50)) + await new Promise(resolve => setTimeout(resolve, 50)) const stats = stream.getStats() @@ -294,7 +293,7 @@ describe('BaileysEventStream', () => { stream.on('*', () => {}) stream.push('messages.upsert', { test: true }) - await new Promise((resolve) => setTimeout(resolve, 50)) + await new Promise(resolve => setTimeout(resolve, 50)) stream.resetStats() @@ -310,7 +309,7 @@ describe('BaileysEventStream', () => { stream.pause() - stream.on('*', (event) => { + stream.on('*', event => { processedOrder.push(event.priority) }) @@ -331,12 +330,12 @@ describe('BaileysEventStream', () => { }) describe('backpressure', () => { - it('should emit backpressure event when high water mark reached', (done) => { + it('should emit backpressure event when high water mark reached', done => { const bpStream = createEventStream({ maxBufferSize: 100, highWaterMark: 5, enableBackpressure: true, - collectMetrics: false, + collectMetrics: false }) bpStream.pause() @@ -352,14 +351,14 @@ describe('BaileysEventStream', () => { } }) - it('should emit drain event when below low water mark', (done) => { + it('should emit drain event when below low water mark', done => { const bpStream = createEventStream({ maxBufferSize: 100, highWaterMark: 5, lowWaterMark: 2, enableBackpressure: true, batchSize: 10, - collectMetrics: false, + collectMetrics: false }) bpStream.pause() @@ -404,7 +403,7 @@ describe('eventFilters', () => { data: {}, timestamp: Date.now(), priority: 'normal', - category: 'message', + category: 'message' } const nonMatchingEvent: StreamEvent = { @@ -413,7 +412,7 @@ describe('eventFilters', () => { data: {}, timestamp: Date.now(), priority: 'normal', - category: 'connection', + category: 'connection' } expect(filter(matchingEvent)).toBe(true) @@ -431,7 +430,7 @@ describe('eventFilters', () => { data: {}, timestamp: Date.now(), priority: 'normal', - category: 'message', + category: 'message' } const nonMatchingEvent: StreamEvent = { @@ -440,7 +439,7 @@ describe('eventFilters', () => { data: {}, timestamp: Date.now(), priority: 'normal', - category: 'presence', + category: 'presence' } expect(filter(matchingEvent)).toBe(true) @@ -458,7 +457,7 @@ describe('eventFilters', () => { data: {}, timestamp: Date.now(), priority: 'critical', - category: 'connection', + category: 'connection' } const lowEvent: StreamEvent = { @@ -467,7 +466,7 @@ describe('eventFilters', () => { data: {}, timestamp: Date.now(), priority: 'low', - category: 'presence', + category: 'presence' } expect(filter(criticalEvent)).toBe(true) @@ -485,7 +484,7 @@ describe('eventFilters', () => { data: {}, timestamp: Date.now(), priority: 'normal', - category: 'message', + category: 'message' } const oldEvent: StreamEvent = { @@ -494,7 +493,7 @@ describe('eventFilters', () => { data: {}, timestamp: Date.now() - 5000, priority: 'normal', - category: 'message', + category: 'message' } expect(filter(recentEvent)).toBe(true) @@ -504,10 +503,7 @@ describe('eventFilters', () => { describe('and', () => { it('should combine filters with AND', () => { - const filter = eventFilters.and( - eventFilters.byType('messages.upsert'), - eventFilters.byMinPriority('high') - ) + const filter = eventFilters.and(eventFilters.byType('messages.upsert'), eventFilters.byMinPriority('high')) const matchingEvent: StreamEvent = { id: '1', @@ -515,7 +511,7 @@ describe('eventFilters', () => { data: {}, timestamp: Date.now(), priority: 'high', - category: 'message', + category: 'message' } const partialMatch: StreamEvent = { @@ -524,7 +520,7 @@ describe('eventFilters', () => { data: {}, timestamp: Date.now(), priority: 'low', - category: 'message', + category: 'message' } expect(filter(matchingEvent)).toBe(true) @@ -534,10 +530,7 @@ describe('eventFilters', () => { describe('or', () => { it('should combine filters with OR', () => { - const filter = eventFilters.or( - eventFilters.byType('messages.upsert'), - eventFilters.byCategory('connection') - ) + const filter = eventFilters.or(eventFilters.byType('messages.upsert'), eventFilters.byCategory('connection')) const typeMatch: StreamEvent = { id: '1', @@ -545,7 +538,7 @@ describe('eventFilters', () => { data: {}, timestamp: Date.now(), priority: 'normal', - category: 'message', + category: 'message' } const categoryMatch: StreamEvent = { @@ -554,7 +547,7 @@ describe('eventFilters', () => { data: {}, timestamp: Date.now(), priority: 'normal', - category: 'connection', + category: 'connection' } const noMatch: StreamEvent = { @@ -563,7 +556,7 @@ describe('eventFilters', () => { data: {}, timestamp: Date.now(), priority: 'normal', - category: 'presence', + category: 'presence' } expect(filter(typeMatch)).toBe(true) @@ -584,7 +577,7 @@ describe('eventTransformers', () => { data: {}, timestamp: Date.now() - 1000, priority: 'normal', - category: 'message', + category: 'message' } const transformed = transformer(event) @@ -604,7 +597,7 @@ describe('eventTransformers', () => { data: {}, timestamp: Date.now(), priority: 'normal', - category: 'message', + category: 'message' } const transformed = transformer(event) @@ -616,7 +609,7 @@ describe('eventTransformers', () => { describe('elevatepriorityIf', () => { it('should elevate priority when condition is met', () => { const transformer = eventTransformers.elevatepriorityIf( - (event) => (event.data as { urgent?: boolean }).urgent === true, + event => (event.data as { urgent?: boolean }).urgent === true, 'critical' ) @@ -626,7 +619,7 @@ describe('eventTransformers', () => { data: { urgent: true }, timestamp: Date.now(), priority: 'normal', - category: 'message', + category: 'message' } const normalEvent: StreamEvent = { @@ -635,7 +628,7 @@ describe('eventTransformers', () => { data: { urgent: false }, timestamp: Date.now(), priority: 'normal', - category: 'message', + category: 'message' } expect(transformer(urgentEvent).priority).toBe('critical') diff --git a/src/__tests__/Utils/baileys-logger-console.test.ts b/src/__tests__/Utils/baileys-logger-console.test.ts index 91ecfa91..521d7d94 100644 --- a/src/__tests__/Utils/baileys-logger-console.test.ts +++ b/src/__tests__/Utils/baileys-logger-console.test.ts @@ -2,20 +2,20 @@ * Unit tests for baileys-logger.ts console-friendly logging functions */ -import { describe, it, expect, beforeEach, afterEach, jest } from '@jest/globals' +import { afterEach, beforeEach, describe, expect, it, jest } from '@jest/globals' import { - logEventBuffer, - logBufferMetrics, - logMessageSent, - logMessageReceived, - logConnection, logAuth, + logBufferMetrics, logCircuitBreaker, - logRetry, - logInfo, - logWarn, + logConnection, logError, + logEventBuffer, + logInfo, logLidMapping, + logMessageReceived, + logMessageSent, + logRetry, + logWarn } from '../../Utils/baileys-logger.js' describe('Baileys Console Logging Functions', () => { @@ -99,7 +99,7 @@ describe('Baileys Console Logging Functions', () => { itemsBuffered: 50, flushCount: 100, historyCacheSize: 200, - buffersInProgress: 1, + buffersInProgress: 1 }) expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('📊 Buffer Metrics')) expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('itemsBuffered: 50')) @@ -116,8 +116,8 @@ describe('Baileys Console Logging Functions', () => { mode: 'aggressive', timeout: 1000, eventRate: 1.34, - isHealthy: true, - }, + isHealthy: true + } }) expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining("mode: 'aggressive'")) expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('timeout: 1000')) diff --git a/src/__tests__/Utils/browser-utils.test.ts b/src/__tests__/Utils/browser-utils.test.ts index ad29a427..0bcd21c3 100644 --- a/src/__tests__/Utils/browser-utils.test.ts +++ b/src/__tests__/Utils/browser-utils.test.ts @@ -1,4 +1,4 @@ -import { Browsers, getPlatformId, isValidBrowserPreset, detectedOSVersions } from '../../Utils/browser-utils' +import { Browsers, detectedOSVersions, getPlatformId, isValidBrowserPreset } from '../../Utils/browser-utils' describe('browser-utils', () => { describe('Browsers', () => { diff --git a/src/__tests__/Utils/cache-utils.test.ts b/src/__tests__/Utils/cache-utils.test.ts index 83081b13..0f2650b8 100644 --- a/src/__tests__/Utils/cache-utils.test.ts +++ b/src/__tests__/Utils/cache-utils.test.ts @@ -2,14 +2,14 @@ * Testes unitários para cache-utils.ts */ -import { describe, it, expect, beforeEach, jest } from '@jest/globals' +import { beforeEach, describe, expect, it, jest } from '@jest/globals' import { Cache, - createCache, - MultiLevelCache, - withCache, - getGlobalCache, clearGlobalCaches, + createCache, + getGlobalCache, + MultiLevelCache, + withCache } from '../../Utils/cache-utils.js' describe('Cache', () => { @@ -19,7 +19,7 @@ describe('Cache', () => { cache = createCache({ ttl: 1000, maxSize: 100, - collectMetrics: false, + collectMetrics: false }) }) @@ -57,13 +57,13 @@ describe('Cache', () => { it('should expire values after TTL', async () => { const shortTtlCache = createCache({ ttl: 50, - collectMetrics: false, + collectMetrics: false }) shortTtlCache.set('expiring', 'value') expect(shortTtlCache.get('expiring')).toBe('value') - await new Promise((resolve) => setTimeout(resolve, 100)) + await new Promise(resolve => setTimeout(resolve, 100)) expect(shortTtlCache.get('expiring')).toBeUndefined() }) @@ -72,7 +72,7 @@ describe('Cache', () => { cache.set('shortLived', 'value', 50) cache.set('longLived', 'value', 5000) - await new Promise((resolve) => setTimeout(resolve, 100)) + await new Promise(resolve => setTimeout(resolve, 100)) expect(cache.get('shortLived')).toBeUndefined() expect(cache.get('longLived')).toBe('value') @@ -101,7 +101,7 @@ describe('Cache', () => { it('should handle async factories', async () => { const factory = jest.fn(async () => { - await new Promise((resolve) => setTimeout(resolve, 10)) + await new Promise(resolve => setTimeout(resolve, 10)) return 'asyncValue' }) diff --git a/src/__tests__/Utils/circuit-breaker.test.ts b/src/__tests__/Utils/circuit-breaker.test.ts index f0fc7bb1..ab66f8b8 100644 --- a/src/__tests__/Utils/circuit-breaker.test.ts +++ b/src/__tests__/Utils/circuit-breaker.test.ts @@ -2,17 +2,17 @@ * Testes unitários para circuit-breaker.ts */ -import { describe, it, expect, beforeEach, afterEach, jest } from '@jest/globals' +import { afterEach, beforeEach, describe, expect, it } from '@jest/globals' import { CircuitBreaker, - createCircuitBreaker, CircuitBreakerRegistry, - globalCircuitRegistry, CircuitOpenError, - CircuitTimeoutError, - withCircuitBreaker, - getCircuitHealth, type CircuitState, + CircuitTimeoutError, + createCircuitBreaker, + getCircuitHealth, + globalCircuitRegistry, + withCircuitBreaker } from '../../Utils/circuit-breaker.js' describe('CircuitBreaker', () => { @@ -25,7 +25,7 @@ describe('CircuitBreaker', () => { successThreshold: 2, resetTimeout: 100, timeout: 1000, - collectMetrics: false, + collectMetrics: false }) }) @@ -50,7 +50,7 @@ describe('CircuitBreaker', () => { it('should execute async operations', async () => { const result = await breaker.execute(async () => { - await new Promise((resolve) => setTimeout(resolve, 10)) + await new Promise(resolve => setTimeout(resolve, 10)) return 'async success' }) expect(result).toBe('async success') @@ -124,7 +124,7 @@ describe('CircuitBreaker', () => { }) it('should transition to half-open after reset timeout', async () => { - await new Promise((resolve) => setTimeout(resolve, 150)) + await new Promise(resolve => setTimeout(resolve, 150)) expect(breaker.isHalfOpen()).toBe(true) }) }) @@ -132,7 +132,7 @@ describe('CircuitBreaker', () => { describe('half-open state', () => { beforeEach(async () => { breaker.trip() - await new Promise((resolve) => setTimeout(resolve, 150)) + await new Promise(resolve => setTimeout(resolve, 150)) }) it('should close after success threshold', async () => { @@ -176,14 +176,12 @@ describe('CircuitBreaker', () => { const slowBreaker = createCircuitBreaker({ name: 'slow', timeout: 50, - collectMetrics: false, + collectMetrics: false }) - await expect( - slowBreaker.execute( - () => new Promise((resolve) => setTimeout(resolve, 200)) - ) - ).rejects.toThrow(CircuitTimeoutError) + await expect(slowBreaker.execute(() => new Promise(resolve => setTimeout(resolve, 200)))).rejects.toThrow( + CircuitTimeoutError + ) slowBreaker.destroy() }) @@ -193,7 +191,7 @@ describe('CircuitBreaker', () => { it('should emit state-change event', async () => { const stateChanges: Array<{ from: CircuitState; to: CircuitState }> = [] - breaker.on('state-change', (change) => { + breaker.on('state-change', change => { stateChanges.push(change) }) @@ -230,7 +228,7 @@ describe('CircuitBreaker', () => { name: 'custom', failureThreshold: 1, shouldCountError: (error: any) => error.message !== 'Ignored', - collectMetrics: false, + collectMetrics: false }) // This error should be ignored @@ -333,7 +331,7 @@ describe('withCircuitBreaker', () => { }, { name: 'wrapped-fn', - collectMetrics: false, + collectMetrics: false } ) diff --git a/src/__tests__/Utils/ctwa-recovery.test.ts b/src/__tests__/Utils/ctwa-recovery.test.ts index fc038494..05a0bfcf 100644 --- a/src/__tests__/Utils/ctwa-recovery.test.ts +++ b/src/__tests__/Utils/ctwa-recovery.test.ts @@ -9,8 +9,8 @@ */ import { proto } from '../../../WAProto/index.js' -import { metrics } from '../../Utils/prometheus-metrics.js' import { NO_MESSAGE_FOUND_ERROR_TEXT } from '../../Utils/decode-wa-message.js' +import { metrics } from '../../Utils/prometheus-metrics.js' describe('CTWA Recovery', () => { describe('Message Detection', () => { diff --git a/src/__tests__/Utils/history.test.ts b/src/__tests__/Utils/history.test.ts index d5eb2928..89214346 100644 --- a/src/__tests__/Utils/history.test.ts +++ b/src/__tests__/Utils/history.test.ts @@ -1,9 +1,9 @@ import { proto } from '../../../WAProto/index.js' import { - processHistoryMessage, extractLidPnFromConversation, extractLidPnFromMessage, - isPersonJid + isPersonJid, + processHistoryMessage } from '../../Utils/history' describe('isPersonJid', () => { @@ -107,23 +107,13 @@ describe('extractLidPnFromMessage', () => { describe('edge cases', () => { it('should return undefined when no alt JID is present', () => { - const result = extractLidPnFromMessage( - '5511999999999@s.whatsapp.net', - undefined, - undefined, - undefined - ) + const result = extractLidPnFromMessage('5511999999999@s.whatsapp.net', undefined, undefined, undefined) expect(result).toBeUndefined() }) it('should return undefined when both are same type (LID)', () => { - const result = extractLidPnFromMessage( - '123456789012345@lid', - '987654321098765@lid', - undefined, - undefined - ) + const result = extractLidPnFromMessage('123456789012345@lid', '987654321098765@lid', undefined, undefined) expect(result).toBeUndefined() }) @@ -140,12 +130,7 @@ describe('extractLidPnFromMessage', () => { }) it('should return undefined for newsletter messages', () => { - const result = extractLidPnFromMessage( - '123456789012345@newsletter', - undefined, - undefined, - undefined - ) + const result = extractLidPnFromMessage('123456789012345@newsletter', undefined, undefined, undefined) expect(result).toBeUndefined() }) @@ -235,11 +220,7 @@ describe('extractLidPnFromMessage', () => { describe('extractLidPnFromConversation', () => { describe('LID chat with pnJid', () => { it('should extract mapping when chat ID is @lid format with pnJid', () => { - const result = extractLidPnFromConversation( - '123456789012345@lid', - undefined, - '5511999999999@s.whatsapp.net' - ) + const result = extractLidPnFromConversation('123456789012345@lid', undefined, '5511999999999@s.whatsapp.net') expect(result).toEqual({ lid: '123456789012345@lid', @@ -248,11 +229,7 @@ describe('extractLidPnFromConversation', () => { }) it('should extract mapping when chat ID is @hosted.lid format with pnJid', () => { - const result = extractLidPnFromConversation( - '123456789012345@hosted.lid', - undefined, - '5511999999999@hosted' - ) + const result = extractLidPnFromConversation('123456789012345@hosted.lid', undefined, '5511999999999@hosted') expect(result).toEqual({ lid: '123456789012345@hosted.lid', @@ -263,11 +240,7 @@ describe('extractLidPnFromConversation', () => { describe('PN chat with lidJid', () => { it('should extract mapping when chat ID is @s.whatsapp.net format with lidJid', () => { - const result = extractLidPnFromConversation( - '5511999999999@s.whatsapp.net', - '123456789012345@lid', - undefined - ) + const result = extractLidPnFromConversation('5511999999999@s.whatsapp.net', '123456789012345@lid', undefined) expect(result).toEqual({ lid: '123456789012345@lid', @@ -276,11 +249,7 @@ describe('extractLidPnFromConversation', () => { }) it('should extract mapping when chat ID is @hosted format with lidJid', () => { - const result = extractLidPnFromConversation( - '5511999999999@hosted', - '123456789012345@hosted.lid', - undefined - ) + const result = extractLidPnFromConversation('5511999999999@hosted', '123456789012345@hosted.lid', undefined) expect(result).toEqual({ lid: '123456789012345@hosted.lid', @@ -291,11 +260,7 @@ describe('extractLidPnFromConversation', () => { describe('edge cases', () => { it('should return undefined for group chats', () => { - const result = extractLidPnFromConversation( - '123456789012345@g.us', - undefined, - undefined - ) + const result = extractLidPnFromConversation('123456789012345@g.us', undefined, undefined) expect(result).toBeUndefined() }) @@ -311,41 +276,25 @@ describe('extractLidPnFromConversation', () => { }) it('should return undefined for broadcast lists (@broadcast)', () => { - const result = extractLidPnFromConversation( - 'status@broadcast', - undefined, - undefined - ) + const result = extractLidPnFromConversation('status@broadcast', undefined, undefined) expect(result).toBeUndefined() }) it('should return undefined when no alternate JID is available', () => { - const result = extractLidPnFromConversation( - '123456789012345@lid', - undefined, - undefined - ) + const result = extractLidPnFromConversation('123456789012345@lid', undefined, undefined) expect(result).toBeUndefined() }) it('should return undefined when both lidJid and pnJid are null', () => { - const result = extractLidPnFromConversation( - '5511999999999@s.whatsapp.net', - null, - null - ) + const result = extractLidPnFromConversation('5511999999999@s.whatsapp.net', null, null) expect(result).toBeUndefined() }) it('should return undefined for LID chat with lidJid (no pnJid)', () => { - const result = extractLidPnFromConversation( - '123456789012345@lid', - '987654321098765@lid', - undefined - ) + const result = extractLidPnFromConversation('123456789012345@lid', '987654321098765@lid', undefined) expect(result).toBeUndefined() }) @@ -507,9 +456,7 @@ describe('processHistoryMessage', () => { const result = processHistoryMessage(historySync) - expect(result.lidPnMappings).toEqual([ - { lid: '11111111111111@lid', pn: '5511999999999@s.whatsapp.net' } - ]) + expect(result.lidPnMappings).toEqual([{ lid: '11111111111111@lid', pn: '5511999999999@s.whatsapp.net' }]) }) it('should extract LID-PN mapping when chat ID is PN with lidJid', () => { @@ -526,17 +473,13 @@ describe('processHistoryMessage', () => { const result = processHistoryMessage(historySync) - expect(result.lidPnMappings).toEqual([ - { lid: '11111111111111@lid', pn: '5511999999999@s.whatsapp.net' } - ]) + expect(result.lidPnMappings).toEqual([{ lid: '11111111111111@lid', pn: '5511999999999@s.whatsapp.net' }]) }) it('should deduplicate mappings from both sources', () => { const historySync: proto.IHistorySync = { syncType: proto.HistorySync.HistorySyncType.INITIAL_BOOTSTRAP, - phoneNumberToLidMappings: [ - { lidJid: '11111111111111@lid', pnJid: '5511999999999@s.whatsapp.net' } - ], + phoneNumberToLidMappings: [{ lidJid: '11111111111111@lid', pnJid: '5511999999999@s.whatsapp.net' }], conversations: [ { id: '11111111111111@lid', @@ -558,9 +501,7 @@ describe('processHistoryMessage', () => { it('should combine mappings from both sources', () => { const historySync: proto.IHistorySync = { syncType: proto.HistorySync.HistorySyncType.INITIAL_BOOTSTRAP, - phoneNumberToLidMappings: [ - { lidJid: '11111111111111@lid', pnJid: '5511999999999@s.whatsapp.net' } - ], + phoneNumberToLidMappings: [{ lidJid: '11111111111111@lid', pnJid: '5511999999999@s.whatsapp.net' }], conversations: [ { id: '22222222222222@lid', @@ -613,9 +554,7 @@ describe('processHistoryMessage', () => { const result = processHistoryMessage(historySync) - expect(result.lidPnMappings).toEqual([ - { lid: '11111111111111@hosted.lid', pn: '5511999999999@hosted' } - ]) + expect(result.lidPnMappings).toEqual([{ lid: '11111111111111@hosted.lid', pn: '5511999999999@hosted' }]) }) it('should extract mappings for all conversation sync types', () => { @@ -639,28 +578,20 @@ describe('processHistoryMessage', () => { const result = processHistoryMessage(historySync) - expect(result.lidPnMappings).toEqual([ - { lid: '11111111111111@lid', pn: '5511999999999@s.whatsapp.net' } - ]) + expect(result.lidPnMappings).toEqual([{ lid: '11111111111111@lid', pn: '5511999999999@s.whatsapp.net' }]) } }) it('should not extract conversation mappings for PUSH_NAME sync type', () => { const historySync: proto.IHistorySync = { syncType: proto.HistorySync.HistorySyncType.PUSH_NAME, - pushnames: [ - { id: '5511999999999@s.whatsapp.net', pushname: 'User Name' } - ], - phoneNumberToLidMappings: [ - { lidJid: '11111111111111@lid', pnJid: '5511999999999@s.whatsapp.net' } - ] + pushnames: [{ id: '5511999999999@s.whatsapp.net', pushname: 'User Name' }], + phoneNumberToLidMappings: [{ lidJid: '11111111111111@lid', pnJid: '5511999999999@s.whatsapp.net' }] } const result = processHistoryMessage(historySync) - expect(result.lidPnMappings).toEqual([ - { lid: '11111111111111@lid', pn: '5511999999999@s.whatsapp.net' } - ]) + expect(result.lidPnMappings).toEqual([{ lid: '11111111111111@lid', pn: '5511999999999@s.whatsapp.net' }]) }) it('should not extract mapping from newsletter conversations', () => { @@ -765,9 +696,7 @@ describe('processHistoryMessage', () => { it('should deduplicate mappings from all three sources', () => { const historySync = { syncType: proto.HistorySync.HistorySyncType.INITIAL_BOOTSTRAP, - phoneNumberToLidMappings: [ - { lidJid: '11111111111111@lid', pnJid: '5511999999999@s.whatsapp.net' } - ], + phoneNumberToLidMappings: [{ lidJid: '11111111111111@lid', pnJid: '5511999999999@s.whatsapp.net' }], conversations: [ { id: '11111111111111@lid', diff --git a/src/__tests__/Utils/process-sync-action.test.ts b/src/__tests__/Utils/process-sync-action.test.ts index ba19d76d..719d2437 100644 --- a/src/__tests__/Utils/process-sync-action.test.ts +++ b/src/__tests__/Utils/process-sync-action.test.ts @@ -158,10 +158,12 @@ describe('processSyncAction', () => { phoneNumber: '5511999@s.whatsapp.net' } ]) - expect(ev.emit).toHaveBeenCalledWith('lid-mapping.update', [{ - lid: '123@lid', - pn: '5511999@s.whatsapp.net' - }]) + expect(ev.emit).toHaveBeenCalledWith('lid-mapping.update', [ + { + lid: '123@lid', + pn: '5511999@s.whatsapp.net' + } + ]) }) it('does not emit events when id is missing', () => { diff --git a/src/__tests__/Utils/retry-utils.test.ts b/src/__tests__/Utils/retry-utils.test.ts index 038d4f9c..c6bf476a 100644 --- a/src/__tests__/Utils/retry-utils.test.ts +++ b/src/__tests__/Utils/retry-utils.test.ts @@ -2,23 +2,23 @@ * Testes unitários para retry-utils.ts */ -import { describe, it, expect, beforeEach, jest } from '@jest/globals' +import { beforeEach, describe, expect, it, jest } from '@jest/globals' import { - retry, - retryWithResult, - createRetrier, - retryable, - RetryManager, - RetryExhaustedError, - RetryAbortedError, calculateDelay, - retryPredicates, - retryConfigs, - getRetryDelayWithJitter, + createRetrier, getAllRetryDelaysWithJitter, + getRetryDelayWithJitter, + retry, RETRY_BACKOFF_DELAYS, RETRY_JITTER_FACTOR, + retryable, + RetryAbortedError, + retryConfigs, type RetryContext, + RetryExhaustedError, + RetryManager, + retryPredicates, + retryWithResult } from '../../Utils/retry-utils.js' describe('retry', () => { @@ -29,10 +29,13 @@ describe('retry', () => { }) it('should return result from async operation', async () => { - const result = await retry(async () => { - await new Promise((resolve) => setTimeout(resolve, 10)) - return 'async success' - }, { collectMetrics: false }) + const result = await retry( + async () => { + await new Promise(resolve => setTimeout(resolve, 10)) + return 'async success' + }, + { collectMetrics: false } + ) expect(result).toBe('async success') }) @@ -48,12 +51,13 @@ describe('retry', () => { if (attempts < 3) { throw new Error('Failing') } + return 'success after retries' }, { maxAttempts: 5, baseDelay: 10, - collectMetrics: false, + collectMetrics: false } ) @@ -70,7 +74,7 @@ describe('retry', () => { { maxAttempts: 3, baseDelay: 10, - collectMetrics: false, + collectMetrics: false } ) ).rejects.toThrow(RetryExhaustedError) @@ -80,7 +84,7 @@ describe('retry', () => { let receivedContext: RetryContext | null = null await retry( - (context) => { + context => { receivedContext = context return 'success' }, @@ -107,7 +111,7 @@ describe('retry', () => { maxAttempts: 5, baseDelay: 10, shouldRetry: () => false, - collectMetrics: false, + collectMetrics: false } ) ).rejects.toThrow(RetryExhaustedError) @@ -127,13 +131,14 @@ describe('retry', () => { if (attempts < 3) { throw new Error('Failing') } + return 'success' }, { maxAttempts: 5, baseDelay: 10, onRetry, - collectMetrics: false, + collectMetrics: false } ) @@ -145,7 +150,7 @@ describe('retry', () => { await retry(() => 'result', { onSuccess, - collectMetrics: false, + collectMetrics: false }) expect(onSuccess).toHaveBeenCalledWith('result', 1) @@ -163,7 +168,7 @@ describe('retry', () => { maxAttempts: 2, baseDelay: 10, onFailure, - collectMetrics: false, + collectMetrics: false } ) ).rejects.toThrow() @@ -178,14 +183,14 @@ describe('retry', () => { const promise = retry( async () => { - await new Promise((resolve) => setTimeout(resolve, 100)) + await new Promise(resolve => setTimeout(resolve, 100)) return 'success' }, { maxAttempts: 5, baseDelay: 50, abortSignal: controller.signal, - collectMetrics: false, + collectMetrics: false } ) @@ -214,7 +219,7 @@ describe('retryWithResult', () => { { maxAttempts: 2, baseDelay: 10, - collectMetrics: false, + collectMetrics: false } ) @@ -287,7 +292,7 @@ describe('createRetrier', () => { const myRetrier = createRetrier({ maxAttempts: 5, baseDelay: 10, - collectMetrics: false, + collectMetrics: false }) let attempts = 0 @@ -315,7 +320,7 @@ describe('retryable', () => { { maxAttempts: 5, baseDelay: 10, - collectMetrics: false, + collectMetrics: false } ) @@ -340,7 +345,7 @@ describe('RetryManager', () => { it('should cancel active retry', async () => { const promise = manager.execute('cancelable', async () => { - await new Promise((resolve) => setTimeout(resolve, 1000)) + await new Promise(resolve => setTimeout(resolve, 1000)) return 'result' }) @@ -351,10 +356,12 @@ describe('RetryManager', () => { it('should check if operation is active', async () => { let resolve: () => void - const promise = manager.execute('active', () => - new Promise((r) => { - resolve = () => r('done') - }) + const promise = manager.execute( + 'active', + () => + new Promise(r => { + resolve = () => r('done') + }) ) expect(manager.isActive('active')).toBe(true) @@ -368,7 +375,7 @@ describe('RetryManager', () => { it('should cancel all operations', async () => { const promises = [ manager.execute('op1', () => new Promise((_, reject) => setTimeout(() => reject(new Error()), 1000))), - manager.execute('op2', () => new Promise((_, reject) => setTimeout(() => reject(new Error()), 1000))), + manager.execute('op2', () => new Promise((_, reject) => setTimeout(() => reject(new Error()), 1000))) ] setTimeout(() => manager.cancelAll(), 20) @@ -425,8 +432,8 @@ describe('retryPredicates', () => { describe('or', () => { it('should combine predicates with OR', () => { const combined = retryPredicates.or( - (e) => e.message.includes('A'), - (e) => e.message.includes('B') + e => e.message.includes('A'), + e => e.message.includes('B') ) expect(combined(new Error('A'), 1)).toBe(true) @@ -438,8 +445,8 @@ describe('retryPredicates', () => { describe('and', () => { it('should combine predicates with AND', () => { const combined = retryPredicates.and( - (e) => e.message.includes('A'), - (e) => e.message.includes('B') + e => e.message.includes('A'), + e => e.message.includes('B') ) expect(combined(new Error('A and B'), 1)).toBe(true) diff --git a/src/__tests__/Utils/structured-logger.test.ts b/src/__tests__/Utils/structured-logger.test.ts index 1c9bf310..48cd4d66 100644 --- a/src/__tests__/Utils/structured-logger.test.ts +++ b/src/__tests__/Utils/structured-logger.test.ts @@ -2,15 +2,15 @@ * Testes unitários para structured-logger.ts */ -import { describe, it, expect, beforeEach, afterEach, jest } from '@jest/globals' +import { afterEach, beforeEach, describe, expect, it, jest } from '@jest/globals' import { - StructuredLogger, createStructuredLogger, - getDefaultLogger, - setDefaultLogger, createTimer, + getDefaultLogger, LOG_LEVEL_VALUES, type LogLevel, + setDefaultLogger, + StructuredLogger } from '../../Utils/structured-logger.js' describe('StructuredLogger', () => { @@ -21,7 +21,7 @@ describe('StructuredLogger', () => { logger = createStructuredLogger({ level: 'debug', name: 'test', - jsonFormat: false, + jsonFormat: false }) consoleSpy = jest.spyOn(console, 'info').mockImplementation(() => {}) jest.spyOn(console, 'debug').mockImplementation(() => {}) @@ -95,14 +95,14 @@ describe('StructuredLogger', () => { it('should redact sensitive fields', () => { const jsonLogger = createStructuredLogger({ level: 'info', - jsonFormat: true, + jsonFormat: true }) // O logger deve sanitizar campos sensíveis jsonLogger.info({ user: 'test', password: 'secret123', - token: 'abc123', + token: 'abc123' }) expect(consoleSpy).toHaveBeenCalled() @@ -167,7 +167,7 @@ describe('StructuredLogger', () => { it('should measure elapsed time', async () => { const timer = createTimer() - await new Promise((resolve) => setTimeout(resolve, 10)) + await new Promise(resolve => setTimeout(resolve, 10)) const elapsed = timer.elapsed() expect(elapsed).toBeGreaterThan(0) @@ -176,7 +176,7 @@ describe('StructuredLogger', () => { it('should format elapsed time', async () => { const timer = createTimer() - await new Promise((resolve) => setTimeout(resolve, 5)) + await new Promise(resolve => setTimeout(resolve, 5)) const formatted = timer.elapsedMs() expect(formatted).toMatch(/\d+\.\d+ms/) diff --git a/src/__tests__/Utils/trace-context.test.ts b/src/__tests__/Utils/trace-context.test.ts index 3c3ebcb6..7e17e128 100644 --- a/src/__tests__/Utils/trace-context.test.ts +++ b/src/__tests__/Utils/trace-context.test.ts @@ -2,36 +2,36 @@ * Testes unitários para trace-context.ts */ -import { describe, it, expect, beforeEach } from '@jest/globals' +import { beforeEach, describe, expect, it } from '@jest/globals' import { + addSpanEvent, + createPrecisionTimer, + createSpan, createTraceContext, + endSpan, + exportContext, + extractTraceHeaders, + generateCorrelationId, + generateSpanId, + generateTraceId, + getAllBaggage, + getBaggage, getCurrentContext, getOrCreateContext, + importContext, + injectTraceHeaders, + removeBaggage, runWithContext, runWithNewContext, - createSpan, - startSpan, - endSpan, - addSpanEvent, + setBaggage, setSpanAttributes, setSpanError, - withSpan, - withSpanSync, - setBaggage, - getBaggage, - getAllBaggage, - removeBaggage, - injectTraceHeaders, - extractTraceHeaders, - exportContext, - importContext, - createPrecisionTimer, - generateTraceId, - generateSpanId, - generateCorrelationId, + type Span, + startSpan, TRACE_HEADERS, type TraceContext, - type Span, + withSpan, + withSpanSync } from '../../Utils/trace-context.js' describe('ID generation', () => { @@ -47,6 +47,7 @@ describe('ID generation', () => { for (let i = 0; i < 100; i++) { ids.add(generateTraceId()) } + expect(ids.size).toBe(100) }) }) @@ -85,7 +86,7 @@ describe('TraceContext', () => { correlationId: 'custom-corr-id', parentSpanId: 'parent-span', baggage: { key: 'value' }, - metadata: { env: 'test' }, + metadata: { env: 'test' } }) expect(context.traceIds.traceId).toBe('custom-trace-id') @@ -174,7 +175,7 @@ describe('Spans', () => { it('should include provided attributes', () => { const span = createSpan({ name: 'test', - attributes: { key: 'value', count: 42 }, + attributes: { key: 'value', count: 42 } }) expect(span.attributes).toEqual({ key: 'value', count: 42 }) @@ -287,7 +288,7 @@ describe('Spans', () => { expect(span.status).toBe('error') expect(span.attributes.error).toBe(true) expect(span.attributes.errorMessage).toBe('Something went wrong') - expect(span.events.some((e) => e.name === 'exception')).toBe(true) + expect(span.events.some(e => e.name === 'exception')).toBe(true) }) }) }) @@ -295,7 +296,7 @@ describe('Spans', () => { describe('withSpan helpers', () => { describe('withSpan', () => { it('should execute async operation within span', async () => { - const result = await withSpan('async-op', async (span) => { + const result = await withSpan('async-op', async span => { expect(span.name).toBe('async-op') return 'async-result' }) @@ -314,7 +315,7 @@ describe('withSpan helpers', () => { describe('withSpanSync', () => { it('should execute sync operation within span', () => { - const result = withSpanSync('sync-op', (span) => { + const result = withSpanSync('sync-op', span => { expect(span.name).toBe('sync-op') return 'sync-result' }) @@ -364,7 +365,7 @@ describe('Header injection/extraction', () => { describe('injectTraceHeaders', () => { it('should inject trace headers', () => { const context = createTraceContext({ - baggage: { user: 'test' }, + baggage: { user: 'test' } }) const headers = runWithContext(context, () => { @@ -394,7 +395,7 @@ describe('Header injection/extraction', () => { [TRACE_HEADERS.TRACE_ID]: 'trace-123', [TRACE_HEADERS.PARENT_SPAN_ID]: 'parent-456', [TRACE_HEADERS.CORRELATION_ID]: 'corr-789', - [TRACE_HEADERS.BAGGAGE]: 'key1=value1,key2=value2', + [TRACE_HEADERS.BAGGAGE]: 'key1=value1,key2=value2' } const options = extractTraceHeaders(headers) @@ -412,7 +413,7 @@ describe('Context serialization', () => { it('should serialize context to JSON string', () => { const context = createTraceContext({ baggage: { key: 'value' }, - metadata: { env: 'test' }, + metadata: { env: 'test' } }) const exported = exportContext(context) @@ -430,10 +431,10 @@ describe('Context serialization', () => { traceIds: { traceId: 'trace-123', spanId: 'span-456', - correlationId: 'corr-789', + correlationId: 'corr-789' }, baggage: { imported: 'value' }, - metadata: { source: 'external' }, + metadata: { source: 'external' } }) const options = importContext(serialized) @@ -455,7 +456,7 @@ describe('PrecisionTimer', () => { it('should measure elapsed time', async () => { const timer = createPrecisionTimer() - await new Promise((resolve) => setTimeout(resolve, 20)) + await new Promise(resolve => setTimeout(resolve, 20)) const elapsed = timer.elapsed() expect(elapsed).toBeGreaterThan(10) @@ -464,7 +465,7 @@ describe('PrecisionTimer', () => { it('should format elapsed time', async () => { const timer = createPrecisionTimer() - await new Promise((resolve) => setTimeout(resolve, 5)) + await new Promise(resolve => setTimeout(resolve, 5)) const formatted = timer.elapsedFormatted() expect(formatted).toMatch(/\d+(\.\d+)?(µs|ms|s)/) @@ -473,11 +474,11 @@ describe('PrecisionTimer', () => { it('should stop timer', async () => { const timer = createPrecisionTimer() - await new Promise((resolve) => setTimeout(resolve, 10)) + await new Promise(resolve => setTimeout(resolve, 10)) const stopped = timer.stop() - await new Promise((resolve) => setTimeout(resolve, 20)) + await new Promise(resolve => setTimeout(resolve, 20)) const afterStop = timer.elapsed() diff --git a/src/__tests__/Utils/unified-session.test.ts b/src/__tests__/Utils/unified-session.test.ts index d0568616..fa768848 100644 --- a/src/__tests__/Utils/unified-session.test.ts +++ b/src/__tests__/Utils/unified-session.test.ts @@ -5,15 +5,15 @@ * official WhatsApp Web client telemetry behavior. */ -import { describe, it, expect, beforeEach, afterEach, jest } from '@jest/globals' -import type { BinaryNode } from '../../WABinary/types.js' +import { afterEach, beforeEach, describe, expect, it, jest } from '@jest/globals' import { - UnifiedSessionManager, createUnifiedSessionManager, extractServerTime, shouldEnableUnifiedSession, + UnifiedSessionManager, type UnifiedSessionOptions } from '../../Utils/unified-session.js' +import type { BinaryNode } from '../../WABinary/types.js' // TimeMs constants for testing (matching unified-session.ts) const TimeMs = { @@ -21,7 +21,7 @@ const TimeMs = { Minute: 60_000, Hour: 3_600_000, Day: 86_400_000, - Week: 604_800_000, + Week: 604_800_000 } as const // Mock the prometheus metrics to avoid side effects @@ -203,7 +203,7 @@ describe('UnifiedSessionManager', () => { expect(mockSendNode).toHaveBeenCalledTimes(1) - const sentNode = mockSendNode.mock.calls[0]?.[0] as BinaryNode | undefined + const sentNode = mockSendNode.mock.calls[0]?.[0] expect(sentNode).toBeDefined() expect(sentNode!.tag).toBe('ib') expect(Array.isArray(sentNode!.content)).toBe(true) diff --git a/src/__tests__/WABinary/jid-utils.test.ts b/src/__tests__/WABinary/jid-utils.test.ts index 456445d3..a4249cd8 100644 --- a/src/__tests__/WABinary/jid-utils.test.ts +++ b/src/__tests__/WABinary/jid-utils.test.ts @@ -1,5 +1,12 @@ import { describe, expect, it } from '@jest/globals' -import { isAnyLidUser, isAnyPnUser, isHostedLidUser, isHostedPnUser, isLidUser, isPnUser } from '../../WABinary/jid-utils' +import { + isAnyLidUser, + isAnyPnUser, + isHostedLidUser, + isHostedPnUser, + isLidUser, + isPnUser +} from '../../WABinary/jid-utils' describe('JID Utility Functions', () => { describe('isAnyLidUser', () => { diff --git a/src/index.ts b/src/index.ts index 7449b7ce..2221611c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -8,7 +8,7 @@ const _origConsoleError = console.error const _errorTimestamps = new Map() const DEDUP_WINDOW_MS = 150 -console.error = function(...args: unknown[]) { +console.error = function (...args: unknown[]) { if (args.length > 0 && typeof args[0] === 'string') { const msg = args[0] const stack = new Error().stack || '' @@ -40,9 +40,7 @@ console.error = function(...args: unknown[]) { const maskedJid = jid && jid.length > 8 ? `${jid.substring(0, 4)}****${jid.substring(jid.length - 4)}` : jid // Format clean message - const cleanMsg = maskedJid - ? `${errorType} | JID: ${maskedJid}` - : errorType + const cleanMsg = maskedJid ? `${errorType} | JID: ${maskedJid}` : errorType // Deduplication key: type + ORIGINAL JID (use unmasked to prevent collisions) const dedupeKey = `${errorType}:${jid || 'unknown'}` @@ -50,7 +48,7 @@ console.error = function(...args: unknown[]) { const lastTime = _errorTimestamps.get(dedupeKey) if (lastTime && now - lastTime < DEDUP_WINDOW_MS) { - return // Skip duplicate within 150ms window + return // Skip duplicate within 150ms window } _errorTimestamps.set(dedupeKey, now)