@nextn/outbound-guard 0.1.1 → 0.1.2
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 +0 -7
- package/dist/{src/index.cjs → index.cjs} +283 -103
- package/dist/index.cjs.map +1 -0
- package/dist/{src/index.d.cts → index.d.cts} +33 -21
- package/dist/{src/index.d.ts → index.d.ts} +33 -21
- package/dist/{src/index.js → index.js} +277 -99
- package/dist/index.js.map +1 -0
- package/package.json +1 -1
- package/dist/demo/loadgen.cjs +0 -454
- package/dist/demo/loadgen.cjs.map +0 -1
- package/dist/demo/loadgen.d.cts +0 -2
- package/dist/demo/loadgen.d.ts +0 -2
- package/dist/demo/loadgen.js +0 -452
- package/dist/demo/loadgen.js.map +0 -1
- package/dist/demo/upstream.cjs +0 -58
- package/dist/demo/upstream.cjs.map +0 -1
- package/dist/demo/upstream.d.cts +0 -2
- package/dist/demo/upstream.d.ts +0 -2
- package/dist/demo/upstream.js +0 -34
- package/dist/demo/upstream.js.map +0 -1
- package/dist/src/index.cjs.map +0 -1
- package/dist/src/index.js.map +0 -1
package/README.md
CHANGED
|
@@ -148,15 +148,8 @@ If the upstream is:
|
|
|
148
148
|
This design keeps failure behavior honest.
|
|
149
149
|
|
|
150
150
|
|
|
151
|
-
Got it 👍
|
|
152
|
-
You want **the same words**, just **clean structure + proper Markdown**, copy-paste ready.
|
|
153
|
-
|
|
154
|
-
Below is **exactly your text**, only reorganized into clear sections with headers and spacing.
|
|
155
|
-
No rewording, no meaning changes.
|
|
156
|
-
|
|
157
151
|
---
|
|
158
152
|
|
|
159
|
-
````md
|
|
160
153
|
## How to tune micro-cache safely
|
|
161
154
|
|
|
162
155
|
Most users only need to understand **two knobs**.
|
|
@@ -18,16 +18,18 @@ var __copyProps = (to, from, except, desc) => {
|
|
|
18
18
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
19
|
|
|
20
20
|
// src/index.ts
|
|
21
|
-
var
|
|
22
|
-
__export(
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
HalfOpenRejectedError: () => HalfOpenRejectedError,
|
|
23
24
|
QueueFullError: () => QueueFullError,
|
|
24
25
|
QueueTimeoutError: () => QueueTimeoutError,
|
|
25
26
|
RequestTimeoutError: () => RequestTimeoutError,
|
|
26
27
|
ResilientHttpClient: () => ResilientHttpClient,
|
|
27
28
|
ResilientHttpError: () => ResilientHttpError,
|
|
28
|
-
UpstreamError: () => UpstreamError
|
|
29
|
+
UpstreamError: () => UpstreamError,
|
|
30
|
+
UpstreamUnhealthyError: () => UpstreamUnhealthyError
|
|
29
31
|
});
|
|
30
|
-
module.exports = __toCommonJS(
|
|
32
|
+
module.exports = __toCommonJS(index_exports);
|
|
31
33
|
|
|
32
34
|
// src/client.ts
|
|
33
35
|
var import_node_events = require("events");
|
|
@@ -64,12 +66,24 @@ var UpstreamError = class extends ResilientHttpError {
|
|
|
64
66
|
this.status = status;
|
|
65
67
|
}
|
|
66
68
|
};
|
|
69
|
+
var UpstreamUnhealthyError = class extends ResilientHttpError {
|
|
70
|
+
constructor(baseUrl, state) {
|
|
71
|
+
super(`Upstream is unhealthy (state=${state}, baseUrl=${baseUrl}).`);
|
|
72
|
+
this.baseUrl = baseUrl;
|
|
73
|
+
this.state = state;
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
var HalfOpenRejectedError = class extends ResilientHttpError {
|
|
77
|
+
constructor(baseUrl) {
|
|
78
|
+
super(`Upstream is HALF_OPEN (probe only) for baseUrl=${baseUrl}.`);
|
|
79
|
+
this.baseUrl = baseUrl;
|
|
80
|
+
}
|
|
81
|
+
};
|
|
67
82
|
|
|
68
83
|
// src/limiter.ts
|
|
69
84
|
var ConcurrencyLimiter = class {
|
|
70
85
|
maxInFlight;
|
|
71
86
|
maxQueue;
|
|
72
|
-
enqueueTimeoutMs;
|
|
73
87
|
inFlight = 0;
|
|
74
88
|
queue = [];
|
|
75
89
|
constructor(opts) {
|
|
@@ -79,17 +93,9 @@ var ConcurrencyLimiter = class {
|
|
|
79
93
|
if (!Number.isFinite(opts.maxQueue) || opts.maxQueue < 0) {
|
|
80
94
|
throw new Error(`maxQueue must be >= 0 (got ${opts.maxQueue})`);
|
|
81
95
|
}
|
|
82
|
-
if (!Number.isFinite(opts.enqueueTimeoutMs) || opts.enqueueTimeoutMs <= 0) {
|
|
83
|
-
throw new Error(`enqueueTimeoutMs must be > 0 (got ${opts.enqueueTimeoutMs})`);
|
|
84
|
-
}
|
|
85
96
|
this.maxInFlight = opts.maxInFlight;
|
|
86
97
|
this.maxQueue = opts.maxQueue;
|
|
87
|
-
this.enqueueTimeoutMs = opts.enqueueTimeoutMs;
|
|
88
98
|
}
|
|
89
|
-
/**
|
|
90
|
-
* Acquire a permit. Resolves once you are allowed to proceed.
|
|
91
|
-
* MUST be followed by `release()` exactly once.
|
|
92
|
-
*/
|
|
93
99
|
acquire() {
|
|
94
100
|
if (this.inFlight < this.maxInFlight) {
|
|
95
101
|
this.inFlight += 1;
|
|
@@ -99,32 +105,36 @@ var ConcurrencyLimiter = class {
|
|
|
99
105
|
return Promise.reject(new QueueFullError(this.maxQueue));
|
|
100
106
|
}
|
|
101
107
|
return new Promise((resolve, reject) => {
|
|
102
|
-
|
|
103
|
-
const idx = this.queue.findIndex((w) => w.resolve === resolve);
|
|
104
|
-
if (idx >= 0) {
|
|
105
|
-
const [w] = this.queue.splice(idx, 1);
|
|
106
|
-
clearTimeout(w.timer);
|
|
107
|
-
}
|
|
108
|
-
reject(new QueueTimeoutError(this.enqueueTimeoutMs));
|
|
109
|
-
}, this.enqueueTimeoutMs);
|
|
110
|
-
this.queue.push({ resolve, reject, timer });
|
|
108
|
+
this.queue.push({ resolve, reject });
|
|
111
109
|
});
|
|
112
110
|
}
|
|
113
111
|
/**
|
|
114
|
-
*
|
|
112
|
+
* Acquire without queueing: either start now or fail.
|
|
113
|
+
* Used for HALF_OPEN probes so recovery never waits behind backlog.
|
|
115
114
|
*/
|
|
115
|
+
acquireNoQueue() {
|
|
116
|
+
if (this.inFlight < this.maxInFlight) {
|
|
117
|
+
this.inFlight += 1;
|
|
118
|
+
return Promise.resolve();
|
|
119
|
+
}
|
|
120
|
+
return Promise.reject(new QueueFullError(0));
|
|
121
|
+
}
|
|
116
122
|
release() {
|
|
117
123
|
if (this.inFlight <= 0) {
|
|
118
124
|
throw new Error("release() called when inFlight is already 0");
|
|
119
125
|
}
|
|
120
126
|
const next = this.queue.shift();
|
|
121
127
|
if (next) {
|
|
122
|
-
clearTimeout(next.timer);
|
|
123
128
|
next.resolve();
|
|
124
129
|
return;
|
|
125
130
|
}
|
|
126
131
|
this.inFlight -= 1;
|
|
127
132
|
}
|
|
133
|
+
flush(err) {
|
|
134
|
+
const q = this.queue;
|
|
135
|
+
this.queue = [];
|
|
136
|
+
for (const w of q) w.reject(err);
|
|
137
|
+
}
|
|
128
138
|
snapshot() {
|
|
129
139
|
return {
|
|
130
140
|
inFlight: this.inFlight,
|
|
@@ -182,6 +192,14 @@ function normalizeUrlForKey(rawUrl) {
|
|
|
182
192
|
if (isHttpDefault || isHttpsDefault) u.port = "";
|
|
183
193
|
return u.toString();
|
|
184
194
|
}
|
|
195
|
+
function baseUrlKey(rawUrl) {
|
|
196
|
+
const u = new URL(rawUrl);
|
|
197
|
+
u.hostname = u.hostname.toLowerCase();
|
|
198
|
+
const isHttpDefault = u.protocol === "http:" && u.port === "80";
|
|
199
|
+
const isHttpsDefault = u.protocol === "https:" && u.port === "443";
|
|
200
|
+
if (isHttpDefault || isHttpsDefault) u.port = "";
|
|
201
|
+
return `${u.protocol}//${u.host}`;
|
|
202
|
+
}
|
|
185
203
|
function defaultMicroCacheKeyFn(req) {
|
|
186
204
|
return `GET ${normalizeUrlForKey(req.url)}`;
|
|
187
205
|
}
|
|
@@ -194,16 +212,46 @@ function sleep(ms) {
|
|
|
194
212
|
function clamp(n, lo, hi) {
|
|
195
213
|
return Math.max(lo, Math.min(hi, n));
|
|
196
214
|
}
|
|
215
|
+
function jitterMs(ms) {
|
|
216
|
+
const mult = 0.8 + Math.random() * 0.4;
|
|
217
|
+
return Math.round(ms * mult);
|
|
218
|
+
}
|
|
219
|
+
var SOFT_FAIL_STATUSES = /* @__PURE__ */ new Set([429, 502, 503, 504]);
|
|
220
|
+
function classifyHttpStatus(status) {
|
|
221
|
+
if (status >= 200 && status < 300) return "SUCCESS";
|
|
222
|
+
if (SOFT_FAIL_STATUSES.has(status)) return "SOFT_FAIL";
|
|
223
|
+
return "SUCCESS";
|
|
224
|
+
}
|
|
225
|
+
function computeRates(window) {
|
|
226
|
+
const total = window.length;
|
|
227
|
+
let hard = 0;
|
|
228
|
+
let soft = 0;
|
|
229
|
+
for (const o of window) {
|
|
230
|
+
if (o === "HARD_FAIL") hard += 1;
|
|
231
|
+
else if (o === "SOFT_FAIL") soft += 1;
|
|
232
|
+
}
|
|
233
|
+
return {
|
|
234
|
+
total,
|
|
235
|
+
hard,
|
|
236
|
+
soft,
|
|
237
|
+
hardFailRate: total === 0 ? 0 : hard / total,
|
|
238
|
+
failRate: total === 0 ? 0 : (hard + soft) / total
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
function shouldCountAsHardFail(err) {
|
|
242
|
+
if (err instanceof UpstreamUnhealthyError) return false;
|
|
243
|
+
if (err instanceof HalfOpenRejectedError) return false;
|
|
244
|
+
if (err instanceof QueueFullError) return false;
|
|
245
|
+
if (err instanceof RequestTimeoutError) return true;
|
|
246
|
+
if (err instanceof ResilientHttpError) return false;
|
|
247
|
+
return true;
|
|
248
|
+
}
|
|
197
249
|
var ResilientHttpClient = class extends import_node_events.EventEmitter {
|
|
198
250
|
constructor(opts) {
|
|
199
251
|
super();
|
|
200
252
|
this.opts = opts;
|
|
201
|
-
this.limiter = new ConcurrencyLimiter({
|
|
202
|
-
maxInFlight: opts.maxInFlight,
|
|
203
|
-
maxQueue: opts.maxQueue,
|
|
204
|
-
enqueueTimeoutMs: opts.enqueueTimeoutMs
|
|
205
|
-
});
|
|
206
253
|
this.requestTimeoutMs = opts.requestTimeoutMs;
|
|
254
|
+
this.healthEnabled = opts.health?.enabled ?? true;
|
|
207
255
|
const mc = opts.microCache;
|
|
208
256
|
if (mc?.enabled) {
|
|
209
257
|
const retry = mc.retry ? {
|
|
@@ -226,8 +274,10 @@ var ResilientHttpClient = class extends import_node_events.EventEmitter {
|
|
|
226
274
|
this.inFlight = /* @__PURE__ */ new Map();
|
|
227
275
|
}
|
|
228
276
|
}
|
|
229
|
-
limiter;
|
|
230
277
|
requestTimeoutMs;
|
|
278
|
+
healthEnabled;
|
|
279
|
+
limiters = /* @__PURE__ */ new Map();
|
|
280
|
+
health = /* @__PURE__ */ new Map();
|
|
231
281
|
microCache;
|
|
232
282
|
cache;
|
|
233
283
|
inFlight;
|
|
@@ -237,14 +287,188 @@ var ResilientHttpClient = class extends import_node_events.EventEmitter {
|
|
|
237
287
|
if (this.microCache?.enabled && req.method === "GET" && req.body == null) {
|
|
238
288
|
return this.requestWithMicroCache(req);
|
|
239
289
|
}
|
|
240
|
-
return this.
|
|
290
|
+
return this.execute(req, { allowProbe: false });
|
|
291
|
+
}
|
|
292
|
+
snapshot() {
|
|
293
|
+
let inFlight = 0;
|
|
294
|
+
let queueDepth = 0;
|
|
295
|
+
for (const l of this.limiters.values()) {
|
|
296
|
+
const s = l.snapshot();
|
|
297
|
+
inFlight += s.inFlight;
|
|
298
|
+
queueDepth += s.queueDepth;
|
|
299
|
+
}
|
|
300
|
+
return { inFlight, queueDepth };
|
|
301
|
+
}
|
|
302
|
+
/* ---------------- internals ---------------- */
|
|
303
|
+
getLimiter(baseKey) {
|
|
304
|
+
let l = this.limiters.get(baseKey);
|
|
305
|
+
if (!l) {
|
|
306
|
+
l = new ConcurrencyLimiter({
|
|
307
|
+
maxInFlight: this.opts.maxInFlight,
|
|
308
|
+
maxQueue: this.opts.maxInFlight * 10
|
|
309
|
+
// hidden factor
|
|
310
|
+
});
|
|
311
|
+
this.limiters.set(baseKey, l);
|
|
312
|
+
}
|
|
313
|
+
return l;
|
|
314
|
+
}
|
|
315
|
+
getHealth(baseKey) {
|
|
316
|
+
let h = this.health.get(baseKey);
|
|
317
|
+
if (!h) {
|
|
318
|
+
h = {
|
|
319
|
+
state: "OPEN",
|
|
320
|
+
window: [],
|
|
321
|
+
windowSize: 20,
|
|
322
|
+
minSamples: 10,
|
|
323
|
+
consecutiveHardFails: 0,
|
|
324
|
+
cooldownBaseMs: 1e3,
|
|
325
|
+
cooldownCapMs: 3e4,
|
|
326
|
+
cooldownMs: 1e3,
|
|
327
|
+
cooldownUntil: 0,
|
|
328
|
+
probeInFlight: false,
|
|
329
|
+
probeRemaining: 0,
|
|
330
|
+
stableNonHard: 0
|
|
331
|
+
};
|
|
332
|
+
this.health.set(baseKey, h);
|
|
333
|
+
}
|
|
334
|
+
return h;
|
|
241
335
|
}
|
|
336
|
+
closeHealth(baseKey, reason) {
|
|
337
|
+
const h = this.getHealth(baseKey);
|
|
338
|
+
if (h.state === "CLOSED") return;
|
|
339
|
+
h.state = "CLOSED";
|
|
340
|
+
h.cooldownUntil = Date.now() + jitterMs(h.cooldownMs);
|
|
341
|
+
h.cooldownMs = Math.min(h.cooldownMs * 2, h.cooldownCapMs);
|
|
342
|
+
this.getLimiter(baseKey).flush(new UpstreamUnhealthyError(baseKey, "CLOSED"));
|
|
343
|
+
const rates = computeRates(h.window);
|
|
344
|
+
this.emit("health:closed", {
|
|
345
|
+
baseUrl: baseKey,
|
|
346
|
+
reason,
|
|
347
|
+
cooldownMs: h.cooldownUntil - Date.now(),
|
|
348
|
+
hardFailRate: rates.hardFailRate,
|
|
349
|
+
failRate: rates.failRate,
|
|
350
|
+
samples: rates.total
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
halfOpenHealth(baseKey) {
|
|
354
|
+
const h = this.getHealth(baseKey);
|
|
355
|
+
if (h.state !== "CLOSED") return;
|
|
356
|
+
h.state = "HALF_OPEN";
|
|
357
|
+
h.probeInFlight = false;
|
|
358
|
+
h.probeRemaining = 1;
|
|
359
|
+
this.emit("health:half_open", { baseUrl: baseKey });
|
|
360
|
+
}
|
|
361
|
+
openHealth(baseKey) {
|
|
362
|
+
const h = this.getHealth(baseKey);
|
|
363
|
+
h.state = "OPEN";
|
|
364
|
+
h.window = [];
|
|
365
|
+
h.consecutiveHardFails = 0;
|
|
366
|
+
h.probeInFlight = false;
|
|
367
|
+
h.probeRemaining = 0;
|
|
368
|
+
h.stableNonHard = 0;
|
|
369
|
+
this.emit("health:open", { baseUrl: baseKey });
|
|
370
|
+
}
|
|
371
|
+
recordOutcome(baseKey, outcome) {
|
|
372
|
+
const h = this.getHealth(baseKey);
|
|
373
|
+
h.window.push(outcome);
|
|
374
|
+
while (h.window.length > h.windowSize) h.window.shift();
|
|
375
|
+
if (outcome === "HARD_FAIL") h.consecutiveHardFails += 1;
|
|
376
|
+
else h.consecutiveHardFails = 0;
|
|
377
|
+
if (h.state === "OPEN") {
|
|
378
|
+
if (outcome !== "HARD_FAIL") {
|
|
379
|
+
h.stableNonHard += 1;
|
|
380
|
+
if (h.stableNonHard >= 5) {
|
|
381
|
+
h.cooldownMs = h.cooldownBaseMs;
|
|
382
|
+
}
|
|
383
|
+
} else {
|
|
384
|
+
h.stableNonHard = 0;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
if (!this.healthEnabled) return;
|
|
388
|
+
if (h.consecutiveHardFails >= 3) {
|
|
389
|
+
this.closeHealth(baseKey, "3 consecutive hard failures");
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
const rates = computeRates(h.window);
|
|
393
|
+
if (rates.total >= h.minSamples) {
|
|
394
|
+
if (rates.hardFailRate >= 0.3) {
|
|
395
|
+
this.closeHealth(baseKey, "hardFailRate >= 30%");
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
if (rates.failRate >= 0.5) {
|
|
399
|
+
this.closeHealth(baseKey, "failRate >= 50%");
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
async execute(req, opts) {
|
|
405
|
+
const baseKey = baseUrlKey(req.url);
|
|
406
|
+
const h = this.getHealth(baseKey);
|
|
407
|
+
const limiter = this.getLimiter(baseKey);
|
|
408
|
+
if (this.healthEnabled) {
|
|
409
|
+
if (h.state === "CLOSED") {
|
|
410
|
+
if (Date.now() >= h.cooldownUntil) {
|
|
411
|
+
this.halfOpenHealth(baseKey);
|
|
412
|
+
} else {
|
|
413
|
+
throw new UpstreamUnhealthyError(baseKey, "CLOSED");
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
if (h.state === "HALF_OPEN") {
|
|
417
|
+
if (!opts.allowProbe) throw new HalfOpenRejectedError(baseKey);
|
|
418
|
+
if (h.probeRemaining <= 0 || h.probeInFlight) throw new HalfOpenRejectedError(baseKey);
|
|
419
|
+
h.probeInFlight = true;
|
|
420
|
+
h.probeRemaining -= 1;
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
const requestId = genRequestId();
|
|
424
|
+
const start = Date.now();
|
|
425
|
+
try {
|
|
426
|
+
if (this.healthEnabled && h.state === "HALF_OPEN") {
|
|
427
|
+
await limiter.acquireNoQueue();
|
|
428
|
+
} else {
|
|
429
|
+
await limiter.acquire();
|
|
430
|
+
}
|
|
431
|
+
} catch (err) {
|
|
432
|
+
this.emit("request:rejected", { requestId, request: req, error: err });
|
|
433
|
+
throw err;
|
|
434
|
+
}
|
|
435
|
+
this.emit("request:start", { requestId, request: req });
|
|
436
|
+
try {
|
|
437
|
+
const res = await doHttpRequest(req, this.requestTimeoutMs);
|
|
438
|
+
const durationMs = Date.now() - start;
|
|
439
|
+
const outcome = classifyHttpStatus(res.status);
|
|
440
|
+
this.recordOutcome(baseKey, outcome);
|
|
441
|
+
if (this.healthEnabled && h.state === "HALF_OPEN") {
|
|
442
|
+
this.emit("health:probe", { baseUrl: baseKey, outcome, status: res.status });
|
|
443
|
+
if (res.status >= 200 && res.status < 300) {
|
|
444
|
+
this.openHealth(baseKey);
|
|
445
|
+
} else {
|
|
446
|
+
this.closeHealth(baseKey, `probe failed status=${res.status}`);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
this.emit("request:success", { requestId, request: req, status: res.status, durationMs });
|
|
450
|
+
return res;
|
|
451
|
+
} catch (err) {
|
|
452
|
+
const durationMs = Date.now() - start;
|
|
453
|
+
if (shouldCountAsHardFail(err)) {
|
|
454
|
+
this.recordOutcome(baseKey, "HARD_FAIL");
|
|
455
|
+
}
|
|
456
|
+
if (this.healthEnabled && h.state === "HALF_OPEN") {
|
|
457
|
+
this.emit("health:probe", { baseUrl: baseKey, outcome: "HARD_FAIL", error: err });
|
|
458
|
+
this.closeHealth(baseKey, "probe hard failure");
|
|
459
|
+
}
|
|
460
|
+
this.emit("request:failure", { requestId, request: req, error: err, durationMs });
|
|
461
|
+
throw err;
|
|
462
|
+
} finally {
|
|
463
|
+
if (this.healthEnabled && h.state === "HALF_OPEN") {
|
|
464
|
+
h.probeInFlight = false;
|
|
465
|
+
}
|
|
466
|
+
limiter.release();
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
/* ---------------- microcache ---------------- */
|
|
242
470
|
cloneResponse(res) {
|
|
243
|
-
return {
|
|
244
|
-
status: res.status,
|
|
245
|
-
headers: { ...res.headers },
|
|
246
|
-
body: new Uint8Array(res.body)
|
|
247
|
-
};
|
|
471
|
+
return { status: res.status, headers: { ...res.headers }, body: new Uint8Array(res.body) };
|
|
248
472
|
}
|
|
249
473
|
maybeCleanupExpired(cache, maxStaleMs) {
|
|
250
474
|
this.microCacheReqCount++;
|
|
@@ -273,21 +497,15 @@ var ResilientHttpClient = class extends import_node_events.EventEmitter {
|
|
|
273
497
|
async fetchWithLeaderRetry(req) {
|
|
274
498
|
const mc = this.microCache;
|
|
275
499
|
const retry = mc.retry;
|
|
276
|
-
if (!retry) return this.
|
|
500
|
+
if (!retry) return this.execute(req, { allowProbe: false });
|
|
277
501
|
const { maxAttempts, baseDelayMs, maxDelayMs, retryOnStatus } = retry;
|
|
278
502
|
let last;
|
|
279
503
|
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
280
|
-
const res = await this.
|
|
504
|
+
const res = await this.execute(req, { allowProbe: false });
|
|
281
505
|
last = res;
|
|
282
506
|
if (this.isRetryableStatus(res.status, retryOnStatus) && attempt < maxAttempts) {
|
|
283
507
|
const delay = this.computeBackoffMs(attempt, baseDelayMs, maxDelayMs);
|
|
284
|
-
this.emit("microcache:retry", {
|
|
285
|
-
url: req.url,
|
|
286
|
-
attempt,
|
|
287
|
-
maxAttempts,
|
|
288
|
-
reason: `status ${res.status}`,
|
|
289
|
-
delayMs: delay
|
|
290
|
-
});
|
|
508
|
+
this.emit("microcache:retry", { url: req.url, attempt, maxAttempts, reason: `status ${res.status}`, delayMs: delay });
|
|
291
509
|
await sleep(delay);
|
|
292
510
|
continue;
|
|
293
511
|
}
|
|
@@ -295,16 +513,6 @@ var ResilientHttpClient = class extends import_node_events.EventEmitter {
|
|
|
295
513
|
}
|
|
296
514
|
return last;
|
|
297
515
|
}
|
|
298
|
-
/**
|
|
299
|
-
* Window behavior:
|
|
300
|
-
* - 0..ttlMs: return cache (fresh)
|
|
301
|
-
* - ttlMs..maxStaleMs: leader refreshes; others get old value until replaced (stale-while-revalidate)
|
|
302
|
-
* - >maxStaleMs: do not serve old; behave like no cache
|
|
303
|
-
*
|
|
304
|
-
* Follower controls (only when no cache is served):
|
|
305
|
-
* - maxWaiters: cap concurrent followers joining the leader
|
|
306
|
-
* - followerTimeoutMs: shared "join window" from first follower; after it expires, late followers fail fast until leader completes
|
|
307
|
-
*/
|
|
308
516
|
async requestWithMicroCache(req) {
|
|
309
517
|
const mc = this.microCache;
|
|
310
518
|
const cache = this.cache;
|
|
@@ -313,20 +521,23 @@ var ResilientHttpClient = class extends import_node_events.EventEmitter {
|
|
|
313
521
|
const key = mc.keyFn(req);
|
|
314
522
|
const now = Date.now();
|
|
315
523
|
const hit0 = cache.get(key);
|
|
316
|
-
if (hit0 && now - hit0.createdAt > mc.maxStaleMs)
|
|
317
|
-
cache.delete(key);
|
|
318
|
-
}
|
|
524
|
+
if (hit0 && now - hit0.createdAt > mc.maxStaleMs) cache.delete(key);
|
|
319
525
|
const hit = cache.get(key);
|
|
320
|
-
if (hit && now < hit.expiresAt)
|
|
321
|
-
|
|
526
|
+
if (hit && now < hit.expiresAt) return this.cloneResponse(hit.value);
|
|
527
|
+
if (this.healthEnabled) {
|
|
528
|
+
const baseKey = baseUrlKey(req.url);
|
|
529
|
+
const h = this.getHealth(baseKey);
|
|
530
|
+
if (h.state === "CLOSED") {
|
|
531
|
+
const staleAllowed = !!hit && now - hit.createdAt <= mc.maxStaleMs;
|
|
532
|
+
if (staleAllowed) return this.cloneResponse(hit.value);
|
|
533
|
+
throw new UpstreamUnhealthyError(baseKey, "CLOSED");
|
|
534
|
+
}
|
|
322
535
|
}
|
|
323
536
|
const group = inFlight.get(key);
|
|
324
537
|
if (group) {
|
|
325
538
|
const h = cache.get(key);
|
|
326
539
|
const staleAllowed = !!h && now - h.createdAt <= mc.maxStaleMs;
|
|
327
|
-
if (h && staleAllowed)
|
|
328
|
-
return this.cloneResponse(h.value);
|
|
329
|
-
}
|
|
540
|
+
if (h && staleAllowed) return this.cloneResponse(h.value);
|
|
330
541
|
const age = now - group.windowStartMs;
|
|
331
542
|
if (age > mc.followerTimeoutMs) {
|
|
332
543
|
const err = new Error(`Follower window closed for key=${key}`);
|
|
@@ -349,24 +560,18 @@ var ResilientHttpClient = class extends import_node_events.EventEmitter {
|
|
|
349
560
|
const prev = cache.get(key);
|
|
350
561
|
const prevStaleAllowed = !!prev && now - prev.createdAt <= mc.maxStaleMs;
|
|
351
562
|
const promise = (async () => {
|
|
352
|
-
const
|
|
563
|
+
const baseKey = baseUrlKey(req.url);
|
|
564
|
+
const h = this.getHealth(baseKey);
|
|
565
|
+
const allowProbe = this.healthEnabled && h.state === "HALF_OPEN";
|
|
566
|
+
const res = allowProbe ? await this.execute(req, { allowProbe: true }) : await this.fetchWithLeaderRetry(req);
|
|
353
567
|
if (res.status >= 200 && res.status < 300) {
|
|
354
568
|
this.evictIfNeeded(cache, mc.maxEntries);
|
|
355
569
|
const t = Date.now();
|
|
356
|
-
cache.set(key, {
|
|
357
|
-
value: this.cloneResponse(res),
|
|
358
|
-
createdAt: t,
|
|
359
|
-
expiresAt: t + mc.ttlMs
|
|
360
|
-
});
|
|
570
|
+
cache.set(key, { value: this.cloneResponse(res), createdAt: t, expiresAt: t + mc.ttlMs });
|
|
361
571
|
}
|
|
362
572
|
return res;
|
|
363
573
|
})();
|
|
364
|
-
|
|
365
|
-
promise,
|
|
366
|
-
windowStartMs: Date.now(),
|
|
367
|
-
waiters: 0
|
|
368
|
-
};
|
|
369
|
-
inFlight.set(key, newGroup);
|
|
574
|
+
inFlight.set(key, { promise, windowStartMs: Date.now(), waiters: 0 });
|
|
370
575
|
try {
|
|
371
576
|
const res = await promise;
|
|
372
577
|
if (!(res.status >= 200 && res.status < 300) && prev && prevStaleAllowed) {
|
|
@@ -383,41 +588,16 @@ var ResilientHttpClient = class extends import_node_events.EventEmitter {
|
|
|
383
588
|
inFlight.delete(key);
|
|
384
589
|
}
|
|
385
590
|
}
|
|
386
|
-
async existingPipeline(req) {
|
|
387
|
-
const requestId = genRequestId();
|
|
388
|
-
try {
|
|
389
|
-
await this.limiter.acquire();
|
|
390
|
-
} catch (err) {
|
|
391
|
-
this.emit("request:rejected", { requestId, request: req, error: err });
|
|
392
|
-
throw err;
|
|
393
|
-
}
|
|
394
|
-
const start = Date.now();
|
|
395
|
-
this.emit("request:start", { requestId, request: req });
|
|
396
|
-
try {
|
|
397
|
-
const res = await doHttpRequest(req, this.requestTimeoutMs);
|
|
398
|
-
const durationMs = Date.now() - start;
|
|
399
|
-
this.emit("request:success", { requestId, request: req, status: res.status, durationMs });
|
|
400
|
-
return res;
|
|
401
|
-
} catch (err) {
|
|
402
|
-
const durationMs = Date.now() - start;
|
|
403
|
-
this.emit("request:failure", { requestId, request: req, error: err, durationMs });
|
|
404
|
-
throw err;
|
|
405
|
-
} finally {
|
|
406
|
-
this.limiter.release();
|
|
407
|
-
}
|
|
408
|
-
}
|
|
409
|
-
snapshot() {
|
|
410
|
-
const s = this.limiter.snapshot();
|
|
411
|
-
return { inFlight: s.inFlight, queueDepth: s.queueDepth };
|
|
412
|
-
}
|
|
413
591
|
};
|
|
414
592
|
// Annotate the CommonJS export names for ESM import in node:
|
|
415
593
|
0 && (module.exports = {
|
|
594
|
+
HalfOpenRejectedError,
|
|
416
595
|
QueueFullError,
|
|
417
596
|
QueueTimeoutError,
|
|
418
597
|
RequestTimeoutError,
|
|
419
598
|
ResilientHttpClient,
|
|
420
599
|
ResilientHttpError,
|
|
421
|
-
UpstreamError
|
|
600
|
+
UpstreamError,
|
|
601
|
+
UpstreamUnhealthyError
|
|
422
602
|
});
|
|
423
603
|
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +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\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 } 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 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"]}
|