Push updated state to clients over a socket/SSE instead of pure polling #129

Closed
opened 2026-09-22 19:41:49 +02:00 by kamilkrzywda · 1 comment
Owner

User request (2026-09-22): "clients should be connected over socket and updated state should be send there."

Current

Client polls /api/state; server is authoritative, sim ticks on the server (PLAN.md). Poll interval = render latency + needless load.

Proposed (lazy-first design)

One-way push via Server-Sent Events: new GET endpoint (e.g. /api/events) that streams a state snapshot immediately on connect and after each advancing tick; client subscribes with native EventSource and renders pushed snapshots, keeping polling as automatic fallback when the stream errors/disconnects. Actions stay REST POSTs — no bidirectional channel needed yet, so no raw-WebSocket server (Next.js App Router route handlers have no native WS upgrade).

If a true WebSocket is required later (multiplayer #101), that's a separate step with a custom server.

Scope notes

  • No change to the /api/state contract; e2e API-level tests keep using page.request
  • Client: replace/augment the poll loop in the state hook; reconnection + fallback logic
  • OpenAPI spec update for the new endpoint (lib/openapi.ts)

Process

Version 0.57.0 — after #128 (idle-slowdown) ships as 0.56.0; branch off master at that point.

Default decision

SSE over real WebSocket (one-way push, zero new deps). Say so if you want WS instead.

User request (2026-09-22): "clients should be connected over socket and updated state should be send there." ## Current Client polls `/api/state`; server is authoritative, sim ticks on the server (PLAN.md). Poll interval = render latency + needless load. ## Proposed (lazy-first design) One-way push via **Server-Sent Events**: new GET endpoint (e.g. `/api/events`) that streams a state snapshot immediately on connect and after each advancing tick; client subscribes with native `EventSource` and renders pushed snapshots, keeping polling as automatic fallback when the stream errors/disconnects. Actions stay REST POSTs — no bidirectional channel needed yet, so no raw-WebSocket server (Next.js App Router route handlers have no native WS upgrade). If a true WebSocket is required later (multiplayer #101), that's a separate step with a custom server. ## Scope notes - No change to the `/api/state` contract; e2e API-level tests keep using `page.request` - Client: replace/augment the poll loop in the state hook; reconnection + fallback logic - OpenAPI spec update for the new endpoint (`lib/openapi.ts`) ## Process Version 0.57.0 — after #128 (idle-slowdown) ships as 0.56.0; branch off master at that point. ## Default decision SSE over real WebSocket (one-way push, zero new deps). Say so if you want WS instead.
Author
Owner

Fixed in v0.57.0 (PR #131, merge commit 18e5c83).

Clients no longer rely on pure polling: new GET /api/events streams full game-state snapshots as Server-Sent Events — the current snapshot immediately on connect, then one after every sim tick (~1 s), so the UI updates push-driven even while idle. Polling remains a fully functional automatic fallback (5 s silence watchdog + stream-error trigger) and /api/state behavior is unchanged.

  • lib/sse.ts — tiny in-process fan-out registry (subscribe/broadcast, per-subscriber try/catch).
  • app/api/events/route.ts — SSE endpoint: snapshot-first-then-subscribe ordering, 15 s heartbeat, X-Accel-Buffering: no (prod sits behind Traefik), cleanup on cancel.
  • lib/sim/engine.ts — broadcast hook in tickNow() after each locked tick, serializing via the same getStateSnapshot() as /api/state, so pushed and polled state can never diverge.
  • Process-wide singletons moved to globalThis (SSE subscriber set, sql.js DB promise, tick lock) because Next.js gives instrumentation.ts and route handlers separate module instances — without this, background-tick pushes would never reach route-handler subscribers.
  • lib/useGameState.ts — EventSource primary with polling fallback; native reconnect, no custom logic.
  • OpenAPI spec gains GET /api/events; endpoint-list assertions move twelve → thirteen.

Tests: unit lib/sse.test.ts (3) + app/api/events/route.test.ts (1); e2e e2e/events.spec.ts — node-side fetch asserts content-type, first event parses as a snapshot, and ≥1 more event arrives with an advanced clock (370/370 unit, 61 e2e listed).

Deployed: https://flight.dev.kkhost.pl (/api/version = 0.57.0; /api/events verified streaming live data:-framed snapshots through Traefik).

Fixed in **v0.57.0** (PR #131, merge commit `18e5c83`). Clients no longer rely on pure polling: new `GET /api/events` streams full game-state snapshots as Server-Sent Events — the current snapshot immediately on connect, then one after every sim tick (~1 s), so the UI updates push-driven even while idle. Polling remains a fully functional automatic fallback (5 s silence watchdog + stream-error trigger) and `/api/state` behavior is unchanged. - `lib/sse.ts` — tiny in-process fan-out registry (`subscribe`/`broadcast`, per-subscriber try/catch). - `app/api/events/route.ts` — SSE endpoint: snapshot-first-then-subscribe ordering, 15 s heartbeat, `X-Accel-Buffering: no` (prod sits behind Traefik), cleanup on cancel. - `lib/sim/engine.ts` — broadcast hook in `tickNow()` after each locked tick, serializing via the same `getStateSnapshot()` as `/api/state`, so pushed and polled state can never diverge. - Process-wide singletons moved to `globalThis` (SSE subscriber set, sql.js DB promise, tick lock) because Next.js gives instrumentation.ts and route handlers separate module instances — without this, background-tick pushes would never reach route-handler subscribers. - `lib/useGameState.ts` — EventSource primary with polling fallback; native reconnect, no custom logic. - OpenAPI spec gains `GET /api/events`; endpoint-list assertions move twelve → thirteen. Tests: unit `lib/sse.test.ts` (3) + `app/api/events/route.test.ts` (1); e2e `e2e/events.spec.ts` — node-side fetch asserts content-type, first event parses as a snapshot, and ≥1 more event arrives with an advanced clock (370/370 unit, 61 e2e listed). Deployed: https://flight.dev.kkhost.pl (`/api/version` = 0.57.0; `/api/events` verified streaming live `data:`-framed snapshots through Traefik).
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: OpenCode/Flight-Ops#129