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
@@ -0,0 +1,434 @@
1
+ /**
2
+ * Barrier/Join semantics tests for parallel step coordination
3
+ *
4
+ * TDD RED Phase: These tests define the expected behavior for:
5
+ * - waitForAll() - all steps must complete
6
+ * - waitForAny(n) - N of M steps must complete
7
+ * - Fanout/convergence patterns
8
+ * - Concurrent execution limits
9
+ * - Barrier timeout handling
10
+ */
11
+
12
+ import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest'
13
+ import {
14
+ Barrier,
15
+ BarrierTimeoutError,
16
+ createBarrier,
17
+ waitForAll,
18
+ waitForAny,
19
+ withConcurrencyLimit,
20
+ type BarrierOptions,
21
+ type BarrierResult,
22
+ } from '../src/barrier.js'
23
+
24
+ describe('Barrier/Join Semantics', () => {
25
+ beforeEach(() => {
26
+ vi.useFakeTimers()
27
+ })
28
+
29
+ afterEach(() => {
30
+ vi.useRealTimers()
31
+ })
32
+
33
+ describe('waitForAll', () => {
34
+ it('should wait for all promises to complete', async () => {
35
+ const step1 = Promise.resolve('result1')
36
+ const step2 = Promise.resolve('result2')
37
+ const step3 = Promise.resolve('result3')
38
+
39
+ const results = await waitForAll([step1, step2, step3])
40
+
41
+ expect(results).toEqual(['result1', 'result2', 'result3'])
42
+ })
43
+
44
+ it('should preserve order of results', async () => {
45
+ // step2 resolves first but should be in position 1
46
+ const step1 = new Promise<number>(resolve => setTimeout(() => resolve(1), 100))
47
+ const step2 = new Promise<number>(resolve => setTimeout(() => resolve(2), 50))
48
+ const step3 = new Promise<number>(resolve => setTimeout(() => resolve(3), 150))
49
+
50
+ const promise = waitForAll([step1, step2, step3])
51
+ await vi.advanceTimersByTimeAsync(200)
52
+ const results = await promise
53
+
54
+ expect(results).toEqual([1, 2, 3])
55
+ })
56
+
57
+ it('should reject if any step fails', async () => {
58
+ const step1 = Promise.resolve('ok')
59
+ const step2 = Promise.reject(new Error('step2 failed'))
60
+ const step3 = Promise.resolve('ok')
61
+
62
+ await expect(waitForAll([step1, step2, step3])).rejects.toThrow('step2 failed')
63
+ })
64
+
65
+ it('should support timeout option', async () => {
66
+ const slowStep = new Promise(resolve => setTimeout(() => resolve('slow'), 5000))
67
+
68
+ const promise = waitForAll([slowStep], { timeout: 1000 })
69
+
70
+ await vi.advanceTimersByTimeAsync(1500)
71
+
72
+ await expect(promise).rejects.toBeInstanceOf(BarrierTimeoutError)
73
+ })
74
+
75
+ it('should return results within timeout', async () => {
76
+ const step1 = new Promise<string>(resolve => setTimeout(() => resolve('fast'), 100))
77
+
78
+ const promise = waitForAll([step1], { timeout: 1000 })
79
+ await vi.advanceTimersByTimeAsync(200)
80
+ const results = await promise
81
+
82
+ expect(results).toEqual(['fast'])
83
+ })
84
+
85
+ it('should handle empty array', async () => {
86
+ const results = await waitForAll([])
87
+ expect(results).toEqual([])
88
+ })
89
+ })
90
+
91
+ describe('waitForAny', () => {
92
+ it('should resolve when N of M steps complete', async () => {
93
+ const step1 = new Promise<string>(resolve => setTimeout(() => resolve('first'), 100))
94
+ const step2 = new Promise<string>(resolve => setTimeout(() => resolve('second'), 200))
95
+ const step3 = new Promise<string>(resolve => setTimeout(() => resolve('third'), 300))
96
+
97
+ const promise = waitForAny(2, [step1, step2, step3])
98
+ await vi.advanceTimersByTimeAsync(250)
99
+ const result = await promise
100
+
101
+ expect(result.completed).toHaveLength(2)
102
+ expect(result.completed).toContain('first')
103
+ expect(result.completed).toContain('second')
104
+ expect(result.pending).toHaveLength(1)
105
+ })
106
+
107
+ it('should resolve immediately when N=0', async () => {
108
+ const step1 = new Promise<string>(resolve => setTimeout(() => resolve('a'), 1000))
109
+
110
+ const result = await waitForAny(0, [step1])
111
+
112
+ expect(result.completed).toHaveLength(0)
113
+ expect(result.pending).toHaveLength(1)
114
+ })
115
+
116
+ it('should reject if not enough steps can complete due to failures', async () => {
117
+ const step1 = Promise.reject(new Error('failed1'))
118
+ const step2 = Promise.reject(new Error('failed2'))
119
+ const step3 = Promise.resolve('ok')
120
+
121
+ // Need 3 but only 1 can succeed
122
+ await expect(waitForAny(3, [step1, step2, step3])).rejects.toThrow()
123
+ })
124
+
125
+ it('should support timeout', async () => {
126
+ const slowStep1 = new Promise(resolve => setTimeout(() => resolve('a'), 5000))
127
+ const slowStep2 = new Promise(resolve => setTimeout(() => resolve('b'), 5000))
128
+
129
+ const promise = waitForAny(2, [slowStep1, slowStep2], { timeout: 1000 })
130
+
131
+ await vi.advanceTimersByTimeAsync(1500)
132
+
133
+ await expect(promise).rejects.toBeInstanceOf(BarrierTimeoutError)
134
+ })
135
+
136
+ it('should return partial results on timeout when configured', async () => {
137
+ const fast = new Promise<string>(resolve => setTimeout(() => resolve('fast'), 100))
138
+ const slow = new Promise<string>(resolve => setTimeout(() => resolve('slow'), 5000))
139
+
140
+ const promise = waitForAny(2, [fast, slow], {
141
+ timeout: 1000,
142
+ returnPartialOnTimeout: true,
143
+ })
144
+
145
+ await vi.advanceTimersByTimeAsync(1500)
146
+ const result = await promise
147
+
148
+ expect(result.completed).toContain('fast')
149
+ expect(result.timedOut).toBe(true)
150
+ })
151
+ })
152
+
153
+ describe('Barrier class', () => {
154
+ it('should create a barrier with expected participants', () => {
155
+ const barrier = createBarrier(3)
156
+
157
+ expect(barrier.expectedCount).toBe(3)
158
+ expect(barrier.arrivedCount).toBe(0)
159
+ expect(barrier.isComplete).toBe(false)
160
+ })
161
+
162
+ it('should track arrivals', async () => {
163
+ const barrier = createBarrier<string>(2)
164
+
165
+ barrier.arrive('first')
166
+ expect(barrier.arrivedCount).toBe(1)
167
+ expect(barrier.isComplete).toBe(false)
168
+
169
+ barrier.arrive('second')
170
+ expect(barrier.arrivedCount).toBe(2)
171
+ expect(barrier.isComplete).toBe(true)
172
+ })
173
+
174
+ it('should resolve wait() when all participants arrive', async () => {
175
+ const barrier = createBarrier<number>(2)
176
+
177
+ const waitPromise = barrier.wait()
178
+
179
+ barrier.arrive(1)
180
+ barrier.arrive(2)
181
+
182
+ const results = await waitPromise
183
+ expect(results).toEqual([1, 2])
184
+ })
185
+
186
+ it('should support timeout on wait()', async () => {
187
+ const barrier = createBarrier<string>(3, { timeout: 1000 })
188
+
189
+ barrier.arrive('first')
190
+
191
+ const promise = barrier.wait()
192
+ await vi.advanceTimersByTimeAsync(1500)
193
+
194
+ await expect(promise).rejects.toBeInstanceOf(BarrierTimeoutError)
195
+ })
196
+
197
+ it('should support abort signal', async () => {
198
+ const controller = new AbortController()
199
+ const barrier = createBarrier<string>(3, { signal: controller.signal })
200
+
201
+ barrier.arrive('first')
202
+
203
+ const promise = barrier.wait()
204
+ controller.abort()
205
+
206
+ await expect(promise).rejects.toThrow(/aborted/i)
207
+ })
208
+
209
+ it('should allow reset for reuse', async () => {
210
+ const barrier = createBarrier<number>(2)
211
+
212
+ barrier.arrive(1)
213
+ barrier.arrive(2)
214
+ expect(barrier.isComplete).toBe(true)
215
+
216
+ barrier.reset()
217
+
218
+ expect(barrier.arrivedCount).toBe(0)
219
+ expect(barrier.isComplete).toBe(false)
220
+ })
221
+
222
+ it('should provide progress information', () => {
223
+ const barrier = createBarrier<string>(4)
224
+
225
+ barrier.arrive('a')
226
+ barrier.arrive('b')
227
+
228
+ const progress = barrier.getProgress()
229
+ expect(progress.arrived).toBe(2)
230
+ expect(progress.expected).toBe(4)
231
+ expect(progress.percentage).toBe(50)
232
+ })
233
+ })
234
+
235
+ describe('Fanout/Convergence patterns', () => {
236
+ it('should support fanout pattern (one to many)', async () => {
237
+ const input = { value: 10 }
238
+
239
+ // Fanout: process input in parallel different ways
240
+ const results = await waitForAll([
241
+ Promise.resolve(input.value * 2),
242
+ Promise.resolve(input.value + 5),
243
+ Promise.resolve(input.value.toString()),
244
+ ])
245
+
246
+ expect(results).toEqual([20, 15, '10'])
247
+ })
248
+
249
+ it('should support convergence pattern (many to one)', async () => {
250
+ const barrier = createBarrier<{ source: string; data: number }>(3)
251
+
252
+ // Simulate multiple sources converging
253
+ barrier.arrive({ source: 'api1', data: 100 })
254
+ barrier.arrive({ source: 'api2', data: 200 })
255
+ barrier.arrive({ source: 'api3', data: 300 })
256
+
257
+ const results = await barrier.wait()
258
+
259
+ // Aggregate at convergence point
260
+ const total = results.reduce((sum, r) => sum + r.data, 0)
261
+ expect(total).toBe(600)
262
+ })
263
+
264
+ it('should support map-reduce pattern', async () => {
265
+ const inputs = [1, 2, 3, 4, 5]
266
+
267
+ // Map phase (fanout)
268
+ const mapped = await waitForAll(inputs.map(x => Promise.resolve(x * 2)))
269
+
270
+ // Reduce phase (convergence)
271
+ const reduced = mapped.reduce((sum, x) => sum + x, 0)
272
+
273
+ expect(reduced).toBe(30)
274
+ })
275
+ })
276
+
277
+ describe('Concurrent execution limits', () => {
278
+ it('should limit concurrent executions', async () => {
279
+ const maxConcurrent = 2
280
+ const tasks = [
281
+ () => new Promise<number>(resolve => setTimeout(() => resolve(1), 100)),
282
+ () => new Promise<number>(resolve => setTimeout(() => resolve(2), 100)),
283
+ () => new Promise<number>(resolve => setTimeout(() => resolve(3), 100)),
284
+ () => new Promise<number>(resolve => setTimeout(() => resolve(4), 100)),
285
+ ]
286
+
287
+ let concurrentCount = 0
288
+ let maxObservedConcurrent = 0
289
+
290
+ const trackedTasks = tasks.map(task => async () => {
291
+ concurrentCount++
292
+ maxObservedConcurrent = Math.max(maxObservedConcurrent, concurrentCount)
293
+ try {
294
+ return await task()
295
+ } finally {
296
+ concurrentCount--
297
+ }
298
+ })
299
+
300
+ const promise = withConcurrencyLimit(trackedTasks, maxConcurrent)
301
+ await vi.advanceTimersByTimeAsync(300)
302
+ const results = await promise
303
+
304
+ expect(results).toEqual([1, 2, 3, 4])
305
+ expect(maxObservedConcurrent).toBeLessThanOrEqual(maxConcurrent)
306
+ })
307
+
308
+ it('should preserve order even with varying task durations', async () => {
309
+ const tasks = [
310
+ () => new Promise<string>(resolve => setTimeout(() => resolve('a'), 300)),
311
+ () => new Promise<string>(resolve => setTimeout(() => resolve('b'), 100)),
312
+ () => new Promise<string>(resolve => setTimeout(() => resolve('c'), 200)),
313
+ ]
314
+
315
+ const promise = withConcurrencyLimit(tasks, 2)
316
+ await vi.advanceTimersByTimeAsync(500)
317
+ const results = await promise
318
+
319
+ expect(results).toEqual(['a', 'b', 'c'])
320
+ })
321
+
322
+ it('should handle task failures gracefully', async () => {
323
+ const tasks = [
324
+ () => Promise.resolve(1),
325
+ () => Promise.reject(new Error('task failed')),
326
+ () => Promise.resolve(3),
327
+ ]
328
+
329
+ await expect(withConcurrencyLimit(tasks, 2)).rejects.toThrow('task failed')
330
+ })
331
+
332
+ it('should support collecting all results including errors', async () => {
333
+ const tasks = [
334
+ () => Promise.resolve(1),
335
+ () => Promise.reject(new Error('failed')),
336
+ () => Promise.resolve(3),
337
+ ]
338
+
339
+ const results = await withConcurrencyLimit(tasks, 2, { collectErrors: true })
340
+
341
+ expect(results[0]).toBe(1)
342
+ expect(results[1]).toBeInstanceOf(Error)
343
+ expect(results[2]).toBe(3)
344
+ })
345
+ })
346
+
347
+ describe('BarrierTimeoutError', () => {
348
+ it('should include timeout details', async () => {
349
+ const barrier = createBarrier<string>(3, { timeout: 1000 })
350
+
351
+ barrier.arrive('first')
352
+
353
+ const promise = barrier.wait()
354
+ await vi.advanceTimersByTimeAsync(1500)
355
+
356
+ let caughtError: BarrierTimeoutError | null = null
357
+ try {
358
+ await promise
359
+ } catch (error) {
360
+ caughtError = error as BarrierTimeoutError
361
+ }
362
+
363
+ expect(caughtError).not.toBeNull()
364
+ expect(caughtError).toBeInstanceOf(BarrierTimeoutError)
365
+ expect(caughtError!.timeout).toBe(1000)
366
+ expect(caughtError!.arrived).toBe(1)
367
+ // Use Number() to handle any serialization edge cases
368
+ expect(Number(caughtError!.expected)).toBe(3)
369
+ })
370
+ })
371
+
372
+ describe('Progress tracking', () => {
373
+ it('should emit progress events', async () => {
374
+ const progressHandler = vi.fn()
375
+ const barrier = createBarrier<string>(3, {
376
+ onProgress: progressHandler,
377
+ })
378
+
379
+ barrier.arrive('a')
380
+ expect(progressHandler).toHaveBeenCalledWith({
381
+ arrived: 1,
382
+ expected: 3,
383
+ percentage: 33,
384
+ latest: 'a',
385
+ })
386
+
387
+ barrier.arrive('b')
388
+ expect(progressHandler).toHaveBeenCalledWith({
389
+ arrived: 2,
390
+ expected: 3,
391
+ percentage: 67,
392
+ latest: 'b',
393
+ })
394
+
395
+ barrier.arrive('c')
396
+ expect(progressHandler).toHaveBeenCalledWith({
397
+ arrived: 3,
398
+ expected: 3,
399
+ percentage: 100,
400
+ latest: 'c',
401
+ })
402
+ })
403
+ })
404
+
405
+ describe('Cancellation support', () => {
406
+ it('should cancel pending tasks when barrier is cancelled', async () => {
407
+ const barrier = createBarrier<string>(3)
408
+
409
+ barrier.arrive('first')
410
+
411
+ const waitPromise = barrier.wait()
412
+ barrier.cancel(new Error('Operation cancelled'))
413
+
414
+ await expect(waitPromise).rejects.toThrow('Operation cancelled')
415
+ })
416
+
417
+ it('should support AbortController', async () => {
418
+ const controller = new AbortController()
419
+
420
+ const slowTasks = [
421
+ new Promise<string>(resolve => setTimeout(() => resolve('a'), 5000)),
422
+ new Promise<string>(resolve => setTimeout(() => resolve('b'), 5000)),
423
+ ]
424
+
425
+ const promise = waitForAll(slowTasks, { signal: controller.signal })
426
+
427
+ // Advance timers a bit then abort
428
+ await vi.advanceTimersByTimeAsync(100)
429
+ controller.abort()
430
+
431
+ await expect(promise).rejects.toThrow(/aborted/i)
432
+ })
433
+ })
434
+ })