brass-runtime 1.17.0 → 1.18.1
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 +36 -3
- package/dist/agent/cli/main.cjs +31 -32
- package/dist/agent/cli/main.js +3 -4
- package/dist/agent/cli/main.mjs +3 -4
- package/dist/agent/index.cjs +4 -5
- package/dist/agent/index.d.ts +1 -1
- package/dist/agent/index.js +3 -4
- package/dist/agent/index.mjs +3 -4
- package/dist/{chunk-7X3K5RMS.js → chunk-22HZQG5F.js} +9 -11
- package/dist/{chunk-GLE2WY7Z.cjs → chunk-2JHJ4YHS.cjs} +417 -124
- package/dist/{chunk-Q2I37RP3.cjs → chunk-2OW6IFY2.cjs} +44 -323
- package/dist/{chunk-7ZPEZ57L.cjs → chunk-5LC7V2OZ.cjs} +18 -20
- package/dist/{chunk-AGR5B2BC.cjs → chunk-5RZ7YITF.cjs} +564 -12
- package/dist/{chunk-DNFJLJMW.mjs → chunk-6MLAZPBL.mjs} +48 -24
- package/dist/{chunk-EJ6BPYVR.mjs → chunk-6V2AWT4R.mjs} +1 -1
- package/dist/{chunk-3AYM6WPJ.js → chunk-7DU7IQHK.js} +20 -299
- package/dist/{chunk-SK7UZRNI.mjs → chunk-7GBJYOX7.mjs} +528 -23
- package/dist/chunk-7TKI527D.cjs +123 -0
- package/dist/{chunk-52OB2ROS.js → chunk-7VQLEN37.js} +2 -4
- package/dist/{chunk-KH4SYAOS.mjs → chunk-B5FKOLTB.mjs} +20 -299
- package/dist/{chunk-FHQGHPMO.mjs → chunk-BC6Q6BCO.mjs} +2 -4
- package/dist/{chunk-4P2HHGAX.mjs → chunk-COOW7BJX.mjs} +32 -11
- package/dist/{chunk-2HQTDLHF.mjs → chunk-EEN5OTCR.mjs} +555 -3
- package/dist/{chunk-KZJQ723N.cjs → chunk-EICAJDNX.cjs} +13 -15
- package/dist/chunk-ELIECDYN.cjs +33 -0
- package/dist/{chunk-GYM3LLGS.mjs → chunk-H626ZTDZ.mjs} +399 -106
- package/dist/{chunk-C3MDXTRZ.js → chunk-HCJ4S3YB.js} +48 -24
- package/dist/{chunk-7JIJOVCT.js → chunk-IPSMXUWA.js} +2 -4
- package/dist/{chunk-4ROBZFL6.cjs → chunk-J6DUHITE.cjs} +6 -8
- package/dist/{chunk-6RY2FFN4.mjs → chunk-JWIEMBE6.mjs} +9 -11
- package/dist/{chunk-PD4EJTQC.cjs → chunk-KNTJ7FQB.cjs} +5 -5
- package/dist/chunk-KTGDLBLD.mjs +123 -0
- package/dist/chunk-LSYQ3C2M.js +33 -0
- package/dist/{chunk-RKGKFN2A.js → chunk-OW5VHAOE.js} +1 -1
- package/dist/{chunk-EOC4UHBS.mjs → chunk-RBHNOKH4.mjs} +2 -2
- package/dist/{chunk-6IXXWIUM.js → chunk-S4HXADU4.js} +555 -3
- package/dist/{chunk-FH2X7BVP.js → chunk-TTSPIU3U.js} +399 -106
- package/dist/{chunk-5QC7LRZ3.js → chunk-UAKAF32U.js} +2 -2
- package/dist/{chunk-CZIVE6NT.cjs → chunk-UUMKZJRJ.cjs} +48 -24
- package/dist/{chunk-MBEJI5HF.mjs → chunk-WCBNXPN6.mjs} +2 -4
- package/dist/{chunk-52PPNNI4.cjs → chunk-WGE2FEZE.cjs} +2 -2
- package/dist/{chunk-WBGRHGBP.cjs → chunk-WI7GZF3B.cjs} +114 -93
- package/dist/chunk-WUDHOZIH.js +6234 -0
- package/dist/{chunk-F6XWZQY4.cjs → chunk-WVSZOPGQ.cjs} +583 -78
- package/dist/chunk-XPIMJQYS.cjs +6234 -0
- package/dist/{chunk-VWIPB6I5.js → chunk-YGR2IN4R.js} +528 -23
- package/dist/chunk-YM3EDNYD.js +123 -0
- package/dist/chunk-YWLLH27R.mjs +33 -0
- package/dist/{chunk-BKK77SBA.js → chunk-YZ5LQ32F.js} +32 -11
- package/dist/chunk-Z3ZZMQUZ.mjs +6234 -0
- package/dist/core/index.cjs +37 -9
- package/dist/core/index.d.ts +19 -152
- package/dist/core/index.js +86 -58
- package/dist/core/index.mjs +86 -58
- package/dist/defaultClient-Cid0JoUR.d.ts +1648 -0
- package/dist/{effect-DIUHZ9IN.d.ts → effect-DnGUuhw6.d.ts} +22 -1
- package/dist/http/index.cjs +206 -59
- package/dist/http/index.d.ts +55 -819
- package/dist/http/index.js +220 -73
- package/dist/http/index.mjs +220 -73
- package/dist/http/testing.cjs +31 -10
- package/dist/http/testing.d.ts +16 -5
- package/dist/http/testing.js +29 -8
- package/dist/http/testing.mjs +29 -8
- package/dist/index.cjs +116 -88
- package/dist/index.d.ts +9 -8
- package/dist/index.js +87 -59
- package/dist/index.mjs +87 -59
- package/dist/{schedule-CK3Ml_7p.d.ts → layer-D2LFcBVx.d.ts} +176 -2
- package/dist/observability/index.cjs +20 -7
- package/dist/observability/index.d.ts +32 -8
- package/dist/observability/index.js +19 -6
- package/dist/observability/index.mjs +19 -6
- package/dist/perf/cli.cjs +26 -28
- package/dist/perf/cli.js +11 -13
- package/dist/perf/cli.mjs +11 -13
- package/dist/perf/index.cjs +13 -15
- package/dist/perf/index.js +11 -13
- package/dist/perf/index.mjs +11 -13
- package/dist/schema/index.cjs +2 -2
- package/dist/schema/index.js +1 -1
- package/dist/schema/index.mjs +1 -1
- package/dist/{server-D6JZ15_e.d.ts → server-Bf1zNYZk.d.ts} +5 -5
- package/dist/{stream-B4oK9JFP.d.ts → stream-I7bkvF7a.d.ts} +1 -1
- package/dist/{tracer-Hwt1cl7h.d.ts → tracer-DF83nLn6.d.ts} +2 -2
- package/dist/{tracing-DqbTKGcf.d.ts → tracing-CWV4gT0u.d.ts} +1 -1
- package/docs/README.md +2 -0
- package/docs/ai/PUBLIC_API.md +28 -7
- package/docs/articles/brass-runtime-http-observability.md +467 -0
- package/docs/frameworks/angular.md +51 -0
- package/docs/frameworks/express.md +58 -0
- package/docs/frameworks/fastify.md +49 -0
- package/docs/frameworks/nestjs.md +53 -0
- package/docs/frameworks/nextjs.md +55 -0
- package/docs/frameworks/react.md +44 -0
- package/docs/frameworks/vanilla.md +56 -0
- package/docs/guides/layers.md +130 -0
- package/docs/http-recipes.md +31 -1
- package/docs/http.md +50 -1
- package/docs/observability.md +132 -0
- package/docs/performance-profiler.md +6 -2
- package/docs/recipes/layers.md +46 -2
- package/docs/recipes/testing.md +25 -0
- package/package.json +6 -2
- package/dist/chunk-3LOYJFRR.cjs +0 -300
- package/dist/chunk-3Y2RIUMM.js +0 -300
- package/dist/chunk-5EC274J5.cjs +0 -2874
- package/dist/chunk-5VRJNBLZ.mjs +0 -2874
- package/dist/chunk-62AZW6UT.cjs +0 -313
- package/dist/chunk-74ZTY6CP.js +0 -2871
- package/dist/chunk-7CMJS3QE.mjs +0 -2871
- package/dist/chunk-A2OM6NEH.mjs +0 -194
- package/dist/chunk-B33ICAKP.js +0 -313
- package/dist/chunk-JF5WGYJJ.cjs +0 -194
- package/dist/chunk-KN32XNTH.mjs +0 -313
- package/dist/chunk-KQLYONSE.cjs +0 -2871
- package/dist/chunk-L2SYFEBS.js +0 -194
- package/dist/chunk-MIIYDLGM.js +0 -2874
- package/dist/chunk-PWC3RBQE.mjs +0 -300
- package/dist/client-CZHU674n.d.ts +0 -820
package/dist/chunk-MIIYDLGM.js
DELETED
|
@@ -1,2874 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
registerHttpEffect
|
|
3
|
-
} from "./chunk-TRM4JUZQ.js";
|
|
4
|
-
import {
|
|
5
|
-
streamFromReadableStream
|
|
6
|
-
} from "./chunk-7JIJOVCT.js";
|
|
7
|
-
import {
|
|
8
|
-
makeScheduleDriver,
|
|
9
|
-
sleep
|
|
10
|
-
} from "./chunk-6IXXWIUM.js";
|
|
11
|
-
import {
|
|
12
|
-
getHttpRequestPolicy,
|
|
13
|
-
isExternalAbortError,
|
|
14
|
-
isHttpError,
|
|
15
|
-
isRetryableHttpError,
|
|
16
|
-
toHttpError,
|
|
17
|
-
withHttpRequestPolicy
|
|
18
|
-
} from "./chunk-3Y2RIUMM.js";
|
|
19
|
-
import {
|
|
20
|
-
fromPromiseAbortable,
|
|
21
|
-
resolveWasmModule
|
|
22
|
-
} from "./chunk-FH2X7BVP.js";
|
|
23
|
-
import {
|
|
24
|
-
Cause,
|
|
25
|
-
asyncEffect,
|
|
26
|
-
asyncFail,
|
|
27
|
-
asyncFlatMap,
|
|
28
|
-
asyncFold,
|
|
29
|
-
asyncSucceed
|
|
30
|
-
} from "./chunk-UB4B6OFY.js";
|
|
31
|
-
import {
|
|
32
|
-
Schema,
|
|
33
|
-
parseConfig
|
|
34
|
-
} from "./chunk-C3MDXTRZ.js";
|
|
35
|
-
|
|
36
|
-
// src/http/retry/wasmRetryPlanner.ts
|
|
37
|
-
var WasmRetryPlannerBridge = class {
|
|
38
|
-
planner;
|
|
39
|
-
constructor(Ctor) {
|
|
40
|
-
this.planner = new Ctor();
|
|
41
|
-
}
|
|
42
|
-
start(options) {
|
|
43
|
-
return this.planner.start(
|
|
44
|
-
options.nowMs,
|
|
45
|
-
options.maxRetries,
|
|
46
|
-
options.baseDelayMs,
|
|
47
|
-
options.maxDelayMs,
|
|
48
|
-
options.maxElapsedMs ?? -1,
|
|
49
|
-
BigInt(this.seed())
|
|
50
|
-
);
|
|
51
|
-
}
|
|
52
|
-
nextDelayMs(retryId, options) {
|
|
53
|
-
const delay = this.planner.next_delay_ms(retryId, options.nowMs, options.retryable, options.retryAfterMs ?? -1);
|
|
54
|
-
return delay < 0 ? void 0 : delay;
|
|
55
|
-
}
|
|
56
|
-
drop(retryId) {
|
|
57
|
-
this.planner.drop_state(retryId);
|
|
58
|
-
}
|
|
59
|
-
stats() {
|
|
60
|
-
return {
|
|
61
|
-
live: this.planner.metric_u64(0),
|
|
62
|
-
planned: this.planner.metric_u64(1),
|
|
63
|
-
exhausted: this.planner.metric_u64(2),
|
|
64
|
-
dropped: this.planner.metric_u64(3)
|
|
65
|
-
};
|
|
66
|
-
}
|
|
67
|
-
seed() {
|
|
68
|
-
return Math.floor(Math.random() * Number.MAX_SAFE_INTEGER);
|
|
69
|
-
}
|
|
70
|
-
};
|
|
71
|
-
function makeWasmRetryPlanner() {
|
|
72
|
-
const mod = resolveWasmModule();
|
|
73
|
-
const Ctor = mod?.BrassWasmRetryPlanner;
|
|
74
|
-
if (!Ctor) throw new Error("brass-runtime wasm retry planner is not available. Run npm run build:wasm first.");
|
|
75
|
-
return new WasmRetryPlannerBridge(Ctor);
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
// src/http/retry/retry.ts
|
|
79
|
-
var defaultRetryableMethods = ["GET", "HEAD", "OPTIONS"];
|
|
80
|
-
var defaultRetryOnStatus = (s) => s === 408 || s === 429 || s === 500 || s === 502 || s === 503 || s === 504;
|
|
81
|
-
var defaultRetryOnError = (e) => isRetryableHttpError(e, { retryOnStatus: defaultRetryOnStatus });
|
|
82
|
-
var clamp = (n, min, max) => Math.max(min, Math.min(max, n));
|
|
83
|
-
var backoffDelayMs = (attempt, base, cap) => {
|
|
84
|
-
const b = Math.max(0, base);
|
|
85
|
-
const c = Math.max(0, cap);
|
|
86
|
-
const exp = b * Math.pow(2, attempt);
|
|
87
|
-
const lim = clamp(exp, 0, c);
|
|
88
|
-
return Math.floor(Math.random() * lim);
|
|
89
|
-
};
|
|
90
|
-
var headerCI = (h, name) => {
|
|
91
|
-
const k = Object.keys(h).find((x) => x.toLowerCase() === name.toLowerCase());
|
|
92
|
-
return k ? h[k] : void 0;
|
|
93
|
-
};
|
|
94
|
-
var retryAfterMs = (headers) => {
|
|
95
|
-
const v = headerCI(headers, "retry-after")?.trim();
|
|
96
|
-
if (!v) return void 0;
|
|
97
|
-
const secs = Number(v);
|
|
98
|
-
if (Number.isFinite(secs)) return Math.max(0, Math.floor(secs * 1e3));
|
|
99
|
-
const t = Date.parse(v);
|
|
100
|
-
if (Number.isFinite(t)) return Math.max(0, t - Date.now());
|
|
101
|
-
return void 0;
|
|
102
|
-
};
|
|
103
|
-
var normalizeRetryBudget = (ms) => {
|
|
104
|
-
if (ms === void 0 || !Number.isFinite(ms)) return void 0;
|
|
105
|
-
return Math.max(0, Math.floor(ms));
|
|
106
|
-
};
|
|
107
|
-
var resolveEffectivePolicy = (req, basePolicy) => {
|
|
108
|
-
const override = getHttpRequestPolicy(req).retry;
|
|
109
|
-
if (override === false) return null;
|
|
110
|
-
if (override === void 0) return basePolicy;
|
|
111
|
-
return {
|
|
112
|
-
...basePolicy,
|
|
113
|
-
...override.maxRetries !== void 0 && { maxRetries: override.maxRetries },
|
|
114
|
-
...override.baseDelayMs !== void 0 && { baseDelayMs: override.baseDelayMs },
|
|
115
|
-
...override.maxDelayMs !== void 0 && { maxDelayMs: override.maxDelayMs },
|
|
116
|
-
...override.schedule !== void 0 && { schedule: override.schedule },
|
|
117
|
-
...override.retryOnStatus !== void 0 && { retryOnStatus: override.retryOnStatus }
|
|
118
|
-
};
|
|
119
|
-
};
|
|
120
|
-
var resolveRetryEngine = (p) => {
|
|
121
|
-
if (p.engine !== void 0) {
|
|
122
|
-
if (p.engine === "ts" || p.engine === "wasm") return p.engine;
|
|
123
|
-
throw new Error(`brass-runtime retry engine must be 'ts' or 'wasm'; received '${String(p.engine)}'`);
|
|
124
|
-
}
|
|
125
|
-
if (p.wasm === true) return "wasm";
|
|
126
|
-
if (p.wasm === false) return "ts";
|
|
127
|
-
return "ts";
|
|
128
|
-
};
|
|
129
|
-
var withRetry = (p) => (next) => {
|
|
130
|
-
const retryOnMethods = p.retryOnMethods ?? defaultRetryableMethods;
|
|
131
|
-
const retryEngine = resolveRetryEngine(p);
|
|
132
|
-
const wasmPlanner = retryEngine === "wasm" ? makeWasmRetryPlanner() : void 0;
|
|
133
|
-
const isMethodRetryable = (req) => retryOnMethods.includes(req.method);
|
|
134
|
-
const nextDelay = (ep, epMaxElapsedMs, retryId, attempt, startedAt, retryable, scheduleDriver, input) => {
|
|
135
|
-
if (!retryable) return void 0;
|
|
136
|
-
const remainingBudget = epMaxElapsedMs === void 0 ? Number.POSITIVE_INFINITY : epMaxElapsedMs - (performance.now() - startedAt);
|
|
137
|
-
if (remainingBudget <= 0) return void 0;
|
|
138
|
-
if (scheduleDriver) {
|
|
139
|
-
const decision = scheduleDriver.next(input);
|
|
140
|
-
if (!decision.continue) return void 0;
|
|
141
|
-
const rawDelay2 = input.retryAfterMs === void 0 ? decision.delayMs : Math.min(input.retryAfterMs, ep.maxDelayMs);
|
|
142
|
-
return {
|
|
143
|
-
delayMs: Math.max(0, Math.min(rawDelay2, remainingBudget))
|
|
144
|
-
};
|
|
145
|
-
}
|
|
146
|
-
if (wasmPlanner && retryId !== void 0) {
|
|
147
|
-
const delay = wasmPlanner.nextDelayMs(retryId, {
|
|
148
|
-
nowMs: performance.now(),
|
|
149
|
-
retryable,
|
|
150
|
-
retryAfterMs: input.retryAfterMs
|
|
151
|
-
});
|
|
152
|
-
return delay === void 0 ? void 0 : { delayMs: delay };
|
|
153
|
-
}
|
|
154
|
-
const rawDelay = input.retryAfterMs === void 0 ? backoffDelayMs(attempt, ep.baseDelayMs, ep.maxDelayMs) : Math.min(input.retryAfterMs, ep.maxDelayMs);
|
|
155
|
-
return { delayMs: Math.max(0, Math.min(rawDelay, remainingBudget)) };
|
|
156
|
-
};
|
|
157
|
-
const sleepWithCleanup = (ms, onCancel) => {
|
|
158
|
-
return asyncEffect((_env, cb) => {
|
|
159
|
-
const delay = Math.max(0, Math.floor(ms));
|
|
160
|
-
const id = setTimeout(() => cb({ _tag: "Success", value: void 0 }), delay);
|
|
161
|
-
return () => {
|
|
162
|
-
clearTimeout(id);
|
|
163
|
-
onCancel();
|
|
164
|
-
};
|
|
165
|
-
});
|
|
166
|
-
};
|
|
167
|
-
const loop = (req, attempt, startedAt, retryId, ep, epMaxElapsedMs, epRetryOnStatus, epRetryOnError, originalPriority, safeDrop, scheduleDriver) => {
|
|
168
|
-
if (!isMethodRetryable(req)) return next(req);
|
|
169
|
-
const effectiveReq = attempt > 0 ? withHttpRequestPolicy(req, { priority: Math.max(0, originalPriority - 1) }) : req;
|
|
170
|
-
const remainingBudget = () => epMaxElapsedMs === void 0 ? Number.POSITIVE_INFINITY : epMaxElapsedMs - (performance.now() - startedAt);
|
|
171
|
-
return asyncFold(
|
|
172
|
-
next(effectiveReq),
|
|
173
|
-
(e) => {
|
|
174
|
-
if (e._tag === "Abort" || e._tag === "BadUrl" || e._tag === "PoolRejected" || e._tag === "PoolClosed" || e._tag === "CircuitBreakerOpen") {
|
|
175
|
-
safeDrop(retryId);
|
|
176
|
-
return asyncFail(e);
|
|
177
|
-
}
|
|
178
|
-
const retryable = attempt < ep.maxRetries && epRetryOnError(e) && remainingBudget() > 0;
|
|
179
|
-
const retryDecision = nextDelay(ep, epMaxElapsedMs, retryId, attempt, startedAt, retryable, scheduleDriver, {
|
|
180
|
-
attempt,
|
|
181
|
-
elapsedMs: performance.now() - startedAt,
|
|
182
|
-
request: req,
|
|
183
|
-
error: e
|
|
184
|
-
});
|
|
185
|
-
if (retryDecision === void 0 || retryDecision.delayMs <= 0 && epMaxElapsedMs !== void 0) {
|
|
186
|
-
safeDrop(retryId);
|
|
187
|
-
return asyncFail(e);
|
|
188
|
-
}
|
|
189
|
-
if (ep.onRetry) {
|
|
190
|
-
ep.onRetry({
|
|
191
|
-
attempt,
|
|
192
|
-
delayMs: retryDecision.delayMs,
|
|
193
|
-
error: e,
|
|
194
|
-
status: void 0,
|
|
195
|
-
url: req.url,
|
|
196
|
-
method: req.method,
|
|
197
|
-
timestamp: Date.now()
|
|
198
|
-
});
|
|
199
|
-
}
|
|
200
|
-
return asyncFlatMap(sleepWithCleanup(retryDecision.delayMs, () => safeDrop(retryId)), () => loop(req, attempt + 1, startedAt, retryId, ep, epMaxElapsedMs, epRetryOnStatus, epRetryOnError, originalPriority, safeDrop, scheduleDriver));
|
|
201
|
-
},
|
|
202
|
-
(w) => {
|
|
203
|
-
const retryable = attempt < ep.maxRetries && epRetryOnStatus(w.status) && remainingBudget() > 0;
|
|
204
|
-
const ra = ep.respectRetryAfter === false ? void 0 : retryAfterMs(w.headers);
|
|
205
|
-
const retryDecision = nextDelay(ep, epMaxElapsedMs, retryId, attempt, startedAt, retryable, scheduleDriver, {
|
|
206
|
-
attempt,
|
|
207
|
-
elapsedMs: performance.now() - startedAt,
|
|
208
|
-
request: req,
|
|
209
|
-
status: w.status,
|
|
210
|
-
retryAfterMs: ra
|
|
211
|
-
});
|
|
212
|
-
if (retryDecision === void 0 || retryDecision.delayMs <= 0 && epMaxElapsedMs !== void 0) {
|
|
213
|
-
safeDrop(retryId);
|
|
214
|
-
return asyncSucceed(w);
|
|
215
|
-
}
|
|
216
|
-
if (ep.onRetry) {
|
|
217
|
-
ep.onRetry({
|
|
218
|
-
attempt,
|
|
219
|
-
delayMs: retryDecision.delayMs,
|
|
220
|
-
error: void 0,
|
|
221
|
-
status: w.status,
|
|
222
|
-
url: req.url,
|
|
223
|
-
method: req.method,
|
|
224
|
-
timestamp: Date.now()
|
|
225
|
-
});
|
|
226
|
-
}
|
|
227
|
-
return asyncFlatMap(sleepWithCleanup(retryDecision.delayMs, () => safeDrop(retryId)), () => loop(req, attempt + 1, startedAt, retryId, ep, epMaxElapsedMs, epRetryOnStatus, epRetryOnError, originalPriority, safeDrop, scheduleDriver));
|
|
228
|
-
}
|
|
229
|
-
);
|
|
230
|
-
};
|
|
231
|
-
return (req) => {
|
|
232
|
-
const effectivePolicy = resolveEffectivePolicy(req, p);
|
|
233
|
-
if (effectivePolicy === null) return next(req);
|
|
234
|
-
if (!isMethodRetryable(req)) return next(req);
|
|
235
|
-
const epRetryOnStatus = effectivePolicy.retryOnStatus ?? defaultRetryOnStatus;
|
|
236
|
-
const epRetryOnError = effectivePolicy.retryOnError ?? defaultRetryOnError;
|
|
237
|
-
const epMaxElapsedMs = normalizeRetryBudget(effectivePolicy.maxElapsedMs);
|
|
238
|
-
const originalPriority = getHttpRequestPolicy(req).priority ?? 5;
|
|
239
|
-
const startedAt = performance.now();
|
|
240
|
-
const retryId = wasmPlanner?.start({
|
|
241
|
-
nowMs: startedAt,
|
|
242
|
-
maxRetries: effectivePolicy.maxRetries,
|
|
243
|
-
baseDelayMs: effectivePolicy.baseDelayMs,
|
|
244
|
-
maxDelayMs: effectivePolicy.maxDelayMs,
|
|
245
|
-
maxElapsedMs: epMaxElapsedMs
|
|
246
|
-
});
|
|
247
|
-
let plannerDropped = false;
|
|
248
|
-
const safeDrop = (id) => {
|
|
249
|
-
if (id !== void 0 && !plannerDropped) {
|
|
250
|
-
plannerDropped = true;
|
|
251
|
-
wasmPlanner?.drop(id);
|
|
252
|
-
}
|
|
253
|
-
};
|
|
254
|
-
const scheduleDriver = effectivePolicy.schedule ? makeScheduleDriver(effectivePolicy.schedule, {
|
|
255
|
-
name: effectivePolicy.schedule.name ?? "http.retry",
|
|
256
|
-
startedAtMs: startedAt,
|
|
257
|
-
onDecision: effectivePolicy.onScheduleDecision
|
|
258
|
-
}) : void 0;
|
|
259
|
-
return loop(req, 0, startedAt, retryId, effectivePolicy, epMaxElapsedMs, epRetryOnStatus, epRetryOnError, originalPriority, safeDrop, scheduleDriver);
|
|
260
|
-
};
|
|
261
|
-
};
|
|
262
|
-
|
|
263
|
-
// src/http/wasmPermitPool.ts
|
|
264
|
-
var DECISION_RUN_NOW = 0;
|
|
265
|
-
var DECISION_QUEUED = 1;
|
|
266
|
-
var WasmHttpPermitPoolBridge = class {
|
|
267
|
-
pool;
|
|
268
|
-
keyCache = /* @__PURE__ */ new Map();
|
|
269
|
-
constructor(Ctor, options) {
|
|
270
|
-
this.pool = new Ctor(options.concurrency, options.maxQueue, toU64(options.queueTimeoutMs));
|
|
271
|
-
}
|
|
272
|
-
acquire(key, subjectId, nowMs2 = Date.now()) {
|
|
273
|
-
const keyId = this.internKey(key);
|
|
274
|
-
const decision = this.pool.acquire(subjectId, keyId, toU64(nowMs2));
|
|
275
|
-
const permitId = this.pool.last_permit_id();
|
|
276
|
-
if (decision === DECISION_RUN_NOW) return { kind: "run", keyId, permitId };
|
|
277
|
-
if (decision === DECISION_QUEUED) return { kind: "queued", keyId, permitId };
|
|
278
|
-
return { kind: "rejected", keyId, permitId };
|
|
279
|
-
}
|
|
280
|
-
release(keyId, nowMs2 = Date.now()) {
|
|
281
|
-
const ptr = this.pool.release(keyId, toU64(nowMs2));
|
|
282
|
-
return this.readEvents(ptr, this.pool.permit_events_len());
|
|
283
|
-
}
|
|
284
|
-
cancel(permitId) {
|
|
285
|
-
this.pool.cancel(permitId);
|
|
286
|
-
}
|
|
287
|
-
advanceTime(nowMs2 = Date.now()) {
|
|
288
|
-
const ptr = this.pool.advance_time(toU64(nowMs2));
|
|
289
|
-
return this.readEvents(ptr, this.pool.permit_events_len());
|
|
290
|
-
}
|
|
291
|
-
nextDeadlineMs() {
|
|
292
|
-
return this.pool.next_deadline_ms();
|
|
293
|
-
}
|
|
294
|
-
stats() {
|
|
295
|
-
return {
|
|
296
|
-
running: this.pool.metric_u64(0),
|
|
297
|
-
queued: this.pool.metric_u64(1),
|
|
298
|
-
acquired: this.pool.metric_u64(2),
|
|
299
|
-
released: this.pool.metric_u64(3),
|
|
300
|
-
rejected: this.pool.metric_u64(4),
|
|
301
|
-
queueTimeouts: this.pool.metric_u64(5),
|
|
302
|
-
keys: this.pool.metric_u64(6)
|
|
303
|
-
};
|
|
304
|
-
}
|
|
305
|
-
internKey(key) {
|
|
306
|
-
const normalized = key.trim().slice(0, 160) || "global";
|
|
307
|
-
let id = this.keyCache.get(normalized);
|
|
308
|
-
if (id === void 0) {
|
|
309
|
-
id = this.pool.intern_key(normalized);
|
|
310
|
-
this.keyCache.set(normalized, id);
|
|
311
|
-
}
|
|
312
|
-
return id;
|
|
313
|
-
}
|
|
314
|
-
readEvents(ptr, len) {
|
|
315
|
-
if (ptr === 0 || len <= 1) return [];
|
|
316
|
-
const words = new Uint32Array(this.pool.memory().buffer, ptr, len);
|
|
317
|
-
const count = words[0] >>> 0;
|
|
318
|
-
const out = [];
|
|
319
|
-
for (let i = 0; i < count; i++) {
|
|
320
|
-
const base = 1 + i * 3;
|
|
321
|
-
if (base + 2 >= words.length) break;
|
|
322
|
-
out.push({
|
|
323
|
-
subjectId: words[base] >>> 0,
|
|
324
|
-
permitId: words[base + 1] >>> 0,
|
|
325
|
-
keyId: words[base + 2] >>> 0
|
|
326
|
-
});
|
|
327
|
-
}
|
|
328
|
-
return out;
|
|
329
|
-
}
|
|
330
|
-
};
|
|
331
|
-
function makeWasmHttpPermitPool(options) {
|
|
332
|
-
const mod = resolveWasmModule();
|
|
333
|
-
const Ctor = mod?.BrassWasmHttpPermitPool;
|
|
334
|
-
if (!Ctor) throw new Error("brass-runtime wasm HTTP permit pool is not available. Run npm run build:wasm first.");
|
|
335
|
-
return new WasmHttpPermitPoolBridge(Ctor, options);
|
|
336
|
-
}
|
|
337
|
-
function toU64(value) {
|
|
338
|
-
return BigInt(Math.max(0, Math.floor(value)));
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
// src/http/pool.ts
|
|
342
|
-
var DEFAULT_CONCURRENCY = 64;
|
|
343
|
-
var DEFAULT_MAX_QUEUE = 256;
|
|
344
|
-
var clampInt = (n, fallback, min) => {
|
|
345
|
-
if (n === void 0 || !Number.isFinite(n)) return fallback;
|
|
346
|
-
return Math.max(min, Math.floor(n));
|
|
347
|
-
};
|
|
348
|
-
var queueTimeoutError = (key, timeoutMs) => ({
|
|
349
|
-
_tag: "PoolTimeout",
|
|
350
|
-
key,
|
|
351
|
-
timeoutMs,
|
|
352
|
-
message: `HTTP pool '${key}' did not grant a slot within ${timeoutMs}ms`
|
|
353
|
-
});
|
|
354
|
-
var poolRejectedError = (key, maxQueue) => ({
|
|
355
|
-
_tag: "PoolRejected",
|
|
356
|
-
key,
|
|
357
|
-
limit: maxQueue,
|
|
358
|
-
message: `HTTP pool '${key}' queue is full`
|
|
359
|
-
});
|
|
360
|
-
var abortError = () => ({ _tag: "Abort" });
|
|
361
|
-
function resolveHttpPoolEngine(config) {
|
|
362
|
-
if (config.engine !== void 0) {
|
|
363
|
-
if (config.engine === "ts" || config.engine === "wasm") return config.engine;
|
|
364
|
-
throw new Error(`brass-runtime HTTP pool engine must be 'ts' or 'wasm'; received '${String(config.engine)}'`);
|
|
365
|
-
}
|
|
366
|
-
if (config.wasm === true) return "wasm";
|
|
367
|
-
if (config.wasm === false) return "ts";
|
|
368
|
-
return "ts";
|
|
369
|
-
}
|
|
370
|
-
function resolveHttpPoolKey(resolver, req, url) {
|
|
371
|
-
const custom = getHttpRequestPolicy(req).poolKey?.trim();
|
|
372
|
-
if (custom) return custom.slice(0, 160);
|
|
373
|
-
const r = resolver ?? "origin";
|
|
374
|
-
if (typeof r === "function") return r(req, url).trim().slice(0, 160) || "global";
|
|
375
|
-
if (r === "global") return "global";
|
|
376
|
-
if (r === "host") return url.host;
|
|
377
|
-
return url.origin;
|
|
378
|
-
}
|
|
379
|
-
var HttpConcurrencyPool = class {
|
|
380
|
-
states = /* @__PURE__ */ new Map();
|
|
381
|
-
concurrency;
|
|
382
|
-
maxQueue;
|
|
383
|
-
queueTimeoutMs;
|
|
384
|
-
keyResolver;
|
|
385
|
-
wasm;
|
|
386
|
-
wasmWaiters = /* @__PURE__ */ new Map();
|
|
387
|
-
wasmTimer;
|
|
388
|
-
nextSubjectId = 1;
|
|
389
|
-
constructor(config = {}) {
|
|
390
|
-
this.concurrency = clampInt(config.concurrency, DEFAULT_CONCURRENCY, 1);
|
|
391
|
-
this.maxQueue = clampInt(config.maxQueue, DEFAULT_MAX_QUEUE, 0);
|
|
392
|
-
this.queueTimeoutMs = config.queueTimeoutMs !== void 0 && Number.isFinite(config.queueTimeoutMs) ? Math.max(0, Math.floor(config.queueTimeoutMs)) : void 0;
|
|
393
|
-
this.keyResolver = config.key;
|
|
394
|
-
const engine = resolveHttpPoolEngine(config);
|
|
395
|
-
this.wasm = engine === "wasm" ? makeWasmHttpPermitPool({
|
|
396
|
-
concurrency: this.concurrency,
|
|
397
|
-
maxQueue: this.maxQueue,
|
|
398
|
-
queueTimeoutMs: this.queueTimeoutMs ?? 0
|
|
399
|
-
}) : void 0;
|
|
400
|
-
}
|
|
401
|
-
acquire(key, signal) {
|
|
402
|
-
return this.wasm ? this.acquireWasm(key, signal) : this.acquireJs(key, signal);
|
|
403
|
-
}
|
|
404
|
-
stats() {
|
|
405
|
-
const keys = Array.from(this.states.values()).map((state) => ({
|
|
406
|
-
key: state.key,
|
|
407
|
-
running: state.running,
|
|
408
|
-
queued: state.queue.length,
|
|
409
|
-
concurrency: this.concurrency,
|
|
410
|
-
maxQueue: this.maxQueue,
|
|
411
|
-
acquired: state.acquired,
|
|
412
|
-
released: state.released,
|
|
413
|
-
rejected: state.rejected,
|
|
414
|
-
queueTimeouts: state.queueTimeouts,
|
|
415
|
-
abortedWhileQueued: state.abortedWhileQueued
|
|
416
|
-
})).sort((a, b) => b.running + b.queued - (a.running + a.queued) || a.key.localeCompare(b.key));
|
|
417
|
-
return keys.reduce((acc, key) => ({
|
|
418
|
-
running: acc.running + key.running,
|
|
419
|
-
queued: acc.queued + key.queued,
|
|
420
|
-
acquired: acc.acquired + key.acquired,
|
|
421
|
-
released: acc.released + key.released,
|
|
422
|
-
rejected: acc.rejected + key.rejected,
|
|
423
|
-
queueTimeouts: acc.queueTimeouts + key.queueTimeouts,
|
|
424
|
-
abortedWhileQueued: acc.abortedWhileQueued + key.abortedWhileQueued,
|
|
425
|
-
wasm: this.wasm?.stats(),
|
|
426
|
-
keys: acc.keys.concat(key)
|
|
427
|
-
}), {
|
|
428
|
-
running: 0,
|
|
429
|
-
queued: 0,
|
|
430
|
-
acquired: 0,
|
|
431
|
-
released: 0,
|
|
432
|
-
rejected: 0,
|
|
433
|
-
queueTimeouts: 0,
|
|
434
|
-
abortedWhileQueued: 0,
|
|
435
|
-
...this.wasm ? { wasm: this.wasm.stats() } : {},
|
|
436
|
-
keys: []
|
|
437
|
-
});
|
|
438
|
-
}
|
|
439
|
-
acquireJs(key, signal) {
|
|
440
|
-
const state = this.getState(key);
|
|
441
|
-
if (signal.aborted) return Promise.reject(abortError());
|
|
442
|
-
if (state.running < this.concurrency) {
|
|
443
|
-
state.running++;
|
|
444
|
-
state.acquired++;
|
|
445
|
-
return Promise.resolve(this.makeLease(state));
|
|
446
|
-
}
|
|
447
|
-
if (state.queue.length >= this.maxQueue) {
|
|
448
|
-
state.rejected++;
|
|
449
|
-
return Promise.reject(poolRejectedError(key, this.maxQueue));
|
|
450
|
-
}
|
|
451
|
-
return new Promise((resolve, reject) => {
|
|
452
|
-
const waiter = { signal, resolve, reject };
|
|
453
|
-
const removeWaiter = () => this.removeWaiter(state, waiter);
|
|
454
|
-
const cleanup = () => this.cleanupWaiter(waiter);
|
|
455
|
-
waiter.abort = () => {
|
|
456
|
-
cleanup();
|
|
457
|
-
removeWaiter();
|
|
458
|
-
state.abortedWhileQueued++;
|
|
459
|
-
reject(abortError());
|
|
460
|
-
};
|
|
461
|
-
signal.addEventListener("abort", waiter.abort, { once: true });
|
|
462
|
-
if (this.queueTimeoutMs !== void 0 && this.queueTimeoutMs > 0) {
|
|
463
|
-
waiter.timer = setTimeout(() => {
|
|
464
|
-
cleanup();
|
|
465
|
-
removeWaiter();
|
|
466
|
-
state.queueTimeouts++;
|
|
467
|
-
reject(queueTimeoutError(key, this.queueTimeoutMs));
|
|
468
|
-
}, this.queueTimeoutMs);
|
|
469
|
-
}
|
|
470
|
-
state.queue.push(waiter);
|
|
471
|
-
});
|
|
472
|
-
}
|
|
473
|
-
acquireWasm(key, signal) {
|
|
474
|
-
const wasm = this.wasm;
|
|
475
|
-
const state = this.getState(key);
|
|
476
|
-
if (signal.aborted) return Promise.reject(abortError());
|
|
477
|
-
const subjectId = this.allocateSubjectId();
|
|
478
|
-
const decision = wasm.acquire(key, subjectId);
|
|
479
|
-
if (decision.kind === "run") {
|
|
480
|
-
state.running++;
|
|
481
|
-
state.acquired++;
|
|
482
|
-
return Promise.resolve(this.makeLease(state, decision.keyId));
|
|
483
|
-
}
|
|
484
|
-
if (decision.kind === "rejected") {
|
|
485
|
-
state.rejected++;
|
|
486
|
-
return Promise.reject(poolRejectedError(key, this.maxQueue));
|
|
487
|
-
}
|
|
488
|
-
return new Promise((resolve, reject) => {
|
|
489
|
-
const waiter = { signal, resolve, reject };
|
|
490
|
-
const removeWaiter = () => this.removeWaiter(state, waiter);
|
|
491
|
-
const cleanup = () => this.cleanupWaiter(waiter);
|
|
492
|
-
waiter.abort = () => {
|
|
493
|
-
cleanup();
|
|
494
|
-
removeWaiter();
|
|
495
|
-
wasm.cancel(decision.permitId);
|
|
496
|
-
this.wasmWaiters.delete(decision.permitId);
|
|
497
|
-
state.abortedWhileQueued++;
|
|
498
|
-
reject(abortError());
|
|
499
|
-
};
|
|
500
|
-
signal.addEventListener("abort", waiter.abort, { once: true });
|
|
501
|
-
state.queue.push(waiter);
|
|
502
|
-
this.wasmWaiters.set(decision.permitId, { waiter, state, keyId: decision.keyId });
|
|
503
|
-
this.scheduleWasmTimeoutPump();
|
|
504
|
-
});
|
|
505
|
-
}
|
|
506
|
-
getState(key) {
|
|
507
|
-
const k = key.trim().slice(0, 160) || "global";
|
|
508
|
-
const existing = this.states.get(k);
|
|
509
|
-
if (existing) return existing;
|
|
510
|
-
const created = {
|
|
511
|
-
key: k,
|
|
512
|
-
running: 0,
|
|
513
|
-
queue: [],
|
|
514
|
-
acquired: 0,
|
|
515
|
-
released: 0,
|
|
516
|
-
rejected: 0,
|
|
517
|
-
queueTimeouts: 0,
|
|
518
|
-
abortedWhileQueued: 0
|
|
519
|
-
};
|
|
520
|
-
this.states.set(k, created);
|
|
521
|
-
return created;
|
|
522
|
-
}
|
|
523
|
-
makeLease(state, wasmKeyId) {
|
|
524
|
-
let released = false;
|
|
525
|
-
return {
|
|
526
|
-
key: state.key,
|
|
527
|
-
release: () => {
|
|
528
|
-
if (released) return;
|
|
529
|
-
released = true;
|
|
530
|
-
if (state.running > 0) state.running--;
|
|
531
|
-
state.released++;
|
|
532
|
-
if (this.wasm && wasmKeyId !== void 0) {
|
|
533
|
-
this.handleWasmGrants(this.wasm.release(wasmKeyId));
|
|
534
|
-
this.scheduleWasmTimeoutPump();
|
|
535
|
-
return;
|
|
536
|
-
}
|
|
537
|
-
this.drain(state);
|
|
538
|
-
}
|
|
539
|
-
};
|
|
540
|
-
}
|
|
541
|
-
drain(state) {
|
|
542
|
-
while (state.running < this.concurrency && state.queue.length > 0) {
|
|
543
|
-
const waiter = state.queue.shift();
|
|
544
|
-
this.cleanupWaiter(waiter);
|
|
545
|
-
if (waiter.signal.aborted) {
|
|
546
|
-
state.abortedWhileQueued++;
|
|
547
|
-
waiter.reject(abortError());
|
|
548
|
-
continue;
|
|
549
|
-
}
|
|
550
|
-
state.running++;
|
|
551
|
-
state.acquired++;
|
|
552
|
-
waiter.resolve(this.makeLease(state));
|
|
553
|
-
}
|
|
554
|
-
}
|
|
555
|
-
handleWasmGrants(events) {
|
|
556
|
-
for (const event of events) {
|
|
557
|
-
const pending = this.wasmWaiters.get(event.permitId);
|
|
558
|
-
if (!pending) continue;
|
|
559
|
-
this.wasmWaiters.delete(event.permitId);
|
|
560
|
-
this.cleanupWaiter(pending.waiter);
|
|
561
|
-
this.removeWaiter(pending.state, pending.waiter);
|
|
562
|
-
if (pending.waiter.signal.aborted) {
|
|
563
|
-
pending.state.abortedWhileQueued++;
|
|
564
|
-
pending.waiter.reject(abortError());
|
|
565
|
-
continue;
|
|
566
|
-
}
|
|
567
|
-
pending.state.running++;
|
|
568
|
-
pending.state.acquired++;
|
|
569
|
-
pending.waiter.resolve(this.makeLease(pending.state, event.keyId));
|
|
570
|
-
}
|
|
571
|
-
}
|
|
572
|
-
handleWasmTimeouts(events) {
|
|
573
|
-
for (const event of events) {
|
|
574
|
-
const pending = this.wasmWaiters.get(event.permitId);
|
|
575
|
-
if (!pending) continue;
|
|
576
|
-
this.wasmWaiters.delete(event.permitId);
|
|
577
|
-
this.cleanupWaiter(pending.waiter);
|
|
578
|
-
this.removeWaiter(pending.state, pending.waiter);
|
|
579
|
-
pending.state.queueTimeouts++;
|
|
580
|
-
pending.waiter.reject(queueTimeoutError(pending.state.key, this.queueTimeoutMs ?? 0));
|
|
581
|
-
}
|
|
582
|
-
}
|
|
583
|
-
scheduleWasmTimeoutPump() {
|
|
584
|
-
if (!this.wasm) return;
|
|
585
|
-
if (this.wasmTimer !== void 0) clearTimeout(this.wasmTimer);
|
|
586
|
-
this.wasmTimer = void 0;
|
|
587
|
-
const next = this.wasm.nextDeadlineMs();
|
|
588
|
-
if (!Number.isFinite(next) || next < 0) return;
|
|
589
|
-
const delay = Math.max(0, Math.min(2 ** 31 - 1, Math.floor(next - Date.now())));
|
|
590
|
-
this.wasmTimer = setTimeout(() => {
|
|
591
|
-
this.wasmTimer = void 0;
|
|
592
|
-
if (!this.wasm) return;
|
|
593
|
-
this.handleWasmTimeouts(this.wasm.advanceTime());
|
|
594
|
-
this.scheduleWasmTimeoutPump();
|
|
595
|
-
}, delay);
|
|
596
|
-
if (typeof this.wasmTimer.unref === "function") this.wasmTimer.unref();
|
|
597
|
-
}
|
|
598
|
-
cleanupWaiter(waiter) {
|
|
599
|
-
if (waiter.timer !== void 0) {
|
|
600
|
-
clearTimeout(waiter.timer);
|
|
601
|
-
waiter.timer = void 0;
|
|
602
|
-
}
|
|
603
|
-
if (waiter.abort) {
|
|
604
|
-
waiter.signal.removeEventListener("abort", waiter.abort);
|
|
605
|
-
waiter.abort = void 0;
|
|
606
|
-
}
|
|
607
|
-
}
|
|
608
|
-
removeWaiter(state, waiter) {
|
|
609
|
-
const idx = state.queue.indexOf(waiter);
|
|
610
|
-
if (idx >= 0) state.queue.splice(idx, 1);
|
|
611
|
-
}
|
|
612
|
-
allocateSubjectId() {
|
|
613
|
-
const id = this.nextSubjectId >>> 0;
|
|
614
|
-
this.nextSubjectId = this.nextSubjectId + 1 >>> 0;
|
|
615
|
-
if (this.nextSubjectId === 0) this.nextSubjectId = 1;
|
|
616
|
-
return id === 0 ? this.allocateSubjectId() : id;
|
|
617
|
-
}
|
|
618
|
-
};
|
|
619
|
-
|
|
620
|
-
// src/http/adaptiveLimiter/latencyWindow.ts
|
|
621
|
-
var LatencyWindow = class {
|
|
622
|
-
buffer;
|
|
623
|
-
sorted = [];
|
|
624
|
-
size;
|
|
625
|
-
head = 0;
|
|
626
|
-
count = 0;
|
|
627
|
-
constructor(size) {
|
|
628
|
-
this.size = Math.max(2, Math.floor(size));
|
|
629
|
-
this.buffer = new Array(this.size);
|
|
630
|
-
}
|
|
631
|
-
/**
|
|
632
|
-
* Record a latency sample. Discards non-positive, NaN, and Infinity values.
|
|
633
|
-
* Evicts the oldest sample when the buffer is full.
|
|
634
|
-
*/
|
|
635
|
-
record(latencyMs) {
|
|
636
|
-
if (!Number.isFinite(latencyMs) || latencyMs <= 0) return;
|
|
637
|
-
const evicted = this.buffer[this.head];
|
|
638
|
-
if (evicted !== void 0 && this.count === this.size) {
|
|
639
|
-
this.removeSorted(evicted);
|
|
640
|
-
}
|
|
641
|
-
this.buffer[this.head] = latencyMs;
|
|
642
|
-
this.insertSorted(latencyMs);
|
|
643
|
-
this.head = (this.head + 1) % this.size;
|
|
644
|
-
if (this.count < this.size) this.count++;
|
|
645
|
-
}
|
|
646
|
-
/**
|
|
647
|
-
* Returns the minimum latency in the current window, or undefined if empty.
|
|
648
|
-
*/
|
|
649
|
-
min() {
|
|
650
|
-
return this.sorted[0];
|
|
651
|
-
}
|
|
652
|
-
/**
|
|
653
|
-
* Computes the percentile using the nearest-rank method.
|
|
654
|
-
* Returns undefined if fewer than 2 samples are present.
|
|
655
|
-
* @param p - Percentile value in [0, 100]
|
|
656
|
-
*/
|
|
657
|
-
percentile(p) {
|
|
658
|
-
if (this.count < 2) return void 0;
|
|
659
|
-
const rank = Math.ceil(p / 100 * this.sorted.length);
|
|
660
|
-
return this.sorted[Math.min(rank, this.sorted.length) - 1];
|
|
661
|
-
}
|
|
662
|
-
/**
|
|
663
|
-
* Computes a percentile where newer samples receive exponentially higher
|
|
664
|
-
* weight. A decay of 1 is identical to `percentile`; lower values adapt
|
|
665
|
-
* faster to recent latency shifts.
|
|
666
|
-
*/
|
|
667
|
-
weightedPercentile(p, decay) {
|
|
668
|
-
if (this.count < 2) return void 0;
|
|
669
|
-
if (!Number.isFinite(decay) || decay >= 1) return this.percentile(p);
|
|
670
|
-
const samples = this.samples();
|
|
671
|
-
const weighted = samples.map((value, index) => ({
|
|
672
|
-
value,
|
|
673
|
-
weight: Math.pow(decay, samples.length - 1 - index)
|
|
674
|
-
}));
|
|
675
|
-
weighted.sort((a, b) => a.value - b.value);
|
|
676
|
-
const total = weighted.reduce((sum, sample) => sum + sample.weight, 0);
|
|
677
|
-
const target = Math.max(0, Math.min(100, p)) / 100 * total;
|
|
678
|
-
let cumulative = 0;
|
|
679
|
-
for (const sample of weighted) {
|
|
680
|
-
cumulative += sample.weight;
|
|
681
|
-
if (cumulative >= target) return sample.value;
|
|
682
|
-
}
|
|
683
|
-
return weighted[weighted.length - 1]?.value;
|
|
684
|
-
}
|
|
685
|
-
/** Number of samples currently in the window. */
|
|
686
|
-
get length() {
|
|
687
|
-
return this.count;
|
|
688
|
-
}
|
|
689
|
-
/** Maximum capacity of the window. */
|
|
690
|
-
get capacity() {
|
|
691
|
-
return this.size;
|
|
692
|
-
}
|
|
693
|
-
/** Returns a copy of the current samples (oldest to newest). */
|
|
694
|
-
samples() {
|
|
695
|
-
const result = new Array(this.count);
|
|
696
|
-
for (let i = 0; i < this.count; i++) {
|
|
697
|
-
const idx = (this.head - this.count + i + this.size) % this.size;
|
|
698
|
-
result[i] = this.buffer[idx];
|
|
699
|
-
}
|
|
700
|
-
return result;
|
|
701
|
-
}
|
|
702
|
-
insertSorted(value) {
|
|
703
|
-
const idx = this.lowerBound(value);
|
|
704
|
-
this.sorted.splice(idx, 0, value);
|
|
705
|
-
}
|
|
706
|
-
removeSorted(value) {
|
|
707
|
-
let idx = this.lowerBound(value);
|
|
708
|
-
while (idx < this.sorted.length && this.sorted[idx] === value) {
|
|
709
|
-
this.sorted.splice(idx, 1);
|
|
710
|
-
return;
|
|
711
|
-
}
|
|
712
|
-
}
|
|
713
|
-
lowerBound(value) {
|
|
714
|
-
let lo = 0;
|
|
715
|
-
let hi = this.sorted.length;
|
|
716
|
-
while (lo < hi) {
|
|
717
|
-
const mid = lo + hi >>> 1;
|
|
718
|
-
if (this.sorted[mid] < value) lo = mid + 1;
|
|
719
|
-
else hi = mid;
|
|
720
|
-
}
|
|
721
|
-
return lo;
|
|
722
|
-
}
|
|
723
|
-
};
|
|
724
|
-
|
|
725
|
-
// src/http/adaptiveLimiter/ema.ts
|
|
726
|
-
var EmaComputer = class {
|
|
727
|
-
alpha;
|
|
728
|
-
current = void 0;
|
|
729
|
-
/**
|
|
730
|
-
* @param alpha - Smoothing factor in (0, 1]. Higher values weight recent samples more.
|
|
731
|
-
*/
|
|
732
|
-
constructor(alpha) {
|
|
733
|
-
this.alpha = alpha;
|
|
734
|
-
}
|
|
735
|
-
/**
|
|
736
|
-
* Update the EMA with a new sample and return the new EMA value.
|
|
737
|
-
* On the first sample, the EMA is initialized to that sample.
|
|
738
|
-
*/
|
|
739
|
-
update(sample) {
|
|
740
|
-
if (this.current === void 0) {
|
|
741
|
-
this.current = sample;
|
|
742
|
-
} else {
|
|
743
|
-
this.current = this.alpha * sample + (1 - this.alpha) * this.current;
|
|
744
|
-
}
|
|
745
|
-
return this.current;
|
|
746
|
-
}
|
|
747
|
-
/** Returns the current EMA value, or undefined if no samples have been recorded. */
|
|
748
|
-
get value() {
|
|
749
|
-
return this.current;
|
|
750
|
-
}
|
|
751
|
-
/** Resets the EMA state. */
|
|
752
|
-
reset() {
|
|
753
|
-
this.current = void 0;
|
|
754
|
-
}
|
|
755
|
-
};
|
|
756
|
-
|
|
757
|
-
// src/http/adaptiveLimiter/gradient.ts
|
|
758
|
-
function computeGradient(minLatency, currentLatency) {
|
|
759
|
-
if (currentLatency <= 0) return 1;
|
|
760
|
-
return minLatency / currentLatency;
|
|
761
|
-
}
|
|
762
|
-
function computeNewLimit(currentLimit, gradient, headroom, minBound, maxBound, options = {}) {
|
|
763
|
-
const decreaseThreshold = options.decreaseThreshold ?? 1;
|
|
764
|
-
const increaseThreshold = options.increaseThreshold ?? 1;
|
|
765
|
-
const maxDecreaseRatio = options.maxDecreaseRatio ?? 1;
|
|
766
|
-
let newLimit;
|
|
767
|
-
if (gradient < decreaseThreshold) {
|
|
768
|
-
const rawLimit = Math.floor(currentLimit * gradient);
|
|
769
|
-
const maxDecrease = Math.max(1, Math.floor(currentLimit * maxDecreaseRatio));
|
|
770
|
-
newLimit = Math.max(rawLimit, currentLimit - maxDecrease);
|
|
771
|
-
} else if (gradient >= increaseThreshold) {
|
|
772
|
-
newLimit = currentLimit + headroom;
|
|
773
|
-
} else {
|
|
774
|
-
newLimit = currentLimit;
|
|
775
|
-
}
|
|
776
|
-
return Math.max(minBound, Math.min(maxBound, newLimit));
|
|
777
|
-
}
|
|
778
|
-
|
|
779
|
-
// src/http/adaptiveLimiter/types.ts
|
|
780
|
-
var adaptiveLimiterPresets = Object.freeze({
|
|
781
|
-
conservative: Object.freeze({
|
|
782
|
-
initialLimit: 8,
|
|
783
|
-
minLimit: 2,
|
|
784
|
-
maxLimit: 64,
|
|
785
|
-
maxQueue: 256,
|
|
786
|
-
queueTimeoutMs: 3e4,
|
|
787
|
-
smoothingFactor: 0.3,
|
|
788
|
-
probeInterval: 40,
|
|
789
|
-
probeJitterRatio: 0.2,
|
|
790
|
-
windowSize: 100,
|
|
791
|
-
minSamples: 80,
|
|
792
|
-
baselineStrategy: "p5",
|
|
793
|
-
windowDecayFactor: 0.99,
|
|
794
|
-
errorWeight: 0.2,
|
|
795
|
-
decreaseThreshold: 0.65,
|
|
796
|
-
maxDecreaseRatio: 0.08,
|
|
797
|
-
headroomStrategy: "fixed",
|
|
798
|
-
queueStrategy: "priority",
|
|
799
|
-
queueLoadShedding: "priority-evict",
|
|
800
|
-
rejectionBackoffThreshold: 2,
|
|
801
|
-
rejectionBackoffMs: 150,
|
|
802
|
-
percentile: "p50"
|
|
803
|
-
}),
|
|
804
|
-
balanced: Object.freeze({
|
|
805
|
-
initialLimit: 16,
|
|
806
|
-
minLimit: 4,
|
|
807
|
-
maxLimit: 128,
|
|
808
|
-
maxQueue: 512,
|
|
809
|
-
queueTimeoutMs: 3e4,
|
|
810
|
-
smoothingFactor: 0.35,
|
|
811
|
-
probeInterval: 25,
|
|
812
|
-
probeJitterRatio: 0.2,
|
|
813
|
-
windowSize: 100,
|
|
814
|
-
minSamples: 50,
|
|
815
|
-
baselineStrategy: "p5",
|
|
816
|
-
windowDecayFactor: 0.98,
|
|
817
|
-
errorWeight: 0.25,
|
|
818
|
-
decreaseThreshold: 0.5,
|
|
819
|
-
maxDecreaseRatio: 0.1,
|
|
820
|
-
headroomStrategy: { type: "proportional", ratio: 0.05 },
|
|
821
|
-
queueStrategy: "priority",
|
|
822
|
-
queueLoadShedding: "priority-evict",
|
|
823
|
-
rejectionBackoffThreshold: 3,
|
|
824
|
-
rejectionBackoffMs: 100,
|
|
825
|
-
percentile: "p50"
|
|
826
|
-
}),
|
|
827
|
-
aggressive: Object.freeze({
|
|
828
|
-
initialLimit: 32,
|
|
829
|
-
minLimit: 8,
|
|
830
|
-
maxLimit: 256,
|
|
831
|
-
maxQueue: 1024,
|
|
832
|
-
queueTimeoutMs: 3e4,
|
|
833
|
-
smoothingFactor: 0.35,
|
|
834
|
-
probeInterval: 20,
|
|
835
|
-
probeJitterRatio: 0.2,
|
|
836
|
-
windowSize: 160,
|
|
837
|
-
minSamples: 100,
|
|
838
|
-
baselineStrategy: "p5",
|
|
839
|
-
windowDecayFactor: 0.98,
|
|
840
|
-
errorWeight: 0.25,
|
|
841
|
-
decreaseThreshold: 0.5,
|
|
842
|
-
maxDecreaseRatio: 0.1,
|
|
843
|
-
headroomStrategy: { type: "proportional", ratio: 0.05 },
|
|
844
|
-
queueStrategy: "priority",
|
|
845
|
-
queueLoadShedding: "priority-evict",
|
|
846
|
-
rejectionBackoffThreshold: 3,
|
|
847
|
-
rejectionBackoffMs: 100,
|
|
848
|
-
percentile: "p50"
|
|
849
|
-
})
|
|
850
|
-
});
|
|
851
|
-
function makeAdaptiveLimiterConfig(preset, overrides = {}) {
|
|
852
|
-
return {
|
|
853
|
-
...adaptiveLimiterPresets[preset],
|
|
854
|
-
...overrides,
|
|
855
|
-
preset
|
|
856
|
-
};
|
|
857
|
-
}
|
|
858
|
-
function validateConfig(config) {
|
|
859
|
-
if (config.preset !== void 0 && config.preset !== "conservative" && config.preset !== "balanced" && config.preset !== "aggressive") {
|
|
860
|
-
throw new Error(
|
|
861
|
-
`AdaptiveLimiter: preset must be "conservative", "balanced", or "aggressive", got ${config.preset}`
|
|
862
|
-
);
|
|
863
|
-
}
|
|
864
|
-
if (config.smoothingFactor !== void 0) {
|
|
865
|
-
if (config.smoothingFactor <= 0 || config.smoothingFactor > 1) {
|
|
866
|
-
throw new Error(
|
|
867
|
-
`AdaptiveLimiter: smoothingFactor must be in (0, 1], got ${config.smoothingFactor}`
|
|
868
|
-
);
|
|
869
|
-
}
|
|
870
|
-
}
|
|
871
|
-
if (config.windowSize !== void 0) {
|
|
872
|
-
if (config.windowSize < 2) {
|
|
873
|
-
throw new Error(
|
|
874
|
-
`AdaptiveLimiter: windowSize must be >= 2, got ${config.windowSize}`
|
|
875
|
-
);
|
|
876
|
-
}
|
|
877
|
-
}
|
|
878
|
-
if (config.probeInterval !== void 0) {
|
|
879
|
-
if (config.probeInterval < 1) {
|
|
880
|
-
throw new Error(
|
|
881
|
-
`AdaptiveLimiter: probeInterval must be >= 1, got ${config.probeInterval}`
|
|
882
|
-
);
|
|
883
|
-
}
|
|
884
|
-
}
|
|
885
|
-
if (config.probeJitterRatio !== void 0) {
|
|
886
|
-
if (config.probeJitterRatio < 0 || config.probeJitterRatio > 1) {
|
|
887
|
-
throw new Error(
|
|
888
|
-
`AdaptiveLimiter: probeJitterRatio must be in [0, 1], got ${config.probeJitterRatio}`
|
|
889
|
-
);
|
|
890
|
-
}
|
|
891
|
-
}
|
|
892
|
-
if (config.minSamples !== void 0 && config.minSamples < 1) {
|
|
893
|
-
throw new Error(
|
|
894
|
-
`AdaptiveLimiter: minSamples must be >= 1, got ${config.minSamples}`
|
|
895
|
-
);
|
|
896
|
-
}
|
|
897
|
-
if (config.baselineStrategy !== void 0 && config.baselineStrategy !== "min" && config.baselineStrategy !== "p5" && config.baselineStrategy !== "ema-low") {
|
|
898
|
-
throw new Error(
|
|
899
|
-
`AdaptiveLimiter: baselineStrategy must be "min", "p5", or "ema-low", got ${config.baselineStrategy}`
|
|
900
|
-
);
|
|
901
|
-
}
|
|
902
|
-
if (config.decreaseCooldownSamples !== void 0 && (!Number.isFinite(config.decreaseCooldownSamples) || config.decreaseCooldownSamples < 0)) {
|
|
903
|
-
throw new Error(
|
|
904
|
-
`AdaptiveLimiter: decreaseCooldownSamples must be >= 0, got ${config.decreaseCooldownSamples}`
|
|
905
|
-
);
|
|
906
|
-
}
|
|
907
|
-
if (config.historySize !== void 0 && (!Number.isFinite(config.historySize) || config.historySize < 0)) {
|
|
908
|
-
throw new Error(
|
|
909
|
-
`AdaptiveLimiter: historySize must be >= 0, got ${config.historySize}`
|
|
910
|
-
);
|
|
911
|
-
}
|
|
912
|
-
if (config.windowDecayFactor !== void 0) {
|
|
913
|
-
if (!Number.isFinite(config.windowDecayFactor) || config.windowDecayFactor <= 0 || config.windowDecayFactor > 1) {
|
|
914
|
-
throw new Error(
|
|
915
|
-
`AdaptiveLimiter: windowDecayFactor must be in (0, 1], got ${config.windowDecayFactor}`
|
|
916
|
-
);
|
|
917
|
-
}
|
|
918
|
-
}
|
|
919
|
-
if (config.errorWeight !== void 0) {
|
|
920
|
-
if (!Number.isFinite(config.errorWeight) || config.errorWeight < 0 || config.errorWeight > 1) {
|
|
921
|
-
throw new Error(
|
|
922
|
-
`AdaptiveLimiter: errorWeight must be in [0, 1], got ${config.errorWeight}`
|
|
923
|
-
);
|
|
924
|
-
}
|
|
925
|
-
}
|
|
926
|
-
if (config.errorSmoothingFactor !== void 0) {
|
|
927
|
-
if (!Number.isFinite(config.errorSmoothingFactor) || config.errorSmoothingFactor <= 0 || config.errorSmoothingFactor > 1) {
|
|
928
|
-
throw new Error(
|
|
929
|
-
`AdaptiveLimiter: errorSmoothingFactor must be in (0, 1], got ${config.errorSmoothingFactor}`
|
|
930
|
-
);
|
|
931
|
-
}
|
|
932
|
-
}
|
|
933
|
-
if (config.errorStatusThreshold !== void 0) {
|
|
934
|
-
if (!Number.isFinite(config.errorStatusThreshold) || config.errorStatusThreshold < 100 || config.errorStatusThreshold > 599) {
|
|
935
|
-
throw new Error(
|
|
936
|
-
`AdaptiveLimiter: errorStatusThreshold must be in [100, 599], got ${config.errorStatusThreshold}`
|
|
937
|
-
);
|
|
938
|
-
}
|
|
939
|
-
}
|
|
940
|
-
if (config.queueStrategy !== void 0 && config.queueStrategy !== "fifo" && config.queueStrategy !== "priority") {
|
|
941
|
-
throw new Error(
|
|
942
|
-
`AdaptiveLimiter: queueStrategy must be "fifo" or "priority", got ${config.queueStrategy}`
|
|
943
|
-
);
|
|
944
|
-
}
|
|
945
|
-
if (config.queueLoadShedding !== void 0 && config.queueLoadShedding !== "reject-new" && config.queueLoadShedding !== "priority-evict") {
|
|
946
|
-
throw new Error(
|
|
947
|
-
`AdaptiveLimiter: queueLoadShedding must be "reject-new" or "priority-evict", got ${config.queueLoadShedding}`
|
|
948
|
-
);
|
|
949
|
-
}
|
|
950
|
-
if (config.rejectionBackoffThreshold !== void 0 && (!Number.isFinite(config.rejectionBackoffThreshold) || config.rejectionBackoffThreshold < 1)) {
|
|
951
|
-
throw new Error(
|
|
952
|
-
`AdaptiveLimiter: rejectionBackoffThreshold must be >= 1, got ${config.rejectionBackoffThreshold}`
|
|
953
|
-
);
|
|
954
|
-
}
|
|
955
|
-
if (config.rejectionBackoffMs !== void 0 && (!Number.isFinite(config.rejectionBackoffMs) || config.rejectionBackoffMs < 1)) {
|
|
956
|
-
throw new Error(
|
|
957
|
-
`AdaptiveLimiter: rejectionBackoffMs must be >= 1, got ${config.rejectionBackoffMs}`
|
|
958
|
-
);
|
|
959
|
-
}
|
|
960
|
-
if (config.stateTtlMs !== void 0 && config.stateTtlMs !== false) {
|
|
961
|
-
if (config.stateTtlMs < 1) {
|
|
962
|
-
throw new Error(
|
|
963
|
-
`AdaptiveLimiter: stateTtlMs must be >= 1ms or false, got ${config.stateTtlMs}`
|
|
964
|
-
);
|
|
965
|
-
}
|
|
966
|
-
}
|
|
967
|
-
if (config.warmupRequests !== void 0 && config.warmupRequests < 0) {
|
|
968
|
-
throw new Error(
|
|
969
|
-
`AdaptiveLimiter: warmupRequests must be >= 0, got ${config.warmupRequests}`
|
|
970
|
-
);
|
|
971
|
-
}
|
|
972
|
-
if (config.decreaseThreshold !== void 0) {
|
|
973
|
-
if (config.decreaseThreshold <= 0 || config.decreaseThreshold > 1) {
|
|
974
|
-
throw new Error(
|
|
975
|
-
`AdaptiveLimiter: decreaseThreshold must be in (0, 1], got ${config.decreaseThreshold}`
|
|
976
|
-
);
|
|
977
|
-
}
|
|
978
|
-
}
|
|
979
|
-
if (config.increaseThreshold !== void 0 && config.increaseThreshold < 1) {
|
|
980
|
-
throw new Error(
|
|
981
|
-
`AdaptiveLimiter: increaseThreshold must be >= 1, got ${config.increaseThreshold}`
|
|
982
|
-
);
|
|
983
|
-
}
|
|
984
|
-
if (config.maxDecreaseRatio !== void 0) {
|
|
985
|
-
if (config.maxDecreaseRatio <= 0 || config.maxDecreaseRatio > 1) {
|
|
986
|
-
throw new Error(
|
|
987
|
-
`AdaptiveLimiter: maxDecreaseRatio must be in (0, 1], got ${config.maxDecreaseRatio}`
|
|
988
|
-
);
|
|
989
|
-
}
|
|
990
|
-
}
|
|
991
|
-
if (config.initialLimit !== void 0 && config.initialLimit < 1) {
|
|
992
|
-
throw new Error(
|
|
993
|
-
`AdaptiveLimiter: initialLimit must be >= 1, got ${config.initialLimit}`
|
|
994
|
-
);
|
|
995
|
-
}
|
|
996
|
-
if (config.minLimit !== void 0 && config.minLimit < 1) {
|
|
997
|
-
throw new Error(
|
|
998
|
-
`AdaptiveLimiter: minLimit must be >= 1, got ${config.minLimit}`
|
|
999
|
-
);
|
|
1000
|
-
}
|
|
1001
|
-
if (config.maxLimit !== void 0 && config.maxLimit < 1) {
|
|
1002
|
-
throw new Error(
|
|
1003
|
-
`AdaptiveLimiter: maxLimit must be >= 1, got ${config.maxLimit}`
|
|
1004
|
-
);
|
|
1005
|
-
}
|
|
1006
|
-
validateHeadroomStrategy(config.headroomStrategy);
|
|
1007
|
-
if (config.slowStartSaturationThreshold !== void 0) {
|
|
1008
|
-
if (config.slowStartSaturationThreshold <= 0 || config.slowStartSaturationThreshold > 1) {
|
|
1009
|
-
throw new Error(
|
|
1010
|
-
`AdaptiveLimiter: slowStartSaturationThreshold must be in (0, 1], got ${config.slowStartSaturationThreshold}`
|
|
1011
|
-
);
|
|
1012
|
-
}
|
|
1013
|
-
}
|
|
1014
|
-
if (config.slowStartSaturationSamples !== void 0 && config.slowStartSaturationSamples < 1) {
|
|
1015
|
-
throw new Error(
|
|
1016
|
-
`AdaptiveLimiter: slowStartSaturationSamples must be >= 1, got ${config.slowStartSaturationSamples}`
|
|
1017
|
-
);
|
|
1018
|
-
}
|
|
1019
|
-
}
|
|
1020
|
-
function validatePositiveNumber(name, value) {
|
|
1021
|
-
if (value !== void 0 && (!Number.isFinite(value) || value <= 0)) {
|
|
1022
|
-
throw new Error(`AdaptiveLimiter: ${name} must be > 0, got ${value}`);
|
|
1023
|
-
}
|
|
1024
|
-
}
|
|
1025
|
-
function validateHeadroomStrategy(strategy) {
|
|
1026
|
-
if (strategy === void 0) return;
|
|
1027
|
-
if (typeof strategy === "function") return;
|
|
1028
|
-
if (typeof strategy === "number") {
|
|
1029
|
-
validatePositiveNumber("headroomStrategy", strategy);
|
|
1030
|
-
return;
|
|
1031
|
-
}
|
|
1032
|
-
if (strategy === "fixed" || strategy === "proportional") return;
|
|
1033
|
-
if (typeof strategy !== "object" || strategy === null) {
|
|
1034
|
-
throw new Error("AdaptiveLimiter: headroomStrategy must be a number, function, 'fixed', 'proportional', or strategy object");
|
|
1035
|
-
}
|
|
1036
|
-
if (strategy.type === "fixed") {
|
|
1037
|
-
validatePositiveNumber("headroomStrategy.value", strategy.value);
|
|
1038
|
-
return;
|
|
1039
|
-
}
|
|
1040
|
-
if (strategy.type === "proportional") {
|
|
1041
|
-
validatePositiveNumber("headroomStrategy.ratio", strategy.ratio);
|
|
1042
|
-
validatePositiveNumber("headroomStrategy.min", strategy.min);
|
|
1043
|
-
validatePositiveNumber("headroomStrategy.max", strategy.max);
|
|
1044
|
-
return;
|
|
1045
|
-
}
|
|
1046
|
-
throw new Error("AdaptiveLimiter: headroomStrategy.type must be 'fixed' or 'proportional'");
|
|
1047
|
-
}
|
|
1048
|
-
var DEFAULTS = {
|
|
1049
|
-
preset: void 0,
|
|
1050
|
-
initialLimit: 10,
|
|
1051
|
-
minLimit: 1,
|
|
1052
|
-
maxLimit: 200,
|
|
1053
|
-
smoothingFactor: 0.5,
|
|
1054
|
-
probeInterval: 10,
|
|
1055
|
-
probeJitterRatio: 0.2,
|
|
1056
|
-
windowSize: 100,
|
|
1057
|
-
minSamples: 10,
|
|
1058
|
-
baselineStrategy: "min",
|
|
1059
|
-
decreaseCooldownSamples: 0,
|
|
1060
|
-
historySize: 32,
|
|
1061
|
-
windowDecayFactor: 1,
|
|
1062
|
-
errorWeight: 0,
|
|
1063
|
-
errorSmoothingFactor: 0.5,
|
|
1064
|
-
errorStatusThreshold: 500,
|
|
1065
|
-
queueStrategy: "fifo",
|
|
1066
|
-
queueLoadShedding: "reject-new",
|
|
1067
|
-
rejectionBackoffThreshold: 3,
|
|
1068
|
-
rejectionBackoffMs: void 0,
|
|
1069
|
-
stateTtlMs: 3e5,
|
|
1070
|
-
warmupRequests: 0,
|
|
1071
|
-
decreaseThreshold: 0.75,
|
|
1072
|
-
increaseThreshold: 1,
|
|
1073
|
-
maxDecreaseRatio: 0.2,
|
|
1074
|
-
headroomStrategy: 1,
|
|
1075
|
-
slowStartRecovery: true,
|
|
1076
|
-
slowStartSaturationThreshold: 0.5,
|
|
1077
|
-
slowStartSaturationSamples: 3,
|
|
1078
|
-
key: "origin",
|
|
1079
|
-
maxQueue: 256,
|
|
1080
|
-
queueTimeoutMs: void 0,
|
|
1081
|
-
onLimitChange: void 0,
|
|
1082
|
-
percentile: "p50"
|
|
1083
|
-
};
|
|
1084
|
-
function resolveConfig(config) {
|
|
1085
|
-
if (!config) return DEFAULTS;
|
|
1086
|
-
validateConfig(config);
|
|
1087
|
-
const effectiveConfig = config.preset === void 0 ? config : { ...adaptiveLimiterPresets[config.preset], ...config };
|
|
1088
|
-
let minLimit = effectiveConfig.minLimit ?? DEFAULTS.minLimit;
|
|
1089
|
-
let maxLimit = effectiveConfig.maxLimit ?? DEFAULTS.maxLimit;
|
|
1090
|
-
if (minLimit > maxLimit) {
|
|
1091
|
-
maxLimit = minLimit;
|
|
1092
|
-
}
|
|
1093
|
-
let initialLimit = effectiveConfig.initialLimit ?? DEFAULTS.initialLimit;
|
|
1094
|
-
initialLimit = Math.max(minLimit, Math.min(maxLimit, initialLimit));
|
|
1095
|
-
const windowSize = Math.floor(effectiveConfig.windowSize ?? DEFAULTS.windowSize);
|
|
1096
|
-
const minSamples = Math.max(1, Math.min(Math.floor(effectiveConfig.minSamples ?? DEFAULTS.minSamples), windowSize));
|
|
1097
|
-
const decreaseCooldownSamples = Math.floor(effectiveConfig.decreaseCooldownSamples ?? DEFAULTS.decreaseCooldownSamples);
|
|
1098
|
-
const historySize = Math.floor(effectiveConfig.historySize ?? DEFAULTS.historySize);
|
|
1099
|
-
const errorSmoothingFactor = effectiveConfig.errorSmoothingFactor ?? effectiveConfig.smoothingFactor ?? DEFAULTS.smoothingFactor;
|
|
1100
|
-
const stateTtlMs = effectiveConfig.stateTtlMs === false ? void 0 : Math.floor(effectiveConfig.stateTtlMs ?? DEFAULTS.stateTtlMs);
|
|
1101
|
-
return {
|
|
1102
|
-
preset: effectiveConfig.preset,
|
|
1103
|
-
initialLimit,
|
|
1104
|
-
minLimit,
|
|
1105
|
-
maxLimit,
|
|
1106
|
-
smoothingFactor: effectiveConfig.smoothingFactor ?? DEFAULTS.smoothingFactor,
|
|
1107
|
-
probeInterval: Math.floor(effectiveConfig.probeInterval ?? DEFAULTS.probeInterval),
|
|
1108
|
-
probeJitterRatio: effectiveConfig.probeJitterRatio ?? DEFAULTS.probeJitterRatio,
|
|
1109
|
-
windowSize,
|
|
1110
|
-
minSamples,
|
|
1111
|
-
baselineStrategy: effectiveConfig.baselineStrategy ?? DEFAULTS.baselineStrategy,
|
|
1112
|
-
decreaseCooldownSamples,
|
|
1113
|
-
historySize,
|
|
1114
|
-
windowDecayFactor: effectiveConfig.windowDecayFactor ?? DEFAULTS.windowDecayFactor,
|
|
1115
|
-
errorWeight: effectiveConfig.errorWeight ?? DEFAULTS.errorWeight,
|
|
1116
|
-
errorSmoothingFactor,
|
|
1117
|
-
errorStatusThreshold: Math.floor(effectiveConfig.errorStatusThreshold ?? DEFAULTS.errorStatusThreshold),
|
|
1118
|
-
queueStrategy: effectiveConfig.queueStrategy ?? DEFAULTS.queueStrategy,
|
|
1119
|
-
queueLoadShedding: effectiveConfig.queueLoadShedding ?? DEFAULTS.queueLoadShedding,
|
|
1120
|
-
rejectionBackoffThreshold: Math.floor(effectiveConfig.rejectionBackoffThreshold ?? DEFAULTS.rejectionBackoffThreshold),
|
|
1121
|
-
rejectionBackoffMs: effectiveConfig.rejectionBackoffMs === void 0 ? DEFAULTS.rejectionBackoffMs : Math.floor(effectiveConfig.rejectionBackoffMs),
|
|
1122
|
-
stateTtlMs,
|
|
1123
|
-
warmupRequests: Math.floor(effectiveConfig.warmupRequests ?? DEFAULTS.warmupRequests),
|
|
1124
|
-
decreaseThreshold: effectiveConfig.decreaseThreshold ?? DEFAULTS.decreaseThreshold,
|
|
1125
|
-
increaseThreshold: effectiveConfig.increaseThreshold ?? DEFAULTS.increaseThreshold,
|
|
1126
|
-
maxDecreaseRatio: effectiveConfig.maxDecreaseRatio ?? DEFAULTS.maxDecreaseRatio,
|
|
1127
|
-
headroomStrategy: effectiveConfig.headroomStrategy ?? DEFAULTS.headroomStrategy,
|
|
1128
|
-
slowStartRecovery: effectiveConfig.slowStartRecovery ?? DEFAULTS.slowStartRecovery,
|
|
1129
|
-
slowStartSaturationThreshold: effectiveConfig.slowStartSaturationThreshold ?? DEFAULTS.slowStartSaturationThreshold,
|
|
1130
|
-
slowStartSaturationSamples: Math.floor(effectiveConfig.slowStartSaturationSamples ?? DEFAULTS.slowStartSaturationSamples),
|
|
1131
|
-
key: effectiveConfig.key ?? DEFAULTS.key,
|
|
1132
|
-
maxQueue: effectiveConfig.maxQueue ?? DEFAULTS.maxQueue,
|
|
1133
|
-
queueTimeoutMs: effectiveConfig.queueTimeoutMs,
|
|
1134
|
-
onLimitChange: effectiveConfig.onLimitChange,
|
|
1135
|
-
percentile: effectiveConfig.percentile ?? DEFAULTS.percentile
|
|
1136
|
-
};
|
|
1137
|
-
}
|
|
1138
|
-
|
|
1139
|
-
// src/http/adaptiveLimiter/adaptiveLimiter.ts
|
|
1140
|
-
var monotonicNow = () => {
|
|
1141
|
-
const perf = globalThis.performance;
|
|
1142
|
-
if (perf && typeof perf.now === "function") return perf.now();
|
|
1143
|
-
return Date.now();
|
|
1144
|
-
};
|
|
1145
|
-
var wallNow = () => Date.now();
|
|
1146
|
-
var DEFAULT_PRIORITY = 5;
|
|
1147
|
-
var clampPriority = (value) => {
|
|
1148
|
-
if (value === void 0 || !Number.isFinite(value)) return DEFAULT_PRIORITY;
|
|
1149
|
-
return Math.max(0, Math.min(9, Math.floor(value)));
|
|
1150
|
-
};
|
|
1151
|
-
var poolTimeoutError = (key, timeoutMs) => ({
|
|
1152
|
-
_tag: "PoolTimeout",
|
|
1153
|
-
key,
|
|
1154
|
-
timeoutMs,
|
|
1155
|
-
message: `Adaptive limiter '${key}' did not grant a slot within ${timeoutMs}ms`
|
|
1156
|
-
});
|
|
1157
|
-
var poolRejectedError2 = (key, limit, retryAfterMs2) => ({
|
|
1158
|
-
_tag: "PoolRejected",
|
|
1159
|
-
key,
|
|
1160
|
-
limit,
|
|
1161
|
-
message: `Adaptive limiter '${key}' queue is full (max ${limit})`,
|
|
1162
|
-
...retryAfterMs2 === void 0 ? {} : { retryAfterMs: retryAfterMs2 }
|
|
1163
|
-
});
|
|
1164
|
-
var abortError2 = () => ({ _tag: "Abort" });
|
|
1165
|
-
var poolClosedError = (key) => ({
|
|
1166
|
-
_tag: "PoolClosed",
|
|
1167
|
-
key,
|
|
1168
|
-
message: `Adaptive limiter '${key}' has been destroyed`
|
|
1169
|
-
});
|
|
1170
|
-
var AdaptiveLimiter = class {
|
|
1171
|
-
config;
|
|
1172
|
-
states = /* @__PURE__ */ new Map();
|
|
1173
|
-
destroyed = false;
|
|
1174
|
-
constructor(config) {
|
|
1175
|
-
this.config = resolveConfig(config);
|
|
1176
|
-
}
|
|
1177
|
-
/** Key resolver for external use by the HTTP client. */
|
|
1178
|
-
get keyResolver() {
|
|
1179
|
-
return this.config.key;
|
|
1180
|
-
}
|
|
1181
|
-
/**
|
|
1182
|
-
* Acquire a concurrency slot for the given key.
|
|
1183
|
-
* Resolves immediately if under the limit, otherwise queues the request.
|
|
1184
|
-
* Rejects with PoolRejected if the queue is full, or PoolTimeout if the queue timeout expires.
|
|
1185
|
-
*/
|
|
1186
|
-
acquire(key, signal, options) {
|
|
1187
|
-
if (this.destroyed) return Promise.reject(poolClosedError(key));
|
|
1188
|
-
const state = this.getOrCreateState(key);
|
|
1189
|
-
this.touch(state);
|
|
1190
|
-
const priority = clampPriority(options?.priority);
|
|
1191
|
-
if (signal.aborted) {
|
|
1192
|
-
this.scheduleIdleEviction(state);
|
|
1193
|
-
return Promise.reject(abortError2());
|
|
1194
|
-
}
|
|
1195
|
-
if (state.inFlight < state.limit) {
|
|
1196
|
-
state.inFlight++;
|
|
1197
|
-
state.acquired++;
|
|
1198
|
-
state.rejectionStreak = 0;
|
|
1199
|
-
return Promise.resolve(this.makeLease(state));
|
|
1200
|
-
}
|
|
1201
|
-
if (state.queue.length >= this.config.maxQueue) {
|
|
1202
|
-
const evicted = this.tryEvictLowerPriorityWaiter(state, priority);
|
|
1203
|
-
if (!evicted) {
|
|
1204
|
-
state.rejected++;
|
|
1205
|
-
state.rejectionStreak++;
|
|
1206
|
-
this.scheduleIdleEviction(state);
|
|
1207
|
-
return Promise.reject(poolRejectedError2(key, this.config.maxQueue, this.suggestedBackoffMs(state)));
|
|
1208
|
-
}
|
|
1209
|
-
}
|
|
1210
|
-
return new Promise((resolve, reject) => {
|
|
1211
|
-
const waiter = {
|
|
1212
|
-
signal,
|
|
1213
|
-
resolve,
|
|
1214
|
-
reject,
|
|
1215
|
-
priority,
|
|
1216
|
-
arrivalOrder: state.queueSequence++
|
|
1217
|
-
};
|
|
1218
|
-
const removeWaiter = () => this.removeWaiter(state, waiter);
|
|
1219
|
-
const cleanup = () => this.cleanupWaiter(waiter);
|
|
1220
|
-
waiter.abort = () => {
|
|
1221
|
-
cleanup();
|
|
1222
|
-
removeWaiter();
|
|
1223
|
-
this.touch(state);
|
|
1224
|
-
state.abortedWhileQueued++;
|
|
1225
|
-
this.scheduleIdleEviction(state);
|
|
1226
|
-
reject(abortError2());
|
|
1227
|
-
};
|
|
1228
|
-
signal.addEventListener("abort", waiter.abort, { once: true });
|
|
1229
|
-
if (this.config.queueTimeoutMs !== void 0 && this.config.queueTimeoutMs > 0) {
|
|
1230
|
-
waiter.timer = setTimeout(() => {
|
|
1231
|
-
cleanup();
|
|
1232
|
-
removeWaiter();
|
|
1233
|
-
this.touch(state);
|
|
1234
|
-
state.queueTimeouts++;
|
|
1235
|
-
this.scheduleIdleEviction(state);
|
|
1236
|
-
reject(poolTimeoutError(key, this.config.queueTimeoutMs));
|
|
1237
|
-
}, this.config.queueTimeoutMs);
|
|
1238
|
-
}
|
|
1239
|
-
this.enqueueWaiter(state, waiter);
|
|
1240
|
-
});
|
|
1241
|
-
}
|
|
1242
|
-
/**
|
|
1243
|
-
* Get stats for a specific key, or aggregate stats if no key is provided.
|
|
1244
|
-
*/
|
|
1245
|
-
stats(key) {
|
|
1246
|
-
this.evictIdleStates();
|
|
1247
|
-
if (key !== void 0) {
|
|
1248
|
-
const state = this.states.get(key);
|
|
1249
|
-
if (!state) {
|
|
1250
|
-
return {
|
|
1251
|
-
limit: this.config.initialLimit,
|
|
1252
|
-
inFlight: 0,
|
|
1253
|
-
queueDepth: 0,
|
|
1254
|
-
gradient: void 0,
|
|
1255
|
-
latencyGradient: void 0,
|
|
1256
|
-
errorRate: void 0,
|
|
1257
|
-
smoothedLatency: void 0,
|
|
1258
|
-
minLatency: void 0,
|
|
1259
|
-
baselineLatency: void 0,
|
|
1260
|
-
p5: void 0,
|
|
1261
|
-
p50: void 0,
|
|
1262
|
-
p99: void 0,
|
|
1263
|
-
probeCount: 0,
|
|
1264
|
-
windowSize: 0,
|
|
1265
|
-
warmupCompletions: 0,
|
|
1266
|
-
slowStart: false,
|
|
1267
|
-
cooldownSamplesRemaining: 0,
|
|
1268
|
-
utilization: 0,
|
|
1269
|
-
requestsPerSecond: 0,
|
|
1270
|
-
completionsPerSecond: 0,
|
|
1271
|
-
rejectionRate: 0,
|
|
1272
|
-
suggestedBackoffMs: void 0
|
|
1273
|
-
};
|
|
1274
|
-
}
|
|
1275
|
-
return this.stateToStats(state);
|
|
1276
|
-
}
|
|
1277
|
-
if (this.states.size === 0) {
|
|
1278
|
-
return {
|
|
1279
|
-
limit: this.config.initialLimit,
|
|
1280
|
-
inFlight: 0,
|
|
1281
|
-
queueDepth: 0,
|
|
1282
|
-
gradient: void 0,
|
|
1283
|
-
latencyGradient: void 0,
|
|
1284
|
-
errorRate: void 0,
|
|
1285
|
-
smoothedLatency: void 0,
|
|
1286
|
-
minLatency: void 0,
|
|
1287
|
-
baselineLatency: void 0,
|
|
1288
|
-
p5: void 0,
|
|
1289
|
-
p50: void 0,
|
|
1290
|
-
p99: void 0,
|
|
1291
|
-
probeCount: 0,
|
|
1292
|
-
windowSize: 0,
|
|
1293
|
-
warmupCompletions: 0,
|
|
1294
|
-
slowStart: false,
|
|
1295
|
-
cooldownSamplesRemaining: 0,
|
|
1296
|
-
utilization: 0,
|
|
1297
|
-
requestsPerSecond: 0,
|
|
1298
|
-
completionsPerSecond: 0,
|
|
1299
|
-
rejectionRate: 0,
|
|
1300
|
-
suggestedBackoffMs: void 0,
|
|
1301
|
-
stateCount: 0,
|
|
1302
|
-
keys: []
|
|
1303
|
-
};
|
|
1304
|
-
}
|
|
1305
|
-
if (this.states.size === 1) {
|
|
1306
|
-
const [state] = this.states.values();
|
|
1307
|
-
return {
|
|
1308
|
-
...this.stateToStats(state),
|
|
1309
|
-
keys: [state.key]
|
|
1310
|
-
};
|
|
1311
|
-
}
|
|
1312
|
-
let totalLimit = 0;
|
|
1313
|
-
let totalInFlight = 0;
|
|
1314
|
-
let totalQueueDepth = 0;
|
|
1315
|
-
let totalProbeCount = 0;
|
|
1316
|
-
let totalWindowSize = 0;
|
|
1317
|
-
let totalRequestsPerSecond = 0;
|
|
1318
|
-
let totalCompletionsPerSecond = 0;
|
|
1319
|
-
let totalRejected = 0;
|
|
1320
|
-
let totalAttempts = 0;
|
|
1321
|
-
const keys = [];
|
|
1322
|
-
let firstState;
|
|
1323
|
-
const now = monotonicNow();
|
|
1324
|
-
for (const state of this.states.values()) {
|
|
1325
|
-
keys.push(state.key);
|
|
1326
|
-
totalLimit += state.limit;
|
|
1327
|
-
totalInFlight += state.inFlight;
|
|
1328
|
-
totalQueueDepth += state.queue.length;
|
|
1329
|
-
totalProbeCount += state.probeCount;
|
|
1330
|
-
totalWindowSize += state.window.length;
|
|
1331
|
-
const stats = this.stateToStats(state, now);
|
|
1332
|
-
totalRequestsPerSecond += stats.requestsPerSecond ?? 0;
|
|
1333
|
-
totalCompletionsPerSecond += stats.completionsPerSecond ?? 0;
|
|
1334
|
-
totalRejected += state.rejected;
|
|
1335
|
-
totalAttempts += state.acquired + state.rejected + state.queueTimeouts + state.abortedWhileQueued;
|
|
1336
|
-
if (!firstState) firstState = state;
|
|
1337
|
-
}
|
|
1338
|
-
const minLatency = firstState?.window.min();
|
|
1339
|
-
return {
|
|
1340
|
-
limit: totalLimit,
|
|
1341
|
-
inFlight: totalInFlight,
|
|
1342
|
-
queueDepth: totalQueueDepth,
|
|
1343
|
-
gradient: firstState?.lastGradient,
|
|
1344
|
-
latencyGradient: firstState?.lastLatencyGradient,
|
|
1345
|
-
errorRate: firstState?.errorEma.value,
|
|
1346
|
-
smoothedLatency: firstState?.ema.value,
|
|
1347
|
-
minLatency,
|
|
1348
|
-
baselineLatency: minLatency === void 0 || firstState === void 0 ? void 0 : this.baselineLatency(firstState, minLatency),
|
|
1349
|
-
p5: firstState === void 0 ? void 0 : this.windowPercentile(firstState, 5),
|
|
1350
|
-
p50: firstState === void 0 ? void 0 : this.windowPercentile(firstState, 50),
|
|
1351
|
-
p99: firstState === void 0 ? void 0 : this.windowPercentile(firstState, 99),
|
|
1352
|
-
probeCount: totalProbeCount,
|
|
1353
|
-
windowSize: totalWindowSize,
|
|
1354
|
-
warmupCompletions: firstState?.warmupCompletions ?? 0,
|
|
1355
|
-
slowStart: firstState?.slowStartActive ?? false,
|
|
1356
|
-
cooldownSamplesRemaining: firstState?.decreaseCooldownRemaining ?? 0,
|
|
1357
|
-
utilization: totalLimit > 0 ? totalInFlight / totalLimit : 0,
|
|
1358
|
-
requestsPerSecond: totalRequestsPerSecond,
|
|
1359
|
-
completionsPerSecond: totalCompletionsPerSecond,
|
|
1360
|
-
rejectionRate: totalAttempts > 0 ? totalRejected / totalAttempts : 0,
|
|
1361
|
-
suggestedBackoffMs: firstState === void 0 ? void 0 : this.suggestedBackoffMs(firstState),
|
|
1362
|
-
stateCount: this.states.size,
|
|
1363
|
-
keys
|
|
1364
|
-
};
|
|
1365
|
-
}
|
|
1366
|
-
/** Return the currently retained per-key state identifiers. */
|
|
1367
|
-
keys() {
|
|
1368
|
-
this.evictIdleStates();
|
|
1369
|
-
return [...this.states.keys()];
|
|
1370
|
-
}
|
|
1371
|
-
/** Return a diagnostic snapshot for one retained key, if present. */
|
|
1372
|
-
snapshot(key) {
|
|
1373
|
-
this.evictIdleStates();
|
|
1374
|
-
const state = this.states.get(key);
|
|
1375
|
-
return state ? this.stateToKeySnapshot(state) : void 0;
|
|
1376
|
-
}
|
|
1377
|
-
/** Return a full diagnostic dump across all retained keys. */
|
|
1378
|
-
dump() {
|
|
1379
|
-
this.evictIdleStates();
|
|
1380
|
-
const keys = [...this.states.keys()];
|
|
1381
|
-
return {
|
|
1382
|
-
stateCount: this.states.size,
|
|
1383
|
-
keys,
|
|
1384
|
-
aggregate: this.stats(),
|
|
1385
|
-
states: [...this.states.values()].map((state) => this.stateToKeySnapshot(state)),
|
|
1386
|
-
history: this.history()
|
|
1387
|
-
};
|
|
1388
|
-
}
|
|
1389
|
-
/** Return retained limit-change history for one key, or all keys if omitted. */
|
|
1390
|
-
history(key) {
|
|
1391
|
-
this.evictIdleStates();
|
|
1392
|
-
if (key !== void 0) {
|
|
1393
|
-
return [...this.states.get(key)?.history ?? []];
|
|
1394
|
-
}
|
|
1395
|
-
return [...this.states.values()].flatMap((state) => state.history).sort((a, b) => a.timestamp - b.timestamp);
|
|
1396
|
-
}
|
|
1397
|
-
/**
|
|
1398
|
-
* Circuit-breaker feedback hook. When a circuit is already open for `key`,
|
|
1399
|
-
* collapse that key's limit to minLimit immediately instead of waiting for
|
|
1400
|
-
* latency gradients to discover the saturation indirectly.
|
|
1401
|
-
*/
|
|
1402
|
-
markCircuitOpen(key) {
|
|
1403
|
-
if (this.destroyed) return;
|
|
1404
|
-
const state = this.getOrCreateState(key);
|
|
1405
|
-
this.touch(state);
|
|
1406
|
-
state.saturationStreak = this.config.slowStartSaturationSamples;
|
|
1407
|
-
state.slowStartActive = this.config.slowStartRecovery;
|
|
1408
|
-
state.slowStartRecoveryStarted = false;
|
|
1409
|
-
state.slowStartHeadroom = Math.max(1, this.resolveHeadroom(state, 0, "circuit-open"));
|
|
1410
|
-
const minLatency = state.window.min() ?? 0;
|
|
1411
|
-
this.applyLimit(
|
|
1412
|
-
state,
|
|
1413
|
-
this.config.minLimit,
|
|
1414
|
-
0,
|
|
1415
|
-
0,
|
|
1416
|
-
state.errorEma.value ?? 0,
|
|
1417
|
-
state.ema.value ?? 0,
|
|
1418
|
-
minLatency,
|
|
1419
|
-
state.baselineEma.value ?? minLatency,
|
|
1420
|
-
"circuit-open"
|
|
1421
|
-
);
|
|
1422
|
-
this.scheduleIdleEviction(state);
|
|
1423
|
-
}
|
|
1424
|
-
/** Destroy the limiter, reject queued waiters, clear timers, and drop state. */
|
|
1425
|
-
destroy() {
|
|
1426
|
-
if (this.destroyed) return;
|
|
1427
|
-
this.destroyed = true;
|
|
1428
|
-
for (const state of this.states.values()) {
|
|
1429
|
-
this.clearIdleTimer(state);
|
|
1430
|
-
for (const waiter of state.queue.splice(0)) {
|
|
1431
|
-
this.cleanupWaiter(waiter);
|
|
1432
|
-
waiter.reject(poolClosedError(state.key));
|
|
1433
|
-
}
|
|
1434
|
-
state.inFlight = 0;
|
|
1435
|
-
}
|
|
1436
|
-
this.states.clear();
|
|
1437
|
-
}
|
|
1438
|
-
/** Alias for destroy(), useful for graceful shutdown code paths. */
|
|
1439
|
-
shutdown() {
|
|
1440
|
-
this.destroy();
|
|
1441
|
-
}
|
|
1442
|
-
getOrCreateState(key) {
|
|
1443
|
-
const existing = this.states.get(key);
|
|
1444
|
-
if (existing) return existing;
|
|
1445
|
-
const now = monotonicNow();
|
|
1446
|
-
const timestamp = wallNow();
|
|
1447
|
-
const state = {
|
|
1448
|
-
key,
|
|
1449
|
-
limit: this.config.initialLimit,
|
|
1450
|
-
inFlight: 0,
|
|
1451
|
-
window: new LatencyWindow(this.config.windowSize),
|
|
1452
|
-
ema: new EmaComputer(this.config.smoothingFactor),
|
|
1453
|
-
baselineEma: new EmaComputer(this.config.smoothingFactor),
|
|
1454
|
-
errorEma: new EmaComputer(this.config.errorSmoothingFactor),
|
|
1455
|
-
probeCount: 0,
|
|
1456
|
-
queue: [],
|
|
1457
|
-
lastGradient: void 0,
|
|
1458
|
-
lastLatencyGradient: void 0,
|
|
1459
|
-
acquired: 0,
|
|
1460
|
-
released: 0,
|
|
1461
|
-
rejected: 0,
|
|
1462
|
-
queueTimeouts: 0,
|
|
1463
|
-
abortedWhileQueued: 0,
|
|
1464
|
-
evictedWhileQueued: 0,
|
|
1465
|
-
rejectionStreak: 0,
|
|
1466
|
-
queueSequence: 0,
|
|
1467
|
-
createdAt: now,
|
|
1468
|
-
createdTimestamp: timestamp,
|
|
1469
|
-
lastActivityAt: now,
|
|
1470
|
-
lastActivityTimestamp: timestamp,
|
|
1471
|
-
nextProbeAt: this.nextProbeInterval(),
|
|
1472
|
-
warmupCompletions: 0,
|
|
1473
|
-
warmupDone: this.config.warmupRequests <= 0,
|
|
1474
|
-
saturationStreak: 0,
|
|
1475
|
-
slowStartActive: false,
|
|
1476
|
-
slowStartRecoveryStarted: false,
|
|
1477
|
-
slowStartHeadroom: 1,
|
|
1478
|
-
decreaseCooldownRemaining: 0,
|
|
1479
|
-
history: []
|
|
1480
|
-
};
|
|
1481
|
-
this.states.set(key, state);
|
|
1482
|
-
this.scheduleIdleEviction(state);
|
|
1483
|
-
return state;
|
|
1484
|
-
}
|
|
1485
|
-
makeLease(state) {
|
|
1486
|
-
let released = false;
|
|
1487
|
-
return {
|
|
1488
|
-
key: state.key,
|
|
1489
|
-
release: (latencyMs, info) => {
|
|
1490
|
-
if (released) return;
|
|
1491
|
-
released = true;
|
|
1492
|
-
if (this.destroyed) return;
|
|
1493
|
-
this.touch(state);
|
|
1494
|
-
if (state.inFlight > 0) state.inFlight--;
|
|
1495
|
-
state.released++;
|
|
1496
|
-
this.recordAndAdjust(state, latencyMs, info);
|
|
1497
|
-
this.drain(state);
|
|
1498
|
-
this.scheduleIdleEviction(state);
|
|
1499
|
-
}
|
|
1500
|
-
};
|
|
1501
|
-
}
|
|
1502
|
-
recordAndAdjust(state, latencyMs, info) {
|
|
1503
|
-
state.window.record(latencyMs);
|
|
1504
|
-
state.errorEma.update(this.isErrorSignal(info) ? 1 : 0);
|
|
1505
|
-
if (Number.isFinite(latencyMs) && latencyMs > 0) {
|
|
1506
|
-
state.ema.update(latencyMs);
|
|
1507
|
-
state.baselineEma.update(this.lowLatencySample(state, latencyMs));
|
|
1508
|
-
}
|
|
1509
|
-
const minLat = state.window.min();
|
|
1510
|
-
const emaValue = state.ema.value;
|
|
1511
|
-
if (minLat === void 0 || emaValue === void 0) return;
|
|
1512
|
-
const currentLatency = this.currentLatency(state, emaValue);
|
|
1513
|
-
if (currentLatency === void 0) return;
|
|
1514
|
-
const baselineLatency = this.baselineLatency(state, minLat);
|
|
1515
|
-
const latencyGradient = computeGradient(baselineLatency, currentLatency);
|
|
1516
|
-
const gradient = this.combineGradientWithErrors(latencyGradient, state.errorEma.value ?? 0);
|
|
1517
|
-
state.lastGradient = gradient;
|
|
1518
|
-
state.lastLatencyGradient = latencyGradient;
|
|
1519
|
-
this.recordSaturation(state, gradient);
|
|
1520
|
-
if (this.tryWarmup(
|
|
1521
|
-
state,
|
|
1522
|
-
gradient,
|
|
1523
|
-
latencyGradient,
|
|
1524
|
-
state.errorEma.value ?? 0,
|
|
1525
|
-
emaValue,
|
|
1526
|
-
minLat,
|
|
1527
|
-
baselineLatency
|
|
1528
|
-
)) return;
|
|
1529
|
-
if (state.window.length < this.config.minSamples) return;
|
|
1530
|
-
const mode = state.slowStartActive && gradient >= this.config.increaseThreshold ? "slow-start" : "stable";
|
|
1531
|
-
let headroom = this.resolveHeadroom(state, gradient, mode);
|
|
1532
|
-
if (mode === "slow-start") {
|
|
1533
|
-
headroom = Math.max(headroom, state.slowStartHeadroom);
|
|
1534
|
-
}
|
|
1535
|
-
const cooldownActive = state.decreaseCooldownRemaining > 0;
|
|
1536
|
-
if (cooldownActive) state.decreaseCooldownRemaining--;
|
|
1537
|
-
const wantsDecrease = gradient < this.config.decreaseThreshold;
|
|
1538
|
-
const decreaseBlockedByCooldown = wantsDecrease && cooldownActive;
|
|
1539
|
-
let newLimit = decreaseBlockedByCooldown ? state.limit : computeNewLimit(
|
|
1540
|
-
state.limit,
|
|
1541
|
-
gradient,
|
|
1542
|
-
headroom,
|
|
1543
|
-
this.config.minLimit,
|
|
1544
|
-
this.config.maxLimit,
|
|
1545
|
-
{
|
|
1546
|
-
decreaseThreshold: this.config.decreaseThreshold,
|
|
1547
|
-
increaseThreshold: this.config.increaseThreshold,
|
|
1548
|
-
maxDecreaseRatio: this.config.maxDecreaseRatio
|
|
1549
|
-
}
|
|
1550
|
-
);
|
|
1551
|
-
let reason = mode === "slow-start" ? "slow-start" : "gradient";
|
|
1552
|
-
if (!decreaseBlockedByCooldown) state.probeCount++;
|
|
1553
|
-
if (!decreaseBlockedByCooldown && state.probeCount >= state.nextProbeAt) {
|
|
1554
|
-
const probeHeadroom = this.resolveHeadroom(state, gradient, "probe");
|
|
1555
|
-
newLimit = Math.min(newLimit + probeHeadroom, this.config.maxLimit);
|
|
1556
|
-
state.probeCount = 0;
|
|
1557
|
-
state.nextProbeAt = this.nextProbeInterval();
|
|
1558
|
-
reason = "probe";
|
|
1559
|
-
}
|
|
1560
|
-
const previousLimit = state.limit;
|
|
1561
|
-
this.applyLimit(state, newLimit, gradient, latencyGradient, state.errorEma.value ?? 0, emaValue, minLat, baselineLatency, reason);
|
|
1562
|
-
if (state.limit < previousLimit) {
|
|
1563
|
-
state.decreaseCooldownRemaining = this.config.decreaseCooldownSamples;
|
|
1564
|
-
}
|
|
1565
|
-
if (gradient < this.config.decreaseThreshold) {
|
|
1566
|
-
if (state.slowStartRecoveryStarted) {
|
|
1567
|
-
state.slowStartActive = false;
|
|
1568
|
-
state.slowStartRecoveryStarted = false;
|
|
1569
|
-
state.slowStartHeadroom = 1;
|
|
1570
|
-
}
|
|
1571
|
-
} else if (state.slowStartActive && gradient >= this.config.increaseThreshold && state.limit > previousLimit) {
|
|
1572
|
-
state.slowStartRecoveryStarted = true;
|
|
1573
|
-
state.slowStartHeadroom = Math.max(
|
|
1574
|
-
1,
|
|
1575
|
-
Math.min(this.config.maxLimit, Math.ceil(headroom * 2))
|
|
1576
|
-
);
|
|
1577
|
-
}
|
|
1578
|
-
}
|
|
1579
|
-
tryWarmup(state, gradient, latencyGradient, errorRate, emaValue, minLat, baselineLatency) {
|
|
1580
|
-
if (state.warmupDone) return false;
|
|
1581
|
-
state.warmupCompletions++;
|
|
1582
|
-
if (gradient < this.config.decreaseThreshold) {
|
|
1583
|
-
state.warmupDone = true;
|
|
1584
|
-
return false;
|
|
1585
|
-
}
|
|
1586
|
-
const progress = Math.min(state.warmupCompletions, this.config.warmupRequests) / this.config.warmupRequests;
|
|
1587
|
-
const target = Math.ceil(
|
|
1588
|
-
this.config.initialLimit + (this.config.maxLimit - this.config.initialLimit) * progress
|
|
1589
|
-
);
|
|
1590
|
-
const warmupHeadroom = this.resolveHeadroom(state, gradient, "warmup");
|
|
1591
|
-
const newLimit = Math.max(
|
|
1592
|
-
state.limit,
|
|
1593
|
-
Math.min(this.config.maxLimit, Math.max(target, state.limit + warmupHeadroom))
|
|
1594
|
-
);
|
|
1595
|
-
if (state.warmupCompletions >= this.config.warmupRequests) {
|
|
1596
|
-
state.warmupDone = true;
|
|
1597
|
-
}
|
|
1598
|
-
this.applyLimit(state, newLimit, gradient, latencyGradient, errorRate, emaValue, minLat, baselineLatency, "warmup");
|
|
1599
|
-
return true;
|
|
1600
|
-
}
|
|
1601
|
-
baselineLatency(state, minLatency) {
|
|
1602
|
-
if (this.config.baselineStrategy === "p5") {
|
|
1603
|
-
return this.windowPercentile(state, 5) ?? minLatency;
|
|
1604
|
-
}
|
|
1605
|
-
if (this.config.baselineStrategy === "ema-low") {
|
|
1606
|
-
return state.baselineEma.value ?? this.windowPercentile(state, 5) ?? minLatency;
|
|
1607
|
-
}
|
|
1608
|
-
return minLatency;
|
|
1609
|
-
}
|
|
1610
|
-
lowLatencySample(state, fallback) {
|
|
1611
|
-
return this.windowPercentile(state, 5) ?? state.window.min() ?? fallback;
|
|
1612
|
-
}
|
|
1613
|
-
isErrorSignal(info) {
|
|
1614
|
-
if (!info) return false;
|
|
1615
|
-
if (info.error) return true;
|
|
1616
|
-
return info.status !== void 0 && info.status >= this.config.errorStatusThreshold;
|
|
1617
|
-
}
|
|
1618
|
-
combineGradientWithErrors(latencyGradient, errorRate) {
|
|
1619
|
-
if (this.config.errorWeight <= 0 || errorRate <= 0) return latencyGradient;
|
|
1620
|
-
const errorGradient = 1 - Math.min(1, errorRate * this.config.errorWeight);
|
|
1621
|
-
return Math.max(0, Math.min(latencyGradient, errorGradient));
|
|
1622
|
-
}
|
|
1623
|
-
windowPercentile(state, percentile) {
|
|
1624
|
-
return this.config.windowDecayFactor < 1 ? state.window.weightedPercentile(percentile, this.config.windowDecayFactor) : state.window.percentile(percentile);
|
|
1625
|
-
}
|
|
1626
|
-
recordSaturation(state, gradient) {
|
|
1627
|
-
if (!this.config.slowStartRecovery) return;
|
|
1628
|
-
if (gradient <= this.config.slowStartSaturationThreshold) {
|
|
1629
|
-
state.saturationStreak++;
|
|
1630
|
-
if (state.saturationStreak >= this.config.slowStartSaturationSamples) {
|
|
1631
|
-
state.slowStartActive = true;
|
|
1632
|
-
state.slowStartRecoveryStarted = false;
|
|
1633
|
-
state.slowStartHeadroom = Math.max(1, this.resolveHeadroom(state, gradient, "slow-start"));
|
|
1634
|
-
}
|
|
1635
|
-
return;
|
|
1636
|
-
}
|
|
1637
|
-
if (gradient >= this.config.increaseThreshold) {
|
|
1638
|
-
state.saturationStreak = 0;
|
|
1639
|
-
}
|
|
1640
|
-
}
|
|
1641
|
-
applyLimit(state, proposedLimit, gradient, latencyGradient, errorRate, smoothedLatency, minLatency, baselineLatency, reason) {
|
|
1642
|
-
const newLimit = Math.max(this.config.minLimit, Math.min(this.config.maxLimit, Math.floor(proposedLimit)));
|
|
1643
|
-
if (newLimit === state.limit) {
|
|
1644
|
-
state.limit = newLimit;
|
|
1645
|
-
return;
|
|
1646
|
-
}
|
|
1647
|
-
const previousLimit = state.limit;
|
|
1648
|
-
state.limit = newLimit;
|
|
1649
|
-
const event = {
|
|
1650
|
-
key: state.key,
|
|
1651
|
-
previousLimit,
|
|
1652
|
-
newLimit,
|
|
1653
|
-
gradient,
|
|
1654
|
-
latencyGradient,
|
|
1655
|
-
errorRate,
|
|
1656
|
-
smoothedLatency,
|
|
1657
|
-
minLatency,
|
|
1658
|
-
baselineLatency,
|
|
1659
|
-
p5: this.windowPercentile(state, 5),
|
|
1660
|
-
timestamp: wallNow(),
|
|
1661
|
-
reason
|
|
1662
|
-
};
|
|
1663
|
-
this.recordLimitChange(state, event);
|
|
1664
|
-
if (this.config.onLimitChange) {
|
|
1665
|
-
this.config.onLimitChange(event);
|
|
1666
|
-
}
|
|
1667
|
-
}
|
|
1668
|
-
recordLimitChange(state, event) {
|
|
1669
|
-
if (this.config.historySize <= 0) return;
|
|
1670
|
-
state.history.push(event);
|
|
1671
|
-
if (state.history.length > this.config.historySize) {
|
|
1672
|
-
state.history.splice(0, state.history.length - this.config.historySize);
|
|
1673
|
-
}
|
|
1674
|
-
}
|
|
1675
|
-
resolveHeadroom(state, gradient, mode) {
|
|
1676
|
-
const strategy = this.config.headroomStrategy;
|
|
1677
|
-
let value;
|
|
1678
|
-
if (typeof strategy === "function") {
|
|
1679
|
-
value = strategy({
|
|
1680
|
-
key: state.key,
|
|
1681
|
-
currentLimit: state.limit,
|
|
1682
|
-
minLimit: this.config.minLimit,
|
|
1683
|
-
maxLimit: this.config.maxLimit,
|
|
1684
|
-
gradient,
|
|
1685
|
-
mode
|
|
1686
|
-
});
|
|
1687
|
-
} else if (typeof strategy === "number") {
|
|
1688
|
-
value = strategy;
|
|
1689
|
-
} else if (strategy === "proportional") {
|
|
1690
|
-
value = Math.ceil(state.limit * 0.05);
|
|
1691
|
-
} else if (strategy === "fixed") {
|
|
1692
|
-
value = 1;
|
|
1693
|
-
} else if (strategy.type === "proportional") {
|
|
1694
|
-
const ratio = strategy.ratio ?? 0.05;
|
|
1695
|
-
const min = strategy.min ?? 1;
|
|
1696
|
-
const max = strategy.max ?? Number.POSITIVE_INFINITY;
|
|
1697
|
-
value = Math.max(min, Math.min(max, Math.ceil(state.limit * ratio)));
|
|
1698
|
-
} else {
|
|
1699
|
-
value = strategy.value ?? 1;
|
|
1700
|
-
}
|
|
1701
|
-
if (!Number.isFinite(value) || value <= 0) return 1;
|
|
1702
|
-
return Math.max(1, Math.ceil(value));
|
|
1703
|
-
}
|
|
1704
|
-
nextProbeInterval() {
|
|
1705
|
-
const base = this.config.probeInterval;
|
|
1706
|
-
const jitter = this.config.probeJitterRatio;
|
|
1707
|
-
if (jitter <= 0) return base;
|
|
1708
|
-
const spread = base * jitter;
|
|
1709
|
-
const min = Math.max(1, base - spread);
|
|
1710
|
-
const max = base + spread;
|
|
1711
|
-
return Math.max(1, Math.round(min + Math.random() * (max - min)));
|
|
1712
|
-
}
|
|
1713
|
-
enqueueWaiter(state, waiter) {
|
|
1714
|
-
state.queue.push(waiter);
|
|
1715
|
-
}
|
|
1716
|
-
dequeueWaiter(state) {
|
|
1717
|
-
if (state.queue.length === 0) return void 0;
|
|
1718
|
-
if (this.config.queueStrategy === "fifo") return state.queue.shift();
|
|
1719
|
-
let bestIdx = 0;
|
|
1720
|
-
for (let i = 1; i < state.queue.length; i++) {
|
|
1721
|
-
const candidate = state.queue[i];
|
|
1722
|
-
const best = state.queue[bestIdx];
|
|
1723
|
-
if (candidate.priority < best.priority || candidate.priority === best.priority && candidate.arrivalOrder < best.arrivalOrder) {
|
|
1724
|
-
bestIdx = i;
|
|
1725
|
-
}
|
|
1726
|
-
}
|
|
1727
|
-
return state.queue.splice(bestIdx, 1)[0];
|
|
1728
|
-
}
|
|
1729
|
-
tryEvictLowerPriorityWaiter(state, newPriority) {
|
|
1730
|
-
if (this.config.queueLoadShedding !== "priority-evict" || state.queue.length === 0) {
|
|
1731
|
-
return false;
|
|
1732
|
-
}
|
|
1733
|
-
let evictIdx = -1;
|
|
1734
|
-
for (let i = 0; i < state.queue.length; i++) {
|
|
1735
|
-
const candidate = state.queue[i];
|
|
1736
|
-
if (candidate.priority <= newPriority) continue;
|
|
1737
|
-
if (evictIdx < 0) {
|
|
1738
|
-
evictIdx = i;
|
|
1739
|
-
continue;
|
|
1740
|
-
}
|
|
1741
|
-
const current = state.queue[evictIdx];
|
|
1742
|
-
if (candidate.priority > current.priority || candidate.priority === current.priority && candidate.arrivalOrder > current.arrivalOrder) {
|
|
1743
|
-
evictIdx = i;
|
|
1744
|
-
}
|
|
1745
|
-
}
|
|
1746
|
-
if (evictIdx < 0) return false;
|
|
1747
|
-
const [evicted] = state.queue.splice(evictIdx, 1);
|
|
1748
|
-
if (!evicted) return false;
|
|
1749
|
-
this.cleanupWaiter(evicted);
|
|
1750
|
-
state.evictedWhileQueued++;
|
|
1751
|
-
state.rejected++;
|
|
1752
|
-
state.rejectionStreak++;
|
|
1753
|
-
evicted.reject(poolRejectedError2(state.key, this.config.maxQueue, this.suggestedBackoffMs(state)));
|
|
1754
|
-
return true;
|
|
1755
|
-
}
|
|
1756
|
-
suggestedBackoffMs(state) {
|
|
1757
|
-
if (this.config.rejectionBackoffMs === void 0) return void 0;
|
|
1758
|
-
if (state.rejectionStreak < this.config.rejectionBackoffThreshold) return void 0;
|
|
1759
|
-
const multiplier = Math.min(8, Math.max(1, state.rejectionStreak - this.config.rejectionBackoffThreshold + 1));
|
|
1760
|
-
return this.config.rejectionBackoffMs * multiplier;
|
|
1761
|
-
}
|
|
1762
|
-
drain(state) {
|
|
1763
|
-
while (state.inFlight < state.limit && state.queue.length > 0) {
|
|
1764
|
-
const waiter = this.dequeueWaiter(state);
|
|
1765
|
-
this.cleanupWaiter(waiter);
|
|
1766
|
-
if (waiter.signal.aborted) {
|
|
1767
|
-
this.touch(state);
|
|
1768
|
-
state.abortedWhileQueued++;
|
|
1769
|
-
waiter.reject(abortError2());
|
|
1770
|
-
continue;
|
|
1771
|
-
}
|
|
1772
|
-
this.touch(state);
|
|
1773
|
-
state.inFlight++;
|
|
1774
|
-
state.acquired++;
|
|
1775
|
-
state.rejectionStreak = 0;
|
|
1776
|
-
waiter.resolve(this.makeLease(state));
|
|
1777
|
-
}
|
|
1778
|
-
}
|
|
1779
|
-
currentLatency(state, emaValue) {
|
|
1780
|
-
const percentileLatency = this.config.percentile === "p99" ? this.windowPercentile(state, 99) : this.windowPercentile(state, 50);
|
|
1781
|
-
return percentileLatency ?? emaValue;
|
|
1782
|
-
}
|
|
1783
|
-
touch(state) {
|
|
1784
|
-
state.lastActivityAt = monotonicNow();
|
|
1785
|
-
state.lastActivityTimestamp = wallNow();
|
|
1786
|
-
this.clearIdleTimer(state);
|
|
1787
|
-
}
|
|
1788
|
-
scheduleIdleEviction(state) {
|
|
1789
|
-
if (this.destroyed || this.config.stateTtlMs === void 0) return;
|
|
1790
|
-
this.clearIdleTimer(state);
|
|
1791
|
-
if (state.inFlight > 0 || state.queue.length > 0) return;
|
|
1792
|
-
const delay = Math.max(1, this.config.stateTtlMs - (monotonicNow() - state.lastActivityAt));
|
|
1793
|
-
state.ttlTimer = setTimeout(() => {
|
|
1794
|
-
state.ttlTimer = void 0;
|
|
1795
|
-
this.evictIdleState(state, monotonicNow());
|
|
1796
|
-
}, delay);
|
|
1797
|
-
state.ttlTimer?.unref?.();
|
|
1798
|
-
}
|
|
1799
|
-
clearIdleTimer(state) {
|
|
1800
|
-
if (state.ttlTimer !== void 0) {
|
|
1801
|
-
clearTimeout(state.ttlTimer);
|
|
1802
|
-
state.ttlTimer = void 0;
|
|
1803
|
-
}
|
|
1804
|
-
}
|
|
1805
|
-
evictIdleStates() {
|
|
1806
|
-
if (this.config.stateTtlMs === void 0) return;
|
|
1807
|
-
const now = monotonicNow();
|
|
1808
|
-
for (const state of Array.from(this.states.values())) {
|
|
1809
|
-
this.evictIdleState(state, now);
|
|
1810
|
-
}
|
|
1811
|
-
}
|
|
1812
|
-
evictIdleState(state, now) {
|
|
1813
|
-
if (this.config.stateTtlMs === void 0) return;
|
|
1814
|
-
if (state.inFlight > 0 || state.queue.length > 0) {
|
|
1815
|
-
this.scheduleIdleEviction(state);
|
|
1816
|
-
return;
|
|
1817
|
-
}
|
|
1818
|
-
if (now - state.lastActivityAt < this.config.stateTtlMs) {
|
|
1819
|
-
this.scheduleIdleEviction(state);
|
|
1820
|
-
return;
|
|
1821
|
-
}
|
|
1822
|
-
this.clearIdleTimer(state);
|
|
1823
|
-
this.states.delete(state.key);
|
|
1824
|
-
}
|
|
1825
|
-
cleanupWaiter(waiter) {
|
|
1826
|
-
if (waiter.timer !== void 0) {
|
|
1827
|
-
clearTimeout(waiter.timer);
|
|
1828
|
-
waiter.timer = void 0;
|
|
1829
|
-
}
|
|
1830
|
-
if (waiter.abort) {
|
|
1831
|
-
waiter.signal.removeEventListener("abort", waiter.abort);
|
|
1832
|
-
waiter.abort = void 0;
|
|
1833
|
-
}
|
|
1834
|
-
}
|
|
1835
|
-
removeWaiter(state, waiter) {
|
|
1836
|
-
const idx = state.queue.indexOf(waiter);
|
|
1837
|
-
if (idx >= 0) state.queue.splice(idx, 1);
|
|
1838
|
-
}
|
|
1839
|
-
stateToStats(state, now = monotonicNow()) {
|
|
1840
|
-
const minLatency = state.window.min();
|
|
1841
|
-
const elapsedSeconds = Math.max((now - state.createdAt) / 1e3, 1e-3);
|
|
1842
|
-
const attempts = state.acquired + state.rejected + state.queueTimeouts + state.abortedWhileQueued;
|
|
1843
|
-
return {
|
|
1844
|
-
limit: state.limit,
|
|
1845
|
-
inFlight: state.inFlight,
|
|
1846
|
-
queueDepth: state.queue.length,
|
|
1847
|
-
gradient: state.lastGradient,
|
|
1848
|
-
latencyGradient: state.lastLatencyGradient,
|
|
1849
|
-
errorRate: state.errorEma.value,
|
|
1850
|
-
smoothedLatency: state.ema.value,
|
|
1851
|
-
minLatency,
|
|
1852
|
-
baselineLatency: minLatency === void 0 ? void 0 : this.baselineLatency(state, minLatency),
|
|
1853
|
-
p5: this.windowPercentile(state, 5),
|
|
1854
|
-
p50: this.windowPercentile(state, 50),
|
|
1855
|
-
p99: this.windowPercentile(state, 99),
|
|
1856
|
-
probeCount: state.probeCount,
|
|
1857
|
-
windowSize: state.window.length,
|
|
1858
|
-
warmupCompletions: state.warmupCompletions,
|
|
1859
|
-
slowStart: state.slowStartActive,
|
|
1860
|
-
cooldownSamplesRemaining: state.decreaseCooldownRemaining,
|
|
1861
|
-
utilization: state.limit > 0 ? state.inFlight / state.limit : 0,
|
|
1862
|
-
requestsPerSecond: state.acquired / elapsedSeconds,
|
|
1863
|
-
completionsPerSecond: state.released / elapsedSeconds,
|
|
1864
|
-
rejectionRate: attempts > 0 ? state.rejected / attempts : 0,
|
|
1865
|
-
suggestedBackoffMs: this.suggestedBackoffMs(state),
|
|
1866
|
-
stateCount: this.states.size
|
|
1867
|
-
};
|
|
1868
|
-
}
|
|
1869
|
-
stateToKeySnapshot(state) {
|
|
1870
|
-
return {
|
|
1871
|
-
...this.stateToStats(state),
|
|
1872
|
-
key: state.key,
|
|
1873
|
-
createdAt: state.createdAt,
|
|
1874
|
-
createdTimestamp: state.createdTimestamp,
|
|
1875
|
-
lastActivityAt: state.lastActivityAt,
|
|
1876
|
-
lastActivityTimestamp: state.lastActivityTimestamp,
|
|
1877
|
-
nextProbeAt: state.nextProbeAt,
|
|
1878
|
-
warmupDone: state.warmupDone,
|
|
1879
|
-
saturationStreak: state.saturationStreak,
|
|
1880
|
-
slowStartRecoveryStarted: state.slowStartRecoveryStarted,
|
|
1881
|
-
history: [...state.history],
|
|
1882
|
-
acquired: state.acquired,
|
|
1883
|
-
released: state.released,
|
|
1884
|
-
rejected: state.rejected,
|
|
1885
|
-
queueTimeouts: state.queueTimeouts,
|
|
1886
|
-
abortedWhileQueued: state.abortedWhileQueued,
|
|
1887
|
-
evictedWhileQueued: state.evictedWhileQueued
|
|
1888
|
-
};
|
|
1889
|
-
}
|
|
1890
|
-
};
|
|
1891
|
-
|
|
1892
|
-
// src/http/optics/lens.ts
|
|
1893
|
-
var Lens = {
|
|
1894
|
-
make(get, set) {
|
|
1895
|
-
return { get, set };
|
|
1896
|
-
},
|
|
1897
|
-
over(ln, f) {
|
|
1898
|
-
return (s) => ln.set(f(ln.get(s)))(s);
|
|
1899
|
-
},
|
|
1900
|
-
compose(ab, sa) {
|
|
1901
|
-
return Lens.make(
|
|
1902
|
-
(s) => ab.get(sa.get(s)),
|
|
1903
|
-
(b) => (s) => sa.set(ab.set(b)(sa.get(s)))(s)
|
|
1904
|
-
);
|
|
1905
|
-
}
|
|
1906
|
-
};
|
|
1907
|
-
|
|
1908
|
-
// src/http/optics/request.ts
|
|
1909
|
-
var Request = {
|
|
1910
|
-
headers: Lens.make(
|
|
1911
|
-
(req) => req.headers ?? {},
|
|
1912
|
-
(headers) => (req) => ({ ...req, headers })
|
|
1913
|
-
)
|
|
1914
|
-
};
|
|
1915
|
-
var mergeHeaders = (extra) => (req) => Lens.over(Request.headers, (h) => ({ ...h, ...extra }))(req);
|
|
1916
|
-
var mergeHeadersUnder = (under) => (req) => Lens.over(Request.headers, (h) => ({ ...under, ...h }))(req);
|
|
1917
|
-
var setHeaderIfMissing = (k, v) => (req) => Lens.over(Request.headers, (h) => h[k] ? h : { ...h, [k]: v })(req);
|
|
1918
|
-
|
|
1919
|
-
// src/http/transport.ts
|
|
1920
|
-
var isTaggedHttpError = (error) => {
|
|
1921
|
-
return isHttpError(error);
|
|
1922
|
-
};
|
|
1923
|
-
var isAbortError = (error) => isExternalAbortError(error);
|
|
1924
|
-
var normalizeHttpError = (error, options) => toHttpError(error, options);
|
|
1925
|
-
var headersOf = (response) => {
|
|
1926
|
-
const headers = {};
|
|
1927
|
-
response.headers.forEach((value, key) => {
|
|
1928
|
-
headers[key] = value;
|
|
1929
|
-
});
|
|
1930
|
-
return headers;
|
|
1931
|
-
};
|
|
1932
|
-
function normalizeHttpHeaders(headers) {
|
|
1933
|
-
if (!headers) return {};
|
|
1934
|
-
if (typeof Headers !== "undefined" && headers instanceof Headers) {
|
|
1935
|
-
const out = {};
|
|
1936
|
-
headers.forEach((value, key) => {
|
|
1937
|
-
out[key] = value;
|
|
1938
|
-
});
|
|
1939
|
-
return out;
|
|
1940
|
-
}
|
|
1941
|
-
if (Array.isArray(headers)) {
|
|
1942
|
-
return headers.reduce((acc, item) => {
|
|
1943
|
-
if (!Array.isArray(item) || item.length < 2) return acc;
|
|
1944
|
-
const [key, value] = item;
|
|
1945
|
-
if (key !== void 0 && value !== void 0 && value !== null) {
|
|
1946
|
-
acc[String(key)] = Array.isArray(value) ? value.map(String).join(", ") : String(value);
|
|
1947
|
-
}
|
|
1948
|
-
return acc;
|
|
1949
|
-
}, {});
|
|
1950
|
-
}
|
|
1951
|
-
if (typeof headers.toJSON === "function") {
|
|
1952
|
-
return normalizeHttpHeaders(headers.toJSON());
|
|
1953
|
-
}
|
|
1954
|
-
if (typeof headers === "object") {
|
|
1955
|
-
return Object.entries(headers).reduce((acc, [key, value]) => {
|
|
1956
|
-
if (value !== void 0 && value !== null) {
|
|
1957
|
-
acc[key] = Array.isArray(value) ? value.map(String).join(", ") : String(value);
|
|
1958
|
-
}
|
|
1959
|
-
return acc;
|
|
1960
|
-
}, {});
|
|
1961
|
-
}
|
|
1962
|
-
return {};
|
|
1963
|
-
}
|
|
1964
|
-
var abortErrorForSignal = (signal) => {
|
|
1965
|
-
const reason = signal.reason;
|
|
1966
|
-
if (isTaggedHttpError(reason) && reason._tag === "Timeout") return reason;
|
|
1967
|
-
return { _tag: "Abort" };
|
|
1968
|
-
};
|
|
1969
|
-
var linkAbortSignals = (...signals) => {
|
|
1970
|
-
const activeSignals = signals.filter((signal) => signal !== void 0);
|
|
1971
|
-
if (activeSignals.length === 0) {
|
|
1972
|
-
const controller2 = new AbortController();
|
|
1973
|
-
return { signal: controller2.signal, cleanup: () => void 0 };
|
|
1974
|
-
}
|
|
1975
|
-
const controller = new AbortController();
|
|
1976
|
-
const listeners = [];
|
|
1977
|
-
const abortFrom = (signal) => {
|
|
1978
|
-
try {
|
|
1979
|
-
controller.abort(signal.reason);
|
|
1980
|
-
} catch {
|
|
1981
|
-
controller.abort();
|
|
1982
|
-
}
|
|
1983
|
-
};
|
|
1984
|
-
for (const signal of activeSignals) {
|
|
1985
|
-
if (signal.aborted) {
|
|
1986
|
-
abortFrom(signal);
|
|
1987
|
-
break;
|
|
1988
|
-
}
|
|
1989
|
-
const abort = () => abortFrom(signal);
|
|
1990
|
-
signal.addEventListener("abort", abort, { once: true });
|
|
1991
|
-
listeners.push({ signal, abort });
|
|
1992
|
-
}
|
|
1993
|
-
return {
|
|
1994
|
-
signal: controller.signal,
|
|
1995
|
-
cleanup: () => {
|
|
1996
|
-
for (const listener of listeners) {
|
|
1997
|
-
listener.signal.removeEventListener("abort", listener.abort);
|
|
1998
|
-
}
|
|
1999
|
-
listeners.length = 0;
|
|
2000
|
-
}
|
|
2001
|
-
};
|
|
2002
|
-
};
|
|
2003
|
-
var nowMs = () => typeof performance !== "undefined" && typeof performance.now === "function" ? performance.now() : Date.now();
|
|
2004
|
-
var hasMethod = (value, name) => typeof value === "object" && value !== null && typeof value[name] === "function";
|
|
2005
|
-
var defaultPromiseBody = (response, mode) => {
|
|
2006
|
-
if (mode === "json" && hasMethod(response, "json")) return response.json();
|
|
2007
|
-
if (mode === "text" && hasMethod(response, "text")) return response.text();
|
|
2008
|
-
if (typeof response === "object" && response !== null) {
|
|
2009
|
-
const record = response;
|
|
2010
|
-
if ("data" in record) return record.data;
|
|
2011
|
-
if ("bodyText" in record) return record.bodyText;
|
|
2012
|
-
if ("body" in record) return record.body;
|
|
2013
|
-
}
|
|
2014
|
-
return response;
|
|
2015
|
-
};
|
|
2016
|
-
var encodePromiseBody = (body, mode) => {
|
|
2017
|
-
if (mode === "text") return body === void 0 || body === null ? "" : String(body);
|
|
2018
|
-
const encoded = JSON.stringify(body);
|
|
2019
|
-
return encoded === void 0 ? "" : encoded;
|
|
2020
|
-
};
|
|
2021
|
-
var finiteNumber = (value) => {
|
|
2022
|
-
const n = typeof value === "number" ? value : Number(value);
|
|
2023
|
-
return Number.isFinite(n) ? n : void 0;
|
|
2024
|
-
};
|
|
2025
|
-
var isPlainRecord = (value) => Object.prototype.toString.call(value) === "[object Object]";
|
|
2026
|
-
var injectSignal = (config, signal) => isPlainRecord(config) ? { ...config, signal } : config;
|
|
2027
|
-
var inferPromiseResponseInfo = (response) => {
|
|
2028
|
-
if (typeof response !== "object" || response === null) {
|
|
2029
|
-
return { status: 200, statusText: "", headers: {} };
|
|
2030
|
-
}
|
|
2031
|
-
const record = response;
|
|
2032
|
-
return {
|
|
2033
|
-
status: finiteNumber(record.status) ?? finiteNumber(record.statusCode) ?? 200,
|
|
2034
|
-
statusText: typeof record.statusText === "string" ? record.statusText : typeof record.statusMessage === "string" ? record.statusMessage : "",
|
|
2035
|
-
headers: record.headers,
|
|
2036
|
-
ms: finiteNumber(record.ms)
|
|
2037
|
-
};
|
|
2038
|
-
};
|
|
2039
|
-
var toPromiseTransportResponse = (bodyText, info) => ({
|
|
2040
|
-
status: info.status ?? 200,
|
|
2041
|
-
statusText: info.statusText ?? "",
|
|
2042
|
-
headers: normalizeHttpHeaders(info.headers),
|
|
2043
|
-
bodyText,
|
|
2044
|
-
...info.ms !== void 0 ? { ms: info.ms } : {},
|
|
2045
|
-
...info.transportMeta !== void 0 ? { transportMeta: info.transportMeta } : {}
|
|
2046
|
-
});
|
|
2047
|
-
function makePromiseHttpTransport(config) {
|
|
2048
|
-
return (context) => asyncEffect((_env, cb) => {
|
|
2049
|
-
let done = false;
|
|
2050
|
-
const finish = (exit) => {
|
|
2051
|
-
if (done) return;
|
|
2052
|
-
done = true;
|
|
2053
|
-
context.signal.removeEventListener("abort", abort);
|
|
2054
|
-
cb(exit);
|
|
2055
|
-
};
|
|
2056
|
-
const fail = (error) => finish({ _tag: "Failure", cause: Cause.fail(error) });
|
|
2057
|
-
const abort = () => fail(abortErrorForSignal(context.signal));
|
|
2058
|
-
if (context.signal.aborted) {
|
|
2059
|
-
abort();
|
|
2060
|
-
return;
|
|
2061
|
-
}
|
|
2062
|
-
context.signal.addEventListener("abort", abort, { once: true });
|
|
2063
|
-
const run = async () => {
|
|
2064
|
-
const startedAt = nowMs();
|
|
2065
|
-
try {
|
|
2066
|
-
const raw = await config.request(context);
|
|
2067
|
-
const durationMs = Math.round(nowMs() - startedAt);
|
|
2068
|
-
const mapped = await config.response(raw, context, { startedAt, durationMs });
|
|
2069
|
-
finish({
|
|
2070
|
-
_tag: "Success",
|
|
2071
|
-
value: {
|
|
2072
|
-
...mapped,
|
|
2073
|
-
ms: mapped.ms ?? durationMs
|
|
2074
|
-
}
|
|
2075
|
-
});
|
|
2076
|
-
} catch (error) {
|
|
2077
|
-
if (context.signal.aborted) {
|
|
2078
|
-
fail(abortErrorForSignal(context.signal));
|
|
2079
|
-
return;
|
|
2080
|
-
}
|
|
2081
|
-
fail(config.error?.(error, context) ?? normalizeHttpError(error, { signal: context.signal }));
|
|
2082
|
-
}
|
|
2083
|
-
};
|
|
2084
|
-
void run();
|
|
2085
|
-
return () => {
|
|
2086
|
-
if (done) return;
|
|
2087
|
-
done = true;
|
|
2088
|
-
context.signal.removeEventListener("abort", abort);
|
|
2089
|
-
};
|
|
2090
|
-
});
|
|
2091
|
-
}
|
|
2092
|
-
function makePromiseHttpTransportBodyBuilder(config) {
|
|
2093
|
-
const makeBodyTransport = (mode, body, responseMapper, currentConfig = config) => makePromiseHttpTransport({
|
|
2094
|
-
...currentConfig,
|
|
2095
|
-
response: async (raw, context, timing) => {
|
|
2096
|
-
const selected = await (body ?? ((value) => defaultPromiseBody(value, mode)))(raw, context, timing);
|
|
2097
|
-
const inferred = inferPromiseResponseInfo(raw);
|
|
2098
|
-
const mapped = responseMapper ? await responseMapper(raw, context, timing) : {};
|
|
2099
|
-
return toPromiseTransportResponse(encodePromiseBody(selected, mode), {
|
|
2100
|
-
...inferred,
|
|
2101
|
-
...mapped,
|
|
2102
|
-
headers: mapped.headers ?? inferred.headers
|
|
2103
|
-
});
|
|
2104
|
-
}
|
|
2105
|
-
});
|
|
2106
|
-
const makeFluentResponseBuilder = (mode, body, currentConfig = config) => {
|
|
2107
|
-
return {
|
|
2108
|
-
response: (responseMapper) => makeBodyTransport(mode, body, responseMapper, currentConfig),
|
|
2109
|
-
error: (error) => makeFluentResponseBuilder(mode, body, { ...currentConfig, error })
|
|
2110
|
-
};
|
|
2111
|
-
};
|
|
2112
|
-
return {
|
|
2113
|
-
response: (response) => makePromiseHttpTransport({ ...config, response }),
|
|
2114
|
-
json: (body, response) => makeBodyTransport("json", body, response),
|
|
2115
|
-
text: (body, response) => makeBodyTransport("text", body, response),
|
|
2116
|
-
fromJson: (body) => makeFluentResponseBuilder("json", body),
|
|
2117
|
-
fromText: (body) => makeFluentResponseBuilder("text", body),
|
|
2118
|
-
error: (error) => makePromiseHttpTransportBodyBuilder({ ...config, error })
|
|
2119
|
-
};
|
|
2120
|
-
}
|
|
2121
|
-
function makePromiseHttpTransportRequestConfigBuilder(requestConfig) {
|
|
2122
|
-
return {
|
|
2123
|
-
send: (send) => makePromiseHttpTransportBodyBuilder({
|
|
2124
|
-
request: async (context) => {
|
|
2125
|
-
const config = await requestConfig({
|
|
2126
|
-
request: context.request,
|
|
2127
|
-
url: context.url
|
|
2128
|
-
});
|
|
2129
|
-
return send(injectSignal(config, context.signal));
|
|
2130
|
-
}
|
|
2131
|
-
})
|
|
2132
|
-
};
|
|
2133
|
-
}
|
|
2134
|
-
function promiseHttpTransport() {
|
|
2135
|
-
return {
|
|
2136
|
-
request: (request) => makePromiseHttpTransportBodyBuilder({ request }),
|
|
2137
|
-
requestConfig: (requestConfig) => makePromiseHttpTransportRequestConfigBuilder(requestConfig)
|
|
2138
|
-
};
|
|
2139
|
-
}
|
|
2140
|
-
var fetchUnavailableError = () => ({
|
|
2141
|
-
_tag: "FetchError",
|
|
2142
|
-
message: "global `fetch` is not available; provide MakeHttpConfig.transport/streamTransport or run in an environment with fetch support."
|
|
2143
|
-
});
|
|
2144
|
-
function makeFetchTransport() {
|
|
2145
|
-
return ({ request, url, signal }) => asyncEffect((_env, cb) => {
|
|
2146
|
-
if (typeof fetch === "undefined") {
|
|
2147
|
-
cb({ _tag: "Failure", cause: Cause.fail(fetchUnavailableError()) });
|
|
2148
|
-
return;
|
|
2149
|
-
}
|
|
2150
|
-
const localController = new AbortController();
|
|
2151
|
-
const linkedSignal = linkAbortSignals(signal, request.init?.signal, localController.signal);
|
|
2152
|
-
let done = false;
|
|
2153
|
-
const finish = (exit) => {
|
|
2154
|
-
if (done) return;
|
|
2155
|
-
done = true;
|
|
2156
|
-
linkedSignal.cleanup();
|
|
2157
|
-
cb(exit);
|
|
2158
|
-
};
|
|
2159
|
-
const run = async () => {
|
|
2160
|
-
try {
|
|
2161
|
-
if (linkedSignal.signal.aborted) throw abortErrorForSignal(linkedSignal.signal);
|
|
2162
|
-
const started = nowMs();
|
|
2163
|
-
const response = await fetch(url, {
|
|
2164
|
-
...request.init ?? {},
|
|
2165
|
-
method: request.method,
|
|
2166
|
-
headers: Request.headers.get(request),
|
|
2167
|
-
body: request.body,
|
|
2168
|
-
signal: linkedSignal.signal
|
|
2169
|
-
});
|
|
2170
|
-
const bodyText = await response.text();
|
|
2171
|
-
const latencyMs = Math.round(nowMs() - started);
|
|
2172
|
-
finish({
|
|
2173
|
-
_tag: "Success",
|
|
2174
|
-
value: {
|
|
2175
|
-
status: response.status,
|
|
2176
|
-
statusText: response.statusText,
|
|
2177
|
-
headers: headersOf(response),
|
|
2178
|
-
bodyText,
|
|
2179
|
-
ms: latencyMs
|
|
2180
|
-
}
|
|
2181
|
-
});
|
|
2182
|
-
} catch (error) {
|
|
2183
|
-
finish({
|
|
2184
|
-
_tag: "Failure",
|
|
2185
|
-
cause: Cause.fail(normalizeHttpError(error, { signal: linkedSignal.signal }))
|
|
2186
|
-
});
|
|
2187
|
-
}
|
|
2188
|
-
};
|
|
2189
|
-
void run();
|
|
2190
|
-
return () => {
|
|
2191
|
-
if (done) return;
|
|
2192
|
-
try {
|
|
2193
|
-
localController.abort();
|
|
2194
|
-
} catch {
|
|
2195
|
-
}
|
|
2196
|
-
};
|
|
2197
|
-
});
|
|
2198
|
-
}
|
|
2199
|
-
function makeFetchStreamTransport() {
|
|
2200
|
-
return ({ request, url, signal }) => asyncEffect((_env, cb) => {
|
|
2201
|
-
if (typeof fetch === "undefined") {
|
|
2202
|
-
cb({ _tag: "Failure", cause: Cause.fail(fetchUnavailableError()) });
|
|
2203
|
-
return;
|
|
2204
|
-
}
|
|
2205
|
-
const localController = new AbortController();
|
|
2206
|
-
const linkedSignal = linkAbortSignals(signal, request.init?.signal, localController.signal);
|
|
2207
|
-
let done = false;
|
|
2208
|
-
let cleanupTransferredToBody = false;
|
|
2209
|
-
const cleanup = () => {
|
|
2210
|
-
if (!cleanupTransferredToBody) linkedSignal.cleanup();
|
|
2211
|
-
};
|
|
2212
|
-
const finish = (exit) => {
|
|
2213
|
-
if (done) return;
|
|
2214
|
-
done = true;
|
|
2215
|
-
cleanup();
|
|
2216
|
-
cb(exit);
|
|
2217
|
-
};
|
|
2218
|
-
const run = async () => {
|
|
2219
|
-
try {
|
|
2220
|
-
if (linkedSignal.signal.aborted) throw abortErrorForSignal(linkedSignal.signal);
|
|
2221
|
-
const started = nowMs();
|
|
2222
|
-
const response = await fetch(url, {
|
|
2223
|
-
...request.init ?? {},
|
|
2224
|
-
method: request.method,
|
|
2225
|
-
headers: Request.headers.get(request),
|
|
2226
|
-
body: request.body,
|
|
2227
|
-
signal: linkedSignal.signal
|
|
2228
|
-
});
|
|
2229
|
-
const headers = headersOf(response);
|
|
2230
|
-
const latencyMs = Math.round(nowMs() - started);
|
|
2231
|
-
const body = streamFromReadableStream(response.body, normalizeHttpError, {
|
|
2232
|
-
signal: linkedSignal.signal,
|
|
2233
|
-
onRelease: linkedSignal.cleanup
|
|
2234
|
-
});
|
|
2235
|
-
cleanupTransferredToBody = response.body !== null;
|
|
2236
|
-
finish({
|
|
2237
|
-
_tag: "Success",
|
|
2238
|
-
value: {
|
|
2239
|
-
status: response.status,
|
|
2240
|
-
statusText: response.statusText,
|
|
2241
|
-
headers,
|
|
2242
|
-
body,
|
|
2243
|
-
ms: latencyMs
|
|
2244
|
-
}
|
|
2245
|
-
});
|
|
2246
|
-
} catch (error) {
|
|
2247
|
-
finish({
|
|
2248
|
-
_tag: "Failure",
|
|
2249
|
-
cause: Cause.fail(normalizeHttpError(error, { signal: linkedSignal.signal }))
|
|
2250
|
-
});
|
|
2251
|
-
}
|
|
2252
|
-
};
|
|
2253
|
-
void run();
|
|
2254
|
-
return () => {
|
|
2255
|
-
if (done) return;
|
|
2256
|
-
try {
|
|
2257
|
-
localController.abort();
|
|
2258
|
-
} catch {
|
|
2259
|
-
}
|
|
2260
|
-
};
|
|
2261
|
-
});
|
|
2262
|
-
}
|
|
2263
|
-
|
|
2264
|
-
// src/http/configValidation.ts
|
|
2265
|
-
var fn = Schema.custom((value) => typeof value === "function", "function");
|
|
2266
|
-
var object = Schema.custom(
|
|
2267
|
-
(value) => typeof value === "object" && value !== null && !Array.isArray(value),
|
|
2268
|
-
"object"
|
|
2269
|
-
);
|
|
2270
|
-
var keyResolver = Schema.union([
|
|
2271
|
-
Schema.enum(["global", "origin", "host"]),
|
|
2272
|
-
fn
|
|
2273
|
-
]);
|
|
2274
|
-
var headroomStrategyConfig = Schema.union([
|
|
2275
|
-
Schema.number({ min: 1 }),
|
|
2276
|
-
Schema.enum(["fixed", "proportional"]),
|
|
2277
|
-
fn,
|
|
2278
|
-
Schema.object({
|
|
2279
|
-
type: Schema.literal("fixed"),
|
|
2280
|
-
value: Schema.number({ min: 1 }).optional()
|
|
2281
|
-
}, { unknownKeys: "passthrough" }),
|
|
2282
|
-
Schema.object({
|
|
2283
|
-
type: Schema.literal("proportional"),
|
|
2284
|
-
ratio: Schema.number({ min: 0 }).refine((n) => n > 0, "ratio must be > 0").optional(),
|
|
2285
|
-
min: Schema.number({ min: 1 }).optional(),
|
|
2286
|
-
max: Schema.number({ min: 1 }).optional()
|
|
2287
|
-
}, { unknownKeys: "passthrough" })
|
|
2288
|
-
]);
|
|
2289
|
-
var poolConfig = Schema.union([
|
|
2290
|
-
Schema.literal(false),
|
|
2291
|
-
Schema.object({
|
|
2292
|
-
concurrency: Schema.number({ min: 1, int: true }).optional(),
|
|
2293
|
-
maxQueue: Schema.number({ min: 0, int: true }).optional(),
|
|
2294
|
-
queueTimeoutMs: Schema.number({ min: 1, int: true }).optional(),
|
|
2295
|
-
key: keyResolver.optional(),
|
|
2296
|
-
engine: Schema.enum(["ts", "wasm"]).optional(),
|
|
2297
|
-
wasm: Schema.boolean().optional()
|
|
2298
|
-
}, { unknownKeys: "passthrough" })
|
|
2299
|
-
]);
|
|
2300
|
-
var adaptiveLimiterConfig = Schema.union([
|
|
2301
|
-
Schema.literal(false),
|
|
2302
|
-
Schema.object({
|
|
2303
|
-
preset: Schema.enum(["conservative", "balanced", "aggressive"]).optional(),
|
|
2304
|
-
initialLimit: Schema.number({ min: 1, int: true }).optional(),
|
|
2305
|
-
minLimit: Schema.number({ min: 1, int: true }).optional(),
|
|
2306
|
-
maxLimit: Schema.number({ min: 1, int: true }).optional(),
|
|
2307
|
-
smoothingFactor: Schema.number({ min: 0, max: 1 }).refine((n) => n > 0, "smoothingFactor must be in (0, 1]").optional(),
|
|
2308
|
-
probeInterval: Schema.number({ min: 1, int: true }).optional(),
|
|
2309
|
-
probeJitterRatio: Schema.number({ min: 0, max: 1 }).optional(),
|
|
2310
|
-
windowSize: Schema.number({ min: 2, int: true }).optional(),
|
|
2311
|
-
minSamples: Schema.number({ min: 1, int: true }).optional(),
|
|
2312
|
-
baselineStrategy: Schema.enum(["min", "p5", "ema-low"]).optional(),
|
|
2313
|
-
decreaseCooldownSamples: Schema.number({ min: 0, int: true }).optional(),
|
|
2314
|
-
historySize: Schema.number({ min: 0, int: true }).optional(),
|
|
2315
|
-
windowDecayFactor: Schema.number({ min: 0, max: 1 }).refine((n) => n > 0, "windowDecayFactor must be in (0, 1]").optional(),
|
|
2316
|
-
errorWeight: Schema.number({ min: 0, max: 1 }).optional(),
|
|
2317
|
-
errorSmoothingFactor: Schema.number({ min: 0, max: 1 }).refine((n) => n > 0, "errorSmoothingFactor must be in (0, 1]").optional(),
|
|
2318
|
-
errorStatusThreshold: Schema.number({ min: 100, max: 599, int: true }).optional(),
|
|
2319
|
-
queueStrategy: Schema.enum(["fifo", "priority"]).optional(),
|
|
2320
|
-
queueLoadShedding: Schema.enum(["reject-new", "priority-evict"]).optional(),
|
|
2321
|
-
rejectionBackoffThreshold: Schema.number({ min: 1, int: true }).optional(),
|
|
2322
|
-
rejectionBackoffMs: Schema.number({ min: 1, int: true }).optional(),
|
|
2323
|
-
stateTtlMs: Schema.union([Schema.literal(false), Schema.number({ min: 1, int: true })]).optional(),
|
|
2324
|
-
warmupRequests: Schema.number({ min: 0, int: true }).optional(),
|
|
2325
|
-
decreaseThreshold: Schema.number({ min: 0, max: 1 }).refine((n) => n > 0, "decreaseThreshold must be in (0, 1]").optional(),
|
|
2326
|
-
increaseThreshold: Schema.number({ min: 1 }).optional(),
|
|
2327
|
-
maxDecreaseRatio: Schema.number({ min: 0, max: 1 }).refine((n) => n > 0, "maxDecreaseRatio must be in (0, 1]").optional(),
|
|
2328
|
-
headroomStrategy: headroomStrategyConfig.optional(),
|
|
2329
|
-
slowStartRecovery: Schema.boolean().optional(),
|
|
2330
|
-
slowStartSaturationThreshold: Schema.number({ min: 0, max: 1 }).refine((n) => n > 0, "slowStartSaturationThreshold must be in (0, 1]").optional(),
|
|
2331
|
-
slowStartSaturationSamples: Schema.number({ min: 1, int: true }).optional(),
|
|
2332
|
-
key: keyResolver.optional(),
|
|
2333
|
-
maxQueue: Schema.number({ min: 0, int: true }).optional(),
|
|
2334
|
-
queueTimeoutMs: Schema.number({ min: 1, int: true }).optional(),
|
|
2335
|
-
onLimitChange: fn.optional(),
|
|
2336
|
-
percentile: Schema.enum(["p50", "p99"]).optional()
|
|
2337
|
-
}, { unknownKeys: "passthrough" })
|
|
2338
|
-
]);
|
|
2339
|
-
var cacheConfig = Schema.union([
|
|
2340
|
-
Schema.literal(false),
|
|
2341
|
-
Schema.object({
|
|
2342
|
-
ttlSeconds: Schema.number({ min: 1, int: true }).optional(),
|
|
2343
|
-
maxEntries: Schema.number({ min: 1, int: true }).optional(),
|
|
2344
|
-
staleWhileRevalidate: Schema.boolean().optional(),
|
|
2345
|
-
cachePolicy: fn.optional(),
|
|
2346
|
-
cacheRelevantHeaders: Schema.array(Schema.string()).optional(),
|
|
2347
|
-
onEvent: fn.optional(),
|
|
2348
|
-
onLifecycleEvent: fn.optional()
|
|
2349
|
-
}, { unknownKeys: "passthrough" })
|
|
2350
|
-
]);
|
|
2351
|
-
var priorityConfig = Schema.union([
|
|
2352
|
-
Schema.literal(false),
|
|
2353
|
-
Schema.object({
|
|
2354
|
-
concurrency: Schema.number({ min: 1, int: true }).optional(),
|
|
2355
|
-
queueTimeoutMs: Schema.number({ min: 1, int: true }).optional(),
|
|
2356
|
-
onEvent: fn.optional()
|
|
2357
|
-
}, { unknownKeys: "passthrough" })
|
|
2358
|
-
]);
|
|
2359
|
-
var retryConfig = Schema.union([
|
|
2360
|
-
Schema.literal(false),
|
|
2361
|
-
Schema.object({
|
|
2362
|
-
maxRetries: Schema.number({ min: 0, int: true }).optional(),
|
|
2363
|
-
baseDelayMs: Schema.number({ min: 0, int: true }).optional(),
|
|
2364
|
-
maxDelayMs: Schema.number({ min: 0, int: true }).optional(),
|
|
2365
|
-
maxElapsedMs: Schema.number({ min: 1, int: true }).optional(),
|
|
2366
|
-
respectRetryAfter: Schema.boolean().optional(),
|
|
2367
|
-
retryOnMethods: Schema.array(Schema.enum(["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"])).optional(),
|
|
2368
|
-
retryOnStatus: fn.optional(),
|
|
2369
|
-
retryOnError: fn.optional(),
|
|
2370
|
-
engine: Schema.enum(["ts", "wasm"]).optional(),
|
|
2371
|
-
wasm: Schema.boolean().optional(),
|
|
2372
|
-
onRetry: fn.optional()
|
|
2373
|
-
}, { unknownKeys: "passthrough" })
|
|
2374
|
-
]);
|
|
2375
|
-
var requestRetryOverrideConfig = Schema.union([
|
|
2376
|
-
Schema.literal(false),
|
|
2377
|
-
Schema.object({
|
|
2378
|
-
maxRetries: Schema.number({ min: 0, int: true }).optional(),
|
|
2379
|
-
baseDelayMs: Schema.number({ min: 0, int: true }).optional(),
|
|
2380
|
-
maxDelayMs: Schema.number({ min: 0, int: true }).optional(),
|
|
2381
|
-
schedule: object.optional(),
|
|
2382
|
-
retryOnStatus: fn.optional()
|
|
2383
|
-
}, { unknownKeys: "passthrough" })
|
|
2384
|
-
]);
|
|
2385
|
-
var requestPolicyConfig = Schema.object({
|
|
2386
|
-
preset: Schema.string({ minLength: 1 }).optional(),
|
|
2387
|
-
lane: Schema.string({ minLength: 1 }).optional(),
|
|
2388
|
-
dedupKey: Schema.string({ minLength: 1 }).optional(),
|
|
2389
|
-
priority: Schema.number({ min: 0, max: 9, int: true }).optional(),
|
|
2390
|
-
retry: requestRetryOverrideConfig.optional(),
|
|
2391
|
-
poolKey: Schema.string({ minLength: 1 }).optional()
|
|
2392
|
-
}, { unknownKeys: "passthrough" });
|
|
2393
|
-
var policyPresetsConfig = Schema.record(Schema.union([requestPolicyConfig, fn]));
|
|
2394
|
-
var compressionConfig = Schema.union([
|
|
2395
|
-
Schema.literal(false),
|
|
2396
|
-
Schema.object({
|
|
2397
|
-
encodings: Schema.array(Schema.enum(["gzip", "br", "deflate"])).optional()
|
|
2398
|
-
}, { unknownKeys: "passthrough" })
|
|
2399
|
-
]);
|
|
2400
|
-
var prewarmConfig = Schema.union([
|
|
2401
|
-
Schema.literal(false),
|
|
2402
|
-
Schema.object({
|
|
2403
|
-
origins: Schema.array(Schema.string()).optional(),
|
|
2404
|
-
afterResponse: fn.optional(),
|
|
2405
|
-
keepAliveDurationMs: Schema.number({ min: 1, int: true }).optional(),
|
|
2406
|
-
budget: Schema.number({ min: 1, int: true }).optional(),
|
|
2407
|
-
probeTimeoutMs: Schema.number({ min: 1, int: true }).optional(),
|
|
2408
|
-
autoRefresh: Schema.boolean().optional(),
|
|
2409
|
-
useClientPool: Schema.boolean().optional(),
|
|
2410
|
-
onEvent: fn.optional()
|
|
2411
|
-
}, { unknownKeys: "passthrough" })
|
|
2412
|
-
]);
|
|
2413
|
-
var batchConfig = Schema.union([
|
|
2414
|
-
Schema.literal(false),
|
|
2415
|
-
Schema.object({
|
|
2416
|
-
batch: object,
|
|
2417
|
-
windowMs: Schema.number({ min: 1, int: true }),
|
|
2418
|
-
maxBatchSize: Schema.number({ min: 2, int: true }),
|
|
2419
|
-
batchKey: fn
|
|
2420
|
-
}, { unknownKeys: "passthrough" })
|
|
2421
|
-
]);
|
|
2422
|
-
var wireConfig = Schema.object({
|
|
2423
|
-
baseUrl: Schema.string().optional(),
|
|
2424
|
-
headers: Schema.record(Schema.string()).optional(),
|
|
2425
|
-
timeoutMs: Schema.number({ min: 1, int: true }).optional(),
|
|
2426
|
-
transport: fn.optional(),
|
|
2427
|
-
streamTransport: fn.optional(),
|
|
2428
|
-
pool: poolConfig.optional(),
|
|
2429
|
-
adaptiveLimiter: adaptiveLimiterConfig.optional()
|
|
2430
|
-
}, { unknownKeys: "passthrough" });
|
|
2431
|
-
var lifecycleConfig = Schema.object({
|
|
2432
|
-
baseUrl: Schema.string().optional(),
|
|
2433
|
-
headers: Schema.record(Schema.string()).optional(),
|
|
2434
|
-
timeoutMs: Schema.number({ min: 1, int: true }).optional(),
|
|
2435
|
-
transport: fn.optional(),
|
|
2436
|
-
streamTransport: fn.optional(),
|
|
2437
|
-
pool: poolConfig.optional(),
|
|
2438
|
-
adaptiveLimiter: adaptiveLimiterConfig.optional(),
|
|
2439
|
-
dedup: Schema.union([Schema.literal(false), object]).optional(),
|
|
2440
|
-
batch: batchConfig.optional(),
|
|
2441
|
-
cache: cacheConfig.optional(),
|
|
2442
|
-
priority: priorityConfig.optional(),
|
|
2443
|
-
retry: retryConfig.optional(),
|
|
2444
|
-
prewarm: prewarmConfig.optional(),
|
|
2445
|
-
onEvent: fn.optional(),
|
|
2446
|
-
policyPresets: policyPresetsConfig.optional()
|
|
2447
|
-
}, { unknownKeys: "passthrough" });
|
|
2448
|
-
var defaultClientConfig = Schema.object({
|
|
2449
|
-
baseUrl: Schema.string().optional(),
|
|
2450
|
-
headers: Schema.record(Schema.string()).optional(),
|
|
2451
|
-
timeoutMs: Schema.number({ min: 1, int: true }).optional(),
|
|
2452
|
-
transport: fn.optional(),
|
|
2453
|
-
streamTransport: fn.optional(),
|
|
2454
|
-
pool: poolConfig.optional(),
|
|
2455
|
-
adaptiveLimiter: adaptiveLimiterConfig.optional(),
|
|
2456
|
-
dedup: Schema.union([Schema.literal(false), object]).optional(),
|
|
2457
|
-
batch: batchConfig.optional(),
|
|
2458
|
-
cache: cacheConfig.optional(),
|
|
2459
|
-
priority: priorityConfig.optional(),
|
|
2460
|
-
retry: retryConfig.optional(),
|
|
2461
|
-
prewarm: prewarmConfig.optional(),
|
|
2462
|
-
onEvent: fn.optional(),
|
|
2463
|
-
preset: Schema.enum(["minimal", "balanced", "default", "production"]).optional(),
|
|
2464
|
-
compression: compressionConfig.optional(),
|
|
2465
|
-
middleware: Schema.array(fn).optional(),
|
|
2466
|
-
policyPresets: policyPresetsConfig.optional()
|
|
2467
|
-
}, { unknownKeys: "passthrough" });
|
|
2468
|
-
function validateMakeHttpConfig(config) {
|
|
2469
|
-
parseConfig("MakeHttpConfig", wireConfig, config);
|
|
2470
|
-
}
|
|
2471
|
-
function validateLifecycleClientConfig(config) {
|
|
2472
|
-
parseConfig("LifecycleClientConfig", lifecycleConfig, config);
|
|
2473
|
-
}
|
|
2474
|
-
function validateDefaultHttpClientConfig(config) {
|
|
2475
|
-
parseConfig("DefaultHttpClientConfig", defaultClientConfig, config);
|
|
2476
|
-
}
|
|
2477
|
-
|
|
2478
|
-
// src/http/client.ts
|
|
2479
|
-
var emptyStats = () => ({
|
|
2480
|
-
inFlight: 0,
|
|
2481
|
-
started: 0,
|
|
2482
|
-
succeeded: 0,
|
|
2483
|
-
failed: 0,
|
|
2484
|
-
aborted: 0,
|
|
2485
|
-
timedOut: 0,
|
|
2486
|
-
poolRejected: 0,
|
|
2487
|
-
poolTimeouts: 0
|
|
2488
|
-
});
|
|
2489
|
-
var decorate = (run, stats = emptyStats, metadata = {}) => {
|
|
2490
|
-
const clientFn = ((req) => run(req));
|
|
2491
|
-
Object.assign(clientFn, metadata);
|
|
2492
|
-
return Object.assign(clientFn, {
|
|
2493
|
-
with: (mw) => decorate(mw(clientFn), stats, metadata),
|
|
2494
|
-
stats
|
|
2495
|
-
}, metadata);
|
|
2496
|
-
};
|
|
2497
|
-
var withMiddleware = (mw) => (c) => decorate(mw(c), c.stats, {
|
|
2498
|
-
adaptiveLimiter: c.adaptiveLimiter,
|
|
2499
|
-
destroy: c.destroy,
|
|
2500
|
-
shutdown: c.shutdown
|
|
2501
|
-
});
|
|
2502
|
-
var decorateStream = (run, stats = emptyStats) => Object.assign(((req) => run(req)), { stats });
|
|
2503
|
-
var normalizeHeadersInit = (h) => {
|
|
2504
|
-
if (!h) return void 0;
|
|
2505
|
-
if (typeof Headers !== "undefined" && h instanceof Headers) {
|
|
2506
|
-
const out = {};
|
|
2507
|
-
h.forEach((v, k) => out[k] = v);
|
|
2508
|
-
return out;
|
|
2509
|
-
}
|
|
2510
|
-
if (Array.isArray(h)) return Object.fromEntries(h);
|
|
2511
|
-
if (typeof h === "object") return { ...h };
|
|
2512
|
-
return void 0;
|
|
2513
|
-
};
|
|
2514
|
-
var normalizeRequest = (defaultHeaders) => (req0) => {
|
|
2515
|
-
let req = Object.keys(defaultHeaders).length ? mergeHeadersUnder(defaultHeaders)(req0) : req0;
|
|
2516
|
-
const initHeaders = normalizeHeadersInit(req0.init?.headers);
|
|
2517
|
-
if (initHeaders && Object.keys(initHeaders).length) {
|
|
2518
|
-
req = mergeHeadersUnder(initHeaders)(req);
|
|
2519
|
-
}
|
|
2520
|
-
return req;
|
|
2521
|
-
};
|
|
2522
|
-
var resolvePositiveTimeout = (value) => {
|
|
2523
|
-
if (value === void 0 || !Number.isFinite(value)) return void 0;
|
|
2524
|
-
const n = Math.floor(value);
|
|
2525
|
-
return n > 0 ? n : void 0;
|
|
2526
|
-
};
|
|
2527
|
-
var makeHttpStats = (pool, adaptiveLimiter) => {
|
|
2528
|
-
const stats = {
|
|
2529
|
-
inFlight: 0,
|
|
2530
|
-
started: 0,
|
|
2531
|
-
succeeded: 0,
|
|
2532
|
-
failed: 0,
|
|
2533
|
-
aborted: 0,
|
|
2534
|
-
timedOut: 0,
|
|
2535
|
-
poolRejected: 0,
|
|
2536
|
-
poolTimeouts: 0
|
|
2537
|
-
};
|
|
2538
|
-
const onStart = () => {
|
|
2539
|
-
stats.inFlight++;
|
|
2540
|
-
stats.started++;
|
|
2541
|
-
};
|
|
2542
|
-
const onFinish = (finish) => {
|
|
2543
|
-
if (stats.inFlight > 0) stats.inFlight--;
|
|
2544
|
-
stats.lastDurationMs = finish.durationMs;
|
|
2545
|
-
if (finish.outcome === "success") {
|
|
2546
|
-
stats.succeeded++;
|
|
2547
|
-
return;
|
|
2548
|
-
}
|
|
2549
|
-
if (finish.outcome === "interrupt") {
|
|
2550
|
-
stats.aborted++;
|
|
2551
|
-
return;
|
|
2552
|
-
}
|
|
2553
|
-
if (finish.outcome === "timeout") {
|
|
2554
|
-
stats.timedOut++;
|
|
2555
|
-
return;
|
|
2556
|
-
}
|
|
2557
|
-
const err = normalizeHttpError(finish.error);
|
|
2558
|
-
switch (err._tag) {
|
|
2559
|
-
case "Abort":
|
|
2560
|
-
stats.aborted++;
|
|
2561
|
-
return;
|
|
2562
|
-
case "Timeout":
|
|
2563
|
-
stats.timedOut++;
|
|
2564
|
-
return;
|
|
2565
|
-
case "PoolRejected":
|
|
2566
|
-
stats.poolRejected++;
|
|
2567
|
-
stats.failed++;
|
|
2568
|
-
return;
|
|
2569
|
-
case "PoolTimeout":
|
|
2570
|
-
stats.poolTimeouts++;
|
|
2571
|
-
stats.failed++;
|
|
2572
|
-
return;
|
|
2573
|
-
default:
|
|
2574
|
-
stats.failed++;
|
|
2575
|
-
return;
|
|
2576
|
-
}
|
|
2577
|
-
};
|
|
2578
|
-
const snapshot = () => ({
|
|
2579
|
-
...stats,
|
|
2580
|
-
...pool ? { pool: pool.stats() } : {},
|
|
2581
|
-
...adaptiveLimiter ? { adaptiveLimiter: adaptiveLimiter.stats() } : {}
|
|
2582
|
-
});
|
|
2583
|
-
return { onStart, onFinish, snapshot };
|
|
2584
|
-
};
|
|
2585
|
-
var makePool = (cfg) => cfg.pool === void 0 || cfg.pool === false ? void 0 : new HttpConcurrencyPool(cfg.pool);
|
|
2586
|
-
var makeAdaptiveLimiter = (cfg) => cfg.adaptiveLimiter === void 0 || cfg.adaptiveLimiter === false ? void 0 : new AdaptiveLimiter(cfg.adaptiveLimiter);
|
|
2587
|
-
var resolveRequestUrl = (req, baseUrl) => {
|
|
2588
|
-
try {
|
|
2589
|
-
return new URL(req.url, baseUrl);
|
|
2590
|
-
} catch {
|
|
2591
|
-
return { _tag: "BadUrl", message: `URL inv\xE1lida: ${req.url}` };
|
|
2592
|
-
}
|
|
2593
|
-
};
|
|
2594
|
-
var fetchLabel = (req, url) => `http:${req.method}:${url.origin}`;
|
|
2595
|
-
var timeoutReason = (req, url, timeoutMs) => ({
|
|
2596
|
-
_tag: "Timeout",
|
|
2597
|
-
timeoutMs,
|
|
2598
|
-
phase: "request",
|
|
2599
|
-
message: `HTTP ${req.method} ${url.origin} timed out after ${timeoutMs}ms`
|
|
2600
|
-
});
|
|
2601
|
-
var requestPriority = (req) => {
|
|
2602
|
-
const fromPolicy = getHttpRequestPolicy(req).priority;
|
|
2603
|
-
if (fromPolicy !== void 0) return fromPolicy;
|
|
2604
|
-
return req.init?.priority;
|
|
2605
|
-
};
|
|
2606
|
-
var exitError = (exit) => {
|
|
2607
|
-
if (exit._tag === "Success") return void 0;
|
|
2608
|
-
const failure = Cause.firstFailure(exit.cause);
|
|
2609
|
-
if (failure._tag === "Some") return failure.value;
|
|
2610
|
-
const defect = Cause.firstDefect(exit.cause);
|
|
2611
|
-
if (defect._tag === "Some") return defect.value;
|
|
2612
|
-
if (Cause.containsInterrupt(exit.cause)) return { _tag: "Abort" };
|
|
2613
|
-
return Cause.toError(exit.cause);
|
|
2614
|
-
};
|
|
2615
|
-
var runTransportEffect = (effect, env, signal) => new Promise((resolve, reject) => {
|
|
2616
|
-
let done = false;
|
|
2617
|
-
let cancel;
|
|
2618
|
-
const finish = (exit) => {
|
|
2619
|
-
if (done) return;
|
|
2620
|
-
done = true;
|
|
2621
|
-
signal.removeEventListener("abort", abort);
|
|
2622
|
-
cancel = void 0;
|
|
2623
|
-
if (exit._tag === "Success") {
|
|
2624
|
-
resolve(exit.value);
|
|
2625
|
-
return;
|
|
2626
|
-
}
|
|
2627
|
-
reject(exitError(exit));
|
|
2628
|
-
};
|
|
2629
|
-
const abort = () => {
|
|
2630
|
-
if (done) return;
|
|
2631
|
-
done = true;
|
|
2632
|
-
signal.removeEventListener("abort", abort);
|
|
2633
|
-
const currentCancel = cancel;
|
|
2634
|
-
cancel = void 0;
|
|
2635
|
-
currentCancel?.();
|
|
2636
|
-
reject(abortErrorForSignal(signal));
|
|
2637
|
-
};
|
|
2638
|
-
if (signal.aborted) {
|
|
2639
|
-
abort();
|
|
2640
|
-
return;
|
|
2641
|
-
}
|
|
2642
|
-
signal.addEventListener("abort", abort, { once: true });
|
|
2643
|
-
try {
|
|
2644
|
-
cancel = registerHttpEffect(effect, env, finish);
|
|
2645
|
-
} catch (error) {
|
|
2646
|
-
if (done) return;
|
|
2647
|
-
done = true;
|
|
2648
|
-
signal.removeEventListener("abort", abort);
|
|
2649
|
-
reject(error);
|
|
2650
|
-
}
|
|
2651
|
-
});
|
|
2652
|
-
var releaseSuccess = (lease, adaptiveLimiter, response) => {
|
|
2653
|
-
if (!lease) return void 0;
|
|
2654
|
-
if (adaptiveLimiter) {
|
|
2655
|
-
lease.release(response.ms, { status: response.status });
|
|
2656
|
-
} else {
|
|
2657
|
-
lease.release();
|
|
2658
|
-
}
|
|
2659
|
-
return void 0;
|
|
2660
|
-
};
|
|
2661
|
-
var releaseFailure = (lease, adaptiveLimiter) => {
|
|
2662
|
-
if (!lease) return void 0;
|
|
2663
|
-
if (adaptiveLimiter) {
|
|
2664
|
-
lease.release(0, { error: true });
|
|
2665
|
-
} else {
|
|
2666
|
-
lease.release();
|
|
2667
|
-
}
|
|
2668
|
-
return void 0;
|
|
2669
|
-
};
|
|
2670
|
-
function makeHttpStream(cfg = {}) {
|
|
2671
|
-
validateMakeHttpConfig(cfg);
|
|
2672
|
-
const baseUrl = cfg.baseUrl ?? "";
|
|
2673
|
-
const defaultHeaders = cfg.headers ?? {};
|
|
2674
|
-
const normalize = normalizeRequest(defaultHeaders);
|
|
2675
|
-
const adaptiveLimiter = makeAdaptiveLimiter(cfg);
|
|
2676
|
-
const pool = adaptiveLimiter ? void 0 : makePool(cfg);
|
|
2677
|
-
const metrics = makeHttpStats(pool, adaptiveLimiter);
|
|
2678
|
-
const transport = cfg.streamTransport ?? makeFetchStreamTransport();
|
|
2679
|
-
const run = (req0) => {
|
|
2680
|
-
const req = normalize(req0);
|
|
2681
|
-
const url = resolveRequestUrl(req, baseUrl);
|
|
2682
|
-
if (!(url instanceof URL)) return asyncFail(url);
|
|
2683
|
-
const timeoutMs = resolvePositiveTimeout(req.timeoutMs ?? cfg.timeoutMs);
|
|
2684
|
-
return fromPromiseAbortable(
|
|
2685
|
-
async (signal, env) => {
|
|
2686
|
-
let lease;
|
|
2687
|
-
const linkedSignal = linkAbortSignals(signal, req.init?.signal);
|
|
2688
|
-
try {
|
|
2689
|
-
if (linkedSignal.signal.aborted) throw abortErrorForSignal(linkedSignal.signal);
|
|
2690
|
-
if (adaptiveLimiter) {
|
|
2691
|
-
const key = resolveHttpPoolKey(adaptiveLimiter.keyResolver, req, url);
|
|
2692
|
-
lease = await adaptiveLimiter.acquire(key, linkedSignal.signal, { priority: requestPriority(req) });
|
|
2693
|
-
} else if (pool) {
|
|
2694
|
-
const key = resolveHttpPoolKey(pool.keyResolver, req, url);
|
|
2695
|
-
lease = await pool.acquire(key, linkedSignal.signal);
|
|
2696
|
-
}
|
|
2697
|
-
const response = await runTransportEffect(
|
|
2698
|
-
transport({ request: req, url, signal: linkedSignal.signal }),
|
|
2699
|
-
env,
|
|
2700
|
-
linkedSignal.signal
|
|
2701
|
-
);
|
|
2702
|
-
lease = releaseSuccess(lease, adaptiveLimiter, response);
|
|
2703
|
-
lease = void 0;
|
|
2704
|
-
return response;
|
|
2705
|
-
} finally {
|
|
2706
|
-
linkedSignal.cleanup();
|
|
2707
|
-
if (lease) {
|
|
2708
|
-
releaseFailure(lease, adaptiveLimiter);
|
|
2709
|
-
}
|
|
2710
|
-
}
|
|
2711
|
-
},
|
|
2712
|
-
normalizeHttpError,
|
|
2713
|
-
{
|
|
2714
|
-
label: fetchLabel(req, url),
|
|
2715
|
-
timeoutMs,
|
|
2716
|
-
timeoutReason: timeoutMs ? () => timeoutReason(req, url, timeoutMs) : void 0,
|
|
2717
|
-
onStart: metrics.onStart,
|
|
2718
|
-
onFinish: metrics.onFinish
|
|
2719
|
-
}
|
|
2720
|
-
);
|
|
2721
|
-
};
|
|
2722
|
-
return decorateStream(run, metrics.snapshot);
|
|
2723
|
-
}
|
|
2724
|
-
function makeHttp(cfg = {}) {
|
|
2725
|
-
validateMakeHttpConfig(cfg);
|
|
2726
|
-
const baseUrl = cfg.baseUrl ?? "";
|
|
2727
|
-
const defaultHeaders = cfg.headers ?? {};
|
|
2728
|
-
const normalize = normalizeRequest(defaultHeaders);
|
|
2729
|
-
const adaptiveLimiter = makeAdaptiveLimiter(cfg);
|
|
2730
|
-
const pool = adaptiveLimiter ? void 0 : makePool(cfg);
|
|
2731
|
-
const metrics = makeHttpStats(pool, adaptiveLimiter);
|
|
2732
|
-
const transport = cfg.transport ?? makeFetchTransport();
|
|
2733
|
-
const run = (req0) => {
|
|
2734
|
-
const req = normalize(req0);
|
|
2735
|
-
const url = resolveRequestUrl(req, baseUrl);
|
|
2736
|
-
if (!(url instanceof URL)) return asyncFail(url);
|
|
2737
|
-
const timeoutMs = resolvePositiveTimeout(req.timeoutMs ?? cfg.timeoutMs);
|
|
2738
|
-
return fromPromiseAbortable(
|
|
2739
|
-
async (signal, env) => {
|
|
2740
|
-
let lease;
|
|
2741
|
-
const linkedSignal = linkAbortSignals(signal, req.init?.signal);
|
|
2742
|
-
try {
|
|
2743
|
-
if (linkedSignal.signal.aborted) throw abortErrorForSignal(linkedSignal.signal);
|
|
2744
|
-
if (adaptiveLimiter) {
|
|
2745
|
-
const key = resolveHttpPoolKey(adaptiveLimiter.keyResolver, req, url);
|
|
2746
|
-
lease = await adaptiveLimiter.acquire(key, linkedSignal.signal, { priority: requestPriority(req) });
|
|
2747
|
-
} else if (pool) {
|
|
2748
|
-
const key = resolveHttpPoolKey(pool.keyResolver, req, url);
|
|
2749
|
-
lease = await pool.acquire(key, linkedSignal.signal);
|
|
2750
|
-
}
|
|
2751
|
-
const response = await runTransportEffect(
|
|
2752
|
-
transport({ request: req, url, signal: linkedSignal.signal }),
|
|
2753
|
-
env,
|
|
2754
|
-
linkedSignal.signal
|
|
2755
|
-
);
|
|
2756
|
-
lease = releaseSuccess(lease, adaptiveLimiter, response);
|
|
2757
|
-
return response;
|
|
2758
|
-
} finally {
|
|
2759
|
-
linkedSignal.cleanup();
|
|
2760
|
-
if (lease) {
|
|
2761
|
-
releaseFailure(lease, adaptiveLimiter);
|
|
2762
|
-
}
|
|
2763
|
-
}
|
|
2764
|
-
},
|
|
2765
|
-
normalizeHttpError,
|
|
2766
|
-
{
|
|
2767
|
-
label: fetchLabel(req, url),
|
|
2768
|
-
timeoutMs,
|
|
2769
|
-
timeoutReason: timeoutMs ? () => timeoutReason(req, url, timeoutMs) : void 0,
|
|
2770
|
-
onStart: metrics.onStart,
|
|
2771
|
-
onFinish: metrics.onFinish
|
|
2772
|
-
}
|
|
2773
|
-
);
|
|
2774
|
-
};
|
|
2775
|
-
return decorate(run, metrics.snapshot, adaptiveLimiter ? {
|
|
2776
|
-
adaptiveLimiter,
|
|
2777
|
-
destroy: () => adaptiveLimiter.destroy(),
|
|
2778
|
-
shutdown: () => adaptiveLimiter.shutdown()
|
|
2779
|
-
} : {});
|
|
2780
|
-
}
|
|
2781
|
-
var withRetryStream = (p) => (next) => {
|
|
2782
|
-
const retryOnStatus = p.retryOnStatus ?? defaultRetryOnStatus;
|
|
2783
|
-
const retryOnError = p.retryOnError ?? defaultRetryOnError;
|
|
2784
|
-
const retryOnMethods = p.retryOnMethods ?? defaultRetryableMethods;
|
|
2785
|
-
const maxElapsedMs = normalizeRetryBudget(p.maxElapsedMs);
|
|
2786
|
-
const run = (req) => {
|
|
2787
|
-
if (!retryOnMethods.includes(req.method)) return next(req);
|
|
2788
|
-
const startedAt = performance.now();
|
|
2789
|
-
const remainingBudget = () => maxElapsedMs === void 0 ? Number.POSITIVE_INFINITY : maxElapsedMs - (performance.now() - startedAt);
|
|
2790
|
-
const delayWithinBudget = (delayMs) => Math.max(0, Math.min(delayMs, remainingBudget()));
|
|
2791
|
-
const loop = (attempt) => asyncFold(
|
|
2792
|
-
next(req),
|
|
2793
|
-
(e) => {
|
|
2794
|
-
if (e._tag === "Abort" || e._tag === "BadUrl" || e._tag === "PoolRejected" || e._tag === "PoolClosed") return asyncFail(e);
|
|
2795
|
-
const canRetry = attempt < p.maxRetries && retryOnError(e) && remainingBudget() > 0;
|
|
2796
|
-
if (!canRetry) return asyncFail(e);
|
|
2797
|
-
const d = delayWithinBudget(backoffDelayMs(attempt, p.baseDelayMs, p.maxDelayMs));
|
|
2798
|
-
if (d <= 0 && maxElapsedMs !== void 0) return asyncFail(e);
|
|
2799
|
-
p.onRetry?.({
|
|
2800
|
-
attempt,
|
|
2801
|
-
delayMs: d,
|
|
2802
|
-
error: e,
|
|
2803
|
-
status: void 0,
|
|
2804
|
-
url: req.url,
|
|
2805
|
-
method: req.method,
|
|
2806
|
-
timestamp: Date.now()
|
|
2807
|
-
});
|
|
2808
|
-
return asyncFlatMap(sleep(d), () => loop(attempt + 1));
|
|
2809
|
-
},
|
|
2810
|
-
(w) => {
|
|
2811
|
-
const canRetry = attempt < p.maxRetries && retryOnStatus(w.status) && remainingBudget() > 0;
|
|
2812
|
-
if (!canRetry) return asyncSucceed(w);
|
|
2813
|
-
const ra = p.respectRetryAfter === false ? void 0 : retryAfterMs(w.headers);
|
|
2814
|
-
const rawDelay = ra === void 0 ? backoffDelayMs(attempt, p.baseDelayMs, p.maxDelayMs) : Math.min(ra, p.maxDelayMs);
|
|
2815
|
-
const d = delayWithinBudget(rawDelay);
|
|
2816
|
-
if (d <= 0 && maxElapsedMs !== void 0) return asyncSucceed(w);
|
|
2817
|
-
p.onRetry?.({
|
|
2818
|
-
attempt,
|
|
2819
|
-
delayMs: d,
|
|
2820
|
-
error: void 0,
|
|
2821
|
-
status: w.status,
|
|
2822
|
-
url: req.url,
|
|
2823
|
-
method: req.method,
|
|
2824
|
-
timestamp: Date.now()
|
|
2825
|
-
});
|
|
2826
|
-
return asyncFlatMap(sleep(d), () => loop(attempt + 1));
|
|
2827
|
-
}
|
|
2828
|
-
);
|
|
2829
|
-
return loop(0);
|
|
2830
|
-
};
|
|
2831
|
-
return decorateStream(run, next.stats);
|
|
2832
|
-
};
|
|
2833
|
-
|
|
2834
|
-
export {
|
|
2835
|
-
mergeHeaders,
|
|
2836
|
-
setHeaderIfMissing,
|
|
2837
|
-
defaultRetryableMethods,
|
|
2838
|
-
defaultRetryOnStatus,
|
|
2839
|
-
defaultRetryOnError,
|
|
2840
|
-
backoffDelayMs,
|
|
2841
|
-
retryAfterMs,
|
|
2842
|
-
normalizeRetryBudget,
|
|
2843
|
-
withRetry,
|
|
2844
|
-
resolveHttpPoolKey,
|
|
2845
|
-
HttpConcurrencyPool,
|
|
2846
|
-
LatencyWindow,
|
|
2847
|
-
EmaComputer,
|
|
2848
|
-
computeGradient,
|
|
2849
|
-
computeNewLimit,
|
|
2850
|
-
adaptiveLimiterPresets,
|
|
2851
|
-
makeAdaptiveLimiterConfig,
|
|
2852
|
-
validateConfig,
|
|
2853
|
-
resolveConfig,
|
|
2854
|
-
AdaptiveLimiter,
|
|
2855
|
-
validateLifecycleClientConfig,
|
|
2856
|
-
validateDefaultHttpClientConfig,
|
|
2857
|
-
isTaggedHttpError,
|
|
2858
|
-
isAbortError,
|
|
2859
|
-
normalizeHttpError,
|
|
2860
|
-
headersOf,
|
|
2861
|
-
normalizeHttpHeaders,
|
|
2862
|
-
abortErrorForSignal,
|
|
2863
|
-
linkAbortSignals,
|
|
2864
|
-
makePromiseHttpTransport,
|
|
2865
|
-
promiseHttpTransport,
|
|
2866
|
-
makeFetchTransport,
|
|
2867
|
-
makeFetchStreamTransport,
|
|
2868
|
-
decorate,
|
|
2869
|
-
withMiddleware,
|
|
2870
|
-
normalizeHeadersInit,
|
|
2871
|
-
makeHttpStream,
|
|
2872
|
-
makeHttp,
|
|
2873
|
-
withRetryStream
|
|
2874
|
-
};
|