-
Notifications
You must be signed in to change notification settings - Fork 2.7k
Expand file tree
/
Copy pathparallel.ts
More file actions
117 lines (106 loc) · 3.62 KB
/
Copy pathparallel.ts
File metadata and controls
117 lines (106 loc) · 3.62 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
/**
* Parallel execution with concurrency control.
*/
/** Result of parallel execution */
export interface ParallelResult<R> {
/** Results array - undefined entries indicate tasks that were skipped due to abort */
results: (R | undefined)[];
/** Whether execution was aborted before all tasks completed */
aborted: boolean;
}
/**
* Execute items with a concurrency limit using a worker pool pattern.
* Results are returned in the same order as input items.
*
* On abort: returns partial results with `aborted: true`. Completed tasks are preserved,
* in-progress tasks will complete with their abort handling, skipped tasks are `undefined`.
*
* On error: fails fast - does not wait for other workers to complete.
*
* @param items - Items to process
* @param concurrency - Maximum concurrent operations
* @param fn - Async function to execute for each item; receives a worker signal that fires on abort or fail-fast so in-flight siblings can cancel
* @param signal - Optional abort signal to stop scheduling new work
*/
export async function mapWithConcurrencyLimit<T, R>(
items: T[],
concurrency: number,
fn: (item: T, index: number, signal: AbortSignal) => Promise<R>,
signal?: AbortSignal,
): Promise<ParallelResult<R>> {
const normalizedConcurrency = Number.isFinite(concurrency) ? Math.floor(concurrency) : items.length;
const effectiveConcurrency = normalizedConcurrency > 0 ? normalizedConcurrency : items.length;
const limit = Math.max(1, Math.min(effectiveConcurrency, items.length));
const results: (R | undefined)[] = new Array(items.length);
let nextIndex = 0;
// Create internal abort controller to cancel workers on any rejection
const abortController = new AbortController();
const workerSignal = signal ? AbortSignal.any([signal, abortController.signal]) : abortController.signal;
// Promise that rejects on first error - used to fail fast (not for abort)
let rejectFirst: (error: unknown) => void;
const firstErrorPromise = new Promise<never>((_, reject) => {
rejectFirst = reject;
});
const worker = async (): Promise<void> => {
while (true) {
// On abort, stop picking up new work - but don't throw
if (workerSignal.aborted) return;
const index = nextIndex++;
if (index >= items.length) return;
try {
results[index] = await fn(items[index], index, workerSignal);
} catch (error) {
// On abort, the fn itself handles it and returns a result
// Only propagate non-abort errors
if (!workerSignal.aborted) {
abortController.abort();
rejectFirst(error);
throw error;
}
}
}
};
// Create worker pool
const workers = Array(limit)
.fill(null)
.map(() => worker());
try {
await Promise.race([Promise.all(workers), firstErrorPromise]);
} catch (error) {
// If aborted, don't rethrow - return partial results
if (signal?.aborted) {
return { results, aborted: true };
}
throw error;
}
return { results, aborted: signal?.aborted ?? false };
}
/**
* Simple counting semaphore for limiting concurrency across independently-scheduled async work.
*/
export class Semaphore {
#max: number;
#current = 0;
#queue: Array<() => void> = [];
constructor(max: number) {
const normalizedMax = Number.isFinite(max) ? Math.floor(max) : Number.POSITIVE_INFINITY;
this.#max = normalizedMax > 0 ? normalizedMax : Number.POSITIVE_INFINITY;
}
async acquire(): Promise<void> {
if (this.#current < this.#max) {
this.#current++;
return;
}
const { promise, resolve } = Promise.withResolvers<void>();
this.#queue.push(resolve);
return promise;
}
release(): void {
const next = this.#queue.shift();
if (next) {
next();
} else {
this.#current--;
}
}
}