async-bulkhead-ts 0.2.0 → 0.2.2

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
+
43
+ const r = await bulkhead.acquire();
42
44
 
43
- if (!admission.ok) {
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,45 @@ const bulkhead = createBulkhead({
83
88
  });
84
89
  ```
85
90
 
86
- When both concurrency and queue limits are exceeded, admission fails immediately.
91
+ > Note: bounded waiting is optional.
92
+ > Future major versions may focus on fail-fast admission only.
87
93
 
88
- Queue ordering is FIFO.
94
+
95
+ Semantics:
96
+ - If `inFlight` < `maxConcurrent`: `acquire()` succeeds immediately.
97
+ - Else if `maxQueue` > 0 and queue has space: `acquire()` waits FIFO.
98
+ - Else: rejected immediately.
89
99
 
90
100
  ## Cancellation
91
101
 
92
- Bulkhead operations can be bound to an `AbortSignal`:
102
+ Waiting can be cancelled with an `AbortSignal`:
93
103
 
94
104
  ```ts
95
105
  await bulkhead.run(
96
- async () => {
97
- await doWork();
98
- },
106
+ async () => doWork(),
99
107
  { signal }
100
108
  );
101
109
  ```
102
110
 
103
111
  Cancellation guarantees:
112
+ - Work that is waiting in the queue can be cancelled before it starts.
113
+ - In-flight work is not forcibly terminated (your function may observe the signal).
114
+ - Capacity is always released correctly for acquired tokens.
115
+ - Cancelled or timed-out waiters do not permanently consume queue capacity.
116
+ - Cancelled waiters will not block subsequent admissions.
117
+ - FIFO order is preserved for non-cancelled waiters.
118
+
119
+ You can also bound waiting time:
120
+
121
+ ```ts
122
+ await bulkhead.run(async () => doWork(), { timeoutMs: 50 });
123
+ ```
124
+
125
+ ## Behavioral Guarantees
104
126
 
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
127
+ - maxConcurrent is never exceeded.
128
+ - pending never exceeds maxQueue.
129
+ - Under cancellation or timeout churn, admission remains bounded and deterministic.
108
130
 
109
131
  ## API
110
132
 
@@ -113,7 +135,7 @@ Cancellation guarantees:
113
135
  ```ts
114
136
  type BulkheadOptions = {
115
137
  maxConcurrent: number;
116
- maxQueue?: number;
138
+ maxQueue?: number; // pending waiters allowed (0 => no waiting)
117
139
  };
118
140
  ```
119
141
 
@@ -121,38 +143,62 @@ Returns:
121
143
 
122
144
  ```ts
123
145
  {
124
- admit(): AdmitResult;
125
- run<T>(fn: () => Promise<T>, options?): Promise<T>;
146
+ tryAcquire(): TryAcquireResult;
147
+ acquire(options?): Promise<AcquireResult>;
148
+ run<T>(fn: (signal?: AbortSignal) => Promise<T>, options?): Promise<T>;
126
149
  stats(): {
127
150
  inFlight: number;
128
- queued: number;
151
+ pending: number;
129
152
  maxConcurrent: number;
130
153
  maxQueue: number;
154
+ aborted?: number;
155
+ timedOut?: number;
156
+ rejected?: number;
157
+ doubleRelease?: number;
131
158
  };
132
159
  }
133
160
  ```
134
161
 
135
- `admit()`
162
+ `tryAcquire()`
136
163
 
137
164
  ```ts
138
- type AdmitResult =
139
- | { ok: true; release: () => void }
140
- | { ok: false; error: BulkheadError };
165
+ export type TryAcquireResult =
166
+ | { ok: true; token: Token }
167
+ | { ok: false; reason: 'concurrency_limit' };
141
168
  ```
142
169
 
143
- Admission failures are returned as typed errors, not strings.
170
+ > `tryAcquire()` never waits and never enqueues; it either acquires immediately or fails fast.
144
171
 
145
- ## Metrics Hooks
172
+ `acquire(options?)`
146
173
 
147
- Optional lifecycle hooks allow integration with metrics systems:
174
+ ```ts
175
+ type AcquireOptions = {
176
+ signal?: AbortSignal;
177
+ timeoutMs?: number; // waiting timeout only
178
+ };
179
+
180
+ type AcquireResult =
181
+ | { ok: true; token: Token }
182
+ | { ok: false; reason: 'concurrency_limit' | 'queue_limit' | 'timeout' | 'aborted' };
183
+ ```
148
184
 
149
- * admission accepted
150
- * queued
151
- * execution started
152
- * released
153
- * rejected
185
+ `run(fn, options?)`
154
186
 
155
- Hooks are synchronous and side-effect–free by design.
187
+ Throws on rejection:
188
+
189
+ ```ts
190
+ class BulkheadRejectedError extends Error {
191
+ readonly code = 'BULKHEAD_REJECTED';
192
+ readonly reason: 'concurrency_limit' | 'queue_limit' | 'timeout' | 'aborted';
193
+ }
194
+ ```
195
+
196
+ The function passed to `run()` receives the same `AbortSignal` (if provided), allowing
197
+ in-flight work to observe cancellation:
198
+
199
+ ```ts
200
+ await bulkhead.run(async (signal) => doWork(signal), { signal });
201
+ ```
156
202
 
157
203
  ## Design Philosophy
158
204
 
@@ -171,6 +217,7 @@ If you need retries, buffering, scheduling, or persistence—compose those aroun
171
217
  * Node.js: 20+ (24 LTS recommended)
172
218
  * Module formats: ESM and CommonJS
173
219
 
220
+
174
221
  ## License
175
222
 
176
223
  MIT © 2026
package/dist/index.cjs CHANGED
@@ -24,6 +24,47 @@ __export(index_exports, {
24
24
  createBulkhead: () => createBulkhead
25
25
  });
26
26
  module.exports = __toCommonJS(index_exports);
27
+ var RingDeque = class {
28
+ buf;
29
+ head = 0;
30
+ tail = 0;
31
+ size = 0;
32
+ constructor(capacity) {
33
+ const cap = Math.max(4, capacity | 0);
34
+ this.buf = new Array(cap);
35
+ }
36
+ get length() {
37
+ return this.size;
38
+ }
39
+ pushBack(item) {
40
+ if (this.size === this.buf.length) {
41
+ this.grow();
42
+ }
43
+ const idx = (this.head + this.size) % this.buf.length;
44
+ this.buf[idx] = item;
45
+ this.size++;
46
+ }
47
+ peekFront() {
48
+ if (this.size === 0) return void 0;
49
+ return this.buf[this.head];
50
+ }
51
+ popFront() {
52
+ if (this.size === 0) return void 0;
53
+ const item = this.buf[this.head];
54
+ this.buf[this.head] = void 0;
55
+ this.head = (this.head + 1) % this.buf.length;
56
+ this.size--;
57
+ return item;
58
+ }
59
+ grow() {
60
+ const newBuf = new Array(this.buf.length * 2);
61
+ for (let i = 0; i < this.size; i++) {
62
+ newBuf[i] = this.buf[(this.head + i) % this.buf.length];
63
+ }
64
+ this.buf = newBuf;
65
+ this.head = 0;
66
+ }
67
+ };
27
68
  var BulkheadRejectedError = class extends Error {
28
69
  constructor(reason) {
29
70
  super(`Bulkhead rejected: ${reason}`);
@@ -41,13 +82,11 @@ function createBulkhead(opts) {
41
82
  throw new Error("maxQueue must be an integer >= 0");
42
83
  }
43
84
  let inFlight = 0;
44
- const q = [];
45
- let qHead = 0;
85
+ const q = new RingDeque(maxQueue + 1);
46
86
  let aborted = 0;
47
87
  let timedOut = 0;
48
88
  let rejected = 0;
49
89
  let doubleRelease = 0;
50
- const pendingCount = () => q.length - qHead;
51
90
  const makeToken = () => {
52
91
  let released = false;
53
92
  return {
@@ -63,26 +102,46 @@ function createBulkhead(opts) {
63
102
  }
64
103
  };
65
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
+ };
66
121
  const cleanupWaiter = (w) => {
67
122
  if (w.abortListener) w.abortListener();
68
123
  if (w.timeoutId) clearTimeout(w.timeoutId);
69
124
  w.abortListener = void 0;
70
125
  w.timeoutId = void 0;
71
126
  };
127
+ const settle = (w, r) => {
128
+ if (w.settled) return;
129
+ w.settled = true;
130
+ if (!w.cancelled && !r.ok) w.cancelled = true;
131
+ cleanupWaiter(w);
132
+ w.resolve(r);
133
+ };
72
134
  const drain = () => {
73
- while (inFlight < opts.maxConcurrent && pendingCount() > 0) {
74
- const w = q[qHead++];
135
+ pruneCancelledFront();
136
+ while (inFlight < opts.maxConcurrent && q.length > 0) {
137
+ const w = q.popFront();
75
138
  if (w.cancelled) {
76
139
  cleanupWaiter(w);
140
+ pruneCancelledFront();
77
141
  continue;
78
142
  }
79
143
  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;
144
+ settle(w, { ok: true, token: makeToken() });
86
145
  }
87
146
  };
88
147
  const tryAcquire = () => {
@@ -90,12 +149,7 @@ function createBulkhead(opts) {
90
149
  inFlight++;
91
150
  return { ok: true, token: makeToken() };
92
151
  }
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" };
152
+ return { ok: false, reason: "concurrency_limit" };
99
153
  };
100
154
  const acquire = (ao = {}) => {
101
155
  if (inFlight < opts.maxConcurrent) {
@@ -114,22 +168,20 @@ function createBulkhead(opts) {
114
168
  const w = {
115
169
  resolve,
116
170
  cancelled: false,
171
+ settled: false,
117
172
  abortListener: void 0,
118
173
  timeoutId: void 0
119
174
  };
120
175
  if (ao.signal) {
121
176
  if (ao.signal.aborted) {
122
177
  aborted++;
123
- resolve({ ok: false, reason: "aborted" });
178
+ settle(w, { ok: false, reason: "aborted" });
124
179
  return;
125
180
  }
126
181
  const onAbort = () => {
127
- if (!w.cancelled) {
128
- w.cancelled = true;
129
- aborted++;
130
- cleanupWaiter(w);
131
- resolve({ ok: false, reason: "aborted" });
132
- }
182
+ aborted++;
183
+ w.cancelled = true;
184
+ settle(w, { ok: false, reason: "aborted" });
133
185
  };
134
186
  ao.signal.addEventListener("abort", onAbort, { once: true });
135
187
  w.abortListener = () => ao.signal.removeEventListener("abort", onAbort);
@@ -137,19 +189,17 @@ function createBulkhead(opts) {
137
189
  if (ao.timeoutMs != null) {
138
190
  if (!Number.isFinite(ao.timeoutMs) || ao.timeoutMs < 0) {
139
191
  timedOut++;
140
- resolve({ ok: false, reason: "timeout" });
192
+ settle(w, { ok: false, reason: "timeout" });
141
193
  return;
142
194
  }
143
195
  w.timeoutId = setTimeout(() => {
144
- if (!w.cancelled) {
145
- w.cancelled = true;
146
- timedOut++;
147
- cleanupWaiter(w);
148
- resolve({ ok: false, reason: "timeout" });
149
- }
196
+ timedOut++;
197
+ w.cancelled = true;
198
+ settle(w, { ok: false, reason: "timeout" });
150
199
  }, ao.timeoutMs);
151
200
  }
152
- q.push(w);
201
+ q.pushBack(w);
202
+ drain();
153
203
  });
154
204
  };
155
205
  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":["/**\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":[]}
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
@@ -1,4 +1,45 @@
1
1
  // src/index.ts
2
+ var RingDeque = class {
3
+ buf;
4
+ head = 0;
5
+ tail = 0;
6
+ size = 0;
7
+ constructor(capacity) {
8
+ const cap = Math.max(4, capacity | 0);
9
+ this.buf = new Array(cap);
10
+ }
11
+ get length() {
12
+ return this.size;
13
+ }
14
+ pushBack(item) {
15
+ if (this.size === this.buf.length) {
16
+ this.grow();
17
+ }
18
+ const idx = (this.head + this.size) % this.buf.length;
19
+ this.buf[idx] = item;
20
+ this.size++;
21
+ }
22
+ peekFront() {
23
+ if (this.size === 0) return void 0;
24
+ return this.buf[this.head];
25
+ }
26
+ popFront() {
27
+ if (this.size === 0) return void 0;
28
+ const item = this.buf[this.head];
29
+ this.buf[this.head] = void 0;
30
+ this.head = (this.head + 1) % this.buf.length;
31
+ this.size--;
32
+ return item;
33
+ }
34
+ grow() {
35
+ const newBuf = new Array(this.buf.length * 2);
36
+ for (let i = 0; i < this.size; i++) {
37
+ newBuf[i] = this.buf[(this.head + i) % this.buf.length];
38
+ }
39
+ this.buf = newBuf;
40
+ this.head = 0;
41
+ }
42
+ };
2
43
  var BulkheadRejectedError = class extends Error {
3
44
  constructor(reason) {
4
45
  super(`Bulkhead rejected: ${reason}`);
@@ -16,13 +57,11 @@ function createBulkhead(opts) {
16
57
  throw new Error("maxQueue must be an integer >= 0");
17
58
  }
18
59
  let inFlight = 0;
19
- const q = [];
20
- let qHead = 0;
60
+ const q = new RingDeque(maxQueue + 1);
21
61
  let aborted = 0;
22
62
  let timedOut = 0;
23
63
  let rejected = 0;
24
64
  let doubleRelease = 0;
25
- const pendingCount = () => q.length - qHead;
26
65
  const makeToken = () => {
27
66
  let released = false;
28
67
  return {
@@ -38,26 +77,46 @@ function createBulkhead(opts) {
38
77
  }
39
78
  };
40
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
+ };
41
96
  const cleanupWaiter = (w) => {
42
97
  if (w.abortListener) w.abortListener();
43
98
  if (w.timeoutId) clearTimeout(w.timeoutId);
44
99
  w.abortListener = void 0;
45
100
  w.timeoutId = void 0;
46
101
  };
102
+ const settle = (w, r) => {
103
+ if (w.settled) return;
104
+ w.settled = true;
105
+ if (!w.cancelled && !r.ok) w.cancelled = true;
106
+ cleanupWaiter(w);
107
+ w.resolve(r);
108
+ };
47
109
  const drain = () => {
48
- while (inFlight < opts.maxConcurrent && pendingCount() > 0) {
49
- const w = q[qHead++];
110
+ pruneCancelledFront();
111
+ while (inFlight < opts.maxConcurrent && q.length > 0) {
112
+ const w = q.popFront();
50
113
  if (w.cancelled) {
51
114
  cleanupWaiter(w);
115
+ pruneCancelledFront();
52
116
  continue;
53
117
  }
54
118
  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;
119
+ settle(w, { ok: true, token: makeToken() });
61
120
  }
62
121
  };
63
122
  const tryAcquire = () => {
@@ -65,12 +124,7 @@ function createBulkhead(opts) {
65
124
  inFlight++;
66
125
  return { ok: true, token: makeToken() };
67
126
  }
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" };
127
+ return { ok: false, reason: "concurrency_limit" };
74
128
  };
75
129
  const acquire = (ao = {}) => {
76
130
  if (inFlight < opts.maxConcurrent) {
@@ -89,22 +143,20 @@ function createBulkhead(opts) {
89
143
  const w = {
90
144
  resolve,
91
145
  cancelled: false,
146
+ settled: false,
92
147
  abortListener: void 0,
93
148
  timeoutId: void 0
94
149
  };
95
150
  if (ao.signal) {
96
151
  if (ao.signal.aborted) {
97
152
  aborted++;
98
- resolve({ ok: false, reason: "aborted" });
153
+ settle(w, { ok: false, reason: "aborted" });
99
154
  return;
100
155
  }
101
156
  const onAbort = () => {
102
- if (!w.cancelled) {
103
- w.cancelled = true;
104
- aborted++;
105
- cleanupWaiter(w);
106
- resolve({ ok: false, reason: "aborted" });
107
- }
157
+ aborted++;
158
+ w.cancelled = true;
159
+ settle(w, { ok: false, reason: "aborted" });
108
160
  };
109
161
  ao.signal.addEventListener("abort", onAbort, { once: true });
110
162
  w.abortListener = () => ao.signal.removeEventListener("abort", onAbort);
@@ -112,19 +164,17 @@ function createBulkhead(opts) {
112
164
  if (ao.timeoutMs != null) {
113
165
  if (!Number.isFinite(ao.timeoutMs) || ao.timeoutMs < 0) {
114
166
  timedOut++;
115
- resolve({ ok: false, reason: "timeout" });
167
+ settle(w, { ok: false, reason: "timeout" });
116
168
  return;
117
169
  }
118
170
  w.timeoutId = setTimeout(() => {
119
- if (!w.cancelled) {
120
- w.cancelled = true;
121
- timedOut++;
122
- cleanupWaiter(w);
123
- resolve({ ok: false, reason: "timeout" });
124
- }
171
+ timedOut++;
172
+ w.cancelled = true;
173
+ settle(w, { ok: false, reason: "timeout" });
125
174
  }, ao.timeoutMs);
126
175
  }
127
- q.push(w);
176
+ q.pushBack(w);
177
+ drain();
128
178
  });
129
179
  };
130
180
  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":["/**\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":[]}
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.2",
4
4
  "description": "Fail-fast admission control + bulkheads for async workloads",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -10,19 +10,17 @@
10
10
  ],
11
11
  "main": "./dist/index.cjs",
12
12
  "module": "./dist/index.js",
13
- "types": "./dist/index.d.ts",
14
13
  "exports": {
15
14
  ".": {
16
15
  "import": {
17
- "types": "./dist/index.d.ts",
18
16
  "default": "./dist/index.js"
19
17
  },
20
18
  "require": {
21
- "types": "./dist/index.d.cts",
22
19
  "default": "./dist/index.cjs"
23
20
  }
24
21
  }
25
22
  },
23
+ "types": "./dist/index.d.ts",
26
24
  "engines": {
27
25
  "node": ">=20"
28
26
  },