npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@kylecodes/concurrency-tools

v0.1.0

Published

Small async concurrency primitives: a bounded pool, an async channel, ordered settled results, and a backpressured streaming map.

Downloads

81

Readme

@kylecodes/concurrency-tools

Small async concurrency primitives for TypeScript, composed from two pieces — a bounded pool and an async channel — into the two helpers you actually reach for:

| Export | What it does | | ------------------------------------------------ | ------------------------------------------------------------------------------------------------------ | | runPool(items, size, fn) | Bounded-concurrency Promise.allSettled(items.map(fn)) — results in input order | | boundedConcurrencyPoolStream(source, size, fn) | Stream fn over a (possibly lazy/infinite) source with backpressure — results in completion order | | createConcurrencyPool({ size }) | The underlying pool: submit, whenCapacityAvailable, onIdle | | createChannel() | The underlying unbounded async queue: push / close / fail / for await |

Zero dependencies. Plain ESM. No timers, no Node-specific APIs — runs on Node ≥18, Bun, Deno, and browsers.

Install

npm install @kylecodes/concurrency-tools
# or
bun add @kylecodes/concurrency-tools

runPool — bounded allSettled over a fixed array

You have N tasks and want at most size in flight, every outcome reported, and results lined up with inputs:

import { runPool } from "@kylecodes/concurrency-tools";

const settled = await runPool(job.tasks, 4, (task) => runTask(task));

for (const [i, outcome] of settled.entries()) {
  if (outcome.status === "fulfilled") {
    console.log(job.tasks[i].id, "→", outcome.value);
  } else {
    console.error(job.tasks[i].id, "failed:", outcome.reason);
  }
}

Like Promise.allSettled, it never rejects — one task's failure is isolated to its own entry and never affects siblings.

boundedConcurrencyPoolStream — backpressured streaming map

The workhorse for pipelines where the input is lazy (paginated API, DB cursor, message queue) and you want results as soon as each one finishes:

import { boundedConcurrencyPoolStream } from "@kylecodes/concurrency-tools";

// A lazy source: pages are only fetched as the pool has room.
async function* messageIds(): AsyncGenerator<string> {
  let pageToken: string | undefined;
  do {
    const page = await listMessages({ pageToken });
    yield* page.ids;
    pageToken = page.nextPageToken;
  } while (pageToken);
}

// At most 8 fetches in flight; each result yields the moment it completes.
for await (const message of boundedConcurrencyPoolStream(
  messageIds(),
  8,
  (id) => fetchMessage(id),
)) {
  await store(message);
}

Properties that make this safe for long pipelines:

  • Backpressure. The source is pulled one item per free slot — a million-row cursor never piles up in memory because the producer suspends while the pool is saturated.
  • Completion order. Fast items don't wait behind slow ones.
  • Fail fast. The first rejection (from fn or the source) fails the stream; in-flight siblings settle quietly and are dropped.
  • Early exit. break-ing out of the loop stops the producer from pulling the source further.

It also makes a tidy worker loop — stream over an infinite source of claimed jobs and let the pool pace how fast you claim:

for await (const _ of boundedConcurrencyPoolStream(
  claimedJobs(),
  concurrency,
  process,
)) {
  // drained for its side effects
}

createConcurrencyPool — the primitive underneath

When the two helpers don't fit, use the pool directly:

import { createConcurrencyPool } from "@kylecodes/concurrency-tools";

const pool = createConcurrencyPool({ size: 3 });

// Run a thunk under the bound. Resolves/rejects with the thunk's own outcome.
const result = await pool.submit(() => doWork());

// Pace a producer: resolves once active + queued < size.
await pool.whenCapacityAvailable();

// Wait for full drain (nothing active, nothing queued). Re-arms after idle.
await pool.onIdle();

pool.activeCount; // thunks currently running
pool.queuedCount; // thunks waiting for a slot

Guarantees: at most size thunks in flight (size: 1 is strictly sequential), one slot refills per completion (no batching), and a throwing thunk always frees its slot — a failure can never stall the pool.

createChannel — a minimal async queue

A single-consumer, unbounded channel connecting a pushing producer to a for await consumer:

import { createChannel } from "@kylecodes/concurrency-tools";

const ch = createChannel<string>();

ch.push("a"); // never blocks
ch.push("b");
ch.close(); // or ch.fail(err) — iteration throws after the buffer drains

for await (const value of ch) {
  console.log(value); // 'a', 'b'
}

Buffered values always drain before a close/fail takes effect, and the first failure wins.

Development

bun install
bun run build   # tsc → dist/ (ESM + .d.ts + source maps)
bun test

License

MIT