/** How a queue is worked through, and what it tells along the way. */ export interface QueueOptions { /** How many items run at once. */ concurrency: number; run: (item: T) => Promise; onStart?: (item: T) => void; onDone?: (item: T, result: R) => void; onError?: (item: T, error: unknown) => void; /** True for an error that ends the run: no item starts after it. */ stopsOn?: (error: unknown) => boolean; /** Read before each start: true once nobody waits for the rest. */ cancelled?: () => boolean; } export interface QueueOutcome { done: T[]; failed: T[]; /** The items never started: after a stop, or once cancelled. */ skipped: T[]; /** Whether an error ended the run. */ stopped: boolean; } /** * Work through items a few at a time, in their order: each worker takes * the next item when it is through with one. An error that ends the run * lets the items already running finish and starts no more. * * @param items - What to run, first first. * @param options - How. * @return What ran, what failed and what never started. */ export const runQueue = async ( items: T[], options: QueueOptions ): Promise> => { const outcome: QueueOutcome = { done: [], failed: [], skipped: [], stopped: false, }; let next = 0; const worker = async (): Promise => { while (next < items.length && !outcome.stopped && !options.cancelled?.()) { const item = items[next]; next += 1; options.onStart?.(item); try { const result = await options.run(item); outcome.done.push(item); options.onDone?.(item, result); } catch (error) { outcome.failed.push(item); options.onError?.(item, error); if (options.stopsOn?.(error)) { outcome.stopped = true; } } } }; const workers = Math.max(1, Math.min(options.concurrency, items.length)); await Promise.all(Array.from({ length: workers }, () => worker())); outcome.skipped = items.slice(next); return outcome; };