/** * 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; } /** 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 = new Set(); private runListeners: Map> = 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; }