## 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
10 KiB
WebSocket Protocol Specification
Overview
KoalaSync uses WebSocket for real-time communication between clients and the relay server. The protocol is based on Socket.IO events with a custom message format.
- Transport: WebSocket only (no long-polling fallback)
- Server URL:
wss://syncserver.koalastuff.net(official) or custom server - Protocol Version:
1.0.0(current),MIN_VERSIONnot enforced (backward compatible) - Authentication: Optional server token for custom relays
Protocol Basics
Message Format
All messages follow the Socket.IO v4 protocol format:
- Engine.IO packet type
42(WEBSOCKET_MESSAGE) - JSON-encoded array:
[eventName, payload]
Example: 42["play",{"currentTime":123.45}]
Event Flow
Client → Server: JOIN_ROOM, PLAY, PAUSE, SEEK, etc.
Server → Client: ROOM_DATA, PEER_STATUS, CONTROL_MODE, etc.
Client ↔ Server: PING/PONG (latency measurement)
Events Reference
Connection & Room Management
JOIN_ROOM (Client → Server)
Join an existing room or create a new one.
Payload:
{
"roomId": "string (alphanum + hyphens, 64 max)",
"peerId": "string (16 chars max)",
"username": "string (30 chars max)",
"password": "string (128 chars max, optional)",
"tabTitle": "string (100 chars max, optional)",
"mediaTitle": "string (100 chars max, optional)",
"protocolVersion": "string (16 chars max)"
}
Server Response:
- Success:
ROOM_DATAevent with room state - Error:
ERRORevent with message
Rate Limit: 10 attempts per IP per minute
Edge Cases:
- Room ID sanitization (alphanumeric + hyphens only)
- Peer ID deduplication (kicks old socket for same peerId)
- Protocol version mismatch → disconnect
LEAVE_ROOM (Client → Server)
Leave the current room.
Payload: None
Server Action:
- Removes peer from room
- Broadcasts
PEER_STATUSwithstatus: 'left'to remaining peers - If room becomes empty, deletes room
Rate Limit: 10 requests per socket per minute
Edge Cases:
- Host leaving → controlMode falls back to 'everyone'
- Last peer leaving → room deletion
ROOM_DATA (Server → Client)
Current room state snapshot sent on join or when room changes.
Payload:
{
"roomId": "string",
"peers": [
{
"peerId": "string",
"username": "string",
"tabTitle": "string",
"mediaTitle": "string",
"playbackState": "playing" | "paused",
"currentTime": "number",
"volume": "number",
"muted": "boolean",
"lastHeartbeat": "timestamp"
}
],
"activeLobby": "object | null",
"hostPeerId": "string | null",
"controlMode": "everyone" | "host-only",
"controllers": ["string"],
"capabilities": ["string"]
}
Triggered by:
- Successful JOIN_ROOM
- Room state changes (peer join/leave, mode change)
ERROR (Server → Client)
Error notification.
Payload:
{
"message": "string"
}
Common Errors:
- "Incompatible protocol version"
- "Invalid password"
- "Room full"
- "Server capacity reached"
Media Control Events
PLAY (Client → Server → Client)
Resume playback for all peers in the room.
Payload:
{
"currentTime": "number (seconds)"
}
Broadcast: Relayed to all peers in room
Gated in host-only mode: Only controllers can initiate
PAUSE (Client → Server → Client)
Pause playback for all peers in the room.
Payload:
{
"currentTime": "number (seconds)"
}
Broadcast: Relayed to all peers in room
Gated in host-only mode: Only controllers can initiate
SEEK (Client → Server → Client)
Seek to specific time in the media.
Payload:
{
"targetTime": "number (seconds)"
}
Broadcast: Relayed to all peers in room
Gated in host-only mode: Only controllers can initiate
Edge Cases:
- Minimum seek delta (0.5s) to avoid spam
- Coalesced with subsequent seeks within 200ms
Sync Coordination
PEER_STATUS (Client → Server → Client)
Heartbeat with current peer state.
Payload:
{
"peerId": "string",
"username": "string",
"tabTitle": "string",
"mediaTitle": "string",
"playbackState": "playing" | "paused",
"currentTime": "number",
"volume": "number",
"muted": "boolean",
"status": "joined" | "left" | "heartbeat"
}
Frequency: Every 15 seconds (HEARTBEAT_INTERVAL)
Purpose:
- Keep-alive
- State synchronization
- Peer presence tracking
FORCE_SYNC_PREPARE (Client → Server → Client)
Initiate force sync sequence.
Payload:
{
"targetTime": "number (seconds)"
}
Sequence:
- Initiator sends PREPARE
- Server broadcasts PREPARE to all peers
- Peers respond with FORCE_SYNC_ACK
- Initiator sends FORCE_SYNC_EXECUTE after timeout or when all ACKs received
- Server broadcasts EXECUTE to all peers
Timeout: 8.5 seconds (FORCE_SYNC_TIMEOUT)
Gated in host-only mode: Only controllers can initiate
FORCE_SYNC_ACK (Client → Server)
Acknowledgment of force sync preparation.
Payload: None
Purpose: Let initiator know peer is ready
FORCE_SYNC_EXECUTE (Client → Server → Client)
Execute the force sync (seek + play).
Payload:
{
"targetTime": "number (seconds)"
}
Broadcast: Relayed to all peers
Effect: All peers seek to targetTime and play
Episode Auto-Sync
EPISODE_LOBBY (Client → Server → Client)
Wait for all peers to load the next episode.
Payload:
{
"episodeId": "string",
"targetTime": "number (seconds, usually 0)"
}
Sequence:
- Initiator sends EPISODE_LOBBY
- Server broadcasts to all peers
- Peers load episode and send EPISODE_READY when paused at targetTime
- When all peers ready or timeout, initiator sends EPISODE_LOBBY_CANCEL or room resumes
Timeout: 60 seconds (EPISODE_LOBBY_TIMEOUT)
Gated in host-only mode: Only controllers can initiate
EPISODE_READY (Client → Server)
Peer is ready for episode sync.
Payload:
{
"episodeId": "string"
}
EPISODE_LOBBY_CANCEL (Client → Server → Client)
Cancel active episode lobby and resume playback.
Payload: None
Host Control Mode
SET_CONTROL_MODE (Client → Server)
Host changes room control mode.
Payload:
{
"controlMode": "everyone" | "host-only"
}
Authorization: Only host (room.hostPeerId) can send
Server Action:
- Validates sender is host
- Updates room.controlMode
- Broadcasts CONTROL_MODE to all peers
Rate Limit: 500ms debounce per room
CONTROL_MODE (Server → Client)
Control mode or role changed.
Payload:
{
"controlMode": "everyone" | "host-only",
"hostPeerId": "string",
"controllers": ["string"]
}
Triggered by:
- SET_CONTROL_MODE from host
- Host leaving room (fallback to 'everyone')
- Controller promotion/demotion
SET_PEER_ROLE (Client → Server)
Owner promotes/demotes a peer to/from controller.
Payload:
{
"peerId": "string",
"controller": "boolean"
}
Authorization: Only owner (room.hostPeerId) can send
Server Action:
- Updates room.controllers set
- Broadcasts CONTROL_MODE to all peers
Rate Limit: 500ms debounce per room
Ping / Latency Measurement
PING (Client → Server → Client)
Measure round-trip time.
Payload:
{
"t": "timestamp (Date.now())",
"target": "peerId (optional, empty = server echo)"
}
Server Response: PONG with same timestamp
Frequency: Every 30 seconds
PONG (Server → Client)
Response to PING.
Payload:
{
"t": "timestamp (from PING)"
}
Administrative Events
GET_ROOMS (Client → Server)
Request list of active rooms (admin only).
Payload: None
Authorization: Requires admin token
Rate Limit: 10 requests per IP per minute
Server Response: ROOM_LIST event
ROOM_LIST (Server → Client)
List of active rooms.
Payload:
{
"rooms": [
{
"roomId": "string",
"peerCount": "number",
"createdAt": "timestamp",
"lastActivity": "timestamp"
}
]
}
Rate Limits
Connection Rate Limits
- Connections: 10 per IP per minute
- Authentication Attempts: 5 per IP per room per minute
- Event Rate: 50 events per 10 seconds per socket
Specific Event Rate Limits
- Health Checks: 10 per IP per minute
- Admin Metrics Auth: 5 per IP per minute
- Room List: 10 per IP per minute (cooldown)
- LEAVE_ROOM: 10 per socket per minute
Debounce Intervals
- Control Mode Changes: 500ms per room
- Role Changes: 500ms per room
Server Capabilities
The server advertises supported features in ROOM_DATA.capabilities:
host-control: Host Control Mode featureco-host: Co-host/promotion feature
Clients should check capabilities before using features.
Protocol Versioning
- PROTOCOL_VERSION: "1.0.0" (current)
- Backward Compatibility: Older clients can connect but may not support all features
- Version Check: Server validates protocolVersion on JOIN_ROOM
Edge Cases & Error Handling
Connection Issues
- Protocol Mismatch: Client disconnected with ERROR message
- Rate Limit Exceeded: Socket disconnected immediately
- Server Restart: Clients auto-reconnect with exponential backoff
Room Management
- Host Leaves: controlMode falls back to 'everyone'
- Room Full: Error response on JOIN_ROOM
- Duplicate PeerId: Old socket kicked on new join
Media Sync
- Seek Spam: Coalesced within 200ms window
- Force Sync Timeout: Auto-executes after 8.5s if not all peers ACK
- Episode Lobby Timeout: Auto-cancels after 60s
Host Control Mode
- Non-host Attempts: Event ignored, CONTROL_MODE sent to sync UI
- Host-only Gating: play/pause/seek/forceSync/episodeLobby blocked for guests
- Desync Handling: Guests can opt-out via dialog, resync button available
Security Considerations
- No Authentication: Trust model is client-enforced (no tokens)
- Rate Limiting: Prevents abuse and DoS
- Input Sanitization: All strings truncated and validated
- No Sensitive Data: Only public room/peer metadata transmitted
Testing Recommendations
- Connection Flow: Join, leave, reconnect, room creation
- Media Sync: Play/pause/seek propagation, force sync sequence
- Host Control: Mode toggle, guest blocking, desync/resync
- Rate Limits: Verify limits enforced, errors logged
- Edge Cases: Host leave, network blips, seek spam
Changelog
- 1.0.0: Initial protocol specification (2026)
- Documented all events from shared/constants.js
- Added rate limit details and edge cases