@nextn/outbound-guard 0.1.3 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +100 -118
- package/dist/index.cjs +198 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +44 -1
- package/dist/index.d.ts +44 -1
- package/dist/index.js +197 -1
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -1,136 +1,118 @@
|
|
|
1
|
+
````md
|
|
1
2
|
# outbound-guard
|
|
2
3
|
|
|
3
|
-
|
|
4
|
+
A **process-local Node.js library for safe outbound HTTP access**.
|
|
4
5
|
|
|
5
|
-
|
|
6
|
-
- Request coalescing + short-lived GET micro-cache (leader/followers, stale-while-refresh)
|
|
7
|
-
- Per-base-URL limiter with bounded FIFO queue (maxQueue = maxInFlight * 10)
|
|
8
|
-
- Lightweight health gate (OPEN → CLOSED → HALF_OPEN probe) with queue flush on close
|
|
9
|
-
- Hard per-attempt timeouts and leader-only retries (no retry amplification)
|
|
10
|
-
- Zero deps beyond `undici`; entirely in-memory and process-local
|
|
6
|
+
It provides **two independent tools**, each solving a different real-world problem when calling external services:
|
|
11
7
|
|
|
12
|
-
|
|
8
|
+
---
|
|
13
9
|
|
|
14
|
-
|
|
15
|
-
npm install @nextn/outbound-guard
|
|
16
|
-
# Node 18+ (tested on 20)
|
|
17
|
-
```
|
|
10
|
+
## What this library provides
|
|
18
11
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
```ts
|
|
22
|
-
import { ResilientHttpClient } from "@nextn/outbound-guard";
|
|
23
|
-
|
|
24
|
-
const client = new ResilientHttpClient({
|
|
25
|
-
// applied per base URL (protocol + host + port)
|
|
26
|
-
maxInFlight: 20,
|
|
27
|
-
requestTimeoutMs: 5_000,
|
|
28
|
-
|
|
29
|
-
microCache: {
|
|
30
|
-
enabled: true,
|
|
31
|
-
ttlMs: 1_000, // fresh window
|
|
32
|
-
maxStaleMs: 10_000, // serve stale while refreshing
|
|
33
|
-
maxEntries: 500,
|
|
34
|
-
maxWaiters: 1_000, // concurrent followers per key
|
|
35
|
-
followerTimeoutMs: 5_000,
|
|
36
|
-
retry: {
|
|
37
|
-
maxAttempts: 3,
|
|
38
|
-
baseDelayMs: 50,
|
|
39
|
-
maxDelayMs: 200,
|
|
40
|
-
retryOnStatus: [429, 502, 503, 504], // leader-only
|
|
41
|
-
},
|
|
42
|
-
},
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
const res = await client.request({
|
|
46
|
-
method: "GET",
|
|
47
|
-
url: "https://third-party.example.com/config",
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
console.log(res.status, Buffer.from(res.body).toString("utf8"));
|
|
51
|
-
```
|
|
12
|
+
### 1. Resilient HTTP Client (requests under load)
|
|
52
13
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
-
|
|
59
|
-
-
|
|
60
|
-
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
###
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
url: "https://example.com/resource",
|
|
89
|
-
headers?: Record<string, string>,
|
|
90
|
-
body?: string | Uint8Array | Buffer,
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
client.snapshot(); // { inFlight, queueDepth }
|
|
94
|
-
client.on(eventName, handler); // see below for event names
|
|
95
|
-
```
|
|
14
|
+
For **on-demand HTTP calls** made by your application (APIs, BFFs, workers).
|
|
15
|
+
|
|
16
|
+
It protects your service when traffic spikes or upstreams misbehave by combining:
|
|
17
|
+
|
|
18
|
+
- request coalescing (duplicate GET collapse)
|
|
19
|
+
- bounded concurrency + queueing
|
|
20
|
+
- per-attempt timeouts
|
|
21
|
+
- a small health gate (OPEN → CLOSED → HALF_OPEN)
|
|
22
|
+
|
|
23
|
+
This is what you use when **your code actively sends requests**.
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
### 2. Instant GET Store (background polling)
|
|
28
|
+
|
|
29
|
+
For **continuously polling a single GET endpoint** and serving its latest value instantly.
|
|
30
|
+
|
|
31
|
+
It runs in the background, fetches on a fixed interval, and lets your code read the most recent value with zero latency.
|
|
32
|
+
|
|
33
|
+
This is what you use when **your code consumes external state**.
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
These two features are independent but complementary.
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## Feature overview
|
|
42
|
+
|
|
43
|
+
| Feature | Purpose | Typical use |
|
|
44
|
+
|------|------|------|
|
|
45
|
+
| **ResilientHttpClient** | Protect outbound requests under load | APIs, gateways, webhook senders |
|
|
46
|
+
| **InstantGetStore** | Continuously poll & cache one GET endpoint | config flags, prices, health endpoints |
|
|
47
|
+
|
|
48
|
+
---
|
|
96
49
|
|
|
97
|
-
|
|
98
|
-
All exported errors extend `ResilientHttpError`:
|
|
99
|
-
- `QueueFullError` – queue capacity hit for that base URL.
|
|
100
|
-
- `RequestTimeoutError` – per-attempt timeout exceeded.
|
|
101
|
-
- `UpstreamUnhealthyError` – circuit is CLOSED for the base URL.
|
|
102
|
-
- `HalfOpenRejectedError` – circuit is HALF_OPEN and the call was not the probe.
|
|
50
|
+
## 1. Resilient HTTP Client
|
|
103
51
|
|
|
104
|
-
|
|
105
|
-
The client is an `EventEmitter`. Useful hooks:
|
|
106
|
-
- `request:start | request:success | request:failure | request:rejected`
|
|
107
|
-
- `health:closed | health:half_open | health:open`
|
|
108
|
-
- `microcache:retry | microcache:refresh_failed`
|
|
52
|
+
A guarded HTTP client that keeps your service predictable under pressure.
|
|
109
53
|
|
|
110
|
-
|
|
54
|
+
### What it does
|
|
55
|
+
- Collapses identical GETs into a single upstream call
|
|
56
|
+
- Enforces per-host concurrency limits
|
|
57
|
+
- Uses a bounded FIFO queue (no unbounded memory)
|
|
58
|
+
- Fails fast when an upstream becomes unhealthy
|
|
59
|
+
- Retries safely without amplification
|
|
111
60
|
|
|
112
|
-
|
|
61
|
+
### When to use
|
|
62
|
+
- Calling third-party APIs
|
|
63
|
+
- Partner integrations
|
|
64
|
+
- Webhooks and background jobs
|
|
65
|
+
- Any request-driven outbound traffic
|
|
113
66
|
|
|
114
|
-
|
|
67
|
+
(Details below ⬇)
|
|
68
|
+
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
## 2. Instant GET Store
|
|
72
|
+
|
|
73
|
+
A lightweight **background poller for a single GET URL**.
|
|
74
|
+
|
|
75
|
+
### What it does
|
|
76
|
+
- Polls one URL on a fixed interval
|
|
77
|
+
- Stores the latest successful response
|
|
78
|
+
- `get()` returns instantly (no network call)
|
|
79
|
+
- Handles retry or stop behavior on failure
|
|
80
|
+
- Optionally waits until first successful fetch
|
|
81
|
+
|
|
82
|
+
### When to use
|
|
83
|
+
- Remote config endpoints
|
|
84
|
+
- Feature flags
|
|
85
|
+
- Prices, rates, counters
|
|
86
|
+
- “Read often, update periodically” data
|
|
87
|
+
|
|
88
|
+
(Details below ⬇)
|
|
89
|
+
|
|
90
|
+
---
|
|
91
|
+
|
|
92
|
+
---
|
|
93
|
+
|
|
94
|
+
## Install
|
|
115
95
|
|
|
116
96
|
```bash
|
|
117
|
-
npm
|
|
118
|
-
|
|
119
|
-
|
|
97
|
+
npm install @nextn/outbound-guard
|
|
98
|
+
````
|
|
99
|
+
|
|
100
|
+
Node 18+ required (tested on Node 20).
|
|
101
|
+
|
|
102
|
+
---
|
|
103
|
+
|
|
104
|
+
## Quick links
|
|
120
105
|
|
|
121
|
-
|
|
106
|
+
* **Demos & usage examples**
|
|
107
|
+
[https://github.com/next-n/guardtest]
|
|
122
108
|
|
|
123
|
-
|
|
124
|
-
- Calling external APIs or partner services from Node.js
|
|
125
|
-
- BFFs/API gateways that must isolate upstream slowness
|
|
126
|
-
- Webhook senders or background workers that need predictable failure behavior
|
|
109
|
+
---
|
|
127
110
|
|
|
128
|
-
##
|
|
129
|
-
- If you need durable delivery across restarts (use queues/outbox)
|
|
130
|
-
- If you need cross-process coordination or distributed rate limiting
|
|
131
|
-
- If you need a service mesh or long-lived caching
|
|
111
|
+
## ─────────────────────────────
|
|
132
112
|
|
|
133
|
-
##
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
113
|
+
## Resilient HTTP Client
|
|
114
|
+
|
|
115
|
+
## ─────────────────────────────
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
```
|
package/dist/index.cjs
CHANGED
|
@@ -21,6 +21,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
21
21
|
var index_exports = {};
|
|
22
22
|
__export(index_exports, {
|
|
23
23
|
HalfOpenRejectedError: () => HalfOpenRejectedError,
|
|
24
|
+
InstantGetStore: () => InstantGetStore,
|
|
24
25
|
QueueFullError: () => QueueFullError,
|
|
25
26
|
QueueTimeoutError: () => QueueTimeoutError,
|
|
26
27
|
RequestTimeoutError: () => RequestTimeoutError,
|
|
@@ -289,6 +290,16 @@ var ResilientHttpClient = class extends import_node_events.EventEmitter {
|
|
|
289
290
|
}
|
|
290
291
|
return this.execute(req, { allowProbe: false });
|
|
291
292
|
}
|
|
293
|
+
/**
|
|
294
|
+
* Run a HALF_OPEN probe. Exactly one probe is allowed per HALF_OPEN window.
|
|
295
|
+
* Normal request() calls are rejected during HALF_OPEN.
|
|
296
|
+
*/
|
|
297
|
+
async probe(req) {
|
|
298
|
+
if (this.microCache?.enabled && req.method === "GET" && req.body == null) {
|
|
299
|
+
return this.execute(req, { allowProbe: true });
|
|
300
|
+
}
|
|
301
|
+
return this.execute(req, { allowProbe: true });
|
|
302
|
+
}
|
|
292
303
|
snapshot() {
|
|
293
304
|
let inFlight = 0;
|
|
294
305
|
let queueDepth = 0;
|
|
@@ -428,8 +439,8 @@ var ResilientHttpClient = class extends import_node_events.EventEmitter {
|
|
|
428
439
|
await limiter.acquireNoQueue();
|
|
429
440
|
} else {
|
|
430
441
|
await limiter.acquire();
|
|
431
|
-
acquired = true;
|
|
432
442
|
}
|
|
443
|
+
acquired = true;
|
|
433
444
|
} catch (err) {
|
|
434
445
|
this.emit("request:rejected", { requestId, request: req, error: err });
|
|
435
446
|
throw err;
|
|
@@ -591,9 +602,195 @@ var ResilientHttpClient = class extends import_node_events.EventEmitter {
|
|
|
591
602
|
}
|
|
592
603
|
}
|
|
593
604
|
};
|
|
605
|
+
|
|
606
|
+
// src/instant_get.ts
|
|
607
|
+
var VALID_MS = 6e4;
|
|
608
|
+
var RETRY_BACKOFF_MS = [1e3, 5e3, 1e4, 3e4, 6e4];
|
|
609
|
+
function cloneResponse(res) {
|
|
610
|
+
return {
|
|
611
|
+
status: res.status,
|
|
612
|
+
headers: { ...res.headers },
|
|
613
|
+
body: new Uint8Array(res.body)
|
|
614
|
+
};
|
|
615
|
+
}
|
|
616
|
+
function headersToRecord(headers) {
|
|
617
|
+
const out = {};
|
|
618
|
+
headers.forEach((v, k) => {
|
|
619
|
+
out[k] = v;
|
|
620
|
+
});
|
|
621
|
+
return out;
|
|
622
|
+
}
|
|
623
|
+
function toRetryCount(onError) {
|
|
624
|
+
if (!onError) return Number.POSITIVE_INFINITY;
|
|
625
|
+
if (onError === "stop") return 0;
|
|
626
|
+
const n = onError.retry;
|
|
627
|
+
if (!Number.isFinite(n)) return Number.POSITIVE_INFINITY;
|
|
628
|
+
return Math.max(0, Math.floor(n));
|
|
629
|
+
}
|
|
630
|
+
function nextBackoffDelayMs(stage) {
|
|
631
|
+
const idx = Math.min(stage, RETRY_BACKOFF_MS.length - 1);
|
|
632
|
+
return RETRY_BACKOFF_MS[idx];
|
|
633
|
+
}
|
|
634
|
+
function sleep2(ms) {
|
|
635
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
636
|
+
}
|
|
637
|
+
async function fetchGet(url) {
|
|
638
|
+
const res = await fetch(url, { method: "GET" });
|
|
639
|
+
const body = new Uint8Array(await res.arrayBuffer());
|
|
640
|
+
return {
|
|
641
|
+
status: res.status,
|
|
642
|
+
headers: headersToRecord(res.headers),
|
|
643
|
+
body
|
|
644
|
+
};
|
|
645
|
+
}
|
|
646
|
+
var InstantGetStore = class {
|
|
647
|
+
state = {
|
|
648
|
+
url: void 0,
|
|
649
|
+
intervalMs: 5e3,
|
|
650
|
+
onError: { retry: Number.POSITIVE_INFINITY },
|
|
651
|
+
timer: void 0,
|
|
652
|
+
inFlight: false,
|
|
653
|
+
last: void 0,
|
|
654
|
+
lastOkAt: void 0,
|
|
655
|
+
lastStatus: void 0,
|
|
656
|
+
lastErrorName: void 0,
|
|
657
|
+
consecutiveErrors: 0,
|
|
658
|
+
retriesRemaining: Number.POSITIVE_INFINITY,
|
|
659
|
+
backoffStage: 0,
|
|
660
|
+
nextDelayMs: void 0,
|
|
661
|
+
ready: false
|
|
662
|
+
};
|
|
663
|
+
start(url, opts) {
|
|
664
|
+
if (this.state.timer) this.stop();
|
|
665
|
+
const intervalMs = opts?.intervalMs ?? 5e3;
|
|
666
|
+
if (!Number.isFinite(intervalMs) || intervalMs <= 0) {
|
|
667
|
+
throw new Error(`intervalMs must be > 0 (got ${intervalMs})`);
|
|
668
|
+
}
|
|
669
|
+
const onError = opts?.onError ?? { retry: Number.POSITIVE_INFINITY };
|
|
670
|
+
this.state.url = url;
|
|
671
|
+
this.state.intervalMs = intervalMs;
|
|
672
|
+
this.state.onError = onError;
|
|
673
|
+
this.state.retriesRemaining = toRetryCount(onError);
|
|
674
|
+
this.state.backoffStage = 0;
|
|
675
|
+
this.state.nextDelayMs = void 0;
|
|
676
|
+
this.state.inFlight = false;
|
|
677
|
+
this.state.ready = false;
|
|
678
|
+
this.scheduleNext(0);
|
|
679
|
+
}
|
|
680
|
+
stop() {
|
|
681
|
+
if (this.state.timer) clearTimeout(this.state.timer);
|
|
682
|
+
this.state.timer = void 0;
|
|
683
|
+
this.state.nextDelayMs = void 0;
|
|
684
|
+
this.state.inFlight = false;
|
|
685
|
+
this.state.ready = false;
|
|
686
|
+
}
|
|
687
|
+
/** Returns true only after first successful fetch (2xx) for the current run. */
|
|
688
|
+
isReady() {
|
|
689
|
+
return this.state.ready;
|
|
690
|
+
}
|
|
691
|
+
/**
|
|
692
|
+
* Polls `ready` every 1 second.
|
|
693
|
+
* Returns true if ready becomes true within timeoutSec (default 10), else false.
|
|
694
|
+
* Never throws.
|
|
695
|
+
*/
|
|
696
|
+
async waitReady(timeoutSec = 10) {
|
|
697
|
+
if (!Number.isFinite(timeoutSec) || timeoutSec < 0) timeoutSec = 10;
|
|
698
|
+
const timeoutMs = Math.floor(timeoutSec * 1e3);
|
|
699
|
+
const start = Date.now();
|
|
700
|
+
while (true) {
|
|
701
|
+
if (this.state.ready) return true;
|
|
702
|
+
if (Date.now() - start >= timeoutMs) return false;
|
|
703
|
+
await sleep2(1e3);
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
/** Instant read: returns last successful response if <= 60s old, else undefined */
|
|
707
|
+
get() {
|
|
708
|
+
if (!this.state.last || !this.state.lastOkAt) return void 0;
|
|
709
|
+
const age = Date.now() - this.state.lastOkAt;
|
|
710
|
+
if (age > VALID_MS) return void 0;
|
|
711
|
+
return cloneResponse(this.state.last);
|
|
712
|
+
}
|
|
713
|
+
snapshot() {
|
|
714
|
+
if (!this.state.url) return void 0;
|
|
715
|
+
return {
|
|
716
|
+
url: this.state.url,
|
|
717
|
+
running: !!this.state.timer,
|
|
718
|
+
intervalMs: this.state.intervalMs,
|
|
719
|
+
lastOkAt: this.state.lastOkAt,
|
|
720
|
+
lastStatus: this.state.lastStatus,
|
|
721
|
+
lastErrorName: this.state.lastErrorName,
|
|
722
|
+
consecutiveErrors: this.state.consecutiveErrors,
|
|
723
|
+
inFlight: this.state.inFlight,
|
|
724
|
+
ready: this.state.ready,
|
|
725
|
+
nextDelayMs: this.state.nextDelayMs
|
|
726
|
+
};
|
|
727
|
+
}
|
|
728
|
+
/* ---------------- internals ---------------- */
|
|
729
|
+
scheduleNext(delayMs) {
|
|
730
|
+
const d = Math.max(0, delayMs);
|
|
731
|
+
this.state.nextDelayMs = d;
|
|
732
|
+
if (this.state.timer) clearTimeout(this.state.timer);
|
|
733
|
+
this.state.timer = setTimeout(() => {
|
|
734
|
+
void this.tick();
|
|
735
|
+
}, d);
|
|
736
|
+
}
|
|
737
|
+
async tick() {
|
|
738
|
+
if (!this.state.url) return;
|
|
739
|
+
if (!this.state.timer) return;
|
|
740
|
+
if (this.state.inFlight) {
|
|
741
|
+
this.scheduleNext(this.state.intervalMs);
|
|
742
|
+
return;
|
|
743
|
+
}
|
|
744
|
+
this.state.inFlight = true;
|
|
745
|
+
try {
|
|
746
|
+
const res = await fetchGet(this.state.url);
|
|
747
|
+
if (res.status < 200 || res.status >= 300) {
|
|
748
|
+
const err = new Error(`Non-2xx status=${res.status}`);
|
|
749
|
+
err.name = "InstantGetNon2xxError";
|
|
750
|
+
throw err;
|
|
751
|
+
}
|
|
752
|
+
this.state.last = cloneResponse(res);
|
|
753
|
+
this.state.lastOkAt = Date.now();
|
|
754
|
+
this.state.lastStatus = res.status;
|
|
755
|
+
this.state.lastErrorName = void 0;
|
|
756
|
+
this.state.consecutiveErrors = 0;
|
|
757
|
+
this.state.backoffStage = 0;
|
|
758
|
+
this.state.retriesRemaining = toRetryCount(this.state.onError);
|
|
759
|
+
this.state.ready = true;
|
|
760
|
+
this.scheduleNext(this.state.intervalMs);
|
|
761
|
+
} catch (err) {
|
|
762
|
+
this.state.lastErrorName = err?.name ?? "Error";
|
|
763
|
+
this.state.consecutiveErrors += 1;
|
|
764
|
+
if (this.state.onError === "stop") {
|
|
765
|
+
this.stop();
|
|
766
|
+
return;
|
|
767
|
+
}
|
|
768
|
+
if (Number.isFinite(this.state.retriesRemaining)) {
|
|
769
|
+
if (this.state.retriesRemaining <= 0) {
|
|
770
|
+
this.stop();
|
|
771
|
+
return;
|
|
772
|
+
}
|
|
773
|
+
this.state.retriesRemaining -= 1;
|
|
774
|
+
if (this.state.retriesRemaining <= 0) {
|
|
775
|
+
this.stop();
|
|
776
|
+
return;
|
|
777
|
+
}
|
|
778
|
+
this.state.backoffStage = 0;
|
|
779
|
+
this.scheduleNext(this.state.intervalMs);
|
|
780
|
+
return;
|
|
781
|
+
}
|
|
782
|
+
const delay = nextBackoffDelayMs(this.state.backoffStage);
|
|
783
|
+
this.state.backoffStage += 1;
|
|
784
|
+
this.scheduleNext(delay);
|
|
785
|
+
} finally {
|
|
786
|
+
this.state.inFlight = false;
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
};
|
|
594
790
|
// Annotate the CommonJS export names for ESM import in node:
|
|
595
791
|
0 && (module.exports = {
|
|
596
792
|
HalfOpenRejectedError,
|
|
793
|
+
InstantGetStore,
|
|
597
794
|
QueueFullError,
|
|
598
795
|
QueueTimeoutError,
|
|
599
796
|
RequestTimeoutError,
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/client.ts","../src/errors.ts","../src/limiter.ts","../src/http.ts"],"sourcesContent":["// src/index.ts\r\nexport * from \"./client.js\";\r\nexport * from \"./types.js\";\r\nexport * from \"./errors.js\";\r\n","// src/client.ts\r\nimport { EventEmitter } from \"node:events\";\r\nimport { ConcurrencyLimiter } from \"./limiter.js\";\r\nimport { doHttpRequest } from \"./http.js\";\r\nimport {\r\n QueueFullError,\r\n RequestTimeoutError,\r\n ResilientHttpError,\r\n HalfOpenRejectedError,\r\n UpstreamUnhealthyError,\r\n} from \"./errors.js\";\r\nimport type {\r\n MicroCacheOptions,\r\n ResilientHttpClientOptions,\r\n ResilientRequest,\r\n ResilientResponse,\r\n} from \"./types.js\";\r\n\r\n/* ---------------------------- helpers ---------------------------- */\r\n\r\nfunction normalizeUrlForKey(rawUrl: string): string {\r\n const u = new URL(rawUrl);\r\n u.hostname = u.hostname.toLowerCase();\r\n\r\n const isHttpDefault = u.protocol === \"http:\" && u.port === \"80\";\r\n const isHttpsDefault = u.protocol === \"https:\" && u.port === \"443\";\r\n if (isHttpDefault || isHttpsDefault) u.port = \"\";\r\n\r\n return u.toString();\r\n}\r\n\r\nfunction baseUrlKey(rawUrl: string): string {\r\n const u = new URL(rawUrl);\r\n u.hostname = u.hostname.toLowerCase();\r\n\r\n const isHttpDefault = u.protocol === \"http:\" && u.port === \"80\";\r\n const isHttpsDefault = u.protocol === \"https:\" && u.port === \"443\";\r\n if (isHttpDefault || isHttpsDefault) u.port = \"\";\r\n\r\n return `${u.protocol}//${u.host}`;\r\n}\r\n\r\nfunction defaultMicroCacheKeyFn(req: ResilientRequest): string {\r\n return `GET ${normalizeUrlForKey(req.url)}`;\r\n}\r\n\r\nfunction genRequestId(): string {\r\n return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;\r\n}\r\n\r\nfunction sleep(ms: number): Promise<void> {\r\n return new Promise((r) => setTimeout(r, ms));\r\n}\r\n\r\nfunction clamp(n: number, lo: number, hi: number): number {\r\n return Math.max(lo, Math.min(hi, n));\r\n}\r\n\r\nfunction jitterMs(ms: number): number {\r\n const mult = 0.8 + Math.random() * 0.4; // [0.8, 1.2]\r\n return Math.round(ms * mult);\r\n}\r\n\r\n\r\n/* ---------------------------- health ---------------------------- */\r\n\r\ntype HealthState = \"OPEN\" | \"CLOSED\" | \"HALF_OPEN\";\r\ntype Outcome = \"SUCCESS\" | \"HARD_FAIL\" | \"SOFT_FAIL\";\r\n\r\ntype HealthTracker = {\r\n state: HealthState;\r\n\r\n window: Outcome[];\r\n windowSize: number; // 20\r\n minSamples: number; // 10\r\n\r\n consecutiveHardFails: number; // trip at 3\r\n\r\n cooldownBaseMs: number; // 1000\r\n cooldownCapMs: number; // 30000\r\n cooldownMs: number; // current backoff\r\n cooldownUntil: number;\r\n\r\n probeInFlight: boolean; // probeConcurrency=1\r\n probeRemaining: number; // 1 probe per half-open\r\n\r\n stableNonHard: number; // reset backoff after 5 non-hard after open\r\n};\r\n\r\nconst SOFT_FAIL_STATUSES = new Set([429, 502, 503, 504]);\r\n\r\nfunction classifyHttpStatus(status: number): Outcome {\r\n if (status >= 200 && status < 300) return \"SUCCESS\";\r\n if (SOFT_FAIL_STATUSES.has(status)) return \"SOFT_FAIL\";\r\n return \"SUCCESS\"; // do not penalize health for other statuses (incl 4xx except 429)\r\n}\r\n\r\nfunction computeRates(window: Outcome[]) {\r\n const total = window.length;\r\n let hard = 0;\r\n let soft = 0;\r\n for (const o of window) {\r\n if (o === \"HARD_FAIL\") hard += 1;\r\n else if (o === \"SOFT_FAIL\") soft += 1;\r\n }\r\n return {\r\n total,\r\n hard,\r\n soft,\r\n hardFailRate: total === 0 ? 0 : hard / total,\r\n failRate: total === 0 ? 0 : (hard + soft) / total,\r\n };\r\n}\r\n\r\n/**\r\n * Only HTTP-layer failures should count as HARD_FAIL.\r\n * Control-plane rejections must NOT poison health.\r\n */\r\nfunction shouldCountAsHardFail(err: unknown): boolean {\r\n if (err instanceof UpstreamUnhealthyError) return false;\r\n if (err instanceof HalfOpenRejectedError) return false;\r\n if (err instanceof QueueFullError) return false;\r\n\r\n // if your http layer throws this, it's a real hard fail (timeout)\r\n if (err instanceof RequestTimeoutError) return true;\r\n\r\n // If it’s a known library error but not one of the above, be conservative:\r\n // treat it as NOT a health signal unless it’s clearly HTTP-related.\r\n if (err instanceof ResilientHttpError) return false;\r\n\r\n // Unknown thrown error: assume it's an HTTP/network failure.\r\n return true;\r\n}\r\n\r\n/* ---------------------------- microcache types ---------------------------- */\r\n\r\ntype CacheEntry = {\r\n createdAt: number;\r\n expiresAt: number;\r\n value: ResilientResponse;\r\n};\r\n\r\ntype InFlightGroup = {\r\n promise: Promise<ResilientResponse>;\r\n windowStartMs: number;\r\n waiters: number;\r\n};\r\n\r\n/* ---------------------------- client ---------------------------- */\r\n\r\nexport class ResilientHttpClient extends EventEmitter {\r\n private readonly requestTimeoutMs: number;\r\n private readonly healthEnabled: boolean;\r\n\r\n private readonly limiters = new Map<string, ConcurrencyLimiter>();\r\n private readonly health = new Map<string, HealthTracker>();\r\n\r\n private readonly microCache?: Required<\r\n Pick<\r\n MicroCacheOptions,\r\n | \"enabled\"\r\n | \"ttlMs\"\r\n | \"maxStaleMs\"\r\n | \"maxEntries\"\r\n | \"maxWaiters\"\r\n | \"followerTimeoutMs\"\r\n | \"keyFn\"\r\n >\r\n > & {\r\n retry?: {\r\n maxAttempts: number;\r\n baseDelayMs: number;\r\n maxDelayMs: number;\r\n retryOnStatus: number[];\r\n };\r\n };\r\n\r\n private cache?: Map<string, CacheEntry>;\r\n private inFlight?: Map<string, InFlightGroup>;\r\n\r\n private microCacheReqCount = 0;\r\n private readonly cleanupEveryNRequests = 100;\r\n\r\n constructor(private readonly opts: ResilientHttpClientOptions) {\r\n super();\r\n this.requestTimeoutMs = opts.requestTimeoutMs;\r\n this.healthEnabled = opts.health?.enabled ?? true;\r\n\r\n const mc = opts.microCache;\r\n if (mc?.enabled) {\r\n const retry = mc.retry\r\n ? {\r\n maxAttempts: mc.retry.maxAttempts ?? 3,\r\n baseDelayMs: mc.retry.baseDelayMs ?? 50,\r\n maxDelayMs: mc.retry.maxDelayMs ?? 200,\r\n retryOnStatus: mc.retry.retryOnStatus ?? [429, 502, 503, 504],\r\n }\r\n : undefined;\r\n\r\n this.microCache = {\r\n enabled: true,\r\n ttlMs: mc.ttlMs ?? 1000,\r\n maxStaleMs: mc.maxStaleMs ?? 10_000,\r\n maxEntries: mc.maxEntries ?? 500,\r\n maxWaiters: mc.maxWaiters ?? 1000,\r\n followerTimeoutMs: mc.followerTimeoutMs ?? 5000,\r\n keyFn: mc.keyFn ?? defaultMicroCacheKeyFn,\r\n retry,\r\n };\r\n\r\n this.cache = new Map();\r\n this.inFlight = new Map();\r\n }\r\n }\r\n\r\n async request(req: ResilientRequest): Promise<ResilientResponse> {\r\n if (this.microCache?.enabled && req.method === \"GET\" && req.body == null) {\r\n return this.requestWithMicroCache(req);\r\n }\r\n return this.execute(req, { allowProbe: false });\r\n }\r\n\r\n snapshot(): { inFlight: number; queueDepth: number } {\r\n let inFlight = 0;\r\n let queueDepth = 0;\r\n for (const l of this.limiters.values()) {\r\n const s = l.snapshot();\r\n inFlight += s.inFlight;\r\n queueDepth += s.queueDepth;\r\n }\r\n return { inFlight, queueDepth };\r\n }\r\n\r\n /* ---------------- internals ---------------- */\r\n\r\n private getLimiter(baseKey: string): ConcurrencyLimiter {\r\n let l = this.limiters.get(baseKey);\r\n if (!l) {\r\n l = new ConcurrencyLimiter({\r\n maxInFlight: this.opts.maxInFlight,\r\n maxQueue: this.opts.maxInFlight * 10, // hidden factor\r\n });\r\n this.limiters.set(baseKey, l);\r\n }\r\n return l;\r\n }\r\n\r\n private getHealth(baseKey: string): HealthTracker {\r\n let h = this.health.get(baseKey);\r\n if (!h) {\r\n h = {\r\n state: \"OPEN\",\r\n window: [],\r\n windowSize: 20,\r\n minSamples: 10,\r\n consecutiveHardFails: 0,\r\n cooldownBaseMs: 1000,\r\n cooldownCapMs: 30_000,\r\n cooldownMs: 1000,\r\n cooldownUntil: 0,\r\n probeInFlight: false,\r\n probeRemaining: 0,\r\n stableNonHard: 0,\r\n };\r\n this.health.set(baseKey, h);\r\n }\r\n return h;\r\n }\r\n\r\n private closeHealth(baseKey: string, reason: string): void {\r\n const h = this.getHealth(baseKey);\r\n if (h.state === \"CLOSED\") return;\r\n\r\n h.state = \"CLOSED\";\r\n h.cooldownUntil = Date.now() + jitterMs(h.cooldownMs);\r\n h.cooldownMs = Math.min(h.cooldownMs * 2, h.cooldownCapMs);\r\n\r\n // reject queued immediately\r\n this.getLimiter(baseKey).flush(new UpstreamUnhealthyError(baseKey, \"CLOSED\"));\r\n\r\n const rates = computeRates(h.window);\r\n this.emit(\"health:closed\", {\r\n baseUrl: baseKey,\r\n reason,\r\n cooldownMs: h.cooldownUntil - Date.now(),\r\n hardFailRate: rates.hardFailRate,\r\n failRate: rates.failRate,\r\n samples: rates.total,\r\n });\r\n }\r\n\r\n private halfOpenHealth(baseKey: string): void {\r\n const h = this.getHealth(baseKey);\r\n if (h.state !== \"CLOSED\") return;\r\n\r\n h.state = \"HALF_OPEN\";\r\n h.probeInFlight = false;\r\n h.probeRemaining = 1;\r\n this.emit(\"health:half_open\", { baseUrl: baseKey });\r\n }\r\n\r\n private openHealth(baseKey: string): void {\r\n const h = this.getHealth(baseKey);\r\n h.state = \"OPEN\";\r\n h.window = [];\r\n h.consecutiveHardFails = 0;\r\n h.probeInFlight = false;\r\n h.probeRemaining = 0;\r\n h.stableNonHard = 0;\r\n this.emit(\"health:open\", { baseUrl: baseKey });\r\n }\r\n\r\n private recordOutcome(baseKey: string, outcome: Outcome): void {\r\n const h = this.getHealth(baseKey);\r\n\r\n h.window.push(outcome);\r\n while (h.window.length > h.windowSize) h.window.shift();\r\n\r\n if (outcome === \"HARD_FAIL\") h.consecutiveHardFails += 1;\r\n else h.consecutiveHardFails = 0;\r\n\r\n // stabilization (reset backoff after 5 non-hard in OPEN)\r\n if (h.state === \"OPEN\") {\r\n if (outcome !== \"HARD_FAIL\") {\r\n h.stableNonHard += 1;\r\n if (h.stableNonHard >= 5) {\r\n h.cooldownMs = h.cooldownBaseMs;\r\n }\r\n } else {\r\n h.stableNonHard = 0;\r\n }\r\n }\r\n\r\n if (!this.healthEnabled) return;\r\n\r\n if (h.consecutiveHardFails >= 3) {\r\n this.closeHealth(baseKey, \"3 consecutive hard failures\");\r\n return;\r\n }\r\n\r\n const rates = computeRates(h.window);\r\n if (rates.total >= h.minSamples) {\r\n if (rates.hardFailRate >= 0.3) {\r\n this.closeHealth(baseKey, \"hardFailRate >= 30%\");\r\n return;\r\n }\r\n if (rates.failRate >= 0.5) {\r\n this.closeHealth(baseKey, \"failRate >= 50%\");\r\n return;\r\n }\r\n }\r\n }\r\n\r\n private async execute(req: ResilientRequest, opts: { allowProbe: boolean }): Promise<ResilientResponse> {\r\n const baseKey = baseUrlKey(req.url);\r\n const h = this.getHealth(baseKey);\r\n const limiter = this.getLimiter(baseKey);\r\n\r\n if (this.healthEnabled) {\r\n if (h.state === \"CLOSED\") {\r\n if (Date.now() >= h.cooldownUntil) {\r\n this.halfOpenHealth(baseKey);\r\n } else {\r\n throw new UpstreamUnhealthyError(baseKey, \"CLOSED\");\r\n }\r\n }\r\n\r\n if (h.state === \"HALF_OPEN\") {\r\n if (!opts.allowProbe) throw new HalfOpenRejectedError(baseKey);\r\n if (h.probeRemaining <= 0 || h.probeInFlight) throw new HalfOpenRejectedError(baseKey);\r\n h.probeInFlight = true;\r\n h.probeRemaining -= 1;\r\n }\r\n }\r\n\r\n const requestId = genRequestId();\r\n const start = Date.now();\r\n let acquired = false;\r\n try {\r\n // Probes should not wait in queue.\r\n if (this.healthEnabled && h.state === \"HALF_OPEN\") {\r\n await limiter.acquireNoQueue();\r\n } else {\r\n await limiter.acquire();\r\n acquired = true;\r\n }\r\n } catch (err) {\r\n this.emit(\"request:rejected\", { requestId, request: req, error: err });\r\n throw err;\r\n }\r\n\r\n this.emit(\"request:start\", { requestId, request: req });\r\n\r\n try {\r\n const res = await doHttpRequest(req, this.requestTimeoutMs);\r\n const durationMs = Date.now() - start;\r\n\r\n const outcome = classifyHttpStatus(res.status);\r\n this.recordOutcome(baseKey, outcome);\r\n\r\n // Probe decision\r\n if (this.healthEnabled && h.state === \"HALF_OPEN\") {\r\n this.emit(\"health:probe\", { baseUrl: baseKey, outcome, status: res.status });\r\n if (res.status >= 200 && res.status < 300) {\r\n this.openHealth(baseKey);\r\n } else {\r\n this.closeHealth(baseKey, `probe failed status=${res.status}`);\r\n }\r\n }\r\n\r\n this.emit(\"request:success\", { requestId, request: req, status: res.status, durationMs });\r\n return res;\r\n } catch (err) {\r\n const durationMs = Date.now() - start;\r\n\r\n if (shouldCountAsHardFail(err)) {\r\n this.recordOutcome(baseKey, \"HARD_FAIL\");\r\n }\r\n\r\n if (this.healthEnabled && h.state === \"HALF_OPEN\") {\r\n this.emit(\"health:probe\", { baseUrl: baseKey, outcome: \"HARD_FAIL\", error: err });\r\n this.closeHealth(baseKey, \"probe hard failure\");\r\n }\r\n\r\n this.emit(\"request:failure\", { requestId, request: req, error: err, durationMs });\r\n throw err;\r\n } finally {\r\n // Clear probe flag (if any)\r\n if (this.healthEnabled && h.state === \"HALF_OPEN\") {\r\n h.probeInFlight = false;\r\n }\r\n if (acquired) limiter.release();\r\n }\r\n }\r\n\r\n /* ---------------- microcache ---------------- */\r\n\r\n private cloneResponse(res: ResilientResponse): ResilientResponse {\r\n return { status: res.status, headers: { ...res.headers }, body: new Uint8Array(res.body) };\r\n }\r\n\r\n private maybeCleanupExpired(cache: Map<string, CacheEntry>, maxStaleMs: number): void {\r\n this.microCacheReqCount++;\r\n if (this.microCacheReqCount % this.cleanupEveryNRequests !== 0) return;\r\n\r\n const now = Date.now();\r\n for (const [k, v] of cache.entries()) {\r\n if (now - v.createdAt > maxStaleMs) cache.delete(k);\r\n }\r\n }\r\n\r\n private evictIfNeeded(cache: Map<string, CacheEntry>, maxEntries: number): void {\r\n while (cache.size >= maxEntries) {\r\n const oldestKey = cache.keys().next().value as string | undefined;\r\n if (!oldestKey) break;\r\n cache.delete(oldestKey);\r\n }\r\n }\r\n\r\n private isRetryableStatus(status: number, retryOnStatus: number[]): boolean {\r\n return retryOnStatus.includes(status);\r\n }\r\n\r\n private computeBackoffMs(attemptIndex: number, baseDelayMs: number, maxDelayMs: number): number {\r\n const exp = baseDelayMs * Math.pow(2, attemptIndex - 1);\r\n const capped = clamp(exp, baseDelayMs, maxDelayMs);\r\n const jitter = 0.5 + Math.random();\r\n return Math.round(capped * jitter);\r\n }\r\n\r\n private async fetchWithLeaderRetry(req: ResilientRequest): Promise<ResilientResponse> {\r\n const mc = this.microCache!;\r\n const retry = mc.retry;\r\n if (!retry) return this.execute(req, { allowProbe: false });\r\n\r\n const { maxAttempts, baseDelayMs, maxDelayMs, retryOnStatus } = retry;\r\n\r\n let last: ResilientResponse | undefined;\r\n for (let attempt = 1; attempt <= maxAttempts; attempt++) {\r\n const res = await this.execute(req, { allowProbe: false });\r\n last = res;\r\n\r\n if (this.isRetryableStatus(res.status, retryOnStatus) && attempt < maxAttempts) {\r\n const delay = this.computeBackoffMs(attempt, baseDelayMs, maxDelayMs);\r\n this.emit(\"microcache:retry\", { url: req.url, attempt, maxAttempts, reason: `status ${res.status}`, delayMs: delay });\r\n await sleep(delay);\r\n continue;\r\n }\r\n return res;\r\n }\r\n return last!;\r\n }\r\n\r\n private async requestWithMicroCache(req: ResilientRequest): Promise<ResilientResponse> {\r\n const mc = this.microCache!;\r\n const cache = this.cache!;\r\n const inFlight = this.inFlight!;\r\n\r\n this.maybeCleanupExpired(cache, mc.maxStaleMs);\r\n\r\n const key = mc.keyFn(req);\r\n const now = Date.now();\r\n\r\n const hit0 = cache.get(key);\r\n if (hit0 && now - hit0.createdAt > mc.maxStaleMs) cache.delete(key);\r\n\r\n const hit = cache.get(key);\r\n if (hit && now < hit.expiresAt) return this.cloneResponse(hit.value);\r\n\r\n // If CLOSED: serve stale if allowed else fail fast\r\n if (this.healthEnabled) {\r\n const baseKey = baseUrlKey(req.url);\r\n const h = this.getHealth(baseKey);\r\n if (h.state === \"CLOSED\") {\r\n const staleAllowed = !!hit && now - hit.createdAt <= mc.maxStaleMs;\r\n if (staleAllowed) return this.cloneResponse(hit!.value);\r\n throw new UpstreamUnhealthyError(baseKey, \"CLOSED\");\r\n }\r\n }\r\n\r\n const group = inFlight.get(key);\r\n if (group) {\r\n const h = cache.get(key);\r\n const staleAllowed = !!h && now - h.createdAt <= mc.maxStaleMs;\r\n\r\n if (h && staleAllowed) return this.cloneResponse(h.value);\r\n\r\n const age = now - group.windowStartMs;\r\n if (age > mc.followerTimeoutMs) {\r\n const err = new Error(`Follower window closed for key=${key}`);\r\n (err as any).name = \"FollowerWindowClosedError\";\r\n throw err;\r\n }\r\n\r\n if (group.waiters >= mc.maxWaiters) {\r\n const err = new Error(`Too many followers for key=${key}`);\r\n (err as any).name = \"TooManyWaitersError\";\r\n throw err;\r\n }\r\n\r\n group.waiters += 1;\r\n try {\r\n const res = await group.promise;\r\n return this.cloneResponse(res);\r\n } finally {\r\n group.waiters -= 1;\r\n }\r\n }\r\n\r\n const prev = cache.get(key);\r\n const prevStaleAllowed = !!prev && now - prev.createdAt <= mc.maxStaleMs;\r\n\r\n const promise = (async () => {\r\n const baseKey = baseUrlKey(req.url);\r\n const h = this.getHealth(baseKey);\r\n const allowProbe = this.healthEnabled && h.state === \"HALF_OPEN\";\r\n\r\n const res = allowProbe\r\n ? await this.execute(req, { allowProbe: true })\r\n : await this.fetchWithLeaderRetry(req);\r\n\r\n if (res.status >= 200 && res.status < 300) {\r\n this.evictIfNeeded(cache, mc.maxEntries);\r\n const t = Date.now();\r\n cache.set(key, { value: this.cloneResponse(res), createdAt: t, expiresAt: t + mc.ttlMs });\r\n }\r\n return res;\r\n })();\r\n\r\n inFlight.set(key, { promise, windowStartMs: Date.now(), waiters: 0 });\r\n\r\n try {\r\n const res = await promise;\r\n\r\n if (!(res.status >= 200 && res.status < 300) && prev && prevStaleAllowed) {\r\n return this.cloneResponse(prev.value);\r\n }\r\n\r\n return this.cloneResponse(res);\r\n } catch (err) {\r\n if (prev && prevStaleAllowed) {\r\n this.emit(\"microcache:refresh_failed\", { key, url: req.url, error: err });\r\n return this.cloneResponse(prev.value);\r\n }\r\n throw err;\r\n } finally {\r\n inFlight.delete(key);\r\n }\r\n }\r\n}\r\n","export abstract class ResilientHttpError extends Error {\r\n public readonly name: string;\r\n constructor(message: string) {\r\n super(message);\r\n this.name = this.constructor.name;\r\n }\r\n}\r\n\r\nexport class QueueFullError extends ResilientHttpError {\r\n constructor(public readonly maxQueue: number) {\r\n super(`Queue is full (maxQueue=${maxQueue}).`);\r\n }\r\n}\r\n\r\nexport class QueueTimeoutError extends ResilientHttpError {\r\n constructor(public readonly enqueueTimeoutMs: number) {\r\n super(`Queue wait exceeded (enqueueTimeoutMs=${enqueueTimeoutMs}).`);\r\n }\r\n}\r\n\r\nexport class RequestTimeoutError extends ResilientHttpError {\r\n constructor(public readonly requestTimeoutMs: number) {\r\n super(`Request timed out (requestTimeoutMs=${requestTimeoutMs}).`);\r\n }\r\n}\r\n\r\n\r\n\r\nexport class UpstreamError extends ResilientHttpError {\r\n constructor(public readonly status: number) {\r\n super(`Upstream returned error status=${status}.`);\r\n }\r\n}\r\n\r\n\r\n\r\n\r\nexport class UpstreamUnhealthyError extends ResilientHttpError {\r\n constructor(public readonly baseUrl: string, public readonly state: string) {\r\n super(`Upstream is unhealthy (state=${state}, baseUrl=${baseUrl}).`);\r\n }\r\n}\r\n\r\nexport class HalfOpenRejectedError extends ResilientHttpError {\r\n constructor(public readonly baseUrl: string) {\r\n super(`Upstream is HALF_OPEN (probe only) for baseUrl=${baseUrl}.`);\r\n }\r\n}\r\n","// src/limiter.ts\r\nimport { QueueFullError } from \"./errors.js\";\r\n\r\ntype ResolveFn = () => void;\r\ntype RejectFn = (err: unknown) => void;\r\n\r\ninterface Waiter {\r\n resolve: ResolveFn;\r\n reject: RejectFn;\r\n}\r\n\r\n/**\r\n * Process-local concurrency limiter with bounded FIFO queue.\r\n *\r\n * - maxInFlight: concurrent permits\r\n * - maxQueue: bounded burst buffer\r\n * - No enqueue-timeout by design.\r\n *\r\n * Also supports:\r\n * - flush(err): reject all queued waiters immediately\r\n * - acquireNoQueue(): for probes (must start now or fail)\r\n */\r\nexport class ConcurrencyLimiter {\r\n private readonly maxInFlight: number;\r\n private readonly maxQueue: number;\r\n\r\n private inFlight = 0;\r\n private queue: Waiter[] = [];\r\n\r\n constructor(opts: { maxInFlight: number; maxQueue: number }) {\r\n if (!Number.isFinite(opts.maxInFlight) || opts.maxInFlight <= 0) {\r\n throw new Error(`maxInFlight must be > 0 (got ${opts.maxInFlight})`);\r\n }\r\n if (!Number.isFinite(opts.maxQueue) || opts.maxQueue < 0) {\r\n throw new Error(`maxQueue must be >= 0 (got ${opts.maxQueue})`);\r\n }\r\n\r\n this.maxInFlight = opts.maxInFlight;\r\n this.maxQueue = opts.maxQueue;\r\n }\r\n\r\n acquire(): Promise<void> {\r\n if (this.inFlight < this.maxInFlight) {\r\n this.inFlight += 1;\r\n return Promise.resolve();\r\n }\r\n\r\n if (this.maxQueue === 0 || this.queue.length >= this.maxQueue) {\r\n return Promise.reject(new QueueFullError(this.maxQueue));\r\n }\r\n\r\n return new Promise<void>((resolve, reject) => {\r\n this.queue.push({ resolve, reject });\r\n });\r\n }\r\n\r\n /**\r\n * Acquire without queueing: either start now or fail.\r\n * Used for HALF_OPEN probes so recovery never waits behind backlog.\r\n */\r\n acquireNoQueue(): Promise<void> {\r\n if (this.inFlight < this.maxInFlight) {\r\n this.inFlight += 1;\r\n return Promise.resolve();\r\n }\r\n // treat as queue full (we don't want a new error type)\r\n return Promise.reject(new QueueFullError(0));\r\n }\r\n\r\n release(): void {\r\n if (this.inFlight <= 0) {\r\n throw new Error(\"release() called when inFlight is already 0\");\r\n }\r\n\r\n const next = this.queue.shift();\r\n if (next) {\r\n next.resolve(); // transfer permit\r\n return;\r\n }\r\n\r\n this.inFlight -= 1;\r\n }\r\n\r\n flush(err: unknown): void {\r\n const q = this.queue;\r\n this.queue = [];\r\n for (const w of q) w.reject(err);\r\n }\r\n\r\n snapshot(): { inFlight: number; queueDepth: number; maxInFlight: number; maxQueue: number } {\r\n return {\r\n inFlight: this.inFlight,\r\n queueDepth: this.queue.length,\r\n maxInFlight: this.maxInFlight,\r\n maxQueue: this.maxQueue,\r\n };\r\n }\r\n}\r\n","// src/http.ts\r\nimport { request as undiciRequest } from \"undici\";\r\nimport { RequestTimeoutError } from \"./errors.js\";\r\nimport type { ResilientRequest, ResilientResponse } from \"./types.js\";\r\n\r\nfunction normalizeHeaders(headers: any): Record<string, string> {\r\n const out: Record<string, string> = {};\r\n if (!headers) return out;\r\n\r\n // undici headers are an object-like structure (headers: Record<string, string | string[]>)\r\n for (const [k, v] of Object.entries(headers)) {\r\n if (Array.isArray(v)) out[k.toLowerCase()] = v.join(\", \");\r\n else if (typeof v === \"string\") out[k.toLowerCase()] = v;\r\n else out[k.toLowerCase()] = String(v);\r\n }\r\n return out;\r\n}\r\n\r\n/**\r\n * Execute a single HTTP request with a hard timeout using AbortController.\r\n * No retries. No breaker. Just raw outbound I/O with a timeout.\r\n */\r\nexport async function doHttpRequest(\r\n req: ResilientRequest,\r\n requestTimeoutMs: number\r\n): Promise<ResilientResponse> {\r\n const ac = new AbortController();\r\n const timer = setTimeout(() => ac.abort(), requestTimeoutMs);\r\n\r\n try {\r\n const res = await undiciRequest(req.url, {\r\n method: req.method,\r\n headers: req.headers,\r\n body: req.body as any,\r\n signal: ac.signal,\r\n });\r\n\r\n const body = await res.body.arrayBuffer();\r\n return {\r\n status: res.statusCode,\r\n headers: normalizeHeaders(res.headers),\r\n body: new Uint8Array(body),\r\n };\r\n } catch (err: any) {\r\n // undici throws AbortError on abort\r\n if (err?.name === \"AbortError\") {\r\n throw new RequestTimeoutError(requestTimeoutMs);\r\n }\r\n throw err;\r\n } finally {\r\n clearTimeout(timer);\r\n }\r\n}\r\n"],"mappings":"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","names":["undiciRequest"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/client.ts","../src/errors.ts","../src/limiter.ts","../src/http.ts","../src/instant_get.ts"],"sourcesContent":["// src/index.ts\r\nexport * from \"./client.js\";\r\nexport * from \"./types.js\";\r\nexport * from \"./errors.js\";\r\nexport * from \"./instant_get.js\"\r\n","// src/client.ts\r\nimport { EventEmitter } from \"node:events\";\r\nimport { ConcurrencyLimiter } from \"./limiter.js\";\r\nimport { doHttpRequest } from \"./http.js\";\r\nimport {\r\n QueueFullError,\r\n RequestTimeoutError,\r\n ResilientHttpError,\r\n HalfOpenRejectedError,\r\n UpstreamUnhealthyError,\r\n} from \"./errors.js\";\r\nimport type {\r\n MicroCacheOptions,\r\n ResilientHttpClientOptions,\r\n ResilientRequest,\r\n ResilientResponse,\r\n} from \"./types.js\";\r\n\r\n/* ---------------------------- helpers ---------------------------- */\r\n\r\nfunction normalizeUrlForKey(rawUrl: string): string {\r\n const u = new URL(rawUrl);\r\n u.hostname = u.hostname.toLowerCase();\r\n\r\n const isHttpDefault = u.protocol === \"http:\" && u.port === \"80\";\r\n const isHttpsDefault = u.protocol === \"https:\" && u.port === \"443\";\r\n if (isHttpDefault || isHttpsDefault) u.port = \"\";\r\n\r\n return u.toString();\r\n}\r\n\r\nfunction baseUrlKey(rawUrl: string): string {\r\n const u = new URL(rawUrl);\r\n u.hostname = u.hostname.toLowerCase();\r\n\r\n const isHttpDefault = u.protocol === \"http:\" && u.port === \"80\";\r\n const isHttpsDefault = u.protocol === \"https:\" && u.port === \"443\";\r\n if (isHttpDefault || isHttpsDefault) u.port = \"\";\r\n\r\n return `${u.protocol}//${u.host}`;\r\n}\r\n\r\nfunction defaultMicroCacheKeyFn(req: ResilientRequest): string {\r\n return `GET ${normalizeUrlForKey(req.url)}`;\r\n}\r\n\r\nfunction genRequestId(): string {\r\n return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;\r\n}\r\n\r\nfunction sleep(ms: number): Promise<void> {\r\n return new Promise((r) => setTimeout(r, ms));\r\n}\r\n\r\nfunction clamp(n: number, lo: number, hi: number): number {\r\n return Math.max(lo, Math.min(hi, n));\r\n}\r\n\r\nfunction jitterMs(ms: number): number {\r\n const mult = 0.8 + Math.random() * 0.4; // [0.8, 1.2]\r\n return Math.round(ms * mult);\r\n}\r\n\r\n\r\n/* ---------------------------- health ---------------------------- */\r\n\r\ntype HealthState = \"OPEN\" | \"CLOSED\" | \"HALF_OPEN\";\r\ntype Outcome = \"SUCCESS\" | \"HARD_FAIL\" | \"SOFT_FAIL\";\r\n\r\ntype HealthTracker = {\r\n state: HealthState;\r\n\r\n window: Outcome[];\r\n windowSize: number; // 20\r\n minSamples: number; // 10\r\n\r\n consecutiveHardFails: number; // trip at 3\r\n\r\n cooldownBaseMs: number; // 1000\r\n cooldownCapMs: number; // 30000\r\n cooldownMs: number; // current backoff\r\n cooldownUntil: number;\r\n\r\n probeInFlight: boolean; // probeConcurrency=1\r\n probeRemaining: number; // 1 probe per half-open\r\n\r\n stableNonHard: number; // reset backoff after 5 non-hard after open\r\n};\r\n\r\nconst SOFT_FAIL_STATUSES = new Set([429, 502, 503, 504]);\r\n\r\nfunction classifyHttpStatus(status: number): Outcome {\r\n if (status >= 200 && status < 300) return \"SUCCESS\";\r\n if (SOFT_FAIL_STATUSES.has(status)) return \"SOFT_FAIL\";\r\n return \"SUCCESS\"; // do not penalize health for other statuses (incl 4xx except 429)\r\n}\r\n\r\nfunction computeRates(window: Outcome[]) {\r\n const total = window.length;\r\n let hard = 0;\r\n let soft = 0;\r\n for (const o of window) {\r\n if (o === \"HARD_FAIL\") hard += 1;\r\n else if (o === \"SOFT_FAIL\") soft += 1;\r\n }\r\n return {\r\n total,\r\n hard,\r\n soft,\r\n hardFailRate: total === 0 ? 0 : hard / total,\r\n failRate: total === 0 ? 0 : (hard + soft) / total,\r\n };\r\n}\r\n\r\n/**\r\n * Only HTTP-layer failures should count as HARD_FAIL.\r\n * Control-plane rejections must NOT poison health.\r\n */\r\nfunction shouldCountAsHardFail(err: unknown): boolean {\r\n if (err instanceof UpstreamUnhealthyError) return false;\r\n if (err instanceof HalfOpenRejectedError) return false;\r\n if (err instanceof QueueFullError) return false;\r\n\r\n // if your http layer throws this, it's a real hard fail (timeout)\r\n if (err instanceof RequestTimeoutError) return true;\r\n\r\n // If it’s a known library error but not one of the above, be conservative:\r\n // treat it as NOT a health signal unless it’s clearly HTTP-related.\r\n if (err instanceof ResilientHttpError) return false;\r\n\r\n // Unknown thrown error: assume it's an HTTP/network failure.\r\n return true;\r\n}\r\n\r\n/* ---------------------------- microcache types ---------------------------- */\r\n\r\ntype CacheEntry = {\r\n createdAt: number;\r\n expiresAt: number;\r\n value: ResilientResponse;\r\n};\r\n\r\ntype InFlightGroup = {\r\n promise: Promise<ResilientResponse>;\r\n windowStartMs: number;\r\n waiters: number;\r\n};\r\n\r\n/* ---------------------------- client ---------------------------- */\r\n\r\nexport class ResilientHttpClient extends EventEmitter {\r\n private readonly requestTimeoutMs: number;\r\n private readonly healthEnabled: boolean;\r\n\r\n private readonly limiters = new Map<string, ConcurrencyLimiter>();\r\n private readonly health = new Map<string, HealthTracker>();\r\n\r\n private readonly microCache?: Required<\r\n Pick<\r\n MicroCacheOptions,\r\n | \"enabled\"\r\n | \"ttlMs\"\r\n | \"maxStaleMs\"\r\n | \"maxEntries\"\r\n | \"maxWaiters\"\r\n | \"followerTimeoutMs\"\r\n | \"keyFn\"\r\n >\r\n > & {\r\n retry?: {\r\n maxAttempts: number;\r\n baseDelayMs: number;\r\n maxDelayMs: number;\r\n retryOnStatus: number[];\r\n };\r\n };\r\n\r\n private cache?: Map<string, CacheEntry>;\r\n private inFlight?: Map<string, InFlightGroup>;\r\n\r\n private microCacheReqCount = 0;\r\n private readonly cleanupEveryNRequests = 100;\r\n\r\n constructor(private readonly opts: ResilientHttpClientOptions) {\r\n super();\r\n this.requestTimeoutMs = opts.requestTimeoutMs;\r\n this.healthEnabled = opts.health?.enabled ?? true;\r\n\r\n const mc = opts.microCache;\r\n if (mc?.enabled) {\r\n const retry = mc.retry\r\n ? {\r\n maxAttempts: mc.retry.maxAttempts ?? 3,\r\n baseDelayMs: mc.retry.baseDelayMs ?? 50,\r\n maxDelayMs: mc.retry.maxDelayMs ?? 200,\r\n retryOnStatus: mc.retry.retryOnStatus ?? [429, 502, 503, 504],\r\n }\r\n : undefined;\r\n\r\n this.microCache = {\r\n enabled: true,\r\n ttlMs: mc.ttlMs ?? 1000,\r\n maxStaleMs: mc.maxStaleMs ?? 10_000,\r\n maxEntries: mc.maxEntries ?? 500,\r\n maxWaiters: mc.maxWaiters ?? 1000,\r\n followerTimeoutMs: mc.followerTimeoutMs ?? 5000,\r\n keyFn: mc.keyFn ?? defaultMicroCacheKeyFn,\r\n retry,\r\n };\r\n\r\n this.cache = new Map();\r\n this.inFlight = new Map();\r\n }\r\n }\r\n\r\n async request(req: ResilientRequest): Promise<ResilientResponse> {\r\n if (this.microCache?.enabled && req.method === \"GET\" && req.body == null) {\r\n return this.requestWithMicroCache(req);\r\n }\r\n return this.execute(req, { allowProbe: false });\r\n }\r\n /**\r\n * Run a HALF_OPEN probe. Exactly one probe is allowed per HALF_OPEN window.\r\n * Normal request() calls are rejected during HALF_OPEN.\r\n */\r\n async probe(req: ResilientRequest): Promise<ResilientResponse> {\r\n if (this.microCache?.enabled && req.method === \"GET\" && req.body == null) {\r\n // microcache path already supports probe, but keep consistent behavior:\r\n // force direct execute so this call is always a real probe attempt.\r\n return this.execute(req, { allowProbe: true });\r\n }\r\n return this.execute(req, { allowProbe: true });\r\n }\r\n\r\n snapshot(): { inFlight: number; queueDepth: number } {\r\n let inFlight = 0;\r\n let queueDepth = 0;\r\n for (const l of this.limiters.values()) {\r\n const s = l.snapshot();\r\n inFlight += s.inFlight;\r\n queueDepth += s.queueDepth;\r\n }\r\n return { inFlight, queueDepth };\r\n }\r\n\r\n /* ---------------- internals ---------------- */\r\n\r\n private getLimiter(baseKey: string): ConcurrencyLimiter {\r\n let l = this.limiters.get(baseKey);\r\n if (!l) {\r\n l = new ConcurrencyLimiter({\r\n maxInFlight: this.opts.maxInFlight,\r\n maxQueue: this.opts.maxInFlight * 10, // hidden factor\r\n });\r\n this.limiters.set(baseKey, l);\r\n }\r\n return l;\r\n }\r\n\r\n private getHealth(baseKey: string): HealthTracker {\r\n let h = this.health.get(baseKey);\r\n if (!h) {\r\n h = {\r\n state: \"OPEN\",\r\n window: [],\r\n windowSize: 20,\r\n minSamples: 10,\r\n consecutiveHardFails: 0,\r\n cooldownBaseMs: 1000,\r\n cooldownCapMs: 30_000,\r\n cooldownMs: 1000,\r\n cooldownUntil: 0,\r\n probeInFlight: false,\r\n probeRemaining: 0,\r\n stableNonHard: 0,\r\n };\r\n this.health.set(baseKey, h);\r\n }\r\n return h;\r\n }\r\n\r\n private closeHealth(baseKey: string, reason: string): void {\r\n const h = this.getHealth(baseKey);\r\n if (h.state === \"CLOSED\") return;\r\n\r\n h.state = \"CLOSED\";\r\n h.cooldownUntil = Date.now() + jitterMs(h.cooldownMs);\r\n h.cooldownMs = Math.min(h.cooldownMs * 2, h.cooldownCapMs);\r\n\r\n // reject queued immediately\r\n this.getLimiter(baseKey).flush(new UpstreamUnhealthyError(baseKey, \"CLOSED\"));\r\n\r\n const rates = computeRates(h.window);\r\n this.emit(\"health:closed\", {\r\n baseUrl: baseKey,\r\n reason,\r\n cooldownMs: h.cooldownUntil - Date.now(),\r\n hardFailRate: rates.hardFailRate,\r\n failRate: rates.failRate,\r\n samples: rates.total,\r\n });\r\n }\r\n\r\n private halfOpenHealth(baseKey: string): void {\r\n const h = this.getHealth(baseKey);\r\n if (h.state !== \"CLOSED\") return;\r\n\r\n h.state = \"HALF_OPEN\";\r\n h.probeInFlight = false;\r\n h.probeRemaining = 1;\r\n this.emit(\"health:half_open\", { baseUrl: baseKey });\r\n }\r\n\r\n private openHealth(baseKey: string): void {\r\n const h = this.getHealth(baseKey);\r\n h.state = \"OPEN\";\r\n h.window = [];\r\n h.consecutiveHardFails = 0;\r\n h.probeInFlight = false;\r\n h.probeRemaining = 0;\r\n h.stableNonHard = 0;\r\n this.emit(\"health:open\", { baseUrl: baseKey });\r\n }\r\n\r\n private recordOutcome(baseKey: string, outcome: Outcome): void {\r\n const h = this.getHealth(baseKey);\r\n\r\n h.window.push(outcome);\r\n while (h.window.length > h.windowSize) h.window.shift();\r\n\r\n if (outcome === \"HARD_FAIL\") h.consecutiveHardFails += 1;\r\n else h.consecutiveHardFails = 0;\r\n\r\n // stabilization (reset backoff after 5 non-hard in OPEN)\r\n if (h.state === \"OPEN\") {\r\n if (outcome !== \"HARD_FAIL\") {\r\n h.stableNonHard += 1;\r\n if (h.stableNonHard >= 5) {\r\n h.cooldownMs = h.cooldownBaseMs;\r\n }\r\n } else {\r\n h.stableNonHard = 0;\r\n }\r\n }\r\n\r\n if (!this.healthEnabled) return;\r\n\r\n if (h.consecutiveHardFails >= 3) {\r\n this.closeHealth(baseKey, \"3 consecutive hard failures\");\r\n return;\r\n }\r\n\r\n const rates = computeRates(h.window);\r\n if (rates.total >= h.minSamples) {\r\n if (rates.hardFailRate >= 0.3) {\r\n this.closeHealth(baseKey, \"hardFailRate >= 30%\");\r\n return;\r\n }\r\n if (rates.failRate >= 0.5) {\r\n this.closeHealth(baseKey, \"failRate >= 50%\");\r\n return;\r\n }\r\n }\r\n }\r\n\r\n private async execute(req: ResilientRequest, opts: { allowProbe: boolean }): Promise<ResilientResponse> {\r\n const baseKey = baseUrlKey(req.url);\r\n const h = this.getHealth(baseKey);\r\n const limiter = this.getLimiter(baseKey);\r\n\r\n if (this.healthEnabled) {\r\n if (h.state === \"CLOSED\") {\r\n if (Date.now() >= h.cooldownUntil) {\r\n this.halfOpenHealth(baseKey);\r\n } else {\r\n throw new UpstreamUnhealthyError(baseKey, \"CLOSED\");\r\n }\r\n }\r\n\r\n if (h.state === \"HALF_OPEN\") {\r\n if (!opts.allowProbe) throw new HalfOpenRejectedError(baseKey);\r\n if (h.probeRemaining <= 0 || h.probeInFlight) throw new HalfOpenRejectedError(baseKey);\r\n h.probeInFlight = true;\r\n h.probeRemaining -= 1;\r\n }\r\n }\r\n\r\n const requestId = genRequestId();\r\n const start = Date.now();\r\n let acquired = false;\r\n try {\r\n // Probes should not wait in queue.\r\n if (this.healthEnabled && h.state === \"HALF_OPEN\") {\r\n await limiter.acquireNoQueue();\r\n } else {\r\n await limiter.acquire();\r\n\r\n }\r\n acquired = true;\r\n } catch (err) {\r\n this.emit(\"request:rejected\", { requestId, request: req, error: err });\r\n throw err;\r\n }\r\n\r\n this.emit(\"request:start\", { requestId, request: req });\r\n\r\n try {\r\n const res = await doHttpRequest(req, this.requestTimeoutMs);\r\n const durationMs = Date.now() - start;\r\n\r\n const outcome = classifyHttpStatus(res.status);\r\n this.recordOutcome(baseKey, outcome);\r\n\r\n // Probe decision\r\n if (this.healthEnabled && h.state === \"HALF_OPEN\") {\r\n this.emit(\"health:probe\", { baseUrl: baseKey, outcome, status: res.status });\r\n if (res.status >= 200 && res.status < 300) {\r\n this.openHealth(baseKey);\r\n } else {\r\n this.closeHealth(baseKey, `probe failed status=${res.status}`);\r\n }\r\n }\r\n\r\n this.emit(\"request:success\", { requestId, request: req, status: res.status, durationMs });\r\n return res;\r\n } catch (err) {\r\n const durationMs = Date.now() - start;\r\n\r\n if (shouldCountAsHardFail(err)) {\r\n this.recordOutcome(baseKey, \"HARD_FAIL\");\r\n }\r\n\r\n if (this.healthEnabled && h.state === \"HALF_OPEN\") {\r\n this.emit(\"health:probe\", { baseUrl: baseKey, outcome: \"HARD_FAIL\", error: err });\r\n this.closeHealth(baseKey, \"probe hard failure\");\r\n }\r\n\r\n this.emit(\"request:failure\", { requestId, request: req, error: err, durationMs });\r\n throw err;\r\n } finally {\r\n // Clear probe flag (if any)\r\n if (this.healthEnabled && h.state === \"HALF_OPEN\") {\r\n h.probeInFlight = false;\r\n }\r\n if (acquired) limiter.release();\r\n }\r\n }\r\n\r\n /* ---------------- microcache ---------------- */\r\n\r\n private cloneResponse(res: ResilientResponse): ResilientResponse {\r\n return { status: res.status, headers: { ...res.headers }, body: new Uint8Array(res.body) };\r\n }\r\n\r\n private maybeCleanupExpired(cache: Map<string, CacheEntry>, maxStaleMs: number): void {\r\n this.microCacheReqCount++;\r\n if (this.microCacheReqCount % this.cleanupEveryNRequests !== 0) return;\r\n\r\n const now = Date.now();\r\n for (const [k, v] of cache.entries()) {\r\n if (now - v.createdAt > maxStaleMs) cache.delete(k);\r\n }\r\n }\r\n\r\n private evictIfNeeded(cache: Map<string, CacheEntry>, maxEntries: number): void {\r\n while (cache.size >= maxEntries) {\r\n const oldestKey = cache.keys().next().value as string | undefined;\r\n if (!oldestKey) break;\r\n cache.delete(oldestKey);\r\n }\r\n }\r\n\r\n private isRetryableStatus(status: number, retryOnStatus: number[]): boolean {\r\n return retryOnStatus.includes(status);\r\n }\r\n\r\n private computeBackoffMs(attemptIndex: number, baseDelayMs: number, maxDelayMs: number): number {\r\n const exp = baseDelayMs * Math.pow(2, attemptIndex - 1);\r\n const capped = clamp(exp, baseDelayMs, maxDelayMs);\r\n const jitter = 0.5 + Math.random();\r\n return Math.round(capped * jitter);\r\n }\r\n\r\n private async fetchWithLeaderRetry(req: ResilientRequest): Promise<ResilientResponse> {\r\n const mc = this.microCache!;\r\n const retry = mc.retry;\r\n if (!retry) return this.execute(req, { allowProbe: false });\r\n\r\n const { maxAttempts, baseDelayMs, maxDelayMs, retryOnStatus } = retry;\r\n\r\n let last: ResilientResponse | undefined;\r\n for (let attempt = 1; attempt <= maxAttempts; attempt++) {\r\n const res = await this.execute(req, { allowProbe: false });\r\n last = res;\r\n\r\n if (this.isRetryableStatus(res.status, retryOnStatus) && attempt < maxAttempts) {\r\n const delay = this.computeBackoffMs(attempt, baseDelayMs, maxDelayMs);\r\n this.emit(\"microcache:retry\", { url: req.url, attempt, maxAttempts, reason: `status ${res.status}`, delayMs: delay });\r\n await sleep(delay);\r\n continue;\r\n }\r\n return res;\r\n }\r\n return last!;\r\n }\r\n\r\n private async requestWithMicroCache(req: ResilientRequest): Promise<ResilientResponse> {\r\n const mc = this.microCache!;\r\n const cache = this.cache!;\r\n const inFlight = this.inFlight!;\r\n\r\n this.maybeCleanupExpired(cache, mc.maxStaleMs);\r\n\r\n const key = mc.keyFn(req);\r\n const now = Date.now();\r\n\r\n const hit0 = cache.get(key);\r\n if (hit0 && now - hit0.createdAt > mc.maxStaleMs) cache.delete(key);\r\n\r\n const hit = cache.get(key);\r\n if (hit && now < hit.expiresAt) return this.cloneResponse(hit.value);\r\n\r\n // If CLOSED: serve stale if allowed else fail fast\r\n if (this.healthEnabled) {\r\n const baseKey = baseUrlKey(req.url);\r\n const h = this.getHealth(baseKey);\r\n if (h.state === \"CLOSED\") {\r\n const staleAllowed = !!hit && now - hit.createdAt <= mc.maxStaleMs;\r\n if (staleAllowed) return this.cloneResponse(hit!.value);\r\n throw new UpstreamUnhealthyError(baseKey, \"CLOSED\");\r\n }\r\n }\r\n\r\n const group = inFlight.get(key);\r\n if (group) {\r\n const h = cache.get(key);\r\n const staleAllowed = !!h && now - h.createdAt <= mc.maxStaleMs;\r\n\r\n if (h && staleAllowed) return this.cloneResponse(h.value);\r\n\r\n const age = now - group.windowStartMs;\r\n if (age > mc.followerTimeoutMs) {\r\n const err = new Error(`Follower window closed for key=${key}`);\r\n (err as any).name = \"FollowerWindowClosedError\";\r\n throw err;\r\n }\r\n\r\n if (group.waiters >= mc.maxWaiters) {\r\n const err = new Error(`Too many followers for key=${key}`);\r\n (err as any).name = \"TooManyWaitersError\";\r\n throw err;\r\n }\r\n\r\n group.waiters += 1;\r\n try {\r\n const res = await group.promise;\r\n return this.cloneResponse(res);\r\n } finally {\r\n group.waiters -= 1;\r\n }\r\n }\r\n\r\n const prev = cache.get(key);\r\n const prevStaleAllowed = !!prev && now - prev.createdAt <= mc.maxStaleMs;\r\n\r\n const promise = (async () => {\r\n const baseKey = baseUrlKey(req.url);\r\n const h = this.getHealth(baseKey);\r\n const allowProbe = this.healthEnabled && h.state === \"HALF_OPEN\";\r\n\r\n const res = allowProbe\r\n ? await this.execute(req, { allowProbe: true })\r\n : await this.fetchWithLeaderRetry(req);\r\n\r\n if (res.status >= 200 && res.status < 300) {\r\n this.evictIfNeeded(cache, mc.maxEntries);\r\n const t = Date.now();\r\n cache.set(key, { value: this.cloneResponse(res), createdAt: t, expiresAt: t + mc.ttlMs });\r\n }\r\n return res;\r\n })();\r\n\r\n inFlight.set(key, { promise, windowStartMs: Date.now(), waiters: 0 });\r\n\r\n try {\r\n const res = await promise;\r\n\r\n if (!(res.status >= 200 && res.status < 300) && prev && prevStaleAllowed) {\r\n return this.cloneResponse(prev.value);\r\n }\r\n\r\n return this.cloneResponse(res);\r\n } catch (err) {\r\n if (prev && prevStaleAllowed) {\r\n this.emit(\"microcache:refresh_failed\", { key, url: req.url, error: err });\r\n return this.cloneResponse(prev.value);\r\n }\r\n throw err;\r\n } finally {\r\n inFlight.delete(key);\r\n }\r\n }\r\n}\r\n","export abstract class ResilientHttpError extends Error {\r\n public readonly name: string;\r\n constructor(message: string) {\r\n super(message);\r\n this.name = this.constructor.name;\r\n }\r\n}\r\n\r\nexport class QueueFullError extends ResilientHttpError {\r\n constructor(public readonly maxQueue: number) {\r\n super(`Queue is full (maxQueue=${maxQueue}).`);\r\n }\r\n}\r\n\r\nexport class QueueTimeoutError extends ResilientHttpError {\r\n constructor(public readonly enqueueTimeoutMs: number) {\r\n super(`Queue wait exceeded (enqueueTimeoutMs=${enqueueTimeoutMs}).`);\r\n }\r\n}\r\n\r\nexport class RequestTimeoutError extends ResilientHttpError {\r\n constructor(public readonly requestTimeoutMs: number) {\r\n super(`Request timed out (requestTimeoutMs=${requestTimeoutMs}).`);\r\n }\r\n}\r\n\r\n\r\n\r\nexport class UpstreamError extends ResilientHttpError {\r\n constructor(public readonly status: number) {\r\n super(`Upstream returned error status=${status}.`);\r\n }\r\n}\r\n\r\n\r\n\r\n\r\nexport class UpstreamUnhealthyError extends ResilientHttpError {\r\n constructor(public readonly baseUrl: string, public readonly state: string) {\r\n super(`Upstream is unhealthy (state=${state}, baseUrl=${baseUrl}).`);\r\n }\r\n}\r\n\r\nexport class HalfOpenRejectedError extends ResilientHttpError {\r\n constructor(public readonly baseUrl: string) {\r\n super(`Upstream is HALF_OPEN (probe only) for baseUrl=${baseUrl}.`);\r\n }\r\n}\r\n\r\n","// src/limiter.ts\r\nimport { QueueFullError } from \"./errors.js\";\r\n\r\ntype ResolveFn = () => void;\r\ntype RejectFn = (err: unknown) => void;\r\n\r\ninterface Waiter {\r\n resolve: ResolveFn;\r\n reject: RejectFn;\r\n}\r\n\r\n/**\r\n * Process-local concurrency limiter with bounded FIFO queue.\r\n *\r\n * - maxInFlight: concurrent permits\r\n * - maxQueue: bounded burst buffer\r\n * - No enqueue-timeout by design.\r\n *\r\n * Also supports:\r\n * - flush(err): reject all queued waiters immediately\r\n * - acquireNoQueue(): for probes (must start now or fail)\r\n */\r\nexport class ConcurrencyLimiter {\r\n private readonly maxInFlight: number;\r\n private readonly maxQueue: number;\r\n\r\n private inFlight = 0;\r\n private queue: Waiter[] = [];\r\n\r\n constructor(opts: { maxInFlight: number; maxQueue: number }) {\r\n if (!Number.isFinite(opts.maxInFlight) || opts.maxInFlight <= 0) {\r\n throw new Error(`maxInFlight must be > 0 (got ${opts.maxInFlight})`);\r\n }\r\n if (!Number.isFinite(opts.maxQueue) || opts.maxQueue < 0) {\r\n throw new Error(`maxQueue must be >= 0 (got ${opts.maxQueue})`);\r\n }\r\n\r\n this.maxInFlight = opts.maxInFlight;\r\n this.maxQueue = opts.maxQueue;\r\n }\r\n\r\n acquire(): Promise<void> {\r\n if (this.inFlight < this.maxInFlight) {\r\n this.inFlight += 1;\r\n return Promise.resolve();\r\n }\r\n\r\n if (this.maxQueue === 0 || this.queue.length >= this.maxQueue) {\r\n return Promise.reject(new QueueFullError(this.maxQueue));\r\n }\r\n\r\n return new Promise<void>((resolve, reject) => {\r\n this.queue.push({ resolve, reject });\r\n });\r\n }\r\n\r\n /**\r\n * Acquire without queueing: either start now or fail.\r\n * Used for HALF_OPEN probes so recovery never waits behind backlog.\r\n */\r\n acquireNoQueue(): Promise<void> {\r\n if (this.inFlight < this.maxInFlight) {\r\n this.inFlight += 1;\r\n return Promise.resolve();\r\n }\r\n // treat as queue full (we don't want a new error type)\r\n return Promise.reject(new QueueFullError(0));\r\n }\r\n\r\n release(): void {\r\n if (this.inFlight <= 0) {\r\n throw new Error(\"release() called when inFlight is already 0\");\r\n }\r\n\r\n const next = this.queue.shift();\r\n if (next) {\r\n next.resolve(); // transfer permit\r\n return;\r\n }\r\n\r\n this.inFlight -= 1;\r\n }\r\n\r\n flush(err: unknown): void {\r\n const q = this.queue;\r\n this.queue = [];\r\n for (const w of q) w.reject(err);\r\n }\r\n\r\n snapshot(): { inFlight: number; queueDepth: number; maxInFlight: number; maxQueue: number } {\r\n return {\r\n inFlight: this.inFlight,\r\n queueDepth: this.queue.length,\r\n maxInFlight: this.maxInFlight,\r\n maxQueue: this.maxQueue,\r\n };\r\n }\r\n}\r\n","// src/http.ts\r\nimport { request as undiciRequest } from \"undici\";\r\nimport { RequestTimeoutError } from \"./errors.js\";\r\nimport type { ResilientRequest, ResilientResponse } from \"./types.js\";\r\n\r\nfunction normalizeHeaders(headers: any): Record<string, string> {\r\n const out: Record<string, string> = {};\r\n if (!headers) return out;\r\n\r\n // undici headers are an object-like structure (headers: Record<string, string | string[]>)\r\n for (const [k, v] of Object.entries(headers)) {\r\n if (Array.isArray(v)) out[k.toLowerCase()] = v.join(\", \");\r\n else if (typeof v === \"string\") out[k.toLowerCase()] = v;\r\n else out[k.toLowerCase()] = String(v);\r\n }\r\n return out;\r\n}\r\n\r\n/**\r\n * Execute a single HTTP request with a hard timeout using AbortController.\r\n * No retries. No breaker. Just raw outbound I/O with a timeout.\r\n */\r\nexport async function doHttpRequest(\r\n req: ResilientRequest,\r\n requestTimeoutMs: number\r\n): Promise<ResilientResponse> {\r\n const ac = new AbortController();\r\n const timer = setTimeout(() => ac.abort(), requestTimeoutMs);\r\n\r\n try {\r\n const res = await undiciRequest(req.url, {\r\n method: req.method,\r\n headers: req.headers,\r\n body: req.body as any,\r\n signal: ac.signal,\r\n });\r\n\r\n const body = await res.body.arrayBuffer();\r\n return {\r\n status: res.statusCode,\r\n headers: normalizeHeaders(res.headers),\r\n body: new Uint8Array(body),\r\n };\r\n } catch (err: any) {\r\n // undici throws AbortError on abort\r\n if (err?.name === \"AbortError\") {\r\n throw new RequestTimeoutError(requestTimeoutMs);\r\n }\r\n throw err;\r\n } finally {\r\n clearTimeout(timer);\r\n }\r\n}\r\n","// src/instant_get.ts\r\n//\r\n// Instant GET store (single URL):\r\n// - GET-only\r\n// - Polls one URL on an interval (default 5000ms)\r\n// - Stores latest successful ResilientResponse\r\n// - get() returns cached value instantly (or undefined)\r\n// - Cached value is valid for 60s only (expired => undefined)\r\n// - On error: stop OR retry N times (default: retry forever)\r\n// - Infinite retry uses backoff: 1s, 5s, 10s, 30s, 60s, 60s...\r\n// - start() never blocks\r\n// - ready flips to true after first successful fetch (2xx)\r\n// - waitReady(timeoutSec=10) polls ready every 1s and returns true/false (never throws)\r\n\r\nimport type {\r\n ResilientResponse,\r\n InstantGetOptions,\r\n InstantGetSnapshot,\r\n InstantGetOnError,\r\n} from \"./types.js\";\r\n\r\nconst VALID_MS = 60_000;\r\nconst RETRY_BACKOFF_MS = [1000, 5000, 10_000, 30_000, 60_000];\r\n\r\ntype Entry = {\r\n url?: string;\r\n intervalMs: number;\r\n onError: InstantGetOnError;\r\n\r\n timer?: NodeJS.Timeout;\r\n inFlight: boolean;\r\n\r\n last?: ResilientResponse;\r\n lastOkAt?: number;\r\n lastStatus?: number;\r\n\r\n lastErrorName?: string;\r\n consecutiveErrors: number;\r\n retriesRemaining: number; // Infinity allowed\r\n\r\n // backoff stage for infinite retry\r\n backoffStage: number;\r\n\r\n // next scheduled delay (for snapshot)\r\n nextDelayMs?: number;\r\n\r\n // readiness: becomes true after first successful fetch (2xx)\r\n ready: boolean;\r\n};\r\n\r\nfunction cloneResponse(res: ResilientResponse): ResilientResponse {\r\n return {\r\n status: res.status,\r\n headers: { ...res.headers },\r\n body: new Uint8Array(res.body),\r\n };\r\n}\r\n\r\nfunction headersToRecord(headers: Headers): Record<string, string> {\r\n const out: Record<string, string> = {};\r\n headers.forEach((v, k) => {\r\n out[k] = v;\r\n });\r\n return out;\r\n}\r\n\r\nfunction toRetryCount(onError?: InstantGetOnError): number {\r\n if (!onError) return Number.POSITIVE_INFINITY;\r\n if (onError === \"stop\") return 0;\r\n\r\n const n = onError.retry;\r\n if (!Number.isFinite(n)) return Number.POSITIVE_INFINITY;\r\n return Math.max(0, Math.floor(n));\r\n}\r\n\r\nfunction nextBackoffDelayMs(stage: number): number {\r\n const idx = Math.min(stage, RETRY_BACKOFF_MS.length - 1);\r\n return RETRY_BACKOFF_MS[idx];\r\n}\r\n\r\nfunction sleep(ms: number): Promise<void> {\r\n return new Promise((r) => setTimeout(r, ms));\r\n}\r\n\r\nasync function fetchGet(url: string): Promise<ResilientResponse> {\r\n const res = await fetch(url, { method: \"GET\" });\r\n\r\n const body = new Uint8Array(await res.arrayBuffer());\r\n return {\r\n status: res.status,\r\n headers: headersToRecord(res.headers),\r\n body,\r\n };\r\n}\r\n\r\nexport class InstantGetStore {\r\n private state: Entry = {\r\n url: undefined,\r\n intervalMs: 5000,\r\n onError: { retry: Number.POSITIVE_INFINITY },\r\n\r\n timer: undefined,\r\n inFlight: false,\r\n\r\n last: undefined,\r\n lastOkAt: undefined,\r\n lastStatus: undefined,\r\n\r\n lastErrorName: undefined,\r\n consecutiveErrors: 0,\r\n retriesRemaining: Number.POSITIVE_INFINITY,\r\n\r\n backoffStage: 0,\r\n nextDelayMs: undefined,\r\n\r\n ready: false,\r\n };\r\n\r\n start(url: string, opts?: InstantGetOptions): void {\r\n // stop previous loop\r\n if (this.state.timer) this.stop();\r\n\r\n const intervalMs = opts?.intervalMs ?? 5000;\r\n if (!Number.isFinite(intervalMs) || intervalMs <= 0) {\r\n throw new Error(`intervalMs must be > 0 (got ${intervalMs})`);\r\n }\r\n\r\n const onError = opts?.onError ?? { retry: Number.POSITIVE_INFINITY };\r\n\r\n this.state.url = url;\r\n this.state.intervalMs = intervalMs;\r\n this.state.onError = onError;\r\n this.state.retriesRemaining = toRetryCount(onError);\r\n\r\n // reset counters for new run\r\n this.state.backoffStage = 0;\r\n this.state.nextDelayMs = undefined;\r\n this.state.inFlight = false;\r\n\r\n // readiness reset for this run\r\n this.state.ready = false;\r\n\r\n // schedule immediate tick\r\n this.scheduleNext(0);\r\n }\r\n\r\n stop(): void {\r\n if (this.state.timer) clearTimeout(this.state.timer);\r\n this.state.timer = undefined;\r\n\r\n this.state.nextDelayMs = undefined;\r\n this.state.inFlight = false;\r\n this.state.ready = false;\r\n }\r\n\r\n /** Returns true only after first successful fetch (2xx) for the current run. */\r\n isReady(): boolean {\r\n return this.state.ready;\r\n }\r\n\r\n /**\r\n * Polls `ready` every 1 second.\r\n * Returns true if ready becomes true within timeoutSec (default 10), else false.\r\n * Never throws.\r\n */\r\n async waitReady(timeoutSec = 10): Promise<boolean> {\r\n if (!Number.isFinite(timeoutSec) || timeoutSec < 0) timeoutSec = 10;\r\n\r\n const timeoutMs = Math.floor(timeoutSec * 1000);\r\n const start = Date.now();\r\n\r\n while (true) {\r\n if (this.state.ready) return true;\r\n if (Date.now() - start >= timeoutMs) return false;\r\n await sleep(1000);\r\n }\r\n }\r\n\r\n /** Instant read: returns last successful response if <= 60s old, else undefined */\r\n get(): ResilientResponse | undefined {\r\n if (!this.state.last || !this.state.lastOkAt) return undefined;\r\n\r\n const age = Date.now() - this.state.lastOkAt;\r\n if (age > VALID_MS) return undefined;\r\n\r\n return cloneResponse(this.state.last);\r\n }\r\n\r\n snapshot(): InstantGetSnapshot | undefined {\r\n if (!this.state.url) return undefined;\r\n\r\n return {\r\n url: this.state.url,\r\n running: !!this.state.timer,\r\n intervalMs: this.state.intervalMs,\r\n\r\n lastOkAt: this.state.lastOkAt,\r\n lastStatus: this.state.lastStatus,\r\n\r\n lastErrorName: this.state.lastErrorName,\r\n consecutiveErrors: this.state.consecutiveErrors,\r\n\r\n inFlight: this.state.inFlight,\r\n ready: this.state.ready,\r\n nextDelayMs: this.state.nextDelayMs,\r\n };\r\n }\r\n\r\n /* ---------------- internals ---------------- */\r\n\r\n private scheduleNext(delayMs: number): void {\r\n const d = Math.max(0, delayMs);\r\n this.state.nextDelayMs = d;\r\n\r\n if (this.state.timer) clearTimeout(this.state.timer);\r\n this.state.timer = setTimeout(() => {\r\n void this.tick();\r\n }, d);\r\n }\r\n\r\n private async tick(): Promise<void> {\r\n if (!this.state.url) return;\r\n if (!this.state.timer) return; // stopped\r\n\r\n if (this.state.inFlight) {\r\n // should be rare with setTimeout loop, but keep safe\r\n this.scheduleNext(this.state.intervalMs);\r\n return;\r\n }\r\n\r\n this.state.inFlight = true;\r\n\r\n try {\r\n const res = await fetchGet(this.state.url);\r\n\r\n // Only 2xx counts as success\r\n if (res.status < 200 || res.status >= 300) {\r\n const err = new Error(`Non-2xx status=${res.status}`);\r\n (err as any).name = \"InstantGetNon2xxError\";\r\n throw err;\r\n }\r\n\r\n // success: store + mark ready\r\n this.state.last = cloneResponse(res);\r\n this.state.lastOkAt = Date.now();\r\n this.state.lastStatus = res.status;\r\n\r\n this.state.lastErrorName = undefined;\r\n this.state.consecutiveErrors = 0;\r\n\r\n // reset backoff + retry budget on success\r\n this.state.backoffStage = 0;\r\n this.state.retriesRemaining = toRetryCount(this.state.onError);\r\n\r\n // becomes ready after first success\r\n this.state.ready = true;\r\n\r\n // next normal interval\r\n this.scheduleNext(this.state.intervalMs);\r\n } catch (err) {\r\n this.state.lastErrorName = (err as any)?.name ?? \"Error\";\r\n this.state.consecutiveErrors += 1;\r\n\r\n // If onError=stop => stop immediately\r\n if (this.state.onError === \"stop\") {\r\n this.stop();\r\n return;\r\n }\r\n\r\n // Finite retry budget: fixed-interval retries, then stop\r\n if (Number.isFinite(this.state.retriesRemaining)) {\r\n if (this.state.retriesRemaining <= 0) {\r\n this.stop();\r\n return;\r\n }\r\n\r\n this.state.retriesRemaining -= 1;\r\n\r\n if (this.state.retriesRemaining <= 0) {\r\n this.stop();\r\n return;\r\n }\r\n\r\n // fixed retry interval\r\n this.state.backoffStage = 0;\r\n this.scheduleNext(this.state.intervalMs);\r\n return;\r\n }\r\n\r\n // Infinite retry uses backoff schedule\r\n const delay = nextBackoffDelayMs(this.state.backoffStage);\r\n this.state.backoffStage += 1;\r\n\r\n this.scheduleNext(delay);\r\n } finally {\r\n this.state.inFlight = false;\r\n }\r\n }\r\n}\r\n"],"mappings":"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","names":["undiciRequest","sleep"]}
|
package/dist/index.d.cts
CHANGED
|
@@ -43,6 +43,25 @@ interface ResilientHttpClientOptions {
|
|
|
43
43
|
health?: HealthOptions;
|
|
44
44
|
microCache?: MicroCacheOptions;
|
|
45
45
|
}
|
|
46
|
+
type InstantGetOnError = "stop" | {
|
|
47
|
+
retry: number;
|
|
48
|
+
};
|
|
49
|
+
interface InstantGetOptions {
|
|
50
|
+
intervalMs?: number;
|
|
51
|
+
onError?: InstantGetOnError;
|
|
52
|
+
}
|
|
53
|
+
interface InstantGetSnapshot {
|
|
54
|
+
url: string;
|
|
55
|
+
running: boolean;
|
|
56
|
+
intervalMs: number;
|
|
57
|
+
lastOkAt?: number;
|
|
58
|
+
lastStatus?: number;
|
|
59
|
+
lastErrorName?: string;
|
|
60
|
+
consecutiveErrors: number;
|
|
61
|
+
inFlight: boolean;
|
|
62
|
+
ready: boolean;
|
|
63
|
+
nextDelayMs?: number;
|
|
64
|
+
}
|
|
46
65
|
|
|
47
66
|
declare class ResilientHttpClient extends EventEmitter {
|
|
48
67
|
private readonly opts;
|
|
@@ -57,6 +76,11 @@ declare class ResilientHttpClient extends EventEmitter {
|
|
|
57
76
|
private readonly cleanupEveryNRequests;
|
|
58
77
|
constructor(opts: ResilientHttpClientOptions);
|
|
59
78
|
request(req: ResilientRequest): Promise<ResilientResponse>;
|
|
79
|
+
/**
|
|
80
|
+
* Run a HALF_OPEN probe. Exactly one probe is allowed per HALF_OPEN window.
|
|
81
|
+
* Normal request() calls are rejected during HALF_OPEN.
|
|
82
|
+
*/
|
|
83
|
+
probe(req: ResilientRequest): Promise<ResilientResponse>;
|
|
60
84
|
snapshot(): {
|
|
61
85
|
inFlight: number;
|
|
62
86
|
queueDepth: number;
|
|
@@ -107,4 +131,23 @@ declare class HalfOpenRejectedError extends ResilientHttpError {
|
|
|
107
131
|
constructor(baseUrl: string);
|
|
108
132
|
}
|
|
109
133
|
|
|
110
|
-
|
|
134
|
+
declare class InstantGetStore {
|
|
135
|
+
private state;
|
|
136
|
+
start(url: string, opts?: InstantGetOptions): void;
|
|
137
|
+
stop(): void;
|
|
138
|
+
/** Returns true only after first successful fetch (2xx) for the current run. */
|
|
139
|
+
isReady(): boolean;
|
|
140
|
+
/**
|
|
141
|
+
* Polls `ready` every 1 second.
|
|
142
|
+
* Returns true if ready becomes true within timeoutSec (default 10), else false.
|
|
143
|
+
* Never throws.
|
|
144
|
+
*/
|
|
145
|
+
waitReady(timeoutSec?: number): Promise<boolean>;
|
|
146
|
+
/** Instant read: returns last successful response if <= 60s old, else undefined */
|
|
147
|
+
get(): ResilientResponse | undefined;
|
|
148
|
+
snapshot(): InstantGetSnapshot | undefined;
|
|
149
|
+
private scheduleNext;
|
|
150
|
+
private tick;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export { HalfOpenRejectedError, type HealthOptions, type HttpMethod, type InstantGetOnError, type InstantGetOptions, type InstantGetSnapshot, InstantGetStore, type MicroCacheOptions, type MicroCacheRetryOptions, QueueFullError, QueueTimeoutError, RequestTimeoutError, ResilientHttpClient, type ResilientHttpClientOptions, ResilientHttpError, type ResilientRequest, type ResilientResponse, UpstreamError, UpstreamUnhealthyError };
|
package/dist/index.d.ts
CHANGED
|
@@ -43,6 +43,25 @@ interface ResilientHttpClientOptions {
|
|
|
43
43
|
health?: HealthOptions;
|
|
44
44
|
microCache?: MicroCacheOptions;
|
|
45
45
|
}
|
|
46
|
+
type InstantGetOnError = "stop" | {
|
|
47
|
+
retry: number;
|
|
48
|
+
};
|
|
49
|
+
interface InstantGetOptions {
|
|
50
|
+
intervalMs?: number;
|
|
51
|
+
onError?: InstantGetOnError;
|
|
52
|
+
}
|
|
53
|
+
interface InstantGetSnapshot {
|
|
54
|
+
url: string;
|
|
55
|
+
running: boolean;
|
|
56
|
+
intervalMs: number;
|
|
57
|
+
lastOkAt?: number;
|
|
58
|
+
lastStatus?: number;
|
|
59
|
+
lastErrorName?: string;
|
|
60
|
+
consecutiveErrors: number;
|
|
61
|
+
inFlight: boolean;
|
|
62
|
+
ready: boolean;
|
|
63
|
+
nextDelayMs?: number;
|
|
64
|
+
}
|
|
46
65
|
|
|
47
66
|
declare class ResilientHttpClient extends EventEmitter {
|
|
48
67
|
private readonly opts;
|
|
@@ -57,6 +76,11 @@ declare class ResilientHttpClient extends EventEmitter {
|
|
|
57
76
|
private readonly cleanupEveryNRequests;
|
|
58
77
|
constructor(opts: ResilientHttpClientOptions);
|
|
59
78
|
request(req: ResilientRequest): Promise<ResilientResponse>;
|
|
79
|
+
/**
|
|
80
|
+
* Run a HALF_OPEN probe. Exactly one probe is allowed per HALF_OPEN window.
|
|
81
|
+
* Normal request() calls are rejected during HALF_OPEN.
|
|
82
|
+
*/
|
|
83
|
+
probe(req: ResilientRequest): Promise<ResilientResponse>;
|
|
60
84
|
snapshot(): {
|
|
61
85
|
inFlight: number;
|
|
62
86
|
queueDepth: number;
|
|
@@ -107,4 +131,23 @@ declare class HalfOpenRejectedError extends ResilientHttpError {
|
|
|
107
131
|
constructor(baseUrl: string);
|
|
108
132
|
}
|
|
109
133
|
|
|
110
|
-
|
|
134
|
+
declare class InstantGetStore {
|
|
135
|
+
private state;
|
|
136
|
+
start(url: string, opts?: InstantGetOptions): void;
|
|
137
|
+
stop(): void;
|
|
138
|
+
/** Returns true only after first successful fetch (2xx) for the current run. */
|
|
139
|
+
isReady(): boolean;
|
|
140
|
+
/**
|
|
141
|
+
* Polls `ready` every 1 second.
|
|
142
|
+
* Returns true if ready becomes true within timeoutSec (default 10), else false.
|
|
143
|
+
* Never throws.
|
|
144
|
+
*/
|
|
145
|
+
waitReady(timeoutSec?: number): Promise<boolean>;
|
|
146
|
+
/** Instant read: returns last successful response if <= 60s old, else undefined */
|
|
147
|
+
get(): ResilientResponse | undefined;
|
|
148
|
+
snapshot(): InstantGetSnapshot | undefined;
|
|
149
|
+
private scheduleNext;
|
|
150
|
+
private tick;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export { HalfOpenRejectedError, type HealthOptions, type HttpMethod, type InstantGetOnError, type InstantGetOptions, type InstantGetSnapshot, InstantGetStore, type MicroCacheOptions, type MicroCacheRetryOptions, QueueFullError, QueueTimeoutError, RequestTimeoutError, ResilientHttpClient, type ResilientHttpClientOptions, ResilientHttpError, type ResilientRequest, type ResilientResponse, UpstreamError, UpstreamUnhealthyError };
|
package/dist/index.js
CHANGED
|
@@ -256,6 +256,16 @@ var ResilientHttpClient = class extends EventEmitter {
|
|
|
256
256
|
}
|
|
257
257
|
return this.execute(req, { allowProbe: false });
|
|
258
258
|
}
|
|
259
|
+
/**
|
|
260
|
+
* Run a HALF_OPEN probe. Exactly one probe is allowed per HALF_OPEN window.
|
|
261
|
+
* Normal request() calls are rejected during HALF_OPEN.
|
|
262
|
+
*/
|
|
263
|
+
async probe(req) {
|
|
264
|
+
if (this.microCache?.enabled && req.method === "GET" && req.body == null) {
|
|
265
|
+
return this.execute(req, { allowProbe: true });
|
|
266
|
+
}
|
|
267
|
+
return this.execute(req, { allowProbe: true });
|
|
268
|
+
}
|
|
259
269
|
snapshot() {
|
|
260
270
|
let inFlight = 0;
|
|
261
271
|
let queueDepth = 0;
|
|
@@ -395,8 +405,8 @@ var ResilientHttpClient = class extends EventEmitter {
|
|
|
395
405
|
await limiter.acquireNoQueue();
|
|
396
406
|
} else {
|
|
397
407
|
await limiter.acquire();
|
|
398
|
-
acquired = true;
|
|
399
408
|
}
|
|
409
|
+
acquired = true;
|
|
400
410
|
} catch (err) {
|
|
401
411
|
this.emit("request:rejected", { requestId, request: req, error: err });
|
|
402
412
|
throw err;
|
|
@@ -558,8 +568,194 @@ var ResilientHttpClient = class extends EventEmitter {
|
|
|
558
568
|
}
|
|
559
569
|
}
|
|
560
570
|
};
|
|
571
|
+
|
|
572
|
+
// src/instant_get.ts
|
|
573
|
+
var VALID_MS = 6e4;
|
|
574
|
+
var RETRY_BACKOFF_MS = [1e3, 5e3, 1e4, 3e4, 6e4];
|
|
575
|
+
function cloneResponse(res) {
|
|
576
|
+
return {
|
|
577
|
+
status: res.status,
|
|
578
|
+
headers: { ...res.headers },
|
|
579
|
+
body: new Uint8Array(res.body)
|
|
580
|
+
};
|
|
581
|
+
}
|
|
582
|
+
function headersToRecord(headers) {
|
|
583
|
+
const out = {};
|
|
584
|
+
headers.forEach((v, k) => {
|
|
585
|
+
out[k] = v;
|
|
586
|
+
});
|
|
587
|
+
return out;
|
|
588
|
+
}
|
|
589
|
+
function toRetryCount(onError) {
|
|
590
|
+
if (!onError) return Number.POSITIVE_INFINITY;
|
|
591
|
+
if (onError === "stop") return 0;
|
|
592
|
+
const n = onError.retry;
|
|
593
|
+
if (!Number.isFinite(n)) return Number.POSITIVE_INFINITY;
|
|
594
|
+
return Math.max(0, Math.floor(n));
|
|
595
|
+
}
|
|
596
|
+
function nextBackoffDelayMs(stage) {
|
|
597
|
+
const idx = Math.min(stage, RETRY_BACKOFF_MS.length - 1);
|
|
598
|
+
return RETRY_BACKOFF_MS[idx];
|
|
599
|
+
}
|
|
600
|
+
function sleep2(ms) {
|
|
601
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
602
|
+
}
|
|
603
|
+
async function fetchGet(url) {
|
|
604
|
+
const res = await fetch(url, { method: "GET" });
|
|
605
|
+
const body = new Uint8Array(await res.arrayBuffer());
|
|
606
|
+
return {
|
|
607
|
+
status: res.status,
|
|
608
|
+
headers: headersToRecord(res.headers),
|
|
609
|
+
body
|
|
610
|
+
};
|
|
611
|
+
}
|
|
612
|
+
var InstantGetStore = class {
|
|
613
|
+
state = {
|
|
614
|
+
url: void 0,
|
|
615
|
+
intervalMs: 5e3,
|
|
616
|
+
onError: { retry: Number.POSITIVE_INFINITY },
|
|
617
|
+
timer: void 0,
|
|
618
|
+
inFlight: false,
|
|
619
|
+
last: void 0,
|
|
620
|
+
lastOkAt: void 0,
|
|
621
|
+
lastStatus: void 0,
|
|
622
|
+
lastErrorName: void 0,
|
|
623
|
+
consecutiveErrors: 0,
|
|
624
|
+
retriesRemaining: Number.POSITIVE_INFINITY,
|
|
625
|
+
backoffStage: 0,
|
|
626
|
+
nextDelayMs: void 0,
|
|
627
|
+
ready: false
|
|
628
|
+
};
|
|
629
|
+
start(url, opts) {
|
|
630
|
+
if (this.state.timer) this.stop();
|
|
631
|
+
const intervalMs = opts?.intervalMs ?? 5e3;
|
|
632
|
+
if (!Number.isFinite(intervalMs) || intervalMs <= 0) {
|
|
633
|
+
throw new Error(`intervalMs must be > 0 (got ${intervalMs})`);
|
|
634
|
+
}
|
|
635
|
+
const onError = opts?.onError ?? { retry: Number.POSITIVE_INFINITY };
|
|
636
|
+
this.state.url = url;
|
|
637
|
+
this.state.intervalMs = intervalMs;
|
|
638
|
+
this.state.onError = onError;
|
|
639
|
+
this.state.retriesRemaining = toRetryCount(onError);
|
|
640
|
+
this.state.backoffStage = 0;
|
|
641
|
+
this.state.nextDelayMs = void 0;
|
|
642
|
+
this.state.inFlight = false;
|
|
643
|
+
this.state.ready = false;
|
|
644
|
+
this.scheduleNext(0);
|
|
645
|
+
}
|
|
646
|
+
stop() {
|
|
647
|
+
if (this.state.timer) clearTimeout(this.state.timer);
|
|
648
|
+
this.state.timer = void 0;
|
|
649
|
+
this.state.nextDelayMs = void 0;
|
|
650
|
+
this.state.inFlight = false;
|
|
651
|
+
this.state.ready = false;
|
|
652
|
+
}
|
|
653
|
+
/** Returns true only after first successful fetch (2xx) for the current run. */
|
|
654
|
+
isReady() {
|
|
655
|
+
return this.state.ready;
|
|
656
|
+
}
|
|
657
|
+
/**
|
|
658
|
+
* Polls `ready` every 1 second.
|
|
659
|
+
* Returns true if ready becomes true within timeoutSec (default 10), else false.
|
|
660
|
+
* Never throws.
|
|
661
|
+
*/
|
|
662
|
+
async waitReady(timeoutSec = 10) {
|
|
663
|
+
if (!Number.isFinite(timeoutSec) || timeoutSec < 0) timeoutSec = 10;
|
|
664
|
+
const timeoutMs = Math.floor(timeoutSec * 1e3);
|
|
665
|
+
const start = Date.now();
|
|
666
|
+
while (true) {
|
|
667
|
+
if (this.state.ready) return true;
|
|
668
|
+
if (Date.now() - start >= timeoutMs) return false;
|
|
669
|
+
await sleep2(1e3);
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
/** Instant read: returns last successful response if <= 60s old, else undefined */
|
|
673
|
+
get() {
|
|
674
|
+
if (!this.state.last || !this.state.lastOkAt) return void 0;
|
|
675
|
+
const age = Date.now() - this.state.lastOkAt;
|
|
676
|
+
if (age > VALID_MS) return void 0;
|
|
677
|
+
return cloneResponse(this.state.last);
|
|
678
|
+
}
|
|
679
|
+
snapshot() {
|
|
680
|
+
if (!this.state.url) return void 0;
|
|
681
|
+
return {
|
|
682
|
+
url: this.state.url,
|
|
683
|
+
running: !!this.state.timer,
|
|
684
|
+
intervalMs: this.state.intervalMs,
|
|
685
|
+
lastOkAt: this.state.lastOkAt,
|
|
686
|
+
lastStatus: this.state.lastStatus,
|
|
687
|
+
lastErrorName: this.state.lastErrorName,
|
|
688
|
+
consecutiveErrors: this.state.consecutiveErrors,
|
|
689
|
+
inFlight: this.state.inFlight,
|
|
690
|
+
ready: this.state.ready,
|
|
691
|
+
nextDelayMs: this.state.nextDelayMs
|
|
692
|
+
};
|
|
693
|
+
}
|
|
694
|
+
/* ---------------- internals ---------------- */
|
|
695
|
+
scheduleNext(delayMs) {
|
|
696
|
+
const d = Math.max(0, delayMs);
|
|
697
|
+
this.state.nextDelayMs = d;
|
|
698
|
+
if (this.state.timer) clearTimeout(this.state.timer);
|
|
699
|
+
this.state.timer = setTimeout(() => {
|
|
700
|
+
void this.tick();
|
|
701
|
+
}, d);
|
|
702
|
+
}
|
|
703
|
+
async tick() {
|
|
704
|
+
if (!this.state.url) return;
|
|
705
|
+
if (!this.state.timer) return;
|
|
706
|
+
if (this.state.inFlight) {
|
|
707
|
+
this.scheduleNext(this.state.intervalMs);
|
|
708
|
+
return;
|
|
709
|
+
}
|
|
710
|
+
this.state.inFlight = true;
|
|
711
|
+
try {
|
|
712
|
+
const res = await fetchGet(this.state.url);
|
|
713
|
+
if (res.status < 200 || res.status >= 300) {
|
|
714
|
+
const err = new Error(`Non-2xx status=${res.status}`);
|
|
715
|
+
err.name = "InstantGetNon2xxError";
|
|
716
|
+
throw err;
|
|
717
|
+
}
|
|
718
|
+
this.state.last = cloneResponse(res);
|
|
719
|
+
this.state.lastOkAt = Date.now();
|
|
720
|
+
this.state.lastStatus = res.status;
|
|
721
|
+
this.state.lastErrorName = void 0;
|
|
722
|
+
this.state.consecutiveErrors = 0;
|
|
723
|
+
this.state.backoffStage = 0;
|
|
724
|
+
this.state.retriesRemaining = toRetryCount(this.state.onError);
|
|
725
|
+
this.state.ready = true;
|
|
726
|
+
this.scheduleNext(this.state.intervalMs);
|
|
727
|
+
} catch (err) {
|
|
728
|
+
this.state.lastErrorName = err?.name ?? "Error";
|
|
729
|
+
this.state.consecutiveErrors += 1;
|
|
730
|
+
if (this.state.onError === "stop") {
|
|
731
|
+
this.stop();
|
|
732
|
+
return;
|
|
733
|
+
}
|
|
734
|
+
if (Number.isFinite(this.state.retriesRemaining)) {
|
|
735
|
+
if (this.state.retriesRemaining <= 0) {
|
|
736
|
+
this.stop();
|
|
737
|
+
return;
|
|
738
|
+
}
|
|
739
|
+
this.state.retriesRemaining -= 1;
|
|
740
|
+
if (this.state.retriesRemaining <= 0) {
|
|
741
|
+
this.stop();
|
|
742
|
+
return;
|
|
743
|
+
}
|
|
744
|
+
this.state.backoffStage = 0;
|
|
745
|
+
this.scheduleNext(this.state.intervalMs);
|
|
746
|
+
return;
|
|
747
|
+
}
|
|
748
|
+
const delay = nextBackoffDelayMs(this.state.backoffStage);
|
|
749
|
+
this.state.backoffStage += 1;
|
|
750
|
+
this.scheduleNext(delay);
|
|
751
|
+
} finally {
|
|
752
|
+
this.state.inFlight = false;
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
};
|
|
561
756
|
export {
|
|
562
757
|
HalfOpenRejectedError,
|
|
758
|
+
InstantGetStore,
|
|
563
759
|
QueueFullError,
|
|
564
760
|
QueueTimeoutError,
|
|
565
761
|
RequestTimeoutError,
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/client.ts","../src/errors.ts","../src/limiter.ts","../src/http.ts"],"sourcesContent":["// src/client.ts\r\nimport { EventEmitter } from \"node:events\";\r\nimport { ConcurrencyLimiter } from \"./limiter.js\";\r\nimport { doHttpRequest } from \"./http.js\";\r\nimport {\r\n QueueFullError,\r\n RequestTimeoutError,\r\n ResilientHttpError,\r\n HalfOpenRejectedError,\r\n UpstreamUnhealthyError,\r\n} from \"./errors.js\";\r\nimport type {\r\n MicroCacheOptions,\r\n ResilientHttpClientOptions,\r\n ResilientRequest,\r\n ResilientResponse,\r\n} from \"./types.js\";\r\n\r\n/* ---------------------------- helpers ---------------------------- */\r\n\r\nfunction normalizeUrlForKey(rawUrl: string): string {\r\n const u = new URL(rawUrl);\r\n u.hostname = u.hostname.toLowerCase();\r\n\r\n const isHttpDefault = u.protocol === \"http:\" && u.port === \"80\";\r\n const isHttpsDefault = u.protocol === \"https:\" && u.port === \"443\";\r\n if (isHttpDefault || isHttpsDefault) u.port = \"\";\r\n\r\n return u.toString();\r\n}\r\n\r\nfunction baseUrlKey(rawUrl: string): string {\r\n const u = new URL(rawUrl);\r\n u.hostname = u.hostname.toLowerCase();\r\n\r\n const isHttpDefault = u.protocol === \"http:\" && u.port === \"80\";\r\n const isHttpsDefault = u.protocol === \"https:\" && u.port === \"443\";\r\n if (isHttpDefault || isHttpsDefault) u.port = \"\";\r\n\r\n return `${u.protocol}//${u.host}`;\r\n}\r\n\r\nfunction defaultMicroCacheKeyFn(req: ResilientRequest): string {\r\n return `GET ${normalizeUrlForKey(req.url)}`;\r\n}\r\n\r\nfunction genRequestId(): string {\r\n return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;\r\n}\r\n\r\nfunction sleep(ms: number): Promise<void> {\r\n return new Promise((r) => setTimeout(r, ms));\r\n}\r\n\r\nfunction clamp(n: number, lo: number, hi: number): number {\r\n return Math.max(lo, Math.min(hi, n));\r\n}\r\n\r\nfunction jitterMs(ms: number): number {\r\n const mult = 0.8 + Math.random() * 0.4; // [0.8, 1.2]\r\n return Math.round(ms * mult);\r\n}\r\n\r\n\r\n/* ---------------------------- health ---------------------------- */\r\n\r\ntype HealthState = \"OPEN\" | \"CLOSED\" | \"HALF_OPEN\";\r\ntype Outcome = \"SUCCESS\" | \"HARD_FAIL\" | \"SOFT_FAIL\";\r\n\r\ntype HealthTracker = {\r\n state: HealthState;\r\n\r\n window: Outcome[];\r\n windowSize: number; // 20\r\n minSamples: number; // 10\r\n\r\n consecutiveHardFails: number; // trip at 3\r\n\r\n cooldownBaseMs: number; // 1000\r\n cooldownCapMs: number; // 30000\r\n cooldownMs: number; // current backoff\r\n cooldownUntil: number;\r\n\r\n probeInFlight: boolean; // probeConcurrency=1\r\n probeRemaining: number; // 1 probe per half-open\r\n\r\n stableNonHard: number; // reset backoff after 5 non-hard after open\r\n};\r\n\r\nconst SOFT_FAIL_STATUSES = new Set([429, 502, 503, 504]);\r\n\r\nfunction classifyHttpStatus(status: number): Outcome {\r\n if (status >= 200 && status < 300) return \"SUCCESS\";\r\n if (SOFT_FAIL_STATUSES.has(status)) return \"SOFT_FAIL\";\r\n return \"SUCCESS\"; // do not penalize health for other statuses (incl 4xx except 429)\r\n}\r\n\r\nfunction computeRates(window: Outcome[]) {\r\n const total = window.length;\r\n let hard = 0;\r\n let soft = 0;\r\n for (const o of window) {\r\n if (o === \"HARD_FAIL\") hard += 1;\r\n else if (o === \"SOFT_FAIL\") soft += 1;\r\n }\r\n return {\r\n total,\r\n hard,\r\n soft,\r\n hardFailRate: total === 0 ? 0 : hard / total,\r\n failRate: total === 0 ? 0 : (hard + soft) / total,\r\n };\r\n}\r\n\r\n/**\r\n * Only HTTP-layer failures should count as HARD_FAIL.\r\n * Control-plane rejections must NOT poison health.\r\n */\r\nfunction shouldCountAsHardFail(err: unknown): boolean {\r\n if (err instanceof UpstreamUnhealthyError) return false;\r\n if (err instanceof HalfOpenRejectedError) return false;\r\n if (err instanceof QueueFullError) return false;\r\n\r\n // if your http layer throws this, it's a real hard fail (timeout)\r\n if (err instanceof RequestTimeoutError) return true;\r\n\r\n // If it’s a known library error but not one of the above, be conservative:\r\n // treat it as NOT a health signal unless it’s clearly HTTP-related.\r\n if (err instanceof ResilientHttpError) return false;\r\n\r\n // Unknown thrown error: assume it's an HTTP/network failure.\r\n return true;\r\n}\r\n\r\n/* ---------------------------- microcache types ---------------------------- */\r\n\r\ntype CacheEntry = {\r\n createdAt: number;\r\n expiresAt: number;\r\n value: ResilientResponse;\r\n};\r\n\r\ntype InFlightGroup = {\r\n promise: Promise<ResilientResponse>;\r\n windowStartMs: number;\r\n waiters: number;\r\n};\r\n\r\n/* ---------------------------- client ---------------------------- */\r\n\r\nexport class ResilientHttpClient extends EventEmitter {\r\n private readonly requestTimeoutMs: number;\r\n private readonly healthEnabled: boolean;\r\n\r\n private readonly limiters = new Map<string, ConcurrencyLimiter>();\r\n private readonly health = new Map<string, HealthTracker>();\r\n\r\n private readonly microCache?: Required<\r\n Pick<\r\n MicroCacheOptions,\r\n | \"enabled\"\r\n | \"ttlMs\"\r\n | \"maxStaleMs\"\r\n | \"maxEntries\"\r\n | \"maxWaiters\"\r\n | \"followerTimeoutMs\"\r\n | \"keyFn\"\r\n >\r\n > & {\r\n retry?: {\r\n maxAttempts: number;\r\n baseDelayMs: number;\r\n maxDelayMs: number;\r\n retryOnStatus: number[];\r\n };\r\n };\r\n\r\n private cache?: Map<string, CacheEntry>;\r\n private inFlight?: Map<string, InFlightGroup>;\r\n\r\n private microCacheReqCount = 0;\r\n private readonly cleanupEveryNRequests = 100;\r\n\r\n constructor(private readonly opts: ResilientHttpClientOptions) {\r\n super();\r\n this.requestTimeoutMs = opts.requestTimeoutMs;\r\n this.healthEnabled = opts.health?.enabled ?? true;\r\n\r\n const mc = opts.microCache;\r\n if (mc?.enabled) {\r\n const retry = mc.retry\r\n ? {\r\n maxAttempts: mc.retry.maxAttempts ?? 3,\r\n baseDelayMs: mc.retry.baseDelayMs ?? 50,\r\n maxDelayMs: mc.retry.maxDelayMs ?? 200,\r\n retryOnStatus: mc.retry.retryOnStatus ?? [429, 502, 503, 504],\r\n }\r\n : undefined;\r\n\r\n this.microCache = {\r\n enabled: true,\r\n ttlMs: mc.ttlMs ?? 1000,\r\n maxStaleMs: mc.maxStaleMs ?? 10_000,\r\n maxEntries: mc.maxEntries ?? 500,\r\n maxWaiters: mc.maxWaiters ?? 1000,\r\n followerTimeoutMs: mc.followerTimeoutMs ?? 5000,\r\n keyFn: mc.keyFn ?? defaultMicroCacheKeyFn,\r\n retry,\r\n };\r\n\r\n this.cache = new Map();\r\n this.inFlight = new Map();\r\n }\r\n }\r\n\r\n async request(req: ResilientRequest): Promise<ResilientResponse> {\r\n if (this.microCache?.enabled && req.method === \"GET\" && req.body == null) {\r\n return this.requestWithMicroCache(req);\r\n }\r\n return this.execute(req, { allowProbe: false });\r\n }\r\n\r\n snapshot(): { inFlight: number; queueDepth: number } {\r\n let inFlight = 0;\r\n let queueDepth = 0;\r\n for (const l of this.limiters.values()) {\r\n const s = l.snapshot();\r\n inFlight += s.inFlight;\r\n queueDepth += s.queueDepth;\r\n }\r\n return { inFlight, queueDepth };\r\n }\r\n\r\n /* ---------------- internals ---------------- */\r\n\r\n private getLimiter(baseKey: string): ConcurrencyLimiter {\r\n let l = this.limiters.get(baseKey);\r\n if (!l) {\r\n l = new ConcurrencyLimiter({\r\n maxInFlight: this.opts.maxInFlight,\r\n maxQueue: this.opts.maxInFlight * 10, // hidden factor\r\n });\r\n this.limiters.set(baseKey, l);\r\n }\r\n return l;\r\n }\r\n\r\n private getHealth(baseKey: string): HealthTracker {\r\n let h = this.health.get(baseKey);\r\n if (!h) {\r\n h = {\r\n state: \"OPEN\",\r\n window: [],\r\n windowSize: 20,\r\n minSamples: 10,\r\n consecutiveHardFails: 0,\r\n cooldownBaseMs: 1000,\r\n cooldownCapMs: 30_000,\r\n cooldownMs: 1000,\r\n cooldownUntil: 0,\r\n probeInFlight: false,\r\n probeRemaining: 0,\r\n stableNonHard: 0,\r\n };\r\n this.health.set(baseKey, h);\r\n }\r\n return h;\r\n }\r\n\r\n private closeHealth(baseKey: string, reason: string): void {\r\n const h = this.getHealth(baseKey);\r\n if (h.state === \"CLOSED\") return;\r\n\r\n h.state = \"CLOSED\";\r\n h.cooldownUntil = Date.now() + jitterMs(h.cooldownMs);\r\n h.cooldownMs = Math.min(h.cooldownMs * 2, h.cooldownCapMs);\r\n\r\n // reject queued immediately\r\n this.getLimiter(baseKey).flush(new UpstreamUnhealthyError(baseKey, \"CLOSED\"));\r\n\r\n const rates = computeRates(h.window);\r\n this.emit(\"health:closed\", {\r\n baseUrl: baseKey,\r\n reason,\r\n cooldownMs: h.cooldownUntil - Date.now(),\r\n hardFailRate: rates.hardFailRate,\r\n failRate: rates.failRate,\r\n samples: rates.total,\r\n });\r\n }\r\n\r\n private halfOpenHealth(baseKey: string): void {\r\n const h = this.getHealth(baseKey);\r\n if (h.state !== \"CLOSED\") return;\r\n\r\n h.state = \"HALF_OPEN\";\r\n h.probeInFlight = false;\r\n h.probeRemaining = 1;\r\n this.emit(\"health:half_open\", { baseUrl: baseKey });\r\n }\r\n\r\n private openHealth(baseKey: string): void {\r\n const h = this.getHealth(baseKey);\r\n h.state = \"OPEN\";\r\n h.window = [];\r\n h.consecutiveHardFails = 0;\r\n h.probeInFlight = false;\r\n h.probeRemaining = 0;\r\n h.stableNonHard = 0;\r\n this.emit(\"health:open\", { baseUrl: baseKey });\r\n }\r\n\r\n private recordOutcome(baseKey: string, outcome: Outcome): void {\r\n const h = this.getHealth(baseKey);\r\n\r\n h.window.push(outcome);\r\n while (h.window.length > h.windowSize) h.window.shift();\r\n\r\n if (outcome === \"HARD_FAIL\") h.consecutiveHardFails += 1;\r\n else h.consecutiveHardFails = 0;\r\n\r\n // stabilization (reset backoff after 5 non-hard in OPEN)\r\n if (h.state === \"OPEN\") {\r\n if (outcome !== \"HARD_FAIL\") {\r\n h.stableNonHard += 1;\r\n if (h.stableNonHard >= 5) {\r\n h.cooldownMs = h.cooldownBaseMs;\r\n }\r\n } else {\r\n h.stableNonHard = 0;\r\n }\r\n }\r\n\r\n if (!this.healthEnabled) return;\r\n\r\n if (h.consecutiveHardFails >= 3) {\r\n this.closeHealth(baseKey, \"3 consecutive hard failures\");\r\n return;\r\n }\r\n\r\n const rates = computeRates(h.window);\r\n if (rates.total >= h.minSamples) {\r\n if (rates.hardFailRate >= 0.3) {\r\n this.closeHealth(baseKey, \"hardFailRate >= 30%\");\r\n return;\r\n }\r\n if (rates.failRate >= 0.5) {\r\n this.closeHealth(baseKey, \"failRate >= 50%\");\r\n return;\r\n }\r\n }\r\n }\r\n\r\n private async execute(req: ResilientRequest, opts: { allowProbe: boolean }): Promise<ResilientResponse> {\r\n const baseKey = baseUrlKey(req.url);\r\n const h = this.getHealth(baseKey);\r\n const limiter = this.getLimiter(baseKey);\r\n\r\n if (this.healthEnabled) {\r\n if (h.state === \"CLOSED\") {\r\n if (Date.now() >= h.cooldownUntil) {\r\n this.halfOpenHealth(baseKey);\r\n } else {\r\n throw new UpstreamUnhealthyError(baseKey, \"CLOSED\");\r\n }\r\n }\r\n\r\n if (h.state === \"HALF_OPEN\") {\r\n if (!opts.allowProbe) throw new HalfOpenRejectedError(baseKey);\r\n if (h.probeRemaining <= 0 || h.probeInFlight) throw new HalfOpenRejectedError(baseKey);\r\n h.probeInFlight = true;\r\n h.probeRemaining -= 1;\r\n }\r\n }\r\n\r\n const requestId = genRequestId();\r\n const start = Date.now();\r\n let acquired = false;\r\n try {\r\n // Probes should not wait in queue.\r\n if (this.healthEnabled && h.state === \"HALF_OPEN\") {\r\n await limiter.acquireNoQueue();\r\n } else {\r\n await limiter.acquire();\r\n acquired = true;\r\n }\r\n } catch (err) {\r\n this.emit(\"request:rejected\", { requestId, request: req, error: err });\r\n throw err;\r\n }\r\n\r\n this.emit(\"request:start\", { requestId, request: req });\r\n\r\n try {\r\n const res = await doHttpRequest(req, this.requestTimeoutMs);\r\n const durationMs = Date.now() - start;\r\n\r\n const outcome = classifyHttpStatus(res.status);\r\n this.recordOutcome(baseKey, outcome);\r\n\r\n // Probe decision\r\n if (this.healthEnabled && h.state === \"HALF_OPEN\") {\r\n this.emit(\"health:probe\", { baseUrl: baseKey, outcome, status: res.status });\r\n if (res.status >= 200 && res.status < 300) {\r\n this.openHealth(baseKey);\r\n } else {\r\n this.closeHealth(baseKey, `probe failed status=${res.status}`);\r\n }\r\n }\r\n\r\n this.emit(\"request:success\", { requestId, request: req, status: res.status, durationMs });\r\n return res;\r\n } catch (err) {\r\n const durationMs = Date.now() - start;\r\n\r\n if (shouldCountAsHardFail(err)) {\r\n this.recordOutcome(baseKey, \"HARD_FAIL\");\r\n }\r\n\r\n if (this.healthEnabled && h.state === \"HALF_OPEN\") {\r\n this.emit(\"health:probe\", { baseUrl: baseKey, outcome: \"HARD_FAIL\", error: err });\r\n this.closeHealth(baseKey, \"probe hard failure\");\r\n }\r\n\r\n this.emit(\"request:failure\", { requestId, request: req, error: err, durationMs });\r\n throw err;\r\n } finally {\r\n // Clear probe flag (if any)\r\n if (this.healthEnabled && h.state === \"HALF_OPEN\") {\r\n h.probeInFlight = false;\r\n }\r\n if (acquired) limiter.release();\r\n }\r\n }\r\n\r\n /* ---------------- microcache ---------------- */\r\n\r\n private cloneResponse(res: ResilientResponse): ResilientResponse {\r\n return { status: res.status, headers: { ...res.headers }, body: new Uint8Array(res.body) };\r\n }\r\n\r\n private maybeCleanupExpired(cache: Map<string, CacheEntry>, maxStaleMs: number): void {\r\n this.microCacheReqCount++;\r\n if (this.microCacheReqCount % this.cleanupEveryNRequests !== 0) return;\r\n\r\n const now = Date.now();\r\n for (const [k, v] of cache.entries()) {\r\n if (now - v.createdAt > maxStaleMs) cache.delete(k);\r\n }\r\n }\r\n\r\n private evictIfNeeded(cache: Map<string, CacheEntry>, maxEntries: number): void {\r\n while (cache.size >= maxEntries) {\r\n const oldestKey = cache.keys().next().value as string | undefined;\r\n if (!oldestKey) break;\r\n cache.delete(oldestKey);\r\n }\r\n }\r\n\r\n private isRetryableStatus(status: number, retryOnStatus: number[]): boolean {\r\n return retryOnStatus.includes(status);\r\n }\r\n\r\n private computeBackoffMs(attemptIndex: number, baseDelayMs: number, maxDelayMs: number): number {\r\n const exp = baseDelayMs * Math.pow(2, attemptIndex - 1);\r\n const capped = clamp(exp, baseDelayMs, maxDelayMs);\r\n const jitter = 0.5 + Math.random();\r\n return Math.round(capped * jitter);\r\n }\r\n\r\n private async fetchWithLeaderRetry(req: ResilientRequest): Promise<ResilientResponse> {\r\n const mc = this.microCache!;\r\n const retry = mc.retry;\r\n if (!retry) return this.execute(req, { allowProbe: false });\r\n\r\n const { maxAttempts, baseDelayMs, maxDelayMs, retryOnStatus } = retry;\r\n\r\n let last: ResilientResponse | undefined;\r\n for (let attempt = 1; attempt <= maxAttempts; attempt++) {\r\n const res = await this.execute(req, { allowProbe: false });\r\n last = res;\r\n\r\n if (this.isRetryableStatus(res.status, retryOnStatus) && attempt < maxAttempts) {\r\n const delay = this.computeBackoffMs(attempt, baseDelayMs, maxDelayMs);\r\n this.emit(\"microcache:retry\", { url: req.url, attempt, maxAttempts, reason: `status ${res.status}`, delayMs: delay });\r\n await sleep(delay);\r\n continue;\r\n }\r\n return res;\r\n }\r\n return last!;\r\n }\r\n\r\n private async requestWithMicroCache(req: ResilientRequest): Promise<ResilientResponse> {\r\n const mc = this.microCache!;\r\n const cache = this.cache!;\r\n const inFlight = this.inFlight!;\r\n\r\n this.maybeCleanupExpired(cache, mc.maxStaleMs);\r\n\r\n const key = mc.keyFn(req);\r\n const now = Date.now();\r\n\r\n const hit0 = cache.get(key);\r\n if (hit0 && now - hit0.createdAt > mc.maxStaleMs) cache.delete(key);\r\n\r\n const hit = cache.get(key);\r\n if (hit && now < hit.expiresAt) return this.cloneResponse(hit.value);\r\n\r\n // If CLOSED: serve stale if allowed else fail fast\r\n if (this.healthEnabled) {\r\n const baseKey = baseUrlKey(req.url);\r\n const h = this.getHealth(baseKey);\r\n if (h.state === \"CLOSED\") {\r\n const staleAllowed = !!hit && now - hit.createdAt <= mc.maxStaleMs;\r\n if (staleAllowed) return this.cloneResponse(hit!.value);\r\n throw new UpstreamUnhealthyError(baseKey, \"CLOSED\");\r\n }\r\n }\r\n\r\n const group = inFlight.get(key);\r\n if (group) {\r\n const h = cache.get(key);\r\n const staleAllowed = !!h && now - h.createdAt <= mc.maxStaleMs;\r\n\r\n if (h && staleAllowed) return this.cloneResponse(h.value);\r\n\r\n const age = now - group.windowStartMs;\r\n if (age > mc.followerTimeoutMs) {\r\n const err = new Error(`Follower window closed for key=${key}`);\r\n (err as any).name = \"FollowerWindowClosedError\";\r\n throw err;\r\n }\r\n\r\n if (group.waiters >= mc.maxWaiters) {\r\n const err = new Error(`Too many followers for key=${key}`);\r\n (err as any).name = \"TooManyWaitersError\";\r\n throw err;\r\n }\r\n\r\n group.waiters += 1;\r\n try {\r\n const res = await group.promise;\r\n return this.cloneResponse(res);\r\n } finally {\r\n group.waiters -= 1;\r\n }\r\n }\r\n\r\n const prev = cache.get(key);\r\n const prevStaleAllowed = !!prev && now - prev.createdAt <= mc.maxStaleMs;\r\n\r\n const promise = (async () => {\r\n const baseKey = baseUrlKey(req.url);\r\n const h = this.getHealth(baseKey);\r\n const allowProbe = this.healthEnabled && h.state === \"HALF_OPEN\";\r\n\r\n const res = allowProbe\r\n ? await this.execute(req, { allowProbe: true })\r\n : await this.fetchWithLeaderRetry(req);\r\n\r\n if (res.status >= 200 && res.status < 300) {\r\n this.evictIfNeeded(cache, mc.maxEntries);\r\n const t = Date.now();\r\n cache.set(key, { value: this.cloneResponse(res), createdAt: t, expiresAt: t + mc.ttlMs });\r\n }\r\n return res;\r\n })();\r\n\r\n inFlight.set(key, { promise, windowStartMs: Date.now(), waiters: 0 });\r\n\r\n try {\r\n const res = await promise;\r\n\r\n if (!(res.status >= 200 && res.status < 300) && prev && prevStaleAllowed) {\r\n return this.cloneResponse(prev.value);\r\n }\r\n\r\n return this.cloneResponse(res);\r\n } catch (err) {\r\n if (prev && prevStaleAllowed) {\r\n this.emit(\"microcache:refresh_failed\", { key, url: req.url, error: err });\r\n return this.cloneResponse(prev.value);\r\n }\r\n throw err;\r\n } finally {\r\n inFlight.delete(key);\r\n }\r\n }\r\n}\r\n","export abstract class ResilientHttpError extends Error {\r\n public readonly name: string;\r\n constructor(message: string) {\r\n super(message);\r\n this.name = this.constructor.name;\r\n }\r\n}\r\n\r\nexport class QueueFullError extends ResilientHttpError {\r\n constructor(public readonly maxQueue: number) {\r\n super(`Queue is full (maxQueue=${maxQueue}).`);\r\n }\r\n}\r\n\r\nexport class QueueTimeoutError extends ResilientHttpError {\r\n constructor(public readonly enqueueTimeoutMs: number) {\r\n super(`Queue wait exceeded (enqueueTimeoutMs=${enqueueTimeoutMs}).`);\r\n }\r\n}\r\n\r\nexport class RequestTimeoutError extends ResilientHttpError {\r\n constructor(public readonly requestTimeoutMs: number) {\r\n super(`Request timed out (requestTimeoutMs=${requestTimeoutMs}).`);\r\n }\r\n}\r\n\r\n\r\n\r\nexport class UpstreamError extends ResilientHttpError {\r\n constructor(public readonly status: number) {\r\n super(`Upstream returned error status=${status}.`);\r\n }\r\n}\r\n\r\n\r\n\r\n\r\nexport class UpstreamUnhealthyError extends ResilientHttpError {\r\n constructor(public readonly baseUrl: string, public readonly state: string) {\r\n super(`Upstream is unhealthy (state=${state}, baseUrl=${baseUrl}).`);\r\n }\r\n}\r\n\r\nexport class HalfOpenRejectedError extends ResilientHttpError {\r\n constructor(public readonly baseUrl: string) {\r\n super(`Upstream is HALF_OPEN (probe only) for baseUrl=${baseUrl}.`);\r\n }\r\n}\r\n","// src/limiter.ts\r\nimport { QueueFullError } from \"./errors.js\";\r\n\r\ntype ResolveFn = () => void;\r\ntype RejectFn = (err: unknown) => void;\r\n\r\ninterface Waiter {\r\n resolve: ResolveFn;\r\n reject: RejectFn;\r\n}\r\n\r\n/**\r\n * Process-local concurrency limiter with bounded FIFO queue.\r\n *\r\n * - maxInFlight: concurrent permits\r\n * - maxQueue: bounded burst buffer\r\n * - No enqueue-timeout by design.\r\n *\r\n * Also supports:\r\n * - flush(err): reject all queued waiters immediately\r\n * - acquireNoQueue(): for probes (must start now or fail)\r\n */\r\nexport class ConcurrencyLimiter {\r\n private readonly maxInFlight: number;\r\n private readonly maxQueue: number;\r\n\r\n private inFlight = 0;\r\n private queue: Waiter[] = [];\r\n\r\n constructor(opts: { maxInFlight: number; maxQueue: number }) {\r\n if (!Number.isFinite(opts.maxInFlight) || opts.maxInFlight <= 0) {\r\n throw new Error(`maxInFlight must be > 0 (got ${opts.maxInFlight})`);\r\n }\r\n if (!Number.isFinite(opts.maxQueue) || opts.maxQueue < 0) {\r\n throw new Error(`maxQueue must be >= 0 (got ${opts.maxQueue})`);\r\n }\r\n\r\n this.maxInFlight = opts.maxInFlight;\r\n this.maxQueue = opts.maxQueue;\r\n }\r\n\r\n acquire(): Promise<void> {\r\n if (this.inFlight < this.maxInFlight) {\r\n this.inFlight += 1;\r\n return Promise.resolve();\r\n }\r\n\r\n if (this.maxQueue === 0 || this.queue.length >= this.maxQueue) {\r\n return Promise.reject(new QueueFullError(this.maxQueue));\r\n }\r\n\r\n return new Promise<void>((resolve, reject) => {\r\n this.queue.push({ resolve, reject });\r\n });\r\n }\r\n\r\n /**\r\n * Acquire without queueing: either start now or fail.\r\n * Used for HALF_OPEN probes so recovery never waits behind backlog.\r\n */\r\n acquireNoQueue(): Promise<void> {\r\n if (this.inFlight < this.maxInFlight) {\r\n this.inFlight += 1;\r\n return Promise.resolve();\r\n }\r\n // treat as queue full (we don't want a new error type)\r\n return Promise.reject(new QueueFullError(0));\r\n }\r\n\r\n release(): void {\r\n if (this.inFlight <= 0) {\r\n throw new Error(\"release() called when inFlight is already 0\");\r\n }\r\n\r\n const next = this.queue.shift();\r\n if (next) {\r\n next.resolve(); // transfer permit\r\n return;\r\n }\r\n\r\n this.inFlight -= 1;\r\n }\r\n\r\n flush(err: unknown): void {\r\n const q = this.queue;\r\n this.queue = [];\r\n for (const w of q) w.reject(err);\r\n }\r\n\r\n snapshot(): { inFlight: number; queueDepth: number; maxInFlight: number; maxQueue: number } {\r\n return {\r\n inFlight: this.inFlight,\r\n queueDepth: this.queue.length,\r\n maxInFlight: this.maxInFlight,\r\n maxQueue: this.maxQueue,\r\n };\r\n }\r\n}\r\n","// src/http.ts\r\nimport { request as undiciRequest } from \"undici\";\r\nimport { RequestTimeoutError } from \"./errors.js\";\r\nimport type { ResilientRequest, ResilientResponse } from \"./types.js\";\r\n\r\nfunction normalizeHeaders(headers: any): Record<string, string> {\r\n const out: Record<string, string> = {};\r\n if (!headers) return out;\r\n\r\n // undici headers are an object-like structure (headers: Record<string, string | string[]>)\r\n for (const [k, v] of Object.entries(headers)) {\r\n if (Array.isArray(v)) out[k.toLowerCase()] = v.join(\", \");\r\n else if (typeof v === \"string\") out[k.toLowerCase()] = v;\r\n else out[k.toLowerCase()] = String(v);\r\n }\r\n return out;\r\n}\r\n\r\n/**\r\n * Execute a single HTTP request with a hard timeout using AbortController.\r\n * No retries. No breaker. Just raw outbound I/O with a timeout.\r\n */\r\nexport async function doHttpRequest(\r\n req: ResilientRequest,\r\n requestTimeoutMs: number\r\n): Promise<ResilientResponse> {\r\n const ac = new AbortController();\r\n const timer = setTimeout(() => ac.abort(), requestTimeoutMs);\r\n\r\n try {\r\n const res = await undiciRequest(req.url, {\r\n method: req.method,\r\n headers: req.headers,\r\n body: req.body as any,\r\n signal: ac.signal,\r\n });\r\n\r\n const body = await res.body.arrayBuffer();\r\n return {\r\n status: res.statusCode,\r\n headers: normalizeHeaders(res.headers),\r\n body: new Uint8Array(body),\r\n };\r\n } catch (err: any) {\r\n // undici throws AbortError on abort\r\n if (err?.name === \"AbortError\") {\r\n throw new RequestTimeoutError(requestTimeoutMs);\r\n }\r\n throw err;\r\n } finally {\r\n clearTimeout(timer);\r\n }\r\n}\r\n"],"mappings":";AACA,SAAS,oBAAoB;;;ACDtB,IAAe,qBAAf,cAA0C,MAAM;AAAA,EACrC;AAAA,EAChB,YAAY,SAAiB;AAC3B,UAAM,OAAO;AACb,SAAK,OAAO,KAAK,YAAY;AAAA,EAC/B;AACF;AAEO,IAAM,iBAAN,cAA6B,mBAAmB;AAAA,EACrD,YAA4B,UAAkB;AAC5C,UAAM,2BAA2B,QAAQ,IAAI;AADnB;AAAA,EAE5B;AACF;AAEO,IAAM,oBAAN,cAAgC,mBAAmB;AAAA,EACxD,YAA4B,kBAA0B;AACpD,UAAM,yCAAyC,gBAAgB,IAAI;AADzC;AAAA,EAE5B;AACF;AAEO,IAAM,sBAAN,cAAkC,mBAAmB;AAAA,EAC1D,YAA4B,kBAA0B;AACpD,UAAM,uCAAuC,gBAAgB,IAAI;AADvC;AAAA,EAE5B;AACF;AAIO,IAAM,gBAAN,cAA4B,mBAAmB;AAAA,EACpD,YAA4B,QAAgB;AAC1C,UAAM,kCAAkC,MAAM,GAAG;AADvB;AAAA,EAE5B;AACF;AAKO,IAAM,yBAAN,cAAqC,mBAAmB;AAAA,EAC7D,YAA4B,SAAiC,OAAe;AAC1E,UAAM,gCAAgC,KAAK,aAAa,OAAO,IAAI;AADzC;AAAiC;AAAA,EAE7D;AACF;AAEO,IAAM,wBAAN,cAAoC,mBAAmB;AAAA,EAC5D,YAA4B,SAAiB;AAC3C,UAAM,kDAAkD,OAAO,GAAG;AADxC;AAAA,EAE5B;AACF;;;ACzBO,IAAM,qBAAN,MAAyB;AAAA,EACb;AAAA,EACA;AAAA,EAET,WAAW;AAAA,EACX,QAAkB,CAAC;AAAA,EAE3B,YAAY,MAAiD;AAC3D,QAAI,CAAC,OAAO,SAAS,KAAK,WAAW,KAAK,KAAK,eAAe,GAAG;AAC/D,YAAM,IAAI,MAAM,gCAAgC,KAAK,WAAW,GAAG;AAAA,IACrE;AACA,QAAI,CAAC,OAAO,SAAS,KAAK,QAAQ,KAAK,KAAK,WAAW,GAAG;AACxD,YAAM,IAAI,MAAM,8BAA8B,KAAK,QAAQ,GAAG;AAAA,IAChE;AAEA,SAAK,cAAc,KAAK;AACxB,SAAK,WAAW,KAAK;AAAA,EACvB;AAAA,EAEA,UAAyB;AACvB,QAAI,KAAK,WAAW,KAAK,aAAa;AACpC,WAAK,YAAY;AACjB,aAAO,QAAQ,QAAQ;AAAA,IACzB;AAEA,QAAI,KAAK,aAAa,KAAK,KAAK,MAAM,UAAU,KAAK,UAAU;AAC7D,aAAO,QAAQ,OAAO,IAAI,eAAe,KAAK,QAAQ,CAAC;AAAA,IACzD;AAEA,WAAO,IAAI,QAAc,CAAC,SAAS,WAAW;AAC5C,WAAK,MAAM,KAAK,EAAE,SAAS,OAAO,CAAC;AAAA,IACrC,CAAC;AAAA,EACH;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,iBAAgC;AAC9B,QAAI,KAAK,WAAW,KAAK,aAAa;AACpC,WAAK,YAAY;AACjB,aAAO,QAAQ,QAAQ;AAAA,IACzB;AAEA,WAAO,QAAQ,OAAO,IAAI,eAAe,CAAC,CAAC;AAAA,EAC7C;AAAA,EAEA,UAAgB;AACd,QAAI,KAAK,YAAY,GAAG;AACtB,YAAM,IAAI,MAAM,6CAA6C;AAAA,IAC/D;AAEA,UAAM,OAAO,KAAK,MAAM,MAAM;AAC9B,QAAI,MAAM;AACR,WAAK,QAAQ;AACb;AAAA,IACF;AAEA,SAAK,YAAY;AAAA,EACnB;AAAA,EAEA,MAAM,KAAoB;AACxB,UAAM,IAAI,KAAK;AACf,SAAK,QAAQ,CAAC;AACd,eAAW,KAAK,EAAG,GAAE,OAAO,GAAG;AAAA,EACjC;AAAA,EAEA,WAA4F;AAC1F,WAAO;AAAA,MACL,UAAU,KAAK;AAAA,MACf,YAAY,KAAK,MAAM;AAAA,MACvB,aAAa,KAAK;AAAA,MAClB,UAAU,KAAK;AAAA,IACjB;AAAA,EACF;AACF;;;AChGA,SAAS,WAAW,qBAAqB;AAIzC,SAAS,iBAAiB,SAAsC;AAC9D,QAAM,MAA8B,CAAC;AACrC,MAAI,CAAC,QAAS,QAAO;AAGrB,aAAW,CAAC,GAAG,CAAC,KAAK,OAAO,QAAQ,OAAO,GAAG;AAC5C,QAAI,MAAM,QAAQ,CAAC,EAAG,KAAI,EAAE,YAAY,CAAC,IAAI,EAAE,KAAK,IAAI;AAAA,aAC/C,OAAO,MAAM,SAAU,KAAI,EAAE,YAAY,CAAC,IAAI;AAAA,QAClD,KAAI,EAAE,YAAY,CAAC,IAAI,OAAO,CAAC;AAAA,EACtC;AACA,SAAO;AACT;AAMA,eAAsB,cACpB,KACA,kBAC4B;AAC5B,QAAM,KAAK,IAAI,gBAAgB;AAC/B,QAAM,QAAQ,WAAW,MAAM,GAAG,MAAM,GAAG,gBAAgB;AAE3D,MAAI;AACF,UAAM,MAAM,MAAM,cAAc,IAAI,KAAK;AAAA,MACvC,QAAQ,IAAI;AAAA,MACZ,SAAS,IAAI;AAAA,MACb,MAAM,IAAI;AAAA,MACV,QAAQ,GAAG;AAAA,IACb,CAAC;AAED,UAAM,OAAO,MAAM,IAAI,KAAK,YAAY;AACxC,WAAO;AAAA,MACL,QAAQ,IAAI;AAAA,MACZ,SAAS,iBAAiB,IAAI,OAAO;AAAA,MACrC,MAAM,IAAI,WAAW,IAAI;AAAA,IAC3B;AAAA,EACF,SAAS,KAAU;AAEjB,QAAI,KAAK,SAAS,cAAc;AAC9B,YAAM,IAAI,oBAAoB,gBAAgB;AAAA,IAChD;AACA,UAAM;AAAA,EACR,UAAE;AACA,iBAAa,KAAK;AAAA,EACpB;AACF;;;AHhCA,SAAS,mBAAmB,QAAwB;AAClD,QAAM,IAAI,IAAI,IAAI,MAAM;AACxB,IAAE,WAAW,EAAE,SAAS,YAAY;AAEpC,QAAM,gBAAgB,EAAE,aAAa,WAAW,EAAE,SAAS;AAC3D,QAAM,iBAAiB,EAAE,aAAa,YAAY,EAAE,SAAS;AAC7D,MAAI,iBAAiB,eAAgB,GAAE,OAAO;AAE9C,SAAO,EAAE,SAAS;AACpB;AAEA,SAAS,WAAW,QAAwB;AAC1C,QAAM,IAAI,IAAI,IAAI,MAAM;AACxB,IAAE,WAAW,EAAE,SAAS,YAAY;AAEpC,QAAM,gBAAgB,EAAE,aAAa,WAAW,EAAE,SAAS;AAC3D,QAAM,iBAAiB,EAAE,aAAa,YAAY,EAAE,SAAS;AAC7D,MAAI,iBAAiB,eAAgB,GAAE,OAAO;AAE9C,SAAO,GAAG,EAAE,QAAQ,KAAK,EAAE,IAAI;AACjC;AAEA,SAAS,uBAAuB,KAA+B;AAC7D,SAAO,OAAO,mBAAmB,IAAI,GAAG,CAAC;AAC3C;AAEA,SAAS,eAAuB;AAC9B,SAAO,GAAG,KAAK,IAAI,EAAE,SAAS,EAAE,CAAC,IAAI,KAAK,OAAO,EAAE,SAAS,EAAE,EAAE,MAAM,GAAG,EAAE,CAAC;AAC9E;AAEA,SAAS,MAAM,IAA2B;AACxC,SAAO,IAAI,QAAQ,CAAC,MAAM,WAAW,GAAG,EAAE,CAAC;AAC7C;AAEA,SAAS,MAAM,GAAW,IAAY,IAAoB;AACxD,SAAO,KAAK,IAAI,IAAI,KAAK,IAAI,IAAI,CAAC,CAAC;AACrC;AAEA,SAAS,SAAS,IAAoB;AACpC,QAAM,OAAO,MAAM,KAAK,OAAO,IAAI;AACnC,SAAO,KAAK,MAAM,KAAK,IAAI;AAC7B;AA4BA,IAAM,qBAAqB,oBAAI,IAAI,CAAC,KAAK,KAAK,KAAK,GAAG,CAAC;AAEvD,SAAS,mBAAmB,QAAyB;AACnD,MAAI,UAAU,OAAO,SAAS,IAAK,QAAO;AAC1C,MAAI,mBAAmB,IAAI,MAAM,EAAG,QAAO;AAC3C,SAAO;AACT;AAEA,SAAS,aAAa,QAAmB;AACvC,QAAM,QAAQ,OAAO;AACrB,MAAI,OAAO;AACX,MAAI,OAAO;AACX,aAAW,KAAK,QAAQ;AACtB,QAAI,MAAM,YAAa,SAAQ;AAAA,aACtB,MAAM,YAAa,SAAQ;AAAA,EACtC;AACA,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,IACA,cAAc,UAAU,IAAI,IAAI,OAAO;AAAA,IACvC,UAAU,UAAU,IAAI,KAAK,OAAO,QAAQ;AAAA,EAC9C;AACF;AAMA,SAAS,sBAAsB,KAAuB;AACpD,MAAI,eAAe,uBAAwB,QAAO;AAClD,MAAI,eAAe,sBAAuB,QAAO;AACjD,MAAI,eAAe,eAAgB,QAAO;AAG1C,MAAI,eAAe,oBAAqB,QAAO;AAI/C,MAAI,eAAe,mBAAoB,QAAO;AAG9C,SAAO;AACT;AAkBO,IAAM,sBAAN,cAAkC,aAAa;AAAA,EAiCpD,YAA6B,MAAkC;AAC7D,UAAM;AADqB;AAE3B,SAAK,mBAAmB,KAAK;AAC7B,SAAK,gBAAgB,KAAK,QAAQ,WAAW;AAE7C,UAAM,KAAK,KAAK;AAChB,QAAI,IAAI,SAAS;AACf,YAAM,QAAQ,GAAG,QACb;AAAA,QACE,aAAa,GAAG,MAAM,eAAe;AAAA,QACrC,aAAa,GAAG,MAAM,eAAe;AAAA,QACrC,YAAY,GAAG,MAAM,cAAc;AAAA,QACnC,eAAe,GAAG,MAAM,iBAAiB,CAAC,KAAK,KAAK,KAAK,GAAG;AAAA,MAC9D,IACA;AAEJ,WAAK,aAAa;AAAA,QAChB,SAAS;AAAA,QACT,OAAO,GAAG,SAAS;AAAA,QACnB,YAAY,GAAG,cAAc;AAAA,QAC7B,YAAY,GAAG,cAAc;AAAA,QAC7B,YAAY,GAAG,cAAc;AAAA,QAC7B,mBAAmB,GAAG,qBAAqB;AAAA,QAC3C,OAAO,GAAG,SAAS;AAAA,QACnB;AAAA,MACF;AAEA,WAAK,QAAQ,oBAAI,IAAI;AACrB,WAAK,WAAW,oBAAI,IAAI;AAAA,IAC1B;AAAA,EACF;AAAA,EA9DiB;AAAA,EACA;AAAA,EAEA,WAAW,oBAAI,IAAgC;AAAA,EAC/C,SAAS,oBAAI,IAA2B;AAAA,EAExC;AAAA,EAoBT;AAAA,EACA;AAAA,EAEA,qBAAqB;AAAA,EACZ,wBAAwB;AAAA,EAkCzC,MAAM,QAAQ,KAAmD;AAC/D,QAAI,KAAK,YAAY,WAAW,IAAI,WAAW,SAAS,IAAI,QAAQ,MAAM;AACxE,aAAO,KAAK,sBAAsB,GAAG;AAAA,IACvC;AACA,WAAO,KAAK,QAAQ,KAAK,EAAE,YAAY,MAAM,CAAC;AAAA,EAChD;AAAA,EAEA,WAAqD;AACnD,QAAI,WAAW;AACf,QAAI,aAAa;AACjB,eAAW,KAAK,KAAK,SAAS,OAAO,GAAG;AACtC,YAAM,IAAI,EAAE,SAAS;AACrB,kBAAY,EAAE;AACd,oBAAc,EAAE;AAAA,IAClB;AACA,WAAO,EAAE,UAAU,WAAW;AAAA,EAChC;AAAA;AAAA,EAIQ,WAAW,SAAqC;AACtD,QAAI,IAAI,KAAK,SAAS,IAAI,OAAO;AACjC,QAAI,CAAC,GAAG;AACN,UAAI,IAAI,mBAAmB;AAAA,QACzB,aAAa,KAAK,KAAK;AAAA,QACvB,UAAU,KAAK,KAAK,cAAc;AAAA;AAAA,MACpC,CAAC;AACD,WAAK,SAAS,IAAI,SAAS,CAAC;AAAA,IAC9B;AACA,WAAO;AAAA,EACT;AAAA,EAEQ,UAAU,SAAgC;AAChD,QAAI,IAAI,KAAK,OAAO,IAAI,OAAO;AAC/B,QAAI,CAAC,GAAG;AACN,UAAI;AAAA,QACF,OAAO;AAAA,QACP,QAAQ,CAAC;AAAA,QACT,YAAY;AAAA,QACZ,YAAY;AAAA,QACZ,sBAAsB;AAAA,QACtB,gBAAgB;AAAA,QAChB,eAAe;AAAA,QACf,YAAY;AAAA,QACZ,eAAe;AAAA,QACf,eAAe;AAAA,QACf,gBAAgB;AAAA,QAChB,eAAe;AAAA,MACjB;AACA,WAAK,OAAO,IAAI,SAAS,CAAC;AAAA,IAC5B;AACA,WAAO;AAAA,EACT;AAAA,EAEQ,YAAY,SAAiB,QAAsB;AACzD,UAAM,IAAI,KAAK,UAAU,OAAO;AAChC,QAAI,EAAE,UAAU,SAAU;AAE1B,MAAE,QAAQ;AACV,MAAE,gBAAgB,KAAK,IAAI,IAAI,SAAS,EAAE,UAAU;AACpD,MAAE,aAAa,KAAK,IAAI,EAAE,aAAa,GAAG,EAAE,aAAa;AAGzD,SAAK,WAAW,OAAO,EAAE,MAAM,IAAI,uBAAuB,SAAS,QAAQ,CAAC;AAE5E,UAAM,QAAQ,aAAa,EAAE,MAAM;AACnC,SAAK,KAAK,iBAAiB;AAAA,MACzB,SAAS;AAAA,MACT;AAAA,MACA,YAAY,EAAE,gBAAgB,KAAK,IAAI;AAAA,MACvC,cAAc,MAAM;AAAA,MACpB,UAAU,MAAM;AAAA,MAChB,SAAS,MAAM;AAAA,IACjB,CAAC;AAAA,EACH;AAAA,EAEQ,eAAe,SAAuB;AAC5C,UAAM,IAAI,KAAK,UAAU,OAAO;AAChC,QAAI,EAAE,UAAU,SAAU;AAE1B,MAAE,QAAQ;AACV,MAAE,gBAAgB;AAClB,MAAE,iBAAiB;AACnB,SAAK,KAAK,oBAAoB,EAAE,SAAS,QAAQ,CAAC;AAAA,EACpD;AAAA,EAEQ,WAAW,SAAuB;AACxC,UAAM,IAAI,KAAK,UAAU,OAAO;AAChC,MAAE,QAAQ;AACV,MAAE,SAAS,CAAC;AACZ,MAAE,uBAAuB;AACzB,MAAE,gBAAgB;AAClB,MAAE,iBAAiB;AACnB,MAAE,gBAAgB;AAClB,SAAK,KAAK,eAAe,EAAE,SAAS,QAAQ,CAAC;AAAA,EAC/C;AAAA,EAEQ,cAAc,SAAiB,SAAwB;AAC7D,UAAM,IAAI,KAAK,UAAU,OAAO;AAEhC,MAAE,OAAO,KAAK,OAAO;AACrB,WAAO,EAAE,OAAO,SAAS,EAAE,WAAY,GAAE,OAAO,MAAM;AAEtD,QAAI,YAAY,YAAa,GAAE,wBAAwB;AAAA,QAClD,GAAE,uBAAuB;AAG9B,QAAI,EAAE,UAAU,QAAQ;AACtB,UAAI,YAAY,aAAa;AAC3B,UAAE,iBAAiB;AACnB,YAAI,EAAE,iBAAiB,GAAG;AACxB,YAAE,aAAa,EAAE;AAAA,QACnB;AAAA,MACF,OAAO;AACL,UAAE,gBAAgB;AAAA,MACpB;AAAA,IACF;AAEA,QAAI,CAAC,KAAK,cAAe;AAEzB,QAAI,EAAE,wBAAwB,GAAG;AAC/B,WAAK,YAAY,SAAS,6BAA6B;AACvD;AAAA,IACF;AAEA,UAAM,QAAQ,aAAa,EAAE,MAAM;AACnC,QAAI,MAAM,SAAS,EAAE,YAAY;AAC/B,UAAI,MAAM,gBAAgB,KAAK;AAC7B,aAAK,YAAY,SAAS,qBAAqB;AAC/C;AAAA,MACF;AACA,UAAI,MAAM,YAAY,KAAK;AACzB,aAAK,YAAY,SAAS,iBAAiB;AAC3C;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAAA,EAEA,MAAc,QAAQ,KAAuB,MAA2D;AACtG,UAAM,UAAU,WAAW,IAAI,GAAG;AAClC,UAAM,IAAI,KAAK,UAAU,OAAO;AAChC,UAAM,UAAU,KAAK,WAAW,OAAO;AAEvC,QAAI,KAAK,eAAe;AACtB,UAAI,EAAE,UAAU,UAAU;AACxB,YAAI,KAAK,IAAI,KAAK,EAAE,eAAe;AACjC,eAAK,eAAe,OAAO;AAAA,QAC7B,OAAO;AACL,gBAAM,IAAI,uBAAuB,SAAS,QAAQ;AAAA,QACpD;AAAA,MACF;AAEA,UAAI,EAAE,UAAU,aAAa;AAC3B,YAAI,CAAC,KAAK,WAAY,OAAM,IAAI,sBAAsB,OAAO;AAC7D,YAAI,EAAE,kBAAkB,KAAK,EAAE,cAAe,OAAM,IAAI,sBAAsB,OAAO;AACrF,UAAE,gBAAgB;AAClB,UAAE,kBAAkB;AAAA,MACtB;AAAA,IACF;AAEA,UAAM,YAAY,aAAa;AAC/B,UAAM,QAAQ,KAAK,IAAI;AACvB,QAAI,WAAW;AACf,QAAI;AAEF,UAAI,KAAK,iBAAiB,EAAE,UAAU,aAAa;AACjD,cAAM,QAAQ,eAAe;AAAA,MAC/B,OAAO;AACL,cAAM,QAAQ,QAAQ;AACtB,mBAAW;AAAA,MACb;AAAA,IACF,SAAS,KAAK;AACZ,WAAK,KAAK,oBAAoB,EAAE,WAAW,SAAS,KAAK,OAAO,IAAI,CAAC;AACrE,YAAM;AAAA,IACR;AAEA,SAAK,KAAK,iBAAiB,EAAE,WAAW,SAAS,IAAI,CAAC;AAEtD,QAAI;AACF,YAAM,MAAM,MAAM,cAAc,KAAK,KAAK,gBAAgB;AAC1D,YAAM,aAAa,KAAK,IAAI,IAAI;AAEhC,YAAM,UAAU,mBAAmB,IAAI,MAAM;AAC7C,WAAK,cAAc,SAAS,OAAO;AAGnC,UAAI,KAAK,iBAAiB,EAAE,UAAU,aAAa;AACjD,aAAK,KAAK,gBAAgB,EAAE,SAAS,SAAS,SAAS,QAAQ,IAAI,OAAO,CAAC;AAC3E,YAAI,IAAI,UAAU,OAAO,IAAI,SAAS,KAAK;AACzC,eAAK,WAAW,OAAO;AAAA,QACzB,OAAO;AACL,eAAK,YAAY,SAAS,uBAAuB,IAAI,MAAM,EAAE;AAAA,QAC/D;AAAA,MACF;AAEA,WAAK,KAAK,mBAAmB,EAAE,WAAW,SAAS,KAAK,QAAQ,IAAI,QAAQ,WAAW,CAAC;AACxF,aAAO;AAAA,IACT,SAAS,KAAK;AACZ,YAAM,aAAa,KAAK,IAAI,IAAI;AAEhC,UAAI,sBAAsB,GAAG,GAAG;AAC9B,aAAK,cAAc,SAAS,WAAW;AAAA,MACzC;AAEA,UAAI,KAAK,iBAAiB,EAAE,UAAU,aAAa;AACjD,aAAK,KAAK,gBAAgB,EAAE,SAAS,SAAS,SAAS,aAAa,OAAO,IAAI,CAAC;AAChF,aAAK,YAAY,SAAS,oBAAoB;AAAA,MAChD;AAEA,WAAK,KAAK,mBAAmB,EAAE,WAAW,SAAS,KAAK,OAAO,KAAK,WAAW,CAAC;AAChF,YAAM;AAAA,IACR,UAAE;AAEA,UAAI,KAAK,iBAAiB,EAAE,UAAU,aAAa;AACjD,UAAE,gBAAgB;AAAA,MACpB;AACA,UAAI,SAAU,SAAQ,QAAQ;AAAA,IAChC;AAAA,EACF;AAAA;AAAA,EAIQ,cAAc,KAA2C;AAC/D,WAAO,EAAE,QAAQ,IAAI,QAAQ,SAAS,EAAE,GAAG,IAAI,QAAQ,GAAG,MAAM,IAAI,WAAW,IAAI,IAAI,EAAE;AAAA,EAC3F;AAAA,EAEQ,oBAAoB,OAAgC,YAA0B;AACpF,SAAK;AACL,QAAI,KAAK,qBAAqB,KAAK,0BAA0B,EAAG;AAEhE,UAAM,MAAM,KAAK,IAAI;AACrB,eAAW,CAAC,GAAG,CAAC,KAAK,MAAM,QAAQ,GAAG;AACpC,UAAI,MAAM,EAAE,YAAY,WAAY,OAAM,OAAO,CAAC;AAAA,IACpD;AAAA,EACF;AAAA,EAEQ,cAAc,OAAgC,YAA0B;AAC9E,WAAO,MAAM,QAAQ,YAAY;AAC/B,YAAM,YAAY,MAAM,KAAK,EAAE,KAAK,EAAE;AACtC,UAAI,CAAC,UAAW;AAChB,YAAM,OAAO,SAAS;AAAA,IACxB;AAAA,EACF;AAAA,EAEQ,kBAAkB,QAAgB,eAAkC;AAC1E,WAAO,cAAc,SAAS,MAAM;AAAA,EACtC;AAAA,EAEQ,iBAAiB,cAAsB,aAAqB,YAA4B;AAC9F,UAAM,MAAM,cAAc,KAAK,IAAI,GAAG,eAAe,CAAC;AACtD,UAAM,SAAS,MAAM,KAAK,aAAa,UAAU;AACjD,UAAM,SAAS,MAAM,KAAK,OAAO;AACjC,WAAO,KAAK,MAAM,SAAS,MAAM;AAAA,EACnC;AAAA,EAEA,MAAc,qBAAqB,KAAmD;AACpF,UAAM,KAAK,KAAK;AAChB,UAAM,QAAQ,GAAG;AACjB,QAAI,CAAC,MAAO,QAAO,KAAK,QAAQ,KAAK,EAAE,YAAY,MAAM,CAAC;AAE1D,UAAM,EAAE,aAAa,aAAa,YAAY,cAAc,IAAI;AAEhE,QAAI;AACJ,aAAS,UAAU,GAAG,WAAW,aAAa,WAAW;AACvD,YAAM,MAAM,MAAM,KAAK,QAAQ,KAAK,EAAE,YAAY,MAAM,CAAC;AACzD,aAAO;AAEP,UAAI,KAAK,kBAAkB,IAAI,QAAQ,aAAa,KAAK,UAAU,aAAa;AAC9E,cAAM,QAAQ,KAAK,iBAAiB,SAAS,aAAa,UAAU;AACpE,aAAK,KAAK,oBAAoB,EAAE,KAAK,IAAI,KAAK,SAAS,aAAa,QAAQ,UAAU,IAAI,MAAM,IAAI,SAAS,MAAM,CAAC;AACpH,cAAM,MAAM,KAAK;AACjB;AAAA,MACF;AACA,aAAO;AAAA,IACT;AACA,WAAO;AAAA,EACT;AAAA,EAEA,MAAc,sBAAsB,KAAmD;AACrF,UAAM,KAAK,KAAK;AAChB,UAAM,QAAQ,KAAK;AACnB,UAAM,WAAW,KAAK;AAEtB,SAAK,oBAAoB,OAAO,GAAG,UAAU;AAE7C,UAAM,MAAM,GAAG,MAAM,GAAG;AACxB,UAAM,MAAM,KAAK,IAAI;AAErB,UAAM,OAAO,MAAM,IAAI,GAAG;AAC1B,QAAI,QAAQ,MAAM,KAAK,YAAY,GAAG,WAAY,OAAM,OAAO,GAAG;AAElE,UAAM,MAAM,MAAM,IAAI,GAAG;AACzB,QAAI,OAAO,MAAM,IAAI,UAAW,QAAO,KAAK,cAAc,IAAI,KAAK;AAGnE,QAAI,KAAK,eAAe;AACtB,YAAM,UAAU,WAAW,IAAI,GAAG;AAClC,YAAM,IAAI,KAAK,UAAU,OAAO;AAChC,UAAI,EAAE,UAAU,UAAU;AACxB,cAAM,eAAe,CAAC,CAAC,OAAO,MAAM,IAAI,aAAa,GAAG;AACxD,YAAI,aAAc,QAAO,KAAK,cAAc,IAAK,KAAK;AACtD,cAAM,IAAI,uBAAuB,SAAS,QAAQ;AAAA,MACpD;AAAA,IACF;AAEA,UAAM,QAAQ,SAAS,IAAI,GAAG;AAC9B,QAAI,OAAO;AACT,YAAM,IAAI,MAAM,IAAI,GAAG;AACvB,YAAM,eAAe,CAAC,CAAC,KAAK,MAAM,EAAE,aAAa,GAAG;AAEpD,UAAI,KAAK,aAAc,QAAO,KAAK,cAAc,EAAE,KAAK;AAExD,YAAM,MAAM,MAAM,MAAM;AACxB,UAAI,MAAM,GAAG,mBAAmB;AAC9B,cAAM,MAAM,IAAI,MAAM,kCAAkC,GAAG,EAAE;AAC7D,QAAC,IAAY,OAAO;AACpB,cAAM;AAAA,MACR;AAEA,UAAI,MAAM,WAAW,GAAG,YAAY;AAClC,cAAM,MAAM,IAAI,MAAM,8BAA8B,GAAG,EAAE;AACzD,QAAC,IAAY,OAAO;AACpB,cAAM;AAAA,MACR;AAEA,YAAM,WAAW;AACjB,UAAI;AACF,cAAM,MAAM,MAAM,MAAM;AACxB,eAAO,KAAK,cAAc,GAAG;AAAA,MAC/B,UAAE;AACA,cAAM,WAAW;AAAA,MACnB;AAAA,IACF;AAEA,UAAM,OAAO,MAAM,IAAI,GAAG;AAC1B,UAAM,mBAAmB,CAAC,CAAC,QAAQ,MAAM,KAAK,aAAa,GAAG;AAE9D,UAAM,WAAW,YAAY;AAC3B,YAAM,UAAU,WAAW,IAAI,GAAG;AAClC,YAAM,IAAI,KAAK,UAAU,OAAO;AAChC,YAAM,aAAa,KAAK,iBAAiB,EAAE,UAAU;AAErD,YAAM,MAAM,aACR,MAAM,KAAK,QAAQ,KAAK,EAAE,YAAY,KAAK,CAAC,IAC5C,MAAM,KAAK,qBAAqB,GAAG;AAEvC,UAAI,IAAI,UAAU,OAAO,IAAI,SAAS,KAAK;AACzC,aAAK,cAAc,OAAO,GAAG,UAAU;AACvC,cAAM,IAAI,KAAK,IAAI;AACnB,cAAM,IAAI,KAAK,EAAE,OAAO,KAAK,cAAc,GAAG,GAAG,WAAW,GAAG,WAAW,IAAI,GAAG,MAAM,CAAC;AAAA,MAC1F;AACA,aAAO;AAAA,IACT,GAAG;AAEH,aAAS,IAAI,KAAK,EAAE,SAAS,eAAe,KAAK,IAAI,GAAG,SAAS,EAAE,CAAC;AAEpE,QAAI;AACF,YAAM,MAAM,MAAM;AAElB,UAAI,EAAE,IAAI,UAAU,OAAO,IAAI,SAAS,QAAQ,QAAQ,kBAAkB;AACxE,eAAO,KAAK,cAAc,KAAK,KAAK;AAAA,MACtC;AAEA,aAAO,KAAK,cAAc,GAAG;AAAA,IAC/B,SAAS,KAAK;AACZ,UAAI,QAAQ,kBAAkB;AAC5B,aAAK,KAAK,6BAA6B,EAAE,KAAK,KAAK,IAAI,KAAK,OAAO,IAAI,CAAC;AACxE,eAAO,KAAK,cAAc,KAAK,KAAK;AAAA,MACtC;AACA,YAAM;AAAA,IACR,UAAE;AACA,eAAS,OAAO,GAAG;AAAA,IACrB;AAAA,EACF;AACF;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/client.ts","../src/errors.ts","../src/limiter.ts","../src/http.ts","../src/instant_get.ts"],"sourcesContent":["// src/client.ts\r\nimport { EventEmitter } from \"node:events\";\r\nimport { ConcurrencyLimiter } from \"./limiter.js\";\r\nimport { doHttpRequest } from \"./http.js\";\r\nimport {\r\n QueueFullError,\r\n RequestTimeoutError,\r\n ResilientHttpError,\r\n HalfOpenRejectedError,\r\n UpstreamUnhealthyError,\r\n} from \"./errors.js\";\r\nimport type {\r\n MicroCacheOptions,\r\n ResilientHttpClientOptions,\r\n ResilientRequest,\r\n ResilientResponse,\r\n} from \"./types.js\";\r\n\r\n/* ---------------------------- helpers ---------------------------- */\r\n\r\nfunction normalizeUrlForKey(rawUrl: string): string {\r\n const u = new URL(rawUrl);\r\n u.hostname = u.hostname.toLowerCase();\r\n\r\n const isHttpDefault = u.protocol === \"http:\" && u.port === \"80\";\r\n const isHttpsDefault = u.protocol === \"https:\" && u.port === \"443\";\r\n if (isHttpDefault || isHttpsDefault) u.port = \"\";\r\n\r\n return u.toString();\r\n}\r\n\r\nfunction baseUrlKey(rawUrl: string): string {\r\n const u = new URL(rawUrl);\r\n u.hostname = u.hostname.toLowerCase();\r\n\r\n const isHttpDefault = u.protocol === \"http:\" && u.port === \"80\";\r\n const isHttpsDefault = u.protocol === \"https:\" && u.port === \"443\";\r\n if (isHttpDefault || isHttpsDefault) u.port = \"\";\r\n\r\n return `${u.protocol}//${u.host}`;\r\n}\r\n\r\nfunction defaultMicroCacheKeyFn(req: ResilientRequest): string {\r\n return `GET ${normalizeUrlForKey(req.url)}`;\r\n}\r\n\r\nfunction genRequestId(): string {\r\n return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;\r\n}\r\n\r\nfunction sleep(ms: number): Promise<void> {\r\n return new Promise((r) => setTimeout(r, ms));\r\n}\r\n\r\nfunction clamp(n: number, lo: number, hi: number): number {\r\n return Math.max(lo, Math.min(hi, n));\r\n}\r\n\r\nfunction jitterMs(ms: number): number {\r\n const mult = 0.8 + Math.random() * 0.4; // [0.8, 1.2]\r\n return Math.round(ms * mult);\r\n}\r\n\r\n\r\n/* ---------------------------- health ---------------------------- */\r\n\r\ntype HealthState = \"OPEN\" | \"CLOSED\" | \"HALF_OPEN\";\r\ntype Outcome = \"SUCCESS\" | \"HARD_FAIL\" | \"SOFT_FAIL\";\r\n\r\ntype HealthTracker = {\r\n state: HealthState;\r\n\r\n window: Outcome[];\r\n windowSize: number; // 20\r\n minSamples: number; // 10\r\n\r\n consecutiveHardFails: number; // trip at 3\r\n\r\n cooldownBaseMs: number; // 1000\r\n cooldownCapMs: number; // 30000\r\n cooldownMs: number; // current backoff\r\n cooldownUntil: number;\r\n\r\n probeInFlight: boolean; // probeConcurrency=1\r\n probeRemaining: number; // 1 probe per half-open\r\n\r\n stableNonHard: number; // reset backoff after 5 non-hard after open\r\n};\r\n\r\nconst SOFT_FAIL_STATUSES = new Set([429, 502, 503, 504]);\r\n\r\nfunction classifyHttpStatus(status: number): Outcome {\r\n if (status >= 200 && status < 300) return \"SUCCESS\";\r\n if (SOFT_FAIL_STATUSES.has(status)) return \"SOFT_FAIL\";\r\n return \"SUCCESS\"; // do not penalize health for other statuses (incl 4xx except 429)\r\n}\r\n\r\nfunction computeRates(window: Outcome[]) {\r\n const total = window.length;\r\n let hard = 0;\r\n let soft = 0;\r\n for (const o of window) {\r\n if (o === \"HARD_FAIL\") hard += 1;\r\n else if (o === \"SOFT_FAIL\") soft += 1;\r\n }\r\n return {\r\n total,\r\n hard,\r\n soft,\r\n hardFailRate: total === 0 ? 0 : hard / total,\r\n failRate: total === 0 ? 0 : (hard + soft) / total,\r\n };\r\n}\r\n\r\n/**\r\n * Only HTTP-layer failures should count as HARD_FAIL.\r\n * Control-plane rejections must NOT poison health.\r\n */\r\nfunction shouldCountAsHardFail(err: unknown): boolean {\r\n if (err instanceof UpstreamUnhealthyError) return false;\r\n if (err instanceof HalfOpenRejectedError) return false;\r\n if (err instanceof QueueFullError) return false;\r\n\r\n // if your http layer throws this, it's a real hard fail (timeout)\r\n if (err instanceof RequestTimeoutError) return true;\r\n\r\n // If it’s a known library error but not one of the above, be conservative:\r\n // treat it as NOT a health signal unless it’s clearly HTTP-related.\r\n if (err instanceof ResilientHttpError) return false;\r\n\r\n // Unknown thrown error: assume it's an HTTP/network failure.\r\n return true;\r\n}\r\n\r\n/* ---------------------------- microcache types ---------------------------- */\r\n\r\ntype CacheEntry = {\r\n createdAt: number;\r\n expiresAt: number;\r\n value: ResilientResponse;\r\n};\r\n\r\ntype InFlightGroup = {\r\n promise: Promise<ResilientResponse>;\r\n windowStartMs: number;\r\n waiters: number;\r\n};\r\n\r\n/* ---------------------------- client ---------------------------- */\r\n\r\nexport class ResilientHttpClient extends EventEmitter {\r\n private readonly requestTimeoutMs: number;\r\n private readonly healthEnabled: boolean;\r\n\r\n private readonly limiters = new Map<string, ConcurrencyLimiter>();\r\n private readonly health = new Map<string, HealthTracker>();\r\n\r\n private readonly microCache?: Required<\r\n Pick<\r\n MicroCacheOptions,\r\n | \"enabled\"\r\n | \"ttlMs\"\r\n | \"maxStaleMs\"\r\n | \"maxEntries\"\r\n | \"maxWaiters\"\r\n | \"followerTimeoutMs\"\r\n | \"keyFn\"\r\n >\r\n > & {\r\n retry?: {\r\n maxAttempts: number;\r\n baseDelayMs: number;\r\n maxDelayMs: number;\r\n retryOnStatus: number[];\r\n };\r\n };\r\n\r\n private cache?: Map<string, CacheEntry>;\r\n private inFlight?: Map<string, InFlightGroup>;\r\n\r\n private microCacheReqCount = 0;\r\n private readonly cleanupEveryNRequests = 100;\r\n\r\n constructor(private readonly opts: ResilientHttpClientOptions) {\r\n super();\r\n this.requestTimeoutMs = opts.requestTimeoutMs;\r\n this.healthEnabled = opts.health?.enabled ?? true;\r\n\r\n const mc = opts.microCache;\r\n if (mc?.enabled) {\r\n const retry = mc.retry\r\n ? {\r\n maxAttempts: mc.retry.maxAttempts ?? 3,\r\n baseDelayMs: mc.retry.baseDelayMs ?? 50,\r\n maxDelayMs: mc.retry.maxDelayMs ?? 200,\r\n retryOnStatus: mc.retry.retryOnStatus ?? [429, 502, 503, 504],\r\n }\r\n : undefined;\r\n\r\n this.microCache = {\r\n enabled: true,\r\n ttlMs: mc.ttlMs ?? 1000,\r\n maxStaleMs: mc.maxStaleMs ?? 10_000,\r\n maxEntries: mc.maxEntries ?? 500,\r\n maxWaiters: mc.maxWaiters ?? 1000,\r\n followerTimeoutMs: mc.followerTimeoutMs ?? 5000,\r\n keyFn: mc.keyFn ?? defaultMicroCacheKeyFn,\r\n retry,\r\n };\r\n\r\n this.cache = new Map();\r\n this.inFlight = new Map();\r\n }\r\n }\r\n\r\n async request(req: ResilientRequest): Promise<ResilientResponse> {\r\n if (this.microCache?.enabled && req.method === \"GET\" && req.body == null) {\r\n return this.requestWithMicroCache(req);\r\n }\r\n return this.execute(req, { allowProbe: false });\r\n }\r\n /**\r\n * Run a HALF_OPEN probe. Exactly one probe is allowed per HALF_OPEN window.\r\n * Normal request() calls are rejected during HALF_OPEN.\r\n */\r\n async probe(req: ResilientRequest): Promise<ResilientResponse> {\r\n if (this.microCache?.enabled && req.method === \"GET\" && req.body == null) {\r\n // microcache path already supports probe, but keep consistent behavior:\r\n // force direct execute so this call is always a real probe attempt.\r\n return this.execute(req, { allowProbe: true });\r\n }\r\n return this.execute(req, { allowProbe: true });\r\n }\r\n\r\n snapshot(): { inFlight: number; queueDepth: number } {\r\n let inFlight = 0;\r\n let queueDepth = 0;\r\n for (const l of this.limiters.values()) {\r\n const s = l.snapshot();\r\n inFlight += s.inFlight;\r\n queueDepth += s.queueDepth;\r\n }\r\n return { inFlight, queueDepth };\r\n }\r\n\r\n /* ---------------- internals ---------------- */\r\n\r\n private getLimiter(baseKey: string): ConcurrencyLimiter {\r\n let l = this.limiters.get(baseKey);\r\n if (!l) {\r\n l = new ConcurrencyLimiter({\r\n maxInFlight: this.opts.maxInFlight,\r\n maxQueue: this.opts.maxInFlight * 10, // hidden factor\r\n });\r\n this.limiters.set(baseKey, l);\r\n }\r\n return l;\r\n }\r\n\r\n private getHealth(baseKey: string): HealthTracker {\r\n let h = this.health.get(baseKey);\r\n if (!h) {\r\n h = {\r\n state: \"OPEN\",\r\n window: [],\r\n windowSize: 20,\r\n minSamples: 10,\r\n consecutiveHardFails: 0,\r\n cooldownBaseMs: 1000,\r\n cooldownCapMs: 30_000,\r\n cooldownMs: 1000,\r\n cooldownUntil: 0,\r\n probeInFlight: false,\r\n probeRemaining: 0,\r\n stableNonHard: 0,\r\n };\r\n this.health.set(baseKey, h);\r\n }\r\n return h;\r\n }\r\n\r\n private closeHealth(baseKey: string, reason: string): void {\r\n const h = this.getHealth(baseKey);\r\n if (h.state === \"CLOSED\") return;\r\n\r\n h.state = \"CLOSED\";\r\n h.cooldownUntil = Date.now() + jitterMs(h.cooldownMs);\r\n h.cooldownMs = Math.min(h.cooldownMs * 2, h.cooldownCapMs);\r\n\r\n // reject queued immediately\r\n this.getLimiter(baseKey).flush(new UpstreamUnhealthyError(baseKey, \"CLOSED\"));\r\n\r\n const rates = computeRates(h.window);\r\n this.emit(\"health:closed\", {\r\n baseUrl: baseKey,\r\n reason,\r\n cooldownMs: h.cooldownUntil - Date.now(),\r\n hardFailRate: rates.hardFailRate,\r\n failRate: rates.failRate,\r\n samples: rates.total,\r\n });\r\n }\r\n\r\n private halfOpenHealth(baseKey: string): void {\r\n const h = this.getHealth(baseKey);\r\n if (h.state !== \"CLOSED\") return;\r\n\r\n h.state = \"HALF_OPEN\";\r\n h.probeInFlight = false;\r\n h.probeRemaining = 1;\r\n this.emit(\"health:half_open\", { baseUrl: baseKey });\r\n }\r\n\r\n private openHealth(baseKey: string): void {\r\n const h = this.getHealth(baseKey);\r\n h.state = \"OPEN\";\r\n h.window = [];\r\n h.consecutiveHardFails = 0;\r\n h.probeInFlight = false;\r\n h.probeRemaining = 0;\r\n h.stableNonHard = 0;\r\n this.emit(\"health:open\", { baseUrl: baseKey });\r\n }\r\n\r\n private recordOutcome(baseKey: string, outcome: Outcome): void {\r\n const h = this.getHealth(baseKey);\r\n\r\n h.window.push(outcome);\r\n while (h.window.length > h.windowSize) h.window.shift();\r\n\r\n if (outcome === \"HARD_FAIL\") h.consecutiveHardFails += 1;\r\n else h.consecutiveHardFails = 0;\r\n\r\n // stabilization (reset backoff after 5 non-hard in OPEN)\r\n if (h.state === \"OPEN\") {\r\n if (outcome !== \"HARD_FAIL\") {\r\n h.stableNonHard += 1;\r\n if (h.stableNonHard >= 5) {\r\n h.cooldownMs = h.cooldownBaseMs;\r\n }\r\n } else {\r\n h.stableNonHard = 0;\r\n }\r\n }\r\n\r\n if (!this.healthEnabled) return;\r\n\r\n if (h.consecutiveHardFails >= 3) {\r\n this.closeHealth(baseKey, \"3 consecutive hard failures\");\r\n return;\r\n }\r\n\r\n const rates = computeRates(h.window);\r\n if (rates.total >= h.minSamples) {\r\n if (rates.hardFailRate >= 0.3) {\r\n this.closeHealth(baseKey, \"hardFailRate >= 30%\");\r\n return;\r\n }\r\n if (rates.failRate >= 0.5) {\r\n this.closeHealth(baseKey, \"failRate >= 50%\");\r\n return;\r\n }\r\n }\r\n }\r\n\r\n private async execute(req: ResilientRequest, opts: { allowProbe: boolean }): Promise<ResilientResponse> {\r\n const baseKey = baseUrlKey(req.url);\r\n const h = this.getHealth(baseKey);\r\n const limiter = this.getLimiter(baseKey);\r\n\r\n if (this.healthEnabled) {\r\n if (h.state === \"CLOSED\") {\r\n if (Date.now() >= h.cooldownUntil) {\r\n this.halfOpenHealth(baseKey);\r\n } else {\r\n throw new UpstreamUnhealthyError(baseKey, \"CLOSED\");\r\n }\r\n }\r\n\r\n if (h.state === \"HALF_OPEN\") {\r\n if (!opts.allowProbe) throw new HalfOpenRejectedError(baseKey);\r\n if (h.probeRemaining <= 0 || h.probeInFlight) throw new HalfOpenRejectedError(baseKey);\r\n h.probeInFlight = true;\r\n h.probeRemaining -= 1;\r\n }\r\n }\r\n\r\n const requestId = genRequestId();\r\n const start = Date.now();\r\n let acquired = false;\r\n try {\r\n // Probes should not wait in queue.\r\n if (this.healthEnabled && h.state === \"HALF_OPEN\") {\r\n await limiter.acquireNoQueue();\r\n } else {\r\n await limiter.acquire();\r\n\r\n }\r\n acquired = true;\r\n } catch (err) {\r\n this.emit(\"request:rejected\", { requestId, request: req, error: err });\r\n throw err;\r\n }\r\n\r\n this.emit(\"request:start\", { requestId, request: req });\r\n\r\n try {\r\n const res = await doHttpRequest(req, this.requestTimeoutMs);\r\n const durationMs = Date.now() - start;\r\n\r\n const outcome = classifyHttpStatus(res.status);\r\n this.recordOutcome(baseKey, outcome);\r\n\r\n // Probe decision\r\n if (this.healthEnabled && h.state === \"HALF_OPEN\") {\r\n this.emit(\"health:probe\", { baseUrl: baseKey, outcome, status: res.status });\r\n if (res.status >= 200 && res.status < 300) {\r\n this.openHealth(baseKey);\r\n } else {\r\n this.closeHealth(baseKey, `probe failed status=${res.status}`);\r\n }\r\n }\r\n\r\n this.emit(\"request:success\", { requestId, request: req, status: res.status, durationMs });\r\n return res;\r\n } catch (err) {\r\n const durationMs = Date.now() - start;\r\n\r\n if (shouldCountAsHardFail(err)) {\r\n this.recordOutcome(baseKey, \"HARD_FAIL\");\r\n }\r\n\r\n if (this.healthEnabled && h.state === \"HALF_OPEN\") {\r\n this.emit(\"health:probe\", { baseUrl: baseKey, outcome: \"HARD_FAIL\", error: err });\r\n this.closeHealth(baseKey, \"probe hard failure\");\r\n }\r\n\r\n this.emit(\"request:failure\", { requestId, request: req, error: err, durationMs });\r\n throw err;\r\n } finally {\r\n // Clear probe flag (if any)\r\n if (this.healthEnabled && h.state === \"HALF_OPEN\") {\r\n h.probeInFlight = false;\r\n }\r\n if (acquired) limiter.release();\r\n }\r\n }\r\n\r\n /* ---------------- microcache ---------------- */\r\n\r\n private cloneResponse(res: ResilientResponse): ResilientResponse {\r\n return { status: res.status, headers: { ...res.headers }, body: new Uint8Array(res.body) };\r\n }\r\n\r\n private maybeCleanupExpired(cache: Map<string, CacheEntry>, maxStaleMs: number): void {\r\n this.microCacheReqCount++;\r\n if (this.microCacheReqCount % this.cleanupEveryNRequests !== 0) return;\r\n\r\n const now = Date.now();\r\n for (const [k, v] of cache.entries()) {\r\n if (now - v.createdAt > maxStaleMs) cache.delete(k);\r\n }\r\n }\r\n\r\n private evictIfNeeded(cache: Map<string, CacheEntry>, maxEntries: number): void {\r\n while (cache.size >= maxEntries) {\r\n const oldestKey = cache.keys().next().value as string | undefined;\r\n if (!oldestKey) break;\r\n cache.delete(oldestKey);\r\n }\r\n }\r\n\r\n private isRetryableStatus(status: number, retryOnStatus: number[]): boolean {\r\n return retryOnStatus.includes(status);\r\n }\r\n\r\n private computeBackoffMs(attemptIndex: number, baseDelayMs: number, maxDelayMs: number): number {\r\n const exp = baseDelayMs * Math.pow(2, attemptIndex - 1);\r\n const capped = clamp(exp, baseDelayMs, maxDelayMs);\r\n const jitter = 0.5 + Math.random();\r\n return Math.round(capped * jitter);\r\n }\r\n\r\n private async fetchWithLeaderRetry(req: ResilientRequest): Promise<ResilientResponse> {\r\n const mc = this.microCache!;\r\n const retry = mc.retry;\r\n if (!retry) return this.execute(req, { allowProbe: false });\r\n\r\n const { maxAttempts, baseDelayMs, maxDelayMs, retryOnStatus } = retry;\r\n\r\n let last: ResilientResponse | undefined;\r\n for (let attempt = 1; attempt <= maxAttempts; attempt++) {\r\n const res = await this.execute(req, { allowProbe: false });\r\n last = res;\r\n\r\n if (this.isRetryableStatus(res.status, retryOnStatus) && attempt < maxAttempts) {\r\n const delay = this.computeBackoffMs(attempt, baseDelayMs, maxDelayMs);\r\n this.emit(\"microcache:retry\", { url: req.url, attempt, maxAttempts, reason: `status ${res.status}`, delayMs: delay });\r\n await sleep(delay);\r\n continue;\r\n }\r\n return res;\r\n }\r\n return last!;\r\n }\r\n\r\n private async requestWithMicroCache(req: ResilientRequest): Promise<ResilientResponse> {\r\n const mc = this.microCache!;\r\n const cache = this.cache!;\r\n const inFlight = this.inFlight!;\r\n\r\n this.maybeCleanupExpired(cache, mc.maxStaleMs);\r\n\r\n const key = mc.keyFn(req);\r\n const now = Date.now();\r\n\r\n const hit0 = cache.get(key);\r\n if (hit0 && now - hit0.createdAt > mc.maxStaleMs) cache.delete(key);\r\n\r\n const hit = cache.get(key);\r\n if (hit && now < hit.expiresAt) return this.cloneResponse(hit.value);\r\n\r\n // If CLOSED: serve stale if allowed else fail fast\r\n if (this.healthEnabled) {\r\n const baseKey = baseUrlKey(req.url);\r\n const h = this.getHealth(baseKey);\r\n if (h.state === \"CLOSED\") {\r\n const staleAllowed = !!hit && now - hit.createdAt <= mc.maxStaleMs;\r\n if (staleAllowed) return this.cloneResponse(hit!.value);\r\n throw new UpstreamUnhealthyError(baseKey, \"CLOSED\");\r\n }\r\n }\r\n\r\n const group = inFlight.get(key);\r\n if (group) {\r\n const h = cache.get(key);\r\n const staleAllowed = !!h && now - h.createdAt <= mc.maxStaleMs;\r\n\r\n if (h && staleAllowed) return this.cloneResponse(h.value);\r\n\r\n const age = now - group.windowStartMs;\r\n if (age > mc.followerTimeoutMs) {\r\n const err = new Error(`Follower window closed for key=${key}`);\r\n (err as any).name = \"FollowerWindowClosedError\";\r\n throw err;\r\n }\r\n\r\n if (group.waiters >= mc.maxWaiters) {\r\n const err = new Error(`Too many followers for key=${key}`);\r\n (err as any).name = \"TooManyWaitersError\";\r\n throw err;\r\n }\r\n\r\n group.waiters += 1;\r\n try {\r\n const res = await group.promise;\r\n return this.cloneResponse(res);\r\n } finally {\r\n group.waiters -= 1;\r\n }\r\n }\r\n\r\n const prev = cache.get(key);\r\n const prevStaleAllowed = !!prev && now - prev.createdAt <= mc.maxStaleMs;\r\n\r\n const promise = (async () => {\r\n const baseKey = baseUrlKey(req.url);\r\n const h = this.getHealth(baseKey);\r\n const allowProbe = this.healthEnabled && h.state === \"HALF_OPEN\";\r\n\r\n const res = allowProbe\r\n ? await this.execute(req, { allowProbe: true })\r\n : await this.fetchWithLeaderRetry(req);\r\n\r\n if (res.status >= 200 && res.status < 300) {\r\n this.evictIfNeeded(cache, mc.maxEntries);\r\n const t = Date.now();\r\n cache.set(key, { value: this.cloneResponse(res), createdAt: t, expiresAt: t + mc.ttlMs });\r\n }\r\n return res;\r\n })();\r\n\r\n inFlight.set(key, { promise, windowStartMs: Date.now(), waiters: 0 });\r\n\r\n try {\r\n const res = await promise;\r\n\r\n if (!(res.status >= 200 && res.status < 300) && prev && prevStaleAllowed) {\r\n return this.cloneResponse(prev.value);\r\n }\r\n\r\n return this.cloneResponse(res);\r\n } catch (err) {\r\n if (prev && prevStaleAllowed) {\r\n this.emit(\"microcache:refresh_failed\", { key, url: req.url, error: err });\r\n return this.cloneResponse(prev.value);\r\n }\r\n throw err;\r\n } finally {\r\n inFlight.delete(key);\r\n }\r\n }\r\n}\r\n","export abstract class ResilientHttpError extends Error {\r\n public readonly name: string;\r\n constructor(message: string) {\r\n super(message);\r\n this.name = this.constructor.name;\r\n }\r\n}\r\n\r\nexport class QueueFullError extends ResilientHttpError {\r\n constructor(public readonly maxQueue: number) {\r\n super(`Queue is full (maxQueue=${maxQueue}).`);\r\n }\r\n}\r\n\r\nexport class QueueTimeoutError extends ResilientHttpError {\r\n constructor(public readonly enqueueTimeoutMs: number) {\r\n super(`Queue wait exceeded (enqueueTimeoutMs=${enqueueTimeoutMs}).`);\r\n }\r\n}\r\n\r\nexport class RequestTimeoutError extends ResilientHttpError {\r\n constructor(public readonly requestTimeoutMs: number) {\r\n super(`Request timed out (requestTimeoutMs=${requestTimeoutMs}).`);\r\n }\r\n}\r\n\r\n\r\n\r\nexport class UpstreamError extends ResilientHttpError {\r\n constructor(public readonly status: number) {\r\n super(`Upstream returned error status=${status}.`);\r\n }\r\n}\r\n\r\n\r\n\r\n\r\nexport class UpstreamUnhealthyError extends ResilientHttpError {\r\n constructor(public readonly baseUrl: string, public readonly state: string) {\r\n super(`Upstream is unhealthy (state=${state}, baseUrl=${baseUrl}).`);\r\n }\r\n}\r\n\r\nexport class HalfOpenRejectedError extends ResilientHttpError {\r\n constructor(public readonly baseUrl: string) {\r\n super(`Upstream is HALF_OPEN (probe only) for baseUrl=${baseUrl}.`);\r\n }\r\n}\r\n\r\n","// src/limiter.ts\r\nimport { QueueFullError } from \"./errors.js\";\r\n\r\ntype ResolveFn = () => void;\r\ntype RejectFn = (err: unknown) => void;\r\n\r\ninterface Waiter {\r\n resolve: ResolveFn;\r\n reject: RejectFn;\r\n}\r\n\r\n/**\r\n * Process-local concurrency limiter with bounded FIFO queue.\r\n *\r\n * - maxInFlight: concurrent permits\r\n * - maxQueue: bounded burst buffer\r\n * - No enqueue-timeout by design.\r\n *\r\n * Also supports:\r\n * - flush(err): reject all queued waiters immediately\r\n * - acquireNoQueue(): for probes (must start now or fail)\r\n */\r\nexport class ConcurrencyLimiter {\r\n private readonly maxInFlight: number;\r\n private readonly maxQueue: number;\r\n\r\n private inFlight = 0;\r\n private queue: Waiter[] = [];\r\n\r\n constructor(opts: { maxInFlight: number; maxQueue: number }) {\r\n if (!Number.isFinite(opts.maxInFlight) || opts.maxInFlight <= 0) {\r\n throw new Error(`maxInFlight must be > 0 (got ${opts.maxInFlight})`);\r\n }\r\n if (!Number.isFinite(opts.maxQueue) || opts.maxQueue < 0) {\r\n throw new Error(`maxQueue must be >= 0 (got ${opts.maxQueue})`);\r\n }\r\n\r\n this.maxInFlight = opts.maxInFlight;\r\n this.maxQueue = opts.maxQueue;\r\n }\r\n\r\n acquire(): Promise<void> {\r\n if (this.inFlight < this.maxInFlight) {\r\n this.inFlight += 1;\r\n return Promise.resolve();\r\n }\r\n\r\n if (this.maxQueue === 0 || this.queue.length >= this.maxQueue) {\r\n return Promise.reject(new QueueFullError(this.maxQueue));\r\n }\r\n\r\n return new Promise<void>((resolve, reject) => {\r\n this.queue.push({ resolve, reject });\r\n });\r\n }\r\n\r\n /**\r\n * Acquire without queueing: either start now or fail.\r\n * Used for HALF_OPEN probes so recovery never waits behind backlog.\r\n */\r\n acquireNoQueue(): Promise<void> {\r\n if (this.inFlight < this.maxInFlight) {\r\n this.inFlight += 1;\r\n return Promise.resolve();\r\n }\r\n // treat as queue full (we don't want a new error type)\r\n return Promise.reject(new QueueFullError(0));\r\n }\r\n\r\n release(): void {\r\n if (this.inFlight <= 0) {\r\n throw new Error(\"release() called when inFlight is already 0\");\r\n }\r\n\r\n const next = this.queue.shift();\r\n if (next) {\r\n next.resolve(); // transfer permit\r\n return;\r\n }\r\n\r\n this.inFlight -= 1;\r\n }\r\n\r\n flush(err: unknown): void {\r\n const q = this.queue;\r\n this.queue = [];\r\n for (const w of q) w.reject(err);\r\n }\r\n\r\n snapshot(): { inFlight: number; queueDepth: number; maxInFlight: number; maxQueue: number } {\r\n return {\r\n inFlight: this.inFlight,\r\n queueDepth: this.queue.length,\r\n maxInFlight: this.maxInFlight,\r\n maxQueue: this.maxQueue,\r\n };\r\n }\r\n}\r\n","// src/http.ts\r\nimport { request as undiciRequest } from \"undici\";\r\nimport { RequestTimeoutError } from \"./errors.js\";\r\nimport type { ResilientRequest, ResilientResponse } from \"./types.js\";\r\n\r\nfunction normalizeHeaders(headers: any): Record<string, string> {\r\n const out: Record<string, string> = {};\r\n if (!headers) return out;\r\n\r\n // undici headers are an object-like structure (headers: Record<string, string | string[]>)\r\n for (const [k, v] of Object.entries(headers)) {\r\n if (Array.isArray(v)) out[k.toLowerCase()] = v.join(\", \");\r\n else if (typeof v === \"string\") out[k.toLowerCase()] = v;\r\n else out[k.toLowerCase()] = String(v);\r\n }\r\n return out;\r\n}\r\n\r\n/**\r\n * Execute a single HTTP request with a hard timeout using AbortController.\r\n * No retries. No breaker. Just raw outbound I/O with a timeout.\r\n */\r\nexport async function doHttpRequest(\r\n req: ResilientRequest,\r\n requestTimeoutMs: number\r\n): Promise<ResilientResponse> {\r\n const ac = new AbortController();\r\n const timer = setTimeout(() => ac.abort(), requestTimeoutMs);\r\n\r\n try {\r\n const res = await undiciRequest(req.url, {\r\n method: req.method,\r\n headers: req.headers,\r\n body: req.body as any,\r\n signal: ac.signal,\r\n });\r\n\r\n const body = await res.body.arrayBuffer();\r\n return {\r\n status: res.statusCode,\r\n headers: normalizeHeaders(res.headers),\r\n body: new Uint8Array(body),\r\n };\r\n } catch (err: any) {\r\n // undici throws AbortError on abort\r\n if (err?.name === \"AbortError\") {\r\n throw new RequestTimeoutError(requestTimeoutMs);\r\n }\r\n throw err;\r\n } finally {\r\n clearTimeout(timer);\r\n }\r\n}\r\n","// src/instant_get.ts\r\n//\r\n// Instant GET store (single URL):\r\n// - GET-only\r\n// - Polls one URL on an interval (default 5000ms)\r\n// - Stores latest successful ResilientResponse\r\n// - get() returns cached value instantly (or undefined)\r\n// - Cached value is valid for 60s only (expired => undefined)\r\n// - On error: stop OR retry N times (default: retry forever)\r\n// - Infinite retry uses backoff: 1s, 5s, 10s, 30s, 60s, 60s...\r\n// - start() never blocks\r\n// - ready flips to true after first successful fetch (2xx)\r\n// - waitReady(timeoutSec=10) polls ready every 1s and returns true/false (never throws)\r\n\r\nimport type {\r\n ResilientResponse,\r\n InstantGetOptions,\r\n InstantGetSnapshot,\r\n InstantGetOnError,\r\n} from \"./types.js\";\r\n\r\nconst VALID_MS = 60_000;\r\nconst RETRY_BACKOFF_MS = [1000, 5000, 10_000, 30_000, 60_000];\r\n\r\ntype Entry = {\r\n url?: string;\r\n intervalMs: number;\r\n onError: InstantGetOnError;\r\n\r\n timer?: NodeJS.Timeout;\r\n inFlight: boolean;\r\n\r\n last?: ResilientResponse;\r\n lastOkAt?: number;\r\n lastStatus?: number;\r\n\r\n lastErrorName?: string;\r\n consecutiveErrors: number;\r\n retriesRemaining: number; // Infinity allowed\r\n\r\n // backoff stage for infinite retry\r\n backoffStage: number;\r\n\r\n // next scheduled delay (for snapshot)\r\n nextDelayMs?: number;\r\n\r\n // readiness: becomes true after first successful fetch (2xx)\r\n ready: boolean;\r\n};\r\n\r\nfunction cloneResponse(res: ResilientResponse): ResilientResponse {\r\n return {\r\n status: res.status,\r\n headers: { ...res.headers },\r\n body: new Uint8Array(res.body),\r\n };\r\n}\r\n\r\nfunction headersToRecord(headers: Headers): Record<string, string> {\r\n const out: Record<string, string> = {};\r\n headers.forEach((v, k) => {\r\n out[k] = v;\r\n });\r\n return out;\r\n}\r\n\r\nfunction toRetryCount(onError?: InstantGetOnError): number {\r\n if (!onError) return Number.POSITIVE_INFINITY;\r\n if (onError === \"stop\") return 0;\r\n\r\n const n = onError.retry;\r\n if (!Number.isFinite(n)) return Number.POSITIVE_INFINITY;\r\n return Math.max(0, Math.floor(n));\r\n}\r\n\r\nfunction nextBackoffDelayMs(stage: number): number {\r\n const idx = Math.min(stage, RETRY_BACKOFF_MS.length - 1);\r\n return RETRY_BACKOFF_MS[idx];\r\n}\r\n\r\nfunction sleep(ms: number): Promise<void> {\r\n return new Promise((r) => setTimeout(r, ms));\r\n}\r\n\r\nasync function fetchGet(url: string): Promise<ResilientResponse> {\r\n const res = await fetch(url, { method: \"GET\" });\r\n\r\n const body = new Uint8Array(await res.arrayBuffer());\r\n return {\r\n status: res.status,\r\n headers: headersToRecord(res.headers),\r\n body,\r\n };\r\n}\r\n\r\nexport class InstantGetStore {\r\n private state: Entry = {\r\n url: undefined,\r\n intervalMs: 5000,\r\n onError: { retry: Number.POSITIVE_INFINITY },\r\n\r\n timer: undefined,\r\n inFlight: false,\r\n\r\n last: undefined,\r\n lastOkAt: undefined,\r\n lastStatus: undefined,\r\n\r\n lastErrorName: undefined,\r\n consecutiveErrors: 0,\r\n retriesRemaining: Number.POSITIVE_INFINITY,\r\n\r\n backoffStage: 0,\r\n nextDelayMs: undefined,\r\n\r\n ready: false,\r\n };\r\n\r\n start(url: string, opts?: InstantGetOptions): void {\r\n // stop previous loop\r\n if (this.state.timer) this.stop();\r\n\r\n const intervalMs = opts?.intervalMs ?? 5000;\r\n if (!Number.isFinite(intervalMs) || intervalMs <= 0) {\r\n throw new Error(`intervalMs must be > 0 (got ${intervalMs})`);\r\n }\r\n\r\n const onError = opts?.onError ?? { retry: Number.POSITIVE_INFINITY };\r\n\r\n this.state.url = url;\r\n this.state.intervalMs = intervalMs;\r\n this.state.onError = onError;\r\n this.state.retriesRemaining = toRetryCount(onError);\r\n\r\n // reset counters for new run\r\n this.state.backoffStage = 0;\r\n this.state.nextDelayMs = undefined;\r\n this.state.inFlight = false;\r\n\r\n // readiness reset for this run\r\n this.state.ready = false;\r\n\r\n // schedule immediate tick\r\n this.scheduleNext(0);\r\n }\r\n\r\n stop(): void {\r\n if (this.state.timer) clearTimeout(this.state.timer);\r\n this.state.timer = undefined;\r\n\r\n this.state.nextDelayMs = undefined;\r\n this.state.inFlight = false;\r\n this.state.ready = false;\r\n }\r\n\r\n /** Returns true only after first successful fetch (2xx) for the current run. */\r\n isReady(): boolean {\r\n return this.state.ready;\r\n }\r\n\r\n /**\r\n * Polls `ready` every 1 second.\r\n * Returns true if ready becomes true within timeoutSec (default 10), else false.\r\n * Never throws.\r\n */\r\n async waitReady(timeoutSec = 10): Promise<boolean> {\r\n if (!Number.isFinite(timeoutSec) || timeoutSec < 0) timeoutSec = 10;\r\n\r\n const timeoutMs = Math.floor(timeoutSec * 1000);\r\n const start = Date.now();\r\n\r\n while (true) {\r\n if (this.state.ready) return true;\r\n if (Date.now() - start >= timeoutMs) return false;\r\n await sleep(1000);\r\n }\r\n }\r\n\r\n /** Instant read: returns last successful response if <= 60s old, else undefined */\r\n get(): ResilientResponse | undefined {\r\n if (!this.state.last || !this.state.lastOkAt) return undefined;\r\n\r\n const age = Date.now() - this.state.lastOkAt;\r\n if (age > VALID_MS) return undefined;\r\n\r\n return cloneResponse(this.state.last);\r\n }\r\n\r\n snapshot(): InstantGetSnapshot | undefined {\r\n if (!this.state.url) return undefined;\r\n\r\n return {\r\n url: this.state.url,\r\n running: !!this.state.timer,\r\n intervalMs: this.state.intervalMs,\r\n\r\n lastOkAt: this.state.lastOkAt,\r\n lastStatus: this.state.lastStatus,\r\n\r\n lastErrorName: this.state.lastErrorName,\r\n consecutiveErrors: this.state.consecutiveErrors,\r\n\r\n inFlight: this.state.inFlight,\r\n ready: this.state.ready,\r\n nextDelayMs: this.state.nextDelayMs,\r\n };\r\n }\r\n\r\n /* ---------------- internals ---------------- */\r\n\r\n private scheduleNext(delayMs: number): void {\r\n const d = Math.max(0, delayMs);\r\n this.state.nextDelayMs = d;\r\n\r\n if (this.state.timer) clearTimeout(this.state.timer);\r\n this.state.timer = setTimeout(() => {\r\n void this.tick();\r\n }, d);\r\n }\r\n\r\n private async tick(): Promise<void> {\r\n if (!this.state.url) return;\r\n if (!this.state.timer) return; // stopped\r\n\r\n if (this.state.inFlight) {\r\n // should be rare with setTimeout loop, but keep safe\r\n this.scheduleNext(this.state.intervalMs);\r\n return;\r\n }\r\n\r\n this.state.inFlight = true;\r\n\r\n try {\r\n const res = await fetchGet(this.state.url);\r\n\r\n // Only 2xx counts as success\r\n if (res.status < 200 || res.status >= 300) {\r\n const err = new Error(`Non-2xx status=${res.status}`);\r\n (err as any).name = \"InstantGetNon2xxError\";\r\n throw err;\r\n }\r\n\r\n // success: store + mark ready\r\n this.state.last = cloneResponse(res);\r\n this.state.lastOkAt = Date.now();\r\n this.state.lastStatus = res.status;\r\n\r\n this.state.lastErrorName = undefined;\r\n this.state.consecutiveErrors = 0;\r\n\r\n // reset backoff + retry budget on success\r\n this.state.backoffStage = 0;\r\n this.state.retriesRemaining = toRetryCount(this.state.onError);\r\n\r\n // becomes ready after first success\r\n this.state.ready = true;\r\n\r\n // next normal interval\r\n this.scheduleNext(this.state.intervalMs);\r\n } catch (err) {\r\n this.state.lastErrorName = (err as any)?.name ?? \"Error\";\r\n this.state.consecutiveErrors += 1;\r\n\r\n // If onError=stop => stop immediately\r\n if (this.state.onError === \"stop\") {\r\n this.stop();\r\n return;\r\n }\r\n\r\n // Finite retry budget: fixed-interval retries, then stop\r\n if (Number.isFinite(this.state.retriesRemaining)) {\r\n if (this.state.retriesRemaining <= 0) {\r\n this.stop();\r\n return;\r\n }\r\n\r\n this.state.retriesRemaining -= 1;\r\n\r\n if (this.state.retriesRemaining <= 0) {\r\n this.stop();\r\n return;\r\n }\r\n\r\n // fixed retry interval\r\n this.state.backoffStage = 0;\r\n this.scheduleNext(this.state.intervalMs);\r\n return;\r\n }\r\n\r\n // Infinite retry uses backoff schedule\r\n const delay = nextBackoffDelayMs(this.state.backoffStage);\r\n this.state.backoffStage += 1;\r\n\r\n this.scheduleNext(delay);\r\n } finally {\r\n this.state.inFlight = false;\r\n }\r\n }\r\n}\r\n"],"mappings":"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","names":["sleep"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nextn/outbound-guard",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.cjs",
|
|
6
6
|
"module": "./dist/index.js",
|
|
@@ -9,9 +9,9 @@
|
|
|
9
9
|
"sideEffects": false,
|
|
10
10
|
"exports": {
|
|
11
11
|
".": {
|
|
12
|
-
"types": "./dist/
|
|
13
|
-
"import": "./dist/
|
|
14
|
-
"require": "./dist/
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"import": "./dist/index.js",
|
|
14
|
+
"require": "./dist/index.cjs"
|
|
15
15
|
}
|
|
16
16
|
},
|
|
17
17
|
|