- Move KNOWN_LIMITATIONS.md → docs/KNOWN_LIMITATIONS.md (+ fix SECURITY.md link). - WS test: in default 'everyone' mode a non-host guest can still drive play/pause/seek/force-sync/episode-lobby — proves host-control OFF == unchanged behavior. - docs/host-control-mode-COHOST-PLAN.md: plan for multi-controller (owner grants drive rights to several co-hosts), built on the capabilities hook; covers the gate generalization, roles, backwards-compat, edge cases, and a separate large-room (510-peer) scaling track. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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.