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