3.7 KiB
3.7 KiB
Change: enterprise-events
Why
@suaveplan/events is the foundation pub/sub substrate — typed event bus, deterministic ordering, backpressure-aware async dispatch, and a public companion/interceptor API that the sibling packages (events-metrics, events-otel, events-schema, events-transport) plug into without monkey-patching. Every domain-event-emitting package will depend on it.
Per the foundation-100-coverage rule (see memory foundation-100-coverage), this ships at 100% coverage from day one.
What Changes
New package: packages/universal/events
Ships as a focused, zero-Node / zero-browser / zero-React TypeScript library. Runtime deps limited to @suaveplan/error, tslib, and zod.
Core surface
EventBus<TMap>— typed bus parameterized by an event-name → payload map. Methods:on,once,off,emit(sync),emitAsync(async with AbortSignal),use(interceptor),clearEventEnvelope<T>—{ id, type, payload, timestamp, traceId? }, produced bywrap(type, payload, options?)createIdGenerator()— deterministic-under-injection id generator (default:crypto.randomUUID()via a single module-local reference that tests can swap)Interceptor<TMap>—(envelope, next) => R | Promise<R>chain-of-responsibility. Registration order defines execution order;next()advances the chain; interceptors can replace, drop (by not callingnext), or observeBoundedQueue<T>— in-memory FIFO with{ capacity, policy: "drop-oldest" | "drop-newest" | "block" | "throw" }, used internally byemitAsyncunder backpressure and exposed for consumersidempotent(fn, { keyOf, cache? })— wraps a handler so a givenkeyOf(envelope)only fires once; the cache is aMapby default, pluggable
Error surface
EventsErrorextendsSuaveplanError<EventsCode>with codes likeEVENTS.LISTENER.THREW,EVENTS.QUEUE.OVERFLOW,EVENTS.QUEUE.ABORTED,EVENTS.SCHEMA.REJECTED,EVENTS.INTERCEPTOR.THREW- Errors capture
{ type, envelopeId, listenerIndex?, queueSize? }incontextand preserve the originalcause
Schemas
EventEnvelopeSchemas(.schemas.ts) — Zod schema for the envelope shape with generic payload; consumers opt in by importing@suaveplan/events/envelope.schemas
Observability hook
withObservability(bus, adapter)— wraps anEventBuswith a passive adapter implementing{ onEmit?, onEmitError?, onEmitComplete?, onListenerThrew? };events-metrics/events-otelregister here
Companion/interceptor API
bus.use(interceptor)is the one public extension point;events-schemavalidates envelopes via an interceptor,events-transportforwards to an external sink via an interceptor,events-metricstimes via an interceptor,events-otelcreates spans via an interceptor. The core ships no direct coupling to any of those.
Determinism
- No
Math.random()/Date.now()at module scope createBus({ clock, idGenerator })accepts injections; defaults are the thin wrappersdefaultClock = () => Date.now()anddefaultIdGenerator = () => crypto.randomUUID()
Coverage
Vitest thresholds locked at 100/100/100/100 in vitest.config.ts. Property-based tests (fast-check) cover:
- dispatch ordering (FIFO across listeners and across emits)
- interceptor composition (associativity, drop-propagation)
- backpressure policies (bounded queue invariants)
- idempotency (given key-collisions, handler called exactly once)
Impact
- Unblocks
events-metrics,events-otel,events-schema,events-transport(each a Layer 2 adapter on top of@suaveplan/events) - Depends on
@suaveplan/error(already published) forSuaveplanErroranddefineCodes - No existing genesis consumers affected