async-bulkhead-ts 0.1.0 → 0.2.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 +67 -19
- package/dist/index.cjs +134 -29
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +38 -11
- package/dist/index.d.ts +38 -11
- package/dist/index.js +133 -29
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -11,6 +11,9 @@ Designed for services that prefer **rejecting work early** over queueing and deg
|
|
|
11
11
|
- ✅ **Fail-fast by default** (no hidden queues)
|
|
12
12
|
- ✅ Simple **bulkhead / concurrency limits**
|
|
13
13
|
- ✅ Explicit admission + release lifecycle
|
|
14
|
+
- ✅ `bulkhead.run(fn)` helper for safe execution
|
|
15
|
+
- ✅ Optional cancellation via `AbortSignal`
|
|
16
|
+
- ✅ Lightweight lifecycle hooks for metrics
|
|
14
17
|
- ✅ Zero dependencies
|
|
15
18
|
- ✅ ESM + CJS support
|
|
16
19
|
- ✅ Node.js **20+**
|
|
@@ -19,7 +22,7 @@ Non-goals (by design):
|
|
|
19
22
|
- ❌ No background workers
|
|
20
23
|
- ❌ No retry logic
|
|
21
24
|
- ❌ No distributed coordination
|
|
22
|
-
- ❌ No metrics backend (hooks
|
|
25
|
+
- ❌ No built-in metrics backend (hooks only)
|
|
23
26
|
|
|
24
27
|
---
|
|
25
28
|
|
|
@@ -29,7 +32,7 @@ Non-goals (by design):
|
|
|
29
32
|
npm install async-bulkhead-ts
|
|
30
33
|
```
|
|
31
34
|
|
|
32
|
-
## Basic Usage
|
|
35
|
+
## Basic Usage (Manual)
|
|
33
36
|
|
|
34
37
|
```ts
|
|
35
38
|
import { createBulkhead } from 'async-bulkhead-ts';
|
|
@@ -39,28 +42,69 @@ const admission = bulkhead.admit();
|
|
|
39
42
|
|
|
40
43
|
if (!admission.ok) {
|
|
41
44
|
// Fail fast — shed load, return 503, etc.
|
|
42
|
-
throw
|
|
45
|
+
throw admission.error;
|
|
43
46
|
}
|
|
44
47
|
|
|
45
48
|
try {
|
|
46
|
-
// Do async work
|
|
47
49
|
await doWork();
|
|
48
50
|
} finally {
|
|
49
51
|
admission.release();
|
|
50
52
|
}
|
|
51
53
|
```
|
|
52
54
|
|
|
55
|
+
You must call `release()` exactly once if admission succeeds.
|
|
56
|
+
Failing to release permanently reduces capacity.
|
|
57
|
+
|
|
58
|
+
## Convenience Helper
|
|
59
|
+
|
|
60
|
+
For most use cases, prefer `bulkhead.run(fn)`:
|
|
61
|
+
|
|
62
|
+
```ts
|
|
63
|
+
await bulkhead.run(async () => {
|
|
64
|
+
await doWork();
|
|
65
|
+
});
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Behavior:
|
|
69
|
+
|
|
70
|
+
* Admission + release handled automatically
|
|
71
|
+
* Still fail-fast
|
|
72
|
+
* Admission failures throw typed errors
|
|
73
|
+
* Supports cancellation via AbortSignal
|
|
74
|
+
|
|
53
75
|
## With a Queue (Optional)
|
|
54
76
|
|
|
55
77
|
Queues are opt-in and bounded.
|
|
56
|
-
|
|
78
|
+
|
|
79
|
+
```ts
|
|
57
80
|
const bulkhead = createBulkhead({
|
|
58
81
|
maxConcurrent: 10,
|
|
59
82
|
maxQueue: 20,
|
|
60
83
|
});
|
|
61
84
|
```
|
|
62
85
|
|
|
63
|
-
|
|
86
|
+
When both concurrency and queue limits are exceeded, admission fails immediately.
|
|
87
|
+
|
|
88
|
+
Queue ordering is FIFO.
|
|
89
|
+
|
|
90
|
+
## Cancellation
|
|
91
|
+
|
|
92
|
+
Bulkhead operations can be bound to an `AbortSignal`:
|
|
93
|
+
|
|
94
|
+
```ts
|
|
95
|
+
await bulkhead.run(
|
|
96
|
+
async () => {
|
|
97
|
+
await doWork();
|
|
98
|
+
},
|
|
99
|
+
{ signal }
|
|
100
|
+
);
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Cancellation guarantees:
|
|
104
|
+
|
|
105
|
+
* Queued work can be cancelled before execution
|
|
106
|
+
* In-flight work observes the signal but is not forcibly terminated
|
|
107
|
+
* Capacity is always released correctly
|
|
64
108
|
|
|
65
109
|
## API
|
|
66
110
|
|
|
@@ -75,9 +119,10 @@ type BulkheadOptions = {
|
|
|
75
119
|
|
|
76
120
|
Returns:
|
|
77
121
|
|
|
78
|
-
```
|
|
122
|
+
```ts
|
|
79
123
|
{
|
|
80
124
|
admit(): AdmitResult;
|
|
125
|
+
run<T>(fn: () => Promise<T>, options?): Promise<T>;
|
|
81
126
|
stats(): {
|
|
82
127
|
inFlight: number;
|
|
83
128
|
queued: number;
|
|
@@ -92,37 +137,40 @@ Returns:
|
|
|
92
137
|
```ts
|
|
93
138
|
type AdmitResult =
|
|
94
139
|
| { ok: true; release: () => void }
|
|
95
|
-
| { ok: false;
|
|
140
|
+
| { ok: false; error: BulkheadError };
|
|
96
141
|
```
|
|
97
142
|
|
|
98
|
-
|
|
143
|
+
Admission failures are returned as typed errors, not strings.
|
|
144
|
+
|
|
145
|
+
## Metrics Hooks
|
|
146
|
+
|
|
147
|
+
Optional lifecycle hooks allow integration with metrics systems:
|
|
99
148
|
|
|
100
|
-
|
|
149
|
+
* admission accepted
|
|
150
|
+
* queued
|
|
151
|
+
* execution started
|
|
152
|
+
* released
|
|
153
|
+
* rejected
|
|
154
|
+
|
|
155
|
+
Hooks are synchronous and side-effect–free by design.
|
|
101
156
|
|
|
102
157
|
## Design Philosophy
|
|
103
158
|
|
|
104
159
|
This library is intentionally small.
|
|
105
160
|
|
|
106
|
-
It exists to enforce
|
|
161
|
+
It exists to enforce backpressure at the boundary of your system:
|
|
107
162
|
|
|
108
163
|
* before request fan-out
|
|
109
164
|
* before hitting downstream dependencies
|
|
110
165
|
* before saturation cascades
|
|
111
166
|
|
|
112
|
-
If you need retries, buffering, scheduling, or persistence—compose those
|
|
167
|
+
If you need retries, buffering, scheduling, or persistence—compose those around this, not inside it.
|
|
113
168
|
|
|
114
169
|
## Compatibility
|
|
115
170
|
|
|
116
171
|
* Node.js: 20+ (24 LTS recommended)
|
|
117
172
|
* Module formats: ESM and CommonJS
|
|
118
173
|
|
|
119
|
-
## Roadmap (Short)
|
|
120
|
-
|
|
121
|
-
* `bulkhead.run(fn)` helper
|
|
122
|
-
* Structured metrics hooks
|
|
123
|
-
* `AbortSignal` / cancellation support
|
|
124
|
-
* Optional time-based admission windows
|
|
125
|
-
|
|
126
174
|
## License
|
|
127
175
|
|
|
128
176
|
MIT © 2026
|
package/dist/index.cjs
CHANGED
|
@@ -20,59 +20,164 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
20
20
|
// src/index.ts
|
|
21
21
|
var index_exports = {};
|
|
22
22
|
__export(index_exports, {
|
|
23
|
+
BulkheadRejectedError: () => BulkheadRejectedError,
|
|
23
24
|
createBulkhead: () => createBulkhead
|
|
24
25
|
});
|
|
25
26
|
module.exports = __toCommonJS(index_exports);
|
|
27
|
+
var BulkheadRejectedError = class extends Error {
|
|
28
|
+
constructor(reason) {
|
|
29
|
+
super(`Bulkhead rejected: ${reason}`);
|
|
30
|
+
this.reason = reason;
|
|
31
|
+
this.name = "BulkheadRejectedError";
|
|
32
|
+
}
|
|
33
|
+
code = "BULKHEAD_REJECTED";
|
|
34
|
+
};
|
|
26
35
|
function createBulkhead(opts) {
|
|
27
36
|
if (!Number.isInteger(opts.maxConcurrent) || opts.maxConcurrent <= 0) {
|
|
28
37
|
throw new Error("maxConcurrent must be a positive integer");
|
|
29
38
|
}
|
|
30
39
|
const maxQueue = opts.maxQueue ?? 0;
|
|
40
|
+
if (!Number.isInteger(maxQueue) || maxQueue < 0) {
|
|
41
|
+
throw new Error("maxQueue must be an integer >= 0");
|
|
42
|
+
}
|
|
31
43
|
let inFlight = 0;
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
44
|
+
const q = [];
|
|
45
|
+
let qHead = 0;
|
|
46
|
+
let aborted = 0;
|
|
47
|
+
let timedOut = 0;
|
|
48
|
+
let rejected = 0;
|
|
49
|
+
let doubleRelease = 0;
|
|
50
|
+
const pendingCount = () => q.length - qHead;
|
|
51
|
+
const makeToken = () => {
|
|
52
|
+
let released = false;
|
|
53
|
+
return {
|
|
54
|
+
release() {
|
|
55
|
+
if (released) {
|
|
56
|
+
doubleRelease++;
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
released = true;
|
|
60
|
+
inFlight--;
|
|
61
|
+
if (inFlight < 0) inFlight = 0;
|
|
62
|
+
drain();
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
};
|
|
66
|
+
const cleanupWaiter = (w) => {
|
|
67
|
+
if (w.abortListener) w.abortListener();
|
|
68
|
+
if (w.timeoutId) clearTimeout(w.timeoutId);
|
|
69
|
+
w.abortListener = void 0;
|
|
70
|
+
w.timeoutId = void 0;
|
|
71
|
+
};
|
|
72
|
+
const drain = () => {
|
|
73
|
+
while (inFlight < opts.maxConcurrent && pendingCount() > 0) {
|
|
74
|
+
const w = q[qHead++];
|
|
75
|
+
if (w.cancelled) {
|
|
76
|
+
cleanupWaiter(w);
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
36
79
|
inFlight++;
|
|
37
|
-
|
|
80
|
+
cleanupWaiter(w);
|
|
81
|
+
w.resolve({ ok: true, token: makeToken() });
|
|
82
|
+
}
|
|
83
|
+
if (qHead > 1024 && qHead * 2 > q.length) {
|
|
84
|
+
q.splice(0, qHead);
|
|
85
|
+
qHead = 0;
|
|
38
86
|
}
|
|
39
87
|
};
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
|
|
88
|
+
const tryAcquire = () => {
|
|
89
|
+
if (inFlight < opts.maxConcurrent) {
|
|
90
|
+
inFlight++;
|
|
91
|
+
return { ok: true, token: makeToken() };
|
|
92
|
+
}
|
|
93
|
+
if (maxQueue > 0 && pendingCount() >= maxQueue) {
|
|
94
|
+
rejected++;
|
|
95
|
+
return { ok: false, reason: "queue_limit" };
|
|
96
|
+
}
|
|
97
|
+
rejected++;
|
|
98
|
+
return { ok: false, reason: maxQueue > 0 ? "queue_limit" : "concurrency_limit" };
|
|
43
99
|
};
|
|
44
|
-
const
|
|
100
|
+
const acquire = (ao = {}) => {
|
|
45
101
|
if (inFlight < opts.maxConcurrent) {
|
|
46
102
|
inFlight++;
|
|
47
|
-
return { ok: true,
|
|
103
|
+
return Promise.resolve({ ok: true, token: makeToken() });
|
|
104
|
+
}
|
|
105
|
+
if (maxQueue === 0) {
|
|
106
|
+
rejected++;
|
|
107
|
+
return Promise.resolve({ ok: false, reason: "concurrency_limit" });
|
|
108
|
+
}
|
|
109
|
+
if (pendingCount() >= maxQueue) {
|
|
110
|
+
rejected++;
|
|
111
|
+
return Promise.resolve({ ok: false, reason: "queue_limit" });
|
|
48
112
|
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
113
|
+
return new Promise((resolve) => {
|
|
114
|
+
const w = {
|
|
115
|
+
resolve,
|
|
116
|
+
cancelled: false,
|
|
117
|
+
abortListener: void 0,
|
|
118
|
+
timeoutId: void 0
|
|
53
119
|
};
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
120
|
+
if (ao.signal) {
|
|
121
|
+
if (ao.signal.aborted) {
|
|
122
|
+
aborted++;
|
|
123
|
+
resolve({ ok: false, reason: "aborted" });
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
const onAbort = () => {
|
|
127
|
+
if (!w.cancelled) {
|
|
128
|
+
w.cancelled = true;
|
|
129
|
+
aborted++;
|
|
130
|
+
cleanupWaiter(w);
|
|
131
|
+
resolve({ ok: false, reason: "aborted" });
|
|
63
132
|
}
|
|
133
|
+
};
|
|
134
|
+
ao.signal.addEventListener("abort", onAbort, { once: true });
|
|
135
|
+
w.abortListener = () => ao.signal.removeEventListener("abort", onAbort);
|
|
136
|
+
}
|
|
137
|
+
if (ao.timeoutMs != null) {
|
|
138
|
+
if (!Number.isFinite(ao.timeoutMs) || ao.timeoutMs < 0) {
|
|
139
|
+
timedOut++;
|
|
140
|
+
resolve({ ok: false, reason: "timeout" });
|
|
141
|
+
return;
|
|
64
142
|
}
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
143
|
+
w.timeoutId = setTimeout(() => {
|
|
144
|
+
if (!w.cancelled) {
|
|
145
|
+
w.cancelled = true;
|
|
146
|
+
timedOut++;
|
|
147
|
+
cleanupWaiter(w);
|
|
148
|
+
resolve({ ok: false, reason: "timeout" });
|
|
149
|
+
}
|
|
150
|
+
}, ao.timeoutMs);
|
|
151
|
+
}
|
|
152
|
+
q.push(w);
|
|
153
|
+
});
|
|
68
154
|
};
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
155
|
+
const run = async (fn, ao = {}) => {
|
|
156
|
+
const r = await acquire(ao);
|
|
157
|
+
if (!r.ok) {
|
|
158
|
+
throw new BulkheadRejectedError(r.reason);
|
|
159
|
+
}
|
|
160
|
+
try {
|
|
161
|
+
return await fn(ao.signal);
|
|
162
|
+
} finally {
|
|
163
|
+
r.token.release();
|
|
164
|
+
}
|
|
72
165
|
};
|
|
166
|
+
const stats = () => ({
|
|
167
|
+
inFlight,
|
|
168
|
+
pending: pendingCount(),
|
|
169
|
+
maxConcurrent: opts.maxConcurrent,
|
|
170
|
+
maxQueue,
|
|
171
|
+
aborted,
|
|
172
|
+
timedOut,
|
|
173
|
+
rejected,
|
|
174
|
+
doubleRelease
|
|
175
|
+
});
|
|
176
|
+
return { tryAcquire, acquire, run, stats };
|
|
73
177
|
}
|
|
74
178
|
// Annotate the CommonJS export names for ESM import in node:
|
|
75
179
|
0 && (module.exports = {
|
|
180
|
+
BulkheadRejectedError,
|
|
76
181
|
createBulkhead
|
|
77
182
|
});
|
|
78
183
|
//# sourceMappingURL=index.cjs.map
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["export type BulkheadOptions = {\n maxConcurrent: number;\n maxQueue?: number; //
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["export 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' | 'queue_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\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 array with head index (cheap shift)\n const q: Waiter[] = [];\n let qHead = 0;\n\n // optional counters\n let aborted = 0;\n let timedOut = 0;\n let rejected = 0;\n let doubleRelease = 0;\n\n const pendingCount = () => q.length - qHead;\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 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 // ---- drain algorithm ----\n const drain = () => {\n while (inFlight < opts.maxConcurrent && pendingCount() > 0) {\n const w = q[qHead++]!;\n // skip cancelled waiters\n if (w.cancelled) {\n cleanupWaiter(w);\n continue;\n }\n\n // grant slot\n inFlight++;\n cleanupWaiter(w);\n w.resolve({ ok: true, token: makeToken() });\n }\n\n // occasional compaction to avoid unbounded growth\n if (qHead > 1024 && qHead * 2 > q.length) {\n q.splice(0, qHead);\n qHead = 0;\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 // tryAcquire never waits; queue_limit matters only if maxQueue configured\n if (maxQueue > 0 && pendingCount() >= maxQueue) {\n rejected++;\n return { ok: false, reason: 'queue_limit' };\n }\n rejected++;\n return { ok: false, reason: maxQueue > 0 ? 'queue_limit' : '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 abortListener: undefined,\n timeoutId: undefined,\n };\n\n // abort support\n if (ao.signal) {\n if (ao.signal.aborted) {\n aborted++;\n resolve({ ok: false, reason: 'aborted' });\n return;\n }\n const onAbort = () => {\n // mark cancelled; drain() will skip it\n if (!w.cancelled) {\n w.cancelled = true;\n aborted++;\n cleanupWaiter(w);\n resolve({ 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 resolve({ ok: false, reason: 'timeout' });\n return;\n }\n w.timeoutId = setTimeout(() => {\n if (!w.cancelled) {\n w.cancelled = true;\n timedOut++;\n cleanupWaiter(w);\n resolve({ ok: false, reason: 'timeout' });\n }\n }, ao.timeoutMs);\n }\n\n q.push(w);\n // NOTE: we do NOT reserve capacity here.\n // A slot is consumed only when drain() grants a token.\n\n // No need to call drain() here because we already know we’re at capacity,\n // but it doesn’t hurt if you want (for races with release).\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":[]}
|
package/dist/index.d.cts
CHANGED
|
@@ -2,21 +2,48 @@ type BulkheadOptions = {
|
|
|
2
2
|
maxConcurrent: number;
|
|
3
3
|
maxQueue?: number;
|
|
4
4
|
};
|
|
5
|
-
type
|
|
5
|
+
type AcquireOptions = {
|
|
6
|
+
signal?: AbortSignal;
|
|
7
|
+
timeoutMs?: number;
|
|
8
|
+
};
|
|
9
|
+
type Stats = {
|
|
10
|
+
inFlight: number;
|
|
11
|
+
pending: number;
|
|
12
|
+
maxConcurrent: number;
|
|
13
|
+
maxQueue: number;
|
|
14
|
+
aborted?: number;
|
|
15
|
+
timedOut?: number;
|
|
16
|
+
rejected?: number;
|
|
17
|
+
doubleRelease?: number;
|
|
18
|
+
};
|
|
19
|
+
type Token = {
|
|
20
|
+
release(): void;
|
|
21
|
+
};
|
|
22
|
+
type TryAcquireResult = {
|
|
23
|
+
ok: true;
|
|
24
|
+
token: Token;
|
|
25
|
+
} | {
|
|
26
|
+
ok: false;
|
|
27
|
+
reason: 'concurrency_limit' | 'queue_limit';
|
|
28
|
+
};
|
|
29
|
+
type AcquireResult = {
|
|
6
30
|
ok: true;
|
|
7
|
-
|
|
31
|
+
token: Token;
|
|
8
32
|
} | {
|
|
9
33
|
ok: false;
|
|
10
|
-
reason:
|
|
34
|
+
reason: 'concurrency_limit' | 'queue_limit' | 'timeout' | 'aborted';
|
|
11
35
|
};
|
|
36
|
+
type RejectReason = 'concurrency_limit' | 'queue_limit' | 'timeout' | 'aborted';
|
|
37
|
+
declare class BulkheadRejectedError extends Error {
|
|
38
|
+
readonly reason: RejectReason;
|
|
39
|
+
readonly code: "BULKHEAD_REJECTED";
|
|
40
|
+
constructor(reason: RejectReason);
|
|
41
|
+
}
|
|
12
42
|
declare function createBulkhead(opts: BulkheadOptions): {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
maxConcurrent: number;
|
|
18
|
-
maxQueue: number;
|
|
19
|
-
};
|
|
43
|
+
tryAcquire: () => TryAcquireResult;
|
|
44
|
+
acquire: (ao?: AcquireOptions) => Promise<AcquireResult>;
|
|
45
|
+
run: <T>(fn: (signal?: AbortSignal) => Promise<T>, ao?: AcquireOptions) => Promise<T>;
|
|
46
|
+
stats: () => Stats;
|
|
20
47
|
};
|
|
21
48
|
|
|
22
|
-
export { type
|
|
49
|
+
export { type AcquireOptions, type AcquireResult, type BulkheadOptions, BulkheadRejectedError, type RejectReason, type Stats, type Token, type TryAcquireResult, createBulkhead };
|
package/dist/index.d.ts
CHANGED
|
@@ -2,21 +2,48 @@ type BulkheadOptions = {
|
|
|
2
2
|
maxConcurrent: number;
|
|
3
3
|
maxQueue?: number;
|
|
4
4
|
};
|
|
5
|
-
type
|
|
5
|
+
type AcquireOptions = {
|
|
6
|
+
signal?: AbortSignal;
|
|
7
|
+
timeoutMs?: number;
|
|
8
|
+
};
|
|
9
|
+
type Stats = {
|
|
10
|
+
inFlight: number;
|
|
11
|
+
pending: number;
|
|
12
|
+
maxConcurrent: number;
|
|
13
|
+
maxQueue: number;
|
|
14
|
+
aborted?: number;
|
|
15
|
+
timedOut?: number;
|
|
16
|
+
rejected?: number;
|
|
17
|
+
doubleRelease?: number;
|
|
18
|
+
};
|
|
19
|
+
type Token = {
|
|
20
|
+
release(): void;
|
|
21
|
+
};
|
|
22
|
+
type TryAcquireResult = {
|
|
23
|
+
ok: true;
|
|
24
|
+
token: Token;
|
|
25
|
+
} | {
|
|
26
|
+
ok: false;
|
|
27
|
+
reason: 'concurrency_limit' | 'queue_limit';
|
|
28
|
+
};
|
|
29
|
+
type AcquireResult = {
|
|
6
30
|
ok: true;
|
|
7
|
-
|
|
31
|
+
token: Token;
|
|
8
32
|
} | {
|
|
9
33
|
ok: false;
|
|
10
|
-
reason:
|
|
34
|
+
reason: 'concurrency_limit' | 'queue_limit' | 'timeout' | 'aborted';
|
|
11
35
|
};
|
|
36
|
+
type RejectReason = 'concurrency_limit' | 'queue_limit' | 'timeout' | 'aborted';
|
|
37
|
+
declare class BulkheadRejectedError extends Error {
|
|
38
|
+
readonly reason: RejectReason;
|
|
39
|
+
readonly code: "BULKHEAD_REJECTED";
|
|
40
|
+
constructor(reason: RejectReason);
|
|
41
|
+
}
|
|
12
42
|
declare function createBulkhead(opts: BulkheadOptions): {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
maxConcurrent: number;
|
|
18
|
-
maxQueue: number;
|
|
19
|
-
};
|
|
43
|
+
tryAcquire: () => TryAcquireResult;
|
|
44
|
+
acquire: (ao?: AcquireOptions) => Promise<AcquireResult>;
|
|
45
|
+
run: <T>(fn: (signal?: AbortSignal) => Promise<T>, ao?: AcquireOptions) => Promise<T>;
|
|
46
|
+
stats: () => Stats;
|
|
20
47
|
};
|
|
21
48
|
|
|
22
|
-
export { type
|
|
49
|
+
export { type AcquireOptions, type AcquireResult, type BulkheadOptions, BulkheadRejectedError, type RejectReason, type Stats, type Token, type TryAcquireResult, createBulkhead };
|
package/dist/index.js
CHANGED
|
@@ -1,53 +1,157 @@
|
|
|
1
1
|
// src/index.ts
|
|
2
|
+
var BulkheadRejectedError = class extends Error {
|
|
3
|
+
constructor(reason) {
|
|
4
|
+
super(`Bulkhead rejected: ${reason}`);
|
|
5
|
+
this.reason = reason;
|
|
6
|
+
this.name = "BulkheadRejectedError";
|
|
7
|
+
}
|
|
8
|
+
code = "BULKHEAD_REJECTED";
|
|
9
|
+
};
|
|
2
10
|
function createBulkhead(opts) {
|
|
3
11
|
if (!Number.isInteger(opts.maxConcurrent) || opts.maxConcurrent <= 0) {
|
|
4
12
|
throw new Error("maxConcurrent must be a positive integer");
|
|
5
13
|
}
|
|
6
14
|
const maxQueue = opts.maxQueue ?? 0;
|
|
15
|
+
if (!Number.isInteger(maxQueue) || maxQueue < 0) {
|
|
16
|
+
throw new Error("maxQueue must be an integer >= 0");
|
|
17
|
+
}
|
|
7
18
|
let inFlight = 0;
|
|
8
|
-
const
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
19
|
+
const q = [];
|
|
20
|
+
let qHead = 0;
|
|
21
|
+
let aborted = 0;
|
|
22
|
+
let timedOut = 0;
|
|
23
|
+
let rejected = 0;
|
|
24
|
+
let doubleRelease = 0;
|
|
25
|
+
const pendingCount = () => q.length - qHead;
|
|
26
|
+
const makeToken = () => {
|
|
27
|
+
let released = false;
|
|
28
|
+
return {
|
|
29
|
+
release() {
|
|
30
|
+
if (released) {
|
|
31
|
+
doubleRelease++;
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
released = true;
|
|
35
|
+
inFlight--;
|
|
36
|
+
if (inFlight < 0) inFlight = 0;
|
|
37
|
+
drain();
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
};
|
|
41
|
+
const cleanupWaiter = (w) => {
|
|
42
|
+
if (w.abortListener) w.abortListener();
|
|
43
|
+
if (w.timeoutId) clearTimeout(w.timeoutId);
|
|
44
|
+
w.abortListener = void 0;
|
|
45
|
+
w.timeoutId = void 0;
|
|
46
|
+
};
|
|
47
|
+
const drain = () => {
|
|
48
|
+
while (inFlight < opts.maxConcurrent && pendingCount() > 0) {
|
|
49
|
+
const w = q[qHead++];
|
|
50
|
+
if (w.cancelled) {
|
|
51
|
+
cleanupWaiter(w);
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
12
54
|
inFlight++;
|
|
13
|
-
|
|
55
|
+
cleanupWaiter(w);
|
|
56
|
+
w.resolve({ ok: true, token: makeToken() });
|
|
57
|
+
}
|
|
58
|
+
if (qHead > 1024 && qHead * 2 > q.length) {
|
|
59
|
+
q.splice(0, qHead);
|
|
60
|
+
qHead = 0;
|
|
14
61
|
}
|
|
15
62
|
};
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
63
|
+
const tryAcquire = () => {
|
|
64
|
+
if (inFlight < opts.maxConcurrent) {
|
|
65
|
+
inFlight++;
|
|
66
|
+
return { ok: true, token: makeToken() };
|
|
67
|
+
}
|
|
68
|
+
if (maxQueue > 0 && pendingCount() >= maxQueue) {
|
|
69
|
+
rejected++;
|
|
70
|
+
return { ok: false, reason: "queue_limit" };
|
|
71
|
+
}
|
|
72
|
+
rejected++;
|
|
73
|
+
return { ok: false, reason: maxQueue > 0 ? "queue_limit" : "concurrency_limit" };
|
|
19
74
|
};
|
|
20
|
-
const
|
|
75
|
+
const acquire = (ao = {}) => {
|
|
21
76
|
if (inFlight < opts.maxConcurrent) {
|
|
22
77
|
inFlight++;
|
|
23
|
-
return { ok: true,
|
|
78
|
+
return Promise.resolve({ ok: true, token: makeToken() });
|
|
79
|
+
}
|
|
80
|
+
if (maxQueue === 0) {
|
|
81
|
+
rejected++;
|
|
82
|
+
return Promise.resolve({ ok: false, reason: "concurrency_limit" });
|
|
83
|
+
}
|
|
84
|
+
if (pendingCount() >= maxQueue) {
|
|
85
|
+
rejected++;
|
|
86
|
+
return Promise.resolve({ ok: false, reason: "queue_limit" });
|
|
24
87
|
}
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
88
|
+
return new Promise((resolve) => {
|
|
89
|
+
const w = {
|
|
90
|
+
resolve,
|
|
91
|
+
cancelled: false,
|
|
92
|
+
abortListener: void 0,
|
|
93
|
+
timeoutId: void 0
|
|
29
94
|
};
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
95
|
+
if (ao.signal) {
|
|
96
|
+
if (ao.signal.aborted) {
|
|
97
|
+
aborted++;
|
|
98
|
+
resolve({ ok: false, reason: "aborted" });
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
const onAbort = () => {
|
|
102
|
+
if (!w.cancelled) {
|
|
103
|
+
w.cancelled = true;
|
|
104
|
+
aborted++;
|
|
105
|
+
cleanupWaiter(w);
|
|
106
|
+
resolve({ ok: false, reason: "aborted" });
|
|
39
107
|
}
|
|
108
|
+
};
|
|
109
|
+
ao.signal.addEventListener("abort", onAbort, { once: true });
|
|
110
|
+
w.abortListener = () => ao.signal.removeEventListener("abort", onAbort);
|
|
111
|
+
}
|
|
112
|
+
if (ao.timeoutMs != null) {
|
|
113
|
+
if (!Number.isFinite(ao.timeoutMs) || ao.timeoutMs < 0) {
|
|
114
|
+
timedOut++;
|
|
115
|
+
resolve({ ok: false, reason: "timeout" });
|
|
116
|
+
return;
|
|
40
117
|
}
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
118
|
+
w.timeoutId = setTimeout(() => {
|
|
119
|
+
if (!w.cancelled) {
|
|
120
|
+
w.cancelled = true;
|
|
121
|
+
timedOut++;
|
|
122
|
+
cleanupWaiter(w);
|
|
123
|
+
resolve({ ok: false, reason: "timeout" });
|
|
124
|
+
}
|
|
125
|
+
}, ao.timeoutMs);
|
|
126
|
+
}
|
|
127
|
+
q.push(w);
|
|
128
|
+
});
|
|
44
129
|
};
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
130
|
+
const run = async (fn, ao = {}) => {
|
|
131
|
+
const r = await acquire(ao);
|
|
132
|
+
if (!r.ok) {
|
|
133
|
+
throw new BulkheadRejectedError(r.reason);
|
|
134
|
+
}
|
|
135
|
+
try {
|
|
136
|
+
return await fn(ao.signal);
|
|
137
|
+
} finally {
|
|
138
|
+
r.token.release();
|
|
139
|
+
}
|
|
48
140
|
};
|
|
141
|
+
const stats = () => ({
|
|
142
|
+
inFlight,
|
|
143
|
+
pending: pendingCount(),
|
|
144
|
+
maxConcurrent: opts.maxConcurrent,
|
|
145
|
+
maxQueue,
|
|
146
|
+
aborted,
|
|
147
|
+
timedOut,
|
|
148
|
+
rejected,
|
|
149
|
+
doubleRelease
|
|
150
|
+
});
|
|
151
|
+
return { tryAcquire, acquire, run, stats };
|
|
49
152
|
}
|
|
50
153
|
export {
|
|
154
|
+
BulkheadRejectedError,
|
|
51
155
|
createBulkhead
|
|
52
156
|
};
|
|
53
157
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["export type BulkheadOptions = {\n maxConcurrent: number;\n maxQueue?: number; //
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["export 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' | 'queue_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\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 array with head index (cheap shift)\n const q: Waiter[] = [];\n let qHead = 0;\n\n // optional counters\n let aborted = 0;\n let timedOut = 0;\n let rejected = 0;\n let doubleRelease = 0;\n\n const pendingCount = () => q.length - qHead;\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 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 // ---- drain algorithm ----\n const drain = () => {\n while (inFlight < opts.maxConcurrent && pendingCount() > 0) {\n const w = q[qHead++]!;\n // skip cancelled waiters\n if (w.cancelled) {\n cleanupWaiter(w);\n continue;\n }\n\n // grant slot\n inFlight++;\n cleanupWaiter(w);\n w.resolve({ ok: true, token: makeToken() });\n }\n\n // occasional compaction to avoid unbounded growth\n if (qHead > 1024 && qHead * 2 > q.length) {\n q.splice(0, qHead);\n qHead = 0;\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 // tryAcquire never waits; queue_limit matters only if maxQueue configured\n if (maxQueue > 0 && pendingCount() >= maxQueue) {\n rejected++;\n return { ok: false, reason: 'queue_limit' };\n }\n rejected++;\n return { ok: false, reason: maxQueue > 0 ? 'queue_limit' : '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 abortListener: undefined,\n timeoutId: undefined,\n };\n\n // abort support\n if (ao.signal) {\n if (ao.signal.aborted) {\n aborted++;\n resolve({ ok: false, reason: 'aborted' });\n return;\n }\n const onAbort = () => {\n // mark cancelled; drain() will skip it\n if (!w.cancelled) {\n w.cancelled = true;\n aborted++;\n cleanupWaiter(w);\n resolve({ 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 resolve({ ok: false, reason: 'timeout' });\n return;\n }\n w.timeoutId = setTimeout(() => {\n if (!w.cancelled) {\n w.cancelled = true;\n timedOut++;\n cleanupWaiter(w);\n resolve({ ok: false, reason: 'timeout' });\n }\n }, ao.timeoutMs);\n }\n\n q.push(w);\n // NOTE: we do NOT reserve capacity here.\n // A slot is consumed only when drain() grants a token.\n\n // No need to call drain() here because we already know we’re at capacity,\n // but it doesn’t hurt if you want (for races with release).\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":[]}
|