@fortify-ts/bulkhead
Advanced tools
+112
| # @fortify-ts/bulkhead | ||
| Bulkhead (concurrency limiter) pattern for the Fortify-TS resilience library. | ||
| ## Installation | ||
| ```bash | ||
| npm install @fortify-ts/bulkhead | ||
| # or | ||
| pnpm add @fortify-ts/bulkhead | ||
| ``` | ||
| ## Features | ||
| - **Concurrency Limiting**: Control maximum parallel executions | ||
| - **Request Queueing**: Queue excess requests with optional timeout | ||
| - **Rejection Handling**: Reject when at capacity | ||
| - **Metrics**: Track active and queued requests | ||
| ## Usage | ||
| ### Basic Usage | ||
| ```typescript | ||
| import { Bulkhead } from '@fortify-ts/bulkhead'; | ||
| const bulkhead = new Bulkhead<Response>({ | ||
| maxConcurrent: 10, | ||
| }); | ||
| try { | ||
| const result = await bulkhead.execute(async (signal) => { | ||
| return fetch('/api/data', { signal }); | ||
| }); | ||
| } catch (error) { | ||
| if (error instanceof BulkheadFullError) { | ||
| console.log('Too many concurrent requests'); | ||
| } | ||
| } | ||
| ``` | ||
| ### With Queueing | ||
| ```typescript | ||
| const bulkhead = new Bulkhead<Response>({ | ||
| maxConcurrent: 10, | ||
| maxQueue: 100, // Queue up to 100 requests | ||
| queueTimeout: 5000, // 5 second queue timeout | ||
| }); | ||
| ``` | ||
| ### Configuration Options | ||
| ```typescript | ||
| const bulkhead = new Bulkhead<Response>({ | ||
| // Maximum concurrent executions | ||
| maxConcurrent: 10, | ||
| // Maximum queued requests (0 = no queue) | ||
| maxQueue: 100, | ||
| // Queue timeout in milliseconds | ||
| queueTimeout: 5000, | ||
| // Rejection callback | ||
| onRejected: (activeCount, queuedCount) => { | ||
| console.log(`Rejected: ${activeCount} active, ${queuedCount} queued`); | ||
| }, | ||
| // Optional logger | ||
| logger: myLogger, | ||
| }); | ||
| ``` | ||
| ### Checking Status | ||
| ```typescript | ||
| // Get current counts | ||
| const activeCount = bulkhead.getActiveCount(); | ||
| const queuedCount = bulkhead.getQueuedCount(); | ||
| // Check if at capacity | ||
| const isFull = activeCount >= config.maxConcurrent; | ||
| ``` | ||
| ### Resource Cleanup | ||
| ```typescript | ||
| // Close bulkhead (rejects queued requests) | ||
| await bulkhead.close(); | ||
| ``` | ||
| ## Configuration Reference | ||
| | Option | Type | Default | Description | | ||
| |--------|------|---------|-------------| | ||
| | `maxConcurrent` | number | 10 | Max parallel executions | | ||
| | `maxQueue` | number | 0 | Max queued requests | | ||
| | `queueTimeout` | number | - | Queue timeout (ms) | | ||
| | `onRejected` | function | - | Rejection callback | | ||
| | `logger` | FortifyLogger | - | Optional logger | | ||
| ## Error Types | ||
| | Error | Description | | ||
| |-------|-------------| | ||
| | `BulkheadFullError` | Bulkhead and queue at capacity | | ||
| | `BulkheadClosedError` | Bulkhead has been closed | | ||
| ## License | ||
| MIT |
+30
-24
@@ -6,3 +6,2 @@ "use strict"; | ||
| var __hasOwnProp = Object.prototype.hasOwnProperty; | ||
| var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; | ||
| var __export = (target, all) => { | ||
@@ -21,3 +20,2 @@ for (var name in all) | ||
| var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); | ||
| var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value); | ||
@@ -39,9 +37,12 @@ // src/index.ts | ||
| var import_zod = require("zod"); | ||
| var MAX_CONCURRENT = 1e4; | ||
| var MAX_QUEUE_SIZE = 1e5; | ||
| var MAX_QUEUE_TIMEOUT_MS = 36e5; | ||
| var bulkheadConfigSchema = import_zod.z.object({ | ||
| /** Maximum number of concurrent executions allowed (default: 10) */ | ||
| maxConcurrent: import_zod.z.number().int().positive().default(10), | ||
| /** Maximum size of overflow queue, 0 means no queue (default: 0) */ | ||
| maxQueue: import_zod.z.number().int().nonnegative().default(0), | ||
| /** Maximum time a request can wait in queue in milliseconds, 0 means no timeout (default: 0) */ | ||
| queueTimeout: import_zod.z.number().int().nonnegative().default(0) | ||
| /** Maximum number of concurrent executions allowed (default: 10, max: 10000) */ | ||
| maxConcurrent: import_zod.z.number().int().positive().max(MAX_CONCURRENT).default(10), | ||
| /** Maximum size of overflow queue, 0 means no queue (default: 0, max: 100000) */ | ||
| maxQueue: import_zod.z.number().int().nonnegative().max(MAX_QUEUE_SIZE).default(0), | ||
| /** Maximum time a request can wait in queue in milliseconds, 0 means no timeout (default: 0, max: 1 hour) */ | ||
| queueTimeout: import_zod.z.number().int().nonnegative().max(MAX_QUEUE_TIMEOUT_MS).default(0) | ||
| }); | ||
@@ -66,8 +67,8 @@ function parseBulkheadConfig(config) { | ||
| this.capacity = capacity; | ||
| __publicField(this, "buffer"); | ||
| __publicField(this, "head", 0); | ||
| __publicField(this, "tail", 0); | ||
| __publicField(this, "count", 0); | ||
| this.buffer = new Array(capacity); | ||
| } | ||
| buffer; | ||
| head = 0; | ||
| tail = 0; | ||
| count = 0; | ||
| /** | ||
@@ -209,2 +210,5 @@ * Add an element to the end of the buffer. | ||
| var Semaphore = class { | ||
| permits; | ||
| maxPermits; | ||
| waitingQueue; | ||
| /** | ||
@@ -217,5 +221,2 @@ * Create a new semaphore. | ||
| constructor(maxPermits, maxQueue = 1e3) { | ||
| __publicField(this, "permits"); | ||
| __publicField(this, "maxPermits"); | ||
| __publicField(this, "waitingQueue"); | ||
| this.maxPermits = maxPermits; | ||
@@ -313,2 +314,7 @@ this.permits = maxPermits; | ||
| var Bulkhead = class { | ||
| config; | ||
| logger; | ||
| semaphore; | ||
| queueSemaphore; | ||
| closed = false; | ||
| /** | ||
@@ -320,7 +326,2 @@ * Create a new Bulkhead instance. | ||
| constructor(config) { | ||
| __publicField(this, "config"); | ||
| __publicField(this, "logger"); | ||
| __publicField(this, "semaphore"); | ||
| __publicField(this, "queueSemaphore"); | ||
| __publicField(this, "closed", false); | ||
| this.config = parseBulkheadConfig(config); | ||
@@ -348,3 +349,3 @@ this.logger = this.config.logger ?? import_core.noopLogger; | ||
| if (this.closed) { | ||
| throw this.createFullError("Bulkhead is closed"); | ||
| throw new import_core.BulkheadClosedError("Bulkhead is closed"); | ||
| } | ||
@@ -373,10 +374,15 @@ if (signal?.aborted) { | ||
| * Close the bulkhead, rejecting all pending requests. | ||
| * | ||
| * @returns Promise that resolves immediately (sync cleanup) | ||
| */ | ||
| close() { | ||
| if (this.closed) return; | ||
| if (this.closed) { | ||
| return Promise.resolve(); | ||
| } | ||
| this.closed = true; | ||
| const closeError = this.createFullError("Bulkhead closed"); | ||
| const closeError = new import_core.BulkheadClosedError("Bulkhead closed"); | ||
| this.semaphore.rejectAll(closeError); | ||
| this.queueSemaphore?.rejectAll(closeError); | ||
| this.logger.info("Bulkhead closed"); | ||
| return Promise.resolve(); | ||
| } | ||
@@ -395,3 +401,3 @@ /** | ||
| try { | ||
| return await operation(signal ?? new AbortController().signal); | ||
| return await operation(signal ?? import_core.NEVER_ABORTED_SIGNAL); | ||
| } finally { | ||
@@ -398,0 +404,0 @@ this.semaphore.release(); |
@@ -1,1 +0,1 @@ | ||
| {"version":3,"sources":["../src/index.ts","../src/bulkhead.ts","../src/config.ts","../src/ring-buffer.ts","../src/semaphore.ts"],"sourcesContent":["export { Bulkhead } from './bulkhead.js';\nexport { Semaphore } from './semaphore.js';\nexport {\n bulkheadConfigSchema,\n type BulkheadConfig,\n type BulkheadConfigInput,\n type BulkheadConfigInputFull,\n parseBulkheadConfig,\n} from './config.js';\n","import {\n type Operation,\n type Pattern,\n type Resettable,\n BulkheadFullError,\n type FortifyLogger,\n noopLogger,\n} from '@fortify-ts/core';\nimport {\n type BulkheadConfig,\n type BulkheadConfigInputFull,\n parseBulkheadConfig,\n} from './config.js';\nimport { Semaphore } from './semaphore.js';\n\n/**\n * Bulkhead pattern implementation for limiting concurrent operations.\n *\n * Prevents resource exhaustion by limiting the number of concurrent executions,\n * with optional queueing for overflow requests.\n *\n * @template T - The return type of operations\n *\n * @example\n * ```typescript\n * const bulkhead = new Bulkhead<Response>({\n * maxConcurrent: 5,\n * maxQueue: 10,\n * queueTimeout: 5000,\n * onRejected: () => console.log('Request rejected'),\n * });\n *\n * const result = await bulkhead.execute(async (signal) => {\n * return fetch('/api/data', { signal });\n * });\n * ```\n */\nexport class Bulkhead<T> implements Pattern<T>, Resettable {\n private readonly config: BulkheadConfig;\n private readonly logger: FortifyLogger;\n private readonly semaphore: Semaphore;\n private readonly queueSemaphore: Semaphore | undefined;\n private closed = false;\n\n /**\n * Create a new Bulkhead instance.\n *\n * @param config - Bulkhead configuration\n */\n constructor(config?: BulkheadConfigInputFull) {\n this.config = parseBulkheadConfig(config);\n this.logger = this.config.logger ?? noopLogger;\n // Execution semaphore: queue capacity = maxQueue (bounded by queue semaphore)\n this.semaphore = new Semaphore(\n this.config.maxConcurrent,\n Math.max(this.config.maxQueue, 1) // At least 1 for edge cases\n );\n\n // Only create queue semaphore if maxQueue > 0\n if (this.config.maxQueue > 0) {\n // Queue semaphore: only used for tryAcquire, queue never used\n this.queueSemaphore = new Semaphore(this.config.maxQueue, 1);\n }\n }\n\n /**\n * Execute an operation within the bulkhead's concurrency limits.\n *\n * @param operation - The async operation to execute\n * @param signal - Optional AbortSignal for cancellation\n * @returns Promise resolving to the operation result\n * @throws {BulkheadFullError} When bulkhead is at capacity and queue is full\n * @throws {DOMException} When cancelled via signal (AbortError)\n */\n async execute(operation: Operation<T>, signal?: AbortSignal): Promise<T> {\n // Check if closed\n if (this.closed) {\n throw this.createFullError('Bulkhead is closed');\n }\n\n // Check if cancelled\n if (signal?.aborted) {\n throw signal.reason ?? new DOMException('Aborted', 'AbortError');\n }\n\n // Try to acquire semaphore immediately\n if (this.semaphore.tryAcquire()) {\n return this.executeWithPermit(operation, signal);\n }\n\n // Bulkhead full, try to queue\n return this.enqueue(operation, signal);\n }\n\n /**\n * Get the number of currently active executions.\n */\n activeCount(): number {\n return this.config.maxConcurrent - this.semaphore.availablePermits();\n }\n\n /**\n * Get the number of requests currently waiting in the queue.\n */\n queuedCount(): number {\n return this.semaphore.queueLength();\n }\n\n /**\n * Close the bulkhead, rejecting all pending requests.\n */\n close(): void {\n if (this.closed) return;\n\n this.closed = true;\n const closeError = this.createFullError('Bulkhead closed');\n this.semaphore.rejectAll(closeError);\n this.queueSemaphore?.rejectAll(closeError);\n this.logger.info('Bulkhead closed');\n }\n\n /**\n * Reset the bulkhead to accept new requests.\n */\n reset(): void {\n this.closed = false;\n this.logger.info('Bulkhead reset');\n }\n\n /**\n * Execute operation with semaphore permit held.\n */\n private async executeWithPermit(\n operation: Operation<T>,\n signal?: AbortSignal\n ): Promise<T> {\n try {\n return await operation(signal ?? new AbortController().signal);\n } finally {\n this.semaphore.release();\n }\n }\n\n /**\n * Attempt to queue the request when bulkhead is full.\n */\n private async enqueue(\n operation: Operation<T>,\n signal?: AbortSignal\n ): Promise<T> {\n // If no queue configured, reject immediately\n if (this.config.maxQueue === 0) {\n this.onRejected();\n throw this.createFullError('Bulkhead is full - no queue configured');\n }\n\n // Try to acquire queue slot\n if (!this.queueSemaphore?.tryAcquire()) {\n // Queue is full, reject\n this.onRejected();\n throw this.createFullError('Bulkhead queue is full');\n }\n\n // Track timeout for cleanup in finally block\n let timeoutId: ReturnType<typeof setTimeout> | undefined;\n\n try {\n // Create combined signal for queue timeout\n let timeoutController: AbortController | undefined;\n let combinedSignal = signal;\n\n if (this.config.queueTimeout > 0) {\n timeoutController = new AbortController();\n\n // Create combined signal\n if (signal) {\n combinedSignal = AbortSignal.any([signal, timeoutController.signal]);\n } else {\n combinedSignal = timeoutController.signal;\n }\n\n // Start timeout with proper cleanup\n timeoutId = setTimeout(() => {\n timeoutController?.abort(new DOMException('Queue timeout', 'TimeoutError'));\n }, this.config.queueTimeout);\n }\n\n // Wait for execution semaphore\n try {\n await this.semaphore.acquire(combinedSignal);\n } catch (error) {\n // If aborted due to queue timeout, call onRejected\n if (\n error instanceof DOMException &&\n error.name === 'TimeoutError'\n ) {\n this.onRejected();\n }\n throw error;\n }\n\n // Got permit, execute\n return await this.executeWithPermit(operation, signal);\n } finally {\n // Clear timeout to prevent memory leak\n if (timeoutId) {\n clearTimeout(timeoutId);\n }\n this.queueSemaphore.release();\n }\n }\n\n /**\n * Handle rejection event.\n */\n private onRejected(): void {\n this.logger.warn('Bulkhead rejection', {\n maxConcurrent: this.config.maxConcurrent,\n maxQueue: this.config.maxQueue,\n });\n\n if (this.config.onRejected) {\n try {\n this.config.onRejected();\n } catch (error) {\n this.logger.error('onRejected callback threw an error', {\n error: error instanceof Error ? error.message : String(error),\n });\n }\n }\n }\n\n /**\n * Create a BulkheadFullError with current state context.\n */\n private createFullError(message?: string): BulkheadFullError {\n return new BulkheadFullError(\n message ?? 'Bulkhead is full',\n this.activeCount(),\n this.queuedCount()\n );\n }\n}\n","import { z } from 'zod';\nimport { type FortifyLogger } from '@fortify-ts/core';\n\n/**\n * Zod schema for Bulkhead configuration.\n */\nexport const bulkheadConfigSchema = z.object({\n /** Maximum number of concurrent executions allowed (default: 10) */\n maxConcurrent: z.number().int().positive().default(10),\n /** Maximum size of overflow queue, 0 means no queue (default: 0) */\n maxQueue: z.number().int().nonnegative().default(0),\n /** Maximum time a request can wait in queue in milliseconds, 0 means no timeout (default: 0) */\n queueTimeout: z.number().int().nonnegative().default(0),\n});\n\n/**\n * Raw config input type (before defaults are applied).\n */\nexport type BulkheadConfigInput = z.input<typeof bulkheadConfigSchema>;\n\n/**\n * Parsed config type (after defaults are applied).\n */\nexport type BulkheadConfigParsed = z.output<typeof bulkheadConfigSchema>;\n\n/**\n * Full configuration type including callbacks and logger.\n */\nexport interface BulkheadConfig extends BulkheadConfigParsed {\n /** Callback when a request is rejected */\n onRejected: (() => void) | undefined;\n /** Logger instance for structured logging */\n logger: FortifyLogger | undefined;\n}\n\n/**\n * Input config type for constructor.\n */\nexport interface BulkheadConfigInputFull extends BulkheadConfigInput {\n onRejected?: () => void;\n logger?: FortifyLogger;\n}\n\n/**\n * Parse and validate bulkhead configuration.\n *\n * @param config - Raw configuration input\n * @returns Validated configuration with defaults applied\n */\nexport function parseBulkheadConfig(config?: BulkheadConfigInputFull): BulkheadConfig {\n const parsed = bulkheadConfigSchema.parse(config ?? {});\n return {\n ...parsed,\n onRejected: config?.onRejected,\n logger: config?.logger,\n };\n}\n","/**\n * O(1) ring buffer queue implementation.\n *\n * Provides O(1) enqueue and dequeue operations by using a circular buffer\n * with head and tail pointers.\n *\n * @template T - The type of elements in the buffer\n */\nexport class RingBuffer<T> {\n private readonly buffer: (T | undefined)[];\n private head = 0;\n private tail = 0;\n private count = 0;\n\n /**\n * Create a new ring buffer.\n *\n * @param capacity - Maximum number of elements the buffer can hold\n */\n constructor(private readonly capacity: number) {\n this.buffer = new Array<T | undefined>(capacity);\n }\n\n /**\n * Add an element to the end of the buffer.\n *\n * @param item - The item to add\n * @returns true if added, false if buffer is full\n */\n push(item: T): boolean {\n if (this.count >= this.capacity) {\n return false;\n }\n\n this.buffer[this.tail] = item;\n this.tail = (this.tail + 1) % this.capacity;\n this.count++;\n return true;\n }\n\n /**\n * Remove and return the element at the front of the buffer.\n *\n * @returns The item at the front, or undefined if empty\n */\n shift(): T | undefined {\n if (this.count === 0) {\n return undefined;\n }\n\n const item = this.buffer[this.head];\n this.buffer[this.head] = undefined; // Help GC\n this.head = (this.head + 1) % this.capacity;\n this.count--;\n return item;\n }\n\n /**\n * Get the number of elements in the buffer.\n */\n get length(): number {\n return this.count;\n }\n\n /**\n * Check if the buffer is empty.\n */\n isEmpty(): boolean {\n return this.count === 0;\n }\n\n /**\n * Check if the buffer is full.\n */\n isFull(): boolean {\n return this.count >= this.capacity;\n }\n\n /**\n * Find the index of an item in the buffer.\n * Note: This is O(n) but should be rare (used for abort handling).\n *\n * @param item - The item to find\n * @returns The index of the item, or -1 if not found\n */\n indexOf(item: T): number {\n for (let i = 0; i < this.count; i++) {\n const idx = (this.head + i) % this.capacity;\n if (this.buffer[idx] === item) {\n return i;\n }\n }\n return -1;\n }\n\n /**\n * Remove an item at the given logical index.\n * Note: This is O(n) due to shifting elements.\n *\n * @param index - The logical index (0 = front of queue)\n * @returns true if item was removed\n */\n removeAt(index: number): boolean {\n if (index < 0 || index >= this.count) {\n return false;\n }\n\n // Shift elements to fill the gap\n for (let i = index; i < this.count - 1; i++) {\n const fromIdx = (this.head + i + 1) % this.capacity;\n const toIdx = (this.head + i) % this.capacity;\n this.buffer[toIdx] = this.buffer[fromIdx];\n }\n\n // Clear the last element\n const lastIdx = (this.head + this.count - 1) % this.capacity;\n this.buffer[lastIdx] = undefined;\n\n // Update tail\n this.tail = (this.tail - 1 + this.capacity) % this.capacity;\n this.count--;\n return true;\n }\n\n /**\n * Remove an item from the buffer.\n * Note: This is O(n) but should be rare (used for abort handling).\n *\n * @param item - The item to remove\n * @returns true if item was found and removed\n */\n remove(item: T): boolean {\n const index = this.indexOf(item);\n if (index === -1) {\n return false;\n }\n return this.removeAt(index);\n }\n\n /**\n * Get all items and clear the buffer.\n *\n * @returns Array of all items in queue order\n */\n drain(): T[] {\n const items: T[] = [];\n for (let i = 0; i < this.count; i++) {\n const idx = (this.head + i) % this.capacity;\n const item = this.buffer[idx];\n if (item !== undefined) {\n items.push(item);\n }\n this.buffer[idx] = undefined; // Help GC\n }\n this.head = 0;\n this.tail = 0;\n this.count = 0;\n return items;\n }\n\n /**\n * Clear all elements from the buffer.\n */\n clear(): void {\n for (let i = 0; i < this.count; i++) {\n const idx = (this.head + i) % this.capacity;\n this.buffer[idx] = undefined;\n }\n this.head = 0;\n this.tail = 0;\n this.count = 0;\n }\n}\n","import { RingBuffer } from './ring-buffer.js';\n\n/**\n * Waiter in the semaphore queue.\n */\ninterface Waiter {\n resolve: () => void;\n reject: (error: Error) => void;\n signal?: AbortSignal;\n onAbort?: () => void;\n}\n\n/**\n * A Promise-based semaphore for limiting concurrent operations.\n * Uses an O(1) ring buffer for the waiting queue.\n */\nexport class Semaphore {\n private permits: number;\n private readonly maxPermits: number;\n private readonly waitingQueue: RingBuffer<Waiter>;\n\n /**\n * Create a new semaphore.\n *\n * @param maxPermits - Maximum number of concurrent permits\n * @param maxQueue - Maximum queue size (defaults to 1000)\n */\n constructor(maxPermits: number, maxQueue = 1000) {\n this.maxPermits = maxPermits;\n this.permits = maxPermits;\n this.waitingQueue = new RingBuffer<Waiter>(maxQueue);\n }\n\n /**\n * Try to acquire a permit without waiting.\n *\n * @returns true if a permit was acquired, false otherwise\n */\n tryAcquire(): boolean {\n if (this.permits > 0) {\n this.permits--;\n return true;\n }\n return false;\n }\n\n /**\n * Acquire a permit, waiting if necessary.\n *\n * @param signal - Optional AbortSignal for cancellation\n * @returns Promise that resolves when permit is acquired\n * @throws {DOMException} When cancelled via signal (AbortError)\n */\n acquire(signal?: AbortSignal): Promise<void> {\n // Check if cancelled\n if (signal?.aborted) {\n const reason = signal.reason instanceof Error\n ? signal.reason\n : new DOMException('Aborted', 'AbortError');\n return Promise.reject(reason);\n }\n\n // Try to acquire immediately\n if (this.permits > 0) {\n this.permits--;\n return Promise.resolve();\n }\n\n // Add to wait queue\n return new Promise<void>((resolve, reject) => {\n const waiter: Waiter = { resolve, reject };\n\n // Set up abort handler\n if (signal) {\n const onAbort = () => {\n if (this.waitingQueue.remove(waiter)) {\n const reason = signal.reason instanceof Error\n ? signal.reason\n : new DOMException('Aborted', 'AbortError');\n reject(reason);\n }\n };\n\n waiter.signal = signal;\n waiter.onAbort = onAbort;\n signal.addEventListener('abort', onAbort, { once: true });\n }\n\n this.waitingQueue.push(waiter);\n });\n }\n\n /**\n * Release a permit back to the semaphore.\n */\n release(): void {\n if (!this.waitingQueue.isEmpty()) {\n // Give permit to next waiter (O(1) with ring buffer)\n const waiter = this.waitingQueue.shift();\n if (waiter) {\n // Clean up abort listener to prevent memory leak\n if (waiter.signal && waiter.onAbort) {\n waiter.signal.removeEventListener('abort', waiter.onAbort);\n }\n waiter.resolve();\n }\n } else if (this.permits < this.maxPermits) {\n // Return permit to pool\n this.permits++;\n }\n }\n\n /**\n * Get the number of available permits.\n */\n availablePermits(): number {\n return this.permits;\n }\n\n /**\n * Get the number of waiters in the queue.\n */\n queueLength(): number {\n return this.waitingQueue.length;\n }\n\n /**\n * Reject all waiters with the given error.\n */\n rejectAll(error: Error): void {\n const waiters = this.waitingQueue.drain();\n for (const waiter of waiters) {\n // Clean up abort listener to prevent memory leak\n if (waiter.signal && waiter.onAbort) {\n waiter.signal.removeEventListener('abort', waiter.onAbort);\n }\n waiter.reject(error);\n }\n }\n}\n"],"mappings":"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","names":[]} | ||
| {"version":3,"sources":["../src/index.ts","../src/bulkhead.ts","../src/config.ts","../src/ring-buffer.ts","../src/semaphore.ts"],"sourcesContent":["export { Bulkhead } from './bulkhead.js';\nexport { Semaphore } from './semaphore.js';\nexport {\n bulkheadConfigSchema,\n type BulkheadConfig,\n type BulkheadConfigInput,\n type BulkheadConfigInputFull,\n parseBulkheadConfig,\n} from './config.js';\n","import {\n type Operation,\n type Pattern,\n type Resettable,\n type Closeable,\n BulkheadFullError,\n BulkheadClosedError,\n type FortifyLogger,\n noopLogger,\n NEVER_ABORTED_SIGNAL,\n} from '@fortify-ts/core';\nimport {\n type BulkheadConfig,\n type BulkheadConfigInputFull,\n parseBulkheadConfig,\n} from './config.js';\nimport { Semaphore } from './semaphore.js';\n\n/**\n * Bulkhead pattern implementation for limiting concurrent operations.\n *\n * Prevents resource exhaustion by limiting the number of concurrent executions,\n * with optional queueing for overflow requests.\n *\n * @template T - The return type of operations\n *\n * @example\n * ```typescript\n * const bulkhead = new Bulkhead<Response>({\n * maxConcurrent: 5,\n * maxQueue: 10,\n * queueTimeout: 5000,\n * onRejected: () => console.log('Request rejected'),\n * });\n *\n * const result = await bulkhead.execute(async (signal) => {\n * return fetch('/api/data', { signal });\n * });\n * ```\n */\nexport class Bulkhead<T> implements Pattern<T>, Resettable, Closeable {\n private readonly config: BulkheadConfig;\n private readonly logger: FortifyLogger;\n private readonly semaphore: Semaphore;\n private readonly queueSemaphore: Semaphore | undefined;\n private closed = false;\n\n /**\n * Create a new Bulkhead instance.\n *\n * @param config - Bulkhead configuration\n */\n constructor(config?: BulkheadConfigInputFull) {\n this.config = parseBulkheadConfig(config);\n this.logger = this.config.logger ?? noopLogger;\n // Execution semaphore: queue capacity = maxQueue (bounded by queue semaphore)\n this.semaphore = new Semaphore(\n this.config.maxConcurrent,\n Math.max(this.config.maxQueue, 1) // At least 1 for edge cases\n );\n\n // Only create queue semaphore if maxQueue > 0\n if (this.config.maxQueue > 0) {\n // Queue semaphore: only used for tryAcquire, queue never used\n this.queueSemaphore = new Semaphore(this.config.maxQueue, 1);\n }\n }\n\n /**\n * Execute an operation within the bulkhead's concurrency limits.\n *\n * @param operation - The async operation to execute\n * @param signal - Optional AbortSignal for cancellation\n * @returns Promise resolving to the operation result\n * @throws {BulkheadFullError} When bulkhead is at capacity and queue is full\n * @throws {DOMException} When cancelled via signal (AbortError)\n */\n async execute(operation: Operation<T>, signal?: AbortSignal): Promise<T> {\n // Check if closed - use specific closed error, not full error\n if (this.closed) {\n throw new BulkheadClosedError('Bulkhead is closed');\n }\n\n // Check if cancelled\n if (signal?.aborted) {\n throw signal.reason ?? new DOMException('Aborted', 'AbortError');\n }\n\n // Try to acquire semaphore immediately\n if (this.semaphore.tryAcquire()) {\n return this.executeWithPermit(operation, signal);\n }\n\n // Bulkhead full, try to queue\n return this.enqueue(operation, signal);\n }\n\n /**\n * Get the number of currently active executions.\n */\n activeCount(): number {\n return this.config.maxConcurrent - this.semaphore.availablePermits();\n }\n\n /**\n * Get the number of requests currently waiting in the queue.\n */\n queuedCount(): number {\n return this.semaphore.queueLength();\n }\n\n /**\n * Close the bulkhead, rejecting all pending requests.\n *\n * @returns Promise that resolves immediately (sync cleanup)\n */\n close(): Promise<void> {\n if (this.closed) {\n return Promise.resolve();\n }\n\n this.closed = true;\n const closeError = new BulkheadClosedError('Bulkhead closed');\n this.semaphore.rejectAll(closeError);\n this.queueSemaphore?.rejectAll(closeError);\n this.logger.info('Bulkhead closed');\n return Promise.resolve();\n }\n\n /**\n * Reset the bulkhead to accept new requests.\n */\n reset(): void {\n this.closed = false;\n this.logger.info('Bulkhead reset');\n }\n\n /**\n * Execute operation with semaphore permit held.\n */\n private async executeWithPermit(\n operation: Operation<T>,\n signal?: AbortSignal\n ): Promise<T> {\n try {\n return await operation(signal ?? NEVER_ABORTED_SIGNAL);\n } finally {\n this.semaphore.release();\n }\n }\n\n /**\n * Attempt to queue the request when bulkhead is full.\n */\n private async enqueue(\n operation: Operation<T>,\n signal?: AbortSignal\n ): Promise<T> {\n // If no queue configured, reject immediately\n if (this.config.maxQueue === 0) {\n this.onRejected();\n throw this.createFullError('Bulkhead is full - no queue configured');\n }\n\n // Try to acquire queue slot\n if (!this.queueSemaphore?.tryAcquire()) {\n // Queue is full, reject\n this.onRejected();\n throw this.createFullError('Bulkhead queue is full');\n }\n\n // Track timeout for cleanup in finally block\n let timeoutId: ReturnType<typeof setTimeout> | undefined;\n\n try {\n // Create combined signal for queue timeout\n let timeoutController: AbortController | undefined;\n let combinedSignal = signal;\n\n if (this.config.queueTimeout > 0) {\n timeoutController = new AbortController();\n\n // Create combined signal\n if (signal) {\n combinedSignal = AbortSignal.any([signal, timeoutController.signal]);\n } else {\n combinedSignal = timeoutController.signal;\n }\n\n // Start timeout with proper cleanup\n timeoutId = setTimeout(() => {\n timeoutController?.abort(new DOMException('Queue timeout', 'TimeoutError'));\n }, this.config.queueTimeout);\n }\n\n // Wait for execution semaphore\n try {\n await this.semaphore.acquire(combinedSignal);\n } catch (error) {\n // If aborted due to queue timeout, call onRejected\n if (\n error instanceof DOMException &&\n error.name === 'TimeoutError'\n ) {\n this.onRejected();\n }\n throw error;\n }\n\n // Got permit, execute\n return await this.executeWithPermit(operation, signal);\n } finally {\n // Clear timeout to prevent memory leak\n if (timeoutId) {\n clearTimeout(timeoutId);\n }\n this.queueSemaphore.release();\n }\n }\n\n /**\n * Handle rejection event.\n */\n private onRejected(): void {\n this.logger.warn('Bulkhead rejection', {\n maxConcurrent: this.config.maxConcurrent,\n maxQueue: this.config.maxQueue,\n });\n\n if (this.config.onRejected) {\n try {\n this.config.onRejected();\n } catch (error) {\n this.logger.error('onRejected callback threw an error', {\n error: error instanceof Error ? error.message : String(error),\n });\n }\n }\n }\n\n /**\n * Create a BulkheadFullError with current state context.\n */\n private createFullError(message?: string): BulkheadFullError {\n return new BulkheadFullError(\n message ?? 'Bulkhead is full',\n this.activeCount(),\n this.queuedCount()\n );\n }\n}\n","import { z } from 'zod';\nimport { type FortifyLogger } from '@fortify-ts/core';\n\n/** Maximum concurrent operations to prevent resource exhaustion */\nconst MAX_CONCURRENT = 10_000;\n\n/** Maximum queue size to prevent memory exhaustion */\nconst MAX_QUEUE_SIZE = 100_000;\n\n/** Maximum queue timeout in milliseconds (1 hour) */\nconst MAX_QUEUE_TIMEOUT_MS = 3_600_000;\n\n/**\n * Zod schema for Bulkhead configuration.\n */\nexport const bulkheadConfigSchema = z.object({\n /** Maximum number of concurrent executions allowed (default: 10, max: 10000) */\n maxConcurrent: z.number().int().positive().max(MAX_CONCURRENT).default(10),\n /** Maximum size of overflow queue, 0 means no queue (default: 0, max: 100000) */\n maxQueue: z.number().int().nonnegative().max(MAX_QUEUE_SIZE).default(0),\n /** Maximum time a request can wait in queue in milliseconds, 0 means no timeout (default: 0, max: 1 hour) */\n queueTimeout: z.number().int().nonnegative().max(MAX_QUEUE_TIMEOUT_MS).default(0),\n});\n\n/**\n * Raw config input type (before defaults are applied).\n */\nexport type BulkheadConfigInput = z.input<typeof bulkheadConfigSchema>;\n\n/**\n * Parsed config type (after defaults are applied).\n */\nexport type BulkheadConfigParsed = z.output<typeof bulkheadConfigSchema>;\n\n/**\n * Full configuration type including callbacks and logger.\n */\nexport interface BulkheadConfig extends BulkheadConfigParsed {\n /** Callback when a request is rejected */\n onRejected: (() => void) | undefined;\n /** Logger instance for structured logging */\n logger: FortifyLogger | undefined;\n}\n\n/**\n * Input config type for constructor.\n */\nexport interface BulkheadConfigInputFull extends BulkheadConfigInput {\n onRejected?: () => void;\n logger?: FortifyLogger;\n}\n\n/**\n * Parse and validate bulkhead configuration.\n *\n * @param config - Raw configuration input\n * @returns Validated configuration with defaults applied\n */\nexport function parseBulkheadConfig(config?: BulkheadConfigInputFull): BulkheadConfig {\n const parsed = bulkheadConfigSchema.parse(config ?? {});\n return {\n ...parsed,\n onRejected: config?.onRejected,\n logger: config?.logger,\n };\n}\n","/**\n * O(1) ring buffer queue implementation.\n *\n * Provides O(1) enqueue and dequeue operations by using a circular buffer\n * with head and tail pointers.\n *\n * @template T - The type of elements in the buffer\n */\nexport class RingBuffer<T> {\n private readonly buffer: (T | undefined)[];\n private head = 0;\n private tail = 0;\n private count = 0;\n\n /**\n * Create a new ring buffer.\n *\n * @param capacity - Maximum number of elements the buffer can hold\n */\n constructor(private readonly capacity: number) {\n this.buffer = new Array<T | undefined>(capacity);\n }\n\n /**\n * Add an element to the end of the buffer.\n *\n * @param item - The item to add\n * @returns true if added, false if buffer is full\n */\n push(item: T): boolean {\n if (this.count >= this.capacity) {\n return false;\n }\n\n this.buffer[this.tail] = item;\n this.tail = (this.tail + 1) % this.capacity;\n this.count++;\n return true;\n }\n\n /**\n * Remove and return the element at the front of the buffer.\n *\n * @returns The item at the front, or undefined if empty\n */\n shift(): T | undefined {\n if (this.count === 0) {\n return undefined;\n }\n\n const item = this.buffer[this.head];\n this.buffer[this.head] = undefined; // Help GC\n this.head = (this.head + 1) % this.capacity;\n this.count--;\n return item;\n }\n\n /**\n * Get the number of elements in the buffer.\n */\n get length(): number {\n return this.count;\n }\n\n /**\n * Check if the buffer is empty.\n */\n isEmpty(): boolean {\n return this.count === 0;\n }\n\n /**\n * Check if the buffer is full.\n */\n isFull(): boolean {\n return this.count >= this.capacity;\n }\n\n /**\n * Find the index of an item in the buffer.\n * Note: This is O(n) but should be rare (used for abort handling).\n *\n * @param item - The item to find\n * @returns The index of the item, or -1 if not found\n */\n indexOf(item: T): number {\n for (let i = 0; i < this.count; i++) {\n const idx = (this.head + i) % this.capacity;\n if (this.buffer[idx] === item) {\n return i;\n }\n }\n return -1;\n }\n\n /**\n * Remove an item at the given logical index.\n * Note: This is O(n) due to shifting elements.\n *\n * @param index - The logical index (0 = front of queue)\n * @returns true if item was removed\n */\n removeAt(index: number): boolean {\n if (index < 0 || index >= this.count) {\n return false;\n }\n\n // Shift elements to fill the gap\n for (let i = index; i < this.count - 1; i++) {\n const fromIdx = (this.head + i + 1) % this.capacity;\n const toIdx = (this.head + i) % this.capacity;\n this.buffer[toIdx] = this.buffer[fromIdx];\n }\n\n // Clear the last element\n const lastIdx = (this.head + this.count - 1) % this.capacity;\n this.buffer[lastIdx] = undefined;\n\n // Update tail\n this.tail = (this.tail - 1 + this.capacity) % this.capacity;\n this.count--;\n return true;\n }\n\n /**\n * Remove an item from the buffer.\n * Note: This is O(n) but should be rare (used for abort handling).\n *\n * @param item - The item to remove\n * @returns true if item was found and removed\n */\n remove(item: T): boolean {\n const index = this.indexOf(item);\n if (index === -1) {\n return false;\n }\n return this.removeAt(index);\n }\n\n /**\n * Get all items and clear the buffer.\n *\n * @returns Array of all items in queue order\n */\n drain(): T[] {\n const items: T[] = [];\n for (let i = 0; i < this.count; i++) {\n const idx = (this.head + i) % this.capacity;\n const item = this.buffer[idx];\n if (item !== undefined) {\n items.push(item);\n }\n this.buffer[idx] = undefined; // Help GC\n }\n this.head = 0;\n this.tail = 0;\n this.count = 0;\n return items;\n }\n\n /**\n * Clear all elements from the buffer.\n */\n clear(): void {\n for (let i = 0; i < this.count; i++) {\n const idx = (this.head + i) % this.capacity;\n this.buffer[idx] = undefined;\n }\n this.head = 0;\n this.tail = 0;\n this.count = 0;\n }\n}\n","import { RingBuffer } from './ring-buffer.js';\n\n/**\n * Waiter in the semaphore queue.\n */\ninterface Waiter {\n resolve: () => void;\n reject: (error: Error) => void;\n signal?: AbortSignal;\n onAbort?: () => void;\n}\n\n/**\n * A Promise-based semaphore for limiting concurrent operations.\n * Uses an O(1) ring buffer for the waiting queue.\n */\nexport class Semaphore {\n private permits: number;\n private readonly maxPermits: number;\n private readonly waitingQueue: RingBuffer<Waiter>;\n\n /**\n * Create a new semaphore.\n *\n * @param maxPermits - Maximum number of concurrent permits\n * @param maxQueue - Maximum queue size (defaults to 1000)\n */\n constructor(maxPermits: number, maxQueue = 1000) {\n this.maxPermits = maxPermits;\n this.permits = maxPermits;\n this.waitingQueue = new RingBuffer<Waiter>(maxQueue);\n }\n\n /**\n * Try to acquire a permit without waiting.\n *\n * @returns true if a permit was acquired, false otherwise\n */\n tryAcquire(): boolean {\n if (this.permits > 0) {\n this.permits--;\n return true;\n }\n return false;\n }\n\n /**\n * Acquire a permit, waiting if necessary.\n *\n * @param signal - Optional AbortSignal for cancellation\n * @returns Promise that resolves when permit is acquired\n * @throws {DOMException} When cancelled via signal (AbortError)\n */\n acquire(signal?: AbortSignal): Promise<void> {\n // Check if cancelled\n if (signal?.aborted) {\n const reason = signal.reason instanceof Error\n ? signal.reason\n : new DOMException('Aborted', 'AbortError');\n return Promise.reject(reason);\n }\n\n // Try to acquire immediately\n if (this.permits > 0) {\n this.permits--;\n return Promise.resolve();\n }\n\n // Add to wait queue\n return new Promise<void>((resolve, reject) => {\n const waiter: Waiter = { resolve, reject };\n\n // Set up abort handler\n if (signal) {\n const onAbort = () => {\n if (this.waitingQueue.remove(waiter)) {\n const reason = signal.reason instanceof Error\n ? signal.reason\n : new DOMException('Aborted', 'AbortError');\n reject(reason);\n }\n };\n\n waiter.signal = signal;\n waiter.onAbort = onAbort;\n signal.addEventListener('abort', onAbort, { once: true });\n }\n\n this.waitingQueue.push(waiter);\n });\n }\n\n /**\n * Release a permit back to the semaphore.\n */\n release(): void {\n if (!this.waitingQueue.isEmpty()) {\n // Give permit to next waiter (O(1) with ring buffer)\n const waiter = this.waitingQueue.shift();\n if (waiter) {\n // Clean up abort listener to prevent memory leak\n if (waiter.signal && waiter.onAbort) {\n waiter.signal.removeEventListener('abort', waiter.onAbort);\n }\n waiter.resolve();\n }\n } else if (this.permits < this.maxPermits) {\n // Return permit to pool\n this.permits++;\n }\n }\n\n /**\n * Get the number of available permits.\n */\n availablePermits(): number {\n return this.permits;\n }\n\n /**\n * Get the number of waiters in the queue.\n */\n queueLength(): number {\n return this.waitingQueue.length;\n }\n\n /**\n * Reject all waiters with the given error.\n */\n rejectAll(error: Error): void {\n const waiters = this.waitingQueue.drain();\n for (const waiter of waiters) {\n // Clean up abort listener to prevent memory leak\n if (waiter.signal && waiter.onAbort) {\n waiter.signal.removeEventListener('abort', waiter.onAbort);\n }\n waiter.reject(error);\n }\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACAA,kBAUO;;;ACVP,iBAAkB;AAIlB,IAAM,iBAAiB;AAGvB,IAAM,iBAAiB;AAGvB,IAAM,uBAAuB;AAKtB,IAAM,uBAAuB,aAAE,OAAO;AAAA;AAAA,EAE3C,eAAe,aAAE,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,cAAc,EAAE,QAAQ,EAAE;AAAA;AAAA,EAEzE,UAAU,aAAE,OAAO,EAAE,IAAI,EAAE,YAAY,EAAE,IAAI,cAAc,EAAE,QAAQ,CAAC;AAAA;AAAA,EAEtE,cAAc,aAAE,OAAO,EAAE,IAAI,EAAE,YAAY,EAAE,IAAI,oBAAoB,EAAE,QAAQ,CAAC;AAClF,CAAC;AAoCM,SAAS,oBAAoB,QAAkD;AACpF,QAAM,SAAS,qBAAqB,MAAM,UAAU,CAAC,CAAC;AACtD,SAAO;AAAA,IACL,GAAG;AAAA,IACH,YAAY,QAAQ;AAAA,IACpB,QAAQ,QAAQ;AAAA,EAClB;AACF;;;ACzDO,IAAM,aAAN,MAAoB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAWzB,YAA6B,UAAkB;AAAlB;AAC3B,SAAK,SAAS,IAAI,MAAqB,QAAQ;AAAA,EACjD;AAAA,EAZiB;AAAA,EACT,OAAO;AAAA,EACP,OAAO;AAAA,EACP,QAAQ;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAiBhB,KAAK,MAAkB;AACrB,QAAI,KAAK,SAAS,KAAK,UAAU;AAC/B,aAAO;AAAA,IACT;AAEA,SAAK,OAAO,KAAK,IAAI,IAAI;AACzB,SAAK,QAAQ,KAAK,OAAO,KAAK,KAAK;AACnC,SAAK;AACL,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,QAAuB;AACrB,QAAI,KAAK,UAAU,GAAG;AACpB,aAAO;AAAA,IACT;AAEA,UAAM,OAAO,KAAK,OAAO,KAAK,IAAI;AAClC,SAAK,OAAO,KAAK,IAAI,IAAI;AACzB,SAAK,QAAQ,KAAK,OAAO,KAAK,KAAK;AACnC,SAAK;AACL,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA,EAKA,IAAI,SAAiB;AACnB,WAAO,KAAK;AAAA,EACd;AAAA;AAAA;AAAA;AAAA,EAKA,UAAmB;AACjB,WAAO,KAAK,UAAU;AAAA,EACxB;AAAA;AAAA;AAAA;AAAA,EAKA,SAAkB;AAChB,WAAO,KAAK,SAAS,KAAK;AAAA,EAC5B;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASA,QAAQ,MAAiB;AACvB,aAAS,IAAI,GAAG,IAAI,KAAK,OAAO,KAAK;AACnC,YAAM,OAAO,KAAK,OAAO,KAAK,KAAK;AACnC,UAAI,KAAK,OAAO,GAAG,MAAM,MAAM;AAC7B,eAAO;AAAA,MACT;AAAA,IACF;AACA,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASA,SAAS,OAAwB;AAC/B,QAAI,QAAQ,KAAK,SAAS,KAAK,OAAO;AACpC,aAAO;AAAA,IACT;AAGA,aAAS,IAAI,OAAO,IAAI,KAAK,QAAQ,GAAG,KAAK;AAC3C,YAAM,WAAW,KAAK,OAAO,IAAI,KAAK,KAAK;AAC3C,YAAM,SAAS,KAAK,OAAO,KAAK,KAAK;AACrC,WAAK,OAAO,KAAK,IAAI,KAAK,OAAO,OAAO;AAAA,IAC1C;AAGA,UAAM,WAAW,KAAK,OAAO,KAAK,QAAQ,KAAK,KAAK;AACpD,SAAK,OAAO,OAAO,IAAI;AAGvB,SAAK,QAAQ,KAAK,OAAO,IAAI,KAAK,YAAY,KAAK;AACnD,SAAK;AACL,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASA,OAAO,MAAkB;AACvB,UAAM,QAAQ,KAAK,QAAQ,IAAI;AAC/B,QAAI,UAAU,IAAI;AAChB,aAAO;AAAA,IACT;AACA,WAAO,KAAK,SAAS,KAAK;AAAA,EAC5B;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,QAAa;AACX,UAAM,QAAa,CAAC;AACpB,aAAS,IAAI,GAAG,IAAI,KAAK,OAAO,KAAK;AACnC,YAAM,OAAO,KAAK,OAAO,KAAK,KAAK;AACnC,YAAM,OAAO,KAAK,OAAO,GAAG;AAC5B,UAAI,SAAS,QAAW;AACtB,cAAM,KAAK,IAAI;AAAA,MACjB;AACA,WAAK,OAAO,GAAG,IAAI;AAAA,IACrB;AACA,SAAK,OAAO;AACZ,SAAK,OAAO;AACZ,SAAK,QAAQ;AACb,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA,EAKA,QAAc;AACZ,aAAS,IAAI,GAAG,IAAI,KAAK,OAAO,KAAK;AACnC,YAAM,OAAO,KAAK,OAAO,KAAK,KAAK;AACnC,WAAK,OAAO,GAAG,IAAI;AAAA,IACrB;AACA,SAAK,OAAO;AACZ,SAAK,OAAO;AACZ,SAAK,QAAQ;AAAA,EACf;AACF;;;AC5JO,IAAM,YAAN,MAAgB;AAAA,EACb;AAAA,EACS;AAAA,EACA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQjB,YAAY,YAAoB,WAAW,KAAM;AAC/C,SAAK,aAAa;AAClB,SAAK,UAAU;AACf,SAAK,eAAe,IAAI,WAAmB,QAAQ;AAAA,EACrD;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,aAAsB;AACpB,QAAI,KAAK,UAAU,GAAG;AACpB,WAAK;AACL,aAAO;AAAA,IACT;AACA,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASA,QAAQ,QAAqC;AAE3C,QAAI,QAAQ,SAAS;AACnB,YAAM,SAAS,OAAO,kBAAkB,QACpC,OAAO,SACP,IAAI,aAAa,WAAW,YAAY;AAC5C,aAAO,QAAQ,OAAO,MAAM;AAAA,IAC9B;AAGA,QAAI,KAAK,UAAU,GAAG;AACpB,WAAK;AACL,aAAO,QAAQ,QAAQ;AAAA,IACzB;AAGA,WAAO,IAAI,QAAc,CAAC,SAAS,WAAW;AAC5C,YAAM,SAAiB,EAAE,SAAS,OAAO;AAGzC,UAAI,QAAQ;AACV,cAAM,UAAU,MAAM;AACpB,cAAI,KAAK,aAAa,OAAO,MAAM,GAAG;AACpC,kBAAM,SAAS,OAAO,kBAAkB,QACpC,OAAO,SACP,IAAI,aAAa,WAAW,YAAY;AAC5C,mBAAO,MAAM;AAAA,UACf;AAAA,QACF;AAEA,eAAO,SAAS;AAChB,eAAO,UAAU;AACjB,eAAO,iBAAiB,SAAS,SAAS,EAAE,MAAM,KAAK,CAAC;AAAA,MAC1D;AAEA,WAAK,aAAa,KAAK,MAAM;AAAA,IAC/B,CAAC;AAAA,EACH;AAAA;AAAA;AAAA;AAAA,EAKA,UAAgB;AACd,QAAI,CAAC,KAAK,aAAa,QAAQ,GAAG;AAEhC,YAAM,SAAS,KAAK,aAAa,MAAM;AACvC,UAAI,QAAQ;AAEV,YAAI,OAAO,UAAU,OAAO,SAAS;AACnC,iBAAO,OAAO,oBAAoB,SAAS,OAAO,OAAO;AAAA,QAC3D;AACA,eAAO,QAAQ;AAAA,MACjB;AAAA,IACF,WAAW,KAAK,UAAU,KAAK,YAAY;AAEzC,WAAK;AAAA,IACP;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,mBAA2B;AACzB,WAAO,KAAK;AAAA,EACd;AAAA;AAAA;AAAA;AAAA,EAKA,cAAsB;AACpB,WAAO,KAAK,aAAa;AAAA,EAC3B;AAAA;AAAA;AAAA;AAAA,EAKA,UAAU,OAAoB;AAC5B,UAAM,UAAU,KAAK,aAAa,MAAM;AACxC,eAAW,UAAU,SAAS;AAE5B,UAAI,OAAO,UAAU,OAAO,SAAS;AACnC,eAAO,OAAO,oBAAoB,SAAS,OAAO,OAAO;AAAA,MAC3D;AACA,aAAO,OAAO,KAAK;AAAA,IACrB;AAAA,EACF;AACF;;;AHnGO,IAAM,WAAN,MAA+D;AAAA,EACnD;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACT,SAAS;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOjB,YAAY,QAAkC;AAC5C,SAAK,SAAS,oBAAoB,MAAM;AACxC,SAAK,SAAS,KAAK,OAAO,UAAU;AAEpC,SAAK,YAAY,IAAI;AAAA,MACnB,KAAK,OAAO;AAAA,MACZ,KAAK,IAAI,KAAK,OAAO,UAAU,CAAC;AAAA;AAAA,IAClC;AAGA,QAAI,KAAK,OAAO,WAAW,GAAG;AAE5B,WAAK,iBAAiB,IAAI,UAAU,KAAK,OAAO,UAAU,CAAC;AAAA,IAC7D;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAWA,MAAM,QAAQ,WAAyB,QAAkC;AAEvE,QAAI,KAAK,QAAQ;AACf,YAAM,IAAI,gCAAoB,oBAAoB;AAAA,IACpD;AAGA,QAAI,QAAQ,SAAS;AACnB,YAAM,OAAO,UAAU,IAAI,aAAa,WAAW,YAAY;AAAA,IACjE;AAGA,QAAI,KAAK,UAAU,WAAW,GAAG;AAC/B,aAAO,KAAK,kBAAkB,WAAW,MAAM;AAAA,IACjD;AAGA,WAAO,KAAK,QAAQ,WAAW,MAAM;AAAA,EACvC;AAAA;AAAA;AAAA;AAAA,EAKA,cAAsB;AACpB,WAAO,KAAK,OAAO,gBAAgB,KAAK,UAAU,iBAAiB;AAAA,EACrE;AAAA;AAAA;AAAA;AAAA,EAKA,cAAsB;AACpB,WAAO,KAAK,UAAU,YAAY;AAAA,EACpC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,QAAuB;AACrB,QAAI,KAAK,QAAQ;AACf,aAAO,QAAQ,QAAQ;AAAA,IACzB;AAEA,SAAK,SAAS;AACd,UAAM,aAAa,IAAI,gCAAoB,iBAAiB;AAC5D,SAAK,UAAU,UAAU,UAAU;AACnC,SAAK,gBAAgB,UAAU,UAAU;AACzC,SAAK,OAAO,KAAK,iBAAiB;AAClC,WAAO,QAAQ,QAAQ;AAAA,EACzB;AAAA;AAAA;AAAA;AAAA,EAKA,QAAc;AACZ,SAAK,SAAS;AACd,SAAK,OAAO,KAAK,gBAAgB;AAAA,EACnC;AAAA;AAAA;AAAA;AAAA,EAKA,MAAc,kBACZ,WACA,QACY;AACZ,QAAI;AACF,aAAO,MAAM,UAAU,UAAU,gCAAoB;AAAA,IACvD,UAAE;AACA,WAAK,UAAU,QAAQ;AAAA,IACzB;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,MAAc,QACZ,WACA,QACY;AAEZ,QAAI,KAAK,OAAO,aAAa,GAAG;AAC9B,WAAK,WAAW;AAChB,YAAM,KAAK,gBAAgB,wCAAwC;AAAA,IACrE;AAGA,QAAI,CAAC,KAAK,gBAAgB,WAAW,GAAG;AAEtC,WAAK,WAAW;AAChB,YAAM,KAAK,gBAAgB,wBAAwB;AAAA,IACrD;AAGA,QAAI;AAEJ,QAAI;AAEF,UAAI;AACJ,UAAI,iBAAiB;AAErB,UAAI,KAAK,OAAO,eAAe,GAAG;AAChC,4BAAoB,IAAI,gBAAgB;AAGxC,YAAI,QAAQ;AACV,2BAAiB,YAAY,IAAI,CAAC,QAAQ,kBAAkB,MAAM,CAAC;AAAA,QACrE,OAAO;AACL,2BAAiB,kBAAkB;AAAA,QACrC;AAGA,oBAAY,WAAW,MAAM;AAC3B,6BAAmB,MAAM,IAAI,aAAa,iBAAiB,cAAc,CAAC;AAAA,QAC5E,GAAG,KAAK,OAAO,YAAY;AAAA,MAC7B;AAGA,UAAI;AACF,cAAM,KAAK,UAAU,QAAQ,cAAc;AAAA,MAC7C,SAAS,OAAO;AAEd,YACE,iBAAiB,gBACjB,MAAM,SAAS,gBACf;AACA,eAAK,WAAW;AAAA,QAClB;AACA,cAAM;AAAA,MACR;AAGA,aAAO,MAAM,KAAK,kBAAkB,WAAW,MAAM;AAAA,IACvD,UAAE;AAEA,UAAI,WAAW;AACb,qBAAa,SAAS;AAAA,MACxB;AACA,WAAK,eAAe,QAAQ;AAAA,IAC9B;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKQ,aAAmB;AACzB,SAAK,OAAO,KAAK,sBAAsB;AAAA,MACrC,eAAe,KAAK,OAAO;AAAA,MAC3B,UAAU,KAAK,OAAO;AAAA,IACxB,CAAC;AAED,QAAI,KAAK,OAAO,YAAY;AAC1B,UAAI;AACF,aAAK,OAAO,WAAW;AAAA,MACzB,SAAS,OAAO;AACd,aAAK,OAAO,MAAM,sCAAsC;AAAA,UACtD,OAAO,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;AAAA,QAC9D,CAAC;AAAA,MACH;AAAA,IACF;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKQ,gBAAgB,SAAqC;AAC3D,WAAO,IAAI;AAAA,MACT,WAAW;AAAA,MACX,KAAK,YAAY;AAAA,MACjB,KAAK,YAAY;AAAA,IACnB;AAAA,EACF;AACF;","names":[]} |
+5
-3
@@ -1,2 +0,2 @@ | ||
| import { FortifyLogger, Pattern, Resettable, Operation } from '@fortify-ts/core'; | ||
| import { FortifyLogger, Pattern, Resettable, Closeable, Operation } from '@fortify-ts/core'; | ||
| import { z } from 'zod'; | ||
@@ -66,3 +66,3 @@ | ||
| */ | ||
| declare class Bulkhead<T> implements Pattern<T>, Resettable { | ||
| declare class Bulkhead<T> implements Pattern<T>, Resettable, Closeable { | ||
| private readonly config; | ||
@@ -99,4 +99,6 @@ private readonly logger; | ||
| * Close the bulkhead, rejecting all pending requests. | ||
| * | ||
| * @returns Promise that resolves immediately (sync cleanup) | ||
| */ | ||
| close(): void; | ||
| close(): Promise<void>; | ||
| /** | ||
@@ -103,0 +105,0 @@ * Reset the bulkhead to accept new requests. |
+5
-3
@@ -1,2 +0,2 @@ | ||
| import { FortifyLogger, Pattern, Resettable, Operation } from '@fortify-ts/core'; | ||
| import { FortifyLogger, Pattern, Resettable, Closeable, Operation } from '@fortify-ts/core'; | ||
| import { z } from 'zod'; | ||
@@ -66,3 +66,3 @@ | ||
| */ | ||
| declare class Bulkhead<T> implements Pattern<T>, Resettable { | ||
| declare class Bulkhead<T> implements Pattern<T>, Resettable, Closeable { | ||
| private readonly config; | ||
@@ -99,4 +99,6 @@ private readonly logger; | ||
| * Close the bulkhead, rejecting all pending requests. | ||
| * | ||
| * @returns Promise that resolves immediately (sync cleanup) | ||
| */ | ||
| close(): void; | ||
| close(): Promise<void>; | ||
| /** | ||
@@ -103,0 +105,0 @@ * Reset the bulkhead to accept new requests. |
+33
-27
@@ -1,9 +0,7 @@ | ||
| var __defProp = Object.defineProperty; | ||
| var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; | ||
| var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value); | ||
| // src/bulkhead.ts | ||
| import { | ||
| BulkheadFullError, | ||
| noopLogger | ||
| BulkheadClosedError, | ||
| noopLogger, | ||
| NEVER_ABORTED_SIGNAL | ||
| } from "@fortify-ts/core"; | ||
@@ -13,9 +11,12 @@ | ||
| import { z } from "zod"; | ||
| var MAX_CONCURRENT = 1e4; | ||
| var MAX_QUEUE_SIZE = 1e5; | ||
| var MAX_QUEUE_TIMEOUT_MS = 36e5; | ||
| var bulkheadConfigSchema = z.object({ | ||
| /** Maximum number of concurrent executions allowed (default: 10) */ | ||
| maxConcurrent: z.number().int().positive().default(10), | ||
| /** Maximum size of overflow queue, 0 means no queue (default: 0) */ | ||
| maxQueue: z.number().int().nonnegative().default(0), | ||
| /** Maximum time a request can wait in queue in milliseconds, 0 means no timeout (default: 0) */ | ||
| queueTimeout: z.number().int().nonnegative().default(0) | ||
| /** Maximum number of concurrent executions allowed (default: 10, max: 10000) */ | ||
| maxConcurrent: z.number().int().positive().max(MAX_CONCURRENT).default(10), | ||
| /** Maximum size of overflow queue, 0 means no queue (default: 0, max: 100000) */ | ||
| maxQueue: z.number().int().nonnegative().max(MAX_QUEUE_SIZE).default(0), | ||
| /** Maximum time a request can wait in queue in milliseconds, 0 means no timeout (default: 0, max: 1 hour) */ | ||
| queueTimeout: z.number().int().nonnegative().max(MAX_QUEUE_TIMEOUT_MS).default(0) | ||
| }); | ||
@@ -40,8 +41,8 @@ function parseBulkheadConfig(config) { | ||
| this.capacity = capacity; | ||
| __publicField(this, "buffer"); | ||
| __publicField(this, "head", 0); | ||
| __publicField(this, "tail", 0); | ||
| __publicField(this, "count", 0); | ||
| this.buffer = new Array(capacity); | ||
| } | ||
| buffer; | ||
| head = 0; | ||
| tail = 0; | ||
| count = 0; | ||
| /** | ||
@@ -183,2 +184,5 @@ * Add an element to the end of the buffer. | ||
| var Semaphore = class { | ||
| permits; | ||
| maxPermits; | ||
| waitingQueue; | ||
| /** | ||
@@ -191,5 +195,2 @@ * Create a new semaphore. | ||
| constructor(maxPermits, maxQueue = 1e3) { | ||
| __publicField(this, "permits"); | ||
| __publicField(this, "maxPermits"); | ||
| __publicField(this, "waitingQueue"); | ||
| this.maxPermits = maxPermits; | ||
@@ -287,2 +288,7 @@ this.permits = maxPermits; | ||
| var Bulkhead = class { | ||
| config; | ||
| logger; | ||
| semaphore; | ||
| queueSemaphore; | ||
| closed = false; | ||
| /** | ||
@@ -294,7 +300,2 @@ * Create a new Bulkhead instance. | ||
| constructor(config) { | ||
| __publicField(this, "config"); | ||
| __publicField(this, "logger"); | ||
| __publicField(this, "semaphore"); | ||
| __publicField(this, "queueSemaphore"); | ||
| __publicField(this, "closed", false); | ||
| this.config = parseBulkheadConfig(config); | ||
@@ -322,3 +323,3 @@ this.logger = this.config.logger ?? noopLogger; | ||
| if (this.closed) { | ||
| throw this.createFullError("Bulkhead is closed"); | ||
| throw new BulkheadClosedError("Bulkhead is closed"); | ||
| } | ||
@@ -347,10 +348,15 @@ if (signal?.aborted) { | ||
| * Close the bulkhead, rejecting all pending requests. | ||
| * | ||
| * @returns Promise that resolves immediately (sync cleanup) | ||
| */ | ||
| close() { | ||
| if (this.closed) return; | ||
| if (this.closed) { | ||
| return Promise.resolve(); | ||
| } | ||
| this.closed = true; | ||
| const closeError = this.createFullError("Bulkhead closed"); | ||
| const closeError = new BulkheadClosedError("Bulkhead closed"); | ||
| this.semaphore.rejectAll(closeError); | ||
| this.queueSemaphore?.rejectAll(closeError); | ||
| this.logger.info("Bulkhead closed"); | ||
| return Promise.resolve(); | ||
| } | ||
@@ -369,3 +375,3 @@ /** | ||
| try { | ||
| return await operation(signal ?? new AbortController().signal); | ||
| return await operation(signal ?? NEVER_ABORTED_SIGNAL); | ||
| } finally { | ||
@@ -372,0 +378,0 @@ this.semaphore.release(); |
@@ -1,1 +0,1 @@ | ||
| {"version":3,"sources":["../src/bulkhead.ts","../src/config.ts","../src/ring-buffer.ts","../src/semaphore.ts"],"sourcesContent":["import {\n type Operation,\n type Pattern,\n type Resettable,\n BulkheadFullError,\n type FortifyLogger,\n noopLogger,\n} from '@fortify-ts/core';\nimport {\n type BulkheadConfig,\n type BulkheadConfigInputFull,\n parseBulkheadConfig,\n} from './config.js';\nimport { Semaphore } from './semaphore.js';\n\n/**\n * Bulkhead pattern implementation for limiting concurrent operations.\n *\n * Prevents resource exhaustion by limiting the number of concurrent executions,\n * with optional queueing for overflow requests.\n *\n * @template T - The return type of operations\n *\n * @example\n * ```typescript\n * const bulkhead = new Bulkhead<Response>({\n * maxConcurrent: 5,\n * maxQueue: 10,\n * queueTimeout: 5000,\n * onRejected: () => console.log('Request rejected'),\n * });\n *\n * const result = await bulkhead.execute(async (signal) => {\n * return fetch('/api/data', { signal });\n * });\n * ```\n */\nexport class Bulkhead<T> implements Pattern<T>, Resettable {\n private readonly config: BulkheadConfig;\n private readonly logger: FortifyLogger;\n private readonly semaphore: Semaphore;\n private readonly queueSemaphore: Semaphore | undefined;\n private closed = false;\n\n /**\n * Create a new Bulkhead instance.\n *\n * @param config - Bulkhead configuration\n */\n constructor(config?: BulkheadConfigInputFull) {\n this.config = parseBulkheadConfig(config);\n this.logger = this.config.logger ?? noopLogger;\n // Execution semaphore: queue capacity = maxQueue (bounded by queue semaphore)\n this.semaphore = new Semaphore(\n this.config.maxConcurrent,\n Math.max(this.config.maxQueue, 1) // At least 1 for edge cases\n );\n\n // Only create queue semaphore if maxQueue > 0\n if (this.config.maxQueue > 0) {\n // Queue semaphore: only used for tryAcquire, queue never used\n this.queueSemaphore = new Semaphore(this.config.maxQueue, 1);\n }\n }\n\n /**\n * Execute an operation within the bulkhead's concurrency limits.\n *\n * @param operation - The async operation to execute\n * @param signal - Optional AbortSignal for cancellation\n * @returns Promise resolving to the operation result\n * @throws {BulkheadFullError} When bulkhead is at capacity and queue is full\n * @throws {DOMException} When cancelled via signal (AbortError)\n */\n async execute(operation: Operation<T>, signal?: AbortSignal): Promise<T> {\n // Check if closed\n if (this.closed) {\n throw this.createFullError('Bulkhead is closed');\n }\n\n // Check if cancelled\n if (signal?.aborted) {\n throw signal.reason ?? new DOMException('Aborted', 'AbortError');\n }\n\n // Try to acquire semaphore immediately\n if (this.semaphore.tryAcquire()) {\n return this.executeWithPermit(operation, signal);\n }\n\n // Bulkhead full, try to queue\n return this.enqueue(operation, signal);\n }\n\n /**\n * Get the number of currently active executions.\n */\n activeCount(): number {\n return this.config.maxConcurrent - this.semaphore.availablePermits();\n }\n\n /**\n * Get the number of requests currently waiting in the queue.\n */\n queuedCount(): number {\n return this.semaphore.queueLength();\n }\n\n /**\n * Close the bulkhead, rejecting all pending requests.\n */\n close(): void {\n if (this.closed) return;\n\n this.closed = true;\n const closeError = this.createFullError('Bulkhead closed');\n this.semaphore.rejectAll(closeError);\n this.queueSemaphore?.rejectAll(closeError);\n this.logger.info('Bulkhead closed');\n }\n\n /**\n * Reset the bulkhead to accept new requests.\n */\n reset(): void {\n this.closed = false;\n this.logger.info('Bulkhead reset');\n }\n\n /**\n * Execute operation with semaphore permit held.\n */\n private async executeWithPermit(\n operation: Operation<T>,\n signal?: AbortSignal\n ): Promise<T> {\n try {\n return await operation(signal ?? new AbortController().signal);\n } finally {\n this.semaphore.release();\n }\n }\n\n /**\n * Attempt to queue the request when bulkhead is full.\n */\n private async enqueue(\n operation: Operation<T>,\n signal?: AbortSignal\n ): Promise<T> {\n // If no queue configured, reject immediately\n if (this.config.maxQueue === 0) {\n this.onRejected();\n throw this.createFullError('Bulkhead is full - no queue configured');\n }\n\n // Try to acquire queue slot\n if (!this.queueSemaphore?.tryAcquire()) {\n // Queue is full, reject\n this.onRejected();\n throw this.createFullError('Bulkhead queue is full');\n }\n\n // Track timeout for cleanup in finally block\n let timeoutId: ReturnType<typeof setTimeout> | undefined;\n\n try {\n // Create combined signal for queue timeout\n let timeoutController: AbortController | undefined;\n let combinedSignal = signal;\n\n if (this.config.queueTimeout > 0) {\n timeoutController = new AbortController();\n\n // Create combined signal\n if (signal) {\n combinedSignal = AbortSignal.any([signal, timeoutController.signal]);\n } else {\n combinedSignal = timeoutController.signal;\n }\n\n // Start timeout with proper cleanup\n timeoutId = setTimeout(() => {\n timeoutController?.abort(new DOMException('Queue timeout', 'TimeoutError'));\n }, this.config.queueTimeout);\n }\n\n // Wait for execution semaphore\n try {\n await this.semaphore.acquire(combinedSignal);\n } catch (error) {\n // If aborted due to queue timeout, call onRejected\n if (\n error instanceof DOMException &&\n error.name === 'TimeoutError'\n ) {\n this.onRejected();\n }\n throw error;\n }\n\n // Got permit, execute\n return await this.executeWithPermit(operation, signal);\n } finally {\n // Clear timeout to prevent memory leak\n if (timeoutId) {\n clearTimeout(timeoutId);\n }\n this.queueSemaphore.release();\n }\n }\n\n /**\n * Handle rejection event.\n */\n private onRejected(): void {\n this.logger.warn('Bulkhead rejection', {\n maxConcurrent: this.config.maxConcurrent,\n maxQueue: this.config.maxQueue,\n });\n\n if (this.config.onRejected) {\n try {\n this.config.onRejected();\n } catch (error) {\n this.logger.error('onRejected callback threw an error', {\n error: error instanceof Error ? error.message : String(error),\n });\n }\n }\n }\n\n /**\n * Create a BulkheadFullError with current state context.\n */\n private createFullError(message?: string): BulkheadFullError {\n return new BulkheadFullError(\n message ?? 'Bulkhead is full',\n this.activeCount(),\n this.queuedCount()\n );\n }\n}\n","import { z } from 'zod';\nimport { type FortifyLogger } from '@fortify-ts/core';\n\n/**\n * Zod schema for Bulkhead configuration.\n */\nexport const bulkheadConfigSchema = z.object({\n /** Maximum number of concurrent executions allowed (default: 10) */\n maxConcurrent: z.number().int().positive().default(10),\n /** Maximum size of overflow queue, 0 means no queue (default: 0) */\n maxQueue: z.number().int().nonnegative().default(0),\n /** Maximum time a request can wait in queue in milliseconds, 0 means no timeout (default: 0) */\n queueTimeout: z.number().int().nonnegative().default(0),\n});\n\n/**\n * Raw config input type (before defaults are applied).\n */\nexport type BulkheadConfigInput = z.input<typeof bulkheadConfigSchema>;\n\n/**\n * Parsed config type (after defaults are applied).\n */\nexport type BulkheadConfigParsed = z.output<typeof bulkheadConfigSchema>;\n\n/**\n * Full configuration type including callbacks and logger.\n */\nexport interface BulkheadConfig extends BulkheadConfigParsed {\n /** Callback when a request is rejected */\n onRejected: (() => void) | undefined;\n /** Logger instance for structured logging */\n logger: FortifyLogger | undefined;\n}\n\n/**\n * Input config type for constructor.\n */\nexport interface BulkheadConfigInputFull extends BulkheadConfigInput {\n onRejected?: () => void;\n logger?: FortifyLogger;\n}\n\n/**\n * Parse and validate bulkhead configuration.\n *\n * @param config - Raw configuration input\n * @returns Validated configuration with defaults applied\n */\nexport function parseBulkheadConfig(config?: BulkheadConfigInputFull): BulkheadConfig {\n const parsed = bulkheadConfigSchema.parse(config ?? {});\n return {\n ...parsed,\n onRejected: config?.onRejected,\n logger: config?.logger,\n };\n}\n","/**\n * O(1) ring buffer queue implementation.\n *\n * Provides O(1) enqueue and dequeue operations by using a circular buffer\n * with head and tail pointers.\n *\n * @template T - The type of elements in the buffer\n */\nexport class RingBuffer<T> {\n private readonly buffer: (T | undefined)[];\n private head = 0;\n private tail = 0;\n private count = 0;\n\n /**\n * Create a new ring buffer.\n *\n * @param capacity - Maximum number of elements the buffer can hold\n */\n constructor(private readonly capacity: number) {\n this.buffer = new Array<T | undefined>(capacity);\n }\n\n /**\n * Add an element to the end of the buffer.\n *\n * @param item - The item to add\n * @returns true if added, false if buffer is full\n */\n push(item: T): boolean {\n if (this.count >= this.capacity) {\n return false;\n }\n\n this.buffer[this.tail] = item;\n this.tail = (this.tail + 1) % this.capacity;\n this.count++;\n return true;\n }\n\n /**\n * Remove and return the element at the front of the buffer.\n *\n * @returns The item at the front, or undefined if empty\n */\n shift(): T | undefined {\n if (this.count === 0) {\n return undefined;\n }\n\n const item = this.buffer[this.head];\n this.buffer[this.head] = undefined; // Help GC\n this.head = (this.head + 1) % this.capacity;\n this.count--;\n return item;\n }\n\n /**\n * Get the number of elements in the buffer.\n */\n get length(): number {\n return this.count;\n }\n\n /**\n * Check if the buffer is empty.\n */\n isEmpty(): boolean {\n return this.count === 0;\n }\n\n /**\n * Check if the buffer is full.\n */\n isFull(): boolean {\n return this.count >= this.capacity;\n }\n\n /**\n * Find the index of an item in the buffer.\n * Note: This is O(n) but should be rare (used for abort handling).\n *\n * @param item - The item to find\n * @returns The index of the item, or -1 if not found\n */\n indexOf(item: T): number {\n for (let i = 0; i < this.count; i++) {\n const idx = (this.head + i) % this.capacity;\n if (this.buffer[idx] === item) {\n return i;\n }\n }\n return -1;\n }\n\n /**\n * Remove an item at the given logical index.\n * Note: This is O(n) due to shifting elements.\n *\n * @param index - The logical index (0 = front of queue)\n * @returns true if item was removed\n */\n removeAt(index: number): boolean {\n if (index < 0 || index >= this.count) {\n return false;\n }\n\n // Shift elements to fill the gap\n for (let i = index; i < this.count - 1; i++) {\n const fromIdx = (this.head + i + 1) % this.capacity;\n const toIdx = (this.head + i) % this.capacity;\n this.buffer[toIdx] = this.buffer[fromIdx];\n }\n\n // Clear the last element\n const lastIdx = (this.head + this.count - 1) % this.capacity;\n this.buffer[lastIdx] = undefined;\n\n // Update tail\n this.tail = (this.tail - 1 + this.capacity) % this.capacity;\n this.count--;\n return true;\n }\n\n /**\n * Remove an item from the buffer.\n * Note: This is O(n) but should be rare (used for abort handling).\n *\n * @param item - The item to remove\n * @returns true if item was found and removed\n */\n remove(item: T): boolean {\n const index = this.indexOf(item);\n if (index === -1) {\n return false;\n }\n return this.removeAt(index);\n }\n\n /**\n * Get all items and clear the buffer.\n *\n * @returns Array of all items in queue order\n */\n drain(): T[] {\n const items: T[] = [];\n for (let i = 0; i < this.count; i++) {\n const idx = (this.head + i) % this.capacity;\n const item = this.buffer[idx];\n if (item !== undefined) {\n items.push(item);\n }\n this.buffer[idx] = undefined; // Help GC\n }\n this.head = 0;\n this.tail = 0;\n this.count = 0;\n return items;\n }\n\n /**\n * Clear all elements from the buffer.\n */\n clear(): void {\n for (let i = 0; i < this.count; i++) {\n const idx = (this.head + i) % this.capacity;\n this.buffer[idx] = undefined;\n }\n this.head = 0;\n this.tail = 0;\n this.count = 0;\n }\n}\n","import { RingBuffer } from './ring-buffer.js';\n\n/**\n * Waiter in the semaphore queue.\n */\ninterface Waiter {\n resolve: () => void;\n reject: (error: Error) => void;\n signal?: AbortSignal;\n onAbort?: () => void;\n}\n\n/**\n * A Promise-based semaphore for limiting concurrent operations.\n * Uses an O(1) ring buffer for the waiting queue.\n */\nexport class Semaphore {\n private permits: number;\n private readonly maxPermits: number;\n private readonly waitingQueue: RingBuffer<Waiter>;\n\n /**\n * Create a new semaphore.\n *\n * @param maxPermits - Maximum number of concurrent permits\n * @param maxQueue - Maximum queue size (defaults to 1000)\n */\n constructor(maxPermits: number, maxQueue = 1000) {\n this.maxPermits = maxPermits;\n this.permits = maxPermits;\n this.waitingQueue = new RingBuffer<Waiter>(maxQueue);\n }\n\n /**\n * Try to acquire a permit without waiting.\n *\n * @returns true if a permit was acquired, false otherwise\n */\n tryAcquire(): boolean {\n if (this.permits > 0) {\n this.permits--;\n return true;\n }\n return false;\n }\n\n /**\n * Acquire a permit, waiting if necessary.\n *\n * @param signal - Optional AbortSignal for cancellation\n * @returns Promise that resolves when permit is acquired\n * @throws {DOMException} When cancelled via signal (AbortError)\n */\n acquire(signal?: AbortSignal): Promise<void> {\n // Check if cancelled\n if (signal?.aborted) {\n const reason = signal.reason instanceof Error\n ? signal.reason\n : new DOMException('Aborted', 'AbortError');\n return Promise.reject(reason);\n }\n\n // Try to acquire immediately\n if (this.permits > 0) {\n this.permits--;\n return Promise.resolve();\n }\n\n // Add to wait queue\n return new Promise<void>((resolve, reject) => {\n const waiter: Waiter = { resolve, reject };\n\n // Set up abort handler\n if (signal) {\n const onAbort = () => {\n if (this.waitingQueue.remove(waiter)) {\n const reason = signal.reason instanceof Error\n ? signal.reason\n : new DOMException('Aborted', 'AbortError');\n reject(reason);\n }\n };\n\n waiter.signal = signal;\n waiter.onAbort = onAbort;\n signal.addEventListener('abort', onAbort, { once: true });\n }\n\n this.waitingQueue.push(waiter);\n });\n }\n\n /**\n * Release a permit back to the semaphore.\n */\n release(): void {\n if (!this.waitingQueue.isEmpty()) {\n // Give permit to next waiter (O(1) with ring buffer)\n const waiter = this.waitingQueue.shift();\n if (waiter) {\n // Clean up abort listener to prevent memory leak\n if (waiter.signal && waiter.onAbort) {\n waiter.signal.removeEventListener('abort', waiter.onAbort);\n }\n waiter.resolve();\n }\n } else if (this.permits < this.maxPermits) {\n // Return permit to pool\n this.permits++;\n }\n }\n\n /**\n * Get the number of available permits.\n */\n availablePermits(): number {\n return this.permits;\n }\n\n /**\n * Get the number of waiters in the queue.\n */\n queueLength(): number {\n return this.waitingQueue.length;\n }\n\n /**\n * Reject all waiters with the given error.\n */\n rejectAll(error: Error): void {\n const waiters = this.waitingQueue.drain();\n for (const waiter of waiters) {\n // Clean up abort listener to prevent memory leak\n if (waiter.signal && waiter.onAbort) {\n waiter.signal.removeEventListener('abort', waiter.onAbort);\n }\n waiter.reject(error);\n }\n }\n}\n"],"mappings":"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","names":[]} | ||
| {"version":3,"sources":["../src/bulkhead.ts","../src/config.ts","../src/ring-buffer.ts","../src/semaphore.ts"],"sourcesContent":["import {\n type Operation,\n type Pattern,\n type Resettable,\n type Closeable,\n BulkheadFullError,\n BulkheadClosedError,\n type FortifyLogger,\n noopLogger,\n NEVER_ABORTED_SIGNAL,\n} from '@fortify-ts/core';\nimport {\n type BulkheadConfig,\n type BulkheadConfigInputFull,\n parseBulkheadConfig,\n} from './config.js';\nimport { Semaphore } from './semaphore.js';\n\n/**\n * Bulkhead pattern implementation for limiting concurrent operations.\n *\n * Prevents resource exhaustion by limiting the number of concurrent executions,\n * with optional queueing for overflow requests.\n *\n * @template T - The return type of operations\n *\n * @example\n * ```typescript\n * const bulkhead = new Bulkhead<Response>({\n * maxConcurrent: 5,\n * maxQueue: 10,\n * queueTimeout: 5000,\n * onRejected: () => console.log('Request rejected'),\n * });\n *\n * const result = await bulkhead.execute(async (signal) => {\n * return fetch('/api/data', { signal });\n * });\n * ```\n */\nexport class Bulkhead<T> implements Pattern<T>, Resettable, Closeable {\n private readonly config: BulkheadConfig;\n private readonly logger: FortifyLogger;\n private readonly semaphore: Semaphore;\n private readonly queueSemaphore: Semaphore | undefined;\n private closed = false;\n\n /**\n * Create a new Bulkhead instance.\n *\n * @param config - Bulkhead configuration\n */\n constructor(config?: BulkheadConfigInputFull) {\n this.config = parseBulkheadConfig(config);\n this.logger = this.config.logger ?? noopLogger;\n // Execution semaphore: queue capacity = maxQueue (bounded by queue semaphore)\n this.semaphore = new Semaphore(\n this.config.maxConcurrent,\n Math.max(this.config.maxQueue, 1) // At least 1 for edge cases\n );\n\n // Only create queue semaphore if maxQueue > 0\n if (this.config.maxQueue > 0) {\n // Queue semaphore: only used for tryAcquire, queue never used\n this.queueSemaphore = new Semaphore(this.config.maxQueue, 1);\n }\n }\n\n /**\n * Execute an operation within the bulkhead's concurrency limits.\n *\n * @param operation - The async operation to execute\n * @param signal - Optional AbortSignal for cancellation\n * @returns Promise resolving to the operation result\n * @throws {BulkheadFullError} When bulkhead is at capacity and queue is full\n * @throws {DOMException} When cancelled via signal (AbortError)\n */\n async execute(operation: Operation<T>, signal?: AbortSignal): Promise<T> {\n // Check if closed - use specific closed error, not full error\n if (this.closed) {\n throw new BulkheadClosedError('Bulkhead is closed');\n }\n\n // Check if cancelled\n if (signal?.aborted) {\n throw signal.reason ?? new DOMException('Aborted', 'AbortError');\n }\n\n // Try to acquire semaphore immediately\n if (this.semaphore.tryAcquire()) {\n return this.executeWithPermit(operation, signal);\n }\n\n // Bulkhead full, try to queue\n return this.enqueue(operation, signal);\n }\n\n /**\n * Get the number of currently active executions.\n */\n activeCount(): number {\n return this.config.maxConcurrent - this.semaphore.availablePermits();\n }\n\n /**\n * Get the number of requests currently waiting in the queue.\n */\n queuedCount(): number {\n return this.semaphore.queueLength();\n }\n\n /**\n * Close the bulkhead, rejecting all pending requests.\n *\n * @returns Promise that resolves immediately (sync cleanup)\n */\n close(): Promise<void> {\n if (this.closed) {\n return Promise.resolve();\n }\n\n this.closed = true;\n const closeError = new BulkheadClosedError('Bulkhead closed');\n this.semaphore.rejectAll(closeError);\n this.queueSemaphore?.rejectAll(closeError);\n this.logger.info('Bulkhead closed');\n return Promise.resolve();\n }\n\n /**\n * Reset the bulkhead to accept new requests.\n */\n reset(): void {\n this.closed = false;\n this.logger.info('Bulkhead reset');\n }\n\n /**\n * Execute operation with semaphore permit held.\n */\n private async executeWithPermit(\n operation: Operation<T>,\n signal?: AbortSignal\n ): Promise<T> {\n try {\n return await operation(signal ?? NEVER_ABORTED_SIGNAL);\n } finally {\n this.semaphore.release();\n }\n }\n\n /**\n * Attempt to queue the request when bulkhead is full.\n */\n private async enqueue(\n operation: Operation<T>,\n signal?: AbortSignal\n ): Promise<T> {\n // If no queue configured, reject immediately\n if (this.config.maxQueue === 0) {\n this.onRejected();\n throw this.createFullError('Bulkhead is full - no queue configured');\n }\n\n // Try to acquire queue slot\n if (!this.queueSemaphore?.tryAcquire()) {\n // Queue is full, reject\n this.onRejected();\n throw this.createFullError('Bulkhead queue is full');\n }\n\n // Track timeout for cleanup in finally block\n let timeoutId: ReturnType<typeof setTimeout> | undefined;\n\n try {\n // Create combined signal for queue timeout\n let timeoutController: AbortController | undefined;\n let combinedSignal = signal;\n\n if (this.config.queueTimeout > 0) {\n timeoutController = new AbortController();\n\n // Create combined signal\n if (signal) {\n combinedSignal = AbortSignal.any([signal, timeoutController.signal]);\n } else {\n combinedSignal = timeoutController.signal;\n }\n\n // Start timeout with proper cleanup\n timeoutId = setTimeout(() => {\n timeoutController?.abort(new DOMException('Queue timeout', 'TimeoutError'));\n }, this.config.queueTimeout);\n }\n\n // Wait for execution semaphore\n try {\n await this.semaphore.acquire(combinedSignal);\n } catch (error) {\n // If aborted due to queue timeout, call onRejected\n if (\n error instanceof DOMException &&\n error.name === 'TimeoutError'\n ) {\n this.onRejected();\n }\n throw error;\n }\n\n // Got permit, execute\n return await this.executeWithPermit(operation, signal);\n } finally {\n // Clear timeout to prevent memory leak\n if (timeoutId) {\n clearTimeout(timeoutId);\n }\n this.queueSemaphore.release();\n }\n }\n\n /**\n * Handle rejection event.\n */\n private onRejected(): void {\n this.logger.warn('Bulkhead rejection', {\n maxConcurrent: this.config.maxConcurrent,\n maxQueue: this.config.maxQueue,\n });\n\n if (this.config.onRejected) {\n try {\n this.config.onRejected();\n } catch (error) {\n this.logger.error('onRejected callback threw an error', {\n error: error instanceof Error ? error.message : String(error),\n });\n }\n }\n }\n\n /**\n * Create a BulkheadFullError with current state context.\n */\n private createFullError(message?: string): BulkheadFullError {\n return new BulkheadFullError(\n message ?? 'Bulkhead is full',\n this.activeCount(),\n this.queuedCount()\n );\n }\n}\n","import { z } from 'zod';\nimport { type FortifyLogger } from '@fortify-ts/core';\n\n/** Maximum concurrent operations to prevent resource exhaustion */\nconst MAX_CONCURRENT = 10_000;\n\n/** Maximum queue size to prevent memory exhaustion */\nconst MAX_QUEUE_SIZE = 100_000;\n\n/** Maximum queue timeout in milliseconds (1 hour) */\nconst MAX_QUEUE_TIMEOUT_MS = 3_600_000;\n\n/**\n * Zod schema for Bulkhead configuration.\n */\nexport const bulkheadConfigSchema = z.object({\n /** Maximum number of concurrent executions allowed (default: 10, max: 10000) */\n maxConcurrent: z.number().int().positive().max(MAX_CONCURRENT).default(10),\n /** Maximum size of overflow queue, 0 means no queue (default: 0, max: 100000) */\n maxQueue: z.number().int().nonnegative().max(MAX_QUEUE_SIZE).default(0),\n /** Maximum time a request can wait in queue in milliseconds, 0 means no timeout (default: 0, max: 1 hour) */\n queueTimeout: z.number().int().nonnegative().max(MAX_QUEUE_TIMEOUT_MS).default(0),\n});\n\n/**\n * Raw config input type (before defaults are applied).\n */\nexport type BulkheadConfigInput = z.input<typeof bulkheadConfigSchema>;\n\n/**\n * Parsed config type (after defaults are applied).\n */\nexport type BulkheadConfigParsed = z.output<typeof bulkheadConfigSchema>;\n\n/**\n * Full configuration type including callbacks and logger.\n */\nexport interface BulkheadConfig extends BulkheadConfigParsed {\n /** Callback when a request is rejected */\n onRejected: (() => void) | undefined;\n /** Logger instance for structured logging */\n logger: FortifyLogger | undefined;\n}\n\n/**\n * Input config type for constructor.\n */\nexport interface BulkheadConfigInputFull extends BulkheadConfigInput {\n onRejected?: () => void;\n logger?: FortifyLogger;\n}\n\n/**\n * Parse and validate bulkhead configuration.\n *\n * @param config - Raw configuration input\n * @returns Validated configuration with defaults applied\n */\nexport function parseBulkheadConfig(config?: BulkheadConfigInputFull): BulkheadConfig {\n const parsed = bulkheadConfigSchema.parse(config ?? {});\n return {\n ...parsed,\n onRejected: config?.onRejected,\n logger: config?.logger,\n };\n}\n","/**\n * O(1) ring buffer queue implementation.\n *\n * Provides O(1) enqueue and dequeue operations by using a circular buffer\n * with head and tail pointers.\n *\n * @template T - The type of elements in the buffer\n */\nexport class RingBuffer<T> {\n private readonly buffer: (T | undefined)[];\n private head = 0;\n private tail = 0;\n private count = 0;\n\n /**\n * Create a new ring buffer.\n *\n * @param capacity - Maximum number of elements the buffer can hold\n */\n constructor(private readonly capacity: number) {\n this.buffer = new Array<T | undefined>(capacity);\n }\n\n /**\n * Add an element to the end of the buffer.\n *\n * @param item - The item to add\n * @returns true if added, false if buffer is full\n */\n push(item: T): boolean {\n if (this.count >= this.capacity) {\n return false;\n }\n\n this.buffer[this.tail] = item;\n this.tail = (this.tail + 1) % this.capacity;\n this.count++;\n return true;\n }\n\n /**\n * Remove and return the element at the front of the buffer.\n *\n * @returns The item at the front, or undefined if empty\n */\n shift(): T | undefined {\n if (this.count === 0) {\n return undefined;\n }\n\n const item = this.buffer[this.head];\n this.buffer[this.head] = undefined; // Help GC\n this.head = (this.head + 1) % this.capacity;\n this.count--;\n return item;\n }\n\n /**\n * Get the number of elements in the buffer.\n */\n get length(): number {\n return this.count;\n }\n\n /**\n * Check if the buffer is empty.\n */\n isEmpty(): boolean {\n return this.count === 0;\n }\n\n /**\n * Check if the buffer is full.\n */\n isFull(): boolean {\n return this.count >= this.capacity;\n }\n\n /**\n * Find the index of an item in the buffer.\n * Note: This is O(n) but should be rare (used for abort handling).\n *\n * @param item - The item to find\n * @returns The index of the item, or -1 if not found\n */\n indexOf(item: T): number {\n for (let i = 0; i < this.count; i++) {\n const idx = (this.head + i) % this.capacity;\n if (this.buffer[idx] === item) {\n return i;\n }\n }\n return -1;\n }\n\n /**\n * Remove an item at the given logical index.\n * Note: This is O(n) due to shifting elements.\n *\n * @param index - The logical index (0 = front of queue)\n * @returns true if item was removed\n */\n removeAt(index: number): boolean {\n if (index < 0 || index >= this.count) {\n return false;\n }\n\n // Shift elements to fill the gap\n for (let i = index; i < this.count - 1; i++) {\n const fromIdx = (this.head + i + 1) % this.capacity;\n const toIdx = (this.head + i) % this.capacity;\n this.buffer[toIdx] = this.buffer[fromIdx];\n }\n\n // Clear the last element\n const lastIdx = (this.head + this.count - 1) % this.capacity;\n this.buffer[lastIdx] = undefined;\n\n // Update tail\n this.tail = (this.tail - 1 + this.capacity) % this.capacity;\n this.count--;\n return true;\n }\n\n /**\n * Remove an item from the buffer.\n * Note: This is O(n) but should be rare (used for abort handling).\n *\n * @param item - The item to remove\n * @returns true if item was found and removed\n */\n remove(item: T): boolean {\n const index = this.indexOf(item);\n if (index === -1) {\n return false;\n }\n return this.removeAt(index);\n }\n\n /**\n * Get all items and clear the buffer.\n *\n * @returns Array of all items in queue order\n */\n drain(): T[] {\n const items: T[] = [];\n for (let i = 0; i < this.count; i++) {\n const idx = (this.head + i) % this.capacity;\n const item = this.buffer[idx];\n if (item !== undefined) {\n items.push(item);\n }\n this.buffer[idx] = undefined; // Help GC\n }\n this.head = 0;\n this.tail = 0;\n this.count = 0;\n return items;\n }\n\n /**\n * Clear all elements from the buffer.\n */\n clear(): void {\n for (let i = 0; i < this.count; i++) {\n const idx = (this.head + i) % this.capacity;\n this.buffer[idx] = undefined;\n }\n this.head = 0;\n this.tail = 0;\n this.count = 0;\n }\n}\n","import { RingBuffer } from './ring-buffer.js';\n\n/**\n * Waiter in the semaphore queue.\n */\ninterface Waiter {\n resolve: () => void;\n reject: (error: Error) => void;\n signal?: AbortSignal;\n onAbort?: () => void;\n}\n\n/**\n * A Promise-based semaphore for limiting concurrent operations.\n * Uses an O(1) ring buffer for the waiting queue.\n */\nexport class Semaphore {\n private permits: number;\n private readonly maxPermits: number;\n private readonly waitingQueue: RingBuffer<Waiter>;\n\n /**\n * Create a new semaphore.\n *\n * @param maxPermits - Maximum number of concurrent permits\n * @param maxQueue - Maximum queue size (defaults to 1000)\n */\n constructor(maxPermits: number, maxQueue = 1000) {\n this.maxPermits = maxPermits;\n this.permits = maxPermits;\n this.waitingQueue = new RingBuffer<Waiter>(maxQueue);\n }\n\n /**\n * Try to acquire a permit without waiting.\n *\n * @returns true if a permit was acquired, false otherwise\n */\n tryAcquire(): boolean {\n if (this.permits > 0) {\n this.permits--;\n return true;\n }\n return false;\n }\n\n /**\n * Acquire a permit, waiting if necessary.\n *\n * @param signal - Optional AbortSignal for cancellation\n * @returns Promise that resolves when permit is acquired\n * @throws {DOMException} When cancelled via signal (AbortError)\n */\n acquire(signal?: AbortSignal): Promise<void> {\n // Check if cancelled\n if (signal?.aborted) {\n const reason = signal.reason instanceof Error\n ? signal.reason\n : new DOMException('Aborted', 'AbortError');\n return Promise.reject(reason);\n }\n\n // Try to acquire immediately\n if (this.permits > 0) {\n this.permits--;\n return Promise.resolve();\n }\n\n // Add to wait queue\n return new Promise<void>((resolve, reject) => {\n const waiter: Waiter = { resolve, reject };\n\n // Set up abort handler\n if (signal) {\n const onAbort = () => {\n if (this.waitingQueue.remove(waiter)) {\n const reason = signal.reason instanceof Error\n ? signal.reason\n : new DOMException('Aborted', 'AbortError');\n reject(reason);\n }\n };\n\n waiter.signal = signal;\n waiter.onAbort = onAbort;\n signal.addEventListener('abort', onAbort, { once: true });\n }\n\n this.waitingQueue.push(waiter);\n });\n }\n\n /**\n * Release a permit back to the semaphore.\n */\n release(): void {\n if (!this.waitingQueue.isEmpty()) {\n // Give permit to next waiter (O(1) with ring buffer)\n const waiter = this.waitingQueue.shift();\n if (waiter) {\n // Clean up abort listener to prevent memory leak\n if (waiter.signal && waiter.onAbort) {\n waiter.signal.removeEventListener('abort', waiter.onAbort);\n }\n waiter.resolve();\n }\n } else if (this.permits < this.maxPermits) {\n // Return permit to pool\n this.permits++;\n }\n }\n\n /**\n * Get the number of available permits.\n */\n availablePermits(): number {\n return this.permits;\n }\n\n /**\n * Get the number of waiters in the queue.\n */\n queueLength(): number {\n return this.waitingQueue.length;\n }\n\n /**\n * Reject all waiters with the given error.\n */\n rejectAll(error: Error): void {\n const waiters = this.waitingQueue.drain();\n for (const waiter of waiters) {\n // Clean up abort listener to prevent memory leak\n if (waiter.signal && waiter.onAbort) {\n waiter.signal.removeEventListener('abort', waiter.onAbort);\n }\n waiter.reject(error);\n }\n }\n}\n"],"mappings":"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","names":[]} |
+2
-2
| { | ||
| "name": "@fortify-ts/bulkhead", | ||
| "version": "0.1.5", | ||
| "version": "0.2.0", | ||
| "description": "Concurrency limiter bulkhead pattern for @fortify-ts", | ||
@@ -26,3 +26,3 @@ "type": "module", | ||
| "zod": "^4.1.13", | ||
| "@fortify-ts/core": "0.2.0" | ||
| "@fortify-ts/core": "0.3.0" | ||
| }, | ||
@@ -29,0 +29,0 @@ "devDependencies": { |
Unpublished package
Supply chain riskPackage version was not found on the registry. It may exist on a different registry and need to be configured to pull from that registry.
Found 1 instance in 1 package
No README
QualityPackage does not have a README. This may indicate a failed publish or a low quality package.
Found 1 instance in 1 package
Unpublished package
Supply chain riskPackage version was not found on the registry. It may exist on a different registry and need to be configured to pull from that registry.
Found 1 instance in 1 package
89465
4.39%9
12.5%1073
1.42%0
-100%113
Infinity%Updated