Files
Toju/agents-docs/features/signaling.md
T
2026-07-14 00:41:05 +02:00

153 lines
9.3 KiB
Markdown

# Signaling (WebSocket)
> **Area:** realtime
> **Status:** Active
> **Last updated:** 2026-07-05
## Overview
The signaling server exposes a single WebSocket per origin that carries identity, room membership, presence, WebRTC SDP/ICE relay, selected server-relayed chat/DM/voice fallbacks, plugin events, and multi-device `account_sync`. The product client implements the consumer in `toju-app/src/app/infrastructure/realtime/signaling/`.
**Canonical contract:** this document and [`server/src/websocket/handler.ts`](../../server/src/websocket/handler.ts). Do **not** treat `toju-app/src/app/shared-kernel/signaling-contracts.ts` as authoritative — it lists legacy types (`join`, `leave`, `chat`, `ice-candidate`) that do not match the live server.
## Responsibilities
- Authenticate connections via `identify` (session token).
- Track per-connection room membership and broadcast room-scoped presence.
- Relay WebRTC offers/answers/ICE between peers that share server membership (or DM/direct-call rules).
- Relay narrow server fallbacks when P2P data channels are unavailable (chat, DM, voice presence).
- Forward `account_sync` payloads to sibling connections for the same user identity.
- It does NOT own: P2P data-channel payloads (attachments, message inventory, custom emoji chunks, plugin message bus), local persistence, or REST server-directory APIs.
## Key concepts
- **Envelope:** JSON object with required `type` string; additional fields vary by type.
- **oderId:** user identity on the wire (legacy spelling, matches server code).
- **clientInstanceId:** per-tab/device id stored in `sessionStorage`; multiple open connections per `oderId` are allowed.
- **connectionScope:** optional string grouping connections (e.g. browser profile).
- **voiceActive:** server marks the connection that owns outbound RTC relay for a user; updated from `voice_state` payloads.
## Ordering invariants
1. **`identify` before anything else** — unauthenticated connections receive `auth_required` for all types except `identify` and `keepalive`.
2. **Per-connection serialization**`handleWebSocketMessage` chains handlers per `connectionId` so `join_server` cannot run while `identify` is still awaiting the token DB lookup.
3. **Client replay on reconnect**`SignalingManager.reIdentifyAndRejoin` sends `identify` then re-joins rooms; see [`toju-app/src/app/infrastructure/realtime/README.md`](../../toju-app/src/app/infrastructure/realtime/README.md).
## Connection lifecycle (server → client)
On connect the server assigns a `connectionId` and may emit:
| type | When |
|------|------|
| `connected` | Immediately after WebSocket open (`server/src/websocket/index.ts`) |
On disconnect, if the connection was voice-active, the server broadcasts a cleared `voice_state` via `finalizeVoiceDisconnectForConnection`.
## Inbound types (client → server)
| type | Auth | Behavior |
|------|------|----------|
| `keepalive` | Optional | Responds `keepalive_ack` with `serverTime` |
| `identify` | N/A (establishes auth) | Validates session token; sets `oderId`, profile fields; evicts stale same `(oderId, connectionScope, clientInstanceId)` sockets; emits `account_sync_peer_online` to siblings |
| `join_server` | Required | Access check; adds `serverId` to connection; sends `server_users` + `plugin_requirements`; may broadcast `user_joined` (identity-aware) |
| `view_server` | Required | Switches `viewedServerId`; refreshes `server_users` + `plugin_requirements` |
| `leave_server` | Required | Removes membership; may broadcast `user_left` with remaining `serverIds` |
| `offer`, `answer`, `ice_candidate` | Required | Relay to `targetUserId` when peers share server membership |
| `direct-message`, `direct-message-status`, `direct-message-mutation`, `direct-message-typing`, `direct-message-sync-request`, `direct-message-sync`, `direct-call` | Required | Relay to `targetUserId` (DM rules — no shared-server requirement) |
| `server_icon_peer_request`, `server_icon_peer_data` | Required | Relay when both users share `serverId` membership |
| `chat_message` | Required | Broadcast to server members (excludes sender connection) |
| `voice_state` | Required | Updates `voiceActive` / snapshot; broadcast to server members |
| `voice_client_takeover` | Required | Notifies sibling connections via `notifyOtherConnectionsForOderId` |
| `account_sync` | Required | Forwards `payload` object to other connections for same `oderId` |
| `typing` | Required | Broadcast `user_typing` to server members |
| `status_update` | Required | Broadcast `status_update` (`online` \| `away` \| `busy` \| `offline`) to joined servers |
| `server_icon_available` | Required | Records local `iconUpdatedAt` per server on the connection |
| `server_icon_sync_request` | Required | Responds `server_icon_sync_peers` with peers having newer icons |
| `plugin_event` | Required | Validates against server plugin metadata; broadcast or `plugin_error` |
Unknown inbound types are logged and ignored.
### `identify` request fields
| Field | Required | Notes |
|-------|----------|-------|
| `token` | Yes | Session token from REST login/register |
| `oderId` | No | If present, must match token user id |
| `displayName` | No | Defaults to existing or `"User"` |
| `description`, `profileUpdatedAt`, `homeSignalServerUrl` | No | Profile card fields |
| `clientInstanceId` | No | Per-tab id for multi-device and voice ownership |
| `connectionScope` | No | Eviction scope for stale sockets |
Errors: `auth_error` with `MISSING_TOKEN`, `INVALID_TOKEN`, or `USER_ID_MISMATCH`.
## Server-emitted types (server → client)
| type | Trigger |
|------|---------|
| `keepalive_ack` | Response to `keepalive` |
| `auth_required` | Message before `identify` |
| `auth_error` | Failed `identify` |
| `access_denied` | `join_server` rejected (`serverId`, `reason`) |
| `server_users` | After join/view; lists unique users in room |
| `user_joined` | New identity in server (excludes same identity's connections) |
| `user_left` | Identity fully left server (`serverIds` = remaining rooms) |
| `plugin_requirements` | Plugin install snapshot after join/view |
| `plugin_error` | Invalid plugin event |
| `server_icon_sync_peers` | Response to `server_icon_sync_request` |
| `account_sync_peer_online` | Sibling connection came online |
| `account_sync` | Relayed multi-device payload |
| `chat_message` | Relayed room chat fallback |
| `user_typing` | Typing indicator |
| `status_update` | Presence status change |
| `voice_state` | Voice roster / disconnect cleanup |
| `voice_client_takeover` | Another tab took voice ownership |
| `plugin_event` | Broadcast plugin event |
| Forwarded RTC/DM | Copies of client messages with `fromUserId` set |
## Relay rules
- **RTC (`offer` / `answer` / `ice_candidate`):** forwarded when sender and target share any server membership.
- **Direct signaling types:** forwarded to `targetUserId` without shared-server check.
- **Server icon P2P:** both users must be members of `message.serverId`.
- **Broadcasts** (`chat_message`, `voice_state`, `typing`, etc.): exclude sender **connection id** (not whole identity) so multi-device sessions still receive updates.
- **`user_joined` / `user_left`:** exclude whole **identity** so other users do not see duplicate join/leave for multiple tabs.
## P2P vs signaling split
| Transport | Carries |
|-----------|---------|
| **WebRTC data channel** | Chat events, attachments, custom emoji, message revisions/inventory, profile avatar bytes, voice/screen control, plugin message bus, game activity |
| **WebSocket signaling** | Identity, membership, presence, RTC SDP/ICE, chat/DM/voice fallbacks, `account_sync`, plugin events |
Server-relayed chat (`chat_message`) and DM types exist so users see written chat and delivery state while data channels are down. Media, attachments, and inventory sync remain peer-plane responsibilities.
## Multi-device (`account_sync`)
The client wraps relayable local changes in `account_sync` envelopes. The server forwards the inner `payload` to other open connections for the same `oderId`. When a device identifies, siblings receive `account_sync_peer_online` and push snapshots (saved servers, friends, emoji library, chat history batches, etc.). See [authentication.md](authentication.md) and domain-specific feature docs.
## Client implementation map
| Concern | Location |
|---------|----------|
| One socket per signal URL | `signaling/signaling.manager.ts` |
| Route picker | `signaling/signaling-transport-handler.ts` |
| Room affinity | `signaling/server-signaling-coordinator.ts` |
| Inbound dispatch | `signaling/signaling-message-handler.ts` |
| Constants (intervals, types) | `realtime.constants.ts` |
## Related
- [authentication.md](authentication.md) — session tokens and `identify` trust boundary
- [direct-messaging.md](direct-messaging.md) — DM envelope relay types
- [voice-webrtc.md](voice-webrtc.md) — RTC relay and `voice_state`
- [plugins.md](plugins.md) — `plugin_event` / `plugin_requirements`
- [message-integrity.md](message-integrity.md) — signed revisions (P2P; `account_sync` relay)
- Product client deep dive: [`toju-app/src/app/infrastructure/realtime/README.md`](../../toju-app/src/app/infrastructure/realtime/README.md)
- Server handler: [`server/src/websocket/handler.ts`](../../server/src/websocket/handler.ts)
## Changelog
| Date | Change |
|------|--------|
| 2026-07-05 | Initial canonical envelope catalog; deprecates `shared-kernel/signaling-contracts.ts` as wire source |