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.
- package/.turbo/turbo-build.log +1 -1
- package/CHANGELOG.md +17 -1
- package/README.md +305 -184
- package/dist/barrier.d.ts +159 -0
- package/dist/barrier.d.ts.map +1 -0
- package/dist/barrier.js +377 -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 +27 -8
- package/dist/context.js.map +1 -1
- package/dist/cron-parser.d.ts +65 -0
- package/dist/cron-parser.d.ts.map +1 -0
- package/dist/cron-parser.js +294 -0
- package/dist/cron-parser.js.map +1 -0
- package/dist/cron-scheduler.d.ts +117 -0
- package/dist/cron-scheduler.d.ts.map +1 -0
- package/dist/cron-scheduler.js +176 -0
- package/dist/cron-scheduler.js.map +1 -0
- package/dist/database-context.d.ts +184 -0
- package/dist/database-context.d.ts.map +1 -0
- package/dist/database-context.js +428 -0
- package/dist/database-context.js.map +1 -0
- 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/digital-objects-adapter.d.ts +159 -0
- package/dist/digital-objects-adapter.d.ts.map +1 -0
- package/dist/digital-objects-adapter.js +229 -0
- package/dist/digital-objects-adapter.js.map +1 -0
- package/dist/durable-execution-cloudflare.d.ts +427 -0
- package/dist/durable-execution-cloudflare.d.ts.map +1 -0
- package/dist/durable-execution-cloudflare.js +510 -0
- package/dist/durable-execution-cloudflare.js.map +1 -0
- package/dist/durable-execution.d.ts +482 -0
- package/dist/durable-execution.d.ts.map +1 -0
- package/dist/durable-execution.js +594 -0
- package/dist/durable-execution.js.map +1 -0
- package/dist/durable-workflow.d.ts +176 -0
- package/dist/durable-workflow.d.ts.map +1 -0
- package/dist/durable-workflow.js +552 -0
- package/dist/durable-workflow.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 +10 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +25 -0
- package/dist/index.js.map +1 -1
- package/dist/logger.d.ts +101 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +115 -0
- package/dist/logger.js.map +1 -0
- package/dist/on.d.ts +35 -10
- package/dist/on.d.ts.map +1 -1
- package/dist/on.js +53 -19
- package/dist/on.js.map +1 -1
- package/dist/runtime.d.ts +169 -0
- package/dist/runtime.d.ts.map +1 -0
- package/dist/runtime.js +275 -0
- package/dist/runtime.js.map +1 -0
- package/dist/send.d.ts.map +1 -1
- package/dist/send.js +4 -3
- package/dist/send.js.map +1 -1
- package/dist/telemetry.d.ts +150 -0
- package/dist/telemetry.d.ts.map +1 -0
- package/dist/telemetry.js +388 -0
- package/dist/telemetry.js.map +1 -0
- package/dist/timer-registry.d.ts +77 -0
- package/dist/timer-registry.d.ts.map +1 -0
- package/dist/timer-registry.js +154 -0
- package/dist/timer-registry.js.map +1 -0
- package/dist/types.d.ts +105 -6
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +17 -1
- package/dist/types.js.map +1 -1
- package/dist/worker/durable-step.d.ts +481 -0
- package/dist/worker/durable-step.d.ts.map +1 -0
- package/dist/worker/durable-step.js +606 -0
- package/dist/worker/durable-step.js.map +1 -0
- package/dist/worker/index.d.ts +106 -0
- package/dist/worker/index.d.ts.map +1 -0
- package/dist/worker/index.js +124 -0
- package/dist/worker/index.js.map +1 -0
- package/dist/worker/state-adapter.d.ts +230 -0
- package/dist/worker/state-adapter.d.ts.map +1 -0
- package/dist/worker/state-adapter.js +409 -0
- package/dist/worker/state-adapter.js.map +1 -0
- package/dist/worker/topological-executor.d.ts +282 -0
- package/dist/worker/topological-executor.d.ts.map +1 -0
- package/dist/worker/topological-executor.js +396 -0
- package/dist/worker/topological-executor.js.map +1 -0
- package/dist/worker/workflow-builder.d.ts +286 -0
- package/dist/worker/workflow-builder.d.ts.map +1 -0
- package/dist/worker/workflow-builder.js +565 -0
- package/dist/worker/workflow-builder.js.map +1 -0
- package/dist/worker.d.ts +800 -0
- package/dist/worker.d.ts.map +1 -0
- package/dist/worker.js +2428 -0
- package/dist/worker.js.map +1 -0
- package/dist/workflow-builder.d.ts +287 -0
- package/dist/workflow-builder.d.ts.map +1 -0
- package/dist/workflow-builder.js +762 -0
- package/dist/workflow-builder.js.map +1 -0
- package/dist/workflow.d.ts +14 -30
- package/dist/workflow.d.ts.map +1 -1
- package/dist/workflow.js +136 -292
- package/dist/workflow.js.map +1 -1
- package/examples/01-ecommerce-order-pipeline.ts +358 -0
- package/examples/02-content-moderation-cascade.ts +454 -0
- package/examples/03-scheduled-reporting-dependencies.ts +479 -0
- package/examples/04-database-persistence.ts +518 -0
- package/examples/README.md +173 -0
- package/package.json +21 -4
- package/src/__tests__/digital-objects-adapter.test.ts +274 -0
- package/src/__tests__/durable-workflow.test.ts +297 -0
- package/src/barrier.ts +507 -0
- package/src/cascade-context.ts +495 -0
- package/src/cascade-executor.ts +588 -0
- package/src/context.ts +51 -17
- package/src/cron-parser.ts +347 -0
- package/src/cron-scheduler.ts +239 -0
- package/src/database-context.ts +658 -0
- package/src/dependency-graph.ts +518 -0
- package/src/digital-objects-adapter.ts +351 -0
- package/src/durable-execution-cloudflare.ts +855 -0
- package/src/durable-execution.ts +1042 -0
- package/src/durable-workflow.ts +717 -0
- package/src/every.ts +104 -35
- package/src/graph/index.ts +19 -0
- package/src/graph/topological-sort.ts +412 -0
- package/src/index.ts +147 -0
- package/src/logger.ts +148 -0
- package/src/on.ts +81 -26
- package/src/runtime.ts +436 -0
- package/src/send.ts +4 -5
- package/src/telemetry.ts +577 -0
- package/src/timer-registry.ts +179 -0
- package/src/types.ts +146 -10
- package/src/worker/durable-step.ts +976 -0
- package/src/worker/index.ts +216 -0
- package/src/worker/state-adapter.ts +589 -0
- package/src/worker/topological-executor.ts +625 -0
- package/src/worker/workflow-builder.ts +871 -0
- package/src/worker.ts +2906 -0
- package/src/workflow-builder.ts +1068 -0
- package/src/workflow.ts +199 -355
- package/test/barrier-join.test.ts +442 -0
- package/test/barrier-unhandled-rejections.test.ts +359 -0
- package/test/cascade-context.test.ts +390 -0
- package/test/cascade-executor.test.ts +852 -0
- package/test/cron-parser.test.ts +314 -0
- package/test/cron-scheduler.test.ts +291 -0
- package/test/database-context.test.ts +770 -0
- package/test/db-provider-adapter.test.ts +862 -0
- package/test/dependency-graph.test.ts +512 -0
- package/test/durable-execution-cloudflare.test.ts +606 -0
- package/test/durable-execution-in-process.test.ts +286 -0
- package/test/durable-execution.test.ts +247 -0
- package/test/e2e/workflow-scenarios.e2e.test.ts +1039 -0
- package/test/graph/topological-sort.test.ts +586 -0
- package/test/integration.test.ts +442 -0
- package/test/rpc-surface.test.ts +946 -0
- package/test/runtime.test.ts +262 -0
- package/test/schedule-timer-cleanup.test.ts +353 -0
- package/test/send-race-conditions.test.ts +400 -0
- package/test/type-safety-every.test.ts +303 -0
- package/test/worker/durable-cascade.test.ts +1117 -0
- package/test/worker/durable-step.test.ts +723 -0
- package/test/worker/topological-executor.test.ts +1240 -0
- package/test/worker/workflow-builder.test.ts +1067 -0
- package/test/worker.test.ts +608 -0
- package/test/workflow-builder.test.ts +1670 -0
- package/test/workflow-cron.test.ts +256 -0
- package/test/workflow-state-adapter.test.ts +923 -0
- package/test/workflow.test.ts +25 -22
- package/tsconfig.json +3 -1
- package/vitest.config.ts +38 -1
- package/vitest.workers.config.ts +44 -0
- package/wrangler.jsonc +22 -0
- package/.turbo/turbo-test.log +0 -7
- package/src/context.js +0 -83
- package/src/every.js +0 -267
- package/src/index.js +0 -71
- package/src/on.js +0 -79
- package/src/send.js +0 -111
- package/src/types.js +0 -4
- package/src/workflow.js +0 -455
- package/test/context.test.js +0 -116
- package/test/every.test.js +0 -282
- package/test/on.test.js +0 -80
- package/test/send.test.js +0 -89
- package/test/workflow.test.js +0 -224
- package/vitest.config.js +0 -7
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for unhandled promise rejections in barrier operations
|
|
3
|
+
*
|
|
4
|
+
* TDD RED Phase: These tests expose unhandled promise rejections that occur when:
|
|
5
|
+
* 1. waitForAll completes successfully but timeout/abort promises are left dangling
|
|
6
|
+
* 2. waitForAny resolves early leaving pending promises that later reject unhandled
|
|
7
|
+
* 3. Barrier abort handlers aren't cleaned up properly
|
|
8
|
+
*
|
|
9
|
+
* These tests verify that NO unhandled rejections occur in any scenario.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
|
|
13
|
+
import {
|
|
14
|
+
waitForAll,
|
|
15
|
+
waitForAny,
|
|
16
|
+
createBarrier,
|
|
17
|
+
withConcurrencyLimit,
|
|
18
|
+
} from '../src/barrier.js'
|
|
19
|
+
|
|
20
|
+
describe('Barrier Unhandled Promise Rejections', () => {
|
|
21
|
+
// Map to track unhandled rejections by promise
|
|
22
|
+
let unhandledMap: Map<Promise<unknown>, unknown>
|
|
23
|
+
let originalUnhandledHandler: NodeJS.UnhandledRejectionListener | undefined
|
|
24
|
+
let originalHandledHandler: NodeJS.RejectionHandledListener | undefined
|
|
25
|
+
|
|
26
|
+
// Helper to get truly unhandled rejections (those not later handled)
|
|
27
|
+
const getUnhandledRejections = () => [...unhandledMap.values()]
|
|
28
|
+
|
|
29
|
+
beforeEach(() => {
|
|
30
|
+
vi.useFakeTimers()
|
|
31
|
+
unhandledMap = new Map()
|
|
32
|
+
|
|
33
|
+
// Capture original handlers
|
|
34
|
+
originalUnhandledHandler = process.listeners('unhandledRejection')[0] as
|
|
35
|
+
| NodeJS.UnhandledRejectionListener
|
|
36
|
+
| undefined
|
|
37
|
+
originalHandledHandler = process.listeners('rejectionHandled')[0] as
|
|
38
|
+
| NodeJS.RejectionHandledListener
|
|
39
|
+
| undefined
|
|
40
|
+
|
|
41
|
+
// Remove existing listeners temporarily
|
|
42
|
+
process.removeAllListeners('unhandledRejection')
|
|
43
|
+
process.removeAllListeners('rejectionHandled')
|
|
44
|
+
|
|
45
|
+
// Track unhandled rejections
|
|
46
|
+
process.on('unhandledRejection', (reason, promise) => {
|
|
47
|
+
unhandledMap.set(promise, reason)
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
// Remove from tracking when handled
|
|
51
|
+
process.on('rejectionHandled', (promise) => {
|
|
52
|
+
unhandledMap.delete(promise)
|
|
53
|
+
})
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
afterEach(() => {
|
|
57
|
+
vi.useRealTimers()
|
|
58
|
+
|
|
59
|
+
// Restore original handlers
|
|
60
|
+
process.removeAllListeners('unhandledRejection')
|
|
61
|
+
process.removeAllListeners('rejectionHandled')
|
|
62
|
+
if (originalUnhandledHandler) {
|
|
63
|
+
process.on('unhandledRejection', originalUnhandledHandler)
|
|
64
|
+
}
|
|
65
|
+
if (originalHandledHandler) {
|
|
66
|
+
process.on('rejectionHandled', originalHandledHandler)
|
|
67
|
+
}
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
// Alias for backwards compatibility in tests
|
|
71
|
+
const unhandledRejections = {
|
|
72
|
+
get length() {
|
|
73
|
+
return getUnhandledRejections().length
|
|
74
|
+
},
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
describe('waitForAll timeout cleanup', () => {
|
|
78
|
+
it('should not leave dangling timeout rejection when promises resolve before timeout', async () => {
|
|
79
|
+
// This test exposes a bug: the timeout promise is never cleaned up
|
|
80
|
+
// when the main promises resolve first, leaving an unhandled rejection
|
|
81
|
+
|
|
82
|
+
const fastPromise = new Promise<string>((resolve) => {
|
|
83
|
+
setTimeout(() => resolve('fast'), 100)
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
const promise = waitForAll([fastPromise], { timeout: 5000 })
|
|
87
|
+
|
|
88
|
+
// Let the fast promise resolve
|
|
89
|
+
await vi.advanceTimersByTimeAsync(200)
|
|
90
|
+
const result = await promise
|
|
91
|
+
|
|
92
|
+
expect(result).toEqual(['fast'])
|
|
93
|
+
|
|
94
|
+
// Now advance past the timeout - the timeout promise should have been cleaned up
|
|
95
|
+
// If not, we'll get an unhandled rejection
|
|
96
|
+
await vi.advanceTimersByTimeAsync(6000)
|
|
97
|
+
|
|
98
|
+
// Give microtasks time to process
|
|
99
|
+
await vi.runAllTimersAsync()
|
|
100
|
+
await Promise.resolve()
|
|
101
|
+
|
|
102
|
+
expect(unhandledRejections).toHaveLength(0)
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
it('should not leave dangling abort rejection when promises resolve before abort', async () => {
|
|
106
|
+
const controller = new AbortController()
|
|
107
|
+
|
|
108
|
+
const fastPromise = new Promise<string>((resolve) => {
|
|
109
|
+
setTimeout(() => resolve('completed'), 100)
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
const promise = waitForAll([fastPromise], { signal: controller.signal })
|
|
113
|
+
|
|
114
|
+
// Let the promise complete normally
|
|
115
|
+
await vi.advanceTimersByTimeAsync(200)
|
|
116
|
+
const result = await promise
|
|
117
|
+
|
|
118
|
+
expect(result).toEqual(['completed'])
|
|
119
|
+
|
|
120
|
+
// Now abort - this shouldn't cause an unhandled rejection
|
|
121
|
+
controller.abort()
|
|
122
|
+
|
|
123
|
+
await Promise.resolve()
|
|
124
|
+
|
|
125
|
+
expect(unhandledRejections).toHaveLength(0)
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
it('should not leave dangling timeout when one promise rejects', async () => {
|
|
129
|
+
// When a promise rejects, the timeout should be cleaned up
|
|
130
|
+
const failingPromise = new Promise<string>((_, reject) => {
|
|
131
|
+
setTimeout(() => reject(new Error('Task failed')), 100)
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
const promise = waitForAll([failingPromise], { timeout: 5000 })
|
|
135
|
+
|
|
136
|
+
await vi.advanceTimersByTimeAsync(200)
|
|
137
|
+
|
|
138
|
+
await expect(promise).rejects.toThrow('Task failed')
|
|
139
|
+
|
|
140
|
+
// Advance past timeout - should not trigger unhandled rejection
|
|
141
|
+
await vi.advanceTimersByTimeAsync(6000)
|
|
142
|
+
await Promise.resolve()
|
|
143
|
+
|
|
144
|
+
expect(unhandledRejections).toHaveLength(0)
|
|
145
|
+
})
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
describe('waitForAny unhandled pending rejections', () => {
|
|
149
|
+
it('should not have unhandled rejections from pending promises that fail after resolution', async () => {
|
|
150
|
+
// This is the critical bug: when waitForAny resolves early,
|
|
151
|
+
// any remaining promises that reject are completely unhandled
|
|
152
|
+
|
|
153
|
+
const fast = new Promise<string>((resolve) => {
|
|
154
|
+
setTimeout(() => resolve('fast'), 100)
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
const slowFailing = new Promise<string>((_, reject) => {
|
|
158
|
+
setTimeout(() => reject(new Error('Slow task failed')), 500)
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
const promise = waitForAny(1, [fast, slowFailing])
|
|
162
|
+
|
|
163
|
+
// Let the fast one complete
|
|
164
|
+
await vi.advanceTimersByTimeAsync(200)
|
|
165
|
+
const result = await promise
|
|
166
|
+
|
|
167
|
+
expect(result.completed).toContain('fast')
|
|
168
|
+
|
|
169
|
+
// Now let the slow one fail - this rejection should be handled
|
|
170
|
+
await vi.advanceTimersByTimeAsync(400)
|
|
171
|
+
await Promise.resolve()
|
|
172
|
+
|
|
173
|
+
// BUG: The slowFailing promise rejection is never caught
|
|
174
|
+
expect(unhandledRejections).toHaveLength(0)
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
it('should handle multiple pending rejections after early resolution', async () => {
|
|
178
|
+
const fast = new Promise<string>((resolve) => {
|
|
179
|
+
setTimeout(() => resolve('winner'), 50)
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
const failing1 = new Promise<string>((_, reject) => {
|
|
183
|
+
setTimeout(() => reject(new Error('Failure 1')), 200)
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
const failing2 = new Promise<string>((_, reject) => {
|
|
187
|
+
setTimeout(() => reject(new Error('Failure 2')), 300)
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
const promise = waitForAny(1, [fast, failing1, failing2])
|
|
191
|
+
|
|
192
|
+
await vi.advanceTimersByTimeAsync(100)
|
|
193
|
+
const result = await promise
|
|
194
|
+
|
|
195
|
+
expect(result.completed).toContain('winner')
|
|
196
|
+
|
|
197
|
+
// Let all failing promises reject
|
|
198
|
+
await vi.advanceTimersByTimeAsync(400)
|
|
199
|
+
await Promise.resolve()
|
|
200
|
+
|
|
201
|
+
// All rejections should be handled, not left dangling
|
|
202
|
+
expect(unhandledRejections).toHaveLength(0)
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
it('should not have unhandled timeout rejection when resolved before timeout', async () => {
|
|
206
|
+
const fast = new Promise<string>((resolve) => {
|
|
207
|
+
setTimeout(() => resolve('quick'), 100)
|
|
208
|
+
})
|
|
209
|
+
|
|
210
|
+
const slow = new Promise<string>((resolve) => {
|
|
211
|
+
setTimeout(() => resolve('slow'), 2000)
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
const promise = waitForAny(1, [fast, slow], { timeout: 5000 })
|
|
215
|
+
|
|
216
|
+
await vi.advanceTimersByTimeAsync(200)
|
|
217
|
+
const result = await promise
|
|
218
|
+
|
|
219
|
+
expect(result.completed).toContain('quick')
|
|
220
|
+
|
|
221
|
+
// Advance past timeout
|
|
222
|
+
await vi.advanceTimersByTimeAsync(6000)
|
|
223
|
+
await Promise.resolve()
|
|
224
|
+
|
|
225
|
+
expect(unhandledRejections).toHaveLength(0)
|
|
226
|
+
})
|
|
227
|
+
})
|
|
228
|
+
|
|
229
|
+
describe('Barrier abort handler cleanup', () => {
|
|
230
|
+
it('should remove abort listener when barrier completes normally', async () => {
|
|
231
|
+
const controller = new AbortController()
|
|
232
|
+
const barrier = createBarrier<string>(2, { signal: controller.signal })
|
|
233
|
+
|
|
234
|
+
const waitPromise = barrier.wait()
|
|
235
|
+
|
|
236
|
+
barrier.arrive('first')
|
|
237
|
+
barrier.arrive('second')
|
|
238
|
+
|
|
239
|
+
const results = await waitPromise
|
|
240
|
+
|
|
241
|
+
expect(results).toEqual(['first', 'second'])
|
|
242
|
+
|
|
243
|
+
// Aborting after completion should not cause issues
|
|
244
|
+
controller.abort()
|
|
245
|
+
|
|
246
|
+
await Promise.resolve()
|
|
247
|
+
|
|
248
|
+
expect(unhandledRejections).toHaveLength(0)
|
|
249
|
+
})
|
|
250
|
+
|
|
251
|
+
it('should cleanup abort listener when barrier times out', async () => {
|
|
252
|
+
const controller = new AbortController()
|
|
253
|
+
const barrier = createBarrier<string>(3, {
|
|
254
|
+
signal: controller.signal,
|
|
255
|
+
timeout: 1000,
|
|
256
|
+
})
|
|
257
|
+
|
|
258
|
+
barrier.arrive('first')
|
|
259
|
+
|
|
260
|
+
const promise = barrier.wait()
|
|
261
|
+
// Pre-attach catch handler to prevent unhandled rejection warning with fake timers
|
|
262
|
+
// This is necessary because fake timers can cause the rejection to fire
|
|
263
|
+
// before the test's assertion handler is ready
|
|
264
|
+
promise.catch(() => {})
|
|
265
|
+
|
|
266
|
+
await vi.advanceTimersByTimeAsync(1500)
|
|
267
|
+
|
|
268
|
+
await expect(promise).rejects.toThrow()
|
|
269
|
+
|
|
270
|
+
// Aborting after timeout should not cause double rejection
|
|
271
|
+
controller.abort()
|
|
272
|
+
|
|
273
|
+
await Promise.resolve()
|
|
274
|
+
|
|
275
|
+
expect(unhandledRejections).toHaveLength(0)
|
|
276
|
+
})
|
|
277
|
+
})
|
|
278
|
+
|
|
279
|
+
describe('withConcurrencyLimit error handling', () => {
|
|
280
|
+
it('should not leave unhandled rejections when failing fast', async () => {
|
|
281
|
+
// When collectErrors is false and a task fails,
|
|
282
|
+
// any other pending tasks that reject should still be handled
|
|
283
|
+
|
|
284
|
+
const tasks = [
|
|
285
|
+
() =>
|
|
286
|
+
new Promise<number>((_, reject) => {
|
|
287
|
+
setTimeout(() => reject(new Error('First failure')), 100)
|
|
288
|
+
}),
|
|
289
|
+
() =>
|
|
290
|
+
new Promise<number>((_, reject) => {
|
|
291
|
+
setTimeout(() => reject(new Error('Second failure')), 200)
|
|
292
|
+
}),
|
|
293
|
+
() =>
|
|
294
|
+
new Promise<number>((resolve) => {
|
|
295
|
+
setTimeout(() => resolve(3), 50)
|
|
296
|
+
}),
|
|
297
|
+
]
|
|
298
|
+
|
|
299
|
+
const promise = withConcurrencyLimit(tasks, 3)
|
|
300
|
+
|
|
301
|
+
await vi.advanceTimersByTimeAsync(150)
|
|
302
|
+
|
|
303
|
+
await expect(promise).rejects.toThrow('First failure')
|
|
304
|
+
|
|
305
|
+
// Let remaining tasks complete/fail
|
|
306
|
+
await vi.advanceTimersByTimeAsync(200)
|
|
307
|
+
await Promise.resolve()
|
|
308
|
+
|
|
309
|
+
expect(unhandledRejections).toHaveLength(0)
|
|
310
|
+
})
|
|
311
|
+
})
|
|
312
|
+
|
|
313
|
+
describe('Race condition scenarios', () => {
|
|
314
|
+
it('should handle simultaneous completion and timeout', async () => {
|
|
315
|
+
// Edge case: what happens when the promise resolves at exactly the timeout?
|
|
316
|
+
const promise = new Promise<string>((resolve) => {
|
|
317
|
+
setTimeout(() => resolve('just in time'), 1000)
|
|
318
|
+
})
|
|
319
|
+
|
|
320
|
+
const waitPromise = waitForAll([promise], { timeout: 1000 })
|
|
321
|
+
|
|
322
|
+
// Both should fire at 1000ms
|
|
323
|
+
await vi.advanceTimersByTimeAsync(1000)
|
|
324
|
+
|
|
325
|
+
// Should not have unhandled rejections regardless of which wins
|
|
326
|
+
await Promise.resolve()
|
|
327
|
+
|
|
328
|
+
// One of these should work without unhandled rejections
|
|
329
|
+
try {
|
|
330
|
+
await waitPromise
|
|
331
|
+
} catch {
|
|
332
|
+
// Timeout might win, that's ok
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// Give any dangling promises time to reject
|
|
336
|
+
await vi.advanceTimersByTimeAsync(1000)
|
|
337
|
+
await Promise.resolve()
|
|
338
|
+
|
|
339
|
+
expect(unhandledRejections).toHaveLength(0)
|
|
340
|
+
})
|
|
341
|
+
|
|
342
|
+
it('should handle abort signal fired after successful completion', async () => {
|
|
343
|
+
const controller = new AbortController()
|
|
344
|
+
|
|
345
|
+
const fastPromise = Promise.resolve('immediate')
|
|
346
|
+
|
|
347
|
+
const result = await waitForAll([fastPromise], { signal: controller.signal })
|
|
348
|
+
|
|
349
|
+
expect(result).toEqual(['immediate'])
|
|
350
|
+
|
|
351
|
+
// Fire abort after already completed
|
|
352
|
+
controller.abort()
|
|
353
|
+
|
|
354
|
+
await Promise.resolve()
|
|
355
|
+
|
|
356
|
+
expect(unhandledRejections).toHaveLength(0)
|
|
357
|
+
})
|
|
358
|
+
})
|
|
359
|
+
})
|