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
@@ -25,7 +25,7 @@ interface MockMessageRetryManager {
/** Mirrors jidNormalizedUser: strips device suffix from JID user part */
function jidNormalizedUser(jid: string): string {
const atIdx = jid.indexOf('@')
if(atIdx < 0) return jid
if (atIdx < 0) return jid
const user = jid.slice(0, atIdx)
const server = jid.slice(atIdx + 1)
const normalizedUser = user.includes(':') ? user.split(':')[0] : user
@@ -46,9 +46,9 @@ async function handleBadAck463(
const jid = jidNormalizedUser(attrs.from)
const key: MockKey = { remoteJid: attrs.from, fromMe: true, id: msgId }
if(attrs.error === '463') {
if (attrs.error === '463') {
const retryKey = `${jid}:${msgId}`
if(msgId && jid && !tcTokenRetriedMsgIds.has(retryKey)) {
if (msgId && jid && !tcTokenRetriedMsgIds.has(retryKey)) {
tcTokenRetriedMsgIds.add(retryKey)
// Each entry auto-expires after 60s — naturally bounded under normal use
setTimeout(() => tcTokenRetriedMsgIds.delete(retryKey), 60_000)
@@ -57,7 +57,7 @@ async function handleBadAck463(
(await getMessage(key)) ??
// Fallback: ack can arrive <30ms after send, before store persists
messageRetryManager?.getRecentMessage(jid, msgId)?.message
if(msg) {
if (msg) {
try {
await delayFn(1500)
await relayMessage(jid, msg, {
@@ -190,7 +190,7 @@ describe('handleBadAck error 463 retry', () => {
})
it('should not retry for non-463 errors', async () => {
for(const errorCode of ['479', '421']) {
for (const errorCode of ['479', '421']) {
mockGetMessage.mockClear()
mockRelayMessage.mockClear()
mockEmit.mockClear()
@@ -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,15 +36,11 @@ 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
if(state.didStartBuffer) {
if (state.didStartBuffer) {
warn()
flush()
state.didStartBuffer = false
@@ -57,12 +53,12 @@ function startBuffer(
* delivers all offline notifications before the safety timer fires.
*/
function onOffline(state: OfflineBufferState, flush: () => void): void {
if(state.offlineBufferTimeout) {
if (state.offlineBufferTimeout) {
clearTimeout(state.offlineBufferTimeout)
state.offlineBufferTimeout = undefined
}
if(state.didStartBuffer) {
if (state.didStartBuffer) {
flush()
state.didStartBuffer = false
}
@@ -73,7 +69,7 @@ function onOffline(state: OfflineBufferState, flush: () => void): void {
* flag so a closing socket cannot emit stale events after the fact.
*/
function onClose(state: OfflineBufferState): void {
if(state.offlineBufferTimeout) {
if (state.offlineBufferTimeout) {
clearTimeout(state.offlineBufferTimeout)
state.offlineBufferTimeout = undefined
}
@@ -97,7 +93,7 @@ describe('offline-buffer safety timer (socket.ts)', () => {
afterEach(() => {
// Clean up any remaining timer to avoid cross-test interference
if(state.offlineBufferTimeout) {
if (state.offlineBufferTimeout) {
clearTimeout(state.offlineBufferTimeout)
}
+17 -17
View File
@@ -596,8 +596,8 @@ describe('tctoken integration scenarios', () => {
const expiredJids: string[] = []
const validJids: string[] = []
for(const [jid, entry] of Object.entries(entries)) {
if(isTcTokenExpired(entry.timestamp)) {
for (const [jid, entry] of Object.entries(entries)) {
if (isTcTokenExpired(entry.timestamp)) {
expiredJids.push(jid)
} else {
validJids.push(jid)
@@ -617,8 +617,8 @@ describe('tctoken integration scenarios', () => {
}
const deletions: Record<string, null> = {}
for(const [jid, entry] of Object.entries(entries)) {
if(isTcTokenExpired(entry.timestamp)) {
for (const [jid, entry] of Object.entries(entries)) {
if (isTcTokenExpired(entry.timestamp)) {
deletions[jid] = null
}
}
@@ -635,8 +635,8 @@ describe('tctoken integration scenarios', () => {
}
const deletions: Record<string, null> = {}
for(const [jid, entry] of Object.entries(entries)) {
if(isTcTokenExpired(entry.timestamp)) {
for (const [jid, entry] of Object.entries(entries)) {
if (isTcTokenExpired(entry.timestamp)) {
deletions[jid] = null
}
}
@@ -652,8 +652,8 @@ describe('tctoken integration scenarios', () => {
}
const deletions: Record<string, null> = {}
for(const [jid, entry] of Object.entries(entries)) {
if(isTcTokenExpired(entry.timestamp)) {
for (const [jid, entry] of Object.entries(entries)) {
if (isTcTokenExpired(entry.timestamp)) {
deletions[jid] = null
}
}
@@ -669,8 +669,8 @@ describe('tctoken integration scenarios', () => {
}
const deletions: Record<string, null> = {}
for(const [jid, entry] of Object.entries(entries)) {
if(isTcTokenExpired((entry as any).timestamp)) {
for (const [jid, entry] of Object.entries(entries)) {
if (isTcTokenExpired((entry as any).timestamp)) {
deletions[jid] = null
}
}
@@ -769,8 +769,8 @@ describe('tctoken integration scenarios', () => {
it('index ignores sentinel key when loading', () => {
const jids = [JID_A, INDEX_KEY, JID_B, '', null as any]
const loaded = new Set<string>()
for(const jid of jids) {
if(jid && jid !== INDEX_KEY) {
for (const jid of jids) {
if (jid && jid !== INDEX_KEY) {
loaded.add(jid)
}
}
@@ -794,9 +794,9 @@ describe('tctoken integration scenarios', () => {
}
const expiredDeletions: Record<string, null> = {}
for(const jid of knownJids) {
for (const jid of knownJids) {
const entry = allTokens[jid]
if(!entry?.token || isTcTokenExpired(entry.timestamp)) {
if (!entry?.token || isTcTokenExpired(entry.timestamp)) {
expiredDeletions[jid] = null
knownJids.delete(jid)
}
@@ -823,7 +823,7 @@ describe('tctoken integration scenarios', () => {
}
// First add → triggers save
if(!knownJids.has(JID_A)) {
if (!knownJids.has(JID_A)) {
knownJids.add(JID_A)
scheduleSave()
}
@@ -831,7 +831,7 @@ describe('tctoken integration scenarios', () => {
expect(saveCount).toBe(1)
// Duplicate add → no save
if(!knownJids.has(JID_A)) {
if (!knownJids.has(JID_A)) {
knownJids.add(JID_A)
scheduleSave()
}
@@ -839,7 +839,7 @@ describe('tctoken integration scenarios', () => {
expect(saveCount).toBe(1)
// New JID → triggers save
if(!knownJids.has(JID_B)) {
if (!knownJids.has(JID_B)) {
knownJids.add(JID_B)
scheduleSave()
}