+15
-0
@@ -8,2 +8,17 @@ # Changelog | ||
| ## [Unreleased] | ||
| ### Added | ||
| - **`onSettle` hook on `use()`.** `use<T>(fn, abortSignal?, priority?, weight?, onSettle?)` accepts an optional `(durationMs, outcome) => void` callback reporting how long `fn()` itself took (not queue-wait time, which `status().metrics` already covers) and whether it resolved (`'success'`) or rejected (`'error'`). Never called if the acquire itself is rejected — there's no operation to time. A throwing hook is caught and logged via `console.warn`, never masking `fn()`'s own result. Unlocks per-operation latency histograms and SLO tracking without hand-threading timing calls through every call site. | ||
| - **`weight` on the `TASKRELEASE` event.** The payload is now `{ queued, running, weight }` — `weight` is the permit count the release actually returned, enabling a weighted-pool utilization dashboard (e.g. tracking how many of N weighted burners are occupied) without re-deriving it from paired acquire/release bookkeeping. | ||
| - **`CIRCUITSTATECHANGE` event** (`'circuit-state-change'`, payload `{ from, to }`) — fires alongside every `CIRCUITOPEN`/`CIRCUITPROBING`/`CIRCUITCLOSE`, so syncing breaker state to an external dashboard takes one handler instead of three. | ||
| - **`peekQueue({ offset?, limit? })`** — bounds how much of a deep queue gets materialized for an admin-debug endpoint. `offset` skips leading entries (in enqueue order); `limit` caps how many are collected after that. `peekQueue()` with no arguments is unchanged — the full queue, as before. | ||
| - **`KeyedSemaphore`** — a lazily-populated registry of one `Semaphore` per key (`forKey(key)`, plus `use()`/`has()`/`delete()`/`shutdown()`/`size`/`keys()`), turning the "one `Semaphore` per resource" pattern the docs already recommended into a one-liner instead of hand-rolled `Map` bookkeeping. No TTL/eviction by design — intended for a small, bounded key space (per-downstream, per-shard), not high-cardinality keys. | ||
| - **`ID` type** (`string | number`) is now exported from the package root, used by `KeyedSemaphore`'s key parameter. | ||
| ### Performance | ||
| - **Dropped the built-in 15m metrics window.** The default set is now 1m/5m/1h/24h. Every `on*`/`sample*` call on the hot acquire/release/timeout path loops over all configured windows, so this is a ~20% cut in per-event metrics overhead. 15m sat between the 5m short-trend window and the 1h medium-trend window without giving dashboards or circuit-breaker consumers a horizon they don't already get from one of its neighbors. Custom `metricsWindows` configs are unaffected — this only changes `DEFAULT_WINDOW_OPTIONS`. | ||
| ## [v1.4.0] - 2026-07-05 | ||
@@ -10,0 +25,0 @@ |
+1
-1
@@ -1,2 +0,2 @@ | ||
| '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 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.cachedIndex=-1;this.cachedUntil=0;this.size=e,this.stepMs=t,this.buckets=new Int32Array(e*2),this.timestamps=new Float64Array(e);}idx(e){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(e=Date.now()){this.buckets[this.idx(e)*2]++;}addTimeout(){this.buckets[this.idx(Date.now())*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);}},v=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.windowBucketCount=Math.ceil(this.window/this.windowBucketWidth),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.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");if(this.windowBucketCount<2)throw new a("SaturationCircuitBreaker window must span at least 2 windowBucketWidth buckets","INVALID_ARGUMENT");this.eventWindow=new A(this.windowBucketCount,this.windowBucketWidth);}get isClosed(){return this.state==="closed"}get isOpen(){return this.state==="open"}get isProbing(){return this.state==="probing"}get hasProbeInFlight(){return this.probeInFlight}get probeTaskId(){return this._probeTaskId}get cooldownRemaining(){return this.isOpen?Math.max(0,this.openUntil-Date.now()):0}trackAttempt(e){this.state==="closed"&&this.eventWindow.addAcquired(e);}recordFailure(){this.eventWindow.addTimeout();}checkAndTransition(){return this.state!=="open"||Date.now()<this.openUntil?false:(this.state="probing",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 y=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(){return this._decayedDelay(Date.now())}_decayedDelay(e){if(this.delay===0)return 0;let t=(e-this.lastTimestamp)/1e3;if(t<=0)return this.delay;let i=this.delay*Math.pow(this.decayFactor,t);return i<1?0:i}onTimeout(e=Date.now()){let t=this._decayedDelay(e),i=t>0?t*2:this.initialTimeout;this.delay=Math.min(i,this.maxTimeout),this.lastTimestamp=e;}reset(){this.delay=0,this.lastTimestamp=0;}};var S=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 g=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]}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 k=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 _=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,m=0,p=0,b=0,f=0,w=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]),m+=this.qSum[h],p+=this.qCount[h],this.qMax[h]>b&&(b=this.qMax[h]),f+=this.latSum[h],w+=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:m/p,max:b,samples:p},latency:{avg:w===0?0:f/w,count:w,total:f}}}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}],I=class{constructor(e=R){this._totalAcquiredFast=0;this._totalAcquiredQueued=0;this._totalReleased=0;this._totalTimeouts=0;this._totalPurged=0;this._totalAborts=0;this._capacity=0;this._circuitOpen=false;this._circuitProbing=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 _(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);}onPurge(e,t){this._totalPurged++;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._circuitProbing=false;}markCircuitProbing(){this._circuitOpen=false,this._circuitProbing=true;}markCircuitClose(){this._circuitOpen=false,this._circuitProbing=false;}getSnapshot(e=Date.now()){let 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,totalPurged:this._totalPurged,totalAborts:this._totalAborts,capacity:this._capacity,circuitOpen:this._circuitOpen,circuitProbing:this._circuitProbing}}}reset(){for(let e of this.windows)e.reset();this._totalAcquiredFast=0,this._totalAcquiredQueued=0,this._totalReleased=0,this._totalTimeouts=0,this._totalPurged=0,this._totalAborts=0,this._capacity=0,this._circuitOpen=false,this._circuitProbing=false;}destroy(){this.reset();}};var E={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[e];if(t===void 0)throw new a(`Semaphore queueOrder must be one of: ${Object.keys(E).join(", ")} (got ${JSON.stringify(e)})`,"INVALID_ARGUMENT");return t}function M(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",CIRCUITPROBING:"circuit-probing",CIRCUITCLOSE:"circuit-close",SHUTDOWN:"shutdown"};var P=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.totalPurged=0;this.totalEvictions=0;this.eventListeners=new Map;this.drainPromise=null;this.drainResolve=null;this.purgeIntervalId=null;this.timeoutTimerId=null;this._runSchedulerBound=()=>{this._runScheduler();};this.lastAdmissionWasProbe=false;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=M({queueOrder:t.queueOrder,comparator:t.comparator});this.queue=new g(i),this.enqueueOrder=new k,this.permits=new T(e),this.circuit=t.circuitBreaker??new v({threshold:t.circuitBreakerThreshold,window:t.circuitBreakerWindow,windowBucketWidth:t.circuitBreakerWindowBucketWidth,cooldown:t.circuitBreakerCooldown,minThroughput:t.circuitBreakerMinThroughput,minFailures:t.circuitBreakerMinFailures}),this.backoff=new y({initialTimeout:t.backoffInitialTimeout,maxTimeout:t.backoffMaxTimeout,decayFactor:t.backoffDecayFactor}),this.metricsCollector=this.metricsEnabled?new I(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){if(this.circuit.isProbing){if(this.circuit.hasProbeInFlight||!this.permits.hasCapacityFor(e))return null;if(this.circuit.markProbeInFlight(),this.permits.acquire(e),this.totalAcquired++,this.metricsCollector!==void 0){let t=Date.now();this.circuit.trackAttempt(t),this.metricsCollector.onAcquireFast(t,this.permits.inFlight,this.queue.size);}else this.circuit.trackAttempt();return 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)}if(this.queue.size>0||!this.permits.hasCapacityFor(e))return null;if(this.permits.acquire(e),this.totalAcquired++,this.metricsCollector!==void 0){let t=Date.now();this.circuit.trackAttempt(t),this.metricsCollector.onAcquireFast(t,this.permits.inFlight,this.queue.size);}else this.circuit.trackAttempt();return 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.isProbing&&(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.circuit.trackAttempt(),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,e),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());}_evictTask(e){let t=e.discard(new a("Circuit breaker opened while task was queued","CIRCUIT_OPEN"));return t&&(this.totalEvictions++,this.emit(n.QUEUEEVICT,{id:e.id,priority:e.priority,enqueueTime:e.enqueueTime,weight:e.weight}),this.debug&&console.warn(`[Semaphore] Evicted queued task #${e.id}: circuit opened`)),t}_evictQueueOnCircuitOpen(){if(this.queue.size===0)return;if(this.circuit.probeTaskId===null){let i=0;for(let r=this.enqueueOrder.peekHead();r!==void 0;r=r.next??void 0)this._evictTask(r)&&i++;this.queue.clear(),this.enqueueOrder.clear(),this._clearTimeout(),i>0&&this.metricsCollector?.sampleQueueDepthAt(Date.now(),this.queue.size);return}let e=0,t=this.enqueueOrder.peekHead();for(;t!==void 0;){let i=t.next??void 0;t.isProbe||(this._evictTask(t)&&e++,this._dequeue(t)),t=i;}e>0&&this.metricsCollector?.sampleQueueDepthAt(Date.now(),this.queue.size);}_timeoutTask(e,t){if(this.totalTimeouts++,this.circuit.recordFailure(),this.backoff.onTimeout(t),e.isProbe)this.circuit.handleProbeFailure(),this.emit(n.CIRCUITOPEN,{timeoutRate:1,recentTimeouts:1,total:1,reason:"probe-failed"}),this.metricsCollector?.markCircuitOpen(),this.debug&&console.warn("[Semaphore] Circuit re-opened: probe timed out");else {let i=this.circuit.evaluateAndTrip();i.tripped&&(this.emit(n.CIRCUITOPEN,{timeoutRate:i.timeoutRate,recentTimeouts:i.failures,total:i.attempts}),this.metricsCollector?.markCircuitOpen(),this.debug&&console.warn(`[Semaphore] Circuit opened. Rate: ${(i.timeoutRate*100).toFixed(1)}%`),this._evictQueueOnCircuitOpen());}this.metricsCollector?.onTimeout(t,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{let e=Date.now();for(;this.queue.size>0&&!this.circuit.isOpen;){let t=this.queue.peek();if(!t||this.circuit.isProbing&&t.id!==this.circuit.probeTaskId||!this.permits.hasCapacityFor(t.weight))break;let i=this.queue.pop();this.enqueueOrder.remove(i),this.enqueueOrder.size===0&&this._clearTimeout(),i.dispatch(()=>{let s=Math.max(0,e-i.enqueueTime);return this.permits.acquire(i.weight),this.totalAcquired++,this.metricsCollector?.onAcquireQueued(e,s,this.permits.inFlight,this.queue.size),this.permits.assertInvariant(this.debug),this._createRelease(i.isProbe,i.weight)})&&this.hasListeners(n.TASKACQUIRE)&&this.emit(n.TASKACQUIRE,{queued:this.queue.size,running:this.permits.capacity-this.permits.available,...i.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.totalPurged++,this.metricsCollector?.onPurge(e,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.CIRCUITPROBING),this.metricsCollector?.markCircuitProbing(),this.debug&&console.info("[Semaphore] Circuit entering probing"),this.schedule()),this.circuit.isOpen)?null:this._tryAcquireFast(e)}acquire(e,t=0,i=1){return this._acquire(e,t,i)}_acquire(e,t=0,i=1){if(this.lastAdmissionWasProbe=false,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.CIRCUITPROBING),this.metricsCollector?.markCircuitProbing(),this.debug&&console.info("[Semaphore] Circuit entering probing"),this.schedule()),this.circuit.isOpen)return Promise.reject(new a(`Circuit breaker open, retry in ${this.circuit.cooldownRemaining}ms`,"CIRCUIT_OPEN"));if(this.circuit.isProbing&&this.circuit.hasProbeInFlight)return Promise.reject(new a("Circuit breaker is probing, probe already in flight","CIRCUIT_PROBING"));if(e?.aborted)return Promise.reject(new a("Semaphore acquire aborted before start","ABORTED"));let r=this.circuit.isProbing,s=this._tryAcquireFast(i);if(s)return this.lastAdmissionWasProbe=r,Promise.resolve(s);let u=this.circuit.isProbing&&!this.circuit.hasProbeInFlight;return !u&&this.rejectOnFull?Promise.reject(new a("Semaphore at capacity (rejectOnFull)","QUEUE_FULL")):!u&&this.queue.size>=this.queueMaxLength?Promise.reject(new a(`Queue full (${this.queueMaxLength})`,"QUEUE_FULL")):(this.lastAdmissionWasProbe=u,new Promise((c,d)=>{let m=++this.taskIdCounter,p=Date.now(),b=u,f=new S({id:m,priority:b?Number.MIN_SAFE_INTEGER:t,enqueueTime:p,isProbe:b,resolve:c,reject:d,abortSignal:e,weight:i});f.arm(()=>this._onTaskAbort(f,d)),b&&this.circuit.claimProbeSlot(m),this.metricsCollector?.sampleQueueDepthAt(p,this.queue.size+1),this._enqueue(f),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=this._acquire(t,i,r),u=this.failurePredicate!==void 0&&this.lastAdmissionWasProbe,c=await s;try{let d=await e();return c(),d}catch(d){if(this.failurePredicate!==void 0){let m=false;try{m=this.failurePredicate(d)===!0;}catch(p){console.warn("[Semaphore] circuitBreakerFailurePredicate threw (rejection treated as non-matching):",p);}m&&(u&&this.circuit.isProbing?(this.circuit.recordFailure(),this.circuit.handleProbeFailure(),this.emit(n.CIRCUITOPEN,{timeoutRate:1,recentTimeouts:1,total:1,reason:"probe-failed"}),this.metricsCollector?.markCircuitOpen(),this.debug&&console.warn("[Semaphore] Circuit re-opened: probe operation failed")):this.reportFailure());}throw c(),d}}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=this.enqueueOrder.peekHead();t!==void 0;t=t.next??void 0)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.totalPurged=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=this.enqueueOrder.peekHead();t!==void 0;t=t.next??void 0)t.discard(new a(e,"SHUTDOWN"));this.queue.clear(),this.enqueueOrder.clear(),this.releaseGeneration++,this.pendingReleaseCount=0,this.permits.reset(),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=0;for(let t=this.enqueueOrder.peekHead();t!==void 0;t=t.next??void 0)t.isProbe&&this.circuit.releaseProbeSlot(),t.discard(new a("Semaphore acquire cancelled","CANCELLED")),e++;this.queue.clear(),this.enqueueOrder.clear(),this._clearTimeout(),this.metricsCollector?.sampleQueueDepthAt(Date.now(),this.queue.size),this.debug&&console.info(`[Semaphore] Cancelled ${e} 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=Date.now(),t=this.metricsCollector?.getSnapshot(e)??null,i=this.metricsCollector!==void 0?t?.windows?.[this.metricsCollector.primaryLabel]?.counts:void 0,r=(this.metricsCollector?.primaryWindowMs??6e4)/1e3,s=i?.acquired??0,u=i?.timeouts??0,c=this.enqueueOrder.peekHead(),d=c===void 0?0:Math.max(0,e-c.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,circuitProbing:this.circuit.isProbing,backoffDelay:Math.round(this.backoff.currentDelay),requestsPerSecond:+(s/r).toFixed(2),timeoutRate1m:s+u>0?+(u/(s+u)*100).toFixed(1):0,queueAge:d},lifetime:{totalAcquired:this.totalAcquired,totalReleased:this.totalReleased,totalTimeouts:this.totalTimeouts,totalPurged:this.totalPurged,totalEvictions:this.totalEvictions,circuitBreakerCooldownRemaining:this.circuit.cooldownRemaining},metrics:t}}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 F={tripped:false},q=class{constructor(){this.state="closed";this.isOpen=false;this.isProbing=false;this.hasProbeInFlight=false;this.probeTaskId=null;this.cooldownRemaining=0;}checkAndTransition(){return false}trackAttempt(){}recordFailure(){}evaluateAndTrip(){return F}markProbeInFlight(){}claimProbeSlot(){}releaseProbeSlot(){}handleProbeSuccess(){}handleProbeFailure(){}reset(){}};var O={tripped:false},C=class{constructor(){this.state="closed";this.hasProbeInFlight=false;this.probeTaskId=null;}get isOpen(){return this.state==="open"}get isProbing(){return false}get cooldownRemaining(){return 0}open(){this.state="open";}close(){this.state="closed";}checkAndTransition(){return false}trackAttempt(){}recordFailure(){}evaluateAndTrip(){return O}markProbeInFlight(){}claimProbeSlot(){}releaseProbeSlot(){}handleProbeSuccess(){}handleProbeFailure(){}reset(){this.state="closed";}};exports.ManualCircuitBreaker=C;exports.NoopCircuitBreaker=q;exports.QUEUE_ORDERINGS=E;exports.SaturationCircuitBreaker=v;exports.Semaphore=P;exports.SemaphoreError=a;exports.SemaphoreEvents=n;//# sourceMappingURL=index.cjs.map | ||
| 'use strict';var o=class extends Error{constructor(e,t){super(e),this.name="SemaphoreError",this.code=t;}};var u=(n,e,t,i,r=true,s=true)=>{if(typeof n!="number"||Number.isNaN(n))throw new o(`${e} must be a valid number`,"INVALID_ARGUMENT");if(r&&!Number.isInteger(n))throw new o(`${e} must be an integer`,"INVALID_ARGUMENT");if(s?n<t:n<=t)throw new o(`${e} must be ${s?">=":">"} ${t}`,"INVALID_ARGUMENT");if(s?n>i:n>=i)throw new o(`${e} must be ${s?"<=":"<"} ${i}`,"INVALID_ARGUMENT");return n};var v=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 P=class{#e;#t;#s;#i;#r=-1;#o=0;constructor(e,t){this.#s=e,this.#i=t,this.#e=new Int32Array(e*2),this.#t=new Float64Array(e);}#a(e){if(e<this.#o&&e>=this.#o-this.#i)return this.#r;let t=Math.floor(e/this.#i)*this.#i,i=Math.floor(t/this.#i)%this.#s;return this.#t[i]!==t&&(this.#t[i]=t,this.#e[i*2]=0,this.#e[i*2+1]=0),this.#r=i,this.#o=t+this.#i,i}addAcquired(e=Date.now()){this.#e[this.#a(e)*2]++;}addTimeout(){this.#e[this.#a(Date.now())*2+1]++;}snapshot(){let e=Date.now(),t=Math.floor(e/this.#i)*this.#i-(this.#s-1)*this.#i,i=0,r=0;for(let s=0;s<this.#s;s++)this.#t[s]>=t&&(i+=this.#e[s*2],r+=this.#e[s*2+1]);return {acquired:i,timeouts:r}}reset(){this.#r=-1,this.#o=0,this.#e.fill(0),this.#t.fill(0);}},T=class{constructor(e={}){this.state="closed";this.#e=0;this.#t=false;this.#s=null;if(this.#r=u(e.threshold??.5,"SaturationCircuitBreaker threshold",0,1,false,false),this.#o=u(e.window??1e4,"SaturationCircuitBreaker window",1e3,Number.MAX_SAFE_INTEGER,true,true),this.#a=u(e.windowBucketWidth??1e3,"SaturationCircuitBreaker windowBucketWidth",1,Number.MAX_SAFE_INTEGER,true,true),this.#u=Math.ceil(this.#o/this.#a),this.#d=u(e.cooldown??5e3,"SaturationCircuitBreaker cooldown",1e3,Number.MAX_SAFE_INTEGER,true,true),this.#c=u(e.minThroughput??10,"SaturationCircuitBreaker minThroughput",1,Number.MAX_SAFE_INTEGER,true,true),this.#h=u(e.minFailures??5,"SaturationCircuitBreaker minFailures",1,Number.MAX_SAFE_INTEGER,true,true),this.#o<this.#a)throw new o("SaturationCircuitBreaker window must be >= windowBucketWidth","INVALID_ARGUMENT");if(this.#c<this.#h)throw new o("SaturationCircuitBreaker minThroughput must be >= minFailures","INVALID_ARGUMENT");if(this.#u<2)throw new o("SaturationCircuitBreaker window must span at least 2 windowBucketWidth buckets","INVALID_ARGUMENT");this.#i=new P(this.#u,this.#a);}#e;#t;#s;#i;#r;#o;#a;#u;#d;#c;#h;get isClosed(){return this.state==="closed"}get isOpen(){return this.state==="open"}get isProbing(){return this.state==="probing"}get hasProbeInFlight(){return this.#t}get probeTaskId(){return this.#s}get cooldownRemaining(){return this.isOpen?Math.max(0,this.#e-Date.now()):0}trackAttempt(e){this.state==="closed"&&this.#i.addAcquired(e);}recordFailure(){this.#i.addTimeout();}checkAndTransition(){return this.state!=="open"||Date.now()<this.#e?false:(this.state="probing",this.#t=false,this.#s=null,true)}evaluateAndTrip(){if(this.state!=="closed")return {tripped:false};let{timeouts:e,acquired:t}=this.#i.snapshot();if(t<this.#c||e<this.#h)return {tripped:false};let i=e/t;return i<this.#r?{tripped:false}:(this.state="open",this.#e=Date.now()+this.#d,{tripped:true,timeoutRate:i,failures:e,attempts:t})}markProbeInFlight(){this.#t=true;}claimProbeSlot(e){this.#t=true,this.#s=e;}releaseProbeSlot(){this.#t=false,this.#s=null;}handleProbeSuccess(){this.state="closed",this.#t=false,this.#s=null,this.#i.reset();}handleProbeFailure(){this.state="open",this.#t=false,this.#s=null,this.#e=Date.now()+this.#d;}reset(){this.state="closed",this.#e=0,this.#t=false,this.#s=null,this.#i.reset();}};var S=class{#e=0;#t=0;#s;#i;#r;constructor(e={}){if(this.#s=u(e.initialTimeout??50,"BackoffTracker initialTimeout",0,Number.MAX_SAFE_INTEGER,true,true),this.#i=u(e.maxTimeout??2e3,"BackoffTracker maxTimeout",0,Number.MAX_SAFE_INTEGER,true,true),this.#r=u(e.decayFactor??.5,"BackoffTracker decayFactor",0,1,false,false),this.#i<this.#s)throw new o("BackoffTracker maxTimeout must be >= initialTimeout","INVALID_ARGUMENT")}get currentDelay(){return this.#o(Date.now())}#o(e){if(this.#e===0)return 0;let t=(e-this.#t)/1e3;if(t<=0)return this.#e;let i=this.#e*Math.pow(this.#r,t);return i<1?0:i}onTimeout(e=Date.now()){let t=this.#o(e),i=t>0?t*2:this.#s;this.#e=Math.min(i,this.#i),this.#t=e;}reset(){this.#e=0,this.#t=0;}};var I=class{constructor(e){this.prev=null;this.next=null;this.heapIndex=-1;this.#e=false;this.id=e.id,this.priority=e.priority,this.enqueueTime=e.enqueueTime,this.isProbe=e.isProbe,this.weight=e.weight??1,this.#i=e.resolve,this.#r=e.reject,this.#s=e.abortSignal;}#e;#t;#s;#i;#r;arm(e){this.#s&&(this.#t=()=>{this.claim()&&e();},this.#s.addEventListener("abort",this.#t));}dispatch(e){return this.claim()?(this.#i(e()),true):false}discard(e){return this.claim()?(this.#r(e),true):false}claim(){return this.#e?false:(this.#e=true,this.#s&&this.#t&&this.#s.removeEventListener("abort",this.#t),true)}reject(e){this.#r(e);}};var g=class{#e=[];#t;constructor(e){this.#t=e;}get size(){return this.#e.length}isEmpty(){return this.#e.length===0}peek(){return this.#e[0]}has(e){return e.heapIndex>=0&&this.#e[e.heapIndex]===e}clear(){for(let e of this.#e)e.heapIndex=-1;this.#e=[];}insert(e){if(e.heapIndex>=0)throw new o("Item is already in a heap","INVALID_ARGUMENT");let t=this.#e.length;this.#e.push(e),e.heapIndex=t,this.#i(t);}pop(){if(this.#e.length===0)return;let e=this.#e[0];e.heapIndex=-1;let t=this.#e.pop();return this.#e.length>0&&t!==e&&(this.#e[0]=t,t.heapIndex=0,this.#r(0)),e}delete(e){let t=e.heapIndex;if(t<0||this.#e[t]!==e)return;e.heapIndex=-1;let i=this.#e.pop();if(t===this.#e.length||i===e)return e;this.#e[t]=i,i.heapIndex=t;let r=this.#e[t-1>>1];return t>0&&this.#t(i,r)<0?this.#i(t):this.#r(t),e}#s(e,t){let i=this.#e[e],r=this.#e[t];this.#e[e]=r,this.#e[t]=i,i.heapIndex=t,r.heapIndex=e;}#i(e){for(;e>0;){let t=e-1>>1;if(this.#t(this.#e[e],this.#e[t])<0)this.#s(e,t),e=t;else break}}#r(e){let t=this.#e.length;for(;;){let i=e,r=2*e+1,s=2*e+2;if(r<t&&this.#t(this.#e[r],this.#e[i])<0&&(i=r),s<t&&this.#t(this.#e[s],this.#e[i])<0&&(i=s),i===e)break;this.#s(e,i),e=i;}}};var k=class{#e=null;#t=null;#s=0;get size(){return this.#s}isEmpty(){return this.#s===0}peekHead(){return this.#e===null?void 0:this.#e}pushTail(e){e.prev=this.#t,e.next=null,this.#t!==null?this.#t.next=e:this.#e=e,this.#t=e,this.#s++;}remove(e){e.prev!==null?e.prev.next=e.next:this.#e=e.next,e.next!==null?e.next.prev=e.prev:this.#t=e.prev,e.prev=e.next=null,this.#s--;}clear(){this.#e=this.#t=null,this.#s=0;}};var R=class{#e;#t;#s;#i=-1;#r=0;#o;#a;#u;#d;#c;#h;#S;#n;#f;#y;#T;constructor(e,t){this.#e=u(e,"CombinedWindow size",1,Number.MAX_SAFE_INTEGER,true,true),this.#t=u(t,"CombinedWindow stepMs",1,Number.MAX_SAFE_INTEGER,true,true),this.#s=new Float64Array(e),this.#o=new Int32Array(e),this.#a=new Int32Array(e),this.#u=new Int32Array(e),this.#d=new Float64Array(e),this.#c=new Int32Array(e),this.#h=new Float64Array(e),this.#S=new Float64Array(e),this.#n=new Int32Array(e),this.#f=new Float64Array(e),this.#y=new Float64Array(e),this.#T=new Int32Array(e);}#m(e){if(e<this.#r&&e>=this.#r-this.#t)return this.#i;let t=Math.floor(e/this.#t),i=t*this.#t,r=t%this.#e;return this.#s[r]!==i&&(this.#s[r]=i,this.#o[r]=0,this.#a[r]=0,this.#u[r]=0,this.#d[r]=0,this.#c[r]=0,this.#h[r]=0,this.#S[r]=0,this.#n[r]=0,this.#f[r]=0,this.#y[r]=0,this.#T[r]=0),this.#i=r,this.#r=i+this.#t,r}#l(e,t){this.#d[e]+=t,this.#c[e]++,t>this.#h[e]&&(this.#h[e]=t);}#v(e,t){this.#S[e]+=t,this.#n[e]++,t>this.#f[e]&&(this.#f[e]=t);}recordAcquire(e,t,i){let r=this.#m(e);this.#o[r]++,this.#l(r,t),this.#v(r,i);}recordAcquireQueued(e,t,i,r){let s=this.#m(e);this.#o[s]++,this.#y[s]+=t,this.#T[s]++,this.#l(s,i),this.#v(s,r);}recordRelease(e,t,i){let r=this.#m(e);this.#a[r]++,this.#l(r,t),this.#v(r,i);}recordTimeoutQueue(e,t){let i=this.#m(e);this.#u[i]++,this.#v(i,t);}sampleBoth(e,t,i){let r=this.#m(e);this.#l(r,t),this.#v(r,i);}addAcquired(e){this.#o[this.#m(e)]++;}addReleased(e){this.#a[this.#m(e)]++;}addTimeout(e){this.#u[this.#m(e)]++;}addLatency(e,t){let i=this.#m(e);this.#y[i]+=t,this.#T[i]++;}sampleInflight(e,t){this.#l(this.#m(e),t);}sampleQueue(e,t){this.#v(this.#m(e),t);}snapshot(e){let t=Math.floor(e/this.#t)*this.#t-(this.#e-1)*this.#t,i=0,r=0,s=0,a=0,d=0,m=0,f=0,p=0,b=0,c=0,y=0;for(let l=0;l<this.#e;l++)this.#s[l]<t||(i+=this.#o[l],r+=this.#a[l],s+=this.#u[l],a+=this.#d[l],d+=this.#c[l],this.#h[l]>m&&(m=this.#h[l]),f+=this.#S[l],p+=this.#n[l],this.#f[l]>b&&(b=this.#f[l]),c+=this.#y[l],y+=this.#T[l]);return {counts:{acquired:i,released:r,timeouts:s},inflight:{avg:d===0?0:a/d,max:m,samples:d},queue:{avg:p===0?0:f/p,max:b,samples:p},latency:{avg:y===0?0:c/y,count:y,total:c}}}reset(){this.#i=-1,this.#r=0,this.#s.fill(0),this.#o.fill(0),this.#a.fill(0),this.#u.fill(0),this.#d.fill(0),this.#c.fill(0),this.#h.fill(0),this.#S.fill(0),this.#n.fill(0),this.#f.fill(0),this.#y.fill(0),this.#T.fill(0);}},x=[{size:60,stepMs:1e3},{size:60,stepMs:5e3},{size:60,stepMs:6e4},{size:60,stepMs:144e4}],E=class{#e;#t;#s=0;#i=0;#r=0;#o=0;#a=0;#u=0;#d=0;#c=false;#h=false;constructor(e=x){if(e.length===0)throw new o("SemaphoreMetrics requires at least one WindowOptions","INVALID_ARGUMENT");this.#e=e.map(({size:r,stepMs:s})=>new R(r,s)),this.#t=e.map(({size:r,stepMs:s})=>{let a=r*s;return a>=36e5&&a%36e5===0?`${a/36e5}h`:a>=6e4&&a%6e4===0?`${a/6e4}m`:a>=1e3&&a%1e3===0?`${a/1e3}s`:`${a}ms`});let t=new Set;for(let r of this.#t){if(t.has(r))throw new o(`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.#t[i],this.primaryWindowMs=e[i].size*e[i].stepMs;}onAcquireFast(e,t,i){this.#s++;for(let r of this.#e)r.recordAcquire(e,t,i);}onAcquireQueued(e,t,i,r){this.#i++;for(let s of this.#e)s.recordAcquireQueued(e,t,i,r);}onRelease(e,t,i){this.#r++;for(let r of this.#e)r.recordRelease(e,t,i);}onTimeout(e,t){this.#o++;for(let i of this.#e)i.recordTimeoutQueue(e,t);}onAbort(e,t){this.#u++;for(let i of this.#e)i.sampleQueue(e,t);}onPurge(e,t){this.#a++;for(let i of this.#e)i.sampleQueue(e,t);}sampleGauges(e,t,i){for(let r of this.#e)r.sampleBoth(e,t,i);}sampleQueueDepthAt(e,t){for(let i of this.#e)i.sampleQueue(e,t);}markAcquireFast(){this.#s++;let e=Date.now();for(let t of this.#e)t.addAcquired(e);}markAcquireQueued(e){this.#i++;let t=Date.now();for(let i of this.#e)i.addAcquired(t),i.addLatency(t,e);}markReleased(){this.#r++;let e=Date.now();for(let t of this.#e)t.addReleased(e);}markTimeout(){this.#o++;let e=Date.now();for(let t of this.#e)t.addTimeout(e);}markAbort(){this.#u++;}sampleInFlight(e){let t=Date.now();for(let i of this.#e)i.sampleInflight(t,e);}sampleQueueDepth(e){let t=Date.now();for(let i of this.#e)i.sampleQueue(t,e);}markCapacityChange(e){this.#d=e;}markCircuitOpen(){this.#c=true,this.#h=false;}markCircuitProbing(){this.#c=false,this.#h=true;}markCircuitClose(){this.#c=false,this.#h=false;}getSnapshot(e=Date.now()){let t={},i=0,r=0;for(let s=0;s<this.#t.length;s++){let a=this.#e[s].snapshot(e);t[this.#t[s]]=a,s===0&&(i=a.inflight.avg,r=a.queue.avg);}return {windows:t,meta:{inFlightLastMinute:Math.round(i),queueDepthLastMinute:Math.round(r),totalAcquiredFast:this.#s,totalAcquiredQueued:this.#i,totalReleased:this.#r,totalTimeouts:this.#o,totalPurged:this.#a,totalAborts:this.#u,capacity:this.#d,circuitOpen:this.#c,circuitProbing:this.#h}}}reset(){for(let e of this.#e)e.reset();this.#s=0,this.#i=0,this.#r=0,this.#o=0,this.#a=0,this.#u=0,this.#d=0,this.#c=false,this.#h=false;}destroy(){this.reset();}};var F={fifo:(n,e)=>n.id-e.id,lifo:(n,e)=>e.id-n.id,fifoWithPriority:(n,e)=>n.priority-e.priority||n.id-e.id,lifoWithPriority:(n,e)=>n.priority-e.priority||e.id-n.id};function q(n){if(n.comparator!==void 0){if(typeof n.comparator!="function")throw new o("Semaphore comparator must be a function","INVALID_ARGUMENT");return n.comparator}let e=n.queueOrder??"fifoWithPriority",t=F[e];if(t===void 0)throw new o(`Semaphore queueOrder must be one of: ${Object.keys(F).join(", ")} (got ${JSON.stringify(e)})`,"INVALID_ARGUMENT");return t}function N(n){let e=q(n);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 h={TASKACQUIRE:"task-acquire",TASKRELEASE:"task-release",TASKTIMEOUT:"task-timeout",TASKABORT:"task-abort",QUEUEPURGE:"queue-purge",QUEUEEVICT:"queue-evict",CIRCUITOPEN:"circuit-open",CIRCUITPROBING:"circuit-probing",CIRCUITCLOSE:"circuit-close",CIRCUITSTATECHANGE:"circuit-state-change",SHUTDOWN:"shutdown"};var w=class{#e;#t;#s;#i;#r;#o;#a;#u;#d;#c;#h;#S;#n;#f;#y=0;#T=0;#m=false;#l=false;#v=0;#R=0;#x=0;#q=0;#O=0;#D=0;#w=new Map;#I=null;#b=null;#k=null;#E=null;constructor(e,t={}){if(u(e,"Semaphore count",0,Number.MAX_SAFE_INTEGER,true,false),this.#h=u(t.purgeIntervalMs??3e3,"Semaphore purgeIntervalMs",500,Number.MAX_SAFE_INTEGER,true,true),this.#u=u(t.queueMaxTimeout??1e4,"Semaphore queueMaxTimeout",1,Number.MAX_SAFE_INTEGER,true,true),this.#a=u(t.queueMaxLength??1024,"Semaphore queueMaxLength",1,Number.MAX_SAFE_INTEGER,true,true),this.#d=u(t.queueMaxAge??3e4,"Semaphore queueMaxAge",1,Number.MAX_SAFE_INTEGER,true,true),this.#c=t.rejectOnFull??false,this.#S=t.metricsEnabled??true,this.#n=t.debug??false,t.circuitBreakerFailurePredicate!==void 0&&typeof t.circuitBreakerFailurePredicate!="function")throw new o("Semaphore circuitBreakerFailurePredicate must be a function","INVALID_ARGUMENT");this.#f=t.circuitBreakerFailurePredicate;let i=N({queueOrder:t.queueOrder,comparator:t.comparator});this.#t=new g(i),this.#s=new k,this.#i=new v(e),this.#r=t.circuitBreaker??new T({threshold:t.circuitBreakerThreshold,window:t.circuitBreakerWindow,windowBucketWidth:t.circuitBreakerWindowBucketWidth,cooldown:t.circuitBreakerCooldown,minThroughput:t.circuitBreakerMinThroughput,minFailures:t.circuitBreakerMinFailures}),this.#o=new S({initialTimeout:t.backoffInitialTimeout,maxTimeout:t.backoffMaxTimeout,decayFactor:t.backoffDecayFactor}),this.#e=this.#S?new E(t.metricsWindows):void 0,this.#e?.markCapacityChange(this.#i.capacity),this.#e?.sampleGauges(Date.now(),this.#i.inFlight,this.#t.size),this.#G();}on(e,t){this.#w.has(e)||this.#w.set(e,new Set),this.#w.get(e).add(t);}off(e,t){this.#w.get(e)?.delete(t),this.#w.get(e)?.size===0&&this.#w.delete(e);}removeAllListeners(e){e?this.#w.delete(e):this.#w.clear();}#F(e){let t=this.#w.get(e);return t!==void 0&&t.size>0}#p(e,...t){let i=this.#w.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);}}}#A(e,t){this.#p(h.CIRCUITSTATECHANGE,{from:e,to:t});}#U(e){if(this.#r.isProbing){if(this.#r.hasProbeInFlight||!this.#i.hasCapacityFor(e))return null;if(this.#r.markProbeInFlight(),this.#i.acquire(e),this.#R++,this.#e!==void 0){let t=Date.now();this.#r.trackAttempt(t),this.#e.onAcquireFast(t,this.#i.inFlight,this.#t.size);}else this.#r.trackAttempt();return this.#F(h.TASKACQUIRE)&&this.#p(h.TASKACQUIRE,{queued:this.#t.size,running:this.#i.capacity-this.#i.available,probe:true}),this.#_(true,e)}if(this.#t.size>0||!this.#i.hasCapacityFor(e))return null;if(this.#i.acquire(e),this.#R++,this.#e!==void 0){let t=Date.now();this.#r.trackAttempt(t),this.#e.onAcquireFast(t,this.#i.inFlight,this.#t.size);}else this.#r.trackAttempt();return this.#F(h.TASKACQUIRE)&&this.#p(h.TASKACQUIRE,{queued:this.#t.size,running:this.#i.capacity-this.#i.available}),this.#_(false,e)}#_(e=false,t=1){this.#y++;let i=this.#T,r=false;return ()=>{if(r||i!==this.#T){this.#n&&console.warn("[Semaphore] release() called after already released (no-op)");return}r=true,this.#y--,this.#i.release(t),this.#x++,this.#e?.onRelease(Date.now(),this.#i.inFlight,this.#t.size),e&&this.#r.isProbing&&(this.#r.handleProbeSuccess(),this.#p(h.CIRCUITCLOSE),this.#A("probing","closed"),this.#e?.markCircuitClose(),this.#n&&console.info("[Semaphore] Circuit closed after successful probe")),this.#i.assertInvariant(this.#n),this.#F(h.TASKRELEASE)&&this.#p(h.TASKRELEASE,{queued:this.#t.size,running:this.#i.capacity-this.#i.available,weight:t}),this.#C(),this.#g();}}#C(){this.#b&&this.#t.size===0&&this.#i.available===this.#i.capacity&&(this.#b(),this.#b=null,this.#I=null);}#$(e){this.#r.trackAttempt(),this.#t.insert(e),this.#s.pushTail(e),this.#V();}#M(e){this.#t.delete(e)!==void 0&&(this.#s.remove(e),this.#s.size===0&&this.#P());}#V(){if(this.#E!==null)return;let e=this.#s.peekHead();if(e===void 0)return;let t=Math.max(0,e.enqueueTime+this.#u-Date.now());this.#E=setTimeout(()=>this.#B(),t);}#P(){this.#E!==null&&(clearTimeout(this.#E),this.#E=null);}#B(){if(this.#E=null,this.#l)return;let e=Date.now(),t=0,i=this.#s.peekHead();for(;i!==void 0&&i.enqueueTime+this.#u<=e;){let r=i.claim();this.#M(i),r&&(this.#H(i,e),t++),i=this.#s.peekHead();}if(i!==void 0){let r=Math.max(0,i.enqueueTime+this.#u-e);this.#E=setTimeout(()=>this.#B(),r);}t>0&&(this.#C(),this.#g());}#Q(e){let t=e.discard(new o("Circuit breaker opened while task was queued","CIRCUIT_OPEN"));return t&&(this.#D++,this.#p(h.QUEUEEVICT,{id:e.id,priority:e.priority,enqueueTime:e.enqueueTime,weight:e.weight}),this.#n&&console.warn(`[Semaphore] Evicted queued task #${e.id}: circuit opened`)),t}#L(){if(this.#t.size===0)return;if(this.#r.probeTaskId===null){let i=0;for(let r=this.#s.peekHead();r!==void 0;r=r.next??void 0)this.#Q(r)&&i++;this.#t.clear(),this.#s.clear(),this.#P(),i>0&&this.#e?.sampleQueueDepthAt(Date.now(),this.#t.size);return}let e=0,t=this.#s.peekHead();for(;t!==void 0;){let i=t.next??void 0;t.isProbe||(this.#Q(t)&&e++,this.#M(t)),t=i;}e>0&&this.#e?.sampleQueueDepthAt(Date.now(),this.#t.size);}#H(e,t){if(this.#q++,this.#r.recordFailure(),this.#o.onTimeout(t),e.isProbe)this.#r.handleProbeFailure(),this.#p(h.CIRCUITOPEN,{timeoutRate:1,recentTimeouts:1,total:1,reason:"probe-failed"}),this.#A("probing","open"),this.#e?.markCircuitOpen(),this.#n&&console.warn("[Semaphore] Circuit re-opened: probe timed out");else {let i=this.#r.evaluateAndTrip();i.tripped&&(this.#p(h.CIRCUITOPEN,{timeoutRate:i.timeoutRate,recentTimeouts:i.failures,total:i.attempts}),this.#A("closed","open"),this.#e?.markCircuitOpen(),this.#n&&console.warn(`[Semaphore] Circuit opened. Rate: ${(i.timeoutRate*100).toFixed(1)}%`),this.#L());}this.#e?.onTimeout(t,this.#t.size),this.#p(h.TASKTIMEOUT,{queueLength:this.#t.size,backoffDelay:this.#o.currentDelay,taskId:e.id}),this.#n&&console.warn(`[Semaphore] Task #${e.id} timed out after ${this.#u}ms`),e.reject(new o(`Semaphore acquire timed out after ${this.#u}ms (queue: ${this.#t.size})`,"TIMEOUT"));}#j(e,t){this.#M(e),e.isProbe&&this.#r.releaseProbeSlot(),this.#e?.onAbort(Date.now(),this.#t.size),this.#p(h.TASKABORT),this.#n&&console.info(`[Semaphore] Task #${e.id} aborted`),t(new o("Semaphore acquire aborted","ABORTED")),this.#C(),this.#g();}#W=()=>{this.#K();};#g(){if(this.#m||this.#l)return;this.#m=true;let e=this.#o.currentDelay;e>0?setTimeout(this.#W,e).unref?.():queueMicrotask(this.#W);}#K(){if(!this.#l){this.#m=false;try{let e=Date.now();for(;this.#t.size>0&&!this.#r.isOpen;){let t=this.#t.peek();if(!t||this.#r.isProbing&&t.id!==this.#r.probeTaskId||!this.#i.hasCapacityFor(t.weight))break;let i=this.#t.pop();this.#s.remove(i),this.#s.size===0&&this.#P(),i.dispatch(()=>{let s=Math.max(0,e-i.enqueueTime);return this.#i.acquire(i.weight),this.#R++,this.#e?.onAcquireQueued(e,s,this.#i.inFlight,this.#t.size),this.#i.assertInvariant(this.#n),this.#_(i.isProbe,i.weight)})&&this.#F(h.TASKACQUIRE)&&this.#p(h.TASKACQUIRE,{queued:this.#t.size,running:this.#i.capacity-this.#i.available,...i.isProbe?{probe:!0}:{}});}this.#C();}catch(e){e instanceof Error?console.error("[Semaphore] Scheduler error:",e.message,e.stack):console.error("[Semaphore] Scheduler error:",e);}}}#G(){this.#k!==null&&clearInterval(this.#k),this.#k=setInterval(()=>{this.#l||this.#X();},this.#h),this.#k.unref?.();}#X(){let e=Date.now(),t=this.#t.size,i=this.#s.peekHead();for(;i!==void 0&&e-i.enqueueTime>this.#d;){this.#r.probeTaskId===i.id&&this.#r.releaseProbeSlot();let r=i.discard(new o(`Task purged after ${this.#d}ms`,"PURGED"));this.#M(i),r&&(this.#O++,this.#e?.onPurge(e,this.#t.size),this.#p(h.QUEUEPURGE,{id:i.id,priority:i.priority,enqueueTime:i.enqueueTime,weight:i.weight}),this.#n&&console.warn(`[Semaphore] Purged stale task #${i.id}`)),i=this.#s.peekHead();}this.#n&&this.#t.size<t&&console.info(`[Semaphore] Purged ${t-this.#t.size} stale tasks`),this.#t.size<t&&(this.#C(),this.#g());}tryAcquire(e=1){return this.#l||!Number.isInteger(e)||e<1||e>this.#i.capacity||(this.#r.checkAndTransition()&&(this.#p(h.CIRCUITPROBING),this.#A("open","probing"),this.#e?.markCircuitProbing(),this.#n&&console.info("[Semaphore] Circuit entering probing"),this.#g()),this.#r.isOpen)?null:this.#U(e)}acquire(e,t=0,i=1){return this.#z(e,t,i)}#N=false;#z(e,t=0,i=1){if(this.#N=false,this.#l)return Promise.reject(new o("Semaphore is shut down","SHUTDOWN"));if(!Number.isInteger(i)||i<1||i>this.#i.capacity)return Promise.reject(new o(`Invalid weight: ${i} (must be integer in 1..${this.#i.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.#r.checkAndTransition()&&(this.#p(h.CIRCUITPROBING),this.#A("open","probing"),this.#e?.markCircuitProbing(),this.#n&&console.info("[Semaphore] Circuit entering probing"),this.#g()),this.#r.isOpen)return Promise.reject(new o(`Circuit breaker open, retry in ${this.#r.cooldownRemaining}ms`,"CIRCUIT_OPEN"));if(this.#r.isProbing&&this.#r.hasProbeInFlight)return Promise.reject(new o("Circuit breaker is probing, probe already in flight","CIRCUIT_PROBING"));if(e?.aborted)return Promise.reject(new o("Semaphore acquire aborted before start","ABORTED"));let r=this.#r.isProbing,s=this.#U(i);if(s)return this.#N=r,Promise.resolve(s);let a=this.#r.isProbing&&!this.#r.hasProbeInFlight;return !a&&this.#c?Promise.reject(new o("Semaphore at capacity (rejectOnFull)","QUEUE_FULL")):!a&&this.#t.size>=this.#a?Promise.reject(new o(`Queue full (${this.#a})`,"QUEUE_FULL")):(this.#N=a,new Promise((d,m)=>{let f=++this.#v,p=Date.now(),b=a,c=new I({id:f,priority:b?Number.MIN_SAFE_INTEGER:t,enqueueTime:p,isProbe:b,resolve:d,reject:m,abortSignal:e,weight:i});c.arm(()=>this.#j(c,m)),b&&this.#r.claimProbeSlot(f),this.#e?.sampleQueueDepthAt(p,this.#t.size+1),this.#$(c),this.#g();}))}peekQueue(e={}){let t=e.offset!==void 0?u(e.offset,"peekQueue offset",0,Number.MAX_SAFE_INTEGER,true,true):0,i=e.limit!==void 0?u(e.limit,"peekQueue limit",0,Number.MAX_SAFE_INTEGER,true,true):Number.POSITIVE_INFINITY,r=[],s=this.#s.peekHead();for(let a=0;s!==void 0&&a<t;a++)s=s.next??void 0;for(;s!==void 0&&r.length<i;)r.push({id:s.id,priority:s.priority,enqueueTime:s.enqueueTime,weight:s.weight,isProbe:s.isProbe}),s=s.next??void 0;return r}async use(e,t,i=0,r=1,s){let a=this.#z(t,i,r),d=this.#f!==void 0&&this.#N,m=await a,f=s?Date.now():0;try{let p=await e(),b=s?Date.now()-f:0;if(m(),s)try{s(b,"success");}catch(c){console.warn("[Semaphore] onSettle threw:",c);}return p}catch(p){let b=s?Date.now()-f:0;if(this.#f!==void 0){let c=false;try{c=this.#f(p)===!0;}catch(y){console.warn("[Semaphore] circuitBreakerFailurePredicate threw (rejection treated as non-matching):",y);}c&&(d&&this.#r.isProbing?(this.#r.recordFailure(),this.#r.handleProbeFailure(),this.#p(h.CIRCUITOPEN,{timeoutRate:1,recentTimeouts:1,total:1,reason:"probe-failed"}),this.#A("probing","open"),this.#e?.markCircuitOpen(),this.#n&&console.warn("[Semaphore] Circuit re-opened: probe operation failed")):this.reportFailure());}if(m(),s)try{s(b,"error");}catch(c){console.warn("[Semaphore] onSettle threw:",c);}throw p}}drain(e){return this.#l?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.#I?this.#I:this.#t.size===0&&this.#i.available===this.#i.capacity?Promise.resolve():(this.#I=new Promise((t,i)=>{if(this.#b=t,e!==void 0){let r=setTimeout(()=>{this.#b=null,this.#I=null,i(new o(`drain() timed out after ${e}ms`,"TIMEOUT"));},e),s=this.#b;this.#b=()=>{clearTimeout(r),s();};}}),this.#I))}reset(e={}){if(this.#l)throw new o("Cannot reset a semaphore that has been shut down","SHUTDOWN");for(let t=this.#s.peekHead();t!==void 0;t=t.next??void 0)t.discard(new o("Semaphore reset","SHUTDOWN"));this.#t.clear(),this.#s.clear(),this.#P(),this.#e?.reset(),this.#i.reset(),this.#o.reset(),this.#r.reset(),this.#T++,this.#y=0,this.#m=false,this.#l=false,this.#v=0,this.#R=0,this.#x=0,this.#q=0,this.#O=0,this.#D=0,e.clearListeners&&this.#w.clear(),this.#b&&(this.#b(),this.#b=null,this.#I=null),this.#G(),this.#e?.markCapacityChange(this.#i.capacity),this.#e?.sampleGauges(Date.now(),this.#i.inFlight,this.#t.size),this.#n&&console.info("[Semaphore] Reset to initial state");}shutdown(e="Semaphore shutdown"){if(!this.#l){this.#l=true,this.#n&&console.info(`[Semaphore] Shutdown: ${e}`),this.#k!==null&&(clearInterval(this.#k),this.#k=null),this.#P();for(let t=this.#s.peekHead();t!==void 0;t=t.next??void 0)t.discard(new o(e,"SHUTDOWN"));this.#t.clear(),this.#s.clear(),this.#T++,this.#y=0,this.#i.reset(),this.#e?.destroy(),this.#e=void 0,this.#b&&(this.#b(),this.#b=null,this.#I=null),this.#p(h.SHUTDOWN,e);}}cancel(){if(this.#l)return;let e=0;for(let t=this.#s.peekHead();t!==void 0;t=t.next??void 0)t.isProbe&&this.#r.releaseProbeSlot(),t.discard(new o("Semaphore acquire cancelled","CANCELLED")),e++;this.#t.clear(),this.#s.clear(),this.#P(),this.#e?.sampleQueueDepthAt(Date.now(),this.#t.size),this.#n&&console.info(`[Semaphore] Cancelled ${e} queued tasks`),this.#C(),this.#g();}reportFailure(){if(this.#l)return;this.#r.recordFailure();let e=this.#r.evaluateAndTrip();e.tripped&&(this.#p(h.CIRCUITOPEN,{timeoutRate:e.timeoutRate,recentTimeouts:e.failures,total:e.attempts,reason:"reported-failure"}),this.#A("closed","open"),this.#e?.markCircuitOpen(),this.#n&&console.warn(`[Semaphore] Circuit opened via reportFailure(). Rate: ${(e.timeoutRate*100).toFixed(1)}%`),this.#L());}status(){let e=Date.now(),t=this.#e?.getSnapshot(e)??null,i=this.#e!==void 0?t?.windows?.[this.#e.primaryLabel]?.counts:void 0,r=(this.#e?.primaryWindowMs??6e4)/1e3,s=i?.acquired??0,a=i?.timeouts??0,d=this.#s.peekHead(),m=d===void 0?0:Math.max(0,e-d.enqueueTime);return {status:{running:this.#i.capacity-this.#i.available,queued:this.#t.size,available:this.#i.available,inFlight:this.#i.inFlight,pendingReleases:this.#y,circuitOpen:this.#r.isOpen,circuitProbing:this.#r.isProbing,backoffDelay:Math.round(this.#o.currentDelay),requestsPerSecond:+(s/r).toFixed(2),timeoutRate1m:s+a>0?+(a/(s+a)*100).toFixed(1):0,queueAge:m},lifetime:{totalAcquired:this.#R,totalReleased:this.#x,totalTimeouts:this.#q,totalPurged:this.#O,totalEvictions:this.#D,circuitBreakerCooldownRemaining:this.#r.cooldownRemaining},metrics:t}}get availablePermits(){return this.#i.available}isAvailable(){return !this.#l&&!this.#r.isOpen&&!this.#i.isFull}get capacity(){return this.#i.capacity}get circuitState(){return this.#r.state}get queueLength(){return this.#t.size}};var M=class{#e=new Map;#t;#s;constructor(e,t={}){this.#t=e,this.#s=t;}forKey(e){let t=this.#e.get(e);return t===void 0&&(t=new w(this.#t,this.#s),this.#e.set(e,t)),t}use(e,t,i,r=0,s=1){return this.forKey(e).use(t,i,r,s)}has(e){return this.#e.has(e)}get size(){return this.#e.size}keys(){return this.#e.keys()}delete(e){let t=this.#e.get(e);return t===void 0?false:(t.shutdown("KeyedSemaphore: key deleted"),this.#e.delete(e),true)}shutdown(e="KeyedSemaphore shutdown"){for(let t of this.#e.values())t.shutdown(e);this.#e.clear();}};var O={tripped:false},A=class{constructor(){this.state="closed";this.isOpen=false;this.isProbing=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 D={tripped:false},C=class{constructor(){this.state="closed";this.hasProbeInFlight=false;this.probeTaskId=null;}get isOpen(){return this.state==="open"}get isProbing(){return false}get cooldownRemaining(){return 0}open(){this.state="open";}close(){this.state="closed";}checkAndTransition(){return false}trackAttempt(){}recordFailure(){}evaluateAndTrip(){return D}markProbeInFlight(){}claimProbeSlot(){}releaseProbeSlot(){}handleProbeSuccess(){}handleProbeFailure(){}reset(){this.state="closed";}};exports.KeyedSemaphore=M;exports.ManualCircuitBreaker=C;exports.NoopCircuitBreaker=A;exports.QUEUE_ORDERINGS=F;exports.SaturationCircuitBreaker=T;exports.Semaphore=w;exports.SemaphoreError=o;exports.SemaphoreEvents=h;//# sourceMappingURL=index.cjs.map | ||
| //# sourceMappingURL=index.cjs.map |
+56
-64
@@ -104,2 +104,9 @@ interface SemaphoreConfig { | ||
| } | ||
| /** Options for `peekQueue()`: bound how much of a deep queue gets materialized. */ | ||
| interface PeekQueueOptions { | ||
| /** Skip this many queued tasks (in enqueue order) before collecting. Must be a non-negative integer. Default: 0. */ | ||
| offset?: number; | ||
| /** Maximum number of tasks to collect after the offset. Must be a non-negative integer. Default: unbounded. */ | ||
| limit?: number; | ||
| } | ||
| type CircuitState = 'closed' | 'open' | 'probing'; | ||
@@ -163,2 +170,4 @@ interface CircuitBreakerConfig { | ||
| } | ||
| /** Passed to `use()`'s optional `onSettle` hook: whether `fn()` resolved or rejected. */ | ||
| type OperationOutcome = 'success' | 'error'; | ||
| declare const SemaphoreEvents: { | ||
@@ -174,2 +183,3 @@ readonly TASKACQUIRE: "task-acquire"; | ||
| readonly CIRCUITCLOSE: "circuit-close"; | ||
| readonly CIRCUITSTATECHANGE: "circuit-state-change"; | ||
| readonly SHUTDOWN: "shutdown"; | ||
@@ -193,2 +203,3 @@ }; | ||
| running: number; | ||
| weight: number; | ||
| }]; | ||
@@ -211,2 +222,6 @@ 'task-timeout': [payload: { | ||
| 'circuit-close': []; | ||
| 'circuit-state-change': [payload: { | ||
| from: CircuitState; | ||
| to: CircuitState; | ||
| }]; | ||
| 'shutdown': [reason: string]; | ||
@@ -254,2 +269,3 @@ } | ||
| } | ||
| type ID = string | number; | ||
| type Comparator<T> = (a: T, b: T) => number; | ||
@@ -262,31 +278,3 @@ interface WindowOptions { | ||
| declare class Semaphore { | ||
| private metricsCollector?; | ||
| private queue; | ||
| private readonly enqueueOrder; | ||
| private readonly permits; | ||
| private readonly circuit; | ||
| private readonly backoff; | ||
| private readonly queueMaxLength; | ||
| private readonly queueMaxTimeout; | ||
| private readonly queueMaxAge; | ||
| private readonly rejectOnFull; | ||
| private readonly purgeIntervalMs; | ||
| private readonly metricsEnabled; | ||
| private readonly debug; | ||
| private readonly failurePredicate?; | ||
| private pendingReleaseCount; | ||
| private releaseGeneration; | ||
| private scheduled; | ||
| private isShutdown; | ||
| private taskIdCounter; | ||
| private totalAcquired; | ||
| private totalReleased; | ||
| private totalTimeouts; | ||
| private totalPurged; | ||
| private totalEvictions; | ||
| private eventListeners; | ||
| private drainPromise; | ||
| private drainResolve; | ||
| private purgeIntervalId; | ||
| private timeoutTimerId; | ||
| #private; | ||
| constructor(count: number, config?: SemaphoreConfig); | ||
@@ -296,21 +284,2 @@ on<E extends SemaphoreEventType>(event: E, listener: SemaphoreEventListener<E>): void; | ||
| removeAllListeners(event?: SemaphoreEventType): void; | ||
| private hasListeners; | ||
| private emit; | ||
| private _tryAcquireFast; | ||
| private _createRelease; | ||
| private _checkDrain; | ||
| private _enqueue; | ||
| private _dequeue; | ||
| private _armTimeout; | ||
| private _clearTimeout; | ||
| private _fireTimeout; | ||
| private _evictTask; | ||
| private _evictQueueOnCircuitOpen; | ||
| private _timeoutTask; | ||
| private _onTaskAbort; | ||
| private readonly _runSchedulerBound; | ||
| private schedule; | ||
| private _runScheduler; | ||
| private _startPurgeInterval; | ||
| private _purgeStaleTasks; | ||
| /** | ||
@@ -330,9 +299,12 @@ * Non-blocking acquire. Returns a release closure or null. | ||
| acquire(abortSignal?: AbortSignal, priority?: number, weight?: number): Promise<() => void>; | ||
| private lastAdmissionWasProbe; | ||
| private _acquire; | ||
| /** | ||
| * Read-only snapshot of the queue, in enqueue order. Entries additionally | ||
| * expose `isProbe` so a circuit-breaker probe is identifiable in the view. | ||
| * | ||
| * `offset`/`limit` bound how much of a deep queue gets materialized — the | ||
| * walk still traverses the skipped prefix (the enqueue-ordered list has no | ||
| * random access), but collection stops as soon as `limit` entries are | ||
| * gathered instead of always copying the whole queue. | ||
| */ | ||
| peekQueue(): (QueuedTaskView & { | ||
| peekQueue(options?: PeekQueueOptions): (QueuedTaskView & { | ||
| isProbe: boolean; | ||
@@ -349,4 +321,8 @@ })[]; | ||
| * @param weight Permits to consume (integer in 1..count). Defaults to 1. | ||
| * @param onSettle Optional hook reporting how long fn() itself took (not | ||
| * queue-wait time) and whether it resolved or rejected. Never called if the | ||
| * acquire itself rejects (no permit means fn() never runs). A throwing hook | ||
| * is caught and logged via console.warn — it never masks fn()'s own result. | ||
| */ | ||
| use<T>(fn: () => Promise<T>, abortSignal?: AbortSignal, priority?: number, weight?: number): Promise<T>; | ||
| use<T>(fn: () => Promise<T>, abortSignal?: AbortSignal, priority?: number, weight?: number, onSettle?: (durationMs: number, outcome: OperationOutcome) => void): Promise<T>; | ||
| /** | ||
@@ -432,2 +408,28 @@ * Resolves once the queue is empty and all permits have been returned. | ||
| declare class KeyedSemaphore { | ||
| #private; | ||
| constructor(count: number, config?: SemaphoreConfig); | ||
| /** The Semaphore for `key`, constructing it (with the registry's shared count/config) on first access. */ | ||
| forKey(key: ID): Semaphore; | ||
| /** Sugar for `forKey(key).use(fn, abortSignal, priority, weight)`. */ | ||
| use<T>(key: ID, fn: () => Promise<T>, abortSignal?: AbortSignal, priority?: number, weight?: number): Promise<T>; | ||
| /** True if `key` already has a constructed Semaphore (does not create one). */ | ||
| has(key: ID): boolean; | ||
| /** Number of keys with a live Semaphore. */ | ||
| get size(): number; | ||
| /** Keys with a live Semaphore, in first-access order. */ | ||
| keys(): IterableIterator<ID>; | ||
| /** | ||
| * Shuts down and forgets `key`'s Semaphore, releasing its purge-interval | ||
| * timer. A later forKey(key) constructs a fresh one. Returns false if `key` | ||
| * had no Semaphore. | ||
| */ | ||
| delete(key: ID): boolean; | ||
| /** | ||
| * Shuts down every key's Semaphore and empties the registry. | ||
| * Terminal — like Semaphore.shutdown(), there is no undo. | ||
| */ | ||
| shutdown(reason?: string): void; | ||
| } | ||
| declare class SemaphoreError extends Error { | ||
@@ -439,14 +441,4 @@ readonly code: SemaphoreErrorCode; | ||
| declare class SaturationCircuitBreaker implements CircuitBreakerStrategy { | ||
| #private; | ||
| state: CircuitState; | ||
| private openUntil; | ||
| private probeInFlight; | ||
| private _probeTaskId; | ||
| private readonly eventWindow; | ||
| private readonly threshold; | ||
| private readonly window; | ||
| private readonly windowBucketWidth; | ||
| private readonly windowBucketCount; | ||
| private readonly cooldown; | ||
| private readonly minThroughput; | ||
| private readonly minFailures; | ||
| constructor(config?: CircuitBreakerConfig); | ||
@@ -540,2 +532,2 @@ get isClosed(): boolean; | ||
| 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 }; | ||
| export { type CircuitBreakerConfig, type CircuitBreakerStrategy, type CircuitState, type CircuitTripResult, type Comparator, type ID, KeyedSemaphore, ManualCircuitBreaker, NoopCircuitBreaker, type OperationOutcome, type PeekQueueOptions, 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 }; |
+56
-64
@@ -104,2 +104,9 @@ interface SemaphoreConfig { | ||
| } | ||
| /** Options for `peekQueue()`: bound how much of a deep queue gets materialized. */ | ||
| interface PeekQueueOptions { | ||
| /** Skip this many queued tasks (in enqueue order) before collecting. Must be a non-negative integer. Default: 0. */ | ||
| offset?: number; | ||
| /** Maximum number of tasks to collect after the offset. Must be a non-negative integer. Default: unbounded. */ | ||
| limit?: number; | ||
| } | ||
| type CircuitState = 'closed' | 'open' | 'probing'; | ||
@@ -163,2 +170,4 @@ interface CircuitBreakerConfig { | ||
| } | ||
| /** Passed to `use()`'s optional `onSettle` hook: whether `fn()` resolved or rejected. */ | ||
| type OperationOutcome = 'success' | 'error'; | ||
| declare const SemaphoreEvents: { | ||
@@ -174,2 +183,3 @@ readonly TASKACQUIRE: "task-acquire"; | ||
| readonly CIRCUITCLOSE: "circuit-close"; | ||
| readonly CIRCUITSTATECHANGE: "circuit-state-change"; | ||
| readonly SHUTDOWN: "shutdown"; | ||
@@ -193,2 +203,3 @@ }; | ||
| running: number; | ||
| weight: number; | ||
| }]; | ||
@@ -211,2 +222,6 @@ 'task-timeout': [payload: { | ||
| 'circuit-close': []; | ||
| 'circuit-state-change': [payload: { | ||
| from: CircuitState; | ||
| to: CircuitState; | ||
| }]; | ||
| 'shutdown': [reason: string]; | ||
@@ -254,2 +269,3 @@ } | ||
| } | ||
| type ID = string | number; | ||
| type Comparator<T> = (a: T, b: T) => number; | ||
@@ -262,31 +278,3 @@ interface WindowOptions { | ||
| declare class Semaphore { | ||
| private metricsCollector?; | ||
| private queue; | ||
| private readonly enqueueOrder; | ||
| private readonly permits; | ||
| private readonly circuit; | ||
| private readonly backoff; | ||
| private readonly queueMaxLength; | ||
| private readonly queueMaxTimeout; | ||
| private readonly queueMaxAge; | ||
| private readonly rejectOnFull; | ||
| private readonly purgeIntervalMs; | ||
| private readonly metricsEnabled; | ||
| private readonly debug; | ||
| private readonly failurePredicate?; | ||
| private pendingReleaseCount; | ||
| private releaseGeneration; | ||
| private scheduled; | ||
| private isShutdown; | ||
| private taskIdCounter; | ||
| private totalAcquired; | ||
| private totalReleased; | ||
| private totalTimeouts; | ||
| private totalPurged; | ||
| private totalEvictions; | ||
| private eventListeners; | ||
| private drainPromise; | ||
| private drainResolve; | ||
| private purgeIntervalId; | ||
| private timeoutTimerId; | ||
| #private; | ||
| constructor(count: number, config?: SemaphoreConfig); | ||
@@ -296,21 +284,2 @@ on<E extends SemaphoreEventType>(event: E, listener: SemaphoreEventListener<E>): void; | ||
| removeAllListeners(event?: SemaphoreEventType): void; | ||
| private hasListeners; | ||
| private emit; | ||
| private _tryAcquireFast; | ||
| private _createRelease; | ||
| private _checkDrain; | ||
| private _enqueue; | ||
| private _dequeue; | ||
| private _armTimeout; | ||
| private _clearTimeout; | ||
| private _fireTimeout; | ||
| private _evictTask; | ||
| private _evictQueueOnCircuitOpen; | ||
| private _timeoutTask; | ||
| private _onTaskAbort; | ||
| private readonly _runSchedulerBound; | ||
| private schedule; | ||
| private _runScheduler; | ||
| private _startPurgeInterval; | ||
| private _purgeStaleTasks; | ||
| /** | ||
@@ -330,9 +299,12 @@ * Non-blocking acquire. Returns a release closure or null. | ||
| acquire(abortSignal?: AbortSignal, priority?: number, weight?: number): Promise<() => void>; | ||
| private lastAdmissionWasProbe; | ||
| private _acquire; | ||
| /** | ||
| * Read-only snapshot of the queue, in enqueue order. Entries additionally | ||
| * expose `isProbe` so a circuit-breaker probe is identifiable in the view. | ||
| * | ||
| * `offset`/`limit` bound how much of a deep queue gets materialized — the | ||
| * walk still traverses the skipped prefix (the enqueue-ordered list has no | ||
| * random access), but collection stops as soon as `limit` entries are | ||
| * gathered instead of always copying the whole queue. | ||
| */ | ||
| peekQueue(): (QueuedTaskView & { | ||
| peekQueue(options?: PeekQueueOptions): (QueuedTaskView & { | ||
| isProbe: boolean; | ||
@@ -349,4 +321,8 @@ })[]; | ||
| * @param weight Permits to consume (integer in 1..count). Defaults to 1. | ||
| * @param onSettle Optional hook reporting how long fn() itself took (not | ||
| * queue-wait time) and whether it resolved or rejected. Never called if the | ||
| * acquire itself rejects (no permit means fn() never runs). A throwing hook | ||
| * is caught and logged via console.warn — it never masks fn()'s own result. | ||
| */ | ||
| use<T>(fn: () => Promise<T>, abortSignal?: AbortSignal, priority?: number, weight?: number): Promise<T>; | ||
| use<T>(fn: () => Promise<T>, abortSignal?: AbortSignal, priority?: number, weight?: number, onSettle?: (durationMs: number, outcome: OperationOutcome) => void): Promise<T>; | ||
| /** | ||
@@ -432,2 +408,28 @@ * Resolves once the queue is empty and all permits have been returned. | ||
| declare class KeyedSemaphore { | ||
| #private; | ||
| constructor(count: number, config?: SemaphoreConfig); | ||
| /** The Semaphore for `key`, constructing it (with the registry's shared count/config) on first access. */ | ||
| forKey(key: ID): Semaphore; | ||
| /** Sugar for `forKey(key).use(fn, abortSignal, priority, weight)`. */ | ||
| use<T>(key: ID, fn: () => Promise<T>, abortSignal?: AbortSignal, priority?: number, weight?: number): Promise<T>; | ||
| /** True if `key` already has a constructed Semaphore (does not create one). */ | ||
| has(key: ID): boolean; | ||
| /** Number of keys with a live Semaphore. */ | ||
| get size(): number; | ||
| /** Keys with a live Semaphore, in first-access order. */ | ||
| keys(): IterableIterator<ID>; | ||
| /** | ||
| * Shuts down and forgets `key`'s Semaphore, releasing its purge-interval | ||
| * timer. A later forKey(key) constructs a fresh one. Returns false if `key` | ||
| * had no Semaphore. | ||
| */ | ||
| delete(key: ID): boolean; | ||
| /** | ||
| * Shuts down every key's Semaphore and empties the registry. | ||
| * Terminal — like Semaphore.shutdown(), there is no undo. | ||
| */ | ||
| shutdown(reason?: string): void; | ||
| } | ||
| declare class SemaphoreError extends Error { | ||
@@ -439,14 +441,4 @@ readonly code: SemaphoreErrorCode; | ||
| declare class SaturationCircuitBreaker implements CircuitBreakerStrategy { | ||
| #private; | ||
| state: CircuitState; | ||
| private openUntil; | ||
| private probeInFlight; | ||
| private _probeTaskId; | ||
| private readonly eventWindow; | ||
| private readonly threshold; | ||
| private readonly window; | ||
| private readonly windowBucketWidth; | ||
| private readonly windowBucketCount; | ||
| private readonly cooldown; | ||
| private readonly minThroughput; | ||
| private readonly minFailures; | ||
| constructor(config?: CircuitBreakerConfig); | ||
@@ -540,2 +532,2 @@ get isClosed(): boolean; | ||
| 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 }; | ||
| export { type CircuitBreakerConfig, type CircuitBreakerStrategy, type CircuitState, type CircuitTripResult, type Comparator, type ID, KeyedSemaphore, ManualCircuitBreaker, NoopCircuitBreaker, type OperationOutcome, type PeekQueueOptions, QUEUE_ORDERINGS, type QueueOrder, type QueuedTaskView, SaturationCircuitBreaker, Semaphore, type SemaphoreConfig, SemaphoreError, type SemaphoreErrorCode, type SemaphoreEventListener, type SemaphoreEventMap, type SemaphoreEventType, SemaphoreEvents, type SemaphoreMetricsSnapshot, type SemaphoreMetricsWindowSnapshot, type WindowOptions }; |
+1
-1
@@ -1,2 +0,2 @@ | ||
| var 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 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.cachedIndex=-1;this.cachedUntil=0;this.size=e,this.stepMs=t,this.buckets=new Int32Array(e*2),this.timestamps=new Float64Array(e);}idx(e){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(e=Date.now()){this.buckets[this.idx(e)*2]++;}addTimeout(){this.buckets[this.idx(Date.now())*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);}},v=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.windowBucketCount=Math.ceil(this.window/this.windowBucketWidth),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.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");if(this.windowBucketCount<2)throw new a("SaturationCircuitBreaker window must span at least 2 windowBucketWidth buckets","INVALID_ARGUMENT");this.eventWindow=new A(this.windowBucketCount,this.windowBucketWidth);}get isClosed(){return this.state==="closed"}get isOpen(){return this.state==="open"}get isProbing(){return this.state==="probing"}get hasProbeInFlight(){return this.probeInFlight}get probeTaskId(){return this._probeTaskId}get cooldownRemaining(){return this.isOpen?Math.max(0,this.openUntil-Date.now()):0}trackAttempt(e){this.state==="closed"&&this.eventWindow.addAcquired(e);}recordFailure(){this.eventWindow.addTimeout();}checkAndTransition(){return this.state!=="open"||Date.now()<this.openUntil?false:(this.state="probing",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 y=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(){return this._decayedDelay(Date.now())}_decayedDelay(e){if(this.delay===0)return 0;let t=(e-this.lastTimestamp)/1e3;if(t<=0)return this.delay;let i=this.delay*Math.pow(this.decayFactor,t);return i<1?0:i}onTimeout(e=Date.now()){let t=this._decayedDelay(e),i=t>0?t*2:this.initialTimeout;this.delay=Math.min(i,this.maxTimeout),this.lastTimestamp=e;}reset(){this.delay=0,this.lastTimestamp=0;}};var S=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 g=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]}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 k=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 _=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,m=0,p=0,b=0,f=0,w=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]),m+=this.qSum[h],p+=this.qCount[h],this.qMax[h]>b&&(b=this.qMax[h]),f+=this.latSum[h],w+=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:m/p,max:b,samples:p},latency:{avg:w===0?0:f/w,count:w,total:f}}}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}],I=class{constructor(e=R){this._totalAcquiredFast=0;this._totalAcquiredQueued=0;this._totalReleased=0;this._totalTimeouts=0;this._totalPurged=0;this._totalAborts=0;this._capacity=0;this._circuitOpen=false;this._circuitProbing=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 _(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);}onPurge(e,t){this._totalPurged++;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._circuitProbing=false;}markCircuitProbing(){this._circuitOpen=false,this._circuitProbing=true;}markCircuitClose(){this._circuitOpen=false,this._circuitProbing=false;}getSnapshot(e=Date.now()){let 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,totalPurged:this._totalPurged,totalAborts:this._totalAborts,capacity:this._capacity,circuitOpen:this._circuitOpen,circuitProbing:this._circuitProbing}}}reset(){for(let e of this.windows)e.reset();this._totalAcquiredFast=0,this._totalAcquiredQueued=0,this._totalReleased=0,this._totalTimeouts=0,this._totalPurged=0,this._totalAborts=0,this._capacity=0,this._circuitOpen=false,this._circuitProbing=false;}destroy(){this.reset();}};var E={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[e];if(t===void 0)throw new a(`Semaphore queueOrder must be one of: ${Object.keys(E).join(", ")} (got ${JSON.stringify(e)})`,"INVALID_ARGUMENT");return t}function M(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",CIRCUITPROBING:"circuit-probing",CIRCUITCLOSE:"circuit-close",SHUTDOWN:"shutdown"};var P=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.totalPurged=0;this.totalEvictions=0;this.eventListeners=new Map;this.drainPromise=null;this.drainResolve=null;this.purgeIntervalId=null;this.timeoutTimerId=null;this._runSchedulerBound=()=>{this._runScheduler();};this.lastAdmissionWasProbe=false;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=M({queueOrder:t.queueOrder,comparator:t.comparator});this.queue=new g(i),this.enqueueOrder=new k,this.permits=new T(e),this.circuit=t.circuitBreaker??new v({threshold:t.circuitBreakerThreshold,window:t.circuitBreakerWindow,windowBucketWidth:t.circuitBreakerWindowBucketWidth,cooldown:t.circuitBreakerCooldown,minThroughput:t.circuitBreakerMinThroughput,minFailures:t.circuitBreakerMinFailures}),this.backoff=new y({initialTimeout:t.backoffInitialTimeout,maxTimeout:t.backoffMaxTimeout,decayFactor:t.backoffDecayFactor}),this.metricsCollector=this.metricsEnabled?new I(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){if(this.circuit.isProbing){if(this.circuit.hasProbeInFlight||!this.permits.hasCapacityFor(e))return null;if(this.circuit.markProbeInFlight(),this.permits.acquire(e),this.totalAcquired++,this.metricsCollector!==void 0){let t=Date.now();this.circuit.trackAttempt(t),this.metricsCollector.onAcquireFast(t,this.permits.inFlight,this.queue.size);}else this.circuit.trackAttempt();return 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)}if(this.queue.size>0||!this.permits.hasCapacityFor(e))return null;if(this.permits.acquire(e),this.totalAcquired++,this.metricsCollector!==void 0){let t=Date.now();this.circuit.trackAttempt(t),this.metricsCollector.onAcquireFast(t,this.permits.inFlight,this.queue.size);}else this.circuit.trackAttempt();return 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.isProbing&&(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.circuit.trackAttempt(),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,e),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());}_evictTask(e){let t=e.discard(new a("Circuit breaker opened while task was queued","CIRCUIT_OPEN"));return t&&(this.totalEvictions++,this.emit(n.QUEUEEVICT,{id:e.id,priority:e.priority,enqueueTime:e.enqueueTime,weight:e.weight}),this.debug&&console.warn(`[Semaphore] Evicted queued task #${e.id}: circuit opened`)),t}_evictQueueOnCircuitOpen(){if(this.queue.size===0)return;if(this.circuit.probeTaskId===null){let i=0;for(let r=this.enqueueOrder.peekHead();r!==void 0;r=r.next??void 0)this._evictTask(r)&&i++;this.queue.clear(),this.enqueueOrder.clear(),this._clearTimeout(),i>0&&this.metricsCollector?.sampleQueueDepthAt(Date.now(),this.queue.size);return}let e=0,t=this.enqueueOrder.peekHead();for(;t!==void 0;){let i=t.next??void 0;t.isProbe||(this._evictTask(t)&&e++,this._dequeue(t)),t=i;}e>0&&this.metricsCollector?.sampleQueueDepthAt(Date.now(),this.queue.size);}_timeoutTask(e,t){if(this.totalTimeouts++,this.circuit.recordFailure(),this.backoff.onTimeout(t),e.isProbe)this.circuit.handleProbeFailure(),this.emit(n.CIRCUITOPEN,{timeoutRate:1,recentTimeouts:1,total:1,reason:"probe-failed"}),this.metricsCollector?.markCircuitOpen(),this.debug&&console.warn("[Semaphore] Circuit re-opened: probe timed out");else {let i=this.circuit.evaluateAndTrip();i.tripped&&(this.emit(n.CIRCUITOPEN,{timeoutRate:i.timeoutRate,recentTimeouts:i.failures,total:i.attempts}),this.metricsCollector?.markCircuitOpen(),this.debug&&console.warn(`[Semaphore] Circuit opened. Rate: ${(i.timeoutRate*100).toFixed(1)}%`),this._evictQueueOnCircuitOpen());}this.metricsCollector?.onTimeout(t,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{let e=Date.now();for(;this.queue.size>0&&!this.circuit.isOpen;){let t=this.queue.peek();if(!t||this.circuit.isProbing&&t.id!==this.circuit.probeTaskId||!this.permits.hasCapacityFor(t.weight))break;let i=this.queue.pop();this.enqueueOrder.remove(i),this.enqueueOrder.size===0&&this._clearTimeout(),i.dispatch(()=>{let s=Math.max(0,e-i.enqueueTime);return this.permits.acquire(i.weight),this.totalAcquired++,this.metricsCollector?.onAcquireQueued(e,s,this.permits.inFlight,this.queue.size),this.permits.assertInvariant(this.debug),this._createRelease(i.isProbe,i.weight)})&&this.hasListeners(n.TASKACQUIRE)&&this.emit(n.TASKACQUIRE,{queued:this.queue.size,running:this.permits.capacity-this.permits.available,...i.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.totalPurged++,this.metricsCollector?.onPurge(e,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.CIRCUITPROBING),this.metricsCollector?.markCircuitProbing(),this.debug&&console.info("[Semaphore] Circuit entering probing"),this.schedule()),this.circuit.isOpen)?null:this._tryAcquireFast(e)}acquire(e,t=0,i=1){return this._acquire(e,t,i)}_acquire(e,t=0,i=1){if(this.lastAdmissionWasProbe=false,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.CIRCUITPROBING),this.metricsCollector?.markCircuitProbing(),this.debug&&console.info("[Semaphore] Circuit entering probing"),this.schedule()),this.circuit.isOpen)return Promise.reject(new a(`Circuit breaker open, retry in ${this.circuit.cooldownRemaining}ms`,"CIRCUIT_OPEN"));if(this.circuit.isProbing&&this.circuit.hasProbeInFlight)return Promise.reject(new a("Circuit breaker is probing, probe already in flight","CIRCUIT_PROBING"));if(e?.aborted)return Promise.reject(new a("Semaphore acquire aborted before start","ABORTED"));let r=this.circuit.isProbing,s=this._tryAcquireFast(i);if(s)return this.lastAdmissionWasProbe=r,Promise.resolve(s);let u=this.circuit.isProbing&&!this.circuit.hasProbeInFlight;return !u&&this.rejectOnFull?Promise.reject(new a("Semaphore at capacity (rejectOnFull)","QUEUE_FULL")):!u&&this.queue.size>=this.queueMaxLength?Promise.reject(new a(`Queue full (${this.queueMaxLength})`,"QUEUE_FULL")):(this.lastAdmissionWasProbe=u,new Promise((c,d)=>{let m=++this.taskIdCounter,p=Date.now(),b=u,f=new S({id:m,priority:b?Number.MIN_SAFE_INTEGER:t,enqueueTime:p,isProbe:b,resolve:c,reject:d,abortSignal:e,weight:i});f.arm(()=>this._onTaskAbort(f,d)),b&&this.circuit.claimProbeSlot(m),this.metricsCollector?.sampleQueueDepthAt(p,this.queue.size+1),this._enqueue(f),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=this._acquire(t,i,r),u=this.failurePredicate!==void 0&&this.lastAdmissionWasProbe,c=await s;try{let d=await e();return c(),d}catch(d){if(this.failurePredicate!==void 0){let m=false;try{m=this.failurePredicate(d)===!0;}catch(p){console.warn("[Semaphore] circuitBreakerFailurePredicate threw (rejection treated as non-matching):",p);}m&&(u&&this.circuit.isProbing?(this.circuit.recordFailure(),this.circuit.handleProbeFailure(),this.emit(n.CIRCUITOPEN,{timeoutRate:1,recentTimeouts:1,total:1,reason:"probe-failed"}),this.metricsCollector?.markCircuitOpen(),this.debug&&console.warn("[Semaphore] Circuit re-opened: probe operation failed")):this.reportFailure());}throw c(),d}}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=this.enqueueOrder.peekHead();t!==void 0;t=t.next??void 0)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.totalPurged=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=this.enqueueOrder.peekHead();t!==void 0;t=t.next??void 0)t.discard(new a(e,"SHUTDOWN"));this.queue.clear(),this.enqueueOrder.clear(),this.releaseGeneration++,this.pendingReleaseCount=0,this.permits.reset(),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=0;for(let t=this.enqueueOrder.peekHead();t!==void 0;t=t.next??void 0)t.isProbe&&this.circuit.releaseProbeSlot(),t.discard(new a("Semaphore acquire cancelled","CANCELLED")),e++;this.queue.clear(),this.enqueueOrder.clear(),this._clearTimeout(),this.metricsCollector?.sampleQueueDepthAt(Date.now(),this.queue.size),this.debug&&console.info(`[Semaphore] Cancelled ${e} 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=Date.now(),t=this.metricsCollector?.getSnapshot(e)??null,i=this.metricsCollector!==void 0?t?.windows?.[this.metricsCollector.primaryLabel]?.counts:void 0,r=(this.metricsCollector?.primaryWindowMs??6e4)/1e3,s=i?.acquired??0,u=i?.timeouts??0,c=this.enqueueOrder.peekHead(),d=c===void 0?0:Math.max(0,e-c.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,circuitProbing:this.circuit.isProbing,backoffDelay:Math.round(this.backoff.currentDelay),requestsPerSecond:+(s/r).toFixed(2),timeoutRate1m:s+u>0?+(u/(s+u)*100).toFixed(1):0,queueAge:d},lifetime:{totalAcquired:this.totalAcquired,totalReleased:this.totalReleased,totalTimeouts:this.totalTimeouts,totalPurged:this.totalPurged,totalEvictions:this.totalEvictions,circuitBreakerCooldownRemaining:this.circuit.cooldownRemaining},metrics:t}}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 F={tripped:false},q=class{constructor(){this.state="closed";this.isOpen=false;this.isProbing=false;this.hasProbeInFlight=false;this.probeTaskId=null;this.cooldownRemaining=0;}checkAndTransition(){return false}trackAttempt(){}recordFailure(){}evaluateAndTrip(){return F}markProbeInFlight(){}claimProbeSlot(){}releaseProbeSlot(){}handleProbeSuccess(){}handleProbeFailure(){}reset(){}};var O={tripped:false},C=class{constructor(){this.state="closed";this.hasProbeInFlight=false;this.probeTaskId=null;}get isOpen(){return this.state==="open"}get isProbing(){return false}get cooldownRemaining(){return 0}open(){this.state="open";}close(){this.state="closed";}checkAndTransition(){return false}trackAttempt(){}recordFailure(){}evaluateAndTrip(){return O}markProbeInFlight(){}claimProbeSlot(){}releaseProbeSlot(){}handleProbeSuccess(){}handleProbeFailure(){}reset(){this.state="closed";}};export{C as ManualCircuitBreaker,q as NoopCircuitBreaker,E as QUEUE_ORDERINGS,v as SaturationCircuitBreaker,P as Semaphore,a as SemaphoreError,n as SemaphoreEvents};//# sourceMappingURL=index.js.map | ||
| var o=class extends Error{constructor(e,t){super(e),this.name="SemaphoreError",this.code=t;}};var u=(n,e,t,i,r=true,s=true)=>{if(typeof n!="number"||Number.isNaN(n))throw new o(`${e} must be a valid number`,"INVALID_ARGUMENT");if(r&&!Number.isInteger(n))throw new o(`${e} must be an integer`,"INVALID_ARGUMENT");if(s?n<t:n<=t)throw new o(`${e} must be ${s?">=":">"} ${t}`,"INVALID_ARGUMENT");if(s?n>i:n>=i)throw new o(`${e} must be ${s?"<=":"<"} ${i}`,"INVALID_ARGUMENT");return n};var v=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 P=class{#e;#t;#s;#i;#r=-1;#o=0;constructor(e,t){this.#s=e,this.#i=t,this.#e=new Int32Array(e*2),this.#t=new Float64Array(e);}#a(e){if(e<this.#o&&e>=this.#o-this.#i)return this.#r;let t=Math.floor(e/this.#i)*this.#i,i=Math.floor(t/this.#i)%this.#s;return this.#t[i]!==t&&(this.#t[i]=t,this.#e[i*2]=0,this.#e[i*2+1]=0),this.#r=i,this.#o=t+this.#i,i}addAcquired(e=Date.now()){this.#e[this.#a(e)*2]++;}addTimeout(){this.#e[this.#a(Date.now())*2+1]++;}snapshot(){let e=Date.now(),t=Math.floor(e/this.#i)*this.#i-(this.#s-1)*this.#i,i=0,r=0;for(let s=0;s<this.#s;s++)this.#t[s]>=t&&(i+=this.#e[s*2],r+=this.#e[s*2+1]);return {acquired:i,timeouts:r}}reset(){this.#r=-1,this.#o=0,this.#e.fill(0),this.#t.fill(0);}},T=class{constructor(e={}){this.state="closed";this.#e=0;this.#t=false;this.#s=null;if(this.#r=u(e.threshold??.5,"SaturationCircuitBreaker threshold",0,1,false,false),this.#o=u(e.window??1e4,"SaturationCircuitBreaker window",1e3,Number.MAX_SAFE_INTEGER,true,true),this.#a=u(e.windowBucketWidth??1e3,"SaturationCircuitBreaker windowBucketWidth",1,Number.MAX_SAFE_INTEGER,true,true),this.#u=Math.ceil(this.#o/this.#a),this.#d=u(e.cooldown??5e3,"SaturationCircuitBreaker cooldown",1e3,Number.MAX_SAFE_INTEGER,true,true),this.#c=u(e.minThroughput??10,"SaturationCircuitBreaker minThroughput",1,Number.MAX_SAFE_INTEGER,true,true),this.#h=u(e.minFailures??5,"SaturationCircuitBreaker minFailures",1,Number.MAX_SAFE_INTEGER,true,true),this.#o<this.#a)throw new o("SaturationCircuitBreaker window must be >= windowBucketWidth","INVALID_ARGUMENT");if(this.#c<this.#h)throw new o("SaturationCircuitBreaker minThroughput must be >= minFailures","INVALID_ARGUMENT");if(this.#u<2)throw new o("SaturationCircuitBreaker window must span at least 2 windowBucketWidth buckets","INVALID_ARGUMENT");this.#i=new P(this.#u,this.#a);}#e;#t;#s;#i;#r;#o;#a;#u;#d;#c;#h;get isClosed(){return this.state==="closed"}get isOpen(){return this.state==="open"}get isProbing(){return this.state==="probing"}get hasProbeInFlight(){return this.#t}get probeTaskId(){return this.#s}get cooldownRemaining(){return this.isOpen?Math.max(0,this.#e-Date.now()):0}trackAttempt(e){this.state==="closed"&&this.#i.addAcquired(e);}recordFailure(){this.#i.addTimeout();}checkAndTransition(){return this.state!=="open"||Date.now()<this.#e?false:(this.state="probing",this.#t=false,this.#s=null,true)}evaluateAndTrip(){if(this.state!=="closed")return {tripped:false};let{timeouts:e,acquired:t}=this.#i.snapshot();if(t<this.#c||e<this.#h)return {tripped:false};let i=e/t;return i<this.#r?{tripped:false}:(this.state="open",this.#e=Date.now()+this.#d,{tripped:true,timeoutRate:i,failures:e,attempts:t})}markProbeInFlight(){this.#t=true;}claimProbeSlot(e){this.#t=true,this.#s=e;}releaseProbeSlot(){this.#t=false,this.#s=null;}handleProbeSuccess(){this.state="closed",this.#t=false,this.#s=null,this.#i.reset();}handleProbeFailure(){this.state="open",this.#t=false,this.#s=null,this.#e=Date.now()+this.#d;}reset(){this.state="closed",this.#e=0,this.#t=false,this.#s=null,this.#i.reset();}};var S=class{#e=0;#t=0;#s;#i;#r;constructor(e={}){if(this.#s=u(e.initialTimeout??50,"BackoffTracker initialTimeout",0,Number.MAX_SAFE_INTEGER,true,true),this.#i=u(e.maxTimeout??2e3,"BackoffTracker maxTimeout",0,Number.MAX_SAFE_INTEGER,true,true),this.#r=u(e.decayFactor??.5,"BackoffTracker decayFactor",0,1,false,false),this.#i<this.#s)throw new o("BackoffTracker maxTimeout must be >= initialTimeout","INVALID_ARGUMENT")}get currentDelay(){return this.#o(Date.now())}#o(e){if(this.#e===0)return 0;let t=(e-this.#t)/1e3;if(t<=0)return this.#e;let i=this.#e*Math.pow(this.#r,t);return i<1?0:i}onTimeout(e=Date.now()){let t=this.#o(e),i=t>0?t*2:this.#s;this.#e=Math.min(i,this.#i),this.#t=e;}reset(){this.#e=0,this.#t=0;}};var I=class{constructor(e){this.prev=null;this.next=null;this.heapIndex=-1;this.#e=false;this.id=e.id,this.priority=e.priority,this.enqueueTime=e.enqueueTime,this.isProbe=e.isProbe,this.weight=e.weight??1,this.#i=e.resolve,this.#r=e.reject,this.#s=e.abortSignal;}#e;#t;#s;#i;#r;arm(e){this.#s&&(this.#t=()=>{this.claim()&&e();},this.#s.addEventListener("abort",this.#t));}dispatch(e){return this.claim()?(this.#i(e()),true):false}discard(e){return this.claim()?(this.#r(e),true):false}claim(){return this.#e?false:(this.#e=true,this.#s&&this.#t&&this.#s.removeEventListener("abort",this.#t),true)}reject(e){this.#r(e);}};var g=class{#e=[];#t;constructor(e){this.#t=e;}get size(){return this.#e.length}isEmpty(){return this.#e.length===0}peek(){return this.#e[0]}has(e){return e.heapIndex>=0&&this.#e[e.heapIndex]===e}clear(){for(let e of this.#e)e.heapIndex=-1;this.#e=[];}insert(e){if(e.heapIndex>=0)throw new o("Item is already in a heap","INVALID_ARGUMENT");let t=this.#e.length;this.#e.push(e),e.heapIndex=t,this.#i(t);}pop(){if(this.#e.length===0)return;let e=this.#e[0];e.heapIndex=-1;let t=this.#e.pop();return this.#e.length>0&&t!==e&&(this.#e[0]=t,t.heapIndex=0,this.#r(0)),e}delete(e){let t=e.heapIndex;if(t<0||this.#e[t]!==e)return;e.heapIndex=-1;let i=this.#e.pop();if(t===this.#e.length||i===e)return e;this.#e[t]=i,i.heapIndex=t;let r=this.#e[t-1>>1];return t>0&&this.#t(i,r)<0?this.#i(t):this.#r(t),e}#s(e,t){let i=this.#e[e],r=this.#e[t];this.#e[e]=r,this.#e[t]=i,i.heapIndex=t,r.heapIndex=e;}#i(e){for(;e>0;){let t=e-1>>1;if(this.#t(this.#e[e],this.#e[t])<0)this.#s(e,t),e=t;else break}}#r(e){let t=this.#e.length;for(;;){let i=e,r=2*e+1,s=2*e+2;if(r<t&&this.#t(this.#e[r],this.#e[i])<0&&(i=r),s<t&&this.#t(this.#e[s],this.#e[i])<0&&(i=s),i===e)break;this.#s(e,i),e=i;}}};var k=class{#e=null;#t=null;#s=0;get size(){return this.#s}isEmpty(){return this.#s===0}peekHead(){return this.#e===null?void 0:this.#e}pushTail(e){e.prev=this.#t,e.next=null,this.#t!==null?this.#t.next=e:this.#e=e,this.#t=e,this.#s++;}remove(e){e.prev!==null?e.prev.next=e.next:this.#e=e.next,e.next!==null?e.next.prev=e.prev:this.#t=e.prev,e.prev=e.next=null,this.#s--;}clear(){this.#e=this.#t=null,this.#s=0;}};var R=class{#e;#t;#s;#i=-1;#r=0;#o;#a;#u;#d;#c;#h;#S;#n;#f;#y;#T;constructor(e,t){this.#e=u(e,"CombinedWindow size",1,Number.MAX_SAFE_INTEGER,true,true),this.#t=u(t,"CombinedWindow stepMs",1,Number.MAX_SAFE_INTEGER,true,true),this.#s=new Float64Array(e),this.#o=new Int32Array(e),this.#a=new Int32Array(e),this.#u=new Int32Array(e),this.#d=new Float64Array(e),this.#c=new Int32Array(e),this.#h=new Float64Array(e),this.#S=new Float64Array(e),this.#n=new Int32Array(e),this.#f=new Float64Array(e),this.#y=new Float64Array(e),this.#T=new Int32Array(e);}#m(e){if(e<this.#r&&e>=this.#r-this.#t)return this.#i;let t=Math.floor(e/this.#t),i=t*this.#t,r=t%this.#e;return this.#s[r]!==i&&(this.#s[r]=i,this.#o[r]=0,this.#a[r]=0,this.#u[r]=0,this.#d[r]=0,this.#c[r]=0,this.#h[r]=0,this.#S[r]=0,this.#n[r]=0,this.#f[r]=0,this.#y[r]=0,this.#T[r]=0),this.#i=r,this.#r=i+this.#t,r}#l(e,t){this.#d[e]+=t,this.#c[e]++,t>this.#h[e]&&(this.#h[e]=t);}#v(e,t){this.#S[e]+=t,this.#n[e]++,t>this.#f[e]&&(this.#f[e]=t);}recordAcquire(e,t,i){let r=this.#m(e);this.#o[r]++,this.#l(r,t),this.#v(r,i);}recordAcquireQueued(e,t,i,r){let s=this.#m(e);this.#o[s]++,this.#y[s]+=t,this.#T[s]++,this.#l(s,i),this.#v(s,r);}recordRelease(e,t,i){let r=this.#m(e);this.#a[r]++,this.#l(r,t),this.#v(r,i);}recordTimeoutQueue(e,t){let i=this.#m(e);this.#u[i]++,this.#v(i,t);}sampleBoth(e,t,i){let r=this.#m(e);this.#l(r,t),this.#v(r,i);}addAcquired(e){this.#o[this.#m(e)]++;}addReleased(e){this.#a[this.#m(e)]++;}addTimeout(e){this.#u[this.#m(e)]++;}addLatency(e,t){let i=this.#m(e);this.#y[i]+=t,this.#T[i]++;}sampleInflight(e,t){this.#l(this.#m(e),t);}sampleQueue(e,t){this.#v(this.#m(e),t);}snapshot(e){let t=Math.floor(e/this.#t)*this.#t-(this.#e-1)*this.#t,i=0,r=0,s=0,a=0,d=0,m=0,f=0,p=0,b=0,c=0,y=0;for(let l=0;l<this.#e;l++)this.#s[l]<t||(i+=this.#o[l],r+=this.#a[l],s+=this.#u[l],a+=this.#d[l],d+=this.#c[l],this.#h[l]>m&&(m=this.#h[l]),f+=this.#S[l],p+=this.#n[l],this.#f[l]>b&&(b=this.#f[l]),c+=this.#y[l],y+=this.#T[l]);return {counts:{acquired:i,released:r,timeouts:s},inflight:{avg:d===0?0:a/d,max:m,samples:d},queue:{avg:p===0?0:f/p,max:b,samples:p},latency:{avg:y===0?0:c/y,count:y,total:c}}}reset(){this.#i=-1,this.#r=0,this.#s.fill(0),this.#o.fill(0),this.#a.fill(0),this.#u.fill(0),this.#d.fill(0),this.#c.fill(0),this.#h.fill(0),this.#S.fill(0),this.#n.fill(0),this.#f.fill(0),this.#y.fill(0),this.#T.fill(0);}},x=[{size:60,stepMs:1e3},{size:60,stepMs:5e3},{size:60,stepMs:6e4},{size:60,stepMs:144e4}],E=class{#e;#t;#s=0;#i=0;#r=0;#o=0;#a=0;#u=0;#d=0;#c=false;#h=false;constructor(e=x){if(e.length===0)throw new o("SemaphoreMetrics requires at least one WindowOptions","INVALID_ARGUMENT");this.#e=e.map(({size:r,stepMs:s})=>new R(r,s)),this.#t=e.map(({size:r,stepMs:s})=>{let a=r*s;return a>=36e5&&a%36e5===0?`${a/36e5}h`:a>=6e4&&a%6e4===0?`${a/6e4}m`:a>=1e3&&a%1e3===0?`${a/1e3}s`:`${a}ms`});let t=new Set;for(let r of this.#t){if(t.has(r))throw new o(`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.#t[i],this.primaryWindowMs=e[i].size*e[i].stepMs;}onAcquireFast(e,t,i){this.#s++;for(let r of this.#e)r.recordAcquire(e,t,i);}onAcquireQueued(e,t,i,r){this.#i++;for(let s of this.#e)s.recordAcquireQueued(e,t,i,r);}onRelease(e,t,i){this.#r++;for(let r of this.#e)r.recordRelease(e,t,i);}onTimeout(e,t){this.#o++;for(let i of this.#e)i.recordTimeoutQueue(e,t);}onAbort(e,t){this.#u++;for(let i of this.#e)i.sampleQueue(e,t);}onPurge(e,t){this.#a++;for(let i of this.#e)i.sampleQueue(e,t);}sampleGauges(e,t,i){for(let r of this.#e)r.sampleBoth(e,t,i);}sampleQueueDepthAt(e,t){for(let i of this.#e)i.sampleQueue(e,t);}markAcquireFast(){this.#s++;let e=Date.now();for(let t of this.#e)t.addAcquired(e);}markAcquireQueued(e){this.#i++;let t=Date.now();for(let i of this.#e)i.addAcquired(t),i.addLatency(t,e);}markReleased(){this.#r++;let e=Date.now();for(let t of this.#e)t.addReleased(e);}markTimeout(){this.#o++;let e=Date.now();for(let t of this.#e)t.addTimeout(e);}markAbort(){this.#u++;}sampleInFlight(e){let t=Date.now();for(let i of this.#e)i.sampleInflight(t,e);}sampleQueueDepth(e){let t=Date.now();for(let i of this.#e)i.sampleQueue(t,e);}markCapacityChange(e){this.#d=e;}markCircuitOpen(){this.#c=true,this.#h=false;}markCircuitProbing(){this.#c=false,this.#h=true;}markCircuitClose(){this.#c=false,this.#h=false;}getSnapshot(e=Date.now()){let t={},i=0,r=0;for(let s=0;s<this.#t.length;s++){let a=this.#e[s].snapshot(e);t[this.#t[s]]=a,s===0&&(i=a.inflight.avg,r=a.queue.avg);}return {windows:t,meta:{inFlightLastMinute:Math.round(i),queueDepthLastMinute:Math.round(r),totalAcquiredFast:this.#s,totalAcquiredQueued:this.#i,totalReleased:this.#r,totalTimeouts:this.#o,totalPurged:this.#a,totalAborts:this.#u,capacity:this.#d,circuitOpen:this.#c,circuitProbing:this.#h}}}reset(){for(let e of this.#e)e.reset();this.#s=0,this.#i=0,this.#r=0,this.#o=0,this.#a=0,this.#u=0,this.#d=0,this.#c=false,this.#h=false;}destroy(){this.reset();}};var F={fifo:(n,e)=>n.id-e.id,lifo:(n,e)=>e.id-n.id,fifoWithPriority:(n,e)=>n.priority-e.priority||n.id-e.id,lifoWithPriority:(n,e)=>n.priority-e.priority||e.id-n.id};function q(n){if(n.comparator!==void 0){if(typeof n.comparator!="function")throw new o("Semaphore comparator must be a function","INVALID_ARGUMENT");return n.comparator}let e=n.queueOrder??"fifoWithPriority",t=F[e];if(t===void 0)throw new o(`Semaphore queueOrder must be one of: ${Object.keys(F).join(", ")} (got ${JSON.stringify(e)})`,"INVALID_ARGUMENT");return t}function N(n){let e=q(n);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 h={TASKACQUIRE:"task-acquire",TASKRELEASE:"task-release",TASKTIMEOUT:"task-timeout",TASKABORT:"task-abort",QUEUEPURGE:"queue-purge",QUEUEEVICT:"queue-evict",CIRCUITOPEN:"circuit-open",CIRCUITPROBING:"circuit-probing",CIRCUITCLOSE:"circuit-close",CIRCUITSTATECHANGE:"circuit-state-change",SHUTDOWN:"shutdown"};var w=class{#e;#t;#s;#i;#r;#o;#a;#u;#d;#c;#h;#S;#n;#f;#y=0;#T=0;#m=false;#l=false;#v=0;#R=0;#x=0;#q=0;#O=0;#D=0;#w=new Map;#I=null;#b=null;#k=null;#E=null;constructor(e,t={}){if(u(e,"Semaphore count",0,Number.MAX_SAFE_INTEGER,true,false),this.#h=u(t.purgeIntervalMs??3e3,"Semaphore purgeIntervalMs",500,Number.MAX_SAFE_INTEGER,true,true),this.#u=u(t.queueMaxTimeout??1e4,"Semaphore queueMaxTimeout",1,Number.MAX_SAFE_INTEGER,true,true),this.#a=u(t.queueMaxLength??1024,"Semaphore queueMaxLength",1,Number.MAX_SAFE_INTEGER,true,true),this.#d=u(t.queueMaxAge??3e4,"Semaphore queueMaxAge",1,Number.MAX_SAFE_INTEGER,true,true),this.#c=t.rejectOnFull??false,this.#S=t.metricsEnabled??true,this.#n=t.debug??false,t.circuitBreakerFailurePredicate!==void 0&&typeof t.circuitBreakerFailurePredicate!="function")throw new o("Semaphore circuitBreakerFailurePredicate must be a function","INVALID_ARGUMENT");this.#f=t.circuitBreakerFailurePredicate;let i=N({queueOrder:t.queueOrder,comparator:t.comparator});this.#t=new g(i),this.#s=new k,this.#i=new v(e),this.#r=t.circuitBreaker??new T({threshold:t.circuitBreakerThreshold,window:t.circuitBreakerWindow,windowBucketWidth:t.circuitBreakerWindowBucketWidth,cooldown:t.circuitBreakerCooldown,minThroughput:t.circuitBreakerMinThroughput,minFailures:t.circuitBreakerMinFailures}),this.#o=new S({initialTimeout:t.backoffInitialTimeout,maxTimeout:t.backoffMaxTimeout,decayFactor:t.backoffDecayFactor}),this.#e=this.#S?new E(t.metricsWindows):void 0,this.#e?.markCapacityChange(this.#i.capacity),this.#e?.sampleGauges(Date.now(),this.#i.inFlight,this.#t.size),this.#G();}on(e,t){this.#w.has(e)||this.#w.set(e,new Set),this.#w.get(e).add(t);}off(e,t){this.#w.get(e)?.delete(t),this.#w.get(e)?.size===0&&this.#w.delete(e);}removeAllListeners(e){e?this.#w.delete(e):this.#w.clear();}#F(e){let t=this.#w.get(e);return t!==void 0&&t.size>0}#p(e,...t){let i=this.#w.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);}}}#A(e,t){this.#p(h.CIRCUITSTATECHANGE,{from:e,to:t});}#U(e){if(this.#r.isProbing){if(this.#r.hasProbeInFlight||!this.#i.hasCapacityFor(e))return null;if(this.#r.markProbeInFlight(),this.#i.acquire(e),this.#R++,this.#e!==void 0){let t=Date.now();this.#r.trackAttempt(t),this.#e.onAcquireFast(t,this.#i.inFlight,this.#t.size);}else this.#r.trackAttempt();return this.#F(h.TASKACQUIRE)&&this.#p(h.TASKACQUIRE,{queued:this.#t.size,running:this.#i.capacity-this.#i.available,probe:true}),this.#_(true,e)}if(this.#t.size>0||!this.#i.hasCapacityFor(e))return null;if(this.#i.acquire(e),this.#R++,this.#e!==void 0){let t=Date.now();this.#r.trackAttempt(t),this.#e.onAcquireFast(t,this.#i.inFlight,this.#t.size);}else this.#r.trackAttempt();return this.#F(h.TASKACQUIRE)&&this.#p(h.TASKACQUIRE,{queued:this.#t.size,running:this.#i.capacity-this.#i.available}),this.#_(false,e)}#_(e=false,t=1){this.#y++;let i=this.#T,r=false;return ()=>{if(r||i!==this.#T){this.#n&&console.warn("[Semaphore] release() called after already released (no-op)");return}r=true,this.#y--,this.#i.release(t),this.#x++,this.#e?.onRelease(Date.now(),this.#i.inFlight,this.#t.size),e&&this.#r.isProbing&&(this.#r.handleProbeSuccess(),this.#p(h.CIRCUITCLOSE),this.#A("probing","closed"),this.#e?.markCircuitClose(),this.#n&&console.info("[Semaphore] Circuit closed after successful probe")),this.#i.assertInvariant(this.#n),this.#F(h.TASKRELEASE)&&this.#p(h.TASKRELEASE,{queued:this.#t.size,running:this.#i.capacity-this.#i.available,weight:t}),this.#C(),this.#g();}}#C(){this.#b&&this.#t.size===0&&this.#i.available===this.#i.capacity&&(this.#b(),this.#b=null,this.#I=null);}#$(e){this.#r.trackAttempt(),this.#t.insert(e),this.#s.pushTail(e),this.#V();}#M(e){this.#t.delete(e)!==void 0&&(this.#s.remove(e),this.#s.size===0&&this.#P());}#V(){if(this.#E!==null)return;let e=this.#s.peekHead();if(e===void 0)return;let t=Math.max(0,e.enqueueTime+this.#u-Date.now());this.#E=setTimeout(()=>this.#B(),t);}#P(){this.#E!==null&&(clearTimeout(this.#E),this.#E=null);}#B(){if(this.#E=null,this.#l)return;let e=Date.now(),t=0,i=this.#s.peekHead();for(;i!==void 0&&i.enqueueTime+this.#u<=e;){let r=i.claim();this.#M(i),r&&(this.#H(i,e),t++),i=this.#s.peekHead();}if(i!==void 0){let r=Math.max(0,i.enqueueTime+this.#u-e);this.#E=setTimeout(()=>this.#B(),r);}t>0&&(this.#C(),this.#g());}#Q(e){let t=e.discard(new o("Circuit breaker opened while task was queued","CIRCUIT_OPEN"));return t&&(this.#D++,this.#p(h.QUEUEEVICT,{id:e.id,priority:e.priority,enqueueTime:e.enqueueTime,weight:e.weight}),this.#n&&console.warn(`[Semaphore] Evicted queued task #${e.id}: circuit opened`)),t}#L(){if(this.#t.size===0)return;if(this.#r.probeTaskId===null){let i=0;for(let r=this.#s.peekHead();r!==void 0;r=r.next??void 0)this.#Q(r)&&i++;this.#t.clear(),this.#s.clear(),this.#P(),i>0&&this.#e?.sampleQueueDepthAt(Date.now(),this.#t.size);return}let e=0,t=this.#s.peekHead();for(;t!==void 0;){let i=t.next??void 0;t.isProbe||(this.#Q(t)&&e++,this.#M(t)),t=i;}e>0&&this.#e?.sampleQueueDepthAt(Date.now(),this.#t.size);}#H(e,t){if(this.#q++,this.#r.recordFailure(),this.#o.onTimeout(t),e.isProbe)this.#r.handleProbeFailure(),this.#p(h.CIRCUITOPEN,{timeoutRate:1,recentTimeouts:1,total:1,reason:"probe-failed"}),this.#A("probing","open"),this.#e?.markCircuitOpen(),this.#n&&console.warn("[Semaphore] Circuit re-opened: probe timed out");else {let i=this.#r.evaluateAndTrip();i.tripped&&(this.#p(h.CIRCUITOPEN,{timeoutRate:i.timeoutRate,recentTimeouts:i.failures,total:i.attempts}),this.#A("closed","open"),this.#e?.markCircuitOpen(),this.#n&&console.warn(`[Semaphore] Circuit opened. Rate: ${(i.timeoutRate*100).toFixed(1)}%`),this.#L());}this.#e?.onTimeout(t,this.#t.size),this.#p(h.TASKTIMEOUT,{queueLength:this.#t.size,backoffDelay:this.#o.currentDelay,taskId:e.id}),this.#n&&console.warn(`[Semaphore] Task #${e.id} timed out after ${this.#u}ms`),e.reject(new o(`Semaphore acquire timed out after ${this.#u}ms (queue: ${this.#t.size})`,"TIMEOUT"));}#j(e,t){this.#M(e),e.isProbe&&this.#r.releaseProbeSlot(),this.#e?.onAbort(Date.now(),this.#t.size),this.#p(h.TASKABORT),this.#n&&console.info(`[Semaphore] Task #${e.id} aborted`),t(new o("Semaphore acquire aborted","ABORTED")),this.#C(),this.#g();}#W=()=>{this.#K();};#g(){if(this.#m||this.#l)return;this.#m=true;let e=this.#o.currentDelay;e>0?setTimeout(this.#W,e).unref?.():queueMicrotask(this.#W);}#K(){if(!this.#l){this.#m=false;try{let e=Date.now();for(;this.#t.size>0&&!this.#r.isOpen;){let t=this.#t.peek();if(!t||this.#r.isProbing&&t.id!==this.#r.probeTaskId||!this.#i.hasCapacityFor(t.weight))break;let i=this.#t.pop();this.#s.remove(i),this.#s.size===0&&this.#P(),i.dispatch(()=>{let s=Math.max(0,e-i.enqueueTime);return this.#i.acquire(i.weight),this.#R++,this.#e?.onAcquireQueued(e,s,this.#i.inFlight,this.#t.size),this.#i.assertInvariant(this.#n),this.#_(i.isProbe,i.weight)})&&this.#F(h.TASKACQUIRE)&&this.#p(h.TASKACQUIRE,{queued:this.#t.size,running:this.#i.capacity-this.#i.available,...i.isProbe?{probe:!0}:{}});}this.#C();}catch(e){e instanceof Error?console.error("[Semaphore] Scheduler error:",e.message,e.stack):console.error("[Semaphore] Scheduler error:",e);}}}#G(){this.#k!==null&&clearInterval(this.#k),this.#k=setInterval(()=>{this.#l||this.#X();},this.#h),this.#k.unref?.();}#X(){let e=Date.now(),t=this.#t.size,i=this.#s.peekHead();for(;i!==void 0&&e-i.enqueueTime>this.#d;){this.#r.probeTaskId===i.id&&this.#r.releaseProbeSlot();let r=i.discard(new o(`Task purged after ${this.#d}ms`,"PURGED"));this.#M(i),r&&(this.#O++,this.#e?.onPurge(e,this.#t.size),this.#p(h.QUEUEPURGE,{id:i.id,priority:i.priority,enqueueTime:i.enqueueTime,weight:i.weight}),this.#n&&console.warn(`[Semaphore] Purged stale task #${i.id}`)),i=this.#s.peekHead();}this.#n&&this.#t.size<t&&console.info(`[Semaphore] Purged ${t-this.#t.size} stale tasks`),this.#t.size<t&&(this.#C(),this.#g());}tryAcquire(e=1){return this.#l||!Number.isInteger(e)||e<1||e>this.#i.capacity||(this.#r.checkAndTransition()&&(this.#p(h.CIRCUITPROBING),this.#A("open","probing"),this.#e?.markCircuitProbing(),this.#n&&console.info("[Semaphore] Circuit entering probing"),this.#g()),this.#r.isOpen)?null:this.#U(e)}acquire(e,t=0,i=1){return this.#z(e,t,i)}#N=false;#z(e,t=0,i=1){if(this.#N=false,this.#l)return Promise.reject(new o("Semaphore is shut down","SHUTDOWN"));if(!Number.isInteger(i)||i<1||i>this.#i.capacity)return Promise.reject(new o(`Invalid weight: ${i} (must be integer in 1..${this.#i.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.#r.checkAndTransition()&&(this.#p(h.CIRCUITPROBING),this.#A("open","probing"),this.#e?.markCircuitProbing(),this.#n&&console.info("[Semaphore] Circuit entering probing"),this.#g()),this.#r.isOpen)return Promise.reject(new o(`Circuit breaker open, retry in ${this.#r.cooldownRemaining}ms`,"CIRCUIT_OPEN"));if(this.#r.isProbing&&this.#r.hasProbeInFlight)return Promise.reject(new o("Circuit breaker is probing, probe already in flight","CIRCUIT_PROBING"));if(e?.aborted)return Promise.reject(new o("Semaphore acquire aborted before start","ABORTED"));let r=this.#r.isProbing,s=this.#U(i);if(s)return this.#N=r,Promise.resolve(s);let a=this.#r.isProbing&&!this.#r.hasProbeInFlight;return !a&&this.#c?Promise.reject(new o("Semaphore at capacity (rejectOnFull)","QUEUE_FULL")):!a&&this.#t.size>=this.#a?Promise.reject(new o(`Queue full (${this.#a})`,"QUEUE_FULL")):(this.#N=a,new Promise((d,m)=>{let f=++this.#v,p=Date.now(),b=a,c=new I({id:f,priority:b?Number.MIN_SAFE_INTEGER:t,enqueueTime:p,isProbe:b,resolve:d,reject:m,abortSignal:e,weight:i});c.arm(()=>this.#j(c,m)),b&&this.#r.claimProbeSlot(f),this.#e?.sampleQueueDepthAt(p,this.#t.size+1),this.#$(c),this.#g();}))}peekQueue(e={}){let t=e.offset!==void 0?u(e.offset,"peekQueue offset",0,Number.MAX_SAFE_INTEGER,true,true):0,i=e.limit!==void 0?u(e.limit,"peekQueue limit",0,Number.MAX_SAFE_INTEGER,true,true):Number.POSITIVE_INFINITY,r=[],s=this.#s.peekHead();for(let a=0;s!==void 0&&a<t;a++)s=s.next??void 0;for(;s!==void 0&&r.length<i;)r.push({id:s.id,priority:s.priority,enqueueTime:s.enqueueTime,weight:s.weight,isProbe:s.isProbe}),s=s.next??void 0;return r}async use(e,t,i=0,r=1,s){let a=this.#z(t,i,r),d=this.#f!==void 0&&this.#N,m=await a,f=s?Date.now():0;try{let p=await e(),b=s?Date.now()-f:0;if(m(),s)try{s(b,"success");}catch(c){console.warn("[Semaphore] onSettle threw:",c);}return p}catch(p){let b=s?Date.now()-f:0;if(this.#f!==void 0){let c=false;try{c=this.#f(p)===!0;}catch(y){console.warn("[Semaphore] circuitBreakerFailurePredicate threw (rejection treated as non-matching):",y);}c&&(d&&this.#r.isProbing?(this.#r.recordFailure(),this.#r.handleProbeFailure(),this.#p(h.CIRCUITOPEN,{timeoutRate:1,recentTimeouts:1,total:1,reason:"probe-failed"}),this.#A("probing","open"),this.#e?.markCircuitOpen(),this.#n&&console.warn("[Semaphore] Circuit re-opened: probe operation failed")):this.reportFailure());}if(m(),s)try{s(b,"error");}catch(c){console.warn("[Semaphore] onSettle threw:",c);}throw p}}drain(e){return this.#l?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.#I?this.#I:this.#t.size===0&&this.#i.available===this.#i.capacity?Promise.resolve():(this.#I=new Promise((t,i)=>{if(this.#b=t,e!==void 0){let r=setTimeout(()=>{this.#b=null,this.#I=null,i(new o(`drain() timed out after ${e}ms`,"TIMEOUT"));},e),s=this.#b;this.#b=()=>{clearTimeout(r),s();};}}),this.#I))}reset(e={}){if(this.#l)throw new o("Cannot reset a semaphore that has been shut down","SHUTDOWN");for(let t=this.#s.peekHead();t!==void 0;t=t.next??void 0)t.discard(new o("Semaphore reset","SHUTDOWN"));this.#t.clear(),this.#s.clear(),this.#P(),this.#e?.reset(),this.#i.reset(),this.#o.reset(),this.#r.reset(),this.#T++,this.#y=0,this.#m=false,this.#l=false,this.#v=0,this.#R=0,this.#x=0,this.#q=0,this.#O=0,this.#D=0,e.clearListeners&&this.#w.clear(),this.#b&&(this.#b(),this.#b=null,this.#I=null),this.#G(),this.#e?.markCapacityChange(this.#i.capacity),this.#e?.sampleGauges(Date.now(),this.#i.inFlight,this.#t.size),this.#n&&console.info("[Semaphore] Reset to initial state");}shutdown(e="Semaphore shutdown"){if(!this.#l){this.#l=true,this.#n&&console.info(`[Semaphore] Shutdown: ${e}`),this.#k!==null&&(clearInterval(this.#k),this.#k=null),this.#P();for(let t=this.#s.peekHead();t!==void 0;t=t.next??void 0)t.discard(new o(e,"SHUTDOWN"));this.#t.clear(),this.#s.clear(),this.#T++,this.#y=0,this.#i.reset(),this.#e?.destroy(),this.#e=void 0,this.#b&&(this.#b(),this.#b=null,this.#I=null),this.#p(h.SHUTDOWN,e);}}cancel(){if(this.#l)return;let e=0;for(let t=this.#s.peekHead();t!==void 0;t=t.next??void 0)t.isProbe&&this.#r.releaseProbeSlot(),t.discard(new o("Semaphore acquire cancelled","CANCELLED")),e++;this.#t.clear(),this.#s.clear(),this.#P(),this.#e?.sampleQueueDepthAt(Date.now(),this.#t.size),this.#n&&console.info(`[Semaphore] Cancelled ${e} queued tasks`),this.#C(),this.#g();}reportFailure(){if(this.#l)return;this.#r.recordFailure();let e=this.#r.evaluateAndTrip();e.tripped&&(this.#p(h.CIRCUITOPEN,{timeoutRate:e.timeoutRate,recentTimeouts:e.failures,total:e.attempts,reason:"reported-failure"}),this.#A("closed","open"),this.#e?.markCircuitOpen(),this.#n&&console.warn(`[Semaphore] Circuit opened via reportFailure(). Rate: ${(e.timeoutRate*100).toFixed(1)}%`),this.#L());}status(){let e=Date.now(),t=this.#e?.getSnapshot(e)??null,i=this.#e!==void 0?t?.windows?.[this.#e.primaryLabel]?.counts:void 0,r=(this.#e?.primaryWindowMs??6e4)/1e3,s=i?.acquired??0,a=i?.timeouts??0,d=this.#s.peekHead(),m=d===void 0?0:Math.max(0,e-d.enqueueTime);return {status:{running:this.#i.capacity-this.#i.available,queued:this.#t.size,available:this.#i.available,inFlight:this.#i.inFlight,pendingReleases:this.#y,circuitOpen:this.#r.isOpen,circuitProbing:this.#r.isProbing,backoffDelay:Math.round(this.#o.currentDelay),requestsPerSecond:+(s/r).toFixed(2),timeoutRate1m:s+a>0?+(a/(s+a)*100).toFixed(1):0,queueAge:m},lifetime:{totalAcquired:this.#R,totalReleased:this.#x,totalTimeouts:this.#q,totalPurged:this.#O,totalEvictions:this.#D,circuitBreakerCooldownRemaining:this.#r.cooldownRemaining},metrics:t}}get availablePermits(){return this.#i.available}isAvailable(){return !this.#l&&!this.#r.isOpen&&!this.#i.isFull}get capacity(){return this.#i.capacity}get circuitState(){return this.#r.state}get queueLength(){return this.#t.size}};var M=class{#e=new Map;#t;#s;constructor(e,t={}){this.#t=e,this.#s=t;}forKey(e){let t=this.#e.get(e);return t===void 0&&(t=new w(this.#t,this.#s),this.#e.set(e,t)),t}use(e,t,i,r=0,s=1){return this.forKey(e).use(t,i,r,s)}has(e){return this.#e.has(e)}get size(){return this.#e.size}keys(){return this.#e.keys()}delete(e){let t=this.#e.get(e);return t===void 0?false:(t.shutdown("KeyedSemaphore: key deleted"),this.#e.delete(e),true)}shutdown(e="KeyedSemaphore shutdown"){for(let t of this.#e.values())t.shutdown(e);this.#e.clear();}};var O={tripped:false},A=class{constructor(){this.state="closed";this.isOpen=false;this.isProbing=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 D={tripped:false},C=class{constructor(){this.state="closed";this.hasProbeInFlight=false;this.probeTaskId=null;}get isOpen(){return this.state==="open"}get isProbing(){return false}get cooldownRemaining(){return 0}open(){this.state="open";}close(){this.state="closed";}checkAndTransition(){return false}trackAttempt(){}recordFailure(){}evaluateAndTrip(){return D}markProbeInFlight(){}claimProbeSlot(){}releaseProbeSlot(){}handleProbeSuccess(){}handleProbeFailure(){}reset(){this.state="closed";}};export{M as KeyedSemaphore,C as ManualCircuitBreaker,A as NoopCircuitBreaker,F as QUEUE_ORDERINGS,T as SaturationCircuitBreaker,w as Semaphore,o as SemaphoreError,h as SemaphoreEvents};//# sourceMappingURL=index.js.map | ||
| //# sourceMappingURL=index.js.map |
+1
-1
| { | ||
| "name": "regulo", | ||
| "version": "1.4.0", | ||
| "version": "1.5.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", |
+119
-100
@@ -24,5 +24,5 @@ # **Regulo** | ||
| - **🌡️ Adaptive backoff** — during a timeout burst, dispatch eases down to a simmer and returns to a full boil on its own once things recover. | ||
| - **📈 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. | ||
| - **📈 Built-in observability** — windowed 1m/5m/1h/24h rollups (throughput, latency, queue depth, in-flight), lifetime counters, and an event stream, all through one `status()` call. | ||
| - **⏳ Head-of-line fairness** — once a caller is in line, nobody jumps the queue ahead of it. | ||
| - **🪶 Small footprint, no supply-chain surface** — a single ~34 KB file with zero runtime dependencies, so there's nothing transitive to audit, update, or trust. Tree-shakeable ESM. | ||
| - **🪶 Small footprint, no supply-chain surface** — a single ~30 KB file with zero runtime dependencies, so there's nothing transitive to audit, update, or trust. Tree-shakeable ESM. | ||
| - **🧯 Production-minded** — graceful `drain()`, `reset()`, `cancel()`, and `shutdown()`; stale-task purging; double-release safety; strict-mode TypeScript types. | ||
@@ -119,62 +119,6 @@ | ||
| ## Example: Express Middleware | ||
| ## Recipes | ||
| Cap concurrent handling of an expensive route and shed load with a `503` when the circuit is open or the queue is full: | ||
| For worked examples — Express middleware, priority lanes, weighted permits, breaker wiring, graceful shutdown, and more — see [**RECIPES.md**](./RECIPES.md). | ||
| ```ts | ||
| import { Semaphore, SemaphoreError, SemaphoreEvents } from 'regulo'; | ||
| import type { RequestHandler } from 'express'; | ||
| /* | ||
| Middleware | ||
| */ | ||
| export function limit(semaphore: Semaphore): RequestHandler { | ||
| return async (req, res, next) => { | ||
| let release: (() => void) | undefined; | ||
| try { | ||
| release = await semaphore.acquire(); | ||
| } catch (error) { | ||
| // CIRCUIT_OPEN | QUEUE_FULL | TIMEOUT all mean the same to a client: | ||
| // we're overloaded, come back later. No need to branch on error.code. | ||
| if (error instanceof SemaphoreError) { | ||
| res.setHeader('Retry-After', '5').sendStatus(503); | ||
| return; | ||
| } | ||
| return next(error); | ||
| } | ||
| // Hold the permit for the whole request; release however the response ends | ||
| // (success, error, or client disconnect). Regulo's release is idempotent. | ||
| res.once('close', release); | ||
| next(); | ||
| }; | ||
| } | ||
| /* | ||
| Usage | ||
| */ | ||
| const reports = new Semaphore(20, { queueMaxLength: 100, queueMaxTimeout: 2000 }); | ||
| app.get('/report', limit(reports), async (req, res) => { | ||
| res.json(await buildExpensiveReport(req.query)); | ||
| }); | ||
| /* | ||
| Metrics | ||
| */ | ||
| // Expose the limiter's state to your metrics endpoint. | ||
| app.get('/metrics/semaphore', (_req, res) => res.json(reports.status())); | ||
| /* | ||
| Event Hooks | ||
| */ | ||
| // Events fire once per state change for the whole limiter — the right place | ||
| // for logging / metrics / alerting, never for responding to a single request. | ||
| reports.on(SemaphoreEvents.CIRCUITOPEN, ({ timeoutRate }) => logger.warn(`reports limiter shedding load (timeout rate ${(timeoutRate * 100).toFixed(0)}%)`)); | ||
| reports.on(SemaphoreEvents.CIRCUITCLOSE, () => logger.info('reports limiter recovered')); | ||
| ``` | ||
| ## API Reference | ||
@@ -203,6 +147,16 @@ | ||
| #### `use<T>(fn, abortSignal?, priority?, weight?): Promise<T>` | ||
| #### `use<T>(fn, abortSignal?, priority?, weight?, onSettle?): Promise<T>` | ||
| 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 probe re-opens the circuit instead of closing it — see [Feeding downstream errors](#feeding-downstream-errors-reportfailure). | ||
| - `onSettle?: (durationMs: number, outcome: 'success' | 'error') => void` — reports how long `fn()` itself took (not queue-wait time) and whether it resolved or rejected. Never called if the acquire itself is rejected (no permit means `fn()` never ran). A throwing hook is caught and logged via `console.warn` — it never masks `fn()`'s own result. This is the hook to feed a per-operation latency histogram or SLO tracker; queue-wait latency is already in [`status().metrics`](#metrics). | ||
| ```ts | ||
| await semaphore.use( | ||
| () => callDownstream(), | ||
| undefined, 0, 1, | ||
| (durationMs, outcome) => histogram.observe({ outcome }, durationMs), | ||
| ); | ||
| ``` | ||
| #### `tryAcquire(weight?): (() => void) | null` | ||
@@ -246,6 +200,18 @@ | ||
| #### `peekQueue(): QueuedTaskView[]` | ||
| #### `peekQueue(options?): QueuedTaskView[]` | ||
| Read-only snapshot of the queue, in enqueue order. Entries additionally carry `isProbe`, so a circuit-breaker probe is identifiable in the view. | ||
| - `options.offset?: number` — skip this many queued tasks before collecting. Default `0`. | ||
| - `options.limit?: number` — collect at most this many tasks after the offset. Default unbounded. | ||
| For a very deep queue, an admin-debug endpoint should page through it instead of always materializing the full array: | ||
| ```ts | ||
| app.get('/admin/semaphore/queue', (req, res) => { | ||
| const offset = Number(req.query.offset ?? 0); | ||
| res.json(semaphore.peekQueue({ offset, limit: 50 })); | ||
| }); | ||
| ``` | ||
| #### `isAvailable(): boolean` | ||
@@ -271,2 +237,53 @@ | ||
| ## Keyed Semaphore | ||
| `KeyedSemaphore` is a lazily-populated registry of one `Semaphore` per key — the [one-`Semaphore`-per-resource](#caveats) pattern without hand-rolled `Map` bookkeeping. Every key shares the same `(count, config)`; the first access constructs that key's `Semaphore`, later accesses return the same instance. | ||
| ```ts | ||
| import { KeyedSemaphore } from 'regulo'; | ||
| import { S3 } from '@aws-sdk/client-s3'; | ||
| const buckets = new KeyedSemaphore(4); // 4 permits per bucket | ||
| const client = new S3({}); | ||
| const fetchFromBucket = (bucket: string, key: string) => | ||
| buckets.use(bucket, () => client.getObject({ Bucket: bucket, Key: key })); | ||
| await Promise.all([ | ||
| fetchFromBucket('bucket-a', 'file-1'), | ||
| fetchFromBucket('bucket-a', 'file-2'), | ||
| fetchFromBucket('bucket-b', 'file-1'), // its own 4-permit pool, own breaker | ||
| ]); | ||
| ``` | ||
| #### `new KeyedSemaphore(count, config?)` | ||
| Same arguments as `Semaphore`. Construction does not validate them — there is nothing to construct yet; the first `forKey()`/`use()` call constructs the underlying `Semaphore` and surfaces any `INVALID_ARGUMENT` then. | ||
| #### `forKey(key: ID): Semaphore` | ||
| Returns `key`'s `Semaphore`, constructing it (with the registry's shared `count`/`config`) on first access. `ID` is `string | number`. | ||
| #### `use<T>(key, fn, abortSignal?, priority?, weight?): Promise<T>` | ||
| Sugar for `forKey(key).use(fn, abortSignal, priority, weight)`. | ||
| #### `has(key): boolean` | ||
| `true` if `key` already has a constructed `Semaphore` — does not create one. | ||
| #### `delete(key): boolean` | ||
| Shuts down and forgets `key`'s `Semaphore`, releasing its purge-interval timer. A later `forKey(key)` constructs a fresh one. Returns `false` if `key` had no `Semaphore`. | ||
| #### `shutdown(reason?): void` | ||
| Shuts down every key's `Semaphore` and empties the registry. Terminal, like `Semaphore.shutdown()`. | ||
| #### `size: number` / `keys(): IterableIterator<ID>` | ||
| Number of keys with a live `Semaphore`, and an iterator over them. | ||
| > **Bounded key space only.** There's no TTL or eviction — a key's `Semaphore` (and its purge-interval timer) lives until `delete()`/`shutdown()`. Use `KeyedSemaphore` for a small, known set of keys (per-downstream, per-shard, per-tenant from a bounded list), not a high-cardinality or unbounded one (e.g. one key per end user) — that leaks a `Semaphore` per key. | ||
| ## Configuration Reference | ||
@@ -293,3 +310,3 @@ | ||
| | `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 | | ||
| | `metricsWindows` | `WindowOptions[]` | `undefined` (falls back to the built-in 1m/5m/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 | | ||
@@ -326,3 +343,3 @@ | `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) | | ||
| metricsEnabled: true, // windowed metrics collection | ||
| // metricsWindows: undefined, // override the default 1m/5m/15m/1h/24h windows | ||
| // metricsWindows: undefined, // override the default 1m/5m/1h/24h windows | ||
| debug: false, // debug logging + permit-pool invariant check | ||
@@ -344,3 +361,3 @@ // Ordering | ||
| | `TASKACQUIRE` | `'task-acquire'` | `{ queued, running, probe? }` | | ||
| | `TASKRELEASE` | `'task-release'` | `{ queued, running }` | | ||
| | `TASKRELEASE` | `'task-release'` | `{ queued, running, weight }` — `weight` is the permit count this release returned | | ||
| | `TASKTIMEOUT` | `'task-timeout'` | `{ queueLength, backoffDelay, taskId }` | | ||
@@ -353,2 +370,3 @@ | `TASKABORT` | `'task-abort'` | none | | ||
| | `CIRCUITCLOSE` | `'circuit-close'` | none | | ||
| | `CIRCUITSTATECHANGE` | `'circuit-state-change'` | `{ from, to }` — fires alongside every `CIRCUITOPEN`/`CIRCUITPROBING`/`CIRCUITCLOSE`, so one handler can sync breaker state to a dashboard instead of wiring up all three | | ||
| | `SHUTDOWN` | `'shutdown'` | `reason: string` | | ||
@@ -551,6 +569,6 @@ | ||
| |---|--:|---| | ||
| | `tryAcquire` + `release` (with metrics) | 3.03M | 1.76x slower | | ||
| | `tryAcquire` + `release` | 5.35M | fastest | | ||
| | `use()` round-trip | 1.16M | 4.61x slower | | ||
| | `use()` round-trip (no metrics) | 1.49M | 3.58x slower | | ||
| | `tryAcquire` + `release` (with metrics) | 2.81M | 1.91x slower | | ||
| | `tryAcquire` + `release` | 5.37M | 1.0x fastest (baseline) | | ||
| | `use()` round-trip | 1.04M | 5.16x slower | | ||
| | `use()` round-trip (no metrics) | 1.34M | 4.01x slower | | ||
@@ -561,5 +579,5 @@ **🎛️ Weighted acquire, uncontended** | ||
| |---|--:|---| | ||
| | `use()` weight=1 | 1.20M | fastest | | ||
| | `use()` weight=4 | 1.18M | 1.01x slower | | ||
| | `use()` weight=16 | 1.14M | 1.05x slower | | ||
| | `use()` weight=1 | 1.13M | 1.01x slower | | ||
| | `use()` weight=4 | 1.14M | 1.0x fastest (baseline) | | ||
| | `use()` weight=16 | 1.08M | 1.06x slower | | ||
@@ -573,6 +591,6 @@ Weighted permits add no meaningful overhead regardless of weight — claiming | ||
| |---|--:|---| | ||
| | concurrency=4 | 845.1k | 1.09x slower | | ||
| | concurrency=16 | 854.8k | 1.08x slower | | ||
| | concurrency=64 | 919.6k | fastest | | ||
| | concurrency=16, random priority | 812.9k | 1.13x slower | | ||
| | concurrency=4 | 806.9k | 1.08x slower | | ||
| | concurrency=16 | 823.7k | 1.05x slower | | ||
| | concurrency=64 | 868.8k | 1.0x fastest (baseline) | | ||
| | concurrency=16, random priority | 727.5k | 1.19x slower | | ||
@@ -583,5 +601,5 @@ **📈 `status()` snapshot cost** | ||
| |---|--:|---| | ||
| | 0 | 725.6k | 1.03x slower | | ||
| | 100 | 747.4k | fastest | | ||
| | 1000 | 739.8k | 1.01x slower | | ||
| | 0 | 666.9k | 1.21x slower | | ||
| | 100 | 807.9k | fastest | | ||
| | 1000 | 801.0k | 1.01x slower | | ||
@@ -597,7 +615,7 @@ `status()` is O(1) in queue depth — the cost is flat across queue depths (within | ||
| |---|--:|---| | ||
| | cockatiel (bulkhead) | 4.17M | fastest | | ||
| | regulo | 1.48M | 2.81x slower | | ||
| | p-queue | 1.19M | 3.52x slower | | ||
| | p-limit | 1.17M | 3.56x slower | | ||
| | regulo (with metrics) | 1.14M | 3.64x slower | | ||
| | cockatiel (bulkhead) | 4.10M | 1.0x fastest (baseline) | | ||
| | regulo | 1.32M | 3.10x slower | | ||
| | regulo (with metrics) | 1.08M | 3.80x slower | | ||
| | p-queue | 1.21M | 3.38x slower | | ||
| | p-limit | 1.17M | 3.50x slower | | ||
@@ -608,7 +626,7 @@ **📊 regulo vs. other libraries — contended throughput @ concurrency=16** (tasks/sec) | ||
| |---|--:|---| | ||
| | cockatiel (bulkhead) | 1.73M | fastest | | ||
| | p-queue | 1.07M | 1.62x slower | | ||
| | regulo | 984.8k | 1.76x slower | | ||
| | regulo (with metrics) | 893.7k | 1.94x slower | | ||
| | p-limit | 877.1k | 1.98x slower | | ||
| | cockatiel (bulkhead) | 1.70M | 1.0x fastest (baseline) | | ||
| | p-queue | 1.10M | 1.54x slower | | ||
| | p-limit | 872.5k | 1.95x slower | | ||
| | regulo | 954.7k | 1.78x slower | | ||
| | regulo (with metrics) | 880.5k | 1.93x slower | | ||
@@ -619,7 +637,7 @@ **🛡️ Circuit breaker overhead — closed/healthy circuit** | ||
| |---|--:|---| | ||
| | regulo `ManualCircuitBreaker` | 5.18M | fastest | | ||
| | regulo `NoopCircuitBreaker` | 4.68M | 1.11x slower | | ||
| | regulo `SaturationCircuitBreaker` | 3.92M | 1.32x slower | | ||
| | cockatiel (circuitBreaker) | 2.79M | 1.86x slower | | ||
| | opossum | 1.61M | 3.21x slower | | ||
| | cockatiel (circuitBreaker) | 2.75M | 1.94x slower | | ||
| | opossum | 1.62M | 3.29x slower | | ||
| | regulo `ManualCircuitBreaker` | 5.33M | 1.0x fastest (baseline) | | ||
| | regulo `NoopCircuitBreaker` | 4.92M | 1.08x slower | | ||
| | regulo `SaturationCircuitBreaker` | 3.82M | 1.40x slower | | ||
@@ -642,3 +660,3 @@ The picture is consistent. Cockatiel's bulkhead is the fastest limiter — and | ||
| more expensive than the limiter itself: SSR renders, database queries, | ||
| downstream API calls, measured in milliseconds. Even at ~890k tasks/sec | ||
| downstream API calls, measured in milliseconds. Even at ~880k tasks/sec | ||
| under contention the per-task overhead is a few microseconds against operations | ||
@@ -665,7 +683,8 @@ thousands of times slower. If you only need a bare concurrency cap on cheap | ||
| ✓ test/breakers-passthrough.test.ts (4 tests) | ||
| ✓ test/keyed.test.ts (6 tests) | ||
| ✓ test/semaphore-edges.test.ts (30 tests) | ||
| ✓ test/semaphore.test.ts (111 tests) | ||
| ✓ test/semaphore.test.ts (120 tests) | ||
| Test Files 12 passed (12) | ||
| Tests 261 passed (261) | ||
| Test Files 13 passed (13) | ||
| Tests 276 passed (276) | ||
| ``` | ||
@@ -678,3 +697,3 @@ | ||
| ----------------|---------|----------|---------|---------| | ||
| All files | 99.7 | 97.54 | 100 | 100 | | ||
| All files | 99.72 | 97.67 | 100 | 100 | | ||
| ----------------|---------|----------|---------|---------| | ||
@@ -687,3 +706,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 [`SaturationCircuitBreaker`](#circuit-breakers) 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 ([`KeyedSemaphore`](#keyed-semaphore) instead, which enables a bounded set of per-esource keys), 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). | ||
@@ -690,0 +709,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
531134
4.93%695
2.81%742
-5.72%