feat: task 2.4 WS client + rAF coalescer + Zustand position store

Eight files under src/live/:
- constants.ts: throughput-discipline numbers (MAX_TRAIL_LENGTH=200,
  reconnect backoff [1s/2s/4s/8s/16s, 30s ceiling], STALE_CONNECTION_MS).
- protocol.ts: zod discriminatedUnion('type', ...) for inbound
  (subscribed / unsubscribed / position / error). PositionEntry +
  SubscribeResult types per processor-ws-contract.
- connection-store.ts: Zustand store with status state machine.
- position-store.ts: Zustand store with latestByDevice + trailsByDevice
  Maps, applySnapshot/applyPositions/clearForEvent/selectDevice
  actions. Ring-buffer cap on trails; same-coordinate dedup.
- coalescer.ts: ~30-line rAF coalescer. Per-device buffer; flushes once
  per animation frame regardless of receive rate.
- ws-client.ts: state machine (idle / connecting / connected /
  reconnecting / closed) with exponential backoff, re-subscribe on
  reconnect, stale-connection check, subscribe correlation via
  pending-map + 5s timeout. URL resolution helper toAbsoluteWsUrl()
  for same-origin path inputs.
- bootstrap.tsx: <LiveBootstrap> creates the client when authenticated,
  wires positions through the coalescer to the position store, tears
  down on logout. getLiveClient() exposes the singleton for 2.7.
- index.ts: barrel re-exports.

main.tsx wraps <App /> in <LiveBootstrap> alongside <AuthBootstrap>.

Deviations:
1. Skipped the setStatus helper inside createLiveClient; conditional
   Parameters<> generics were hostile. Direct
   useConnectionStore.getState().setStatus(...) at the ~6 call sites.
2. subscribe() adds to the subscriptions Set even when not connected
   (so it replays on reconnect). Caller handles 'not-connected' by
   waiting for connection-store status transition.
3. onPosition returns an unsubscribe fn (Set-based). Multi-handler is
   free; lets future debug panels/tests attach.

Bundle: src/live/ adds ~15KB raw to the main bundle (mostly zod's
discriminated-union runtime). Total 393KB / 120KB gz.
This commit is contained in:
2026-05-02 21:42:43 +02:00
parent c18026e5d8
commit 218d6b9c00
12 changed files with 678 additions and 4 deletions
+1 -1
View File
@@ -51,7 +51,7 @@ When Phase 2 is done:
| 2.1 | [MapView singleton + mapReady gate](./01-mapview-singleton.md) | 🟩 |
| 2.2 | [Tile-source switcher](./02-tile-source-switcher.md) | 🟩 |
| 2.3 | [Sprite preload (racing categories)](./03-sprite-preload.md) | 🟩 |
| 2.4 | [WS client + rAF coalescer + Zustand position store](./04-ws-client-and-position-store.md) | |
| 2.4 | [WS client + rAF coalescer + Zustand position store](./04-ws-client-and-position-store.md) | 🟩 |
| 2.5 | [MapPositions (clustered + selected sources)](./05-map-positions.md) | ⬜ |
| 2.6 | [MapTrails (bounded ring buffer, polyline rendering)](./06-map-trails.md) | ⬜ |
| 2.7 | [Event picker (subscription driver)](./07-event-picker.md) | ⬜ |