ai-workflows 2.1.1 → 2.3.0

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