WebSockets

Real-time updates (opponent moves, presence) use API Gateway WebSockets.

Key files

Mounted from Skeleton while the app runs.

Connection flow

  1. Obtain JWT via getAuthToken().
  2. Open WebSocket to WS_ENDPOINT from config.
  3. On open, send subscribe with token, invisible, watchVersion: 1, and optional games (the current desired watch list).
  4. Server stores the connection (including watchingGames when games is provided), sends a presence snapshot, and debounces a join broadcast.
  5. GameWatch keeps desiredWatchGames in Zustand and sends watchGames whenever the list changes, with retries at 250ms and 1s after connect or tab return.

Backend flow: WebSockets.

Game subscriptions

useGameWatch builds the watch set from:

When the set changes, the client sends watchGames with the full desired list. The server only delivers game events to matching subscribers.

Resync: scheduleWatchGamesSync() sends watchGames immediately and retries at 250ms and 1s. It runs on connect, when the watch list changes, and when the tab becomes visible while already connected. Pending retries are cancelled on disconnect.

On game messages, MyWebSocket dispatches:

Reconnection

Exponential backoff on disconnect:

Presence

Incoming connections messages use snapshot/delta format:

// snapshot (subscribe, syncPresence)
{ type: "snapshot", seq, totalCount, visibleUserIds }

// delta (debounced join/leave)
{ type: "delta", seq, joins, leaves }

applyPresenceMessage updates Zustand connections (totalCount, visibleUserIds, seq):

Periodic resync: every 10 minutes while the tab is visible and the socket is open, send syncPresence (also on tab becoming visible). This caps drift from missed deltas.

Users can set invisible to opt out of visible presence (stored in Zustand).

Related