13 — State Management (WS / Realtime Module)
- 1.
RealtimeState(Bloc state) - 2. Event routing
- 3. Reconnect state machine (source-grounded)
- 4. Room membership state
- 5. Persistence
- 6. Testing strategy (client)
How the realtime layer plugs into client state management (Bloc). Shared conventions: 00-shared/06 (State Management). The module owns one
RealtimeBloc; everything else subscribes. Server-side state (rooms, sockets) is ephemeral and resettable by design — there is no persistence layer in source (gateway holds only live socket references).
1. RealtimeState (Bloc state)
| State | Meaning | Exposes |
|---|---|---|
RealtimeIdle | app start, never connected | — |
RealtimeConnecting | socket opening, handshake in flight | attempt # |
RealtimeConnected | handshake OK, in tenant room (ws.gateway.ts:50) | userId, tenantId (from auth store) |
RealtimeReconnecting | retrying after unexpected close | attempt #, nextBackoffMs |
RealtimeOffline | retries exhausted / transport dead | lastError, manual retry available |
RealtimeAuthError | handshake rejected (ws.gateway.ts:54-56) | needs token refresh |
Single cubit/bloc, app-scoped (DI singleton), mirrored by AppRealtimeStatusDot
(07 §1). No screen holds connection state locally — never a second source of truth.
2. Event routing
RealtimeClient.events (Stream<WsEnvelope>)
→ RealtimeBloc.router (pure function: envelope → typed app event)
→ feature blocs subscribe:
NotificationBloc ← notification.created
FeesDashboardBloc ← payment.completed
AttendanceBloc ← attendance.updated
HomeworkBloc ← homework.published
CommunicationBloc ← communication.* (planned)
AdminRealtimeBloc ← all (debug view, proposed)
Rules:
- Router is a pure map
eventType → AppEvent; unknown types → ignored (06§6 — additive protocol). - Feature blocs own their merge semantics via
AppLiveList(06§3.1);RealtimeBlocnever touches domain data. - Subscription teardown is explicit (bloc
close()cancels stream subs) — prevents fan-out leaks on screen navigation (14Q5).
3. Reconnect state machine (source-grounded)
Facts driving the machine: server verifies JWT on every connection (ws.gateway.ts:42-44);
disconnects silently on failure (ws.gateway.ts:54-56); tenant room is joined
automatically (ws.gateway.ts:50) but extra rooms die with the socket
(ws.gateway.ts:63-68) — so every successful reconnect re-runs: handshake → join →
re-subscribe rooms → reconcile.
stateDiagram-v2
[*] --> Idle
Idle --> Connecting : app start / manual retry
Connecting --> Connected : handshake ok (ws.gateway.ts:42-50)
Connecting --> AuthError : verify fails (ws.gateway.ts:54-56)
Connecting --> Reconnecting : transport error
Connected --> Reconnecting : heartbeat loss / close
Connected --> Disconnecting : user logout
Reconnecting --> Connecting : backoff delay elapsed
Reconnecting --> Offline : attempts > maxBackoffCap
Offline --> Connecting : "Retry now" / backoff timer
AuthError --> Connecting : token refreshed via REST
AuthError --> SignedOut : refresh failed (06 §1.2)
Disconnecting --> Idle : socket closed cleanly
Offline --> [*] : app destroyed
Notes:
- Backoff: exponential with jitter, 1 s → 30 s cap (
15§4); jitter avoids reconnect storms (14Q1). - On
ConnectedfromReconnecting: emitRealtimeReconnected→ subscribers run reconcile (re-subscribe rooms + one refetch per visible live screen,06§3.1).
4. Room membership state
| Room | Who manages | Source |
|---|---|---|
tenant:{tenantId} | server, automatic | ws.gateway.ts:50 |
| extra rooms | client, re-applied after each reconnect | ws.gateway.ts:63-68 |
RealtimeBloc keeps a Set<String> subscribedRooms; after every Connected, it replays
subscribe for each — this is the only piece of state that survives reconnects by design.
5. Persistence
- No socket state is persisted (no replay — fire-and-forget bridge,
ws-bridge.service.ts:16-22). - Token source: shared auth store (secure storage);
RealtimeBlocreads it at connect and after refresh — never stores its own copy (single source of truth per00-shared/06). - Last-envelope metadata (eventType, occurredAt) kept in-memory for the status sheet
(
05§4) — non-critical, dropped on restart.
6. Testing strategy (client)
RealtimeBlocunit tests with a fakeRealtimeClient(envelope-in → state-out).- Router tests: every mapped eventType → typed event; unknown → ignored.
- State machine tests: full reconnect matrix incl. jitter bounds, auth-error→refresh.
AppLiveListmerge tests: hit/miss/insert/refetch-bound (pure logic, no socket).