- ARCHITECTURE.md: startup -> lazy connect, reconnect when in room - HOW_IT_WORKS.md: on-demand connection, heartbeat while connected - PRIVACY.md: clarify alarms only during active sessions - SYNC_GUIDE.md: build-extension.cjs + episode-utils injection - StoreDescription.md: on-demand relay, no persistent connection - README.md + extension/README.md: npm run build:extension - extension/README.md: replace Dual Heartbeat with On-Demand Connection - website/privacy.html: add lazy-connect privacy note
7.8 KiB
KoalaSync Architecture
This document describes the communication flows and internal logic of the KoalaSync system.
1. Extension Connection (Lazy Connect)
- Initialization: On startup,
background.jsreads settings (Server URL, Username, Last Room) fromchrome.storage.sync. No WebSocket connection is established at this point. - On-Demand Connection: The extension only connects when needed — either the user opens the popup with saved room credentials, or when actively in a room. When not in a room, no connection exists. This improves privacy (IP not exposed while idle) and reduces battery/network usage.
- WebSocket Handshake (when connecting):
- Background creates a
new WebSocketto/socket.io/?EIO=4&transport=websocket&version=1.0.0. - Server performs security checks:
- IP Rate Limit: Checks if the IP has exceeded connection limits.
- Protocol Version: Client must match the server's protocol (currently
1.0.0).
- Server responds with Engine.IO handshake (
0) and the client joins the namespace (40).
- Background creates a
- Room Join: Background emits
JOIN_ROOMcontainingroomId,password,peerId, andusername. - Deduplication: If a user joins with a
peerIdthat already has an active socket, the server kills the old socket to prevent "Ghost Peers". Deduplication re-validates after acquiring the room creation lock to avoid kicking the wrong socket during concurrent joins.
2. Media Event Synchronization
When a user interacts with a video:
- Detection:
content.jslistens to native events (play,pause,seeked) on the<video>element, including videos inside Shadow DOM (YouTube, Netflix, etc.). - Prevention of Loops: Uses an
expectedEventsSet to distinguish between user actions and programmatic actions. Expected events are consumed on match and expire via timeout. Timeout IDs are cleaned up immediately to prevent memory leaks. - Reporting:
content.jssends aCONTENT_EVENTtobackground.js. - Relay: The Server forwards the event to all other peers in the room.
- Execution: Remote peers receive the command via
SERVER_COMMAND(which includes the originalsenderIdfor correct ACK routing) and callvideo.play(),video.pause(), orvideo.currentTime = targetTime. - ACK Routing:
content.jsechoes thecommandSenderIdback inCMD_ACK, ensuring theEVENT_ACKis routed to the correct initiating peer even when multiple commands arrive concurrently.
3. Two-Phase Force Sync
Ensures all peers are buffered and synchronized before resuming:
- Prepare: Initiator sends
FORCE_SYNC_PREPAREwith the target timestamp. - Buffer: Peers seek and pause. Once buffered (
readyState >= 3), they send aFORCE_SYNC_ACK. (Note:content.jslimits polling to 8000ms). - Execute: Once the Initiator collects ACKs (or after an 8.5s timeout), they send
FORCE_SYNC_EXECUTE.Important
Network Transit Buffer Rule: The orchestrator (
background.js) must always use a timeout at least 500ms longer than the worker (content.js) to account for IPC and network transit time. Never align them exactly 1:1, as this will introduce a race condition on slow connections. - Resume: All peers call
play()simultaneously.
4. Episode Auto-Sync
Maintains continuous synchronized viewing when watching series:
- Detection:
content.jsmonitors the Media Session API for title changes. - Lobby Creation: When a new title is detected, the peer initiates an
EPISODE_LOBBYand broadcasts the new title. - Wait State: All peers freeze their video until they have also loaded the exact same title.
- Mid-Lobby Joins: If a new user joins the room during an active lobby, the lobby initiator broadcasts the active lobby state so the newcomer can sync up.
- Resume: Once all peers report
EPISODE_READY, the lobby is resolved and playback resumes perfectly.
5. Peer Lifecycle & Dual Heartbeat
To maintain a clean room state and eliminate "Ghost Peers":
- Session Heartbeat (Background): Every 30 seconds,
background.jssends an "I'm alive" signal to the server. This keeps you in the room even if no video is playing. - Video Heartbeat (Content): Every 15 seconds,
content.jssends current playback metadata (time, title, state) if a video is found. - Server Pruning: The server runs a "Reaper" every 2 minutes. If a peer has sent zero activity (no events and no heartbeats) for 5 minutes, they are forcefully disconnected.
- Immediate Cleanup: Rooms are deleted instantly when the last peer leaves or disconnects.
- Reconnect Strategy (while in room): Aggressive backoff — 500ms base, 1.5x multiplier, capped at 5s. Max 20 attempts before marking as failed. Events are queued during disconnect and flushed after namespace rejoin. When not in a room, no reconnection occurs.
Caution
Identity Rule: Differentiate between
peerIdandsocket.id. Usesocket.idexclusively for ephemeral transport routing on the server. UsepeerIdexclusively for identity, state management, and room tracking across the stack.
6. Broadcast Protocol & Routing
KoalaSync uses a megaphone routing approach to minimize server logic:
emit()Broadcast Behavior: Anyemit()from the extension client is unconditionally broadcast to all other peers in the room. It is not a direct message.- Storm Prevention: When dispatching state updates in response to a new user joining (e.g., an active lobby state), ensure ONLY the initiator (or a designated leader) calls
emit()to preventO(N)broadcast storms.
7. Security & Stability
- Service Worker Lifecycle: Uses
chrome.alarms(30s interval) to prevent the Manifest V3 service worker from suspending while in an active room. On wake, runtime state is restored fromchrome.storage.sessionviaensureState(). - Reconnect Visualization: Badge shows "..." (orange) during reconnect. Popup displays "Reconnecting..." with attempt counter.
- Rate Limiting: Server-side per-socket and per-IP rate limits to prevent sync-spamming or simple DoS. Public health endpoints are limited to 10 requests/minute/IP and cached server-side for 60 seconds, wrong admin-metrics bearer attempts to 5 requests/minute/IP, and room discovery to one request every 10 seconds per socket. Real client IP is taken from the trusted reverse proxy hop, so the Node port must stay private behind Caddy or another trusted proxy.
- Room Creation Lock: Per-room mutex prevents race conditions when multiple peers join a new room simultaneously.
- CORS: Allows
chrome-extension://origins for WebSocket fallback compatibility. - Message Buffer:
maxHttpBufferSizeset to 4KB to accommodate largeJOIN_ROOMpayloads. - Process Guards:
uncaughtExceptionandunhandledRejectionhandlers prevent silent server crashes. - Noise Filtering: Uses a curated blacklist of domains (Search Engines, Social Media) to declutter the "Target Tab" selector in the popup.
- Diagnostics: A "Dev" tab provides real-time access to the underlying
<video>state (readyState,paused,currentTime) for easier troubleshooting.
8. Constant Synchronization & Consistency
To maintain a "Single Source of Truth" across the server and extension without using a bundler:
- Relay Server & Extension Modules:
background.jsandpopup.jsimport constants directly fromshared/constants.js. - Content Scripts: To ensure zero-latency execution,
content.jsuses a synchronized copy ofEVENTSand constants. - Automation: The
npm run build:extensionscript automatically injectsEVENTS,HEARTBEAT_INTERVAL, andepisode-utils.jsfunctions intocontent.jsduring the build process, eliminating the risk of manual mirror mismatch. - Verification: Any protocol change is automatically propagated across the stack by running the build script.