import type { Logger } from "../contracts/extension.js"; import type { EventHandler, EventHookDescriptor, FilterDescriptor, FilterHandler, ServiceHandle, } from "../contracts/hooks.js"; import { applyFilterChain, dispatchEventAsync, dispatchEventSync, type FilterEntry, sortFilters, } from "./pure.js"; export interface Bus { readonly on: (hook: EventHookDescriptor, handler: EventHandler) => () => void; readonly emit: (hook: EventHookDescriptor, payload: T) => void; readonly emitAsync: ( hook: EventHookDescriptor, payload: T, timeoutMs?: number, ) => Promise; readonly addFilter: ( hook: FilterDescriptor, fn: FilterHandler, opts?: { readonly priority?: number }, ) => () => void; readonly applyFilters: ( hook: FilterDescriptor, value: T, opts?: { readonly failClosed?: boolean }, ) => Promise; readonly provideService: (handle: ServiceHandle, impl: T) => void; readonly getService: (handle: ServiceHandle) => T; } interface StoredFilterEntry { readonly fn: unknown; readonly priority: number; readonly order: number; } export function createBus(logger: Logger): Bus { const eventHandlers = new Map>(); const filterEntries = new Map(); const services = new Map(); let filterOrderCounter = 0; return { on(hook: EventHookDescriptor, handler: EventHandler): () => void { let set = eventHandlers.get(hook.id); if (set === undefined) { set = new Set(); eventHandlers.set(hook.id, set); } const stored: unknown = handler; set.add(stored); return () => { const current = eventHandlers.get(hook.id); if (current !== undefined) current.delete(stored); }; }, emit(hook: EventHookDescriptor, payload: T): void { const set = eventHandlers.get(hook.id); if (set === undefined || set.size === 0) return; const handlers = [...set] as Array>; dispatchEventSync(handlers, payload, logger, hook.id); }, async emitAsync( hook: EventHookDescriptor, payload: T, timeoutMs?: number, ): Promise { const set = eventHandlers.get(hook.id); if (set === undefined || set.size === 0) return; const handlers = [...set] as Array>; await dispatchEventAsync(handlers, payload, logger, hook.id, timeoutMs); }, addFilter( hook: FilterDescriptor, fn: FilterHandler, opts?: { readonly priority?: number }, ): () => void { let entries = filterEntries.get(hook.id); if (entries === undefined) { entries = []; filterEntries.set(hook.id, entries); } const entry: StoredFilterEntry = { fn, priority: opts?.priority ?? 0, order: filterOrderCounter++, }; entries.push(entry); return () => { const current = filterEntries.get(hook.id); if (current === undefined) return; const idx = current.indexOf(entry); if (idx !== -1) current.splice(idx, 1); }; }, async applyFilters( hook: FilterDescriptor, value: T, opts?: { readonly failClosed?: boolean }, ): Promise { const entries = filterEntries.get(hook.id); if (entries === undefined || entries.length === 0) return value; const sorted = sortFilters(entries as ReadonlyArray>); const fns = sorted.map((e) => e.fn) as Array>; return applyFilterChain(fns, value, logger, hook.id, opts?.failClosed ?? false); }, provideService(handle: ServiceHandle, impl: T): void { if (services.has(handle.id)) { throw new Error( `Service "${handle.id}" is already provided. Only one provider per handle is allowed.`, ); } services.set(handle.id, impl); }, getService(handle: ServiceHandle): T { const impl = services.get(handle.id); if (impl === undefined) { throw new Error( `Service "${handle.id}" has no provider. Call provideService before getService.`, ); } return impl as T; }, }; }