An async task queue runs jobs under a fixed concurrency limit: it starts up to concurrency tasks at once and holds the rest in a FIFO line until a slot frees up. You've hit this shape whenever a batch of uploads, API calls, or image conversions would overwhelm the network or a rate limit if fired all at once, so you cap how many run in parallel and feed the rest in as earlier ones finish.
Implement asyncTaskQueue(concurrency). It returns a queue object. Calling add(taskFn) enqueues an async task and returns a promise that settles with taskFn()'s result (or its rejection). At most concurrency tasks run at once; the rest wait in the order they were added. A task that rejects must not stop the queue — the next waiting task takes its slot. The queue also exposes onIdle(), a promise that resolves once everything (running and queued) has finished, plus live size and pending counts.
type Task<T> = () => Promise<T> | T;
interface TaskQueue {
add<T>(task: Task<T>): Promise<T>; // enqueue; settles with task()'s outcome
onIdle(): Promise<void>; // resolves when running + queued both hit 0
readonly size: number; // tasks still waiting (not yet started)
readonly pending: number; // tasks running right now
}
function asyncTaskQueue(concurrency: number): TaskQueue;
const queue = asyncTaskQueue(2); // at most 2 run at once
const p1 = queue.add(() => fetchUser(1));
const p2 = queue.add(() => fetchUser(2));
const p3 = queue.add(() => fetchUser(3)); // waits for a free slot
queue.pending; // 2 (p1, p2 are running)
queue.size; // 1 (p3 is waiting)
await queue.onIdle(); // resolves after all three finish
const queue = asyncTaskQueue(1); // strictly one at a time
const a = queue.add(async () => {
throw new Error('boom');
});
const b = queue.add(async () => 'ok');
await a.catch((e) => e.message); // 'boom'
await b; // 'ok' — b still ran, the rejection did not stall the queue
concurrency tasks in flight; the rest wait in FIFO order and start as slots free up.add is independent — the promise it returns settles with that one task's own result or rejection, never another task's.size vs pending — size counts tasks still waiting; pending counts tasks running now. onIdle() resolves when both are 0.concurrency is a positive integer — you don't need to validate it, change it later, or support cancellation.Unlock the solution & editor
Runnable editor + tests
Solve in the browser with instant Jest feedback.
Detailed solutions
Walkthroughs, edge cases, and complexity notes.
Multi-framework variants
React, Vue, Vanilla, Angular — same question, different stacks.