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:
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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}`
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
+12
-12
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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' })
|
||||
|
||||
Reference in New Issue
Block a user