Initial Commit
This commit is contained in:
103
packages/foundation/flow/workflow/src/middleware/hook/hook.ts
Normal file
103
packages/foundation/flow/workflow/src/middleware/hook/hook.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
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 };
|
||||
}
|
||||
Reference in New Issue
Block a user