import type { Clock } from "@suaveplan/types"; import type { HookEvent, HookListener, HookPayload, StepResult, } from "../../types/common.js"; import { defaultClock, now } from "../../utils/utils.js"; /** * Registry that manages hook listeners and dispatches events during * workflow execution. */ export class HookRegistry { private readonly _listeners = new Map[]>(); private readonly _clock: Clock; constructor(options?: { clock?: Clock }) { this._clock = options?.clock ?? defaultClock; } on(event: HookEvent, listener: HookListener): () => void { const existing = this._listeners.get(event) ?? []; existing.push(listener); this._listeners.set(event, existing); return () => { const current = this._listeners.get(event); if (current) { const idx = current.indexOf(listener); if (idx >= 0) current.splice(idx, 1); } }; } once(event: HookEvent, listener: HookListener): void { const wrapped: HookListener = (payload) => { unregister(); return listener(payload); }; const unregister = this.on(event, wrapped); } off(event?: HookEvent): void { if (event) { this._listeners.delete(event); } else { this._listeners.clear(); } } async emit( event: HookEvent, data: Omit, "event" | "timestamp"> ): Promise { const listeners = this._listeners.get(event); if (!listeners || listeners.length === 0) return; const payload: HookPayload = { ...data, event, timestamp: now(this._clock), }; for (const listener of listeners) { await listener(payload); } } listenerCount(event: HookEvent): number { return this._listeners.get(event)?.length ?? 0; } get events(): HookEvent[] { return [...this._listeners.keys()]; } get totalListeners(): number { let count = 0; for (const listeners of this._listeners.values()) { count += listeners.length; } return count; } } /** Factory function to create a hook registry. */ export function createHookRegistry(options?: { clock?: Clock; }): HookRegistry { return new HookRegistry(options); } /** Helper to build a step-related hook payload. */ export function buildStepPayload( workflowId: string, context: TCtx, stepId: string, stepResult?: StepResult, error?: Error ): Omit, "event" | "timestamp"> { return { workflowId, context, stepId, stepResult, error }; }