Skip to content

feat: match WhatsApp Web keepalive, dead socket, and reconnect behavior - #341

Merged
jlucaso1 merged 8 commits into
mainfrom
feat/match-wa-web-keepalive
Mar 14, 2026
Merged

feat: match WhatsApp Web keepalive, dead socket, and reconnect behavior#341
jlucaso1 merged 8 commits into
mainfrom
feat/match-wa-web-keepalive

Conversation

@jlucaso1

@jlucaso1 jlucaso1 commented Mar 14, 2026

Copy link
Copy Markdown
Collaborator

Summary

Aligns keepalive, dead-socket detection, and reconnection logic with real WhatsApp Web behavior, cross-referenced against captured JS modules (WAWebCommsConfig, WAComms, WAWebCommsHandleStanza).

Changes

Keepalive ping (src/keepalive.rs)

  • Lower interval floor from 20s to 15s — matches WA Web healthCheckInterval = 15, formula 15*(1+random()) = 15-30s
  • Skip ping when IQ responses are pending — matches WA Web activePing || pendingIqs.size guard
  • Skip ping when data was received recently (within 15s) — matches WA Web maybeScheduleHealthCheck event-driven rescheduling
  • RTT-aware clock skew from pong t field — matches WA Web Math.round((startTime + rtt/2) / 1000 - serverTime) via onClockSkewUpdate

Dead socket timer (src/keepalive.rs + src/client.rs)

  • Track last_data_sent_ms on every send_node — matches WA Web callStanza → deadSocketTimer.onOrBefore(deadSocketTime)
  • Track last_data_received_ms on every DataReceived — matches WA Web parseAndHandleStanza → deadSocketTimer.cancel()
  • Fire when data was sent but no reply arrived within 20s — matches WA Web deadSocketTime = 20_000 → softCloseSocket()
  • Action: reconnect_immediately() — matches WA Web socket loop reconnection

Fibonacci reconnect backoff (src/client.rs)

  • Replace linear backoff (0/2/4/.../30s) with Fibonacci (1/1/2/3/5/8/13/.../900s) with ±10% jitter
  • Matches WA Web { type: "fibonacci", first: 1000, second: 1000, jitter: 0.1, max: 9e5 }

Clock skew (src/unified_session.rs)

  • Add update_server_time_offset_with_rtt for RTT-adjusted midpoint calculation

WA Web modules referenced

  • WAWebCommsConfig (I7eq_pFgCZw.js) — healthCheckInterval, deadSocketTime, socketReconnectBackoffAlgo
  • WAComms (TSXjrkVar4k.js) — sendPing, maybeScheduleHealthCheck, deadSocketTimer, parseAndHandleStanza, callStanza
  • WAWebCommsHandleStanza (3vCxym4tMOo.js) — server ping response format

Test plan

  • 7 error classification tests (exhaustive IqError coverage)
  • 3 ms_since helper tests
  • 6 is_dead_socket tests (all edge cases: never sent, received after send, sent recently, stale no reply, old reply, recent reply)
  • 2 constants sanity tests (interval, dead socket time match WA Web values)
  • 3 fibonacci_backoff tests (sequence, max cap, first attempt)
  • 3 pong format tests (with id, without id, no-id ping smoke)
  • cargo fmt --all clean
  • cargo clippy --all --tests clean

Summary by CodeRabbit

  • New Features

    • Per-client last-send and last-receive timestamps for improved connection visibility.
    • RTT-based server time synchronization for more accurate clock offset.
  • Bug Fixes

    • Improved dead-socket detection with immediate reconnect when a socket is unresponsive.
    • Skip keepalive pings when recent activity or pending responses exist.
  • Performance

    • Replaced linear reconnect delay with Fibonacci-style backoff (with jitter and max cap).
  • Tests & Docs

    • Added unit tests and updated test comments and inline documentation.

Cross-referenced against WAWebCommsConfig, WAComms, and
WAWebCommsHandleStanza from captured WhatsApp Web JS bundle.
@coderabbitai

coderabbitai Bot commented Mar 14, 2026

Copy link
Copy Markdown

Warning

Rate limit exceeded

@jlucaso1 has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 10 minutes and 36 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: d08f069d-d7be-4940-bab2-6b6fd429de58

📥 Commits

Reviewing files that changed from the base of the PR and between f20fd65 and b94ffe5.

📒 Files selected for processing (2)
  • src/client.rs
  • tests/e2e/tests/chatstate_ttl.rs
📝 Walkthrough

Walkthrough

Adds per-client send/receive timestamps, replaces linear reconnect delay with a Fibonacci backoff (with jitter and cap), reworks keepalive to IQ-based RTT measurement with activity gating and dead-socket detection (20s), and adds an RTT-aware server time offset update method.

Changes

Cohort / File(s) Summary
Client Backoff & Activity Tracking
src/client.rs
Adds last_data_received_ms and last_data_sent_ms (pub(crate) Arc), initializes/resets them, updates timestamps on receive/send, introduces RECONNECT_BACKOFF_STEP and replaces linear reconnect delay with fibonacci_backoff(attempt) (jitter, 900s cap), and adds tests.
Keepalive & Dead-Socket Logic
src/keepalive.rs
Adds ms_since() and is_dead_socket() helpers; changes KEEP_ALIVE_INTERVAL_MIN to 15s and adds DEAD_SOCKET_TIME (20s); moves keepalive to IQ-based ping via send_iq measuring RTT; skips pings on recent activity or pending IQs; triggers immediate reconnect on dead socket; updates error handling and adds module tests.
Server Time Offset (RTT-aware)
src/unified_session.rs
Adds pub fn update_server_time_offset_with_rtt(&self, node: &Node, start_time_ms: i64, rtt_ms: i64) to compute/store server time offset using RTT midpoint calculation.
Tests & Docs
tests/e2e/tests/*, in-code docs
Updates test comments to reflect backoff-based timing; adds unit tests for fibonacci_backoff and keepalive helpers; adds inline doc comments describing backoff semantics and timing.

Sequence Diagram

sequenceDiagram
    participant Client
    participant Keepalive
    participant Server
    participant SessionMgr as UnifiedSessionManager

    loop keepalive loop
        Keepalive->>Keepalive: ms_since / is_dead_socket(last_sent,last_received)
        alt dead socket
            Keepalive->>Client: request reconnect (fibonacci_backoff)
        else recent activity
            Keepalive->>Keepalive: reset errors, skip ping
        else no recent activity
            Keepalive->>Server: send IQ keepalive (record start_time)
            Server-->>Keepalive: IQ response (contains node.t)
            Keepalive->>Keepalive: compute rtt_ms
            Keepalive->>SessionMgr: update_server_time_offset_with_rtt(node,start_time_ms,rtt_ms)
            SessionMgr->>SessionMgr: compute and store server_time_offset_ms
        end
    end

    note over Client,Keepalive: Data events update last_data_received_ms & last_data_sent_ms
Loading

Estimated Code Review Effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly Related PRs

Poem

🐰 I counted hops in Fibonacci rhyme,
Timed each ping and measured time,
Found the dead sockets, stitched the threads,
Midpoint clocks and jittered beds—
A carrot-cheering rabbit nods: well-timed! 🥕

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately captures the main changes: implementing WhatsApp Web-aligned keepalive, dead socket detection, and reconnect backoff behavior across multiple files.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/match-wa-web-keepalive
📝 Coding Plan
  • Generate coding plan for human review comments

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions

github-actions Bot commented Mar 14, 2026

Copy link
Copy Markdown

🐰 Bencher Report

Branchfeat/match-wa-web-keepalive
Testbedubuntu-latest
Click to view all benchmark results
BenchmarkInstructionsBenchmark Result
instructions
(Result Δ%)
Upper Boundary
instructions
(Limit %)
binary_benchmark::attr_parser_group::bench_attr_parser attr_lookup:setup_attr_marshaled()📈 view plot
🚷 view threshold
6,323.00
(-7.66%)Baseline: 6,847.81
7,190.20
(87.94%)
binary_benchmark::child_iteration_group::bench_get_children_by_tag📈 view plot
🚷 view threshold
850,843.00
(+0.01%)Baseline: 850,784.55
893,323.77
(95.24%)
binary_benchmark::jid_optimization_group::bench_jid_to_owned_access jid_access:setup_jid_heavy_marshaled()📈 view plot
🚷 view threshold
23,058.00
(-1.19%)Baseline: 23,336.08
24,502.89
(94.10%)
binary_benchmark::marshal_group::bench_marshal_allocating📈 view plot
🚷 view threshold
119,261.00
(-8.24%)Baseline: 129,967.59
136,465.97
(87.39%)
binary_benchmark::marshal_group::bench_marshal_auto_allocating📈 view plot
🚷 view threshold
119,289.00
(+0.04%)Baseline: 119,241.65
125,203.73
(95.28%)
binary_benchmark::marshal_group::bench_marshal_auto_huge_bytes_allocating📈 view plot
🚷 view threshold
534,036.00
(+0.00%)Baseline: 534,032.61
560,734.24
(95.24%)
binary_benchmark::marshal_group::bench_marshal_auto_long_string📈 view plot
🚷 view threshold
17,363.00
(+0.05%)Baseline: 17,354.57
18,222.30
(95.28%)
binary_benchmark::marshal_group::bench_marshal_auto_many_children_allocating📈 view plot
🚷 view threshold
17,139,029.00
(+0.02%)Baseline: 17,135,780.43
17,992,569.45
(95.26%)
binary_benchmark::marshal_group::bench_marshal_exact_allocating📈 view plot
🚷 view threshold
176,762.00
(+0.03%)Baseline: 176,714.65
185,550.38
(95.26%)
binary_benchmark::marshal_group::bench_marshal_exact_huge_bytes_allocating📈 view plot
🚷 view threshold
535,449.00
(+0.00%)Baseline: 535,445.61
562,217.89
(95.24%)
binary_benchmark::marshal_group::bench_marshal_exact_long_string📈 view plot
🚷 view threshold
19,417.00
(+0.04%)Baseline: 19,408.57
20,379.00
(95.28%)
binary_benchmark::marshal_group::bench_marshal_exact_many_children_allocating📈 view plot
🚷 view threshold
42,776,646.00
(+0.02%)Baseline: 42,770,179.20
44,908,688.16
(95.25%)
binary_benchmark::marshal_group::bench_marshal_huge_bytes_allocating📈 view plot
🚷 view threshold
534,475.00
(+0.00%)Baseline: 534,471.61
561,195.19
(95.24%)
binary_benchmark::marshal_group::bench_marshal_long_string📈 view plot
🚷 view threshold
17,336.00
(-4.60%)Baseline: 18,171.36
19,079.92
(90.86%)
binary_benchmark::marshal_group::bench_marshal_many_children_allocating📈 view plot
🚷 view threshold
17,139,912.00
(+0.02%)Baseline: 17,136,610.65
17,993,441.18
(95.26%)
binary_benchmark::marshal_group::bench_marshal_reusing_buffer📈 view plot
🚷 view threshold
129,162.00
(-3.41%)Baseline: 133,717.96
140,403.86
(91.99%)
binary_benchmark::marshal_group::bench_marshal_reusing_buffer_vec_writer📈 view plot
🚷 view threshold
119,361.00
(+0.04%)Baseline: 119,313.65
125,279.33
(95.28%)
binary_benchmark::roundtrip_group::bench_roundtrip large:setup_large_marshaled()📈 view plot
🚷 view threshold
94,389.00
(-5.94%)Baseline: 100,350.13
105,367.64
(89.58%)
binary_benchmark::roundtrip_group::bench_roundtrip small:setup_small_marshaled()📈 view plot
🚷 view threshold
7,378.00
(-6.76%)Baseline: 7,912.85
8,308.49
(88.80%)
binary_benchmark::roundtrip_group::bench_roundtrip_auto large:setup_large_marshaled()📈 view plot
🚷 view threshold
94,420.00
(+0.47%)Baseline: 93,982.13
98,681.24
(95.68%)
binary_benchmark::roundtrip_group::bench_roundtrip_auto small:setup_small_marshaled()📈 view plot
🚷 view threshold
7,401.00
(+1.14%)Baseline: 7,317.39
7,683.26
(96.33%)
binary_benchmark::roundtrip_group::bench_roundtrip_exact large:setup_large_marshaled()📈 view plot
🚷 view threshold
110,205.00
(+0.40%)Baseline: 109,767.13
115,255.49
(95.62%)
binary_benchmark::roundtrip_group::bench_roundtrip_exact small:setup_small_marshaled()📈 view plot
🚷 view threshold
8,913.00
(+0.95%)Baseline: 8,829.39
9,270.86
(96.14%)
binary_benchmark::unmarshal_group::bench_unmarshal large:setup_large_marshaled()📈 view plot
🚷 view threshold
45,476.00
(-4.84%)Baseline: 47,789.77
50,179.26
(90.63%)
binary_benchmark::unmarshal_group::bench_unmarshal small:setup_small_marshaled()📈 view plot
🚷 view threshold
2,717.00
(-7.85%)Baseline: 2,948.34
3,095.75
(87.77%)
binary_benchmark::unpack_group::bench_unpack_compressed📈 view plot
🚷 view threshold
556,092.00
(+4.66%)Baseline: 531,325.11
557,891.37
(99.68%)
binary_benchmark::unpack_group::bench_unpack_uncompressed📈 view plot
🚷 view threshold
771.00
(-0.56%)Baseline: 775.31
814.08
(94.71%)
libsignal_benchmark::conversation_group::bench_full_dm_conversation full:setup_conversation_data()📈 view plot
🚷 view threshold
27,682,736.00
(-0.16%)Baseline: 27,727,670.04
29,114,053.55
(95.08%)
libsignal_benchmark::dm_group::bench_dm_decrypt_first_message decrypt_prekey:setup_dm_with_first_message()📈 view plot
🚷 view threshold
5,540,322.00
(-0.23%)Baseline: 5,553,019.54
5,830,670.52
(95.02%)
libsignal_benchmark::dm_group::bench_dm_encrypt_first_message first_msg:setup_dm_session()📈 view plot
🚷 view threshold
178,094.00
(+0.04%)Baseline: 178,026.46
186,927.79
(95.27%)
libsignal_benchmark::dm_group::bench_dm_encrypt_subsequent_message subsequent:setup_established_dm_session()📈 view plot
🚷 view threshold
178,905.00
(+0.04%)Baseline: 178,838.46
187,780.39
(95.27%)
libsignal_benchmark::dm_group::bench_dm_session_establishment setup:setup_dm_users()📈 view plot
🚷 view threshold
17,274,621.00
(-0.06%)Baseline: 17,284,849.27
18,149,091.74
(95.18%)
libsignal_benchmark::group_messaging_group::bench_group_create_distribution_message create:setup_group_sender()📈 view plot
🚷 view threshold
295,884.00
(+0.02%)Baseline: 295,835.89
310,627.69
(95.25%)
libsignal_benchmark::group_messaging_group::bench_group_decrypt_message decrypt:setup_group_with_encrypted_message()📈 view plot
🚷 view threshold
12,534,411.00
(-0.53%)Baseline: 12,601,316.39
13,231,382.21
(94.73%)
libsignal_benchmark::group_messaging_group::bench_group_encrypt_message encrypt:setup_group_with_distribution()📈 view plot
🚷 view threshold
715,609.00
(-0.02%)Baseline: 715,741.49
751,528.57
(95.22%)
libsignal_benchmark::session_optimization_group::bench_decrypt_with_previous_session previous_session:setup_with_archived_sessions()📈 view plot
🚷 view threshold
41,823.00
(+0.04%)Baseline: 41,807.33
43,897.69
(95.27%)
libsignal_benchmark::session_optimization_group::bench_message_key_eviction eviction:setup_message_key_eviction()📈 view plot
🚷 view threshold
15,561,842.00
(+0.00%)Baseline: 15,561,656.88
16,339,739.73
(95.24%)
libsignal_benchmark::session_optimization_group::bench_out_of_order_decryption out_of_order:setup_out_of_order_messages()📈 view plot
🚷 view threshold
5,504,859.00
(-0.16%)Baseline: 5,513,465.24
5,789,138.50
(95.09%)
libsignal_benchmark::session_optimization_group::bench_promote_matching_session promote:setup_promote_matching_session()📈 view plot
🚷 view threshold
956,774.00
(-0.31%)Baseline: 959,725.08
1,007,711.34
(94.95%)
libsignal_benchmark::signature_group::bench_key_generation keygen📈 view plot
🚷 view threshold
2,822,723.00
(-0.01%)Baseline: 2,822,936.31
2,964,083.13
(95.23%)
libsignal_benchmark::signature_group::bench_signature_creation sign:setup_keypair_with_message()📈 view plot
🚷 view threshold
3,444,364.00
(-1.53%)Baseline: 3,497,993.17
3,672,892.83
(93.78%)
libsignal_benchmark::signature_group::bench_signature_verification verify:setup_keypair_with_message()📈 view plot
🚷 view threshold
126,900,985.00
(+1.14%)Baseline: 125,465,404.56
131,738,674.79
(96.33%)
reporting_token_benchmark::content_extraction_group::bench_content_extraction extended:setup_extended_message()📈 view plot
🚷 view threshold
11,812.00
(+0.15%)Baseline: 11,794.23
12,383.95
(95.38%)
reporting_token_benchmark::content_extraction_group::bench_content_extraction simple:setup_simple_message()📈 view plot
🚷 view threshold
3,825.00
(+0.05%)Baseline: 3,822.90
4,014.05
(95.29%)
reporting_token_benchmark::full_generation_group::bench_full_token_generation extended:setup_full_gen_extended()📈 view plot
🚷 view threshold
87,972.00
(+0.00%)Baseline: 87,971.06
92,369.61
(95.24%)
reporting_token_benchmark::full_generation_group::bench_full_token_generation simple:setup_full_gen_simple()📈 view plot
🚷 view threshold
80,008.00
(-0.02%)Baseline: 80,022.72
84,023.86
(95.22%)
reporting_token_benchmark::key_derivation_group::bench_key_derivation📈 view plot
🚷 view threshold
51,011.00
(-0.02%)Baseline: 51,023.45
53,574.62
(95.21%)
reporting_token_benchmark::message_encoding_group::bench_message_encoding extended:setup_extended_message()📈 view plot
🚷 view threshold
5,762.00
(+0.33%)Baseline: 5,742.82
6,029.96
(95.56%)
reporting_token_benchmark::message_encoding_group::bench_message_encoding simple:setup_simple_message()📈 view plot
🚷 view threshold
2,122.00
(+0.24%)Baseline: 2,117.01
2,222.86
(95.46%)
reporting_token_benchmark::token_calculation_group::bench_token_calculation📈 view plot
🚷 view threshold
21,920.00
(+0.02%)Baseline: 21,914.54
23,010.27
(95.26%)
🐰 View full continuous benchmarking report in Bencher

- Reset last_data_sent_ms and last_data_received_ms in
  cleanup_connection_state to prevent stale timestamps from
  triggering immediate dead-socket reconnect on next connection.
- Adjust reconnect() delay from fibonacci(2)=2s to fibonacci(4)=5s
  to maintain the offline window e2e tests depend on (old linear
  backoff was 4s).
The negative-assertion timeouts (3s) in offline receipt and presence
coalescing tests assumed the old linear backoff delay (~4s). With
fibonacci backoff, reconnect() produces ~2s delay (fibonacci(2)),
so B reconnects before the 3s check. Reduce timeouts to 1s.

Also reset dead-socket timestamps in cleanup_connection_state to
prevent stale values from triggering immediate reconnect.
…ndow

Replace magic number with a named constant (RECONNECT_BACKOFF_STEP=4)
so the reconnect() delay is self-documenting and tests can reason
about it. fibonacci_backoff(4) ≈ 5s, which is longer than the mock
server's CHATSTATE_TTL_SECS=3 so TTL-expiry tests pass.

Update all e2e test comments to reference the constant instead of
hardcoded timing assumptions.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
tests/e2e/tests/chatstate_ttl.rs (1)

17-21: ⚠️ Potential issue | 🟡 Minor

Inconsistent timing reference in doc comment.

Line 17 correctly states ~5s offline window (see RECONNECT_BACKOFF_STEP), but line 21 still references ~4s reconnect. With fibonacci_backoff(4) yielding ~5s, line 21 should be updated for consistency.

📝 Suggested fix
-/// Requires mock server with CHATSTATE_TTL_SECS=3 (so TTL expires before the ~4s reconnect).
+/// Requires mock server with CHATSTATE_TTL_SECS=3 (so TTL expires before the ~5s reconnect).
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/e2e/tests/chatstate_ttl.rs` around lines 17 - 21, Update the doc
comment to use a consistent timing reference: replace the "~4s reconnect"
wording with "~5s reconnect" (or reference
RECONNECT_BACKOFF_STEP/fibonacci_backoff(4)) so it matches the earlier "~5s
offline window (see RECONNECT_BACKOFF_STEP)" and the actual fibonacci_backoff(4)
result used in the test.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@tests/e2e/tests/chatstate_ttl.rs`:
- Around line 17-21: Update the doc comment to use a consistent timing
reference: replace the "~4s reconnect" wording with "~5s reconnect" (or
reference RECONNECT_BACKOFF_STEP/fibonacci_backoff(4)) so it matches the earlier
"~5s offline window (see RECONNECT_BACKOFF_STEP)" and the actual
fibonacci_backoff(4) result used in the test.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 205463c0-bb12-439f-8445-995d528ab919

📥 Commits

Reviewing files that changed from the base of the PR and between 5df5d35 and f20fd65.

📒 Files selected for processing (3)
  • src/client.rs
  • tests/e2e/tests/chatstate_ttl.rs
  • tests/e2e/tests/offline_receipts.rs

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant