async-bulkhead-ts 0.2.0 → 0.2.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 CHANGED
@@ -1,19 +1,19 @@
1
1
  # async-bulkhead-ts
2
2
 
3
- Fail-fast **admission control** and **bulkheads** for async workloads in Node.js.
3
+ Fail-fast **admission control** (async bulkheads) for Node.js / TypeScript.
4
4
 
5
- Designed for services that prefer **rejecting work early** over queueing and degrading under load.
5
+ Designed for services that prefer **rejecting work early** over silently degrading via growing queues and timeouts.
6
6
 
7
7
  ---
8
8
 
9
9
  ## Features
10
10
 
11
- - ✅ **Fail-fast by default** (no hidden queues)
12
- - ✅ Simple **bulkhead / concurrency limits**
13
- - ✅ Explicit admission + release lifecycle
14
- - ✅ `bulkhead.run(fn)` helper for safe execution
15
- - ✅ Optional cancellation via `AbortSignal`
16
- - ✅ Lightweight lifecycle hooks for metrics
11
+ - ✅ Hard **max in-flight** concurrency (`maxConcurrent`)
12
+ - ✅ Optional **bounded FIFO waiting** (`maxQueue`)
13
+ - ✅ **Fail-fast by default** (`maxQueue: 0`)
14
+ - ✅ Explicit acquire + release via a **token**
15
+ - ✅ `bulkhead.run(fn)` helper (acquire + `finally` release)
16
+ - ✅ Optional waiting **timeout** and **AbortSignal** cancellation
17
17
  - ✅ Zero dependencies
18
18
  - ✅ ESM + CJS support
19
19
  - ✅ Node.js **20+**
@@ -22,7 +22,6 @@ Non-goals (by design):
22
22
  - ❌ No background workers
23
23
  - ❌ No retry logic
24
24
  - ❌ No distributed coordination
25
- - ❌ No built-in metrics backend (hooks only)
26
25
 
27
26
  ---
28
27
 
@@ -32,49 +31,55 @@ Non-goals (by design):
32
31
  npm install async-bulkhead-ts
33
32
  ```
34
33
 
35
- ## Basic Usage (Manual)
34
+ ## Basic Usage (Manual acquire/release)
36
35
 
37
36
  ```ts
38
37
  import { createBulkhead } from 'async-bulkhead-ts';
39
38
 
40
- const bulkhead = createBulkhead({ maxConcurrent: 10 });
41
- const admission = bulkhead.admit();
39
+ const bulkhead = createBulkhead({
40
+ maxConcurrent: 10,
41
+ });
42
42
 
43
- if (!admission.ok) {
43
+ const r = await bulkhead.acquire();
44
+
45
+ if (!r.ok) {
44
46
  // Fail fast — shed load, return 503, etc.
45
- throw admission.error;
47
+ // r.reason is one of:
48
+ // 'concurrency_limit' | 'queue_limit' | 'timeout' | 'aborted'
49
+ throw new Error(`Rejected: ${r.reason}`);
46
50
  }
47
51
 
48
52
  try {
49
53
  await doWork();
50
54
  } finally {
51
- admission.release();
55
+ r.token.release();
52
56
  }
53
57
  ```
54
58
 
55
- You must call `release()` exactly once if admission succeeds.
56
- Failing to release permanently reduces capacity.
59
+ You must call `token.release()` exactly once if acquisition succeeds.
60
+ Failing to release permanently reduces available capacity.
57
61
 
58
62
  ## Convenience Helper
59
63
 
60
64
  For most use cases, prefer `bulkhead.run(fn)`:
61
65
 
62
66
  ```ts
63
- await bulkhead.run(async () => {
64
- await doWork();
65
- });
67
+ await bulkhead.run(async () => doWork());
66
68
  ```
67
69
 
68
70
  Behavior:
69
71
 
70
- * Admission + release handled automatically
71
- * Still fail-fast
72
- * Admission failures throw typed errors
73
- * Supports cancellation via AbortSignal
72
+ - Acquire + release handled automatically (finally release)
73
+ - Still fail-fast (unless you configure `maxQueue`)
74
+ - Rejections throw a typed `BulkheadRejectedError` when using `run()`
75
+ - The provided `AbortSignal` is passed through to the function
76
+ - Supports waiting cancellation via `AbortSignal` and `timeoutMs`
77
+
78
+ > The signal passed to `run()` only affects admission and observation; in-flight work is not forcibly cancelled.
74
79
 
75
80
  ## With a Queue (Optional)
76
81
 
77
- Queues are opt-in and bounded.
82
+ Waiting is opt-in and bounded (FIFO).
78
83
 
79
84
  ```ts
80
85
  const bulkhead = createBulkhead({
@@ -83,28 +88,32 @@ const bulkhead = createBulkhead({
83
88
  });
84
89
  ```
85
90
 
86
- When both concurrency and queue limits are exceeded, admission fails immediately.
87
-
88
- Queue ordering is FIFO.
91
+ Semantics:
92
+ - If `inFlight` < `maxConcurrent`: `acquire()` succeeds immediately.
93
+ - Else if `maxQueue` > 0 and queue has space: `acquire()` waits FIFO.
94
+ - Else: rejected immediately.
89
95
 
90
96
  ## Cancellation
91
97
 
92
- Bulkhead operations can be bound to an `AbortSignal`:
98
+ Waiting can be cancelled with an `AbortSignal`:
93
99
 
94
100
  ```ts
95
101
  await bulkhead.run(
96
- async () => {
97
- await doWork();
98
- },
102
+ async () => doWork(),
99
103
  { signal }
100
104
  );
101
105
  ```
102
106
 
103
107
  Cancellation guarantees:
108
+ - Work that is waiting in the queue can be cancelled before it starts.
109
+ - In-flight work is not forcibly terminated (your function may observe the signal).
110
+ - Capacity is always released correctly for acquired tokens.
111
+
112
+ You can also bound waiting time:
104
113
 
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
114
+ ```ts
115
+ await bulkhead.run(async () => doWork(), { timeoutMs: 50 });
116
+ ```
108
117
 
109
118
  ## API
110
119
 
@@ -113,7 +122,7 @@ Cancellation guarantees:
113
122
  ```ts
114
123
  type BulkheadOptions = {
115
124
  maxConcurrent: number;
116
- maxQueue?: number;
125
+ maxQueue?: number; // pending waiters allowed (0 => no waiting)
117
126
  };
118
127
  ```
119
128
 
@@ -121,38 +130,62 @@ Returns:
121
130
 
122
131
  ```ts
123
132
  {
124
- admit(): AdmitResult;
125
- run<T>(fn: () => Promise<T>, options?): Promise<T>;
133
+ tryAcquire(): TryAcquireResult;
134
+ acquire(options?): Promise<AcquireResult>;
135
+ run<T>(fn: (signal?: AbortSignal) => Promise<T>, options?): Promise<T>;
126
136
  stats(): {
127
137
  inFlight: number;
128
- queued: number;
138
+ pending: number;
129
139
  maxConcurrent: number;
130
140
  maxQueue: number;
141
+ aborted?: number;
142
+ timedOut?: number;
143
+ rejected?: number;
144
+ doubleRelease?: number;
131
145
  };
132
146
  }
133
147
  ```
134
148
 
135
- `admit()`
149
+ `tryAcquire()`
150
+
151
+ ```ts
152
+ export type TryAcquireResult =
153
+ | { ok: true; token: Token }
154
+ | { ok: false; reason: 'concurrency_limit' };
155
+ ```
156
+
157
+ > `tryAcquire()` never waits and never enqueues; it either acquires immediately or fails fast.
158
+
159
+ `acquire(options?)`
136
160
 
137
161
  ```ts
138
- type AdmitResult =
139
- | { ok: true; release: () => void }
140
- | { ok: false; error: BulkheadError };
162
+ type AcquireOptions = {
163
+ signal?: AbortSignal;
164
+ timeoutMs?: number; // waiting timeout only
165
+ };
166
+
167
+ type AcquireResult =
168
+ | { ok: true; token: Token }
169
+ | { ok: false; reason: 'concurrency_limit' | 'queue_limit' | 'timeout' | 'aborted' };
141
170
  ```
142
171
 
143
- Admission failures are returned as typed errors, not strings.
172
+ `run(fn, options?)`
144
173
 
145
- ## Metrics Hooks
174
+ Throws on rejection:
146
175
 
147
- Optional lifecycle hooks allow integration with metrics systems:
176
+ ```ts
177
+ class BulkheadRejectedError extends Error {
178
+ readonly code = 'BULKHEAD_REJECTED';
179
+ readonly reason: 'concurrency_limit' | 'queue_limit' | 'timeout' | 'aborted';
180
+ }
181
+ ```
148
182
 
149
- * admission accepted
150
- * queued
151
- * execution started
152
- * released
153
- * rejected
183
+ The function passed to `run()` receives the same `AbortSignal` (if provided), allowing
184
+ in-flight work to observe cancellation:
154
185
 
155
- Hooks are synchronous and side-effect–free by design.
186
+ ```ts
187
+ await bulkhead.run(async (signal) => doWork(signal), { signal });
188
+ ```
156
189
 
157
190
  ## Design Philosophy
158
191
 
@@ -171,6 +204,7 @@ If you need retries, buffering, scheduling, or persistence—compose those aroun
171
204
  * Node.js: 20+ (24 LTS recommended)
172
205
  * Module formats: ESM and CommonJS
173
206
 
207
+
174
208
  ## License
175
209
 
176
210
  MIT © 2026
package/dist/index.cjs CHANGED
@@ -47,7 +47,25 @@ function createBulkhead(opts) {
47
47
  let timedOut = 0;
48
48
  let rejected = 0;
49
49
  let doubleRelease = 0;
50
- const pendingCount = () => q.length - qHead;
50
+ const pruneHead = () => {
51
+ while (qHead < q.length) {
52
+ const w = q[qHead];
53
+ if (w.cancelled || w.settled) {
54
+ cleanupWaiter(w);
55
+ qHead++;
56
+ continue;
57
+ }
58
+ break;
59
+ }
60
+ if (qHead > 1024 && qHead * 2 > q.length) {
61
+ q.splice(0, qHead);
62
+ qHead = 0;
63
+ }
64
+ };
65
+ const pendingCount = () => {
66
+ pruneHead();
67
+ return q.length - qHead;
68
+ };
51
69
  const makeToken = () => {
52
70
  let released = false;
53
71
  return {
@@ -69,20 +87,23 @@ function createBulkhead(opts) {
69
87
  w.abortListener = void 0;
70
88
  w.timeoutId = void 0;
71
89
  };
90
+ const settle = (w, r) => {
91
+ if (w.settled) return;
92
+ w.settled = true;
93
+ if (!w.cancelled && !r.ok) w.cancelled = true;
94
+ cleanupWaiter(w);
95
+ w.resolve(r);
96
+ };
72
97
  const drain = () => {
98
+ pruneHead();
73
99
  while (inFlight < opts.maxConcurrent && pendingCount() > 0) {
74
100
  const w = q[qHead++];
75
- if (w.cancelled) {
101
+ if (w.cancelled || w.settled) {
76
102
  cleanupWaiter(w);
77
103
  continue;
78
104
  }
79
105
  inFlight++;
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;
106
+ settle(w, { ok: true, token: makeToken() });
86
107
  }
87
108
  };
88
109
  const tryAcquire = () => {
@@ -90,12 +111,7 @@ function createBulkhead(opts) {
90
111
  inFlight++;
91
112
  return { ok: true, token: makeToken() };
92
113
  }
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" };
114
+ return { ok: false, reason: "concurrency_limit" };
99
115
  };
100
116
  const acquire = (ao = {}) => {
101
117
  if (inFlight < opts.maxConcurrent) {
@@ -114,22 +130,20 @@ function createBulkhead(opts) {
114
130
  const w = {
115
131
  resolve,
116
132
  cancelled: false,
133
+ settled: false,
117
134
  abortListener: void 0,
118
135
  timeoutId: void 0
119
136
  };
120
137
  if (ao.signal) {
121
138
  if (ao.signal.aborted) {
122
139
  aborted++;
123
- resolve({ ok: false, reason: "aborted" });
140
+ settle(w, { ok: false, reason: "aborted" });
124
141
  return;
125
142
  }
126
143
  const onAbort = () => {
127
- if (!w.cancelled) {
128
- w.cancelled = true;
129
- aborted++;
130
- cleanupWaiter(w);
131
- resolve({ ok: false, reason: "aborted" });
132
- }
144
+ aborted++;
145
+ w.cancelled = true;
146
+ settle(w, { ok: false, reason: "aborted" });
133
147
  };
134
148
  ao.signal.addEventListener("abort", onAbort, { once: true });
135
149
  w.abortListener = () => ao.signal.removeEventListener("abort", onAbort);
@@ -137,19 +151,17 @@ function createBulkhead(opts) {
137
151
  if (ao.timeoutMs != null) {
138
152
  if (!Number.isFinite(ao.timeoutMs) || ao.timeoutMs < 0) {
139
153
  timedOut++;
140
- resolve({ ok: false, reason: "timeout" });
154
+ settle(w, { ok: false, reason: "timeout" });
141
155
  return;
142
156
  }
143
157
  w.timeoutId = setTimeout(() => {
144
- if (!w.cancelled) {
145
- w.cancelled = true;
146
- timedOut++;
147
- cleanupWaiter(w);
148
- resolve({ ok: false, reason: "timeout" });
149
- }
158
+ timedOut++;
159
+ w.cancelled = true;
160
+ settle(w, { ok: false, reason: "timeout" });
150
161
  }, ao.timeoutMs);
151
162
  }
152
163
  q.push(w);
164
+ drain();
153
165
  });
154
166
  };
155
167
  const run = async (fn, ao = {}) => {
@@ -1 +1 @@
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":[]}
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' };\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 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 pruneHead = () => {\n // Advance past already-cancelled/settled waiters so they don't count against maxQueue.\n while (qHead < q.length) {\n const w = q[qHead]!;\n if (w.cancelled || w.settled) {\n cleanupWaiter(w);\n qHead++;\n continue;\n }\n break;\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 const pendingCount = () => {\n pruneHead();\n return q.length - qHead;\n };\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 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 // Important: prune even when there's no capacity, so cancelled/settled\n // waiters stop consuming maxQueue immediately.\n pruneHead();\n\n while (inFlight < opts.maxConcurrent && pendingCount() > 0) {\n const w = q[qHead++]!;\n // skip cancelled waiters\n if (w.cancelled || w.settled) {\n cleanupWaiter(w);\n continue;\n }\n\n // grant slot\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.push(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":[]}
package/dist/index.d.cts CHANGED
@@ -24,7 +24,7 @@ type TryAcquireResult = {
24
24
  token: Token;
25
25
  } | {
26
26
  ok: false;
27
- reason: 'concurrency_limit' | 'queue_limit';
27
+ reason: 'concurrency_limit';
28
28
  };
29
29
  type AcquireResult = {
30
30
  ok: true;
package/dist/index.d.ts CHANGED
@@ -24,7 +24,7 @@ type TryAcquireResult = {
24
24
  token: Token;
25
25
  } | {
26
26
  ok: false;
27
- reason: 'concurrency_limit' | 'queue_limit';
27
+ reason: 'concurrency_limit';
28
28
  };
29
29
  type AcquireResult = {
30
30
  ok: true;
package/dist/index.js CHANGED
@@ -22,7 +22,25 @@ function createBulkhead(opts) {
22
22
  let timedOut = 0;
23
23
  let rejected = 0;
24
24
  let doubleRelease = 0;
25
- const pendingCount = () => q.length - qHead;
25
+ const pruneHead = () => {
26
+ while (qHead < q.length) {
27
+ const w = q[qHead];
28
+ if (w.cancelled || w.settled) {
29
+ cleanupWaiter(w);
30
+ qHead++;
31
+ continue;
32
+ }
33
+ break;
34
+ }
35
+ if (qHead > 1024 && qHead * 2 > q.length) {
36
+ q.splice(0, qHead);
37
+ qHead = 0;
38
+ }
39
+ };
40
+ const pendingCount = () => {
41
+ pruneHead();
42
+ return q.length - qHead;
43
+ };
26
44
  const makeToken = () => {
27
45
  let released = false;
28
46
  return {
@@ -44,20 +62,23 @@ function createBulkhead(opts) {
44
62
  w.abortListener = void 0;
45
63
  w.timeoutId = void 0;
46
64
  };
65
+ const settle = (w, r) => {
66
+ if (w.settled) return;
67
+ w.settled = true;
68
+ if (!w.cancelled && !r.ok) w.cancelled = true;
69
+ cleanupWaiter(w);
70
+ w.resolve(r);
71
+ };
47
72
  const drain = () => {
73
+ pruneHead();
48
74
  while (inFlight < opts.maxConcurrent && pendingCount() > 0) {
49
75
  const w = q[qHead++];
50
- if (w.cancelled) {
76
+ if (w.cancelled || w.settled) {
51
77
  cleanupWaiter(w);
52
78
  continue;
53
79
  }
54
80
  inFlight++;
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;
81
+ settle(w, { ok: true, token: makeToken() });
61
82
  }
62
83
  };
63
84
  const tryAcquire = () => {
@@ -65,12 +86,7 @@ function createBulkhead(opts) {
65
86
  inFlight++;
66
87
  return { ok: true, token: makeToken() };
67
88
  }
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" };
89
+ return { ok: false, reason: "concurrency_limit" };
74
90
  };
75
91
  const acquire = (ao = {}) => {
76
92
  if (inFlight < opts.maxConcurrent) {
@@ -89,22 +105,20 @@ function createBulkhead(opts) {
89
105
  const w = {
90
106
  resolve,
91
107
  cancelled: false,
108
+ settled: false,
92
109
  abortListener: void 0,
93
110
  timeoutId: void 0
94
111
  };
95
112
  if (ao.signal) {
96
113
  if (ao.signal.aborted) {
97
114
  aborted++;
98
- resolve({ ok: false, reason: "aborted" });
115
+ settle(w, { ok: false, reason: "aborted" });
99
116
  return;
100
117
  }
101
118
  const onAbort = () => {
102
- if (!w.cancelled) {
103
- w.cancelled = true;
104
- aborted++;
105
- cleanupWaiter(w);
106
- resolve({ ok: false, reason: "aborted" });
107
- }
119
+ aborted++;
120
+ w.cancelled = true;
121
+ settle(w, { ok: false, reason: "aborted" });
108
122
  };
109
123
  ao.signal.addEventListener("abort", onAbort, { once: true });
110
124
  w.abortListener = () => ao.signal.removeEventListener("abort", onAbort);
@@ -112,19 +126,17 @@ function createBulkhead(opts) {
112
126
  if (ao.timeoutMs != null) {
113
127
  if (!Number.isFinite(ao.timeoutMs) || ao.timeoutMs < 0) {
114
128
  timedOut++;
115
- resolve({ ok: false, reason: "timeout" });
129
+ settle(w, { ok: false, reason: "timeout" });
116
130
  return;
117
131
  }
118
132
  w.timeoutId = setTimeout(() => {
119
- if (!w.cancelled) {
120
- w.cancelled = true;
121
- timedOut++;
122
- cleanupWaiter(w);
123
- resolve({ ok: false, reason: "timeout" });
124
- }
133
+ timedOut++;
134
+ w.cancelled = true;
135
+ settle(w, { ok: false, reason: "timeout" });
125
136
  }, ao.timeoutMs);
126
137
  }
127
138
  q.push(w);
139
+ drain();
128
140
  });
129
141
  };
130
142
  const run = async (fn, ao = {}) => {
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; // 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":[]}
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' };\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 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 pruneHead = () => {\n // Advance past already-cancelled/settled waiters so they don't count against maxQueue.\n while (qHead < q.length) {\n const w = q[qHead]!;\n if (w.cancelled || w.settled) {\n cleanupWaiter(w);\n qHead++;\n continue;\n }\n break;\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 const pendingCount = () => {\n pruneHead();\n return q.length - qHead;\n };\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 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 // Important: prune even when there's no capacity, so cancelled/settled\n // waiters stop consuming maxQueue immediately.\n pruneHead();\n\n while (inFlight < opts.maxConcurrent && pendingCount() > 0) {\n const w = q[qHead++]!;\n // skip cancelled waiters\n if (w.cancelled || w.settled) {\n cleanupWaiter(w);\n continue;\n }\n\n // grant slot\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.push(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":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "async-bulkhead-ts",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "Fail-fast admission control + bulkheads for async workloads",
5
5
  "license": "MIT",
6
6
  "type": "module",