diff options
| author | Adam Malczewski <[email protected]> | 2026-06-04 22:40:46 +0900 |
|---|---|---|
| committer | Adam Malczewski <[email protected]> | 2026-06-04 22:40:56 +0900 |
| commit | 669c269d972f7bfcd9036017ff5e668d2eeafdd8 (patch) | |
| tree | 9dfd465feb78c1c30c75d09528476b0531689bcd /packages/kernel/src/bus/bus.ts | |
| parent | 0af8b1de733a199dbf5dcf779cc45f495048b1c3 (diff) | |
| download | dispatch-669c269d972f7bfcd9036017ff5e668d2eeafdd8.tar.gz dispatch-669c269d972f7bfcd9036017ff5e668d2eeafdd8.zip | |
feat(kernel): event/hook bus (events, filters, services) — pure dispatch + stateful shell
Diffstat (limited to 'packages/kernel/src/bus/bus.ts')
| -rw-r--r-- | packages/kernel/src/bus/bus.ts | 139 |
1 files changed, 139 insertions, 0 deletions
diff --git a/packages/kernel/src/bus/bus.ts b/packages/kernel/src/bus/bus.ts new file mode 100644 index 0000000..03d692e --- /dev/null +++ b/packages/kernel/src/bus/bus.ts @@ -0,0 +1,139 @@ +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: <T>(hook: EventHookDescriptor<T>, handler: EventHandler<T>) => () => void; + readonly emit: <T>(hook: EventHookDescriptor<T>, payload: T) => void; + readonly emitAsync: <T>( + hook: EventHookDescriptor<T>, + payload: T, + timeoutMs?: number, + ) => Promise<void>; + readonly addFilter: <T>( + hook: FilterDescriptor<T>, + fn: FilterHandler<T>, + opts?: { readonly priority?: number }, + ) => () => void; + readonly applyFilters: <T>( + hook: FilterDescriptor<T>, + value: T, + opts?: { readonly failClosed?: boolean }, + ) => Promise<T>; + readonly provideService: <T>(handle: ServiceHandle<T>, impl: T) => void; + readonly getService: <T>(handle: ServiceHandle<T>) => T; +} + +interface StoredFilterEntry { + readonly fn: unknown; + readonly priority: number; + readonly order: number; +} + +export function createBus(logger: Logger): Bus { + const eventHandlers = new Map<string, Set<unknown>>(); + const filterEntries = new Map<string, StoredFilterEntry[]>(); + const services = new Map<string, unknown>(); + let filterOrderCounter = 0; + + return { + on<T>(hook: EventHookDescriptor<T>, handler: EventHandler<T>): () => 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<T>(hook: EventHookDescriptor<T>, payload: T): void { + const set = eventHandlers.get(hook.id); + if (set === undefined || set.size === 0) return; + const handlers = [...set] as Array<EventHandler<T>>; + dispatchEventSync(handlers, payload, logger, hook.id); + }, + + async emitAsync<T>( + hook: EventHookDescriptor<T>, + payload: T, + timeoutMs?: number, + ): Promise<void> { + const set = eventHandlers.get(hook.id); + if (set === undefined || set.size === 0) return; + const handlers = [...set] as Array<EventHandler<T>>; + await dispatchEventAsync(handlers, payload, logger, hook.id, timeoutMs); + }, + + addFilter<T>( + hook: FilterDescriptor<T>, + fn: FilterHandler<T>, + 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<T>( + hook: FilterDescriptor<T>, + value: T, + opts?: { readonly failClosed?: boolean }, + ): Promise<T> { + const entries = filterEntries.get(hook.id); + if (entries === undefined || entries.length === 0) return value; + const sorted = sortFilters(entries as ReadonlyArray<FilterEntry<T>>); + const fns = sorted.map((e) => e.fn) as Array<FilterHandler<T>>; + return applyFilterChain(fns, value, logger, hook.id, opts?.failClosed ?? false); + }, + + provideService<T>(handle: ServiceHandle<T>, 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<T>(handle: ServiceHandle<T>): 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; + }, + }; +} |
