WebSocket Event Stream
nestr intermediate 4 min read
ELI5
The Engine’s WebSocket is a kitchen pass-through window: every time a chef finishes a dish (compress, extract, prune, cache update, metrics tick), they ring a bell with the dish name on it. Every browser holding a ticket hears every bell.
Technical Deep Dive
engine/internal/server/websocket.go defines a fan-out hub. The handler upgrades the HTTP connection (after auth — see nestr-006) and registers a client; the hub keeps a set of clients and broadcasts JSON-encoded events to all of them.
Event Catalogue
| Constant | Wire string | Emitter |
|---|---|---|
EventPelletCompressed | pellet:compressed | handleCompressPellet after Store.Compress |
EventPelletExtracted | pellet:extracted | handleExtractPellet |
EventPelletPruned | pellet:pruned | handlePrunePellets |
EventCacheUpdated | cache:updated | stats handler / store mutations |
EventMetricsUpdated | metrics:updated | metrics ticker |
Broadcast Sequence
sequenceDiagram participant H as REST Handler participant S as pellets.Store participant Hub as ws.Hub participant C1 as Client A participant C2 as Client B H->>S: Compress(source, level) S-->>H: *Pellet H->>Hub: Broadcast{type:"pellet:compressed", data:Pellet} Hub-->>C1: JSON frame Hub-->>C2: JSON frame H-->>H: respondJSON(success)Client Lifecycle
- Upgrade succeeds →
hub.register(client); client gets a buffered send channel. - Client read pump: ignored payloads (server-to-client only in this revision).
- Client write pump: drains the send channel into the socket; on full buffer the client is dropped to keep one slow consumer from blocking the hub.
- Disconnect →
hub.unregister(client).
The wire format is JSON: {"type": "pellet:compressed", "data": {...}, "ts": "..."}. Unlike systems with a versioned envelope (e.g. choco’s EventEnvelope), nestr’s WS messages are not versioned — clients are expected to ignore unknown type values.
Key Terms
- Hub → a single in-process actor owning the client set and the broadcast channel.
- Send buffer → per-client bounded channel; overflow drops the client, not the message.
- Event type → a
stringconstant; the wire form is the same string with no namespace prefix.
Q&A
Q: What happens to events emitted while no clients are connected? A: They are dropped. The hub has no replay log; it is fan-out only, not pub/sub with retention.
Q: Can a client subscribe to only one event type? A: No — there is no subscription filter. Filtering is the client’s responsibility.
Q: Is delivery ordered across event types? A: Per-client yes (single goroutine writes the socket); cross-client ordering is not guaranteed because each client has its own send goroutine.
Examples
A dashboard hook in web/src/hooks/useWebSocket.ts parses each frame, switches on type, and calls queryClient.invalidateQueries(pelletKeys.lists()) for pellet:compressed and pellet:pruned, prompting an immediate refetch instead of waiting for the 30 s polling interval.
neighbors on the map
- WebSocket Session Lifecycle adding a new privileged WS handler
- EventEnvelope Wire Wrapper publishing a new domain event proto
- NATS Subject Taxonomy wiring a new consumer to the right stream
- ProtocolMessage Envelope adding a new wire message type