fix(app-state) - missing key Blocked state, history sync status & 120s timeout
fix(app-state) - missing key Blocked state, history sync status & 120s timeout
This commit is contained in:
@@ -156,6 +156,9 @@ export type MediaType = keyof typeof MEDIA_HKDF_KEY_MAPPING
|
|||||||
|
|
||||||
export const MEDIA_KEYS = Object.keys(MEDIA_PATH_MAP) as MediaType[]
|
export const MEDIA_KEYS = Object.keys(MEDIA_PATH_MAP) as MediaType[]
|
||||||
|
|
||||||
|
/** 120s timeout for history sync stall detection, same as WA Web's handleChunkProgress / restartPausedTimer (g = 120) */
|
||||||
|
export const HISTORY_SYNC_PAUSED_TIMEOUT_MS = 120_000
|
||||||
|
|
||||||
export const MIN_PREKEY_COUNT = 5
|
export const MIN_PREKEY_COUNT = 5
|
||||||
|
|
||||||
// Moderate prekey count (upstream uses 812, reduced to balance rate limiting and availability)
|
// Moderate prekey count (upstream uses 812, reduced to balance rate limiting and availability)
|
||||||
|
|||||||
+119
-4
@@ -2,7 +2,7 @@ import NodeCache from '@cacheable/node-cache'
|
|||||||
import { Boom } from '@hapi/boom'
|
import { Boom } from '@hapi/boom'
|
||||||
import { LRUCache } from 'lru-cache'
|
import { LRUCache } from 'lru-cache'
|
||||||
import { proto } from '../../WAProto/index.js'
|
import { proto } from '../../WAProto/index.js'
|
||||||
import { DEFAULT_CACHE_MAX_KEYS, DEFAULT_CACHE_TTLS, 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 {
|
import type {
|
||||||
BotListInfo,
|
BotListInfo,
|
||||||
CacheStore,
|
CacheStore,
|
||||||
@@ -42,6 +42,8 @@ import {
|
|||||||
generateProfilePicture,
|
generateProfilePicture,
|
||||||
getHistoryMsg,
|
getHistoryMsg,
|
||||||
isAppStateSyncIrrecoverable,
|
isAppStateSyncIrrecoverable,
|
||||||
|
isMissingKeyError,
|
||||||
|
MAX_SYNC_ATTEMPTS,
|
||||||
newLTHashState,
|
newLTHashState,
|
||||||
processSyncAction
|
processSyncAction
|
||||||
} from '../Utils'
|
} from '../Utils'
|
||||||
@@ -106,6 +108,17 @@ export const makeChatsSocket = (config: SocketConfig) => {
|
|||||||
// Timeout for AwaitingInitialSync state
|
// Timeout for AwaitingInitialSync state
|
||||||
let awaitingSyncTimeout: NodeJS.Timeout | undefined
|
let awaitingSyncTimeout: NodeJS.Timeout | undefined
|
||||||
|
|
||||||
|
// In-memory history sync completion tracking (resets on reconnection)
|
||||||
|
const historySyncStatus = {
|
||||||
|
initialBootstrapComplete: false,
|
||||||
|
recentSyncComplete: false
|
||||||
|
}
|
||||||
|
let historySyncPausedTimeout: NodeJS.Timeout | undefined
|
||||||
|
|
||||||
|
// Collections blocked on missing app state sync keys (mirrors WA Web's "Blocked" state).
|
||||||
|
// When a key arrives via APP_STATE_SYNC_KEY_SHARE, these are re-synced.
|
||||||
|
const blockedCollections = new Set<WAPatchName>()
|
||||||
|
|
||||||
const placeholderResendCache =
|
const placeholderResendCache =
|
||||||
config.placeholderResendCache ||
|
config.placeholderResendCache ||
|
||||||
(new NodeCache<number>({
|
(new NodeCache<number>({
|
||||||
@@ -695,7 +708,6 @@ export const makeChatsSocket = (config: SocketConfig) => {
|
|||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
attemptsMap[name] = (attemptsMap[name] || 0) + 1
|
attemptsMap[name] = (attemptsMap[name] || 0) + 1
|
||||||
|
|
||||||
const irrecoverable = isAppStateSyncIrrecoverable(error, attemptsMap[name])
|
|
||||||
const logData = {
|
const logData = {
|
||||||
name,
|
name,
|
||||||
attempt: attemptsMap[name],
|
attempt: attemptsMap[name],
|
||||||
@@ -705,7 +717,23 @@ export const makeChatsSocket = (config: SocketConfig) => {
|
|||||||
error: error.stack
|
error: error.stack
|
||||||
}
|
}
|
||||||
|
|
||||||
if (irrecoverable) {
|
if (isMissingKeyError(error) && attemptsMap[name] >= MAX_SYNC_ATTEMPTS) {
|
||||||
|
// WA Web treats missing keys as "Blocked" — park the collection
|
||||||
|
// until the key arrives via APP_STATE_SYNC_KEY_SHARE.
|
||||||
|
logger.warn(
|
||||||
|
logData,
|
||||||
|
`${name} blocked on missing key from v${states[name].version}, parking after ${attemptsMap[name]} attempts`
|
||||||
|
)
|
||||||
|
blockedCollections.add(name)
|
||||||
|
collectionsToHandle.delete(name)
|
||||||
|
} else if (isMissingKeyError(error)) {
|
||||||
|
// Retry with a snapshot which may use a different key.
|
||||||
|
logger.info(
|
||||||
|
logData,
|
||||||
|
`${name} blocked on missing key from v${states[name].version}, retrying with snapshot`
|
||||||
|
)
|
||||||
|
forceSnapshotCollections.add(name)
|
||||||
|
} else if (isAppStateSyncIrrecoverable(error, attemptsMap[name])) {
|
||||||
logger.warn(logData, `failed to sync ${name} from v${states[name].version}, giving up`)
|
logger.warn(logData, `failed to sync ${name} from v${states[name].version}, giving up`)
|
||||||
// reset persisted version to null so the next resyncAppState call
|
// reset persisted version to null so the next resyncAppState call
|
||||||
// requests a full snapshot instead of reusing the stale version that caused the error
|
// requests a full snapshot instead of reusing the stale version that caused the error
|
||||||
@@ -1230,6 +1258,56 @@ export const makeChatsSocket = (config: SocketConfig) => {
|
|||||||
PROCESSABLE_HISTORY_TYPES.includes(historyMsg.syncType! as proto.HistorySync.HistorySyncType)
|
PROCESSABLE_HISTORY_TYPES.includes(historyMsg.syncType! as proto.HistorySync.HistorySyncType)
|
||||||
: false
|
: false
|
||||||
|
|
||||||
|
if (historyMsg && shouldProcessHistoryMsg) {
|
||||||
|
const syncType = historyMsg.syncType as proto.HistorySync.HistorySyncType
|
||||||
|
|
||||||
|
// INITIAL_BOOTSTRAP — fire immediately, no progress check (same as WA Web K function)
|
||||||
|
if (
|
||||||
|
syncType === proto.HistorySync.HistorySyncType.INITIAL_BOOTSTRAP &&
|
||||||
|
!historySyncStatus.initialBootstrapComplete
|
||||||
|
) {
|
||||||
|
historySyncStatus.initialBootstrapComplete = true
|
||||||
|
ev.emit('messaging-history.status', {
|
||||||
|
syncType,
|
||||||
|
status: 'complete',
|
||||||
|
explicit: true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// RECENT with progress === 100 — explicit completion
|
||||||
|
if (
|
||||||
|
syncType === proto.HistorySync.HistorySyncType.RECENT &&
|
||||||
|
historyMsg.progress === 100 &&
|
||||||
|
!historySyncStatus.recentSyncComplete
|
||||||
|
) {
|
||||||
|
historySyncStatus.recentSyncComplete = true
|
||||||
|
clearTimeout(historySyncPausedTimeout)
|
||||||
|
historySyncPausedTimeout = undefined
|
||||||
|
ev.emit('messaging-history.status', {
|
||||||
|
syncType,
|
||||||
|
status: 'complete',
|
||||||
|
explicit: true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset 120s paused timeout on any RECENT chunk (like WA Web's handleChunkProgress)
|
||||||
|
if (syncType === proto.HistorySync.HistorySyncType.RECENT && !historySyncStatus.recentSyncComplete) {
|
||||||
|
clearTimeout(historySyncPausedTimeout)
|
||||||
|
historySyncPausedTimeout = setTimeout(() => {
|
||||||
|
if (!historySyncStatus.recentSyncComplete) {
|
||||||
|
historySyncStatus.recentSyncComplete = true
|
||||||
|
ev.emit('messaging-history.status', {
|
||||||
|
syncType: proto.HistorySync.HistorySyncType.RECENT,
|
||||||
|
status: 'paused',
|
||||||
|
explicit: false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
historySyncPausedTimeout = undefined
|
||||||
|
}, HISTORY_SYNC_PAUSED_TIMEOUT_MS)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// State machine: decide on sync and flush
|
// State machine: decide on sync and flush
|
||||||
if (historyMsg && syncState === SyncState.AwaitingInitialSync) {
|
if (historyMsg && syncState === SyncState.AwaitingInitialSync) {
|
||||||
if (awaitingSyncTimeout) {
|
if (awaitingSyncTimeout) {
|
||||||
@@ -1250,6 +1328,8 @@ export const makeChatsSocket = (config: SocketConfig) => {
|
|||||||
|
|
||||||
const doAppStateSync = async () => {
|
const doAppStateSync = async () => {
|
||||||
if (syncState === SyncState.Syncing) {
|
if (syncState === SyncState.Syncing) {
|
||||||
|
// All collections will be synced, so clear any blocked ones
|
||||||
|
blockedCollections.clear()
|
||||||
logger.info('Doing app state sync')
|
logger.info('Doing app state sync')
|
||||||
await resyncAppState(ALL_WA_PATCH_NAMES, true)
|
await resyncAppState(ALL_WA_PATCH_NAMES, true)
|
||||||
|
|
||||||
@@ -1330,6 +1410,9 @@ export const makeChatsSocket = (config: SocketConfig) => {
|
|||||||
|
|
||||||
// Clean up app state sync key cache on connection close
|
// Clean up app state sync key cache on connection close
|
||||||
if (connection === 'close') {
|
if (connection === 'close') {
|
||||||
|
blockedCollections.clear()
|
||||||
|
clearTimeout(historySyncPausedTimeout)
|
||||||
|
historySyncPausedTimeout = undefined
|
||||||
appStateSyncKeyCache.clear()
|
appStateSyncKeyCache.clear()
|
||||||
logger.debug('App state sync key cache cleared on connection close')
|
logger.debug('App state sync key cache cleared on connection close')
|
||||||
}
|
}
|
||||||
@@ -1338,6 +1421,11 @@ export const makeChatsSocket = (config: SocketConfig) => {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
historySyncStatus.initialBootstrapComplete = false
|
||||||
|
historySyncStatus.recentSyncComplete = false
|
||||||
|
clearTimeout(historySyncPausedTimeout)
|
||||||
|
historySyncPausedTimeout = undefined
|
||||||
|
|
||||||
syncState = SyncState.AwaitingInitialSync
|
syncState = SyncState.AwaitingInitialSync
|
||||||
logger.info('Connection is now AwaitingInitialSync, buffering events')
|
logger.info('Connection is now AwaitingInitialSync, buffering events')
|
||||||
ev.buffer()
|
ev.buffer()
|
||||||
@@ -1355,7 +1443,7 @@ export const makeChatsSocket = (config: SocketConfig) => {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info('History sync is enabled, awaiting notification with a 20s timeout.')
|
logger.info('First connection, awaiting history sync notification with a 20s timeout.')
|
||||||
|
|
||||||
if (awaitingSyncTimeout) {
|
if (awaitingSyncTimeout) {
|
||||||
clearTimeout(awaitingSyncTimeout)
|
clearTimeout(awaitingSyncTimeout)
|
||||||
@@ -1367,10 +1455,37 @@ export const makeChatsSocket = (config: SocketConfig) => {
|
|||||||
logger.warn('Timeout in AwaitingInitialSync, forcing state to Online and flushing buffer')
|
logger.warn('Timeout in AwaitingInitialSync, forcing state to Online and flushing buffer')
|
||||||
syncState = SyncState.Online
|
syncState = SyncState.Online
|
||||||
ev.flush()
|
ev.flush()
|
||||||
|
|
||||||
|
// Increment so subsequent reconnections skip the 20s wait.
|
||||||
|
// Late-arriving history is still processed via processMessage
|
||||||
|
// regardless of the state machine phase.
|
||||||
|
const accountSyncCounter = (authState.creds.accountSyncCounter || 0) + 1
|
||||||
|
ev.emit('creds.update', { accountSyncCounter })
|
||||||
}
|
}
|
||||||
}, 20_000)
|
}, 20_000)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// When an app state sync key arrives (myAppStateKeyId is set) and there are
|
||||||
|
// collections blocked on a missing key, trigger a re-sync for just those collections.
|
||||||
|
// This mirrors WA Web's Blocked → retry-on-key-arrival behavior.
|
||||||
|
ev.on('creds.update', ({ myAppStateKeyId }) => {
|
||||||
|
if (!myAppStateKeyId || blockedCollections.size === 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we're in the middle of a full sync, doAppStateSync handles all collections
|
||||||
|
if (syncState === SyncState.Syncing) {
|
||||||
|
blockedCollections.clear()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const collections = [...blockedCollections] as WAPatchName[]
|
||||||
|
blockedCollections.clear()
|
||||||
|
|
||||||
|
logger.info({ collections }, 'app state sync key arrived, re-syncing blocked collections')
|
||||||
|
resyncAppState(collections, false).catch(error => onUnexpectedError(error, 'blocked collections resync'))
|
||||||
|
})
|
||||||
|
|
||||||
ev.on('lid-mapping.update', async mappings => {
|
ev.on('lid-mapping.update', async mappings => {
|
||||||
try {
|
try {
|
||||||
const result = await signalRepository.lidMapping.storeLIDPNMappings(mappings)
|
const result = await signalRepository.lidMapping.storeLIDPNMappings(mappings)
|
||||||
|
|||||||
@@ -30,8 +30,21 @@ export type BaileysEventMap = {
|
|||||||
isLatest?: boolean
|
isLatest?: boolean
|
||||||
progress?: number | null
|
progress?: number | null
|
||||||
syncType?: proto.HistorySync.HistorySyncType | null
|
syncType?: proto.HistorySync.HistorySyncType | null
|
||||||
|
chunkOrder?: number | null
|
||||||
peerDataRequestSessionId?: string | null
|
peerDataRequestSessionId?: string | null
|
||||||
}
|
}
|
||||||
|
/** signals history sync milestones (completion or stall) per sync type */
|
||||||
|
'messaging-history.status': {
|
||||||
|
/** which sync phase this status refers to */
|
||||||
|
syncType: proto.HistorySync.HistorySyncType
|
||||||
|
/** the status of this sync phase */
|
||||||
|
status: 'complete' | 'paused'
|
||||||
|
/**
|
||||||
|
* progress === 100 was received from the server.
|
||||||
|
* when false, completion was inferred via timeout (no more chunks arriving).
|
||||||
|
*/
|
||||||
|
explicit: boolean
|
||||||
|
}
|
||||||
/** upsert chats */
|
/** upsert chats */
|
||||||
'chats.upsert': Chat[]
|
'chats.upsert': Chat[]
|
||||||
/** update the given chats */
|
/** update the given chats */
|
||||||
@@ -176,6 +189,7 @@ export type BufferedEventData = {
|
|||||||
isLatest: boolean
|
isLatest: boolean
|
||||||
progress?: number | null
|
progress?: number | null
|
||||||
syncType?: proto.HistorySync.HistorySyncType
|
syncType?: proto.HistorySync.HistorySyncType
|
||||||
|
chunkOrder?: number | null
|
||||||
peerDataRequestSessionId?: string
|
peerDataRequestSessionId?: string
|
||||||
}
|
}
|
||||||
chatUpserts: { [jid: string]: Chat }
|
chatUpserts: { [jid: string]: Chat }
|
||||||
|
|||||||
+18
-17
@@ -144,19 +144,21 @@ export const ensureLTHashStateVersion = (state: LTHashState): LTHashState => {
|
|||||||
export const MAX_SYNC_ATTEMPTS = 2
|
export const MAX_SYNC_ATTEMPTS = 2
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Matches WA Web's SyncdFatalError classification:
|
* Check if an error is a missing app state sync key.
|
||||||
* XMPP 400/404/405/406 are fatal, TypeError indicates WASM crash.
|
* WA Web treats these as "Blocked" (waits for key arrival), not fatal.
|
||||||
|
* In Baileys we retry with a snapshot which may use a different key.
|
||||||
|
*/
|
||||||
|
export const isMissingKeyError = (error: any): boolean => {
|
||||||
|
return error?.data?.isMissingKey === true
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines if an app state sync error is unrecoverable.
|
||||||
|
* TypeError indicates a WASM crash; otherwise we give up after MAX_SYNC_ATTEMPTS.
|
||||||
|
* Missing keys are NOT checked here — they are handled separately as "Blocked".
|
||||||
*/
|
*/
|
||||||
export const isAppStateSyncIrrecoverable = (error: any, attempts: number): boolean => {
|
export const isAppStateSyncIrrecoverable = (error: any, attempts: number): boolean => {
|
||||||
const statusCode = error?.output?.statusCode
|
return attempts >= MAX_SYNC_ATTEMPTS || error?.name === 'TypeError'
|
||||||
return (
|
|
||||||
attempts >= MAX_SYNC_ATTEMPTS ||
|
|
||||||
statusCode === 400 ||
|
|
||||||
statusCode === 404 ||
|
|
||||||
statusCode === 405 ||
|
|
||||||
statusCode === 406 ||
|
|
||||||
error?.name === 'TypeError'
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const encodeSyncdPatch = async (
|
export const encodeSyncdPatch = async (
|
||||||
@@ -167,7 +169,7 @@ export const encodeSyncdPatch = async (
|
|||||||
) => {
|
) => {
|
||||||
const key = !!myAppStateKeyId ? await getAppStateSyncKey(myAppStateKeyId) : undefined
|
const key = !!myAppStateKeyId ? await getAppStateSyncKey(myAppStateKeyId) : undefined
|
||||||
if (!key) {
|
if (!key) {
|
||||||
throw new Boom(`myAppStateKey ("${myAppStateKeyId}") not present`, { statusCode: 404 })
|
throw new Boom(`myAppStateKey ("${myAppStateKeyId}") not present`, { data: { isMissingKey: true } })
|
||||||
}
|
}
|
||||||
|
|
||||||
const encKeyId = Buffer.from(myAppStateKeyId, 'base64')
|
const encKeyId = Buffer.from(myAppStateKeyId, 'base64')
|
||||||
@@ -310,8 +312,7 @@ export const decodeSyncdMutations = async (
|
|||||||
const keyEnc = await getAppStateSyncKey(base64Key)
|
const keyEnc = await getAppStateSyncKey(base64Key)
|
||||||
if (!keyEnc) {
|
if (!keyEnc) {
|
||||||
throw new Boom(`failed to find key "${base64Key}" to decode mutation`, {
|
throw new Boom(`failed to find key "${base64Key}" to decode mutation`, {
|
||||||
statusCode: 404,
|
data: { isMissingKey: true, msgMutations }
|
||||||
data: { msgMutations }
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -341,7 +342,7 @@ export const decodeSyncdPatch = async (
|
|||||||
const base64Key = Buffer.from(msgKeyId).toString('base64')
|
const base64Key = Buffer.from(msgKeyId).toString('base64')
|
||||||
const mainKeyObj = await getAppStateSyncKey(base64Key)
|
const mainKeyObj = await getAppStateSyncKey(base64Key)
|
||||||
if (!mainKeyObj) {
|
if (!mainKeyObj) {
|
||||||
throw new Boom(`failed to find key "${base64Key}" to decode patch`, { statusCode: 404, data: { msg } })
|
throw new Boom(`failed to find key "${base64Key}" to decode patch`, { data: { isMissingKey: true, msg } })
|
||||||
}
|
}
|
||||||
|
|
||||||
const mainKeyData = mainKeyObj.keyData
|
const mainKeyData = mainKeyObj.keyData
|
||||||
@@ -513,7 +514,7 @@ export const decodeSyncdSnapshot = async (
|
|||||||
const base64Key = Buffer.from(snapKeyId).toString('base64')
|
const base64Key = Buffer.from(snapKeyId).toString('base64')
|
||||||
const keyEnc = await getAppStateSyncKey(base64Key)
|
const keyEnc = await getAppStateSyncKey(base64Key)
|
||||||
if (!keyEnc) {
|
if (!keyEnc) {
|
||||||
throw new Boom(`failed to find key "${base64Key}" to decode mutation`, { statusCode: 404 })
|
throw new Boom(`failed to find key "${base64Key}" to decode mutation`, { data: { isMissingKey: true } })
|
||||||
}
|
}
|
||||||
|
|
||||||
const snapKeyData = keyEnc.keyData
|
const snapKeyData = keyEnc.keyData
|
||||||
@@ -602,7 +603,7 @@ export const decodePatches = async (
|
|||||||
const base64Key = Buffer.from(patchKeyId).toString('base64')
|
const base64Key = Buffer.from(patchKeyId).toString('base64')
|
||||||
const keyEnc = await getAppStateSyncKey(base64Key)
|
const keyEnc = await getAppStateSyncKey(base64Key)
|
||||||
if (!keyEnc) {
|
if (!keyEnc) {
|
||||||
throw new Boom(`failed to find key "${base64Key}" to decode mutation`, { statusCode: 404 })
|
throw new Boom(`failed to find key "${base64Key}" to decode mutation`, { data: { isMissingKey: true } })
|
||||||
}
|
}
|
||||||
|
|
||||||
const patchKeyData = keyEnc.keyData
|
const patchKeyData = keyEnc.keyData
|
||||||
|
|||||||
@@ -967,6 +967,7 @@ function append<E extends BufferableEvent>(
|
|||||||
data.historySets.empty = false
|
data.historySets.empty = false
|
||||||
data.historySets.syncType = eventData.syncType
|
data.historySets.syncType = eventData.syncType
|
||||||
data.historySets.progress = eventData.progress
|
data.historySets.progress = eventData.progress
|
||||||
|
data.historySets.chunkOrder = eventData.chunkOrder
|
||||||
data.historySets.peerDataRequestSessionId = eventData.peerDataRequestSessionId
|
data.historySets.peerDataRequestSessionId = eventData.peerDataRequestSessionId
|
||||||
data.historySets.isLatest = eventData.isLatest || data.historySets.isLatest
|
data.historySets.isLatest = eventData.isLatest || data.historySets.isLatest
|
||||||
|
|
||||||
@@ -1291,6 +1292,7 @@ function consolidateEvents(data: BufferedEventData) {
|
|||||||
syncType: data.historySets.syncType,
|
syncType: data.historySets.syncType,
|
||||||
progress: data.historySets.progress,
|
progress: data.historySets.progress,
|
||||||
isLatest: data.historySets.isLatest,
|
isLatest: data.historySets.isLatest,
|
||||||
|
chunkOrder: data.historySets.chunkOrder,
|
||||||
peerDataRequestSessionId: data.historySets.peerDataRequestSessionId
|
peerDataRequestSessionId: data.historySets.peerDataRequestSessionId
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -382,6 +382,7 @@ const processMessage = async (
|
|||||||
ev.emit('messaging-history.set', {
|
ev.emit('messaging-history.set', {
|
||||||
...data,
|
...data,
|
||||||
isLatest: histNotification.syncType !== proto.HistorySync.HistorySyncType.ON_DEMAND ? isLatest : undefined,
|
isLatest: histNotification.syncType !== proto.HistorySync.HistorySyncType.ON_DEMAND ? isLatest : undefined,
|
||||||
|
chunkOrder: histNotification.chunkOrder,
|
||||||
peerDataRequestSessionId: histNotification.peerDataRequestSessionId
|
peerDataRequestSessionId: histNotification.peerDataRequestSessionId
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
decodeSyncdPatch,
|
decodeSyncdPatch,
|
||||||
decodeSyncdSnapshot,
|
decodeSyncdSnapshot,
|
||||||
isAppStateSyncIrrecoverable,
|
isAppStateSyncIrrecoverable,
|
||||||
|
isMissingKeyError,
|
||||||
MAX_SYNC_ATTEMPTS,
|
MAX_SYNC_ATTEMPTS,
|
||||||
newLTHashState
|
newLTHashState
|
||||||
} from '../../Utils/chat-utils'
|
} from '../../Utils/chat-utils'
|
||||||
@@ -12,8 +13,8 @@ import {
|
|||||||
const missingKeyFn = async () => null
|
const missingKeyFn = async () => null
|
||||||
|
|
||||||
describe('App State Sync', () => {
|
describe('App State Sync', () => {
|
||||||
describe('missing key errors throw with statusCode 404', () => {
|
describe('missing key errors are marked with isMissingKey (Blocked in WA Web)', () => {
|
||||||
it('decodeSyncdPatch throws 404 on missing key', async () => {
|
it('decodeSyncdPatch throws with isMissingKey on missing key', async () => {
|
||||||
const msg: proto.ISyncdPatch = {
|
const msg: proto.ISyncdPatch = {
|
||||||
keyId: { id: Buffer.from('missing-key') },
|
keyId: { id: Buffer.from('missing-key') },
|
||||||
mutations: [],
|
mutations: [],
|
||||||
@@ -22,12 +23,15 @@ describe('App State Sync', () => {
|
|||||||
patchMac: Buffer.alloc(32)
|
patchMac: Buffer.alloc(32)
|
||||||
}
|
}
|
||||||
|
|
||||||
await expect(
|
try {
|
||||||
decodeSyncdPatch(msg, 'regular_low', newLTHashState(), missingKeyFn, () => {}, true)
|
await decodeSyncdPatch(msg, 'regular_low', newLTHashState(), missingKeyFn, () => {}, true)
|
||||||
).rejects.toMatchObject({ output: { statusCode: 404 } })
|
fail('should have thrown')
|
||||||
|
} catch (error: any) {
|
||||||
|
expect(isMissingKeyError(error)).toBe(true)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
it('decodeSyncdSnapshot throws 404 on missing snapshot key', async () => {
|
it('decodeSyncdSnapshot throws with isMissingKey on missing snapshot key', async () => {
|
||||||
const snapshot: proto.ISyncdSnapshot = {
|
const snapshot: proto.ISyncdSnapshot = {
|
||||||
version: { version: 1 as any },
|
version: { version: 1 as any },
|
||||||
records: [],
|
records: [],
|
||||||
@@ -35,12 +39,15 @@ describe('App State Sync', () => {
|
|||||||
mac: Buffer.alloc(32)
|
mac: Buffer.alloc(32)
|
||||||
}
|
}
|
||||||
|
|
||||||
await expect(decodeSyncdSnapshot('regular_low', snapshot, missingKeyFn, undefined, true)).rejects.toMatchObject({
|
try {
|
||||||
output: { statusCode: 404 }
|
await decodeSyncdSnapshot('regular_low', snapshot, missingKeyFn, undefined, true)
|
||||||
})
|
fail('should have thrown')
|
||||||
|
} catch (error: any) {
|
||||||
|
expect(isMissingKeyError(error)).toBe(true)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
it('decodeSyncdMutations throws 404 on missing mutation key', async () => {
|
it('decodeSyncdMutations throws with isMissingKey on missing mutation key', async () => {
|
||||||
const records: proto.ISyncdRecord[] = [
|
const records: proto.ISyncdRecord[] = [
|
||||||
{
|
{
|
||||||
keyId: { id: Buffer.from('missing-key') },
|
keyId: { id: Buffer.from('missing-key') },
|
||||||
@@ -49,29 +56,36 @@ describe('App State Sync', () => {
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
await expect(decodeSyncdMutations(records, newLTHashState(), missingKeyFn, () => {}, true)).rejects.toMatchObject(
|
try {
|
||||||
{ output: { statusCode: 404 } }
|
await decodeSyncdMutations(records, newLTHashState(), missingKeyFn, () => {}, true)
|
||||||
)
|
fail('should have thrown')
|
||||||
|
} catch (error: any) {
|
||||||
|
expect(isMissingKeyError(error)).toBe(true)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
it('decodeSyncdMutations throws 404 even with validateMacs=false', async () => {
|
it('missing key errors are NOT irrecoverable on first attempt', async () => {
|
||||||
const records: proto.ISyncdRecord[] = [
|
const error = new Boom('missing key', { data: { isMissingKey: true } })
|
||||||
{
|
expect(isMissingKeyError(error)).toBe(true)
|
||||||
keyId: { id: Buffer.from('missing-key') },
|
expect(isAppStateSyncIrrecoverable(error, 1)).toBe(false)
|
||||||
value: { blob: Buffer.alloc(64) },
|
|
||||||
index: { blob: Buffer.alloc(32) }
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
await expect(
|
|
||||||
decodeSyncdMutations(records, newLTHashState(), missingKeyFn, () => {}, false)
|
|
||||||
).rejects.toMatchObject({ output: { statusCode: 404 } })
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('isAppStateSyncIrrecoverable', () => {
|
describe('isAppStateSyncIrrecoverable', () => {
|
||||||
it.each([400, 404, 405, 406])('should be irrecoverable for status %d on first attempt', statusCode => {
|
it('should NOT be irrecoverable for status 400 (dead code path removed)', () => {
|
||||||
expect(isAppStateSyncIrrecoverable(new Boom('test', { statusCode }), 1)).toBe(true)
|
expect(isAppStateSyncIrrecoverable(new Boom('test', { statusCode: 400 }), 1)).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should NOT be irrecoverable for status 404 (missing key is Blocked, not Fatal)', () => {
|
||||||
|
expect(isAppStateSyncIrrecoverable(new Boom('test', { statusCode: 404 }), 1)).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should NOT be irrecoverable for status 405', () => {
|
||||||
|
expect(isAppStateSyncIrrecoverable(new Boom('test', { statusCode: 405 }), 1)).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should NOT be irrecoverable for status 406', () => {
|
||||||
|
expect(isAppStateSyncIrrecoverable(new Boom('test', { statusCode: 406 }), 1)).toBe(false)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should be irrecoverable for TypeError', () => {
|
it('should be irrecoverable for TypeError', () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user