## Added - **WebSocket Protocol Specification** (docs/PROTOCOL.md) - Complete reference for all 20+ events with payload schemas, rate limits, and edge cases - **LEAVE_ROOM Rate Limiting** - 10 requests/socket/minute to prevent abuse - **Vitest Testing Framework** - Modern test setup with coverage reporting - **Host Control Mode Documentation** - Consolidated EN documentation (docs/host-control-mode.md) with references to internal implementation ## Changed - **Graceful Shutdown** - Socket.IO clients properly disconnected during server shutdown (server/index.js) - **CI Workflow** - Added test step with continue-on-error for gradual rollout - **README** - Added protocol documentation link - **CHANGELOG** - Updated with all new features and improvements ## Moved - Internal documentation moved to docs/internal/ for better organization - Host Control Mode docs consolidated from 4 files to 1 comprehensive guide ## Technical Details - Protocol spec: 495 lines, covers all events from shared/constants.js - Rate limiter: Follows existing pattern (checkAuthRate, checkEventRate) - Vitest: 6 tests for rate limiter, all passing - Documentation: Host Control Mode doc includes edge cases, testing checklist, architecture decisions
6.7 KiB
Co-Host (Multi-Controller) — Implementation Plan
Branch base: feature/host-control-mode (builds directly on it).
Goal: let the room owner grant playback control to several peers (co-hosts), not
just one — e.g. 4 of N people in a room may drive play/pause/seek, the rest are guests.
This is the second server-gated feature the capabilities hook was designed for
(CAPABILITIES.CO_HOST = 'co-host', already stubbed in shared/constants.js).
1. Roles
| Role | Can drive (play/pause/seek/force-sync/episode-lobby) | Can promote/demote + toggle mode |
|---|---|---|
| Owner (room creator, = today's "host") | yes (always a controller) | yes |
| Controller (co-host) | yes (in host-only mode) |
no |
| Guest | only in everyone mode |
no |
The single-host feature is just the special case controllers = { owner }.
2. Data model
Server (room object)
ownerPeerId— the creator / manager. KeephostPeerIdas an alias (= ownerPeerId) so older clients keep working.controllers: Set<peerId>— peers allowed to drive. Always contains ownerPeerId.controlMode: 'everyone' | 'host-only'— unchanged wire values ('host-only'now means "restricted to controllers", not "single host").MAX_CONTROLLERScap (e.g. 10) to bound the set + payload.
Shared constants
CAPABILITIES.CO_HOST = 'co-host'(un-stub it) → add toSERVER_CAPABILITIES.- New events:
SET_PEER_ROLE(client→server):{ peerId, controller: boolean }— owner promotes/demotes.- Extend
CONTROL_MODE(server→client) payload:{ controlMode, ownerPeerId, hostPeerId, controllers: [peerId...] }.
ROOM_DATAgainsownerPeerId+controllers.
3. Gate generalization (the core change)
Today the gate compares against a single hostPeerId. Generalize to set membership:
- Server relay gate (
server/index.js):controlMode === 'host-only' && !room.controllers.has(mapping.peerId)→ drop. (Wasmapping.peerId !== room.hostPeerId.) - Background gates (sender + receiver): replace
amHost()/senderId !== hostPeerIdwith controller-set membership:controllers.includes(myPeerId)/senderId ∈ controllers. - Helpers: split
amHost()intoamOwner()(manage rights) andamController()(drive rights). The desync/snap-back path keys on!amController()instead of!amHost().
SET_PEER_ROLE handler (server): validate sender is owner, target is a current peer in the
room, enforce MAX_CONTROLLERS, always keep owner in the set, then broadcast CONTROL_MODE
with the new controllers.
4. Client + UI
- Owner sees the peer list with a per-peer "Controller" toggle (promote/demote) plus the existing mode toggle.
- Controllers see a "Controller" badge and are NOT locked out of the remote-control buttons.
- Guests see "Guest" + the host-only notice (unchanged).
- The promote UI + co-host badges render only when the relay advertises the
co-hostcapability (feature detection, same pattern ashostControlSupported). - i18n: new keys (
ROLE_CONTROLLER,BTN_PROMOTE,BTN_DEMOTE, …) across all locales.
5. Backwards compatibility
- New client + old server (host-control only, no
co-hostcapability): no co-host UI; behaves as today's single-host. ✓ - Old client + new server: ignores
controllers/SET_PEER_ROLE. An old client that the owner promotes still gates itself (its sender-gate only knows!amHost), so it can't drive — it degrades to a guest. Co-host requires a client that understandscontrollers. Document this; not a crash. ✓ - No
PROTOCOL_VERSIONbump needed — purely additive, same as host-control.
6. Edge cases
- Controller leaves →
removePeerFromRoomalso doesroom.controllers.delete(peerId). - Owner leaves → fallback: promote the earliest remaining controller to owner (prefer
a controller over a random peer); if none, earliest peer; keep the rest of the set. Reuse
the
peerJoinLocksguard so a reconnect/second-tab doesn't demote (same fix as host). - Promote a peer not in the room → server rejects (target must be a live peer).
- Promote beyond
MAX_CONTROLLERS→ server rejects, re-syncs the owner's UI. everyonemode → thecontrollersset is still maintained (so flipping tohost-onlykeeps the chosen co-hosts), it just isn't enforced while ineveryone.- peerId spoofing → unchanged accepted limitation (see
docs/KNOWN_LIMITATIONS.md); co-host doesn't widen it materially (still bounded to a temporary room).
7. Scale: the "4 of 510 people" part — read this
The role change above is moderate. Putting 510 people in one room is a separate, larger problem and should be its own track:
MAX_PEERS_PER_ROOMis 25 today. 510 needs a large raise + load testing.- The real bottleneck at scale is heartbeat fan-out, not control events. Every peer heartbeats and the relay broadcasts each to all peers → O(N²) per interval. At 510 that's ~510×509 / 15s ≈ 17k msg/s just for heartbeats — the scaling wall.
- Co-host actually helps the control-event side: in
host-onlymode only the few controllers emit play/pause/seek, so event sources drop from N to K (e.g. 4). Restricting who can drive is synergistic with big rooms. - Large rooms therefore need (independent of co-host):
- Heartbeat fan-out reduction — e.g. only relay controller/owner heartbeats to everyone, relay guest heartbeats only to the owner/controllers (for the UI), or server-side aggregation into periodic snapshots instead of per-peer relay.
ROOM_DATApayload trimming — a 510-entry peer list is large; send counts + controller details, lazy-load the full roster.- Possibly the socket.io Redis adapter for horizontal scaling, and broadcast tuning.
8. Effort estimate
- Co-host roles (server gate generalization +
SET_PEER_ROLE+ owner-leave fallback + client gates + promote UI + i18n), at the current ≤25-peer scale: ~3–4 dev days (same shape as host-control itself — mostly generalizing host→controller-set). - Large-room scaling (510): a separate ~1–2 week track (heartbeat redesign + payload trimming + cap raise + load testing), independent of co-host. Recommend shipping co-host at the current cap first, then scaling rooms as its own project.
9. Suggested sequencing
CAPABILITIES.CO_HOST+controllers/ownerPeerIdin room state +ROOM_DATA(additive).- Server
SET_PEER_ROLE+ gate generalization + owner-leave fallback + WS tests. - Background: controller-set membership in both gates +
amOwner/amController. - Popup: promote/demote toggles (owner) + Controller badge + i18n.
- (Separate track) large-room scaling.