Add docs/architecture/ — a curated blueprint set with 10 Mermaid diagrams covering system context, deployment topology, server package map, REST request lifecycle, WebSocket auth/replay/dispatch, the full data model (migrations 001-015), voice/E2EE flow, and the client module map. Add docs/audit-2026-07-19.md — successor to audit-2026-04-07.md: re-verifies carried-over findings, catalogues spec-vs-code drift in api.md/protocol.md/schema.md (incl. the announcement channel-type contradiction and the undocumented voice-E2EE protocol surface), records server/client/CI findings with file:line evidence, and closes with a 12-item prioritized improvement backlog. Link both from the README docs index. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UA17KPvqGBX3XbXYnMf1rA
5.0 KiB
WebSocket / Real-time Engine
Verified against: commit ddc49f0, 2026-07-19
The Server/ws package (~6.9k LOC, the largest in the server) implements the
real-time engine: a single Hub owning all client connections, a topic-based
pub/sub, a monotonic sequence counter, a 3-tier reconnect replay pipeline, and a
dual (V1 legacy + V2 typed) command dispatch. Message-type constants live in
Server/ws/message_types.go (client↔server) and mirror
Client/tauri-client/src/lib/protocolTypes.ts.
Both files claim to be "Generated from docs/protocol-schema.json — single source of truth", but no such file exists in the repository — the constants are maintained by hand on both sides. See audit-2026-07-19.md §3.
D4a — Connect, authenticate, replay
sequenceDiagram
autonumber
participant C as Client (ws.ts via Rust ws_proxy)
participant S as ws.ServeWS
participant H as Hub
C->>S: WSS upgrade /api/v1/ws (Origin checked)
Note over S: no HTTP AuthMiddleware —<br/>auth is in-band, 10s deadline
C->>S: {type:"auth", payload:{token, last_seq}}
S->>S: validate token hash → session expiry → user → ban
S->>H: register (kicks previous conn of same user)
S-->>C: auth_ok {user, server_name, motd, replay_source}
alt last_seq within in-memory ring buffer (Tier 1)
H-->>C: replay EventsSinceFiltered (perm-filtered, fail-closed)
else last_seq within events table (Tier 2, max 5000)
H-->>C: replay from cold-tier EventStore
else too far behind, or channel visibility changed (Tier 3)
H-->>C: full "ready" re-sync snapshot
end
loop steady state
C->>H: chat_send / reaction_add / voice_join / …
H-->>C: seq-stamped broadcasts (chat_message, presence, …)
C->>S: ping (every 30s) → pong
end
What this shows. Auth is deliberately in-band (the WS route mounts without
AuthMiddleware). Every broadcast is assigned a monotonic seq under a
dedicated mutex; the client reports its last_seq on reconnect and the hub
picks the cheapest replay tier. A visibilityChangeSeq watermark forces a full
re-sync whenever channel visibility changed while the client was away, so
permission changes can never be replayed around. auth_ok.replay_source
(none|buffer|db) reports which tier served the reconnect and feeds the
ws_reconnect_tier_total metric.
D4b — Broadcast fanout and backpressure
flowchart LR
EV["deliverBroadcast<br/>assign seq"] --> RB["EventRingBuffer<br/>(1000, Tier 1)"]
EV --> EP["EventPersister<br/>async batched → events table<br/>(Tier 2; drops if queue full)"]
EV --> PLG["plugin EventSink"]
EV --> PS["PubSub topics<br/>global / channel:N / voice:N / user:N<br/>(per-topic 100 msg/s limit)"]
PS --> CH{"per-client queues"}
CH --> HI["sendHigh (64)<br/>DMs, mentions"]
CH --> NO["send (256)<br/>chat, reactions"]
CH --> LO["sendLow (64)<br/>typing, presence"]
HI --> WP["writePump<br/>drains high-first"]
NO --> WP
LO --> WP
WP -->|"high/normal full →<br/>disconnect (forces replay)"| X["client"]
LO -.->|"low full → silently dropped"| X
What this shows. Overflow policy is intentional: dropping a chat message
would corrupt state, so a full normal/high queue disconnects the client and the
replay pipeline restores consistency; typing/presence are lossy by design. The
global broadcast channel (1024) drops with a broadcastDrops counter when
saturated.
D4c — Dual dispatch (V1 → V2 strangler-fig)
stateDiagram-v2
[*] --> handleMessage
handleMessage --> V2: registry.hasV2(type)
handleMessage --> V1: no V2 handler
V2: V2 typed command path
V2: strict parse → Command → Result{mutations, events}
V1: V1 legacy path
V1: lenient parse → registry.Dispatch → imperative handler
V2 --> [*]
V1 --> [*]
What this shows. Both dispatch generations are registered in NewHub and
live simultaneously; each message type is tried against the stricter V2 typed
path first and falls back to V1. Two parsers and two handler registries must be
kept in sync until the migration completes — tracked as an audit finding.
The Hub also owns: stale-client sweep (90s), revoked-session sweep (30s, plus
per-connection revalidation every 10 messages), stale-voice-state sweep (60s),
panic containment on the run loop (3 panics/60s → stop), LiveKit client and
optional managed subprocess, and the voice E2EE key-holder map
(voice-e2ee.md). Many collaborators are attached
post-construction via SetLiveKit / SetEventPersister /
SetPluginRegistry setters that "must be called before Run" — temporal
coupling noted in the audit.
Source of truth: Server/ws/hub.go, Server/ws/serve.go,
Server/ws/client.go, Server/ws/handlers.go, Server/ws/command.go,
Server/ws/pubsub.go, Server/ws/ringbuffer.go, Server/ws/event_persister.go,
Server/ws/message_types.go, Server/migrations/014_events_table.sql.