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
|
@@ -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
|
+
})
|