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.
- package/.turbo/turbo-build.log +4 -5
- package/.turbo/turbo-test.log +169 -0
- package/CHANGELOG.md +29 -0
- package/LICENSE +21 -0
- package/README.md +303 -184
- package/dist/barrier.d.ts +153 -0
- package/dist/barrier.d.ts.map +1 -0
- package/dist/barrier.js +339 -0
- package/dist/barrier.js.map +1 -0
- package/dist/cascade-context.d.ts +149 -0
- package/dist/cascade-context.d.ts.map +1 -0
- package/dist/cascade-context.js +324 -0
- package/dist/cascade-context.js.map +1 -0
- package/dist/cascade-executor.d.ts +196 -0
- package/dist/cascade-executor.d.ts.map +1 -0
- package/dist/cascade-executor.js +384 -0
- package/dist/cascade-executor.js.map +1 -0
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +4 -1
- package/dist/context.js.map +1 -1
- package/dist/dependency-graph.d.ts +157 -0
- package/dist/dependency-graph.d.ts.map +1 -0
- package/dist/dependency-graph.js +382 -0
- package/dist/dependency-graph.js.map +1 -0
- package/dist/every.d.ts +31 -2
- package/dist/every.d.ts.map +1 -1
- package/dist/every.js +63 -32
- package/dist/every.js.map +1 -1
- package/dist/graph/index.d.ts +8 -0
- package/dist/graph/index.d.ts.map +1 -0
- package/dist/graph/index.js +8 -0
- package/dist/graph/index.js.map +1 -0
- package/dist/graph/topological-sort.d.ts +121 -0
- package/dist/graph/topological-sort.d.ts.map +1 -0
- package/dist/graph/topological-sort.js +292 -0
- package/dist/graph/topological-sort.js.map +1 -0
- package/dist/index.d.ts +6 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10 -0
- package/dist/index.js.map +1 -1
- package/dist/on.d.ts +35 -10
- package/dist/on.d.ts.map +1 -1
- package/dist/on.js +52 -18
- package/dist/on.js.map +1 -1
- package/dist/send.d.ts +0 -5
- package/dist/send.d.ts.map +1 -1
- package/dist/send.js +1 -14
- package/dist/send.js.map +1 -1
- package/dist/timer-registry.d.ts +52 -0
- package/dist/timer-registry.d.ts.map +1 -0
- package/dist/timer-registry.js +120 -0
- package/dist/timer-registry.js.map +1 -0
- package/dist/types.d.ts +171 -9
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +17 -1
- package/dist/types.js.map +1 -1
- package/dist/workflow.d.ts.map +1 -1
- package/dist/workflow.js +22 -18
- package/dist/workflow.js.map +1 -1
- package/package.json +12 -16
- package/src/barrier.ts +466 -0
- package/src/cascade-context.ts +488 -0
- package/src/cascade-executor.ts +587 -0
- package/src/context.js +83 -0
- package/src/context.ts +12 -7
- package/src/dependency-graph.ts +518 -0
- package/src/every.js +267 -0
- package/src/every.ts +104 -35
- package/src/graph/index.ts +19 -0
- package/src/graph/topological-sort.ts +414 -0
- package/src/index.js +71 -0
- package/src/index.ts +78 -0
- package/src/on.js +79 -0
- package/src/on.ts +81 -25
- package/src/send.js +111 -0
- package/src/send.ts +1 -16
- package/src/timer-registry.ts +145 -0
- package/src/types.js +4 -0
- package/src/types.ts +218 -11
- package/src/workflow.js +455 -0
- package/src/workflow.ts +32 -23
- package/test/barrier-join.test.ts +434 -0
- package/test/barrier-unhandled-rejections.test.ts +359 -0
- package/test/cascade-context.test.ts +390 -0
- package/test/cascade-executor.test.ts +859 -0
- package/test/context.test.js +116 -0
- package/test/dependency-graph.test.ts +512 -0
- package/test/every.test.js +282 -0
- package/test/graph/topological-sort.test.ts +586 -0
- package/test/on.test.js +80 -0
- package/test/schedule-timer-cleanup.test.ts +344 -0
- package/test/send-race-conditions.test.ts +410 -0
- package/test/send.test.js +89 -0
- package/test/type-safety-every.test.ts +303 -0
- package/test/types-event-handler.test.ts +225 -0
- package/test/types-proxy-autocomplete.test.ts +345 -0
- package/test/workflow.test.js +224 -0
- 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 {
|
|
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
|
-
*
|
|
62
|
+
* Handler registration callback type
|
|
63
|
+
* Used by createTypedOnProxy to customize handler registration
|
|
49
64
|
*/
|
|
50
|
-
type
|
|
51
|
-
|
|
52
|
-
|
|
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
|
|
73
|
+
* Create a typed OnProxy with proper TypeScript generics
|
|
58
74
|
*
|
|
59
|
-
* This creates a proxy that allows:
|
|
60
|
-
*
|
|
61
|
-
*
|
|
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
|
|
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
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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