M2: WebSocket hub — hello, replay, broadcast, heartbeat, revoke (ENG-254) #17

Merged demi.dev
targeting main from stoopkidddd/eng-254-m2-websocket-hub-hello-replay-broadcast-heartbeat-revoke
Merged opened by and assigned to demi.dev
targeting main from stoopkidddd/eng-254-m2-websocket-hub-hello-replay-broadcast-heartbeat-revoke

Linear: https://linear.app/engineereddev/issue/ENG-254

What's in this PR:

  • ws/hub.ts: GET /api/ws upgrade on the same HTTP server (ws). hello within WS_HELLO_TIMEOUT_MS or 4401; the token is checked through the device registry's hash map (a revoked token fails at once); ready; replay from since (oldest first, ≤ 200) then replay_done; resync when since is unknown or > 200 behind; since: null → no replay. Any other frame, invalid JSON or a repeated hello → 4400.
  • Broadcast of push, push_deleted, device_added, device_updated, device_removed to every authenticated socket. On revoke, the event goes to the others first, then the device's sockets close with 4401.
  • Heartbeat: app-level ping every WS_PING_SECONDS; two missed pongs → terminate. Backpressure: bufferedAmount > 1 MB → terminate.
  • Presence: open-socket counts per device feed DeviceDTO.online (devices list, /api/me, events), with device_updated debounced 2 s.
  • Shutdown hook closes every socket with 1012. ws/timers.ts provides injected timers for fast tests.

Verify (lead, on 4f2605b, based on 555a737):

  • pnpm install --frozen-lockfile → exit 0
  • mise run check:server → exit 0 (Tests 215 passed (215), including 16 real-socket scenarios)
  • mise run e2e:server → exit 0 (14 tests; bundled runtime with the WebSocket on MinIO)
  • No pneumo-e2e-* containers left.

Deviations:

  • A repeated hello closes with 4400 (the spec says "any frame other than hello/pong after hello"; a second hello is treated as invalid).
  • Termination happens after two complete missed pong windows.

🤖 Generated with Claude Code

Commits 0