mirror of
https://github.com/Shik3i/KoalaSync.git
synced 2026-08-09 10:49:37 +00:00
463f871958
## 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
287 lines
9.0 KiB
Markdown
287 lines
9.0 KiB
Markdown
# Host Control Mode
|
|
|
|
## Overview
|
|
|
|
Host Control Mode allows the room host to control playback for all participants. Guests can attempt to play/pause/seek, but their actions are not broadcast to the room unless they explicitly choose to desync and watch on their own.
|
|
|
|
## Modes
|
|
|
|
### `everyone` (Default)
|
|
- Anyone in the room can control playback
|
|
- All play/pause/seek actions are broadcast to all peers
|
|
- Traditional KoalaSync behavior
|
|
|
|
### `host-only`
|
|
- Only the host (and promoted controllers) can control the room
|
|
- Guest actions are blocked and they are snapped back to the host's position
|
|
- Guests can choose to desync and watch independently
|
|
|
|
## Protocol Events
|
|
|
|
### `SET_CONTROL_MODE` (Client → Server)
|
|
|
|
Sent by the host to change the room's control mode.
|
|
|
|
**Payload:**
|
|
```json
|
|
{
|
|
"controlMode": "everyone" | "host-only"
|
|
}
|
|
```
|
|
|
|
**Authorization:** Only the host (room.hostPeerId) can send this event.
|
|
|
|
**Server Response:** Broadcasts `CONTROL_MODE` to all peers in the room.
|
|
|
|
### `CONTROL_MODE` (Server → Client)
|
|
|
|
Broadcast when the control mode changes or when a peer joins a room.
|
|
|
|
**Payload:**
|
|
```json
|
|
{
|
|
"controlMode": "everyone" | "host-only",
|
|
"hostPeerId": "string",
|
|
"controllers": ["string"]
|
|
}
|
|
```
|
|
|
|
**Triggered by:**
|
|
- Host changing the control mode
|
|
- Host leaving the room (fallback to 'everyone')
|
|
- Controller promotion/demotion
|
|
|
|
### `SET_PEER_ROLE` (Client → Server)
|
|
|
|
Sent by the host to promote or demote a peer to/from controller status.
|
|
|
|
**Payload:**
|
|
```json
|
|
{
|
|
"peerId": "string",
|
|
"controller": "boolean"
|
|
}
|
|
```
|
|
|
|
**Authorization:** Only the host (room.hostPeerId) can send this event.
|
|
|
|
**Server Response:** Broadcasts `CONTROL_MODE` to all peers in the room.
|
|
|
|
## Server-Side Implementation
|
|
|
|
### Room Object
|
|
|
|
```javascript
|
|
room = {
|
|
hostPeerId: "peer-id", // First peer to join the room
|
|
controlMode: "everyone", // 'everyone' | 'host-only'
|
|
controllers: ["peer-id"], // Peers allowed to control in host-only mode
|
|
lastControlModeChangeAt: 0, // Timestamp for rate limiting
|
|
lastRoleChangeAt: 0 // Timestamp for rate limiting
|
|
}
|
|
```
|
|
|
|
### Key Behaviors
|
|
|
|
1. **Host Migration:** When the host leaves, the room falls back to 'everyone' mode
|
|
2. **Controller Set:** Always includes the host, plus any promoted peers
|
|
3. **Rate Limiting:** Control mode changes are debounced to 500ms per room
|
|
|
|
## Client-Side Implementation
|
|
|
|
### State Management (background.js)
|
|
|
|
```javascript
|
|
let controlMode = CONTROL_MODES.EVERYONE;
|
|
let hostPeerId = null;
|
|
let controllers = [];
|
|
|
|
function amHost() { return hostPeerId === peerId; }
|
|
function amController() { return amHost() || controllers.includes(peerId); }
|
|
```
|
|
|
|
### Event Gating
|
|
|
|
**Sender-side gate (background.js):**
|
|
- Blocks `PLAY`, `PAUSE`, `SEEK`, `FORCE_SYNC_*`, `EPISODE_LOBBY_*` from non-controllers
|
|
- Sends `HOST_BLOCKED` message to content script instead
|
|
|
|
**Receiver-side gate (background.js):**
|
|
- Ignores gated events from non-controllers in host-only mode
|
|
- Prevents malicious or outdated clients from bypassing controls
|
|
|
|
### Snap-Back Logic (content.js)
|
|
|
|
When a guest's action is blocked:
|
|
1. Show dialog: "Stay in sync" (default) or "Watch on my own"
|
|
2. If "Stay in sync": snap player back to host's position
|
|
3. If "Watch on my own": set `hcmDesynced = true`, show resync button
|
|
|
|
### Host Sync Target Calculation
|
|
|
|
```javascript
|
|
function getHostSyncTarget() {
|
|
const host = currentRoom.peers.find(p => p.peerId === hostPeerId);
|
|
if (!host) return null;
|
|
|
|
let targetTime = host.currentTime;
|
|
|
|
// Extrapolate if host is playing
|
|
if (host.playbackState === 'playing' && host.lastHeartbeat) {
|
|
const elapsedSec = (Date.now() - host.lastHeartbeat) / 1000;
|
|
if (elapsedSec > 0 && elapsedSec <= 30) { // Max 30s extrapolation
|
|
targetTime += elapsedSec;
|
|
}
|
|
}
|
|
|
|
return { playbackState: host.playbackState, targetTime };
|
|
}
|
|
```
|
|
|
|
## Edge Cases
|
|
|
|
### 1. Host Leaves Room
|
|
- **Behavior:** Room falls back to 'everyone' mode
|
|
- **Implementation:** `removePeerFromRoom` checks if leaving peer is host
|
|
- **Broadcast:** `CONTROL_MODE` event sent to all remaining peers
|
|
|
|
### 2. Controller Promoted During Force Sync
|
|
- **Problem:** Controller's `FORCE_SYNC_EXECUTE` would be blocked
|
|
- **Solution:** `forceSyncInitiator` field tracks who started the sync
|
|
- **Behavior:** Initiator's EXECUTE is allowed even if demoted mid-sync
|
|
|
|
### 3. Guest Seek Spam
|
|
- **Problem:** Guest could spam seeks to disrupt
|
|
- **Solution:** Cooldown period after snap-back (1000ms)
|
|
- **Behavior:** Subsequent actions within cooldown are ignored
|
|
|
|
### 4. Host State Stale
|
|
- **Problem:** Host heartbeat hasn't arrived recently
|
|
- **Solution:** Max 30s extrapolation, then use last known position
|
|
- **Behavior:** Prevents overshooting if host paused but heartbeat delayed
|
|
|
|
### 5. Old Client Without Feature
|
|
- **Problem:** Client doesn't know about host-only mode
|
|
- **Solution:** Receiver-side gate in all clients
|
|
- **Behavior:** Modern clients ignore events from non-controllers
|
|
|
|
## User Experience
|
|
|
|
### Host UI
|
|
- Toggle: "Only I can control" (visible only to host)
|
|
- Role badges: "Host", "Controller", or "Guest"
|
|
- Guest notice: "The host controls playback" (when host-only active)
|
|
- Controller management: Promote/demote peers in participant list
|
|
|
|
### Guest UI
|
|
- **Blocked Action:** Dialog with choices:
|
|
- "Stay in sync" (default, auto-closes after 8s)
|
|
- "Watch on my own" (enters desync mode)
|
|
- **Desync Mode:** "Solo" badge with "Resync" button
|
|
- **Resync:** Returns to synchronized state with host
|
|
|
|
### Co-Host UI
|
|
- Same controls as host (can play/pause/seek/force-sync)
|
|
- "Controller" badge instead of "Host"
|
|
- Cannot promote/demote other controllers
|
|
|
|
## Testing Checklist
|
|
|
|
### Basic Functionality
|
|
1. ✅ Host can toggle between 'everyone' and 'host-only'
|
|
2. ✅ Guests see "host controls playback" notice in host-only
|
|
3. ✅ Guest play/pause/seek blocked in host-only
|
|
4. ✅ Guest snapped back to host position when blocked
|
|
5. ✅ Guest can choose "watch on my own" and desync
|
|
6. ✅ Desynced guest can resync with host
|
|
|
|
### Edge Cases
|
|
7. ✅ Host leaving room falls back to 'everyone'
|
|
8. ✅ Multiple rapid guest actions don't cause loop
|
|
9. ✅ Guest can desync during buffering/throttling
|
|
10. ✅ Old client events ignored by modern clients
|
|
11. ✅ Force sync initiated by controller works
|
|
12. ✅ Episode lobby initiated by controller works
|
|
|
|
### UI/UX
|
|
13. ✅ Host toggle only visible to host
|
|
14. ✅ Role badges show correct roles
|
|
15. ✅ Desync dialog auto-closes after timeout
|
|
16. ✅ Resync button visible when desynced
|
|
17. ✅ Controller promotion UI visible to host
|
|
|
|
## Architecture Decisions
|
|
|
|
### Why Double Gating?
|
|
**Sender-side + Receiver-side gates provide defense in depth:**
|
|
- Sender-side: Clean UX with dialog for guests
|
|
- Receiver-side: Protects against old/buggy/malicious clients
|
|
- Server-side: Central enforcement point (optional but recommended)
|
|
|
|
### Why Extrapolation?
|
|
**Linear extrapolation from last heartbeat provides ~±1s accuracy:**
|
|
- Better than freezing at last known position
|
|
- Handles minor network jitter gracefully
|
|
- Capped at 30s to avoid overshooting on stale data
|
|
|
|
### Why Cooldown?
|
|
**600-1000ms cooldown prevents control loops:**
|
|
- Guest pause → snap-back → pause → snap-back...
|
|
- Allows legitimate desync after cooldown expires
|
|
- Doesn't block deliberate user actions
|
|
|
|
## Migration Path
|
|
|
|
### From 'everyone' to 'host-only'
|
|
1. Host toggles mode to 'host-only'
|
|
2. Server validates host authority
|
|
3. Server broadcasts `CONTROL_MODE` to all peers
|
|
4. All clients update local state
|
|
5. Existing playback continues uninterrupted
|
|
6. Future guest actions are gated
|
|
|
|
### From 'host-only' to 'everyone'
|
|
1. Host toggles mode to 'everyone'
|
|
2. Server validates host authority
|
|
3. Server broadcasts `CONTROL_MODE` to all peers
|
|
4. All clients update local state
|
|
5. All peers regain control immediately
|
|
|
|
## Performance Considerations
|
|
|
|
- **Memory:** Minimal overhead (~100 bytes per room for HCM state)
|
|
- **CPU:** Snap-back calculation is O(1), negligible impact
|
|
- **Network:** No additional traffic beyond existing heartbeats
|
|
- **Storage:** Control mode persisted in room state, no additional storage
|
|
|
|
## Security Considerations
|
|
|
|
- **No Authentication:** Trust model is client-enforced (no tokens)
|
|
- **No Encryption:** Feature doesn't handle sensitive data
|
|
- **Rate Limiting:** Control mode changes debounced to prevent spam
|
|
- **Validation:** All mode changes validated server-side
|
|
|
|
## Future Enhancements
|
|
|
|
### Planned
|
|
- Host transfer button (manual host migration)
|
|
- Temporary controller promotion (time-limited)
|
|
- Guest request control (notification to host)
|
|
|
|
### Considered but Rejected
|
|
- Password-protected host transfer (too complex)
|
|
- Vote-based control mode (social complexity)
|
|
- Per-action permissions (UI overload)
|
|
|
|
## Changelog
|
|
|
|
- **2.5.0**: Initial implementation (July 2026)
|
|
- **2.5.1**: Added co-host support and controller promotion
|
|
- **2.5.2**: Improved desync UI with resync button
|
|
- **2.6.0**: Added server-side rate limiting for mode changes
|
|
|
|
## See Also
|
|
|
|
- [Protocol Specification](PROTOCOL.md) — Technical event details
|
|
- [Architecture](ARCHITECTURE.md) — System overview
|
|
- [Internal Documentation](internal/host-control-mode-plan.md) — Original implementation plan (German) |