Skip to content

Repository files navigation

async-pool

A tiny, typed concurrency limiter. Run N async tasks at a time over any iterable, get results back in input order, and stop early with an AbortSignal.

  • Zero dependencies, ESM + CJS, full TypeScript types.
  • Ordered results — the output array lines up with the input, no matter what order tasks finish in.
  • Lazy — works with async iterables and infinite generators; items are only pulled as capacity frees up.
  • Two failure modespool fails fast like Promise.all, pool.settled reports every outcome like Promise.allSettled.
  • ~1 kB minified, no setTimeout, no queue library, no globals.

Install

npm install @dustfillerr-code/async-pool

Usage

import { pool } from '@dustfillerr-code/async-pool';

const urls = ['/a', '/b', '/c', '/d', '/e'];

// At most 2 requests in flight; results come back in `urls` order.
const bodies = await pool(urls, (url) => fetch(url).then((r) => r.text()), {
  concurrency: 2,
});

The worker receives the index too, and may be synchronous:

const labelled = await pool(urls, (url, index) => `${index}: ${url}`);

Collecting failures instead of aborting

pool rejects on the first failing task. When you want every outcome instead, use pool.settled (also exported as settled):

import { pool } from '@dustfillerr-code/async-pool';

const results = await pool.settled(urls, (url) => fetch(url), { concurrency: 4 });

const ok = results.filter((r) => r.status === 'fulfilled');
const failed = results.filter((r) => r.status === 'rejected');

Each entry is { status: 'fulfilled', value } or { status: 'rejected', reason }, in input order.

Cancelling

const controller = new AbortController();
setTimeout(() => controller.abort(), 5_000);

try {
  await pool(urls, (url) => fetch(url, { signal: controller.signal }), {
    signal: controller.signal,
  });
} catch (error) {
  // rejects with `controller.signal.reason`
}

Aborting stops the pool from scheduling new tasks. It cannot cancel work that is already running — pass the signal into your worker as well, as above, if the work itself needs to be interruptible.

Streaming sources

Because items are pulled lazily, the source can be an async iterable or an effectively unbounded generator. Only concurrency items are ever pulled ahead:

async function* rows(cursor: string) {
  while (cursor) {
    const page = await fetchPage(cursor);
    yield* page.rows;
    cursor = page.next;
  }
}

const processed = await pool(rows(start), (row) => index(row), { concurrency: 8 });

API

pool(items, worker, options?): Promise<R[]>

Parameter Type Description
items Iterable<T> | AsyncIterable<T> Source of work. Pulled lazily.
worker (item: T, index: number) => R | PromiseLike<R> Turns one item into a result.
options.concurrency number Max tasks in flight. Positive integer or Infinity. Default 4.
options.signal AbortSignal Stops scheduling new tasks.

Resolves with one result per item, in input order.

Rejects as soon as any task rejects. Like Promise.all, tasks already in flight keep running but their results are discarded, and no further items are pulled. Rejections from those in-flight tasks are absorbed, so they never surface as unhandled rejections.

pool.settled(items, worker, options?): Promise<PoolSettledResult<R>[]>

Same arguments. Never rejects because of a failing task — every task that started gets an entry, in input order:

type PoolSettledResult<R> =
  | { status: 'fulfilled'; value: R }
  | { status: 'rejected'; reason: unknown };

If aborted, the array covers only the items that actually started. It still rejects if the source iterable itself throws, or if the arguments are invalid.

Also available as a named export: import { settled } from '@dustfillerr-code/async-pool'.

Errors

Condition Result
concurrency is not a positive integer or Infinity rejects with TypeError
items is not iterable rejects with TypeError
worker is not a function rejects with TypeError
Signal already aborted pool rejects with signal.reason; pool.settled resolves with []
Source iterable throws rejects with that error, in both modes

Notes on behaviour

  • Ordering is by input index, not completion. A slow first item does not hold up later ones from running, only from being returned.
  • The source iterator is closed (return()) when the pool stops early, so try/finally cleanup in your generators runs.
  • concurrency: Infinity pulls the whole source eagerly — the same as Promise.all(Array.from(items).map(worker)).
  • No timers. The pool is driven purely by task settlement, so it adds nothing to the event loop.

Development

npm install
npm run lint
npm run typecheck
npm test
npm run build

License

MIT © dustfillerr-code

About

A tiny, typed concurrency limiter: run N async tasks at a time over any iterable, with ordered results and AbortSignal support.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages