Skip to content

Events

An event is a fire-and-forget publish on an actor's $events subject. Any actor or external client may subscribe; if nobody is subscribed, the event is dropped. Events are the protocol's pub/sub primitive.

Source: WORKSTREAMS/core-and-packaging/ACTOR-COMMUNICATION-CONTRACT.md § "static emits — Pub/Sub Events". Quoted: "I fire these. Subscribe if you care. Ephemeral — if nobody is listening, the event is lost. For live updates, not historical replay."

Wire pattern

{root}.{mount}.$events

The actor publishes one envelope per event. Subscribers receive a copy. There is no acknowledgement, no retry, no broker-side queue. NATS delivers to current subscribers and discards the message.

Example (root = COM.NIMBLETEC.RICHARD-SANTOMAURO):

publish:   COM.NIMBLETEC.RICHARD-SANTOMAURO.system.runtimes.$events
payload:   { op: 'runtimes.added', runtimeId: 'rt_chat_01', type: 'chat',
             app: 'chat', sessionMount: 'chat' }

The op name on the wire is the event name, taken from the actor's static emits declaration. From projects/matrix-3/packages/system-runtimes/src/RuntimeManagerActor.ts line 339:

typescript
this.emit('runtimes.added', { runtimeId, type, app, sessionMount });

Subscribing

Subscribers attach a handler to {mount}/$events:

typescript
this.subscribe('system.runtimes/$events', (payload, meta) => {
  // payload includes the op name (the actor encodes it)
});

Wildcards work normally:

chat/#                          ← all events under chat
chat.conversation/+/$events     ← per-conversation events

Declaration shape

From ACTOR-COMMUNICATION-CONTRACT.md:

typescript
static emits: Record<string, {
  description: string;       // What triggers this event. When. How often.
  schema: Record<string, FieldDescriptor>;
  frequency?: 'high' | 'medium' | 'low' | 'burst' | 'periodic';
}>;

Real-world example (verbatim from projects/matrix-3/packages/system-platform/src/ServiceRegistryActor.ts:189):

typescript
static override emits = {
  'service.registered':   { description: 'Emitted when a service endpoint is registered' },
  'service.deregistered': { description: 'Emitted when a service endpoint is removed' },
  'registry.registered':  { description: 'Emitted when a logical mount claim is registered' },
  'registry.heartbeat':   { description: 'Emitted when a logical mount claim heartbeat is accepted' },
  'registry.unregistered':{ description: 'Emitted when logical mount claims are removed' },
};

Each entry's description is what $introspect returns and what protocol gateways surface. The schema field (per-field descriptors) is documented in ACTOR-COMMUNICATION-CONTRACT.md; many production actors today use the short form (just description).

Frequency hints

The optional frequency declaration tells consumers what to expect:

ValueRate of fire
high10+ per second (e.g., market.tick)
medium1–10 per second (e.g., chat token streams)
low<1 per second (e.g., status changes)
burstbursty — bursts of high-rate then quiet
periodicscheduled — every N minutes

This is a hint for consumer-side rate-limiting and UI throttling. The framework does not enforce it.

Events vs streams vs state

Propertyemits (events)streams (JetStream)state (KV)
Persistencenonepersisted, replayablepersisted, last-value
Late subscriber sees historynoyes (replay)yes (current value)
Multiple consumersyes (fan-out)yes (consumer groups)yes
Fits high-volume telemetryyesyes (with retention cost)no — overwrites
Fits dashboardsneeds subscription on connectyesyes — best fit

Use events for "right now, broadcast to whoever is here". Use streams for "replayable history". Use state for "current value, watchable".

Trace context propagation

Events carry the traceId/spanId/parentSpanId fields from the envelope when the publishing actor was acting under a trace. Subscribers that want to continue the trace start a child span using parentSpanId. The framework helpers (startSpan, getActiveTraceContext) live in projects/matrix-3/packages/core/src/telemetry/tracing.ts.

What events do NOT do

  • They do not retry. If a subscriber is slow, NATS drops to backpressure rules; if a subscriber crashes, it misses events for the duration of the outage.
  • They do not order across publishers. Two actors publishing to the same event topic are ordered per-publisher only.
  • They are not durable. A late subscriber sees only events published after it subscribed. Use streams when this matters.
  • They are not authenticated end-to-end. Subscribe is gated by NATS ACL on the subject; the publisher's identity is in the envelope's from field but is not signed.

Idempotent subscription

MatrixActor subscriptions are tracked in a SubscriptionBag. Disposing the actor automatically tears down all its subscriptions. Subscribing to the same (topic, handler) twice is a no-op at the subscription bag level; at the broker level NATS allows duplicate subscriptions and will deliver duplicates if both are active.

See also