WebSocket Session Lifecycle
chronicle intermediate 5 min read
ELI5
Each WebSocket connection is a person in a soundproof booth. The relay only un-mutes their headset once they’ve shown ID (authenticated) AND signed into a specific room (document_joined). Until both flags flip true, broadcasts arrive at the booth but are not played.
Technical Deep Dive
SessionState
SessionState (relay/src/handlers/websocket.rs) holds per-connection mutable state behind Arc<Mutex<...>>:
| Field | Set when |
|---|---|
connection_id | At upgrade |
document_id | From URL path /ws/documents/:id |
user_id, user_name, session_id | After auth_result success |
color | From WsParams.color (or hashed default after auth) |
authenticated | Flipped by handshake |
document_joined | Flipped by JoinDocument |
is_authenticated() requires both authenticated == true AND user_id.is_some().
Channels
On upgrade the connection subscribes to two tokio::sync::broadcast channels obtained from AppState:
document_channel(document_id)— buffer 1024, used for operations + sync.presence_channel(document_id)— buffer 256, used for cursor + join/leave.
A separate mpsc::channel::<Vec<u8>>(32) (response_tx) carries direct replies (auth_result, joined_document, snapshots). The send task multiplexes all three with tokio::select!.
Lifecycle
stateDiagram-v2 [*] --> Upgraded: ws.on_upgrade Upgraded --> Pending: register Connection (Connecting) Pending --> Authenticated: authenticate ok Pending --> [*]: authenticate fail / drop Authenticated --> Joined: join_document Joined --> Authenticated: leave_document Authenticated --> [*]: socket close Joined --> [*]: socket close note right of Joined broadcasts (doc_rx, presence_rx) delivered only here end noteSend-Path Filter
sequenceDiagram autonumber participant Doc as broadcast(document_channel) participant Pres as broadcast(presence_channel) participant Resp as mpsc(response_tx) participant Send as send_task participant Client par Direct response Resp->>Send: bytes Send->>Client: Text(bytes) and Document broadcast Doc->>Send: bytes Send->>Send: lock SessionState alt is_authenticated && document_joined Send->>Client: Binary(bytes) else Send->>Send: drop end and Presence broadcast Pres->>Send: bytes Send->>Send: lock SessionState alt is_authenticated && document_joined Send->>Client: Binary(bytes) end endCleanup
When the socket closes (either side), ConnectionManager::unregister_connection removes the connection and decrements active stats; the presence service should also leave the document but that is performed at the message layer.
Key Terms
- send_task → spawned
tokio::select!loop that owns the WebSocket sender half; the only place authoritative auth/joined gating happens. - doc_rx / presence_rx → broadcast subscriber receivers; their backpressure semantics are “lag = drop oldest”, which is acceptable because clients can
sync_requestto recover. - response_tx → why direct responses (e.g.
auth_result) bypass the broadcast filter — they are addressed to one connection and sent beforedocument_joinedis true.
Q&A
Q: Why split direct responses onto an mpsc channel instead of broadcasting them?
A: Replies like auth_result must reach the client before any gating flag is set; routing them through the broadcast filter would deadlock the handshake.
Q: What is the message format on the broadcast channels — text or binary?
A: The send task sends them as Message::Binary. Direct responses go out as Message::Text (UTF-8 JSON).
Q: Is the Connection registered before or after authentication?
A: Before — registered in Connecting state immediately after upgrade so peak_concurrent_connections is accurate.
Examples
A subtle bug class: a client that authenticates but forgets join_document will see ping/pong work and auth_result arrive, but every operation broadcast silently disappears — the gate is doing exactly its job.
neighbors on the map
- Auth-First WebSocket Handshake debugging AUTH_REQUIRED errors on the relay
- Presence Broadcast Channels diagnosing missing cursor updates in the editor
- GRACE Session Structure & Context Lifecycle starting a new GRACE session