| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
| Name | Name | Last commit date | ||
|---|---|---|---|---|
A tiny, dependency-free concurrency pool for JavaScript and TypeScript. Run at most N async tasks at once — call pool.exec(task) from anywhere, anytime.
Most concurrency helpers want your work bundled into an array or an iterator up front. poolctl doesn't. You create one pool with a hard limit and call exec(task) wherever the work happens to originate — now, later, or from ten different files. Everything that goes through the same pool shares one budget.
npm install poolctlimport { Pool } from "poolctl";Deno:
import { Pool } from "https://raw.githubusercontent.com/dev-a-loper/task-pool/main/mod.ts";Cap concurrent requests without collecting them by hand:
import { Pool } from "poolctl";
const pool = new Pool(5); // at most 5 tasks run at once
const urls = [
"https://example.com/1",
"https://example.com/2",
"https://example.com/3",
// ...thousands more
];
// Never more than 5 requests in flight, no matter how long `urls` is.
await Promise.all(urls.map((url) => pool.exec(() => fetch(url))));Pass a function (() => fetch(url)), not the promise. poolctl invokes the function only once a slot is free — that's what keeps concurrency bounded.
Different parts of your code, even different modules, can funnel through one limit. A single import gives you an app-wide budget:
// dbPool.ts — one budget for database calls, process-wide
import { Pool } from "poolctl";
export const dbPool = new Pool(10);// anywhere in your app — both calls count against the same limit of 10
import { dbPool } from "./dbPool";
await dbPool.exec(() => query("SELECT ..."));
await dbPool.exec(() => query("INSERT ..."));You get a global "no more than 10 DB operations at once" guarantee across the entire process, with no shared state to wire up.
This is where poolctl pulls away from iterable-based tools like Deno's pooledMap or p-map. When work shows up as events or requests — not a known array — you still get a hard concurrency limit:
import { Pool } from "poolctl";
// At most 5 expensive operations run at once across ALL incoming requests,
// even under heavy concurrent traffic. Extra requests simply queue.
const pool = new Pool(5);
server.on("request", async (req, res) => {
try {
const data = await pool.exec(() => expensiveWork(req));
res.send(data);
} catch (err) {
res.status(500).send(String(err));
}
});There's no "add everything to an array first" step — each request schedules itself as it arrives, and the pool does the rest.
A throwing task rejects the exec promise like any normal async call, and its slot is always released — so one failure can't clog the pool:
import { Pool } from "poolctl";
const pool = new Pool(2);
try {
await pool.exec(async () => {
throw new Error("boom");
});
} catch (err) {
console.error(err); // Error: boom
}
// The slot was freed despite the throw — the pool is still fully usable.
console.log(pool.getWorkingCount()); // 0
console.log(pool.getPendingCount()); // 0Mix it with Promise.allSettled to run a batch and collect every outcome, successful or not:
const results = await Promise.allSettled(
items.map((item) => pool.exec(() => process(item))),
);
const ok = results.filter((r) => r.status === "fulfilled");
const failed = results.filter((r) => r.status === "rejected");The live counters make backpressure and progress monitoring trivial:
import { Pool } from "poolctl";
const pool = new Pool(8);
const items = Array.from({ length: 500 }, (_, i) => i);
const tick = setInterval(() => {
console.log(
`running ${pool.getWorkingCount()}/${pool.getSize()} · queued ${pool.getPendingCount()}`,
);
}, 100);
await Promise.all(items.map((i) => pool.exec(() => fetch(`https://example.com/${i}`))));
clearInterval(tick);
console.log("done");Create a pool that runs at most size tasks concurrently.
Run a task within the pool's limit. If the pool is full, the call waits (FIFO) until a slot frees up.
In TypeScript, the return type is inferred from the task, so results stay typed end-to-end:
const id = await pool.exec(async () => 42); // number
const name = await pool.exec(async () => "x"); // stringThe configured maximum concurrency (size).
How many tasks are running right now.
How many tasks are queued, waiting for a slot.
Full generated type docs: https://doc.deno.land/https://raw.githubusercontent.com/dev-a-loper/task-pool/main/mod.ts
| Capability | poolctl | Deno pooledMap | p-limit | p-queue |
|---|---|---|---|---|
| Usage style | ad-hoc pool.exec(fn) | one-shot map over an iterable | limit(fn) wrapper | ad-hoc queue.add(fn) |
| Call from anywhere; tasks arrive over time | ✅ | ❌ needs all inputs up front | ✅ | ✅ |
| One shared budget across many sources | ✅ | ➖ per single call | ✅ | ✅ |
| Get each result via await | ✅ | async iterable (input order) | ✅ | ✅ |
| Live working / pending counts | ✅ | ❌ | ✅ | ✅ |
| Priority · timeout · pause · rate-limit · events | ❌ | ❌ | ❌ | ✅ |
| Runtime dependencies | 0 | 0 (std) | 0 | EventEmitter3 |
| ESM and CommonJS | ✅ | ESM | ESM only | ESM only |
| First-class Deno | ✅ | ✅ | via npm: | via npm: |
| Footprint | ~3 kB | bundled with Deno | ~2 kB | larger |
A few honest notes:
poolctl is intentionally minimal. Use a different tool if you need:
If "at most N at once, from anywhere, with zero fuss" is all you need — that's poolctl.
poolctl keeps a running count of in-flight tasks and a FIFO queue of waiters (a hand-rolled linked list, so enqueue/dequeue are O(1)). When exec is called on a full pool, it creates a deferred promise, parks it at the back of the queue, and awaits it. When a running task finishes, the waiter at the front of the queue is resolved and starts running. Because the slot is released in a finally block, it's always freed — even if the task throws.
The whole thing is ~60 lines; the source is the best documentation.
MIT
| Back | FazBrowse Home | New Git URL |