fix: eliminate 40s message delivery delay caused by libsignal console dumps
fix: eliminate 40s message delivery delay caused by libsignal console dumps
This commit is contained in:
@@ -24,9 +24,6 @@ jobs:
|
||||
corepack enable
|
||||
corepack prepare yarn@4.x --activate
|
||||
|
||||
- name: Fix Git config
|
||||
run: git config --global core.autocrlf input
|
||||
|
||||
- name: Configure Git for HTTPS
|
||||
run: |
|
||||
# Reescreve SSH -> HTTPS para repositórios públicos do GitHub (sem token, evita erros de autenticação)
|
||||
@@ -48,6 +45,8 @@ jobs:
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GIT_TERMINAL_PROMPT: 0
|
||||
YARN_HTTP_TIMEOUT: 600000
|
||||
YARN_HTTP_RETRY: 10
|
||||
run: yarn install --immutable
|
||||
|
||||
- name: Auto-fix formatting
|
||||
|
||||
@@ -1 +1,3 @@
|
||||
nodeLinker: node-modules
|
||||
httpRetry: 10
|
||||
httpTimeout: 600000
|
||||
|
||||
@@ -552,7 +552,10 @@ export function makeLibSignalRepository(
|
||||
|
||||
// All devices already confirmed as migrated — skip DB lookups entirely
|
||||
if (uncachedDevices.length === 0) {
|
||||
logger.debug({ fromJid, totalDevices: userDevices.length }, 'bulk device migration - all devices already cached, skipping')
|
||||
logger.debug(
|
||||
{ fromJid, totalDevices: userDevices.length },
|
||||
'bulk device migration - all devices already cached, skipping'
|
||||
)
|
||||
return { migrated: 0, skipped: 0, total: userDevices.length }
|
||||
}
|
||||
|
||||
@@ -593,6 +596,7 @@ export function makeLibSignalRepository(
|
||||
for (const device of uncachedDevices) {
|
||||
migratedSessionCache.set(`${user}.${device}`, true)
|
||||
}
|
||||
|
||||
return { migrated: 0, skipped: 0, total: userDevices.length }
|
||||
}
|
||||
|
||||
|
||||
+9
-2
@@ -2,7 +2,12 @@ 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_MAX_KEYS, DEFAULT_CACHE_TTLS, HISTORY_SYNC_PAUSED_TIMEOUT_MS, PROCESSABLE_HISTORY_TYPES } from '../Defaults'
|
||||
import {
|
||||
DEFAULT_CACHE_MAX_KEYS,
|
||||
DEFAULT_CACHE_TTLS,
|
||||
HISTORY_SYNC_PAUSED_TIMEOUT_MS,
|
||||
PROCESSABLE_HISTORY_TYPES
|
||||
} from '../Defaults'
|
||||
import type {
|
||||
BotListInfo,
|
||||
CacheStore,
|
||||
@@ -1438,7 +1443,9 @@ export const makeChatsSocket = (config: SocketConfig) => {
|
||||
// of the state machine phase (see the Syncing → Online path below).
|
||||
const isReconnection = (authState.creds.accountSyncCounter ?? 0) > 0
|
||||
if (isReconnection) {
|
||||
logger.info('Reconnection detected (accountSyncCounter > 0), skipping AwaitingInitialSync wait. Transitioning to Online immediately.')
|
||||
logger.info(
|
||||
'Reconnection detected (accountSyncCounter > 0), skipping AwaitingInitialSync wait. Transitioning to Online immediately.'
|
||||
)
|
||||
blockedCollections.clear()
|
||||
syncState = SyncState.Online
|
||||
const accountSyncCounter = (authState.creds.accountSyncCounter || 0) + 1
|
||||
|
||||
+44
-19
@@ -47,8 +47,8 @@ import {
|
||||
MISSING_KEYS_ERROR_TEXT,
|
||||
NACK_REASONS,
|
||||
NO_MESSAGE_FOUND_ERROR_TEXT,
|
||||
SERVER_ERROR_CODES,
|
||||
normalizeMessageJids,
|
||||
SERVER_ERROR_CODES,
|
||||
toNumber,
|
||||
unixTimestampSeconds,
|
||||
xmppPreKey,
|
||||
@@ -63,6 +63,7 @@ import {
|
||||
recordMessageReceived,
|
||||
recordMessageRetry
|
||||
} from '../Utils/prometheus-metrics.js'
|
||||
import { isTcTokenExpired, resolveTcTokenJid } from '../Utils/tc-token-utils'
|
||||
import {
|
||||
areJidsSameUser,
|
||||
type BinaryNode,
|
||||
@@ -81,7 +82,6 @@ import {
|
||||
jidNormalizedUser,
|
||||
S_WHATSAPP_NET
|
||||
} from '../WABinary'
|
||||
import { isTcTokenExpired, resolveTcTokenJid } from '../Utils/tc-token-utils'
|
||||
import { extractGroupMetadata } from './groups'
|
||||
import { makeMessagesSocket } from './messages-send'
|
||||
|
||||
@@ -177,7 +177,9 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
|
||||
if (pruneEntry?.timestamp) {
|
||||
lastTcTokenPruneTs = Number(pruneEntry.timestamp)
|
||||
}
|
||||
} catch { /* first run or corrupt index — start fresh */ }
|
||||
} catch {
|
||||
/* first run or corrupt index — start fresh */
|
||||
}
|
||||
})()
|
||||
|
||||
/** Debounced save of the tctoken JID index (5s) */
|
||||
@@ -722,7 +724,8 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
|
||||
// When a session is refreshed (identity change), re-issue tctoken fire-and-forget
|
||||
if (result.action === 'session_refreshed') {
|
||||
const normalizedJid = jidNormalizedUser(from)
|
||||
resolveTcTokenJid(normalizedJid, getLIDForPN).then(async (tcJid) => {
|
||||
resolveTcTokenJid(normalizedJid, getLIDForPN)
|
||||
.then(async tcJid => {
|
||||
const tcData = await authState.keys.get('tctoken', [tcJid])
|
||||
const entry = tcData[tcJid]
|
||||
if (entry?.token?.length && !isTcTokenExpired(entry.timestamp)) {
|
||||
@@ -732,7 +735,10 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
|
||||
logTcToken('reissue_fail', { jid: normalizedJid, error: err?.message })
|
||||
})
|
||||
}
|
||||
}).catch(() => { /* ignore resolution errors */ })
|
||||
})
|
||||
.catch(() => {
|
||||
/* ignore resolution errors */
|
||||
})
|
||||
}
|
||||
|
||||
if (result.action === 'no_identity_node') {
|
||||
@@ -1072,7 +1078,7 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
|
||||
if (type === 'trusted_contact' && content instanceof Uint8Array) {
|
||||
// Resolve to LID for consistent storage key
|
||||
const senderLid = attrs.sender_lid ? jidNormalizedUser(attrs.sender_lid) : undefined
|
||||
const storageJid = senderLid || await resolveTcTokenJid(from, getLIDForPN)
|
||||
const storageJid = senderLid || (await resolveTcTokenJid(from, getLIDForPN))
|
||||
|
||||
// Timestamp monotonicity guard — only store if incoming >= existing
|
||||
const existingData = await authState.keys.get('tctoken', [storageJid])
|
||||
@@ -1263,7 +1269,7 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
|
||||
}
|
||||
|
||||
if (shouldIgnoreJid(remoteJid!) && remoteJid !== S_WHATSAPP_NET) {
|
||||
logger.debug({ remoteJid }, 'ignoring receipt from jid')
|
||||
logger.trace({ remoteJid }, 'ignoring receipt from jid')
|
||||
await sendMessageAck(node)
|
||||
return
|
||||
}
|
||||
@@ -1343,7 +1349,7 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
|
||||
const handleNotification = async (node: BinaryNode) => {
|
||||
const remoteJid = node.attrs.from
|
||||
if (shouldIgnoreJid(remoteJid!) && remoteJid !== S_WHATSAPP_NET) {
|
||||
logger.debug({ remoteJid, id: node.attrs.id }, 'ignored notification')
|
||||
logger.trace({ remoteJid }, 'ignored notification')
|
||||
await sendMessageAck(node)
|
||||
return
|
||||
}
|
||||
@@ -1379,8 +1385,11 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
|
||||
|
||||
const handleMessage = async (node: BinaryNode) => {
|
||||
if (shouldIgnoreJid(node.attrs.from!) && node.attrs.from !== S_WHATSAPP_NET) {
|
||||
logger.debug({ key: node.attrs.key }, 'ignored message')
|
||||
await sendMessageAck(node, NACK_REASONS.UnhandledError)
|
||||
logger.trace({ from: node.attrs.from }, 'ignored message')
|
||||
// Send a clean ACK (no error code) so the server considers the
|
||||
// message delivered. Using error 500 (UnhandledError) previously
|
||||
// caused the server to retry delivery, generating duplicate traffic.
|
||||
await sendMessageAck(node)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1893,7 +1902,6 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
|
||||
tcTokenRetriedMsgIds.add(retryKey)
|
||||
// Each entry auto-expires after 60s — naturally bounded under normal use
|
||||
setTimeout(() => tcTokenRetriedMsgIds.delete(retryKey), 60_000)
|
||||
|
||||
;(async () => {
|
||||
try {
|
||||
await delay(1500)
|
||||
@@ -1906,13 +1914,18 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
|
||||
logTcToken('retry_463_ok', { jid, msgId })
|
||||
} else {
|
||||
logger.warn({ jid, msgId }, '463 retry: message not found in store')
|
||||
ev.emit('messages.update', [{ key, update: { status: WAMessageStatus.ERROR, messageStubParameters: ['463'] } }])
|
||||
ev.emit('messages.update', [
|
||||
{ key, update: { status: WAMessageStatus.ERROR, messageStubParameters: ['463'] } }
|
||||
])
|
||||
}
|
||||
} catch (err: any) {
|
||||
logger.warn({ jid, msgId, err: err?.message }, '463 retry failed')
|
||||
ev.emit('messages.update', [{ key, update: { status: WAMessageStatus.ERROR, messageStubParameters: ['463'] } }])
|
||||
ev.emit('messages.update', [
|
||||
{ key, update: { status: WAMessageStatus.ERROR, messageStubParameters: ['463'] } }
|
||||
])
|
||||
}
|
||||
})()
|
||||
|
||||
return
|
||||
}
|
||||
} else if (attrs.error === SERVER_ERROR_CODES.SmaxInvalid) {
|
||||
@@ -2092,16 +2105,24 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
|
||||
tcTokenIndexSaveTimer = undefined
|
||||
// Await index load first — prevents overwriting a more complete persisted index
|
||||
// if the connection closes before the initial load finishes.
|
||||
tcTokenIndexLoaded.then(() => {
|
||||
Promise.resolve(authState.keys.set({
|
||||
tcTokenIndexLoaded
|
||||
.then(() => {
|
||||
Promise.resolve(
|
||||
authState.keys.set({
|
||||
tctoken: {
|
||||
[TC_TOKEN_INDEX_KEY]: {
|
||||
token: Buffer.from(JSON.stringify([...tcTokenKnownJids]), 'utf8'),
|
||||
timestamp: unixTimestampSeconds().toString()
|
||||
}
|
||||
}
|
||||
})).catch(() => { /* non-critical */ })
|
||||
}).catch(() => { /* non-critical */ })
|
||||
})
|
||||
).catch(() => {
|
||||
/* non-critical */
|
||||
})
|
||||
})
|
||||
.catch(() => {
|
||||
/* non-critical */
|
||||
})
|
||||
}
|
||||
|
||||
if (typeof isOnline !== 'undefined') {
|
||||
@@ -2115,14 +2136,18 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
|
||||
if (now - lastTcTokenPruneTs > ONE_DAY_MS) {
|
||||
lastTcTokenPruneTs = now
|
||||
// Persist prune timestamp so it survives restarts
|
||||
Promise.resolve(authState.keys.set({
|
||||
Promise.resolve(
|
||||
authState.keys.set({
|
||||
tctoken: {
|
||||
[TC_TOKEN_PRUNE_TS_KEY]: {
|
||||
token: Buffer.alloc(0),
|
||||
timestamp: now.toString()
|
||||
}
|
||||
}
|
||||
})).catch(() => { /* non-critical */ })
|
||||
})
|
||||
).catch(() => {
|
||||
/* non-critical */
|
||||
})
|
||||
pruneExpiredTcTokens().catch(err => {
|
||||
logger.debug({ err: err?.message }, 'tctoken pruning failed')
|
||||
})
|
||||
|
||||
@@ -1457,9 +1457,7 @@ export const makeMessagesSocket = (config: SocketConfig) => {
|
||||
const is1on1Send = !isGroup && !isRetryResend && !isStatus && !isNewsletter && !isPeerMessage
|
||||
|
||||
// Resolve destination to LID for tctoken storage — matches Signal session key pattern
|
||||
const tcTokenJid = is1on1Send
|
||||
? await resolveTcTokenJid(destinationJid, getLIDForPN)
|
||||
: destinationJid
|
||||
const tcTokenJid = is1on1Send ? await resolveTcTokenJid(destinationJid, getLIDForPN) : destinationJid
|
||||
const contactTcTokenData = is1on1Send ? await authState.keys.get('tctoken', [tcTokenJid]) : {}
|
||||
const existingTokenEntry = contactTcTokenData[tcTokenJid]
|
||||
let tcTokenBuffer = existingTokenEntry?.token
|
||||
|
||||
@@ -321,7 +321,7 @@ export const makeSocket = (config: SocketConfig) => {
|
||||
} catch (error) {
|
||||
// Catch timeout and return undefined instead of throwing
|
||||
if (error instanceof Boom && error.output?.statusCode === DisconnectReason.timedOut) {
|
||||
logger?.warn?.({ msgId }, 'timed out waiting for message')
|
||||
logger?.debug?.({ msgId }, 'timed out waiting for message')
|
||||
return undefined
|
||||
}
|
||||
|
||||
@@ -1539,6 +1539,7 @@ export const makeSocket = (config: SocketConfig) => {
|
||||
if (passiveResult.status === 'rejected') {
|
||||
logger.warn({ err: passiveResult.reason }, 'failed to send initial passive iq')
|
||||
}
|
||||
|
||||
for (const result of results.slice(1)) {
|
||||
if (result.status === 'rejected') {
|
||||
logger.warn({ err: result.reason }, 'background key operation failed after login (non-critical)')
|
||||
@@ -1559,8 +1560,8 @@ export const makeSocket = (config: SocketConfig) => {
|
||||
// for the same storage locks while the offline-message backlog is draining.
|
||||
// start() is idempotent (guarded by cleanupInterval check) so deferring is safe.
|
||||
const _cleanupStartTimer = setTimeout(() => sessionCleanup.start(), 5_000)
|
||||
if (typeof (_cleanupStartTimer as NodeJS.Timeout).unref === 'function') {
|
||||
;(_cleanupStartTimer as NodeJS.Timeout).unref()
|
||||
if (typeof _cleanupStartTimer.unref === 'function') {
|
||||
_cleanupStartTimer.unref()
|
||||
}
|
||||
|
||||
// Start session activity tracker immediately (lightweight, no DB scan)
|
||||
|
||||
@@ -958,7 +958,19 @@ export function logLidMapping(
|
||||
* // Output: [BAILEYS] 🔑 TcToken expired → 5511999999999@s.whatsapp.net { age: 32d }
|
||||
*/
|
||||
export function logTcToken(
|
||||
event: 'stored' | 'expired' | 'fetch' | 'fetched' | 'reissue' | 'reissue_ok' | 'reissue_fail' | 'prune' | 'error_463' | 'error_479' | 'attached' | 'retry_463_ok',
|
||||
event:
|
||||
| 'stored'
|
||||
| 'expired'
|
||||
| 'fetch'
|
||||
| 'fetched'
|
||||
| 'reissue'
|
||||
| 'reissue_ok'
|
||||
| 'reissue_fail'
|
||||
| 'prune'
|
||||
| 'error_463'
|
||||
| 'error_479'
|
||||
| 'attached'
|
||||
| 'retry_463_ok',
|
||||
data?: Record<string, unknown>,
|
||||
sessionName?: string
|
||||
): void {
|
||||
|
||||
@@ -138,6 +138,7 @@ export const ensureLTHashStateVersion = (state: LTHashState): LTHashState => {
|
||||
if (typeof state.version !== 'number' || isNaN(state.version)) {
|
||||
state.version = 0
|
||||
}
|
||||
|
||||
return state
|
||||
}
|
||||
|
||||
|
||||
@@ -180,6 +180,7 @@ export class MessageRetryManager {
|
||||
// to prevent repeated futile lookups on subsequent retry receipts.
|
||||
this.messageKeyIndex.delete(id)
|
||||
}
|
||||
|
||||
return message
|
||||
}
|
||||
|
||||
|
||||
@@ -1241,7 +1241,7 @@ export const generateWAMessageContent = async (
|
||||
const generated = await generateCarouselMessage(carouselOptions, options)
|
||||
// Direct interactiveMessage — no viewOnceMessage wrapper, no root header/body/footer
|
||||
m.interactiveMessage = generated.interactiveMessage
|
||||
return m as proto.IMessage
|
||||
return m
|
||||
}
|
||||
// Check for nativeList
|
||||
else if (hasNonNullishProperty(message, 'nativeList')) {
|
||||
@@ -1349,7 +1349,10 @@ export const generateWAMessageContent = async (
|
||||
}))
|
||||
|
||||
m.listMessage = listMessage
|
||||
options.logger?.info({ sections: listMessage.sections?.length || 0 }, '[Interactive] Sending listMessage (sections: ' + (listMessage.sections?.length || 0) + ')')
|
||||
options.logger?.info(
|
||||
{ sections: listMessage.sections?.length || 0 },
|
||||
'[Interactive] Sending listMessage (sections: ' + (listMessage.sections?.length || 0) + ')'
|
||||
)
|
||||
} else if (hasNonNullishProperty(message, 'carousel')) {
|
||||
// Process carousel/interactive messages with viewOnceMessage wrapper
|
||||
const carousel = (message as any).carousel
|
||||
@@ -1810,7 +1813,8 @@ export const generateWAMessageFromContent = (
|
||||
}
|
||||
|
||||
// Skip ephemeral contextInfo for carousel messages
|
||||
const isCarouselEphemeral = !!(message as any)?.interactiveMessage?.carouselMessage ||
|
||||
const isCarouselEphemeral =
|
||||
!!(message as any)?.interactiveMessage?.carouselMessage ||
|
||||
!!(message as any)?.viewOnceMessage?.message?.interactiveMessage?.carouselMessage
|
||||
if (
|
||||
// if we want to send a disappearing message
|
||||
@@ -1834,7 +1838,8 @@ export const generateWAMessageFromContent = (
|
||||
|
||||
// Skip Message.create() for carousel — InteractiveMessage has oneOf (fields 4-7)
|
||||
// and create() may corrupt the carouselMessage/nativeFlowMessage oneOf resolution
|
||||
const isCarouselMsg = !!(message as any)?.interactiveMessage?.carouselMessage ||
|
||||
const isCarouselMsg =
|
||||
!!(message as any)?.interactiveMessage?.carouselMessage ||
|
||||
!!(message as any)?.viewOnceMessage?.message?.interactiveMessage?.carouselMessage
|
||||
if (!isCarouselMsg) {
|
||||
message = WAProto.Message.create(message)
|
||||
|
||||
@@ -178,7 +178,6 @@ export class UnifiedSessionManager {
|
||||
const serverTimeNum = typeof serverTime === 'string' ? parseInt(serverTime, 10) : serverTime
|
||||
|
||||
if (isNaN(serverTimeNum) || serverTimeNum <= 0) {
|
||||
this.options.logger.debug?.({ serverTime }, 'Invalid server time received, ignoring')
|
||||
return
|
||||
}
|
||||
|
||||
@@ -187,12 +186,20 @@ export class UnifiedSessionManager {
|
||||
const localTimeMs = Date.now()
|
||||
const newOffset = serverTimeMs - localTimeMs
|
||||
|
||||
// Reject outliers: if the new offset differs from the current stable
|
||||
// offset by more than 30 seconds, this timestamp is likely stale
|
||||
// (e.g. replayed device notification with old 't' value).
|
||||
// The first sample (offset === 0) is always accepted.
|
||||
const MAX_DRIFT_MS = 30_000
|
||||
if (this.state.serverTimeOffset !== 0 && Math.abs(newOffset - this.state.serverTimeOffset) > MAX_DRIFT_MS) {
|
||||
return
|
||||
}
|
||||
|
||||
// Only update if the offset changed significantly (>1 second)
|
||||
if (Math.abs(newOffset - this.state.serverTimeOffset) > 1000) {
|
||||
const oldOffset = this.state.serverTimeOffset
|
||||
this.state.serverTimeOffset = newOffset
|
||||
|
||||
this.options.logger.debug?.({ oldOffset, newOffset, serverTime: serverTimeNum }, 'Server time offset updated')
|
||||
this.options.logger.trace?.({ newOffset, serverTime: serverTimeNum }, 'Server time offset updated')
|
||||
|
||||
// Record metric
|
||||
metrics.socketEvents?.inc({ event: 'server_time_sync' })
|
||||
|
||||
@@ -274,7 +274,9 @@ describe('handleBadAck error 463 retry', () => {
|
||||
it('should fall back to messageRetryManager when getMessage returns undefined', async () => {
|
||||
const cachedMsg = { conversation: 'cached' }
|
||||
const mockRetryManager: MockMessageRetryManager = {
|
||||
getRecentMessage: jest.fn<(jid: string, msgId: string) => { message: any } | undefined>().mockReturnValue({ message: cachedMsg })
|
||||
getRecentMessage: jest
|
||||
.fn<(jid: string, msgId: string) => { message: any } | undefined>()
|
||||
.mockReturnValue({ message: cachedMsg })
|
||||
}
|
||||
mockGetMessage.mockResolvedValue(undefined)
|
||||
mockRelayMessage.mockResolvedValue(undefined)
|
||||
|
||||
@@ -36,11 +36,7 @@ function makeState(): OfflineBufferState {
|
||||
* Mirrors the process.nextTick block that arms the offline-buffer timer.
|
||||
* Only called when creds.me?.id is set (reconnection path).
|
||||
*/
|
||||
function startBuffer(
|
||||
state: OfflineBufferState,
|
||||
flush: () => void,
|
||||
warn: () => void
|
||||
): void {
|
||||
function startBuffer(state: OfflineBufferState, flush: () => void, warn: () => void): void {
|
||||
state.didStartBuffer = true
|
||||
state.offlineBufferTimeout = setTimeout(() => {
|
||||
state.offlineBufferTimeout = undefined
|
||||
|
||||
@@ -3,6 +3,28 @@
|
||||
// This MUST be at the very top to intercept console before libsignal loads
|
||||
// ============================================
|
||||
const _origConsoleError = console.error
|
||||
const _origConsoleLog = console.log
|
||||
const _origConsoleInfo = console.info
|
||||
|
||||
// Suppress libsignal session lifecycle dumps from console.log / console.info.
|
||||
// libsignal's session_record.js uses console.info("Removing old closed session:", obj)
|
||||
// and console.log("Closing session:", obj) which dump full session objects (~500ms I/O each).
|
||||
const _SESSION_LIFECYCLE_RE = /^(Closing session|Removing old closed session)/
|
||||
console.log = function (...args: unknown[]) {
|
||||
if (args.length > 0 && typeof args[0] === 'string' && _SESSION_LIFECYCLE_RE.test(args[0])) {
|
||||
return
|
||||
}
|
||||
|
||||
_origConsoleLog.apply(console, args)
|
||||
}
|
||||
|
||||
console.info = function (...args: unknown[]) {
|
||||
if (args.length > 0 && typeof args[0] === 'string' && _SESSION_LIFECYCLE_RE.test(args[0])) {
|
||||
return
|
||||
}
|
||||
|
||||
_origConsoleInfo.apply(console, args)
|
||||
}
|
||||
|
||||
// Track errors by type + JID to avoid duplicates (using Map for better performance)
|
||||
const _errorTimestamps = new Map<string, number>()
|
||||
|
||||
Reference in New Issue
Block a user