Commit Graph

1 Commits

Author SHA1 Message Date
K. O. A.
03e1c1d57e
feat(server): session-sync relay WebSocket /ws/sync/{session_id} (#1030) (#1032)
Some checks are pending
ship-ci / ci (push) Waiting to run
* feat(server): add session-sync relay WebSocket /ws/sync/{session_id}

Cross-device followers (splitscreen's upcoming LAN pop-out mode,
feedBack-plugin-splitscreen#21) need a machine-crossing replacement for
BroadcastChannel — the one link in the follower architecture that cannot
leave the host browser. Chart data already streams per-client over
/ws/highway, so all that's missing is a dumb live-state channel.

Add a fan-out room endpoint: a JSON text frame from one client is relayed
verbatim to every other client on the same session id. No schema, no
history, no persistence — rooms are created on first join and GC'd when
the last socket leaves. The statelessness is deliberate: an idle room is
indistinguishable from a nonexistent one, and a host that crashes and
rejoins the same id resumes publishing to reconnecting subscribers with
no server-side coordination.

Caps for a LAN-exposable port: 16 KB frames (1009), 16 sockets/room and
32 rooms (1013), 120 msg/s sustained / 240 burst per socket (1008),
text-only (1003), session id validated against [A-Za-z0-9_-]{4,64}. An
over-limit socket is closed individually; a peer that dies mid-fan-out
is dropped without wedging delivery to the rest.

Closes #1030

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Kris Anderson <topkoa@gmail.com>

* fix(ws_sync): bound stalled peer sends; cap ws frames at the transport

Review feedback (CodeRabbit on #1032):

- A peer that stops draining its socket left send_text() pending forever;
  since publishers await the fan-out gather, one stalled peer stalled
  every publisher's receive loop behind it. Fan-out sends are now bounded
  by SEND_TIMEOUT_SECONDS (5 s) so a stall becomes an eviction through
  the existing failed-send drop path.

- uvicorn buffers inbound WS frames up to its 16 MB default before the
  handler's 16 KB check ever runs, so the DoS bound wasn't enforced at
  the transport. main.py now passes ws_max_size=64 KB (no client sends
  large frames: the highway WS receives only small control messages, and
  the relay keeps its tighter application cap as the primary limit).

Regression tests for both; the desktop's own uvicorn spawn gets the
matching --ws-max-size flag with the feedBack-desktop follow-up work.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Kris Anderson <topkoa@gmail.com>

---------

Signed-off-by: Kris Anderson <topkoa@gmail.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-22 15:45:56 -04:00