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
package/src/barrier.ts
ADDED
|
@@ -0,0 +1,507 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Barrier/Join semantics for parallel step coordination
|
|
3
|
+
*
|
|
4
|
+
* Provides synchronization primitives for coordinating parallel workflow steps:
|
|
5
|
+
* - waitForAll() - wait for all steps to complete
|
|
6
|
+
* - waitForAny(n) - wait for N of M steps to complete
|
|
7
|
+
* - Barrier class - manual synchronization point
|
|
8
|
+
* - withConcurrencyLimit() - limit concurrent executions
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Progress information for a barrier
|
|
13
|
+
*/
|
|
14
|
+
export interface BarrierProgress<T = unknown> {
|
|
15
|
+
/** Number of participants that have arrived */
|
|
16
|
+
arrived: number
|
|
17
|
+
/** Expected number of participants */
|
|
18
|
+
expected: number
|
|
19
|
+
/** Percentage complete (0-100) */
|
|
20
|
+
percentage: number
|
|
21
|
+
/** Most recently arrived value */
|
|
22
|
+
latest?: T
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Options for barrier creation
|
|
27
|
+
*/
|
|
28
|
+
export interface BarrierOptions<T = unknown> {
|
|
29
|
+
/** Timeout in milliseconds */
|
|
30
|
+
timeout?: number
|
|
31
|
+
/** AbortSignal for cancellation */
|
|
32
|
+
signal?: AbortSignal
|
|
33
|
+
/** Progress callback */
|
|
34
|
+
onProgress?: (progress: BarrierProgress<T>) => void
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Options for waitForAll
|
|
39
|
+
*/
|
|
40
|
+
export interface WaitForAllOptions {
|
|
41
|
+
/** Timeout in milliseconds */
|
|
42
|
+
timeout?: number
|
|
43
|
+
/** AbortSignal for cancellation */
|
|
44
|
+
signal?: AbortSignal
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Options for waitForAny
|
|
49
|
+
*/
|
|
50
|
+
export interface WaitForAnyOptions {
|
|
51
|
+
/** Timeout in milliseconds */
|
|
52
|
+
timeout?: number
|
|
53
|
+
/** AbortSignal for cancellation */
|
|
54
|
+
signal?: AbortSignal
|
|
55
|
+
/** Return partial results on timeout instead of throwing */
|
|
56
|
+
returnPartialOnTimeout?: boolean
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Result from waitForAny
|
|
61
|
+
*/
|
|
62
|
+
export interface WaitForAnyResult<T> {
|
|
63
|
+
/** Completed values */
|
|
64
|
+
completed: T[]
|
|
65
|
+
/** Number of still pending promises */
|
|
66
|
+
pending: number[]
|
|
67
|
+
/** Whether the operation timed out */
|
|
68
|
+
timedOut?: boolean
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Options for concurrency limiting
|
|
73
|
+
*/
|
|
74
|
+
export interface ConcurrencyOptions {
|
|
75
|
+
/** Collect errors instead of failing fast */
|
|
76
|
+
collectErrors?: boolean
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Result type for barrier operations
|
|
81
|
+
*/
|
|
82
|
+
export interface BarrierResult<T> {
|
|
83
|
+
values: T[]
|
|
84
|
+
timedOut: boolean
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Error thrown when a barrier times out
|
|
89
|
+
*/
|
|
90
|
+
export class BarrierTimeoutError extends Error {
|
|
91
|
+
public readonly timeout: number
|
|
92
|
+
public readonly arrived: number
|
|
93
|
+
public readonly expected: number
|
|
94
|
+
|
|
95
|
+
constructor(timeout: number, arrived: number, expected: number) {
|
|
96
|
+
super(`Barrier timeout after ${timeout}ms: ${arrived}/${expected} participants arrived`)
|
|
97
|
+
this.name = 'BarrierTimeoutError'
|
|
98
|
+
this.timeout = timeout
|
|
99
|
+
this.arrived = arrived
|
|
100
|
+
this.expected = expected
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Barrier class for manual synchronization
|
|
106
|
+
*/
|
|
107
|
+
export class Barrier<T = unknown> {
|
|
108
|
+
private readonly _expected: number
|
|
109
|
+
private readonly _options: BarrierOptions<T>
|
|
110
|
+
private _arrived: T[] = []
|
|
111
|
+
private _waitResolve: ((values: T[]) => void) | null = null
|
|
112
|
+
private _waitReject: ((error: Error) => void) | null = null
|
|
113
|
+
private _timeoutId: ReturnType<typeof setTimeout> | null = null
|
|
114
|
+
private _cancelled = false
|
|
115
|
+
private _cancelError: Error | null = null
|
|
116
|
+
private _abortHandler: (() => void) | null = null
|
|
117
|
+
|
|
118
|
+
constructor(expectedCount: number, options: BarrierOptions<T> = {}) {
|
|
119
|
+
this._expected = expectedCount
|
|
120
|
+
this._options = options
|
|
121
|
+
|
|
122
|
+
// Set up abort signal listener with proper cleanup tracking
|
|
123
|
+
if (options.signal) {
|
|
124
|
+
this._abortHandler = () => {
|
|
125
|
+
this.cancel(new Error('Operation aborted'))
|
|
126
|
+
}
|
|
127
|
+
options.signal.addEventListener('abort', this._abortHandler, { once: true })
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Number of expected participants
|
|
133
|
+
*/
|
|
134
|
+
get expectedCount(): number {
|
|
135
|
+
return this._expected
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Number of participants that have arrived
|
|
140
|
+
*/
|
|
141
|
+
get arrivedCount(): number {
|
|
142
|
+
return this._arrived.length
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Whether all expected participants have arrived
|
|
147
|
+
*/
|
|
148
|
+
get isComplete(): boolean {
|
|
149
|
+
return this._arrived.length >= this._expected
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Record a participant's arrival at the barrier
|
|
154
|
+
*/
|
|
155
|
+
arrive(value: T): void {
|
|
156
|
+
if (this._cancelled) {
|
|
157
|
+
return
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
this._arrived.push(value)
|
|
161
|
+
|
|
162
|
+
// Emit progress
|
|
163
|
+
if (this._options.onProgress) {
|
|
164
|
+
this._options.onProgress(this.getProgress())
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Check if barrier is complete
|
|
168
|
+
if (this.isComplete && this._waitResolve) {
|
|
169
|
+
this._clearTimeout()
|
|
170
|
+
this._waitResolve([...this._arrived])
|
|
171
|
+
this._waitResolve = null
|
|
172
|
+
this._waitReject = null
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Wait for all participants to arrive
|
|
178
|
+
*/
|
|
179
|
+
wait(): Promise<T[]> {
|
|
180
|
+
if (this._cancelled && this._cancelError) {
|
|
181
|
+
return Promise.reject(this._cancelError)
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (this.isComplete) {
|
|
185
|
+
return Promise.resolve([...this._arrived])
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return new Promise<T[]>((resolve, reject) => {
|
|
189
|
+
this._waitResolve = resolve
|
|
190
|
+
this._waitReject = reject
|
|
191
|
+
|
|
192
|
+
// Set up timeout if specified
|
|
193
|
+
if (this._options.timeout) {
|
|
194
|
+
this._timeoutId = setTimeout(() => {
|
|
195
|
+
if (this._waitReject) {
|
|
196
|
+
const error = new BarrierTimeoutError(
|
|
197
|
+
this._options.timeout!,
|
|
198
|
+
this._arrived.length,
|
|
199
|
+
this._expected
|
|
200
|
+
)
|
|
201
|
+
this._waitReject(error)
|
|
202
|
+
this._waitResolve = null
|
|
203
|
+
this._waitReject = null
|
|
204
|
+
}
|
|
205
|
+
}, this._options.timeout)
|
|
206
|
+
}
|
|
207
|
+
})
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Reset the barrier for reuse
|
|
212
|
+
*/
|
|
213
|
+
reset(): void {
|
|
214
|
+
this._clearTimeout()
|
|
215
|
+
this._clearAbortHandler()
|
|
216
|
+
this._arrived = []
|
|
217
|
+
this._waitResolve = null
|
|
218
|
+
this._waitReject = null
|
|
219
|
+
this._cancelled = false
|
|
220
|
+
this._cancelError = null
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Cancel the barrier operation
|
|
225
|
+
*/
|
|
226
|
+
cancel(error: Error): void {
|
|
227
|
+
this._cancelled = true
|
|
228
|
+
this._cancelError = error
|
|
229
|
+
this._clearTimeout()
|
|
230
|
+
this._clearAbortHandler()
|
|
231
|
+
|
|
232
|
+
if (this._waitReject) {
|
|
233
|
+
this._waitReject(error)
|
|
234
|
+
this._waitResolve = null
|
|
235
|
+
this._waitReject = null
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Dispose of the barrier and cleanup all resources
|
|
241
|
+
*/
|
|
242
|
+
dispose(): void {
|
|
243
|
+
this._clearTimeout()
|
|
244
|
+
this._clearAbortHandler()
|
|
245
|
+
this._waitResolve = null
|
|
246
|
+
this._waitReject = null
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Get current progress information
|
|
251
|
+
*/
|
|
252
|
+
getProgress(): BarrierProgress<T> {
|
|
253
|
+
const latestValue = this._arrived[this._arrived.length - 1]
|
|
254
|
+
return {
|
|
255
|
+
arrived: this._arrived.length,
|
|
256
|
+
expected: this._expected,
|
|
257
|
+
percentage: Math.round((this._arrived.length / this._expected) * 100),
|
|
258
|
+
...(latestValue !== undefined && { latest: latestValue }),
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
private _clearTimeout(): void {
|
|
263
|
+
if (this._timeoutId) {
|
|
264
|
+
clearTimeout(this._timeoutId)
|
|
265
|
+
this._timeoutId = null
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
private _clearAbortHandler(): void {
|
|
270
|
+
if (this._abortHandler && this._options.signal) {
|
|
271
|
+
this._options.signal.removeEventListener('abort', this._abortHandler)
|
|
272
|
+
this._abortHandler = null
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Create a new barrier
|
|
279
|
+
*/
|
|
280
|
+
export function createBarrier<T = unknown>(
|
|
281
|
+
expectedCount: number,
|
|
282
|
+
options: BarrierOptions<T> = {}
|
|
283
|
+
): Barrier<T> {
|
|
284
|
+
return new Barrier<T>(expectedCount, options)
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Wait for all promises to complete
|
|
289
|
+
*
|
|
290
|
+
* Similar to Promise.all but with timeout and cancellation support
|
|
291
|
+
*/
|
|
292
|
+
export async function waitForAll<T>(
|
|
293
|
+
promises: Promise<T>[],
|
|
294
|
+
options: WaitForAllOptions = {}
|
|
295
|
+
): Promise<T[]> {
|
|
296
|
+
if (promises.length === 0) {
|
|
297
|
+
return []
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const { timeout, signal } = options
|
|
301
|
+
|
|
302
|
+
// Track cleanup handlers
|
|
303
|
+
let abortHandler: (() => void) | null = null
|
|
304
|
+
let timeoutId: ReturnType<typeof setTimeout> | null = null
|
|
305
|
+
|
|
306
|
+
const cleanup = () => {
|
|
307
|
+
if (abortHandler && signal) {
|
|
308
|
+
signal.removeEventListener('abort', abortHandler)
|
|
309
|
+
abortHandler = null
|
|
310
|
+
}
|
|
311
|
+
if (timeoutId) {
|
|
312
|
+
clearTimeout(timeoutId)
|
|
313
|
+
timeoutId = null
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// Build array of promises to race against
|
|
318
|
+
const racers: Promise<T[]>[] = [Promise.all(promises)]
|
|
319
|
+
|
|
320
|
+
// Add abort signal handling
|
|
321
|
+
if (signal) {
|
|
322
|
+
const abortPromise = new Promise<never>((_, reject) => {
|
|
323
|
+
abortHandler = () => {
|
|
324
|
+
reject(new Error('Operation aborted'))
|
|
325
|
+
}
|
|
326
|
+
if (signal.aborted) {
|
|
327
|
+
abortHandler()
|
|
328
|
+
} else {
|
|
329
|
+
signal.addEventListener('abort', abortHandler, { once: true })
|
|
330
|
+
}
|
|
331
|
+
})
|
|
332
|
+
racers.push(abortPromise)
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// Add timeout if specified
|
|
336
|
+
if (timeout) {
|
|
337
|
+
const timeoutPromise = new Promise<never>((_, reject) => {
|
|
338
|
+
timeoutId = setTimeout(() => {
|
|
339
|
+
reject(new BarrierTimeoutError(timeout, 0, promises.length))
|
|
340
|
+
}, timeout)
|
|
341
|
+
})
|
|
342
|
+
racers.push(timeoutPromise)
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
try {
|
|
346
|
+
return await Promise.race(racers)
|
|
347
|
+
} finally {
|
|
348
|
+
cleanup()
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* Wait for N of M promises to complete
|
|
354
|
+
*/
|
|
355
|
+
export async function waitForAny<T>(
|
|
356
|
+
n: number,
|
|
357
|
+
promises: Promise<T>[],
|
|
358
|
+
options: WaitForAnyOptions = {}
|
|
359
|
+
): Promise<WaitForAnyResult<T>> {
|
|
360
|
+
const { timeout, returnPartialOnTimeout = false } = options
|
|
361
|
+
|
|
362
|
+
if (n === 0) {
|
|
363
|
+
return {
|
|
364
|
+
completed: [],
|
|
365
|
+
pending: promises.map((_, i) => i),
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
if (n > promises.length) {
|
|
370
|
+
throw new Error(`Cannot wait for ${n} of ${promises.length} promises`)
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
const completed: T[] = []
|
|
374
|
+
const errors: Error[] = []
|
|
375
|
+
const pendingIndices = new Set(promises.map((_, i) => i))
|
|
376
|
+
let resolved = false
|
|
377
|
+
let timedOut = false
|
|
378
|
+
|
|
379
|
+
return new Promise<WaitForAnyResult<T>>((resolve, reject) => {
|
|
380
|
+
let timeoutId: ReturnType<typeof setTimeout> | null = null
|
|
381
|
+
|
|
382
|
+
const checkComplete = () => {
|
|
383
|
+
if (resolved) return
|
|
384
|
+
|
|
385
|
+
// Check if we have enough completions
|
|
386
|
+
if (completed.length >= n) {
|
|
387
|
+
resolved = true
|
|
388
|
+
if (timeoutId) clearTimeout(timeoutId)
|
|
389
|
+
resolve({
|
|
390
|
+
completed: [...completed],
|
|
391
|
+
pending: [...pendingIndices],
|
|
392
|
+
})
|
|
393
|
+
return
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// Check if it's impossible to complete (too many failures)
|
|
397
|
+
const remaining = pendingIndices.size
|
|
398
|
+
const canComplete = completed.length + remaining
|
|
399
|
+
if (canComplete < n) {
|
|
400
|
+
resolved = true
|
|
401
|
+
if (timeoutId) clearTimeout(timeoutId)
|
|
402
|
+
reject(new Error(`Cannot complete: need ${n} but only ${canComplete} can succeed`))
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// Track each promise
|
|
407
|
+
promises.forEach((promise, index) => {
|
|
408
|
+
promise
|
|
409
|
+
.then((value) => {
|
|
410
|
+
if (resolved) return
|
|
411
|
+
completed.push(value)
|
|
412
|
+
pendingIndices.delete(index)
|
|
413
|
+
checkComplete()
|
|
414
|
+
})
|
|
415
|
+
.catch((error) => {
|
|
416
|
+
if (resolved) return
|
|
417
|
+
errors.push(error)
|
|
418
|
+
pendingIndices.delete(index)
|
|
419
|
+
checkComplete()
|
|
420
|
+
})
|
|
421
|
+
})
|
|
422
|
+
|
|
423
|
+
// Set up timeout
|
|
424
|
+
if (timeout) {
|
|
425
|
+
timeoutId = setTimeout(() => {
|
|
426
|
+
if (resolved) return
|
|
427
|
+
timedOut = true
|
|
428
|
+
|
|
429
|
+
if (returnPartialOnTimeout) {
|
|
430
|
+
resolved = true
|
|
431
|
+
resolve({
|
|
432
|
+
completed: [...completed],
|
|
433
|
+
pending: [...pendingIndices],
|
|
434
|
+
timedOut: true,
|
|
435
|
+
})
|
|
436
|
+
} else {
|
|
437
|
+
resolved = true
|
|
438
|
+
reject(new BarrierTimeoutError(timeout, completed.length, n))
|
|
439
|
+
}
|
|
440
|
+
}, timeout)
|
|
441
|
+
}
|
|
442
|
+
})
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/**
|
|
446
|
+
* Execute tasks with a concurrency limit
|
|
447
|
+
*/
|
|
448
|
+
export async function withConcurrencyLimit<T>(
|
|
449
|
+
tasks: (() => Promise<T>)[],
|
|
450
|
+
limit: number,
|
|
451
|
+
options: ConcurrencyOptions = {}
|
|
452
|
+
): Promise<(T | Error)[]> {
|
|
453
|
+
const { collectErrors = false } = options
|
|
454
|
+
|
|
455
|
+
if (tasks.length === 0) {
|
|
456
|
+
return []
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
const results: (T | Error)[] = new Array(tasks.length)
|
|
460
|
+
let currentIndex = 0
|
|
461
|
+
let hasError = false
|
|
462
|
+
let firstError: Error | null = null
|
|
463
|
+
|
|
464
|
+
const runTask = async (): Promise<void> => {
|
|
465
|
+
while (currentIndex < tasks.length) {
|
|
466
|
+
const index = currentIndex++
|
|
467
|
+
const task = tasks[index]
|
|
468
|
+
|
|
469
|
+
if (!task) continue
|
|
470
|
+
|
|
471
|
+
try {
|
|
472
|
+
results[index] = await task()
|
|
473
|
+
} catch (error) {
|
|
474
|
+
const err = error instanceof Error ? error : new Error(String(error))
|
|
475
|
+
if (collectErrors) {
|
|
476
|
+
results[index] = err
|
|
477
|
+
} else {
|
|
478
|
+
hasError = true
|
|
479
|
+
if (!firstError) firstError = err
|
|
480
|
+
throw err
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
if (hasError && !collectErrors) {
|
|
485
|
+
break
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// Start workers up to the limit
|
|
491
|
+
const workers = Array(Math.min(limit, tasks.length))
|
|
492
|
+
.fill(null)
|
|
493
|
+
.map(() => runTask())
|
|
494
|
+
|
|
495
|
+
if (collectErrors) {
|
|
496
|
+
await Promise.allSettled(workers)
|
|
497
|
+
} else {
|
|
498
|
+
try {
|
|
499
|
+
await Promise.all(workers)
|
|
500
|
+
} catch {
|
|
501
|
+
// Re-throw the first error
|
|
502
|
+
if (firstError) throw firstError
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
return results
|
|
507
|
+
}
|