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
EventBusclass and the runtime event contract are stable in v1.1.0;subscribeCoalescedis marked experimental for opt-in high-volume subscribers.
The event envelope
Every emitted event carries a stable, immutable envelope:
| Field | Type | Description |
|---|---|---|
id | string | Unique event identifier, assigned by the bus |
seq | number | Monotonic sequence number, assigned by the bus at emission |
type | RuntimeEventType | Event type (query, cache, fetch, mutation, scheduler, storage) |
timestamp | number | Creation time (Date.now()) |
source | EventSource | Originating subsystem |
payload | object | Event-specific data |
metadata | object? | 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
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:
Behavior:
- Opt-in only. The default synchronous
subscribepath 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:fetchStartflood, 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.