104 lines
2.8 KiB
TypeScript
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 };
|
|
}
|