async-bulkhead-ts 0.2.2 → 0.3.0
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 +81 -21
- package/dist/index.cjs +77 -39
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +7 -3
- package/dist/index.d.ts +7 -3
- package/dist/index.js +77 -39
- package/dist/index.js.map +1 -1
- package/package.json +7 -1
package/README.md
CHANGED
|
@@ -14,11 +14,13 @@ Designed for services that prefer **rejecting work early** over silently degradi
|
|
|
14
14
|
- ✅ Explicit acquire + release via a **token**
|
|
15
15
|
- ✅ `bulkhead.run(fn)` helper (acquire + `finally` release)
|
|
16
16
|
- ✅ Optional waiting **timeout** and **AbortSignal** cancellation
|
|
17
|
+
- ✅ Graceful shutdown via **`close()`** + **`drain()`**
|
|
17
18
|
- ✅ Zero dependencies
|
|
18
19
|
- ✅ ESM + CJS support
|
|
19
20
|
- ✅ Node.js **20+**
|
|
20
21
|
|
|
21
22
|
Non-goals (by design):
|
|
23
|
+
|
|
22
24
|
- ❌ No background workers
|
|
23
25
|
- ❌ No retry logic
|
|
24
26
|
- ❌ No distributed coordination
|
|
@@ -45,7 +47,7 @@ const r = await bulkhead.acquire();
|
|
|
45
47
|
if (!r.ok) {
|
|
46
48
|
// Fail fast — shed load, return 503, etc.
|
|
47
49
|
// r.reason is one of:
|
|
48
|
-
// 'concurrency_limit' | 'queue_limit' | 'timeout' | 'aborted'
|
|
50
|
+
// 'concurrency_limit' | 'queue_limit' | 'timeout' | 'aborted' | 'shutdown'
|
|
49
51
|
throw new Error(`Rejected: ${r.reason}`);
|
|
50
52
|
}
|
|
51
53
|
|
|
@@ -91,8 +93,8 @@ const bulkhead = createBulkhead({
|
|
|
91
93
|
> Note: bounded waiting is optional.
|
|
92
94
|
> Future major versions may focus on fail-fast admission only.
|
|
93
95
|
|
|
94
|
-
|
|
95
96
|
Semantics:
|
|
97
|
+
|
|
96
98
|
- If `inFlight` < `maxConcurrent`: `acquire()` succeeds immediately.
|
|
97
99
|
- Else if `maxQueue` > 0 and queue has space: `acquire()` waits FIFO.
|
|
98
100
|
- Else: rejected immediately.
|
|
@@ -109,6 +111,7 @@ await bulkhead.run(
|
|
|
109
111
|
```
|
|
110
112
|
|
|
111
113
|
Cancellation guarantees:
|
|
114
|
+
|
|
112
115
|
- Work that is waiting in the queue can be cancelled before it starts.
|
|
113
116
|
- In-flight work is not forcibly terminated (your function may observe the signal).
|
|
114
117
|
- Capacity is always released correctly for acquired tokens.
|
|
@@ -122,6 +125,33 @@ You can also bound waiting time:
|
|
|
122
125
|
await bulkhead.run(async () => doWork(), { timeoutMs: 50 });
|
|
123
126
|
```
|
|
124
127
|
|
|
128
|
+
## Graceful Shutdown
|
|
129
|
+
|
|
130
|
+
`close()` stops admission. `drain()` waits for in-flight work to finish. Together they give you a clean shutdown sequence:
|
|
131
|
+
|
|
132
|
+
```ts
|
|
133
|
+
// In your SIGTERM handler:
|
|
134
|
+
bulkhead.close();
|
|
135
|
+
|
|
136
|
+
// All pending waiters are rejected with 'shutdown'.
|
|
137
|
+
// All future acquire/run calls reject immediately with 'shutdown'.
|
|
138
|
+
// In-flight work is not interrupted — tokens release normally.
|
|
139
|
+
|
|
140
|
+
await bulkhead.drain();
|
|
141
|
+
// Resolves when inFlight reaches zero.
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
`close()` is synchronous, idempotent, and irreversible. If you need a fresh bulkhead, create a new instance.
|
|
145
|
+
|
|
146
|
+
`drain()` is an observation primitive — it tells you when work finishes, but it cannot force work to complete. The bulkhead does not own in-flight work. If your functions support cancellation, signal them via the `AbortSignal` you already hold.
|
|
147
|
+
|
|
148
|
+
`drain()` also works without `close()`. On its own it resolves when current in-flight and pending work completes, but new work can still be admitted:
|
|
149
|
+
|
|
150
|
+
```ts
|
|
151
|
+
// Wait for current work to finish, without stopping new admissions.
|
|
152
|
+
await bulkhead.drain();
|
|
153
|
+
```
|
|
154
|
+
|
|
125
155
|
## Behavioral Guarantees
|
|
126
156
|
|
|
127
157
|
- maxConcurrent is never exceeded.
|
|
@@ -146,16 +176,9 @@ Returns:
|
|
|
146
176
|
tryAcquire(): TryAcquireResult;
|
|
147
177
|
acquire(options?): Promise<AcquireResult>;
|
|
148
178
|
run<T>(fn: (signal?: AbortSignal) => Promise<T>, options?): Promise<T>;
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
maxConcurrent: number;
|
|
153
|
-
maxQueue: number;
|
|
154
|
-
aborted?: number;
|
|
155
|
-
timedOut?: number;
|
|
156
|
-
rejected?: number;
|
|
157
|
-
doubleRelease?: number;
|
|
158
|
-
};
|
|
179
|
+
close(): void;
|
|
180
|
+
drain(): Promise<void>;
|
|
181
|
+
stats(): Stats;
|
|
159
182
|
}
|
|
160
183
|
```
|
|
161
184
|
|
|
@@ -164,7 +187,7 @@ Returns:
|
|
|
164
187
|
```ts
|
|
165
188
|
export type TryAcquireResult =
|
|
166
189
|
| { ok: true; token: Token }
|
|
167
|
-
| { ok: false; reason: 'concurrency_limit' };
|
|
190
|
+
| { ok: false; reason: 'concurrency_limit' | 'shutdown' };
|
|
168
191
|
```
|
|
169
192
|
|
|
170
193
|
> `tryAcquire()` never waits and never enqueues; it either acquires immediately or fails fast.
|
|
@@ -179,7 +202,14 @@ type AcquireOptions = {
|
|
|
179
202
|
|
|
180
203
|
type AcquireResult =
|
|
181
204
|
| { ok: true; token: Token }
|
|
182
|
-
| { ok: false; reason:
|
|
205
|
+
| { ok: false; reason: RejectReason };
|
|
206
|
+
|
|
207
|
+
type RejectReason =
|
|
208
|
+
| 'concurrency_limit'
|
|
209
|
+
| 'queue_limit'
|
|
210
|
+
| 'timeout'
|
|
211
|
+
| 'aborted'
|
|
212
|
+
| 'shutdown';
|
|
183
213
|
```
|
|
184
214
|
|
|
185
215
|
`run(fn, options?)`
|
|
@@ -189,7 +219,7 @@ Throws on rejection:
|
|
|
189
219
|
```ts
|
|
190
220
|
class BulkheadRejectedError extends Error {
|
|
191
221
|
readonly code = 'BULKHEAD_REJECTED';
|
|
192
|
-
readonly reason:
|
|
222
|
+
readonly reason: RejectReason;
|
|
193
223
|
}
|
|
194
224
|
```
|
|
195
225
|
|
|
@@ -200,24 +230,54 @@ in-flight work to observe cancellation:
|
|
|
200
230
|
await bulkhead.run(async (signal) => doWork(signal), { signal });
|
|
201
231
|
```
|
|
202
232
|
|
|
233
|
+
`close()`
|
|
234
|
+
|
|
235
|
+
Stops admission permanently. Rejects all pending waiters with `'shutdown'`. All future `tryAcquire`/`acquire`/`run` calls reject immediately with `'shutdown'`. In-flight tokens remain valid. Idempotent.
|
|
236
|
+
|
|
237
|
+
`drain()`
|
|
238
|
+
|
|
239
|
+
Returns a `Promise<void>` that resolves when `inFlight` and pending both reach zero. Multiple concurrent calls all resolve at the same moment. Works with or without `close()`.
|
|
240
|
+
|
|
241
|
+
`stats()`
|
|
242
|
+
|
|
243
|
+
```ts
|
|
244
|
+
type Stats = {
|
|
245
|
+
inFlight: number;
|
|
246
|
+
pending: number;
|
|
247
|
+
maxConcurrent: number;
|
|
248
|
+
maxQueue: number;
|
|
249
|
+
closed: boolean;
|
|
250
|
+
// debug counters:
|
|
251
|
+
aborted?: number;
|
|
252
|
+
timedOut?: number;
|
|
253
|
+
rejected?: number;
|
|
254
|
+
doubleRelease?: number;
|
|
255
|
+
inFlightUnderflow?: number;
|
|
256
|
+
};
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
`stats()` is a pure read with no side effects.
|
|
260
|
+
|
|
261
|
+
`inFlightUnderflow` should always be 0. A nonzero value indicates a bug in the library.
|
|
262
|
+
|
|
203
263
|
## Design Philosophy
|
|
204
264
|
|
|
205
265
|
This library is intentionally small.
|
|
206
266
|
|
|
207
267
|
It exists to enforce backpressure at the boundary of your system:
|
|
208
268
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
269
|
+
- before request fan-out
|
|
270
|
+
- before hitting downstream dependencies
|
|
271
|
+
- before saturation cascades
|
|
212
272
|
|
|
213
273
|
If you need retries, buffering, scheduling, or persistence—compose those around this, not inside it.
|
|
214
274
|
|
|
215
275
|
## Compatibility
|
|
216
276
|
|
|
217
|
-
|
|
218
|
-
|
|
277
|
+
- Node.js: 20+ (24 LTS recommended)
|
|
278
|
+
- Module formats: ESM and CommonJS
|
|
219
279
|
|
|
220
280
|
|
|
221
281
|
## License
|
|
222
282
|
|
|
223
|
-
MIT © 2026
|
|
283
|
+
MIT © 2026
|
package/dist/index.cjs
CHANGED
|
@@ -82,42 +82,15 @@ function createBulkhead(opts) {
|
|
|
82
82
|
throw new Error("maxQueue must be an integer >= 0");
|
|
83
83
|
}
|
|
84
84
|
let inFlight = 0;
|
|
85
|
+
let closed = false;
|
|
86
|
+
let livePending = 0;
|
|
85
87
|
const q = new RingDeque(maxQueue + 1);
|
|
88
|
+
let drainWaiters = [];
|
|
86
89
|
let aborted = 0;
|
|
87
90
|
let timedOut = 0;
|
|
88
91
|
let rejected = 0;
|
|
89
92
|
let doubleRelease = 0;
|
|
90
|
-
|
|
91
|
-
let released = false;
|
|
92
|
-
return {
|
|
93
|
-
release() {
|
|
94
|
-
if (released) {
|
|
95
|
-
doubleRelease++;
|
|
96
|
-
return;
|
|
97
|
-
}
|
|
98
|
-
released = true;
|
|
99
|
-
inFlight--;
|
|
100
|
-
if (inFlight < 0) inFlight = 0;
|
|
101
|
-
drain();
|
|
102
|
-
}
|
|
103
|
-
};
|
|
104
|
-
};
|
|
105
|
-
const pruneCancelledFront = () => {
|
|
106
|
-
while (q.length > 0) {
|
|
107
|
-
const w = q.peekFront();
|
|
108
|
-
if (w.cancelled || w.settled) {
|
|
109
|
-
cleanupWaiter(w);
|
|
110
|
-
q.popFront();
|
|
111
|
-
continue;
|
|
112
|
-
} else {
|
|
113
|
-
break;
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
};
|
|
117
|
-
const pendingCount = () => {
|
|
118
|
-
pruneCancelledFront();
|
|
119
|
-
return q.length;
|
|
120
|
-
};
|
|
93
|
+
let inFlightUnderflow = 0;
|
|
121
94
|
const cleanupWaiter = (w) => {
|
|
122
95
|
if (w.abortListener) w.abortListener();
|
|
123
96
|
if (w.timeoutId) clearTimeout(w.timeoutId);
|
|
@@ -129,14 +102,50 @@ function createBulkhead(opts) {
|
|
|
129
102
|
w.settled = true;
|
|
130
103
|
if (!w.cancelled && !r.ok) w.cancelled = true;
|
|
131
104
|
cleanupWaiter(w);
|
|
105
|
+
livePending--;
|
|
132
106
|
w.resolve(r);
|
|
133
107
|
};
|
|
134
|
-
const
|
|
108
|
+
const pruneCancelledFront = () => {
|
|
109
|
+
while (q.length > 0) {
|
|
110
|
+
const w = q.peekFront();
|
|
111
|
+
if (w.cancelled || w.settled) {
|
|
112
|
+
q.popFront();
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
break;
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
const notifyDrainWaiters = () => {
|
|
119
|
+
if (inFlight === 0 && livePending === 0 && drainWaiters.length > 0) {
|
|
120
|
+
const waiters = drainWaiters;
|
|
121
|
+
drainWaiters = [];
|
|
122
|
+
for (const resolve of waiters) resolve();
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
const makeToken = () => {
|
|
126
|
+
let released = false;
|
|
127
|
+
return {
|
|
128
|
+
release() {
|
|
129
|
+
if (released) {
|
|
130
|
+
doubleRelease++;
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
released = true;
|
|
134
|
+
inFlight--;
|
|
135
|
+
if (inFlight < 0) {
|
|
136
|
+
inFlightUnderflow++;
|
|
137
|
+
inFlight = 0;
|
|
138
|
+
}
|
|
139
|
+
pump();
|
|
140
|
+
notifyDrainWaiters();
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
};
|
|
144
|
+
const pump = () => {
|
|
135
145
|
pruneCancelledFront();
|
|
136
146
|
while (inFlight < opts.maxConcurrent && q.length > 0) {
|
|
137
147
|
const w = q.popFront();
|
|
138
|
-
if (w.cancelled) {
|
|
139
|
-
cleanupWaiter(w);
|
|
148
|
+
if (w.cancelled || w.settled) {
|
|
140
149
|
pruneCancelledFront();
|
|
141
150
|
continue;
|
|
142
151
|
}
|
|
@@ -144,7 +153,27 @@ function createBulkhead(opts) {
|
|
|
144
153
|
settle(w, { ok: true, token: makeToken() });
|
|
145
154
|
}
|
|
146
155
|
};
|
|
156
|
+
const close = () => {
|
|
157
|
+
if (closed) return;
|
|
158
|
+
closed = true;
|
|
159
|
+
while (q.length > 0) {
|
|
160
|
+
const w = q.popFront();
|
|
161
|
+
if (w.settled || w.cancelled) continue;
|
|
162
|
+
rejected++;
|
|
163
|
+
settle(w, { ok: false, reason: "shutdown" });
|
|
164
|
+
}
|
|
165
|
+
notifyDrainWaiters();
|
|
166
|
+
};
|
|
167
|
+
const drainFn = () => {
|
|
168
|
+
if (inFlight === 0 && livePending === 0) return Promise.resolve();
|
|
169
|
+
return new Promise((resolve) => {
|
|
170
|
+
drainWaiters.push(resolve);
|
|
171
|
+
});
|
|
172
|
+
};
|
|
147
173
|
const tryAcquire = () => {
|
|
174
|
+
if (closed) {
|
|
175
|
+
return { ok: false, reason: "shutdown" };
|
|
176
|
+
}
|
|
148
177
|
if (inFlight < opts.maxConcurrent) {
|
|
149
178
|
inFlight++;
|
|
150
179
|
return { ok: true, token: makeToken() };
|
|
@@ -152,6 +181,10 @@ function createBulkhead(opts) {
|
|
|
152
181
|
return { ok: false, reason: "concurrency_limit" };
|
|
153
182
|
};
|
|
154
183
|
const acquire = (ao = {}) => {
|
|
184
|
+
if (closed) {
|
|
185
|
+
rejected++;
|
|
186
|
+
return Promise.resolve({ ok: false, reason: "shutdown" });
|
|
187
|
+
}
|
|
155
188
|
if (inFlight < opts.maxConcurrent) {
|
|
156
189
|
inFlight++;
|
|
157
190
|
return Promise.resolve({ ok: true, token: makeToken() });
|
|
@@ -160,7 +193,7 @@ function createBulkhead(opts) {
|
|
|
160
193
|
rejected++;
|
|
161
194
|
return Promise.resolve({ ok: false, reason: "concurrency_limit" });
|
|
162
195
|
}
|
|
163
|
-
if (
|
|
196
|
+
if (livePending >= maxQueue) {
|
|
164
197
|
rejected++;
|
|
165
198
|
return Promise.resolve({ ok: false, reason: "queue_limit" });
|
|
166
199
|
}
|
|
@@ -172,6 +205,7 @@ function createBulkhead(opts) {
|
|
|
172
205
|
abortListener: void 0,
|
|
173
206
|
timeoutId: void 0
|
|
174
207
|
};
|
|
208
|
+
livePending++;
|
|
175
209
|
if (ao.signal) {
|
|
176
210
|
if (ao.signal.aborted) {
|
|
177
211
|
aborted++;
|
|
@@ -199,7 +233,9 @@ function createBulkhead(opts) {
|
|
|
199
233
|
}, ao.timeoutMs);
|
|
200
234
|
}
|
|
201
235
|
q.pushBack(w);
|
|
202
|
-
|
|
236
|
+
if (inFlight < opts.maxConcurrent) {
|
|
237
|
+
pump();
|
|
238
|
+
}
|
|
203
239
|
});
|
|
204
240
|
};
|
|
205
241
|
const run = async (fn, ao = {}) => {
|
|
@@ -215,15 +251,17 @@ function createBulkhead(opts) {
|
|
|
215
251
|
};
|
|
216
252
|
const stats = () => ({
|
|
217
253
|
inFlight,
|
|
218
|
-
pending:
|
|
254
|
+
pending: livePending,
|
|
219
255
|
maxConcurrent: opts.maxConcurrent,
|
|
220
256
|
maxQueue,
|
|
257
|
+
closed,
|
|
221
258
|
aborted,
|
|
222
259
|
timedOut,
|
|
223
260
|
rejected,
|
|
224
|
-
doubleRelease
|
|
261
|
+
doubleRelease,
|
|
262
|
+
inFlightUnderflow
|
|
225
263
|
});
|
|
226
|
-
return { tryAcquire, acquire, run, stats };
|
|
264
|
+
return { tryAcquire, acquire, run, stats, close, drain: drainFn };
|
|
227
265
|
}
|
|
228
266
|
// Annotate the CommonJS export names for ESM import in node:
|
|
229
267
|
0 && (module.exports = {
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["/**\n * A small ring-buffer queue.\n * - O(1) pushBack / popFront\n * - avoids array shift and head-index + compaction heuristics \n */\nclass RingDeque<T> {\n private buf: Array<T | undefined>;\n private head = 0;\n private tail = 0;\n private size = 0;\n\n constructor(capacity: number) {\n const cap = Math.max(4, capacity | 0);\n this.buf = new Array<T | undefined>(cap);\n }\n\n get length() {\n return this.size;\n }\n\n pushBack(item: T) {\n if (this.size === this.buf.length) {\n this.grow();\n }\n const idx = (this.head + this.size) % this.buf.length;\n this.buf[idx] = item;\n this.size++;\n }\n\n peekFront(): T | undefined {\n if (this.size === 0) return undefined;\n return this.buf[this.head];\n }\n\n popFront(): T | undefined {\n if (this.size === 0) return undefined;\n const item = this.buf[this.head];\n this.buf[this.head] = undefined; // help GC\n this.head = (this.head + 1) % this.buf.length;\n this.size--;\n return item;\n }\n\n private grow() {\n const newBuf = new Array<T | undefined>(this.buf.length * 2);\n for (let i = 0; i < this.size; i++) {\n newBuf[i] = this.buf[(this.head + i) % this.buf.length];\n }\n this.buf = newBuf;\n this.head = 0;\n }\n}\n\nexport type BulkheadOptions = {\n maxConcurrent: number;\n maxQueue?: number; // pending waiters allowed (0 => no waiting)\n};\n\nexport type AcquireOptions = {\n signal?: AbortSignal;\n timeoutMs?: number; // waiting timeout only\n};\n\nexport type Stats = {\n inFlight: number;\n pending: number;\n maxConcurrent: number;\n maxQueue: number;\n // optional debug counters:\n aborted?: number;\n timedOut?: number;\n rejected?: number;\n doubleRelease?: number;\n};\n\nexport type Token = { release(): void };\n\nexport type TryAcquireResult =\n | { ok: true; token: Token }\n | { ok: false; reason: 'concurrency_limit' };\n\nexport type AcquireResult =\n | { ok: true; token: Token }\n | { ok: false; reason: 'concurrency_limit' | 'queue_limit' | 'timeout' | 'aborted' };\n\ntype Waiter = {\n resolve: (r: AcquireResult) => void;\n cancelled: boolean;\n settled: boolean;\n\n abortListener: (() => void) | undefined;\n timeoutId: ReturnType<typeof setTimeout> | undefined;\n};\n\nexport type RejectReason = 'concurrency_limit' | 'queue_limit' | 'timeout' | 'aborted';\n\nexport class BulkheadRejectedError extends Error {\n readonly code = 'BULKHEAD_REJECTED' as const;\n\n constructor(readonly reason: RejectReason) {\n super(`Bulkhead rejected: ${reason}`);\n this.name = 'BulkheadRejectedError';\n }\n}\n\nexport function createBulkhead(opts: BulkheadOptions) {\n // ---- validate ----\n if (!Number.isInteger(opts.maxConcurrent) || opts.maxConcurrent <= 0) {\n throw new Error('maxConcurrent must be a positive integer');\n }\n const maxQueue = opts.maxQueue ?? 0;\n if (!Number.isInteger(maxQueue) || maxQueue < 0) {\n throw new Error('maxQueue must be an integer >= 0');\n }\n\n // ---- state ----\n let inFlight = 0;\n\n // FIFO queue as deque (no head index, just pushBack / popFront)\n const q = new RingDeque<Waiter>(maxQueue + 1); // +1 to avoid full queue edge case\n\n // optional counters\n let aborted = 0;\n let timedOut = 0;\n let rejected = 0;\n let doubleRelease = 0;\n\n // ---- token factory ----\n const makeToken = (): Token => {\n let released = false;\n return {\n release() {\n if (released) {\n doubleRelease++;\n return; // idempotent; consider throw in dev builds if you prefer\n }\n released = true;\n inFlight--;\n if (inFlight < 0) inFlight = 0; // defensive, should never happen\n drain();\n },\n };\n };\n\n const pruneCancelledFront = () => {\n // Remove cancelled/settled waiters at the front so they stop consuming maxQueue.\n while (q.length > 0) {\n const w = q.peekFront()!;\n if (w.cancelled || w.settled) {\n cleanupWaiter(w);\n q.popFront();\n continue;\n } else {\n break;\n }\n }\n }\n\n const pendingCount = () => {\n pruneCancelledFront();\n return q.length;\n }\n\n const cleanupWaiter = (w: Waiter) => {\n if (w.abortListener) w.abortListener();\n if (w.timeoutId) clearTimeout(w.timeoutId);\n w.abortListener = undefined;\n w.timeoutId = undefined;\n };\n\n const settle = (w: Waiter, r: AcquireResult) => {\n if (w.settled) return;\n w.settled = true;\n // Once settled, it's effectively cancelled for drain-skipping purposes.\n // (We keep cancelled separate because drain checks it.)\n if (!w.cancelled && !r.ok) w.cancelled = true;\n cleanupWaiter(w);\n w.resolve(r);\n };\n\n // ---- drain algorithm ----\n const drain = () => {\n // Prune first so cancelled/settled waiters don't block the head.\n pruneCancelledFront();\n while (inFlight < opts.maxConcurrent && q.length > 0) {\n const w = q.popFront()!; \n // If it was cancelled after peek/prune but before pop, skip it.\n if (w.cancelled) {\n cleanupWaiter(w);\n pruneCancelledFront(); // in case there are more cancelled after this one\n continue;\n }\n inFlight++;\n settle(w, { ok: true, token: makeToken() });\n }\n };\n\n // ---- public APIs ----\n\n const tryAcquire = (): TryAcquireResult => {\n if (inFlight < opts.maxConcurrent) {\n inFlight++;\n return { ok: true, token: makeToken() };\n }\n return { ok: false, reason: 'concurrency_limit' };\n };\n\n const acquire = (ao: AcquireOptions = {}): Promise<AcquireResult> => {\n // immediate fast path\n if (inFlight < opts.maxConcurrent) {\n inFlight++;\n return Promise.resolve({ ok: true, token: makeToken() });\n }\n\n // no waiting allowed\n if (maxQueue === 0) {\n rejected++;\n return Promise.resolve({ ok: false, reason: 'concurrency_limit' });\n }\n\n // bounded waiting\n if (pendingCount() >= maxQueue) {\n rejected++;\n return Promise.resolve({ ok: false, reason: 'queue_limit' });\n }\n\n // enqueue\n return new Promise<AcquireResult>((resolve) => {\n const w: Waiter = {\n resolve,\n cancelled: false,\n settled: false,\n abortListener: undefined,\n timeoutId: undefined,\n };\n\n // abort support\n if (ao.signal) {\n if (ao.signal.aborted) {\n aborted++;\n settle(w, { ok: false, reason: 'aborted' });\n return;\n }\n const onAbort = () => {\n // mark cancelled; drain() will skip it\n aborted++;\n w.cancelled = true;\n settle(w, { ok: false, reason: 'aborted' });\n };\n\n ao.signal.addEventListener('abort', onAbort, { once: true });\n w.abortListener = () => ao.signal!.removeEventListener('abort', onAbort);\n }\n\n // timeout support (waiting only)\n if (ao.timeoutMs != null) {\n if (!Number.isFinite(ao.timeoutMs) || ao.timeoutMs < 0) {\n // invalid => treat as immediate timeout\n timedOut++;\n settle(w, { ok: false, reason: 'timeout' });\n return;\n }\n w.timeoutId = setTimeout(() => {\n timedOut++;\n w.cancelled = true;\n settle(w, { ok: false, reason: 'timeout' });\n }, ao.timeoutMs);\n }\n\n q.pushBack(w);\n drain(); // required: capacity may have freed after the fast-path check but before enqueue\n });\n };\n\n const run = async <T>(\n fn: (signal?: AbortSignal) => Promise<T>,\n ao: AcquireOptions = {},\n ): Promise<T> => {\n const r = await acquire(ao);\n if (!r.ok) {\n throw new BulkheadRejectedError(r.reason);\n }\n try {\n return await fn(ao.signal);\n } finally {\n r.token.release();\n }\n };\n\n const stats = (): Stats => ({\n inFlight,\n pending: pendingCount(),\n maxConcurrent: opts.maxConcurrent,\n maxQueue,\n aborted,\n timedOut,\n rejected,\n doubleRelease,\n });\n\n return { tryAcquire, acquire, run, stats };\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["/**\n * A small ring-buffer queue.\n * - O(1) pushBack / popFront\n * - avoids array shift and head-index + compaction heuristics\n */\nclass RingDeque<T> {\n private buf: Array<T | undefined>;\n private head = 0;\n private tail = 0;\n private size = 0;\n\n constructor(capacity: number) {\n const cap = Math.max(4, capacity | 0);\n this.buf = new Array<T | undefined>(cap);\n }\n\n get length() {\n return this.size;\n }\n\n pushBack(item: T) {\n if (this.size === this.buf.length) {\n this.grow();\n }\n const idx = (this.head + this.size) % this.buf.length;\n this.buf[idx] = item;\n this.size++;\n }\n\n peekFront(): T | undefined {\n if (this.size === 0) return undefined;\n return this.buf[this.head];\n }\n\n popFront(): T | undefined {\n if (this.size === 0) return undefined;\n const item = this.buf[this.head];\n this.buf[this.head] = undefined; // help GC\n this.head = (this.head + 1) % this.buf.length;\n this.size--;\n return item;\n }\n\n private grow() {\n const newBuf = new Array<T | undefined>(this.buf.length * 2);\n for (let i = 0; i < this.size; i++) {\n newBuf[i] = this.buf[(this.head + i) % this.buf.length];\n }\n this.buf = newBuf;\n this.head = 0;\n }\n}\n\nexport type BulkheadOptions = {\n maxConcurrent: number;\n maxQueue?: number; // pending waiters allowed (0 => no waiting)\n};\n\nexport type AcquireOptions = {\n signal?: AbortSignal;\n timeoutMs?: number; // waiting timeout only\n};\n\nexport type Stats = {\n inFlight: number;\n pending: number;\n maxConcurrent: number;\n maxQueue: number;\n closed: boolean;\n // optional debug counters:\n aborted?: number;\n timedOut?: number;\n rejected?: number;\n doubleRelease?: number;\n inFlightUnderflow?: number;\n};\n\nexport type Token = { release(): void };\n\nexport type TryAcquireResult =\n | { ok: true; token: Token }\n | { ok: false; reason: 'concurrency_limit' | 'shutdown' };\n\nexport type AcquireResult =\n | { ok: true; token: Token }\n | { ok: false; reason: RejectReason };\n\ntype Waiter = {\n resolve: (r: AcquireResult) => void;\n cancelled: boolean;\n settled: boolean;\n\n abortListener: (() => void) | undefined;\n timeoutId: ReturnType<typeof setTimeout> | undefined;\n};\n\nexport type RejectReason =\n | 'concurrency_limit'\n | 'queue_limit'\n | 'timeout'\n | 'aborted'\n | 'shutdown';\n\nexport class BulkheadRejectedError extends Error {\n readonly code = 'BULKHEAD_REJECTED' as const;\n\n constructor(readonly reason: RejectReason) {\n super(`Bulkhead rejected: ${reason}`);\n this.name = 'BulkheadRejectedError';\n }\n}\n\nexport function createBulkhead(opts: BulkheadOptions) {\n // ---- validate ----\n if (!Number.isInteger(opts.maxConcurrent) || opts.maxConcurrent <= 0) {\n throw new Error('maxConcurrent must be a positive integer');\n }\n const maxQueue = opts.maxQueue ?? 0;\n if (!Number.isInteger(maxQueue) || maxQueue < 0) {\n throw new Error('maxQueue must be an integer >= 0');\n }\n\n // ---- state ----\n let inFlight = 0;\n let closed = false;\n\n // Live pending count — the number of waiters in the queue that have not\n // been settled (admitted, cancelled, timed out, or aborted). Tracked\n // separately from `q.length` so that `stats()` is a pure read — the\n // queue may contain stale (cancelled/settled) entries that haven't been\n // pruned yet, but `livePending` is always accurate.\n let livePending = 0;\n\n // FIFO queue as deque (no head index, just pushBack / popFront)\n const q = new RingDeque<Waiter>(maxQueue + 1); // +1 to avoid full queue edge case\n\n // Drain waiters — resolve functions for pending drain() promises.\n let drainWaiters: Array<() => void> = [];\n\n // optional counters\n let aborted = 0;\n let timedOut = 0;\n let rejected = 0;\n let doubleRelease = 0;\n let inFlightUnderflow = 0;\n\n // ---- internal helpers ----\n\n const cleanupWaiter = (w: Waiter) => {\n if (w.abortListener) w.abortListener();\n if (w.timeoutId) clearTimeout(w.timeoutId);\n w.abortListener = undefined;\n w.timeoutId = undefined;\n };\n\n const settle = (w: Waiter, r: AcquireResult) => {\n if (w.settled) return;\n w.settled = true;\n // Once settled, it's effectively cancelled for pump-skipping purposes.\n if (!w.cancelled && !r.ok) w.cancelled = true;\n cleanupWaiter(w);\n livePending--;\n w.resolve(r);\n };\n\n /**\n * Remove cancelled/settled waiters from the front of the queue so the\n * deque doesn't accumulate stale entries. Called from pump() and\n * release paths — never from stats().\n */\n const pruneCancelledFront = () => {\n while (q.length > 0) {\n const w = q.peekFront()!;\n if (w.cancelled || w.settled) {\n q.popFront();\n continue;\n }\n break;\n }\n };\n\n /** Notify drain() waiters if inFlight has reached zero. */\n const notifyDrainWaiters = () => {\n if (inFlight === 0 && livePending === 0 && drainWaiters.length > 0) {\n const waiters = drainWaiters;\n drainWaiters = [];\n for (const resolve of waiters) resolve();\n }\n };\n\n // ---- token factory ----\n const makeToken = (): Token => {\n let released = false;\n return {\n release() {\n if (released) {\n doubleRelease++;\n return;\n }\n released = true;\n inFlight--;\n if (inFlight < 0) {\n inFlightUnderflow++;\n inFlight = 0;\n }\n pump();\n notifyDrainWaiters();\n },\n };\n };\n\n // ---- pump: admit waiters from the queue when capacity frees ----\n const pump = () => {\n pruneCancelledFront();\n while (inFlight < opts.maxConcurrent && q.length > 0) {\n const w = q.popFront()!;\n if (w.cancelled || w.settled) {\n pruneCancelledFront();\n continue;\n }\n inFlight++;\n settle(w, { ok: true, token: makeToken() });\n }\n };\n\n // ---- close(): reject all pending, block future admission ----\n const close = (): void => {\n if (closed) return;\n closed = true;\n\n // Reject all pending waiters.\n while (q.length > 0) {\n const w = q.popFront()!;\n if (w.settled || w.cancelled) continue;\n rejected++;\n settle(w, { ok: false, reason: 'shutdown' });\n }\n\n // If nothing is in-flight, notify drain waiters immediately.\n notifyDrainWaiters();\n };\n\n // ---- drain(): wait for inFlight to reach zero ----\n const drainFn = (): Promise<void> => {\n if (inFlight === 0 && livePending === 0) return Promise.resolve();\n return new Promise<void>((resolve) => {\n drainWaiters.push(resolve);\n });\n };\n\n // ---- public APIs ----\n\n const tryAcquire = (): TryAcquireResult => {\n if (closed) {\n return { ok: false, reason: 'shutdown' };\n }\n if (inFlight < opts.maxConcurrent) {\n inFlight++;\n return { ok: true, token: makeToken() };\n }\n return { ok: false, reason: 'concurrency_limit' };\n };\n\n const acquire = (ao: AcquireOptions = {}): Promise<AcquireResult> => {\n // closed fast path\n if (closed) {\n rejected++;\n return Promise.resolve({ ok: false, reason: 'shutdown' });\n }\n\n // immediate fast path\n if (inFlight < opts.maxConcurrent) {\n inFlight++;\n return Promise.resolve({ ok: true, token: makeToken() });\n }\n\n // no waiting allowed\n if (maxQueue === 0) {\n rejected++;\n return Promise.resolve({ ok: false, reason: 'concurrency_limit' });\n }\n\n // bounded waiting\n if (livePending >= maxQueue) {\n rejected++;\n return Promise.resolve({ ok: false, reason: 'queue_limit' });\n }\n\n // enqueue\n return new Promise<AcquireResult>((resolve) => {\n const w: Waiter = {\n resolve,\n cancelled: false,\n settled: false,\n abortListener: undefined,\n timeoutId: undefined,\n };\n\n livePending++;\n\n // abort support\n if (ao.signal) {\n if (ao.signal.aborted) {\n aborted++;\n settle(w, { ok: false, reason: 'aborted' });\n return;\n }\n const onAbort = () => {\n aborted++;\n w.cancelled = true;\n settle(w, { ok: false, reason: 'aborted' });\n };\n\n ao.signal.addEventListener('abort', onAbort, { once: true });\n w.abortListener = () => ao.signal!.removeEventListener('abort', onAbort);\n }\n\n // timeout support (waiting only)\n if (ao.timeoutMs != null) {\n if (!Number.isFinite(ao.timeoutMs) || ao.timeoutMs < 0) {\n timedOut++;\n settle(w, { ok: false, reason: 'timeout' });\n return;\n }\n w.timeoutId = setTimeout(() => {\n timedOut++;\n w.cancelled = true;\n settle(w, { ok: false, reason: 'timeout' });\n }, ao.timeoutMs);\n }\n\n q.pushBack(w);\n // Capacity may have freed after the fast-path check but before enqueue.\n if (inFlight < opts.maxConcurrent) {\n pump();\n }\n });\n };\n\n const run = async <T>(\n fn: (signal?: AbortSignal) => Promise<T>,\n ao: AcquireOptions = {},\n ): Promise<T> => {\n const r = await acquire(ao);\n if (!r.ok) {\n throw new BulkheadRejectedError(r.reason);\n }\n try {\n return await fn(ao.signal);\n } finally {\n r.token.release();\n }\n };\n\n const stats = (): Stats => ({\n inFlight,\n pending: livePending,\n maxConcurrent: opts.maxConcurrent,\n maxQueue,\n closed,\n aborted,\n timedOut,\n rejected,\n doubleRelease,\n inFlightUnderflow,\n });\n\n return { tryAcquire, acquire, run, stats, close, drain: drainFn };\n}\n"],"mappings":"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","names":[]}
|
package/dist/index.d.cts
CHANGED
|
@@ -11,10 +11,12 @@ type Stats = {
|
|
|
11
11
|
pending: number;
|
|
12
12
|
maxConcurrent: number;
|
|
13
13
|
maxQueue: number;
|
|
14
|
+
closed: boolean;
|
|
14
15
|
aborted?: number;
|
|
15
16
|
timedOut?: number;
|
|
16
17
|
rejected?: number;
|
|
17
18
|
doubleRelease?: number;
|
|
19
|
+
inFlightUnderflow?: number;
|
|
18
20
|
};
|
|
19
21
|
type Token = {
|
|
20
22
|
release(): void;
|
|
@@ -24,16 +26,16 @@ type TryAcquireResult = {
|
|
|
24
26
|
token: Token;
|
|
25
27
|
} | {
|
|
26
28
|
ok: false;
|
|
27
|
-
reason: 'concurrency_limit';
|
|
29
|
+
reason: 'concurrency_limit' | 'shutdown';
|
|
28
30
|
};
|
|
29
31
|
type AcquireResult = {
|
|
30
32
|
ok: true;
|
|
31
33
|
token: Token;
|
|
32
34
|
} | {
|
|
33
35
|
ok: false;
|
|
34
|
-
reason:
|
|
36
|
+
reason: RejectReason;
|
|
35
37
|
};
|
|
36
|
-
type RejectReason = 'concurrency_limit' | 'queue_limit' | 'timeout' | 'aborted';
|
|
38
|
+
type RejectReason = 'concurrency_limit' | 'queue_limit' | 'timeout' | 'aborted' | 'shutdown';
|
|
37
39
|
declare class BulkheadRejectedError extends Error {
|
|
38
40
|
readonly reason: RejectReason;
|
|
39
41
|
readonly code: "BULKHEAD_REJECTED";
|
|
@@ -44,6 +46,8 @@ declare function createBulkhead(opts: BulkheadOptions): {
|
|
|
44
46
|
acquire: (ao?: AcquireOptions) => Promise<AcquireResult>;
|
|
45
47
|
run: <T>(fn: (signal?: AbortSignal) => Promise<T>, ao?: AcquireOptions) => Promise<T>;
|
|
46
48
|
stats: () => Stats;
|
|
49
|
+
close: () => void;
|
|
50
|
+
drain: () => Promise<void>;
|
|
47
51
|
};
|
|
48
52
|
|
|
49
53
|
export { type AcquireOptions, type AcquireResult, type BulkheadOptions, BulkheadRejectedError, type RejectReason, type Stats, type Token, type TryAcquireResult, createBulkhead };
|
package/dist/index.d.ts
CHANGED
|
@@ -11,10 +11,12 @@ type Stats = {
|
|
|
11
11
|
pending: number;
|
|
12
12
|
maxConcurrent: number;
|
|
13
13
|
maxQueue: number;
|
|
14
|
+
closed: boolean;
|
|
14
15
|
aborted?: number;
|
|
15
16
|
timedOut?: number;
|
|
16
17
|
rejected?: number;
|
|
17
18
|
doubleRelease?: number;
|
|
19
|
+
inFlightUnderflow?: number;
|
|
18
20
|
};
|
|
19
21
|
type Token = {
|
|
20
22
|
release(): void;
|
|
@@ -24,16 +26,16 @@ type TryAcquireResult = {
|
|
|
24
26
|
token: Token;
|
|
25
27
|
} | {
|
|
26
28
|
ok: false;
|
|
27
|
-
reason: 'concurrency_limit';
|
|
29
|
+
reason: 'concurrency_limit' | 'shutdown';
|
|
28
30
|
};
|
|
29
31
|
type AcquireResult = {
|
|
30
32
|
ok: true;
|
|
31
33
|
token: Token;
|
|
32
34
|
} | {
|
|
33
35
|
ok: false;
|
|
34
|
-
reason:
|
|
36
|
+
reason: RejectReason;
|
|
35
37
|
};
|
|
36
|
-
type RejectReason = 'concurrency_limit' | 'queue_limit' | 'timeout' | 'aborted';
|
|
38
|
+
type RejectReason = 'concurrency_limit' | 'queue_limit' | 'timeout' | 'aborted' | 'shutdown';
|
|
37
39
|
declare class BulkheadRejectedError extends Error {
|
|
38
40
|
readonly reason: RejectReason;
|
|
39
41
|
readonly code: "BULKHEAD_REJECTED";
|
|
@@ -44,6 +46,8 @@ declare function createBulkhead(opts: BulkheadOptions): {
|
|
|
44
46
|
acquire: (ao?: AcquireOptions) => Promise<AcquireResult>;
|
|
45
47
|
run: <T>(fn: (signal?: AbortSignal) => Promise<T>, ao?: AcquireOptions) => Promise<T>;
|
|
46
48
|
stats: () => Stats;
|
|
49
|
+
close: () => void;
|
|
50
|
+
drain: () => Promise<void>;
|
|
47
51
|
};
|
|
48
52
|
|
|
49
53
|
export { type AcquireOptions, type AcquireResult, type BulkheadOptions, BulkheadRejectedError, type RejectReason, type Stats, type Token, type TryAcquireResult, createBulkhead };
|
package/dist/index.js
CHANGED
|
@@ -57,42 +57,15 @@ function createBulkhead(opts) {
|
|
|
57
57
|
throw new Error("maxQueue must be an integer >= 0");
|
|
58
58
|
}
|
|
59
59
|
let inFlight = 0;
|
|
60
|
+
let closed = false;
|
|
61
|
+
let livePending = 0;
|
|
60
62
|
const q = new RingDeque(maxQueue + 1);
|
|
63
|
+
let drainWaiters = [];
|
|
61
64
|
let aborted = 0;
|
|
62
65
|
let timedOut = 0;
|
|
63
66
|
let rejected = 0;
|
|
64
67
|
let doubleRelease = 0;
|
|
65
|
-
|
|
66
|
-
let released = false;
|
|
67
|
-
return {
|
|
68
|
-
release() {
|
|
69
|
-
if (released) {
|
|
70
|
-
doubleRelease++;
|
|
71
|
-
return;
|
|
72
|
-
}
|
|
73
|
-
released = true;
|
|
74
|
-
inFlight--;
|
|
75
|
-
if (inFlight < 0) inFlight = 0;
|
|
76
|
-
drain();
|
|
77
|
-
}
|
|
78
|
-
};
|
|
79
|
-
};
|
|
80
|
-
const pruneCancelledFront = () => {
|
|
81
|
-
while (q.length > 0) {
|
|
82
|
-
const w = q.peekFront();
|
|
83
|
-
if (w.cancelled || w.settled) {
|
|
84
|
-
cleanupWaiter(w);
|
|
85
|
-
q.popFront();
|
|
86
|
-
continue;
|
|
87
|
-
} else {
|
|
88
|
-
break;
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
};
|
|
92
|
-
const pendingCount = () => {
|
|
93
|
-
pruneCancelledFront();
|
|
94
|
-
return q.length;
|
|
95
|
-
};
|
|
68
|
+
let inFlightUnderflow = 0;
|
|
96
69
|
const cleanupWaiter = (w) => {
|
|
97
70
|
if (w.abortListener) w.abortListener();
|
|
98
71
|
if (w.timeoutId) clearTimeout(w.timeoutId);
|
|
@@ -104,14 +77,50 @@ function createBulkhead(opts) {
|
|
|
104
77
|
w.settled = true;
|
|
105
78
|
if (!w.cancelled && !r.ok) w.cancelled = true;
|
|
106
79
|
cleanupWaiter(w);
|
|
80
|
+
livePending--;
|
|
107
81
|
w.resolve(r);
|
|
108
82
|
};
|
|
109
|
-
const
|
|
83
|
+
const pruneCancelledFront = () => {
|
|
84
|
+
while (q.length > 0) {
|
|
85
|
+
const w = q.peekFront();
|
|
86
|
+
if (w.cancelled || w.settled) {
|
|
87
|
+
q.popFront();
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
break;
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
const notifyDrainWaiters = () => {
|
|
94
|
+
if (inFlight === 0 && livePending === 0 && drainWaiters.length > 0) {
|
|
95
|
+
const waiters = drainWaiters;
|
|
96
|
+
drainWaiters = [];
|
|
97
|
+
for (const resolve of waiters) resolve();
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
const makeToken = () => {
|
|
101
|
+
let released = false;
|
|
102
|
+
return {
|
|
103
|
+
release() {
|
|
104
|
+
if (released) {
|
|
105
|
+
doubleRelease++;
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
released = true;
|
|
109
|
+
inFlight--;
|
|
110
|
+
if (inFlight < 0) {
|
|
111
|
+
inFlightUnderflow++;
|
|
112
|
+
inFlight = 0;
|
|
113
|
+
}
|
|
114
|
+
pump();
|
|
115
|
+
notifyDrainWaiters();
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
};
|
|
119
|
+
const pump = () => {
|
|
110
120
|
pruneCancelledFront();
|
|
111
121
|
while (inFlight < opts.maxConcurrent && q.length > 0) {
|
|
112
122
|
const w = q.popFront();
|
|
113
|
-
if (w.cancelled) {
|
|
114
|
-
cleanupWaiter(w);
|
|
123
|
+
if (w.cancelled || w.settled) {
|
|
115
124
|
pruneCancelledFront();
|
|
116
125
|
continue;
|
|
117
126
|
}
|
|
@@ -119,7 +128,27 @@ function createBulkhead(opts) {
|
|
|
119
128
|
settle(w, { ok: true, token: makeToken() });
|
|
120
129
|
}
|
|
121
130
|
};
|
|
131
|
+
const close = () => {
|
|
132
|
+
if (closed) return;
|
|
133
|
+
closed = true;
|
|
134
|
+
while (q.length > 0) {
|
|
135
|
+
const w = q.popFront();
|
|
136
|
+
if (w.settled || w.cancelled) continue;
|
|
137
|
+
rejected++;
|
|
138
|
+
settle(w, { ok: false, reason: "shutdown" });
|
|
139
|
+
}
|
|
140
|
+
notifyDrainWaiters();
|
|
141
|
+
};
|
|
142
|
+
const drainFn = () => {
|
|
143
|
+
if (inFlight === 0 && livePending === 0) return Promise.resolve();
|
|
144
|
+
return new Promise((resolve) => {
|
|
145
|
+
drainWaiters.push(resolve);
|
|
146
|
+
});
|
|
147
|
+
};
|
|
122
148
|
const tryAcquire = () => {
|
|
149
|
+
if (closed) {
|
|
150
|
+
return { ok: false, reason: "shutdown" };
|
|
151
|
+
}
|
|
123
152
|
if (inFlight < opts.maxConcurrent) {
|
|
124
153
|
inFlight++;
|
|
125
154
|
return { ok: true, token: makeToken() };
|
|
@@ -127,6 +156,10 @@ function createBulkhead(opts) {
|
|
|
127
156
|
return { ok: false, reason: "concurrency_limit" };
|
|
128
157
|
};
|
|
129
158
|
const acquire = (ao = {}) => {
|
|
159
|
+
if (closed) {
|
|
160
|
+
rejected++;
|
|
161
|
+
return Promise.resolve({ ok: false, reason: "shutdown" });
|
|
162
|
+
}
|
|
130
163
|
if (inFlight < opts.maxConcurrent) {
|
|
131
164
|
inFlight++;
|
|
132
165
|
return Promise.resolve({ ok: true, token: makeToken() });
|
|
@@ -135,7 +168,7 @@ function createBulkhead(opts) {
|
|
|
135
168
|
rejected++;
|
|
136
169
|
return Promise.resolve({ ok: false, reason: "concurrency_limit" });
|
|
137
170
|
}
|
|
138
|
-
if (
|
|
171
|
+
if (livePending >= maxQueue) {
|
|
139
172
|
rejected++;
|
|
140
173
|
return Promise.resolve({ ok: false, reason: "queue_limit" });
|
|
141
174
|
}
|
|
@@ -147,6 +180,7 @@ function createBulkhead(opts) {
|
|
|
147
180
|
abortListener: void 0,
|
|
148
181
|
timeoutId: void 0
|
|
149
182
|
};
|
|
183
|
+
livePending++;
|
|
150
184
|
if (ao.signal) {
|
|
151
185
|
if (ao.signal.aborted) {
|
|
152
186
|
aborted++;
|
|
@@ -174,7 +208,9 @@ function createBulkhead(opts) {
|
|
|
174
208
|
}, ao.timeoutMs);
|
|
175
209
|
}
|
|
176
210
|
q.pushBack(w);
|
|
177
|
-
|
|
211
|
+
if (inFlight < opts.maxConcurrent) {
|
|
212
|
+
pump();
|
|
213
|
+
}
|
|
178
214
|
});
|
|
179
215
|
};
|
|
180
216
|
const run = async (fn, ao = {}) => {
|
|
@@ -190,15 +226,17 @@ function createBulkhead(opts) {
|
|
|
190
226
|
};
|
|
191
227
|
const stats = () => ({
|
|
192
228
|
inFlight,
|
|
193
|
-
pending:
|
|
229
|
+
pending: livePending,
|
|
194
230
|
maxConcurrent: opts.maxConcurrent,
|
|
195
231
|
maxQueue,
|
|
232
|
+
closed,
|
|
196
233
|
aborted,
|
|
197
234
|
timedOut,
|
|
198
235
|
rejected,
|
|
199
|
-
doubleRelease
|
|
236
|
+
doubleRelease,
|
|
237
|
+
inFlightUnderflow
|
|
200
238
|
});
|
|
201
|
-
return { tryAcquire, acquire, run, stats };
|
|
239
|
+
return { tryAcquire, acquire, run, stats, close, drain: drainFn };
|
|
202
240
|
}
|
|
203
241
|
export {
|
|
204
242
|
BulkheadRejectedError,
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["/**\n * A small ring-buffer queue.\n * - O(1) pushBack / popFront\n * - avoids array shift and head-index + compaction heuristics \n */\nclass RingDeque<T> {\n private buf: Array<T | undefined>;\n private head = 0;\n private tail = 0;\n private size = 0;\n\n constructor(capacity: number) {\n const cap = Math.max(4, capacity | 0);\n this.buf = new Array<T | undefined>(cap);\n }\n\n get length() {\n return this.size;\n }\n\n pushBack(item: T) {\n if (this.size === this.buf.length) {\n this.grow();\n }\n const idx = (this.head + this.size) % this.buf.length;\n this.buf[idx] = item;\n this.size++;\n }\n\n peekFront(): T | undefined {\n if (this.size === 0) return undefined;\n return this.buf[this.head];\n }\n\n popFront(): T | undefined {\n if (this.size === 0) return undefined;\n const item = this.buf[this.head];\n this.buf[this.head] = undefined; // help GC\n this.head = (this.head + 1) % this.buf.length;\n this.size--;\n return item;\n }\n\n private grow() {\n const newBuf = new Array<T | undefined>(this.buf.length * 2);\n for (let i = 0; i < this.size; i++) {\n newBuf[i] = this.buf[(this.head + i) % this.buf.length];\n }\n this.buf = newBuf;\n this.head = 0;\n }\n}\n\nexport type BulkheadOptions = {\n maxConcurrent: number;\n maxQueue?: number; // pending waiters allowed (0 => no waiting)\n};\n\nexport type AcquireOptions = {\n signal?: AbortSignal;\n timeoutMs?: number; // waiting timeout only\n};\n\nexport type Stats = {\n inFlight: number;\n pending: number;\n maxConcurrent: number;\n maxQueue: number;\n // optional debug counters:\n aborted?: number;\n timedOut?: number;\n rejected?: number;\n doubleRelease?: number;\n};\n\nexport type Token = { release(): void };\n\nexport type TryAcquireResult =\n | { ok: true; token: Token }\n | { ok: false; reason: 'concurrency_limit' };\n\nexport type AcquireResult =\n | { ok: true; token: Token }\n | { ok: false; reason: 'concurrency_limit' | 'queue_limit' | 'timeout' | 'aborted' };\n\ntype Waiter = {\n resolve: (r: AcquireResult) => void;\n cancelled: boolean;\n settled: boolean;\n\n abortListener: (() => void) | undefined;\n timeoutId: ReturnType<typeof setTimeout> | undefined;\n};\n\nexport type RejectReason = 'concurrency_limit' | 'queue_limit' | 'timeout' | 'aborted';\n\nexport class BulkheadRejectedError extends Error {\n readonly code = 'BULKHEAD_REJECTED' as const;\n\n constructor(readonly reason: RejectReason) {\n super(`Bulkhead rejected: ${reason}`);\n this.name = 'BulkheadRejectedError';\n }\n}\n\nexport function createBulkhead(opts: BulkheadOptions) {\n // ---- validate ----\n if (!Number.isInteger(opts.maxConcurrent) || opts.maxConcurrent <= 0) {\n throw new Error('maxConcurrent must be a positive integer');\n }\n const maxQueue = opts.maxQueue ?? 0;\n if (!Number.isInteger(maxQueue) || maxQueue < 0) {\n throw new Error('maxQueue must be an integer >= 0');\n }\n\n // ---- state ----\n let inFlight = 0;\n\n // FIFO queue as deque (no head index, just pushBack / popFront)\n const q = new RingDeque<Waiter>(maxQueue + 1); // +1 to avoid full queue edge case\n\n // optional counters\n let aborted = 0;\n let timedOut = 0;\n let rejected = 0;\n let doubleRelease = 0;\n\n // ---- token factory ----\n const makeToken = (): Token => {\n let released = false;\n return {\n release() {\n if (released) {\n doubleRelease++;\n return; // idempotent; consider throw in dev builds if you prefer\n }\n released = true;\n inFlight--;\n if (inFlight < 0) inFlight = 0; // defensive, should never happen\n drain();\n },\n };\n };\n\n const pruneCancelledFront = () => {\n // Remove cancelled/settled waiters at the front so they stop consuming maxQueue.\n while (q.length > 0) {\n const w = q.peekFront()!;\n if (w.cancelled || w.settled) {\n cleanupWaiter(w);\n q.popFront();\n continue;\n } else {\n break;\n }\n }\n }\n\n const pendingCount = () => {\n pruneCancelledFront();\n return q.length;\n }\n\n const cleanupWaiter = (w: Waiter) => {\n if (w.abortListener) w.abortListener();\n if (w.timeoutId) clearTimeout(w.timeoutId);\n w.abortListener = undefined;\n w.timeoutId = undefined;\n };\n\n const settle = (w: Waiter, r: AcquireResult) => {\n if (w.settled) return;\n w.settled = true;\n // Once settled, it's effectively cancelled for drain-skipping purposes.\n // (We keep cancelled separate because drain checks it.)\n if (!w.cancelled && !r.ok) w.cancelled = true;\n cleanupWaiter(w);\n w.resolve(r);\n };\n\n // ---- drain algorithm ----\n const drain = () => {\n // Prune first so cancelled/settled waiters don't block the head.\n pruneCancelledFront();\n while (inFlight < opts.maxConcurrent && q.length > 0) {\n const w = q.popFront()!; \n // If it was cancelled after peek/prune but before pop, skip it.\n if (w.cancelled) {\n cleanupWaiter(w);\n pruneCancelledFront(); // in case there are more cancelled after this one\n continue;\n }\n inFlight++;\n settle(w, { ok: true, token: makeToken() });\n }\n };\n\n // ---- public APIs ----\n\n const tryAcquire = (): TryAcquireResult => {\n if (inFlight < opts.maxConcurrent) {\n inFlight++;\n return { ok: true, token: makeToken() };\n }\n return { ok: false, reason: 'concurrency_limit' };\n };\n\n const acquire = (ao: AcquireOptions = {}): Promise<AcquireResult> => {\n // immediate fast path\n if (inFlight < opts.maxConcurrent) {\n inFlight++;\n return Promise.resolve({ ok: true, token: makeToken() });\n }\n\n // no waiting allowed\n if (maxQueue === 0) {\n rejected++;\n return Promise.resolve({ ok: false, reason: 'concurrency_limit' });\n }\n\n // bounded waiting\n if (pendingCount() >= maxQueue) {\n rejected++;\n return Promise.resolve({ ok: false, reason: 'queue_limit' });\n }\n\n // enqueue\n return new Promise<AcquireResult>((resolve) => {\n const w: Waiter = {\n resolve,\n cancelled: false,\n settled: false,\n abortListener: undefined,\n timeoutId: undefined,\n };\n\n // abort support\n if (ao.signal) {\n if (ao.signal.aborted) {\n aborted++;\n settle(w, { ok: false, reason: 'aborted' });\n return;\n }\n const onAbort = () => {\n // mark cancelled; drain() will skip it\n aborted++;\n w.cancelled = true;\n settle(w, { ok: false, reason: 'aborted' });\n };\n\n ao.signal.addEventListener('abort', onAbort, { once: true });\n w.abortListener = () => ao.signal!.removeEventListener('abort', onAbort);\n }\n\n // timeout support (waiting only)\n if (ao.timeoutMs != null) {\n if (!Number.isFinite(ao.timeoutMs) || ao.timeoutMs < 0) {\n // invalid => treat as immediate timeout\n timedOut++;\n settle(w, { ok: false, reason: 'timeout' });\n return;\n }\n w.timeoutId = setTimeout(() => {\n timedOut++;\n w.cancelled = true;\n settle(w, { ok: false, reason: 'timeout' });\n }, ao.timeoutMs);\n }\n\n q.pushBack(w);\n drain(); // required: capacity may have freed after the fast-path check but before enqueue\n });\n };\n\n const run = async <T>(\n fn: (signal?: AbortSignal) => Promise<T>,\n ao: AcquireOptions = {},\n ): Promise<T> => {\n const r = await acquire(ao);\n if (!r.ok) {\n throw new BulkheadRejectedError(r.reason);\n }\n try {\n return await fn(ao.signal);\n } finally {\n r.token.release();\n }\n };\n\n const stats = (): Stats => ({\n inFlight,\n pending: pendingCount(),\n maxConcurrent: opts.maxConcurrent,\n maxQueue,\n aborted,\n timedOut,\n rejected,\n doubleRelease,\n });\n\n return { tryAcquire, acquire, run, stats };\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["/**\n * A small ring-buffer queue.\n * - O(1) pushBack / popFront\n * - avoids array shift and head-index + compaction heuristics\n */\nclass RingDeque<T> {\n private buf: Array<T | undefined>;\n private head = 0;\n private tail = 0;\n private size = 0;\n\n constructor(capacity: number) {\n const cap = Math.max(4, capacity | 0);\n this.buf = new Array<T | undefined>(cap);\n }\n\n get length() {\n return this.size;\n }\n\n pushBack(item: T) {\n if (this.size === this.buf.length) {\n this.grow();\n }\n const idx = (this.head + this.size) % this.buf.length;\n this.buf[idx] = item;\n this.size++;\n }\n\n peekFront(): T | undefined {\n if (this.size === 0) return undefined;\n return this.buf[this.head];\n }\n\n popFront(): T | undefined {\n if (this.size === 0) return undefined;\n const item = this.buf[this.head];\n this.buf[this.head] = undefined; // help GC\n this.head = (this.head + 1) % this.buf.length;\n this.size--;\n return item;\n }\n\n private grow() {\n const newBuf = new Array<T | undefined>(this.buf.length * 2);\n for (let i = 0; i < this.size; i++) {\n newBuf[i] = this.buf[(this.head + i) % this.buf.length];\n }\n this.buf = newBuf;\n this.head = 0;\n }\n}\n\nexport type BulkheadOptions = {\n maxConcurrent: number;\n maxQueue?: number; // pending waiters allowed (0 => no waiting)\n};\n\nexport type AcquireOptions = {\n signal?: AbortSignal;\n timeoutMs?: number; // waiting timeout only\n};\n\nexport type Stats = {\n inFlight: number;\n pending: number;\n maxConcurrent: number;\n maxQueue: number;\n closed: boolean;\n // optional debug counters:\n aborted?: number;\n timedOut?: number;\n rejected?: number;\n doubleRelease?: number;\n inFlightUnderflow?: number;\n};\n\nexport type Token = { release(): void };\n\nexport type TryAcquireResult =\n | { ok: true; token: Token }\n | { ok: false; reason: 'concurrency_limit' | 'shutdown' };\n\nexport type AcquireResult =\n | { ok: true; token: Token }\n | { ok: false; reason: RejectReason };\n\ntype Waiter = {\n resolve: (r: AcquireResult) => void;\n cancelled: boolean;\n settled: boolean;\n\n abortListener: (() => void) | undefined;\n timeoutId: ReturnType<typeof setTimeout> | undefined;\n};\n\nexport type RejectReason =\n | 'concurrency_limit'\n | 'queue_limit'\n | 'timeout'\n | 'aborted'\n | 'shutdown';\n\nexport class BulkheadRejectedError extends Error {\n readonly code = 'BULKHEAD_REJECTED' as const;\n\n constructor(readonly reason: RejectReason) {\n super(`Bulkhead rejected: ${reason}`);\n this.name = 'BulkheadRejectedError';\n }\n}\n\nexport function createBulkhead(opts: BulkheadOptions) {\n // ---- validate ----\n if (!Number.isInteger(opts.maxConcurrent) || opts.maxConcurrent <= 0) {\n throw new Error('maxConcurrent must be a positive integer');\n }\n const maxQueue = opts.maxQueue ?? 0;\n if (!Number.isInteger(maxQueue) || maxQueue < 0) {\n throw new Error('maxQueue must be an integer >= 0');\n }\n\n // ---- state ----\n let inFlight = 0;\n let closed = false;\n\n // Live pending count — the number of waiters in the queue that have not\n // been settled (admitted, cancelled, timed out, or aborted). Tracked\n // separately from `q.length` so that `stats()` is a pure read — the\n // queue may contain stale (cancelled/settled) entries that haven't been\n // pruned yet, but `livePending` is always accurate.\n let livePending = 0;\n\n // FIFO queue as deque (no head index, just pushBack / popFront)\n const q = new RingDeque<Waiter>(maxQueue + 1); // +1 to avoid full queue edge case\n\n // Drain waiters — resolve functions for pending drain() promises.\n let drainWaiters: Array<() => void> = [];\n\n // optional counters\n let aborted = 0;\n let timedOut = 0;\n let rejected = 0;\n let doubleRelease = 0;\n let inFlightUnderflow = 0;\n\n // ---- internal helpers ----\n\n const cleanupWaiter = (w: Waiter) => {\n if (w.abortListener) w.abortListener();\n if (w.timeoutId) clearTimeout(w.timeoutId);\n w.abortListener = undefined;\n w.timeoutId = undefined;\n };\n\n const settle = (w: Waiter, r: AcquireResult) => {\n if (w.settled) return;\n w.settled = true;\n // Once settled, it's effectively cancelled for pump-skipping purposes.\n if (!w.cancelled && !r.ok) w.cancelled = true;\n cleanupWaiter(w);\n livePending--;\n w.resolve(r);\n };\n\n /**\n * Remove cancelled/settled waiters from the front of the queue so the\n * deque doesn't accumulate stale entries. Called from pump() and\n * release paths — never from stats().\n */\n const pruneCancelledFront = () => {\n while (q.length > 0) {\n const w = q.peekFront()!;\n if (w.cancelled || w.settled) {\n q.popFront();\n continue;\n }\n break;\n }\n };\n\n /** Notify drain() waiters if inFlight has reached zero. */\n const notifyDrainWaiters = () => {\n if (inFlight === 0 && livePending === 0 && drainWaiters.length > 0) {\n const waiters = drainWaiters;\n drainWaiters = [];\n for (const resolve of waiters) resolve();\n }\n };\n\n // ---- token factory ----\n const makeToken = (): Token => {\n let released = false;\n return {\n release() {\n if (released) {\n doubleRelease++;\n return;\n }\n released = true;\n inFlight--;\n if (inFlight < 0) {\n inFlightUnderflow++;\n inFlight = 0;\n }\n pump();\n notifyDrainWaiters();\n },\n };\n };\n\n // ---- pump: admit waiters from the queue when capacity frees ----\n const pump = () => {\n pruneCancelledFront();\n while (inFlight < opts.maxConcurrent && q.length > 0) {\n const w = q.popFront()!;\n if (w.cancelled || w.settled) {\n pruneCancelledFront();\n continue;\n }\n inFlight++;\n settle(w, { ok: true, token: makeToken() });\n }\n };\n\n // ---- close(): reject all pending, block future admission ----\n const close = (): void => {\n if (closed) return;\n closed = true;\n\n // Reject all pending waiters.\n while (q.length > 0) {\n const w = q.popFront()!;\n if (w.settled || w.cancelled) continue;\n rejected++;\n settle(w, { ok: false, reason: 'shutdown' });\n }\n\n // If nothing is in-flight, notify drain waiters immediately.\n notifyDrainWaiters();\n };\n\n // ---- drain(): wait for inFlight to reach zero ----\n const drainFn = (): Promise<void> => {\n if (inFlight === 0 && livePending === 0) return Promise.resolve();\n return new Promise<void>((resolve) => {\n drainWaiters.push(resolve);\n });\n };\n\n // ---- public APIs ----\n\n const tryAcquire = (): TryAcquireResult => {\n if (closed) {\n return { ok: false, reason: 'shutdown' };\n }\n if (inFlight < opts.maxConcurrent) {\n inFlight++;\n return { ok: true, token: makeToken() };\n }\n return { ok: false, reason: 'concurrency_limit' };\n };\n\n const acquire = (ao: AcquireOptions = {}): Promise<AcquireResult> => {\n // closed fast path\n if (closed) {\n rejected++;\n return Promise.resolve({ ok: false, reason: 'shutdown' });\n }\n\n // immediate fast path\n if (inFlight < opts.maxConcurrent) {\n inFlight++;\n return Promise.resolve({ ok: true, token: makeToken() });\n }\n\n // no waiting allowed\n if (maxQueue === 0) {\n rejected++;\n return Promise.resolve({ ok: false, reason: 'concurrency_limit' });\n }\n\n // bounded waiting\n if (livePending >= maxQueue) {\n rejected++;\n return Promise.resolve({ ok: false, reason: 'queue_limit' });\n }\n\n // enqueue\n return new Promise<AcquireResult>((resolve) => {\n const w: Waiter = {\n resolve,\n cancelled: false,\n settled: false,\n abortListener: undefined,\n timeoutId: undefined,\n };\n\n livePending++;\n\n // abort support\n if (ao.signal) {\n if (ao.signal.aborted) {\n aborted++;\n settle(w, { ok: false, reason: 'aborted' });\n return;\n }\n const onAbort = () => {\n aborted++;\n w.cancelled = true;\n settle(w, { ok: false, reason: 'aborted' });\n };\n\n ao.signal.addEventListener('abort', onAbort, { once: true });\n w.abortListener = () => ao.signal!.removeEventListener('abort', onAbort);\n }\n\n // timeout support (waiting only)\n if (ao.timeoutMs != null) {\n if (!Number.isFinite(ao.timeoutMs) || ao.timeoutMs < 0) {\n timedOut++;\n settle(w, { ok: false, reason: 'timeout' });\n return;\n }\n w.timeoutId = setTimeout(() => {\n timedOut++;\n w.cancelled = true;\n settle(w, { ok: false, reason: 'timeout' });\n }, ao.timeoutMs);\n }\n\n q.pushBack(w);\n // Capacity may have freed after the fast-path check but before enqueue.\n if (inFlight < opts.maxConcurrent) {\n pump();\n }\n });\n };\n\n const run = async <T>(\n fn: (signal?: AbortSignal) => Promise<T>,\n ao: AcquireOptions = {},\n ): Promise<T> => {\n const r = await acquire(ao);\n if (!r.ok) {\n throw new BulkheadRejectedError(r.reason);\n }\n try {\n return await fn(ao.signal);\n } finally {\n r.token.release();\n }\n };\n\n const stats = (): Stats => ({\n inFlight,\n pending: livePending,\n maxConcurrent: opts.maxConcurrent,\n maxQueue,\n closed,\n aborted,\n timedOut,\n rejected,\n doubleRelease,\n inFlightUnderflow,\n });\n\n return { tryAcquire, acquire, run, stats, close, drain: drainFn };\n}\n"],"mappings":"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","names":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "async-bulkhead-ts",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"overrides": {
|
|
5
|
+
"minimatch": "^10.2.1",
|
|
6
|
+
"@eslint/eslintrc": {
|
|
7
|
+
"ajv": "^6.12.6"
|
|
8
|
+
}
|
|
9
|
+
},
|
|
4
10
|
"description": "Fail-fast admission control + bulkheads for async workloads",
|
|
5
11
|
"license": "MIT",
|
|
6
12
|
"type": "module",
|