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:
Renato Alcara
2026-02-27 08:43:54 -03:00
committed by GitHub
parent efc927728b
commit d233a7856f
18 changed files with 359 additions and 277 deletions
+13 -1
View File
@@ -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 {
+1
View File
@@ -138,6 +138,7 @@ export const ensureLTHashStateVersion = (state: LTHashState): LTHashState => {
if (typeof state.version !== 'number' || isNaN(state.version)) {
state.version = 0
}
return state
}
+5 -5
View File
@@ -376,9 +376,9 @@ export const getErrorCodeFromStreamError = (node: BinaryNode) => {
// Conflict child: type attribute determines connectionReplaced vs loggedOut.
// WA Web default: any type other than 'replaced' is treated as device_removed (loggedOut).
if(reason === 'conflict') {
if (reason === 'conflict') {
const conflictType = reasonNode!.attrs?.type
if(conflictType === 'replaced') {
if (conflictType === 'replaced') {
return { reason: 'replaced', statusCode: DisconnectReason.connectionReplaced }
}
@@ -388,11 +388,11 @@ export const getErrorCodeFromStreamError = (node: BinaryNode) => {
// Child-level code parsing: parent code attr > child code attr > CODE_MAP from child tag > badSession
const statusCode = +(node.attrs.code || reasonNode?.attrs?.code || CODE_MAP[reason] || DisconnectReason.badSession)
if(statusCode === DisconnectReason.restartRequired) {
if (statusCode === DisconnectReason.restartRequired) {
reason = 'restart required'
} else if(statusCode === DisconnectReason.sessionInvalidated) {
} else if (statusCode === DisconnectReason.sessionInvalidated) {
reason = 'session invalidated'
} else if(node.attrs.code) {
} else if (node.attrs.code) {
reason = `code ${statusCode}`
}
+1
View File
@@ -180,6 +180,7 @@ export class MessageRetryManager {
// to prevent repeated futile lookups on subsequent retry receipts.
this.messageKeyIndex.delete(id)
}
return message
}
+9 -4
View File
@@ -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)
+12 -12
View File
@@ -18,9 +18,9 @@ const TC_TOKEN_NUM_BUCKETS = 4
* If WA ever diverges these, add a `mode` parameter here.
*/
export function isTcTokenExpired(timestamp: number | string | null | undefined): boolean {
if(timestamp === null || timestamp === undefined) return true
if (timestamp === null || timestamp === undefined) return true
const ts = typeof timestamp === 'string' ? Number(timestamp) : timestamp
if(isNaN(ts)) return true
if (isNaN(ts)) return true
const now = Math.floor(Date.now() / 1000)
const currentBucket = Math.floor(now / TC_TOKEN_BUCKET_DURATION)
const cutoffBucket = currentBucket - (TC_TOKEN_NUM_BUCKETS - 1)
@@ -35,7 +35,7 @@ export function isTcTokenExpired(timestamp: number | string | null | undefined):
* Returns true if senderTimestamp is null/undefined or in a previous bucket.
*/
export function shouldSendNewTcToken(senderTimestamp: number | undefined): boolean {
if(senderTimestamp === undefined) return true
if (senderTimestamp === undefined) return true
const now = Math.floor(Date.now() / 1000)
const currentBucket = Math.floor(now / TC_TOKEN_BUCKET_DURATION)
const senderBucket = Math.floor(senderTimestamp / TC_TOKEN_BUCKET_DURATION)
@@ -58,7 +58,7 @@ export async function resolveTcTokenJid(
getLIDForPN: (pn: string) => Promise<string | null>
): Promise<string> {
const normalized = jidNormalizedUser(jid)
if(isLidUser(normalized)) return normalized
if (isLidUser(normalized)) return normalized
const lid = await getLIDForPN(normalized)
return lid ?? normalized
}
@@ -84,9 +84,9 @@ export async function buildTcTokenFromJid({
const entry = tcTokenData?.[storageJid]
const tcTokenBuffer = entry?.token
if(!tcTokenBuffer?.length || isTcTokenExpired(entry?.timestamp)) {
if (!tcTokenBuffer?.length || isTcTokenExpired(entry?.timestamp)) {
// Opportunistic cleanup: remove expired token from store
if(tcTokenBuffer) {
if (tcTokenBuffer) {
await authState.keys.set({ tctoken: { [storageJid]: null } })
}
@@ -100,7 +100,7 @@ export async function buildTcTokenFromJid({
})
return baseContent
} catch(error) {
} catch (error) {
return baseContent.length > 0 ? baseContent : undefined
}
}
@@ -127,11 +127,11 @@ export async function storeTcTokensFromIqResult({
onNewJidStored
}: StoreTcTokensParams) {
const tokensNode = getBinaryNodeChild(result, 'tokens')
if(!tokensNode) return
if (!tokensNode) return
const tokenNodes = getBinaryNodeChildren(tokensNode, 'token')
for(const tokenNode of tokenNodes) {
if(tokenNode.attrs.type !== 'trusted_contact' || !(tokenNode.content instanceof Uint8Array)) {
for (const tokenNode of tokenNodes) {
if (tokenNode.attrs.type !== 'trusted_contact' || !(tokenNode.content instanceof Uint8Array)) {
continue
}
@@ -144,13 +144,13 @@ export async function storeTcTokensFromIqResult({
// Matches WA Web handleIncomingTcToken
const existingTs = existingEntry?.timestamp ? Number(existingEntry.timestamp) : 0
const incomingTs = tokenNode.attrs.t ? Number(tokenNode.attrs.t) : 0
if(existingTs > 0 && incomingTs > 0 && existingTs > incomingTs) {
if (existingTs > 0 && incomingTs > 0 && existingTs > incomingTs) {
continue
}
// Don't store timestamp-less tokens at all — isTcTokenExpired treats them
// as immediately expired regardless of whether an existing entry is present
if(!incomingTs) {
if (!incomingTs) {
continue
}
+10 -3
View File
@@ -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' })