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.
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.
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.
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).
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
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 nativeEventSourceand 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
/api/statecontract; e2e API-level tests keep usingpage.requestlib/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.
Fixed in v0.57.0 (PR #131, merge commit
18e5c83).Clients no longer rely on pure polling: new
GET /api/eventsstreams 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/statebehavior 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 intickNow()after each locked tick, serializing via the samegetStateSnapshot()as/api/state, so pushed and polled state can never diverge.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.GET /api/events; endpoint-list assertions move twelve → thirteen.Tests: unit
lib/sse.test.ts(3) +app/api/events/route.test.ts(1); e2ee2e/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/eventsverified streaming livedata:-framed snapshots through Traefik).