Files
wehub-resource-sync c48612c494
CI / E2E Tests (push) Has been cancelled
CI / Lint, Typecheck & Unit Tests (push) Has been cancelled
Docs Build / Build docs site (push) Has been cancelled
Publish @openmaic packages / Build, validate & publish (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:03:23 +08:00

76 lines
2.6 KiB
TypeScript

/** A FIFO counting semaphore: at most `size` `run()` callbacks execute at once. */
function createSemaphore(size: number) {
const max = Math.max(1, Math.floor(size));
let active = 0;
const queue: Array<() => void> = [];
const pump = () => {
while (active < max && queue.length > 0) {
active += 1;
const start = queue.shift()!;
start();
}
};
return {
run<R>(fn: () => Promise<R>): Promise<R> {
return new Promise<R>((resolve, reject) => {
queue.push(() => {
fn()
.then(resolve, reject)
.finally(() => {
active -= 1;
pump();
});
});
pump();
});
},
};
}
/**
* Start `fn` over every item with at most `limit` calls in flight at once, and
* return one promise per item **immediately**, in input order — without awaiting
* them. Each item acquires a `limit`-sized semaphore slot before `fn` runs, so
* all the promises exist up front but only `limit` execute concurrently.
*
* This is the no-barrier primitive: the caller can `await` the promises in any
* order (e.g. sequentially) and each resolves as soon as *its* work is done,
* while later items keep running in the background. `shouldContinue` is checked
* when an item reaches the front of the queue; once it returns false, the
* remaining items resolve to `undefined` without running `fn`.
*
* `limit` is clamped to `[1, items.length]`, so a raw/too-large concurrency is
* safe to pass.
*/
export function lazyBoundedMap<T, R>(
items: readonly T[],
limit: number,
fn: (item: T, index: number) => Promise<R>,
options?: { shouldContinue?: () => boolean },
): Array<Promise<R | undefined>> {
const shouldContinue = options?.shouldContinue ?? (() => true);
const semaphore = createSemaphore(Math.min(Math.floor(limit), items.length || 1));
return items.map((item, index) =>
semaphore.run(async () => (shouldContinue() ? fn(item, index) : undefined)),
);
}
/**
* Run `fn` over `items` with at most `limit` calls in flight at once and await
* them all (a barrier). Results are returned in input order; a slot is
* `undefined` if its item was skipped because `shouldContinue` turned false.
*
* Prefer {@link lazyBoundedMap} when you can consume results incrementally —
* this wrapper waits for every item before returning.
*/
export async function mapWithConcurrency<T, R>(
items: readonly T[],
limit: number,
fn: (item: T, index: number) => Promise<R>,
options?: { shouldContinue?: () => boolean },
): Promise<Array<R | undefined>> {
return Promise.all(lazyBoundedMap(items, limit, fn, options));
}