CI lint failed with:
process-message.ts:127:6 error Blocks are nested too deeply (5). Maximum allowed is 4 max-depth
The previous shape nested `if (pnsToResolve.length) → try → if (mappings) → for → if (pn && lid)` = 5 levels. Flattened to 4 by:
- Using `mappings ?? []` so the for-of can iterate without an outer guard
- Inverting the inner predicate to `if (!pn || !lid) continue`
No behavior change. CI should now pass.
Addresses all 4 PR #387 review findings (Copilot + CodeRabbit):
#1 (Copilot, Major) — Promise.all still issued one getLIDForPN call per
chat. Replaced with a single getLIDsForPNs batch over deduped PN inputs:
turns O(N) round-trips into 1, AND shares USync retry across PNs that
miss cache. LID inputs (and @hosted.lid) skip the lookup entirely.
#4 (CodeRabbit, Major) — Promise.all is all-or-nothing: a single
resolveTcTokenJid rejection (transient DB error) would reject the whole
storeTcTokensFromHistorySync call AND abort the surrounding
HISTORY_SYNC_NOTIFICATION handler — meaning messaging-history.set
never fires and EVERY tctoken in the chunk is lost. Wrapped the batch
call in try/catch with per-chat fallback (storage under unresolved jid),
matching resolveTcTokenJid's null-LID branch.
#2 (Copilot/CodeRabbit, Minor) — `if (!candidates.length) return` was
dead code: after the early return for empty tokenChats, candidates is a
1:1 map of tokenChats and can never be empty. Removed.
#3 (CodeRabbit, Minor) — `as { ... }[]` type assertion was redundant
(TypeScript already infers the shape from the async map callback).
Removed — inferred typing now catches future shape drift.
Bonus cleanup: dropped unused `resolveTcTokenJid` import.
Tests: 35/35 suites, 824/824 passing. Customizations untouched.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
PR #386 added storeTcTokensFromHistorySync which iterates chats in a
sync chunk and calls resolveTcTokenJid (an async getLIDForPN lookup)
per candidate. The original shape used sequential `await` inside a
for-of, so each chunk blocked for O(N) round-trips before
messaging-history.set could fire downstream.
In production this surfaced as systemic message-delivery latency:
under heavy history sync (re-scan / multi-device pairing) the event
buffer backed up, the adaptive flush mode escalated to "aggressive",
and outbound sends competed with the sync queue for socket time.
QR re-scans made it worse because a fresh history sync triggered
another round of sequential resolutions.
Fix:
1. Pre-filter chats with `tcToken && tcTokenTimestamp > 0` so we
never spin up promises for the empty cases (most chats in a chunk).
2. Parallelise the resolveTcTokenJid lookups via Promise.all — same
semantics, but all N getLIDForPN calls run concurrently instead
of serially.
Downstream (loop that builds `entries`) is untouched: it remains
synchronous and keeps the same-batch dedup guard, so monotonicity
guarantees stay intact.
Customizations untouched: zero diff in carousel/buttons/lists/
LID-PN normalization/Bad MAC/proto.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* perf: optimize history sync memory and CPU usage
Aligns with Baileys upstream PR #2333 . Surgical port of
4 micro-optimizations + 2 test files. No collision with InfiniteAPI's
LID/PN, carousel, buttons, Bad MAC or app state sync resilience code.
Changes:
1. WAProto longToString/longToNumber: use native BigInt instead of
Long library division loops. ~4.6x faster per call (237ns -> 52ns).
BigInt.toString() delegates to V8 native C++. Same change applied
to fix-imports.js (template) and index.js (generated artifact) so
future regenerations stay in sync.
2. downloadHistory: stream-pipe createInflate instead of buffering
the full compressed payload then inflating. Cuts RSS peak ~50%
on 50MB history payloads.
3. processHistoryMessage: drop the { ...chat } shallow copy before
pushing to chats[]. The decoded protobuf object is not referenced
after the push.
4. downloadEncryptedContent transform: skip Buffer.concat when
remainingBytes is empty (the common case — chunks usually arrive
AES-aligned). ~19x faster per chunk (78ms -> 4ms over 25k chunks).
Tests:
- bigint-validation.test.ts (NEW): 32 parameterized cases covering
zero, MAX_SAFE_INTEGER boundary, max int64/uint64, negative values,
tricky bit patterns and powers of 2; plus 200-iter fuzz comparing
old Long vs new BigInt implementations for equivalence.
- proto-tojson-long.test.ts: kept the existing fork-only test for
string-in-Long-field graceful handling, appended 4 upstream tests
covering Long->string fast path, large unsigned > MAX_SAFE_INTEGER,
zero/small values and encode/decode roundtrip.
* test(binary): cover reconnection sync skip for both signals
Ports the upstream reconnection-sync-skip test from Baileys PR #2350
and adapts it to cover InfiniteAPI's dual-signal reconnect detection
in chats.ts:
1. accountSyncCounter > 0 (a previous full sync completed)
2. socketSkippedOfflineBuffer (forwarded from socket.ts when
hadStaleRoutingInfo is true, e.g. routingInfo discarded on start)
Both are required because they cover different reconnect scenarios
and the absence of the second branch causes a buffer mismatch where
socket.ts skips the offline buffer while chats.ts still waits in
AwaitingInitialSync — stalling live messages for up to 4 seconds.
Co-Authored-By: Renato Alcara
* perf(messages-recv): early-ignore JIDs before buffer/queue
Aligns with Baileys upstream PR #2352. Moves the shouldIgnoreJid check
out of handleMessage/handleReceipt/handleNotification and into
processNode so ignored stanzas are acked and dropped before entering
ev.buffer(), the offline queue, or the keyed mutexes. Skips the LID->PN
resolution work in handleReceipt for ignored receipts as a side effect.
Diverges from upstream by excluding type='call' from the early-ignore
check — InfiniteAPI's handleCall has never used shouldIgnoreJid, so
keeping calls outside this filter preserves existing behavior for
integrators that filter messages but still want call events.
Carousel, buttons, LID/PN normalization, Bad MAC fix and retry manager
untouched.
Co-Authored-By: Renato Alcara
* feat: add inbound username support and USync username protocol
Aligns with Baileys upstream PR #2480. WhatsApp is rolling out an
inbound username field that maps to the user's LID. This change is
purely additive — it captures and propagates the new optional field
through types, decoders, USync queries, and group/contact events.
Skipped intentionally to preserve InfiniteAPI's LID/PN customization:
- handleGroupNotification in messages-recv.ts (custom LID->PN flow on
groups.upsert / participants.map). Username for these specific
events still flows via process-message's emitParticipantsUpdate
(reads message.key.participantUsername added in decode-wa-message).
Carousel/buttons code (messages.ts, messages-send.ts) untouched.
Co-Authored-By: Renato Alcara
Adds experimental native protocol mode (WAM\x05) as alternative to
web protocol (WA\x06\x03). Configurable via BAILEYS_PROTOCOL env var:
- web (default): standard WhatsApp Web protocol
- native: Android native protocol (may enable view-once media on companions)
When native mode is enabled:
- NOISE_WA_HEADER changes from WA\x06\x03 to WAM\x05
- DICT_VERSION changes from 3 to 5
- WebInfo is omitted from ClientPayload
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add webdPayload to WebInfo during connection with:
- supportsE2EImage/Video/Audio/Document = true
- supportsMediaRetry = true
- features bitmask with view-once capability flag
This may enable the WA server to deliver view-once media content
to Baileys companions instead of just <unavailable> placeholders.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When receiving <unavailable type='view_once'/>, attempt to request
the actual media content from the primary phone via PDO
(Peer Data Operation / PLACEHOLDER_MESSAGE_RESEND).
If the phone responds, the full media (url, mediaKey, directPath)
arrives via messages.upsert, allowing consumers to display the image.
Falls back to placeholder if PDO fails or phone doesn't respond.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The early unavailable handler at line ~2261 was intercepting
view_once stanzas and silently discarding them (sendMessageAck + return)
before the view_once_unavailable_fanout handler could emit them.
Now emits a viewOnceMessage placeholder with isViewOnce=true so
consumers receive the event in messages.upsert.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Two issues found during view-once testing on linked devices:
1. msmsg block removal:
The "temporary fix" that discarded all msmsg-type encrypted messages
was also blocking legitimate view-once messages. On linked devices,
view-once arrives as msmsg encryption type. The block was originally
added to prevent crashes from missing messageSecret, but the existing
try/catch in the decrypt path already handles those errors gracefully
by marking the message as a CIPHERTEXT stub.
2. view_once_unavailable_fanout emission:
When a linked device receives a view-once, the WA server sends only
<unavailable type="view_once"/> (no <enc> with actual media content).
Previously this was silently acknowledged without emitting any event,
so consumers (zpro, astra-api, etc.) never knew a view-once arrived.
Now it emits the message as a CIPHERTEXT stub with key.isViewOnce=true
so consumers can display "view-once message received" in their UI.
Co-Authored-By: Renato Alcara
Two issues found during view-once testing on linked devices:
1. msmsg block removal:
The "temporary fix" that discarded all msmsg-type encrypted messages
was also blocking legitimate view-once messages. On linked devices,
view-once arrives as msmsg encryption type. The block was originally
added to prevent crashes from missing messageSecret, but the existing
try/catch in the decrypt path already handles those errors gracefully
by marking the message as a CIPHERTEXT stub.
2. view_once_unavailable_fanout emission:
When a linked device receives a view-once, the WA server sends only
<unavailable type="view_once"/> (no <enc> with actual media content).
Previously this was silently acknowledged without emitting any event,
so consumers (zpro, astra-api, etc.) never knew a view-once arrived.
Now it emits the message as a CIPHERTEXT stub with key.isViewOnce=true
so consumers can display "view-once message received" in their UI.
Co-Authored-By: Renato Alcara