poolctl 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/README.md +285 -20
  2. package/package.json +1 -1
package/README.md CHANGED
@@ -1,28 +1,293 @@
1
- a simple task queue for javascript which ensures maximum concurrent tasks are
2
- less than the pool size it is mostly useful for requesting thousands of
3
- resources concurrently but avoid and keep maximum connections behind a boundary
4
- example :
1
+ # poolctl
5
2
 
6
- ### example
3
+ > A tiny, dependency-free concurrency pool for JavaScript and TypeScript.
4
+ > Run at most _N_ async tasks at once — call `pool.exec(task)` from anywhere, anytime.
5
+
6
+ [![npm version](https://img.shields.io/npm/v/poolctl)](https://www.npmjs.com/package/poolctl)
7
+ [![license](https://img.shields.io/npm/l/poolctl)](./LICENSE)
8
+
9
+ Most concurrency helpers want your work bundled into an array or an iterator up
10
+ front. `poolctl` doesn't. You create one pool with a hard limit and call
11
+ `exec(task)` wherever the work happens to originate — now, later, or from ten
12
+ different files. Everything that goes through the same pool shares one budget.
13
+
14
+ ## Table of contents
15
+
16
+ - [Why poolctl?](#why-poolctl)
17
+ - [Install](#install)
18
+ - [Examples](#examples)
19
+ - [Quick start](#quick-start)
20
+ - [One pool, many sources](#one-pool-many-sources)
21
+ - [Tasks that arrive over time](#tasks-that-arrive-over-time)
22
+ - [Errors are just rejections](#errors-are-just-rejections)
23
+ - [Watching the pool](#watching-the-pool)
24
+ - [API](#api)
25
+ - [How it compares](#how-it-compares)
26
+ - [When to use something else](#when-to-reach-for-something-else)
27
+ - [How it works](#how-it-works)
28
+ - [License](#license)
29
+
30
+ ## Why poolctl?
31
+
32
+ - **Ad-hoc, not array-bound.** You don't collect tasks into an array or wire up
33
+ an iterator. Just `await pool.exec(() => doWork())` at the call site — even
34
+ for tasks that arrive over time (requests, events, streams, a message loop)
35
+ where you don't know the full set of work ahead of time.
36
+ - **One pool, many sources, one limit.** A single `Pool` instance enforces one
37
+ global concurrency budget. Import it anywhere; every caller — wherever it
38
+ lives — draws from the same limit. No coordination, no manual counting.
39
+ - **It's just `await`.** `exec` returns the task's resolved value, and a thrown
40
+ error propagates to the `await`er. Composes with plain `try/catch` and
41
+ `Promise.all` — no events to subscribe to, no result arrays to drain.
42
+ - **Error-safe by construction.** A task's slot is released in a `finally`
43
+ block, so a thrown error can never leak a slot or stall the pool.
44
+ - **Live introspection.** `getSize()`, `getWorkingCount()`, and
45
+ `getPendingCount()` tell you exactly how full the pool is — handy for
46
+ backpressure decisions and monitoring.
47
+ - **Fair FIFO ordering.** When the pool is full, waiters are released in the
48
+ order they arrived — predictable, starvation-free scheduling.
49
+ - **Tiny and dependency-free.** Roughly 60 lines of source, **0 runtime
50
+ dependencies**, ~3 kB published. Easy to read in full, easy to audit.
51
+ - **Runs everywhere.** Pure Promise logic with no platform APIs, so it works in
52
+ Deno, Node, browsers, and workers. Ships full TypeScript types and supports
53
+ both **ESM and CommonJS** consumers.
54
+
55
+ ## Install
56
+
57
+ ```bash
58
+ npm install poolctl
59
+ ```
60
+
61
+ ```ts
62
+ import { Pool } from "poolctl";
63
+ ```
64
+
65
+ **Deno:**
66
+
67
+ ```ts
68
+ import { Pool } from "https://raw.githubusercontent.com/dev-a-loper/task-pool/main/mod.ts";
69
+ ```
70
+
71
+ ## Examples
72
+
73
+ ### Quick start
74
+
75
+ Cap concurrent requests without collecting them by hand:
76
+
77
+ ```ts
78
+ import { Pool } from "poolctl";
79
+
80
+ const pool = new Pool(5); // at most 5 tasks run at once
7
81
 
8
- ```typescript
9
- const pool = new Pool(5);
10
82
  const urls = [
11
- "https://www.google.com/1",
12
- "https://www.google.com/2",
13
- "https://www.google.com/3",
14
- "https://www.google.com/4",
15
- "https://www.google.com/5",
16
- "https://www.google.com/6",
17
- "https://www.google.com/7",
83
+ "https://example.com/1",
84
+ "https://example.com/2",
85
+ "https://example.com/3",
86
+ // ...thousands more
18
87
  ];
19
- // here maximum concurrent url being fetched is 5
20
- await Promise.all(urls.map((u) => pool.exec(() => fetch(u))));
88
+
89
+ // Never more than 5 requests in flight, no matter how long `urls` is.
90
+ await Promise.all(urls.map((url) => pool.exec(() => fetch(url))));
91
+ ```
92
+
93
+ > Pass a **function** (`() => fetch(url)`), not the promise. `poolctl` invokes
94
+ > the function only once a slot is free — that's what keeps concurrency bounded.
95
+
96
+ ### One pool, many sources
97
+
98
+ Different parts of your code, even different modules, can funnel through one
99
+ limit. A single import gives you an app-wide budget:
100
+
101
+ ```ts
102
+ // dbPool.ts — one budget for database calls, process-wide
103
+ import { Pool } from "poolctl";
104
+ export const dbPool = new Pool(10);
105
+ ```
106
+
107
+ ```ts
108
+ // anywhere in your app — both calls count against the same limit of 10
109
+ import { dbPool } from "./dbPool";
110
+
111
+ await dbPool.exec(() => query("SELECT ..."));
112
+ await dbPool.exec(() => query("INSERT ..."));
113
+ ```
114
+
115
+ You get a global "no more than 10 DB operations at once" guarantee across the
116
+ entire process, with no shared state to wire up.
117
+
118
+ ### Tasks that arrive over time
119
+
120
+ This is where `poolctl` pulls away from iterable-based tools like Deno's
121
+ `pooledMap` or `p-map`. When work shows up as **events or requests** — not a
122
+ known array — you still get a hard concurrency limit:
123
+
124
+ ```ts
125
+ import { Pool } from "poolctl";
126
+
127
+ // At most 5 expensive operations run at once across ALL incoming requests,
128
+ // even under heavy concurrent traffic. Extra requests simply queue.
129
+ const pool = new Pool(5);
130
+
131
+ server.on("request", async (req, res) => {
132
+ try {
133
+ const data = await pool.exec(() => expensiveWork(req));
134
+ res.send(data);
135
+ } catch (err) {
136
+ res.status(500).send(String(err));
137
+ }
138
+ });
139
+ ```
140
+
141
+ There's no "add everything to an array first" step — each request schedules
142
+ itself as it arrives, and the pool does the rest.
143
+
144
+ ### Errors are just rejections
145
+
146
+ A throwing task rejects the `exec` promise like any normal async call, and its
147
+ slot is **always** released — so one failure can't clog the pool:
148
+
149
+ ```ts
150
+ import { Pool } from "poolctl";
151
+
152
+ const pool = new Pool(2);
153
+
154
+ try {
155
+ await pool.exec(async () => {
156
+ throw new Error("boom");
157
+ });
158
+ } catch (err) {
159
+ console.error(err); // Error: boom
160
+ }
161
+
162
+ // The slot was freed despite the throw — the pool is still fully usable.
163
+ console.log(pool.getWorkingCount()); // 0
164
+ console.log(pool.getPendingCount()); // 0
165
+ ```
166
+
167
+ Mix it with `Promise.allSettled` to run a batch and collect every outcome,
168
+ successful or not:
169
+
170
+ ```ts
171
+ const results = await Promise.allSettled(
172
+ items.map((item) => pool.exec(() => process(item))),
173
+ );
174
+
175
+ const ok = results.filter((r) => r.status === "fulfilled");
176
+ const failed = results.filter((r) => r.status === "rejected");
177
+ ```
178
+
179
+ ### Watching the pool
180
+
181
+ The live counters make backpressure and progress monitoring trivial:
182
+
183
+ ```ts
184
+ import { Pool } from "poolctl";
185
+
186
+ const pool = new Pool(8);
187
+ const items = Array.from({ length: 500 }, (_, i) => i);
188
+
189
+ const tick = setInterval(() => {
190
+ console.log(
191
+ `running ${pool.getWorkingCount()}/${pool.getSize()} · queued ${pool.getPendingCount()}`,
192
+ );
193
+ }, 100);
194
+
195
+ await Promise.all(items.map((i) => pool.exec(() => fetch(`https://example.com/${i}`))));
196
+
197
+ clearInterval(tick);
198
+ console.log("done");
199
+ ```
200
+
201
+ ## API
202
+
203
+ ### `new Pool(size)`
204
+
205
+ Create a pool that runs at most `size` tasks concurrently.
206
+
207
+ - **`size: number`** — maximum concurrent tasks. Throws if `< 1`.
208
+
209
+ ### `pool.exec(task)`
210
+
211
+ Run a task within the pool's limit. If the pool is full, the call waits (FIFO)
212
+ until a slot frees up.
213
+
214
+ - **`task: () => Promise<T>`** — a function returning the work to do.
215
+ - **Returns** `Promise<T>` — resolves with the task's result, or rejects with
216
+ its error. The slot is released in a `finally` block, so it's always freed —
217
+ even when the task throws.
218
+
219
+ In TypeScript, the return type is inferred from the task, so results stay
220
+ typed end-to-end:
221
+
222
+ ```ts
223
+ const id = await pool.exec(async () => 42); // number
224
+ const name = await pool.exec(async () => "x"); // string
21
225
  ```
22
226
 
23
- ### Api Docs
227
+ ### `pool.getSize()`
228
+
229
+ The configured maximum concurrency (`size`).
230
+
231
+ ### `pool.getWorkingCount()`
232
+
233
+ How many tasks are running right now.
234
+
235
+ ### `pool.getPendingCount()`
236
+
237
+ How many tasks are queued, waiting for a slot.
238
+
239
+ Full generated type docs: https://doc.deno.land/https://raw.githubusercontent.com/dev-a-loper/task-pool/main/mod.ts
240
+
241
+ ## How it compares
242
+
243
+ | Capability | **poolctl** | Deno `pooledMap` | `p-limit` | `p-queue` |
244
+ |---|:---:|:---:|:---:|:---:|
245
+ | Usage style | ad-hoc `pool.exec(fn)` | one-shot map over an iterable | `limit(fn)` wrapper | ad-hoc `queue.add(fn)` |
246
+ | Call from anywhere; tasks arrive over time | ✅ | ❌ needs all inputs up front | ✅ | ✅ |
247
+ | One shared budget across many sources | ✅ | ➖ per single call | ✅ | ✅ |
248
+ | Get each result via `await` | ✅ | async iterable (input order) | ✅ | ✅ |
249
+ | Live working / pending counts | ✅ | ❌ | ✅ | ✅ |
250
+ | Priority · timeout · pause · rate-limit · events | ❌ | ❌ | ❌ | ✅ |
251
+ | Runtime dependencies | 0 | 0 (std) | 0 | `EventEmitter3` |
252
+ | ESM **and** CommonJS | ✅ | ESM | ESM only | ESM only |
253
+ | First-class Deno | ✅ | ✅ | via `npm:` | via `npm:` |
254
+ | Footprint | ~3 kB | bundled with Deno | ~2 kB | larger |
255
+
256
+ A few honest notes:
257
+
258
+ - **`pooledMap` and `p-map`** are excellent when you have a **known collection**
259
+ to transform with bounded concurrency and want results in order. They're not
260
+ designed to be a long-lived pool you call into from arbitrary places over
261
+ time — that's `poolctl`'s job.
262
+ - **`p-limit`** is the closest in spirit to `poolctl`: ad-hoc, shared budget,
263
+ with introspection. Pick `poolctl` if you prefer a class-based API, want dual
264
+ ESM/CommonJS output, or want a Deno-first origin. They're peers, not rivals.
265
+ - **`p-queue`** is far more capable — priority, per-task timeout, pause/resume,
266
+ rate limiting, events. It's also larger, ESM-only, and has a dependency.
267
+
268
+ ## When to reach for something else
269
+
270
+ `poolctl` is intentionally minimal. Use a different tool if you need:
271
+
272
+ - **Priority ordering** or a **custom queue** → `p-queue`
273
+ - **Per-task timeouts**, **pause/resume**, or **rate limiting** (N per second) → `p-queue`
274
+ - **Events** or **AbortSignal-based cancellation** → `p-queue`
275
+ - A bounded **map over a known array/iterable** → Deno `pooledMap` or `p-map`
276
+
277
+ If "at most _N_ at once, from anywhere, with zero fuss" is all you need — that's
278
+ `poolctl`.
279
+
280
+ ## How it works
281
+
282
+ `poolctl` keeps a running count of in-flight tasks and a FIFO queue of waiters
283
+ (a hand-rolled linked list, so `enqueue`/`dequeue` are O(1)). When `exec` is
284
+ called on a full pool, it creates a deferred promise, parks it at the back of
285
+ the queue, and awaits it. When a running task finishes, the waiter at the front
286
+ of the queue is resolved and starts running. Because the slot is released in a
287
+ `finally` block, it's always freed — even if the task throws.
288
+
289
+ The whole thing is ~60 lines; the source is the best documentation.
24
290
 
25
- https://doc.deno.land/https://raw.githubusercontent.com/ehsan2003/task-pool/main/mod.ts
291
+ ## License
26
292
 
27
- the code is simple so if you confused about anything read the code its most of
28
- the time best way to understand things or just open an issue
293
+ MIT
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "poolctl",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "A simple concurrency-limiting task pool for JavaScript. Runs at most N async tasks at a time.",
5
5
  "repository": {
6
6
  "type": "git",