ai-workflows 2.1.1 → 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 +1 -1
- package/.turbo/turbo-test.log +165 -3
- package/CHANGELOG.md +10 -1
- 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/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 +88 -0
- 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 +15 -11
- package/dist/workflow.js.map +1 -1
- package/package.json +11 -11
- package/src/barrier.ts +466 -0
- package/src/cascade-context.ts +488 -0
- package/src/cascade-executor.ts +587 -0
- package/src/context.ts +12 -7
- package/src/dependency-graph.ts +518 -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.ts +78 -0
- package/src/on.ts +81 -25
- package/src/timer-registry.ts +145 -0
- package/src/types.ts +121 -0
- package/src/workflow.ts +23 -16
- 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/dependency-graph.test.ts +512 -0
- package/test/graph/topological-sort.test.ts +586 -0
- package/test/schedule-timer-cleanup.test.ts +344 -0
- package/test/send-race-conditions.test.ts +410 -0
- package/test/type-safety-every.test.ts +303 -0
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Topological Sort Implementation for Workflow Execution Ordering
|
|
3
|
+
*
|
|
4
|
+
* Provides multiple algorithms for topologically sorting workflow steps:
|
|
5
|
+
* - Kahn's algorithm (BFS-based, good for detecting cycles)
|
|
6
|
+
* - DFS-based algorithm (classic approach, provides cycle path)
|
|
7
|
+
*
|
|
8
|
+
* Features:
|
|
9
|
+
* - Cycle detection with path reporting
|
|
10
|
+
* - Parallel execution level grouping
|
|
11
|
+
* - Stable, deterministic ordering
|
|
12
|
+
* - Support for missing dependency handling
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* A node that can be topologically sorted
|
|
17
|
+
*/
|
|
18
|
+
export interface SortableNode {
|
|
19
|
+
/** Unique identifier for the node */
|
|
20
|
+
id: string
|
|
21
|
+
/** IDs of nodes this node depends on */
|
|
22
|
+
dependencies: string[]
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Execution level containing nodes that can run in parallel
|
|
27
|
+
*/
|
|
28
|
+
export interface ExecutionLevel {
|
|
29
|
+
/** Level number (0 = no dependencies, 1 = depends on level 0, etc.) */
|
|
30
|
+
level: number
|
|
31
|
+
/** Node IDs that can run concurrently at this level */
|
|
32
|
+
nodes: string[]
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Options for topological sort
|
|
37
|
+
*/
|
|
38
|
+
export interface TopologicalSortOptions {
|
|
39
|
+
/** Throw CycleDetectedError instead of returning hasCycle: true (default: false) */
|
|
40
|
+
throwOnCycle?: boolean
|
|
41
|
+
/** Which algorithm to use (default: 'kahn') */
|
|
42
|
+
algorithm?: 'kahn' | 'dfs'
|
|
43
|
+
/** Throw on missing dependencies (default: false) */
|
|
44
|
+
strict?: boolean
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Result of topological sort operation
|
|
49
|
+
*/
|
|
50
|
+
export interface TopologicalSortResult {
|
|
51
|
+
/** Sorted node IDs in execution order */
|
|
52
|
+
order: string[]
|
|
53
|
+
/** Whether a cycle was detected */
|
|
54
|
+
hasCycle: boolean
|
|
55
|
+
/** Path of nodes forming the cycle (if detected) */
|
|
56
|
+
cyclePath?: string[]
|
|
57
|
+
/** Additional metadata from the algorithm */
|
|
58
|
+
metadata?: {
|
|
59
|
+
/** In-degrees for each node (Kahn's algorithm) */
|
|
60
|
+
inDegrees?: Record<string, number>
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Error thrown when a cycle is detected in the dependency graph
|
|
66
|
+
*/
|
|
67
|
+
export class CycleDetectedError extends Error {
|
|
68
|
+
/** The path of nodes forming the cycle */
|
|
69
|
+
cyclePath: string[]
|
|
70
|
+
|
|
71
|
+
constructor(cyclePath: string[]) {
|
|
72
|
+
const pathStr = cyclePath.join(' -> ')
|
|
73
|
+
super(`Cycle detected in dependency graph: ${pathStr}`)
|
|
74
|
+
this.name = 'CycleDetectedError'
|
|
75
|
+
this.cyclePath = cyclePath
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Error thrown when a dependency references a non-existent node
|
|
81
|
+
*/
|
|
82
|
+
export class MissingNodeError extends Error {
|
|
83
|
+
/** The missing node ID */
|
|
84
|
+
missingNode: string
|
|
85
|
+
/** The node that references the missing node */
|
|
86
|
+
referencedBy: string
|
|
87
|
+
|
|
88
|
+
constructor(missingNode: string, referencedBy: string) {
|
|
89
|
+
super(
|
|
90
|
+
`Missing dependency '${missingNode}' referenced by '${referencedBy}'`
|
|
91
|
+
)
|
|
92
|
+
this.name = 'MissingNodeError'
|
|
93
|
+
this.missingNode = missingNode
|
|
94
|
+
this.referencedBy = referencedBy
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Build adjacency list and compute in-degrees for Kahn's algorithm
|
|
100
|
+
*/
|
|
101
|
+
function buildGraph(
|
|
102
|
+
nodes: SortableNode[],
|
|
103
|
+
strict: boolean
|
|
104
|
+
): {
|
|
105
|
+
adjacencyList: Map<string, string[]>
|
|
106
|
+
inDegrees: Map<string, number>
|
|
107
|
+
nodeSet: Set<string>
|
|
108
|
+
} {
|
|
109
|
+
const nodeSet = new Set(nodes.map((n) => n.id))
|
|
110
|
+
const adjacencyList = new Map<string, string[]>()
|
|
111
|
+
const inDegrees = new Map<string, number>()
|
|
112
|
+
|
|
113
|
+
// Initialize all nodes
|
|
114
|
+
for (const node of nodes) {
|
|
115
|
+
adjacencyList.set(node.id, [])
|
|
116
|
+
inDegrees.set(node.id, 0)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Build edges (dependency -> dependent)
|
|
120
|
+
for (const node of nodes) {
|
|
121
|
+
// Deduplicate dependencies
|
|
122
|
+
const uniqueDeps = [...new Set(node.dependencies)]
|
|
123
|
+
|
|
124
|
+
for (const dep of uniqueDeps) {
|
|
125
|
+
if (!nodeSet.has(dep)) {
|
|
126
|
+
if (strict) {
|
|
127
|
+
throw new MissingNodeError(dep, node.id)
|
|
128
|
+
}
|
|
129
|
+
// Skip missing dependencies in non-strict mode
|
|
130
|
+
continue
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Add edge from dependency to this node
|
|
134
|
+
adjacencyList.get(dep)!.push(node.id)
|
|
135
|
+
inDegrees.set(node.id, inDegrees.get(node.id)! + 1)
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return { adjacencyList, inDegrees, nodeSet }
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Topological sort using Kahn's algorithm (BFS-based)
|
|
144
|
+
*
|
|
145
|
+
* Algorithm:
|
|
146
|
+
* 1. Calculate in-degree for each node
|
|
147
|
+
* 2. Add all nodes with in-degree 0 to queue
|
|
148
|
+
* 3. While queue not empty:
|
|
149
|
+
* - Remove node from queue, add to result
|
|
150
|
+
* - Decrease in-degree of all dependents
|
|
151
|
+
* - Add nodes with in-degree 0 to queue
|
|
152
|
+
* 4. If result size < node count, cycle exists
|
|
153
|
+
*/
|
|
154
|
+
export function topologicalSortKahn(
|
|
155
|
+
nodes: SortableNode[],
|
|
156
|
+
options: TopologicalSortOptions = {}
|
|
157
|
+
): TopologicalSortResult {
|
|
158
|
+
const { strict = false } = options
|
|
159
|
+
|
|
160
|
+
if (nodes.length === 0) {
|
|
161
|
+
return { order: [], hasCycle: false }
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const { adjacencyList, inDegrees, nodeSet } = buildGraph(nodes, strict)
|
|
165
|
+
|
|
166
|
+
const order: string[] = []
|
|
167
|
+
const inDegreesCopy = new Map(inDegrees)
|
|
168
|
+
|
|
169
|
+
// Start with nodes that have no dependencies (in-degree 0)
|
|
170
|
+
// Sort alphabetically for determinism
|
|
171
|
+
const queue: string[] = [...nodeSet]
|
|
172
|
+
.filter((id) => inDegreesCopy.get(id) === 0)
|
|
173
|
+
.sort()
|
|
174
|
+
|
|
175
|
+
while (queue.length > 0) {
|
|
176
|
+
// Sort queue for deterministic ordering
|
|
177
|
+
queue.sort()
|
|
178
|
+
const current = queue.shift()!
|
|
179
|
+
order.push(current)
|
|
180
|
+
|
|
181
|
+
// Decrease in-degree for all dependents
|
|
182
|
+
for (const dependent of adjacencyList.get(current) || []) {
|
|
183
|
+
const newDegree = inDegreesCopy.get(dependent)! - 1
|
|
184
|
+
inDegreesCopy.set(dependent, newDegree)
|
|
185
|
+
|
|
186
|
+
if (newDegree === 0) {
|
|
187
|
+
queue.push(dependent)
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// If we didn't process all nodes, there's a cycle
|
|
193
|
+
const hasCycle = order.length < nodeSet.size
|
|
194
|
+
|
|
195
|
+
// Convert in-degrees to record
|
|
196
|
+
const inDegreesRecord: Record<string, number> = {}
|
|
197
|
+
for (const [id, degree] of inDegrees) {
|
|
198
|
+
inDegreesRecord[id] = degree
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return {
|
|
202
|
+
order,
|
|
203
|
+
hasCycle,
|
|
204
|
+
metadata: {
|
|
205
|
+
inDegrees: inDegreesRecord,
|
|
206
|
+
},
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Topological sort using DFS-based algorithm
|
|
212
|
+
*
|
|
213
|
+
* Algorithm:
|
|
214
|
+
* 1. For each unvisited node:
|
|
215
|
+
* - Mark as "in progress"
|
|
216
|
+
* - Recursively visit all dependencies
|
|
217
|
+
* - Mark as "visited" and add to result (in reverse)
|
|
218
|
+
* 2. If we encounter a node "in progress", we found a cycle
|
|
219
|
+
*/
|
|
220
|
+
export function topologicalSortDFS(
|
|
221
|
+
nodes: SortableNode[],
|
|
222
|
+
options: TopologicalSortOptions = {}
|
|
223
|
+
): TopologicalSortResult {
|
|
224
|
+
const { strict = false } = options
|
|
225
|
+
|
|
226
|
+
if (nodes.length === 0) {
|
|
227
|
+
return { order: [], hasCycle: false }
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const nodeMap = new Map(nodes.map((n) => [n.id, n]))
|
|
231
|
+
const nodeSet = new Set(nodes.map((n) => n.id))
|
|
232
|
+
|
|
233
|
+
const visited = new Set<string>()
|
|
234
|
+
const inProgress = new Set<string>()
|
|
235
|
+
const order: string[] = []
|
|
236
|
+
let cyclePath: string[] | undefined
|
|
237
|
+
|
|
238
|
+
function dfs(nodeId: string, path: string[]): boolean {
|
|
239
|
+
if (inProgress.has(nodeId)) {
|
|
240
|
+
// Found a cycle - construct the cycle path
|
|
241
|
+
const cycleStart = path.indexOf(nodeId)
|
|
242
|
+
cyclePath = [...path.slice(cycleStart), nodeId]
|
|
243
|
+
return true // Cycle detected
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (visited.has(nodeId)) {
|
|
247
|
+
return false // Already processed
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
inProgress.add(nodeId)
|
|
251
|
+
path.push(nodeId)
|
|
252
|
+
|
|
253
|
+
const node = nodeMap.get(nodeId)
|
|
254
|
+
if (node) {
|
|
255
|
+
// Deduplicate and sort dependencies for determinism
|
|
256
|
+
const uniqueDeps = [...new Set(node.dependencies)].sort()
|
|
257
|
+
|
|
258
|
+
for (const dep of uniqueDeps) {
|
|
259
|
+
if (!nodeSet.has(dep)) {
|
|
260
|
+
if (strict) {
|
|
261
|
+
throw new MissingNodeError(dep, nodeId)
|
|
262
|
+
}
|
|
263
|
+
continue // Skip missing deps in non-strict mode
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
if (dfs(dep, path)) {
|
|
267
|
+
return true // Propagate cycle detection
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
path.pop()
|
|
273
|
+
inProgress.delete(nodeId)
|
|
274
|
+
visited.add(nodeId)
|
|
275
|
+
order.push(nodeId)
|
|
276
|
+
|
|
277
|
+
return false
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Process nodes in sorted order for determinism
|
|
281
|
+
const sortedNodeIds = [...nodeSet].sort()
|
|
282
|
+
|
|
283
|
+
for (const nodeId of sortedNodeIds) {
|
|
284
|
+
if (!visited.has(nodeId)) {
|
|
285
|
+
if (dfs(nodeId, [])) {
|
|
286
|
+
break // Stop on first cycle
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const hasCycle = cyclePath !== undefined
|
|
292
|
+
|
|
293
|
+
return {
|
|
294
|
+
order: hasCycle ? order : order, // DFS produces correct order
|
|
295
|
+
hasCycle,
|
|
296
|
+
cyclePath,
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Main topological sort function with algorithm selection
|
|
302
|
+
*
|
|
303
|
+
* @param nodes - Array of nodes to sort
|
|
304
|
+
* @param options - Sort options
|
|
305
|
+
* @returns Sorted result with order and cycle information
|
|
306
|
+
*/
|
|
307
|
+
export function topologicalSort(
|
|
308
|
+
nodes: SortableNode[],
|
|
309
|
+
options: TopologicalSortOptions = {}
|
|
310
|
+
): TopologicalSortResult {
|
|
311
|
+
const { algorithm = 'dfs', throwOnCycle = false } = options
|
|
312
|
+
|
|
313
|
+
let result: TopologicalSortResult
|
|
314
|
+
|
|
315
|
+
if (algorithm === 'kahn') {
|
|
316
|
+
result = topologicalSortKahn(nodes, options)
|
|
317
|
+
|
|
318
|
+
// Kahn's algorithm doesn't provide cycle path, so detect it separately
|
|
319
|
+
if (result.hasCycle) {
|
|
320
|
+
const dfsResult = topologicalSortDFS(nodes, options)
|
|
321
|
+
result.cyclePath = dfsResult.cyclePath
|
|
322
|
+
}
|
|
323
|
+
} else {
|
|
324
|
+
result = topologicalSortDFS(nodes, options)
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
if (result.hasCycle && throwOnCycle) {
|
|
328
|
+
throw new CycleDetectedError(result.cyclePath || ['unknown'])
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
return result
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Get execution levels for parallel execution
|
|
336
|
+
*
|
|
337
|
+
* Groups nodes by their execution level:
|
|
338
|
+
* - Level 0: Nodes with no dependencies
|
|
339
|
+
* - Level N: Nodes whose dependencies are all at level < N
|
|
340
|
+
*
|
|
341
|
+
* @param nodes - Array of nodes to analyze
|
|
342
|
+
* @returns Array of execution levels, sorted by level number
|
|
343
|
+
* @throws CycleDetectedError if a cycle is detected
|
|
344
|
+
*/
|
|
345
|
+
export function getExecutionLevels(nodes: SortableNode[]): ExecutionLevel[] {
|
|
346
|
+
if (nodes.length === 0) {
|
|
347
|
+
return []
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// First, verify no cycles exist
|
|
351
|
+
const sortResult = topologicalSort(nodes, { throwOnCycle: true })
|
|
352
|
+
|
|
353
|
+
const nodeMap = new Map(nodes.map((n) => [n.id, n]))
|
|
354
|
+
const nodeSet = new Set(nodes.map((n) => n.id))
|
|
355
|
+
const levels = new Map<string, number>()
|
|
356
|
+
|
|
357
|
+
// Calculate level for each node
|
|
358
|
+
function calculateLevel(nodeId: string): number {
|
|
359
|
+
if (levels.has(nodeId)) {
|
|
360
|
+
return levels.get(nodeId)!
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
const node = nodeMap.get(nodeId)
|
|
364
|
+
if (!node) {
|
|
365
|
+
return 0
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// Filter to only existing dependencies
|
|
369
|
+
const validDeps = node.dependencies.filter((d) => nodeSet.has(d))
|
|
370
|
+
|
|
371
|
+
if (validDeps.length === 0) {
|
|
372
|
+
levels.set(nodeId, 0)
|
|
373
|
+
return 0
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
let maxDepLevel = -1
|
|
377
|
+
for (const dep of validDeps) {
|
|
378
|
+
const depLevel = calculateLevel(dep)
|
|
379
|
+
maxDepLevel = Math.max(maxDepLevel, depLevel)
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
const level = maxDepLevel + 1
|
|
383
|
+
levels.set(nodeId, level)
|
|
384
|
+
return level
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// Calculate levels for all nodes
|
|
388
|
+
for (const node of nodes) {
|
|
389
|
+
calculateLevel(node.id)
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// Group nodes by level
|
|
393
|
+
const levelGroups = new Map<number, string[]>()
|
|
394
|
+
for (const [nodeId, level] of levels) {
|
|
395
|
+
if (!levelGroups.has(level)) {
|
|
396
|
+
levelGroups.set(level, [])
|
|
397
|
+
}
|
|
398
|
+
levelGroups.get(level)!.push(nodeId)
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// Convert to sorted array of ExecutionLevels
|
|
402
|
+
const result: ExecutionLevel[] = []
|
|
403
|
+
const sortedLevels = [...levelGroups.keys()].sort((a, b) => a - b)
|
|
404
|
+
|
|
405
|
+
for (const level of sortedLevels) {
|
|
406
|
+
result.push({
|
|
407
|
+
level,
|
|
408
|
+
// Sort nodes within level for determinism
|
|
409
|
+
nodes: levelGroups.get(level)!.sort(),
|
|
410
|
+
})
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
return result
|
|
414
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -84,6 +84,82 @@ export { send, getEventBus } from './send.js'
|
|
|
84
84
|
// Context
|
|
85
85
|
export { createWorkflowContext, createIsolatedContext } from './context.js'
|
|
86
86
|
|
|
87
|
+
// Cascade Context - Correlation IDs and step metadata
|
|
88
|
+
export {
|
|
89
|
+
createCascadeContext,
|
|
90
|
+
recordStep,
|
|
91
|
+
withCascadeContext,
|
|
92
|
+
type CascadeContext,
|
|
93
|
+
type CascadeStep,
|
|
94
|
+
type CascadeContextOptions,
|
|
95
|
+
type SerializedCascadeContext,
|
|
96
|
+
type SerializedCascadeStep,
|
|
97
|
+
type TraceContext,
|
|
98
|
+
type FiveWHEvent,
|
|
99
|
+
type StepStatus,
|
|
100
|
+
} from './cascade-context.js'
|
|
101
|
+
|
|
102
|
+
// Dependency Graph
|
|
103
|
+
export {
|
|
104
|
+
DependencyGraph,
|
|
105
|
+
CircularDependencyError,
|
|
106
|
+
MissingDependencyError,
|
|
107
|
+
type GraphNode,
|
|
108
|
+
type ParallelGroup,
|
|
109
|
+
type GraphJSON,
|
|
110
|
+
type EventRegistrationWithDeps,
|
|
111
|
+
} from './dependency-graph.js'
|
|
112
|
+
|
|
113
|
+
// Topological Sort - Execution ordering algorithms
|
|
114
|
+
export {
|
|
115
|
+
topologicalSort,
|
|
116
|
+
topologicalSortKahn,
|
|
117
|
+
topologicalSortDFS,
|
|
118
|
+
getExecutionLevels,
|
|
119
|
+
CycleDetectedError,
|
|
120
|
+
MissingNodeError,
|
|
121
|
+
type SortableNode,
|
|
122
|
+
type ExecutionLevel,
|
|
123
|
+
type TopologicalSortOptions,
|
|
124
|
+
type TopologicalSortResult,
|
|
125
|
+
} from './graph/topological-sort.js'
|
|
126
|
+
|
|
127
|
+
// Barrier/Join Semantics - Parallel step coordination
|
|
128
|
+
export {
|
|
129
|
+
Barrier,
|
|
130
|
+
BarrierTimeoutError,
|
|
131
|
+
createBarrier,
|
|
132
|
+
waitForAll,
|
|
133
|
+
waitForAny,
|
|
134
|
+
withConcurrencyLimit,
|
|
135
|
+
type BarrierOptions,
|
|
136
|
+
type BarrierProgress,
|
|
137
|
+
type BarrierResult,
|
|
138
|
+
type WaitForAllOptions,
|
|
139
|
+
type WaitForAnyOptions,
|
|
140
|
+
type WaitForAnyResult,
|
|
141
|
+
type ConcurrencyOptions,
|
|
142
|
+
} from './barrier.js'
|
|
143
|
+
|
|
144
|
+
// Cascade Executor - code -> generative -> agentic -> human pattern
|
|
145
|
+
export {
|
|
146
|
+
CascadeExecutor,
|
|
147
|
+
CascadeTimeoutError,
|
|
148
|
+
TierSkippedError,
|
|
149
|
+
AllTiersFailedError,
|
|
150
|
+
TIER_ORDER,
|
|
151
|
+
DEFAULT_TIER_TIMEOUTS,
|
|
152
|
+
type CapabilityTier,
|
|
153
|
+
type TierHandler,
|
|
154
|
+
type TierContext,
|
|
155
|
+
type TierResult,
|
|
156
|
+
type TierRetryConfig,
|
|
157
|
+
type CascadeConfig,
|
|
158
|
+
type CascadeResult,
|
|
159
|
+
type CascadeMetrics,
|
|
160
|
+
type SkipCondition,
|
|
161
|
+
} from './cascade-executor.js'
|
|
162
|
+
|
|
87
163
|
// Types
|
|
88
164
|
export type {
|
|
89
165
|
EventHandler,
|
|
@@ -103,4 +179,6 @@ export type {
|
|
|
103
179
|
DatabaseContext,
|
|
104
180
|
ActionData,
|
|
105
181
|
ArtifactData,
|
|
182
|
+
DependencyConfig,
|
|
183
|
+
DependencyType,
|
|
106
184
|
} from './types.js'
|
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
|
/**
|