WebSocket Hub & Rooms
sparki intermediate 5 min read
ELI5
The hub is a switchboard. Every connected dashboard, TUI and mobile client plugs in once and gets put in a room (e.g., build:<id>). When the engine has news, it can shout to one client, one room, or everyone — three knobs, no more.
Technical Deep Dive
internal/websocket/hub.go defines Hub, Client, and three message types. The hub runs one goroutine that selects over register, unregister, broadcast, roomBroadcast, and directMessage.
Class Diagram
classDiagram class Hub { -map~Client,bool~ clients -map~string,map~Client,bool~~ rooms -map~UUID,Client~ clientsByID -chan Client register -chan Client unregister -chan Message broadcast -chan RoomMessage roomBroadcast -chan DirectMessage directMessage -HubMetrics metrics +Run(ctx) } class Client { +UUID ID +chan Message send } class Message { +string Type +map~string,any~ Payload } class RoomMessage { +string Room +Message Message } class DirectMessage { +UUID ClientID +Message Message } Hub --> Client : tracks Hub --> Message : broadcasts Hub --> RoomMessage : roomBroadcast Hub --> DirectMessage : directMessageChannel Buffer Sizes
| Channel | Buffer |
|---|---|
register | 256 |
unregister | 256 |
broadcast | 1024 |
roomBroadcast | 1024 |
directMessage | 1024 |
Three Send Paths
sequenceDiagram participant E as engine code participant H as Hub participant C as Client(s) E->>H: hub.broadcast <- msg H-->>C: send to every clients[true] E->>H: hub.roomBroadcast <- {room, msg} H-->>C: send to clients in rooms[room] E->>H: hub.directMessage <- {clientID, msg} H-->>C: send to clientsByID[clientID]Indexes
clients— set of all currently-registered clients (used by the all-clients broadcast path).rooms— map<roomName, set>. A client is added to a room via the handler-level join action; one client may sit in many rooms. clientsByID— map<UUID, Client> for O(1) direct message routing.
Metrics
HubMetrics tracks TotalConnections, ActiveConnections, MessagesSent, MessagesReceived, BroadcastsSent. Mutated under mu sync.RWMutex.
Key Terms
- room → string-keyed subscription bucket; conventionally
build:<id>,project:<id>,deploy:<id> - register/unregister → channels the connection upgrade handler writes to on connect/disconnect
- direct message → addressed by client UUID, used for per-user notifications
- broadcast → fan-out to every connected client; reserve for global system messages
Q&A
Q: What happens if a client’s send channel is full?
A: The hub’s standard pattern is to drop the slow client (close send, remove from clients and rooms) so a single stuck consumer cannot back up the central goroutine.
Q: How are direct messages routed?
A: Through clientsByID keyed by uuid.UUID. The handler that creates a Client populates this map at register-time.
Q: Are room memberships persisted? A: No. Rooms are in-memory only; a hub restart drops all subscriptions and clients must re-join.
Examples
A build progresses: the worker (sparki-004) emits a log line → handler converts it to a Message{Type:"build.log", Payload:{...}} → calls hub.roomBroadcast <- &RoomMessage{Room:"build:"+id, Message:msg} → every dashboard tab subscribed to that build receives the line within one event-loop tick.
neighbors on the map
- WebSocket Session Lifecycle adding a new privileged WS handler
- NATS Subject Taxonomy wiring a new consumer to the right stream
- Presence Broadcast Channels diagnosing missing cursor updates in the editor