# EventBus module — `@suaveplan/rules/events` _(no submodules)_ ## Purpose Pub/sub bus used by the engine and downstream consumers (Hono middleware, audit logs, observability sinks) to broadcast lifecycle events without coupling. Topics are drawn from the typed `RuleEngineEvent` union — adding a new topic is a type change rather than a string literal. The bus supports plain `on` / `off` / `once` subscriptions, priority-ordered `hook` registrations (higher fires first), a bounded ring-buffer history (default 1000 entries), an awaitable `waitFor(event, { timeoutMs?, signal? })` API, and OTel emission counting every payload by `type`. A sibling `HookManager` class scopes the priority-hook surface to a single topic so callers can manage hooks without holding the parent bus — useful for engine plugins that own a handful of lifecycle hooks but should not have a back-channel to the global event stream. ## Features - Typed topic API — every `on` / `emit` is keyed by `RuleEngineEvent`. - Priority hooks — `hook(event, { priority, listener })`; sorted descending by priority on register; finite-priority enforcement raises `RULES.HOOK_INVALID`. - Awaitable `waitFor` — resolves with the payload on first match; rejects with `RULES.EVENT_TIMEOUT` past `timeoutMs` or `RULES.EVENT_ABORTED` when the supplied `AbortSignal` fires (including pre-aborted). - Bounded history — `getHistory()` returns the last N payloads in arrival order; `clearHistory()` drops the buffer. - Injectable `Clock` — `bus.now()` for callers that timestamp their own payloads. - OTel counter — every `emit` / `emitSync` increments `rules.event.emitted_total{type}`. - `HookManager` — standalone priority-hook bag; `register` / `remove` / `invoke` / `size`. ## Usage: Basic Example ```ts import { createEventBus } from "@suaveplan/rules/events"; const bus = createEventBus(); bus.on("rule.passed", (payload) => { console.log(`rule passed at ${payload.timestamp}`); }); await bus.emit({ type: "rule.passed", timestamp: Date.now() }); ``` ## Usage: Advanced Example ```ts import { createEventBus, createHookManager } from "@suaveplan/rules/events"; const bus = createEventBus({ historySize: 200, clock: () => Date.now() }); // Priority-ordered hooks — higher fires first bus.hook("rule.passed", { priority: 100, listener: async (payload) => audit.append(payload), }); bus.hook("rule.passed", { priority: 10, listener: async (payload) => metrics.increment("rule.passed", { type: payload.type }), }); // Await the next event with a hard 5s ceiling and caller-side abort const controller = new AbortController(); setTimeout(() => controller.abort(), 5000); try { const payload = await bus.waitFor("ruleset.completed", { timeoutMs: 5000, signal: controller.signal, }); console.log("ruleset finished:", payload.timestamp); } catch (err) { console.warn("waitFor failed:", err); } // Standalone hook manager — convenient for engine plugin internals const mgr = createHookManager(); mgr.register({ priority: 1, listener: () => undefined }); await mgr.invoke({ type: "rule.failed", timestamp: bus.now() }); ``` ## API Reference ### `class EventBus` - `new EventBus(options?)` — `options.clock` defaults to `() => Date.now()`. `options.historySize` defaults to `DEFAULT_EVENT_HISTORY = 1000`; `<= 0` raises `RULES.HOOK_INVALID`. - `on(event, listener)` → unsubscribe fn. - `off(event, listener)` → boolean. - `once(event, listener)` → unsubscribe fn; auto-removes after first fire. - `hook(event, { priority, listener, id? })` → unsubscribe fn. Sorted descending; non-finite priority raises `RULES.HOOK_INVALID`. - `emit(payload)` → `Promise`. Records history, increments OTel counter, runs hooks first (in priority order), then listeners (in registration order). Awaits every callback. - `emitSync(payload)` — fire-and-forget variant; same ordering but callbacks are not awaited. - `waitFor(event, options?)` → `Promise`. Rejects with `RULES.EVENT_TIMEOUT` or `RULES.EVENT_ABORTED`. - `getHistory()` / `clearHistory()` — history-buffer accessors. - `now()` — convenience over the injected clock. ### `class HookManager` Standalone priority-hook bag scoped to one topic. Methods: `register`, `remove`, `invoke`, `size`. Same priority + finite-number rules as the bus. ### Factory aliases `createEventBus(options?)`, `createHookManager()`. ## Implementation Notes - The bus runs hooks **before** listeners on `emit`. This lets plugins inspect and (in future) decorate the payload before user-land code observes it. - `on` / `hook` snapshots their bucket via `[...bucket]` before iterating so that unsubscribing during emission does not skip subsequent callbacks. - `waitFor` registers a one-shot listener plus optional abort + timeout handlers. Every cleanup path (resolve, reject-timeout, reject-abort, pre-abort) runs every cleanup function exactly once via the local `cleanup: (() => void)[]` queue. - `historyBuffer` is a plain array trimmed with `shift()`. Trimming via a ring index would shave a microsecond per insert but adds bookkeeping; the cost is dominated by listener / hook execution in any realistic workload. - OTel emission is per-`emit` and not per-listener, so the counter measures bus activity rather than work done by subscribers. Subscribers that want their own metrics emit via their own meter. - `emitSync` does not return a value (no `Promise` to await), but callback exceptions are NOT silently swallowed — the runtime will surface them as unhandled promise rejections through the `void` discard. Callers that need synchronous error handling should use `emit` and `await` it.