Files
boocode/packages/ion/src/engine/event-emitter.ts
indifferentketchup 02063072ab chore: add ion package, codesight wiki, work plans, ascli config
New @boocode/ion package (v0.0.1) for inference optimization network.
.codesight/ wiki artifacts for codebase documentation.
.omo/ work plans for openspec cleanup and enhanced file panel.
2026-06-07 22:16:45 +00:00

214 lines
5.8 KiB
TypeScript

/**
* Typed event emitter for the Ion workflow engine.
*
* Provides a singleton event bus for workflow lifecycle events.
* Supports both global and run-scoped subscriptions.
*/
// ---------------------------------------------------------------------------
// Event types
// ---------------------------------------------------------------------------
export type WorkflowEventType =
| 'workflow_started'
| 'workflow_completed'
| 'workflow_failed'
| 'workflow_cancelled'
| 'node_started'
| 'node_completed'
| 'node_failed'
| 'node_skipped'
| 'loop_iteration_started'
| 'loop_iteration_completed'
| 'approval_pending';
// ---------------------------------------------------------------------------
// Event shapes
// ---------------------------------------------------------------------------
export interface WorkflowEventBase {
/** Discriminator for the event type. */
type: WorkflowEventType;
/** The workflow run id. */
runId: string;
/** The node id (when applicable). */
nodeId?: string;
/** The workflow name. */
workflowName?: string;
/** Error message (for failure events). */
error?: string;
/** Human-readable step name. */
stepName?: string;
/** Arbitrary metadata attached to the event. */
metadata?: Record<string, unknown>;
}
/** Specific shapes for strongly-typed event handling. */
export interface WorkflowStartedEvent extends WorkflowEventBase {
type: 'workflow_started';
workflowName: string;
}
export interface WorkflowCompletedEvent extends WorkflowEventBase {
type: 'workflow_completed';
workflowName: string;
}
export interface WorkflowFailedEvent extends WorkflowEventBase {
type: 'workflow_failed';
error: string;
}
export interface WorkflowCancelledEvent extends WorkflowEventBase {
type: 'workflow_cancelled';
}
export interface NodeStartedEvent extends WorkflowEventBase {
type: 'node_started';
nodeId: string;
stepName?: string;
}
export interface NodeCompletedEvent extends WorkflowEventBase {
type: 'node_completed';
nodeId: string;
stepName?: string;
}
export interface NodeFailedEvent extends WorkflowEventBase {
type: 'node_failed';
nodeId: string;
error: string;
}
export interface NodeSkippedEvent extends WorkflowEventBase {
type: 'node_skipped';
nodeId: string;
stepName?: string;
}
export interface LoopIterationStartedEvent extends WorkflowEventBase {
type: 'loop_iteration_started';
nodeId: string;
metadata?: { iteration: number };
}
export interface LoopIterationCompletedEvent extends WorkflowEventBase {
type: 'loop_iteration_completed';
nodeId: string;
metadata?: { iteration: number };
}
export interface ApprovalPendingEvent extends WorkflowEventBase {
type: 'approval_pending';
nodeId: string;
metadata?: { approver?: string; reason?: string };
}
export type WorkflowEvent =
| WorkflowStartedEvent
| WorkflowCompletedEvent
| WorkflowFailedEvent
| WorkflowCancelledEvent
| NodeStartedEvent
| NodeCompletedEvent
| NodeFailedEvent
| NodeSkippedEvent
| LoopIterationStartedEvent
| LoopIterationCompletedEvent
| ApprovalPendingEvent;
// ---------------------------------------------------------------------------
// Event handler type
// ---------------------------------------------------------------------------
export type WorkflowEventHandler = (event: WorkflowEvent) => void;
// ---------------------------------------------------------------------------
// WorkflowEventEmitter — singleton event bus
// ---------------------------------------------------------------------------
export class WorkflowEventEmitter {
private listeners: Set<WorkflowEventHandler> = new Set();
private runListeners: Map<string, Set<WorkflowEventHandler>> = new Map();
/** Subscribe to all workflow events. */
subscribe(handler: WorkflowEventHandler): () => void {
this.listeners.add(handler);
return () => {
this.listeners.delete(handler);
};
}
/** Unsubscribe a handler from all events. */
unsubscribe(handler: WorkflowEventHandler): void {
this.listeners.delete(handler);
}
/** Emit an event to all subscribers (global + run-scoped). */
emit(event: WorkflowEvent): void {
for (const handler of this.listeners) {
try {
handler(event);
} catch {
// Swallow handler errors to prevent cascading failures.
}
}
// Also notify run-scoped listeners.
const runHandlers = this.runListeners.get(event.runId);
if (runHandlers) {
for (const handler of runHandlers) {
try {
handler(event);
} catch {
// Swallow handler errors to prevent cascading failures.
}
}
}
}
/** Remove all global listeners and run-scoped listeners. */
clear(): void {
this.listeners.clear();
this.runListeners.clear();
}
// -- Run-scoped subscriptions ------------------------------------------------
/** Register a run-scoped event listener. */
registerRun(runId: string, handler: WorkflowEventHandler): () => void {
let handlers = this.runListeners.get(runId);
if (!handlers) {
handlers = new Set();
this.runListeners.set(runId, handlers);
}
handlers.add(handler);
return () => {
handlers!.delete(handler);
if (handlers!.size === 0) {
this.runListeners.delete(runId);
}
};
}
/** Unregister all listeners for a specific run. */
unregisterRun(runId: string): void {
this.runListeners.delete(runId);
}
}
// ---------------------------------------------------------------------------
// Singleton factory
// ---------------------------------------------------------------------------
let instance: WorkflowEventEmitter | undefined;
/** Get the singleton WorkflowEventEmitter instance. */
export function getWorkflowEventEmitter(): WorkflowEventEmitter {
if (!instance) {
instance = new WorkflowEventEmitter();
}
return instance;
}