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.
- package/README.md +285 -20
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,28 +1,293 @@
|
|
|
1
|
-
|
|
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
|
-
|
|
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
|
+
[](https://www.npmjs.com/package/poolctl)
|
|
7
|
+
[](./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://
|
|
12
|
-
"https://
|
|
13
|
-
"https://
|
|
14
|
-
|
|
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
|
-
|
|
20
|
-
|
|
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
|
-
###
|
|
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
|
-
|
|
291
|
+
## License
|
|
26
292
|
|
|
27
|
-
|
|
28
|
-
the time best way to understand things or just open an issue
|
|
293
|
+
MIT
|