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:
@@ -156,10 +156,11 @@ For 2.3, just include it in the registry and it'll be there when 2.5 needs it.
|
||||
|
||||
**Deviations from spec:**
|
||||
|
||||
1. Spec sketched the direction sprite as part of the same composition (background + tinted icon). Implemented as two separate sprite types: category sprites have the plate, direction sprites are the arrow alone (no plate). Reason: the direction sprite is rendered as a *separate* symbol layer in 2.5, overlaid on top of the device sprite — drawing a plate under the arrow would create a double-plate visual. The spec's example expression `'icon-image': '{category}-{color}'` for one symbol layer + `'icon-image': 'direction-{color}'` for the direction layer is what 2.5 will actually consume.
|
||||
1. Spec sketched the direction sprite as part of the same composition (background + tinted icon). Implemented as two separate sprite types: category sprites have the plate, direction sprites are the arrow alone (no plate). Reason: the direction sprite is rendered as a _separate_ symbol layer in 2.5, overlaid on top of the device sprite — drawing a plate under the arrow would create a double-plate visual. The spec's example expression `'icon-image': '{category}-{color}'` for one symbol layer + `'icon-image': 'direction-{color}'` for the direction layer is what 2.5 will actually consume.
|
||||
2. Spec showed sample colours as `success: 'green'`. Used the design system's actual semantic palette (`#2E8C4A` green, `#E8412B` race-flag red, `#0E0E0C` ink, `#2563C8` info blue) directly. When 3.8 lands, these get rebound to TRM design tokens via CSS variables.
|
||||
|
||||
**Smoke check (local `pnpm dev`):**
|
||||
|
||||
- App boots; `getSpriteRegistry().size` returns `32` after the page settles.
|
||||
- `/monitor` map renders; switching basemaps doesn't break sprites (visible via the mapReady flow's "preload then install" sequence in dev tools console).
|
||||
- No "Image with id X is missing" warnings.
|
||||
|
||||
Reference in New Issue
Block a user