CRUMB a card from devarno-cloud

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

ConstantWire stringEmitter
EventPelletCompressedpellet:compressedhandleCompressPellet after Store.Compress
EventPelletExtractedpellet:extractedhandleExtractPellet
EventPelletPrunedpellet:prunedhandlePrunePellets
EventCacheUpdatedcache:updatedstats handler / store mutations
EventMetricsUpdatedmetrics:updatedmetrics 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 string constant; 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