# 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`** — typed bus parameterized by an event-name → payload map. Methods: `on`, `once`, `off`, `emit` (sync), `emitAsync` (async with AbortSignal), `use` (interceptor), `clear` - **`EventEnvelope`** — `{ id, type, payload, timestamp, traceId? }`, produced by `wrap(type, payload, options?)` - **`createIdGenerator()`** — deterministic-under-injection id generator (default: `crypto.randomUUID()` via a single module-local reference that tests can swap) - **`Interceptor`** — `(envelope, next) => R | Promise` chain-of-responsibility. Registration order defines execution order; `next()` advances the chain; interceptors can replace, drop (by not calling `next`), or observe - **`BoundedQueue`** — in-memory FIFO with `{ capacity, policy: "drop-oldest" | "drop-newest" | "block" | "throw" }`, used internally by `emitAsync` under backpressure and exposed for consumers - **`idempotent(fn, { keyOf, cache? })`** — wraps a handler so a given `keyOf(envelope)` only fires once; the cache is a `Map` by default, pluggable #### Error surface - **`EventsError`** extends `SuaveplanError` with codes like `EVENTS.LISTENER.THREW`, `EVENTS.QUEUE.OVERFLOW`, `EVENTS.QUEUE.ABORTED`, `EVENTS.SCHEMA.REJECTED`, `EVENTS.INTERCEPTOR.THREW` - Errors capture `{ type, envelopeId, listenerIndex?, queueSize? }` in `context` and preserve the original `cause` #### 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 an `EventBus` with a passive adapter implementing `{ onEmit?, onEmitError?, onEmitComplete?, onListenerThrew? }`; `events-metrics` / `events-otel` register here #### Companion/interceptor API - `bus.use(interceptor)` is the one public extension point; `events-schema` validates envelopes via an interceptor, `events-transport` forwards to an external sink via an interceptor, `events-metrics` times via an interceptor, `events-otel` creates 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 wrappers `defaultClock = () => Date.now()` and `defaultIdGenerator = () => 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) for `SuaveplanError` and `defineCodes` - No existing genesis consumers affected