🎩 You're Invited:Meet the Socket team at Black Hat in Las Vegas, August 3-6.RSVP
Sign In

regulo

Package Overview
Dependencies
Maintainers
1
Versions
14
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

regulo - npm Package Compare versions

Comparing version
1.1.0
to
1.3.0
+75
-91
CHANGELOG.md

@@ -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,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

@@ -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 };

@@ -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,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
{
"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