mirror of
https://github.com/Shik3i/KoalaSync.git
synced 2026-08-08 10:23:19 +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
496 lines
10 KiB
Markdown
496 lines
10 KiB
Markdown
# 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_VERSION` not 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:**
|
|
```json
|
|
{
|
|
"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_DATA` event with room state
|
|
- Error: `ERROR` event 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_STATUS` with `status: '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:**
|
|
```json
|
|
{
|
|
"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:**
|
|
```json
|
|
{
|
|
"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:**
|
|
```json
|
|
{
|
|
"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:**
|
|
```json
|
|
{
|
|
"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:**
|
|
```json
|
|
{
|
|
"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:**
|
|
```json
|
|
{
|
|
"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:**
|
|
```json
|
|
{
|
|
"targetTime": "number (seconds)"
|
|
}
|
|
```
|
|
|
|
**Sequence:**
|
|
1. Initiator sends PREPARE
|
|
2. Server broadcasts PREPARE to all peers
|
|
3. Peers respond with FORCE_SYNC_ACK
|
|
4. Initiator sends FORCE_SYNC_EXECUTE after timeout or when all ACKs received
|
|
5. 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:**
|
|
```json
|
|
{
|
|
"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:**
|
|
```json
|
|
{
|
|
"episodeId": "string",
|
|
"targetTime": "number (seconds, usually 0)"
|
|
}
|
|
```
|
|
|
|
**Sequence:**
|
|
1. Initiator sends EPISODE_LOBBY
|
|
2. Server broadcasts to all peers
|
|
3. Peers load episode and send EPISODE_READY when paused at targetTime
|
|
4. 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:**
|
|
```json
|
|
{
|
|
"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:**
|
|
```json
|
|
{
|
|
"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:**
|
|
```json
|
|
{
|
|
"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:**
|
|
```json
|
|
{
|
|
"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:**
|
|
```json
|
|
{
|
|
"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:**
|
|
```json
|
|
{
|
|
"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:**
|
|
```json
|
|
{
|
|
"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 feature
|
|
- `co-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
|
|
|
|
1. **Connection Flow**: Join, leave, reconnect, room creation
|
|
2. **Media Sync**: Play/pause/seek propagation, force sync sequence
|
|
3. **Host Control**: Mode toggle, guest blocking, desync/resync
|
|
4. **Rate Limits**: Verify limits enforced, errors logged
|
|
5. **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
|