Files
suaveplan/packages/foundation/flow/workflow/src/middleware/hook/hook.ts
2026-05-27 23:47:53 +01:00

104 lines
2.8 KiB
TypeScript

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<TCtx = unknown> {
private readonly _listeners = new Map<HookEvent, HookListener<TCtx>[]>();
private readonly _clock: Clock;
constructor(options?: { clock?: Clock }) {
this._clock = options?.clock ?? defaultClock;
}
on(event: HookEvent, listener: HookListener<TCtx>): () => 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<TCtx>): void {
const wrapped: HookListener<TCtx> = (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<HookPayload<TCtx>, "event" | "timestamp">
): Promise<void> {
const listeners = this._listeners.get(event);
if (!listeners || listeners.length === 0) return;
const payload: HookPayload<TCtx> = {
...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<TCtx = unknown>(options?: {
clock?: Clock;
}): HookRegistry<TCtx> {
return new HookRegistry(options);
}
/** Helper to build a step-related hook payload. */
export function buildStepPayload<TCtx>(
workflowId: string,
context: TCtx,
stepId: string,
stepResult?: StepResult,
error?: Error
): Omit<HookPayload<TCtx>, "event" | "timestamp"> {
return { workflowId, context, stepId, stepResult, error };
}