Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

15 — Flutter Implementation Guide (WS / Realtime Module)

How to implement the realtime client in Flutter. Shared architecture: 00-shared/11 (Flutter App Architecture). State machine it must implement: 13.


1. Transport choice — read this first (honest note)

The server is socket.io (ws.gateway.ts:10-11), which speaks the engine.io handshake + protocol, not plain RFC 6455 WebSocket. Two options:

OptionPackageFit
A — recommendedsocket_io_clientFull protocol parity: engine.io handshake, long-polling fallback, built-in reconnect, auth.token field, namespaces (/ws)
Bweb_socket_channel (raw WS)Only works if the server runs with transports: ['websocket'] only and the client reproduces the engine.io sid handshake dance — fragile; not recommended for production

This guide uses web_socket_channel as requested, but the raw-WebSocket path must be validated against the engine.io upgrade flow before adoption; if parity is not achievable, switch to option A — the RealtimeClient abstraction (07 §6) makes the swap a one-file change.

2. Connection setup

final uri = Uri.parse(
  '$WS_SCHEME://$WS_HOST/ws/socket.io/?token=$token'   // query.token path (ws.gateway.ts:37-39)
);
final channel = IOWebSocketChannel.connect(uri);
  • Prefer the query token with web_socket_channel (no auth map available on raw sockets; socket_io_client can use auth: {'token': ...} instead — both accepted by source: ws.gateway.ts:37-39).
  • WS_SCHEME = wss in prod; namespace path /ws (ws.gateway.ts:20-23).
  • Token from the secure auth store; never log the URI (contains token, 10 §8).
  • Verify failure server-side = close (ws.gateway.ts:54-56) — treat any early done during handshake as AuthError until proven otherwise (13 §1).

3. Envelope handling

class WsEnvelope {
  final String eventType;
  final DateTime occurredAt;
  final Map<String, dynamic> payload;
  // validate exactly these 3 keys — anything else = protocol violation (06 §6)
}

Server emits {eventType, occurredAt, payload} (ws-bridge.service.ts:17-21). Route via the pure RealtimeBloc router (13 §2). Ignore unknown eventTypes.

4. Auto-reconnect with backoff (state machine in 13 §3)

Future<void> _runReconnectLoop() async {
  var attempt = 0;
  while (_shouldRetry) {
    await Future<void>.delayed(backoffDelay(attempt)); // 1s * 2^attempt, cap 30s
    final ok = await _connectOnce();
    if (ok) return;
    attempt++;
    if (attempt >= 5 && !await _refreshToken()) {      // auth on reconnect (Q2)
      _emit(RealtimeAuthError()); return;               // → sign-out path (06 §1.2)
    }
  }
  _emit(RealtimeOffline());                             // manual "Retry now" resumes
}

Duration backoffDelay(int n) {
  final base = min(1 << n, 30) * 1.seconds;             // exponential, cap 30s
  return base + Random().nextInt(250).milliseconds;     // jitter → no storms (Q1)
}

Rules:

  • On every successful reconnect: re-subscribe extra rooms (rooms die with the socket, ws.gateway.ts:63-68) then run one reconcile refetch per visible live screen (06 §3.1).
  • Token expiry check before reconnecting (06 §6); refresh via REST when needed.

5. Heartbeat

  • Server side runs socket.io ping/pong (defaults; untuned, G6). On raw WS, you must detect silent half-open loss yourself: no frame (incl. pong) for pingTimeout+grace → close channel → Reconnecting. IOWebSocketChannel does not heartbeat — add a Timer watchdog if using option B (14 Q4).

6. Bloc integration

class RealtimeBloc extends Cubit<RealtimeState> {
  RealtimeBloc(this._client) {
    _sub = _client.events.listen(_route);   // envelope → typed app events (13 §2)
    _client.state.listen(emit);
  }
  // close(): cancel _sub — no fan-out leaks after navigation (Q5)
}

Feature blocs subscribe to router output only; AppLiveList (07 §3) owns merge semantics; never mutate lists during active edits (10 §2).

7. Offline UX wiring

  • AppRealtimeStatusDot + AppConnectionBanner bind to RealtimeBloc.state (11 §3); banner only for offline/authError; live-region announcements (00-shared/09).
  • Status sheet (proposed) reads last-envelope metadata held by the bloc (13 §5).

8. Security checklist

  • Token only from secure storage; URI built at runtime (never in code/logs).
  • No payload content in logs (10 §8); debug view redacts (09 §5).
  • Validate envelope shape before routing (14 Q8); cap payload size.
  • Reject/ignore any server message that isn't a valid envelope.

9. Testing

  • RealtimeClient behind a fake for widget tests (Q2, Q5, Q7).
  • Backoff unit test: delays bounded, jittered, no duplicates (Q1).
  • Integration: socket_io_client against the real e2e server (npm run test:e2e, 14 test-surface table).