Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion advanced/websocket-handling.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -1072,7 +1072,7 @@ The watchdog is anchored to `SessionStats::first_send_since_recv_ms` — the **f

The dead-socket check runs on **every** keepalive tick — not just after a failed ping. This catches scenarios where pending IQs caused the ping to be skipped, or where the ping "succeeded" but the connection died immediately after. When a dead socket is detected, the client calls `reconnect_immediately()` and exits the keepalive loop.

WA Web's `deadSocketTimer.onOrBefore` (`WA/Shift/Timer.js`) arms on the first `callStanza` after a receive and is cancelled by `parseAndHandleStanza`; subsequent sends never push the deadline back out. The keepalive loop approximates this by checking `is_dead_socket(first_send_since_recv, last_recv)` unconditionally each iteration, where `first_send_since_recv` is the armed-anchor value described above (not `last_data_sent_ms`, which still tracks the most recent send for telemetry).
WA Web's `deadSocketTimer.onOrBefore` (`WA/Shift/Timer.js`) arms on the first `callStanza` after a receive and is cancelled by `parseAndHandleStanza`; subsequent sends never push the deadline back out. The keepalive loop approximates this by checking `is_dead_socket_at(first_send_since_recv, last_recv, now)` unconditionally each iteration, where `first_send_since_recv` is the armed-anchor value described above. The tick reads the clock once into `now` and evaluates both the dead-socket check and the elapsed-time log message against that single instant, rather than re-reading the clock for each. There is no `last_data_sent_ms` field — nothing reads a "most recent send" timestamp, only the armed anchor.

### Error classification

Expand Down
7 changes: 5 additions & 2 deletions api/client.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -1981,10 +1981,13 @@ Cumulative wire I/O and activity counters for this client session, always record
| `reconnects` | `u64` | Reconnect attempts started by the auto-reconnect loop |
| `reconnect_errors` | `u32` | Consecutive reconnect failures (resets on success) |
| `resends_throttled` | `u64` | Outbound resends dropped by the per-chat rate limiter — surfaces storm chats |
| `last_data_sent_ms` | `u64` | Timestamp (ms since UNIX epoch) of the last sent frame |
| `last_data_received_ms` | `u64` | Timestamp (ms since UNIX epoch) of the last received data |

Most counters are monotonic over the client's lifetime and survive reconnects. Two fields are the exceptions: the two timestamps reset on connection teardown, and `reconnect_errors` itself resets to `0` on every successful reconnect (it counts *consecutive* failures, not a lifetime total).
Most counters are monotonic over the client's lifetime and survive reconnects. `last_data_received_ms` is the exception: it resets on connection teardown. `reconnect_errors` also resets, to `0` on every successful reconnect (it counts *consecutive* failures, not a lifetime total).

<Note>
**Breaking**: `last_data_sent_ms` was removed — nothing internal ever read it, and stamping it cost a clock read on every frame written (the client's hottest path, and a call out of the module on wasm32/embedded targets). `frames_sent` answers "is it still sending?"; there is no drop-in replacement for "when did I last write?" — an embedder that needs that timestamp should stamp it at its own send call site rather than have the wire path pay for it.
</Note>

**Example:**

Expand Down
2 changes: 1 addition & 1 deletion concepts/architecture.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -384,7 +384,7 @@ See [WebSocket & Noise Protocol - Message loop](/advanced/websocket-handling#2-m

### Keepalive loop

The keepalive loop runs as a **separate spawned task**, fully decoupled from the read loop. This ensures keepalive pings are never blocked by frame processing — even during large offline sync batches that take seconds to drain. The two loops communicate solely through atomic timestamps (`last_data_received_ms`, `last_data_sent_ms`, and `first_send_since_recv_ms` — the dead-socket watchdog anchor).
The keepalive loop runs as a **separate spawned task**, fully decoupled from the read loop. This ensures keepalive pings are never blocked by frame processing — even during large offline sync batches that take seconds to drain. The two loops communicate solely through atomic timestamps (`last_data_received_ms` and `first_send_since_recv_ms` — the dead-socket watchdog anchor). There is no "last send" timestamp: nothing reads one, and stamping every frame written would cost a clock read on the client's hottest path.

```rust
const KEEP_ALIVE_INTERVAL_MIN: Duration = Duration::from_secs(15);
Expand Down