SoulCache

Events

SoulCache event system — the EventBus envelope, ordering, and subscription contract

Events

SoulCache routes cross-subsystem communication through a single internal EventBus. Every runtime state change is observable as an event, which makes the runtime diagnosable without re-instrumentation (devtools, persistence, and telemetry attach as passive subscribers).

Note: the EventBus class and the runtime event contract are stable in v1.1.0; subscribeCoalesced is marked experimental for opt-in high-volume subscribers.

The event envelope

Every emitted event carries a stable, immutable envelope:

FieldTypeDescription
idstringUnique event identifier, assigned by the bus
seqnumberMonotonic sequence number, assigned by the bus at emission
typeRuntimeEventTypeEvent type (query, cache, fetch, mutation, scheduler, storage)
timestampnumberCreation time (Date.now())
sourceEventSourceOriginating subsystem
payloadobjectEvent-specific data
metadataobject?Optional additional metadata

The id, seq, and timestamp fields are assigned by the bus at emission time; emitters omit them.

Ordering

  • Events are delivered synchronously in FIFO order per subscription.
  • The sequence number strictly increases across the lifetime of a bus, providing deterministic global ordering across all event types and subscribers. It survives clear() so ordering tokens never restart.

Delivery guarantees

  • Delivery is synchronous and FIFO (a public contract).
  • A throwing subscriber is isolated: its error is captured and does not prevent delivery to other subscribers or crash the runtime.

Subscribing

import { EventBus } from '@soulcache/core';
 
const bus = new EventBus();
 
const unsubscribe = bus.subscribe('query.created', (event) => {
  console.log('Query created at seq', event.seq);
});
 
bus.emit({
  type: 'query.created',
  source: 'query-runtime',
  payload: { queryId: '123', queryKey: ['users'] },
});
 
unsubscribe();

The raw EventBus class is the public entry point for runtime events in @soulcache/core; application consumers subscribe directly to the event types they care about.

Opt-in coalesced delivery (experimental)

Status: experimental. Its contract is pinned here so downstream consumers (devtools, persistence) rely on documented semantics before it becomes stable.

High-volume, loss-tolerant subscribers (devtools taps, persistence writers) can opt into coalesced delivery instead of the default synchronous path:

const unsubscribe = bus.subscribeCoalesced(
  'query.created',
  (event) => {
    // receives the merged batch on the microtask after emission
  },
  { cap: 1000 },
);

Behavior:

  • Opt-in only. The default synchronous subscribe path is unchanged and never drops events; coalesced mode exists only when explicitly subscribed.
  • Batching. Events buffered during the current tick are delivered once, on the microtask after the tick, in sequence-number order.
  • Coalescing. Within a buffered batch, same-type events collapse to the latest (intermediate events are dropped). A query:fetchStart flood, for example, collapses to its newest occurrence.
  • Bounded queue. Each coalescing subscriber owns a queue with a documented cap (default 1000; configurable via { cap }). The queue never exceeds the cap, so memory stays bounded under an event storm.
  • Drop semantics. When the queue is full and a new event type arrives, the oldest buffered event is evicted. Events dropped by coalescing or eviction are intermediates only; surviving events are delivered at-least-once.
  • Never blocks. Emission is never blocked by a slow coalescing subscriber; buffering and draining are deferred to the microtask queue.
  • Isolation. A throwing handler is isolated and logged; it never breaks the batch, other subscribers, or the runtime.
  • Wildcard. subscribeCoalesced('*', handler) receives every event type through the same bounded, coalescing queue.

Unsubscribing stops delivery immediately; a batch buffered but not yet drained at unsubscribe time is discarded.

On this page