async-bulkhead-ts 0.2.2 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -14,11 +14,13 @@ Designed for services that prefer **rejecting work early** over silently degradi
14
14
  - ✅ Explicit acquire + release via a **token**
15
15
  - ✅ `bulkhead.run(fn)` helper (acquire + `finally` release)
16
16
  - ✅ Optional waiting **timeout** and **AbortSignal** cancellation
17
+ - ✅ Graceful shutdown via **`close()`** + **`drain()`**
17
18
  - ✅ Zero dependencies
18
19
  - ✅ ESM + CJS support
19
20
  - ✅ Node.js **20+**
20
21
 
21
22
  Non-goals (by design):
23
+
22
24
  - ❌ No background workers
23
25
  - ❌ No retry logic
24
26
  - ❌ No distributed coordination
@@ -45,7 +47,7 @@ const r = await bulkhead.acquire();
45
47
  if (!r.ok) {
46
48
  // Fail fast — shed load, return 503, etc.
47
49
  // r.reason is one of:
48
- // 'concurrency_limit' | 'queue_limit' | 'timeout' | 'aborted'
50
+ // 'concurrency_limit' | 'queue_limit' | 'timeout' | 'aborted' | 'shutdown'
49
51
  throw new Error(`Rejected: ${r.reason}`);
50
52
  }
51
53
 
@@ -91,8 +93,8 @@ const bulkhead = createBulkhead({
91
93
  > Note: bounded waiting is optional.
92
94
  > Future major versions may focus on fail-fast admission only.
93
95
 
94
-
95
96
  Semantics:
97
+
96
98
  - If `inFlight` < `maxConcurrent`: `acquire()` succeeds immediately.
97
99
  - Else if `maxQueue` > 0 and queue has space: `acquire()` waits FIFO.
98
100
  - Else: rejected immediately.
@@ -109,6 +111,7 @@ await bulkhead.run(
109
111
  ```
110
112
 
111
113
  Cancellation guarantees:
114
+
112
115
  - Work that is waiting in the queue can be cancelled before it starts.
113
116
  - In-flight work is not forcibly terminated (your function may observe the signal).
114
117
  - Capacity is always released correctly for acquired tokens.
@@ -122,6 +125,33 @@ You can also bound waiting time:
122
125
  await bulkhead.run(async () => doWork(), { timeoutMs: 50 });
123
126
  ```
124
127
 
128
+ ## Graceful Shutdown
129
+
130
+ `close()` stops admission. `drain()` waits for in-flight work to finish. Together they give you a clean shutdown sequence:
131
+
132
+ ```ts
133
+ // In your SIGTERM handler:
134
+ bulkhead.close();
135
+
136
+ // All pending waiters are rejected with 'shutdown'.
137
+ // All future acquire/run calls reject immediately with 'shutdown'.
138
+ // In-flight work is not interrupted — tokens release normally.
139
+
140
+ await bulkhead.drain();
141
+ // Resolves when inFlight reaches zero.
142
+ ```
143
+
144
+ `close()` is synchronous, idempotent, and irreversible. If you need a fresh bulkhead, create a new instance.
145
+
146
+ `drain()` is an observation primitive — it tells you when work finishes, but it cannot force work to complete. The bulkhead does not own in-flight work. If your functions support cancellation, signal them via the `AbortSignal` you already hold.
147
+
148
+ `drain()` also works without `close()`. On its own it resolves when current in-flight and pending work completes, but new work can still be admitted:
149
+
150
+ ```ts
151
+ // Wait for current work to finish, without stopping new admissions.
152
+ await bulkhead.drain();
153
+ ```
154
+
125
155
  ## Behavioral Guarantees
126
156
 
127
157
  - maxConcurrent is never exceeded.
@@ -146,16 +176,9 @@ Returns:
146
176
  tryAcquire(): TryAcquireResult;
147
177
  acquire(options?): Promise<AcquireResult>;
148
178
  run<T>(fn: (signal?: AbortSignal) => Promise<T>, options?): Promise<T>;
149
- stats(): {
150
- inFlight: number;
151
- pending: number;
152
- maxConcurrent: number;
153
- maxQueue: number;
154
- aborted?: number;
155
- timedOut?: number;
156
- rejected?: number;
157
- doubleRelease?: number;
158
- };
179
+ close(): void;
180
+ drain(): Promise<void>;
181
+ stats(): Stats;
159
182
  }
160
183
  ```
161
184
 
@@ -164,7 +187,7 @@ Returns:
164
187
  ```ts
165
188
  export type TryAcquireResult =
166
189
  | { ok: true; token: Token }
167
- | { ok: false; reason: 'concurrency_limit' };
190
+ | { ok: false; reason: 'concurrency_limit' | 'shutdown' };
168
191
  ```
169
192
 
170
193
  > `tryAcquire()` never waits and never enqueues; it either acquires immediately or fails fast.
@@ -179,7 +202,14 @@ type AcquireOptions = {
179
202
 
180
203
  type AcquireResult =
181
204
  | { ok: true; token: Token }
182
- | { ok: false; reason: 'concurrency_limit' | 'queue_limit' | 'timeout' | 'aborted' };
205
+ | { ok: false; reason: RejectReason };
206
+
207
+ type RejectReason =
208
+ | 'concurrency_limit'
209
+ | 'queue_limit'
210
+ | 'timeout'
211
+ | 'aborted'
212
+ | 'shutdown';
183
213
  ```
184
214
 
185
215
  `run(fn, options?)`
@@ -189,7 +219,7 @@ Throws on rejection:
189
219
  ```ts
190
220
  class BulkheadRejectedError extends Error {
191
221
  readonly code = 'BULKHEAD_REJECTED';
192
- readonly reason: 'concurrency_limit' | 'queue_limit' | 'timeout' | 'aborted';
222
+ readonly reason: RejectReason;
193
223
  }
194
224
  ```
195
225
 
@@ -200,24 +230,54 @@ in-flight work to observe cancellation:
200
230
  await bulkhead.run(async (signal) => doWork(signal), { signal });
201
231
  ```
202
232
 
233
+ `close()`
234
+
235
+ Stops admission permanently. Rejects all pending waiters with `'shutdown'`. All future `tryAcquire`/`acquire`/`run` calls reject immediately with `'shutdown'`. In-flight tokens remain valid. Idempotent.
236
+
237
+ `drain()`
238
+
239
+ Returns a `Promise<void>` that resolves when `inFlight` and pending both reach zero. Multiple concurrent calls all resolve at the same moment. Works with or without `close()`.
240
+
241
+ `stats()`
242
+
243
+ ```ts
244
+ type Stats = {
245
+ inFlight: number;
246
+ pending: number;
247
+ maxConcurrent: number;
248
+ maxQueue: number;
249
+ closed: boolean;
250
+ // debug counters:
251
+ aborted?: number;
252
+ timedOut?: number;
253
+ rejected?: number;
254
+ doubleRelease?: number;
255
+ inFlightUnderflow?: number;
256
+ };
257
+ ```
258
+
259
+ `stats()` is a pure read with no side effects.
260
+
261
+ `inFlightUnderflow` should always be 0. A nonzero value indicates a bug in the library.
262
+
203
263
  ## Design Philosophy
204
264
 
205
265
  This library is intentionally small.
206
266
 
207
267
  It exists to enforce backpressure at the boundary of your system:
208
268
 
209
- * before request fan-out
210
- * before hitting downstream dependencies
211
- * before saturation cascades
269
+ - before request fan-out
270
+ - before hitting downstream dependencies
271
+ - before saturation cascades
212
272
 
213
273
  If you need retries, buffering, scheduling, or persistence—compose those around this, not inside it.
214
274
 
215
275
  ## Compatibility
216
276
 
217
- * Node.js: 20+ (24 LTS recommended)
218
- * Module formats: ESM and CommonJS
277
+ - Node.js: 20+ (24 LTS recommended)
278
+ - Module formats: ESM and CommonJS
219
279
 
220
280
 
221
281
  ## License
222
282
 
223
- MIT © 2026
283
+ MIT © 2026
package/dist/index.cjs CHANGED
@@ -82,42 +82,15 @@ function createBulkhead(opts) {
82
82
  throw new Error("maxQueue must be an integer >= 0");
83
83
  }
84
84
  let inFlight = 0;
85
+ let closed = false;
86
+ let livePending = 0;
85
87
  const q = new RingDeque(maxQueue + 1);
88
+ let drainWaiters = [];
86
89
  let aborted = 0;
87
90
  let timedOut = 0;
88
91
  let rejected = 0;
89
92
  let doubleRelease = 0;
90
- const makeToken = () => {
91
- let released = false;
92
- return {
93
- release() {
94
- if (released) {
95
- doubleRelease++;
96
- return;
97
- }
98
- released = true;
99
- inFlight--;
100
- if (inFlight < 0) inFlight = 0;
101
- drain();
102
- }
103
- };
104
- };
105
- const pruneCancelledFront = () => {
106
- while (q.length > 0) {
107
- const w = q.peekFront();
108
- if (w.cancelled || w.settled) {
109
- cleanupWaiter(w);
110
- q.popFront();
111
- continue;
112
- } else {
113
- break;
114
- }
115
- }
116
- };
117
- const pendingCount = () => {
118
- pruneCancelledFront();
119
- return q.length;
120
- };
93
+ let inFlightUnderflow = 0;
121
94
  const cleanupWaiter = (w) => {
122
95
  if (w.abortListener) w.abortListener();
123
96
  if (w.timeoutId) clearTimeout(w.timeoutId);
@@ -129,14 +102,50 @@ function createBulkhead(opts) {
129
102
  w.settled = true;
130
103
  if (!w.cancelled && !r.ok) w.cancelled = true;
131
104
  cleanupWaiter(w);
105
+ livePending--;
132
106
  w.resolve(r);
133
107
  };
134
- const drain = () => {
108
+ const pruneCancelledFront = () => {
109
+ while (q.length > 0) {
110
+ const w = q.peekFront();
111
+ if (w.cancelled || w.settled) {
112
+ q.popFront();
113
+ continue;
114
+ }
115
+ break;
116
+ }
117
+ };
118
+ const notifyDrainWaiters = () => {
119
+ if (inFlight === 0 && livePending === 0 && drainWaiters.length > 0) {
120
+ const waiters = drainWaiters;
121
+ drainWaiters = [];
122
+ for (const resolve of waiters) resolve();
123
+ }
124
+ };
125
+ const makeToken = () => {
126
+ let released = false;
127
+ return {
128
+ release() {
129
+ if (released) {
130
+ doubleRelease++;
131
+ return;
132
+ }
133
+ released = true;
134
+ inFlight--;
135
+ if (inFlight < 0) {
136
+ inFlightUnderflow++;
137
+ inFlight = 0;
138
+ }
139
+ pump();
140
+ notifyDrainWaiters();
141
+ }
142
+ };
143
+ };
144
+ const pump = () => {
135
145
  pruneCancelledFront();
136
146
  while (inFlight < opts.maxConcurrent && q.length > 0) {
137
147
  const w = q.popFront();
138
- if (w.cancelled) {
139
- cleanupWaiter(w);
148
+ if (w.cancelled || w.settled) {
140
149
  pruneCancelledFront();
141
150
  continue;
142
151
  }
@@ -144,7 +153,27 @@ function createBulkhead(opts) {
144
153
  settle(w, { ok: true, token: makeToken() });
145
154
  }
146
155
  };
156
+ const close = () => {
157
+ if (closed) return;
158
+ closed = true;
159
+ while (q.length > 0) {
160
+ const w = q.popFront();
161
+ if (w.settled || w.cancelled) continue;
162
+ rejected++;
163
+ settle(w, { ok: false, reason: "shutdown" });
164
+ }
165
+ notifyDrainWaiters();
166
+ };
167
+ const drainFn = () => {
168
+ if (inFlight === 0 && livePending === 0) return Promise.resolve();
169
+ return new Promise((resolve) => {
170
+ drainWaiters.push(resolve);
171
+ });
172
+ };
147
173
  const tryAcquire = () => {
174
+ if (closed) {
175
+ return { ok: false, reason: "shutdown" };
176
+ }
148
177
  if (inFlight < opts.maxConcurrent) {
149
178
  inFlight++;
150
179
  return { ok: true, token: makeToken() };
@@ -152,6 +181,10 @@ function createBulkhead(opts) {
152
181
  return { ok: false, reason: "concurrency_limit" };
153
182
  };
154
183
  const acquire = (ao = {}) => {
184
+ if (closed) {
185
+ rejected++;
186
+ return Promise.resolve({ ok: false, reason: "shutdown" });
187
+ }
155
188
  if (inFlight < opts.maxConcurrent) {
156
189
  inFlight++;
157
190
  return Promise.resolve({ ok: true, token: makeToken() });
@@ -160,7 +193,7 @@ function createBulkhead(opts) {
160
193
  rejected++;
161
194
  return Promise.resolve({ ok: false, reason: "concurrency_limit" });
162
195
  }
163
- if (pendingCount() >= maxQueue) {
196
+ if (livePending >= maxQueue) {
164
197
  rejected++;
165
198
  return Promise.resolve({ ok: false, reason: "queue_limit" });
166
199
  }
@@ -172,6 +205,7 @@ function createBulkhead(opts) {
172
205
  abortListener: void 0,
173
206
  timeoutId: void 0
174
207
  };
208
+ livePending++;
175
209
  if (ao.signal) {
176
210
  if (ao.signal.aborted) {
177
211
  aborted++;
@@ -199,7 +233,9 @@ function createBulkhead(opts) {
199
233
  }, ao.timeoutMs);
200
234
  }
201
235
  q.pushBack(w);
202
- drain();
236
+ if (inFlight < opts.maxConcurrent) {
237
+ pump();
238
+ }
203
239
  });
204
240
  };
205
241
  const run = async (fn, ao = {}) => {
@@ -215,15 +251,17 @@ function createBulkhead(opts) {
215
251
  };
216
252
  const stats = () => ({
217
253
  inFlight,
218
- pending: pendingCount(),
254
+ pending: livePending,
219
255
  maxConcurrent: opts.maxConcurrent,
220
256
  maxQueue,
257
+ closed,
221
258
  aborted,
222
259
  timedOut,
223
260
  rejected,
224
- doubleRelease
261
+ doubleRelease,
262
+ inFlightUnderflow
225
263
  });
226
- return { tryAcquire, acquire, run, stats };
264
+ return { tryAcquire, acquire, run, stats, close, drain: drainFn };
227
265
  }
228
266
  // Annotate the CommonJS export names for ESM import in node:
229
267
  0 && (module.exports = {
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts"],"sourcesContent":["/**\n * A small ring-buffer queue.\n * - O(1) pushBack / popFront\n * - avoids array shift and head-index + compaction heuristics \n */\nclass RingDeque<T> {\n private buf: Array<T | undefined>;\n private head = 0;\n private tail = 0;\n private size = 0;\n\n constructor(capacity: number) {\n const cap = Math.max(4, capacity | 0);\n this.buf = new Array<T | undefined>(cap);\n }\n\n get length() {\n return this.size;\n }\n\n pushBack(item: T) {\n if (this.size === this.buf.length) {\n this.grow();\n }\n const idx = (this.head + this.size) % this.buf.length;\n this.buf[idx] = item;\n this.size++;\n }\n\n peekFront(): T | undefined {\n if (this.size === 0) return undefined;\n return this.buf[this.head];\n }\n\n popFront(): T | undefined {\n if (this.size === 0) return undefined;\n const item = this.buf[this.head];\n this.buf[this.head] = undefined; // help GC\n this.head = (this.head + 1) % this.buf.length;\n this.size--;\n return item;\n }\n\n private grow() {\n const newBuf = new Array<T | undefined>(this.buf.length * 2);\n for (let i = 0; i < this.size; i++) {\n newBuf[i] = this.buf[(this.head + i) % this.buf.length];\n }\n this.buf = newBuf;\n this.head = 0;\n }\n}\n\nexport type BulkheadOptions = {\n maxConcurrent: number;\n maxQueue?: number; // pending waiters allowed (0 => no waiting)\n};\n\nexport type AcquireOptions = {\n signal?: AbortSignal;\n timeoutMs?: number; // waiting timeout only\n};\n\nexport type Stats = {\n inFlight: number;\n pending: number;\n maxConcurrent: number;\n maxQueue: number;\n // optional debug counters:\n aborted?: number;\n timedOut?: number;\n rejected?: number;\n doubleRelease?: number;\n};\n\nexport type Token = { release(): void };\n\nexport type TryAcquireResult =\n | { ok: true; token: Token }\n | { ok: false; reason: 'concurrency_limit' };\n\nexport type AcquireResult =\n | { ok: true; token: Token }\n | { ok: false; reason: 'concurrency_limit' | 'queue_limit' | 'timeout' | 'aborted' };\n\ntype Waiter = {\n resolve: (r: AcquireResult) => void;\n cancelled: boolean;\n settled: boolean;\n\n abortListener: (() => void) | undefined;\n timeoutId: ReturnType<typeof setTimeout> | undefined;\n};\n\nexport type RejectReason = 'concurrency_limit' | 'queue_limit' | 'timeout' | 'aborted';\n\nexport class BulkheadRejectedError extends Error {\n readonly code = 'BULKHEAD_REJECTED' as const;\n\n constructor(readonly reason: RejectReason) {\n super(`Bulkhead rejected: ${reason}`);\n this.name = 'BulkheadRejectedError';\n }\n}\n\nexport function createBulkhead(opts: BulkheadOptions) {\n // ---- validate ----\n if (!Number.isInteger(opts.maxConcurrent) || opts.maxConcurrent <= 0) {\n throw new Error('maxConcurrent must be a positive integer');\n }\n const maxQueue = opts.maxQueue ?? 0;\n if (!Number.isInteger(maxQueue) || maxQueue < 0) {\n throw new Error('maxQueue must be an integer >= 0');\n }\n\n // ---- state ----\n let inFlight = 0;\n\n // FIFO queue as deque (no head index, just pushBack / popFront)\n const q = new RingDeque<Waiter>(maxQueue + 1); // +1 to avoid full queue edge case\n\n // optional counters\n let aborted = 0;\n let timedOut = 0;\n let rejected = 0;\n let doubleRelease = 0;\n\n // ---- token factory ----\n const makeToken = (): Token => {\n let released = false;\n return {\n release() {\n if (released) {\n doubleRelease++;\n return; // idempotent; consider throw in dev builds if you prefer\n }\n released = true;\n inFlight--;\n if (inFlight < 0) inFlight = 0; // defensive, should never happen\n drain();\n },\n };\n };\n\n const pruneCancelledFront = () => {\n // Remove cancelled/settled waiters at the front so they stop consuming maxQueue.\n while (q.length > 0) {\n const w = q.peekFront()!;\n if (w.cancelled || w.settled) {\n cleanupWaiter(w);\n q.popFront();\n continue;\n } else {\n break;\n }\n }\n }\n\n const pendingCount = () => {\n pruneCancelledFront();\n return q.length;\n }\n\n const cleanupWaiter = (w: Waiter) => {\n if (w.abortListener) w.abortListener();\n if (w.timeoutId) clearTimeout(w.timeoutId);\n w.abortListener = undefined;\n w.timeoutId = undefined;\n };\n\n const settle = (w: Waiter, r: AcquireResult) => {\n if (w.settled) return;\n w.settled = true;\n // Once settled, it's effectively cancelled for drain-skipping purposes.\n // (We keep cancelled separate because drain checks it.)\n if (!w.cancelled && !r.ok) w.cancelled = true;\n cleanupWaiter(w);\n w.resolve(r);\n };\n\n // ---- drain algorithm ----\n const drain = () => {\n // Prune first so cancelled/settled waiters don't block the head.\n pruneCancelledFront();\n while (inFlight < opts.maxConcurrent && q.length > 0) {\n const w = q.popFront()!; \n // If it was cancelled after peek/prune but before pop, skip it.\n if (w.cancelled) {\n cleanupWaiter(w);\n pruneCancelledFront(); // in case there are more cancelled after this one\n continue;\n }\n inFlight++;\n settle(w, { ok: true, token: makeToken() });\n }\n };\n\n // ---- public APIs ----\n\n const tryAcquire = (): TryAcquireResult => {\n if (inFlight < opts.maxConcurrent) {\n inFlight++;\n return { ok: true, token: makeToken() };\n }\n return { ok: false, reason: 'concurrency_limit' };\n };\n\n const acquire = (ao: AcquireOptions = {}): Promise<AcquireResult> => {\n // immediate fast path\n if (inFlight < opts.maxConcurrent) {\n inFlight++;\n return Promise.resolve({ ok: true, token: makeToken() });\n }\n\n // no waiting allowed\n if (maxQueue === 0) {\n rejected++;\n return Promise.resolve({ ok: false, reason: 'concurrency_limit' });\n }\n\n // bounded waiting\n if (pendingCount() >= maxQueue) {\n rejected++;\n return Promise.resolve({ ok: false, reason: 'queue_limit' });\n }\n\n // enqueue\n return new Promise<AcquireResult>((resolve) => {\n const w: Waiter = {\n resolve,\n cancelled: false,\n settled: false,\n abortListener: undefined,\n timeoutId: undefined,\n };\n\n // abort support\n if (ao.signal) {\n if (ao.signal.aborted) {\n aborted++;\n settle(w, { ok: false, reason: 'aborted' });\n return;\n }\n const onAbort = () => {\n // mark cancelled; drain() will skip it\n aborted++;\n w.cancelled = true;\n settle(w, { ok: false, reason: 'aborted' });\n };\n\n ao.signal.addEventListener('abort', onAbort, { once: true });\n w.abortListener = () => ao.signal!.removeEventListener('abort', onAbort);\n }\n\n // timeout support (waiting only)\n if (ao.timeoutMs != null) {\n if (!Number.isFinite(ao.timeoutMs) || ao.timeoutMs < 0) {\n // invalid => treat as immediate timeout\n timedOut++;\n settle(w, { ok: false, reason: 'timeout' });\n return;\n }\n w.timeoutId = setTimeout(() => {\n timedOut++;\n w.cancelled = true;\n settle(w, { ok: false, reason: 'timeout' });\n }, ao.timeoutMs);\n }\n\n q.pushBack(w);\n drain(); // required: capacity may have freed after the fast-path check but before enqueue\n });\n };\n\n const run = async <T>(\n fn: (signal?: AbortSignal) => Promise<T>,\n ao: AcquireOptions = {},\n ): Promise<T> => {\n const r = await acquire(ao);\n if (!r.ok) {\n throw new BulkheadRejectedError(r.reason);\n }\n try {\n return await fn(ao.signal);\n } finally {\n r.token.release();\n }\n };\n\n const stats = (): Stats => ({\n inFlight,\n pending: pendingCount(),\n maxConcurrent: opts.maxConcurrent,\n maxQueue,\n aborted,\n timedOut,\n rejected,\n doubleRelease,\n });\n\n return { tryAcquire, acquire, run, stats };\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/index.ts"],"sourcesContent":["/**\n * A small ring-buffer queue.\n * - O(1) pushBack / popFront\n * - avoids array shift and head-index + compaction heuristics\n */\nclass RingDeque<T> {\n private buf: Array<T | undefined>;\n private head = 0;\n private tail = 0;\n private size = 0;\n\n constructor(capacity: number) {\n const cap = Math.max(4, capacity | 0);\n this.buf = new Array<T | undefined>(cap);\n }\n\n get length() {\n return this.size;\n }\n\n pushBack(item: T) {\n if (this.size === this.buf.length) {\n this.grow();\n }\n const idx = (this.head + this.size) % this.buf.length;\n this.buf[idx] = item;\n this.size++;\n }\n\n peekFront(): T | undefined {\n if (this.size === 0) return undefined;\n return this.buf[this.head];\n }\n\n popFront(): T | undefined {\n if (this.size === 0) return undefined;\n const item = this.buf[this.head];\n this.buf[this.head] = undefined; // help GC\n this.head = (this.head + 1) % this.buf.length;\n this.size--;\n return item;\n }\n\n private grow() {\n const newBuf = new Array<T | undefined>(this.buf.length * 2);\n for (let i = 0; i < this.size; i++) {\n newBuf[i] = this.buf[(this.head + i) % this.buf.length];\n }\n this.buf = newBuf;\n this.head = 0;\n }\n}\n\nexport type BulkheadOptions = {\n maxConcurrent: number;\n maxQueue?: number; // pending waiters allowed (0 => no waiting)\n};\n\nexport type AcquireOptions = {\n signal?: AbortSignal;\n timeoutMs?: number; // waiting timeout only\n};\n\nexport type Stats = {\n inFlight: number;\n pending: number;\n maxConcurrent: number;\n maxQueue: number;\n closed: boolean;\n // optional debug counters:\n aborted?: number;\n timedOut?: number;\n rejected?: number;\n doubleRelease?: number;\n inFlightUnderflow?: number;\n};\n\nexport type Token = { release(): void };\n\nexport type TryAcquireResult =\n | { ok: true; token: Token }\n | { ok: false; reason: 'concurrency_limit' | 'shutdown' };\n\nexport type AcquireResult =\n | { ok: true; token: Token }\n | { ok: false; reason: RejectReason };\n\ntype Waiter = {\n resolve: (r: AcquireResult) => void;\n cancelled: boolean;\n settled: boolean;\n\n abortListener: (() => void) | undefined;\n timeoutId: ReturnType<typeof setTimeout> | undefined;\n};\n\nexport type RejectReason =\n | 'concurrency_limit'\n | 'queue_limit'\n | 'timeout'\n | 'aborted'\n | 'shutdown';\n\nexport class BulkheadRejectedError extends Error {\n readonly code = 'BULKHEAD_REJECTED' as const;\n\n constructor(readonly reason: RejectReason) {\n super(`Bulkhead rejected: ${reason}`);\n this.name = 'BulkheadRejectedError';\n }\n}\n\nexport function createBulkhead(opts: BulkheadOptions) {\n // ---- validate ----\n if (!Number.isInteger(opts.maxConcurrent) || opts.maxConcurrent <= 0) {\n throw new Error('maxConcurrent must be a positive integer');\n }\n const maxQueue = opts.maxQueue ?? 0;\n if (!Number.isInteger(maxQueue) || maxQueue < 0) {\n throw new Error('maxQueue must be an integer >= 0');\n }\n\n // ---- state ----\n let inFlight = 0;\n let closed = false;\n\n // Live pending count — the number of waiters in the queue that have not\n // been settled (admitted, cancelled, timed out, or aborted). Tracked\n // separately from `q.length` so that `stats()` is a pure read — the\n // queue may contain stale (cancelled/settled) entries that haven't been\n // pruned yet, but `livePending` is always accurate.\n let livePending = 0;\n\n // FIFO queue as deque (no head index, just pushBack / popFront)\n const q = new RingDeque<Waiter>(maxQueue + 1); // +1 to avoid full queue edge case\n\n // Drain waiters — resolve functions for pending drain() promises.\n let drainWaiters: Array<() => void> = [];\n\n // optional counters\n let aborted = 0;\n let timedOut = 0;\n let rejected = 0;\n let doubleRelease = 0;\n let inFlightUnderflow = 0;\n\n // ---- internal helpers ----\n\n const cleanupWaiter = (w: Waiter) => {\n if (w.abortListener) w.abortListener();\n if (w.timeoutId) clearTimeout(w.timeoutId);\n w.abortListener = undefined;\n w.timeoutId = undefined;\n };\n\n const settle = (w: Waiter, r: AcquireResult) => {\n if (w.settled) return;\n w.settled = true;\n // Once settled, it's effectively cancelled for pump-skipping purposes.\n if (!w.cancelled && !r.ok) w.cancelled = true;\n cleanupWaiter(w);\n livePending--;\n w.resolve(r);\n };\n\n /**\n * Remove cancelled/settled waiters from the front of the queue so the\n * deque doesn't accumulate stale entries. Called from pump() and\n * release paths — never from stats().\n */\n const pruneCancelledFront = () => {\n while (q.length > 0) {\n const w = q.peekFront()!;\n if (w.cancelled || w.settled) {\n q.popFront();\n continue;\n }\n break;\n }\n };\n\n /** Notify drain() waiters if inFlight has reached zero. */\n const notifyDrainWaiters = () => {\n if (inFlight === 0 && livePending === 0 && drainWaiters.length > 0) {\n const waiters = drainWaiters;\n drainWaiters = [];\n for (const resolve of waiters) resolve();\n }\n };\n\n // ---- token factory ----\n const makeToken = (): Token => {\n let released = false;\n return {\n release() {\n if (released) {\n doubleRelease++;\n return;\n }\n released = true;\n inFlight--;\n if (inFlight < 0) {\n inFlightUnderflow++;\n inFlight = 0;\n }\n pump();\n notifyDrainWaiters();\n },\n };\n };\n\n // ---- pump: admit waiters from the queue when capacity frees ----\n const pump = () => {\n pruneCancelledFront();\n while (inFlight < opts.maxConcurrent && q.length > 0) {\n const w = q.popFront()!;\n if (w.cancelled || w.settled) {\n pruneCancelledFront();\n continue;\n }\n inFlight++;\n settle(w, { ok: true, token: makeToken() });\n }\n };\n\n // ---- close(): reject all pending, block future admission ----\n const close = (): void => {\n if (closed) return;\n closed = true;\n\n // Reject all pending waiters.\n while (q.length > 0) {\n const w = q.popFront()!;\n if (w.settled || w.cancelled) continue;\n rejected++;\n settle(w, { ok: false, reason: 'shutdown' });\n }\n\n // If nothing is in-flight, notify drain waiters immediately.\n notifyDrainWaiters();\n };\n\n // ---- drain(): wait for inFlight to reach zero ----\n const drainFn = (): Promise<void> => {\n if (inFlight === 0 && livePending === 0) return Promise.resolve();\n return new Promise<void>((resolve) => {\n drainWaiters.push(resolve);\n });\n };\n\n // ---- public APIs ----\n\n const tryAcquire = (): TryAcquireResult => {\n if (closed) {\n return { ok: false, reason: 'shutdown' };\n }\n if (inFlight < opts.maxConcurrent) {\n inFlight++;\n return { ok: true, token: makeToken() };\n }\n return { ok: false, reason: 'concurrency_limit' };\n };\n\n const acquire = (ao: AcquireOptions = {}): Promise<AcquireResult> => {\n // closed fast path\n if (closed) {\n rejected++;\n return Promise.resolve({ ok: false, reason: 'shutdown' });\n }\n\n // immediate fast path\n if (inFlight < opts.maxConcurrent) {\n inFlight++;\n return Promise.resolve({ ok: true, token: makeToken() });\n }\n\n // no waiting allowed\n if (maxQueue === 0) {\n rejected++;\n return Promise.resolve({ ok: false, reason: 'concurrency_limit' });\n }\n\n // bounded waiting\n if (livePending >= maxQueue) {\n rejected++;\n return Promise.resolve({ ok: false, reason: 'queue_limit' });\n }\n\n // enqueue\n return new Promise<AcquireResult>((resolve) => {\n const w: Waiter = {\n resolve,\n cancelled: false,\n settled: false,\n abortListener: undefined,\n timeoutId: undefined,\n };\n\n livePending++;\n\n // abort support\n if (ao.signal) {\n if (ao.signal.aborted) {\n aborted++;\n settle(w, { ok: false, reason: 'aborted' });\n return;\n }\n const onAbort = () => {\n aborted++;\n w.cancelled = true;\n settle(w, { ok: false, reason: 'aborted' });\n };\n\n ao.signal.addEventListener('abort', onAbort, { once: true });\n w.abortListener = () => ao.signal!.removeEventListener('abort', onAbort);\n }\n\n // timeout support (waiting only)\n if (ao.timeoutMs != null) {\n if (!Number.isFinite(ao.timeoutMs) || ao.timeoutMs < 0) {\n timedOut++;\n settle(w, { ok: false, reason: 'timeout' });\n return;\n }\n w.timeoutId = setTimeout(() => {\n timedOut++;\n w.cancelled = true;\n settle(w, { ok: false, reason: 'timeout' });\n }, ao.timeoutMs);\n }\n\n q.pushBack(w);\n // Capacity may have freed after the fast-path check but before enqueue.\n if (inFlight < opts.maxConcurrent) {\n pump();\n }\n });\n };\n\n const run = async <T>(\n fn: (signal?: AbortSignal) => Promise<T>,\n ao: AcquireOptions = {},\n ): Promise<T> => {\n const r = await acquire(ao);\n if (!r.ok) {\n throw new BulkheadRejectedError(r.reason);\n }\n try {\n return await fn(ao.signal);\n } finally {\n r.token.release();\n }\n };\n\n const stats = (): Stats => ({\n inFlight,\n pending: livePending,\n maxConcurrent: opts.maxConcurrent,\n maxQueue,\n closed,\n aborted,\n timedOut,\n rejected,\n doubleRelease,\n inFlightUnderflow,\n });\n\n return { tryAcquire, acquire, run, stats, close, drain: drainFn };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAKA,IAAM,YAAN,MAAmB;AAAA,EACT;AAAA,EACA,OAAO;AAAA,EACP,OAAO;AAAA,EACP,OAAO;AAAA,EAEf,YAAY,UAAkB;AAC5B,UAAM,MAAM,KAAK,IAAI,GAAG,WAAW,CAAC;AACpC,SAAK,MAAM,IAAI,MAAqB,GAAG;AAAA,EACzC;AAAA,EAEA,IAAI,SAAS;AACX,WAAO,KAAK;AAAA,EACd;AAAA,EAEA,SAAS,MAAS;AAChB,QAAI,KAAK,SAAS,KAAK,IAAI,QAAQ;AACjC,WAAK,KAAK;AAAA,IACZ;AACA,UAAM,OAAO,KAAK,OAAO,KAAK,QAAQ,KAAK,IAAI;AAC/C,SAAK,IAAI,GAAG,IAAI;AAChB,SAAK;AAAA,EACP;AAAA,EAEA,YAA2B;AACzB,QAAI,KAAK,SAAS,EAAG,QAAO;AAC5B,WAAO,KAAK,IAAI,KAAK,IAAI;AAAA,EAC3B;AAAA,EAEA,WAA0B;AACxB,QAAI,KAAK,SAAS,EAAG,QAAO;AAC5B,UAAM,OAAO,KAAK,IAAI,KAAK,IAAI;AAC/B,SAAK,IAAI,KAAK,IAAI,IAAI;AACtB,SAAK,QAAQ,KAAK,OAAO,KAAK,KAAK,IAAI;AACvC,SAAK;AACL,WAAO;AAAA,EACT;AAAA,EAEQ,OAAO;AACb,UAAM,SAAS,IAAI,MAAqB,KAAK,IAAI,SAAS,CAAC;AAC3D,aAAS,IAAI,GAAG,IAAI,KAAK,MAAM,KAAK;AAClC,aAAO,CAAC,IAAI,KAAK,KAAK,KAAK,OAAO,KAAK,KAAK,IAAI,MAAM;AAAA,IACxD;AACA,SAAK,MAAM;AACX,SAAK,OAAO;AAAA,EACd;AACF;AAoDO,IAAM,wBAAN,cAAoC,MAAM;AAAA,EAG/C,YAAqB,QAAsB;AACzC,UAAM,sBAAsB,MAAM,EAAE;AADjB;AAEnB,SAAK,OAAO;AAAA,EACd;AAAA,EALS,OAAO;AAMlB;AAEO,SAAS,eAAe,MAAuB;AAEpD,MAAI,CAAC,OAAO,UAAU,KAAK,aAAa,KAAK,KAAK,iBAAiB,GAAG;AACpE,UAAM,IAAI,MAAM,0CAA0C;AAAA,EAC5D;AACA,QAAM,WAAW,KAAK,YAAY;AAClC,MAAI,CAAC,OAAO,UAAU,QAAQ,KAAK,WAAW,GAAG;AAC/C,UAAM,IAAI,MAAM,kCAAkC;AAAA,EACpD;AAGA,MAAI,WAAW;AACf,MAAI,SAAS;AAOb,MAAI,cAAc;AAGlB,QAAM,IAAI,IAAI,UAAkB,WAAW,CAAC;AAG5C,MAAI,eAAkC,CAAC;AAGvC,MAAI,UAAU;AACd,MAAI,WAAW;AACf,MAAI,WAAW;AACf,MAAI,gBAAgB;AACpB,MAAI,oBAAoB;AAIxB,QAAM,gBAAgB,CAAC,MAAc;AACnC,QAAI,EAAE,cAAe,GAAE,cAAc;AACrC,QAAI,EAAE,UAAW,cAAa,EAAE,SAAS;AACzC,MAAE,gBAAgB;AAClB,MAAE,YAAY;AAAA,EAChB;AAEA,QAAM,SAAS,CAAC,GAAW,MAAqB;AAC9C,QAAI,EAAE,QAAS;AACf,MAAE,UAAU;AAEZ,QAAI,CAAC,EAAE,aAAa,CAAC,EAAE,GAAI,GAAE,YAAY;AACzC,kBAAc,CAAC;AACf;AACA,MAAE,QAAQ,CAAC;AAAA,EACb;AAOA,QAAM,sBAAsB,MAAM;AAChC,WAAO,EAAE,SAAS,GAAG;AACnB,YAAM,IAAI,EAAE,UAAU;AACtB,UAAI,EAAE,aAAa,EAAE,SAAS;AAC5B,UAAE,SAAS;AACX;AAAA,MACF;AACA;AAAA,IACF;AAAA,EACF;AAGA,QAAM,qBAAqB,MAAM;AAC/B,QAAI,aAAa,KAAK,gBAAgB,KAAK,aAAa,SAAS,GAAG;AAClE,YAAM,UAAU;AAChB,qBAAe,CAAC;AAChB,iBAAW,WAAW,QAAS,SAAQ;AAAA,IACzC;AAAA,EACF;AAGA,QAAM,YAAY,MAAa;AAC7B,QAAI,WAAW;AACf,WAAO;AAAA,MACL,UAAU;AACR,YAAI,UAAU;AACZ;AACA;AAAA,QACF;AACA,mBAAW;AACX;AACA,YAAI,WAAW,GAAG;AAChB;AACA,qBAAW;AAAA,QACb;AACA,aAAK;AACL,2BAAmB;AAAA,MACrB;AAAA,IACF;AAAA,EACF;AAGA,QAAM,OAAO,MAAM;AACjB,wBAAoB;AACpB,WAAO,WAAW,KAAK,iBAAiB,EAAE,SAAS,GAAG;AACpD,YAAM,IAAI,EAAE,SAAS;AACrB,UAAI,EAAE,aAAa,EAAE,SAAS;AAC5B,4BAAoB;AACpB;AAAA,MACF;AACA;AACA,aAAO,GAAG,EAAE,IAAI,MAAM,OAAO,UAAU,EAAE,CAAC;AAAA,IAC5C;AAAA,EACF;AAGA,QAAM,QAAQ,MAAY;AACxB,QAAI,OAAQ;AACZ,aAAS;AAGT,WAAO,EAAE,SAAS,GAAG;AACnB,YAAM,IAAI,EAAE,SAAS;AACrB,UAAI,EAAE,WAAW,EAAE,UAAW;AAC9B;AACA,aAAO,GAAG,EAAE,IAAI,OAAO,QAAQ,WAAW,CAAC;AAAA,IAC7C;AAGA,uBAAmB;AAAA,EACrB;AAGA,QAAM,UAAU,MAAqB;AACnC,QAAI,aAAa,KAAK,gBAAgB,EAAG,QAAO,QAAQ,QAAQ;AAChE,WAAO,IAAI,QAAc,CAAC,YAAY;AACpC,mBAAa,KAAK,OAAO;AAAA,IAC3B,CAAC;AAAA,EACH;AAIA,QAAM,aAAa,MAAwB;AACzC,QAAI,QAAQ;AACV,aAAO,EAAE,IAAI,OAAO,QAAQ,WAAW;AAAA,IACzC;AACA,QAAI,WAAW,KAAK,eAAe;AACjC;AACA,aAAO,EAAE,IAAI,MAAM,OAAO,UAAU,EAAE;AAAA,IACxC;AACA,WAAO,EAAE,IAAI,OAAO,QAAQ,oBAAoB;AAAA,EAClD;AAEA,QAAM,UAAU,CAAC,KAAqB,CAAC,MAA8B;AAEnE,QAAI,QAAQ;AACV;AACA,aAAO,QAAQ,QAAQ,EAAE,IAAI,OAAO,QAAQ,WAAW,CAAC;AAAA,IAC1D;AAGA,QAAI,WAAW,KAAK,eAAe;AACjC;AACA,aAAO,QAAQ,QAAQ,EAAE,IAAI,MAAM,OAAO,UAAU,EAAE,CAAC;AAAA,IACzD;AAGA,QAAI,aAAa,GAAG;AAClB;AACA,aAAO,QAAQ,QAAQ,EAAE,IAAI,OAAO,QAAQ,oBAAoB,CAAC;AAAA,IACnE;AAGA,QAAI,eAAe,UAAU;AAC3B;AACA,aAAO,QAAQ,QAAQ,EAAE,IAAI,OAAO,QAAQ,cAAc,CAAC;AAAA,IAC7D;AAGA,WAAO,IAAI,QAAuB,CAAC,YAAY;AAC7C,YAAM,IAAY;AAAA,QAChB;AAAA,QACA,WAAW;AAAA,QACX,SAAS;AAAA,QACT,eAAe;AAAA,QACf,WAAW;AAAA,MACb;AAEA;AAGA,UAAI,GAAG,QAAQ;AACb,YAAI,GAAG,OAAO,SAAS;AACrB;AACA,iBAAO,GAAG,EAAE,IAAI,OAAO,QAAQ,UAAU,CAAC;AAC1C;AAAA,QACF;AACA,cAAM,UAAU,MAAM;AACpB;AACA,YAAE,YAAY;AACd,iBAAO,GAAG,EAAE,IAAI,OAAO,QAAQ,UAAU,CAAC;AAAA,QAC5C;AAEA,WAAG,OAAO,iBAAiB,SAAS,SAAS,EAAE,MAAM,KAAK,CAAC;AAC3D,UAAE,gBAAgB,MAAM,GAAG,OAAQ,oBAAoB,SAAS,OAAO;AAAA,MACzE;AAGA,UAAI,GAAG,aAAa,MAAM;AACxB,YAAI,CAAC,OAAO,SAAS,GAAG,SAAS,KAAK,GAAG,YAAY,GAAG;AACtD;AACA,iBAAO,GAAG,EAAE,IAAI,OAAO,QAAQ,UAAU,CAAC;AAC1C;AAAA,QACF;AACA,UAAE,YAAY,WAAW,MAAM;AAC7B;AACA,YAAE,YAAY;AACd,iBAAO,GAAG,EAAE,IAAI,OAAO,QAAQ,UAAU,CAAC;AAAA,QAC5C,GAAG,GAAG,SAAS;AAAA,MACjB;AAEA,QAAE,SAAS,CAAC;AAEZ,UAAI,WAAW,KAAK,eAAe;AACjC,aAAK;AAAA,MACP;AAAA,IACF,CAAC;AAAA,EACH;AAEA,QAAM,MAAM,OACV,IACA,KAAqB,CAAC,MACP;AACf,UAAM,IAAI,MAAM,QAAQ,EAAE;AAC1B,QAAI,CAAC,EAAE,IAAI;AACT,YAAM,IAAI,sBAAsB,EAAE,MAAM;AAAA,IAC1C;AACA,QAAI;AACF,aAAO,MAAM,GAAG,GAAG,MAAM;AAAA,IAC3B,UAAE;AACA,QAAE,MAAM,QAAQ;AAAA,IAClB;AAAA,EACF;AAEA,QAAM,QAAQ,OAAc;AAAA,IAC1B;AAAA,IACA,SAAS;AAAA,IACT,eAAe,KAAK;AAAA,IACpB;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF;AAEA,SAAO,EAAE,YAAY,SAAS,KAAK,OAAO,OAAO,OAAO,QAAQ;AAClE;","names":[]}
package/dist/index.d.cts CHANGED
@@ -11,10 +11,12 @@ type Stats = {
11
11
  pending: number;
12
12
  maxConcurrent: number;
13
13
  maxQueue: number;
14
+ closed: boolean;
14
15
  aborted?: number;
15
16
  timedOut?: number;
16
17
  rejected?: number;
17
18
  doubleRelease?: number;
19
+ inFlightUnderflow?: number;
18
20
  };
19
21
  type Token = {
20
22
  release(): void;
@@ -24,16 +26,16 @@ type TryAcquireResult = {
24
26
  token: Token;
25
27
  } | {
26
28
  ok: false;
27
- reason: 'concurrency_limit';
29
+ reason: 'concurrency_limit' | 'shutdown';
28
30
  };
29
31
  type AcquireResult = {
30
32
  ok: true;
31
33
  token: Token;
32
34
  } | {
33
35
  ok: false;
34
- reason: 'concurrency_limit' | 'queue_limit' | 'timeout' | 'aborted';
36
+ reason: RejectReason;
35
37
  };
36
- type RejectReason = 'concurrency_limit' | 'queue_limit' | 'timeout' | 'aborted';
38
+ type RejectReason = 'concurrency_limit' | 'queue_limit' | 'timeout' | 'aborted' | 'shutdown';
37
39
  declare class BulkheadRejectedError extends Error {
38
40
  readonly reason: RejectReason;
39
41
  readonly code: "BULKHEAD_REJECTED";
@@ -44,6 +46,8 @@ declare function createBulkhead(opts: BulkheadOptions): {
44
46
  acquire: (ao?: AcquireOptions) => Promise<AcquireResult>;
45
47
  run: <T>(fn: (signal?: AbortSignal) => Promise<T>, ao?: AcquireOptions) => Promise<T>;
46
48
  stats: () => Stats;
49
+ close: () => void;
50
+ drain: () => Promise<void>;
47
51
  };
48
52
 
49
53
  export { type AcquireOptions, type AcquireResult, type BulkheadOptions, BulkheadRejectedError, type RejectReason, type Stats, type Token, type TryAcquireResult, createBulkhead };
package/dist/index.d.ts CHANGED
@@ -11,10 +11,12 @@ type Stats = {
11
11
  pending: number;
12
12
  maxConcurrent: number;
13
13
  maxQueue: number;
14
+ closed: boolean;
14
15
  aborted?: number;
15
16
  timedOut?: number;
16
17
  rejected?: number;
17
18
  doubleRelease?: number;
19
+ inFlightUnderflow?: number;
18
20
  };
19
21
  type Token = {
20
22
  release(): void;
@@ -24,16 +26,16 @@ type TryAcquireResult = {
24
26
  token: Token;
25
27
  } | {
26
28
  ok: false;
27
- reason: 'concurrency_limit';
29
+ reason: 'concurrency_limit' | 'shutdown';
28
30
  };
29
31
  type AcquireResult = {
30
32
  ok: true;
31
33
  token: Token;
32
34
  } | {
33
35
  ok: false;
34
- reason: 'concurrency_limit' | 'queue_limit' | 'timeout' | 'aborted';
36
+ reason: RejectReason;
35
37
  };
36
- type RejectReason = 'concurrency_limit' | 'queue_limit' | 'timeout' | 'aborted';
38
+ type RejectReason = 'concurrency_limit' | 'queue_limit' | 'timeout' | 'aborted' | 'shutdown';
37
39
  declare class BulkheadRejectedError extends Error {
38
40
  readonly reason: RejectReason;
39
41
  readonly code: "BULKHEAD_REJECTED";
@@ -44,6 +46,8 @@ declare function createBulkhead(opts: BulkheadOptions): {
44
46
  acquire: (ao?: AcquireOptions) => Promise<AcquireResult>;
45
47
  run: <T>(fn: (signal?: AbortSignal) => Promise<T>, ao?: AcquireOptions) => Promise<T>;
46
48
  stats: () => Stats;
49
+ close: () => void;
50
+ drain: () => Promise<void>;
47
51
  };
48
52
 
49
53
  export { type AcquireOptions, type AcquireResult, type BulkheadOptions, BulkheadRejectedError, type RejectReason, type Stats, type Token, type TryAcquireResult, createBulkhead };
package/dist/index.js CHANGED
@@ -57,42 +57,15 @@ function createBulkhead(opts) {
57
57
  throw new Error("maxQueue must be an integer >= 0");
58
58
  }
59
59
  let inFlight = 0;
60
+ let closed = false;
61
+ let livePending = 0;
60
62
  const q = new RingDeque(maxQueue + 1);
63
+ let drainWaiters = [];
61
64
  let aborted = 0;
62
65
  let timedOut = 0;
63
66
  let rejected = 0;
64
67
  let doubleRelease = 0;
65
- const makeToken = () => {
66
- let released = false;
67
- return {
68
- release() {
69
- if (released) {
70
- doubleRelease++;
71
- return;
72
- }
73
- released = true;
74
- inFlight--;
75
- if (inFlight < 0) inFlight = 0;
76
- drain();
77
- }
78
- };
79
- };
80
- const pruneCancelledFront = () => {
81
- while (q.length > 0) {
82
- const w = q.peekFront();
83
- if (w.cancelled || w.settled) {
84
- cleanupWaiter(w);
85
- q.popFront();
86
- continue;
87
- } else {
88
- break;
89
- }
90
- }
91
- };
92
- const pendingCount = () => {
93
- pruneCancelledFront();
94
- return q.length;
95
- };
68
+ let inFlightUnderflow = 0;
96
69
  const cleanupWaiter = (w) => {
97
70
  if (w.abortListener) w.abortListener();
98
71
  if (w.timeoutId) clearTimeout(w.timeoutId);
@@ -104,14 +77,50 @@ function createBulkhead(opts) {
104
77
  w.settled = true;
105
78
  if (!w.cancelled && !r.ok) w.cancelled = true;
106
79
  cleanupWaiter(w);
80
+ livePending--;
107
81
  w.resolve(r);
108
82
  };
109
- const drain = () => {
83
+ const pruneCancelledFront = () => {
84
+ while (q.length > 0) {
85
+ const w = q.peekFront();
86
+ if (w.cancelled || w.settled) {
87
+ q.popFront();
88
+ continue;
89
+ }
90
+ break;
91
+ }
92
+ };
93
+ const notifyDrainWaiters = () => {
94
+ if (inFlight === 0 && livePending === 0 && drainWaiters.length > 0) {
95
+ const waiters = drainWaiters;
96
+ drainWaiters = [];
97
+ for (const resolve of waiters) resolve();
98
+ }
99
+ };
100
+ const makeToken = () => {
101
+ let released = false;
102
+ return {
103
+ release() {
104
+ if (released) {
105
+ doubleRelease++;
106
+ return;
107
+ }
108
+ released = true;
109
+ inFlight--;
110
+ if (inFlight < 0) {
111
+ inFlightUnderflow++;
112
+ inFlight = 0;
113
+ }
114
+ pump();
115
+ notifyDrainWaiters();
116
+ }
117
+ };
118
+ };
119
+ const pump = () => {
110
120
  pruneCancelledFront();
111
121
  while (inFlight < opts.maxConcurrent && q.length > 0) {
112
122
  const w = q.popFront();
113
- if (w.cancelled) {
114
- cleanupWaiter(w);
123
+ if (w.cancelled || w.settled) {
115
124
  pruneCancelledFront();
116
125
  continue;
117
126
  }
@@ -119,7 +128,27 @@ function createBulkhead(opts) {
119
128
  settle(w, { ok: true, token: makeToken() });
120
129
  }
121
130
  };
131
+ const close = () => {
132
+ if (closed) return;
133
+ closed = true;
134
+ while (q.length > 0) {
135
+ const w = q.popFront();
136
+ if (w.settled || w.cancelled) continue;
137
+ rejected++;
138
+ settle(w, { ok: false, reason: "shutdown" });
139
+ }
140
+ notifyDrainWaiters();
141
+ };
142
+ const drainFn = () => {
143
+ if (inFlight === 0 && livePending === 0) return Promise.resolve();
144
+ return new Promise((resolve) => {
145
+ drainWaiters.push(resolve);
146
+ });
147
+ };
122
148
  const tryAcquire = () => {
149
+ if (closed) {
150
+ return { ok: false, reason: "shutdown" };
151
+ }
123
152
  if (inFlight < opts.maxConcurrent) {
124
153
  inFlight++;
125
154
  return { ok: true, token: makeToken() };
@@ -127,6 +156,10 @@ function createBulkhead(opts) {
127
156
  return { ok: false, reason: "concurrency_limit" };
128
157
  };
129
158
  const acquire = (ao = {}) => {
159
+ if (closed) {
160
+ rejected++;
161
+ return Promise.resolve({ ok: false, reason: "shutdown" });
162
+ }
130
163
  if (inFlight < opts.maxConcurrent) {
131
164
  inFlight++;
132
165
  return Promise.resolve({ ok: true, token: makeToken() });
@@ -135,7 +168,7 @@ function createBulkhead(opts) {
135
168
  rejected++;
136
169
  return Promise.resolve({ ok: false, reason: "concurrency_limit" });
137
170
  }
138
- if (pendingCount() >= maxQueue) {
171
+ if (livePending >= maxQueue) {
139
172
  rejected++;
140
173
  return Promise.resolve({ ok: false, reason: "queue_limit" });
141
174
  }
@@ -147,6 +180,7 @@ function createBulkhead(opts) {
147
180
  abortListener: void 0,
148
181
  timeoutId: void 0
149
182
  };
183
+ livePending++;
150
184
  if (ao.signal) {
151
185
  if (ao.signal.aborted) {
152
186
  aborted++;
@@ -174,7 +208,9 @@ function createBulkhead(opts) {
174
208
  }, ao.timeoutMs);
175
209
  }
176
210
  q.pushBack(w);
177
- drain();
211
+ if (inFlight < opts.maxConcurrent) {
212
+ pump();
213
+ }
178
214
  });
179
215
  };
180
216
  const run = async (fn, ao = {}) => {
@@ -190,15 +226,17 @@ function createBulkhead(opts) {
190
226
  };
191
227
  const stats = () => ({
192
228
  inFlight,
193
- pending: pendingCount(),
229
+ pending: livePending,
194
230
  maxConcurrent: opts.maxConcurrent,
195
231
  maxQueue,
232
+ closed,
196
233
  aborted,
197
234
  timedOut,
198
235
  rejected,
199
- doubleRelease
236
+ doubleRelease,
237
+ inFlightUnderflow
200
238
  });
201
- return { tryAcquire, acquire, run, stats };
239
+ return { tryAcquire, acquire, run, stats, close, drain: drainFn };
202
240
  }
203
241
  export {
204
242
  BulkheadRejectedError,
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts"],"sourcesContent":["/**\n * A small ring-buffer queue.\n * - O(1) pushBack / popFront\n * - avoids array shift and head-index + compaction heuristics \n */\nclass RingDeque<T> {\n private buf: Array<T | undefined>;\n private head = 0;\n private tail = 0;\n private size = 0;\n\n constructor(capacity: number) {\n const cap = Math.max(4, capacity | 0);\n this.buf = new Array<T | undefined>(cap);\n }\n\n get length() {\n return this.size;\n }\n\n pushBack(item: T) {\n if (this.size === this.buf.length) {\n this.grow();\n }\n const idx = (this.head + this.size) % this.buf.length;\n this.buf[idx] = item;\n this.size++;\n }\n\n peekFront(): T | undefined {\n if (this.size === 0) return undefined;\n return this.buf[this.head];\n }\n\n popFront(): T | undefined {\n if (this.size === 0) return undefined;\n const item = this.buf[this.head];\n this.buf[this.head] = undefined; // help GC\n this.head = (this.head + 1) % this.buf.length;\n this.size--;\n return item;\n }\n\n private grow() {\n const newBuf = new Array<T | undefined>(this.buf.length * 2);\n for (let i = 0; i < this.size; i++) {\n newBuf[i] = this.buf[(this.head + i) % this.buf.length];\n }\n this.buf = newBuf;\n this.head = 0;\n }\n}\n\nexport type BulkheadOptions = {\n maxConcurrent: number;\n maxQueue?: number; // pending waiters allowed (0 => no waiting)\n};\n\nexport type AcquireOptions = {\n signal?: AbortSignal;\n timeoutMs?: number; // waiting timeout only\n};\n\nexport type Stats = {\n inFlight: number;\n pending: number;\n maxConcurrent: number;\n maxQueue: number;\n // optional debug counters:\n aborted?: number;\n timedOut?: number;\n rejected?: number;\n doubleRelease?: number;\n};\n\nexport type Token = { release(): void };\n\nexport type TryAcquireResult =\n | { ok: true; token: Token }\n | { ok: false; reason: 'concurrency_limit' };\n\nexport type AcquireResult =\n | { ok: true; token: Token }\n | { ok: false; reason: 'concurrency_limit' | 'queue_limit' | 'timeout' | 'aborted' };\n\ntype Waiter = {\n resolve: (r: AcquireResult) => void;\n cancelled: boolean;\n settled: boolean;\n\n abortListener: (() => void) | undefined;\n timeoutId: ReturnType<typeof setTimeout> | undefined;\n};\n\nexport type RejectReason = 'concurrency_limit' | 'queue_limit' | 'timeout' | 'aborted';\n\nexport class BulkheadRejectedError extends Error {\n readonly code = 'BULKHEAD_REJECTED' as const;\n\n constructor(readonly reason: RejectReason) {\n super(`Bulkhead rejected: ${reason}`);\n this.name = 'BulkheadRejectedError';\n }\n}\n\nexport function createBulkhead(opts: BulkheadOptions) {\n // ---- validate ----\n if (!Number.isInteger(opts.maxConcurrent) || opts.maxConcurrent <= 0) {\n throw new Error('maxConcurrent must be a positive integer');\n }\n const maxQueue = opts.maxQueue ?? 0;\n if (!Number.isInteger(maxQueue) || maxQueue < 0) {\n throw new Error('maxQueue must be an integer >= 0');\n }\n\n // ---- state ----\n let inFlight = 0;\n\n // FIFO queue as deque (no head index, just pushBack / popFront)\n const q = new RingDeque<Waiter>(maxQueue + 1); // +1 to avoid full queue edge case\n\n // optional counters\n let aborted = 0;\n let timedOut = 0;\n let rejected = 0;\n let doubleRelease = 0;\n\n // ---- token factory ----\n const makeToken = (): Token => {\n let released = false;\n return {\n release() {\n if (released) {\n doubleRelease++;\n return; // idempotent; consider throw in dev builds if you prefer\n }\n released = true;\n inFlight--;\n if (inFlight < 0) inFlight = 0; // defensive, should never happen\n drain();\n },\n };\n };\n\n const pruneCancelledFront = () => {\n // Remove cancelled/settled waiters at the front so they stop consuming maxQueue.\n while (q.length > 0) {\n const w = q.peekFront()!;\n if (w.cancelled || w.settled) {\n cleanupWaiter(w);\n q.popFront();\n continue;\n } else {\n break;\n }\n }\n }\n\n const pendingCount = () => {\n pruneCancelledFront();\n return q.length;\n }\n\n const cleanupWaiter = (w: Waiter) => {\n if (w.abortListener) w.abortListener();\n if (w.timeoutId) clearTimeout(w.timeoutId);\n w.abortListener = undefined;\n w.timeoutId = undefined;\n };\n\n const settle = (w: Waiter, r: AcquireResult) => {\n if (w.settled) return;\n w.settled = true;\n // Once settled, it's effectively cancelled for drain-skipping purposes.\n // (We keep cancelled separate because drain checks it.)\n if (!w.cancelled && !r.ok) w.cancelled = true;\n cleanupWaiter(w);\n w.resolve(r);\n };\n\n // ---- drain algorithm ----\n const drain = () => {\n // Prune first so cancelled/settled waiters don't block the head.\n pruneCancelledFront();\n while (inFlight < opts.maxConcurrent && q.length > 0) {\n const w = q.popFront()!; \n // If it was cancelled after peek/prune but before pop, skip it.\n if (w.cancelled) {\n cleanupWaiter(w);\n pruneCancelledFront(); // in case there are more cancelled after this one\n continue;\n }\n inFlight++;\n settle(w, { ok: true, token: makeToken() });\n }\n };\n\n // ---- public APIs ----\n\n const tryAcquire = (): TryAcquireResult => {\n if (inFlight < opts.maxConcurrent) {\n inFlight++;\n return { ok: true, token: makeToken() };\n }\n return { ok: false, reason: 'concurrency_limit' };\n };\n\n const acquire = (ao: AcquireOptions = {}): Promise<AcquireResult> => {\n // immediate fast path\n if (inFlight < opts.maxConcurrent) {\n inFlight++;\n return Promise.resolve({ ok: true, token: makeToken() });\n }\n\n // no waiting allowed\n if (maxQueue === 0) {\n rejected++;\n return Promise.resolve({ ok: false, reason: 'concurrency_limit' });\n }\n\n // bounded waiting\n if (pendingCount() >= maxQueue) {\n rejected++;\n return Promise.resolve({ ok: false, reason: 'queue_limit' });\n }\n\n // enqueue\n return new Promise<AcquireResult>((resolve) => {\n const w: Waiter = {\n resolve,\n cancelled: false,\n settled: false,\n abortListener: undefined,\n timeoutId: undefined,\n };\n\n // abort support\n if (ao.signal) {\n if (ao.signal.aborted) {\n aborted++;\n settle(w, { ok: false, reason: 'aborted' });\n return;\n }\n const onAbort = () => {\n // mark cancelled; drain() will skip it\n aborted++;\n w.cancelled = true;\n settle(w, { ok: false, reason: 'aborted' });\n };\n\n ao.signal.addEventListener('abort', onAbort, { once: true });\n w.abortListener = () => ao.signal!.removeEventListener('abort', onAbort);\n }\n\n // timeout support (waiting only)\n if (ao.timeoutMs != null) {\n if (!Number.isFinite(ao.timeoutMs) || ao.timeoutMs < 0) {\n // invalid => treat as immediate timeout\n timedOut++;\n settle(w, { ok: false, reason: 'timeout' });\n return;\n }\n w.timeoutId = setTimeout(() => {\n timedOut++;\n w.cancelled = true;\n settle(w, { ok: false, reason: 'timeout' });\n }, ao.timeoutMs);\n }\n\n q.pushBack(w);\n drain(); // required: capacity may have freed after the fast-path check but before enqueue\n });\n };\n\n const run = async <T>(\n fn: (signal?: AbortSignal) => Promise<T>,\n ao: AcquireOptions = {},\n ): Promise<T> => {\n const r = await acquire(ao);\n if (!r.ok) {\n throw new BulkheadRejectedError(r.reason);\n }\n try {\n return await fn(ao.signal);\n } finally {\n r.token.release();\n }\n };\n\n const stats = (): Stats => ({\n inFlight,\n pending: pendingCount(),\n maxConcurrent: opts.maxConcurrent,\n maxQueue,\n aborted,\n timedOut,\n rejected,\n doubleRelease,\n });\n\n return { tryAcquire, acquire, run, stats };\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/index.ts"],"sourcesContent":["/**\n * A small ring-buffer queue.\n * - O(1) pushBack / popFront\n * - avoids array shift and head-index + compaction heuristics\n */\nclass RingDeque<T> {\n private buf: Array<T | undefined>;\n private head = 0;\n private tail = 0;\n private size = 0;\n\n constructor(capacity: number) {\n const cap = Math.max(4, capacity | 0);\n this.buf = new Array<T | undefined>(cap);\n }\n\n get length() {\n return this.size;\n }\n\n pushBack(item: T) {\n if (this.size === this.buf.length) {\n this.grow();\n }\n const idx = (this.head + this.size) % this.buf.length;\n this.buf[idx] = item;\n this.size++;\n }\n\n peekFront(): T | undefined {\n if (this.size === 0) return undefined;\n return this.buf[this.head];\n }\n\n popFront(): T | undefined {\n if (this.size === 0) return undefined;\n const item = this.buf[this.head];\n this.buf[this.head] = undefined; // help GC\n this.head = (this.head + 1) % this.buf.length;\n this.size--;\n return item;\n }\n\n private grow() {\n const newBuf = new Array<T | undefined>(this.buf.length * 2);\n for (let i = 0; i < this.size; i++) {\n newBuf[i] = this.buf[(this.head + i) % this.buf.length];\n }\n this.buf = newBuf;\n this.head = 0;\n }\n}\n\nexport type BulkheadOptions = {\n maxConcurrent: number;\n maxQueue?: number; // pending waiters allowed (0 => no waiting)\n};\n\nexport type AcquireOptions = {\n signal?: AbortSignal;\n timeoutMs?: number; // waiting timeout only\n};\n\nexport type Stats = {\n inFlight: number;\n pending: number;\n maxConcurrent: number;\n maxQueue: number;\n closed: boolean;\n // optional debug counters:\n aborted?: number;\n timedOut?: number;\n rejected?: number;\n doubleRelease?: number;\n inFlightUnderflow?: number;\n};\n\nexport type Token = { release(): void };\n\nexport type TryAcquireResult =\n | { ok: true; token: Token }\n | { ok: false; reason: 'concurrency_limit' | 'shutdown' };\n\nexport type AcquireResult =\n | { ok: true; token: Token }\n | { ok: false; reason: RejectReason };\n\ntype Waiter = {\n resolve: (r: AcquireResult) => void;\n cancelled: boolean;\n settled: boolean;\n\n abortListener: (() => void) | undefined;\n timeoutId: ReturnType<typeof setTimeout> | undefined;\n};\n\nexport type RejectReason =\n | 'concurrency_limit'\n | 'queue_limit'\n | 'timeout'\n | 'aborted'\n | 'shutdown';\n\nexport class BulkheadRejectedError extends Error {\n readonly code = 'BULKHEAD_REJECTED' as const;\n\n constructor(readonly reason: RejectReason) {\n super(`Bulkhead rejected: ${reason}`);\n this.name = 'BulkheadRejectedError';\n }\n}\n\nexport function createBulkhead(opts: BulkheadOptions) {\n // ---- validate ----\n if (!Number.isInteger(opts.maxConcurrent) || opts.maxConcurrent <= 0) {\n throw new Error('maxConcurrent must be a positive integer');\n }\n const maxQueue = opts.maxQueue ?? 0;\n if (!Number.isInteger(maxQueue) || maxQueue < 0) {\n throw new Error('maxQueue must be an integer >= 0');\n }\n\n // ---- state ----\n let inFlight = 0;\n let closed = false;\n\n // Live pending count — the number of waiters in the queue that have not\n // been settled (admitted, cancelled, timed out, or aborted). Tracked\n // separately from `q.length` so that `stats()` is a pure read — the\n // queue may contain stale (cancelled/settled) entries that haven't been\n // pruned yet, but `livePending` is always accurate.\n let livePending = 0;\n\n // FIFO queue as deque (no head index, just pushBack / popFront)\n const q = new RingDeque<Waiter>(maxQueue + 1); // +1 to avoid full queue edge case\n\n // Drain waiters — resolve functions for pending drain() promises.\n let drainWaiters: Array<() => void> = [];\n\n // optional counters\n let aborted = 0;\n let timedOut = 0;\n let rejected = 0;\n let doubleRelease = 0;\n let inFlightUnderflow = 0;\n\n // ---- internal helpers ----\n\n const cleanupWaiter = (w: Waiter) => {\n if (w.abortListener) w.abortListener();\n if (w.timeoutId) clearTimeout(w.timeoutId);\n w.abortListener = undefined;\n w.timeoutId = undefined;\n };\n\n const settle = (w: Waiter, r: AcquireResult) => {\n if (w.settled) return;\n w.settled = true;\n // Once settled, it's effectively cancelled for pump-skipping purposes.\n if (!w.cancelled && !r.ok) w.cancelled = true;\n cleanupWaiter(w);\n livePending--;\n w.resolve(r);\n };\n\n /**\n * Remove cancelled/settled waiters from the front of the queue so the\n * deque doesn't accumulate stale entries. Called from pump() and\n * release paths — never from stats().\n */\n const pruneCancelledFront = () => {\n while (q.length > 0) {\n const w = q.peekFront()!;\n if (w.cancelled || w.settled) {\n q.popFront();\n continue;\n }\n break;\n }\n };\n\n /** Notify drain() waiters if inFlight has reached zero. */\n const notifyDrainWaiters = () => {\n if (inFlight === 0 && livePending === 0 && drainWaiters.length > 0) {\n const waiters = drainWaiters;\n drainWaiters = [];\n for (const resolve of waiters) resolve();\n }\n };\n\n // ---- token factory ----\n const makeToken = (): Token => {\n let released = false;\n return {\n release() {\n if (released) {\n doubleRelease++;\n return;\n }\n released = true;\n inFlight--;\n if (inFlight < 0) {\n inFlightUnderflow++;\n inFlight = 0;\n }\n pump();\n notifyDrainWaiters();\n },\n };\n };\n\n // ---- pump: admit waiters from the queue when capacity frees ----\n const pump = () => {\n pruneCancelledFront();\n while (inFlight < opts.maxConcurrent && q.length > 0) {\n const w = q.popFront()!;\n if (w.cancelled || w.settled) {\n pruneCancelledFront();\n continue;\n }\n inFlight++;\n settle(w, { ok: true, token: makeToken() });\n }\n };\n\n // ---- close(): reject all pending, block future admission ----\n const close = (): void => {\n if (closed) return;\n closed = true;\n\n // Reject all pending waiters.\n while (q.length > 0) {\n const w = q.popFront()!;\n if (w.settled || w.cancelled) continue;\n rejected++;\n settle(w, { ok: false, reason: 'shutdown' });\n }\n\n // If nothing is in-flight, notify drain waiters immediately.\n notifyDrainWaiters();\n };\n\n // ---- drain(): wait for inFlight to reach zero ----\n const drainFn = (): Promise<void> => {\n if (inFlight === 0 && livePending === 0) return Promise.resolve();\n return new Promise<void>((resolve) => {\n drainWaiters.push(resolve);\n });\n };\n\n // ---- public APIs ----\n\n const tryAcquire = (): TryAcquireResult => {\n if (closed) {\n return { ok: false, reason: 'shutdown' };\n }\n if (inFlight < opts.maxConcurrent) {\n inFlight++;\n return { ok: true, token: makeToken() };\n }\n return { ok: false, reason: 'concurrency_limit' };\n };\n\n const acquire = (ao: AcquireOptions = {}): Promise<AcquireResult> => {\n // closed fast path\n if (closed) {\n rejected++;\n return Promise.resolve({ ok: false, reason: 'shutdown' });\n }\n\n // immediate fast path\n if (inFlight < opts.maxConcurrent) {\n inFlight++;\n return Promise.resolve({ ok: true, token: makeToken() });\n }\n\n // no waiting allowed\n if (maxQueue === 0) {\n rejected++;\n return Promise.resolve({ ok: false, reason: 'concurrency_limit' });\n }\n\n // bounded waiting\n if (livePending >= maxQueue) {\n rejected++;\n return Promise.resolve({ ok: false, reason: 'queue_limit' });\n }\n\n // enqueue\n return new Promise<AcquireResult>((resolve) => {\n const w: Waiter = {\n resolve,\n cancelled: false,\n settled: false,\n abortListener: undefined,\n timeoutId: undefined,\n };\n\n livePending++;\n\n // abort support\n if (ao.signal) {\n if (ao.signal.aborted) {\n aborted++;\n settle(w, { ok: false, reason: 'aborted' });\n return;\n }\n const onAbort = () => {\n aborted++;\n w.cancelled = true;\n settle(w, { ok: false, reason: 'aborted' });\n };\n\n ao.signal.addEventListener('abort', onAbort, { once: true });\n w.abortListener = () => ao.signal!.removeEventListener('abort', onAbort);\n }\n\n // timeout support (waiting only)\n if (ao.timeoutMs != null) {\n if (!Number.isFinite(ao.timeoutMs) || ao.timeoutMs < 0) {\n timedOut++;\n settle(w, { ok: false, reason: 'timeout' });\n return;\n }\n w.timeoutId = setTimeout(() => {\n timedOut++;\n w.cancelled = true;\n settle(w, { ok: false, reason: 'timeout' });\n }, ao.timeoutMs);\n }\n\n q.pushBack(w);\n // Capacity may have freed after the fast-path check but before enqueue.\n if (inFlight < opts.maxConcurrent) {\n pump();\n }\n });\n };\n\n const run = async <T>(\n fn: (signal?: AbortSignal) => Promise<T>,\n ao: AcquireOptions = {},\n ): Promise<T> => {\n const r = await acquire(ao);\n if (!r.ok) {\n throw new BulkheadRejectedError(r.reason);\n }\n try {\n return await fn(ao.signal);\n } finally {\n r.token.release();\n }\n };\n\n const stats = (): Stats => ({\n inFlight,\n pending: livePending,\n maxConcurrent: opts.maxConcurrent,\n maxQueue,\n closed,\n aborted,\n timedOut,\n rejected,\n doubleRelease,\n inFlightUnderflow,\n });\n\n return { tryAcquire, acquire, run, stats, close, drain: drainFn };\n}\n"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,6 +1,12 @@
1
1
  {
2
2
  "name": "async-bulkhead-ts",
3
- "version": "0.2.2",
3
+ "version": "0.3.0",
4
+ "overrides": {
5
+ "minimatch": "^10.2.1",
6
+ "@eslint/eslintrc": {
7
+ "ajv": "^6.12.6"
8
+ }
9
+ },
4
10
  "description": "Fail-fast admission control + bulkheads for async workloads",
5
11
  "license": "MIT",
6
12
  "type": "module",