/** * EventDispatcher — the central event bus (one per runner). * * Pattern: Observer (GoF) + Pub/Sub over a typed discriminated union. * Role: Single flat dispatcher for every event emitted during a run. * Replaces DOM-style bubbling — we have a central bus by * construction, so tree propagation is unnecessary. * Emits: N/A — this IS the emitter. It ROUTES events to listeners. * * Semantics: * - Observers are ALWAYS fire-and-forget (inherited from footprintjs's * recorder contract). Promise returns are never awaited. * - Listener errors are caught; they become `agentfootprint.error.fatal` * events with stage:'observer'. The run continues. * - Dispatch is O(1) hash lookup by event type. * - Zero allocation when no listener for an event type AND no wildcard. * - Dev-mode wraps listeners to warn on async listener Promise return. * - Lifecycle: subscriptions release via the returned Unsubscribe or an * AbortSignal (`{ signal }`); `removeAllListeners()` is the bulk * escape hatch for long-lived server consumers; `listenerCount()` is * the leak diagnostic. Every removal path prunes emptied buckets and * detaches abort handlers, so listener storage is bounded by LIVE * subscriptions — never by subscription history. */ import type { AgentfootprintEvent, AgentfootprintEventMap, AgentfootprintEventType } from './registry.js'; export type EventListener = (event: AgentfootprintEventMap[K]) => void; export type WildcardListener = (event: AgentfootprintEvent) => void; export interface ListenOptions { readonly once?: boolean; readonly signal?: AbortSignal; } export type Unsubscribe = () => void; export type DomainWildcard = 'agentfootprint.composition.*' | 'agentfootprint.agent.*' | 'agentfootprint.stream.*' | 'agentfootprint.context.*' | 'agentfootprint.memory.*' | 'agentfootprint.tools.*' | 'agentfootprint.skill.*' | 'agentfootprint.permission.*' | 'agentfootprint.risk.*' | 'agentfootprint.fallback.*' | 'agentfootprint.cost.*' | 'agentfootprint.eval.*' | 'agentfootprint.error.*' | 'agentfootprint.pause.*' | 'agentfootprint.checkin.*' | 'agentfootprint.embedding.*'; export type AllWildcard = '*'; export type WildcardSubscription = DomainWildcard | AllWildcard; /** * Central event bus. One per executable runner. * * Zero-alloc fast path: if `hasListenersFor(type)` is false AND there are * no wildcards, `dispatch` returns immediately without iteration. */ export declare class EventDispatcher { private readonly byType; private readonly domainWildcards; private readonly allWildcards; /** * Fast-path check. Returns true when at least one listener would fire * for this type. Used by emitters to skip event-object allocation. */ hasListenersFor(type: AgentfootprintEventType): boolean; /** * Subscribe a typed listener for a specific event type. * * The listener signature is `(event) => void` by design — Promises are * NOT awaited. See dispatch() for details. */ on(type: K, listener: EventListener, options?: ListenOptions): Unsubscribe; /** Subscribe to a domain wildcard ('agentfootprint.context.*') or '*'. */ on(type: WildcardSubscription, listener: WildcardListener, options?: ListenOptions): Unsubscribe; /** * Subscribe a one-shot listener. Fires at most once and then auto-removes. * Equivalent to `on(type, listener, { once: true })`. Accepts `{ signal }` * for AbortSignal auto-cleanup, same as `on()`. */ once(type: K, listener: EventListener, options?: Omit): Unsubscribe; once(type: WildcardSubscription, listener: WildcardListener, options?: Omit): Unsubscribe; /** * Shared subscribe path for on()/once(). The public overloads constrain * `type` to either typed keys or wildcards; internally the dispatcher's * bucket logic accepts any string and classifies by shape. */ private subscribe; /** * Remove a specific listener for a type. Prefer AbortSignal for auto-cleanup. * * Because listeners are wrapped in dev mode, identity is preserved via a * WeakMap in addListener — consumers pass the original function. */ off(type: K, listener: EventListener): void; off(type: WildcardSubscription, listener: WildcardListener): void; /** * Lifecycle escape hatch — drop EVERY listener (typed, domain-wildcard, * and `'*'`) in one call. For long-lived server consumers that reuse one * runner across many requests: when you can't thread an AbortSignal or * keep every Unsubscribe handle, call this between requests to guarantee * the dispatcher holds zero subscriptions. * * Safe to call mid-dispatch: the bucket currently being iterated * finishes its already-taken snapshot (same semantics as `off()` during * dispatch), buckets the in-flight dispatch has NOT yet reached deliver * nothing (DOM-like "stop now"), and every SUBSEQUENT event sees no * listeners. Abort handlers registered on consumer AbortSignals via * `{ signal }` are detached too. Previously returned Unsubscribe * handles become harmless no-ops. */ removeAllListeners(): void; /** * Diagnostic — how many listeners the dispatcher currently retains. * * - `listenerCount()` — TOTAL across every bucket (typed + domain * wildcards + `'*'`). The number long-lived consumers watch to verify * per-run subscriptions are being released (leak detection). * - `listenerCount(type)` — listeners registered under that exact * subscription key (`'agentfootprint.agent.turn_start'`, * `'agentfootprint.context.*'`, or `'*'`). NOTE: counts the bucket * only — a typed count does NOT include wildcard listeners that would * also fire for that type. "Would anything fire?" is * `hasListenersFor()`. */ listenerCount(type?: AgentfootprintEventType | WildcardSubscription): number; /** * Route an event to all matching listeners (typed + domain-wildcard + all). * * Fire-and-forget: any returned Promise is IGNORED. Listener exceptions * are caught and re-dispatched as `error.fatal` events with scope='observer'. * The run continues regardless. */ dispatch(event: AgentfootprintEvent): void; private addListener; /** * Bounded-leak guarantee: a bucket emptied by ANY removal path is * deleted from its Map so `byType` / `domainWildcards` never retain * empty Sets for event types subscribed once and released. The * identity check (`get(...) === bucket`) guards stale Unsubscribe * closures — they must never delete a NEWER bucket re-created under * the same key after this one was pruned. (`allWildcards` is a stable * field, not a Map entry — nothing to prune.) */ private pruneBucket; private ensureBucket; private bucketFor; private fireBucket; private domainKey; } //# sourceMappingURL=dispatcher.d.ts.map