+75
-91
@@ -8,2 +8,52 @@ # Changelog | ||
| ## [v1.3.0] - 2026-07-02 | ||
| ### Added | ||
| - **Pluggable circuit breakers.** The breaker behind `Semaphore` is now a strategy. The new `CircuitBreakerStrategy` interface (exported) defines the exact contract the semaphore drives, and an instance can be injected via the new `circuitBreaker` config option — it overrides the `circuitBreaker*` numeric options, the same precedence `comparator` has over `queueOrder`. The built-ins live in a breakers module (`src/breakers/`): | ||
| - `SaturationCircuitBreaker` — the existing default, behavior unchanged; a windowed failure-rate breaker whose meaning follows its signal (queue timeouts → saturation; reported errors → error rate). | ||
| - `NoopCircuitBreaker` — never trips; the semaphore as a pure limiter. | ||
| - `ManualCircuitBreaker` — an operator kill switch (`open()`/`close()`); no cooldown, no probe. | ||
| - **`Semaphore.reportFailure()`** — feeds an external failure signal (e.g. downstream 5xx errors) into the breaker, making the default breaker double as an error-rate breaker: the same window, threshold, cooldown, probe recovery, and queue eviction apply, and a trip emits `CIRCUITOPEN` with `reason: 'reported-failure'`. Reported failures influence trip decisions only while the circuit is closed; no-op after shutdown. | ||
| - **`circuitBreakerFailurePredicate` config option** — declarative fault scoring for `use()`. Rejections from your function for which the predicate returns `true` count as one breaker failure each (as via `reportFailure()`), and half-open probes become fault-aware: a probe dispatched through `use()` whose operation fails with a matching error re-opens the circuit (`CIRCUITOPEN` with `reason: 'half-open-probe-failed'`) instead of closing it on release; a non-matching rejection still counts as a successful probe. A predicate that throws is logged via `console.warn` and treated as non-matching. Only observes `use()` — bare `acquire()`/`tryAcquire()` callers use `reportFailure()`. | ||
| - Exported the `CircuitState` type from the package root. | ||
| ### Changed (BREAKING) | ||
| - **Renamed the breaker's public names to match the breakers module.** The `CircuitBreaker` export is now `SaturationCircuitBreaker`, and its `recordTimeout()` method is now `recordFailure()` (the failure signal is caller-defined, not inherently a timeout). Both are straight renames with unchanged behavior; no compatibility aliases are kept. Migration: `import { CircuitBreaker } from 'regulo'` → `import { SaturationCircuitBreaker } from 'regulo'`; `breaker.recordTimeout()` → `breaker.recordFailure()`. Shipping this rename in a minor release is a deliberate project decision, documented here and in the README, in place of carrying aliases forward. | ||
| ### Performance | ||
| - **Current-bucket caching in the metrics and breaker windows.** Hot-path bucket resolution is now two comparisons instead of a float division + modulo + timestamp check per event per window; the full computation runs only on step rollover. Measured on the benchmark suite: uncontended `tryAcquire`+`release` ≈ +80%, `use()` round-trip ≈ +65%, contended throughput ≈ +30–50%. | ||
| - **Pre-bound scheduler callback** — scheduler wakeups no longer allocate a fresh closure per `schedule()` on the contended path. | ||
| [1.3.0]: https://github.com/greenstick/regulo/releases/tag/v1.3.0 | ||
| ## [v1.2.0] - 2026-07-02 | ||
| ### Added | ||
| - **`peekQueue()` method:** Provides a read-only snapshot array of the current queue state in strict enqueue order (`QueuedTaskView[]`). | ||
| - **`capacity` and `circuitState` getters:** Publicly exposes the total configured permit capacity and the active state of the circuit breaker (`'closed' | 'open' | 'half-open'`) directly on the `Semaphore` instance. | ||
| - **Customizable circuit breaker window buckets:** Added the `circuitBreakerWindowBucketWidth` configuration property to tune the granularity/resolution of the circuit breaker's sliding failure window. | ||
| - **Customizable metrics windows:** Added the `metricsWindows` configuration option to override the built-in time windows (1m/5m/15m/1h/24h) with bespoke tracking horizons. Exported the underlying `WindowOptions` type from the package root. Windows that collide on the same horizon label (which would silently overwrite each other in the snapshot) are rejected at construction with `INVALID_ARGUMENT`. | ||
| - **`QUEUEEVICT` event and `totalEvictions` lifetime counter:** Tasks evicted by a circuit trip are now observable — one `QUEUEEVICT` event (payload: `QueuedTaskView`) per evicted task, plus a `totalEvictions` counter in `status().lifetime`, so eviction volume is distinguishable from ordinary open-circuit rejections. | ||
| ### Changed | ||
| - **Immediate queue eviction on circuit trip:** When a timeout causes the circuit breaker to trip open, all other non-probe queued tasks are instantly evicted and rejected with a `CIRCUIT_OPEN` error code, preventing them from waiting out their individual lifetimes and surfacing misleading `TIMEOUT` rejections. | ||
| - **Narrowed `QUEUEPURGE` payload:** The `QUEUEPURGE` event now cleanly emits the narrow `QueuedTaskView` shape instead of leaking the raw internal `QueuedTask` instance and its mutative structural links. | ||
| - **Unconditional listener exception logging:** Errors thrown inside attached event listeners are now always surfaced via `console.warn` instead of being silenced when `debug: false` is active. | ||
| - **Shared `drain()` promise semantics:** Clarified that concurrent, overlapping invocations of `drain()` return the identical in-flight promise. The `timeoutMs` parameter of the *first* caller governs the cycle; subsequent caller deadlines are bypassed. | ||
| - **Standardized internal validation errors:** Attempting to double-insert a node into the internal heap or initializing metrics with an empty array now properly throws a `SemaphoreError` with the `INVALID_ARGUMENT` code rather than a generic runtime `Error`. | ||
| - **`tryAcquire()` no longer records a circuit-breaker attempt.** Previously every `tryAcquire` call — including ones that returned `null` — counted toward the breaker window's attempt denominator, diluting the timeout rate. Since `tryAcquire` can never queue (and therefore never time out), it no longer participates in breaker accounting at all. In `tryAcquire`-heavy workloads the breaker now trips somewhat more readily than in 1.1.0. | ||
| - **`status().metrics` returns `null` after `shutdown()`** (previously a zeroed snapshot). The collector's typed-array buffers are released on shutdown. | ||
| - **`status()` rate fields are computed over the shortest configured metrics window** (`requestsPerSecond`, `timeoutRate1m`). With the default windows this is the 1m window, unchanged; with custom `metricsWindows` the fields previously looked up a hard-coded `'1m'` label and silently reported 0 when it was absent. | ||
| ### Fixed | ||
| - **`drain()` resolution no longer depends on a scheduler tick.** The idle check now runs synchronously at every transition that can reach idle (release, timeout, abort, purge, `cancel()`). Previously it ran only on scheduler wakeups, which adaptive backoff defers on an *unref'd* timer — delaying drain resolution by up to the backoff delay and, in a process with nothing else keeping the event loop alive, allowing exit before a pending `drain()` resolved. | ||
| [1.2.0]: https://github.com/greenstick/regulo/releases/tag/v1.2.0 | ||
| ## [v1.1.0] - 2026-06-28 | ||
@@ -13,41 +63,21 @@ | ||
| - **Typed event payloads.** `on()` and `off()` are now generic over the event | ||
| name, so a listener's argument is inferred from the event (no more `any`). | ||
| Exposed via the new `SemaphoreEventMap` and `SemaphoreEventListener<E>` types, | ||
| both exported from the package entry point. Emitted payloads are unchanged; | ||
| this is purely a compile-time improvement. | ||
| - **`INVALID_ARGUMENT` error code** for argument and configuration validation, | ||
| so these failures can be distinguished programmatically via `error.code` like | ||
| every other rejection. | ||
| - **Typed event payloads.** `on()` and `off()` are now generic over the event name, so a listener's argument is inferred from the event (no more `any`). Exposed via the new `SemaphoreEventMap` and `SemaphoreEventListener<E>` types, both exported from the package entry point. Emitted payloads are unchanged; this is purely a compile-time improvement. | ||
| - **`INVALID_ARGUMENT` error code** for argument and configuration validation, so these failures can be distinguished programmatically via `error.code` like every other rejection. | ||
| ### Changed | ||
| - **Argument and configuration validation now throws `SemaphoreError`** (with | ||
| code `INVALID_ARGUMENT`) instead of a plain `Error`. This covers the | ||
| `Semaphore` constructor, the `CircuitBreaker`/`BackoffTracker` config checks, | ||
| the `queueOrder`/`comparator` checks, and an invalid `drain()` timeout. Because | ||
| `SemaphoreError extends Error`, existing `instanceof Error` and `toThrow()` | ||
| handling is unaffected. | ||
| - **`reset()` now throws `SemaphoreError('SHUTDOWN')` when called on a | ||
| shut-down instance** instead of silently reviving it. This makes `shutdown()` | ||
| genuinely terminal, matching its documented "cannot be reversed" contract. If | ||
| you previously relied on `reset()` to restart a shut-down semaphore, construct | ||
| a new instance instead. | ||
| - **Argument and configuration validation now throws `SemaphoreError`** (with code `INVALID_ARGUMENT`) instead of a plain `Error`. This covers the `Semaphore` constructor, the `CircuitBreaker`/`BackoffTracker` config checks, the `queueOrder`/`comparator` checks, and an invalid `drain()` timeout. Because `SemaphoreError extends Error`, existing `instanceof Error` and `toThrow()` handling is unaffected. | ||
| - **`reset()` now throws `SemaphoreError('SHUTDOWN')` when called on a shut-down instance** instead of silently reviving it. This makes `shutdown()` genuinely terminal, matching its documented "cannot be reversed" contract. If you previously relied on `reset()` to restart a shut-down semaphore, construct a new instance instead. | ||
| ### Performance | ||
| - **`emit()` skips the defensive listener-array snapshot when a single listener | ||
| is registered** (the common case), invoking it directly. Multi-listener | ||
| emission still snapshots to stay safe against mid-emit mutation. | ||
| - **`emit()` skips the defensive listener-array snapshot when a single listener is registered** (the common case), invoking it directly. Multi-listener emission still snapshots to stay safe against mid-emit mutation. | ||
| ### Internal | ||
| - Added a `branches: 85` coverage threshold (alongside the existing `lines: 90`) | ||
| so a branch-coverage regression is caught in CI. | ||
| - Added a `branches: 85` coverage threshold (alongside the existing `lines: 90`) so a branch-coverage regression is caught in CI. | ||
| ### Documentation | ||
| - Corrected several broken in-page anchor links and heading typos, refreshed the | ||
| events/error-code references for the typed events and `INVALID_ARGUMENT` code, | ||
| and clarified the `reset()`/`shutdown()`/`drain()` semantics. | ||
| - Corrected several broken in-page anchor links and heading typos, refreshed the events/error-code references for the typed events and `INVALID_ARGUMENT` code, and clarified the `reset()`/`shutdown()`/`drain()` semantics. | ||
| - Refreshed all benchmark tables with a new run (Node v22.16.0, darwin x64). | ||
@@ -61,15 +91,4 @@ | ||
| - **The queue-wait timeout is now driven by a single shared deadline timer** | ||
| instead of one `setTimeout`/`clearTimeout` per queued task. Because every task | ||
| shares `queueMaxTimeout` and the enqueue-ordered index is sorted by deadline, | ||
| the oldest task is always the next to expire, so one self-re-arming timer | ||
| suffices. Timeout precision and circuit-breaker trip timing are unchanged; the | ||
| removed per-task timer churn lifts contended throughput by roughly 15–19% and | ||
| is what makes regulo viable for shorter-duration work (small DB pulls, cache | ||
| fills) rather than only millisecond-scale operations. | ||
| - **The priority heap's index is now intrusive.** Each task stores its own heap | ||
| slot (`heapIndex`) instead of the heap maintaining a separate `Map<id, index>`, | ||
| so every sift writes a plain property instead of a hashed map entry. This lifts | ||
| contended throughput by a further ~25–30% (deep queues benefit most); with | ||
| metrics disabled, contended throughput now sits alongside `p-limit`/`p-queue`. | ||
| - **The queue-wait timeout is now driven by a single shared deadline timer** instead of one `setTimeout`/`clearTimeout` per queued task. Because every task shares `queueMaxTimeout` and the enqueue-ordered index is sorted by deadline, the oldest task is always the next to expire, so one self-re-arming timer suffices. Timeout precision and circuit-breaker trip timing are unchanged; the removed per-task timer churn lifts contended throughput by roughly 15–19% and is what makes regulo viable for shorter-duration work (small DB pulls, cache fills) rather than only millisecond-scale operations. | ||
| - **The priority heap's index is now intrusive.** Each task stores its own heap slot (`heapIndex`) instead of the heap maintaining a separate `Map<id, index>`, so every sift writes a plain property instead of a hashed map entry. This lifts contended throughput by a further ~25–30% (deep queues benefit most); with metrics disabled, contended throughput now sits alongside `p-limit`/`p-queue`. | ||
@@ -82,43 +101,19 @@ [1.0.5]: https://github.com/greenstick/regulo/releases/tag/v1.0.5 | ||
| - **Renamed the queue-ordering presets** so priority is an explicit, named axis | ||
| rather than an implicit default. `queueOrder` values are now: | ||
| `'fifo'` / `'lifo'` (order purely by enqueue time, priority ignored) and | ||
| `'fifoWithPriority'` / `'lifoWithPriority'` (priority primary, enqueue-time | ||
| tie-break). Previously `'fifo'`/`'lifo'` were priority-primary and the | ||
| priority-less variants were `'fifoIgnorePriority'`/`'lifoIgnorePriority'`. | ||
| Migration: `'fifo'` → `'fifoWithPriority'`, `'lifo'` → `'lifoWithPriority'`, | ||
| `'fifoIgnorePriority'` → `'fifo'`, `'lifoIgnorePriority'` → `'lifo'`. | ||
| - **Default `queueOrder` is now `'fifoWithPriority'`** (was `'fifo'`). Dispatch | ||
| behavior with no `queueOrder` set is unchanged — priority is still honored by | ||
| default — but the default's *name* changed. | ||
| - **`queueMaxLength` now defaults to `1024`** (was `Number.MAX_SAFE_INTEGER`, | ||
| i.e. effectively unbounded). Once the queue is full, further `acquire()` calls | ||
| reject with `QUEUE_FULL`. This adds a finite back-pressure guardrail by | ||
| default; pass `queueMaxLength: Number.MAX_SAFE_INTEGER` to restore the previous | ||
| unbounded behavior. | ||
| - **Renamed the queue-ordering presets** so priority is an explicit, named axis rather than an implicit default. `queueOrder` values are now: `'fifo'` / `'lifo'` (order purely by enqueue time, priority ignored) and `'fifoWithPriority'` / `'lifoWithPriority'` (priority primary, enqueue-time tie-break). Previously `'fifo'`/`'lifo'` were priority-primary and the priority-less variants were `'fifoIgnorePriority'`/`'lifoIgnorePriority'`. Migration: `'fifo'` → `'fifoWithPriority'`, `'lifo'` → `'lifoWithPriority'`, `'fifoIgnorePriority'` → `'fifo'`, `'lifoIgnorePriority'` → `'lifo'`. | ||
| - **Default `queueOrder` is now `'fifoWithPriority'`** (was `'fifo'`). Dispatch behavior with no `queueOrder` set is unchanged — priority is still honored by default — but the default's *name* changed. | ||
| - **`queueMaxLength` now defaults to `1024`** (was `Number.MAX_SAFE_INTEGER`, i.e. effectively unbounded). Once the queue is full, further `acquire()` calls reject with `QUEUE_FULL`. This adds a finite back-pressure guardrail by default; pass `queueMaxLength: Number.MAX_SAFE_INTEGER` to restore the previous unbounded behavior. | ||
| ### Performance | ||
| - **`status()` is now O(1) in queue depth** (was O(N)). Queue age is read from a | ||
| new enqueue-ordered index instead of cloning and scanning the queue, so | ||
| `status()` is safe to call on a metrics scrape path even with deep queues. The | ||
| `status()` snapshot benchmark is now flat across queue depths. | ||
| - **The stale-task purge sweep is now O(s)** in the number of tasks actually | ||
| evicted per tick (was O(N) every tick), by walking the enqueue-ordered index | ||
| from the head and stopping at the first task young enough to keep. | ||
| - **`status()` is now O(1) in queue depth** (was O(N)). Queue age is read from a new enqueue-ordered index instead of cloning and scanning the queue, so `status()` is safe to call on a metrics scrape path even with deep queues. The `status()` snapshot benchmark is now flat across queue depths. | ||
| - **The stale-task purge sweep is now O(s)** in the number of tasks actually evicted per tick (was O(N) every tick), by walking the enqueue-ordered index from the head and stopping at the first task young enough to keep. | ||
| ### Internal | ||
| - Added `IntrusiveList`, an insertion-ordered index kept alongside the priority | ||
| heap (pointers stored on the task itself, so no per-task allocation or second | ||
| map on the hot path), and removed the now-unnecessary `QueueAgeCache`. | ||
| - Added `IntrusiveList`, an insertion-ordered index kept alongside the priority heap (pointers stored on the task itself, so no per-task allocation or second map on the hot path), and removed the now-unnecessary `QueueAgeCache`. | ||
| ### Documentation | ||
| - Added a "Choosing an ordering, and its implications" section explaining how | ||
| ordering interacts with weighted permits and head-of-line dispatch. | ||
| - Documented that a single `Semaphore` is one failure domain (its circuit | ||
| breaker and backoff are shared across all work routed through it), and that | ||
| `weight`/`priority` should not be used to multiplex unrelated task types or | ||
| downstreams through one instance. | ||
| - Added a "Choosing an ordering, and its implications" section explaining how ordering interacts with weighted permits and head-of-line dispatch. | ||
| - Documented that a single `Semaphore` is one failure domain (its circuit breaker and backoff are shared across all work routed through it), and that `weight`/`priority` should not be used to multiplex unrelated task types or downstreams through one instance. | ||
@@ -134,23 +129,12 @@ [1.0.4]: https://github.com/greenstick/regulo/releases/tag/v1.0.4 | ||
| - Priority-queue semaphore with weighted permits and head-of-line-fair dispatch. | ||
| - Configurable queue ordering: `fifo` (default), `lifo`, `fifoIgnorePriority`, | ||
| `lifoIgnorePriority`, or a custom `comparator`. Invalid comparator results | ||
| (`NaN` / non-number) fall back to a stable `id` tie-break. | ||
| - Integrated saturation circuit breaker (closed → open → half-open → closed) | ||
| with cooldown and single-probe recovery, exported standalone as `CircuitBreaker`. | ||
| - Adaptive, wall-clock-decaying backoff that throttles dispatch during timeout | ||
| bursts and returns to zero on its own. | ||
| - Built-in windowed metrics (1m/5m/15m/1h/24h) for throughput, latency, queue | ||
| depth, and in-flight count, plus lifetime counters, surfaced via `status()`. | ||
| - Lifecycle controls: `acquire`, `use`, `tryAcquire`, `drain`, `reset`, | ||
| `cancel`, `shutdown`. | ||
| - Event stream: `task-acquire`, `task-release`, `task-timeout`, `task-abort`, | ||
| `queue-purge`, `circuit-open`, `circuit-half-open`, `circuit-close`, | ||
| `shutdown`. All events fire regardless of `debug`. | ||
| - `AbortSignal` support, stale-task purging, and double-release-safe release | ||
| closures. | ||
| - Parameter validation for `count`, `weight`, `priority`, `drain` timeout, and | ||
| all configuration options, with dedicated `INVALID_WEIGHT` and | ||
| `INVALID_PRIORITY` error codes. | ||
| - Configurable queue ordering: `fifo` (default), `lifo`, `fifoIgnorePriority`, `lifoIgnorePriority`, or a custom `comparator`. Invalid comparator results (`NaN` / non-number) fall back to a stable `id` tie-break. | ||
| - Integrated saturation circuit breaker (closed → open → half-open → closed) with cooldown and single-probe recovery, exported standalone as `CircuitBreaker`. | ||
| - Adaptive, wall-clock-decaying backoff that throttles dispatch during timeout bursts and returns to zero on its own. | ||
| - Built-in windowed metrics (1m/5m/15m/1h/24h) for throughput, latency, queue depth, and in-flight count, plus lifetime counters, surfaced via `status()`. | ||
| - Lifecycle controls: `acquire`, `use`, `tryAcquire`, `drain`, `reset`, `cancel`, `shutdown`. | ||
| - Event stream: `task-acquire`, `task-release`, `task-timeout`, `task-abort`, `queue-purge`, `circuit-open`, `circuit-half-open`, `circuit-close`, `shutdown`. All events fire regardless of `debug`. | ||
| - `AbortSignal` support, stale-task purging, and double-release-safe release closures. | ||
| - Parameter validation for `count`, `weight`, `priority`, `drain` timeout, and all configuration options, with dedicated `INVALID_WEIGHT` and `INVALID_PRIORITY` error codes. | ||
| - Strict-mode TypeScript types; ESM + CJS builds; zero runtime dependencies. | ||
| [1.0.0]: https://github.com/greenstick/regulo/releases/tag/v1.0.0 |
+1
-1
@@ -1,2 +0,2 @@ | ||
| 'use strict';var o=class extends Error{constructor(e,t){super(e),this.name="SemaphoreError",this.code=t;}};var u=(a,e,t,i,r=true,s=true)=>{if(typeof a!="number"||Number.isNaN(a))throw new o(`${e} must be a valid number`,"INVALID_ARGUMENT");if(r&&!Number.isInteger(a))throw new o(`${e} must be an integer`,"INVALID_ARGUMENT");if(s?a<t:a<=t)throw new o(`${e} must be ${s?">=":">"} ${t}`,"INVALID_ARGUMENT");if(s?a>i:a>=i)throw new o(`${e} must be ${s?"<=":"<"} ${i}`,"INVALID_ARGUMENT");return a};var T=class{constructor(e){this._inFlight=0;this.capacity=e,this._available=e;}get available(){return this._available}get inFlight(){return this._inFlight}get isFull(){return this._available===0}hasCapacityFor(e){return this._available>=e}acquire(e=1){this._available-=e,this._inFlight+=e;}release(e=1){this._available=Math.min(this._available+e,this.capacity),this._inFlight=this._inFlight<e?0:this._inFlight-e;}assertInvariant(e){if(!e)return;let t=this._inFlight+this._available;t!==this.capacity&&console.error(`[Semaphore] Invariant violation: inFlight(${this._inFlight}) + available(${this._available}) = ${t}, expected capacity(${this.capacity})`);}reset(){this._available=this.capacity,this._inFlight=0;}};var A=class{constructor(e,t){this.size=e,this.stepMs=t,this.buckets=new Int32Array(e*2),this.timestamps=new Float64Array(e);}idx(){let e=Date.now(),t=Math.floor(e/this.stepMs)*this.stepMs,i=Math.floor(t/this.stepMs)%this.size;return this.timestamps[i]!==t&&(this.timestamps[i]=t,this.buckets[i*2]=0,this.buckets[i*2+1]=0),i}addAcquired(){this.buckets[this.idx()*2]++;}addTimeout(){this.buckets[this.idx()*2+1]++;}snapshot(){let e=Date.now(),t=Math.floor(e/this.stepMs)*this.stepMs-(this.size-1)*this.stepMs,i=0,r=0;for(let s=0;s<this.size;s++)this.timestamps[s]>=t&&(i+=this.buckets[s*2],r+=this.buckets[s*2+1]);return {acquired:i,timeouts:r}}reset(){this.buckets.fill(0),this.timestamps.fill(0);}},b=class{constructor(e={}){this.state="closed";this.openUntil=0;this.probeInFlight=false;this._probeTaskId=null;if(this.threshold=u(e.threshold??.5,"CircuitBreaker threshold",0,1,false,false),this.window=u(e.window??1e4,"CircuitBreaker window",1e3,Number.MAX_SAFE_INTEGER,true,true),this.cooldown=u(e.cooldown??5e3,"CircuitBreaker cooldown",1e3,Number.MAX_SAFE_INTEGER,true,true),this.minThroughput=u(e.minThroughput??10,"CircuitBreaker minThroughput",1,Number.MAX_SAFE_INTEGER,true,true),this.minFailures=u(e.minFailures??5,"CircuitBreaker minFailures",1,Number.MAX_SAFE_INTEGER,true,true),this.eventWindow=new A(Math.ceil(this.window/1e3),1e3),this.minThroughput<this.minFailures)throw new o("CircuitBreaker minThroughput must be >= minFailures","INVALID_ARGUMENT")}get isClosed(){return this.state==="closed"}get isOpen(){return this.state==="open"}get isHalfOpen(){return this.state==="half-open"}get hasProbeInFlight(){return this.probeInFlight}get probeTaskId(){return this._probeTaskId}get cooldownRemaining(){return this.isOpen?Math.max(0,this.openUntil-Date.now()):0}trackAttempt(){this.state==="closed"&&this.eventWindow.addAcquired();}recordTimeout(){this.eventWindow.addTimeout();}checkAndTransition(){return this.state!=="open"||Date.now()<this.openUntil?false:(this.state="half-open",this.probeInFlight=false,this._probeTaskId=null,true)}evaluateAndTrip(){if(this.state!=="closed")return {tripped:false};let{timeouts:e,acquired:t}=this.eventWindow.snapshot();if(t<this.minThroughput||e<this.minFailures)return {tripped:false};let i=e/t;return i<this.threshold?{tripped:false}:(this.state="open",this.openUntil=Date.now()+this.cooldown,{tripped:true,timeoutRate:i,failures:e,attempts:t})}markProbeInFlight(){this.probeInFlight=true;}claimProbeSlot(e){this.probeInFlight=true,this._probeTaskId=e;}releaseProbeSlot(){this.probeInFlight=false,this._probeTaskId=null;}handleProbeSuccess(){this.state="closed",this.probeInFlight=false,this._probeTaskId=null,this.eventWindow.reset();}handleProbeFailure(){this.state="open",this.probeInFlight=false,this._probeTaskId=null,this.openUntil=Date.now()+this.cooldown;}reset(){this.state="closed",this.openUntil=0,this.probeInFlight=false,this._probeTaskId=null,this.eventWindow.reset();}};var w=class{constructor(e={}){this.delay=0;this.lastTimestamp=0;if(this.initialTimeout=u(e.initialTimeout??50,"BackoffTracker initialTimeout",0,Number.MAX_SAFE_INTEGER,true,true),this.maxTimeout=u(e.maxTimeout??2e3,"BackoffTracker maxTimeout",0,Number.MAX_SAFE_INTEGER,true,true),this.decayFactor=u(e.decayFactor??.5,"BackoffTracker decayFactor",0,1,false,false),this.maxTimeout<this.initialTimeout)throw new o("BackoffTracker maxTimeout must be >= initialTimeout","INVALID_ARGUMENT")}get currentDelay(){if(this.delay===0)return 0;let e=(Date.now()-this.lastTimestamp)/1e3;if(e<=0)return this.delay;let t=this.delay*Math.pow(this.decayFactor,e);return t<1?0:t}onTimeout(){let e=this.currentDelay,t=e>0?e*2:this.initialTimeout;this.delay=Math.min(t,this.maxTimeout),this.lastTimestamp=Date.now();}reset(){this.delay=0,this.lastTimestamp=0;}};var y=class{constructor(e){this.prev=null;this.next=null;this.heapIndex=-1;this.completed=false;this.id=e.id,this.priority=e.priority,this.enqueueTime=e.enqueueTime,this.isProbe=e.isProbe,this.weight=e.weight??1,this._resolve=e.resolve,this._reject=e.reject,this.abortSignal=e.abortSignal;}arm(e){this.abortSignal&&(this.abortListener=()=>{this.claim()&&e();},this.abortSignal.addEventListener("abort",this.abortListener));}dispatch(e){return this.claim()?(this._resolve(e()),true):false}discard(e){return this.claim()?(this._reject(e),true):false}claim(){return this.completed?false:(this.completed=true,this.abortSignal&&this.abortListener&&this.abortSignal.removeEventListener("abort",this.abortListener),true)}reject(e){this._reject(e);}};var S=class{constructor(e){this.heap=[];this.compare=e;}get size(){return this.heap.length}isEmpty(){return this.heap.length===0}peek(){return this.heap[0]}toArray(){return [...this.heap]}has(e){return e.heapIndex>=0&&this.heap[e.heapIndex]===e}clear(){for(let e of this.heap)e.heapIndex=-1;this.heap=[];}insert(e){if(e.heapIndex>=0)throw new Error("Item is already in a heap");let t=this.heap.length;this.heap.push(e),e.heapIndex=t,this._bubbleUp(t);}pop(){if(this.heap.length===0)return;let e=this.heap[0];e.heapIndex=-1;let t=this.heap.pop();return this.heap.length>0&&t!==e&&(this.heap[0]=t,t.heapIndex=0,this._bubbleDown(0)),e}delete(e){let t=e.heapIndex;if(t<0||this.heap[t]!==e)return;e.heapIndex=-1;let i=this.heap.pop();if(t===this.heap.length||i===e)return e;this.heap[t]=i,i.heapIndex=t;let r=this.heap[t-1>>1];return t>0&&this.compare(i,r)<0?this._bubbleUp(t):this._bubbleDown(t),e}_swap(e,t){let i=this.heap[e],r=this.heap[t];this.heap[e]=r,this.heap[t]=i,i.heapIndex=t,r.heapIndex=e;}_bubbleUp(e){for(;e>0;){let t=e-1>>1;if(this.compare(this.heap[e],this.heap[t])<0)this._swap(e,t),e=t;else break}}_bubbleDown(e){let t=this.heap.length;for(;;){let i=e,r=2*e+1,s=2*e+2;if(r<t&&this.compare(this.heap[r],this.heap[i])<0&&(i=r),s<t&&this.compare(this.heap[s],this.heap[i])<0&&(i=s),i===e)break;this._swap(e,i),e=i;}}};var q=class{constructor(){this.head=null;this.tail=null;this._size=0;}get size(){return this._size}isEmpty(){return this._size===0}peekHead(){return this.head===null?void 0:this.head}pushTail(e){e.prev=this.tail,e.next=null,this.tail!==null?this.tail.next=e:this.head=e,this.tail=e,this._size++;}remove(e){e.prev!==null?e.prev.next=e.next:this.head=e.next,e.next!==null?e.next.prev=e.prev:this.tail=e.prev,e.prev=e.next=null,this._size--;}clear(){this.head=this.tail=null,this._size=0;}};var E=class{constructor(e,t){if(e<=0)throw new Error("CombinedWindow size must be > 0");this.size=e,this.stepMs=t,this.timestamps=new Float64Array(e),this.acquired=new Int32Array(e),this.released=new Int32Array(e),this.timeouts=new Int32Array(e),this.ifSum=new Float64Array(e),this.ifCount=new Int32Array(e),this.ifMax=new Float64Array(e),this.qSum=new Float64Array(e),this.qCount=new Int32Array(e),this.qMax=new Float64Array(e),this.latSum=new Float64Array(e),this.latCount=new Int32Array(e);}bucket(e){let t=Math.floor(e/this.stepMs),i=t*this.stepMs,r=t%this.size;return this.timestamps[r]!==i&&(this.timestamps[r]=i,this.acquired[r]=0,this.released[r]=0,this.timeouts[r]=0,this.ifSum[r]=0,this.ifCount[r]=0,this.ifMax[r]=0,this.qSum[r]=0,this.qCount[r]=0,this.qMax[r]=0,this.latSum[r]=0,this.latCount[r]=0),r}addInflight(e,t){this.ifSum[e]+=t,this.ifCount[e]++,t>this.ifMax[e]&&(this.ifMax[e]=t);}addQueue(e,t){this.qSum[e]+=t,this.qCount[e]++,t>this.qMax[e]&&(this.qMax[e]=t);}recordAcquire(e,t,i){let r=this.bucket(e);this.acquired[r]++,this.addInflight(r,t),this.addQueue(r,i);}recordAcquireQueued(e,t,i,r){let s=this.bucket(e);this.acquired[s]++,this.latSum[s]+=t,this.latCount[s]++,this.addInflight(s,i),this.addQueue(s,r);}recordRelease(e,t,i){let r=this.bucket(e);this.released[r]++,this.addInflight(r,t),this.addQueue(r,i);}recordTimeoutQueue(e,t){let i=this.bucket(e);this.timeouts[i]++,this.addQueue(i,t);}sampleBoth(e,t,i){let r=this.bucket(e);this.addInflight(r,t),this.addQueue(r,i);}addAcquired(e){this.acquired[this.bucket(e)]++;}addReleased(e){this.released[this.bucket(e)]++;}addTimeout(e){this.timeouts[this.bucket(e)]++;}addLatency(e,t){let i=this.bucket(e);this.latSum[i]+=t,this.latCount[i]++;}sampleInflight(e,t){this.addInflight(this.bucket(e),t);}sampleQueue(e,t){this.addQueue(this.bucket(e),t);}snapshot(e){let t=Math.floor(e/this.stepMs)*this.stepMs-(this.size-1)*this.stepMs,i=0,r=0,s=0,h=0,p=0,m=0,f=0,c=0,d=0,g=0,v=0;for(let l=0;l<this.size;l++)this.timestamps[l]<t||(i+=this.acquired[l],r+=this.released[l],s+=this.timeouts[l],h+=this.ifSum[l],p+=this.ifCount[l],this.ifMax[l]>m&&(m=this.ifMax[l]),f+=this.qSum[l],c+=this.qCount[l],this.qMax[l]>d&&(d=this.qMax[l]),g+=this.latSum[l],v+=this.latCount[l]);return {counts:{acquired:i,released:r,timeouts:s},inflight:{avg:p===0?0:h/p,max:m,samples:p},queue:{avg:c===0?0:f/c,max:d,samples:c},latency:{avg:v===0?0:g/v,count:v,total:g}}}reset(){this.timestamps.fill(0),this.acquired.fill(0),this.released.fill(0),this.timeouts.fill(0),this.ifSum.fill(0),this.ifCount.fill(0),this.ifMax.fill(0),this.qSum.fill(0),this.qCount.fill(0),this.qMax.fill(0),this.latSum.fill(0),this.latCount.fill(0);}},M=[{size:60,stepMs:1e3},{size:60,stepMs:5e3},{size:60,stepMs:15e3},{size:60,stepMs:6e4},{size:60,stepMs:144e4}],I=class{constructor(e=M){this._totalAcquiredFast=0;this._totalAcquiredQueued=0;this._totalReleased=0;this._totalTimeouts=0;this._totalAborts=0;this._capacity=0;this._circuitOpen=false;this._circuitHalfOpen=false;if(e.length===0)throw new Error("SemaphoreMetrics requires at least one WindowOptions");this.windows=e.map(({size:t,stepMs:i})=>new E(t,i)),this.windowLabels=e.map(({size:t,stepMs:i})=>{let r=t*i;return r>=36e5&&r%36e5===0?`${r/36e5}h`:r>=6e4&&r%6e4===0?`${r/6e4}m`:r>=1e3&&r%1e3===0?`${r/1e3}s`:`${r}ms`});}onAcquireFast(e,t,i){this._totalAcquiredFast++;for(let r of this.windows)r.recordAcquire(e,t,i);}onAcquireQueued(e,t,i,r){this._totalAcquiredQueued++;for(let s of this.windows)s.recordAcquireQueued(e,t,i,r);}onRelease(e,t,i){this._totalReleased++;for(let r of this.windows)r.recordRelease(e,t,i);}onTimeout(e,t){this._totalTimeouts++;for(let i of this.windows)i.recordTimeoutQueue(e,t);}onAbort(e,t){this._totalAborts++;for(let i of this.windows)i.sampleQueue(e,t);}sampleGauges(e,t,i){for(let r of this.windows)r.sampleBoth(e,t,i);}sampleQueueDepthAt(e,t){for(let i of this.windows)i.sampleQueue(e,t);}markAcquireFast(){this._totalAcquiredFast++;let e=Date.now();for(let t of this.windows)t.addAcquired(e);}markAcquireQueued(e){this._totalAcquiredQueued++;let t=Date.now();for(let i of this.windows)i.addAcquired(t),i.addLatency(t,e);}markReleased(){this._totalReleased++;let e=Date.now();for(let t of this.windows)t.addReleased(e);}markTimeout(){this._totalTimeouts++;let e=Date.now();for(let t of this.windows)t.addTimeout(e);}markAbort(){this._totalAborts++;}sampleInFlight(e){let t=Date.now();for(let i of this.windows)i.sampleInflight(t,e);}sampleQueueDepth(e){let t=Date.now();for(let i of this.windows)i.sampleQueue(t,e);}markCapacityChange(e){this._capacity=e;}markCircuitOpen(){this._circuitOpen=true,this._circuitHalfOpen=false;}markCircuitHalfOpen(){this._circuitOpen=false,this._circuitHalfOpen=true;}markCircuitClose(){this._circuitOpen=false,this._circuitHalfOpen=false;}getSnapshot(){let e=Date.now(),t={},i=0,r=0;for(let s=0;s<this.windowLabels.length;s++){let h=this.windows[s].snapshot(e);t[this.windowLabels[s]]=h,s===0&&(i=h.inflight.avg,r=h.queue.avg);}return {windows:t,meta:{inFlightLastMinute:Math.round(i),queueDepthLastMinute:Math.round(r),totalAcquiredFast:this._totalAcquiredFast,totalAcquiredQueued:this._totalAcquiredQueued,totalReleased:this._totalReleased,totalTimeouts:this._totalTimeouts,totalAborts:this._totalAborts,capacity:this._capacity,circuitOpen:this._circuitOpen,circuitHalfOpen:this._circuitHalfOpen}}}reset(){for(let e of this.windows)e.reset();this._totalAcquiredFast=0,this._totalAcquiredQueued=0,this._totalReleased=0,this._totalTimeouts=0,this._totalAborts=0,this._capacity=0,this._circuitOpen=false,this._circuitHalfOpen=false;}destroy(){this.reset();}};var k={fifo:(a,e)=>a.id-e.id,lifo:(a,e)=>e.id-a.id,fifoWithPriority:(a,e)=>a.priority-e.priority||a.id-e.id,lifoWithPriority:(a,e)=>a.priority-e.priority||e.id-a.id};function x(a){if(a.comparator!==void 0){if(typeof a.comparator!="function")throw new o("Semaphore comparator must be a function","INVALID_ARGUMENT");return a.comparator}let e=a.queueOrder??"fifoWithPriority",t=k[e];if(t===void 0)throw new o(`Semaphore queueOrder must be one of: ${Object.keys(k).join(", ")} (got ${JSON.stringify(e)})`,"INVALID_ARGUMENT");return t}function C(a){let e=x(a);return (t,i)=>{if(t.isProbe!==i.isProbe)return t.isProbe?-1:1;let r=e(t,i);return typeof r!="number"||Number.isNaN(r)?t.id-i.id:r}}var n={TASKACQUIRE:"task-acquire",TASKRELEASE:"task-release",TASKTIMEOUT:"task-timeout",TASKABORT:"task-abort",QUEUEPURGE:"queue-purge",CIRCUITOPEN:"circuit-open",CIRCUITHALFOPEN:"circuit-half-open",CIRCUITCLOSE:"circuit-close",SHUTDOWN:"shutdown"};var _=class{constructor(e,t={}){this.pendingReleaseCount=0;this.releaseGeneration=0;this.scheduled=false;this.isShutdown=false;this.taskIdCounter=0;this.totalAcquired=0;this.totalReleased=0;this.totalTimeouts=0;this.eventListeners=new Map;this.drainPromise=null;this.drainResolve=null;this.purgeIntervalId=null;this.timeoutTimerId=null;u(e,"Semaphore count",0,Number.MAX_SAFE_INTEGER,true,false),this.purgeIntervalMs=u(t.purgeIntervalMs??3e3,"Semaphore purgeIntervalMs",500,Number.MAX_SAFE_INTEGER,true,true),this.queueMaxTimeout=u(t.queueMaxTimeout??1e4,"Semaphore queueMaxTimeout",1,Number.MAX_SAFE_INTEGER,true,true),this.queueMaxLength=u(t.queueMaxLength??1024,"Semaphore queueMaxLength",1,Number.MAX_SAFE_INTEGER,true,true),this.queueMaxAge=u(t.queueMaxAge??3e4,"Semaphore queueMaxAge",1,Number.MAX_SAFE_INTEGER,true,true),this.rejectOnFull=t.rejectOnFull??false,this.metricsEnabled=t.metricsEnabled??true,this.debug=t.debug??false;let i=C({queueOrder:t.queueOrder,comparator:t.comparator});this.queue=new S(i),this.enqueueOrder=new q,this.permits=new T(e),this.circuit=new b({threshold:t.circuitBreakerThreshold,window:t.circuitBreakerWindow,cooldown:t.circuitBreakerCooldown,minThroughput:t.circuitBreakerMinThroughput,minFailures:t.circuitBreakerMinFailures}),this.backoff=new w({initialTimeout:t.backoffInitialTimeout,maxTimeout:t.backoffMaxTimeout,decayFactor:t.backoffDecayFactor}),this.metricsCollector=this.metricsEnabled?new I:void 0,this.metricsCollector?.markCapacityChange(this.permits.capacity),this.metricsCollector?.sampleGauges(Date.now(),this.permits.inFlight,this.queue.size),this._startPurgeInterval();}on(e,t){this.eventListeners.has(e)||this.eventListeners.set(e,new Set),this.eventListeners.get(e).add(t);}off(e,t){this.eventListeners.get(e)?.delete(t),this.eventListeners.get(e)?.size===0&&this.eventListeners.delete(e);}removeAllListeners(e){e?this.eventListeners.delete(e):this.eventListeners.clear();}hasListeners(e){let t=this.eventListeners.get(e);return t!==void 0&&t.size>0}emit(e,...t){let i=this.eventListeners.get(e);if(!(!i||i.size===0)){if(i.size===1){let r=i.values().next().value;try{r(...t);}catch(s){this.debug&&console.warn(`[Semaphore] Error in listener for "${e}":`,s);}return}for(let r of Array.from(i))try{r(...t);}catch(s){this.debug&&console.warn(`[Semaphore] Error in listener for "${e}":`,s);}}}_tryAcquireFast(e){return this.circuit.isHalfOpen?this.circuit.hasProbeInFlight||!this.permits.hasCapacityFor(e)?null:(this.circuit.markProbeInFlight(),this.permits.acquire(e),this.totalAcquired++,this.metricsCollector?.onAcquireFast(Date.now(),this.permits.inFlight,this.queue.size),this.hasListeners(n.TASKACQUIRE)&&this.emit(n.TASKACQUIRE,{queued:this.queue.size,running:this.permits.capacity-this.permits.available,probe:true}),this._createRelease(true,e)):this.queue.size>0||!this.permits.hasCapacityFor(e)?null:(this.permits.acquire(e),this.totalAcquired++,this.metricsCollector?.onAcquireFast(Date.now(),this.permits.inFlight,this.queue.size),this.hasListeners(n.TASKACQUIRE)&&this.emit(n.TASKACQUIRE,{queued:this.queue.size,running:this.permits.capacity-this.permits.available}),this._createRelease(false,e))}_createRelease(e=false,t=1){this.pendingReleaseCount++;let i=this.releaseGeneration,r=false;return ()=>{if(r||i!==this.releaseGeneration){this.debug&&console.warn("[Semaphore] release() called after already released (no-op)");return}r=true,this.pendingReleaseCount--,this.permits.release(t),this.totalReleased++,this.metricsCollector?.onRelease(Date.now(),this.permits.inFlight,this.queue.size),e&&this.circuit.isHalfOpen&&(this.circuit.handleProbeSuccess(),this.emit(n.CIRCUITCLOSE),this.metricsCollector?.markCircuitClose(),this.debug&&console.info("[Semaphore] Circuit closed after successful probe")),this.permits.assertInvariant(this.debug),this.hasListeners(n.TASKRELEASE)&&this.emit(n.TASKRELEASE,{queued:this.queue.size,running:this.permits.capacity-this.permits.available}),this.schedule();}}_enqueue(e){this.queue.insert(e),this.enqueueOrder.pushTail(e),this._armTimeout();}_dequeue(e){this.queue.delete(e)!==void 0&&(this.enqueueOrder.remove(e),this.enqueueOrder.size===0&&this._clearTimeout());}_armTimeout(){if(this.timeoutTimerId!==null)return;let e=this.enqueueOrder.peekHead();if(e===void 0)return;let t=Math.max(0,e.enqueueTime+this.queueMaxTimeout-Date.now());this.timeoutTimerId=setTimeout(()=>this._fireTimeout(),t);}_clearTimeout(){this.timeoutTimerId!==null&&(clearTimeout(this.timeoutTimerId),this.timeoutTimerId=null);}_fireTimeout(){if(this.timeoutTimerId=null,this.isShutdown)return;let e=Date.now(),t=0,i=this.enqueueOrder.peekHead();for(;i!==void 0&&i.enqueueTime+this.queueMaxTimeout<=e;){let r=i.claim();this._dequeue(i),r&&(this._timeoutTask(i),t++),i=this.enqueueOrder.peekHead();}if(i!==void 0){let r=Math.max(0,i.enqueueTime+this.queueMaxTimeout-e);this.timeoutTimerId=setTimeout(()=>this._fireTimeout(),r);}t>0&&this.schedule();}_timeoutTask(e){if(this.totalTimeouts++,this.circuit.recordTimeout(),this.backoff.onTimeout(),e.isProbe)this.circuit.handleProbeFailure(),this.emit(n.CIRCUITOPEN,{timeoutRate:1,recentTimeouts:1,total:1,reason:"half-open-probe-failed"}),this.metricsCollector?.markCircuitOpen(),this.debug&&console.warn("[Semaphore] Circuit re-opened: half-open probe timed out");else {let t=this.circuit.evaluateAndTrip();t.tripped&&(this.emit(n.CIRCUITOPEN,{timeoutRate:t.timeoutRate,recentTimeouts:t.failures,total:t.attempts}),this.metricsCollector?.markCircuitOpen(),this.debug&&console.warn(`[Semaphore] Circuit opened. Rate: ${(t.timeoutRate*100).toFixed(1)}%`));}this.metricsCollector?.onTimeout(Date.now(),this.queue.size),this.emit(n.TASKTIMEOUT,{queueLength:this.queue.size,backoffDelay:this.backoff.currentDelay,taskId:e.id}),this.debug&&console.warn(`[Semaphore] Task #${e.id} timed out after ${this.queueMaxTimeout}ms`),e.reject(new o(`Semaphore acquire timed out after ${this.queueMaxTimeout}ms (queue: ${this.queue.size})`,"TIMEOUT"));}_onTaskAbort(e,t){this._dequeue(e),e.isProbe&&this.circuit.releaseProbeSlot(),this.metricsCollector?.onAbort(Date.now(),this.queue.size),this.emit(n.TASKABORT),this.debug&&console.info(`[Semaphore] Task #${e.id} aborted`),t(new o("Semaphore acquire aborted","ABORTED")),this.schedule();}schedule(){if(this.scheduled||this.isShutdown)return;this.scheduled=true;let e=this.backoff.currentDelay;e>0?setTimeout(()=>{this._runScheduler();},e).unref?.():queueMicrotask(()=>{this._runScheduler();});}_runScheduler(){if(!this.isShutdown){this.scheduled=false;try{for(;this.queue.size>0&&!this.circuit.isOpen;){let e=this.queue.peek();if(!e||this.circuit.isHalfOpen&&e.id!==this.circuit.probeTaskId||!this.permits.hasCapacityFor(e.weight))break;let t=this.queue.pop();this.enqueueOrder.remove(t),this.enqueueOrder.size===0&&this._clearTimeout();let i=Date.now(),r=Math.max(0,i-t.enqueueTime);this.permits.acquire(t.weight),this.totalAcquired++,this.metricsCollector?.onAcquireQueued(i,r,this.permits.inFlight,this.queue.size),this.permits.assertInvariant(this.debug),t.dispatch(()=>this._createRelease(t.isProbe,t.weight))&&this.hasListeners(n.TASKACQUIRE)&&this.emit(n.TASKACQUIRE,{queued:this.queue.size,running:this.permits.capacity-this.permits.available,...t.isProbe?{probe:!0}:{}});}this.queue.size===0&&this.permits.available===this.permits.capacity&&this.drainResolve&&(this.drainResolve(),this.drainResolve=null,this.drainPromise=null);}catch(e){e instanceof Error?console.error("[Semaphore] Scheduler error:",e.message,e.stack):console.error("[Semaphore] Scheduler error:",e);}}}_startPurgeInterval(){this.purgeIntervalId!==null&&clearInterval(this.purgeIntervalId),this.purgeIntervalId=setInterval(()=>{this.isShutdown||this._purgeStaleTasks();},this.purgeIntervalMs),this.purgeIntervalId.unref?.();}_purgeStaleTasks(){let e=Date.now(),t=this.queue.size,i=this.enqueueOrder.peekHead();for(;i!==void 0&&e-i.enqueueTime>this.queueMaxAge;){this.circuit.probeTaskId===i.id&&this.circuit.releaseProbeSlot();let r=i.discard(new o(`Task purged after ${this.queueMaxAge}ms`,"PURGED"));this._dequeue(i),r&&(this.totalTimeouts++,this.metricsCollector?.onTimeout(Date.now(),this.queue.size),this.emit(n.QUEUEPURGE,i),this.debug&&console.warn(`[Semaphore] Purged stale task #${i.id}`)),i=this.enqueueOrder.peekHead();}this.debug&&this.queue.size<t&&console.info(`[Semaphore] Purged ${t-this.queue.size} stale tasks`),this.queue.size<t&&this.schedule();}tryAcquire(e=1){return this.isShutdown||!Number.isInteger(e)||e<1||e>this.permits.capacity||(this.circuit.checkAndTransition()&&(this.emit(n.CIRCUITHALFOPEN),this.metricsCollector?.markCircuitHalfOpen(),this.debug&&console.info("[Semaphore] Circuit entering half-open"),this.schedule()),this.circuit.isOpen)?null:(this.circuit.trackAttempt(),this._tryAcquireFast(e))}acquire(e,t=0,i=1){if(this.isShutdown)return Promise.reject(new o("Semaphore is shut down","SHUTDOWN"));if(!Number.isInteger(i)||i<1||i>this.permits.capacity)return Promise.reject(new o(`Invalid weight: ${i} (must be integer in 1..${this.permits.capacity})`,"INVALID_WEIGHT"));if(typeof t!="number"||!Number.isFinite(t))return Promise.reject(new o(`Invalid priority: ${t} (must be a finite number)`,"INVALID_PRIORITY"));if(this.circuit.checkAndTransition()&&(this.emit(n.CIRCUITHALFOPEN),this.metricsCollector?.markCircuitHalfOpen(),this.debug&&console.info("[Semaphore] Circuit entering half-open"),this.schedule()),this.circuit.isOpen)return Promise.reject(new o(`Circuit breaker open, retry in ${this.circuit.cooldownRemaining}ms`,"CIRCUIT_OPEN"));if(this.circuit.isHalfOpen&&this.circuit.hasProbeInFlight)return Promise.reject(new o("Circuit breaker half-open, probe in flight","CIRCUIT_HALF_OPEN"));if(e?.aborted)return Promise.reject(new o("Semaphore acquire aborted before start","ABORTED"));this.circuit.trackAttempt();let r=this._tryAcquireFast(i);if(r)return Promise.resolve(r);let s=this.circuit.isHalfOpen&&!this.circuit.hasProbeInFlight;return !s&&this.rejectOnFull?Promise.reject(new o("Semaphore at capacity (rejectOnFull)","QUEUE_FULL")):!s&&this.queue.size>=this.queueMaxLength?Promise.reject(new o(`Queue full (${this.queueMaxLength})`,"QUEUE_FULL")):new Promise((h,p)=>{let m=++this.taskIdCounter,f=Date.now(),c=s,d=new y({id:m,priority:c?Number.MIN_SAFE_INTEGER:t,enqueueTime:f,isProbe:c,resolve:h,reject:p,abortSignal:e,weight:i});d.arm(()=>this._onTaskAbort(d,p)),c&&this.circuit.claimProbeSlot(m),this.metricsCollector?.sampleQueueDepthAt(Date.now(),this.queue.size+1),this._enqueue(d),this.schedule();})}async use(e,t,i=0,r=1){let s=await this.acquire(t,i,r);try{return await e()}finally{s();}}drain(e){return this.isShutdown?Promise.reject(new o("Cannot drain: semaphore is shut down","SHUTDOWN")):(e!==void 0&&u(e,"drain timeoutMs",1,Number.MAX_SAFE_INTEGER,true,true),this.drainPromise?this.drainPromise:this.queue.size===0&&this.permits.available===this.permits.capacity?Promise.resolve():(this.drainPromise=new Promise((t,i)=>{if(this.drainResolve=t,e!==void 0){let r=setTimeout(()=>{this.drainResolve=null,this.drainPromise=null,i(new o(`drain() timed out after ${e}ms`,"TIMEOUT"));},e),s=this.drainResolve;this.drainResolve=()=>{clearTimeout(r),s();};}}),this.drainPromise))}reset(e={}){if(this.isShutdown)throw new o("Cannot reset a semaphore that has been shut down","SHUTDOWN");for(let t of this.queue.toArray())t.discard(new o("Semaphore reset","SHUTDOWN"));this.queue.clear(),this.enqueueOrder.clear(),this._clearTimeout(),this.metricsCollector?.reset(),this.permits.reset(),this.backoff.reset(),this.circuit.reset(),this.releaseGeneration++,this.pendingReleaseCount=0,this.scheduled=false,this.isShutdown=false,this.taskIdCounter=0,this.totalAcquired=0,this.totalReleased=0,this.totalTimeouts=0,e.clearListeners&&this.eventListeners.clear(),this.drainResolve&&(this.drainResolve(),this.drainResolve=null,this.drainPromise=null),this._startPurgeInterval(),this.metricsCollector?.markCapacityChange(this.permits.capacity),this.metricsCollector?.sampleGauges(Date.now(),this.permits.inFlight,this.queue.size),this.debug&&console.info("[Semaphore] Reset to initial state");}shutdown(e="Semaphore shutdown"){if(!this.isShutdown){this.isShutdown=true,this.debug&&console.info(`[Semaphore] Shutdown: ${e}`),this.purgeIntervalId!==null&&(clearInterval(this.purgeIntervalId),this.purgeIntervalId=null),this._clearTimeout();for(let t of this.queue.toArray())t.discard(new o(e,"SHUTDOWN"));this.queue.clear(),this.enqueueOrder.clear(),this.metricsCollector?.destroy(),this.drainResolve&&(this.drainResolve(),this.drainResolve=null,this.drainPromise=null),this.emit(n.SHUTDOWN,e);}}cancel(){if(this.isShutdown)return;let e=this.queue.toArray();for(let t of e)t.isProbe&&this.circuit.releaseProbeSlot(),t.discard(new o("Semaphore acquire cancelled","CANCELLED")),this._dequeue(t);this.metricsCollector?.sampleQueueDepthAt(Date.now(),this.queue.size),this.debug&&console.info(`[Semaphore] Cancelled ${e.length} queued tasks`),this.schedule();}status(){let e=this.metricsCollector?.getSnapshot()??null,t=e?.windows?.["1m"]?.counts,i=t?.acquired??0,r=t?.timeouts??0,s=this.enqueueOrder.peekHead(),h=s===void 0?0:Math.max(0,Date.now()-s.enqueueTime);return {status:{running:this.permits.capacity-this.permits.available,queued:this.queue.size,available:this.permits.available,inFlight:this.permits.inFlight,pendingReleases:this.pendingReleaseCount,circuitOpen:this.circuit.isOpen,circuitHalfOpen:this.circuit.isHalfOpen,backoffDelay:Math.round(this.backoff.currentDelay),requestsPerSecond:+(i/60).toFixed(2),timeoutRate1m:i+r>0?+(r/(i+r)*100).toFixed(1):0,queueAge:h},lifetime:{totalAcquired:this.totalAcquired,totalReleased:this.totalReleased,totalTimeouts:this.totalTimeouts,circuitBreakerCooldownRemaining:this.circuit.cooldownRemaining},metrics:e}}isAvailable(){return !this.isShutdown&&!this.circuit.isOpen&&!this.permits.isFull}get queueLength(){return this.queue.size}get availablePermits(){return this.permits.available}};exports.CircuitBreaker=b;exports.QUEUE_ORDERINGS=k;exports.Semaphore=_;exports.SemaphoreError=o;exports.SemaphoreEvents=n;//# sourceMappingURL=index.cjs.map | ||
| 'use strict';var a=class extends Error{constructor(e,t){super(e),this.name="SemaphoreError",this.code=t;}};var l=(o,e,t,i,r=true,s=true)=>{if(typeof o!="number"||Number.isNaN(o))throw new a(`${e} must be a valid number`,"INVALID_ARGUMENT");if(r&&!Number.isInteger(o))throw new a(`${e} must be an integer`,"INVALID_ARGUMENT");if(s?o<t:o<=t)throw new a(`${e} must be ${s?">=":">"} ${t}`,"INVALID_ARGUMENT");if(s?o>i:o>=i)throw new a(`${e} must be ${s?"<=":"<"} ${i}`,"INVALID_ARGUMENT");return o};var w=class{constructor(e){this._inFlight=0;this.capacity=e,this._available=e;}get available(){return this._available}get inFlight(){return this._inFlight}get isFull(){return this._available===0}hasCapacityFor(e){return this._available>=e}acquire(e=1){this._available-=e,this._inFlight+=e;}release(e=1){this._available=Math.min(this._available+e,this.capacity),this._inFlight=this._inFlight<e?0:this._inFlight-e;}assertInvariant(e){if(!e)return;let t=this._inFlight+this._available;t!==this.capacity&&console.error(`[Semaphore] Invariant violation: inFlight(${this._inFlight}) + available(${this._available}) = ${t}, expected capacity(${this.capacity})`);}reset(){this._available=this.capacity,this._inFlight=0;}};var A=class{constructor(e,t){this.cachedIndex=-1;this.cachedUntil=0;this.size=e,this.stepMs=t,this.buckets=new Int32Array(e*2),this.timestamps=new Float64Array(e);}idx(){let e=Date.now();if(e<this.cachedUntil&&e>=this.cachedUntil-this.stepMs)return this.cachedIndex;let t=Math.floor(e/this.stepMs)*this.stepMs,i=Math.floor(t/this.stepMs)%this.size;return this.timestamps[i]!==t&&(this.timestamps[i]=t,this.buckets[i*2]=0,this.buckets[i*2+1]=0),this.cachedIndex=i,this.cachedUntil=t+this.stepMs,i}addAcquired(){this.buckets[this.idx()*2]++;}addTimeout(){this.buckets[this.idx()*2+1]++;}snapshot(){let e=Date.now(),t=Math.floor(e/this.stepMs)*this.stepMs-(this.size-1)*this.stepMs,i=0,r=0;for(let s=0;s<this.size;s++)this.timestamps[s]>=t&&(i+=this.buckets[s*2],r+=this.buckets[s*2+1]);return {acquired:i,timeouts:r}}reset(){this.cachedIndex=-1,this.cachedUntil=0,this.buckets.fill(0),this.timestamps.fill(0);}},f=class{constructor(e={}){this.state="closed";this.openUntil=0;this.probeInFlight=false;this._probeTaskId=null;if(this.threshold=l(e.threshold??.5,"SaturationCircuitBreaker threshold",0,1,false,false),this.window=l(e.window??1e4,"SaturationCircuitBreaker window",1e3,Number.MAX_SAFE_INTEGER,true,true),this.windowBucketWidth=l(e.windowBucketWidth??1e3,"SaturationCircuitBreaker windowBucketWidth",1,Number.MAX_SAFE_INTEGER,true,true),this.cooldown=l(e.cooldown??5e3,"SaturationCircuitBreaker cooldown",1e3,Number.MAX_SAFE_INTEGER,true,true),this.minThroughput=l(e.minThroughput??10,"SaturationCircuitBreaker minThroughput",1,Number.MAX_SAFE_INTEGER,true,true),this.minFailures=l(e.minFailures??5,"SaturationCircuitBreaker minFailures",1,Number.MAX_SAFE_INTEGER,true,true),this.eventWindow=new A(Math.ceil(this.window/this.windowBucketWidth),this.windowBucketWidth),this.window<this.windowBucketWidth)throw new a("SaturationCircuitBreaker window must be >= windowBucketWidth","INVALID_ARGUMENT");if(this.minThroughput<this.minFailures)throw new a("SaturationCircuitBreaker minThroughput must be >= minFailures","INVALID_ARGUMENT")}get isClosed(){return this.state==="closed"}get isOpen(){return this.state==="open"}get isHalfOpen(){return this.state==="half-open"}get hasProbeInFlight(){return this.probeInFlight}get probeTaskId(){return this._probeTaskId}get cooldownRemaining(){return this.isOpen?Math.max(0,this.openUntil-Date.now()):0}trackAttempt(){this.state==="closed"&&this.eventWindow.addAcquired();}recordFailure(){this.eventWindow.addTimeout();}checkAndTransition(){return this.state!=="open"||Date.now()<this.openUntil?false:(this.state="half-open",this.probeInFlight=false,this._probeTaskId=null,true)}evaluateAndTrip(){if(this.state!=="closed")return {tripped:false};let{timeouts:e,acquired:t}=this.eventWindow.snapshot();if(t<this.minThroughput||e<this.minFailures)return {tripped:false};let i=e/t;return i<this.threshold?{tripped:false}:(this.state="open",this.openUntil=Date.now()+this.cooldown,{tripped:true,timeoutRate:i,failures:e,attempts:t})}markProbeInFlight(){this.probeInFlight=true;}claimProbeSlot(e){this.probeInFlight=true,this._probeTaskId=e;}releaseProbeSlot(){this.probeInFlight=false,this._probeTaskId=null;}handleProbeSuccess(){this.state="closed",this.probeInFlight=false,this._probeTaskId=null,this.eventWindow.reset();}handleProbeFailure(){this.state="open",this.probeInFlight=false,this._probeTaskId=null,this.openUntil=Date.now()+this.cooldown;}reset(){this.state="closed",this.openUntil=0,this.probeInFlight=false,this._probeTaskId=null,this.eventWindow.reset();}};var T=class{constructor(e={}){this.delay=0;this.lastTimestamp=0;if(this.initialTimeout=l(e.initialTimeout??50,"BackoffTracker initialTimeout",0,Number.MAX_SAFE_INTEGER,true,true),this.maxTimeout=l(e.maxTimeout??2e3,"BackoffTracker maxTimeout",0,Number.MAX_SAFE_INTEGER,true,true),this.decayFactor=l(e.decayFactor??.5,"BackoffTracker decayFactor",0,1,false,false),this.maxTimeout<this.initialTimeout)throw new a("BackoffTracker maxTimeout must be >= initialTimeout","INVALID_ARGUMENT")}get currentDelay(){if(this.delay===0)return 0;let e=(Date.now()-this.lastTimestamp)/1e3;if(e<=0)return this.delay;let t=this.delay*Math.pow(this.decayFactor,e);return t<1?0:t}onTimeout(){let e=this.currentDelay,t=e>0?e*2:this.initialTimeout;this.delay=Math.min(t,this.maxTimeout),this.lastTimestamp=Date.now();}reset(){this.delay=0,this.lastTimestamp=0;}};var y=class{constructor(e){this.prev=null;this.next=null;this.heapIndex=-1;this.completed=false;this.id=e.id,this.priority=e.priority,this.enqueueTime=e.enqueueTime,this.isProbe=e.isProbe,this.weight=e.weight??1,this._resolve=e.resolve,this._reject=e.reject,this.abortSignal=e.abortSignal;}arm(e){this.abortSignal&&(this.abortListener=()=>{this.claim()&&e();},this.abortSignal.addEventListener("abort",this.abortListener));}dispatch(e){return this.claim()?(this._resolve(e()),true):false}discard(e){return this.claim()?(this._reject(e),true):false}claim(){return this.completed?false:(this.completed=true,this.abortSignal&&this.abortListener&&this.abortSignal.removeEventListener("abort",this.abortListener),true)}reject(e){this._reject(e);}};var S=class{constructor(e){this.heap=[];this.compare=e;}get size(){return this.heap.length}isEmpty(){return this.heap.length===0}peek(){return this.heap[0]}toArray(){return [...this.heap]}has(e){return e.heapIndex>=0&&this.heap[e.heapIndex]===e}clear(){for(let e of this.heap)e.heapIndex=-1;this.heap=[];}insert(e){if(e.heapIndex>=0)throw new a("Item is already in a heap","INVALID_ARGUMENT");let t=this.heap.length;this.heap.push(e),e.heapIndex=t,this._bubbleUp(t);}pop(){if(this.heap.length===0)return;let e=this.heap[0];e.heapIndex=-1;let t=this.heap.pop();return this.heap.length>0&&t!==e&&(this.heap[0]=t,t.heapIndex=0,this._bubbleDown(0)),e}delete(e){let t=e.heapIndex;if(t<0||this.heap[t]!==e)return;e.heapIndex=-1;let i=this.heap.pop();if(t===this.heap.length||i===e)return e;this.heap[t]=i,i.heapIndex=t;let r=this.heap[t-1>>1];return t>0&&this.compare(i,r)<0?this._bubbleUp(t):this._bubbleDown(t),e}_swap(e,t){let i=this.heap[e],r=this.heap[t];this.heap[e]=r,this.heap[t]=i,i.heapIndex=t,r.heapIndex=e;}_bubbleUp(e){for(;e>0;){let t=e-1>>1;if(this.compare(this.heap[e],this.heap[t])<0)this._swap(e,t),e=t;else break}}_bubbleDown(e){let t=this.heap.length;for(;;){let i=e,r=2*e+1,s=2*e+2;if(r<t&&this.compare(this.heap[r],this.heap[i])<0&&(i=r),s<t&&this.compare(this.heap[s],this.heap[i])<0&&(i=s),i===e)break;this._swap(e,i),e=i;}}};var I=class{constructor(){this.head=null;this.tail=null;this._size=0;}get size(){return this._size}isEmpty(){return this._size===0}peekHead(){return this.head===null?void 0:this.head}pushTail(e){e.prev=this.tail,e.next=null,this.tail!==null?this.tail.next=e:this.head=e,this.tail=e,this._size++;}remove(e){e.prev!==null?e.prev.next=e.next:this.head=e.next,e.next!==null?e.next.prev=e.prev:this.tail=e.prev,e.prev=e.next=null,this._size--;}clear(){this.head=this.tail=null,this._size=0;}};var E=class{constructor(e,t){this.cachedIndex=-1;this.cachedUntil=0;this.size=l(e,"CombinedWindow size",1,Number.MAX_SAFE_INTEGER,true,true),this.stepMs=l(t,"CombinedWindow stepMs",1,Number.MAX_SAFE_INTEGER,true,true),this.timestamps=new Float64Array(e),this.acquired=new Int32Array(e),this.released=new Int32Array(e),this.timeouts=new Int32Array(e),this.ifSum=new Float64Array(e),this.ifCount=new Int32Array(e),this.ifMax=new Float64Array(e),this.qSum=new Float64Array(e),this.qCount=new Int32Array(e),this.qMax=new Float64Array(e),this.latSum=new Float64Array(e),this.latCount=new Int32Array(e);}bucket(e){if(e<this.cachedUntil&&e>=this.cachedUntil-this.stepMs)return this.cachedIndex;let t=Math.floor(e/this.stepMs),i=t*this.stepMs,r=t%this.size;return this.timestamps[r]!==i&&(this.timestamps[r]=i,this.acquired[r]=0,this.released[r]=0,this.timeouts[r]=0,this.ifSum[r]=0,this.ifCount[r]=0,this.ifMax[r]=0,this.qSum[r]=0,this.qCount[r]=0,this.qMax[r]=0,this.latSum[r]=0,this.latCount[r]=0),this.cachedIndex=r,this.cachedUntil=i+this.stepMs,r}addInflight(e,t){this.ifSum[e]+=t,this.ifCount[e]++,t>this.ifMax[e]&&(this.ifMax[e]=t);}addQueue(e,t){this.qSum[e]+=t,this.qCount[e]++,t>this.qMax[e]&&(this.qMax[e]=t);}recordAcquire(e,t,i){let r=this.bucket(e);this.acquired[r]++,this.addInflight(r,t),this.addQueue(r,i);}recordAcquireQueued(e,t,i,r){let s=this.bucket(e);this.acquired[s]++,this.latSum[s]+=t,this.latCount[s]++,this.addInflight(s,i),this.addQueue(s,r);}recordRelease(e,t,i){let r=this.bucket(e);this.released[r]++,this.addInflight(r,t),this.addQueue(r,i);}recordTimeoutQueue(e,t){let i=this.bucket(e);this.timeouts[i]++,this.addQueue(i,t);}sampleBoth(e,t,i){let r=this.bucket(e);this.addInflight(r,t),this.addQueue(r,i);}addAcquired(e){this.acquired[this.bucket(e)]++;}addReleased(e){this.released[this.bucket(e)]++;}addTimeout(e){this.timeouts[this.bucket(e)]++;}addLatency(e,t){let i=this.bucket(e);this.latSum[i]+=t,this.latCount[i]++;}sampleInflight(e,t){this.addInflight(this.bucket(e),t);}sampleQueue(e,t){this.addQueue(this.bucket(e),t);}snapshot(e){let t=Math.floor(e/this.stepMs)*this.stepMs-(this.size-1)*this.stepMs,i=0,r=0,s=0,u=0,c=0,d=0,b=0,p=0,m=0,C=0,v=0;for(let h=0;h<this.size;h++)this.timestamps[h]<t||(i+=this.acquired[h],r+=this.released[h],s+=this.timeouts[h],u+=this.ifSum[h],c+=this.ifCount[h],this.ifMax[h]>d&&(d=this.ifMax[h]),b+=this.qSum[h],p+=this.qCount[h],this.qMax[h]>m&&(m=this.qMax[h]),C+=this.latSum[h],v+=this.latCount[h]);return {counts:{acquired:i,released:r,timeouts:s},inflight:{avg:c===0?0:u/c,max:d,samples:c},queue:{avg:p===0?0:b/p,max:m,samples:p},latency:{avg:v===0?0:C/v,count:v,total:C}}}reset(){this.cachedIndex=-1,this.cachedUntil=0,this.timestamps.fill(0),this.acquired.fill(0),this.released.fill(0),this.timeouts.fill(0),this.ifSum.fill(0),this.ifCount.fill(0),this.ifMax.fill(0),this.qSum.fill(0),this.qCount.fill(0),this.qMax.fill(0),this.latSum.fill(0),this.latCount.fill(0);}},R=[{size:60,stepMs:1e3},{size:60,stepMs:5e3},{size:60,stepMs:15e3},{size:60,stepMs:6e4},{size:60,stepMs:144e4}],k=class{constructor(e=R){this._totalAcquiredFast=0;this._totalAcquiredQueued=0;this._totalReleased=0;this._totalTimeouts=0;this._totalAborts=0;this._capacity=0;this._circuitOpen=false;this._circuitHalfOpen=false;if(e.length===0)throw new a("SemaphoreMetrics requires at least one WindowOptions","INVALID_ARGUMENT");this.windows=e.map(({size:r,stepMs:s})=>new E(r,s)),this.windowLabels=e.map(({size:r,stepMs:s})=>{let u=r*s;return u>=36e5&&u%36e5===0?`${u/36e5}h`:u>=6e4&&u%6e4===0?`${u/6e4}m`:u>=1e3&&u%1e3===0?`${u/1e3}s`:`${u}ms`});let t=new Set;for(let r of this.windowLabels){if(t.has(r))throw new a(`SemaphoreMetrics windows produce duplicate label "${r}" (two windows cover the same horizon)`,"INVALID_ARGUMENT");t.add(r);}let i=0;for(let r=1;r<e.length;r++)e[r].size*e[r].stepMs<e[i].size*e[i].stepMs&&(i=r);this.primaryLabel=this.windowLabels[i],this.primaryWindowMs=e[i].size*e[i].stepMs;}onAcquireFast(e,t,i){this._totalAcquiredFast++;for(let r of this.windows)r.recordAcquire(e,t,i);}onAcquireQueued(e,t,i,r){this._totalAcquiredQueued++;for(let s of this.windows)s.recordAcquireQueued(e,t,i,r);}onRelease(e,t,i){this._totalReleased++;for(let r of this.windows)r.recordRelease(e,t,i);}onTimeout(e,t){this._totalTimeouts++;for(let i of this.windows)i.recordTimeoutQueue(e,t);}onAbort(e,t){this._totalAborts++;for(let i of this.windows)i.sampleQueue(e,t);}sampleGauges(e,t,i){for(let r of this.windows)r.sampleBoth(e,t,i);}sampleQueueDepthAt(e,t){for(let i of this.windows)i.sampleQueue(e,t);}markAcquireFast(){this._totalAcquiredFast++;let e=Date.now();for(let t of this.windows)t.addAcquired(e);}markAcquireQueued(e){this._totalAcquiredQueued++;let t=Date.now();for(let i of this.windows)i.addAcquired(t),i.addLatency(t,e);}markReleased(){this._totalReleased++;let e=Date.now();for(let t of this.windows)t.addReleased(e);}markTimeout(){this._totalTimeouts++;let e=Date.now();for(let t of this.windows)t.addTimeout(e);}markAbort(){this._totalAborts++;}sampleInFlight(e){let t=Date.now();for(let i of this.windows)i.sampleInflight(t,e);}sampleQueueDepth(e){let t=Date.now();for(let i of this.windows)i.sampleQueue(t,e);}markCapacityChange(e){this._capacity=e;}markCircuitOpen(){this._circuitOpen=true,this._circuitHalfOpen=false;}markCircuitHalfOpen(){this._circuitOpen=false,this._circuitHalfOpen=true;}markCircuitClose(){this._circuitOpen=false,this._circuitHalfOpen=false;}getSnapshot(){let e=Date.now(),t={},i=0,r=0;for(let s=0;s<this.windowLabels.length;s++){let u=this.windows[s].snapshot(e);t[this.windowLabels[s]]=u,s===0&&(i=u.inflight.avg,r=u.queue.avg);}return {windows:t,meta:{inFlightLastMinute:Math.round(i),queueDepthLastMinute:Math.round(r),totalAcquiredFast:this._totalAcquiredFast,totalAcquiredQueued:this._totalAcquiredQueued,totalReleased:this._totalReleased,totalTimeouts:this._totalTimeouts,totalAborts:this._totalAborts,capacity:this._capacity,circuitOpen:this._circuitOpen,circuitHalfOpen:this._circuitHalfOpen}}}reset(){for(let e of this.windows)e.reset();this._totalAcquiredFast=0,this._totalAcquiredQueued=0,this._totalReleased=0,this._totalTimeouts=0,this._totalAborts=0,this._capacity=0,this._circuitOpen=false,this._circuitHalfOpen=false;}destroy(){this.reset();}};var _={fifo:(o,e)=>o.id-e.id,lifo:(o,e)=>e.id-o.id,fifoWithPriority:(o,e)=>o.priority-e.priority||o.id-e.id,lifoWithPriority:(o,e)=>o.priority-e.priority||e.id-o.id};function x(o){if(o.comparator!==void 0){if(typeof o.comparator!="function")throw new a("Semaphore comparator must be a function","INVALID_ARGUMENT");return o.comparator}let e=o.queueOrder??"fifoWithPriority",t=_[e];if(t===void 0)throw new a(`Semaphore queueOrder must be one of: ${Object.keys(_).join(", ")} (got ${JSON.stringify(e)})`,"INVALID_ARGUMENT");return t}function F(o){let e=x(o);return (t,i)=>{if(t.isProbe!==i.isProbe)return t.isProbe?-1:1;let r=e(t,i);return typeof r!="number"||Number.isNaN(r)?t.id-i.id:r}}var n={TASKACQUIRE:"task-acquire",TASKRELEASE:"task-release",TASKTIMEOUT:"task-timeout",TASKABORT:"task-abort",QUEUEPURGE:"queue-purge",QUEUEEVICT:"queue-evict",CIRCUITOPEN:"circuit-open",CIRCUITHALFOPEN:"circuit-half-open",CIRCUITCLOSE:"circuit-close",SHUTDOWN:"shutdown"};var M=class{constructor(e,t={}){this.pendingReleaseCount=0;this.releaseGeneration=0;this.scheduled=false;this.isShutdown=false;this.taskIdCounter=0;this.totalAcquired=0;this.totalReleased=0;this.totalTimeouts=0;this.totalEvictions=0;this.eventListeners=new Map;this.drainPromise=null;this.drainResolve=null;this.purgeIntervalId=null;this.timeoutTimerId=null;this._runSchedulerBound=()=>{this._runScheduler();};if(l(e,"Semaphore count",0,Number.MAX_SAFE_INTEGER,true,false),this.purgeIntervalMs=l(t.purgeIntervalMs??3e3,"Semaphore purgeIntervalMs",500,Number.MAX_SAFE_INTEGER,true,true),this.queueMaxTimeout=l(t.queueMaxTimeout??1e4,"Semaphore queueMaxTimeout",1,Number.MAX_SAFE_INTEGER,true,true),this.queueMaxLength=l(t.queueMaxLength??1024,"Semaphore queueMaxLength",1,Number.MAX_SAFE_INTEGER,true,true),this.queueMaxAge=l(t.queueMaxAge??3e4,"Semaphore queueMaxAge",1,Number.MAX_SAFE_INTEGER,true,true),this.rejectOnFull=t.rejectOnFull??false,this.metricsEnabled=t.metricsEnabled??true,this.debug=t.debug??false,t.circuitBreakerFailurePredicate!==void 0&&typeof t.circuitBreakerFailurePredicate!="function")throw new a("Semaphore circuitBreakerFailurePredicate must be a function","INVALID_ARGUMENT");this.failurePredicate=t.circuitBreakerFailurePredicate;let i=F({queueOrder:t.queueOrder,comparator:t.comparator});this.queue=new S(i),this.enqueueOrder=new I,this.permits=new w(e),this.circuit=t.circuitBreaker??new f({threshold:t.circuitBreakerThreshold,window:t.circuitBreakerWindow,windowBucketWidth:t.circuitBreakerWindowBucketWidth,cooldown:t.circuitBreakerCooldown,minThroughput:t.circuitBreakerMinThroughput,minFailures:t.circuitBreakerMinFailures}),this.backoff=new T({initialTimeout:t.backoffInitialTimeout,maxTimeout:t.backoffMaxTimeout,decayFactor:t.backoffDecayFactor}),this.metricsCollector=this.metricsEnabled?new k(t.metricsWindows):void 0,this.metricsCollector?.markCapacityChange(this.permits.capacity),this.metricsCollector?.sampleGauges(Date.now(),this.permits.inFlight,this.queue.size),this._startPurgeInterval();}on(e,t){this.eventListeners.has(e)||this.eventListeners.set(e,new Set),this.eventListeners.get(e).add(t);}off(e,t){this.eventListeners.get(e)?.delete(t),this.eventListeners.get(e)?.size===0&&this.eventListeners.delete(e);}removeAllListeners(e){e?this.eventListeners.delete(e):this.eventListeners.clear();}hasListeners(e){let t=this.eventListeners.get(e);return t!==void 0&&t.size>0}emit(e,...t){let i=this.eventListeners.get(e);if(!(!i||i.size===0)){if(i.size===1){let r=i.values().next().value;try{r(...t);}catch(s){console.warn(`[Semaphore] Error in listener for "${e}":`,s);}return}for(let r of Array.from(i))try{r(...t);}catch(s){console.warn(`[Semaphore] Error in listener for "${e}":`,s);}}}_tryAcquireFast(e){return this.circuit.isHalfOpen?this.circuit.hasProbeInFlight||!this.permits.hasCapacityFor(e)?null:(this.circuit.markProbeInFlight(),this.permits.acquire(e),this.totalAcquired++,this.metricsCollector?.onAcquireFast(Date.now(),this.permits.inFlight,this.queue.size),this.hasListeners(n.TASKACQUIRE)&&this.emit(n.TASKACQUIRE,{queued:this.queue.size,running:this.permits.capacity-this.permits.available,probe:true}),this._createRelease(true,e)):this.queue.size>0||!this.permits.hasCapacityFor(e)?null:(this.permits.acquire(e),this.totalAcquired++,this.metricsCollector?.onAcquireFast(Date.now(),this.permits.inFlight,this.queue.size),this.hasListeners(n.TASKACQUIRE)&&this.emit(n.TASKACQUIRE,{queued:this.queue.size,running:this.permits.capacity-this.permits.available}),this._createRelease(false,e))}_createRelease(e=false,t=1){this.pendingReleaseCount++;let i=this.releaseGeneration,r=false;return ()=>{if(r||i!==this.releaseGeneration){this.debug&&console.warn("[Semaphore] release() called after already released (no-op)");return}r=true,this.pendingReleaseCount--,this.permits.release(t),this.totalReleased++,this.metricsCollector?.onRelease(Date.now(),this.permits.inFlight,this.queue.size),e&&this.circuit.isHalfOpen&&(this.circuit.handleProbeSuccess(),this.emit(n.CIRCUITCLOSE),this.metricsCollector?.markCircuitClose(),this.debug&&console.info("[Semaphore] Circuit closed after successful probe")),this.permits.assertInvariant(this.debug),this.hasListeners(n.TASKRELEASE)&&this.emit(n.TASKRELEASE,{queued:this.queue.size,running:this.permits.capacity-this.permits.available}),this._checkDrain(),this.schedule();}}_checkDrain(){this.drainResolve&&this.queue.size===0&&this.permits.available===this.permits.capacity&&(this.drainResolve(),this.drainResolve=null,this.drainPromise=null);}_enqueue(e){this.queue.insert(e),this.enqueueOrder.pushTail(e),this._armTimeout();}_dequeue(e){this.queue.delete(e)!==void 0&&(this.enqueueOrder.remove(e),this.enqueueOrder.size===0&&this._clearTimeout());}_armTimeout(){if(this.timeoutTimerId!==null)return;let e=this.enqueueOrder.peekHead();if(e===void 0)return;let t=Math.max(0,e.enqueueTime+this.queueMaxTimeout-Date.now());this.timeoutTimerId=setTimeout(()=>this._fireTimeout(),t);}_clearTimeout(){this.timeoutTimerId!==null&&(clearTimeout(this.timeoutTimerId),this.timeoutTimerId=null);}_fireTimeout(){if(this.timeoutTimerId=null,this.isShutdown)return;let e=Date.now(),t=0,i=this.enqueueOrder.peekHead();for(;i!==void 0&&i.enqueueTime+this.queueMaxTimeout<=e;){let r=i.claim();this._dequeue(i),r&&(this._timeoutTask(i),t++),i=this.enqueueOrder.peekHead();}if(i!==void 0){let r=Math.max(0,i.enqueueTime+this.queueMaxTimeout-e);this.timeoutTimerId=setTimeout(()=>this._fireTimeout(),r);}t>0&&(this._checkDrain(),this.schedule());}_evictQueueOnCircuitOpen(e){if(this.queue.size===0)return;let t=0;for(let i of this.queue.toArray()){if(i===e||i.isProbe)continue;let r=i.discard(new a("Circuit breaker opened while task was queued","CIRCUIT_OPEN"));this._dequeue(i),r&&(t++,this.totalEvictions++,this.emit(n.QUEUEEVICT,{id:i.id,priority:i.priority,enqueueTime:i.enqueueTime,weight:i.weight}),this.debug&&console.warn(`[Semaphore] Evicted queued task #${i.id}: circuit opened`));}t>0&&this.metricsCollector?.sampleQueueDepthAt(Date.now(),this.queue.size);}_timeoutTask(e){if(this.totalTimeouts++,this.circuit.recordFailure(),this.backoff.onTimeout(),e.isProbe)this.circuit.handleProbeFailure(),this.emit(n.CIRCUITOPEN,{timeoutRate:1,recentTimeouts:1,total:1,reason:"half-open-probe-failed"}),this.metricsCollector?.markCircuitOpen(),this.debug&&console.warn("[Semaphore] Circuit re-opened: half-open probe timed out");else {let t=this.circuit.evaluateAndTrip();t.tripped&&(this.emit(n.CIRCUITOPEN,{timeoutRate:t.timeoutRate,recentTimeouts:t.failures,total:t.attempts}),this.metricsCollector?.markCircuitOpen(),this.debug&&console.warn(`[Semaphore] Circuit opened. Rate: ${(t.timeoutRate*100).toFixed(1)}%`),this._evictQueueOnCircuitOpen(e));}this.metricsCollector?.onTimeout(Date.now(),this.queue.size),this.emit(n.TASKTIMEOUT,{queueLength:this.queue.size,backoffDelay:this.backoff.currentDelay,taskId:e.id}),this.debug&&console.warn(`[Semaphore] Task #${e.id} timed out after ${this.queueMaxTimeout}ms`),e.reject(new a(`Semaphore acquire timed out after ${this.queueMaxTimeout}ms (queue: ${this.queue.size})`,"TIMEOUT"));}_onTaskAbort(e,t){this._dequeue(e),e.isProbe&&this.circuit.releaseProbeSlot(),this.metricsCollector?.onAbort(Date.now(),this.queue.size),this.emit(n.TASKABORT),this.debug&&console.info(`[Semaphore] Task #${e.id} aborted`),t(new a("Semaphore acquire aborted","ABORTED")),this._checkDrain(),this.schedule();}schedule(){if(this.scheduled||this.isShutdown)return;this.scheduled=true;let e=this.backoff.currentDelay;e>0?setTimeout(this._runSchedulerBound,e).unref?.():queueMicrotask(this._runSchedulerBound);}_runScheduler(){if(!this.isShutdown){this.scheduled=false;try{for(;this.queue.size>0&&!this.circuit.isOpen;){let e=this.queue.peek();if(!e||this.circuit.isHalfOpen&&e.id!==this.circuit.probeTaskId||!this.permits.hasCapacityFor(e.weight))break;let t=this.queue.pop();this.enqueueOrder.remove(t),this.enqueueOrder.size===0&&this._clearTimeout();let i=Date.now(),r=Math.max(0,i-t.enqueueTime);this.permits.acquire(t.weight),this.totalAcquired++,this.metricsCollector?.onAcquireQueued(i,r,this.permits.inFlight,this.queue.size),this.permits.assertInvariant(this.debug),t.dispatch(()=>this._createRelease(t.isProbe,t.weight))&&this.hasListeners(n.TASKACQUIRE)&&this.emit(n.TASKACQUIRE,{queued:this.queue.size,running:this.permits.capacity-this.permits.available,...t.isProbe?{probe:!0}:{}});}this._checkDrain();}catch(e){e instanceof Error?console.error("[Semaphore] Scheduler error:",e.message,e.stack):console.error("[Semaphore] Scheduler error:",e);}}}_startPurgeInterval(){this.purgeIntervalId!==null&&clearInterval(this.purgeIntervalId),this.purgeIntervalId=setInterval(()=>{this.isShutdown||this._purgeStaleTasks();},this.purgeIntervalMs),this.purgeIntervalId.unref?.();}_purgeStaleTasks(){let e=Date.now(),t=this.queue.size,i=this.enqueueOrder.peekHead();for(;i!==void 0&&e-i.enqueueTime>this.queueMaxAge;){this.circuit.probeTaskId===i.id&&this.circuit.releaseProbeSlot();let r=i.discard(new a(`Task purged after ${this.queueMaxAge}ms`,"PURGED"));this._dequeue(i),r&&(this.totalTimeouts++,this.metricsCollector?.onTimeout(Date.now(),this.queue.size),this.emit(n.QUEUEPURGE,{id:i.id,priority:i.priority,enqueueTime:i.enqueueTime,weight:i.weight}),this.debug&&console.warn(`[Semaphore] Purged stale task #${i.id}`)),i=this.enqueueOrder.peekHead();}this.debug&&this.queue.size<t&&console.info(`[Semaphore] Purged ${t-this.queue.size} stale tasks`),this.queue.size<t&&(this._checkDrain(),this.schedule());}tryAcquire(e=1){return this.isShutdown||!Number.isInteger(e)||e<1||e>this.permits.capacity||(this.circuit.checkAndTransition()&&(this.emit(n.CIRCUITHALFOPEN),this.metricsCollector?.markCircuitHalfOpen(),this.debug&&console.info("[Semaphore] Circuit entering half-open"),this.schedule()),this.circuit.isOpen)?null:this._tryAcquireFast(e)}acquire(e,t=0,i=1){if(this.isShutdown)return Promise.reject(new a("Semaphore is shut down","SHUTDOWN"));if(!Number.isInteger(i)||i<1||i>this.permits.capacity)return Promise.reject(new a(`Invalid weight: ${i} (must be integer in 1..${this.permits.capacity})`,"INVALID_WEIGHT"));if(typeof t!="number"||!Number.isFinite(t))return Promise.reject(new a(`Invalid priority: ${t} (must be a finite number)`,"INVALID_PRIORITY"));if(this.circuit.checkAndTransition()&&(this.emit(n.CIRCUITHALFOPEN),this.metricsCollector?.markCircuitHalfOpen(),this.debug&&console.info("[Semaphore] Circuit entering half-open"),this.schedule()),this.circuit.isOpen)return Promise.reject(new a(`Circuit breaker open, retry in ${this.circuit.cooldownRemaining}ms`,"CIRCUIT_OPEN"));if(this.circuit.isHalfOpen&&this.circuit.hasProbeInFlight)return Promise.reject(new a("Circuit breaker half-open, probe in flight","CIRCUIT_HALF_OPEN"));if(e?.aborted)return Promise.reject(new a("Semaphore acquire aborted before start","ABORTED"));this.circuit.trackAttempt();let r=this._tryAcquireFast(i);if(r)return Promise.resolve(r);let s=this.circuit.isHalfOpen&&!this.circuit.hasProbeInFlight;return !s&&this.rejectOnFull?Promise.reject(new a("Semaphore at capacity (rejectOnFull)","QUEUE_FULL")):!s&&this.queue.size>=this.queueMaxLength?Promise.reject(new a(`Queue full (${this.queueMaxLength})`,"QUEUE_FULL")):new Promise((u,c)=>{let d=++this.taskIdCounter,b=Date.now(),p=s,m=new y({id:d,priority:p?Number.MIN_SAFE_INTEGER:t,enqueueTime:b,isProbe:p,resolve:u,reject:c,abortSignal:e,weight:i});m.arm(()=>this._onTaskAbort(m,c)),p&&this.circuit.claimProbeSlot(d),this.metricsCollector?.sampleQueueDepthAt(Date.now(),this.queue.size+1),this._enqueue(m),this.schedule();})}peekQueue(){let e=[],t=this.enqueueOrder.peekHead();for(;t!==void 0;)e.push({id:t.id,priority:t.priority,enqueueTime:t.enqueueTime,weight:t.weight,isProbe:t.isProbe}),t=t.next??void 0;return e}async use(e,t,i=0,r=1){let s=await this.acquire(t,i,r),u=this.failurePredicate!==void 0&&this.circuit.isHalfOpen;try{let c=await e();return s(),c}catch(c){if(this.failurePredicate!==void 0){let d=false;try{d=this.failurePredicate(c)===!0;}catch(b){console.warn("[Semaphore] circuitBreakerFailurePredicate threw (rejection treated as non-matching):",b);}d&&(u&&this.circuit.isHalfOpen?(this.circuit.recordFailure(),this.circuit.handleProbeFailure(),this.emit(n.CIRCUITOPEN,{timeoutRate:1,recentTimeouts:1,total:1,reason:"half-open-probe-failed"}),this.metricsCollector?.markCircuitOpen(),this.debug&&console.warn("[Semaphore] Circuit re-opened: half-open probe operation failed")):this.reportFailure());}throw s(),c}}drain(e){return this.isShutdown?Promise.reject(new a("Cannot drain: semaphore is shut down","SHUTDOWN")):(e!==void 0&&l(e,"drain timeoutMs",1,Number.MAX_SAFE_INTEGER,true,true),this.drainPromise?this.drainPromise:this.queue.size===0&&this.permits.available===this.permits.capacity?Promise.resolve():(this.drainPromise=new Promise((t,i)=>{if(this.drainResolve=t,e!==void 0){let r=setTimeout(()=>{this.drainResolve=null,this.drainPromise=null,i(new a(`drain() timed out after ${e}ms`,"TIMEOUT"));},e),s=this.drainResolve;this.drainResolve=()=>{clearTimeout(r),s();};}}),this.drainPromise))}reset(e={}){if(this.isShutdown)throw new a("Cannot reset a semaphore that has been shut down","SHUTDOWN");for(let t of this.queue.toArray())t.discard(new a("Semaphore reset","SHUTDOWN"));this.queue.clear(),this.enqueueOrder.clear(),this._clearTimeout(),this.metricsCollector?.reset(),this.permits.reset(),this.backoff.reset(),this.circuit.reset(),this.releaseGeneration++,this.pendingReleaseCount=0,this.scheduled=false,this.isShutdown=false,this.taskIdCounter=0,this.totalAcquired=0,this.totalReleased=0,this.totalTimeouts=0,this.totalEvictions=0,e.clearListeners&&this.eventListeners.clear(),this.drainResolve&&(this.drainResolve(),this.drainResolve=null,this.drainPromise=null),this._startPurgeInterval(),this.metricsCollector?.markCapacityChange(this.permits.capacity),this.metricsCollector?.sampleGauges(Date.now(),this.permits.inFlight,this.queue.size),this.debug&&console.info("[Semaphore] Reset to initial state");}shutdown(e="Semaphore shutdown"){if(!this.isShutdown){this.isShutdown=true,this.debug&&console.info(`[Semaphore] Shutdown: ${e}`),this.purgeIntervalId!==null&&(clearInterval(this.purgeIntervalId),this.purgeIntervalId=null),this._clearTimeout();for(let t of this.queue.toArray())t.discard(new a(e,"SHUTDOWN"));this.queue.clear(),this.enqueueOrder.clear(),this.metricsCollector?.destroy(),this.metricsCollector=void 0,this.drainResolve&&(this.drainResolve(),this.drainResolve=null,this.drainPromise=null),this.emit(n.SHUTDOWN,e);}}cancel(){if(this.isShutdown)return;let e=this.queue.toArray();for(let t of e)t.isProbe&&this.circuit.releaseProbeSlot(),t.discard(new a("Semaphore acquire cancelled","CANCELLED")),this._dequeue(t);this.metricsCollector?.sampleQueueDepthAt(Date.now(),this.queue.size),this.debug&&console.info(`[Semaphore] Cancelled ${e.length} queued tasks`),this._checkDrain(),this.schedule();}reportFailure(){if(this.isShutdown)return;this.circuit.recordFailure();let e=this.circuit.evaluateAndTrip();e.tripped&&(this.emit(n.CIRCUITOPEN,{timeoutRate:e.timeoutRate,recentTimeouts:e.failures,total:e.attempts,reason:"reported-failure"}),this.metricsCollector?.markCircuitOpen(),this.debug&&console.warn(`[Semaphore] Circuit opened via reportFailure(). Rate: ${(e.timeoutRate*100).toFixed(1)}%`),this._evictQueueOnCircuitOpen());}status(){let e=this.metricsCollector?.getSnapshot()??null,t=this.metricsCollector!==void 0?e?.windows?.[this.metricsCollector.primaryLabel]?.counts:void 0,i=(this.metricsCollector?.primaryWindowMs??6e4)/1e3,r=t?.acquired??0,s=t?.timeouts??0,u=this.enqueueOrder.peekHead(),c=u===void 0?0:Math.max(0,Date.now()-u.enqueueTime);return {status:{running:this.permits.capacity-this.permits.available,queued:this.queue.size,available:this.permits.available,inFlight:this.permits.inFlight,pendingReleases:this.pendingReleaseCount,circuitOpen:this.circuit.isOpen,circuitHalfOpen:this.circuit.isHalfOpen,backoffDelay:Math.round(this.backoff.currentDelay),requestsPerSecond:+(r/i).toFixed(2),timeoutRate1m:r+s>0?+(s/(r+s)*100).toFixed(1):0,queueAge:c},lifetime:{totalAcquired:this.totalAcquired,totalReleased:this.totalReleased,totalTimeouts:this.totalTimeouts,totalEvictions:this.totalEvictions,circuitBreakerCooldownRemaining:this.circuit.cooldownRemaining},metrics:e}}get availablePermits(){return this.permits.available}isAvailable(){return !this.isShutdown&&!this.circuit.isOpen&&!this.permits.isFull}get capacity(){return this.permits.capacity}get circuitState(){return this.circuit.state}get queueLength(){return this.queue.size}};var O={tripped:false},q=class{constructor(){this.state="closed";this.isOpen=false;this.isHalfOpen=false;this.hasProbeInFlight=false;this.probeTaskId=null;this.cooldownRemaining=0;}checkAndTransition(){return false}trackAttempt(){}recordFailure(){}evaluateAndTrip(){return O}markProbeInFlight(){}claimProbeSlot(){}releaseProbeSlot(){}handleProbeSuccess(){}handleProbeFailure(){}reset(){}};var P={tripped:false},g=class{constructor(){this.state="closed";this.hasProbeInFlight=false;this.probeTaskId=null;}get isOpen(){return this.state==="open"}get isHalfOpen(){return false}get cooldownRemaining(){return 0}open(){this.state="open";}close(){this.state="closed";}checkAndTransition(){return false}trackAttempt(){}recordFailure(){}evaluateAndTrip(){return P}markProbeInFlight(){}claimProbeSlot(){}releaseProbeSlot(){}handleProbeSuccess(){}handleProbeFailure(){}reset(){this.state="closed";}};exports.ManualCircuitBreaker=g;exports.NoopCircuitBreaker=q;exports.QUEUE_ORDERINGS=_;exports.SaturationCircuitBreaker=f;exports.Semaphore=M;exports.SemaphoreError=a;exports.SemaphoreEvents=n;//# sourceMappingURL=index.cjs.map | ||
| //# sourceMappingURL=index.cjs.map |
+169
-8
@@ -18,2 +18,4 @@ interface SemaphoreConfig { | ||
| circuitBreakerWindow?: number; | ||
| /** The time-width of the circuit window breaker. Denominator for calculating bucket count with: window / windowBucketWidth. Default: 1000 */ | ||
| circuitBreakerWindowBucketWidth?: number; | ||
| /** Milliseconds the circuit stays open before allowing a probe. Min: 1000. Default: 5000 */ | ||
@@ -25,2 +27,24 @@ circuitBreakerCooldown?: number; | ||
| circuitBreakerMinFailures?: number; | ||
| /** | ||
| * A circuit breaker instance to use instead of the built-in saturation | ||
| * breaker. Overrides all `circuitBreaker*` numeric options (the same | ||
| * precedence `comparator` has over `queueOrder`). See the breakers module: | ||
| * `SaturationCircuitBreaker` (the default), `NoopCircuitBreaker` (never | ||
| * trips — a pure limiter), `ManualCircuitBreaker` (an ops kill-switch), or | ||
| * any object implementing {@link CircuitBreakerStrategy}. | ||
| */ | ||
| circuitBreaker?: CircuitBreakerStrategy; | ||
| /** | ||
| * When set, `use()` feeds rejections from your function into the circuit | ||
| * breaker: a rejection for which the predicate returns `true` counts as one | ||
| * breaker failure (as if `reportFailure()` were called), and — unlike | ||
| * `reportFailure()` — it also makes half-open probes fault-aware: a probe | ||
| * dispatched through `use()` whose operation fails a matching error re-opens | ||
| * the circuit instead of closing it on release. | ||
| * | ||
| * The predicate must not throw; if it does, the error is logged via | ||
| * `console.warn` and the rejection is treated as non-matching. Only affects | ||
| * `use()` — `acquire()`/`tryAcquire()` callers use `reportFailure()`. | ||
| */ | ||
| circuitBreakerFailurePredicate?: (error: unknown) => boolean; | ||
| /** Reject immediately when all permits are held (no queuing). Default: false */ | ||
@@ -32,2 +56,4 @@ rejectOnFull?: boolean; | ||
| metricsEnabled?: boolean; | ||
| /** Metric windows to use for collection. Default: undefined */ | ||
| metricsWindows?: WindowOptions[]; | ||
| /** Enable debug logging (console output and the PermitPool invariant check). Does not affect which events are emitted — all events fire regardless. Default: false */ | ||
@@ -50,2 +76,7 @@ debug?: boolean; | ||
| * comparator, so it never needs to account for them. | ||
| * | ||
| * Must be a consistent total order and must not throw. A comparator that | ||
| * returns `NaN` or a non-number degrades safely to a stable id tie-break, | ||
| * but a thrown exception propagates out of enqueue/dispatch and can leave | ||
| * the priority heap partially sifted. | ||
| */ | ||
@@ -71,5 +102,7 @@ comparator?: Comparator<QueuedTaskView>; | ||
| } | ||
| type CircuitState = 'closed' | 'open' | 'half-open'; | ||
| interface CircuitBreakerConfig { | ||
| readonly threshold?: number; | ||
| readonly window?: number; | ||
| readonly windowBucketWidth?: number; | ||
| readonly cooldown?: number; | ||
@@ -87,2 +120,43 @@ readonly minThroughput?: number; | ||
| }; | ||
| /** | ||
| * The contract between the Semaphore and any circuit breaker implementation. | ||
| * | ||
| * The semaphore drives every breaker through exactly this surface, so any | ||
| * implementation of it — the built-in saturation breaker, a no-op breaker, a | ||
| * manual kill-switch, or your own — composes into a `Semaphore` via the | ||
| * `circuitBreaker` config option. The built-ins live in the breakers module | ||
| * (`SaturationCircuitBreaker`, `NoopCircuitBreaker`, `ManualCircuitBreaker`). | ||
| * | ||
| * Contract notes for implementers: | ||
| * - `checkAndTransition()` is called at the top of every acquire; return true | ||
| * exactly once per open → half-open transition (the semaphore emits | ||
| * CIRCUITHALFOPEN when it sees true). | ||
| * - `evaluateAndTrip()` may only report `tripped: true` for a closed → open | ||
| * transition; the semaphore then emits CIRCUITOPEN and evicts the queue. | ||
| * - The probe-slot methods manage the single half-open probe; implementations | ||
| * that never enter half-open may treat them as no-ops. | ||
| * - Methods must not throw. | ||
| */ | ||
| interface CircuitBreakerStrategy { | ||
| readonly state: CircuitState; | ||
| readonly isOpen: boolean; | ||
| readonly isHalfOpen: boolean; | ||
| readonly hasProbeInFlight: boolean; | ||
| readonly probeTaskId: number | null; | ||
| readonly cooldownRemaining: number; | ||
| /** open → half-open when ready; true if the transition just occurred. */ | ||
| checkAndTransition(): boolean; | ||
| /** Record one admission attempt (called per acquire while relevant). */ | ||
| trackAttempt(): void; | ||
| /** Record one failure signal (queue timeout, or Semaphore.reportFailure()). */ | ||
| recordFailure(): void; | ||
| /** Evaluate the trip condition; report trip data if the circuit just opened. */ | ||
| evaluateAndTrip(): CircuitTripResult; | ||
| markProbeInFlight(): void; | ||
| claimProbeSlot(taskId: number): void; | ||
| releaseProbeSlot(): void; | ||
| handleProbeSuccess(): void; | ||
| handleProbeFailure(): void; | ||
| reset(): void; | ||
| } | ||
| declare const SemaphoreEvents: { | ||
@@ -94,2 +168,3 @@ readonly TASKACQUIRE: "task-acquire"; | ||
| readonly QUEUEPURGE: "queue-purge"; | ||
| readonly QUEUEEVICT: "queue-evict"; | ||
| readonly CIRCUITOPEN: "circuit-open"; | ||
@@ -124,2 +199,3 @@ readonly CIRCUITHALFOPEN: "circuit-half-open"; | ||
| 'queue-purge': [task: QueuedTaskView]; | ||
| 'queue-evict': [task: QueuedTaskView]; | ||
| 'circuit-open': [payload: { | ||
@@ -175,4 +251,9 @@ timeoutRate: number; | ||
| type Comparator<T> = (a: T, b: T) => number; | ||
| interface WindowOptions { | ||
| size: number; | ||
| stepMs: number; | ||
| } | ||
| declare class Semaphore { | ||
| private metricsCollector?; | ||
| private queue; | ||
@@ -183,3 +264,2 @@ private readonly enqueueOrder; | ||
| private readonly backoff; | ||
| private readonly metricsCollector?; | ||
| private readonly queueMaxLength; | ||
@@ -192,2 +272,3 @@ private readonly queueMaxTimeout; | ||
| private readonly debug; | ||
| private readonly failurePredicate?; | ||
| private pendingReleaseCount; | ||
@@ -201,2 +282,3 @@ private releaseGeneration; | ||
| private totalTimeouts; | ||
| private totalEvictions; | ||
| private eventListeners; | ||
@@ -215,2 +297,3 @@ private drainPromise; | ||
| private _createRelease; | ||
| private _checkDrain; | ||
| private _enqueue; | ||
@@ -221,4 +304,6 @@ private _dequeue; | ||
| private _fireTimeout; | ||
| private _evictQueueOnCircuitOpen; | ||
| private _timeoutTask; | ||
| private _onTaskAbort; | ||
| private readonly _runSchedulerBound; | ||
| private schedule; | ||
@@ -243,4 +328,16 @@ private _runScheduler; | ||
| /** | ||
| * Read-only snapshot of the queue, in enqueue order. Entries additionally | ||
| * expose `isProbe` so a circuit-breaker probe is identifiable in the view. | ||
| */ | ||
| peekQueue(): (QueuedTaskView & { | ||
| isProbe: boolean; | ||
| })[]; | ||
| /** | ||
| * Preferred entry point. Acquires a permit, runs fn(), then releases. | ||
| * The permit is always released even if fn() throws. | ||
| * | ||
| * With `circuitBreakerFailurePredicate` configured, rejections from fn() | ||
| * that match the predicate feed the circuit breaker: one failure per match | ||
| * (as via reportFailure()), and a matching failure on a half-open probe | ||
| * re-opens the circuit instead of closing it on release. | ||
| * @param weight Permits to consume (integer in 1..count). Defaults to 1. | ||
@@ -275,2 +372,14 @@ */ | ||
| cancel(): void; | ||
| /** | ||
| * Feed an external failure signal (e.g. a downstream error) into the | ||
| * circuit breaker. Records one failure and evaluates the trip condition; | ||
| * if the circuit trips, queued tasks are evicted with CIRCUIT_OPEN exactly | ||
| * as on a saturation trip. This is how the default breaker doubles as an | ||
| * error-rate breaker: report the failures you care about and the same | ||
| * window, threshold, cooldown, and probe recovery apply. | ||
| * | ||
| * Only influences trip decisions while the circuit is closed — probe | ||
| * outcomes in half-open remain acquisition-based. No-op after shutdown. | ||
| */ | ||
| reportFailure(): void; | ||
| /** Returns a snapshot of current operating state, lifetime counters, and windowed metrics. */ | ||
@@ -297,2 +406,4 @@ status(): { | ||
| totalTimeouts: number; | ||
| /** Tasks evicted from the queue by a circuit trip (rejected CIRCUIT_OPEN while queued). */ | ||
| totalEvictions: number; | ||
| circuitBreakerCooldownRemaining: number; | ||
@@ -302,6 +413,13 @@ }; | ||
| }; | ||
| /** True if the semaphore is not shut down, circuit is not open, and a permit is available. */ | ||
| get availablePermits(): number; | ||
| /** | ||
| * True if the semaphore is not shut down, the circuit is not open, and a | ||
| * permit is free. This is a capacity signal, not a dispatch guarantee: | ||
| * tasks may still be queued ahead (head-of-line fairness), in which case | ||
| * tryAcquire() returns null even while isAvailable() is true. | ||
| */ | ||
| isAvailable(): boolean; | ||
| get capacity(): number; | ||
| get circuitState(): CircuitState; | ||
| get queueLength(): number; | ||
| get availablePermits(): number; | ||
| } | ||
@@ -314,4 +432,4 @@ | ||
| declare class CircuitBreaker { | ||
| private state; | ||
| declare class SaturationCircuitBreaker implements CircuitBreakerStrategy { | ||
| state: CircuitState; | ||
| private openUntil; | ||
@@ -323,2 +441,3 @@ private probeInFlight; | ||
| private readonly window; | ||
| private readonly windowBucketWidth; | ||
| private readonly cooldown; | ||
@@ -339,4 +458,4 @@ private readonly minThroughput; | ||
| trackAttempt(): void; | ||
| /** Records a timeout unconditionally (used for probe timeouts too). */ | ||
| recordTimeout(): void; | ||
| /** Records a failure unconditionally (used for probe timeouts too). */ | ||
| recordFailure(): void; | ||
| /** | ||
@@ -367,5 +486,47 @@ * open → half-open if the cooldown has elapsed. | ||
| declare class NoopCircuitBreaker implements CircuitBreakerStrategy { | ||
| readonly state: CircuitState; | ||
| readonly isOpen = false; | ||
| readonly isHalfOpen = false; | ||
| readonly hasProbeInFlight = false; | ||
| readonly probeTaskId: null; | ||
| readonly cooldownRemaining = 0; | ||
| checkAndTransition(): boolean; | ||
| trackAttempt(): void; | ||
| recordFailure(): void; | ||
| evaluateAndTrip(): CircuitTripResult; | ||
| markProbeInFlight(): void; | ||
| claimProbeSlot(): void; | ||
| releaseProbeSlot(): void; | ||
| handleProbeSuccess(): void; | ||
| handleProbeFailure(): void; | ||
| reset(): void; | ||
| } | ||
| declare class ManualCircuitBreaker implements CircuitBreakerStrategy { | ||
| state: CircuitState; | ||
| get isOpen(): boolean; | ||
| get isHalfOpen(): boolean; | ||
| readonly hasProbeInFlight = false; | ||
| readonly probeTaskId: null; | ||
| get cooldownRemaining(): number; | ||
| /** Open the circuit: new acquires reject with CIRCUIT_OPEN until close(). */ | ||
| open(): void; | ||
| /** Close the circuit and resume normal admission. */ | ||
| close(): void; | ||
| checkAndTransition(): boolean; | ||
| trackAttempt(): void; | ||
| recordFailure(): void; | ||
| evaluateAndTrip(): CircuitTripResult; | ||
| markProbeInFlight(): void; | ||
| claimProbeSlot(): void; | ||
| releaseProbeSlot(): void; | ||
| handleProbeSuccess(): void; | ||
| handleProbeFailure(): void; | ||
| reset(): void; | ||
| } | ||
| /** Built-in orderings, keyed by `queueOrder`. */ | ||
| declare const QUEUE_ORDERINGS: Record<QueueOrder, Comparator<QueuedTaskView>>; | ||
| export { CircuitBreaker, type CircuitBreakerConfig, type CircuitTripResult, type Comparator, QUEUE_ORDERINGS, type QueueOrder, type QueuedTaskView, Semaphore, type SemaphoreConfig, SemaphoreError, type SemaphoreErrorCode, type SemaphoreEventListener, type SemaphoreEventMap, type SemaphoreEventType, SemaphoreEvents, type SemaphoreMetricsSnapshot, type SemaphoreMetricsWindowSnapshot }; | ||
| export { type CircuitBreakerConfig, type CircuitBreakerStrategy, type CircuitState, type CircuitTripResult, type Comparator, ManualCircuitBreaker, NoopCircuitBreaker, QUEUE_ORDERINGS, type QueueOrder, type QueuedTaskView, SaturationCircuitBreaker, Semaphore, type SemaphoreConfig, SemaphoreError, type SemaphoreErrorCode, type SemaphoreEventListener, type SemaphoreEventMap, type SemaphoreEventType, SemaphoreEvents, type SemaphoreMetricsSnapshot, type SemaphoreMetricsWindowSnapshot, type WindowOptions }; |
+169
-8
@@ -18,2 +18,4 @@ interface SemaphoreConfig { | ||
| circuitBreakerWindow?: number; | ||
| /** The time-width of the circuit window breaker. Denominator for calculating bucket count with: window / windowBucketWidth. Default: 1000 */ | ||
| circuitBreakerWindowBucketWidth?: number; | ||
| /** Milliseconds the circuit stays open before allowing a probe. Min: 1000. Default: 5000 */ | ||
@@ -25,2 +27,24 @@ circuitBreakerCooldown?: number; | ||
| circuitBreakerMinFailures?: number; | ||
| /** | ||
| * A circuit breaker instance to use instead of the built-in saturation | ||
| * breaker. Overrides all `circuitBreaker*` numeric options (the same | ||
| * precedence `comparator` has over `queueOrder`). See the breakers module: | ||
| * `SaturationCircuitBreaker` (the default), `NoopCircuitBreaker` (never | ||
| * trips — a pure limiter), `ManualCircuitBreaker` (an ops kill-switch), or | ||
| * any object implementing {@link CircuitBreakerStrategy}. | ||
| */ | ||
| circuitBreaker?: CircuitBreakerStrategy; | ||
| /** | ||
| * When set, `use()` feeds rejections from your function into the circuit | ||
| * breaker: a rejection for which the predicate returns `true` counts as one | ||
| * breaker failure (as if `reportFailure()` were called), and — unlike | ||
| * `reportFailure()` — it also makes half-open probes fault-aware: a probe | ||
| * dispatched through `use()` whose operation fails a matching error re-opens | ||
| * the circuit instead of closing it on release. | ||
| * | ||
| * The predicate must not throw; if it does, the error is logged via | ||
| * `console.warn` and the rejection is treated as non-matching. Only affects | ||
| * `use()` — `acquire()`/`tryAcquire()` callers use `reportFailure()`. | ||
| */ | ||
| circuitBreakerFailurePredicate?: (error: unknown) => boolean; | ||
| /** Reject immediately when all permits are held (no queuing). Default: false */ | ||
@@ -32,2 +56,4 @@ rejectOnFull?: boolean; | ||
| metricsEnabled?: boolean; | ||
| /** Metric windows to use for collection. Default: undefined */ | ||
| metricsWindows?: WindowOptions[]; | ||
| /** Enable debug logging (console output and the PermitPool invariant check). Does not affect which events are emitted — all events fire regardless. Default: false */ | ||
@@ -50,2 +76,7 @@ debug?: boolean; | ||
| * comparator, so it never needs to account for them. | ||
| * | ||
| * Must be a consistent total order and must not throw. A comparator that | ||
| * returns `NaN` or a non-number degrades safely to a stable id tie-break, | ||
| * but a thrown exception propagates out of enqueue/dispatch and can leave | ||
| * the priority heap partially sifted. | ||
| */ | ||
@@ -71,5 +102,7 @@ comparator?: Comparator<QueuedTaskView>; | ||
| } | ||
| type CircuitState = 'closed' | 'open' | 'half-open'; | ||
| interface CircuitBreakerConfig { | ||
| readonly threshold?: number; | ||
| readonly window?: number; | ||
| readonly windowBucketWidth?: number; | ||
| readonly cooldown?: number; | ||
@@ -87,2 +120,43 @@ readonly minThroughput?: number; | ||
| }; | ||
| /** | ||
| * The contract between the Semaphore and any circuit breaker implementation. | ||
| * | ||
| * The semaphore drives every breaker through exactly this surface, so any | ||
| * implementation of it — the built-in saturation breaker, a no-op breaker, a | ||
| * manual kill-switch, or your own — composes into a `Semaphore` via the | ||
| * `circuitBreaker` config option. The built-ins live in the breakers module | ||
| * (`SaturationCircuitBreaker`, `NoopCircuitBreaker`, `ManualCircuitBreaker`). | ||
| * | ||
| * Contract notes for implementers: | ||
| * - `checkAndTransition()` is called at the top of every acquire; return true | ||
| * exactly once per open → half-open transition (the semaphore emits | ||
| * CIRCUITHALFOPEN when it sees true). | ||
| * - `evaluateAndTrip()` may only report `tripped: true` for a closed → open | ||
| * transition; the semaphore then emits CIRCUITOPEN and evicts the queue. | ||
| * - The probe-slot methods manage the single half-open probe; implementations | ||
| * that never enter half-open may treat them as no-ops. | ||
| * - Methods must not throw. | ||
| */ | ||
| interface CircuitBreakerStrategy { | ||
| readonly state: CircuitState; | ||
| readonly isOpen: boolean; | ||
| readonly isHalfOpen: boolean; | ||
| readonly hasProbeInFlight: boolean; | ||
| readonly probeTaskId: number | null; | ||
| readonly cooldownRemaining: number; | ||
| /** open → half-open when ready; true if the transition just occurred. */ | ||
| checkAndTransition(): boolean; | ||
| /** Record one admission attempt (called per acquire while relevant). */ | ||
| trackAttempt(): void; | ||
| /** Record one failure signal (queue timeout, or Semaphore.reportFailure()). */ | ||
| recordFailure(): void; | ||
| /** Evaluate the trip condition; report trip data if the circuit just opened. */ | ||
| evaluateAndTrip(): CircuitTripResult; | ||
| markProbeInFlight(): void; | ||
| claimProbeSlot(taskId: number): void; | ||
| releaseProbeSlot(): void; | ||
| handleProbeSuccess(): void; | ||
| handleProbeFailure(): void; | ||
| reset(): void; | ||
| } | ||
| declare const SemaphoreEvents: { | ||
@@ -94,2 +168,3 @@ readonly TASKACQUIRE: "task-acquire"; | ||
| readonly QUEUEPURGE: "queue-purge"; | ||
| readonly QUEUEEVICT: "queue-evict"; | ||
| readonly CIRCUITOPEN: "circuit-open"; | ||
@@ -124,2 +199,3 @@ readonly CIRCUITHALFOPEN: "circuit-half-open"; | ||
| 'queue-purge': [task: QueuedTaskView]; | ||
| 'queue-evict': [task: QueuedTaskView]; | ||
| 'circuit-open': [payload: { | ||
@@ -175,4 +251,9 @@ timeoutRate: number; | ||
| type Comparator<T> = (a: T, b: T) => number; | ||
| interface WindowOptions { | ||
| size: number; | ||
| stepMs: number; | ||
| } | ||
| declare class Semaphore { | ||
| private metricsCollector?; | ||
| private queue; | ||
@@ -183,3 +264,2 @@ private readonly enqueueOrder; | ||
| private readonly backoff; | ||
| private readonly metricsCollector?; | ||
| private readonly queueMaxLength; | ||
@@ -192,2 +272,3 @@ private readonly queueMaxTimeout; | ||
| private readonly debug; | ||
| private readonly failurePredicate?; | ||
| private pendingReleaseCount; | ||
@@ -201,2 +282,3 @@ private releaseGeneration; | ||
| private totalTimeouts; | ||
| private totalEvictions; | ||
| private eventListeners; | ||
@@ -215,2 +297,3 @@ private drainPromise; | ||
| private _createRelease; | ||
| private _checkDrain; | ||
| private _enqueue; | ||
@@ -221,4 +304,6 @@ private _dequeue; | ||
| private _fireTimeout; | ||
| private _evictQueueOnCircuitOpen; | ||
| private _timeoutTask; | ||
| private _onTaskAbort; | ||
| private readonly _runSchedulerBound; | ||
| private schedule; | ||
@@ -243,4 +328,16 @@ private _runScheduler; | ||
| /** | ||
| * Read-only snapshot of the queue, in enqueue order. Entries additionally | ||
| * expose `isProbe` so a circuit-breaker probe is identifiable in the view. | ||
| */ | ||
| peekQueue(): (QueuedTaskView & { | ||
| isProbe: boolean; | ||
| })[]; | ||
| /** | ||
| * Preferred entry point. Acquires a permit, runs fn(), then releases. | ||
| * The permit is always released even if fn() throws. | ||
| * | ||
| * With `circuitBreakerFailurePredicate` configured, rejections from fn() | ||
| * that match the predicate feed the circuit breaker: one failure per match | ||
| * (as via reportFailure()), and a matching failure on a half-open probe | ||
| * re-opens the circuit instead of closing it on release. | ||
| * @param weight Permits to consume (integer in 1..count). Defaults to 1. | ||
@@ -275,2 +372,14 @@ */ | ||
| cancel(): void; | ||
| /** | ||
| * Feed an external failure signal (e.g. a downstream error) into the | ||
| * circuit breaker. Records one failure and evaluates the trip condition; | ||
| * if the circuit trips, queued tasks are evicted with CIRCUIT_OPEN exactly | ||
| * as on a saturation trip. This is how the default breaker doubles as an | ||
| * error-rate breaker: report the failures you care about and the same | ||
| * window, threshold, cooldown, and probe recovery apply. | ||
| * | ||
| * Only influences trip decisions while the circuit is closed — probe | ||
| * outcomes in half-open remain acquisition-based. No-op after shutdown. | ||
| */ | ||
| reportFailure(): void; | ||
| /** Returns a snapshot of current operating state, lifetime counters, and windowed metrics. */ | ||
@@ -297,2 +406,4 @@ status(): { | ||
| totalTimeouts: number; | ||
| /** Tasks evicted from the queue by a circuit trip (rejected CIRCUIT_OPEN while queued). */ | ||
| totalEvictions: number; | ||
| circuitBreakerCooldownRemaining: number; | ||
@@ -302,6 +413,13 @@ }; | ||
| }; | ||
| /** True if the semaphore is not shut down, circuit is not open, and a permit is available. */ | ||
| get availablePermits(): number; | ||
| /** | ||
| * True if the semaphore is not shut down, the circuit is not open, and a | ||
| * permit is free. This is a capacity signal, not a dispatch guarantee: | ||
| * tasks may still be queued ahead (head-of-line fairness), in which case | ||
| * tryAcquire() returns null even while isAvailable() is true. | ||
| */ | ||
| isAvailable(): boolean; | ||
| get capacity(): number; | ||
| get circuitState(): CircuitState; | ||
| get queueLength(): number; | ||
| get availablePermits(): number; | ||
| } | ||
@@ -314,4 +432,4 @@ | ||
| declare class CircuitBreaker { | ||
| private state; | ||
| declare class SaturationCircuitBreaker implements CircuitBreakerStrategy { | ||
| state: CircuitState; | ||
| private openUntil; | ||
@@ -323,2 +441,3 @@ private probeInFlight; | ||
| private readonly window; | ||
| private readonly windowBucketWidth; | ||
| private readonly cooldown; | ||
@@ -339,4 +458,4 @@ private readonly minThroughput; | ||
| trackAttempt(): void; | ||
| /** Records a timeout unconditionally (used for probe timeouts too). */ | ||
| recordTimeout(): void; | ||
| /** Records a failure unconditionally (used for probe timeouts too). */ | ||
| recordFailure(): void; | ||
| /** | ||
@@ -367,5 +486,47 @@ * open → half-open if the cooldown has elapsed. | ||
| declare class NoopCircuitBreaker implements CircuitBreakerStrategy { | ||
| readonly state: CircuitState; | ||
| readonly isOpen = false; | ||
| readonly isHalfOpen = false; | ||
| readonly hasProbeInFlight = false; | ||
| readonly probeTaskId: null; | ||
| readonly cooldownRemaining = 0; | ||
| checkAndTransition(): boolean; | ||
| trackAttempt(): void; | ||
| recordFailure(): void; | ||
| evaluateAndTrip(): CircuitTripResult; | ||
| markProbeInFlight(): void; | ||
| claimProbeSlot(): void; | ||
| releaseProbeSlot(): void; | ||
| handleProbeSuccess(): void; | ||
| handleProbeFailure(): void; | ||
| reset(): void; | ||
| } | ||
| declare class ManualCircuitBreaker implements CircuitBreakerStrategy { | ||
| state: CircuitState; | ||
| get isOpen(): boolean; | ||
| get isHalfOpen(): boolean; | ||
| readonly hasProbeInFlight = false; | ||
| readonly probeTaskId: null; | ||
| get cooldownRemaining(): number; | ||
| /** Open the circuit: new acquires reject with CIRCUIT_OPEN until close(). */ | ||
| open(): void; | ||
| /** Close the circuit and resume normal admission. */ | ||
| close(): void; | ||
| checkAndTransition(): boolean; | ||
| trackAttempt(): void; | ||
| recordFailure(): void; | ||
| evaluateAndTrip(): CircuitTripResult; | ||
| markProbeInFlight(): void; | ||
| claimProbeSlot(): void; | ||
| releaseProbeSlot(): void; | ||
| handleProbeSuccess(): void; | ||
| handleProbeFailure(): void; | ||
| reset(): void; | ||
| } | ||
| /** Built-in orderings, keyed by `queueOrder`. */ | ||
| declare const QUEUE_ORDERINGS: Record<QueueOrder, Comparator<QueuedTaskView>>; | ||
| export { CircuitBreaker, type CircuitBreakerConfig, type CircuitTripResult, type Comparator, QUEUE_ORDERINGS, type QueueOrder, type QueuedTaskView, Semaphore, type SemaphoreConfig, SemaphoreError, type SemaphoreErrorCode, type SemaphoreEventListener, type SemaphoreEventMap, type SemaphoreEventType, SemaphoreEvents, type SemaphoreMetricsSnapshot, type SemaphoreMetricsWindowSnapshot }; | ||
| export { type CircuitBreakerConfig, type CircuitBreakerStrategy, type CircuitState, type CircuitTripResult, type Comparator, ManualCircuitBreaker, NoopCircuitBreaker, QUEUE_ORDERINGS, type QueueOrder, type QueuedTaskView, SaturationCircuitBreaker, Semaphore, type SemaphoreConfig, SemaphoreError, type SemaphoreErrorCode, type SemaphoreEventListener, type SemaphoreEventMap, type SemaphoreEventType, SemaphoreEvents, type SemaphoreMetricsSnapshot, type SemaphoreMetricsWindowSnapshot, type WindowOptions }; |
+1
-1
@@ -1,2 +0,2 @@ | ||
| var o=class extends Error{constructor(e,t){super(e),this.name="SemaphoreError",this.code=t;}};var u=(a,e,t,i,r=true,s=true)=>{if(typeof a!="number"||Number.isNaN(a))throw new o(`${e} must be a valid number`,"INVALID_ARGUMENT");if(r&&!Number.isInteger(a))throw new o(`${e} must be an integer`,"INVALID_ARGUMENT");if(s?a<t:a<=t)throw new o(`${e} must be ${s?">=":">"} ${t}`,"INVALID_ARGUMENT");if(s?a>i:a>=i)throw new o(`${e} must be ${s?"<=":"<"} ${i}`,"INVALID_ARGUMENT");return a};var T=class{constructor(e){this._inFlight=0;this.capacity=e,this._available=e;}get available(){return this._available}get inFlight(){return this._inFlight}get isFull(){return this._available===0}hasCapacityFor(e){return this._available>=e}acquire(e=1){this._available-=e,this._inFlight+=e;}release(e=1){this._available=Math.min(this._available+e,this.capacity),this._inFlight=this._inFlight<e?0:this._inFlight-e;}assertInvariant(e){if(!e)return;let t=this._inFlight+this._available;t!==this.capacity&&console.error(`[Semaphore] Invariant violation: inFlight(${this._inFlight}) + available(${this._available}) = ${t}, expected capacity(${this.capacity})`);}reset(){this._available=this.capacity,this._inFlight=0;}};var A=class{constructor(e,t){this.size=e,this.stepMs=t,this.buckets=new Int32Array(e*2),this.timestamps=new Float64Array(e);}idx(){let e=Date.now(),t=Math.floor(e/this.stepMs)*this.stepMs,i=Math.floor(t/this.stepMs)%this.size;return this.timestamps[i]!==t&&(this.timestamps[i]=t,this.buckets[i*2]=0,this.buckets[i*2+1]=0),i}addAcquired(){this.buckets[this.idx()*2]++;}addTimeout(){this.buckets[this.idx()*2+1]++;}snapshot(){let e=Date.now(),t=Math.floor(e/this.stepMs)*this.stepMs-(this.size-1)*this.stepMs,i=0,r=0;for(let s=0;s<this.size;s++)this.timestamps[s]>=t&&(i+=this.buckets[s*2],r+=this.buckets[s*2+1]);return {acquired:i,timeouts:r}}reset(){this.buckets.fill(0),this.timestamps.fill(0);}},b=class{constructor(e={}){this.state="closed";this.openUntil=0;this.probeInFlight=false;this._probeTaskId=null;if(this.threshold=u(e.threshold??.5,"CircuitBreaker threshold",0,1,false,false),this.window=u(e.window??1e4,"CircuitBreaker window",1e3,Number.MAX_SAFE_INTEGER,true,true),this.cooldown=u(e.cooldown??5e3,"CircuitBreaker cooldown",1e3,Number.MAX_SAFE_INTEGER,true,true),this.minThroughput=u(e.minThroughput??10,"CircuitBreaker minThroughput",1,Number.MAX_SAFE_INTEGER,true,true),this.minFailures=u(e.minFailures??5,"CircuitBreaker minFailures",1,Number.MAX_SAFE_INTEGER,true,true),this.eventWindow=new A(Math.ceil(this.window/1e3),1e3),this.minThroughput<this.minFailures)throw new o("CircuitBreaker minThroughput must be >= minFailures","INVALID_ARGUMENT")}get isClosed(){return this.state==="closed"}get isOpen(){return this.state==="open"}get isHalfOpen(){return this.state==="half-open"}get hasProbeInFlight(){return this.probeInFlight}get probeTaskId(){return this._probeTaskId}get cooldownRemaining(){return this.isOpen?Math.max(0,this.openUntil-Date.now()):0}trackAttempt(){this.state==="closed"&&this.eventWindow.addAcquired();}recordTimeout(){this.eventWindow.addTimeout();}checkAndTransition(){return this.state!=="open"||Date.now()<this.openUntil?false:(this.state="half-open",this.probeInFlight=false,this._probeTaskId=null,true)}evaluateAndTrip(){if(this.state!=="closed")return {tripped:false};let{timeouts:e,acquired:t}=this.eventWindow.snapshot();if(t<this.minThroughput||e<this.minFailures)return {tripped:false};let i=e/t;return i<this.threshold?{tripped:false}:(this.state="open",this.openUntil=Date.now()+this.cooldown,{tripped:true,timeoutRate:i,failures:e,attempts:t})}markProbeInFlight(){this.probeInFlight=true;}claimProbeSlot(e){this.probeInFlight=true,this._probeTaskId=e;}releaseProbeSlot(){this.probeInFlight=false,this._probeTaskId=null;}handleProbeSuccess(){this.state="closed",this.probeInFlight=false,this._probeTaskId=null,this.eventWindow.reset();}handleProbeFailure(){this.state="open",this.probeInFlight=false,this._probeTaskId=null,this.openUntil=Date.now()+this.cooldown;}reset(){this.state="closed",this.openUntil=0,this.probeInFlight=false,this._probeTaskId=null,this.eventWindow.reset();}};var w=class{constructor(e={}){this.delay=0;this.lastTimestamp=0;if(this.initialTimeout=u(e.initialTimeout??50,"BackoffTracker initialTimeout",0,Number.MAX_SAFE_INTEGER,true,true),this.maxTimeout=u(e.maxTimeout??2e3,"BackoffTracker maxTimeout",0,Number.MAX_SAFE_INTEGER,true,true),this.decayFactor=u(e.decayFactor??.5,"BackoffTracker decayFactor",0,1,false,false),this.maxTimeout<this.initialTimeout)throw new o("BackoffTracker maxTimeout must be >= initialTimeout","INVALID_ARGUMENT")}get currentDelay(){if(this.delay===0)return 0;let e=(Date.now()-this.lastTimestamp)/1e3;if(e<=0)return this.delay;let t=this.delay*Math.pow(this.decayFactor,e);return t<1?0:t}onTimeout(){let e=this.currentDelay,t=e>0?e*2:this.initialTimeout;this.delay=Math.min(t,this.maxTimeout),this.lastTimestamp=Date.now();}reset(){this.delay=0,this.lastTimestamp=0;}};var y=class{constructor(e){this.prev=null;this.next=null;this.heapIndex=-1;this.completed=false;this.id=e.id,this.priority=e.priority,this.enqueueTime=e.enqueueTime,this.isProbe=e.isProbe,this.weight=e.weight??1,this._resolve=e.resolve,this._reject=e.reject,this.abortSignal=e.abortSignal;}arm(e){this.abortSignal&&(this.abortListener=()=>{this.claim()&&e();},this.abortSignal.addEventListener("abort",this.abortListener));}dispatch(e){return this.claim()?(this._resolve(e()),true):false}discard(e){return this.claim()?(this._reject(e),true):false}claim(){return this.completed?false:(this.completed=true,this.abortSignal&&this.abortListener&&this.abortSignal.removeEventListener("abort",this.abortListener),true)}reject(e){this._reject(e);}};var S=class{constructor(e){this.heap=[];this.compare=e;}get size(){return this.heap.length}isEmpty(){return this.heap.length===0}peek(){return this.heap[0]}toArray(){return [...this.heap]}has(e){return e.heapIndex>=0&&this.heap[e.heapIndex]===e}clear(){for(let e of this.heap)e.heapIndex=-1;this.heap=[];}insert(e){if(e.heapIndex>=0)throw new Error("Item is already in a heap");let t=this.heap.length;this.heap.push(e),e.heapIndex=t,this._bubbleUp(t);}pop(){if(this.heap.length===0)return;let e=this.heap[0];e.heapIndex=-1;let t=this.heap.pop();return this.heap.length>0&&t!==e&&(this.heap[0]=t,t.heapIndex=0,this._bubbleDown(0)),e}delete(e){let t=e.heapIndex;if(t<0||this.heap[t]!==e)return;e.heapIndex=-1;let i=this.heap.pop();if(t===this.heap.length||i===e)return e;this.heap[t]=i,i.heapIndex=t;let r=this.heap[t-1>>1];return t>0&&this.compare(i,r)<0?this._bubbleUp(t):this._bubbleDown(t),e}_swap(e,t){let i=this.heap[e],r=this.heap[t];this.heap[e]=r,this.heap[t]=i,i.heapIndex=t,r.heapIndex=e;}_bubbleUp(e){for(;e>0;){let t=e-1>>1;if(this.compare(this.heap[e],this.heap[t])<0)this._swap(e,t),e=t;else break}}_bubbleDown(e){let t=this.heap.length;for(;;){let i=e,r=2*e+1,s=2*e+2;if(r<t&&this.compare(this.heap[r],this.heap[i])<0&&(i=r),s<t&&this.compare(this.heap[s],this.heap[i])<0&&(i=s),i===e)break;this._swap(e,i),e=i;}}};var q=class{constructor(){this.head=null;this.tail=null;this._size=0;}get size(){return this._size}isEmpty(){return this._size===0}peekHead(){return this.head===null?void 0:this.head}pushTail(e){e.prev=this.tail,e.next=null,this.tail!==null?this.tail.next=e:this.head=e,this.tail=e,this._size++;}remove(e){e.prev!==null?e.prev.next=e.next:this.head=e.next,e.next!==null?e.next.prev=e.prev:this.tail=e.prev,e.prev=e.next=null,this._size--;}clear(){this.head=this.tail=null,this._size=0;}};var E=class{constructor(e,t){if(e<=0)throw new Error("CombinedWindow size must be > 0");this.size=e,this.stepMs=t,this.timestamps=new Float64Array(e),this.acquired=new Int32Array(e),this.released=new Int32Array(e),this.timeouts=new Int32Array(e),this.ifSum=new Float64Array(e),this.ifCount=new Int32Array(e),this.ifMax=new Float64Array(e),this.qSum=new Float64Array(e),this.qCount=new Int32Array(e),this.qMax=new Float64Array(e),this.latSum=new Float64Array(e),this.latCount=new Int32Array(e);}bucket(e){let t=Math.floor(e/this.stepMs),i=t*this.stepMs,r=t%this.size;return this.timestamps[r]!==i&&(this.timestamps[r]=i,this.acquired[r]=0,this.released[r]=0,this.timeouts[r]=0,this.ifSum[r]=0,this.ifCount[r]=0,this.ifMax[r]=0,this.qSum[r]=0,this.qCount[r]=0,this.qMax[r]=0,this.latSum[r]=0,this.latCount[r]=0),r}addInflight(e,t){this.ifSum[e]+=t,this.ifCount[e]++,t>this.ifMax[e]&&(this.ifMax[e]=t);}addQueue(e,t){this.qSum[e]+=t,this.qCount[e]++,t>this.qMax[e]&&(this.qMax[e]=t);}recordAcquire(e,t,i){let r=this.bucket(e);this.acquired[r]++,this.addInflight(r,t),this.addQueue(r,i);}recordAcquireQueued(e,t,i,r){let s=this.bucket(e);this.acquired[s]++,this.latSum[s]+=t,this.latCount[s]++,this.addInflight(s,i),this.addQueue(s,r);}recordRelease(e,t,i){let r=this.bucket(e);this.released[r]++,this.addInflight(r,t),this.addQueue(r,i);}recordTimeoutQueue(e,t){let i=this.bucket(e);this.timeouts[i]++,this.addQueue(i,t);}sampleBoth(e,t,i){let r=this.bucket(e);this.addInflight(r,t),this.addQueue(r,i);}addAcquired(e){this.acquired[this.bucket(e)]++;}addReleased(e){this.released[this.bucket(e)]++;}addTimeout(e){this.timeouts[this.bucket(e)]++;}addLatency(e,t){let i=this.bucket(e);this.latSum[i]+=t,this.latCount[i]++;}sampleInflight(e,t){this.addInflight(this.bucket(e),t);}sampleQueue(e,t){this.addQueue(this.bucket(e),t);}snapshot(e){let t=Math.floor(e/this.stepMs)*this.stepMs-(this.size-1)*this.stepMs,i=0,r=0,s=0,h=0,p=0,m=0,f=0,c=0,d=0,g=0,v=0;for(let l=0;l<this.size;l++)this.timestamps[l]<t||(i+=this.acquired[l],r+=this.released[l],s+=this.timeouts[l],h+=this.ifSum[l],p+=this.ifCount[l],this.ifMax[l]>m&&(m=this.ifMax[l]),f+=this.qSum[l],c+=this.qCount[l],this.qMax[l]>d&&(d=this.qMax[l]),g+=this.latSum[l],v+=this.latCount[l]);return {counts:{acquired:i,released:r,timeouts:s},inflight:{avg:p===0?0:h/p,max:m,samples:p},queue:{avg:c===0?0:f/c,max:d,samples:c},latency:{avg:v===0?0:g/v,count:v,total:g}}}reset(){this.timestamps.fill(0),this.acquired.fill(0),this.released.fill(0),this.timeouts.fill(0),this.ifSum.fill(0),this.ifCount.fill(0),this.ifMax.fill(0),this.qSum.fill(0),this.qCount.fill(0),this.qMax.fill(0),this.latSum.fill(0),this.latCount.fill(0);}},M=[{size:60,stepMs:1e3},{size:60,stepMs:5e3},{size:60,stepMs:15e3},{size:60,stepMs:6e4},{size:60,stepMs:144e4}],I=class{constructor(e=M){this._totalAcquiredFast=0;this._totalAcquiredQueued=0;this._totalReleased=0;this._totalTimeouts=0;this._totalAborts=0;this._capacity=0;this._circuitOpen=false;this._circuitHalfOpen=false;if(e.length===0)throw new Error("SemaphoreMetrics requires at least one WindowOptions");this.windows=e.map(({size:t,stepMs:i})=>new E(t,i)),this.windowLabels=e.map(({size:t,stepMs:i})=>{let r=t*i;return r>=36e5&&r%36e5===0?`${r/36e5}h`:r>=6e4&&r%6e4===0?`${r/6e4}m`:r>=1e3&&r%1e3===0?`${r/1e3}s`:`${r}ms`});}onAcquireFast(e,t,i){this._totalAcquiredFast++;for(let r of this.windows)r.recordAcquire(e,t,i);}onAcquireQueued(e,t,i,r){this._totalAcquiredQueued++;for(let s of this.windows)s.recordAcquireQueued(e,t,i,r);}onRelease(e,t,i){this._totalReleased++;for(let r of this.windows)r.recordRelease(e,t,i);}onTimeout(e,t){this._totalTimeouts++;for(let i of this.windows)i.recordTimeoutQueue(e,t);}onAbort(e,t){this._totalAborts++;for(let i of this.windows)i.sampleQueue(e,t);}sampleGauges(e,t,i){for(let r of this.windows)r.sampleBoth(e,t,i);}sampleQueueDepthAt(e,t){for(let i of this.windows)i.sampleQueue(e,t);}markAcquireFast(){this._totalAcquiredFast++;let e=Date.now();for(let t of this.windows)t.addAcquired(e);}markAcquireQueued(e){this._totalAcquiredQueued++;let t=Date.now();for(let i of this.windows)i.addAcquired(t),i.addLatency(t,e);}markReleased(){this._totalReleased++;let e=Date.now();for(let t of this.windows)t.addReleased(e);}markTimeout(){this._totalTimeouts++;let e=Date.now();for(let t of this.windows)t.addTimeout(e);}markAbort(){this._totalAborts++;}sampleInFlight(e){let t=Date.now();for(let i of this.windows)i.sampleInflight(t,e);}sampleQueueDepth(e){let t=Date.now();for(let i of this.windows)i.sampleQueue(t,e);}markCapacityChange(e){this._capacity=e;}markCircuitOpen(){this._circuitOpen=true,this._circuitHalfOpen=false;}markCircuitHalfOpen(){this._circuitOpen=false,this._circuitHalfOpen=true;}markCircuitClose(){this._circuitOpen=false,this._circuitHalfOpen=false;}getSnapshot(){let e=Date.now(),t={},i=0,r=0;for(let s=0;s<this.windowLabels.length;s++){let h=this.windows[s].snapshot(e);t[this.windowLabels[s]]=h,s===0&&(i=h.inflight.avg,r=h.queue.avg);}return {windows:t,meta:{inFlightLastMinute:Math.round(i),queueDepthLastMinute:Math.round(r),totalAcquiredFast:this._totalAcquiredFast,totalAcquiredQueued:this._totalAcquiredQueued,totalReleased:this._totalReleased,totalTimeouts:this._totalTimeouts,totalAborts:this._totalAborts,capacity:this._capacity,circuitOpen:this._circuitOpen,circuitHalfOpen:this._circuitHalfOpen}}}reset(){for(let e of this.windows)e.reset();this._totalAcquiredFast=0,this._totalAcquiredQueued=0,this._totalReleased=0,this._totalTimeouts=0,this._totalAborts=0,this._capacity=0,this._circuitOpen=false,this._circuitHalfOpen=false;}destroy(){this.reset();}};var k={fifo:(a,e)=>a.id-e.id,lifo:(a,e)=>e.id-a.id,fifoWithPriority:(a,e)=>a.priority-e.priority||a.id-e.id,lifoWithPriority:(a,e)=>a.priority-e.priority||e.id-a.id};function x(a){if(a.comparator!==void 0){if(typeof a.comparator!="function")throw new o("Semaphore comparator must be a function","INVALID_ARGUMENT");return a.comparator}let e=a.queueOrder??"fifoWithPriority",t=k[e];if(t===void 0)throw new o(`Semaphore queueOrder must be one of: ${Object.keys(k).join(", ")} (got ${JSON.stringify(e)})`,"INVALID_ARGUMENT");return t}function C(a){let e=x(a);return (t,i)=>{if(t.isProbe!==i.isProbe)return t.isProbe?-1:1;let r=e(t,i);return typeof r!="number"||Number.isNaN(r)?t.id-i.id:r}}var n={TASKACQUIRE:"task-acquire",TASKRELEASE:"task-release",TASKTIMEOUT:"task-timeout",TASKABORT:"task-abort",QUEUEPURGE:"queue-purge",CIRCUITOPEN:"circuit-open",CIRCUITHALFOPEN:"circuit-half-open",CIRCUITCLOSE:"circuit-close",SHUTDOWN:"shutdown"};var _=class{constructor(e,t={}){this.pendingReleaseCount=0;this.releaseGeneration=0;this.scheduled=false;this.isShutdown=false;this.taskIdCounter=0;this.totalAcquired=0;this.totalReleased=0;this.totalTimeouts=0;this.eventListeners=new Map;this.drainPromise=null;this.drainResolve=null;this.purgeIntervalId=null;this.timeoutTimerId=null;u(e,"Semaphore count",0,Number.MAX_SAFE_INTEGER,true,false),this.purgeIntervalMs=u(t.purgeIntervalMs??3e3,"Semaphore purgeIntervalMs",500,Number.MAX_SAFE_INTEGER,true,true),this.queueMaxTimeout=u(t.queueMaxTimeout??1e4,"Semaphore queueMaxTimeout",1,Number.MAX_SAFE_INTEGER,true,true),this.queueMaxLength=u(t.queueMaxLength??1024,"Semaphore queueMaxLength",1,Number.MAX_SAFE_INTEGER,true,true),this.queueMaxAge=u(t.queueMaxAge??3e4,"Semaphore queueMaxAge",1,Number.MAX_SAFE_INTEGER,true,true),this.rejectOnFull=t.rejectOnFull??false,this.metricsEnabled=t.metricsEnabled??true,this.debug=t.debug??false;let i=C({queueOrder:t.queueOrder,comparator:t.comparator});this.queue=new S(i),this.enqueueOrder=new q,this.permits=new T(e),this.circuit=new b({threshold:t.circuitBreakerThreshold,window:t.circuitBreakerWindow,cooldown:t.circuitBreakerCooldown,minThroughput:t.circuitBreakerMinThroughput,minFailures:t.circuitBreakerMinFailures}),this.backoff=new w({initialTimeout:t.backoffInitialTimeout,maxTimeout:t.backoffMaxTimeout,decayFactor:t.backoffDecayFactor}),this.metricsCollector=this.metricsEnabled?new I:void 0,this.metricsCollector?.markCapacityChange(this.permits.capacity),this.metricsCollector?.sampleGauges(Date.now(),this.permits.inFlight,this.queue.size),this._startPurgeInterval();}on(e,t){this.eventListeners.has(e)||this.eventListeners.set(e,new Set),this.eventListeners.get(e).add(t);}off(e,t){this.eventListeners.get(e)?.delete(t),this.eventListeners.get(e)?.size===0&&this.eventListeners.delete(e);}removeAllListeners(e){e?this.eventListeners.delete(e):this.eventListeners.clear();}hasListeners(e){let t=this.eventListeners.get(e);return t!==void 0&&t.size>0}emit(e,...t){let i=this.eventListeners.get(e);if(!(!i||i.size===0)){if(i.size===1){let r=i.values().next().value;try{r(...t);}catch(s){this.debug&&console.warn(`[Semaphore] Error in listener for "${e}":`,s);}return}for(let r of Array.from(i))try{r(...t);}catch(s){this.debug&&console.warn(`[Semaphore] Error in listener for "${e}":`,s);}}}_tryAcquireFast(e){return this.circuit.isHalfOpen?this.circuit.hasProbeInFlight||!this.permits.hasCapacityFor(e)?null:(this.circuit.markProbeInFlight(),this.permits.acquire(e),this.totalAcquired++,this.metricsCollector?.onAcquireFast(Date.now(),this.permits.inFlight,this.queue.size),this.hasListeners(n.TASKACQUIRE)&&this.emit(n.TASKACQUIRE,{queued:this.queue.size,running:this.permits.capacity-this.permits.available,probe:true}),this._createRelease(true,e)):this.queue.size>0||!this.permits.hasCapacityFor(e)?null:(this.permits.acquire(e),this.totalAcquired++,this.metricsCollector?.onAcquireFast(Date.now(),this.permits.inFlight,this.queue.size),this.hasListeners(n.TASKACQUIRE)&&this.emit(n.TASKACQUIRE,{queued:this.queue.size,running:this.permits.capacity-this.permits.available}),this._createRelease(false,e))}_createRelease(e=false,t=1){this.pendingReleaseCount++;let i=this.releaseGeneration,r=false;return ()=>{if(r||i!==this.releaseGeneration){this.debug&&console.warn("[Semaphore] release() called after already released (no-op)");return}r=true,this.pendingReleaseCount--,this.permits.release(t),this.totalReleased++,this.metricsCollector?.onRelease(Date.now(),this.permits.inFlight,this.queue.size),e&&this.circuit.isHalfOpen&&(this.circuit.handleProbeSuccess(),this.emit(n.CIRCUITCLOSE),this.metricsCollector?.markCircuitClose(),this.debug&&console.info("[Semaphore] Circuit closed after successful probe")),this.permits.assertInvariant(this.debug),this.hasListeners(n.TASKRELEASE)&&this.emit(n.TASKRELEASE,{queued:this.queue.size,running:this.permits.capacity-this.permits.available}),this.schedule();}}_enqueue(e){this.queue.insert(e),this.enqueueOrder.pushTail(e),this._armTimeout();}_dequeue(e){this.queue.delete(e)!==void 0&&(this.enqueueOrder.remove(e),this.enqueueOrder.size===0&&this._clearTimeout());}_armTimeout(){if(this.timeoutTimerId!==null)return;let e=this.enqueueOrder.peekHead();if(e===void 0)return;let t=Math.max(0,e.enqueueTime+this.queueMaxTimeout-Date.now());this.timeoutTimerId=setTimeout(()=>this._fireTimeout(),t);}_clearTimeout(){this.timeoutTimerId!==null&&(clearTimeout(this.timeoutTimerId),this.timeoutTimerId=null);}_fireTimeout(){if(this.timeoutTimerId=null,this.isShutdown)return;let e=Date.now(),t=0,i=this.enqueueOrder.peekHead();for(;i!==void 0&&i.enqueueTime+this.queueMaxTimeout<=e;){let r=i.claim();this._dequeue(i),r&&(this._timeoutTask(i),t++),i=this.enqueueOrder.peekHead();}if(i!==void 0){let r=Math.max(0,i.enqueueTime+this.queueMaxTimeout-e);this.timeoutTimerId=setTimeout(()=>this._fireTimeout(),r);}t>0&&this.schedule();}_timeoutTask(e){if(this.totalTimeouts++,this.circuit.recordTimeout(),this.backoff.onTimeout(),e.isProbe)this.circuit.handleProbeFailure(),this.emit(n.CIRCUITOPEN,{timeoutRate:1,recentTimeouts:1,total:1,reason:"half-open-probe-failed"}),this.metricsCollector?.markCircuitOpen(),this.debug&&console.warn("[Semaphore] Circuit re-opened: half-open probe timed out");else {let t=this.circuit.evaluateAndTrip();t.tripped&&(this.emit(n.CIRCUITOPEN,{timeoutRate:t.timeoutRate,recentTimeouts:t.failures,total:t.attempts}),this.metricsCollector?.markCircuitOpen(),this.debug&&console.warn(`[Semaphore] Circuit opened. Rate: ${(t.timeoutRate*100).toFixed(1)}%`));}this.metricsCollector?.onTimeout(Date.now(),this.queue.size),this.emit(n.TASKTIMEOUT,{queueLength:this.queue.size,backoffDelay:this.backoff.currentDelay,taskId:e.id}),this.debug&&console.warn(`[Semaphore] Task #${e.id} timed out after ${this.queueMaxTimeout}ms`),e.reject(new o(`Semaphore acquire timed out after ${this.queueMaxTimeout}ms (queue: ${this.queue.size})`,"TIMEOUT"));}_onTaskAbort(e,t){this._dequeue(e),e.isProbe&&this.circuit.releaseProbeSlot(),this.metricsCollector?.onAbort(Date.now(),this.queue.size),this.emit(n.TASKABORT),this.debug&&console.info(`[Semaphore] Task #${e.id} aborted`),t(new o("Semaphore acquire aborted","ABORTED")),this.schedule();}schedule(){if(this.scheduled||this.isShutdown)return;this.scheduled=true;let e=this.backoff.currentDelay;e>0?setTimeout(()=>{this._runScheduler();},e).unref?.():queueMicrotask(()=>{this._runScheduler();});}_runScheduler(){if(!this.isShutdown){this.scheduled=false;try{for(;this.queue.size>0&&!this.circuit.isOpen;){let e=this.queue.peek();if(!e||this.circuit.isHalfOpen&&e.id!==this.circuit.probeTaskId||!this.permits.hasCapacityFor(e.weight))break;let t=this.queue.pop();this.enqueueOrder.remove(t),this.enqueueOrder.size===0&&this._clearTimeout();let i=Date.now(),r=Math.max(0,i-t.enqueueTime);this.permits.acquire(t.weight),this.totalAcquired++,this.metricsCollector?.onAcquireQueued(i,r,this.permits.inFlight,this.queue.size),this.permits.assertInvariant(this.debug),t.dispatch(()=>this._createRelease(t.isProbe,t.weight))&&this.hasListeners(n.TASKACQUIRE)&&this.emit(n.TASKACQUIRE,{queued:this.queue.size,running:this.permits.capacity-this.permits.available,...t.isProbe?{probe:!0}:{}});}this.queue.size===0&&this.permits.available===this.permits.capacity&&this.drainResolve&&(this.drainResolve(),this.drainResolve=null,this.drainPromise=null);}catch(e){e instanceof Error?console.error("[Semaphore] Scheduler error:",e.message,e.stack):console.error("[Semaphore] Scheduler error:",e);}}}_startPurgeInterval(){this.purgeIntervalId!==null&&clearInterval(this.purgeIntervalId),this.purgeIntervalId=setInterval(()=>{this.isShutdown||this._purgeStaleTasks();},this.purgeIntervalMs),this.purgeIntervalId.unref?.();}_purgeStaleTasks(){let e=Date.now(),t=this.queue.size,i=this.enqueueOrder.peekHead();for(;i!==void 0&&e-i.enqueueTime>this.queueMaxAge;){this.circuit.probeTaskId===i.id&&this.circuit.releaseProbeSlot();let r=i.discard(new o(`Task purged after ${this.queueMaxAge}ms`,"PURGED"));this._dequeue(i),r&&(this.totalTimeouts++,this.metricsCollector?.onTimeout(Date.now(),this.queue.size),this.emit(n.QUEUEPURGE,i),this.debug&&console.warn(`[Semaphore] Purged stale task #${i.id}`)),i=this.enqueueOrder.peekHead();}this.debug&&this.queue.size<t&&console.info(`[Semaphore] Purged ${t-this.queue.size} stale tasks`),this.queue.size<t&&this.schedule();}tryAcquire(e=1){return this.isShutdown||!Number.isInteger(e)||e<1||e>this.permits.capacity||(this.circuit.checkAndTransition()&&(this.emit(n.CIRCUITHALFOPEN),this.metricsCollector?.markCircuitHalfOpen(),this.debug&&console.info("[Semaphore] Circuit entering half-open"),this.schedule()),this.circuit.isOpen)?null:(this.circuit.trackAttempt(),this._tryAcquireFast(e))}acquire(e,t=0,i=1){if(this.isShutdown)return Promise.reject(new o("Semaphore is shut down","SHUTDOWN"));if(!Number.isInteger(i)||i<1||i>this.permits.capacity)return Promise.reject(new o(`Invalid weight: ${i} (must be integer in 1..${this.permits.capacity})`,"INVALID_WEIGHT"));if(typeof t!="number"||!Number.isFinite(t))return Promise.reject(new o(`Invalid priority: ${t} (must be a finite number)`,"INVALID_PRIORITY"));if(this.circuit.checkAndTransition()&&(this.emit(n.CIRCUITHALFOPEN),this.metricsCollector?.markCircuitHalfOpen(),this.debug&&console.info("[Semaphore] Circuit entering half-open"),this.schedule()),this.circuit.isOpen)return Promise.reject(new o(`Circuit breaker open, retry in ${this.circuit.cooldownRemaining}ms`,"CIRCUIT_OPEN"));if(this.circuit.isHalfOpen&&this.circuit.hasProbeInFlight)return Promise.reject(new o("Circuit breaker half-open, probe in flight","CIRCUIT_HALF_OPEN"));if(e?.aborted)return Promise.reject(new o("Semaphore acquire aborted before start","ABORTED"));this.circuit.trackAttempt();let r=this._tryAcquireFast(i);if(r)return Promise.resolve(r);let s=this.circuit.isHalfOpen&&!this.circuit.hasProbeInFlight;return !s&&this.rejectOnFull?Promise.reject(new o("Semaphore at capacity (rejectOnFull)","QUEUE_FULL")):!s&&this.queue.size>=this.queueMaxLength?Promise.reject(new o(`Queue full (${this.queueMaxLength})`,"QUEUE_FULL")):new Promise((h,p)=>{let m=++this.taskIdCounter,f=Date.now(),c=s,d=new y({id:m,priority:c?Number.MIN_SAFE_INTEGER:t,enqueueTime:f,isProbe:c,resolve:h,reject:p,abortSignal:e,weight:i});d.arm(()=>this._onTaskAbort(d,p)),c&&this.circuit.claimProbeSlot(m),this.metricsCollector?.sampleQueueDepthAt(Date.now(),this.queue.size+1),this._enqueue(d),this.schedule();})}async use(e,t,i=0,r=1){let s=await this.acquire(t,i,r);try{return await e()}finally{s();}}drain(e){return this.isShutdown?Promise.reject(new o("Cannot drain: semaphore is shut down","SHUTDOWN")):(e!==void 0&&u(e,"drain timeoutMs",1,Number.MAX_SAFE_INTEGER,true,true),this.drainPromise?this.drainPromise:this.queue.size===0&&this.permits.available===this.permits.capacity?Promise.resolve():(this.drainPromise=new Promise((t,i)=>{if(this.drainResolve=t,e!==void 0){let r=setTimeout(()=>{this.drainResolve=null,this.drainPromise=null,i(new o(`drain() timed out after ${e}ms`,"TIMEOUT"));},e),s=this.drainResolve;this.drainResolve=()=>{clearTimeout(r),s();};}}),this.drainPromise))}reset(e={}){if(this.isShutdown)throw new o("Cannot reset a semaphore that has been shut down","SHUTDOWN");for(let t of this.queue.toArray())t.discard(new o("Semaphore reset","SHUTDOWN"));this.queue.clear(),this.enqueueOrder.clear(),this._clearTimeout(),this.metricsCollector?.reset(),this.permits.reset(),this.backoff.reset(),this.circuit.reset(),this.releaseGeneration++,this.pendingReleaseCount=0,this.scheduled=false,this.isShutdown=false,this.taskIdCounter=0,this.totalAcquired=0,this.totalReleased=0,this.totalTimeouts=0,e.clearListeners&&this.eventListeners.clear(),this.drainResolve&&(this.drainResolve(),this.drainResolve=null,this.drainPromise=null),this._startPurgeInterval(),this.metricsCollector?.markCapacityChange(this.permits.capacity),this.metricsCollector?.sampleGauges(Date.now(),this.permits.inFlight,this.queue.size),this.debug&&console.info("[Semaphore] Reset to initial state");}shutdown(e="Semaphore shutdown"){if(!this.isShutdown){this.isShutdown=true,this.debug&&console.info(`[Semaphore] Shutdown: ${e}`),this.purgeIntervalId!==null&&(clearInterval(this.purgeIntervalId),this.purgeIntervalId=null),this._clearTimeout();for(let t of this.queue.toArray())t.discard(new o(e,"SHUTDOWN"));this.queue.clear(),this.enqueueOrder.clear(),this.metricsCollector?.destroy(),this.drainResolve&&(this.drainResolve(),this.drainResolve=null,this.drainPromise=null),this.emit(n.SHUTDOWN,e);}}cancel(){if(this.isShutdown)return;let e=this.queue.toArray();for(let t of e)t.isProbe&&this.circuit.releaseProbeSlot(),t.discard(new o("Semaphore acquire cancelled","CANCELLED")),this._dequeue(t);this.metricsCollector?.sampleQueueDepthAt(Date.now(),this.queue.size),this.debug&&console.info(`[Semaphore] Cancelled ${e.length} queued tasks`),this.schedule();}status(){let e=this.metricsCollector?.getSnapshot()??null,t=e?.windows?.["1m"]?.counts,i=t?.acquired??0,r=t?.timeouts??0,s=this.enqueueOrder.peekHead(),h=s===void 0?0:Math.max(0,Date.now()-s.enqueueTime);return {status:{running:this.permits.capacity-this.permits.available,queued:this.queue.size,available:this.permits.available,inFlight:this.permits.inFlight,pendingReleases:this.pendingReleaseCount,circuitOpen:this.circuit.isOpen,circuitHalfOpen:this.circuit.isHalfOpen,backoffDelay:Math.round(this.backoff.currentDelay),requestsPerSecond:+(i/60).toFixed(2),timeoutRate1m:i+r>0?+(r/(i+r)*100).toFixed(1):0,queueAge:h},lifetime:{totalAcquired:this.totalAcquired,totalReleased:this.totalReleased,totalTimeouts:this.totalTimeouts,circuitBreakerCooldownRemaining:this.circuit.cooldownRemaining},metrics:e}}isAvailable(){return !this.isShutdown&&!this.circuit.isOpen&&!this.permits.isFull}get queueLength(){return this.queue.size}get availablePermits(){return this.permits.available}};export{b as CircuitBreaker,k as QUEUE_ORDERINGS,_ as Semaphore,o as SemaphoreError,n as SemaphoreEvents};//# sourceMappingURL=index.js.map | ||
| var a=class extends Error{constructor(e,t){super(e),this.name="SemaphoreError",this.code=t;}};var l=(o,e,t,i,r=true,s=true)=>{if(typeof o!="number"||Number.isNaN(o))throw new a(`${e} must be a valid number`,"INVALID_ARGUMENT");if(r&&!Number.isInteger(o))throw new a(`${e} must be an integer`,"INVALID_ARGUMENT");if(s?o<t:o<=t)throw new a(`${e} must be ${s?">=":">"} ${t}`,"INVALID_ARGUMENT");if(s?o>i:o>=i)throw new a(`${e} must be ${s?"<=":"<"} ${i}`,"INVALID_ARGUMENT");return o};var w=class{constructor(e){this._inFlight=0;this.capacity=e,this._available=e;}get available(){return this._available}get inFlight(){return this._inFlight}get isFull(){return this._available===0}hasCapacityFor(e){return this._available>=e}acquire(e=1){this._available-=e,this._inFlight+=e;}release(e=1){this._available=Math.min(this._available+e,this.capacity),this._inFlight=this._inFlight<e?0:this._inFlight-e;}assertInvariant(e){if(!e)return;let t=this._inFlight+this._available;t!==this.capacity&&console.error(`[Semaphore] Invariant violation: inFlight(${this._inFlight}) + available(${this._available}) = ${t}, expected capacity(${this.capacity})`);}reset(){this._available=this.capacity,this._inFlight=0;}};var A=class{constructor(e,t){this.cachedIndex=-1;this.cachedUntil=0;this.size=e,this.stepMs=t,this.buckets=new Int32Array(e*2),this.timestamps=new Float64Array(e);}idx(){let e=Date.now();if(e<this.cachedUntil&&e>=this.cachedUntil-this.stepMs)return this.cachedIndex;let t=Math.floor(e/this.stepMs)*this.stepMs,i=Math.floor(t/this.stepMs)%this.size;return this.timestamps[i]!==t&&(this.timestamps[i]=t,this.buckets[i*2]=0,this.buckets[i*2+1]=0),this.cachedIndex=i,this.cachedUntil=t+this.stepMs,i}addAcquired(){this.buckets[this.idx()*2]++;}addTimeout(){this.buckets[this.idx()*2+1]++;}snapshot(){let e=Date.now(),t=Math.floor(e/this.stepMs)*this.stepMs-(this.size-1)*this.stepMs,i=0,r=0;for(let s=0;s<this.size;s++)this.timestamps[s]>=t&&(i+=this.buckets[s*2],r+=this.buckets[s*2+1]);return {acquired:i,timeouts:r}}reset(){this.cachedIndex=-1,this.cachedUntil=0,this.buckets.fill(0),this.timestamps.fill(0);}},f=class{constructor(e={}){this.state="closed";this.openUntil=0;this.probeInFlight=false;this._probeTaskId=null;if(this.threshold=l(e.threshold??.5,"SaturationCircuitBreaker threshold",0,1,false,false),this.window=l(e.window??1e4,"SaturationCircuitBreaker window",1e3,Number.MAX_SAFE_INTEGER,true,true),this.windowBucketWidth=l(e.windowBucketWidth??1e3,"SaturationCircuitBreaker windowBucketWidth",1,Number.MAX_SAFE_INTEGER,true,true),this.cooldown=l(e.cooldown??5e3,"SaturationCircuitBreaker cooldown",1e3,Number.MAX_SAFE_INTEGER,true,true),this.minThroughput=l(e.minThroughput??10,"SaturationCircuitBreaker minThroughput",1,Number.MAX_SAFE_INTEGER,true,true),this.minFailures=l(e.minFailures??5,"SaturationCircuitBreaker minFailures",1,Number.MAX_SAFE_INTEGER,true,true),this.eventWindow=new A(Math.ceil(this.window/this.windowBucketWidth),this.windowBucketWidth),this.window<this.windowBucketWidth)throw new a("SaturationCircuitBreaker window must be >= windowBucketWidth","INVALID_ARGUMENT");if(this.minThroughput<this.minFailures)throw new a("SaturationCircuitBreaker minThroughput must be >= minFailures","INVALID_ARGUMENT")}get isClosed(){return this.state==="closed"}get isOpen(){return this.state==="open"}get isHalfOpen(){return this.state==="half-open"}get hasProbeInFlight(){return this.probeInFlight}get probeTaskId(){return this._probeTaskId}get cooldownRemaining(){return this.isOpen?Math.max(0,this.openUntil-Date.now()):0}trackAttempt(){this.state==="closed"&&this.eventWindow.addAcquired();}recordFailure(){this.eventWindow.addTimeout();}checkAndTransition(){return this.state!=="open"||Date.now()<this.openUntil?false:(this.state="half-open",this.probeInFlight=false,this._probeTaskId=null,true)}evaluateAndTrip(){if(this.state!=="closed")return {tripped:false};let{timeouts:e,acquired:t}=this.eventWindow.snapshot();if(t<this.minThroughput||e<this.minFailures)return {tripped:false};let i=e/t;return i<this.threshold?{tripped:false}:(this.state="open",this.openUntil=Date.now()+this.cooldown,{tripped:true,timeoutRate:i,failures:e,attempts:t})}markProbeInFlight(){this.probeInFlight=true;}claimProbeSlot(e){this.probeInFlight=true,this._probeTaskId=e;}releaseProbeSlot(){this.probeInFlight=false,this._probeTaskId=null;}handleProbeSuccess(){this.state="closed",this.probeInFlight=false,this._probeTaskId=null,this.eventWindow.reset();}handleProbeFailure(){this.state="open",this.probeInFlight=false,this._probeTaskId=null,this.openUntil=Date.now()+this.cooldown;}reset(){this.state="closed",this.openUntil=0,this.probeInFlight=false,this._probeTaskId=null,this.eventWindow.reset();}};var T=class{constructor(e={}){this.delay=0;this.lastTimestamp=0;if(this.initialTimeout=l(e.initialTimeout??50,"BackoffTracker initialTimeout",0,Number.MAX_SAFE_INTEGER,true,true),this.maxTimeout=l(e.maxTimeout??2e3,"BackoffTracker maxTimeout",0,Number.MAX_SAFE_INTEGER,true,true),this.decayFactor=l(e.decayFactor??.5,"BackoffTracker decayFactor",0,1,false,false),this.maxTimeout<this.initialTimeout)throw new a("BackoffTracker maxTimeout must be >= initialTimeout","INVALID_ARGUMENT")}get currentDelay(){if(this.delay===0)return 0;let e=(Date.now()-this.lastTimestamp)/1e3;if(e<=0)return this.delay;let t=this.delay*Math.pow(this.decayFactor,e);return t<1?0:t}onTimeout(){let e=this.currentDelay,t=e>0?e*2:this.initialTimeout;this.delay=Math.min(t,this.maxTimeout),this.lastTimestamp=Date.now();}reset(){this.delay=0,this.lastTimestamp=0;}};var y=class{constructor(e){this.prev=null;this.next=null;this.heapIndex=-1;this.completed=false;this.id=e.id,this.priority=e.priority,this.enqueueTime=e.enqueueTime,this.isProbe=e.isProbe,this.weight=e.weight??1,this._resolve=e.resolve,this._reject=e.reject,this.abortSignal=e.abortSignal;}arm(e){this.abortSignal&&(this.abortListener=()=>{this.claim()&&e();},this.abortSignal.addEventListener("abort",this.abortListener));}dispatch(e){return this.claim()?(this._resolve(e()),true):false}discard(e){return this.claim()?(this._reject(e),true):false}claim(){return this.completed?false:(this.completed=true,this.abortSignal&&this.abortListener&&this.abortSignal.removeEventListener("abort",this.abortListener),true)}reject(e){this._reject(e);}};var S=class{constructor(e){this.heap=[];this.compare=e;}get size(){return this.heap.length}isEmpty(){return this.heap.length===0}peek(){return this.heap[0]}toArray(){return [...this.heap]}has(e){return e.heapIndex>=0&&this.heap[e.heapIndex]===e}clear(){for(let e of this.heap)e.heapIndex=-1;this.heap=[];}insert(e){if(e.heapIndex>=0)throw new a("Item is already in a heap","INVALID_ARGUMENT");let t=this.heap.length;this.heap.push(e),e.heapIndex=t,this._bubbleUp(t);}pop(){if(this.heap.length===0)return;let e=this.heap[0];e.heapIndex=-1;let t=this.heap.pop();return this.heap.length>0&&t!==e&&(this.heap[0]=t,t.heapIndex=0,this._bubbleDown(0)),e}delete(e){let t=e.heapIndex;if(t<0||this.heap[t]!==e)return;e.heapIndex=-1;let i=this.heap.pop();if(t===this.heap.length||i===e)return e;this.heap[t]=i,i.heapIndex=t;let r=this.heap[t-1>>1];return t>0&&this.compare(i,r)<0?this._bubbleUp(t):this._bubbleDown(t),e}_swap(e,t){let i=this.heap[e],r=this.heap[t];this.heap[e]=r,this.heap[t]=i,i.heapIndex=t,r.heapIndex=e;}_bubbleUp(e){for(;e>0;){let t=e-1>>1;if(this.compare(this.heap[e],this.heap[t])<0)this._swap(e,t),e=t;else break}}_bubbleDown(e){let t=this.heap.length;for(;;){let i=e,r=2*e+1,s=2*e+2;if(r<t&&this.compare(this.heap[r],this.heap[i])<0&&(i=r),s<t&&this.compare(this.heap[s],this.heap[i])<0&&(i=s),i===e)break;this._swap(e,i),e=i;}}};var I=class{constructor(){this.head=null;this.tail=null;this._size=0;}get size(){return this._size}isEmpty(){return this._size===0}peekHead(){return this.head===null?void 0:this.head}pushTail(e){e.prev=this.tail,e.next=null,this.tail!==null?this.tail.next=e:this.head=e,this.tail=e,this._size++;}remove(e){e.prev!==null?e.prev.next=e.next:this.head=e.next,e.next!==null?e.next.prev=e.prev:this.tail=e.prev,e.prev=e.next=null,this._size--;}clear(){this.head=this.tail=null,this._size=0;}};var E=class{constructor(e,t){this.cachedIndex=-1;this.cachedUntil=0;this.size=l(e,"CombinedWindow size",1,Number.MAX_SAFE_INTEGER,true,true),this.stepMs=l(t,"CombinedWindow stepMs",1,Number.MAX_SAFE_INTEGER,true,true),this.timestamps=new Float64Array(e),this.acquired=new Int32Array(e),this.released=new Int32Array(e),this.timeouts=new Int32Array(e),this.ifSum=new Float64Array(e),this.ifCount=new Int32Array(e),this.ifMax=new Float64Array(e),this.qSum=new Float64Array(e),this.qCount=new Int32Array(e),this.qMax=new Float64Array(e),this.latSum=new Float64Array(e),this.latCount=new Int32Array(e);}bucket(e){if(e<this.cachedUntil&&e>=this.cachedUntil-this.stepMs)return this.cachedIndex;let t=Math.floor(e/this.stepMs),i=t*this.stepMs,r=t%this.size;return this.timestamps[r]!==i&&(this.timestamps[r]=i,this.acquired[r]=0,this.released[r]=0,this.timeouts[r]=0,this.ifSum[r]=0,this.ifCount[r]=0,this.ifMax[r]=0,this.qSum[r]=0,this.qCount[r]=0,this.qMax[r]=0,this.latSum[r]=0,this.latCount[r]=0),this.cachedIndex=r,this.cachedUntil=i+this.stepMs,r}addInflight(e,t){this.ifSum[e]+=t,this.ifCount[e]++,t>this.ifMax[e]&&(this.ifMax[e]=t);}addQueue(e,t){this.qSum[e]+=t,this.qCount[e]++,t>this.qMax[e]&&(this.qMax[e]=t);}recordAcquire(e,t,i){let r=this.bucket(e);this.acquired[r]++,this.addInflight(r,t),this.addQueue(r,i);}recordAcquireQueued(e,t,i,r){let s=this.bucket(e);this.acquired[s]++,this.latSum[s]+=t,this.latCount[s]++,this.addInflight(s,i),this.addQueue(s,r);}recordRelease(e,t,i){let r=this.bucket(e);this.released[r]++,this.addInflight(r,t),this.addQueue(r,i);}recordTimeoutQueue(e,t){let i=this.bucket(e);this.timeouts[i]++,this.addQueue(i,t);}sampleBoth(e,t,i){let r=this.bucket(e);this.addInflight(r,t),this.addQueue(r,i);}addAcquired(e){this.acquired[this.bucket(e)]++;}addReleased(e){this.released[this.bucket(e)]++;}addTimeout(e){this.timeouts[this.bucket(e)]++;}addLatency(e,t){let i=this.bucket(e);this.latSum[i]+=t,this.latCount[i]++;}sampleInflight(e,t){this.addInflight(this.bucket(e),t);}sampleQueue(e,t){this.addQueue(this.bucket(e),t);}snapshot(e){let t=Math.floor(e/this.stepMs)*this.stepMs-(this.size-1)*this.stepMs,i=0,r=0,s=0,u=0,c=0,d=0,b=0,p=0,m=0,C=0,v=0;for(let h=0;h<this.size;h++)this.timestamps[h]<t||(i+=this.acquired[h],r+=this.released[h],s+=this.timeouts[h],u+=this.ifSum[h],c+=this.ifCount[h],this.ifMax[h]>d&&(d=this.ifMax[h]),b+=this.qSum[h],p+=this.qCount[h],this.qMax[h]>m&&(m=this.qMax[h]),C+=this.latSum[h],v+=this.latCount[h]);return {counts:{acquired:i,released:r,timeouts:s},inflight:{avg:c===0?0:u/c,max:d,samples:c},queue:{avg:p===0?0:b/p,max:m,samples:p},latency:{avg:v===0?0:C/v,count:v,total:C}}}reset(){this.cachedIndex=-1,this.cachedUntil=0,this.timestamps.fill(0),this.acquired.fill(0),this.released.fill(0),this.timeouts.fill(0),this.ifSum.fill(0),this.ifCount.fill(0),this.ifMax.fill(0),this.qSum.fill(0),this.qCount.fill(0),this.qMax.fill(0),this.latSum.fill(0),this.latCount.fill(0);}},R=[{size:60,stepMs:1e3},{size:60,stepMs:5e3},{size:60,stepMs:15e3},{size:60,stepMs:6e4},{size:60,stepMs:144e4}],k=class{constructor(e=R){this._totalAcquiredFast=0;this._totalAcquiredQueued=0;this._totalReleased=0;this._totalTimeouts=0;this._totalAborts=0;this._capacity=0;this._circuitOpen=false;this._circuitHalfOpen=false;if(e.length===0)throw new a("SemaphoreMetrics requires at least one WindowOptions","INVALID_ARGUMENT");this.windows=e.map(({size:r,stepMs:s})=>new E(r,s)),this.windowLabels=e.map(({size:r,stepMs:s})=>{let u=r*s;return u>=36e5&&u%36e5===0?`${u/36e5}h`:u>=6e4&&u%6e4===0?`${u/6e4}m`:u>=1e3&&u%1e3===0?`${u/1e3}s`:`${u}ms`});let t=new Set;for(let r of this.windowLabels){if(t.has(r))throw new a(`SemaphoreMetrics windows produce duplicate label "${r}" (two windows cover the same horizon)`,"INVALID_ARGUMENT");t.add(r);}let i=0;for(let r=1;r<e.length;r++)e[r].size*e[r].stepMs<e[i].size*e[i].stepMs&&(i=r);this.primaryLabel=this.windowLabels[i],this.primaryWindowMs=e[i].size*e[i].stepMs;}onAcquireFast(e,t,i){this._totalAcquiredFast++;for(let r of this.windows)r.recordAcquire(e,t,i);}onAcquireQueued(e,t,i,r){this._totalAcquiredQueued++;for(let s of this.windows)s.recordAcquireQueued(e,t,i,r);}onRelease(e,t,i){this._totalReleased++;for(let r of this.windows)r.recordRelease(e,t,i);}onTimeout(e,t){this._totalTimeouts++;for(let i of this.windows)i.recordTimeoutQueue(e,t);}onAbort(e,t){this._totalAborts++;for(let i of this.windows)i.sampleQueue(e,t);}sampleGauges(e,t,i){for(let r of this.windows)r.sampleBoth(e,t,i);}sampleQueueDepthAt(e,t){for(let i of this.windows)i.sampleQueue(e,t);}markAcquireFast(){this._totalAcquiredFast++;let e=Date.now();for(let t of this.windows)t.addAcquired(e);}markAcquireQueued(e){this._totalAcquiredQueued++;let t=Date.now();for(let i of this.windows)i.addAcquired(t),i.addLatency(t,e);}markReleased(){this._totalReleased++;let e=Date.now();for(let t of this.windows)t.addReleased(e);}markTimeout(){this._totalTimeouts++;let e=Date.now();for(let t of this.windows)t.addTimeout(e);}markAbort(){this._totalAborts++;}sampleInFlight(e){let t=Date.now();for(let i of this.windows)i.sampleInflight(t,e);}sampleQueueDepth(e){let t=Date.now();for(let i of this.windows)i.sampleQueue(t,e);}markCapacityChange(e){this._capacity=e;}markCircuitOpen(){this._circuitOpen=true,this._circuitHalfOpen=false;}markCircuitHalfOpen(){this._circuitOpen=false,this._circuitHalfOpen=true;}markCircuitClose(){this._circuitOpen=false,this._circuitHalfOpen=false;}getSnapshot(){let e=Date.now(),t={},i=0,r=0;for(let s=0;s<this.windowLabels.length;s++){let u=this.windows[s].snapshot(e);t[this.windowLabels[s]]=u,s===0&&(i=u.inflight.avg,r=u.queue.avg);}return {windows:t,meta:{inFlightLastMinute:Math.round(i),queueDepthLastMinute:Math.round(r),totalAcquiredFast:this._totalAcquiredFast,totalAcquiredQueued:this._totalAcquiredQueued,totalReleased:this._totalReleased,totalTimeouts:this._totalTimeouts,totalAborts:this._totalAborts,capacity:this._capacity,circuitOpen:this._circuitOpen,circuitHalfOpen:this._circuitHalfOpen}}}reset(){for(let e of this.windows)e.reset();this._totalAcquiredFast=0,this._totalAcquiredQueued=0,this._totalReleased=0,this._totalTimeouts=0,this._totalAborts=0,this._capacity=0,this._circuitOpen=false,this._circuitHalfOpen=false;}destroy(){this.reset();}};var _={fifo:(o,e)=>o.id-e.id,lifo:(o,e)=>e.id-o.id,fifoWithPriority:(o,e)=>o.priority-e.priority||o.id-e.id,lifoWithPriority:(o,e)=>o.priority-e.priority||e.id-o.id};function x(o){if(o.comparator!==void 0){if(typeof o.comparator!="function")throw new a("Semaphore comparator must be a function","INVALID_ARGUMENT");return o.comparator}let e=o.queueOrder??"fifoWithPriority",t=_[e];if(t===void 0)throw new a(`Semaphore queueOrder must be one of: ${Object.keys(_).join(", ")} (got ${JSON.stringify(e)})`,"INVALID_ARGUMENT");return t}function F(o){let e=x(o);return (t,i)=>{if(t.isProbe!==i.isProbe)return t.isProbe?-1:1;let r=e(t,i);return typeof r!="number"||Number.isNaN(r)?t.id-i.id:r}}var n={TASKACQUIRE:"task-acquire",TASKRELEASE:"task-release",TASKTIMEOUT:"task-timeout",TASKABORT:"task-abort",QUEUEPURGE:"queue-purge",QUEUEEVICT:"queue-evict",CIRCUITOPEN:"circuit-open",CIRCUITHALFOPEN:"circuit-half-open",CIRCUITCLOSE:"circuit-close",SHUTDOWN:"shutdown"};var M=class{constructor(e,t={}){this.pendingReleaseCount=0;this.releaseGeneration=0;this.scheduled=false;this.isShutdown=false;this.taskIdCounter=0;this.totalAcquired=0;this.totalReleased=0;this.totalTimeouts=0;this.totalEvictions=0;this.eventListeners=new Map;this.drainPromise=null;this.drainResolve=null;this.purgeIntervalId=null;this.timeoutTimerId=null;this._runSchedulerBound=()=>{this._runScheduler();};if(l(e,"Semaphore count",0,Number.MAX_SAFE_INTEGER,true,false),this.purgeIntervalMs=l(t.purgeIntervalMs??3e3,"Semaphore purgeIntervalMs",500,Number.MAX_SAFE_INTEGER,true,true),this.queueMaxTimeout=l(t.queueMaxTimeout??1e4,"Semaphore queueMaxTimeout",1,Number.MAX_SAFE_INTEGER,true,true),this.queueMaxLength=l(t.queueMaxLength??1024,"Semaphore queueMaxLength",1,Number.MAX_SAFE_INTEGER,true,true),this.queueMaxAge=l(t.queueMaxAge??3e4,"Semaphore queueMaxAge",1,Number.MAX_SAFE_INTEGER,true,true),this.rejectOnFull=t.rejectOnFull??false,this.metricsEnabled=t.metricsEnabled??true,this.debug=t.debug??false,t.circuitBreakerFailurePredicate!==void 0&&typeof t.circuitBreakerFailurePredicate!="function")throw new a("Semaphore circuitBreakerFailurePredicate must be a function","INVALID_ARGUMENT");this.failurePredicate=t.circuitBreakerFailurePredicate;let i=F({queueOrder:t.queueOrder,comparator:t.comparator});this.queue=new S(i),this.enqueueOrder=new I,this.permits=new w(e),this.circuit=t.circuitBreaker??new f({threshold:t.circuitBreakerThreshold,window:t.circuitBreakerWindow,windowBucketWidth:t.circuitBreakerWindowBucketWidth,cooldown:t.circuitBreakerCooldown,minThroughput:t.circuitBreakerMinThroughput,minFailures:t.circuitBreakerMinFailures}),this.backoff=new T({initialTimeout:t.backoffInitialTimeout,maxTimeout:t.backoffMaxTimeout,decayFactor:t.backoffDecayFactor}),this.metricsCollector=this.metricsEnabled?new k(t.metricsWindows):void 0,this.metricsCollector?.markCapacityChange(this.permits.capacity),this.metricsCollector?.sampleGauges(Date.now(),this.permits.inFlight,this.queue.size),this._startPurgeInterval();}on(e,t){this.eventListeners.has(e)||this.eventListeners.set(e,new Set),this.eventListeners.get(e).add(t);}off(e,t){this.eventListeners.get(e)?.delete(t),this.eventListeners.get(e)?.size===0&&this.eventListeners.delete(e);}removeAllListeners(e){e?this.eventListeners.delete(e):this.eventListeners.clear();}hasListeners(e){let t=this.eventListeners.get(e);return t!==void 0&&t.size>0}emit(e,...t){let i=this.eventListeners.get(e);if(!(!i||i.size===0)){if(i.size===1){let r=i.values().next().value;try{r(...t);}catch(s){console.warn(`[Semaphore] Error in listener for "${e}":`,s);}return}for(let r of Array.from(i))try{r(...t);}catch(s){console.warn(`[Semaphore] Error in listener for "${e}":`,s);}}}_tryAcquireFast(e){return this.circuit.isHalfOpen?this.circuit.hasProbeInFlight||!this.permits.hasCapacityFor(e)?null:(this.circuit.markProbeInFlight(),this.permits.acquire(e),this.totalAcquired++,this.metricsCollector?.onAcquireFast(Date.now(),this.permits.inFlight,this.queue.size),this.hasListeners(n.TASKACQUIRE)&&this.emit(n.TASKACQUIRE,{queued:this.queue.size,running:this.permits.capacity-this.permits.available,probe:true}),this._createRelease(true,e)):this.queue.size>0||!this.permits.hasCapacityFor(e)?null:(this.permits.acquire(e),this.totalAcquired++,this.metricsCollector?.onAcquireFast(Date.now(),this.permits.inFlight,this.queue.size),this.hasListeners(n.TASKACQUIRE)&&this.emit(n.TASKACQUIRE,{queued:this.queue.size,running:this.permits.capacity-this.permits.available}),this._createRelease(false,e))}_createRelease(e=false,t=1){this.pendingReleaseCount++;let i=this.releaseGeneration,r=false;return ()=>{if(r||i!==this.releaseGeneration){this.debug&&console.warn("[Semaphore] release() called after already released (no-op)");return}r=true,this.pendingReleaseCount--,this.permits.release(t),this.totalReleased++,this.metricsCollector?.onRelease(Date.now(),this.permits.inFlight,this.queue.size),e&&this.circuit.isHalfOpen&&(this.circuit.handleProbeSuccess(),this.emit(n.CIRCUITCLOSE),this.metricsCollector?.markCircuitClose(),this.debug&&console.info("[Semaphore] Circuit closed after successful probe")),this.permits.assertInvariant(this.debug),this.hasListeners(n.TASKRELEASE)&&this.emit(n.TASKRELEASE,{queued:this.queue.size,running:this.permits.capacity-this.permits.available}),this._checkDrain(),this.schedule();}}_checkDrain(){this.drainResolve&&this.queue.size===0&&this.permits.available===this.permits.capacity&&(this.drainResolve(),this.drainResolve=null,this.drainPromise=null);}_enqueue(e){this.queue.insert(e),this.enqueueOrder.pushTail(e),this._armTimeout();}_dequeue(e){this.queue.delete(e)!==void 0&&(this.enqueueOrder.remove(e),this.enqueueOrder.size===0&&this._clearTimeout());}_armTimeout(){if(this.timeoutTimerId!==null)return;let e=this.enqueueOrder.peekHead();if(e===void 0)return;let t=Math.max(0,e.enqueueTime+this.queueMaxTimeout-Date.now());this.timeoutTimerId=setTimeout(()=>this._fireTimeout(),t);}_clearTimeout(){this.timeoutTimerId!==null&&(clearTimeout(this.timeoutTimerId),this.timeoutTimerId=null);}_fireTimeout(){if(this.timeoutTimerId=null,this.isShutdown)return;let e=Date.now(),t=0,i=this.enqueueOrder.peekHead();for(;i!==void 0&&i.enqueueTime+this.queueMaxTimeout<=e;){let r=i.claim();this._dequeue(i),r&&(this._timeoutTask(i),t++),i=this.enqueueOrder.peekHead();}if(i!==void 0){let r=Math.max(0,i.enqueueTime+this.queueMaxTimeout-e);this.timeoutTimerId=setTimeout(()=>this._fireTimeout(),r);}t>0&&(this._checkDrain(),this.schedule());}_evictQueueOnCircuitOpen(e){if(this.queue.size===0)return;let t=0;for(let i of this.queue.toArray()){if(i===e||i.isProbe)continue;let r=i.discard(new a("Circuit breaker opened while task was queued","CIRCUIT_OPEN"));this._dequeue(i),r&&(t++,this.totalEvictions++,this.emit(n.QUEUEEVICT,{id:i.id,priority:i.priority,enqueueTime:i.enqueueTime,weight:i.weight}),this.debug&&console.warn(`[Semaphore] Evicted queued task #${i.id}: circuit opened`));}t>0&&this.metricsCollector?.sampleQueueDepthAt(Date.now(),this.queue.size);}_timeoutTask(e){if(this.totalTimeouts++,this.circuit.recordFailure(),this.backoff.onTimeout(),e.isProbe)this.circuit.handleProbeFailure(),this.emit(n.CIRCUITOPEN,{timeoutRate:1,recentTimeouts:1,total:1,reason:"half-open-probe-failed"}),this.metricsCollector?.markCircuitOpen(),this.debug&&console.warn("[Semaphore] Circuit re-opened: half-open probe timed out");else {let t=this.circuit.evaluateAndTrip();t.tripped&&(this.emit(n.CIRCUITOPEN,{timeoutRate:t.timeoutRate,recentTimeouts:t.failures,total:t.attempts}),this.metricsCollector?.markCircuitOpen(),this.debug&&console.warn(`[Semaphore] Circuit opened. Rate: ${(t.timeoutRate*100).toFixed(1)}%`),this._evictQueueOnCircuitOpen(e));}this.metricsCollector?.onTimeout(Date.now(),this.queue.size),this.emit(n.TASKTIMEOUT,{queueLength:this.queue.size,backoffDelay:this.backoff.currentDelay,taskId:e.id}),this.debug&&console.warn(`[Semaphore] Task #${e.id} timed out after ${this.queueMaxTimeout}ms`),e.reject(new a(`Semaphore acquire timed out after ${this.queueMaxTimeout}ms (queue: ${this.queue.size})`,"TIMEOUT"));}_onTaskAbort(e,t){this._dequeue(e),e.isProbe&&this.circuit.releaseProbeSlot(),this.metricsCollector?.onAbort(Date.now(),this.queue.size),this.emit(n.TASKABORT),this.debug&&console.info(`[Semaphore] Task #${e.id} aborted`),t(new a("Semaphore acquire aborted","ABORTED")),this._checkDrain(),this.schedule();}schedule(){if(this.scheduled||this.isShutdown)return;this.scheduled=true;let e=this.backoff.currentDelay;e>0?setTimeout(this._runSchedulerBound,e).unref?.():queueMicrotask(this._runSchedulerBound);}_runScheduler(){if(!this.isShutdown){this.scheduled=false;try{for(;this.queue.size>0&&!this.circuit.isOpen;){let e=this.queue.peek();if(!e||this.circuit.isHalfOpen&&e.id!==this.circuit.probeTaskId||!this.permits.hasCapacityFor(e.weight))break;let t=this.queue.pop();this.enqueueOrder.remove(t),this.enqueueOrder.size===0&&this._clearTimeout();let i=Date.now(),r=Math.max(0,i-t.enqueueTime);this.permits.acquire(t.weight),this.totalAcquired++,this.metricsCollector?.onAcquireQueued(i,r,this.permits.inFlight,this.queue.size),this.permits.assertInvariant(this.debug),t.dispatch(()=>this._createRelease(t.isProbe,t.weight))&&this.hasListeners(n.TASKACQUIRE)&&this.emit(n.TASKACQUIRE,{queued:this.queue.size,running:this.permits.capacity-this.permits.available,...t.isProbe?{probe:!0}:{}});}this._checkDrain();}catch(e){e instanceof Error?console.error("[Semaphore] Scheduler error:",e.message,e.stack):console.error("[Semaphore] Scheduler error:",e);}}}_startPurgeInterval(){this.purgeIntervalId!==null&&clearInterval(this.purgeIntervalId),this.purgeIntervalId=setInterval(()=>{this.isShutdown||this._purgeStaleTasks();},this.purgeIntervalMs),this.purgeIntervalId.unref?.();}_purgeStaleTasks(){let e=Date.now(),t=this.queue.size,i=this.enqueueOrder.peekHead();for(;i!==void 0&&e-i.enqueueTime>this.queueMaxAge;){this.circuit.probeTaskId===i.id&&this.circuit.releaseProbeSlot();let r=i.discard(new a(`Task purged after ${this.queueMaxAge}ms`,"PURGED"));this._dequeue(i),r&&(this.totalTimeouts++,this.metricsCollector?.onTimeout(Date.now(),this.queue.size),this.emit(n.QUEUEPURGE,{id:i.id,priority:i.priority,enqueueTime:i.enqueueTime,weight:i.weight}),this.debug&&console.warn(`[Semaphore] Purged stale task #${i.id}`)),i=this.enqueueOrder.peekHead();}this.debug&&this.queue.size<t&&console.info(`[Semaphore] Purged ${t-this.queue.size} stale tasks`),this.queue.size<t&&(this._checkDrain(),this.schedule());}tryAcquire(e=1){return this.isShutdown||!Number.isInteger(e)||e<1||e>this.permits.capacity||(this.circuit.checkAndTransition()&&(this.emit(n.CIRCUITHALFOPEN),this.metricsCollector?.markCircuitHalfOpen(),this.debug&&console.info("[Semaphore] Circuit entering half-open"),this.schedule()),this.circuit.isOpen)?null:this._tryAcquireFast(e)}acquire(e,t=0,i=1){if(this.isShutdown)return Promise.reject(new a("Semaphore is shut down","SHUTDOWN"));if(!Number.isInteger(i)||i<1||i>this.permits.capacity)return Promise.reject(new a(`Invalid weight: ${i} (must be integer in 1..${this.permits.capacity})`,"INVALID_WEIGHT"));if(typeof t!="number"||!Number.isFinite(t))return Promise.reject(new a(`Invalid priority: ${t} (must be a finite number)`,"INVALID_PRIORITY"));if(this.circuit.checkAndTransition()&&(this.emit(n.CIRCUITHALFOPEN),this.metricsCollector?.markCircuitHalfOpen(),this.debug&&console.info("[Semaphore] Circuit entering half-open"),this.schedule()),this.circuit.isOpen)return Promise.reject(new a(`Circuit breaker open, retry in ${this.circuit.cooldownRemaining}ms`,"CIRCUIT_OPEN"));if(this.circuit.isHalfOpen&&this.circuit.hasProbeInFlight)return Promise.reject(new a("Circuit breaker half-open, probe in flight","CIRCUIT_HALF_OPEN"));if(e?.aborted)return Promise.reject(new a("Semaphore acquire aborted before start","ABORTED"));this.circuit.trackAttempt();let r=this._tryAcquireFast(i);if(r)return Promise.resolve(r);let s=this.circuit.isHalfOpen&&!this.circuit.hasProbeInFlight;return !s&&this.rejectOnFull?Promise.reject(new a("Semaphore at capacity (rejectOnFull)","QUEUE_FULL")):!s&&this.queue.size>=this.queueMaxLength?Promise.reject(new a(`Queue full (${this.queueMaxLength})`,"QUEUE_FULL")):new Promise((u,c)=>{let d=++this.taskIdCounter,b=Date.now(),p=s,m=new y({id:d,priority:p?Number.MIN_SAFE_INTEGER:t,enqueueTime:b,isProbe:p,resolve:u,reject:c,abortSignal:e,weight:i});m.arm(()=>this._onTaskAbort(m,c)),p&&this.circuit.claimProbeSlot(d),this.metricsCollector?.sampleQueueDepthAt(Date.now(),this.queue.size+1),this._enqueue(m),this.schedule();})}peekQueue(){let e=[],t=this.enqueueOrder.peekHead();for(;t!==void 0;)e.push({id:t.id,priority:t.priority,enqueueTime:t.enqueueTime,weight:t.weight,isProbe:t.isProbe}),t=t.next??void 0;return e}async use(e,t,i=0,r=1){let s=await this.acquire(t,i,r),u=this.failurePredicate!==void 0&&this.circuit.isHalfOpen;try{let c=await e();return s(),c}catch(c){if(this.failurePredicate!==void 0){let d=false;try{d=this.failurePredicate(c)===!0;}catch(b){console.warn("[Semaphore] circuitBreakerFailurePredicate threw (rejection treated as non-matching):",b);}d&&(u&&this.circuit.isHalfOpen?(this.circuit.recordFailure(),this.circuit.handleProbeFailure(),this.emit(n.CIRCUITOPEN,{timeoutRate:1,recentTimeouts:1,total:1,reason:"half-open-probe-failed"}),this.metricsCollector?.markCircuitOpen(),this.debug&&console.warn("[Semaphore] Circuit re-opened: half-open probe operation failed")):this.reportFailure());}throw s(),c}}drain(e){return this.isShutdown?Promise.reject(new a("Cannot drain: semaphore is shut down","SHUTDOWN")):(e!==void 0&&l(e,"drain timeoutMs",1,Number.MAX_SAFE_INTEGER,true,true),this.drainPromise?this.drainPromise:this.queue.size===0&&this.permits.available===this.permits.capacity?Promise.resolve():(this.drainPromise=new Promise((t,i)=>{if(this.drainResolve=t,e!==void 0){let r=setTimeout(()=>{this.drainResolve=null,this.drainPromise=null,i(new a(`drain() timed out after ${e}ms`,"TIMEOUT"));},e),s=this.drainResolve;this.drainResolve=()=>{clearTimeout(r),s();};}}),this.drainPromise))}reset(e={}){if(this.isShutdown)throw new a("Cannot reset a semaphore that has been shut down","SHUTDOWN");for(let t of this.queue.toArray())t.discard(new a("Semaphore reset","SHUTDOWN"));this.queue.clear(),this.enqueueOrder.clear(),this._clearTimeout(),this.metricsCollector?.reset(),this.permits.reset(),this.backoff.reset(),this.circuit.reset(),this.releaseGeneration++,this.pendingReleaseCount=0,this.scheduled=false,this.isShutdown=false,this.taskIdCounter=0,this.totalAcquired=0,this.totalReleased=0,this.totalTimeouts=0,this.totalEvictions=0,e.clearListeners&&this.eventListeners.clear(),this.drainResolve&&(this.drainResolve(),this.drainResolve=null,this.drainPromise=null),this._startPurgeInterval(),this.metricsCollector?.markCapacityChange(this.permits.capacity),this.metricsCollector?.sampleGauges(Date.now(),this.permits.inFlight,this.queue.size),this.debug&&console.info("[Semaphore] Reset to initial state");}shutdown(e="Semaphore shutdown"){if(!this.isShutdown){this.isShutdown=true,this.debug&&console.info(`[Semaphore] Shutdown: ${e}`),this.purgeIntervalId!==null&&(clearInterval(this.purgeIntervalId),this.purgeIntervalId=null),this._clearTimeout();for(let t of this.queue.toArray())t.discard(new a(e,"SHUTDOWN"));this.queue.clear(),this.enqueueOrder.clear(),this.metricsCollector?.destroy(),this.metricsCollector=void 0,this.drainResolve&&(this.drainResolve(),this.drainResolve=null,this.drainPromise=null),this.emit(n.SHUTDOWN,e);}}cancel(){if(this.isShutdown)return;let e=this.queue.toArray();for(let t of e)t.isProbe&&this.circuit.releaseProbeSlot(),t.discard(new a("Semaphore acquire cancelled","CANCELLED")),this._dequeue(t);this.metricsCollector?.sampleQueueDepthAt(Date.now(),this.queue.size),this.debug&&console.info(`[Semaphore] Cancelled ${e.length} queued tasks`),this._checkDrain(),this.schedule();}reportFailure(){if(this.isShutdown)return;this.circuit.recordFailure();let e=this.circuit.evaluateAndTrip();e.tripped&&(this.emit(n.CIRCUITOPEN,{timeoutRate:e.timeoutRate,recentTimeouts:e.failures,total:e.attempts,reason:"reported-failure"}),this.metricsCollector?.markCircuitOpen(),this.debug&&console.warn(`[Semaphore] Circuit opened via reportFailure(). Rate: ${(e.timeoutRate*100).toFixed(1)}%`),this._evictQueueOnCircuitOpen());}status(){let e=this.metricsCollector?.getSnapshot()??null,t=this.metricsCollector!==void 0?e?.windows?.[this.metricsCollector.primaryLabel]?.counts:void 0,i=(this.metricsCollector?.primaryWindowMs??6e4)/1e3,r=t?.acquired??0,s=t?.timeouts??0,u=this.enqueueOrder.peekHead(),c=u===void 0?0:Math.max(0,Date.now()-u.enqueueTime);return {status:{running:this.permits.capacity-this.permits.available,queued:this.queue.size,available:this.permits.available,inFlight:this.permits.inFlight,pendingReleases:this.pendingReleaseCount,circuitOpen:this.circuit.isOpen,circuitHalfOpen:this.circuit.isHalfOpen,backoffDelay:Math.round(this.backoff.currentDelay),requestsPerSecond:+(r/i).toFixed(2),timeoutRate1m:r+s>0?+(s/(r+s)*100).toFixed(1):0,queueAge:c},lifetime:{totalAcquired:this.totalAcquired,totalReleased:this.totalReleased,totalTimeouts:this.totalTimeouts,totalEvictions:this.totalEvictions,circuitBreakerCooldownRemaining:this.circuit.cooldownRemaining},metrics:e}}get availablePermits(){return this.permits.available}isAvailable(){return !this.isShutdown&&!this.circuit.isOpen&&!this.permits.isFull}get capacity(){return this.permits.capacity}get circuitState(){return this.circuit.state}get queueLength(){return this.queue.size}};var O={tripped:false},q=class{constructor(){this.state="closed";this.isOpen=false;this.isHalfOpen=false;this.hasProbeInFlight=false;this.probeTaskId=null;this.cooldownRemaining=0;}checkAndTransition(){return false}trackAttempt(){}recordFailure(){}evaluateAndTrip(){return O}markProbeInFlight(){}claimProbeSlot(){}releaseProbeSlot(){}handleProbeSuccess(){}handleProbeFailure(){}reset(){}};var P={tripped:false},g=class{constructor(){this.state="closed";this.hasProbeInFlight=false;this.probeTaskId=null;}get isOpen(){return this.state==="open"}get isHalfOpen(){return false}get cooldownRemaining(){return 0}open(){this.state="open";}close(){this.state="closed";}checkAndTransition(){return false}trackAttempt(){}recordFailure(){}evaluateAndTrip(){return P}markProbeInFlight(){}claimProbeSlot(){}releaseProbeSlot(){}handleProbeSuccess(){}handleProbeFailure(){}reset(){this.state="closed";}};export{g as ManualCircuitBreaker,q as NoopCircuitBreaker,_ as QUEUE_ORDERINGS,f as SaturationCircuitBreaker,M as Semaphore,a as SemaphoreError,n as SemaphoreEvents};//# sourceMappingURL=index.js.map | ||
| //# sourceMappingURL=index.js.map |
+1
-1
| { | ||
| "name": "regulo", | ||
| "version": "1.1.0", | ||
| "version": "1.3.0", | ||
| "description": "Control the heat — a priority-queue semaphore with weighted permits, a saturation circuit breaker, adaptive backoff, and built-in metrics. Zero dependencies.", | ||
@@ -5,0 +5,0 @@ "license": "MIT", |
+145
-52
@@ -22,3 +22,3 @@ # **Regulo** | ||
| - **🎛️ Bounded concurrency, with priority and weighting** — set how many burners are lit, send important work to the front, and let one heavy job claim more than one burner. | ||
| - **🛡️ Saturation circuit breaker** — when work backs up faster than it clears, **Regulo** takes the pot off the heat: it opens the circuit and sheds load immediately, then probes for recovery and closes again on its own. (See [How the circuit breaker works](#how-the-circuit-breaker-works) — it trips on saturation, not on your operation's errors.) | ||
| - **🛡️ Saturation circuit breaker** — when work backs up faster than it clears, **Regulo** takes the pot off the heat: it opens the circuit and sheds load immediately, then probes for recovery and closes again on its own. (See [How the circuit breaker works](#how-the-circuit-breaker-works) — it trips on saturation, not on your operation's errors.) The breaker is pluggable: feed it downstream errors with `reportFailure()`, or swap in a no-op breaker, a manual kill switch, or your own — see [Circuit breakers](#circuit-breakers). | ||
| - **🌡️ Adaptive backoff** — during a timeout burst, dispatch eases down to a simmer and returns to a full boil on its own once things recover. | ||
@@ -110,3 +110,3 @@ - **📈 Built-in observability** — windowed 1m/5m/15m/1h/24h rollups (throughput, latency, queue depth, in-flight), lifetime counters, and an event stream, all through one `status()` call. | ||
| The breaker is a **saturation breaker, not a fault breaker**. It watches the rate of *queue-acquisition timeouts* — callers that waited longer than `queueMaxTimeout` for a permit — over a sliding window. When that rate crosses `circuitBreakerThreshold` (and the minimum count guards are met), the circuit opens and new requests are rejected immediately with `CIRCUIT_OPEN`. After the cooldown elapses, one probe request is allowed through; if it succeeds the circuit closes, if it times out the circuit re-opens and the cooldown restarts. | ||
| The breaker is a **saturation breaker, not a fault breaker**. It watches the rate of *queue-acquisition timeouts* — callers that waited longer than `queueMaxTimeout` for a permit — over a sliding window. When that rate crosses `circuitBreakerThreshold` (and the minimum count guards are met), the circuit opens and new requests are rejected immediately with `CIRCUIT_OPEN`. Tasks already waiting in the queue are evicted at that same moment and rejected with `CIRCUIT_OPEN` too — they would otherwise sit out their full `queueMaxTimeout` only to surface a misleading `TIMEOUT`. Each eviction emits a `QUEUEEVICT` event and increments the `totalEvictions` lifetime counter. After the cooldown elapses, one probe request is allowed through; if it succeeds the circuit closes, if it times out the circuit re-opens and the cooldown restarts. | ||
@@ -118,3 +118,3 @@ What this means in practice: | ||
| If you also need to trip on downstream *errors* (not just saturation), pair the semaphore with a conventional fault breaker around your operation, or use the standalone [`CircuitBreaker`](#standalone-circuitbreaker) and feed it your own failure signal. | ||
| If you also need to trip on downstream *errors* (not just saturation), call [`reportFailure()`](#feeding-downstream-errors-reportfailure) with your own failure signal — the same breaker window applies — or swap the breaker entirely via [Circuit breakers](#circuit-breakers). | ||
@@ -206,3 +206,3 @@ ## Example: Express Middleware | ||
| Preferred entry point. Acquires a permit, runs `fn()`, and releases — always, even if `fn` throws. | ||
| Preferred entry point. Acquires a permit, runs `fn()`, and releases — always, even if `fn` throws. With `circuitBreakerFailurePredicate` configured, rejections from `fn()` that match the predicate count as breaker failures, and a matching failure on a half-open probe re-opens the circuit instead of closing it — see [Feeding downstream errors](#feeding-downstream-errors-reportfailure). | ||
@@ -217,3 +217,3 @@ #### `tryAcquire(weight?): (() => void) | null` | ||
| Resolves when the queue is empty and all permits are returned. Multiple callers share the same promise. Pass `timeoutMs` (a positive integer) to set a deadline — rejects with `TIMEOUT` if not idle in time; an invalid value throws `SemaphoreError` (`INVALID_ARGUMENT`) synchronously. Calling `drain()` after `shutdown()` rejects with `SHUTDOWN`. | ||
| Resolves when the queue is empty and all permits are returned. Multiple callers share the same promise — if a `drain()` is already in flight, later calls return that same promise as-is, so only the *first* caller's `timeoutMs` (if any) governs; a later caller's own `timeoutMs` argument is not applied. Pass `timeoutMs` (a positive integer) to set a deadline — rejects with `TIMEOUT` if not idle in time; an invalid value throws `SemaphoreError` (`INVALID_ARGUMENT`) synchronously, even if it turns out an in-flight drain's promise is what gets returned. Calling `drain()` after `shutdown()` rejects with `SHUTDOWN`. | ||
@@ -230,5 +230,9 @@ > Without `timeoutMs`, `drain()` can block indefinitely if a caller holds a permit and never releases it. | ||
| #### `reportFailure(): void` | ||
| Feeds an external failure signal (e.g. a downstream error) into the circuit breaker: records one failure and evaluates the trip condition. A trip behaves exactly like a saturation trip — queued tasks are evicted with `CIRCUIT_OPEN` and `CIRCUITOPEN` fires with `reason: 'reported-failure'`. Only influences trip decisions while the circuit is closed; no-op after `shutdown()`. For `use()`-based workloads, `circuitBreakerFailurePredicate` automates this and additionally makes half-open probes fault-aware. See [Feeding downstream errors](#feeding-downstream-errors-reportfailure). | ||
| #### `shutdown(reason?): void` | ||
| Permanently stops the semaphore — kills the gas. All queued tasks are rejected, the purge interval is cleared, and metrics collection stops. This is terminal: it cannot be reversed, and a later `reset()` on a shut-down instance throws rather than reviving it. Calling `shutdown()` again is a no-op. | ||
| Permanently stops the semaphore — kills the gas. All queued tasks are rejected, the purge interval is cleared, and metrics collection stops (`status().metrics` returns `null` afterwards; the collector's buffers are released). This is terminal: it cannot be reversed, and a later `reset()` on a shut-down instance throws rather than reviving it. Calling `shutdown()` again is a no-op. | ||
@@ -245,5 +249,9 @@ #### `on(event, listener) / off(event, listener) / removeAllListeners(event?)` | ||
| #### `peekQueue(): QueuedTaskView[]` | ||
| Read-only snapshot of the queue, in enqueue order. Entries additionally carry `isProbe`, so a circuit-breaker probe is identifiable in the view. | ||
| #### `isAvailable(): boolean` | ||
| Returns `true` if the semaphore is not shut down, the circuit is not open, and a permit is available. | ||
| Returns `true` if the semaphore is not shut down, the circuit is not open, and a permit is available. This is a capacity signal, not a dispatch guarantee: tasks may still be queued ahead (head-of-line fairness), in which case `tryAcquire()` returns `null` even while `isAvailable()` is `true`. | ||
@@ -258,2 +266,10 @@ #### `queueLength: number` | ||
| #### `capacity: number` | ||
| Total permits the semaphore was constructed with. | ||
| #### `circuitState: 'closed' | 'open' | 'half-open'` | ||
| Current circuit breaker state. | ||
| ## Configuration Reference | ||
@@ -269,5 +285,8 @@ | ||
| | `circuitBreakerWindow` | `number` | `10000` | Sliding window size in ms for the failure rate. Min: `1000` | | ||
| | `circuitBreakerWindowBucketWidth` | `number` | `1000` | Width (ms) of each bucket in the circuit breaker's sliding window; bucket count = `window / windowBucketWidth`. Min: `1` | | ||
| | `circuitBreakerCooldown` | `number` | `5000` | ms the circuit stays open before allowing a probe. Min: `1000` | | ||
| | `circuitBreakerMinThroughput` | `number` | `10` | Min requests in window before circuit can trip | | ||
| | `circuitBreakerMinFailures` | `number` | `5` | Min failures in window before circuit can trip | | ||
| | `circuitBreaker` | `CircuitBreakerStrategy` | — | A breaker instance to use instead of the built-in saturation breaker; overrides all `circuitBreaker*` options. See [Circuit breakers](#circuit-breakers) | | ||
| | `circuitBreakerFailurePredicate` | `(error: unknown) => boolean` | — | When set, `use()` counts matching rejections as breaker failures and half-open probes become fault-aware. Must not throw. See [Feeding downstream errors](#feeding-downstream-errors-reportfailure) | | ||
| | `backoffInitialTimeout` | `number` | `50` | Initial backoff delay (ms) applied to scheduler wakeup on first timeout | | ||
@@ -278,4 +297,5 @@ | `backoffMaxTimeout` | `number` | `2000` | Max backoff delay (ms) applied to scheduler wakeup | | ||
| | `metricsEnabled` | `boolean` | `true` | Enable windowed metrics collection | | ||
| | `metricsWindows` | `WindowOptions[]` | `undefined` (falls back to the built-in 1m/5m/15m/1h/24h set) | Overrides the windows behind `status().metrics`. Each entry is `{ size, stepMs }`; window length = `size × stepMs`. Two windows may not cover the same horizon (their labels would collide); `status()`'s rate fields are computed over the shortest window | | ||
| | `queueOrder` | `'fifo' \| 'lifo' \| 'fifoWithPriority' \| 'lifoWithPriority'` | `'fifoWithPriority'` | Queue dispatch order. `fifo`/`lifo` order purely by enqueue time; the `*WithPriority` variants make priority primary and break ties by enqueue time. See [Choosing an ordering](#choosing-an-ordering-and-its-implications). Ignored if `comparator` is set | | ||
| | `comparator` | `(a, b) => number` | — | Custom ordering over queued tasks (lower sorts/dispatches first); overrides `queueOrder` | | ||
| | `comparator` | `(a, b) => number` | — | Custom ordering over queued tasks (lower sorts/dispatches first); overrides `queueOrder`. Must be a consistent total order and must not throw (a `NaN`/non-number result degrades safely to an id tie-break; an exception does not) | | ||
| | `debug` | `boolean` | `false` | Enable debug logging and the permit-pool invariant check. Does not gate events — all events fire regardless | | ||
@@ -297,5 +317,8 @@ | ||
| circuitBreakerWindow: 10000, // ms sliding window for the rate; min 1000 | ||
| circuitBreakerWindowBucketWidth: 1000, // ms per bucket; window / windowBucketWidth = bucket count | ||
| circuitBreakerCooldown: 5000, // ms open before a probe is allowed; min 1000 | ||
| circuitBreakerMinThroughput: 10, // min requests in window before it can trip; min 1 | ||
| circuitBreakerMinFailures: 5, // min failures in window before it can trip; min 1 | ||
| // circuitBreaker: undefined, // no default — a CircuitBreakerStrategy instance overrides the options above | ||
| // circuitBreakerFailurePredicate: undefined, // no default — use() rejections matching it count as breaker failures | ||
| // Adaptive backoff | ||
@@ -308,2 +331,3 @@ backoffInitialTimeout: 50, // ms initial delay on first timeout in a burst | ||
| metricsEnabled: true, // windowed metrics collection | ||
| // metricsWindows: undefined, // override the default 1m/5m/15m/1h/24h windows | ||
| debug: false, // debug logging + permit-pool invariant check | ||
@@ -329,2 +353,3 @@ // Ordering | ||
| | `QUEUEPURGE` | `'queue-purge'` | `QueuedTaskView` — `{ id, priority, enqueueTime, weight }` | | ||
| | `QUEUEEVICT` | `'queue-evict'` | `QueuedTaskView` — task evicted (rejected `CIRCUIT_OPEN`) because the circuit tripped while it was queued | | ||
| | `CIRCUITOPEN` | `'circuit-open'` | `{ timeoutRate, recentTimeouts, total, reason? }` | | ||
@@ -386,4 +411,4 @@ | `CIRCUITHALFOPEN` | `'circuit-half-open'` | none | | ||
| backoffDelay: number, // current backoff delay (ms) applied to scheduler wakeup | ||
| requestsPerSecond: number, // based on 1m window | ||
| timeoutRate1m: number, // timeout % over last 1m | ||
| requestsPerSecond: number, // over the shortest metrics window (1m by default) | ||
| timeoutRate1m: number, // timeout % over the shortest metrics window (1m by default) | ||
| queueAge: number, // ms since oldest queued task was enqueued | ||
@@ -395,2 +420,3 @@ }, | ||
| totalTimeouts: number, | ||
| totalEvictions: number, // tasks evicted from the queue by a circuit trip | ||
| circuitBreakerCooldownRemaining: number, // ms until circuit may probe | ||
@@ -402,10 +428,71 @@ }, | ||
| ## Standalone `CircuitBreaker` | ||
| ## Circuit Breakers | ||
| `CircuitBreaker` can be used independently — e.g. to wrap an HTTP client. Unlike the semaphore's saturation breaker, here you decide what counts as a failure by calling `recordTimeout()` on whatever signal you choose: | ||
| The breaker behind `Semaphore` is a pluggable strategy. Every breaker — the built-ins below and any you write — implements the `CircuitBreakerStrategy` contract (exported from the package root), and composes into a `Semaphore` via the `circuitBreaker` config option. Injecting an instance overrides the `circuitBreaker*` numeric options, the same precedence `comparator` has over `queueOrder`. | ||
| > **Renamed in 1.3.0:** the former `CircuitBreaker` export is now `SaturationCircuitBreaker`, and its `recordTimeout()` method is now `recordFailure()`. Both are straight renames — behavior is unchanged. See the [CHANGELOG](./CHANGELOG.md) for the migration. | ||
| | Breaker | Behavior | | ||
| |---|---| | ||
| | `SaturationCircuitBreaker` | The default. A windowed failure-rate breaker: fed queue timeouts by the semaphore it trips on saturation (the wiring described above); fed your own signal (via [`reportFailure()`](#feeding-downstream-errors-reportfailure) or standalone) it trips on whatever you define as failure. | | ||
| | `NoopCircuitBreaker` | Never trips — the semaphore as a pure limiter, with no load shedding. | | ||
| | `ManualCircuitBreaker` | An operator kill switch: `open()` sheds new acquires with `CIRCUIT_OPEN` until `close()`. No cooldown, no probe — recovery is a deliberate action. It gates *new* acquires only; call `cancel()` after `open()` if the queue should be shed too. | | ||
| ```ts | ||
| import { CircuitBreaker } from 'regulo'; | ||
| import { Semaphore, NoopCircuitBreaker, ManualCircuitBreaker } from 'regulo'; | ||
| const circuitBreaker = new CircuitBreaker({ | ||
| // Pure limiter — no breaker bookkeeping at all | ||
| const pool = new Semaphore(10, { circuitBreaker: new NoopCircuitBreaker() }); | ||
| // Ops kill switch | ||
| const kill = new ManualCircuitBreaker(); | ||
| const reports = new Semaphore(20, { circuitBreaker: kill }); | ||
| // ... later, from an admin endpoint: | ||
| kill.open(); // shed load now | ||
| kill.close(); // resume | ||
| ``` | ||
| To write your own, implement `CircuitBreakerStrategy` and pass an instance as `circuitBreaker`. The contract notes live on the exported type's JSDoc — the essentials: `checkAndTransition()` returns `true` exactly once per open → half-open transition, `evaluateAndTrip()` reports closed → open trips (the semaphore then emits `CIRCUITOPEN` and evicts the queue), the probe-slot methods may be no-ops if you never enter half-open, and methods must not throw. | ||
| ### Feeding downstream errors: `reportFailure()` | ||
| The default breaker trips on saturation, not on your operation's errors (see [How the circuit breaker works](#how-the-circuit-breaker-works)). If you also want error-driven tripping, report the failures you care about — the same window, threshold, cooldown, and probe recovery apply: | ||
| ```ts | ||
| const semaphore = new Semaphore(10, { circuitBreakerThreshold: 0.3 }); | ||
| await semaphore.use(async () => { | ||
| try { | ||
| return await callDownstream(); | ||
| } catch (error) { | ||
| if (isServerError(error)) semaphore.reportFailure(); // count 5xx-style failures only | ||
| throw error; | ||
| } | ||
| }); | ||
| ``` | ||
| A trip via `reportFailure()` behaves exactly like a saturation trip: queued tasks are evicted with `CIRCUIT_OPEN` and the `CIRCUITOPEN` event fires with `reason: 'reported-failure'`. Reported failures influence trip decisions only while the circuit is closed — half-open probe outcomes remain acquisition-based. | ||
| For `use()`-based workloads, `circuitBreakerFailurePredicate` does this declaratively — and goes one step further: | ||
| ```ts | ||
| const semaphore = new Semaphore(10, { | ||
| circuitBreakerFailurePredicate: (error) => isServerError(error), | ||
| }); | ||
| // Matching rejections from fn() count as breaker failures automatically — | ||
| // no manual reportFailure() call needed. | ||
| await semaphore.use(() => callDownstream()); | ||
| ``` | ||
| With the predicate set, half-open probes become **fault-aware**: a probe dispatched through `use()` whose operation fails with a matching error re-opens the circuit (the `CIRCUITOPEN` event fires with `reason: 'half-open-probe-failed'`) instead of closing it on release. A probe whose rejection does *not* match still counts as a successful probe. The predicate must not throw; a thrown predicate is logged via `console.warn` and the rejection is treated as non-matching. The predicate only observes `use()` — callers using bare `acquire()`/`tryAcquire()` should call `reportFailure()` themselves. | ||
| ### Standalone usage | ||
| `SaturationCircuitBreaker` can be used independently — e.g. to wrap an HTTP client — where you decide what counts as a failure by calling `recordFailure()` on whatever signal you choose: | ||
| ```ts | ||
| import { SaturationCircuitBreaker } from 'regulo'; | ||
| const circuitBreaker = new SaturationCircuitBreaker({ | ||
| threshold: 0.5, | ||
@@ -431,3 +518,3 @@ window: 10000, | ||
| } catch (error) { | ||
| circuitBreaker.recordTimeout(); | ||
| circuitBreaker.recordFailure(); | ||
| if (circuitBreaker.isHalfOpen) circuitBreaker.handleProbeFailure(); | ||
@@ -469,6 +556,6 @@ else circuitBreaker.evaluateAndTrip(); | ||
| |---|--:|---| | ||
| | `tryAcquire` + `release` | 2.22M | 2.29x slower | | ||
| | `tryAcquire` + `release` (no metrics) | 5.10M | fastest | | ||
| | `use()` round-trip | 1.10M | 4.65x slower | | ||
| | `use()` round-trip (no metrics) | 1.70M | 3.01x slower | | ||
| | `tryAcquire` + `release` | 3.07M | 2.90x slower | | ||
| | `tryAcquire` + `release` (no metrics) | 8.91M | fastest | | ||
| | `use()` round-trip | 1.24M | 7.16x slower | | ||
| | `use()` round-trip (no metrics) | 1.79M | 4.97x slower | | ||
@@ -479,5 +566,5 @@ **🎛️ Weighted acquire, uncontended** | ||
| |---|--:|---| | ||
| | `use()` weight=1 | 1.19M | fastest | | ||
| | `use()` weight=4 | 1.17M | 1.02x slower | | ||
| | `use()` weight=16 | 1.17M | 1.01x slower | | ||
| | `use()` weight=1 | 1.24M | 1.03x slower | | ||
| | `use()` weight=4 | 1.26M | 1.01x slower | | ||
| | `use()` weight=16 | 1.28M | fastest | | ||
@@ -491,6 +578,6 @@ Weighted permits add no meaningful overhead regardless of weight — claiming | ||
| |---|--:|---| | ||
| | concurrency=4 | 726.4k | 1.11x slower | | ||
| | concurrency=16 | 779.4k | 1.03x slower | | ||
| | concurrency=64 | 805.5k | fastest | | ||
| | concurrency=16, random priority | 684.2k | 1.18x slower | | ||
| | concurrency=4 | 843.2k | 1.03x slower | | ||
| | concurrency=16 | 856.6k | 1.02x slower | | ||
| | concurrency=64 | 871.9k | fastest | | ||
| | concurrency=16, random priority | 761.2k | 1.15x slower | | ||
@@ -501,5 +588,5 @@ **📈 `status()` snapshot cost** | ||
| |---|--:|---| | ||
| | 0 | 720.7k | 1.02x slower | | ||
| | 100 | 731.9k | fastest | | ||
| | 1000 | 729.4k | 1.00x slower | | ||
| | 0 | 764.8k | fastest | | ||
| | 100 | 754.3k | 1.01x slower | | ||
| | 1000 | 757.0k | 1.01x slower | | ||
@@ -515,7 +602,7 @@ `status()` is O(1) in queue depth — the cost is flat across queue depths (within | ||
| |---|--:|---| | ||
| | cockatiel (bulkhead) | 4.00M | fastest | | ||
| | p-limit | 1.21M | 3.32x slower | | ||
| | p-queue | 1.20M | 3.34x slower | | ||
| | regulo (no metrics) | 1.57M | 2.54x slower | | ||
| | regulo | 1.05M | 3.82x slower | | ||
| | cockatiel (bulkhead) | 4.11M | fastest | | ||
| | p-limit | 1.25M | 3.29x slower | | ||
| | p-queue | 1.25M | 3.30x slower | | ||
| | regulo (no metrics) | 1.72M | 2.40x slower | | ||
| | regulo | 1.24M | 3.33x slower | | ||
@@ -526,7 +613,7 @@ **📊 regulo vs. other libraries — contended throughput @ concurrency=16** (tasks/sec) | ||
| |---|--:|---| | ||
| | cockatiel (bulkhead) | 1.69M | fastest | | ||
| | p-queue | 1.07M | 1.58x slower | | ||
| | regulo (no metrics) | 982.7k | 1.72x slower | | ||
| | p-limit | 870.5k | 1.94x slower | | ||
| | regulo | 753.8k | 2.24x slower | | ||
| | cockatiel (bulkhead) | 1.70M | fastest | | ||
| | p-queue | 1.03M | 1.65x slower | | ||
| | regulo (no metrics) | 1.04M | 1.64x slower | | ||
| | p-limit | 903.5k | 1.89x slower | | ||
| | regulo | 836.6k | 2.04x slower | | ||
@@ -537,14 +624,20 @@ **🛡️ Circuit breaker overhead — closed/healthy circuit** | ||
| |---|--:|---| | ||
| | regulo `CircuitBreaker` | 3.64M | fastest | | ||
| | cockatiel (circuitBreaker) | 2.68M | 1.36x slower | | ||
| | opossum | 1.57M | 2.33x slower | | ||
| | regulo `ManualCircuitBreaker` | 5.06M | fastest | | ||
| | regulo `NoopCircuitBreaker` | 4.72M | 1.07x slower | | ||
| | regulo `SaturationCircuitBreaker` | 3.85M | 1.31x slower | | ||
| | cockatiel (circuitBreaker) | 2.80M | 1.81x slower | | ||
| | opossum | 1.53M | 3.31x slower | | ||
| The picture is consistent. Cockatiel's bulkhead is the fastest limiter — and | ||
| **Regulo** trades raw limiter throughput for an integrated priority heap, weighted | ||
| permits, a saturation breaker, and (by default) windowed metrics in one component. | ||
| With metrics disabled its contended throughput sits right alongside `p-limit` and | ||
| `p-queue`, so most of the remaining gap to a bare limiter is the windowed metrics | ||
| you can turn off. On the breaker axis the integration goes the other way: | ||
| **Regulo**'s standalone `CircuitBreaker` is the fastest of the three, because it | ||
| defers failure accounting to an explicit timeout signal instead of bookkeeping a | ||
| permits, a pluggable circuit breaker, and (by default) windowed metrics in one | ||
| component. Even with metrics *enabled* its uncontended round-trip now sits right | ||
| alongside `p-limit` and `p-queue`; with metrics disabled it pulls ahead of both | ||
| uncontended and matches them under contention. On the breaker axis the | ||
| integration goes the other way: every breaker in **Regulo**'s [breakers | ||
| module](#circuit-breakers) adds less per-call overhead than the fault breakers | ||
| it is compared against. `NoopCircuitBreaker` and `ManualCircuitBreaker` show the | ||
| floor (they do no accounting at all), and even `SaturationCircuitBreaker` — the | ||
| default, and the only one keeping a real failure window — stays ahead because it | ||
| defers failure accounting to an explicit failure signal instead of bookkeeping a | ||
| rolling window on every call. | ||
@@ -554,3 +647,3 @@ | ||
| more expensive than the limiter itself: SSR renders, database queries, | ||
| downstream API calls, measured in milliseconds. Even at ~750k tasks/sec | ||
| downstream API calls, measured in milliseconds. Even at ~670k tasks/sec | ||
| under contention the per-task overhead is a few microseconds against operations | ||
@@ -575,6 +668,6 @@ thousands of times slower. If you only need a bare concurrency cap on cheap | ||
| ✓ test/list.test.ts (8 tests) | ||
| ✓ test/semaphore.test.ts (95 tests) | ||
| ✓ test/semaphore.test.ts (110 tests) | ||
| Test Files 9 passed (9) | ||
| Tests 191 passed (191) | ||
| Tests 206 passed (206) | ||
| ``` | ||
@@ -586,3 +679,3 @@ | ||
| - **One `Semaphore` is one failure domain.** The circuit breaker and adaptive backoff are per-instance and shared across everything routed through it, so a saturation event on one dependency trips the breaker for *all* work in that instance. Don't multiplex unrelated downstreams or task types through a single `Semaphore` (and don't use `weight`/`priority` to fake it) — use one `Semaphore` per protected resource, or one capacity pool plus a standalone [`CircuitBreaker`](#standalone-circuitbreaker) per downstream key. See [Choosing an ordering](#choosing-an-ordering-and-its-implications). | ||
| - **One `Semaphore` is one failure domain.** The circuit breaker and adaptive backoff are per-instance and shared across everything routed through it, so a saturation event on one dependency trips the breaker for *all* work in that instance. Don't multiplex unrelated downstreams or task types through a single `Semaphore` (and don't use `weight`/`priority` to fake it) — use one `Semaphore` per protected resource, or one capacity pool plus a standalone [`SaturationCircuitBreaker`](#circuit-breakers) per downstream key. See [Choosing an ordering](#choosing-an-ordering-and-its-implications). | ||
| - **A free permit can sit idle behind a heavier head.** The scheduler never dispatches past a head that doesn't fit, so under [weighted permits](#core-concepts) one heavy task at the head can stall throughput even when there's capacity for the lighter tasks behind it. This is by design (it stops light work starving heavy work); how often it bites depends on your ordering — see [Choosing an ordering](#choosing-an-ordering-and-its-implications). | ||
@@ -589,0 +682,0 @@ - **`drain()` without a timeout can block indefinitely** if a permit holder never releases. Always pass `timeoutMs` in graceful-shutdown paths. |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
468125
21.95%758
34.88%661
16.37%