+67
-13
@@ -8,2 +8,69 @@ # Changelog | ||
| ## [v1.1.0] - 2026-06-28 | ||
| ### Added | ||
| - **Typed event payloads.** `on()` and `off()` are now generic over the event | ||
| name, so a listener's argument is inferred from the event (no more `any`). | ||
| Exposed via the new `SemaphoreEventMap` and `SemaphoreEventListener<E>` types, | ||
| both exported from the package entry point. Emitted payloads are unchanged; | ||
| this is purely a compile-time improvement. | ||
| - **`INVALID_ARGUMENT` error code** for argument and configuration validation, | ||
| so these failures can be distinguished programmatically via `error.code` like | ||
| every other rejection. | ||
| ### Changed | ||
| - **Argument and configuration validation now throws `SemaphoreError`** (with | ||
| code `INVALID_ARGUMENT`) instead of a plain `Error`. This covers the | ||
| `Semaphore` constructor, the `CircuitBreaker`/`BackoffTracker` config checks, | ||
| the `queueOrder`/`comparator` checks, and an invalid `drain()` timeout. Because | ||
| `SemaphoreError extends Error`, existing `instanceof Error` and `toThrow()` | ||
| handling is unaffected. | ||
| - **`reset()` now throws `SemaphoreError('SHUTDOWN')` when called on a | ||
| shut-down instance** instead of silently reviving it. This makes `shutdown()` | ||
| genuinely terminal, matching its documented "cannot be reversed" contract. If | ||
| you previously relied on `reset()` to restart a shut-down semaphore, construct | ||
| a new instance instead. | ||
| ### Performance | ||
| - **`emit()` skips the defensive listener-array snapshot when a single listener | ||
| is registered** (the common case), invoking it directly. Multi-listener | ||
| emission still snapshots to stay safe against mid-emit mutation. | ||
| ### Internal | ||
| - Added a `branches: 85` coverage threshold (alongside the existing `lines: 90`) | ||
| so a branch-coverage regression is caught in CI. | ||
| ### Documentation | ||
| - Corrected several broken in-page anchor links and heading typos, refreshed the | ||
| events/error-code references for the typed events and `INVALID_ARGUMENT` code, | ||
| and clarified the `reset()`/`shutdown()`/`drain()` semantics. | ||
| - Refreshed all benchmark tables with a new run (Node v22.16.0, darwin x64). | ||
| [1.1.0]: https://github.com/greenstick/regulo/releases/tag/v1.1.0 | ||
| ## [v1.0.5] - 2026-06-26 | ||
| ### Performance | ||
| - **The queue-wait timeout is now driven by a single shared deadline timer** | ||
| instead of one `setTimeout`/`clearTimeout` per queued task. Because every task | ||
| shares `queueMaxTimeout` and the enqueue-ordered index is sorted by deadline, | ||
| the oldest task is always the next to expire, so one self-re-arming timer | ||
| suffices. Timeout precision and circuit-breaker trip timing are unchanged; the | ||
| removed per-task timer churn lifts contended throughput by roughly 15โ19% and | ||
| is what makes regulo viable for shorter-duration work (small DB pulls, cache | ||
| fills) rather than only millisecond-scale operations. | ||
| - **The priority heap's index is now intrusive.** Each task stores its own heap | ||
| slot (`heapIndex`) instead of the heap maintaining a separate `Map<id, index>`, | ||
| so every sift writes a plain property instead of a hashed map entry. This lifts | ||
| contended throughput by a further ~25โ30% (deep queues benefit most); with | ||
| metrics disabled, contended throughput now sits alongside `p-limit`/`p-queue`. | ||
| [1.0.5]: https://github.com/greenstick/regulo/releases/tag/v1.0.5 | ||
| ## [v1.0.4] - 2026-06-26 | ||
@@ -39,15 +106,2 @@ | ||
| from the head and stopping at the first task young enough to keep. | ||
| - **The queue-wait timeout is now driven by a single shared deadline timer** | ||
| instead of one `setTimeout`/`clearTimeout` per queued task. Because every task | ||
| shares `queueMaxTimeout` and the enqueue-ordered index is sorted by deadline, | ||
| the oldest task is always the next to expire, so one self-re-arming timer | ||
| suffices. Timeout precision and circuit-breaker trip timing are unchanged; the | ||
| removed per-task timer churn lifts contended throughput by roughly 15โ19% and | ||
| is what makes regulo viable for shorter-duration work (small DB pulls, cache | ||
| fills) rather than only millisecond-scale operations. | ||
| - **The priority heap's index is now intrusive.** Each task stores its own heap | ||
| slot (`heapIndex`) instead of the heap maintaining a separate `Map<id, index>`, | ||
| so every sift writes a plain property instead of a hashed map entry. This lifts | ||
| contended throughput by a further ~25โ30% (deep queues benefit most); with | ||
| metrics disabled, contended throughput now sits alongside `p-limit`/`p-queue`. | ||
@@ -54,0 +108,0 @@ ### Internal |
+1
-1
@@ -1,2 +0,2 @@ | ||
| "use strict";var k=Object.defineProperty;var x=Object.getOwnPropertyDescriptor;var F=Object.getOwnPropertyNames;var O=Object.prototype.hasOwnProperty;var R=(a,e)=>{for(var t in e)k(a,t,{get:e[t],enumerable:!0})},P=(a,e,t,i)=>{if(e&&typeof e=="object"||typeof e=="function")for(let r of F(e))!O.call(a,r)&&r!==t&&k(a,r,{get:()=>e[r],enumerable:!(i=x(e,r))||i.enumerable});return a};var N=a=>P(k({},"__esModule",{value:!0}),a);var Q={};R(Q,{CircuitBreaker:()=>b,QUEUE_ORDERINGS:()=>I,Semaphore:()=>A,SemaphoreError:()=>o,SemaphoreEvents:()=>u});module.exports=N(Q);var n=(a,e,t,i,r=!0,s=!0)=>{if(typeof a!="number"||Number.isNaN(a))throw new Error(`${e} must be a valid number`);if(r&&!Number.isInteger(a))throw new Error(`${e} must be an integer`);if(s?a<t:a<=t)throw new Error(`${e} must be ${s?">=":">"} ${t}`);if(s?a>i:a>=i)throw new Error(`${e} must be ${s?"<=":"<"} ${i}`);return a};var o=class extends Error{constructor(e,t){super(e),this.name="SemaphoreError",this.code=t}};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 _=class{constructor(e,t){this.size=e,this.stepMs=t,this.buckets=new Int32Array(e*2),this.timestamps=new Float64Array(e)}idx(){let e=Date.now(),t=Math.floor(e/this.stepMs)*this.stepMs,i=Math.floor(t/this.stepMs)%this.size;return this.timestamps[i]!==t&&(this.timestamps[i]=t,this.buckets[i*2]=0,this.buckets[i*2+1]=0),i}addAcquired(){this.buckets[this.idx()*2]++}addTimeout(){this.buckets[this.idx()*2+1]++}snapshot(){let e=Date.now(),t=Math.floor(e/this.stepMs)*this.stepMs-(this.size-1)*this.stepMs,i=0,r=0;for(let s=0;s<this.size;s++)this.timestamps[s]>=t&&(i+=this.buckets[s*2],r+=this.buckets[s*2+1]);return{acquired:i,timeouts:r}}reset(){this.buckets.fill(0),this.timestamps.fill(0)}},b=class{constructor(e={}){this.state="closed";this.openUntil=0;this.probeInFlight=!1;this._probeTaskId=null;if(this.threshold=n(e.threshold??.5,"CircuitBreaker threshold",0,1,!1,!1),this.window=n(e.window??1e4,"CircuitBreaker window",1e3,Number.MAX_SAFE_INTEGER,!0,!0),this.cooldown=n(e.cooldown??5e3,"CircuitBreaker cooldown",1e3,Number.MAX_SAFE_INTEGER,!0,!0),this.minThroughput=n(e.minThroughput??10,"CircuitBreaker minThroughput",1,Number.MAX_SAFE_INTEGER,!0,!0),this.minFailures=n(e.minFailures??5,"CircuitBreaker minFailures",1,Number.MAX_SAFE_INTEGER,!0,!0),this.eventWindow=new _(Math.ceil(this.window/1e3),1e3),this.minThroughput<this.minFailures)throw new Error("CircuitBreaker minThroughput must be >= minFailures")}get isClosed(){return this.state==="closed"}get isOpen(){return this.state==="open"}get isHalfOpen(){return this.state==="half-open"}get hasProbeInFlight(){return this.probeInFlight}get probeTaskId(){return this._probeTaskId}get cooldownRemaining(){return this.isOpen?Math.max(0,this.openUntil-Date.now()):0}trackAttempt(){this.state==="closed"&&this.eventWindow.addAcquired()}recordTimeout(){this.eventWindow.addTimeout()}checkAndTransition(){return this.state!=="open"||Date.now()<this.openUntil?!1:(this.state="half-open",this.probeInFlight=!1,this._probeTaskId=null,!0)}evaluateAndTrip(){if(this.state!=="closed")return{tripped:!1};let{timeouts:e,acquired:t}=this.eventWindow.snapshot();if(t<this.minThroughput||e<this.minFailures)return{tripped:!1};let i=e/t;return i<this.threshold?{tripped:!1}:(this.state="open",this.openUntil=Date.now()+this.cooldown,{tripped:!0,timeoutRate:i,failures:e,attempts:t})}markProbeInFlight(){this.probeInFlight=!0}claimProbeSlot(e){this.probeInFlight=!0,this._probeTaskId=e}releaseProbeSlot(){this.probeInFlight=!1,this._probeTaskId=null}handleProbeSuccess(){this.state="closed",this.probeInFlight=!1,this._probeTaskId=null,this.eventWindow.reset()}handleProbeFailure(){this.state="open",this.probeInFlight=!1,this._probeTaskId=null,this.openUntil=Date.now()+this.cooldown}reset(){this.state="closed",this.openUntil=0,this.probeInFlight=!1,this._probeTaskId=null,this.eventWindow.reset()}};var w=class{constructor(e={}){this.delay=0;this.lastTimestamp=0;if(this.initialTimeout=n(e.initialTimeout??50,"BackoffTracker initialTimeout",0,Number.MAX_SAFE_INTEGER,!0,!0),this.maxTimeout=n(e.maxTimeout??2e3,"BackoffTracker maxTimeout",0,Number.MAX_SAFE_INTEGER,!0,!0),this.decayFactor=n(e.decayFactor??.5,"BackoffTracker decayFactor",0,1,!1,!1),this.maxTimeout<this.initialTimeout)throw new Error("BackoffTracker maxTimeout must be >= initialTimeout")}get currentDelay(){if(this.delay===0)return 0;let e=(Date.now()-this.lastTimestamp)/1e3;if(e<=0)return this.delay;let t=this.delay*Math.pow(this.decayFactor,e);return t<1?0:t}onTimeout(){let e=this.currentDelay,t=e>0?e*2:this.initialTimeout;this.delay=Math.min(t,this.maxTimeout),this.lastTimestamp=Date.now()}reset(){this.delay=0,this.lastTimestamp=0}};var y=class{constructor(e){this.prev=null;this.next=null;this.heapIndex=-1;this.completed=!1;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()),!0):!1}discard(e){return this.claim()?(this._reject(e),!0):!1}claim(){return this.completed?!1:(this.completed=!0,this.abortSignal&&this.abortListener&&this.abortSignal.removeEventListener("abort",this.abortListener),!0)}reject(e){this._reject(e)}};var q=class{constructor(e){this.heap=[];this.compare=e}get size(){return this.heap.length}isEmpty(){return this.heap.length===0}peek(){return this.heap[0]}toArray(){return[...this.heap]}has(e){return e.heapIndex>=0&&this.heap[e.heapIndex]===e}clear(){for(let e of this.heap)e.heapIndex=-1;this.heap=[]}insert(e){if(e.heapIndex>=0)throw new Error("Item is already in a heap");let t=this.heap.length;this.heap.push(e),e.heapIndex=t,this._bubbleUp(t)}pop(){if(this.heap.length===0)return;let e=this.heap[0];e.heapIndex=-1;let t=this.heap.pop();return this.heap.length>0&&t!==e&&(this.heap[0]=t,t.heapIndex=0,this._bubbleDown(0)),e}delete(e){let t=e.heapIndex;if(t<0||this.heap[t]!==e)return;e.heapIndex=-1;let i=this.heap.pop();if(t===this.heap.length||i===e)return e;this.heap[t]=i,i.heapIndex=t;let r=this.heap[t-1>>1];return t>0&&this.compare(i,r)<0?this._bubbleUp(t):this._bubbleDown(t),e}_swap(e,t){let i=this.heap[e],r=this.heap[t];this.heap[e]=r,this.heap[t]=i,i.heapIndex=t,r.heapIndex=e}_bubbleUp(e){for(;e>0;){let t=e-1>>1;if(this.compare(this.heap[e],this.heap[t])<0)this._swap(e,t),e=t;else break}}_bubbleDown(e){let t=this.heap.length;for(;;){let i=e,r=2*e+1,s=2*e+2;if(r<t&&this.compare(this.heap[r],this.heap[i])<0&&(i=r),s<t&&this.compare(this.heap[s],this.heap[i])<0&&(i=s),i===e)break;this._swap(e,i),e=i}}};var S=class{constructor(){this.head=null;this.tail=null;this._size=0}get size(){return this._size}isEmpty(){return this._size===0}peekHead(){return this.head===null?void 0:this.head}pushTail(e){e.prev=this.tail,e.next=null,this.tail!==null?this.tail.next=e:this.head=e,this.tail=e,this._size++}remove(e){e.prev!==null?e.prev.next=e.next:this.head=e.next,e.next!==null?e.next.prev=e.prev:this.tail=e.prev,e.prev=e.next=null,this._size--}clear(){this.head=this.tail=null,this._size=0}};var E=class{constructor(e,t){if(e<=0)throw new Error("CombinedWindow size must be > 0");this.size=e,this.stepMs=t,this.timestamps=new Float64Array(e),this.acquired=new Int32Array(e),this.released=new Int32Array(e),this.timeouts=new Int32Array(e),this.ifSum=new Float64Array(e),this.ifCount=new Int32Array(e),this.ifMax=new Float64Array(e),this.qSum=new Float64Array(e),this.qCount=new Int32Array(e),this.qMax=new Float64Array(e),this.latSum=new Float64Array(e),this.latCount=new Int32Array(e)}bucket(e){let t=Math.floor(e/this.stepMs),i=t*this.stepMs,r=t%this.size;return this.timestamps[r]!==i&&(this.timestamps[r]=i,this.acquired[r]=0,this.released[r]=0,this.timeouts[r]=0,this.ifSum[r]=0,this.ifCount[r]=0,this.ifMax[r]=0,this.qSum[r]=0,this.qCount[r]=0,this.qMax[r]=0,this.latSum[r]=0,this.latCount[r]=0),r}addInflight(e,t){this.ifSum[e]+=t,this.ifCount[e]++,t>this.ifMax[e]&&(this.ifMax[e]=t)}addQueue(e,t){this.qSum[e]+=t,this.qCount[e]++,t>this.qMax[e]&&(this.qMax[e]=t)}recordAcquire(e,t,i){let r=this.bucket(e);this.acquired[r]++,this.addInflight(r,t),this.addQueue(r,i)}recordAcquireQueued(e,t,i,r){let s=this.bucket(e);this.acquired[s]++,this.latSum[s]+=t,this.latCount[s]++,this.addInflight(s,i),this.addQueue(s,r)}recordRelease(e,t,i){let r=this.bucket(e);this.released[r]++,this.addInflight(r,t),this.addQueue(r,i)}recordTimeoutQueue(e,t){let i=this.bucket(e);this.timeouts[i]++,this.addQueue(i,t)}sampleBoth(e,t,i){let r=this.bucket(e);this.addInflight(r,t),this.addQueue(r,i)}addAcquired(e){this.acquired[this.bucket(e)]++}addReleased(e){this.released[this.bucket(e)]++}addTimeout(e){this.timeouts[this.bucket(e)]++}addLatency(e,t){let i=this.bucket(e);this.latSum[i]+=t,this.latCount[i]++}sampleInflight(e,t){this.addInflight(this.bucket(e),t)}sampleQueue(e,t){this.addQueue(this.bucket(e),t)}snapshot(e){let t=Math.floor(e/this.stepMs)*this.stepMs-(this.size-1)*this.stepMs,i=0,r=0,s=0,h=0,c=0,m=0,f=0,p=0,d=0,C=0,v=0;for(let l=0;l<this.size;l++)this.timestamps[l]<t||(i+=this.acquired[l],r+=this.released[l],s+=this.timeouts[l],h+=this.ifSum[l],c+=this.ifCount[l],this.ifMax[l]>m&&(m=this.ifMax[l]),f+=this.qSum[l],p+=this.qCount[l],this.qMax[l]>d&&(d=this.qMax[l]),C+=this.latSum[l],v+=this.latCount[l]);return{counts:{acquired:i,released:r,timeouts:s},inflight:{avg:c===0?0:h/c,max:m,samples:c},queue:{avg:p===0?0:f/p,max:d,samples:p},latency:{avg:v===0?0:C/v,count:v,total:C}}}reset(){this.timestamps.fill(0),this.acquired.fill(0),this.released.fill(0),this.timeouts.fill(0),this.ifSum.fill(0),this.ifCount.fill(0),this.ifMax.fill(0),this.qSum.fill(0),this.qCount.fill(0),this.qMax.fill(0),this.latSum.fill(0),this.latCount.fill(0)}},D=[{size:60,stepMs:1e3},{size:60,stepMs:5e3},{size:60,stepMs:15e3},{size:60,stepMs:6e4},{size:60,stepMs:144e4}],g=class{constructor(e=D){this._totalAcquiredFast=0;this._totalAcquiredQueued=0;this._totalReleased=0;this._totalTimeouts=0;this._totalAborts=0;this._capacity=0;this._circuitOpen=!1;this._circuitHalfOpen=!1;if(e.length===0)throw new Error("SemaphoreMetrics requires at least one WindowOptions");this.windows=e.map(({size:t,stepMs:i})=>new E(t,i)),this.windowLabels=e.map(({size:t,stepMs:i})=>{let r=t*i;return r>=36e5&&r%36e5===0?`${r/36e5}h`:r>=6e4&&r%6e4===0?`${r/6e4}m`:r>=1e3&&r%1e3===0?`${r/1e3}s`:`${r}ms`})}onAcquireFast(e,t,i){this._totalAcquiredFast++;for(let r of this.windows)r.recordAcquire(e,t,i)}onAcquireQueued(e,t,i,r){this._totalAcquiredQueued++;for(let s of this.windows)s.recordAcquireQueued(e,t,i,r)}onRelease(e,t,i){this._totalReleased++;for(let r of this.windows)r.recordRelease(e,t,i)}onTimeout(e,t){this._totalTimeouts++;for(let i of this.windows)i.recordTimeoutQueue(e,t)}onAbort(e,t){this._totalAborts++;for(let i of this.windows)i.sampleQueue(e,t)}sampleGauges(e,t,i){for(let r of this.windows)r.sampleBoth(e,t,i)}sampleQueueDepthAt(e,t){for(let i of this.windows)i.sampleQueue(e,t)}markAcquireFast(){this._totalAcquiredFast++;let e=Date.now();for(let t of this.windows)t.addAcquired(e)}markAcquireQueued(e){this._totalAcquiredQueued++;let t=Date.now();for(let i of this.windows)i.addAcquired(t),i.addLatency(t,e)}markReleased(){this._totalReleased++;let e=Date.now();for(let t of this.windows)t.addReleased(e)}markTimeout(){this._totalTimeouts++;let e=Date.now();for(let t of this.windows)t.addTimeout(e)}markAbort(){this._totalAborts++}sampleInFlight(e){let t=Date.now();for(let i of this.windows)i.sampleInflight(t,e)}sampleQueueDepth(e){let t=Date.now();for(let i of this.windows)i.sampleQueue(t,e)}markCapacityChange(e){this._capacity=e}markCircuitOpen(){this._circuitOpen=!0,this._circuitHalfOpen=!1}markCircuitHalfOpen(){this._circuitOpen=!1,this._circuitHalfOpen=!0}markCircuitClose(){this._circuitOpen=!1,this._circuitHalfOpen=!1}getSnapshot(){let e=Date.now(),t={},i=0,r=0;for(let s=0;s<this.windowLabels.length;s++){let h=this.windows[s].snapshot(e);t[this.windowLabels[s]]=h,s===0&&(i=h.inflight.avg,r=h.queue.avg)}return{windows:t,meta:{inFlightLastMinute:Math.round(i),queueDepthLastMinute:Math.round(r),totalAcquiredFast:this._totalAcquiredFast,totalAcquiredQueued:this._totalAcquiredQueued,totalReleased:this._totalReleased,totalTimeouts:this._totalTimeouts,totalAborts:this._totalAborts,capacity:this._capacity,circuitOpen:this._circuitOpen,circuitHalfOpen:this._circuitHalfOpen}}}reset(){for(let e of this.windows)e.reset();this._totalAcquiredFast=0,this._totalAcquiredQueued=0,this._totalReleased=0,this._totalTimeouts=0,this._totalAborts=0,this._capacity=0,this._circuitOpen=!1,this._circuitHalfOpen=!1}destroy(){this.reset()}};var I={fifo:(a,e)=>a.id-e.id,lifo:(a,e)=>e.id-a.id,fifoWithPriority:(a,e)=>a.priority-e.priority||a.id-e.id,lifoWithPriority:(a,e)=>a.priority-e.priority||e.id-a.id};function L(a){if(a.comparator!==void 0){if(typeof a.comparator!="function")throw new Error("Semaphore comparator must be a function");return a.comparator}let e=a.queueOrder??"fifoWithPriority",t=I[e];if(t===void 0)throw new Error(`Semaphore queueOrder must be one of: ${Object.keys(I).join(", ")} (got ${JSON.stringify(e)})`);return t}function M(a){let e=L(a);return(t,i)=>{if(t.isProbe!==i.isProbe)return t.isProbe?-1:1;let r=e(t,i);return typeof r!="number"||Number.isNaN(r)?t.id-i.id:r}}var u={TASKACQUIRE:"task-acquire",TASKRELEASE:"task-release",TASKTIMEOUT:"task-timeout",TASKABORT:"task-abort",QUEUEPURGE:"queue-purge",CIRCUITOPEN:"circuit-open",CIRCUITHALFOPEN:"circuit-half-open",CIRCUITCLOSE:"circuit-close",SHUTDOWN:"shutdown"};var A=class{constructor(e,t={}){this.pendingReleaseCount=0;this.releaseGeneration=0;this.scheduled=!1;this.isShutdown=!1;this.taskIdCounter=0;this.totalAcquired=0;this.totalReleased=0;this.totalTimeouts=0;this.eventListeners=new Map;this.drainPromise=null;this.drainResolve=null;this.purgeIntervalId=null;this.timeoutTimerId=null;n(e,"Semaphore count",0,Number.MAX_SAFE_INTEGER,!0,!1),this.purgeIntervalMs=n(t.purgeIntervalMs??3e3,"Semaphore purgeIntervalMs",500,Number.MAX_SAFE_INTEGER,!0,!0),this.queueMaxTimeout=n(t.queueMaxTimeout??1e4,"Semaphore queueMaxTimeout",1,Number.MAX_SAFE_INTEGER,!0,!0),this.queueMaxLength=n(t.queueMaxLength??1024,"Semaphore queueMaxLength",1,Number.MAX_SAFE_INTEGER,!0,!0),this.queueMaxAge=n(t.queueMaxAge??3e4,"Semaphore queueMaxAge",1,Number.MAX_SAFE_INTEGER,!0,!0),this.rejectOnFull=t.rejectOnFull??!1,this.metricsEnabled=t.metricsEnabled??!0,this.debug=t.debug??!1;let i=M({queueOrder:t.queueOrder,comparator:t.comparator});this.queue=new q(i),this.enqueueOrder=new S,this.permits=new T(e),this.circuit=new b({threshold:t.circuitBreakerThreshold,window:t.circuitBreakerWindow,cooldown:t.circuitBreakerCooldown,minThroughput:t.circuitBreakerMinThroughput,minFailures:t.circuitBreakerMinFailures}),this.backoff=new w({initialTimeout:t.backoffInitialTimeout,maxTimeout:t.backoffMaxTimeout,decayFactor:t.backoffDecayFactor}),this.metricsCollector=this.metricsEnabled?new g: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))for(let r of Array.from(i))try{r(...t)}catch(s){this.debug&&console.warn(`[Semaphore] Error in listener for "${e}":`,s)}}_tryAcquireFast(e){return this.circuit.isHalfOpen?this.circuit.hasProbeInFlight||!this.permits.hasCapacityFor(e)?null:(this.circuit.markProbeInFlight(),this.permits.acquire(e),this.totalAcquired++,this.metricsCollector?.onAcquireFast(Date.now(),this.permits.inFlight,this.queue.size),this.hasListeners(u.TASKACQUIRE)&&this.emit(u.TASKACQUIRE,{queued:this.queue.size,running:this.permits.capacity-this.permits.available,probe:!0}),this._createRelease(!0,e)):this.queue.size>0||!this.permits.hasCapacityFor(e)?null:(this.permits.acquire(e),this.totalAcquired++,this.metricsCollector?.onAcquireFast(Date.now(),this.permits.inFlight,this.queue.size),this.hasListeners(u.TASKACQUIRE)&&this.emit(u.TASKACQUIRE,{queued:this.queue.size,running:this.permits.capacity-this.permits.available}),this._createRelease(!1,e))}_createRelease(e=!1,t=1){this.pendingReleaseCount++;let i=this.releaseGeneration,r=!1;return()=>{if(r||i!==this.releaseGeneration){this.debug&&console.warn("[Semaphore] release() called after already released (no-op)");return}r=!0,this.pendingReleaseCount--,this.permits.release(t),this.totalReleased++,this.metricsCollector?.onRelease(Date.now(),this.permits.inFlight,this.queue.size),e&&this.circuit.isHalfOpen&&(this.circuit.handleProbeSuccess(),this.emit(u.CIRCUITCLOSE),this.metricsCollector?.markCircuitClose(),this.debug&&console.info("[Semaphore] Circuit closed after successful probe")),this.permits.assertInvariant(this.debug),this.hasListeners(u.TASKRELEASE)&&this.emit(u.TASKRELEASE,{queued:this.queue.size,running:this.permits.capacity-this.permits.available}),this.schedule()}}_enqueue(e){this.queue.insert(e),this.enqueueOrder.pushTail(e),this._armTimeout()}_dequeue(e){this.queue.delete(e)!==void 0&&(this.enqueueOrder.remove(e),this.enqueueOrder.size===0&&this._clearTimeout())}_armTimeout(){if(this.timeoutTimerId!==null)return;let e=this.enqueueOrder.peekHead();if(e===void 0)return;let t=Math.max(0,e.enqueueTime+this.queueMaxTimeout-Date.now());this.timeoutTimerId=setTimeout(()=>this._fireTimeout(),t)}_clearTimeout(){this.timeoutTimerId!==null&&(clearTimeout(this.timeoutTimerId),this.timeoutTimerId=null)}_fireTimeout(){if(this.timeoutTimerId=null,this.isShutdown)return;let e=Date.now(),t=0,i=this.enqueueOrder.peekHead();for(;i!==void 0&&i.enqueueTime+this.queueMaxTimeout<=e;){let r=i.claim();this._dequeue(i),r&&(this._timeoutTask(i),t++),i=this.enqueueOrder.peekHead()}if(i!==void 0){let r=Math.max(0,i.enqueueTime+this.queueMaxTimeout-e);this.timeoutTimerId=setTimeout(()=>this._fireTimeout(),r)}t>0&&this.schedule()}_timeoutTask(e){if(this.totalTimeouts++,this.circuit.recordTimeout(),this.backoff.onTimeout(),e.isProbe)this.circuit.handleProbeFailure(),this.emit(u.CIRCUITOPEN,{timeoutRate:1,recentTimeouts:1,total:1,reason:"half-open-probe-failed"}),this.metricsCollector?.markCircuitOpen(),this.debug&&console.warn("[Semaphore] Circuit re-opened: half-open probe timed out");else{let t=this.circuit.evaluateAndTrip();t.tripped&&(this.emit(u.CIRCUITOPEN,{timeoutRate:t.timeoutRate,recentTimeouts:t.failures,total:t.attempts}),this.metricsCollector?.markCircuitOpen(),this.debug&&console.warn(`[Semaphore] Circuit opened. Rate: ${(t.timeoutRate*100).toFixed(1)}%`))}this.metricsCollector?.onTimeout(Date.now(),this.queue.size),this.emit(u.TASKTIMEOUT,{queueLength:this.queue.size,backoffDelay:this.backoff.currentDelay,taskId:e.id}),this.debug&&console.warn(`[Semaphore] Task #${e.id} timed out after ${this.queueMaxTimeout}ms`),e.reject(new o(`Semaphore acquire timed out after ${this.queueMaxTimeout}ms (queue: ${this.queue.size})`,"TIMEOUT"))}_onTaskAbort(e,t){this._dequeue(e),e.isProbe&&this.circuit.releaseProbeSlot(),this.metricsCollector?.onAbort(Date.now(),this.queue.size),this.emit(u.TASKABORT),this.debug&&console.info(`[Semaphore] Task #${e.id} aborted`),t(new o("Semaphore acquire aborted","ABORTED")),this.schedule()}schedule(){if(this.scheduled||this.isShutdown)return;this.scheduled=!0;let e=this.backoff.currentDelay;e>0?setTimeout(()=>{this._runScheduler()},e).unref?.():queueMicrotask(()=>{this._runScheduler()})}_runScheduler(){if(!this.isShutdown){this.scheduled=!1;try{for(;this.queue.size>0&&!this.circuit.isOpen;){let e=this.queue.peek();if(!e||this.circuit.isHalfOpen&&e.id!==this.circuit.probeTaskId||!this.permits.hasCapacityFor(e.weight))break;let t=this.queue.pop();this.enqueueOrder.remove(t),this.enqueueOrder.size===0&&this._clearTimeout();let i=Date.now(),r=Math.max(0,i-t.enqueueTime);this.permits.acquire(t.weight),this.totalAcquired++,this.metricsCollector?.onAcquireQueued(i,r,this.permits.inFlight,this.queue.size),this.permits.assertInvariant(this.debug),t.dispatch(()=>this._createRelease(t.isProbe,t.weight))&&this.hasListeners(u.TASKACQUIRE)&&this.emit(u.TASKACQUIRE,{queued:this.queue.size,running:this.permits.capacity-this.permits.available,...t.isProbe?{probe:!0}:{}})}this.queue.size===0&&this.permits.available===this.permits.capacity&&this.drainResolve&&(this.drainResolve(),this.drainResolve=null,this.drainPromise=null)}catch(e){e instanceof Error?console.error("[Semaphore] Scheduler error:",e.message,e.stack):console.error("[Semaphore] Scheduler error:",e)}}}_startPurgeInterval(){this.purgeIntervalId!==null&&clearInterval(this.purgeIntervalId),this.purgeIntervalId=setInterval(()=>{this.isShutdown||this._purgeStaleTasks()},this.purgeIntervalMs),this.purgeIntervalId.unref?.()}_purgeStaleTasks(){let e=Date.now(),t=this.queue.size,i=this.enqueueOrder.peekHead();for(;i!==void 0&&e-i.enqueueTime>this.queueMaxAge;){this.circuit.probeTaskId===i.id&&this.circuit.releaseProbeSlot();let r=i.discard(new o(`Task purged after ${this.queueMaxAge}ms`,"PURGED"));this._dequeue(i),r&&(this.totalTimeouts++,this.metricsCollector?.onTimeout(Date.now(),this.queue.size),this.emit(u.QUEUEPURGE,i),this.debug&&console.warn(`[Semaphore] Purged stale task #${i.id}`)),i=this.enqueueOrder.peekHead()}this.debug&&this.queue.size<t&&console.info(`[Semaphore] Purged ${t-this.queue.size} stale tasks`),this.queue.size<t&&this.schedule()}tryAcquire(e=1){return this.isShutdown||!Number.isInteger(e)||e<1||e>this.permits.capacity||(this.circuit.checkAndTransition()&&(this.emit(u.CIRCUITHALFOPEN),this.metricsCollector?.markCircuitHalfOpen(),this.debug&&console.info("[Semaphore] Circuit entering half-open"),this.schedule()),this.circuit.isOpen)?null:(this.circuit.trackAttempt(),this._tryAcquireFast(e))}acquire(e,t=0,i=1){if(this.isShutdown)return Promise.reject(new o("Semaphore is shut down","SHUTDOWN"));if(!Number.isInteger(i)||i<1||i>this.permits.capacity)return Promise.reject(new o(`Invalid weight: ${i} (must be integer in 1..${this.permits.capacity})`,"INVALID_WEIGHT"));if(typeof t!="number"||!Number.isFinite(t))return Promise.reject(new o(`Invalid priority: ${t} (must be a finite number)`,"INVALID_PRIORITY"));if(this.circuit.checkAndTransition()&&(this.emit(u.CIRCUITHALFOPEN),this.metricsCollector?.markCircuitHalfOpen(),this.debug&&console.info("[Semaphore] Circuit entering half-open"),this.schedule()),this.circuit.isOpen)return Promise.reject(new o(`Circuit breaker open, retry in ${this.circuit.cooldownRemaining}ms`,"CIRCUIT_OPEN"));if(this.circuit.isHalfOpen&&this.circuit.hasProbeInFlight)return Promise.reject(new o("Circuit breaker half-open, probe in flight","CIRCUIT_HALF_OPEN"));if(e?.aborted)return Promise.reject(new o("Semaphore acquire aborted before start","ABORTED"));this.circuit.trackAttempt();let r=this._tryAcquireFast(i);if(r)return Promise.resolve(r);let s=this.circuit.isHalfOpen&&!this.circuit.hasProbeInFlight;return!s&&this.rejectOnFull?Promise.reject(new o("Semaphore at capacity (rejectOnFull)","QUEUE_FULL")):!s&&this.queue.size>=this.queueMaxLength?Promise.reject(new o(`Queue full (${this.queueMaxLength})`,"QUEUE_FULL")):new Promise((h,c)=>{let m=++this.taskIdCounter,f=Date.now(),p=s,d=new y({id:m,priority:p?Number.MIN_SAFE_INTEGER:t,enqueueTime:f,isProbe:p,resolve:h,reject:c,abortSignal:e,weight:i});d.arm(()=>this._onTaskAbort(d,c)),p&&this.circuit.claimProbeSlot(m),this.metricsCollector?.sampleQueueDepthAt(Date.now(),this.queue.size+1),this._enqueue(d),this.schedule()})}async use(e,t,i=0,r=1){let s=await this.acquire(t,i,r);try{return await e()}finally{s()}}drain(e){return this.isShutdown?Promise.reject(new o("Cannot drain: semaphore is shut down","SHUTDOWN")):(e!==void 0&&n(e,"drain timeoutMs",1,Number.MAX_SAFE_INTEGER,!0,!0),this.drainPromise?this.drainPromise:this.queue.size===0&&this.permits.available===this.permits.capacity?Promise.resolve():(this.drainPromise=new Promise((t,i)=>{if(this.drainResolve=t,e!==void 0){let r=setTimeout(()=>{this.drainResolve=null,this.drainPromise=null,i(new o(`drain() timed out after ${e}ms`,"TIMEOUT"))},e),s=this.drainResolve;this.drainResolve=()=>{clearTimeout(r),s()}}}),this.drainPromise))}reset(e={}){for(let t of this.queue.toArray())t.discard(new o("Semaphore reset","SHUTDOWN"));this.queue.clear(),this.enqueueOrder.clear(),this._clearTimeout(),this.metricsCollector?.reset(),this.permits.reset(),this.backoff.reset(),this.circuit.reset(),this.releaseGeneration++,this.pendingReleaseCount=0,this.scheduled=!1,this.isShutdown=!1,this.taskIdCounter=0,this.totalAcquired=0,this.totalReleased=0,this.totalTimeouts=0,e.clearListeners&&this.eventListeners.clear(),this.drainResolve&&(this.drainResolve(),this.drainResolve=null,this.drainPromise=null),this._startPurgeInterval(),this.metricsCollector?.markCapacityChange(this.permits.capacity),this.metricsCollector?.sampleGauges(Date.now(),this.permits.inFlight,this.queue.size),this.debug&&console.info("[Semaphore] Reset to initial state")}shutdown(e="Semaphore shutdown"){if(!this.isShutdown){this.isShutdown=!0,this.debug&&console.info(`[Semaphore] Shutdown: ${e}`),this.purgeIntervalId!==null&&(clearInterval(this.purgeIntervalId),this.purgeIntervalId=null),this._clearTimeout();for(let t of this.queue.toArray())t.discard(new o(e,"SHUTDOWN"));this.queue.clear(),this.enqueueOrder.clear(),this.metricsCollector?.destroy(),this.drainResolve&&(this.drainResolve(),this.drainResolve=null,this.drainPromise=null),this.emit(u.SHUTDOWN,e)}}cancel(){if(this.isShutdown)return;let e=this.queue.toArray();for(let t of e)t.isProbe&&this.circuit.releaseProbeSlot(),t.discard(new o("Semaphore acquire cancelled","CANCELLED")),this._dequeue(t);this.metricsCollector?.sampleQueueDepthAt(Date.now(),this.queue.size),this.debug&&console.info(`[Semaphore] Cancelled ${e.length} queued tasks`),this.schedule()}status(){let e=this.metricsCollector?.getSnapshot()??null,t=e?.windows?.["1m"]?.counts,i=t?.acquired??0,r=t?.timeouts??0,s=this.enqueueOrder.peekHead(),h=s===void 0?0:Math.max(0,Date.now()-s.enqueueTime);return{status:{running:this.permits.capacity-this.permits.available,queued:this.queue.size,available:this.permits.available,inFlight:this.permits.inFlight,pendingReleases:this.pendingReleaseCount,circuitOpen:this.circuit.isOpen,circuitHalfOpen:this.circuit.isHalfOpen,backoffDelay:Math.round(this.backoff.currentDelay),requestsPerSecond:+(i/60).toFixed(2),timeoutRate1m:i+r>0?+(r/(i+r)*100).toFixed(1):0,queueAge:h},lifetime:{totalAcquired:this.totalAcquired,totalReleased:this.totalReleased,totalTimeouts:this.totalTimeouts,circuitBreakerCooldownRemaining:this.circuit.cooldownRemaining},metrics:e}}isAvailable(){return!this.isShutdown&&!this.circuit.isOpen&&!this.permits.isFull}get queueLength(){return this.queue.size}get availablePermits(){return this.permits.available}};0&&(module.exports={CircuitBreaker,QUEUE_ORDERINGS,Semaphore,SemaphoreError,SemaphoreEvents}); | ||
| 'use strict';var o=class extends Error{constructor(e,t){super(e),this.name="SemaphoreError",this.code=t;}};var u=(a,e,t,i,r=true,s=true)=>{if(typeof a!="number"||Number.isNaN(a))throw new o(`${e} must be a valid number`,"INVALID_ARGUMENT");if(r&&!Number.isInteger(a))throw new o(`${e} must be an integer`,"INVALID_ARGUMENT");if(s?a<t:a<=t)throw new o(`${e} must be ${s?">=":">"} ${t}`,"INVALID_ARGUMENT");if(s?a>i:a>=i)throw new o(`${e} must be ${s?"<=":"<"} ${i}`,"INVALID_ARGUMENT");return a};var T=class{constructor(e){this._inFlight=0;this.capacity=e,this._available=e;}get available(){return this._available}get inFlight(){return this._inFlight}get isFull(){return this._available===0}hasCapacityFor(e){return this._available>=e}acquire(e=1){this._available-=e,this._inFlight+=e;}release(e=1){this._available=Math.min(this._available+e,this.capacity),this._inFlight=this._inFlight<e?0:this._inFlight-e;}assertInvariant(e){if(!e)return;let t=this._inFlight+this._available;t!==this.capacity&&console.error(`[Semaphore] Invariant violation: inFlight(${this._inFlight}) + available(${this._available}) = ${t}, expected capacity(${this.capacity})`);}reset(){this._available=this.capacity,this._inFlight=0;}};var A=class{constructor(e,t){this.size=e,this.stepMs=t,this.buckets=new Int32Array(e*2),this.timestamps=new Float64Array(e);}idx(){let e=Date.now(),t=Math.floor(e/this.stepMs)*this.stepMs,i=Math.floor(t/this.stepMs)%this.size;return this.timestamps[i]!==t&&(this.timestamps[i]=t,this.buckets[i*2]=0,this.buckets[i*2+1]=0),i}addAcquired(){this.buckets[this.idx()*2]++;}addTimeout(){this.buckets[this.idx()*2+1]++;}snapshot(){let e=Date.now(),t=Math.floor(e/this.stepMs)*this.stepMs-(this.size-1)*this.stepMs,i=0,r=0;for(let s=0;s<this.size;s++)this.timestamps[s]>=t&&(i+=this.buckets[s*2],r+=this.buckets[s*2+1]);return {acquired:i,timeouts:r}}reset(){this.buckets.fill(0),this.timestamps.fill(0);}},b=class{constructor(e={}){this.state="closed";this.openUntil=0;this.probeInFlight=false;this._probeTaskId=null;if(this.threshold=u(e.threshold??.5,"CircuitBreaker threshold",0,1,false,false),this.window=u(e.window??1e4,"CircuitBreaker window",1e3,Number.MAX_SAFE_INTEGER,true,true),this.cooldown=u(e.cooldown??5e3,"CircuitBreaker cooldown",1e3,Number.MAX_SAFE_INTEGER,true,true),this.minThroughput=u(e.minThroughput??10,"CircuitBreaker minThroughput",1,Number.MAX_SAFE_INTEGER,true,true),this.minFailures=u(e.minFailures??5,"CircuitBreaker minFailures",1,Number.MAX_SAFE_INTEGER,true,true),this.eventWindow=new A(Math.ceil(this.window/1e3),1e3),this.minThroughput<this.minFailures)throw new o("CircuitBreaker minThroughput must be >= minFailures","INVALID_ARGUMENT")}get isClosed(){return this.state==="closed"}get isOpen(){return this.state==="open"}get isHalfOpen(){return this.state==="half-open"}get hasProbeInFlight(){return this.probeInFlight}get probeTaskId(){return this._probeTaskId}get cooldownRemaining(){return this.isOpen?Math.max(0,this.openUntil-Date.now()):0}trackAttempt(){this.state==="closed"&&this.eventWindow.addAcquired();}recordTimeout(){this.eventWindow.addTimeout();}checkAndTransition(){return this.state!=="open"||Date.now()<this.openUntil?false:(this.state="half-open",this.probeInFlight=false,this._probeTaskId=null,true)}evaluateAndTrip(){if(this.state!=="closed")return {tripped:false};let{timeouts:e,acquired:t}=this.eventWindow.snapshot();if(t<this.minThroughput||e<this.minFailures)return {tripped:false};let i=e/t;return i<this.threshold?{tripped:false}:(this.state="open",this.openUntil=Date.now()+this.cooldown,{tripped:true,timeoutRate:i,failures:e,attempts:t})}markProbeInFlight(){this.probeInFlight=true;}claimProbeSlot(e){this.probeInFlight=true,this._probeTaskId=e;}releaseProbeSlot(){this.probeInFlight=false,this._probeTaskId=null;}handleProbeSuccess(){this.state="closed",this.probeInFlight=false,this._probeTaskId=null,this.eventWindow.reset();}handleProbeFailure(){this.state="open",this.probeInFlight=false,this._probeTaskId=null,this.openUntil=Date.now()+this.cooldown;}reset(){this.state="closed",this.openUntil=0,this.probeInFlight=false,this._probeTaskId=null,this.eventWindow.reset();}};var w=class{constructor(e={}){this.delay=0;this.lastTimestamp=0;if(this.initialTimeout=u(e.initialTimeout??50,"BackoffTracker initialTimeout",0,Number.MAX_SAFE_INTEGER,true,true),this.maxTimeout=u(e.maxTimeout??2e3,"BackoffTracker maxTimeout",0,Number.MAX_SAFE_INTEGER,true,true),this.decayFactor=u(e.decayFactor??.5,"BackoffTracker decayFactor",0,1,false,false),this.maxTimeout<this.initialTimeout)throw new o("BackoffTracker maxTimeout must be >= initialTimeout","INVALID_ARGUMENT")}get currentDelay(){if(this.delay===0)return 0;let e=(Date.now()-this.lastTimestamp)/1e3;if(e<=0)return this.delay;let t=this.delay*Math.pow(this.decayFactor,e);return t<1?0:t}onTimeout(){let e=this.currentDelay,t=e>0?e*2:this.initialTimeout;this.delay=Math.min(t,this.maxTimeout),this.lastTimestamp=Date.now();}reset(){this.delay=0,this.lastTimestamp=0;}};var y=class{constructor(e){this.prev=null;this.next=null;this.heapIndex=-1;this.completed=false;this.id=e.id,this.priority=e.priority,this.enqueueTime=e.enqueueTime,this.isProbe=e.isProbe,this.weight=e.weight??1,this._resolve=e.resolve,this._reject=e.reject,this.abortSignal=e.abortSignal;}arm(e){this.abortSignal&&(this.abortListener=()=>{this.claim()&&e();},this.abortSignal.addEventListener("abort",this.abortListener));}dispatch(e){return this.claim()?(this._resolve(e()),true):false}discard(e){return this.claim()?(this._reject(e),true):false}claim(){return this.completed?false:(this.completed=true,this.abortSignal&&this.abortListener&&this.abortSignal.removeEventListener("abort",this.abortListener),true)}reject(e){this._reject(e);}};var S=class{constructor(e){this.heap=[];this.compare=e;}get size(){return this.heap.length}isEmpty(){return this.heap.length===0}peek(){return this.heap[0]}toArray(){return [...this.heap]}has(e){return e.heapIndex>=0&&this.heap[e.heapIndex]===e}clear(){for(let e of this.heap)e.heapIndex=-1;this.heap=[];}insert(e){if(e.heapIndex>=0)throw new Error("Item is already in a heap");let t=this.heap.length;this.heap.push(e),e.heapIndex=t,this._bubbleUp(t);}pop(){if(this.heap.length===0)return;let e=this.heap[0];e.heapIndex=-1;let t=this.heap.pop();return this.heap.length>0&&t!==e&&(this.heap[0]=t,t.heapIndex=0,this._bubbleDown(0)),e}delete(e){let t=e.heapIndex;if(t<0||this.heap[t]!==e)return;e.heapIndex=-1;let i=this.heap.pop();if(t===this.heap.length||i===e)return e;this.heap[t]=i,i.heapIndex=t;let r=this.heap[t-1>>1];return t>0&&this.compare(i,r)<0?this._bubbleUp(t):this._bubbleDown(t),e}_swap(e,t){let i=this.heap[e],r=this.heap[t];this.heap[e]=r,this.heap[t]=i,i.heapIndex=t,r.heapIndex=e;}_bubbleUp(e){for(;e>0;){let t=e-1>>1;if(this.compare(this.heap[e],this.heap[t])<0)this._swap(e,t),e=t;else break}}_bubbleDown(e){let t=this.heap.length;for(;;){let i=e,r=2*e+1,s=2*e+2;if(r<t&&this.compare(this.heap[r],this.heap[i])<0&&(i=r),s<t&&this.compare(this.heap[s],this.heap[i])<0&&(i=s),i===e)break;this._swap(e,i),e=i;}}};var q=class{constructor(){this.head=null;this.tail=null;this._size=0;}get size(){return this._size}isEmpty(){return this._size===0}peekHead(){return this.head===null?void 0:this.head}pushTail(e){e.prev=this.tail,e.next=null,this.tail!==null?this.tail.next=e:this.head=e,this.tail=e,this._size++;}remove(e){e.prev!==null?e.prev.next=e.next:this.head=e.next,e.next!==null?e.next.prev=e.prev:this.tail=e.prev,e.prev=e.next=null,this._size--;}clear(){this.head=this.tail=null,this._size=0;}};var E=class{constructor(e,t){if(e<=0)throw new Error("CombinedWindow size must be > 0");this.size=e,this.stepMs=t,this.timestamps=new Float64Array(e),this.acquired=new Int32Array(e),this.released=new Int32Array(e),this.timeouts=new Int32Array(e),this.ifSum=new Float64Array(e),this.ifCount=new Int32Array(e),this.ifMax=new Float64Array(e),this.qSum=new Float64Array(e),this.qCount=new Int32Array(e),this.qMax=new Float64Array(e),this.latSum=new Float64Array(e),this.latCount=new Int32Array(e);}bucket(e){let t=Math.floor(e/this.stepMs),i=t*this.stepMs,r=t%this.size;return this.timestamps[r]!==i&&(this.timestamps[r]=i,this.acquired[r]=0,this.released[r]=0,this.timeouts[r]=0,this.ifSum[r]=0,this.ifCount[r]=0,this.ifMax[r]=0,this.qSum[r]=0,this.qCount[r]=0,this.qMax[r]=0,this.latSum[r]=0,this.latCount[r]=0),r}addInflight(e,t){this.ifSum[e]+=t,this.ifCount[e]++,t>this.ifMax[e]&&(this.ifMax[e]=t);}addQueue(e,t){this.qSum[e]+=t,this.qCount[e]++,t>this.qMax[e]&&(this.qMax[e]=t);}recordAcquire(e,t,i){let r=this.bucket(e);this.acquired[r]++,this.addInflight(r,t),this.addQueue(r,i);}recordAcquireQueued(e,t,i,r){let s=this.bucket(e);this.acquired[s]++,this.latSum[s]+=t,this.latCount[s]++,this.addInflight(s,i),this.addQueue(s,r);}recordRelease(e,t,i){let r=this.bucket(e);this.released[r]++,this.addInflight(r,t),this.addQueue(r,i);}recordTimeoutQueue(e,t){let i=this.bucket(e);this.timeouts[i]++,this.addQueue(i,t);}sampleBoth(e,t,i){let r=this.bucket(e);this.addInflight(r,t),this.addQueue(r,i);}addAcquired(e){this.acquired[this.bucket(e)]++;}addReleased(e){this.released[this.bucket(e)]++;}addTimeout(e){this.timeouts[this.bucket(e)]++;}addLatency(e,t){let i=this.bucket(e);this.latSum[i]+=t,this.latCount[i]++;}sampleInflight(e,t){this.addInflight(this.bucket(e),t);}sampleQueue(e,t){this.addQueue(this.bucket(e),t);}snapshot(e){let t=Math.floor(e/this.stepMs)*this.stepMs-(this.size-1)*this.stepMs,i=0,r=0,s=0,h=0,p=0,m=0,f=0,c=0,d=0,g=0,v=0;for(let l=0;l<this.size;l++)this.timestamps[l]<t||(i+=this.acquired[l],r+=this.released[l],s+=this.timeouts[l],h+=this.ifSum[l],p+=this.ifCount[l],this.ifMax[l]>m&&(m=this.ifMax[l]),f+=this.qSum[l],c+=this.qCount[l],this.qMax[l]>d&&(d=this.qMax[l]),g+=this.latSum[l],v+=this.latCount[l]);return {counts:{acquired:i,released:r,timeouts:s},inflight:{avg:p===0?0:h/p,max:m,samples:p},queue:{avg:c===0?0:f/c,max:d,samples:c},latency:{avg:v===0?0:g/v,count:v,total:g}}}reset(){this.timestamps.fill(0),this.acquired.fill(0),this.released.fill(0),this.timeouts.fill(0),this.ifSum.fill(0),this.ifCount.fill(0),this.ifMax.fill(0),this.qSum.fill(0),this.qCount.fill(0),this.qMax.fill(0),this.latSum.fill(0),this.latCount.fill(0);}},M=[{size:60,stepMs:1e3},{size:60,stepMs:5e3},{size:60,stepMs:15e3},{size:60,stepMs:6e4},{size:60,stepMs:144e4}],I=class{constructor(e=M){this._totalAcquiredFast=0;this._totalAcquiredQueued=0;this._totalReleased=0;this._totalTimeouts=0;this._totalAborts=0;this._capacity=0;this._circuitOpen=false;this._circuitHalfOpen=false;if(e.length===0)throw new Error("SemaphoreMetrics requires at least one WindowOptions");this.windows=e.map(({size:t,stepMs:i})=>new E(t,i)),this.windowLabels=e.map(({size:t,stepMs:i})=>{let r=t*i;return r>=36e5&&r%36e5===0?`${r/36e5}h`:r>=6e4&&r%6e4===0?`${r/6e4}m`:r>=1e3&&r%1e3===0?`${r/1e3}s`:`${r}ms`});}onAcquireFast(e,t,i){this._totalAcquiredFast++;for(let r of this.windows)r.recordAcquire(e,t,i);}onAcquireQueued(e,t,i,r){this._totalAcquiredQueued++;for(let s of this.windows)s.recordAcquireQueued(e,t,i,r);}onRelease(e,t,i){this._totalReleased++;for(let r of this.windows)r.recordRelease(e,t,i);}onTimeout(e,t){this._totalTimeouts++;for(let i of this.windows)i.recordTimeoutQueue(e,t);}onAbort(e,t){this._totalAborts++;for(let i of this.windows)i.sampleQueue(e,t);}sampleGauges(e,t,i){for(let r of this.windows)r.sampleBoth(e,t,i);}sampleQueueDepthAt(e,t){for(let i of this.windows)i.sampleQueue(e,t);}markAcquireFast(){this._totalAcquiredFast++;let e=Date.now();for(let t of this.windows)t.addAcquired(e);}markAcquireQueued(e){this._totalAcquiredQueued++;let t=Date.now();for(let i of this.windows)i.addAcquired(t),i.addLatency(t,e);}markReleased(){this._totalReleased++;let e=Date.now();for(let t of this.windows)t.addReleased(e);}markTimeout(){this._totalTimeouts++;let e=Date.now();for(let t of this.windows)t.addTimeout(e);}markAbort(){this._totalAborts++;}sampleInFlight(e){let t=Date.now();for(let i of this.windows)i.sampleInflight(t,e);}sampleQueueDepth(e){let t=Date.now();for(let i of this.windows)i.sampleQueue(t,e);}markCapacityChange(e){this._capacity=e;}markCircuitOpen(){this._circuitOpen=true,this._circuitHalfOpen=false;}markCircuitHalfOpen(){this._circuitOpen=false,this._circuitHalfOpen=true;}markCircuitClose(){this._circuitOpen=false,this._circuitHalfOpen=false;}getSnapshot(){let e=Date.now(),t={},i=0,r=0;for(let s=0;s<this.windowLabels.length;s++){let h=this.windows[s].snapshot(e);t[this.windowLabels[s]]=h,s===0&&(i=h.inflight.avg,r=h.queue.avg);}return {windows:t,meta:{inFlightLastMinute:Math.round(i),queueDepthLastMinute:Math.round(r),totalAcquiredFast:this._totalAcquiredFast,totalAcquiredQueued:this._totalAcquiredQueued,totalReleased:this._totalReleased,totalTimeouts:this._totalTimeouts,totalAborts:this._totalAborts,capacity:this._capacity,circuitOpen:this._circuitOpen,circuitHalfOpen:this._circuitHalfOpen}}}reset(){for(let e of this.windows)e.reset();this._totalAcquiredFast=0,this._totalAcquiredQueued=0,this._totalReleased=0,this._totalTimeouts=0,this._totalAborts=0,this._capacity=0,this._circuitOpen=false,this._circuitHalfOpen=false;}destroy(){this.reset();}};var k={fifo:(a,e)=>a.id-e.id,lifo:(a,e)=>e.id-a.id,fifoWithPriority:(a,e)=>a.priority-e.priority||a.id-e.id,lifoWithPriority:(a,e)=>a.priority-e.priority||e.id-a.id};function x(a){if(a.comparator!==void 0){if(typeof a.comparator!="function")throw new o("Semaphore comparator must be a function","INVALID_ARGUMENT");return a.comparator}let e=a.queueOrder??"fifoWithPriority",t=k[e];if(t===void 0)throw new o(`Semaphore queueOrder must be one of: ${Object.keys(k).join(", ")} (got ${JSON.stringify(e)})`,"INVALID_ARGUMENT");return t}function C(a){let e=x(a);return (t,i)=>{if(t.isProbe!==i.isProbe)return t.isProbe?-1:1;let r=e(t,i);return typeof r!="number"||Number.isNaN(r)?t.id-i.id:r}}var n={TASKACQUIRE:"task-acquire",TASKRELEASE:"task-release",TASKTIMEOUT:"task-timeout",TASKABORT:"task-abort",QUEUEPURGE:"queue-purge",CIRCUITOPEN:"circuit-open",CIRCUITHALFOPEN:"circuit-half-open",CIRCUITCLOSE:"circuit-close",SHUTDOWN:"shutdown"};var _=class{constructor(e,t={}){this.pendingReleaseCount=0;this.releaseGeneration=0;this.scheduled=false;this.isShutdown=false;this.taskIdCounter=0;this.totalAcquired=0;this.totalReleased=0;this.totalTimeouts=0;this.eventListeners=new Map;this.drainPromise=null;this.drainResolve=null;this.purgeIntervalId=null;this.timeoutTimerId=null;u(e,"Semaphore count",0,Number.MAX_SAFE_INTEGER,true,false),this.purgeIntervalMs=u(t.purgeIntervalMs??3e3,"Semaphore purgeIntervalMs",500,Number.MAX_SAFE_INTEGER,true,true),this.queueMaxTimeout=u(t.queueMaxTimeout??1e4,"Semaphore queueMaxTimeout",1,Number.MAX_SAFE_INTEGER,true,true),this.queueMaxLength=u(t.queueMaxLength??1024,"Semaphore queueMaxLength",1,Number.MAX_SAFE_INTEGER,true,true),this.queueMaxAge=u(t.queueMaxAge??3e4,"Semaphore queueMaxAge",1,Number.MAX_SAFE_INTEGER,true,true),this.rejectOnFull=t.rejectOnFull??false,this.metricsEnabled=t.metricsEnabled??true,this.debug=t.debug??false;let i=C({queueOrder:t.queueOrder,comparator:t.comparator});this.queue=new S(i),this.enqueueOrder=new q,this.permits=new T(e),this.circuit=new b({threshold:t.circuitBreakerThreshold,window:t.circuitBreakerWindow,cooldown:t.circuitBreakerCooldown,minThroughput:t.circuitBreakerMinThroughput,minFailures:t.circuitBreakerMinFailures}),this.backoff=new w({initialTimeout:t.backoffInitialTimeout,maxTimeout:t.backoffMaxTimeout,decayFactor:t.backoffDecayFactor}),this.metricsCollector=this.metricsEnabled?new I:void 0,this.metricsCollector?.markCapacityChange(this.permits.capacity),this.metricsCollector?.sampleGauges(Date.now(),this.permits.inFlight,this.queue.size),this._startPurgeInterval();}on(e,t){this.eventListeners.has(e)||this.eventListeners.set(e,new Set),this.eventListeners.get(e).add(t);}off(e,t){this.eventListeners.get(e)?.delete(t),this.eventListeners.get(e)?.size===0&&this.eventListeners.delete(e);}removeAllListeners(e){e?this.eventListeners.delete(e):this.eventListeners.clear();}hasListeners(e){let t=this.eventListeners.get(e);return t!==void 0&&t.size>0}emit(e,...t){let i=this.eventListeners.get(e);if(!(!i||i.size===0)){if(i.size===1){let r=i.values().next().value;try{r(...t);}catch(s){this.debug&&console.warn(`[Semaphore] Error in listener for "${e}":`,s);}return}for(let r of Array.from(i))try{r(...t);}catch(s){this.debug&&console.warn(`[Semaphore] Error in listener for "${e}":`,s);}}}_tryAcquireFast(e){return this.circuit.isHalfOpen?this.circuit.hasProbeInFlight||!this.permits.hasCapacityFor(e)?null:(this.circuit.markProbeInFlight(),this.permits.acquire(e),this.totalAcquired++,this.metricsCollector?.onAcquireFast(Date.now(),this.permits.inFlight,this.queue.size),this.hasListeners(n.TASKACQUIRE)&&this.emit(n.TASKACQUIRE,{queued:this.queue.size,running:this.permits.capacity-this.permits.available,probe:true}),this._createRelease(true,e)):this.queue.size>0||!this.permits.hasCapacityFor(e)?null:(this.permits.acquire(e),this.totalAcquired++,this.metricsCollector?.onAcquireFast(Date.now(),this.permits.inFlight,this.queue.size),this.hasListeners(n.TASKACQUIRE)&&this.emit(n.TASKACQUIRE,{queued:this.queue.size,running:this.permits.capacity-this.permits.available}),this._createRelease(false,e))}_createRelease(e=false,t=1){this.pendingReleaseCount++;let i=this.releaseGeneration,r=false;return ()=>{if(r||i!==this.releaseGeneration){this.debug&&console.warn("[Semaphore] release() called after already released (no-op)");return}r=true,this.pendingReleaseCount--,this.permits.release(t),this.totalReleased++,this.metricsCollector?.onRelease(Date.now(),this.permits.inFlight,this.queue.size),e&&this.circuit.isHalfOpen&&(this.circuit.handleProbeSuccess(),this.emit(n.CIRCUITCLOSE),this.metricsCollector?.markCircuitClose(),this.debug&&console.info("[Semaphore] Circuit closed after successful probe")),this.permits.assertInvariant(this.debug),this.hasListeners(n.TASKRELEASE)&&this.emit(n.TASKRELEASE,{queued:this.queue.size,running:this.permits.capacity-this.permits.available}),this.schedule();}}_enqueue(e){this.queue.insert(e),this.enqueueOrder.pushTail(e),this._armTimeout();}_dequeue(e){this.queue.delete(e)!==void 0&&(this.enqueueOrder.remove(e),this.enqueueOrder.size===0&&this._clearTimeout());}_armTimeout(){if(this.timeoutTimerId!==null)return;let e=this.enqueueOrder.peekHead();if(e===void 0)return;let t=Math.max(0,e.enqueueTime+this.queueMaxTimeout-Date.now());this.timeoutTimerId=setTimeout(()=>this._fireTimeout(),t);}_clearTimeout(){this.timeoutTimerId!==null&&(clearTimeout(this.timeoutTimerId),this.timeoutTimerId=null);}_fireTimeout(){if(this.timeoutTimerId=null,this.isShutdown)return;let e=Date.now(),t=0,i=this.enqueueOrder.peekHead();for(;i!==void 0&&i.enqueueTime+this.queueMaxTimeout<=e;){let r=i.claim();this._dequeue(i),r&&(this._timeoutTask(i),t++),i=this.enqueueOrder.peekHead();}if(i!==void 0){let r=Math.max(0,i.enqueueTime+this.queueMaxTimeout-e);this.timeoutTimerId=setTimeout(()=>this._fireTimeout(),r);}t>0&&this.schedule();}_timeoutTask(e){if(this.totalTimeouts++,this.circuit.recordTimeout(),this.backoff.onTimeout(),e.isProbe)this.circuit.handleProbeFailure(),this.emit(n.CIRCUITOPEN,{timeoutRate:1,recentTimeouts:1,total:1,reason:"half-open-probe-failed"}),this.metricsCollector?.markCircuitOpen(),this.debug&&console.warn("[Semaphore] Circuit re-opened: half-open probe timed out");else {let t=this.circuit.evaluateAndTrip();t.tripped&&(this.emit(n.CIRCUITOPEN,{timeoutRate:t.timeoutRate,recentTimeouts:t.failures,total:t.attempts}),this.metricsCollector?.markCircuitOpen(),this.debug&&console.warn(`[Semaphore] Circuit opened. Rate: ${(t.timeoutRate*100).toFixed(1)}%`));}this.metricsCollector?.onTimeout(Date.now(),this.queue.size),this.emit(n.TASKTIMEOUT,{queueLength:this.queue.size,backoffDelay:this.backoff.currentDelay,taskId:e.id}),this.debug&&console.warn(`[Semaphore] Task #${e.id} timed out after ${this.queueMaxTimeout}ms`),e.reject(new o(`Semaphore acquire timed out after ${this.queueMaxTimeout}ms (queue: ${this.queue.size})`,"TIMEOUT"));}_onTaskAbort(e,t){this._dequeue(e),e.isProbe&&this.circuit.releaseProbeSlot(),this.metricsCollector?.onAbort(Date.now(),this.queue.size),this.emit(n.TASKABORT),this.debug&&console.info(`[Semaphore] Task #${e.id} aborted`),t(new o("Semaphore acquire aborted","ABORTED")),this.schedule();}schedule(){if(this.scheduled||this.isShutdown)return;this.scheduled=true;let e=this.backoff.currentDelay;e>0?setTimeout(()=>{this._runScheduler();},e).unref?.():queueMicrotask(()=>{this._runScheduler();});}_runScheduler(){if(!this.isShutdown){this.scheduled=false;try{for(;this.queue.size>0&&!this.circuit.isOpen;){let e=this.queue.peek();if(!e||this.circuit.isHalfOpen&&e.id!==this.circuit.probeTaskId||!this.permits.hasCapacityFor(e.weight))break;let t=this.queue.pop();this.enqueueOrder.remove(t),this.enqueueOrder.size===0&&this._clearTimeout();let i=Date.now(),r=Math.max(0,i-t.enqueueTime);this.permits.acquire(t.weight),this.totalAcquired++,this.metricsCollector?.onAcquireQueued(i,r,this.permits.inFlight,this.queue.size),this.permits.assertInvariant(this.debug),t.dispatch(()=>this._createRelease(t.isProbe,t.weight))&&this.hasListeners(n.TASKACQUIRE)&&this.emit(n.TASKACQUIRE,{queued:this.queue.size,running:this.permits.capacity-this.permits.available,...t.isProbe?{probe:!0}:{}});}this.queue.size===0&&this.permits.available===this.permits.capacity&&this.drainResolve&&(this.drainResolve(),this.drainResolve=null,this.drainPromise=null);}catch(e){e instanceof Error?console.error("[Semaphore] Scheduler error:",e.message,e.stack):console.error("[Semaphore] Scheduler error:",e);}}}_startPurgeInterval(){this.purgeIntervalId!==null&&clearInterval(this.purgeIntervalId),this.purgeIntervalId=setInterval(()=>{this.isShutdown||this._purgeStaleTasks();},this.purgeIntervalMs),this.purgeIntervalId.unref?.();}_purgeStaleTasks(){let e=Date.now(),t=this.queue.size,i=this.enqueueOrder.peekHead();for(;i!==void 0&&e-i.enqueueTime>this.queueMaxAge;){this.circuit.probeTaskId===i.id&&this.circuit.releaseProbeSlot();let r=i.discard(new o(`Task purged after ${this.queueMaxAge}ms`,"PURGED"));this._dequeue(i),r&&(this.totalTimeouts++,this.metricsCollector?.onTimeout(Date.now(),this.queue.size),this.emit(n.QUEUEPURGE,i),this.debug&&console.warn(`[Semaphore] Purged stale task #${i.id}`)),i=this.enqueueOrder.peekHead();}this.debug&&this.queue.size<t&&console.info(`[Semaphore] Purged ${t-this.queue.size} stale tasks`),this.queue.size<t&&this.schedule();}tryAcquire(e=1){return this.isShutdown||!Number.isInteger(e)||e<1||e>this.permits.capacity||(this.circuit.checkAndTransition()&&(this.emit(n.CIRCUITHALFOPEN),this.metricsCollector?.markCircuitHalfOpen(),this.debug&&console.info("[Semaphore] Circuit entering half-open"),this.schedule()),this.circuit.isOpen)?null:(this.circuit.trackAttempt(),this._tryAcquireFast(e))}acquire(e,t=0,i=1){if(this.isShutdown)return Promise.reject(new o("Semaphore is shut down","SHUTDOWN"));if(!Number.isInteger(i)||i<1||i>this.permits.capacity)return Promise.reject(new o(`Invalid weight: ${i} (must be integer in 1..${this.permits.capacity})`,"INVALID_WEIGHT"));if(typeof t!="number"||!Number.isFinite(t))return Promise.reject(new o(`Invalid priority: ${t} (must be a finite number)`,"INVALID_PRIORITY"));if(this.circuit.checkAndTransition()&&(this.emit(n.CIRCUITHALFOPEN),this.metricsCollector?.markCircuitHalfOpen(),this.debug&&console.info("[Semaphore] Circuit entering half-open"),this.schedule()),this.circuit.isOpen)return Promise.reject(new o(`Circuit breaker open, retry in ${this.circuit.cooldownRemaining}ms`,"CIRCUIT_OPEN"));if(this.circuit.isHalfOpen&&this.circuit.hasProbeInFlight)return Promise.reject(new o("Circuit breaker half-open, probe in flight","CIRCUIT_HALF_OPEN"));if(e?.aborted)return Promise.reject(new o("Semaphore acquire aborted before start","ABORTED"));this.circuit.trackAttempt();let r=this._tryAcquireFast(i);if(r)return Promise.resolve(r);let s=this.circuit.isHalfOpen&&!this.circuit.hasProbeInFlight;return !s&&this.rejectOnFull?Promise.reject(new o("Semaphore at capacity (rejectOnFull)","QUEUE_FULL")):!s&&this.queue.size>=this.queueMaxLength?Promise.reject(new o(`Queue full (${this.queueMaxLength})`,"QUEUE_FULL")):new Promise((h,p)=>{let m=++this.taskIdCounter,f=Date.now(),c=s,d=new y({id:m,priority:c?Number.MIN_SAFE_INTEGER:t,enqueueTime:f,isProbe:c,resolve:h,reject:p,abortSignal:e,weight:i});d.arm(()=>this._onTaskAbort(d,p)),c&&this.circuit.claimProbeSlot(m),this.metricsCollector?.sampleQueueDepthAt(Date.now(),this.queue.size+1),this._enqueue(d),this.schedule();})}async use(e,t,i=0,r=1){let s=await this.acquire(t,i,r);try{return await e()}finally{s();}}drain(e){return this.isShutdown?Promise.reject(new o("Cannot drain: semaphore is shut down","SHUTDOWN")):(e!==void 0&&u(e,"drain timeoutMs",1,Number.MAX_SAFE_INTEGER,true,true),this.drainPromise?this.drainPromise:this.queue.size===0&&this.permits.available===this.permits.capacity?Promise.resolve():(this.drainPromise=new Promise((t,i)=>{if(this.drainResolve=t,e!==void 0){let r=setTimeout(()=>{this.drainResolve=null,this.drainPromise=null,i(new o(`drain() timed out after ${e}ms`,"TIMEOUT"));},e),s=this.drainResolve;this.drainResolve=()=>{clearTimeout(r),s();};}}),this.drainPromise))}reset(e={}){if(this.isShutdown)throw new o("Cannot reset a semaphore that has been shut down","SHUTDOWN");for(let t of this.queue.toArray())t.discard(new o("Semaphore reset","SHUTDOWN"));this.queue.clear(),this.enqueueOrder.clear(),this._clearTimeout(),this.metricsCollector?.reset(),this.permits.reset(),this.backoff.reset(),this.circuit.reset(),this.releaseGeneration++,this.pendingReleaseCount=0,this.scheduled=false,this.isShutdown=false,this.taskIdCounter=0,this.totalAcquired=0,this.totalReleased=0,this.totalTimeouts=0,e.clearListeners&&this.eventListeners.clear(),this.drainResolve&&(this.drainResolve(),this.drainResolve=null,this.drainPromise=null),this._startPurgeInterval(),this.metricsCollector?.markCapacityChange(this.permits.capacity),this.metricsCollector?.sampleGauges(Date.now(),this.permits.inFlight,this.queue.size),this.debug&&console.info("[Semaphore] Reset to initial state");}shutdown(e="Semaphore shutdown"){if(!this.isShutdown){this.isShutdown=true,this.debug&&console.info(`[Semaphore] Shutdown: ${e}`),this.purgeIntervalId!==null&&(clearInterval(this.purgeIntervalId),this.purgeIntervalId=null),this._clearTimeout();for(let t of this.queue.toArray())t.discard(new o(e,"SHUTDOWN"));this.queue.clear(),this.enqueueOrder.clear(),this.metricsCollector?.destroy(),this.drainResolve&&(this.drainResolve(),this.drainResolve=null,this.drainPromise=null),this.emit(n.SHUTDOWN,e);}}cancel(){if(this.isShutdown)return;let e=this.queue.toArray();for(let t of e)t.isProbe&&this.circuit.releaseProbeSlot(),t.discard(new o("Semaphore acquire cancelled","CANCELLED")),this._dequeue(t);this.metricsCollector?.sampleQueueDepthAt(Date.now(),this.queue.size),this.debug&&console.info(`[Semaphore] Cancelled ${e.length} queued tasks`),this.schedule();}status(){let e=this.metricsCollector?.getSnapshot()??null,t=e?.windows?.["1m"]?.counts,i=t?.acquired??0,r=t?.timeouts??0,s=this.enqueueOrder.peekHead(),h=s===void 0?0:Math.max(0,Date.now()-s.enqueueTime);return {status:{running:this.permits.capacity-this.permits.available,queued:this.queue.size,available:this.permits.available,inFlight:this.permits.inFlight,pendingReleases:this.pendingReleaseCount,circuitOpen:this.circuit.isOpen,circuitHalfOpen:this.circuit.isHalfOpen,backoffDelay:Math.round(this.backoff.currentDelay),requestsPerSecond:+(i/60).toFixed(2),timeoutRate1m:i+r>0?+(r/(i+r)*100).toFixed(1):0,queueAge:h},lifetime:{totalAcquired:this.totalAcquired,totalReleased:this.totalReleased,totalTimeouts:this.totalTimeouts,circuitBreakerCooldownRemaining:this.circuit.cooldownRemaining},metrics:e}}isAvailable(){return !this.isShutdown&&!this.circuit.isOpen&&!this.permits.isFull}get queueLength(){return this.queue.size}get availablePermits(){return this.permits.available}};exports.CircuitBreaker=b;exports.QUEUE_ORDERINGS=k;exports.Semaphore=_;exports.SemaphoreError=o;exports.SemaphoreEvents=n;//# sourceMappingURL=index.cjs.map | ||
| //# sourceMappingURL=index.cjs.map |
+42
-4
@@ -94,3 +94,38 @@ interface SemaphoreConfig { | ||
| type SemaphoreEventType = typeof SemaphoreEvents[keyof typeof SemaphoreEvents]; | ||
| type SemaphoreErrorCode = 'CIRCUIT_OPEN' | 'CIRCUIT_HALF_OPEN' | 'INVALID_WEIGHT' | 'INVALID_PRIORITY' | 'QUEUE_FULL' | 'TIMEOUT' | 'ABORTED' | 'CANCELLED' | 'SHUTDOWN' | 'PURGED'; | ||
| /** | ||
| * Maps each event to its listener argument tuple, so `on`/`off` give consumers | ||
| * a precisely-typed payload instead of `any`. The tuple form (`[Payload]` or | ||
| * `[]`) lets events carry zero or one argument while keeping the listener | ||
| * signature exact. | ||
| */ | ||
| interface SemaphoreEventMap { | ||
| 'task-acquire': [payload: { | ||
| queued: number; | ||
| running: number; | ||
| probe?: boolean; | ||
| }]; | ||
| 'task-release': [payload: { | ||
| queued: number; | ||
| running: number; | ||
| }]; | ||
| 'task-timeout': [payload: { | ||
| queueLength: number; | ||
| backoffDelay: number; | ||
| taskId: number; | ||
| }]; | ||
| 'task-abort': []; | ||
| 'queue-purge': [task: QueuedTaskView]; | ||
| 'circuit-open': [payload: { | ||
| timeoutRate: number; | ||
| recentTimeouts: number; | ||
| total: number; | ||
| reason?: string; | ||
| }]; | ||
| 'circuit-half-open': []; | ||
| 'circuit-close': []; | ||
| 'shutdown': [reason: string]; | ||
| } | ||
| /** Listener signature for a given event, derived from {@link SemaphoreEventMap}. */ | ||
| type SemaphoreEventListener<E extends SemaphoreEventType> = (...args: SemaphoreEventMap[E]) => void; | ||
| type SemaphoreErrorCode = 'CIRCUIT_OPEN' | 'CIRCUIT_HALF_OPEN' | 'INVALID_ARGUMENT' | 'INVALID_WEIGHT' | 'INVALID_PRIORITY' | 'QUEUE_FULL' | 'TIMEOUT' | 'ABORTED' | 'CANCELLED' | 'SHUTDOWN' | 'PURGED'; | ||
| interface EventWindowSnapshot { | ||
@@ -162,4 +197,4 @@ acquired: number; | ||
| constructor(count: number, config?: SemaphoreConfig); | ||
| on(event: SemaphoreEventType, listener: (...args: any[]) => void): void; | ||
| off(event: SemaphoreEventType, listener: (...args: any[]) => void): void; | ||
| on<E extends SemaphoreEventType>(event: E, listener: SemaphoreEventListener<E>): void; | ||
| off<E extends SemaphoreEventType>(event: E, listener: SemaphoreEventListener<E>): void; | ||
| removeAllListeners(event?: SemaphoreEventType): void; | ||
@@ -210,2 +245,5 @@ private hasListeners; | ||
| * Event listeners are preserved unless { clearListeners: true } is passed. | ||
| * | ||
| * Throws SemaphoreError('SHUTDOWN') if the semaphore has been shut down: | ||
| * shutdown() is terminal and cannot be reversed by reset(). | ||
| */ | ||
@@ -314,2 +352,2 @@ reset(options?: { | ||
| export { CircuitBreaker, type CircuitBreakerConfig, type CircuitTripResult, type Comparator, QUEUE_ORDERINGS, type QueueOrder, type QueuedTaskView, Semaphore, type SemaphoreConfig, SemaphoreError, type SemaphoreErrorCode, type SemaphoreEventType, SemaphoreEvents, type SemaphoreMetricsSnapshot, type SemaphoreMetricsWindowSnapshot }; | ||
| export { CircuitBreaker, type CircuitBreakerConfig, type CircuitTripResult, type Comparator, QUEUE_ORDERINGS, type QueueOrder, type QueuedTaskView, Semaphore, type SemaphoreConfig, SemaphoreError, type SemaphoreErrorCode, type SemaphoreEventListener, type SemaphoreEventMap, type SemaphoreEventType, SemaphoreEvents, type SemaphoreMetricsSnapshot, type SemaphoreMetricsWindowSnapshot }; |
+42
-4
@@ -94,3 +94,38 @@ interface SemaphoreConfig { | ||
| type SemaphoreEventType = typeof SemaphoreEvents[keyof typeof SemaphoreEvents]; | ||
| type SemaphoreErrorCode = 'CIRCUIT_OPEN' | 'CIRCUIT_HALF_OPEN' | 'INVALID_WEIGHT' | 'INVALID_PRIORITY' | 'QUEUE_FULL' | 'TIMEOUT' | 'ABORTED' | 'CANCELLED' | 'SHUTDOWN' | 'PURGED'; | ||
| /** | ||
| * Maps each event to its listener argument tuple, so `on`/`off` give consumers | ||
| * a precisely-typed payload instead of `any`. The tuple form (`[Payload]` or | ||
| * `[]`) lets events carry zero or one argument while keeping the listener | ||
| * signature exact. | ||
| */ | ||
| interface SemaphoreEventMap { | ||
| 'task-acquire': [payload: { | ||
| queued: number; | ||
| running: number; | ||
| probe?: boolean; | ||
| }]; | ||
| 'task-release': [payload: { | ||
| queued: number; | ||
| running: number; | ||
| }]; | ||
| 'task-timeout': [payload: { | ||
| queueLength: number; | ||
| backoffDelay: number; | ||
| taskId: number; | ||
| }]; | ||
| 'task-abort': []; | ||
| 'queue-purge': [task: QueuedTaskView]; | ||
| 'circuit-open': [payload: { | ||
| timeoutRate: number; | ||
| recentTimeouts: number; | ||
| total: number; | ||
| reason?: string; | ||
| }]; | ||
| 'circuit-half-open': []; | ||
| 'circuit-close': []; | ||
| 'shutdown': [reason: string]; | ||
| } | ||
| /** Listener signature for a given event, derived from {@link SemaphoreEventMap}. */ | ||
| type SemaphoreEventListener<E extends SemaphoreEventType> = (...args: SemaphoreEventMap[E]) => void; | ||
| type SemaphoreErrorCode = 'CIRCUIT_OPEN' | 'CIRCUIT_HALF_OPEN' | 'INVALID_ARGUMENT' | 'INVALID_WEIGHT' | 'INVALID_PRIORITY' | 'QUEUE_FULL' | 'TIMEOUT' | 'ABORTED' | 'CANCELLED' | 'SHUTDOWN' | 'PURGED'; | ||
| interface EventWindowSnapshot { | ||
@@ -162,4 +197,4 @@ acquired: number; | ||
| constructor(count: number, config?: SemaphoreConfig); | ||
| on(event: SemaphoreEventType, listener: (...args: any[]) => void): void; | ||
| off(event: SemaphoreEventType, listener: (...args: any[]) => void): void; | ||
| on<E extends SemaphoreEventType>(event: E, listener: SemaphoreEventListener<E>): void; | ||
| off<E extends SemaphoreEventType>(event: E, listener: SemaphoreEventListener<E>): void; | ||
| removeAllListeners(event?: SemaphoreEventType): void; | ||
@@ -210,2 +245,5 @@ private hasListeners; | ||
| * Event listeners are preserved unless { clearListeners: true } is passed. | ||
| * | ||
| * Throws SemaphoreError('SHUTDOWN') if the semaphore has been shut down: | ||
| * shutdown() is terminal and cannot be reversed by reset(). | ||
| */ | ||
@@ -314,2 +352,2 @@ reset(options?: { | ||
| export { CircuitBreaker, type CircuitBreakerConfig, type CircuitTripResult, type Comparator, QUEUE_ORDERINGS, type QueueOrder, type QueuedTaskView, Semaphore, type SemaphoreConfig, SemaphoreError, type SemaphoreErrorCode, type SemaphoreEventType, SemaphoreEvents, type SemaphoreMetricsSnapshot, type SemaphoreMetricsWindowSnapshot }; | ||
| export { CircuitBreaker, type CircuitBreakerConfig, type CircuitTripResult, type Comparator, QUEUE_ORDERINGS, type QueueOrder, type QueuedTaskView, Semaphore, type SemaphoreConfig, SemaphoreError, type SemaphoreErrorCode, type SemaphoreEventListener, type SemaphoreEventMap, type SemaphoreEventType, SemaphoreEvents, type SemaphoreMetricsSnapshot, type SemaphoreMetricsWindowSnapshot }; |
+1
-1
@@ -1,2 +0,2 @@ | ||
| var n=(a,e,t,i,r=!0,s=!0)=>{if(typeof a!="number"||Number.isNaN(a))throw new Error(`${e} must be a valid number`);if(r&&!Number.isInteger(a))throw new Error(`${e} must be an integer`);if(s?a<t:a<=t)throw new Error(`${e} must be ${s?">=":">"} ${t}`);if(s?a>i:a>=i)throw new Error(`${e} must be ${s?"<=":"<"} ${i}`);return a};var o=class extends Error{constructor(e,t){super(e),this.name="SemaphoreError",this.code=t}};var T=class{constructor(e){this._inFlight=0;this.capacity=e,this._available=e}get available(){return this._available}get inFlight(){return this._inFlight}get isFull(){return this._available===0}hasCapacityFor(e){return this._available>=e}acquire(e=1){this._available-=e,this._inFlight+=e}release(e=1){this._available=Math.min(this._available+e,this.capacity),this._inFlight=this._inFlight<e?0:this._inFlight-e}assertInvariant(e){if(!e)return;let t=this._inFlight+this._available;t!==this.capacity&&console.error(`[Semaphore] Invariant violation: inFlight(${this._inFlight}) + available(${this._available}) = ${t}, expected capacity(${this.capacity})`)}reset(){this._available=this.capacity,this._inFlight=0}};var A=class{constructor(e,t){this.size=e,this.stepMs=t,this.buckets=new Int32Array(e*2),this.timestamps=new Float64Array(e)}idx(){let e=Date.now(),t=Math.floor(e/this.stepMs)*this.stepMs,i=Math.floor(t/this.stepMs)%this.size;return this.timestamps[i]!==t&&(this.timestamps[i]=t,this.buckets[i*2]=0,this.buckets[i*2+1]=0),i}addAcquired(){this.buckets[this.idx()*2]++}addTimeout(){this.buckets[this.idx()*2+1]++}snapshot(){let e=Date.now(),t=Math.floor(e/this.stepMs)*this.stepMs-(this.size-1)*this.stepMs,i=0,r=0;for(let s=0;s<this.size;s++)this.timestamps[s]>=t&&(i+=this.buckets[s*2],r+=this.buckets[s*2+1]);return{acquired:i,timeouts:r}}reset(){this.buckets.fill(0),this.timestamps.fill(0)}},b=class{constructor(e={}){this.state="closed";this.openUntil=0;this.probeInFlight=!1;this._probeTaskId=null;if(this.threshold=n(e.threshold??.5,"CircuitBreaker threshold",0,1,!1,!1),this.window=n(e.window??1e4,"CircuitBreaker window",1e3,Number.MAX_SAFE_INTEGER,!0,!0),this.cooldown=n(e.cooldown??5e3,"CircuitBreaker cooldown",1e3,Number.MAX_SAFE_INTEGER,!0,!0),this.minThroughput=n(e.minThroughput??10,"CircuitBreaker minThroughput",1,Number.MAX_SAFE_INTEGER,!0,!0),this.minFailures=n(e.minFailures??5,"CircuitBreaker minFailures",1,Number.MAX_SAFE_INTEGER,!0,!0),this.eventWindow=new A(Math.ceil(this.window/1e3),1e3),this.minThroughput<this.minFailures)throw new Error("CircuitBreaker minThroughput must be >= minFailures")}get isClosed(){return this.state==="closed"}get isOpen(){return this.state==="open"}get isHalfOpen(){return this.state==="half-open"}get hasProbeInFlight(){return this.probeInFlight}get probeTaskId(){return this._probeTaskId}get cooldownRemaining(){return this.isOpen?Math.max(0,this.openUntil-Date.now()):0}trackAttempt(){this.state==="closed"&&this.eventWindow.addAcquired()}recordTimeout(){this.eventWindow.addTimeout()}checkAndTransition(){return this.state!=="open"||Date.now()<this.openUntil?!1:(this.state="half-open",this.probeInFlight=!1,this._probeTaskId=null,!0)}evaluateAndTrip(){if(this.state!=="closed")return{tripped:!1};let{timeouts:e,acquired:t}=this.eventWindow.snapshot();if(t<this.minThroughput||e<this.minFailures)return{tripped:!1};let i=e/t;return i<this.threshold?{tripped:!1}:(this.state="open",this.openUntil=Date.now()+this.cooldown,{tripped:!0,timeoutRate:i,failures:e,attempts:t})}markProbeInFlight(){this.probeInFlight=!0}claimProbeSlot(e){this.probeInFlight=!0,this._probeTaskId=e}releaseProbeSlot(){this.probeInFlight=!1,this._probeTaskId=null}handleProbeSuccess(){this.state="closed",this.probeInFlight=!1,this._probeTaskId=null,this.eventWindow.reset()}handleProbeFailure(){this.state="open",this.probeInFlight=!1,this._probeTaskId=null,this.openUntil=Date.now()+this.cooldown}reset(){this.state="closed",this.openUntil=0,this.probeInFlight=!1,this._probeTaskId=null,this.eventWindow.reset()}};var w=class{constructor(e={}){this.delay=0;this.lastTimestamp=0;if(this.initialTimeout=n(e.initialTimeout??50,"BackoffTracker initialTimeout",0,Number.MAX_SAFE_INTEGER,!0,!0),this.maxTimeout=n(e.maxTimeout??2e3,"BackoffTracker maxTimeout",0,Number.MAX_SAFE_INTEGER,!0,!0),this.decayFactor=n(e.decayFactor??.5,"BackoffTracker decayFactor",0,1,!1,!1),this.maxTimeout<this.initialTimeout)throw new Error("BackoffTracker maxTimeout must be >= initialTimeout")}get currentDelay(){if(this.delay===0)return 0;let e=(Date.now()-this.lastTimestamp)/1e3;if(e<=0)return this.delay;let t=this.delay*Math.pow(this.decayFactor,e);return t<1?0:t}onTimeout(){let e=this.currentDelay,t=e>0?e*2:this.initialTimeout;this.delay=Math.min(t,this.maxTimeout),this.lastTimestamp=Date.now()}reset(){this.delay=0,this.lastTimestamp=0}};var y=class{constructor(e){this.prev=null;this.next=null;this.heapIndex=-1;this.completed=!1;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()),!0):!1}discard(e){return this.claim()?(this._reject(e),!0):!1}claim(){return this.completed?!1:(this.completed=!0,this.abortSignal&&this.abortListener&&this.abortSignal.removeEventListener("abort",this.abortListener),!0)}reject(e){this._reject(e)}};var q=class{constructor(e){this.heap=[];this.compare=e}get size(){return this.heap.length}isEmpty(){return this.heap.length===0}peek(){return this.heap[0]}toArray(){return[...this.heap]}has(e){return e.heapIndex>=0&&this.heap[e.heapIndex]===e}clear(){for(let e of this.heap)e.heapIndex=-1;this.heap=[]}insert(e){if(e.heapIndex>=0)throw new Error("Item is already in a heap");let t=this.heap.length;this.heap.push(e),e.heapIndex=t,this._bubbleUp(t)}pop(){if(this.heap.length===0)return;let e=this.heap[0];e.heapIndex=-1;let t=this.heap.pop();return this.heap.length>0&&t!==e&&(this.heap[0]=t,t.heapIndex=0,this._bubbleDown(0)),e}delete(e){let t=e.heapIndex;if(t<0||this.heap[t]!==e)return;e.heapIndex=-1;let i=this.heap.pop();if(t===this.heap.length||i===e)return e;this.heap[t]=i,i.heapIndex=t;let r=this.heap[t-1>>1];return t>0&&this.compare(i,r)<0?this._bubbleUp(t):this._bubbleDown(t),e}_swap(e,t){let i=this.heap[e],r=this.heap[t];this.heap[e]=r,this.heap[t]=i,i.heapIndex=t,r.heapIndex=e}_bubbleUp(e){for(;e>0;){let t=e-1>>1;if(this.compare(this.heap[e],this.heap[t])<0)this._swap(e,t),e=t;else break}}_bubbleDown(e){let t=this.heap.length;for(;;){let i=e,r=2*e+1,s=2*e+2;if(r<t&&this.compare(this.heap[r],this.heap[i])<0&&(i=r),s<t&&this.compare(this.heap[s],this.heap[i])<0&&(i=s),i===e)break;this._swap(e,i),e=i}}};var S=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 C=class{constructor(e,t){if(e<=0)throw new Error("CombinedWindow size must be > 0");this.size=e,this.stepMs=t,this.timestamps=new Float64Array(e),this.acquired=new Int32Array(e),this.released=new Int32Array(e),this.timeouts=new Int32Array(e),this.ifSum=new Float64Array(e),this.ifCount=new Int32Array(e),this.ifMax=new Float64Array(e),this.qSum=new Float64Array(e),this.qCount=new Int32Array(e),this.qMax=new Float64Array(e),this.latSum=new Float64Array(e),this.latCount=new Int32Array(e)}bucket(e){let t=Math.floor(e/this.stepMs),i=t*this.stepMs,r=t%this.size;return this.timestamps[r]!==i&&(this.timestamps[r]=i,this.acquired[r]=0,this.released[r]=0,this.timeouts[r]=0,this.ifSum[r]=0,this.ifCount[r]=0,this.ifMax[r]=0,this.qSum[r]=0,this.qCount[r]=0,this.qMax[r]=0,this.latSum[r]=0,this.latCount[r]=0),r}addInflight(e,t){this.ifSum[e]+=t,this.ifCount[e]++,t>this.ifMax[e]&&(this.ifMax[e]=t)}addQueue(e,t){this.qSum[e]+=t,this.qCount[e]++,t>this.qMax[e]&&(this.qMax[e]=t)}recordAcquire(e,t,i){let r=this.bucket(e);this.acquired[r]++,this.addInflight(r,t),this.addQueue(r,i)}recordAcquireQueued(e,t,i,r){let s=this.bucket(e);this.acquired[s]++,this.latSum[s]+=t,this.latCount[s]++,this.addInflight(s,i),this.addQueue(s,r)}recordRelease(e,t,i){let r=this.bucket(e);this.released[r]++,this.addInflight(r,t),this.addQueue(r,i)}recordTimeoutQueue(e,t){let i=this.bucket(e);this.timeouts[i]++,this.addQueue(i,t)}sampleBoth(e,t,i){let r=this.bucket(e);this.addInflight(r,t),this.addQueue(r,i)}addAcquired(e){this.acquired[this.bucket(e)]++}addReleased(e){this.released[this.bucket(e)]++}addTimeout(e){this.timeouts[this.bucket(e)]++}addLatency(e,t){let i=this.bucket(e);this.latSum[i]+=t,this.latCount[i]++}sampleInflight(e,t){this.addInflight(this.bucket(e),t)}sampleQueue(e,t){this.addQueue(this.bucket(e),t)}snapshot(e){let t=Math.floor(e/this.stepMs)*this.stepMs-(this.size-1)*this.stepMs,i=0,r=0,s=0,h=0,c=0,m=0,f=0,p=0,d=0,I=0,v=0;for(let l=0;l<this.size;l++)this.timestamps[l]<t||(i+=this.acquired[l],r+=this.released[l],s+=this.timeouts[l],h+=this.ifSum[l],c+=this.ifCount[l],this.ifMax[l]>m&&(m=this.ifMax[l]),f+=this.qSum[l],p+=this.qCount[l],this.qMax[l]>d&&(d=this.qMax[l]),I+=this.latSum[l],v+=this.latCount[l]);return{counts:{acquired:i,released:r,timeouts:s},inflight:{avg:c===0?0:h/c,max:m,samples:c},queue:{avg:p===0?0:f/p,max:d,samples:p},latency:{avg:v===0?0:I/v,count:v,total:I}}}reset(){this.timestamps.fill(0),this.acquired.fill(0),this.released.fill(0),this.timeouts.fill(0),this.ifSum.fill(0),this.ifCount.fill(0),this.ifMax.fill(0),this.qSum.fill(0),this.qCount.fill(0),this.qMax.fill(0),this.latSum.fill(0),this.latCount.fill(0)}},M=[{size:60,stepMs:1e3},{size:60,stepMs:5e3},{size:60,stepMs:15e3},{size:60,stepMs:6e4},{size:60,stepMs:144e4}],g=class{constructor(e=M){this._totalAcquiredFast=0;this._totalAcquiredQueued=0;this._totalReleased=0;this._totalTimeouts=0;this._totalAborts=0;this._capacity=0;this._circuitOpen=!1;this._circuitHalfOpen=!1;if(e.length===0)throw new Error("SemaphoreMetrics requires at least one WindowOptions");this.windows=e.map(({size:t,stepMs:i})=>new C(t,i)),this.windowLabels=e.map(({size:t,stepMs:i})=>{let r=t*i;return r>=36e5&&r%36e5===0?`${r/36e5}h`:r>=6e4&&r%6e4===0?`${r/6e4}m`:r>=1e3&&r%1e3===0?`${r/1e3}s`:`${r}ms`})}onAcquireFast(e,t,i){this._totalAcquiredFast++;for(let r of this.windows)r.recordAcquire(e,t,i)}onAcquireQueued(e,t,i,r){this._totalAcquiredQueued++;for(let s of this.windows)s.recordAcquireQueued(e,t,i,r)}onRelease(e,t,i){this._totalReleased++;for(let r of this.windows)r.recordRelease(e,t,i)}onTimeout(e,t){this._totalTimeouts++;for(let i of this.windows)i.recordTimeoutQueue(e,t)}onAbort(e,t){this._totalAborts++;for(let i of this.windows)i.sampleQueue(e,t)}sampleGauges(e,t,i){for(let r of this.windows)r.sampleBoth(e,t,i)}sampleQueueDepthAt(e,t){for(let i of this.windows)i.sampleQueue(e,t)}markAcquireFast(){this._totalAcquiredFast++;let e=Date.now();for(let t of this.windows)t.addAcquired(e)}markAcquireQueued(e){this._totalAcquiredQueued++;let t=Date.now();for(let i of this.windows)i.addAcquired(t),i.addLatency(t,e)}markReleased(){this._totalReleased++;let e=Date.now();for(let t of this.windows)t.addReleased(e)}markTimeout(){this._totalTimeouts++;let e=Date.now();for(let t of this.windows)t.addTimeout(e)}markAbort(){this._totalAborts++}sampleInFlight(e){let t=Date.now();for(let i of this.windows)i.sampleInflight(t,e)}sampleQueueDepth(e){let t=Date.now();for(let i of this.windows)i.sampleQueue(t,e)}markCapacityChange(e){this._capacity=e}markCircuitOpen(){this._circuitOpen=!0,this._circuitHalfOpen=!1}markCircuitHalfOpen(){this._circuitOpen=!1,this._circuitHalfOpen=!0}markCircuitClose(){this._circuitOpen=!1,this._circuitHalfOpen=!1}getSnapshot(){let e=Date.now(),t={},i=0,r=0;for(let s=0;s<this.windowLabels.length;s++){let h=this.windows[s].snapshot(e);t[this.windowLabels[s]]=h,s===0&&(i=h.inflight.avg,r=h.queue.avg)}return{windows:t,meta:{inFlightLastMinute:Math.round(i),queueDepthLastMinute:Math.round(r),totalAcquiredFast:this._totalAcquiredFast,totalAcquiredQueued:this._totalAcquiredQueued,totalReleased:this._totalReleased,totalTimeouts:this._totalTimeouts,totalAborts:this._totalAborts,capacity:this._capacity,circuitOpen:this._circuitOpen,circuitHalfOpen:this._circuitHalfOpen}}}reset(){for(let e of this.windows)e.reset();this._totalAcquiredFast=0,this._totalAcquiredQueued=0,this._totalReleased=0,this._totalTimeouts=0,this._totalAborts=0,this._capacity=0,this._circuitOpen=!1,this._circuitHalfOpen=!1}destroy(){this.reset()}};var k={fifo:(a,e)=>a.id-e.id,lifo:(a,e)=>e.id-a.id,fifoWithPriority:(a,e)=>a.priority-e.priority||a.id-e.id,lifoWithPriority:(a,e)=>a.priority-e.priority||e.id-a.id};function x(a){if(a.comparator!==void 0){if(typeof a.comparator!="function")throw new Error("Semaphore comparator must be a function");return a.comparator}let e=a.queueOrder??"fifoWithPriority",t=k[e];if(t===void 0)throw new Error(`Semaphore queueOrder must be one of: ${Object.keys(k).join(", ")} (got ${JSON.stringify(e)})`);return t}function E(a){let e=x(a);return(t,i)=>{if(t.isProbe!==i.isProbe)return t.isProbe?-1:1;let r=e(t,i);return typeof r!="number"||Number.isNaN(r)?t.id-i.id:r}}var u={TASKACQUIRE:"task-acquire",TASKRELEASE:"task-release",TASKTIMEOUT:"task-timeout",TASKABORT:"task-abort",QUEUEPURGE:"queue-purge",CIRCUITOPEN:"circuit-open",CIRCUITHALFOPEN:"circuit-half-open",CIRCUITCLOSE:"circuit-close",SHUTDOWN:"shutdown"};var _=class{constructor(e,t={}){this.pendingReleaseCount=0;this.releaseGeneration=0;this.scheduled=!1;this.isShutdown=!1;this.taskIdCounter=0;this.totalAcquired=0;this.totalReleased=0;this.totalTimeouts=0;this.eventListeners=new Map;this.drainPromise=null;this.drainResolve=null;this.purgeIntervalId=null;this.timeoutTimerId=null;n(e,"Semaphore count",0,Number.MAX_SAFE_INTEGER,!0,!1),this.purgeIntervalMs=n(t.purgeIntervalMs??3e3,"Semaphore purgeIntervalMs",500,Number.MAX_SAFE_INTEGER,!0,!0),this.queueMaxTimeout=n(t.queueMaxTimeout??1e4,"Semaphore queueMaxTimeout",1,Number.MAX_SAFE_INTEGER,!0,!0),this.queueMaxLength=n(t.queueMaxLength??1024,"Semaphore queueMaxLength",1,Number.MAX_SAFE_INTEGER,!0,!0),this.queueMaxAge=n(t.queueMaxAge??3e4,"Semaphore queueMaxAge",1,Number.MAX_SAFE_INTEGER,!0,!0),this.rejectOnFull=t.rejectOnFull??!1,this.metricsEnabled=t.metricsEnabled??!0,this.debug=t.debug??!1;let i=E({queueOrder:t.queueOrder,comparator:t.comparator});this.queue=new q(i),this.enqueueOrder=new S,this.permits=new T(e),this.circuit=new b({threshold:t.circuitBreakerThreshold,window:t.circuitBreakerWindow,cooldown:t.circuitBreakerCooldown,minThroughput:t.circuitBreakerMinThroughput,minFailures:t.circuitBreakerMinFailures}),this.backoff=new w({initialTimeout:t.backoffInitialTimeout,maxTimeout:t.backoffMaxTimeout,decayFactor:t.backoffDecayFactor}),this.metricsCollector=this.metricsEnabled?new g: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))for(let r of Array.from(i))try{r(...t)}catch(s){this.debug&&console.warn(`[Semaphore] Error in listener for "${e}":`,s)}}_tryAcquireFast(e){return this.circuit.isHalfOpen?this.circuit.hasProbeInFlight||!this.permits.hasCapacityFor(e)?null:(this.circuit.markProbeInFlight(),this.permits.acquire(e),this.totalAcquired++,this.metricsCollector?.onAcquireFast(Date.now(),this.permits.inFlight,this.queue.size),this.hasListeners(u.TASKACQUIRE)&&this.emit(u.TASKACQUIRE,{queued:this.queue.size,running:this.permits.capacity-this.permits.available,probe:!0}),this._createRelease(!0,e)):this.queue.size>0||!this.permits.hasCapacityFor(e)?null:(this.permits.acquire(e),this.totalAcquired++,this.metricsCollector?.onAcquireFast(Date.now(),this.permits.inFlight,this.queue.size),this.hasListeners(u.TASKACQUIRE)&&this.emit(u.TASKACQUIRE,{queued:this.queue.size,running:this.permits.capacity-this.permits.available}),this._createRelease(!1,e))}_createRelease(e=!1,t=1){this.pendingReleaseCount++;let i=this.releaseGeneration,r=!1;return()=>{if(r||i!==this.releaseGeneration){this.debug&&console.warn("[Semaphore] release() called after already released (no-op)");return}r=!0,this.pendingReleaseCount--,this.permits.release(t),this.totalReleased++,this.metricsCollector?.onRelease(Date.now(),this.permits.inFlight,this.queue.size),e&&this.circuit.isHalfOpen&&(this.circuit.handleProbeSuccess(),this.emit(u.CIRCUITCLOSE),this.metricsCollector?.markCircuitClose(),this.debug&&console.info("[Semaphore] Circuit closed after successful probe")),this.permits.assertInvariant(this.debug),this.hasListeners(u.TASKRELEASE)&&this.emit(u.TASKRELEASE,{queued:this.queue.size,running:this.permits.capacity-this.permits.available}),this.schedule()}}_enqueue(e){this.queue.insert(e),this.enqueueOrder.pushTail(e),this._armTimeout()}_dequeue(e){this.queue.delete(e)!==void 0&&(this.enqueueOrder.remove(e),this.enqueueOrder.size===0&&this._clearTimeout())}_armTimeout(){if(this.timeoutTimerId!==null)return;let e=this.enqueueOrder.peekHead();if(e===void 0)return;let t=Math.max(0,e.enqueueTime+this.queueMaxTimeout-Date.now());this.timeoutTimerId=setTimeout(()=>this._fireTimeout(),t)}_clearTimeout(){this.timeoutTimerId!==null&&(clearTimeout(this.timeoutTimerId),this.timeoutTimerId=null)}_fireTimeout(){if(this.timeoutTimerId=null,this.isShutdown)return;let e=Date.now(),t=0,i=this.enqueueOrder.peekHead();for(;i!==void 0&&i.enqueueTime+this.queueMaxTimeout<=e;){let r=i.claim();this._dequeue(i),r&&(this._timeoutTask(i),t++),i=this.enqueueOrder.peekHead()}if(i!==void 0){let r=Math.max(0,i.enqueueTime+this.queueMaxTimeout-e);this.timeoutTimerId=setTimeout(()=>this._fireTimeout(),r)}t>0&&this.schedule()}_timeoutTask(e){if(this.totalTimeouts++,this.circuit.recordTimeout(),this.backoff.onTimeout(),e.isProbe)this.circuit.handleProbeFailure(),this.emit(u.CIRCUITOPEN,{timeoutRate:1,recentTimeouts:1,total:1,reason:"half-open-probe-failed"}),this.metricsCollector?.markCircuitOpen(),this.debug&&console.warn("[Semaphore] Circuit re-opened: half-open probe timed out");else{let t=this.circuit.evaluateAndTrip();t.tripped&&(this.emit(u.CIRCUITOPEN,{timeoutRate:t.timeoutRate,recentTimeouts:t.failures,total:t.attempts}),this.metricsCollector?.markCircuitOpen(),this.debug&&console.warn(`[Semaphore] Circuit opened. Rate: ${(t.timeoutRate*100).toFixed(1)}%`))}this.metricsCollector?.onTimeout(Date.now(),this.queue.size),this.emit(u.TASKTIMEOUT,{queueLength:this.queue.size,backoffDelay:this.backoff.currentDelay,taskId:e.id}),this.debug&&console.warn(`[Semaphore] Task #${e.id} timed out after ${this.queueMaxTimeout}ms`),e.reject(new o(`Semaphore acquire timed out after ${this.queueMaxTimeout}ms (queue: ${this.queue.size})`,"TIMEOUT"))}_onTaskAbort(e,t){this._dequeue(e),e.isProbe&&this.circuit.releaseProbeSlot(),this.metricsCollector?.onAbort(Date.now(),this.queue.size),this.emit(u.TASKABORT),this.debug&&console.info(`[Semaphore] Task #${e.id} aborted`),t(new o("Semaphore acquire aborted","ABORTED")),this.schedule()}schedule(){if(this.scheduled||this.isShutdown)return;this.scheduled=!0;let e=this.backoff.currentDelay;e>0?setTimeout(()=>{this._runScheduler()},e).unref?.():queueMicrotask(()=>{this._runScheduler()})}_runScheduler(){if(!this.isShutdown){this.scheduled=!1;try{for(;this.queue.size>0&&!this.circuit.isOpen;){let e=this.queue.peek();if(!e||this.circuit.isHalfOpen&&e.id!==this.circuit.probeTaskId||!this.permits.hasCapacityFor(e.weight))break;let t=this.queue.pop();this.enqueueOrder.remove(t),this.enqueueOrder.size===0&&this._clearTimeout();let i=Date.now(),r=Math.max(0,i-t.enqueueTime);this.permits.acquire(t.weight),this.totalAcquired++,this.metricsCollector?.onAcquireQueued(i,r,this.permits.inFlight,this.queue.size),this.permits.assertInvariant(this.debug),t.dispatch(()=>this._createRelease(t.isProbe,t.weight))&&this.hasListeners(u.TASKACQUIRE)&&this.emit(u.TASKACQUIRE,{queued:this.queue.size,running:this.permits.capacity-this.permits.available,...t.isProbe?{probe:!0}:{}})}this.queue.size===0&&this.permits.available===this.permits.capacity&&this.drainResolve&&(this.drainResolve(),this.drainResolve=null,this.drainPromise=null)}catch(e){e instanceof Error?console.error("[Semaphore] Scheduler error:",e.message,e.stack):console.error("[Semaphore] Scheduler error:",e)}}}_startPurgeInterval(){this.purgeIntervalId!==null&&clearInterval(this.purgeIntervalId),this.purgeIntervalId=setInterval(()=>{this.isShutdown||this._purgeStaleTasks()},this.purgeIntervalMs),this.purgeIntervalId.unref?.()}_purgeStaleTasks(){let e=Date.now(),t=this.queue.size,i=this.enqueueOrder.peekHead();for(;i!==void 0&&e-i.enqueueTime>this.queueMaxAge;){this.circuit.probeTaskId===i.id&&this.circuit.releaseProbeSlot();let r=i.discard(new o(`Task purged after ${this.queueMaxAge}ms`,"PURGED"));this._dequeue(i),r&&(this.totalTimeouts++,this.metricsCollector?.onTimeout(Date.now(),this.queue.size),this.emit(u.QUEUEPURGE,i),this.debug&&console.warn(`[Semaphore] Purged stale task #${i.id}`)),i=this.enqueueOrder.peekHead()}this.debug&&this.queue.size<t&&console.info(`[Semaphore] Purged ${t-this.queue.size} stale tasks`),this.queue.size<t&&this.schedule()}tryAcquire(e=1){return this.isShutdown||!Number.isInteger(e)||e<1||e>this.permits.capacity||(this.circuit.checkAndTransition()&&(this.emit(u.CIRCUITHALFOPEN),this.metricsCollector?.markCircuitHalfOpen(),this.debug&&console.info("[Semaphore] Circuit entering half-open"),this.schedule()),this.circuit.isOpen)?null:(this.circuit.trackAttempt(),this._tryAcquireFast(e))}acquire(e,t=0,i=1){if(this.isShutdown)return Promise.reject(new o("Semaphore is shut down","SHUTDOWN"));if(!Number.isInteger(i)||i<1||i>this.permits.capacity)return Promise.reject(new o(`Invalid weight: ${i} (must be integer in 1..${this.permits.capacity})`,"INVALID_WEIGHT"));if(typeof t!="number"||!Number.isFinite(t))return Promise.reject(new o(`Invalid priority: ${t} (must be a finite number)`,"INVALID_PRIORITY"));if(this.circuit.checkAndTransition()&&(this.emit(u.CIRCUITHALFOPEN),this.metricsCollector?.markCircuitHalfOpen(),this.debug&&console.info("[Semaphore] Circuit entering half-open"),this.schedule()),this.circuit.isOpen)return Promise.reject(new o(`Circuit breaker open, retry in ${this.circuit.cooldownRemaining}ms`,"CIRCUIT_OPEN"));if(this.circuit.isHalfOpen&&this.circuit.hasProbeInFlight)return Promise.reject(new o("Circuit breaker half-open, probe in flight","CIRCUIT_HALF_OPEN"));if(e?.aborted)return Promise.reject(new o("Semaphore acquire aborted before start","ABORTED"));this.circuit.trackAttempt();let r=this._tryAcquireFast(i);if(r)return Promise.resolve(r);let s=this.circuit.isHalfOpen&&!this.circuit.hasProbeInFlight;return!s&&this.rejectOnFull?Promise.reject(new o("Semaphore at capacity (rejectOnFull)","QUEUE_FULL")):!s&&this.queue.size>=this.queueMaxLength?Promise.reject(new o(`Queue full (${this.queueMaxLength})`,"QUEUE_FULL")):new Promise((h,c)=>{let m=++this.taskIdCounter,f=Date.now(),p=s,d=new y({id:m,priority:p?Number.MIN_SAFE_INTEGER:t,enqueueTime:f,isProbe:p,resolve:h,reject:c,abortSignal:e,weight:i});d.arm(()=>this._onTaskAbort(d,c)),p&&this.circuit.claimProbeSlot(m),this.metricsCollector?.sampleQueueDepthAt(Date.now(),this.queue.size+1),this._enqueue(d),this.schedule()})}async use(e,t,i=0,r=1){let s=await this.acquire(t,i,r);try{return await e()}finally{s()}}drain(e){return this.isShutdown?Promise.reject(new o("Cannot drain: semaphore is shut down","SHUTDOWN")):(e!==void 0&&n(e,"drain timeoutMs",1,Number.MAX_SAFE_INTEGER,!0,!0),this.drainPromise?this.drainPromise:this.queue.size===0&&this.permits.available===this.permits.capacity?Promise.resolve():(this.drainPromise=new Promise((t,i)=>{if(this.drainResolve=t,e!==void 0){let r=setTimeout(()=>{this.drainResolve=null,this.drainPromise=null,i(new o(`drain() timed out after ${e}ms`,"TIMEOUT"))},e),s=this.drainResolve;this.drainResolve=()=>{clearTimeout(r),s()}}}),this.drainPromise))}reset(e={}){for(let t of this.queue.toArray())t.discard(new o("Semaphore reset","SHUTDOWN"));this.queue.clear(),this.enqueueOrder.clear(),this._clearTimeout(),this.metricsCollector?.reset(),this.permits.reset(),this.backoff.reset(),this.circuit.reset(),this.releaseGeneration++,this.pendingReleaseCount=0,this.scheduled=!1,this.isShutdown=!1,this.taskIdCounter=0,this.totalAcquired=0,this.totalReleased=0,this.totalTimeouts=0,e.clearListeners&&this.eventListeners.clear(),this.drainResolve&&(this.drainResolve(),this.drainResolve=null,this.drainPromise=null),this._startPurgeInterval(),this.metricsCollector?.markCapacityChange(this.permits.capacity),this.metricsCollector?.sampleGauges(Date.now(),this.permits.inFlight,this.queue.size),this.debug&&console.info("[Semaphore] Reset to initial state")}shutdown(e="Semaphore shutdown"){if(!this.isShutdown){this.isShutdown=!0,this.debug&&console.info(`[Semaphore] Shutdown: ${e}`),this.purgeIntervalId!==null&&(clearInterval(this.purgeIntervalId),this.purgeIntervalId=null),this._clearTimeout();for(let t of this.queue.toArray())t.discard(new o(e,"SHUTDOWN"));this.queue.clear(),this.enqueueOrder.clear(),this.metricsCollector?.destroy(),this.drainResolve&&(this.drainResolve(),this.drainResolve=null,this.drainPromise=null),this.emit(u.SHUTDOWN,e)}}cancel(){if(this.isShutdown)return;let e=this.queue.toArray();for(let t of e)t.isProbe&&this.circuit.releaseProbeSlot(),t.discard(new o("Semaphore acquire cancelled","CANCELLED")),this._dequeue(t);this.metricsCollector?.sampleQueueDepthAt(Date.now(),this.queue.size),this.debug&&console.info(`[Semaphore] Cancelled ${e.length} queued tasks`),this.schedule()}status(){let e=this.metricsCollector?.getSnapshot()??null,t=e?.windows?.["1m"]?.counts,i=t?.acquired??0,r=t?.timeouts??0,s=this.enqueueOrder.peekHead(),h=s===void 0?0:Math.max(0,Date.now()-s.enqueueTime);return{status:{running:this.permits.capacity-this.permits.available,queued:this.queue.size,available:this.permits.available,inFlight:this.permits.inFlight,pendingReleases:this.pendingReleaseCount,circuitOpen:this.circuit.isOpen,circuitHalfOpen:this.circuit.isHalfOpen,backoffDelay:Math.round(this.backoff.currentDelay),requestsPerSecond:+(i/60).toFixed(2),timeoutRate1m:i+r>0?+(r/(i+r)*100).toFixed(1):0,queueAge:h},lifetime:{totalAcquired:this.totalAcquired,totalReleased:this.totalReleased,totalTimeouts:this.totalTimeouts,circuitBreakerCooldownRemaining:this.circuit.cooldownRemaining},metrics:e}}isAvailable(){return!this.isShutdown&&!this.circuit.isOpen&&!this.permits.isFull}get queueLength(){return this.queue.size}get availablePermits(){return this.permits.available}};export{b as CircuitBreaker,k as QUEUE_ORDERINGS,_ as Semaphore,o as SemaphoreError,u as SemaphoreEvents}; | ||
| var o=class extends Error{constructor(e,t){super(e),this.name="SemaphoreError",this.code=t;}};var u=(a,e,t,i,r=true,s=true)=>{if(typeof a!="number"||Number.isNaN(a))throw new o(`${e} must be a valid number`,"INVALID_ARGUMENT");if(r&&!Number.isInteger(a))throw new o(`${e} must be an integer`,"INVALID_ARGUMENT");if(s?a<t:a<=t)throw new o(`${e} must be ${s?">=":">"} ${t}`,"INVALID_ARGUMENT");if(s?a>i:a>=i)throw new o(`${e} must be ${s?"<=":"<"} ${i}`,"INVALID_ARGUMENT");return a};var T=class{constructor(e){this._inFlight=0;this.capacity=e,this._available=e;}get available(){return this._available}get inFlight(){return this._inFlight}get isFull(){return this._available===0}hasCapacityFor(e){return this._available>=e}acquire(e=1){this._available-=e,this._inFlight+=e;}release(e=1){this._available=Math.min(this._available+e,this.capacity),this._inFlight=this._inFlight<e?0:this._inFlight-e;}assertInvariant(e){if(!e)return;let t=this._inFlight+this._available;t!==this.capacity&&console.error(`[Semaphore] Invariant violation: inFlight(${this._inFlight}) + available(${this._available}) = ${t}, expected capacity(${this.capacity})`);}reset(){this._available=this.capacity,this._inFlight=0;}};var A=class{constructor(e,t){this.size=e,this.stepMs=t,this.buckets=new Int32Array(e*2),this.timestamps=new Float64Array(e);}idx(){let e=Date.now(),t=Math.floor(e/this.stepMs)*this.stepMs,i=Math.floor(t/this.stepMs)%this.size;return this.timestamps[i]!==t&&(this.timestamps[i]=t,this.buckets[i*2]=0,this.buckets[i*2+1]=0),i}addAcquired(){this.buckets[this.idx()*2]++;}addTimeout(){this.buckets[this.idx()*2+1]++;}snapshot(){let e=Date.now(),t=Math.floor(e/this.stepMs)*this.stepMs-(this.size-1)*this.stepMs,i=0,r=0;for(let s=0;s<this.size;s++)this.timestamps[s]>=t&&(i+=this.buckets[s*2],r+=this.buckets[s*2+1]);return {acquired:i,timeouts:r}}reset(){this.buckets.fill(0),this.timestamps.fill(0);}},b=class{constructor(e={}){this.state="closed";this.openUntil=0;this.probeInFlight=false;this._probeTaskId=null;if(this.threshold=u(e.threshold??.5,"CircuitBreaker threshold",0,1,false,false),this.window=u(e.window??1e4,"CircuitBreaker window",1e3,Number.MAX_SAFE_INTEGER,true,true),this.cooldown=u(e.cooldown??5e3,"CircuitBreaker cooldown",1e3,Number.MAX_SAFE_INTEGER,true,true),this.minThroughput=u(e.minThroughput??10,"CircuitBreaker minThroughput",1,Number.MAX_SAFE_INTEGER,true,true),this.minFailures=u(e.minFailures??5,"CircuitBreaker minFailures",1,Number.MAX_SAFE_INTEGER,true,true),this.eventWindow=new A(Math.ceil(this.window/1e3),1e3),this.minThroughput<this.minFailures)throw new o("CircuitBreaker minThroughput must be >= minFailures","INVALID_ARGUMENT")}get isClosed(){return this.state==="closed"}get isOpen(){return this.state==="open"}get isHalfOpen(){return this.state==="half-open"}get hasProbeInFlight(){return this.probeInFlight}get probeTaskId(){return this._probeTaskId}get cooldownRemaining(){return this.isOpen?Math.max(0,this.openUntil-Date.now()):0}trackAttempt(){this.state==="closed"&&this.eventWindow.addAcquired();}recordTimeout(){this.eventWindow.addTimeout();}checkAndTransition(){return this.state!=="open"||Date.now()<this.openUntil?false:(this.state="half-open",this.probeInFlight=false,this._probeTaskId=null,true)}evaluateAndTrip(){if(this.state!=="closed")return {tripped:false};let{timeouts:e,acquired:t}=this.eventWindow.snapshot();if(t<this.minThroughput||e<this.minFailures)return {tripped:false};let i=e/t;return i<this.threshold?{tripped:false}:(this.state="open",this.openUntil=Date.now()+this.cooldown,{tripped:true,timeoutRate:i,failures:e,attempts:t})}markProbeInFlight(){this.probeInFlight=true;}claimProbeSlot(e){this.probeInFlight=true,this._probeTaskId=e;}releaseProbeSlot(){this.probeInFlight=false,this._probeTaskId=null;}handleProbeSuccess(){this.state="closed",this.probeInFlight=false,this._probeTaskId=null,this.eventWindow.reset();}handleProbeFailure(){this.state="open",this.probeInFlight=false,this._probeTaskId=null,this.openUntil=Date.now()+this.cooldown;}reset(){this.state="closed",this.openUntil=0,this.probeInFlight=false,this._probeTaskId=null,this.eventWindow.reset();}};var w=class{constructor(e={}){this.delay=0;this.lastTimestamp=0;if(this.initialTimeout=u(e.initialTimeout??50,"BackoffTracker initialTimeout",0,Number.MAX_SAFE_INTEGER,true,true),this.maxTimeout=u(e.maxTimeout??2e3,"BackoffTracker maxTimeout",0,Number.MAX_SAFE_INTEGER,true,true),this.decayFactor=u(e.decayFactor??.5,"BackoffTracker decayFactor",0,1,false,false),this.maxTimeout<this.initialTimeout)throw new o("BackoffTracker maxTimeout must be >= initialTimeout","INVALID_ARGUMENT")}get currentDelay(){if(this.delay===0)return 0;let e=(Date.now()-this.lastTimestamp)/1e3;if(e<=0)return this.delay;let t=this.delay*Math.pow(this.decayFactor,e);return t<1?0:t}onTimeout(){let e=this.currentDelay,t=e>0?e*2:this.initialTimeout;this.delay=Math.min(t,this.maxTimeout),this.lastTimestamp=Date.now();}reset(){this.delay=0,this.lastTimestamp=0;}};var y=class{constructor(e){this.prev=null;this.next=null;this.heapIndex=-1;this.completed=false;this.id=e.id,this.priority=e.priority,this.enqueueTime=e.enqueueTime,this.isProbe=e.isProbe,this.weight=e.weight??1,this._resolve=e.resolve,this._reject=e.reject,this.abortSignal=e.abortSignal;}arm(e){this.abortSignal&&(this.abortListener=()=>{this.claim()&&e();},this.abortSignal.addEventListener("abort",this.abortListener));}dispatch(e){return this.claim()?(this._resolve(e()),true):false}discard(e){return this.claim()?(this._reject(e),true):false}claim(){return this.completed?false:(this.completed=true,this.abortSignal&&this.abortListener&&this.abortSignal.removeEventListener("abort",this.abortListener),true)}reject(e){this._reject(e);}};var S=class{constructor(e){this.heap=[];this.compare=e;}get size(){return this.heap.length}isEmpty(){return this.heap.length===0}peek(){return this.heap[0]}toArray(){return [...this.heap]}has(e){return e.heapIndex>=0&&this.heap[e.heapIndex]===e}clear(){for(let e of this.heap)e.heapIndex=-1;this.heap=[];}insert(e){if(e.heapIndex>=0)throw new Error("Item is already in a heap");let t=this.heap.length;this.heap.push(e),e.heapIndex=t,this._bubbleUp(t);}pop(){if(this.heap.length===0)return;let e=this.heap[0];e.heapIndex=-1;let t=this.heap.pop();return this.heap.length>0&&t!==e&&(this.heap[0]=t,t.heapIndex=0,this._bubbleDown(0)),e}delete(e){let t=e.heapIndex;if(t<0||this.heap[t]!==e)return;e.heapIndex=-1;let i=this.heap.pop();if(t===this.heap.length||i===e)return e;this.heap[t]=i,i.heapIndex=t;let r=this.heap[t-1>>1];return t>0&&this.compare(i,r)<0?this._bubbleUp(t):this._bubbleDown(t),e}_swap(e,t){let i=this.heap[e],r=this.heap[t];this.heap[e]=r,this.heap[t]=i,i.heapIndex=t,r.heapIndex=e;}_bubbleUp(e){for(;e>0;){let t=e-1>>1;if(this.compare(this.heap[e],this.heap[t])<0)this._swap(e,t),e=t;else break}}_bubbleDown(e){let t=this.heap.length;for(;;){let i=e,r=2*e+1,s=2*e+2;if(r<t&&this.compare(this.heap[r],this.heap[i])<0&&(i=r),s<t&&this.compare(this.heap[s],this.heap[i])<0&&(i=s),i===e)break;this._swap(e,i),e=i;}}};var q=class{constructor(){this.head=null;this.tail=null;this._size=0;}get size(){return this._size}isEmpty(){return this._size===0}peekHead(){return this.head===null?void 0:this.head}pushTail(e){e.prev=this.tail,e.next=null,this.tail!==null?this.tail.next=e:this.head=e,this.tail=e,this._size++;}remove(e){e.prev!==null?e.prev.next=e.next:this.head=e.next,e.next!==null?e.next.prev=e.prev:this.tail=e.prev,e.prev=e.next=null,this._size--;}clear(){this.head=this.tail=null,this._size=0;}};var E=class{constructor(e,t){if(e<=0)throw new Error("CombinedWindow size must be > 0");this.size=e,this.stepMs=t,this.timestamps=new Float64Array(e),this.acquired=new Int32Array(e),this.released=new Int32Array(e),this.timeouts=new Int32Array(e),this.ifSum=new Float64Array(e),this.ifCount=new Int32Array(e),this.ifMax=new Float64Array(e),this.qSum=new Float64Array(e),this.qCount=new Int32Array(e),this.qMax=new Float64Array(e),this.latSum=new Float64Array(e),this.latCount=new Int32Array(e);}bucket(e){let t=Math.floor(e/this.stepMs),i=t*this.stepMs,r=t%this.size;return this.timestamps[r]!==i&&(this.timestamps[r]=i,this.acquired[r]=0,this.released[r]=0,this.timeouts[r]=0,this.ifSum[r]=0,this.ifCount[r]=0,this.ifMax[r]=0,this.qSum[r]=0,this.qCount[r]=0,this.qMax[r]=0,this.latSum[r]=0,this.latCount[r]=0),r}addInflight(e,t){this.ifSum[e]+=t,this.ifCount[e]++,t>this.ifMax[e]&&(this.ifMax[e]=t);}addQueue(e,t){this.qSum[e]+=t,this.qCount[e]++,t>this.qMax[e]&&(this.qMax[e]=t);}recordAcquire(e,t,i){let r=this.bucket(e);this.acquired[r]++,this.addInflight(r,t),this.addQueue(r,i);}recordAcquireQueued(e,t,i,r){let s=this.bucket(e);this.acquired[s]++,this.latSum[s]+=t,this.latCount[s]++,this.addInflight(s,i),this.addQueue(s,r);}recordRelease(e,t,i){let r=this.bucket(e);this.released[r]++,this.addInflight(r,t),this.addQueue(r,i);}recordTimeoutQueue(e,t){let i=this.bucket(e);this.timeouts[i]++,this.addQueue(i,t);}sampleBoth(e,t,i){let r=this.bucket(e);this.addInflight(r,t),this.addQueue(r,i);}addAcquired(e){this.acquired[this.bucket(e)]++;}addReleased(e){this.released[this.bucket(e)]++;}addTimeout(e){this.timeouts[this.bucket(e)]++;}addLatency(e,t){let i=this.bucket(e);this.latSum[i]+=t,this.latCount[i]++;}sampleInflight(e,t){this.addInflight(this.bucket(e),t);}sampleQueue(e,t){this.addQueue(this.bucket(e),t);}snapshot(e){let t=Math.floor(e/this.stepMs)*this.stepMs-(this.size-1)*this.stepMs,i=0,r=0,s=0,h=0,p=0,m=0,f=0,c=0,d=0,g=0,v=0;for(let l=0;l<this.size;l++)this.timestamps[l]<t||(i+=this.acquired[l],r+=this.released[l],s+=this.timeouts[l],h+=this.ifSum[l],p+=this.ifCount[l],this.ifMax[l]>m&&(m=this.ifMax[l]),f+=this.qSum[l],c+=this.qCount[l],this.qMax[l]>d&&(d=this.qMax[l]),g+=this.latSum[l],v+=this.latCount[l]);return {counts:{acquired:i,released:r,timeouts:s},inflight:{avg:p===0?0:h/p,max:m,samples:p},queue:{avg:c===0?0:f/c,max:d,samples:c},latency:{avg:v===0?0:g/v,count:v,total:g}}}reset(){this.timestamps.fill(0),this.acquired.fill(0),this.released.fill(0),this.timeouts.fill(0),this.ifSum.fill(0),this.ifCount.fill(0),this.ifMax.fill(0),this.qSum.fill(0),this.qCount.fill(0),this.qMax.fill(0),this.latSum.fill(0),this.latCount.fill(0);}},M=[{size:60,stepMs:1e3},{size:60,stepMs:5e3},{size:60,stepMs:15e3},{size:60,stepMs:6e4},{size:60,stepMs:144e4}],I=class{constructor(e=M){this._totalAcquiredFast=0;this._totalAcquiredQueued=0;this._totalReleased=0;this._totalTimeouts=0;this._totalAborts=0;this._capacity=0;this._circuitOpen=false;this._circuitHalfOpen=false;if(e.length===0)throw new Error("SemaphoreMetrics requires at least one WindowOptions");this.windows=e.map(({size:t,stepMs:i})=>new E(t,i)),this.windowLabels=e.map(({size:t,stepMs:i})=>{let r=t*i;return r>=36e5&&r%36e5===0?`${r/36e5}h`:r>=6e4&&r%6e4===0?`${r/6e4}m`:r>=1e3&&r%1e3===0?`${r/1e3}s`:`${r}ms`});}onAcquireFast(e,t,i){this._totalAcquiredFast++;for(let r of this.windows)r.recordAcquire(e,t,i);}onAcquireQueued(e,t,i,r){this._totalAcquiredQueued++;for(let s of this.windows)s.recordAcquireQueued(e,t,i,r);}onRelease(e,t,i){this._totalReleased++;for(let r of this.windows)r.recordRelease(e,t,i);}onTimeout(e,t){this._totalTimeouts++;for(let i of this.windows)i.recordTimeoutQueue(e,t);}onAbort(e,t){this._totalAborts++;for(let i of this.windows)i.sampleQueue(e,t);}sampleGauges(e,t,i){for(let r of this.windows)r.sampleBoth(e,t,i);}sampleQueueDepthAt(e,t){for(let i of this.windows)i.sampleQueue(e,t);}markAcquireFast(){this._totalAcquiredFast++;let e=Date.now();for(let t of this.windows)t.addAcquired(e);}markAcquireQueued(e){this._totalAcquiredQueued++;let t=Date.now();for(let i of this.windows)i.addAcquired(t),i.addLatency(t,e);}markReleased(){this._totalReleased++;let e=Date.now();for(let t of this.windows)t.addReleased(e);}markTimeout(){this._totalTimeouts++;let e=Date.now();for(let t of this.windows)t.addTimeout(e);}markAbort(){this._totalAborts++;}sampleInFlight(e){let t=Date.now();for(let i of this.windows)i.sampleInflight(t,e);}sampleQueueDepth(e){let t=Date.now();for(let i of this.windows)i.sampleQueue(t,e);}markCapacityChange(e){this._capacity=e;}markCircuitOpen(){this._circuitOpen=true,this._circuitHalfOpen=false;}markCircuitHalfOpen(){this._circuitOpen=false,this._circuitHalfOpen=true;}markCircuitClose(){this._circuitOpen=false,this._circuitHalfOpen=false;}getSnapshot(){let e=Date.now(),t={},i=0,r=0;for(let s=0;s<this.windowLabels.length;s++){let h=this.windows[s].snapshot(e);t[this.windowLabels[s]]=h,s===0&&(i=h.inflight.avg,r=h.queue.avg);}return {windows:t,meta:{inFlightLastMinute:Math.round(i),queueDepthLastMinute:Math.round(r),totalAcquiredFast:this._totalAcquiredFast,totalAcquiredQueued:this._totalAcquiredQueued,totalReleased:this._totalReleased,totalTimeouts:this._totalTimeouts,totalAborts:this._totalAborts,capacity:this._capacity,circuitOpen:this._circuitOpen,circuitHalfOpen:this._circuitHalfOpen}}}reset(){for(let e of this.windows)e.reset();this._totalAcquiredFast=0,this._totalAcquiredQueued=0,this._totalReleased=0,this._totalTimeouts=0,this._totalAborts=0,this._capacity=0,this._circuitOpen=false,this._circuitHalfOpen=false;}destroy(){this.reset();}};var k={fifo:(a,e)=>a.id-e.id,lifo:(a,e)=>e.id-a.id,fifoWithPriority:(a,e)=>a.priority-e.priority||a.id-e.id,lifoWithPriority:(a,e)=>a.priority-e.priority||e.id-a.id};function x(a){if(a.comparator!==void 0){if(typeof a.comparator!="function")throw new o("Semaphore comparator must be a function","INVALID_ARGUMENT");return a.comparator}let e=a.queueOrder??"fifoWithPriority",t=k[e];if(t===void 0)throw new o(`Semaphore queueOrder must be one of: ${Object.keys(k).join(", ")} (got ${JSON.stringify(e)})`,"INVALID_ARGUMENT");return t}function C(a){let e=x(a);return (t,i)=>{if(t.isProbe!==i.isProbe)return t.isProbe?-1:1;let r=e(t,i);return typeof r!="number"||Number.isNaN(r)?t.id-i.id:r}}var n={TASKACQUIRE:"task-acquire",TASKRELEASE:"task-release",TASKTIMEOUT:"task-timeout",TASKABORT:"task-abort",QUEUEPURGE:"queue-purge",CIRCUITOPEN:"circuit-open",CIRCUITHALFOPEN:"circuit-half-open",CIRCUITCLOSE:"circuit-close",SHUTDOWN:"shutdown"};var _=class{constructor(e,t={}){this.pendingReleaseCount=0;this.releaseGeneration=0;this.scheduled=false;this.isShutdown=false;this.taskIdCounter=0;this.totalAcquired=0;this.totalReleased=0;this.totalTimeouts=0;this.eventListeners=new Map;this.drainPromise=null;this.drainResolve=null;this.purgeIntervalId=null;this.timeoutTimerId=null;u(e,"Semaphore count",0,Number.MAX_SAFE_INTEGER,true,false),this.purgeIntervalMs=u(t.purgeIntervalMs??3e3,"Semaphore purgeIntervalMs",500,Number.MAX_SAFE_INTEGER,true,true),this.queueMaxTimeout=u(t.queueMaxTimeout??1e4,"Semaphore queueMaxTimeout",1,Number.MAX_SAFE_INTEGER,true,true),this.queueMaxLength=u(t.queueMaxLength??1024,"Semaphore queueMaxLength",1,Number.MAX_SAFE_INTEGER,true,true),this.queueMaxAge=u(t.queueMaxAge??3e4,"Semaphore queueMaxAge",1,Number.MAX_SAFE_INTEGER,true,true),this.rejectOnFull=t.rejectOnFull??false,this.metricsEnabled=t.metricsEnabled??true,this.debug=t.debug??false;let i=C({queueOrder:t.queueOrder,comparator:t.comparator});this.queue=new S(i),this.enqueueOrder=new q,this.permits=new T(e),this.circuit=new b({threshold:t.circuitBreakerThreshold,window:t.circuitBreakerWindow,cooldown:t.circuitBreakerCooldown,minThroughput:t.circuitBreakerMinThroughput,minFailures:t.circuitBreakerMinFailures}),this.backoff=new w({initialTimeout:t.backoffInitialTimeout,maxTimeout:t.backoffMaxTimeout,decayFactor:t.backoffDecayFactor}),this.metricsCollector=this.metricsEnabled?new I:void 0,this.metricsCollector?.markCapacityChange(this.permits.capacity),this.metricsCollector?.sampleGauges(Date.now(),this.permits.inFlight,this.queue.size),this._startPurgeInterval();}on(e,t){this.eventListeners.has(e)||this.eventListeners.set(e,new Set),this.eventListeners.get(e).add(t);}off(e,t){this.eventListeners.get(e)?.delete(t),this.eventListeners.get(e)?.size===0&&this.eventListeners.delete(e);}removeAllListeners(e){e?this.eventListeners.delete(e):this.eventListeners.clear();}hasListeners(e){let t=this.eventListeners.get(e);return t!==void 0&&t.size>0}emit(e,...t){let i=this.eventListeners.get(e);if(!(!i||i.size===0)){if(i.size===1){let r=i.values().next().value;try{r(...t);}catch(s){this.debug&&console.warn(`[Semaphore] Error in listener for "${e}":`,s);}return}for(let r of Array.from(i))try{r(...t);}catch(s){this.debug&&console.warn(`[Semaphore] Error in listener for "${e}":`,s);}}}_tryAcquireFast(e){return this.circuit.isHalfOpen?this.circuit.hasProbeInFlight||!this.permits.hasCapacityFor(e)?null:(this.circuit.markProbeInFlight(),this.permits.acquire(e),this.totalAcquired++,this.metricsCollector?.onAcquireFast(Date.now(),this.permits.inFlight,this.queue.size),this.hasListeners(n.TASKACQUIRE)&&this.emit(n.TASKACQUIRE,{queued:this.queue.size,running:this.permits.capacity-this.permits.available,probe:true}),this._createRelease(true,e)):this.queue.size>0||!this.permits.hasCapacityFor(e)?null:(this.permits.acquire(e),this.totalAcquired++,this.metricsCollector?.onAcquireFast(Date.now(),this.permits.inFlight,this.queue.size),this.hasListeners(n.TASKACQUIRE)&&this.emit(n.TASKACQUIRE,{queued:this.queue.size,running:this.permits.capacity-this.permits.available}),this._createRelease(false,e))}_createRelease(e=false,t=1){this.pendingReleaseCount++;let i=this.releaseGeneration,r=false;return ()=>{if(r||i!==this.releaseGeneration){this.debug&&console.warn("[Semaphore] release() called after already released (no-op)");return}r=true,this.pendingReleaseCount--,this.permits.release(t),this.totalReleased++,this.metricsCollector?.onRelease(Date.now(),this.permits.inFlight,this.queue.size),e&&this.circuit.isHalfOpen&&(this.circuit.handleProbeSuccess(),this.emit(n.CIRCUITCLOSE),this.metricsCollector?.markCircuitClose(),this.debug&&console.info("[Semaphore] Circuit closed after successful probe")),this.permits.assertInvariant(this.debug),this.hasListeners(n.TASKRELEASE)&&this.emit(n.TASKRELEASE,{queued:this.queue.size,running:this.permits.capacity-this.permits.available}),this.schedule();}}_enqueue(e){this.queue.insert(e),this.enqueueOrder.pushTail(e),this._armTimeout();}_dequeue(e){this.queue.delete(e)!==void 0&&(this.enqueueOrder.remove(e),this.enqueueOrder.size===0&&this._clearTimeout());}_armTimeout(){if(this.timeoutTimerId!==null)return;let e=this.enqueueOrder.peekHead();if(e===void 0)return;let t=Math.max(0,e.enqueueTime+this.queueMaxTimeout-Date.now());this.timeoutTimerId=setTimeout(()=>this._fireTimeout(),t);}_clearTimeout(){this.timeoutTimerId!==null&&(clearTimeout(this.timeoutTimerId),this.timeoutTimerId=null);}_fireTimeout(){if(this.timeoutTimerId=null,this.isShutdown)return;let e=Date.now(),t=0,i=this.enqueueOrder.peekHead();for(;i!==void 0&&i.enqueueTime+this.queueMaxTimeout<=e;){let r=i.claim();this._dequeue(i),r&&(this._timeoutTask(i),t++),i=this.enqueueOrder.peekHead();}if(i!==void 0){let r=Math.max(0,i.enqueueTime+this.queueMaxTimeout-e);this.timeoutTimerId=setTimeout(()=>this._fireTimeout(),r);}t>0&&this.schedule();}_timeoutTask(e){if(this.totalTimeouts++,this.circuit.recordTimeout(),this.backoff.onTimeout(),e.isProbe)this.circuit.handleProbeFailure(),this.emit(n.CIRCUITOPEN,{timeoutRate:1,recentTimeouts:1,total:1,reason:"half-open-probe-failed"}),this.metricsCollector?.markCircuitOpen(),this.debug&&console.warn("[Semaphore] Circuit re-opened: half-open probe timed out");else {let t=this.circuit.evaluateAndTrip();t.tripped&&(this.emit(n.CIRCUITOPEN,{timeoutRate:t.timeoutRate,recentTimeouts:t.failures,total:t.attempts}),this.metricsCollector?.markCircuitOpen(),this.debug&&console.warn(`[Semaphore] Circuit opened. Rate: ${(t.timeoutRate*100).toFixed(1)}%`));}this.metricsCollector?.onTimeout(Date.now(),this.queue.size),this.emit(n.TASKTIMEOUT,{queueLength:this.queue.size,backoffDelay:this.backoff.currentDelay,taskId:e.id}),this.debug&&console.warn(`[Semaphore] Task #${e.id} timed out after ${this.queueMaxTimeout}ms`),e.reject(new o(`Semaphore acquire timed out after ${this.queueMaxTimeout}ms (queue: ${this.queue.size})`,"TIMEOUT"));}_onTaskAbort(e,t){this._dequeue(e),e.isProbe&&this.circuit.releaseProbeSlot(),this.metricsCollector?.onAbort(Date.now(),this.queue.size),this.emit(n.TASKABORT),this.debug&&console.info(`[Semaphore] Task #${e.id} aborted`),t(new o("Semaphore acquire aborted","ABORTED")),this.schedule();}schedule(){if(this.scheduled||this.isShutdown)return;this.scheduled=true;let e=this.backoff.currentDelay;e>0?setTimeout(()=>{this._runScheduler();},e).unref?.():queueMicrotask(()=>{this._runScheduler();});}_runScheduler(){if(!this.isShutdown){this.scheduled=false;try{for(;this.queue.size>0&&!this.circuit.isOpen;){let e=this.queue.peek();if(!e||this.circuit.isHalfOpen&&e.id!==this.circuit.probeTaskId||!this.permits.hasCapacityFor(e.weight))break;let t=this.queue.pop();this.enqueueOrder.remove(t),this.enqueueOrder.size===0&&this._clearTimeout();let i=Date.now(),r=Math.max(0,i-t.enqueueTime);this.permits.acquire(t.weight),this.totalAcquired++,this.metricsCollector?.onAcquireQueued(i,r,this.permits.inFlight,this.queue.size),this.permits.assertInvariant(this.debug),t.dispatch(()=>this._createRelease(t.isProbe,t.weight))&&this.hasListeners(n.TASKACQUIRE)&&this.emit(n.TASKACQUIRE,{queued:this.queue.size,running:this.permits.capacity-this.permits.available,...t.isProbe?{probe:!0}:{}});}this.queue.size===0&&this.permits.available===this.permits.capacity&&this.drainResolve&&(this.drainResolve(),this.drainResolve=null,this.drainPromise=null);}catch(e){e instanceof Error?console.error("[Semaphore] Scheduler error:",e.message,e.stack):console.error("[Semaphore] Scheduler error:",e);}}}_startPurgeInterval(){this.purgeIntervalId!==null&&clearInterval(this.purgeIntervalId),this.purgeIntervalId=setInterval(()=>{this.isShutdown||this._purgeStaleTasks();},this.purgeIntervalMs),this.purgeIntervalId.unref?.();}_purgeStaleTasks(){let e=Date.now(),t=this.queue.size,i=this.enqueueOrder.peekHead();for(;i!==void 0&&e-i.enqueueTime>this.queueMaxAge;){this.circuit.probeTaskId===i.id&&this.circuit.releaseProbeSlot();let r=i.discard(new o(`Task purged after ${this.queueMaxAge}ms`,"PURGED"));this._dequeue(i),r&&(this.totalTimeouts++,this.metricsCollector?.onTimeout(Date.now(),this.queue.size),this.emit(n.QUEUEPURGE,i),this.debug&&console.warn(`[Semaphore] Purged stale task #${i.id}`)),i=this.enqueueOrder.peekHead();}this.debug&&this.queue.size<t&&console.info(`[Semaphore] Purged ${t-this.queue.size} stale tasks`),this.queue.size<t&&this.schedule();}tryAcquire(e=1){return this.isShutdown||!Number.isInteger(e)||e<1||e>this.permits.capacity||(this.circuit.checkAndTransition()&&(this.emit(n.CIRCUITHALFOPEN),this.metricsCollector?.markCircuitHalfOpen(),this.debug&&console.info("[Semaphore] Circuit entering half-open"),this.schedule()),this.circuit.isOpen)?null:(this.circuit.trackAttempt(),this._tryAcquireFast(e))}acquire(e,t=0,i=1){if(this.isShutdown)return Promise.reject(new o("Semaphore is shut down","SHUTDOWN"));if(!Number.isInteger(i)||i<1||i>this.permits.capacity)return Promise.reject(new o(`Invalid weight: ${i} (must be integer in 1..${this.permits.capacity})`,"INVALID_WEIGHT"));if(typeof t!="number"||!Number.isFinite(t))return Promise.reject(new o(`Invalid priority: ${t} (must be a finite number)`,"INVALID_PRIORITY"));if(this.circuit.checkAndTransition()&&(this.emit(n.CIRCUITHALFOPEN),this.metricsCollector?.markCircuitHalfOpen(),this.debug&&console.info("[Semaphore] Circuit entering half-open"),this.schedule()),this.circuit.isOpen)return Promise.reject(new o(`Circuit breaker open, retry in ${this.circuit.cooldownRemaining}ms`,"CIRCUIT_OPEN"));if(this.circuit.isHalfOpen&&this.circuit.hasProbeInFlight)return Promise.reject(new o("Circuit breaker half-open, probe in flight","CIRCUIT_HALF_OPEN"));if(e?.aborted)return Promise.reject(new o("Semaphore acquire aborted before start","ABORTED"));this.circuit.trackAttempt();let r=this._tryAcquireFast(i);if(r)return Promise.resolve(r);let s=this.circuit.isHalfOpen&&!this.circuit.hasProbeInFlight;return !s&&this.rejectOnFull?Promise.reject(new o("Semaphore at capacity (rejectOnFull)","QUEUE_FULL")):!s&&this.queue.size>=this.queueMaxLength?Promise.reject(new o(`Queue full (${this.queueMaxLength})`,"QUEUE_FULL")):new Promise((h,p)=>{let m=++this.taskIdCounter,f=Date.now(),c=s,d=new y({id:m,priority:c?Number.MIN_SAFE_INTEGER:t,enqueueTime:f,isProbe:c,resolve:h,reject:p,abortSignal:e,weight:i});d.arm(()=>this._onTaskAbort(d,p)),c&&this.circuit.claimProbeSlot(m),this.metricsCollector?.sampleQueueDepthAt(Date.now(),this.queue.size+1),this._enqueue(d),this.schedule();})}async use(e,t,i=0,r=1){let s=await this.acquire(t,i,r);try{return await e()}finally{s();}}drain(e){return this.isShutdown?Promise.reject(new o("Cannot drain: semaphore is shut down","SHUTDOWN")):(e!==void 0&&u(e,"drain timeoutMs",1,Number.MAX_SAFE_INTEGER,true,true),this.drainPromise?this.drainPromise:this.queue.size===0&&this.permits.available===this.permits.capacity?Promise.resolve():(this.drainPromise=new Promise((t,i)=>{if(this.drainResolve=t,e!==void 0){let r=setTimeout(()=>{this.drainResolve=null,this.drainPromise=null,i(new o(`drain() timed out after ${e}ms`,"TIMEOUT"));},e),s=this.drainResolve;this.drainResolve=()=>{clearTimeout(r),s();};}}),this.drainPromise))}reset(e={}){if(this.isShutdown)throw new o("Cannot reset a semaphore that has been shut down","SHUTDOWN");for(let t of this.queue.toArray())t.discard(new o("Semaphore reset","SHUTDOWN"));this.queue.clear(),this.enqueueOrder.clear(),this._clearTimeout(),this.metricsCollector?.reset(),this.permits.reset(),this.backoff.reset(),this.circuit.reset(),this.releaseGeneration++,this.pendingReleaseCount=0,this.scheduled=false,this.isShutdown=false,this.taskIdCounter=0,this.totalAcquired=0,this.totalReleased=0,this.totalTimeouts=0,e.clearListeners&&this.eventListeners.clear(),this.drainResolve&&(this.drainResolve(),this.drainResolve=null,this.drainPromise=null),this._startPurgeInterval(),this.metricsCollector?.markCapacityChange(this.permits.capacity),this.metricsCollector?.sampleGauges(Date.now(),this.permits.inFlight,this.queue.size),this.debug&&console.info("[Semaphore] Reset to initial state");}shutdown(e="Semaphore shutdown"){if(!this.isShutdown){this.isShutdown=true,this.debug&&console.info(`[Semaphore] Shutdown: ${e}`),this.purgeIntervalId!==null&&(clearInterval(this.purgeIntervalId),this.purgeIntervalId=null),this._clearTimeout();for(let t of this.queue.toArray())t.discard(new o(e,"SHUTDOWN"));this.queue.clear(),this.enqueueOrder.clear(),this.metricsCollector?.destroy(),this.drainResolve&&(this.drainResolve(),this.drainResolve=null,this.drainPromise=null),this.emit(n.SHUTDOWN,e);}}cancel(){if(this.isShutdown)return;let e=this.queue.toArray();for(let t of e)t.isProbe&&this.circuit.releaseProbeSlot(),t.discard(new o("Semaphore acquire cancelled","CANCELLED")),this._dequeue(t);this.metricsCollector?.sampleQueueDepthAt(Date.now(),this.queue.size),this.debug&&console.info(`[Semaphore] Cancelled ${e.length} queued tasks`),this.schedule();}status(){let e=this.metricsCollector?.getSnapshot()??null,t=e?.windows?.["1m"]?.counts,i=t?.acquired??0,r=t?.timeouts??0,s=this.enqueueOrder.peekHead(),h=s===void 0?0:Math.max(0,Date.now()-s.enqueueTime);return {status:{running:this.permits.capacity-this.permits.available,queued:this.queue.size,available:this.permits.available,inFlight:this.permits.inFlight,pendingReleases:this.pendingReleaseCount,circuitOpen:this.circuit.isOpen,circuitHalfOpen:this.circuit.isHalfOpen,backoffDelay:Math.round(this.backoff.currentDelay),requestsPerSecond:+(i/60).toFixed(2),timeoutRate1m:i+r>0?+(r/(i+r)*100).toFixed(1):0,queueAge:h},lifetime:{totalAcquired:this.totalAcquired,totalReleased:this.totalReleased,totalTimeouts:this.totalTimeouts,circuitBreakerCooldownRemaining:this.circuit.cooldownRemaining},metrics:e}}isAvailable(){return !this.isShutdown&&!this.circuit.isOpen&&!this.permits.isFull}get queueLength(){return this.queue.size}get availablePermits(){return this.permits.available}};export{b as CircuitBreaker,k as QUEUE_ORDERINGS,_ as Semaphore,o as SemaphoreError,n as SemaphoreEvents};//# sourceMappingURL=index.js.map | ||
| //# sourceMappingURL=index.js.map |
+1
-1
| { | ||
| "name": "regulo", | ||
| "version": "1.0.5", | ||
| "version": "1.1.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", |
+89
-87
@@ -1,2 +0,2 @@ | ||
| # ๐๏ธ **Regulo** | ||
| # **Regulo** | ||
@@ -19,3 +19,3 @@ **๐ฅ Control the heat** | ||
| ## โจ Highlights | ||
| ## Highlights | ||
@@ -30,3 +30,3 @@ - **๐๏ธ Bounded concurrency, with priority and weighting** โ set how many burners are lit, send important work to the front, and let one heavy job claim more than one burner. | ||
| ## ๐ฆ Install | ||
| ## Install | ||
@@ -39,3 +39,3 @@ ```bash | ||
| ## ๐ Quick start | ||
| ## Quick start | ||
@@ -54,20 +54,4 @@ ```ts | ||
| ## โ๏ธ How it compares | ||
| ## Core concepts | ||
| **Regulo** overlaps with several well-known libraries but sits at the intersection of bounded concurrency, prioritization, and resilience, with built-in observability. | ||
| | Capability | regulo | p-limit | p-queue | opossum | cockatiel | | ||
| |---|---|---|---|---|---| | ||
| | Bounded concurrency | Yes | Yes | Yes | No | Yes (bulkhead) | | ||
| | Priority queue | Yes | No | Yes | โ | No | | ||
| | Weighted permits | Yes | No | No | โ | No | | ||
| | Circuit breaker | Yes | No | No | Yes | Yes | | ||
| | Adaptive backoff | Yes | No | No | No | No | | ||
| | Windowed metrics | Yes | No | Basic | Yes | No | | ||
| | Dependencies | Zero | Minimal | Minimal | Several | Zero | | ||
| Capabilities reflect each project's commonly documented feature set at the time of writing; check the respective projects for their current state. If you only need a concurrency cap, `p-limit` is smaller and simpler. If you need rich resilience policy composition (retry, timeout, fallback), `cockatiel` is a strong choice. Reach for `regulo` when you want prioritized, weighted concurrency limiting that you can monitor and that protects itself under sustained load. | ||
| ## ๐ฏ Core concepts | ||
| **Semaphore** โ holds a fixed pool of permits (the burners). Callers acquire a permit before doing work and release it when done. When all permits are held, callers queue until one frees up, or until their timeout fires. | ||
@@ -105,3 +89,3 @@ | ||
| ## ๐งญ Choosing an ordering, and its implications | ||
| ## Choosing an Ordering and Its Implications | ||
@@ -128,3 +112,3 @@ Two facts are true of **every** ordering, because they live in the scheduler, not the comparator: | ||
| ## ๐ก How the circuit breaker works | ||
| ## How the Circuit Breaker Works | ||
@@ -140,3 +124,3 @@ The breaker is a **saturation breaker, not a fault breaker**. It watches the rate of *queue-acquisition timeouts* โ callers that waited longer than `queueMaxTimeout` for a permit โ over a sliding window. When that rate crosses `circuitBreakerThreshold` (and the minimum count guards are met), the circuit opens and new requests are rejected immediately with `CIRCUIT_OPEN`. After the cooldown elapses, one probe request is allowed through; if it succeeds the circuit closes, if it times out the circuit re-opens and the cooldown restarts. | ||
| ## ๐ฅ Recipe: Express middleware | ||
| ## Example: Express Middleware | ||
@@ -201,3 +185,3 @@ Cap concurrent handling of an expensive route and shed load with a `503` when the circuit is open or the queue is full: | ||
| ## ๐ API reference | ||
| ## API Reference | ||
@@ -225,7 +209,7 @@ ### `new Semaphore(count, config?)` | ||
| ### `use<T>(fn, abortSignal?, priority?, weight?): Promise<T>` | ||
| #### `use<T>(fn, abortSignal?, priority?, weight?): Promise<T>` | ||
| Preferred entry point. Acquires a permit, runs `fn()`, and releases โ always, even if `fn` throws. | ||
| ### `tryAcquire(weight?): (() => void) | null` | ||
| #### `tryAcquire(weight?): (() => void) | null` | ||
@@ -236,43 +220,43 @@ Non-blocking. Returns a release closure, or `null` if insufficient permits are available **or any tasks are already queued** (head-of-line fairness โ `tryAcquire` never jumps the queue). | ||
| ### `drain(timeoutMs?): Promise<void>` | ||
| #### `drain(timeoutMs?): Promise<void>` | ||
| Resolves when the queue is empty and all permits are returned. Multiple callers share the same promise. Pass `timeoutMs` (a positive integer) to set a deadline โ rejects with `TIMEOUT` if not idle in time; an invalid value throws synchronously. | ||
| Resolves when the queue is empty and all permits are returned. Multiple callers share the same promise. Pass `timeoutMs` (a positive integer) to set a deadline โ rejects with `TIMEOUT` if not idle in time; an invalid value throws `SemaphoreError` (`INVALID_ARGUMENT`) synchronously. Calling `drain()` after `shutdown()` rejects with `SHUTDOWN`. | ||
| > Without `timeoutMs`, `drain()` can block indefinitely if a caller holds a permit and never releases it. | ||
| ### `reset(options?): void` | ||
| #### `reset(options?): void` | ||
| Rejects all queued tasks (`SHUTDOWN`) and restores the semaphore to its initial state. Event listeners are preserved unless `{ clearListeners: true }` is passed. | ||
| Rejects all queued tasks (`SHUTDOWN`) and restores the semaphore to its initial state, so it can be reused. Event listeners are preserved unless `{ clearListeners: true }` is passed. Throws `SemaphoreError` (`SHUTDOWN`) if called after `shutdown()` โ a shut-down instance is terminal and cannot be revived. | ||
| ### `cancel(): void` | ||
| #### `cancel(): void` | ||
| Rejects all currently queued tasks with `CANCELLED`. In-flight permits are unaffected and the semaphore remains fully operational (unlike `shutdown()`). | ||
| ### `shutdown(reason?): void` | ||
| #### `shutdown(reason?): void` | ||
| Permanently stops the semaphore โ kills the gas. All queued tasks are rejected. Unlike `reset()`, this cannot be reversed. | ||
| Permanently stops the semaphore โ kills the gas. All queued tasks are rejected, the purge interval is cleared, and metrics collection stops. This is terminal: it cannot be reversed, and a later `reset()` on a shut-down instance throws rather than reviving it. Calling `shutdown()` again is a no-op. | ||
| ### `on(event, listener) / off(event, listener) / removeAllListeners(event?)` | ||
| #### `on(event, listener) / off(event, listener) / removeAllListeners(event?)` | ||
| Standard event emitter interface. See [Events reference](#events-reference) below. | ||
| Standard event emitter interface. Listeners are fully typed โ the payload type is inferred from the event, so `on(SemaphoreEvents.TASKACQUIRE, p => โฆ)` gives `p` the correct shape with no `any`. See [Events reference](#events-reference) below. | ||
| ### `status()` | ||
| #### `status()` | ||
| Returns a snapshot of current operating state. See [`status()` output](#status-output) for the full shape. | ||
| Returns a snapshot of current operating state. See [Metrics](#metrics) for the full shape. | ||
| > `status()` is O(1) in queue depth โ safe to call on a metrics scrape path. (Queue age is read from an enqueue-ordered index, not by scanning the queue.) | ||
| ### `isAvailable(): boolean` | ||
| #### `isAvailable(): boolean` | ||
| Returns `true` if the semaphore is not shut down, the circuit is not open, and a permit is available. | ||
| ### `queueLength: number` | ||
| #### `queueLength: number` | ||
| Current number of tasks waiting for a permit. | ||
| ### `availablePermits: number` | ||
| #### `availablePermits: number` | ||
| Number of permits not currently held. | ||
| ## โ๏ธ Configuration reference | ||
| ## Configuration Reference | ||
@@ -332,5 +316,5 @@ | Option | Type | Default | Description | | ||
| ## โ Events reference | ||
| ## Events Reference | ||
| Listen with `Semaphore.on(SemaphoreEvents.CIRCUITOPEN, handler)`. | ||
| Listen with `Semaphore.on(SemaphoreEvents.CIRCUITOPEN, handler)`. Payloads are typed per event (see `SemaphoreEventMap`); a handler's argument is inferred from the event constant. | ||
@@ -343,3 +327,3 @@ | Event constant | String value | Payload | | ||
| | `TASKABORT` | `'task-abort'` | none | | ||
| | `QUEUEPURGE` | `'queue-purge'` | `QueuedTask` | | ||
| | `QUEUEPURGE` | `'queue-purge'` | `QueuedTaskView` โ `{ id, priority, enqueueTime, weight }` | | ||
| | `CIRCUITOPEN` | `'circuit-open'` | `{ timeoutRate, recentTimeouts, total, reason? }` | | ||
@@ -350,10 +334,11 @@ | `CIRCUITHALFOPEN` | `'circuit-half-open'` | none | | ||
| ## ๐จ Error codes | ||
| ## Error Codes | ||
| All rejections are `SemaphoreError` instances with a `code` property. | ||
| Every error **Regulo** raises โ whether rejected from a promise or thrown synchronously โ is a `SemaphoreError` instance with a `code` property you can switch on. | ||
| | Code | When thrown | | ||
| | Code | When raised | | ||
| |---|---| | ||
| | `CIRCUIT_OPEN` | Circuit breaker is open | | ||
| | `CIRCUIT_HALF_OPEN` | Circuit is half-open and a probe is already in flight | | ||
| | `INVALID_ARGUMENT` | Invalid constructor/config value, or an invalid `drain()` timeout (thrown synchronously) | | ||
| | `INVALID_WEIGHT` | `weight` is not an integer in `1..count` | | ||
@@ -365,3 +350,3 @@ | `INVALID_PRIORITY` | `priority` is not a finite number | | ||
| | `CANCELLED` | Task was rejected by `cancel()` | | ||
| | `SHUTDOWN` | `shutdown()` or `reset()` was called while the task was queued | | ||
| | `SHUTDOWN` | `shutdown()` or `reset()` was called while the task was queued; or `reset()`/`drain()` was called on an already shut-down instance | | ||
| | `PURGED` | Task was ejected by the stale-task purge interval (`queueMaxAge` exceeded) | | ||
@@ -388,4 +373,5 @@ | ||
| ## `status()` output | ||
| ## Metrics | ||
| Generated with `status()`. | ||
| ```ts | ||
@@ -416,3 +402,3 @@ { | ||
| ## ๐ Standalone `CircuitBreaker` | ||
| ## Standalone `CircuitBreaker` | ||
@@ -453,8 +439,24 @@ `CircuitBreaker` can be used independently โ e.g. to wrap an HTTP client. Unlike the semaphore's saturation breaker, here you decide what counts as a failure by calling `recordTimeout()` on whatever signal you choose: | ||
| ## โก Benchmarks | ||
| ## Feature Comparison | ||
| **Regulo** overlaps with several well-known libraries but sits at the intersection of bounded concurrency, prioritization, and resilience, with built-in observability. | ||
| | Capability | regulo | p-limit | p-queue | opossum | cockatiel | | ||
| |---|---|---|---|---|---| | ||
| | Bounded concurrency | Yes | Yes | Yes | No | Yes (bulkhead) | | ||
| | Priority queue | Yes | No | Yes | โ | No | | ||
| | Weighted permits | Yes | No | No | โ | No | | ||
| | Circuit breaker | Yes | No | No | Yes | Yes | | ||
| | Adaptive backoff | Yes | No | No | No | No | | ||
| | Windowed metrics | Yes | No | Basic | Yes | No | | ||
| | Dependencies | Zero | Minimal | Minimal | Several | Zero | | ||
| Capabilities reflect each project's commonly documented feature set at the time of writing; check the respective projects for their current state. If you only need a concurrency cap, `p-limit` is smaller and simpler. If you need rich resilience policy composition (retry, timeout, fallback), `cockatiel` is a strong choice. Reach for `regulo` when you want prioritized, weighted concurrency limiting that you can monitor and that protects itself under sustained load. | ||
| ## Benchmarks | ||
| Full, reproducible benchmarks live in [`benchmarks/`](./benchmarks) โ run them | ||
| yourself with `npm run benchmark:all`. Figures below are from a real run on | ||
| Node v22.16.0, darwin x64, mid-2018 Intel i9 Macbook Pro; your numbers will differ โ re-run locally. Each | ||
| library from [How it compares](#how-it-compares) is benchmarked only on the | ||
| library from [Feature comparison](#feature-comparison) is benchmarked only on the | ||
| axis it actually shares with **Regulo**: the concurrency limiters on capping | ||
@@ -467,6 +469,6 @@ concurrency, the circuit breakers on per-call overhead. | ||
| |---|--:|---| | ||
| | `tryAcquire` + `release` | 1.96M | 2.40x slower | | ||
| | `tryAcquire` + `release` (no metrics) | 4.71M | fastest | | ||
| | `use()` round-trip | 1.03M | 4.55x slower | | ||
| | `use()` round-trip (no metrics) | 1.49M | 3.15x slower | | ||
| | `tryAcquire` + `release` | 2.22M | 2.29x slower | | ||
| | `tryAcquire` + `release` (no metrics) | 5.10M | fastest | | ||
| | `use()` round-trip | 1.10M | 4.65x slower | | ||
| | `use()` round-trip (no metrics) | 1.70M | 3.01x slower | | ||
@@ -477,5 +479,5 @@ **๐๏ธ Weighted acquire, uncontended** | ||
| |---|--:|---| | ||
| | `use()` weight=1 | 1.05M | fastest | | ||
| | `use()` weight=4 | 1.03M | 1.02x slower | | ||
| | `use()` weight=16 | 1.00M | 1.05x slower | | ||
| | `use()` weight=1 | 1.19M | fastest | | ||
| | `use()` weight=4 | 1.17M | 1.02x slower | | ||
| | `use()` weight=16 | 1.17M | 1.01x slower | | ||
@@ -489,6 +491,6 @@ Weighted permits add no meaningful overhead regardless of weight โ claiming | ||
| |---|--:|---| | ||
| | concurrency=4 | 647.5k | 1.05x slower | | ||
| | concurrency=16 | 669.5k | 1.01x slower | | ||
| | concurrency=64 | 679.0k | fastest | | ||
| | concurrency=16, random priority | 597.2k | 1.14x slower | | ||
| | concurrency=4 | 726.4k | 1.11x slower | | ||
| | concurrency=16 | 779.4k | 1.03x slower | | ||
| | concurrency=64 | 805.5k | fastest | | ||
| | concurrency=16, random priority | 684.2k | 1.18x slower | | ||
@@ -499,5 +501,5 @@ **๐ `status()` snapshot cost** | ||
| |---|--:|---| | ||
| | 0 | 675.9k | 1.02x slower | | ||
| | 100 | 686.9k | fastest | | ||
| | 1000 | 665.9k | 1.03x slower | | ||
| | 0 | 720.7k | 1.02x slower | | ||
| | 100 | 731.9k | fastest | | ||
| | 1000 | 729.4k | 1.00x slower | | ||
@@ -513,7 +515,7 @@ `status()` is O(1) in queue depth โ the cost is flat across queue depths (within | ||
| |---|--:|---| | ||
| | cockatiel (bulkhead) | 3.86M | fastest | | ||
| | p-queue | 1.10M | 3.50x slower | | ||
| | p-limit | 1.09M | 3.55x slower | | ||
| | regulo (no metrics) | 1.46M | 2.65x slower | | ||
| | regulo | 927.7k | 4.16x slower | | ||
| | cockatiel (bulkhead) | 4.00M | fastest | | ||
| | p-limit | 1.21M | 3.32x slower | | ||
| | p-queue | 1.20M | 3.34x slower | | ||
| | regulo (no metrics) | 1.57M | 2.54x slower | | ||
| | regulo | 1.05M | 3.82x slower | | ||
@@ -524,7 +526,7 @@ **๐ regulo vs. other libraries โ contended throughput @ concurrency=16** (tasks/sec) | ||
| |---|--:|---| | ||
| | cockatiel (bulkhead) | 1.64M | fastest | | ||
| | p-queue | 975.6k | 1.68x slower | | ||
| | regulo (no metrics) | 884.1k | 1.85x slower | | ||
| | p-limit | 868.1k | 1.89x slower | | ||
| | regulo | 670.6k | 2.45x slower | | ||
| | cockatiel (bulkhead) | 1.69M | fastest | | ||
| | p-queue | 1.07M | 1.58x slower | | ||
| | regulo (no metrics) | 982.7k | 1.72x slower | | ||
| | p-limit | 870.5k | 1.94x slower | | ||
| | regulo | 753.8k | 2.24x slower | | ||
@@ -535,5 +537,5 @@ **๐ก๏ธ Circuit breaker overhead โ closed/healthy circuit** | ||
| |---|--:|---| | ||
| | regulo `CircuitBreaker` | 3.29M | fastest | | ||
| | cockatiel (circuitBreaker) | 2.42M | 1.36x slower | | ||
| | opossum | 1.46M | 2.26x slower | | ||
| | regulo `CircuitBreaker` | 3.64M | fastest | | ||
| | cockatiel (circuitBreaker) | 2.68M | 1.36x slower | | ||
| | opossum | 1.57M | 2.33x slower | | ||
@@ -552,8 +554,8 @@ 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 ~670k tasks/sec | ||
| downstream API calls, measured in milliseconds. Even at ~750k tasks/sec | ||
| under contention the per-task overhead is a few microseconds against operations | ||
| thousands of times slower. If you only need a bare concurrency cap on cheap | ||
| work in a hot loop, reach for a leaner limiter; see [How it compares](#how-it-compares). | ||
| work in a hot loop, reach for a leaner limiter; see [Feature comparison](#feature-comparison). | ||
| ## ๐งช Test coverage | ||
| ## Test coverage | ||
@@ -573,9 +575,9 @@ ``` | ||
| โ test/list.test.ts (8 tests) | ||
| โ test/semaphore.test.ts (93 tests) | ||
| โ test/semaphore.test.ts (95 tests) | ||
| Test Files 9 passed (9) | ||
| Tests 189 passed (189) | ||
| Tests 191 passed (191) | ||
| ``` | ||
| ## โ ๏ธ Caveats | ||
| ## Caveats | ||
@@ -589,4 +591,4 @@ Before you crank the dial, know where the edges are: | ||
| ## ๐ License | ||
| ## License | ||
| [MIT](./LICENSE) |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
383874
12.69%562
7.87%568
0.35%