ai-workflows 2.0.2 → 2.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (98) hide show
  1. package/.turbo/turbo-build.log +4 -5
  2. package/.turbo/turbo-test.log +169 -0
  3. package/CHANGELOG.md +29 -0
  4. package/LICENSE +21 -0
  5. package/README.md +303 -184
  6. package/dist/barrier.d.ts +153 -0
  7. package/dist/barrier.d.ts.map +1 -0
  8. package/dist/barrier.js +339 -0
  9. package/dist/barrier.js.map +1 -0
  10. package/dist/cascade-context.d.ts +149 -0
  11. package/dist/cascade-context.d.ts.map +1 -0
  12. package/dist/cascade-context.js +324 -0
  13. package/dist/cascade-context.js.map +1 -0
  14. package/dist/cascade-executor.d.ts +196 -0
  15. package/dist/cascade-executor.d.ts.map +1 -0
  16. package/dist/cascade-executor.js +384 -0
  17. package/dist/cascade-executor.js.map +1 -0
  18. package/dist/context.d.ts.map +1 -1
  19. package/dist/context.js +4 -1
  20. package/dist/context.js.map +1 -1
  21. package/dist/dependency-graph.d.ts +157 -0
  22. package/dist/dependency-graph.d.ts.map +1 -0
  23. package/dist/dependency-graph.js +382 -0
  24. package/dist/dependency-graph.js.map +1 -0
  25. package/dist/every.d.ts +31 -2
  26. package/dist/every.d.ts.map +1 -1
  27. package/dist/every.js +63 -32
  28. package/dist/every.js.map +1 -1
  29. package/dist/graph/index.d.ts +8 -0
  30. package/dist/graph/index.d.ts.map +1 -0
  31. package/dist/graph/index.js +8 -0
  32. package/dist/graph/index.js.map +1 -0
  33. package/dist/graph/topological-sort.d.ts +121 -0
  34. package/dist/graph/topological-sort.d.ts.map +1 -0
  35. package/dist/graph/topological-sort.js +292 -0
  36. package/dist/graph/topological-sort.js.map +1 -0
  37. package/dist/index.d.ts +6 -1
  38. package/dist/index.d.ts.map +1 -1
  39. package/dist/index.js +10 -0
  40. package/dist/index.js.map +1 -1
  41. package/dist/on.d.ts +35 -10
  42. package/dist/on.d.ts.map +1 -1
  43. package/dist/on.js +52 -18
  44. package/dist/on.js.map +1 -1
  45. package/dist/send.d.ts +0 -5
  46. package/dist/send.d.ts.map +1 -1
  47. package/dist/send.js +1 -14
  48. package/dist/send.js.map +1 -1
  49. package/dist/timer-registry.d.ts +52 -0
  50. package/dist/timer-registry.d.ts.map +1 -0
  51. package/dist/timer-registry.js +120 -0
  52. package/dist/timer-registry.js.map +1 -0
  53. package/dist/types.d.ts +171 -9
  54. package/dist/types.d.ts.map +1 -1
  55. package/dist/types.js +17 -1
  56. package/dist/types.js.map +1 -1
  57. package/dist/workflow.d.ts.map +1 -1
  58. package/dist/workflow.js +22 -18
  59. package/dist/workflow.js.map +1 -1
  60. package/package.json +12 -16
  61. package/src/barrier.ts +466 -0
  62. package/src/cascade-context.ts +488 -0
  63. package/src/cascade-executor.ts +587 -0
  64. package/src/context.js +83 -0
  65. package/src/context.ts +12 -7
  66. package/src/dependency-graph.ts +518 -0
  67. package/src/every.js +267 -0
  68. package/src/every.ts +104 -35
  69. package/src/graph/index.ts +19 -0
  70. package/src/graph/topological-sort.ts +414 -0
  71. package/src/index.js +71 -0
  72. package/src/index.ts +78 -0
  73. package/src/on.js +79 -0
  74. package/src/on.ts +81 -25
  75. package/src/send.js +111 -0
  76. package/src/send.ts +1 -16
  77. package/src/timer-registry.ts +145 -0
  78. package/src/types.js +4 -0
  79. package/src/types.ts +218 -11
  80. package/src/workflow.js +455 -0
  81. package/src/workflow.ts +32 -23
  82. package/test/barrier-join.test.ts +434 -0
  83. package/test/barrier-unhandled-rejections.test.ts +359 -0
  84. package/test/cascade-context.test.ts +390 -0
  85. package/test/cascade-executor.test.ts +859 -0
  86. package/test/context.test.js +116 -0
  87. package/test/dependency-graph.test.ts +512 -0
  88. package/test/every.test.js +282 -0
  89. package/test/graph/topological-sort.test.ts +586 -0
  90. package/test/on.test.js +80 -0
  91. package/test/schedule-timer-cleanup.test.ts +344 -0
  92. package/test/send-race-conditions.test.ts +410 -0
  93. package/test/send.test.js +89 -0
  94. package/test/type-safety-every.test.ts +303 -0
  95. package/test/types-event-handler.test.ts +225 -0
  96. package/test/types-proxy-autocomplete.test.ts +345 -0
  97. package/test/workflow.test.js +224 -0
  98. package/vitest.config.js +7 -0
package/src/on.ts CHANGED
@@ -5,9 +5,21 @@
5
5
  * on.Customer.created(customer => { ... })
6
6
  * on.Order.completed(order => { ... })
7
7
  * on.Payment.failed(payment => { ... })
8
+ *
9
+ * With dependencies:
10
+ * on.Step2.complete(handler, { dependsOn: 'Step1.complete' })
11
+ * on.Step3.complete(handler, { dependsOn: ['Step1.complete', 'Step2.complete'] })
8
12
  */
9
13
 
10
- import type { EventHandler, EventRegistration } from './types.js'
14
+ import type {
15
+ EventHandler,
16
+ EventRegistration,
17
+ DependencyConfig,
18
+ OnProxy,
19
+ NounEventProxy,
20
+ OnProxyHandler,
21
+ NounEventProxyHandler,
22
+ } from './types.js'
11
23
 
12
24
  /**
13
25
  * Registry of event handlers
@@ -34,50 +46,94 @@ export function clearEventHandlers(): void {
34
46
  export function registerEventHandler(
35
47
  noun: string,
36
48
  event: string,
37
- handler: EventHandler
49
+ handler: EventHandler,
50
+ dependencies?: DependencyConfig
38
51
  ): void {
39
52
  eventRegistry.push({
40
53
  noun,
41
54
  event,
42
55
  handler,
43
56
  source: handler.toString(),
57
+ dependencies,
44
58
  })
45
59
  }
46
60
 
47
61
  /**
48
- * Event proxy type for on.Noun.event pattern
62
+ * Handler registration callback type
63
+ * Used by createTypedOnProxy to customize handler registration
49
64
  */
50
- type EventProxy = {
51
- [noun: string]: {
52
- [event: string]: (handler: EventHandler) => void
53
- }
54
- }
65
+ export type OnProxyRegistrationCallback = (
66
+ noun: string,
67
+ event: string,
68
+ handler: EventHandler,
69
+ dependencies?: DependencyConfig
70
+ ) => void
55
71
 
56
72
  /**
57
- * Create the `on` proxy
73
+ * Create a typed OnProxy with proper TypeScript generics
58
74
  *
59
- * This creates a proxy that allows:
60
- * on.Customer.created(handler)
61
- * on.Order.shipped(handler)
75
+ * This factory function creates a two-level proxy that allows:
76
+ * proxy.Customer.created(handler)
77
+ * proxy.Order.shipped(handler)
62
78
  *
63
79
  * The first property access captures the noun (Customer, Order)
64
80
  * The second property access captures the event (created, shipped)
65
- * The function call registers the handler
81
+ * The function call invokes the registration callback
82
+ *
83
+ * @param registerCallback - Function called when a handler is registered
84
+ * @returns A properly typed OnProxy
85
+ *
86
+ * @example
87
+ * ```ts
88
+ * // Create proxy with custom registration
89
+ * const myOn = createTypedOnProxy((noun, event, handler, deps) => {
90
+ * myRegistry.push({ noun, event, handler, deps })
91
+ * })
92
+ *
93
+ * myOn.Customer.created(handler) // Properly typed!
94
+ * ```
66
95
  */
67
- function createOnProxy(): EventProxy {
68
- return new Proxy({} as EventProxy, {
69
- get(_target, noun: string) {
70
- // Return a proxy for the event level
71
- return new Proxy({}, {
72
- get(_eventTarget, event: string) {
73
- // Return a function that registers the handler
74
- return (handler: EventHandler) => {
75
- registerEventHandler(noun, event, handler)
76
- }
77
- }
78
- })
96
+ export function createTypedOnProxy(registerCallback: OnProxyRegistrationCallback): OnProxy {
97
+ // Create typed handler for the noun level (event accessors)
98
+ const createNounHandler = (noun: string): NounEventProxyHandler => ({
99
+ get(
100
+ _target: Record<string, (handler: EventHandler, dependencies?: DependencyConfig) => void>,
101
+ event: string,
102
+ _receiver: unknown
103
+ ): (handler: EventHandler, dependencies?: DependencyConfig) => void {
104
+ // Return a function that registers the handler with optional dependencies
105
+ return (handler: EventHandler, dependencies?: DependencyConfig) => {
106
+ registerCallback(noun, event, handler, dependencies)
107
+ }
79
108
  }
80
109
  })
110
+
111
+ // Create typed handler for the top-level proxy (noun accessors)
112
+ const onHandler: OnProxyHandler = {
113
+ get(
114
+ _target: Record<string, NounEventProxy>,
115
+ noun: string,
116
+ _receiver: unknown
117
+ ): NounEventProxy {
118
+ // Return a proxy for the event level with typed handler
119
+ const eventTarget: Record<string, (handler: EventHandler, dependencies?: DependencyConfig) => void> = {}
120
+ return new Proxy(eventTarget, createNounHandler(noun)) as NounEventProxy
121
+ }
122
+ }
123
+
124
+ // Create and return the typed OnProxy
125
+ const target: Record<string, NounEventProxy> = {}
126
+ return new Proxy(target, onHandler) as OnProxy
127
+ }
128
+
129
+ /**
130
+ * Create the `on` proxy using the global event registry
131
+ *
132
+ * This is the default implementation that uses registerEventHandler
133
+ * for backward compatibility with the standalone `on` export.
134
+ */
135
+ function createOnProxy(): OnProxy {
136
+ return createTypedOnProxy(registerEventHandler)
81
137
  }
82
138
 
83
139
  /**
package/src/send.js ADDED
@@ -0,0 +1,111 @@
1
+ /**
2
+ * Event emission using send('Noun.event', data)
3
+ *
4
+ * Usage:
5
+ * send('Customer.created', customer)
6
+ * send('Order.completed', order)
7
+ */
8
+ import { getEventHandlers } from './on.js';
9
+ import { createWorkflowContext } from './context.js';
10
+ /**
11
+ * Event bus for managing event delivery
12
+ */
13
+ class EventBus {
14
+ pending = [];
15
+ processing = false;
16
+ /**
17
+ * Emit an event
18
+ */
19
+ async emit(event, data) {
20
+ this.pending.push({ event, data });
21
+ if (!this.processing) {
22
+ await this.process();
23
+ }
24
+ }
25
+ /**
26
+ * Process pending events
27
+ */
28
+ async process() {
29
+ this.processing = true;
30
+ while (this.pending.length > 0) {
31
+ const item = this.pending.shift();
32
+ await this.deliver(item.event, item.data);
33
+ }
34
+ this.processing = false;
35
+ }
36
+ /**
37
+ * Deliver an event to matching handlers
38
+ */
39
+ async deliver(event, data) {
40
+ const parsed = parseEvent(event);
41
+ if (!parsed) {
42
+ console.warn(`Invalid event format: ${event}. Expected Noun.event`);
43
+ return;
44
+ }
45
+ const handlers = getEventHandlers();
46
+ const matching = handlers.filter(h => h.noun === parsed.noun && h.event === parsed.event);
47
+ if (matching.length === 0) {
48
+ // No handlers registered - that's okay, event is just not handled
49
+ return;
50
+ }
51
+ // Create workflow context for handlers
52
+ const ctx = createWorkflowContext(this);
53
+ // Execute all matching handlers
54
+ await Promise.all(matching.map(async ({ handler }) => {
55
+ try {
56
+ await handler(data, ctx);
57
+ }
58
+ catch (error) {
59
+ console.error(`Error in handler for ${event}:`, error);
60
+ }
61
+ }));
62
+ }
63
+ }
64
+ /**
65
+ * Parse event string into noun and event
66
+ */
67
+ export function parseEvent(event) {
68
+ const parts = event.split('.');
69
+ if (parts.length !== 2) {
70
+ return null;
71
+ }
72
+ const [noun, eventName] = parts;
73
+ if (!noun || !eventName) {
74
+ return null;
75
+ }
76
+ return { noun, event: eventName };
77
+ }
78
+ /**
79
+ * Global event bus instance
80
+ */
81
+ const globalEventBus = new EventBus();
82
+ /**
83
+ * Send an event
84
+ *
85
+ * @example
86
+ * ```ts
87
+ * import { send } from 'ai-workflows'
88
+ *
89
+ * // Emit a customer created event
90
+ * await send('Customer.created', {
91
+ * id: '123',
92
+ * name: 'John Doe',
93
+ * email: 'john@example.com'
94
+ * })
95
+ *
96
+ * // Emit an order completed event
97
+ * await send('Order.completed', {
98
+ * id: 'order-456',
99
+ * total: 99.99
100
+ * })
101
+ * ```
102
+ */
103
+ export async function send(event, data) {
104
+ await globalEventBus.emit(event, data);
105
+ }
106
+ /**
107
+ * Get the global event bus (for testing/internal use)
108
+ */
109
+ export function getEventBus() {
110
+ return globalEventBus;
111
+ }
package/src/send.ts CHANGED
@@ -6,9 +6,9 @@
6
6
  * send('Order.completed', order)
7
7
  */
8
8
 
9
- import type { ParsedEvent } from './types.js'
10
9
  import { getEventHandlers } from './on.js'
11
10
  import { createWorkflowContext } from './context.js'
11
+ import { parseEvent } from './workflow.js'
12
12
 
13
13
  /**
14
14
  * Event bus for managing event delivery
@@ -78,21 +78,6 @@ class EventBus {
78
78
  }
79
79
  }
80
80
 
81
- /**
82
- * Parse event string into noun and event
83
- */
84
- export function parseEvent(event: string): ParsedEvent | null {
85
- const parts = event.split('.')
86
- if (parts.length !== 2) {
87
- return null
88
- }
89
- const [noun, eventName] = parts
90
- if (!noun || !eventName) {
91
- return null
92
- }
93
- return { noun, event: eventName }
94
- }
95
-
96
81
  /**
97
82
  * Global event bus instance
98
83
  */
@@ -0,0 +1,145 @@
1
+ /**
2
+ * Global timer registry for workflow timers
3
+ *
4
+ * This module tracks all active timers across workflows to enable:
5
+ * - Timer cleanup when workflows are destroyed
6
+ * - Global timer count for debugging
7
+ * - Process exit cleanup
8
+ */
9
+
10
+ interface TimerEntry {
11
+ timerId: NodeJS.Timeout
12
+ workflowId: string
13
+ registeredAt: number
14
+ }
15
+
16
+ /**
17
+ * Global registry of active timers
18
+ */
19
+ const activeTimers: Map<string, TimerEntry> = new Map()
20
+
21
+ /**
22
+ * Counter for generating unique timer IDs
23
+ */
24
+ let timerCounter = 0
25
+
26
+ /**
27
+ * Generate a unique timer ID
28
+ */
29
+ function generateTimerId(workflowId: string): string {
30
+ return `${workflowId}-timer-${++timerCounter}`
31
+ }
32
+
33
+ /**
34
+ * Register a timer in the global registry
35
+ */
36
+ export function registerTimer(workflowId: string, timerId: NodeJS.Timeout): string {
37
+ const id = generateTimerId(workflowId)
38
+ activeTimers.set(id, {
39
+ timerId,
40
+ workflowId,
41
+ registeredAt: Date.now(),
42
+ })
43
+ return id
44
+ }
45
+
46
+ /**
47
+ * Unregister a timer from the global registry
48
+ */
49
+ export function unregisterTimer(id: string): boolean {
50
+ const entry = activeTimers.get(id)
51
+ if (entry) {
52
+ clearInterval(entry.timerId)
53
+ activeTimers.delete(id)
54
+ return true
55
+ }
56
+ return false
57
+ }
58
+
59
+ /**
60
+ * Get all timer IDs for a specific workflow
61
+ */
62
+ export function getTimerIdsForWorkflow(workflowId: string): string[] {
63
+ const ids: string[] = []
64
+ for (const [id, entry] of activeTimers) {
65
+ if (entry.workflowId === workflowId) {
66
+ ids.push(id)
67
+ }
68
+ }
69
+ return ids
70
+ }
71
+
72
+ /**
73
+ * Clear all timers for a specific workflow
74
+ */
75
+ export function clearTimersForWorkflow(workflowId: string): number {
76
+ const ids = getTimerIdsForWorkflow(workflowId)
77
+ for (const id of ids) {
78
+ unregisterTimer(id)
79
+ }
80
+ return ids.length
81
+ }
82
+
83
+ /**
84
+ * Get the count of all active timers
85
+ */
86
+ export function getActiveTimerCount(): number {
87
+ return activeTimers.size
88
+ }
89
+
90
+ /**
91
+ * Clear all timers from all workflows
92
+ */
93
+ export function clearAllTimers(): void {
94
+ for (const [id, entry] of activeTimers) {
95
+ clearInterval(entry.timerId)
96
+ activeTimers.delete(id)
97
+ }
98
+ }
99
+
100
+ /**
101
+ * Timer registry object for external access
102
+ */
103
+ export const timerRegistry = {
104
+ register: registerTimer,
105
+ unregister: unregisterTimer,
106
+ getTimerIdsForWorkflow,
107
+ clearForWorkflow: clearTimersForWorkflow,
108
+ getActiveCount: getActiveTimerCount,
109
+ clearAll: clearAllTimers,
110
+ getAll: () => Array.from(activeTimers.entries()),
111
+ }
112
+
113
+ // Register global cleanup functions immediately on module load
114
+ // Register on both globalThis and global for maximum compatibility
115
+ declare const global: typeof globalThis
116
+
117
+ function registerGlobalFunctions(target: typeof globalThis) {
118
+ (target as unknown as Record<string, unknown>).getActiveWorkflowTimerCount = getActiveTimerCount;
119
+ (target as unknown as Record<string, unknown>).clearAllWorkflowTimers = clearAllTimers
120
+ }
121
+
122
+ // Register on globalThis (standard)
123
+ if (typeof globalThis !== 'undefined') {
124
+ registerGlobalFunctions(globalThis)
125
+ }
126
+
127
+ // Also register on global (Node.js specific, used in some test environments)
128
+ if (typeof global !== 'undefined' && global !== globalThis) {
129
+ registerGlobalFunctions(global)
130
+ }
131
+
132
+ // Register process exit handlers for cleanup
133
+ let cleanupRegistered = false
134
+
135
+ export function registerProcessCleanup(): void {
136
+ if (cleanupRegistered) return
137
+ cleanupRegistered = true
138
+
139
+ const cleanup = () => {
140
+ clearAllTimers()
141
+ }
142
+
143
+ process.on('exit', cleanup)
144
+ process.on('beforeExit', cleanup)
145
+ }
package/src/types.js ADDED
@@ -0,0 +1,4 @@
1
+ /**
2
+ * Types for ai-workflows
3
+ */
4
+ export {};