brass-runtime 1.15.0 → 1.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +409 -137
- package/dist/agent/cli/main.cjs +40 -35
- package/dist/agent/cli/main.js +9 -4
- package/dist/agent/cli/main.mjs +9 -4
- package/dist/agent/index.cjs +8 -4
- package/dist/agent/index.d.ts +1 -1
- package/dist/agent/index.js +7 -3
- package/dist/agent/index.mjs +7 -3
- package/dist/{chunk-PPUXIH5R.js → chunk-2WC63LJK.mjs} +11 -7
- package/dist/chunk-3RG5ZIWI.js +10 -0
- package/dist/chunk-45F7OKGT.cjs +104 -0
- package/dist/chunk-5YOQOXEQ.cjs +2491 -0
- package/dist/{chunk-STVLQ3XD.cjs → chunk-7HUOJA4W.cjs} +78 -74
- package/dist/{chunk-BMH5AV44.js → chunk-7LVI2GIN.js} +251 -370
- package/dist/chunk-7TL2LHQJ.js +2491 -0
- package/dist/chunk-7V4KY4RL.mjs +104 -0
- package/dist/chunk-7XOPAB5Q.js +2143 -0
- package/dist/chunk-CCKHV5BT.mjs +193 -0
- package/dist/{chunk-AR22SXML.js → chunk-CY33PGEX.mjs} +488 -421
- package/dist/chunk-DJQ7OMMB.cjs +144 -0
- package/dist/chunk-F5EUMJL7.mjs +2143 -0
- package/dist/chunk-FM4W4QPL.js +193 -0
- package/dist/{chunk-TO7IKXYT.js → chunk-G3XGCZDQ.js} +1 -1
- package/dist/{chunk-BDF4AMWX.mjs → chunk-G6IQOE4P.mjs} +251 -370
- package/dist/chunk-GOV47PPB.mjs +552 -0
- package/dist/chunk-H55LI6WY.js +93 -0
- package/dist/chunk-IJT6RRQ5.cjs +93 -0
- package/dist/{chunk-ELOOF35R.mjs → chunk-J3H54ZRV.mjs} +1 -1
- package/dist/chunk-JF4XXPZ5.cjs +552 -0
- package/dist/chunk-JNFRRJYH.cjs +2143 -0
- package/dist/chunk-JX3LZQJH.cjs +354 -0
- package/dist/chunk-K2T3DV26.mjs +93 -0
- package/dist/chunk-KCPT2D6G.js +552 -0
- package/dist/chunk-MWXMNYJS.cjs +1110 -0
- package/dist/{chunk-VEZNF5GZ.cjs → chunk-N6VHMOWB.cjs} +130 -126
- package/dist/{chunk-3QMOKAS5.js → chunk-NC5SDRYE.js} +9 -5
- package/dist/chunk-NOYZIMUJ.mjs +144 -0
- package/dist/{chunk-R3R2FVLG.cjs → chunk-NYL4D7SK.cjs} +5 -5
- package/dist/chunk-OBGZSXTJ.cjs +10 -0
- package/dist/{chunk-4NHES7VK.mjs → chunk-OOGJ73B6.js} +11 -7
- package/dist/chunk-PNVFW245.js +144 -0
- package/dist/chunk-PRWCB3QL.mjs +2491 -0
- package/dist/{chunk-JFPU5GQI.mjs → chunk-QY5FKYEQ.js} +488 -421
- package/dist/chunk-ROJC3NBJ.js +104 -0
- package/dist/chunk-SPUEME2B.cjs +343 -0
- package/dist/chunk-TDVMADDN.js +343 -0
- package/dist/chunk-TVN5I4U6.cjs +193 -0
- package/dist/chunk-U5KWK3PX.mjs +343 -0
- package/dist/chunk-VFIUZG7J.mjs +354 -0
- package/dist/{chunk-TGIFUAK4.cjs → chunk-WQ5QNU5R.cjs} +459 -578
- package/dist/chunk-XDZOO4L5.js +354 -0
- package/dist/chunk-Y6FXYEAI.mjs +10 -0
- package/dist/{chunk-K6M7MDZ4.mjs → chunk-ZGLD4TVZ.mjs} +9 -5
- package/dist/client-CtFmoDvM.d.ts +645 -0
- package/dist/core/index.cjs +72 -4
- package/dist/core/index.d.ts +92 -198
- package/dist/core/index.js +106 -38
- package/dist/core/index.mjs +106 -38
- package/dist/{effect-CMOQKX8y.d.ts → effect-CGNl5Rqp.d.ts} +107 -1
- package/dist/effectRunner-3ZHAD3LE.cjs +8 -0
- package/dist/effectRunner-A4CHJXJI.js +8 -0
- package/dist/effectRunner-OPUF6QRN.mjs +8 -0
- package/dist/http/index.cjs +2189 -1271
- package/dist/http/index.d.ts +830 -270
- package/dist/http/index.js +2008 -1090
- package/dist/http/index.mjs +2008 -1090
- package/dist/http/testing.cjs +159 -0
- package/dist/http/testing.d.ts +42 -0
- package/dist/http/testing.js +159 -0
- package/dist/http/testing.mjs +159 -0
- package/dist/index.cjs +246 -178
- package/dist/index.d.ts +9 -35
- package/dist/index.js +120 -52
- package/dist/index.mjs +120 -52
- package/dist/observability/index.cjs +677 -0
- package/dist/observability/index.d.ts +79 -0
- package/dist/observability/index.js +677 -0
- package/dist/observability/index.mjs +677 -0
- package/dist/schedule-Fque9Abz.d.ts +70 -0
- package/dist/schema/index.cjs +25 -0
- package/dist/schema/index.d.ts +177 -0
- package/dist/schema/index.js +25 -0
- package/dist/schema/index.mjs +25 -0
- package/dist/server-C8hDXA74.d.ts +674 -0
- package/dist/{stream-FQm9h4Mg.d.ts → stream-dvSs0QS5.d.ts} +1 -1
- package/dist/tracer-B5tRH9H7.d.ts +230 -0
- package/dist/tracing-Dt9S_6V8.d.ts +148 -0
- package/package.json +27 -1
- package/dist/chunk-BDYEENHT.js +0 -224
- package/dist/chunk-MS34J5LY.cjs +0 -224
- package/dist/chunk-UMAZLXAB.mjs +0 -224
- package/dist/chunk-XPZNXSVN.cjs +0 -1043
- package/dist/tracing-DNT9jEbr.d.ts +0 -106
package/dist/http/index.js
CHANGED
|
@@ -1,937 +1,182 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
3
|
-
} from "../chunk-
|
|
2
|
+
registerHttpEffect
|
|
3
|
+
} from "../chunk-H55LI6WY.js";
|
|
4
4
|
import {
|
|
5
|
+
fixed,
|
|
5
6
|
makeCircuitBreaker,
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
resource,
|
|
8
|
+
take
|
|
9
|
+
} from "../chunk-KCPT2D6G.js";
|
|
10
|
+
import {
|
|
11
|
+
AdaptiveLimiter,
|
|
12
|
+
EmaComputer,
|
|
13
|
+
HttpConcurrencyPool,
|
|
14
|
+
LatencyWindow,
|
|
15
|
+
adaptiveLimiterPresets,
|
|
16
|
+
backoffDelayMs,
|
|
17
|
+
computeGradient,
|
|
18
|
+
computeNewLimit,
|
|
19
|
+
decorate,
|
|
20
|
+
defaultRetryOnError,
|
|
21
|
+
defaultRetryOnStatus,
|
|
22
|
+
defaultRetryableMethods,
|
|
23
|
+
makeAdaptiveLimiterConfig,
|
|
24
|
+
makeHttp,
|
|
25
|
+
makeHttpStream,
|
|
26
|
+
mergeHeaders,
|
|
27
|
+
normalizeHeadersInit,
|
|
28
|
+
normalizeRetryBudget,
|
|
29
|
+
resolveConfig,
|
|
30
|
+
resolveHttpPoolKey,
|
|
31
|
+
retryAfterMs,
|
|
32
|
+
setHeaderIfMissing,
|
|
33
|
+
validateConfig,
|
|
34
|
+
validateDefaultHttpClientConfig,
|
|
35
|
+
validateLifecycleClientConfig,
|
|
36
|
+
withMiddleware,
|
|
37
|
+
withRetry,
|
|
38
|
+
withRetryStream
|
|
39
|
+
} from "../chunk-7TL2LHQJ.js";
|
|
40
|
+
import "../chunk-OOGJ73B6.js";
|
|
41
|
+
import "../chunk-ROJC3NBJ.js";
|
|
42
|
+
import {
|
|
43
|
+
healthToHttpResponse,
|
|
44
|
+
makeRuntimeHealth,
|
|
45
|
+
runObservedHttpServerEffect
|
|
46
|
+
} from "../chunk-7XOPAB5Q.js";
|
|
47
|
+
import "../chunk-XDZOO4L5.js";
|
|
48
|
+
import "../chunk-G3XGCZDQ.js";
|
|
49
|
+
import "../chunk-FM4W4QPL.js";
|
|
50
|
+
import {
|
|
51
|
+
Runtime,
|
|
52
|
+
fromPromiseAbortable,
|
|
53
|
+
toPromise
|
|
54
|
+
} from "../chunk-7LVI2GIN.js";
|
|
9
55
|
import {
|
|
10
56
|
Cause,
|
|
11
|
-
__require,
|
|
12
57
|
asyncEffect,
|
|
13
58
|
asyncFail,
|
|
14
59
|
asyncFlatMap,
|
|
15
60
|
asyncFold,
|
|
16
61
|
asyncSucceed,
|
|
17
|
-
fromPromiseAbortable,
|
|
18
62
|
mapTryAsync,
|
|
19
|
-
resolveWasmModule,
|
|
20
|
-
toPromise,
|
|
21
63
|
withAsyncPromise
|
|
22
|
-
} from "../chunk-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
};
|
|
39
|
-
|
|
40
|
-
// src/http/optics/request.ts
|
|
41
|
-
var Request = {
|
|
42
|
-
headers: Lens.make(
|
|
43
|
-
(req) => req.headers ?? {},
|
|
44
|
-
(headers) => (req) => ({ ...req, headers })
|
|
45
|
-
)
|
|
46
|
-
};
|
|
47
|
-
var mergeHeaders = (extra) => (req) => Lens.over(Request.headers, (h) => ({ ...h, ...extra }))(req);
|
|
48
|
-
var mergeHeadersUnder = (under) => (req) => Lens.over(Request.headers, (h) => ({ ...under, ...h }))(req);
|
|
49
|
-
var setHeaderIfMissing = (k, v) => (req) => Lens.over(Request.headers, (h) => h[k] ? h : { ...h, [k]: v })(req);
|
|
64
|
+
} from "../chunk-PNVFW245.js";
|
|
65
|
+
import {
|
|
66
|
+
ConfigValidationError,
|
|
67
|
+
Schema,
|
|
68
|
+
SchemaValidationException,
|
|
69
|
+
formatIssues,
|
|
70
|
+
isSchema,
|
|
71
|
+
makeSchemaIssue,
|
|
72
|
+
parseConfig,
|
|
73
|
+
s,
|
|
74
|
+
schema,
|
|
75
|
+
validateValue
|
|
76
|
+
} from "../chunk-TDVMADDN.js";
|
|
77
|
+
import {
|
|
78
|
+
__require
|
|
79
|
+
} from "../chunk-3RG5ZIWI.js";
|
|
50
80
|
|
|
51
|
-
// src/http/
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
}
|
|
57
|
-
start(options) {
|
|
58
|
-
return this.planner.start(
|
|
59
|
-
options.nowMs,
|
|
60
|
-
options.maxRetries,
|
|
61
|
-
options.baseDelayMs,
|
|
62
|
-
options.maxDelayMs,
|
|
63
|
-
options.maxElapsedMs ?? -1,
|
|
64
|
-
BigInt(this.seed())
|
|
65
|
-
);
|
|
66
|
-
}
|
|
67
|
-
nextDelayMs(retryId, options) {
|
|
68
|
-
const delay = this.planner.next_delay_ms(retryId, options.nowMs, options.retryable, options.retryAfterMs ?? -1);
|
|
69
|
-
return delay < 0 ? void 0 : delay;
|
|
70
|
-
}
|
|
71
|
-
drop(retryId) {
|
|
72
|
-
this.planner.drop_state(retryId);
|
|
73
|
-
}
|
|
74
|
-
stats() {
|
|
81
|
+
// src/http/validation.ts
|
|
82
|
+
function decodeJsonBody(bodyText, validator, options = {}) {
|
|
83
|
+
let parsed;
|
|
84
|
+
try {
|
|
85
|
+
parsed = JSON.parse(bodyText);
|
|
86
|
+
} catch (error) {
|
|
75
87
|
return {
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
88
|
+
success: false,
|
|
89
|
+
error: {
|
|
90
|
+
_tag: "ValidationError",
|
|
91
|
+
message: `JSON parse error: ${error instanceof Error ? error.message : String(error)}`,
|
|
92
|
+
body: bodyText,
|
|
93
|
+
phase: "response",
|
|
94
|
+
schema: options.schemaName,
|
|
95
|
+
issues: [makeSchemaIssue([], "valid JSON", bodyText, "Response body is not valid JSON")]
|
|
96
|
+
}
|
|
80
97
|
};
|
|
81
98
|
}
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
var defaultRetryableMethods = ["GET", "HEAD", "OPTIONS"];
|
|
95
|
-
var defaultRetryOnStatus = (s) => s === 408 || s === 429 || s === 500 || s === 502 || s === 503 || s === 504;
|
|
96
|
-
var defaultRetryOnError = (e) => e._tag === "FetchError" || e._tag === "Timeout" || e._tag === "PoolTimeout";
|
|
97
|
-
var clamp = (n, min, max) => Math.max(min, Math.min(max, n));
|
|
98
|
-
var backoffDelayMs = (attempt, base, cap) => {
|
|
99
|
-
const b = Math.max(0, base);
|
|
100
|
-
const c = Math.max(0, cap);
|
|
101
|
-
const exp = b * Math.pow(2, attempt);
|
|
102
|
-
const lim = clamp(exp, 0, c);
|
|
103
|
-
return Math.floor(Math.random() * lim);
|
|
104
|
-
};
|
|
105
|
-
var headerCI = (h, name) => {
|
|
106
|
-
const k = Object.keys(h).find((x) => x.toLowerCase() === name.toLowerCase());
|
|
107
|
-
return k ? h[k] : void 0;
|
|
108
|
-
};
|
|
109
|
-
var retryAfterMs = (headers) => {
|
|
110
|
-
const v = headerCI(headers, "retry-after")?.trim();
|
|
111
|
-
if (!v) return void 0;
|
|
112
|
-
const secs = Number(v);
|
|
113
|
-
if (Number.isFinite(secs)) return Math.max(0, Math.floor(secs * 1e3));
|
|
114
|
-
const t = Date.parse(v);
|
|
115
|
-
if (Number.isFinite(t)) return Math.max(0, t - Date.now());
|
|
116
|
-
return void 0;
|
|
117
|
-
};
|
|
118
|
-
var normalizeRetryBudget = (ms) => {
|
|
119
|
-
if (ms === void 0 || !Number.isFinite(ms)) return void 0;
|
|
120
|
-
return Math.max(0, Math.floor(ms));
|
|
121
|
-
};
|
|
122
|
-
var resolveEffectivePolicy = (req, basePolicy) => {
|
|
123
|
-
const override = req.retry;
|
|
124
|
-
if (override === false) return null;
|
|
125
|
-
if (override === void 0) return basePolicy;
|
|
99
|
+
if (!validator) return { success: true, data: parsed };
|
|
100
|
+
let legacyMessage;
|
|
101
|
+
const validation = isSchema(validator) ? validator.safeParse(parsed) : (() => {
|
|
102
|
+
const result = validator(parsed);
|
|
103
|
+
if (result.success) return { success: true, data: result.data };
|
|
104
|
+
legacyMessage = result.error;
|
|
105
|
+
return {
|
|
106
|
+
success: false,
|
|
107
|
+
issues: result.issues ?? [makeSchemaIssue([], "valid JSON shape", parsed, result.error)]
|
|
108
|
+
};
|
|
109
|
+
})();
|
|
110
|
+
if (validation.success) return { success: true, data: validation.data };
|
|
126
111
|
return {
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
if (p.engine !== void 0) {
|
|
136
|
-
if (p.engine === "ts" || p.engine === "wasm") return p.engine;
|
|
137
|
-
throw new Error(`brass-runtime retry engine must be 'ts' or 'wasm'; received '${String(p.engine)}'`);
|
|
138
|
-
}
|
|
139
|
-
if (p.wasm === true) return "wasm";
|
|
140
|
-
if (p.wasm === false) return "ts";
|
|
141
|
-
return "ts";
|
|
142
|
-
};
|
|
143
|
-
var withRetry = (p) => (next) => {
|
|
144
|
-
const retryOnMethods = p.retryOnMethods ?? defaultRetryableMethods;
|
|
145
|
-
const retryEngine = resolveRetryEngine(p);
|
|
146
|
-
const wasmPlanner = retryEngine === "wasm" ? makeWasmRetryPlanner() : void 0;
|
|
147
|
-
const isMethodRetryable = (req) => retryOnMethods.includes(req.method);
|
|
148
|
-
const nextDelay = (ep, epMaxElapsedMs, retryId, attempt, startedAt, retryable, retryAfter) => {
|
|
149
|
-
if (!retryable) return void 0;
|
|
150
|
-
if (wasmPlanner && retryId !== void 0) {
|
|
151
|
-
return wasmPlanner.nextDelayMs(retryId, {
|
|
152
|
-
nowMs: performance.now(),
|
|
153
|
-
retryable,
|
|
154
|
-
retryAfterMs: retryAfter
|
|
155
|
-
});
|
|
112
|
+
success: false,
|
|
113
|
+
error: {
|
|
114
|
+
_tag: "ValidationError",
|
|
115
|
+
message: legacyMessage ?? `JSON response failed validation: ${formatIssues(validation.issues)}`,
|
|
116
|
+
body: bodyText,
|
|
117
|
+
phase: "response",
|
|
118
|
+
schema: options.schemaName,
|
|
119
|
+
issues: validation.issues
|
|
156
120
|
}
|
|
157
|
-
const remainingBudget = epMaxElapsedMs === void 0 ? Number.POSITIVE_INFINITY : epMaxElapsedMs - (performance.now() - startedAt);
|
|
158
|
-
if (remainingBudget <= 0) return void 0;
|
|
159
|
-
const rawDelay = retryAfter === void 0 ? backoffDelayMs(attempt, ep.baseDelayMs, ep.maxDelayMs) : Math.min(retryAfter, ep.maxDelayMs);
|
|
160
|
-
return Math.max(0, Math.min(rawDelay, remainingBudget));
|
|
161
|
-
};
|
|
162
|
-
const sleepWithCleanup = (ms, onCancel) => {
|
|
163
|
-
return asyncEffect((_env, cb) => {
|
|
164
|
-
const delay = Math.max(0, Math.floor(ms));
|
|
165
|
-
const id = setTimeout(() => cb({ _tag: "Success", value: void 0 }), delay);
|
|
166
|
-
return () => {
|
|
167
|
-
clearTimeout(id);
|
|
168
|
-
onCancel();
|
|
169
|
-
};
|
|
170
|
-
});
|
|
171
|
-
};
|
|
172
|
-
const loop = (req, attempt, startedAt, retryId, ep, epMaxElapsedMs, epRetryOnStatus, epRetryOnError, originalPriority, safeDrop) => {
|
|
173
|
-
if (!isMethodRetryable(req)) return next(req);
|
|
174
|
-
const effectiveReq = attempt > 0 ? (() => {
|
|
175
|
-
const boostedReq = { ...req };
|
|
176
|
-
boostedReq.priority = Math.max(0, originalPriority - 1);
|
|
177
|
-
return boostedReq;
|
|
178
|
-
})() : req;
|
|
179
|
-
const remainingBudget = () => epMaxElapsedMs === void 0 ? Number.POSITIVE_INFINITY : epMaxElapsedMs - (performance.now() - startedAt);
|
|
180
|
-
return asyncFold(
|
|
181
|
-
next(effectiveReq),
|
|
182
|
-
(e) => {
|
|
183
|
-
if (e._tag === "Abort" || e._tag === "BadUrl" || e._tag === "PoolRejected" || e._tag === "CircuitBreakerOpen") {
|
|
184
|
-
safeDrop(retryId);
|
|
185
|
-
return asyncFail(e);
|
|
186
|
-
}
|
|
187
|
-
const retryable = attempt < ep.maxRetries && epRetryOnError(e) && remainingBudget() > 0;
|
|
188
|
-
const d = nextDelay(ep, epMaxElapsedMs, retryId, attempt, startedAt, retryable);
|
|
189
|
-
if (d === void 0 || d <= 0 && epMaxElapsedMs !== void 0) {
|
|
190
|
-
safeDrop(retryId);
|
|
191
|
-
return asyncFail(e);
|
|
192
|
-
}
|
|
193
|
-
if (ep.onRetry) {
|
|
194
|
-
ep.onRetry({
|
|
195
|
-
attempt,
|
|
196
|
-
delayMs: d,
|
|
197
|
-
error: e,
|
|
198
|
-
status: void 0,
|
|
199
|
-
url: req.url,
|
|
200
|
-
method: req.method,
|
|
201
|
-
timestamp: Date.now()
|
|
202
|
-
});
|
|
203
|
-
}
|
|
204
|
-
return asyncFlatMap(sleepWithCleanup(d, () => safeDrop(retryId)), () => loop(req, attempt + 1, startedAt, retryId, ep, epMaxElapsedMs, epRetryOnStatus, epRetryOnError, originalPriority, safeDrop));
|
|
205
|
-
},
|
|
206
|
-
(w) => {
|
|
207
|
-
const retryable = attempt < ep.maxRetries && epRetryOnStatus(w.status) && remainingBudget() > 0;
|
|
208
|
-
const ra = ep.respectRetryAfter === false ? void 0 : retryAfterMs(w.headers);
|
|
209
|
-
const d = nextDelay(ep, epMaxElapsedMs, retryId, attempt, startedAt, retryable, ra);
|
|
210
|
-
if (d === void 0 || d <= 0 && epMaxElapsedMs !== void 0) {
|
|
211
|
-
safeDrop(retryId);
|
|
212
|
-
return asyncSucceed(w);
|
|
213
|
-
}
|
|
214
|
-
if (ep.onRetry) {
|
|
215
|
-
ep.onRetry({
|
|
216
|
-
attempt,
|
|
217
|
-
delayMs: d,
|
|
218
|
-
error: void 0,
|
|
219
|
-
status: w.status,
|
|
220
|
-
url: req.url,
|
|
221
|
-
method: req.method,
|
|
222
|
-
timestamp: Date.now()
|
|
223
|
-
});
|
|
224
|
-
}
|
|
225
|
-
return asyncFlatMap(sleepWithCleanup(d, () => safeDrop(retryId)), () => loop(req, attempt + 1, startedAt, retryId, ep, epMaxElapsedMs, epRetryOnStatus, epRetryOnError, originalPriority, safeDrop));
|
|
226
|
-
}
|
|
227
|
-
);
|
|
228
121
|
};
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
baseDelayMs: effectivePolicy.baseDelayMs,
|
|
242
|
-
maxDelayMs: effectivePolicy.maxDelayMs,
|
|
243
|
-
maxElapsedMs: epMaxElapsedMs
|
|
244
|
-
});
|
|
245
|
-
let plannerDropped = false;
|
|
246
|
-
const safeDrop = (id) => {
|
|
247
|
-
if (id !== void 0 && !plannerDropped) {
|
|
248
|
-
plannerDropped = true;
|
|
249
|
-
wasmPlanner?.drop(id);
|
|
250
|
-
}
|
|
251
|
-
};
|
|
252
|
-
return loop(req, 0, startedAt, retryId, effectivePolicy, epMaxElapsedMs, epRetryOnStatus, epRetryOnError, originalPriority, safeDrop);
|
|
253
|
-
};
|
|
254
|
-
};
|
|
255
|
-
|
|
256
|
-
// src/http/wasmPermitPool.ts
|
|
257
|
-
var DECISION_RUN_NOW = 0;
|
|
258
|
-
var DECISION_QUEUED = 1;
|
|
259
|
-
var WasmHttpPermitPoolBridge = class {
|
|
260
|
-
pool;
|
|
261
|
-
keyCache = /* @__PURE__ */ new Map();
|
|
262
|
-
constructor(Ctor, options) {
|
|
263
|
-
this.pool = new Ctor(options.concurrency, options.maxQueue, toU64(options.queueTimeoutMs));
|
|
264
|
-
}
|
|
265
|
-
acquire(key, subjectId, nowMs = Date.now()) {
|
|
266
|
-
const keyId = this.internKey(key);
|
|
267
|
-
const decision = this.pool.acquire(subjectId, keyId, toU64(nowMs));
|
|
268
|
-
const permitId = this.pool.last_permit_id();
|
|
269
|
-
if (decision === DECISION_RUN_NOW) return { kind: "run", keyId, permitId };
|
|
270
|
-
if (decision === DECISION_QUEUED) return { kind: "queued", keyId, permitId };
|
|
271
|
-
return { kind: "rejected", keyId, permitId };
|
|
272
|
-
}
|
|
273
|
-
release(keyId, nowMs = Date.now()) {
|
|
274
|
-
const ptr = this.pool.release(keyId, toU64(nowMs));
|
|
275
|
-
return this.readEvents(ptr, this.pool.permit_events_len());
|
|
276
|
-
}
|
|
277
|
-
cancel(permitId) {
|
|
278
|
-
this.pool.cancel(permitId);
|
|
279
|
-
}
|
|
280
|
-
advanceTime(nowMs = Date.now()) {
|
|
281
|
-
const ptr = this.pool.advance_time(toU64(nowMs));
|
|
282
|
-
return this.readEvents(ptr, this.pool.permit_events_len());
|
|
283
|
-
}
|
|
284
|
-
nextDeadlineMs() {
|
|
285
|
-
return this.pool.next_deadline_ms();
|
|
286
|
-
}
|
|
287
|
-
stats() {
|
|
288
|
-
return {
|
|
289
|
-
running: this.pool.metric_u64(0),
|
|
290
|
-
queued: this.pool.metric_u64(1),
|
|
291
|
-
acquired: this.pool.metric_u64(2),
|
|
292
|
-
released: this.pool.metric_u64(3),
|
|
293
|
-
rejected: this.pool.metric_u64(4),
|
|
294
|
-
queueTimeouts: this.pool.metric_u64(5),
|
|
295
|
-
keys: this.pool.metric_u64(6)
|
|
296
|
-
};
|
|
297
|
-
}
|
|
298
|
-
internKey(key) {
|
|
299
|
-
const normalized = key.trim().slice(0, 160) || "global";
|
|
300
|
-
let id = this.keyCache.get(normalized);
|
|
301
|
-
if (id === void 0) {
|
|
302
|
-
id = this.pool.intern_key(normalized);
|
|
303
|
-
this.keyCache.set(normalized, id);
|
|
304
|
-
}
|
|
305
|
-
return id;
|
|
306
|
-
}
|
|
307
|
-
readEvents(ptr, len) {
|
|
308
|
-
if (ptr === 0 || len <= 1) return [];
|
|
309
|
-
const words = new Uint32Array(this.pool.memory().buffer, ptr, len);
|
|
310
|
-
const count = words[0] >>> 0;
|
|
311
|
-
const out = [];
|
|
312
|
-
for (let i = 0; i < count; i++) {
|
|
313
|
-
const base = 1 + i * 3;
|
|
314
|
-
if (base + 2 >= words.length) break;
|
|
315
|
-
out.push({
|
|
316
|
-
subjectId: words[base] >>> 0,
|
|
317
|
-
permitId: words[base + 1] >>> 0,
|
|
318
|
-
keyId: words[base + 2] >>> 0
|
|
122
|
+
}
|
|
123
|
+
function encodeJsonBodyEffect(bodyObj, validator, options = {}) {
|
|
124
|
+
if (validator) {
|
|
125
|
+
const validation = validateValue(bodyObj, validator);
|
|
126
|
+
if (!validation.success) {
|
|
127
|
+
return asyncFail({
|
|
128
|
+
_tag: "ValidationError",
|
|
129
|
+
message: `JSON request body failed validation: ${formatIssues(validation.issues)}`,
|
|
130
|
+
body: previewJson(bodyObj),
|
|
131
|
+
phase: "request",
|
|
132
|
+
schema: options.schemaName,
|
|
133
|
+
issues: validation.issues
|
|
319
134
|
});
|
|
320
135
|
}
|
|
321
|
-
return out;
|
|
322
136
|
}
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
var DEFAULT_CONCURRENCY = 64;
|
|
336
|
-
var DEFAULT_MAX_QUEUE = 256;
|
|
337
|
-
var clampInt = (n, fallback, min) => {
|
|
338
|
-
if (n === void 0 || !Number.isFinite(n)) return fallback;
|
|
339
|
-
return Math.max(min, Math.floor(n));
|
|
340
|
-
};
|
|
341
|
-
var queueTimeoutError = (key, timeoutMs) => ({
|
|
342
|
-
_tag: "PoolTimeout",
|
|
343
|
-
key,
|
|
344
|
-
timeoutMs,
|
|
345
|
-
message: `HTTP pool '${key}' did not grant a slot within ${timeoutMs}ms`
|
|
346
|
-
});
|
|
347
|
-
var poolRejectedError = (key, maxQueue) => ({
|
|
348
|
-
_tag: "PoolRejected",
|
|
349
|
-
key,
|
|
350
|
-
limit: maxQueue,
|
|
351
|
-
message: `HTTP pool '${key}' queue is full`
|
|
352
|
-
});
|
|
353
|
-
var abortError = () => ({ _tag: "Abort" });
|
|
354
|
-
function resolveHttpPoolEngine(config) {
|
|
355
|
-
if (config.engine !== void 0) {
|
|
356
|
-
if (config.engine === "ts" || config.engine === "wasm") return config.engine;
|
|
357
|
-
throw new Error(`brass-runtime HTTP pool engine must be 'ts' or 'wasm'; received '${String(config.engine)}'`);
|
|
358
|
-
}
|
|
359
|
-
if (config.wasm === true) return "wasm";
|
|
360
|
-
if (config.wasm === false) return "ts";
|
|
361
|
-
return "ts";
|
|
362
|
-
}
|
|
363
|
-
function resolveHttpPoolKey(resolver, req, url) {
|
|
364
|
-
const custom = req.poolKey?.trim();
|
|
365
|
-
if (custom) return custom.slice(0, 160);
|
|
366
|
-
const r = resolver ?? "origin";
|
|
367
|
-
if (typeof r === "function") return r(req, url).trim().slice(0, 160) || "global";
|
|
368
|
-
if (r === "global") return "global";
|
|
369
|
-
if (r === "host") return url.host;
|
|
370
|
-
return url.origin;
|
|
371
|
-
}
|
|
372
|
-
var HttpConcurrencyPool = class {
|
|
373
|
-
states = /* @__PURE__ */ new Map();
|
|
374
|
-
concurrency;
|
|
375
|
-
maxQueue;
|
|
376
|
-
queueTimeoutMs;
|
|
377
|
-
keyResolver;
|
|
378
|
-
wasm;
|
|
379
|
-
wasmWaiters = /* @__PURE__ */ new Map();
|
|
380
|
-
wasmTimer;
|
|
381
|
-
nextSubjectId = 1;
|
|
382
|
-
constructor(config = {}) {
|
|
383
|
-
this.concurrency = clampInt(config.concurrency, DEFAULT_CONCURRENCY, 1);
|
|
384
|
-
this.maxQueue = clampInt(config.maxQueue, DEFAULT_MAX_QUEUE, 0);
|
|
385
|
-
this.queueTimeoutMs = config.queueTimeoutMs !== void 0 && Number.isFinite(config.queueTimeoutMs) ? Math.max(0, Math.floor(config.queueTimeoutMs)) : void 0;
|
|
386
|
-
this.keyResolver = config.key;
|
|
387
|
-
const engine = resolveHttpPoolEngine(config);
|
|
388
|
-
this.wasm = engine === "wasm" ? makeWasmHttpPermitPool({
|
|
389
|
-
concurrency: this.concurrency,
|
|
390
|
-
maxQueue: this.maxQueue,
|
|
391
|
-
queueTimeoutMs: this.queueTimeoutMs ?? 0
|
|
392
|
-
}) : void 0;
|
|
393
|
-
}
|
|
394
|
-
acquire(key, signal) {
|
|
395
|
-
return this.wasm ? this.acquireWasm(key, signal) : this.acquireJs(key, signal);
|
|
396
|
-
}
|
|
397
|
-
stats() {
|
|
398
|
-
const keys = Array.from(this.states.values()).map((state) => ({
|
|
399
|
-
key: state.key,
|
|
400
|
-
running: state.running,
|
|
401
|
-
queued: state.queue.length,
|
|
402
|
-
concurrency: this.concurrency,
|
|
403
|
-
maxQueue: this.maxQueue,
|
|
404
|
-
acquired: state.acquired,
|
|
405
|
-
released: state.released,
|
|
406
|
-
rejected: state.rejected,
|
|
407
|
-
queueTimeouts: state.queueTimeouts,
|
|
408
|
-
abortedWhileQueued: state.abortedWhileQueued
|
|
409
|
-
})).sort((a, b) => b.running + b.queued - (a.running + a.queued) || a.key.localeCompare(b.key));
|
|
410
|
-
return keys.reduce((acc, key) => ({
|
|
411
|
-
running: acc.running + key.running,
|
|
412
|
-
queued: acc.queued + key.queued,
|
|
413
|
-
acquired: acc.acquired + key.acquired,
|
|
414
|
-
released: acc.released + key.released,
|
|
415
|
-
rejected: acc.rejected + key.rejected,
|
|
416
|
-
queueTimeouts: acc.queueTimeouts + key.queueTimeouts,
|
|
417
|
-
abortedWhileQueued: acc.abortedWhileQueued + key.abortedWhileQueued,
|
|
418
|
-
wasm: this.wasm?.stats(),
|
|
419
|
-
keys: acc.keys.concat(key)
|
|
420
|
-
}), {
|
|
421
|
-
running: 0,
|
|
422
|
-
queued: 0,
|
|
423
|
-
acquired: 0,
|
|
424
|
-
released: 0,
|
|
425
|
-
rejected: 0,
|
|
426
|
-
queueTimeouts: 0,
|
|
427
|
-
abortedWhileQueued: 0,
|
|
428
|
-
...this.wasm ? { wasm: this.wasm.stats() } : {},
|
|
429
|
-
keys: []
|
|
430
|
-
});
|
|
431
|
-
}
|
|
432
|
-
acquireJs(key, signal) {
|
|
433
|
-
const state = this.getState(key);
|
|
434
|
-
if (signal.aborted) return Promise.reject(abortError());
|
|
435
|
-
if (state.running < this.concurrency) {
|
|
436
|
-
state.running++;
|
|
437
|
-
state.acquired++;
|
|
438
|
-
return Promise.resolve(this.makeLease(state));
|
|
439
|
-
}
|
|
440
|
-
if (state.queue.length >= this.maxQueue) {
|
|
441
|
-
state.rejected++;
|
|
442
|
-
return Promise.reject(poolRejectedError(key, this.maxQueue));
|
|
443
|
-
}
|
|
444
|
-
return new Promise((resolve, reject) => {
|
|
445
|
-
const waiter = { signal, resolve, reject };
|
|
446
|
-
const removeWaiter = () => this.removeWaiter(state, waiter);
|
|
447
|
-
const cleanup = () => this.cleanupWaiter(waiter);
|
|
448
|
-
waiter.abort = () => {
|
|
449
|
-
cleanup();
|
|
450
|
-
removeWaiter();
|
|
451
|
-
state.abortedWhileQueued++;
|
|
452
|
-
reject(abortError());
|
|
453
|
-
};
|
|
454
|
-
signal.addEventListener("abort", waiter.abort, { once: true });
|
|
455
|
-
if (this.queueTimeoutMs !== void 0 && this.queueTimeoutMs > 0) {
|
|
456
|
-
waiter.timer = setTimeout(() => {
|
|
457
|
-
cleanup();
|
|
458
|
-
removeWaiter();
|
|
459
|
-
state.queueTimeouts++;
|
|
460
|
-
reject(queueTimeoutError(key, this.queueTimeoutMs));
|
|
461
|
-
}, this.queueTimeoutMs);
|
|
462
|
-
}
|
|
463
|
-
state.queue.push(waiter);
|
|
464
|
-
});
|
|
465
|
-
}
|
|
466
|
-
acquireWasm(key, signal) {
|
|
467
|
-
const wasm = this.wasm;
|
|
468
|
-
const state = this.getState(key);
|
|
469
|
-
if (signal.aborted) return Promise.reject(abortError());
|
|
470
|
-
const subjectId = this.allocateSubjectId();
|
|
471
|
-
const decision = wasm.acquire(key, subjectId);
|
|
472
|
-
if (decision.kind === "run") {
|
|
473
|
-
state.running++;
|
|
474
|
-
state.acquired++;
|
|
475
|
-
return Promise.resolve(this.makeLease(state, decision.keyId));
|
|
476
|
-
}
|
|
477
|
-
if (decision.kind === "rejected") {
|
|
478
|
-
state.rejected++;
|
|
479
|
-
return Promise.reject(poolRejectedError(key, this.maxQueue));
|
|
480
|
-
}
|
|
481
|
-
return new Promise((resolve, reject) => {
|
|
482
|
-
const waiter = { signal, resolve, reject };
|
|
483
|
-
const removeWaiter = () => this.removeWaiter(state, waiter);
|
|
484
|
-
const cleanup = () => this.cleanupWaiter(waiter);
|
|
485
|
-
waiter.abort = () => {
|
|
486
|
-
cleanup();
|
|
487
|
-
removeWaiter();
|
|
488
|
-
wasm.cancel(decision.permitId);
|
|
489
|
-
this.wasmWaiters.delete(decision.permitId);
|
|
490
|
-
state.abortedWhileQueued++;
|
|
491
|
-
reject(abortError());
|
|
492
|
-
};
|
|
493
|
-
signal.addEventListener("abort", waiter.abort, { once: true });
|
|
494
|
-
state.queue.push(waiter);
|
|
495
|
-
this.wasmWaiters.set(decision.permitId, { waiter, state, keyId: decision.keyId });
|
|
496
|
-
this.scheduleWasmTimeoutPump();
|
|
137
|
+
try {
|
|
138
|
+
return asyncSucceed(JSON.stringify(bodyObj ?? {}));
|
|
139
|
+
} catch (error) {
|
|
140
|
+
return asyncFail({
|
|
141
|
+
_tag: "ValidationError",
|
|
142
|
+
message: `JSON request body could not be serialized: ${error instanceof Error ? error.message : String(error)}`,
|
|
143
|
+
body: "",
|
|
144
|
+
phase: "request",
|
|
145
|
+
schema: options.schemaName,
|
|
146
|
+
issues: [
|
|
147
|
+
makeSchemaIssue([], "JSON-serializable value", bodyObj, "Request body could not be serialized to JSON")
|
|
148
|
+
]
|
|
497
149
|
});
|
|
498
150
|
}
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
released = true;
|
|
523
|
-
if (state.running > 0) state.running--;
|
|
524
|
-
state.released++;
|
|
525
|
-
if (this.wasm && wasmKeyId !== void 0) {
|
|
526
|
-
this.handleWasmGrants(this.wasm.release(wasmKeyId));
|
|
527
|
-
this.scheduleWasmTimeoutPump();
|
|
528
|
-
return;
|
|
529
|
-
}
|
|
530
|
-
this.drain(state);
|
|
531
|
-
}
|
|
532
|
-
};
|
|
533
|
-
}
|
|
534
|
-
drain(state) {
|
|
535
|
-
while (state.running < this.concurrency && state.queue.length > 0) {
|
|
536
|
-
const waiter = state.queue.shift();
|
|
537
|
-
this.cleanupWaiter(waiter);
|
|
538
|
-
if (waiter.signal.aborted) {
|
|
539
|
-
state.abortedWhileQueued++;
|
|
540
|
-
waiter.reject(abortError());
|
|
541
|
-
continue;
|
|
542
|
-
}
|
|
543
|
-
state.running++;
|
|
544
|
-
state.acquired++;
|
|
545
|
-
waiter.resolve(this.makeLease(state));
|
|
546
|
-
}
|
|
547
|
-
}
|
|
548
|
-
handleWasmGrants(events) {
|
|
549
|
-
for (const event of events) {
|
|
550
|
-
const pending = this.wasmWaiters.get(event.permitId);
|
|
551
|
-
if (!pending) continue;
|
|
552
|
-
this.wasmWaiters.delete(event.permitId);
|
|
553
|
-
this.cleanupWaiter(pending.waiter);
|
|
554
|
-
this.removeWaiter(pending.state, pending.waiter);
|
|
555
|
-
if (pending.waiter.signal.aborted) {
|
|
556
|
-
pending.state.abortedWhileQueued++;
|
|
557
|
-
pending.waiter.reject(abortError());
|
|
558
|
-
continue;
|
|
559
|
-
}
|
|
560
|
-
pending.state.running++;
|
|
561
|
-
pending.state.acquired++;
|
|
562
|
-
pending.waiter.resolve(this.makeLease(pending.state, event.keyId));
|
|
563
|
-
}
|
|
564
|
-
}
|
|
565
|
-
handleWasmTimeouts(events) {
|
|
566
|
-
for (const event of events) {
|
|
567
|
-
const pending = this.wasmWaiters.get(event.permitId);
|
|
568
|
-
if (!pending) continue;
|
|
569
|
-
this.wasmWaiters.delete(event.permitId);
|
|
570
|
-
this.cleanupWaiter(pending.waiter);
|
|
571
|
-
this.removeWaiter(pending.state, pending.waiter);
|
|
572
|
-
pending.state.queueTimeouts++;
|
|
573
|
-
pending.waiter.reject(queueTimeoutError(pending.state.key, this.queueTimeoutMs ?? 0));
|
|
574
|
-
}
|
|
575
|
-
}
|
|
576
|
-
scheduleWasmTimeoutPump() {
|
|
577
|
-
if (!this.wasm) return;
|
|
578
|
-
if (this.wasmTimer !== void 0) clearTimeout(this.wasmTimer);
|
|
579
|
-
this.wasmTimer = void 0;
|
|
580
|
-
const next = this.wasm.nextDeadlineMs();
|
|
581
|
-
if (!Number.isFinite(next) || next < 0) return;
|
|
582
|
-
const delay = Math.max(0, Math.min(2 ** 31 - 1, Math.floor(next - Date.now())));
|
|
583
|
-
this.wasmTimer = setTimeout(() => {
|
|
584
|
-
this.wasmTimer = void 0;
|
|
585
|
-
if (!this.wasm) return;
|
|
586
|
-
this.handleWasmTimeouts(this.wasm.advanceTime());
|
|
587
|
-
this.scheduleWasmTimeoutPump();
|
|
588
|
-
}, delay);
|
|
589
|
-
if (typeof this.wasmTimer.unref === "function") this.wasmTimer.unref();
|
|
590
|
-
}
|
|
591
|
-
cleanupWaiter(waiter) {
|
|
592
|
-
if (waiter.timer !== void 0) {
|
|
593
|
-
clearTimeout(waiter.timer);
|
|
594
|
-
waiter.timer = void 0;
|
|
595
|
-
}
|
|
596
|
-
if (waiter.abort) {
|
|
597
|
-
waiter.signal.removeEventListener("abort", waiter.abort);
|
|
598
|
-
waiter.abort = void 0;
|
|
599
|
-
}
|
|
600
|
-
}
|
|
601
|
-
removeWaiter(state, waiter) {
|
|
602
|
-
const idx = state.queue.indexOf(waiter);
|
|
603
|
-
if (idx >= 0) state.queue.splice(idx, 1);
|
|
604
|
-
}
|
|
605
|
-
allocateSubjectId() {
|
|
606
|
-
const id = this.nextSubjectId >>> 0;
|
|
607
|
-
this.nextSubjectId = this.nextSubjectId + 1 >>> 0;
|
|
608
|
-
if (this.nextSubjectId === 0) this.nextSubjectId = 1;
|
|
609
|
-
return id === 0 ? this.allocateSubjectId() : id;
|
|
610
|
-
}
|
|
611
|
-
};
|
|
612
|
-
|
|
613
|
-
// src/http/client.ts
|
|
614
|
-
var emptyStats = () => ({
|
|
615
|
-
inFlight: 0,
|
|
616
|
-
started: 0,
|
|
617
|
-
succeeded: 0,
|
|
618
|
-
failed: 0,
|
|
619
|
-
aborted: 0,
|
|
620
|
-
timedOut: 0,
|
|
621
|
-
poolRejected: 0,
|
|
622
|
-
poolTimeouts: 0
|
|
623
|
-
});
|
|
624
|
-
var decorate = (run, stats = emptyStats) => Object.assign(((req) => run(req)), {
|
|
625
|
-
with: (mw) => decorate(mw(run), stats),
|
|
626
|
-
stats
|
|
627
|
-
});
|
|
628
|
-
var withMiddleware = (mw) => (c) => decorate(mw(c), c.stats);
|
|
629
|
-
var decorateStream = (run, stats = emptyStats) => Object.assign(((req) => run(req)), { stats });
|
|
630
|
-
var isTaggedHttpError = (e) => {
|
|
631
|
-
if (typeof e !== "object" || e === null || !("_tag" in e)) return false;
|
|
632
|
-
const tag = e._tag;
|
|
633
|
-
return tag === "Abort" || tag === "BadUrl" || tag === "FetchError" || tag === "Timeout" || tag === "PoolRejected" || tag === "PoolTimeout";
|
|
634
|
-
};
|
|
635
|
-
var isAbortError = (e) => typeof e === "object" && e !== null && "name" in e && e.name === "AbortError";
|
|
636
|
-
var normalizeHttpError = (e) => {
|
|
637
|
-
if (isTaggedHttpError(e)) return e;
|
|
638
|
-
if (isAbortError(e)) return { _tag: "Abort" };
|
|
639
|
-
return { _tag: "FetchError", message: String(e) };
|
|
640
|
-
};
|
|
641
|
-
var normalizeHeadersInit = (h) => {
|
|
642
|
-
if (!h) return void 0;
|
|
643
|
-
if (typeof Headers !== "undefined" && h instanceof Headers) {
|
|
644
|
-
const out = {};
|
|
645
|
-
h.forEach((v, k) => out[k] = v);
|
|
646
|
-
return out;
|
|
647
|
-
}
|
|
648
|
-
if (Array.isArray(h)) return Object.fromEntries(h);
|
|
649
|
-
if (typeof h === "object") return { ...h };
|
|
650
|
-
return void 0;
|
|
651
|
-
};
|
|
652
|
-
var normalizeRequest = (defaultHeaders) => (req0) => {
|
|
653
|
-
let req = Object.keys(defaultHeaders).length ? mergeHeadersUnder(defaultHeaders)(req0) : req0;
|
|
654
|
-
const initHeaders = normalizeHeadersInit(req0.init?.headers);
|
|
655
|
-
if (initHeaders && Object.keys(initHeaders).length) {
|
|
656
|
-
req = mergeHeadersUnder(initHeaders)(req);
|
|
657
|
-
}
|
|
658
|
-
return req;
|
|
659
|
-
};
|
|
660
|
-
var resolvePositiveTimeout = (value) => {
|
|
661
|
-
if (value === void 0 || !Number.isFinite(value)) return void 0;
|
|
662
|
-
const n = Math.floor(value);
|
|
663
|
-
return n > 0 ? n : void 0;
|
|
664
|
-
};
|
|
665
|
-
var makeHttpStats = (pool) => {
|
|
666
|
-
const stats = {
|
|
667
|
-
inFlight: 0,
|
|
668
|
-
started: 0,
|
|
669
|
-
succeeded: 0,
|
|
670
|
-
failed: 0,
|
|
671
|
-
aborted: 0,
|
|
672
|
-
timedOut: 0,
|
|
673
|
-
poolRejected: 0,
|
|
674
|
-
poolTimeouts: 0
|
|
675
|
-
};
|
|
676
|
-
const onStart = () => {
|
|
677
|
-
stats.inFlight++;
|
|
678
|
-
stats.started++;
|
|
679
|
-
};
|
|
680
|
-
const onFinish = (finish) => {
|
|
681
|
-
if (stats.inFlight > 0) stats.inFlight--;
|
|
682
|
-
stats.lastDurationMs = finish.durationMs;
|
|
683
|
-
if (finish.outcome === "success") {
|
|
684
|
-
stats.succeeded++;
|
|
685
|
-
return;
|
|
686
|
-
}
|
|
687
|
-
if (finish.outcome === "interrupt") {
|
|
688
|
-
stats.aborted++;
|
|
689
|
-
return;
|
|
690
|
-
}
|
|
691
|
-
if (finish.outcome === "timeout") {
|
|
692
|
-
stats.timedOut++;
|
|
693
|
-
return;
|
|
694
|
-
}
|
|
695
|
-
const err = normalizeHttpError(finish.error);
|
|
696
|
-
switch (err._tag) {
|
|
697
|
-
case "Abort":
|
|
698
|
-
stats.aborted++;
|
|
699
|
-
return;
|
|
700
|
-
case "Timeout":
|
|
701
|
-
stats.timedOut++;
|
|
702
|
-
return;
|
|
703
|
-
case "PoolRejected":
|
|
704
|
-
stats.poolRejected++;
|
|
705
|
-
stats.failed++;
|
|
706
|
-
return;
|
|
707
|
-
case "PoolTimeout":
|
|
708
|
-
stats.poolTimeouts++;
|
|
709
|
-
stats.failed++;
|
|
710
|
-
return;
|
|
711
|
-
default:
|
|
712
|
-
stats.failed++;
|
|
713
|
-
return;
|
|
714
|
-
}
|
|
715
|
-
};
|
|
716
|
-
const snapshot = () => ({
|
|
717
|
-
...stats,
|
|
718
|
-
...pool ? { pool: pool.stats() } : {}
|
|
719
|
-
});
|
|
720
|
-
return { onStart, onFinish, snapshot };
|
|
721
|
-
};
|
|
722
|
-
var makePool = (cfg) => cfg.pool === void 0 || cfg.pool === false ? void 0 : new HttpConcurrencyPool(cfg.pool);
|
|
723
|
-
var resolveRequestUrl = (req, baseUrl) => {
|
|
151
|
+
}
|
|
152
|
+
function decodeJsonBodyEffect(bodyText, validator, options) {
|
|
153
|
+
const result = decodeJsonBody(bodyText, validator, options);
|
|
154
|
+
return result.success ? asyncSucceed(result.data) : asyncFail(result.error);
|
|
155
|
+
}
|
|
156
|
+
function validatedJson(client, validator) {
|
|
157
|
+
return (req) => asyncFold(
|
|
158
|
+
client(req),
|
|
159
|
+
(error) => asyncFail(error),
|
|
160
|
+
(response) => decodeJsonBodyEffect(response.bodyText, validator)
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
function validatedJsonResponse(client, validator) {
|
|
164
|
+
return (req) => asyncFold(
|
|
165
|
+
client(req),
|
|
166
|
+
(error) => asyncFail(error),
|
|
167
|
+
(response) => asyncFlatMap(
|
|
168
|
+
decodeJsonBodyEffect(response.bodyText, validator),
|
|
169
|
+
(body) => asyncSucceed({ ...response, body })
|
|
170
|
+
)
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
function previewJson(value) {
|
|
724
174
|
try {
|
|
725
|
-
return
|
|
175
|
+
return JSON.stringify(value);
|
|
726
176
|
} catch {
|
|
727
|
-
return
|
|
177
|
+
return "[unserializable]";
|
|
728
178
|
}
|
|
729
|
-
}
|
|
730
|
-
var headersOf = (res) => {
|
|
731
|
-
const headers = {};
|
|
732
|
-
res.headers.forEach((v, k) => headers[k] = v);
|
|
733
|
-
return headers;
|
|
734
|
-
};
|
|
735
|
-
var fetchLabel = (req, url) => `http:${req.method}:${url.origin}`;
|
|
736
|
-
var timeoutReason = (req, url, timeoutMs) => ({
|
|
737
|
-
_tag: "Timeout",
|
|
738
|
-
timeoutMs,
|
|
739
|
-
phase: "request",
|
|
740
|
-
message: `HTTP ${req.method} ${url.origin} timed out after ${timeoutMs}ms`
|
|
741
|
-
});
|
|
742
|
-
var linkAbortSignals = (runtimeSignal, requestSignal) => {
|
|
743
|
-
if (!requestSignal) return { signal: runtimeSignal, cleanup: () => void 0 };
|
|
744
|
-
const controller = new AbortController();
|
|
745
|
-
const abort = (source) => {
|
|
746
|
-
try {
|
|
747
|
-
controller.abort(source.reason);
|
|
748
|
-
} catch {
|
|
749
|
-
controller.abort();
|
|
750
|
-
}
|
|
751
|
-
};
|
|
752
|
-
const abortFromRuntime = () => abort(runtimeSignal);
|
|
753
|
-
const abortFromRequest = () => abort(requestSignal);
|
|
754
|
-
if (runtimeSignal.aborted) abortFromRuntime();
|
|
755
|
-
else runtimeSignal.addEventListener("abort", abortFromRuntime, { once: true });
|
|
756
|
-
if (requestSignal.aborted) abortFromRequest();
|
|
757
|
-
else requestSignal.addEventListener("abort", abortFromRequest, { once: true });
|
|
758
|
-
return {
|
|
759
|
-
signal: controller.signal,
|
|
760
|
-
cleanup: () => {
|
|
761
|
-
runtimeSignal.removeEventListener("abort", abortFromRuntime);
|
|
762
|
-
requestSignal.removeEventListener("abort", abortFromRequest);
|
|
763
|
-
}
|
|
764
|
-
};
|
|
765
|
-
};
|
|
766
|
-
function makeHttpStream(cfg = {}) {
|
|
767
|
-
const baseUrl = cfg.baseUrl ?? "";
|
|
768
|
-
const defaultHeaders = cfg.headers ?? {};
|
|
769
|
-
const normalize = normalizeRequest(defaultHeaders);
|
|
770
|
-
const pool = makePool(cfg);
|
|
771
|
-
const metrics = makeHttpStats(pool);
|
|
772
|
-
const run = (req0) => {
|
|
773
|
-
const req = normalize(req0);
|
|
774
|
-
const url = resolveRequestUrl(req, baseUrl);
|
|
775
|
-
if (!(url instanceof URL)) return asyncFail(url);
|
|
776
|
-
const timeoutMs = resolvePositiveTimeout(req.timeoutMs ?? cfg.timeoutMs);
|
|
777
|
-
return fromPromiseAbortable(
|
|
778
|
-
async (signal) => {
|
|
779
|
-
let lease;
|
|
780
|
-
const linkedSignal = linkAbortSignals(signal, req.init?.signal);
|
|
781
|
-
let cleanupTransferredToBody = false;
|
|
782
|
-
try {
|
|
783
|
-
if (pool) {
|
|
784
|
-
const key = resolveHttpPoolKey(pool.keyResolver, req, url);
|
|
785
|
-
lease = await pool.acquire(key, linkedSignal.signal);
|
|
786
|
-
}
|
|
787
|
-
const started = performance.now();
|
|
788
|
-
const res = await fetch(url, {
|
|
789
|
-
...req.init ?? {},
|
|
790
|
-
method: req.method,
|
|
791
|
-
headers: Request.headers.get(req),
|
|
792
|
-
body: req.body,
|
|
793
|
-
signal: linkedSignal.signal
|
|
794
|
-
});
|
|
795
|
-
const headers = headersOf(res);
|
|
796
|
-
const body = streamFromReadableStream(res.body, normalizeHttpError, {
|
|
797
|
-
signal: linkedSignal.signal,
|
|
798
|
-
onRelease: linkedSignal.cleanup
|
|
799
|
-
});
|
|
800
|
-
cleanupTransferredToBody = res.body !== null;
|
|
801
|
-
lease?.release();
|
|
802
|
-
lease = void 0;
|
|
803
|
-
return {
|
|
804
|
-
status: res.status,
|
|
805
|
-
statusText: res.statusText,
|
|
806
|
-
headers,
|
|
807
|
-
body,
|
|
808
|
-
ms: Math.round(performance.now() - started)
|
|
809
|
-
};
|
|
810
|
-
} finally {
|
|
811
|
-
if (!cleanupTransferredToBody) {
|
|
812
|
-
linkedSignal.cleanup();
|
|
813
|
-
}
|
|
814
|
-
lease?.release();
|
|
815
|
-
}
|
|
816
|
-
},
|
|
817
|
-
normalizeHttpError,
|
|
818
|
-
{
|
|
819
|
-
label: fetchLabel(req, url),
|
|
820
|
-
timeoutMs,
|
|
821
|
-
timeoutReason: timeoutMs ? () => timeoutReason(req, url, timeoutMs) : void 0,
|
|
822
|
-
onStart: metrics.onStart,
|
|
823
|
-
onFinish: metrics.onFinish
|
|
824
|
-
}
|
|
825
|
-
);
|
|
826
|
-
};
|
|
827
|
-
return decorateStream(run, metrics.snapshot);
|
|
828
|
-
}
|
|
829
|
-
function makeHttp(cfg = {}) {
|
|
830
|
-
const baseUrl = cfg.baseUrl ?? "";
|
|
831
|
-
const defaultHeaders = cfg.headers ?? {};
|
|
832
|
-
const normalize = normalizeRequest(defaultHeaders);
|
|
833
|
-
const pool = makePool(cfg);
|
|
834
|
-
const metrics = makeHttpStats(pool);
|
|
835
|
-
const run = (req0) => {
|
|
836
|
-
const req = normalize(req0);
|
|
837
|
-
const url = resolveRequestUrl(req, baseUrl);
|
|
838
|
-
if (!(url instanceof URL)) return asyncFail(url);
|
|
839
|
-
const timeoutMs = resolvePositiveTimeout(req.timeoutMs ?? cfg.timeoutMs);
|
|
840
|
-
return fromPromiseAbortable(
|
|
841
|
-
async (signal) => {
|
|
842
|
-
let lease;
|
|
843
|
-
const linkedSignal = linkAbortSignals(signal, req.init?.signal);
|
|
844
|
-
try {
|
|
845
|
-
if (pool) {
|
|
846
|
-
const key = resolveHttpPoolKey(pool.keyResolver, req, url);
|
|
847
|
-
lease = await pool.acquire(key, linkedSignal.signal);
|
|
848
|
-
}
|
|
849
|
-
const started = performance.now();
|
|
850
|
-
const res = await fetch(url, {
|
|
851
|
-
...req.init ?? {},
|
|
852
|
-
method: req.method,
|
|
853
|
-
headers: Request.headers.get(req),
|
|
854
|
-
body: req.body,
|
|
855
|
-
signal: linkedSignal.signal
|
|
856
|
-
});
|
|
857
|
-
const bodyText = await res.text();
|
|
858
|
-
const headers = headersOf(res);
|
|
859
|
-
return {
|
|
860
|
-
status: res.status,
|
|
861
|
-
statusText: res.statusText,
|
|
862
|
-
headers,
|
|
863
|
-
bodyText,
|
|
864
|
-
ms: Math.round(performance.now() - started)
|
|
865
|
-
};
|
|
866
|
-
} finally {
|
|
867
|
-
linkedSignal.cleanup();
|
|
868
|
-
lease?.release();
|
|
869
|
-
}
|
|
870
|
-
},
|
|
871
|
-
normalizeHttpError,
|
|
872
|
-
{
|
|
873
|
-
label: fetchLabel(req, url),
|
|
874
|
-
timeoutMs,
|
|
875
|
-
timeoutReason: timeoutMs ? () => timeoutReason(req, url, timeoutMs) : void 0,
|
|
876
|
-
onStart: metrics.onStart,
|
|
877
|
-
onFinish: metrics.onFinish
|
|
878
|
-
}
|
|
879
|
-
);
|
|
880
|
-
};
|
|
881
|
-
return decorate(run, metrics.snapshot);
|
|
882
|
-
}
|
|
883
|
-
var withRetryStream = (p) => (next) => {
|
|
884
|
-
const retryOnStatus = p.retryOnStatus ?? defaultRetryOnStatus;
|
|
885
|
-
const retryOnError = p.retryOnError ?? defaultRetryOnError;
|
|
886
|
-
const retryOnMethods = p.retryOnMethods ?? defaultRetryableMethods;
|
|
887
|
-
const maxElapsedMs = normalizeRetryBudget(p.maxElapsedMs);
|
|
888
|
-
const run = (req) => {
|
|
889
|
-
if (!retryOnMethods.includes(req.method)) return next(req);
|
|
890
|
-
const startedAt = performance.now();
|
|
891
|
-
const remainingBudget = () => maxElapsedMs === void 0 ? Number.POSITIVE_INFINITY : maxElapsedMs - (performance.now() - startedAt);
|
|
892
|
-
const delayWithinBudget = (delayMs) => Math.max(0, Math.min(delayMs, remainingBudget()));
|
|
893
|
-
const loop = (attempt) => asyncFold(
|
|
894
|
-
next(req),
|
|
895
|
-
(e) => {
|
|
896
|
-
if (e._tag === "Abort" || e._tag === "BadUrl" || e._tag === "PoolRejected") return asyncFail(e);
|
|
897
|
-
const canRetry = attempt < p.maxRetries && retryOnError(e) && remainingBudget() > 0;
|
|
898
|
-
if (!canRetry) return asyncFail(e);
|
|
899
|
-
const d = delayWithinBudget(backoffDelayMs(attempt, p.baseDelayMs, p.maxDelayMs));
|
|
900
|
-
if (d <= 0 && maxElapsedMs !== void 0) return asyncFail(e);
|
|
901
|
-
p.onRetry?.({
|
|
902
|
-
attempt,
|
|
903
|
-
delayMs: d,
|
|
904
|
-
error: e,
|
|
905
|
-
status: void 0,
|
|
906
|
-
url: req.url,
|
|
907
|
-
method: req.method,
|
|
908
|
-
timestamp: Date.now()
|
|
909
|
-
});
|
|
910
|
-
return asyncFlatMap(sleep(d), () => loop(attempt + 1));
|
|
911
|
-
},
|
|
912
|
-
(w) => {
|
|
913
|
-
const canRetry = attempt < p.maxRetries && retryOnStatus(w.status) && remainingBudget() > 0;
|
|
914
|
-
if (!canRetry) return asyncSucceed(w);
|
|
915
|
-
const ra = p.respectRetryAfter === false ? void 0 : retryAfterMs(w.headers);
|
|
916
|
-
const rawDelay = ra === void 0 ? backoffDelayMs(attempt, p.baseDelayMs, p.maxDelayMs) : Math.min(ra, p.maxDelayMs);
|
|
917
|
-
const d = delayWithinBudget(rawDelay);
|
|
918
|
-
if (d <= 0 && maxElapsedMs !== void 0) return asyncSucceed(w);
|
|
919
|
-
p.onRetry?.({
|
|
920
|
-
attempt,
|
|
921
|
-
delayMs: d,
|
|
922
|
-
error: void 0,
|
|
923
|
-
status: w.status,
|
|
924
|
-
url: req.url,
|
|
925
|
-
method: req.method,
|
|
926
|
-
timestamp: Date.now()
|
|
927
|
-
});
|
|
928
|
-
return asyncFlatMap(sleep(d), () => loop(attempt + 1));
|
|
929
|
-
}
|
|
930
|
-
);
|
|
931
|
-
return loop(0);
|
|
932
|
-
};
|
|
933
|
-
return decorateStream(run, next.stats);
|
|
934
|
-
};
|
|
179
|
+
}
|
|
935
180
|
|
|
936
181
|
// src/http/httpClient.ts
|
|
937
182
|
var resolveFinalUrl = (baseUrl, url) => {
|
|
@@ -946,7 +191,7 @@ var createHttpCore = (cfg = {}) => {
|
|
|
946
191
|
const withPromise = (eff) => withAsyncPromise((e, env) => toPromise(e, env))(eff);
|
|
947
192
|
const requestRaw = (req) => wire(req);
|
|
948
193
|
const splitInit = (init) => {
|
|
949
|
-
const { headers, timeoutMs, poolKey, ...rest } = init ?? {};
|
|
194
|
+
const { headers, timeoutMs, poolKey, schema: schema2, schemaName, bodySchema, bodySchemaName, ...rest } = init ?? {};
|
|
950
195
|
return {
|
|
951
196
|
headers: normalizeHeadersInit(headers),
|
|
952
197
|
timeoutMs: typeof timeoutMs === "number" ? timeoutMs : void 0,
|
|
@@ -955,24 +200,28 @@ var createHttpCore = (cfg = {}) => {
|
|
|
955
200
|
};
|
|
956
201
|
};
|
|
957
202
|
const applyInitHeaders = (headers) => (req) => headers ? mergeHeaders(headers)(req) : req;
|
|
958
|
-
const
|
|
959
|
-
const
|
|
203
|
+
const buildReq2 = (method, url, init, body) => {
|
|
204
|
+
const s2 = splitInit(init);
|
|
960
205
|
const req = {
|
|
961
206
|
method,
|
|
962
207
|
url,
|
|
963
208
|
...body && body.length > 0 ? { body } : {},
|
|
964
|
-
...
|
|
965
|
-
...
|
|
966
|
-
init:
|
|
209
|
+
...s2.timeoutMs !== void 0 ? { timeoutMs: s2.timeoutMs } : {},
|
|
210
|
+
...s2.poolKey !== void 0 ? { poolKey: s2.poolKey } : {},
|
|
211
|
+
init: s2.init
|
|
967
212
|
};
|
|
968
|
-
return applyInitHeaders(
|
|
213
|
+
return applyInitHeaders(s2.headers)(req);
|
|
969
214
|
};
|
|
970
|
-
const
|
|
215
|
+
const toResponse2 = (w, body) => ({
|
|
971
216
|
status: w.status,
|
|
972
217
|
statusText: w.statusText,
|
|
973
218
|
headers: w.headers,
|
|
974
219
|
body
|
|
975
220
|
});
|
|
221
|
+
const decodeResponse2 = (w, validator, schemaName) => asyncFlatMap(
|
|
222
|
+
decodeJsonBodyEffect(w.bodyText, validator, { schemaName }),
|
|
223
|
+
(body) => asyncSucceed(toResponse2(w, body))
|
|
224
|
+
);
|
|
976
225
|
return {
|
|
977
226
|
cfg,
|
|
978
227
|
wire,
|
|
@@ -980,8 +229,9 @@ var createHttpCore = (cfg = {}) => {
|
|
|
980
229
|
requestRaw,
|
|
981
230
|
splitInit,
|
|
982
231
|
applyInitHeaders,
|
|
983
|
-
buildReq,
|
|
984
|
-
toResponse
|
|
232
|
+
buildReq: buildReq2,
|
|
233
|
+
toResponse: toResponse2,
|
|
234
|
+
decodeResponse: decodeResponse2
|
|
985
235
|
};
|
|
986
236
|
};
|
|
987
237
|
function httpClient(cfg = {}) {
|
|
@@ -995,20 +245,25 @@ function httpClient(cfg = {}) {
|
|
|
995
245
|
const req = core.buildReq("GET", url, init);
|
|
996
246
|
return core.withPromise(mapTryAsync(requestRaw(req), (w) => core.toResponse(w, w.bodyText)));
|
|
997
247
|
};
|
|
998
|
-
const getJson = (url, init) => {
|
|
248
|
+
const getJson = ((url, init) => {
|
|
999
249
|
const base = core.buildReq("GET", url, init);
|
|
1000
250
|
const req = setHeaderIfMissing("accept", "application/json")(base);
|
|
1001
|
-
return core.withPromise(
|
|
1002
|
-
};
|
|
1003
|
-
const postJson = (url, bodyObj, init) => {
|
|
1004
|
-
const base = core.buildReq("POST", url, init, JSON.stringify(bodyObj ?? {}));
|
|
1005
|
-
const req = setHeaderIfMissing("content-type", "application/json")(
|
|
1006
|
-
setHeaderIfMissing("accept", "application/json")(base)
|
|
1007
|
-
);
|
|
251
|
+
return core.withPromise(asyncFlatMap(requestRaw(req), (w) => core.decodeResponse(w, init?.schema, init?.schemaName)));
|
|
252
|
+
});
|
|
253
|
+
const postJson = ((url, bodyObj, init) => {
|
|
1008
254
|
return core.withPromise(
|
|
1009
|
-
|
|
255
|
+
asyncFlatMap(
|
|
256
|
+
encodeJsonBodyEffect(bodyObj, init?.bodySchema, { schemaName: init?.bodySchemaName }),
|
|
257
|
+
(bodyText) => {
|
|
258
|
+
const base = core.buildReq("POST", url, init, bodyText);
|
|
259
|
+
const req = setHeaderIfMissing("content-type", "application/json")(
|
|
260
|
+
setHeaderIfMissing("accept", "application/json")(base)
|
|
261
|
+
);
|
|
262
|
+
return asyncFlatMap(requestRaw(req), (w) => core.decodeResponse(w, init?.schema, init?.schemaName));
|
|
263
|
+
}
|
|
264
|
+
)
|
|
1010
265
|
);
|
|
1011
|
-
};
|
|
266
|
+
});
|
|
1012
267
|
return {
|
|
1013
268
|
request,
|
|
1014
269
|
get,
|
|
@@ -1050,17 +305,28 @@ function httpClientWithMeta(cfg = {}) {
|
|
|
1050
305
|
return request(req);
|
|
1051
306
|
};
|
|
1052
307
|
const postJson = (url, bodyObj, init) => {
|
|
1053
|
-
const base = core.buildReq("POST", url, init, JSON.stringify(bodyObj ?? {}));
|
|
1054
|
-
const req = setHeaderIfMissing("content-type", "application/json")(
|
|
1055
|
-
setHeaderIfMissing("accept", "application/json")(base)
|
|
1056
|
-
);
|
|
1057
308
|
const startedAt = Date.now();
|
|
1058
309
|
return core.withPromise(
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
310
|
+
asyncFlatMap(
|
|
311
|
+
encodeJsonBodyEffect(bodyObj, init?.bodySchema, { schemaName: init?.bodySchemaName }),
|
|
312
|
+
(bodyText) => {
|
|
313
|
+
const base = core.buildReq("POST", url, init, bodyText);
|
|
314
|
+
const req = setHeaderIfMissing("content-type", "application/json")(
|
|
315
|
+
setHeaderIfMissing("accept", "application/json")(base)
|
|
316
|
+
);
|
|
317
|
+
return asyncFlatMap(
|
|
318
|
+
core.requestRaw(req),
|
|
319
|
+
(w) => asyncFlatMap(
|
|
320
|
+
core.decodeResponse(w, init?.schema, init?.schemaName),
|
|
321
|
+
(response) => asyncSucceed({
|
|
322
|
+
wire: w,
|
|
323
|
+
response,
|
|
324
|
+
meta: mkMeta(req, w, startedAt)
|
|
325
|
+
})
|
|
326
|
+
)
|
|
327
|
+
);
|
|
328
|
+
}
|
|
329
|
+
)
|
|
1064
330
|
);
|
|
1065
331
|
};
|
|
1066
332
|
const getText = (url, init) => {
|
|
@@ -1079,11 +345,17 @@ function httpClientWithMeta(cfg = {}) {
|
|
|
1079
345
|
const req = setHeaderIfMissing("accept", "application/json")(base);
|
|
1080
346
|
const startedAt = Date.now();
|
|
1081
347
|
return core.withPromise(
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
348
|
+
asyncFlatMap(
|
|
349
|
+
core.requestRaw(req),
|
|
350
|
+
(w) => asyncFlatMap(
|
|
351
|
+
core.decodeResponse(w, init?.schema, init?.schemaName),
|
|
352
|
+
(response) => asyncSucceed({
|
|
353
|
+
wire: w,
|
|
354
|
+
response,
|
|
355
|
+
meta: mkMeta(req, w, startedAt)
|
|
356
|
+
})
|
|
357
|
+
)
|
|
358
|
+
)
|
|
1087
359
|
);
|
|
1088
360
|
};
|
|
1089
361
|
return {
|
|
@@ -1124,137 +396,77 @@ function httpClientStream(cfg = {}) {
|
|
|
1124
396
|
return make(wire);
|
|
1125
397
|
}
|
|
1126
398
|
|
|
1127
|
-
// src/http/effectRunner.ts
|
|
1128
|
-
function registerHttpEffect(effect, env, cb) {
|
|
1129
|
-
let done = false;
|
|
1130
|
-
let currentCancel;
|
|
1131
|
-
const finish = (exit) => {
|
|
1132
|
-
if (done) return;
|
|
1133
|
-
done = true;
|
|
1134
|
-
currentCancel = void 0;
|
|
1135
|
-
cb(exit);
|
|
1136
|
-
};
|
|
1137
|
-
const run = (current, cont) => {
|
|
1138
|
-
if (done) return;
|
|
1139
|
-
switch (current._tag) {
|
|
1140
|
-
case "Succeed":
|
|
1141
|
-
cont({ _tag: "Success", value: current.value });
|
|
1142
|
-
return;
|
|
1143
|
-
case "Fail":
|
|
1144
|
-
cont({ _tag: "Failure", cause: Cause.fail(current.error) });
|
|
1145
|
-
return;
|
|
1146
|
-
case "Sync":
|
|
1147
|
-
try {
|
|
1148
|
-
cont({ _tag: "Success", value: current.thunk(env) });
|
|
1149
|
-
} catch (e) {
|
|
1150
|
-
cont({ _tag: "Failure", cause: Cause.die(e) });
|
|
1151
|
-
}
|
|
1152
|
-
return;
|
|
1153
|
-
case "Async": {
|
|
1154
|
-
const cancel = current.register(env, (exit) => {
|
|
1155
|
-
currentCancel = void 0;
|
|
1156
|
-
if (done) return;
|
|
1157
|
-
cont(exit);
|
|
1158
|
-
});
|
|
1159
|
-
currentCancel = typeof cancel === "function" ? cancel : void 0;
|
|
1160
|
-
return;
|
|
1161
|
-
}
|
|
1162
|
-
case "FlatMap":
|
|
1163
|
-
run(current.first, (exit) => {
|
|
1164
|
-
if (done) return;
|
|
1165
|
-
if (exit._tag === "Failure") {
|
|
1166
|
-
cont(exit);
|
|
1167
|
-
return;
|
|
1168
|
-
}
|
|
1169
|
-
try {
|
|
1170
|
-
run(current.andThen(exit.value), cont);
|
|
1171
|
-
} catch (e) {
|
|
1172
|
-
cont({ _tag: "Failure", cause: Cause.die(e) });
|
|
1173
|
-
}
|
|
1174
|
-
});
|
|
1175
|
-
return;
|
|
1176
|
-
case "Fold":
|
|
1177
|
-
run(current.first, (exit) => {
|
|
1178
|
-
if (done) return;
|
|
1179
|
-
try {
|
|
1180
|
-
if (exit._tag === "Success") {
|
|
1181
|
-
run(current.onSuccess(exit.value), cont);
|
|
1182
|
-
return;
|
|
1183
|
-
}
|
|
1184
|
-
if (exit.cause._tag === "Fail") {
|
|
1185
|
-
run(current.onFailure(exit.cause.error), cont);
|
|
1186
|
-
return;
|
|
1187
|
-
}
|
|
1188
|
-
cont(exit);
|
|
1189
|
-
} catch (e) {
|
|
1190
|
-
cont({ _tag: "Failure", cause: Cause.die(e) });
|
|
1191
|
-
}
|
|
1192
|
-
});
|
|
1193
|
-
return;
|
|
1194
|
-
case "Fork":
|
|
1195
|
-
cont({ _tag: "Success", value: void 0 });
|
|
1196
|
-
return;
|
|
1197
|
-
}
|
|
1198
|
-
};
|
|
1199
|
-
run(effect, finish);
|
|
1200
|
-
return () => {
|
|
1201
|
-
if (done) return;
|
|
1202
|
-
const cancel = currentCancel;
|
|
1203
|
-
currentCancel = void 0;
|
|
1204
|
-
done = true;
|
|
1205
|
-
try {
|
|
1206
|
-
cancel?.();
|
|
1207
|
-
} finally {
|
|
1208
|
-
cb({ _tag: "Failure", cause: Cause.interrupt() });
|
|
1209
|
-
}
|
|
1210
|
-
};
|
|
1211
|
-
}
|
|
1212
|
-
|
|
1213
399
|
// src/http/circuitBreaker.ts
|
|
1214
400
|
function withCircuitBreaker(config = {}) {
|
|
1215
401
|
if (config.perOrigin) {
|
|
1216
402
|
const breakers = /* @__PURE__ */ new Map();
|
|
1217
|
-
const getBreaker = (url) => {
|
|
403
|
+
const getBreaker = (url, onOpen) => {
|
|
1218
404
|
try {
|
|
1219
405
|
const origin = new URL(url).origin;
|
|
1220
406
|
if (!breakers.has(origin)) {
|
|
1221
|
-
breakers.set(origin, makeCircuitBreaker(
|
|
407
|
+
breakers.set(origin, makeCircuitBreaker({
|
|
408
|
+
...config,
|
|
409
|
+
onStateChange: (from, to) => {
|
|
410
|
+
config.onStateChange?.(from, to);
|
|
411
|
+
if (to === "open") onOpen();
|
|
412
|
+
}
|
|
413
|
+
}));
|
|
1222
414
|
}
|
|
1223
415
|
return breakers.get(origin);
|
|
1224
416
|
} catch {
|
|
1225
417
|
if (!breakers.has("__global__")) {
|
|
1226
|
-
breakers.set("__global__", makeCircuitBreaker(
|
|
418
|
+
breakers.set("__global__", makeCircuitBreaker({
|
|
419
|
+
...config,
|
|
420
|
+
onStateChange: (from, to) => {
|
|
421
|
+
config.onStateChange?.(from, to);
|
|
422
|
+
if (to === "open") onOpen();
|
|
423
|
+
}
|
|
424
|
+
}));
|
|
1227
425
|
}
|
|
1228
426
|
return breakers.get("__global__");
|
|
1229
427
|
}
|
|
1230
428
|
};
|
|
1231
429
|
return (next) => (req) => {
|
|
1232
|
-
const
|
|
1233
|
-
|
|
430
|
+
const limiter = resolveAdaptiveLimiter(config, next);
|
|
431
|
+
const limiterKey = resolveAdaptiveLimiterKey(config, limiter, req);
|
|
432
|
+
const onOpen = () => limiterKey !== void 0 && limiter?.markCircuitOpen(limiterKey);
|
|
433
|
+
const breaker2 = getBreaker(req.url, onOpen);
|
|
434
|
+
return protectLazy(breaker2, next, req, onOpen);
|
|
1234
435
|
};
|
|
1235
436
|
}
|
|
1236
437
|
const breaker = makeCircuitBreaker({
|
|
1237
438
|
...config,
|
|
439
|
+
onStateChange: (from, to) => {
|
|
440
|
+
config.onStateChange?.(from, to);
|
|
441
|
+
},
|
|
1238
442
|
isFailure: config.isFailure ?? ((e) => {
|
|
1239
443
|
const err = e;
|
|
1240
444
|
return err._tag !== "BadUrl" && err._tag !== "Abort";
|
|
1241
445
|
})
|
|
1242
446
|
});
|
|
1243
447
|
return (next) => (req) => {
|
|
1244
|
-
|
|
448
|
+
const limiter = resolveAdaptiveLimiter(config, next);
|
|
449
|
+
const limiterKey = resolveAdaptiveLimiterKey(config, limiter, req);
|
|
450
|
+
const onOpen = () => limiterKey !== void 0 && limiter?.markCircuitOpen(limiterKey);
|
|
451
|
+
return protectLazy(breaker, next, req, onOpen);
|
|
1245
452
|
};
|
|
1246
453
|
}
|
|
1247
|
-
function protectLazy(breaker, next, req) {
|
|
454
|
+
function protectLazy(breaker, next, req, onOpen) {
|
|
1248
455
|
return {
|
|
1249
456
|
_tag: "Async",
|
|
1250
457
|
register: (env, cb) => {
|
|
1251
458
|
let cancel;
|
|
1252
459
|
try {
|
|
460
|
+
if (breaker.state() === "open") onOpen?.();
|
|
461
|
+
const finish = (exit) => {
|
|
462
|
+
if (breaker.state() === "open") onOpen?.();
|
|
463
|
+
cb(exit);
|
|
464
|
+
};
|
|
1253
465
|
const deferred = {
|
|
1254
466
|
_tag: "Async",
|
|
1255
467
|
register: (innerEnv, innerCb) => registerHttpEffect(next(req), innerEnv, innerCb)
|
|
1256
468
|
};
|
|
1257
|
-
cancel = registerHttpEffect(breaker.protect(deferred), env,
|
|
469
|
+
cancel = registerHttpEffect(breaker.protect(deferred), env, finish);
|
|
1258
470
|
} catch (error) {
|
|
1259
471
|
cb({
|
|
1260
472
|
_tag: "Failure",
|
|
@@ -1268,48 +480,95 @@ function protectLazy(breaker, next, req) {
|
|
|
1268
480
|
}
|
|
1269
481
|
};
|
|
1270
482
|
}
|
|
483
|
+
function resolveAdaptiveLimiter(config, next) {
|
|
484
|
+
return config.adaptiveLimiter ?? next.adaptiveLimiter;
|
|
485
|
+
}
|
|
486
|
+
function resolveAdaptiveLimiterKey(config, limiter, req) {
|
|
487
|
+
if (!limiter) return void 0;
|
|
488
|
+
if (config.adaptiveLimiterKey) return config.adaptiveLimiterKey(req);
|
|
489
|
+
if (req.poolKey) return req.poolKey;
|
|
490
|
+
if (limiter.keyResolver === "global") return "global";
|
|
491
|
+
try {
|
|
492
|
+
const url = new URL(req.url);
|
|
493
|
+
if (limiter.keyResolver === "host") return url.host;
|
|
494
|
+
return url.origin;
|
|
495
|
+
} catch {
|
|
496
|
+
return "global";
|
|
497
|
+
}
|
|
498
|
+
}
|
|
1271
499
|
|
|
1272
500
|
// src/http/tracing.ts
|
|
1273
501
|
function withTracing(tracer) {
|
|
1274
502
|
return (next) => (req) => {
|
|
1275
503
|
return tracer.span(
|
|
1276
|
-
`HTTP ${req.method} ${req.url}`,
|
|
1277
|
-
next(req),
|
|
1278
|
-
{
|
|
1279
|
-
"http.method": req.method,
|
|
1280
|
-
"http.url": req.url,
|
|
1281
|
-
...req.headers?.["content-type"] ? { "http.content_type": req.headers["content-type"] } : {}
|
|
1282
|
-
}
|
|
1283
|
-
);
|
|
1284
|
-
};
|
|
1285
|
-
}
|
|
1286
|
-
|
|
1287
|
-
// src/http/validation.ts
|
|
1288
|
-
function validatedJson(client, validator) {
|
|
1289
|
-
return (req) => asyncFold(
|
|
1290
|
-
client(req),
|
|
1291
|
-
(error) => asyncFail(error),
|
|
1292
|
-
(response) => {
|
|
1293
|
-
try {
|
|
1294
|
-
const parsed = JSON.parse(response.bodyText);
|
|
1295
|
-
const result = validator(parsed);
|
|
1296
|
-
if (result.success) {
|
|
1297
|
-
return asyncSucceed(result.data);
|
|
1298
|
-
}
|
|
1299
|
-
return asyncFail({
|
|
1300
|
-
_tag: "ValidationError",
|
|
1301
|
-
message: result.error,
|
|
1302
|
-
body: response.bodyText
|
|
1303
|
-
});
|
|
1304
|
-
} catch (e) {
|
|
1305
|
-
return asyncFail({
|
|
1306
|
-
_tag: "ValidationError",
|
|
1307
|
-
message: `JSON parse error: ${String(e)}`,
|
|
1308
|
-
body: response.bodyText
|
|
1309
|
-
});
|
|
504
|
+
`HTTP ${req.method} ${req.url}`,
|
|
505
|
+
next(req),
|
|
506
|
+
{
|
|
507
|
+
"http.method": req.method,
|
|
508
|
+
"http.url": req.url,
|
|
509
|
+
...req.headers?.["content-type"] ? { "http.content_type": req.headers["content-type"] } : {}
|
|
1310
510
|
}
|
|
1311
|
-
|
|
1312
|
-
|
|
511
|
+
);
|
|
512
|
+
};
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
// src/http/errors.ts
|
|
516
|
+
var HTTP_ERROR_TAGS = /* @__PURE__ */ new Set([
|
|
517
|
+
"Abort",
|
|
518
|
+
"BadUrl",
|
|
519
|
+
"FetchError",
|
|
520
|
+
"Timeout",
|
|
521
|
+
"PoolRejected",
|
|
522
|
+
"PoolTimeout",
|
|
523
|
+
"PoolClosed",
|
|
524
|
+
"BatchSplitError"
|
|
525
|
+
]);
|
|
526
|
+
function isHttpError(error) {
|
|
527
|
+
return hasTag(error) && HTTP_ERROR_TAGS.has(error._tag);
|
|
528
|
+
}
|
|
529
|
+
function isValidationError(error) {
|
|
530
|
+
return hasTag(error) && error._tag === "ValidationError";
|
|
531
|
+
}
|
|
532
|
+
function isCircuitBreakerOpen(error) {
|
|
533
|
+
return hasTag(error) && error._tag === "CircuitBreakerOpen";
|
|
534
|
+
}
|
|
535
|
+
function isKnownHttpError(error) {
|
|
536
|
+
return isHttpError(error) || isValidationError(error) || isCircuitBreakerOpen(error);
|
|
537
|
+
}
|
|
538
|
+
function matchHttpError(error, handlers) {
|
|
539
|
+
if (isKnownHttpError(error)) {
|
|
540
|
+
const handler = handlers[error._tag];
|
|
541
|
+
if (handler) return handler(error);
|
|
542
|
+
}
|
|
543
|
+
return handlers.default?.(error);
|
|
544
|
+
}
|
|
545
|
+
function formatHttpError(error) {
|
|
546
|
+
if (isValidationError(error)) {
|
|
547
|
+
const phase = error.phase ? `${error.phase} ` : "";
|
|
548
|
+
return `${phase}validation failed: ${error.message}`;
|
|
549
|
+
}
|
|
550
|
+
if (isCircuitBreakerOpen(error)) {
|
|
551
|
+
return `Circuit breaker is open after ${error.failures} failure(s)`;
|
|
552
|
+
}
|
|
553
|
+
if (!isHttpError(error)) {
|
|
554
|
+
return error instanceof Error ? error.message : String(error);
|
|
555
|
+
}
|
|
556
|
+
switch (error._tag) {
|
|
557
|
+
case "Abort":
|
|
558
|
+
return "HTTP request aborted";
|
|
559
|
+
case "BadUrl":
|
|
560
|
+
case "FetchError":
|
|
561
|
+
case "PoolRejected":
|
|
562
|
+
case "PoolTimeout":
|
|
563
|
+
case "PoolClosed":
|
|
564
|
+
case "BatchSplitError":
|
|
565
|
+
return error.message;
|
|
566
|
+
case "Timeout":
|
|
567
|
+
return error.message;
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
function hasTag(error) {
|
|
571
|
+
return typeof error === "object" && error !== null && typeof error._tag === "string";
|
|
1313
572
|
}
|
|
1314
573
|
|
|
1315
574
|
// src/http/body.ts
|
|
@@ -1522,6 +781,186 @@ function rejectAll(entry, err) {
|
|
|
1522
781
|
}
|
|
1523
782
|
}
|
|
1524
783
|
|
|
784
|
+
// src/http/lifecycle/batch.ts
|
|
785
|
+
function clamp(value, min, max) {
|
|
786
|
+
if (!Number.isFinite(value)) return min;
|
|
787
|
+
return Math.max(min, Math.min(max, Math.floor(value)));
|
|
788
|
+
}
|
|
789
|
+
function safeEmitBatch(onEvent, event) {
|
|
790
|
+
if (!onEvent) return;
|
|
791
|
+
try {
|
|
792
|
+
onEvent(event);
|
|
793
|
+
} catch {
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
function withBatch(config, onEvent) {
|
|
797
|
+
const windowMs = clamp(config.windowMs, 1, 5e3);
|
|
798
|
+
const maxBatchSize = clamp(config.maxBatchSize, 2, 1e4);
|
|
799
|
+
const { batch, batchKey } = config;
|
|
800
|
+
const groups = /* @__PURE__ */ new Map();
|
|
801
|
+
return (next) => {
|
|
802
|
+
function dispatch(group) {
|
|
803
|
+
group.dispatched = true;
|
|
804
|
+
if (group.timer !== null) {
|
|
805
|
+
clearTimeout(group.timer);
|
|
806
|
+
group.timer = null;
|
|
807
|
+
}
|
|
808
|
+
groups.delete(group.key);
|
|
809
|
+
const requests = group.waiters.map((w) => w.request);
|
|
810
|
+
const waiterCount = group.waiters.length;
|
|
811
|
+
let coalescedReq;
|
|
812
|
+
try {
|
|
813
|
+
coalescedReq = batch.coalesce(requests);
|
|
814
|
+
} catch (err) {
|
|
815
|
+
const error = { _tag: "FetchError", message: String(err) };
|
|
816
|
+
rejectAllWaiters(group, error);
|
|
817
|
+
return;
|
|
818
|
+
}
|
|
819
|
+
coalescedReq = {
|
|
820
|
+
...coalescedReq,
|
|
821
|
+
init: {
|
|
822
|
+
...coalescedReq.init ?? {},
|
|
823
|
+
signal: group.controller.signal
|
|
824
|
+
}
|
|
825
|
+
};
|
|
826
|
+
safeEmitBatch(onEvent, { type: "batch-dispatch", batchKey: group.key, batchSize: waiterCount });
|
|
827
|
+
const innerEffect = next(coalescedReq);
|
|
828
|
+
registerHttpEffect(innerEffect, void 0, (exit) => {
|
|
829
|
+
if (exit._tag === "Success") {
|
|
830
|
+
let responses;
|
|
831
|
+
try {
|
|
832
|
+
responses = batch.split(exit.value, requests);
|
|
833
|
+
} catch (err2) {
|
|
834
|
+
const error = { _tag: "FetchError", message: String(err2) };
|
|
835
|
+
rejectAllWaiters(group, error);
|
|
836
|
+
return;
|
|
837
|
+
}
|
|
838
|
+
if (responses.length !== waiterCount) {
|
|
839
|
+
const error = {
|
|
840
|
+
_tag: "BatchSplitError",
|
|
841
|
+
expected: waiterCount,
|
|
842
|
+
actual: responses.length,
|
|
843
|
+
message: `split returned ${responses.length} responses but expected ${waiterCount}`
|
|
844
|
+
};
|
|
845
|
+
rejectAllWaiters(group, error);
|
|
846
|
+
return;
|
|
847
|
+
}
|
|
848
|
+
const waiters = group.waiters.slice();
|
|
849
|
+
for (let i = 0; i < waiters.length; i++) {
|
|
850
|
+
waiters[i].resolve(responses[i]);
|
|
851
|
+
}
|
|
852
|
+
return;
|
|
853
|
+
}
|
|
854
|
+
if (exit.cause._tag === "Fail") {
|
|
855
|
+
rejectAllWaiters(group, exit.cause.error);
|
|
856
|
+
return;
|
|
857
|
+
}
|
|
858
|
+
if (exit.cause._tag === "Interrupt") {
|
|
859
|
+
rejectAllWaiters(group, { _tag: "Abort" });
|
|
860
|
+
return;
|
|
861
|
+
}
|
|
862
|
+
const err = { _tag: "FetchError", message: String(exit.cause.defect ?? "unknown") };
|
|
863
|
+
rejectAllWaiters(group, err);
|
|
864
|
+
});
|
|
865
|
+
}
|
|
866
|
+
return (req) => {
|
|
867
|
+
let key;
|
|
868
|
+
try {
|
|
869
|
+
key = batchKey(req);
|
|
870
|
+
} catch {
|
|
871
|
+
return next(req);
|
|
872
|
+
}
|
|
873
|
+
if (!key) {
|
|
874
|
+
return next(req);
|
|
875
|
+
}
|
|
876
|
+
return {
|
|
877
|
+
_tag: "Async",
|
|
878
|
+
register: (_env, cb) => {
|
|
879
|
+
let callerDone = false;
|
|
880
|
+
const finishCaller = (exit) => {
|
|
881
|
+
if (callerDone) return;
|
|
882
|
+
callerDone = true;
|
|
883
|
+
cb(exit);
|
|
884
|
+
};
|
|
885
|
+
const waiter = {
|
|
886
|
+
request: req,
|
|
887
|
+
resolve: (res) => {
|
|
888
|
+
finishCaller({ _tag: "Success", value: res });
|
|
889
|
+
},
|
|
890
|
+
reject: (err) => {
|
|
891
|
+
finishCaller({ _tag: "Failure", cause: Cause.fail(err) });
|
|
892
|
+
}
|
|
893
|
+
};
|
|
894
|
+
const existing = groups.get(key);
|
|
895
|
+
if (existing && !existing.dispatched) {
|
|
896
|
+
existing.waiters.push(waiter);
|
|
897
|
+
safeEmitBatch(onEvent, { type: "batch-hit", batchKey: key });
|
|
898
|
+
if (existing.waiters.length >= maxBatchSize) {
|
|
899
|
+
dispatch(existing);
|
|
900
|
+
}
|
|
901
|
+
return () => {
|
|
902
|
+
if (callerDone) return;
|
|
903
|
+
const idx = existing.waiters.indexOf(waiter);
|
|
904
|
+
if (idx >= 0) {
|
|
905
|
+
existing.waiters.splice(idx, 1);
|
|
906
|
+
}
|
|
907
|
+
if (existing.waiters.length === 0 && !existing.dispatched) {
|
|
908
|
+
if (existing.timer !== null) {
|
|
909
|
+
clearTimeout(existing.timer);
|
|
910
|
+
existing.timer = null;
|
|
911
|
+
}
|
|
912
|
+
groups.delete(key);
|
|
913
|
+
}
|
|
914
|
+
if (existing.waiters.length === 0 && existing.dispatched) {
|
|
915
|
+
existing.controller.abort();
|
|
916
|
+
}
|
|
917
|
+
finishCaller({ _tag: "Failure", cause: Cause.interrupt() });
|
|
918
|
+
};
|
|
919
|
+
}
|
|
920
|
+
const controller = new AbortController();
|
|
921
|
+
const group = {
|
|
922
|
+
key,
|
|
923
|
+
controller,
|
|
924
|
+
waiters: [waiter],
|
|
925
|
+
timer: null,
|
|
926
|
+
dispatched: false
|
|
927
|
+
};
|
|
928
|
+
group.timer = setTimeout(() => {
|
|
929
|
+
if (!group.dispatched && group.waiters.length > 0) {
|
|
930
|
+
dispatch(group);
|
|
931
|
+
}
|
|
932
|
+
}, windowMs);
|
|
933
|
+
groups.set(key, group);
|
|
934
|
+
return () => {
|
|
935
|
+
if (callerDone) return;
|
|
936
|
+
const idx = group.waiters.indexOf(waiter);
|
|
937
|
+
if (idx >= 0) {
|
|
938
|
+
group.waiters.splice(idx, 1);
|
|
939
|
+
}
|
|
940
|
+
if (group.waiters.length === 0 && !group.dispatched) {
|
|
941
|
+
if (group.timer !== null) {
|
|
942
|
+
clearTimeout(group.timer);
|
|
943
|
+
group.timer = null;
|
|
944
|
+
}
|
|
945
|
+
groups.delete(key);
|
|
946
|
+
}
|
|
947
|
+
if (group.waiters.length === 0 && group.dispatched) {
|
|
948
|
+
group.controller.abort();
|
|
949
|
+
}
|
|
950
|
+
finishCaller({ _tag: "Failure", cause: Cause.interrupt() });
|
|
951
|
+
};
|
|
952
|
+
}
|
|
953
|
+
};
|
|
954
|
+
};
|
|
955
|
+
};
|
|
956
|
+
}
|
|
957
|
+
function rejectAllWaiters(group, err) {
|
|
958
|
+
const waiters = group.waiters.slice();
|
|
959
|
+
for (const w of waiters) {
|
|
960
|
+
w.reject(err);
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
|
|
1525
964
|
// src/http/lifecycle/timing.ts
|
|
1526
965
|
var now = typeof performance !== "undefined" && typeof performance.now === "function" ? () => performance.now() : () => Date.now();
|
|
1527
966
|
|
|
@@ -2096,7 +1535,7 @@ var PriorityQueue = class {
|
|
|
2096
1535
|
};
|
|
2097
1536
|
|
|
2098
1537
|
// src/http/lifecycle/priorityScheduler.ts
|
|
2099
|
-
var
|
|
1538
|
+
var DEFAULT_CONCURRENCY = 32;
|
|
2100
1539
|
function extractPriority(req) {
|
|
2101
1540
|
const fromReq = req.priority;
|
|
2102
1541
|
if (fromReq !== void 0) return clampPriority(fromReq);
|
|
@@ -2216,7 +1655,7 @@ function withPriority(config) {
|
|
|
2216
1655
|
return Object.assign(middleware, { queueDepth });
|
|
2217
1656
|
}
|
|
2218
1657
|
function resolveConcurrency(value) {
|
|
2219
|
-
if (value === void 0 || !Number.isFinite(value)) return
|
|
1658
|
+
if (value === void 0 || !Number.isFinite(value)) return DEFAULT_CONCURRENCY;
|
|
2220
1659
|
return Math.max(1, Math.floor(value));
|
|
2221
1660
|
}
|
|
2222
1661
|
function resolveQueueTimeout(value) {
|
|
@@ -2240,6 +1679,8 @@ var LifecycleStatsTracker = class {
|
|
|
2240
1679
|
_requestsCompleted = 0;
|
|
2241
1680
|
_requestsFailed = 0;
|
|
2242
1681
|
_retries = 0;
|
|
1682
|
+
_batchDispatches = 0;
|
|
1683
|
+
_batchedRequests = 0;
|
|
2243
1684
|
_onEvent;
|
|
2244
1685
|
_wireStats;
|
|
2245
1686
|
/**
|
|
@@ -2400,6 +1841,19 @@ var LifecycleStatsTracker = class {
|
|
|
2400
1841
|
retry() {
|
|
2401
1842
|
this._retries++;
|
|
2402
1843
|
}
|
|
1844
|
+
/**
|
|
1845
|
+
* Records a batch dispatch. Increments the batch dispatches counter by 1.
|
|
1846
|
+
*/
|
|
1847
|
+
batchDispatch() {
|
|
1848
|
+
this._batchDispatches++;
|
|
1849
|
+
}
|
|
1850
|
+
/**
|
|
1851
|
+
* Records requests that were coalesced into a batch.
|
|
1852
|
+
* @param count - The number of individual requests in the batch.
|
|
1853
|
+
*/
|
|
1854
|
+
batchedRequests(count) {
|
|
1855
|
+
this._batchedRequests += count;
|
|
1856
|
+
}
|
|
2403
1857
|
// --- Event emission ---
|
|
2404
1858
|
/**
|
|
2405
1859
|
* Emits a lifecycle event to the registered `onEvent` callback.
|
|
@@ -2468,11 +1922,447 @@ var LifecycleStatsTracker = class {
|
|
|
2468
1922
|
requestsCompleted: this._requestsCompleted,
|
|
2469
1923
|
requestsFailed: this._requestsFailed,
|
|
2470
1924
|
retries: this._retries,
|
|
1925
|
+
batchDispatches: this._batchDispatches,
|
|
1926
|
+
batchedRequests: this._batchedRequests,
|
|
2471
1927
|
wire: this._wireStats()
|
|
2472
1928
|
});
|
|
2473
1929
|
}
|
|
2474
1930
|
};
|
|
2475
1931
|
|
|
1932
|
+
// src/http/prewarm/validation.ts
|
|
1933
|
+
function validateOrigin(origin) {
|
|
1934
|
+
if (!origin || typeof origin !== "string") {
|
|
1935
|
+
throw new Error(`validateOrigin: origin must be a non-empty string, got "${origin}"`);
|
|
1936
|
+
}
|
|
1937
|
+
const trimmed = origin.trim();
|
|
1938
|
+
if (!trimmed) {
|
|
1939
|
+
throw new Error(`validateOrigin: origin must be a non-empty string, got "${origin}"`);
|
|
1940
|
+
}
|
|
1941
|
+
const stripped = stripTrailingSlashes(trimmed);
|
|
1942
|
+
const lower = stripped.toLowerCase();
|
|
1943
|
+
if (!lower.startsWith("http://") && !lower.startsWith("https://")) {
|
|
1944
|
+
throw new Error(
|
|
1945
|
+
`validateOrigin: invalid origin "${origin}" \u2014 must start with http:// or https://`
|
|
1946
|
+
);
|
|
1947
|
+
}
|
|
1948
|
+
let parsed;
|
|
1949
|
+
try {
|
|
1950
|
+
parsed = new URL(stripped);
|
|
1951
|
+
} catch {
|
|
1952
|
+
throw new Error(
|
|
1953
|
+
`validateOrigin: invalid origin "${origin}" \u2014 must be a valid URL origin (scheme + host + optional port)`
|
|
1954
|
+
);
|
|
1955
|
+
}
|
|
1956
|
+
if (parsed.pathname !== "/" && parsed.pathname !== "") {
|
|
1957
|
+
throw new Error(
|
|
1958
|
+
`validateOrigin: invalid origin "${origin}" \u2014 must not contain a path`
|
|
1959
|
+
);
|
|
1960
|
+
}
|
|
1961
|
+
if (parsed.search) {
|
|
1962
|
+
throw new Error(
|
|
1963
|
+
`validateOrigin: invalid origin "${origin}" \u2014 must not contain query parameters`
|
|
1964
|
+
);
|
|
1965
|
+
}
|
|
1966
|
+
if (parsed.hash) {
|
|
1967
|
+
throw new Error(
|
|
1968
|
+
`validateOrigin: invalid origin "${origin}" \u2014 must not contain a fragment`
|
|
1969
|
+
);
|
|
1970
|
+
}
|
|
1971
|
+
return parsed.origin;
|
|
1972
|
+
}
|
|
1973
|
+
function stripTrailingSlashes(value) {
|
|
1974
|
+
let end = value.length;
|
|
1975
|
+
while (end > 0 && value.charCodeAt(end - 1) === 47) {
|
|
1976
|
+
end--;
|
|
1977
|
+
}
|
|
1978
|
+
return end === value.length ? value : value.slice(0, end);
|
|
1979
|
+
}
|
|
1980
|
+
|
|
1981
|
+
// src/http/prewarm/platform.ts
|
|
1982
|
+
function detectPlatform() {
|
|
1983
|
+
return typeof window !== "undefined" && typeof window.document !== "undefined" ? "browser" : "node";
|
|
1984
|
+
}
|
|
1985
|
+
function validateFetchAvailable() {
|
|
1986
|
+
if (typeof globalThis.fetch !== "function") {
|
|
1987
|
+
throw new Error(
|
|
1988
|
+
"makePrewarmManager: global fetch is not available. Requires Node.js 18+ or modern browser."
|
|
1989
|
+
);
|
|
1990
|
+
}
|
|
1991
|
+
if (typeof globalThis.AbortController !== "function") {
|
|
1992
|
+
throw new Error(
|
|
1993
|
+
"makePrewarmManager: global AbortController is not available."
|
|
1994
|
+
);
|
|
1995
|
+
}
|
|
1996
|
+
}
|
|
1997
|
+
|
|
1998
|
+
// src/http/prewarm/probe.ts
|
|
1999
|
+
async function executeProbe(origin, options) {
|
|
2000
|
+
const { timeoutMs, signal, platform } = options;
|
|
2001
|
+
const url = `${origin}/`;
|
|
2002
|
+
const start = Date.now();
|
|
2003
|
+
const timeoutController = new AbortController();
|
|
2004
|
+
const timeoutId = setTimeout(() => timeoutController.abort(), timeoutMs);
|
|
2005
|
+
const onExternalAbort = () => timeoutController.abort();
|
|
2006
|
+
signal.addEventListener("abort", onExternalAbort);
|
|
2007
|
+
try {
|
|
2008
|
+
if (options.client) {
|
|
2009
|
+
const result = await executeProbeViaClient(options.client, url, timeoutController.signal);
|
|
2010
|
+
const durationMs2 = Date.now() - start;
|
|
2011
|
+
if (result.ok) {
|
|
2012
|
+
return { ok: true, durationMs: durationMs2 };
|
|
2013
|
+
}
|
|
2014
|
+
return { ok: false, durationMs: durationMs2, error: result.error };
|
|
2015
|
+
}
|
|
2016
|
+
const fetchOptions = {
|
|
2017
|
+
method: "HEAD",
|
|
2018
|
+
signal: timeoutController.signal,
|
|
2019
|
+
// Prevent caching of probe requests
|
|
2020
|
+
cache: "no-store"
|
|
2021
|
+
};
|
|
2022
|
+
if (platform === "browser") {
|
|
2023
|
+
fetchOptions.mode = "no-cors";
|
|
2024
|
+
}
|
|
2025
|
+
await globalThis.fetch(url, fetchOptions);
|
|
2026
|
+
const durationMs = Date.now() - start;
|
|
2027
|
+
return { ok: true, durationMs };
|
|
2028
|
+
} catch (err) {
|
|
2029
|
+
const durationMs = Date.now() - start;
|
|
2030
|
+
if (signal.aborted) {
|
|
2031
|
+
return { ok: false, durationMs, error: "cancelled" };
|
|
2032
|
+
}
|
|
2033
|
+
if (timeoutController.signal.aborted && !signal.aborted) {
|
|
2034
|
+
return { ok: false, durationMs, error: `Probe timed out after ${timeoutMs}ms` };
|
|
2035
|
+
}
|
|
2036
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2037
|
+
return { ok: false, durationMs, error: message };
|
|
2038
|
+
} finally {
|
|
2039
|
+
clearTimeout(timeoutId);
|
|
2040
|
+
signal.removeEventListener("abort", onExternalAbort);
|
|
2041
|
+
}
|
|
2042
|
+
}
|
|
2043
|
+
async function executeProbeViaClient(client, url, signal) {
|
|
2044
|
+
const { registerHttpEffect: registerHttpEffect2 } = await import("../effectRunner-A4CHJXJI.js");
|
|
2045
|
+
return new Promise((resolve) => {
|
|
2046
|
+
const effect = client({
|
|
2047
|
+
method: "HEAD",
|
|
2048
|
+
url,
|
|
2049
|
+
init: { signal }
|
|
2050
|
+
});
|
|
2051
|
+
const cancel = registerHttpEffect2(effect, {}, (exit) => {
|
|
2052
|
+
if (exit._tag === "Success") {
|
|
2053
|
+
resolve({ ok: true });
|
|
2054
|
+
} else {
|
|
2055
|
+
const cause = exit.cause;
|
|
2056
|
+
if (cause?._tag === "Fail") {
|
|
2057
|
+
const err = cause.error;
|
|
2058
|
+
resolve({ ok: false, error: err?.message ?? err?._tag ?? "Unknown error" });
|
|
2059
|
+
} else {
|
|
2060
|
+
resolve({ ok: false, error: "cancelled" });
|
|
2061
|
+
}
|
|
2062
|
+
}
|
|
2063
|
+
});
|
|
2064
|
+
if (signal.aborted) {
|
|
2065
|
+
cancel();
|
|
2066
|
+
} else {
|
|
2067
|
+
signal.addEventListener("abort", () => {
|
|
2068
|
+
cancel();
|
|
2069
|
+
}, { once: true });
|
|
2070
|
+
}
|
|
2071
|
+
});
|
|
2072
|
+
}
|
|
2073
|
+
|
|
2074
|
+
// src/http/prewarm/connectionState.ts
|
|
2075
|
+
function makeConnectionStateMap(origins, keepAliveDurationMs) {
|
|
2076
|
+
const entries = /* @__PURE__ */ new Map();
|
|
2077
|
+
for (const origin of origins) {
|
|
2078
|
+
entries.set(origin, {
|
|
2079
|
+
origin,
|
|
2080
|
+
status: "idle",
|
|
2081
|
+
lastProbeAt: void 0,
|
|
2082
|
+
warmUntil: void 0
|
|
2083
|
+
});
|
|
2084
|
+
}
|
|
2085
|
+
function markWarm(origin, now2) {
|
|
2086
|
+
const entry = entries.get(origin);
|
|
2087
|
+
if (!entry) return;
|
|
2088
|
+
const timestamp = now2 ?? Date.now();
|
|
2089
|
+
entry.status = "warm";
|
|
2090
|
+
entry.lastProbeAt = timestamp;
|
|
2091
|
+
entry.warmUntil = timestamp + keepAliveDurationMs;
|
|
2092
|
+
}
|
|
2093
|
+
function markExpired(origin) {
|
|
2094
|
+
const entry = entries.get(origin);
|
|
2095
|
+
if (!entry) return;
|
|
2096
|
+
entry.status = "expired";
|
|
2097
|
+
}
|
|
2098
|
+
function markIdle(origin) {
|
|
2099
|
+
const entry = entries.get(origin);
|
|
2100
|
+
if (!entry) return;
|
|
2101
|
+
entry.status = "idle";
|
|
2102
|
+
entry.lastProbeAt = void 0;
|
|
2103
|
+
entry.warmUntil = void 0;
|
|
2104
|
+
}
|
|
2105
|
+
function markProbing(origin) {
|
|
2106
|
+
const entry = entries.get(origin);
|
|
2107
|
+
if (!entry) return;
|
|
2108
|
+
entry.status = "probing";
|
|
2109
|
+
}
|
|
2110
|
+
function isWarm(origin, now2) {
|
|
2111
|
+
const entry = entries.get(origin);
|
|
2112
|
+
if (!entry) return false;
|
|
2113
|
+
if (entry.status !== "warm") return false;
|
|
2114
|
+
if (entry.lastProbeAt === void 0 || entry.warmUntil === void 0) return false;
|
|
2115
|
+
const currentTime = now2 ?? Date.now();
|
|
2116
|
+
if (currentTime >= entry.warmUntil) {
|
|
2117
|
+
entry.status = "expired";
|
|
2118
|
+
return false;
|
|
2119
|
+
}
|
|
2120
|
+
return true;
|
|
2121
|
+
}
|
|
2122
|
+
function getState(origin) {
|
|
2123
|
+
const entry = entries.get(origin);
|
|
2124
|
+
if (!entry) return void 0;
|
|
2125
|
+
return {
|
|
2126
|
+
origin: entry.origin,
|
|
2127
|
+
status: entry.status,
|
|
2128
|
+
lastProbeAt: entry.lastProbeAt,
|
|
2129
|
+
warmUntil: entry.warmUntil
|
|
2130
|
+
};
|
|
2131
|
+
}
|
|
2132
|
+
function snapshot() {
|
|
2133
|
+
const result = [];
|
|
2134
|
+
for (const entry of entries.values()) {
|
|
2135
|
+
result.push({
|
|
2136
|
+
origin: entry.origin,
|
|
2137
|
+
status: entry.status,
|
|
2138
|
+
lastProbeAt: entry.lastProbeAt,
|
|
2139
|
+
warmUntil: entry.warmUntil
|
|
2140
|
+
});
|
|
2141
|
+
}
|
|
2142
|
+
return { origins: result };
|
|
2143
|
+
}
|
|
2144
|
+
return { markWarm, markExpired, markIdle, markProbing, isWarm, getState, snapshot };
|
|
2145
|
+
}
|
|
2146
|
+
|
|
2147
|
+
// src/http/prewarm/budgetSemaphore.ts
|
|
2148
|
+
function makeBudgetSemaphore(capacity) {
|
|
2149
|
+
if (capacity < 1 || !Number.isFinite(capacity)) {
|
|
2150
|
+
throw new Error(`makeBudgetSemaphore: capacity must be >= 1, got ${capacity}`);
|
|
2151
|
+
}
|
|
2152
|
+
capacity = Math.floor(capacity);
|
|
2153
|
+
let active = 0;
|
|
2154
|
+
const waiters = [];
|
|
2155
|
+
function release() {
|
|
2156
|
+
active--;
|
|
2157
|
+
if (waiters.length > 0) {
|
|
2158
|
+
const next = waiters.shift();
|
|
2159
|
+
active++;
|
|
2160
|
+
next({ release });
|
|
2161
|
+
}
|
|
2162
|
+
}
|
|
2163
|
+
function acquire() {
|
|
2164
|
+
if (active < capacity) {
|
|
2165
|
+
active++;
|
|
2166
|
+
return Promise.resolve({ release });
|
|
2167
|
+
}
|
|
2168
|
+
return new Promise((resolve) => {
|
|
2169
|
+
waiters.push(resolve);
|
|
2170
|
+
});
|
|
2171
|
+
}
|
|
2172
|
+
function tryAcquire() {
|
|
2173
|
+
if (active < capacity) {
|
|
2174
|
+
active++;
|
|
2175
|
+
return { release };
|
|
2176
|
+
}
|
|
2177
|
+
return void 0;
|
|
2178
|
+
}
|
|
2179
|
+
function available() {
|
|
2180
|
+
return capacity - active;
|
|
2181
|
+
}
|
|
2182
|
+
function queued() {
|
|
2183
|
+
return waiters.length;
|
|
2184
|
+
}
|
|
2185
|
+
return { acquire, tryAcquire, available, queued };
|
|
2186
|
+
}
|
|
2187
|
+
|
|
2188
|
+
// src/http/prewarm/prewarmManager.ts
|
|
2189
|
+
var DEFAULTS = {
|
|
2190
|
+
keepAliveDurationMs: 55e3,
|
|
2191
|
+
budget: 4,
|
|
2192
|
+
probeTimeoutMs: 5e3,
|
|
2193
|
+
autoRefresh: false,
|
|
2194
|
+
useClientPool: false
|
|
2195
|
+
};
|
|
2196
|
+
function makePrewarmManager(config) {
|
|
2197
|
+
validateFetchAvailable();
|
|
2198
|
+
const keepAliveDurationMs = config.keepAliveDurationMs ?? DEFAULTS.keepAliveDurationMs;
|
|
2199
|
+
const budget = config.budget ?? DEFAULTS.budget;
|
|
2200
|
+
const probeTimeoutMs = config.probeTimeoutMs ?? DEFAULTS.probeTimeoutMs;
|
|
2201
|
+
const autoRefresh = config.autoRefresh ?? DEFAULTS.autoRefresh;
|
|
2202
|
+
const useClientPool = config.useClientPool ?? DEFAULTS.useClientPool;
|
|
2203
|
+
const onEvent = config.onEvent;
|
|
2204
|
+
const client = config.client;
|
|
2205
|
+
const origins = config.origins.map((o) => validateOrigin(o));
|
|
2206
|
+
const platform = detectPlatform();
|
|
2207
|
+
const stateMap = makeConnectionStateMap(origins, keepAliveDurationMs);
|
|
2208
|
+
const semaphore = makeBudgetSemaphore(budget);
|
|
2209
|
+
const abortControllers = /* @__PURE__ */ new Map();
|
|
2210
|
+
const refreshTimers = /* @__PURE__ */ new Map();
|
|
2211
|
+
let disposed = false;
|
|
2212
|
+
function emit3(event) {
|
|
2213
|
+
if (onEvent) {
|
|
2214
|
+
try {
|
|
2215
|
+
onEvent(event);
|
|
2216
|
+
} catch {
|
|
2217
|
+
}
|
|
2218
|
+
}
|
|
2219
|
+
}
|
|
2220
|
+
function scheduleAutoRefresh(origin) {
|
|
2221
|
+
if (!autoRefresh || disposed) return;
|
|
2222
|
+
clearRefreshTimer(origin);
|
|
2223
|
+
const delay = Math.floor(0.8 * keepAliveDurationMs);
|
|
2224
|
+
const timer = setTimeout(() => {
|
|
2225
|
+
refreshTimers.delete(origin);
|
|
2226
|
+
if (disposed) return;
|
|
2227
|
+
if (stateMap.isWarm(origin)) {
|
|
2228
|
+
stateMap.markExpired(origin);
|
|
2229
|
+
emit3({
|
|
2230
|
+
type: "connection-expired",
|
|
2231
|
+
origin,
|
|
2232
|
+
timestamp: Date.now()
|
|
2233
|
+
});
|
|
2234
|
+
}
|
|
2235
|
+
warm(origin).catch(() => {
|
|
2236
|
+
});
|
|
2237
|
+
}, delay);
|
|
2238
|
+
if (typeof timer === "object" && "unref" in timer) {
|
|
2239
|
+
timer.unref();
|
|
2240
|
+
}
|
|
2241
|
+
refreshTimers.set(origin, timer);
|
|
2242
|
+
}
|
|
2243
|
+
function clearRefreshTimer(origin) {
|
|
2244
|
+
const timer = refreshTimers.get(origin);
|
|
2245
|
+
if (timer !== void 0) {
|
|
2246
|
+
clearTimeout(timer);
|
|
2247
|
+
refreshTimers.delete(origin);
|
|
2248
|
+
}
|
|
2249
|
+
}
|
|
2250
|
+
function clearAllRefreshTimers() {
|
|
2251
|
+
for (const [, timer] of refreshTimers) {
|
|
2252
|
+
clearTimeout(timer);
|
|
2253
|
+
}
|
|
2254
|
+
refreshTimers.clear();
|
|
2255
|
+
}
|
|
2256
|
+
async function warm(origin) {
|
|
2257
|
+
if (disposed) {
|
|
2258
|
+
return { origin, status: "cancelled", durationMs: 0 };
|
|
2259
|
+
}
|
|
2260
|
+
if (stateMap.isWarm(origin)) {
|
|
2261
|
+
return { origin, status: "already-warm", durationMs: 0 };
|
|
2262
|
+
}
|
|
2263
|
+
const { release } = await semaphore.acquire();
|
|
2264
|
+
if (disposed) {
|
|
2265
|
+
release();
|
|
2266
|
+
return { origin, status: "cancelled", durationMs: 0 };
|
|
2267
|
+
}
|
|
2268
|
+
const controller = new AbortController();
|
|
2269
|
+
abortControllers.set(origin, controller);
|
|
2270
|
+
stateMap.markProbing(origin);
|
|
2271
|
+
try {
|
|
2272
|
+
const probeOptions = {
|
|
2273
|
+
timeoutMs: probeTimeoutMs,
|
|
2274
|
+
signal: controller.signal,
|
|
2275
|
+
platform,
|
|
2276
|
+
client: useClientPool ? client : void 0
|
|
2277
|
+
};
|
|
2278
|
+
const outcome = await executeProbe(origin, probeOptions);
|
|
2279
|
+
if (controller.signal.aborted) {
|
|
2280
|
+
stateMap.markIdle(origin);
|
|
2281
|
+
emit3({
|
|
2282
|
+
type: "connection-cancelled",
|
|
2283
|
+
origin,
|
|
2284
|
+
timestamp: Date.now()
|
|
2285
|
+
});
|
|
2286
|
+
return { origin, status: "cancelled", durationMs: outcome.durationMs };
|
|
2287
|
+
}
|
|
2288
|
+
if (outcome.ok) {
|
|
2289
|
+
stateMap.markWarm(origin);
|
|
2290
|
+
emit3({
|
|
2291
|
+
type: "connection-warmed",
|
|
2292
|
+
origin,
|
|
2293
|
+
timestamp: Date.now(),
|
|
2294
|
+
durationMs: outcome.durationMs
|
|
2295
|
+
});
|
|
2296
|
+
scheduleAutoRefresh(origin);
|
|
2297
|
+
return { origin, status: "warmed", durationMs: outcome.durationMs };
|
|
2298
|
+
} else {
|
|
2299
|
+
stateMap.markIdle(origin);
|
|
2300
|
+
emit3({
|
|
2301
|
+
type: "connection-failed",
|
|
2302
|
+
origin,
|
|
2303
|
+
timestamp: Date.now(),
|
|
2304
|
+
error: outcome.error
|
|
2305
|
+
});
|
|
2306
|
+
return { origin, status: "failed", durationMs: outcome.durationMs, error: outcome.error };
|
|
2307
|
+
}
|
|
2308
|
+
} catch (err) {
|
|
2309
|
+
stateMap.markIdle(origin);
|
|
2310
|
+
const error = err instanceof Error ? err.message : String(err);
|
|
2311
|
+
emit3({
|
|
2312
|
+
type: "connection-failed",
|
|
2313
|
+
origin,
|
|
2314
|
+
timestamp: Date.now(),
|
|
2315
|
+
error
|
|
2316
|
+
});
|
|
2317
|
+
return { origin, status: "failed", durationMs: 0, error };
|
|
2318
|
+
} finally {
|
|
2319
|
+
abortControllers.delete(origin);
|
|
2320
|
+
release();
|
|
2321
|
+
}
|
|
2322
|
+
}
|
|
2323
|
+
async function warmAll() {
|
|
2324
|
+
const results = await Promise.all(origins.map((o) => warm(o)));
|
|
2325
|
+
return results;
|
|
2326
|
+
}
|
|
2327
|
+
function cancel(origin) {
|
|
2328
|
+
const controller = abortControllers.get(origin);
|
|
2329
|
+
if (controller) {
|
|
2330
|
+
controller.abort();
|
|
2331
|
+
}
|
|
2332
|
+
clearRefreshTimer(origin);
|
|
2333
|
+
}
|
|
2334
|
+
function cancelAll() {
|
|
2335
|
+
for (const [, controller] of abortControllers) {
|
|
2336
|
+
controller.abort();
|
|
2337
|
+
}
|
|
2338
|
+
clearAllRefreshTimers();
|
|
2339
|
+
}
|
|
2340
|
+
function isWarm(origin) {
|
|
2341
|
+
if (disposed) return false;
|
|
2342
|
+
return stateMap.isWarm(origin);
|
|
2343
|
+
}
|
|
2344
|
+
function status() {
|
|
2345
|
+
return stateMap.snapshot();
|
|
2346
|
+
}
|
|
2347
|
+
function dispose() {
|
|
2348
|
+
if (disposed) return;
|
|
2349
|
+
disposed = true;
|
|
2350
|
+
cancelAll();
|
|
2351
|
+
for (const origin of origins) {
|
|
2352
|
+
stateMap.markIdle(origin);
|
|
2353
|
+
}
|
|
2354
|
+
}
|
|
2355
|
+
return {
|
|
2356
|
+
warm,
|
|
2357
|
+
warmAll,
|
|
2358
|
+
isWarm,
|
|
2359
|
+
cancel,
|
|
2360
|
+
cancelAll,
|
|
2361
|
+
status,
|
|
2362
|
+
dispose
|
|
2363
|
+
};
|
|
2364
|
+
}
|
|
2365
|
+
|
|
2476
2366
|
// src/http/lifecycle/lifecycleClient.ts
|
|
2477
2367
|
function validateGlobals() {
|
|
2478
2368
|
if (typeof fetch === "undefined") {
|
|
@@ -2487,10 +2377,11 @@ function validateGlobals() {
|
|
|
2487
2377
|
}
|
|
2488
2378
|
}
|
|
2489
2379
|
function extractWireConfig(config) {
|
|
2490
|
-
const { dedup, cache, priority, retry, onEvent, ...wireConfig } = config;
|
|
2380
|
+
const { dedup, batch, cache, priority, retry, prewarm, onEvent, ...wireConfig } = config;
|
|
2491
2381
|
return wireConfig;
|
|
2492
2382
|
}
|
|
2493
2383
|
function makeLifecycleClient(config = {}) {
|
|
2384
|
+
validateLifecycleClientConfig(config);
|
|
2494
2385
|
validateGlobals();
|
|
2495
2386
|
const wireConfig = extractWireConfig(config);
|
|
2496
2387
|
const wireClient = makeHttp(wireConfig);
|
|
@@ -2500,15 +2391,38 @@ function makeLifecycleClient(config = {}) {
|
|
|
2500
2391
|
wireStats: wireClient.stats
|
|
2501
2392
|
});
|
|
2502
2393
|
const hasDedup = config.dedup !== void 0 && config.dedup !== false;
|
|
2394
|
+
const hasBatch = config.batch !== void 0 && config.batch !== false;
|
|
2503
2395
|
const hasCache = config.cache !== void 0 && config.cache !== false;
|
|
2504
2396
|
const hasPriority = config.priority !== void 0 && config.priority !== false;
|
|
2505
2397
|
const hasRetry = config.retry !== void 0 && config.retry !== false;
|
|
2506
|
-
if (!hasDedup && !hasCache && !hasPriority && !hasRetry) {
|
|
2398
|
+
if (!hasDedup && !hasBatch && !hasCache && !hasPriority && !hasRetry) {
|
|
2399
|
+
let prewarmMgr2;
|
|
2400
|
+
const hasPrewarm2 = config.prewarm !== void 0 && config.prewarm !== false;
|
|
2401
|
+
if (hasPrewarm2) {
|
|
2402
|
+
const prewarmConfig = config.prewarm;
|
|
2403
|
+
const prewarmOrigins = prewarmConfig.origins ?? [];
|
|
2404
|
+
if (prewarmOrigins.length > 0) {
|
|
2405
|
+
prewarmMgr2 = makePrewarmManager({
|
|
2406
|
+
origins: prewarmOrigins,
|
|
2407
|
+
keepAliveDurationMs: prewarmConfig.keepAliveDurationMs,
|
|
2408
|
+
budget: prewarmConfig.budget,
|
|
2409
|
+
probeTimeoutMs: prewarmConfig.probeTimeoutMs,
|
|
2410
|
+
autoRefresh: prewarmConfig.autoRefresh,
|
|
2411
|
+
useClientPool: prewarmConfig.useClientPool,
|
|
2412
|
+
client: prewarmConfig.useClientPool ? wireClient : void 0,
|
|
2413
|
+
onEvent: prewarmConfig.onEvent
|
|
2414
|
+
});
|
|
2415
|
+
}
|
|
2416
|
+
}
|
|
2507
2417
|
return buildLifecycleClient(wireClient, tracker, {
|
|
2508
2418
|
cacheInvalidate: noopInvalidate,
|
|
2509
2419
|
cacheClear: noopClear,
|
|
2510
|
-
cancelAll: () => cancelControllers(activeControllers),
|
|
2511
|
-
activeControllers
|
|
2420
|
+
cancelAll: () => cancelControllers(activeControllers, prewarmMgr2),
|
|
2421
|
+
shutdown: () => shutdownClient(activeControllers, prewarmMgr2, wireClient.shutdown),
|
|
2422
|
+
activeControllers,
|
|
2423
|
+
adaptiveLimiter: wireClient.adaptiveLimiter,
|
|
2424
|
+
prewarmManager: prewarmMgr2,
|
|
2425
|
+
afterResponse: hasPrewarm2 ? config.prewarm.afterResponse : void 0
|
|
2512
2426
|
});
|
|
2513
2427
|
}
|
|
2514
2428
|
let priorityMiddleware;
|
|
@@ -2555,6 +2469,17 @@ function makeLifecycleClient(config = {}) {
|
|
|
2555
2469
|
}
|
|
2556
2470
|
});
|
|
2557
2471
|
}
|
|
2472
|
+
let batchMiddleware;
|
|
2473
|
+
if (hasBatch) {
|
|
2474
|
+
const batchConfig = config.batch;
|
|
2475
|
+
batchMiddleware = withBatch(batchConfig, (event) => {
|
|
2476
|
+
if (event.type === "batch-dispatch") {
|
|
2477
|
+
tracker.batchDispatch();
|
|
2478
|
+
tracker.batchedRequests(event.batchSize ?? 0);
|
|
2479
|
+
}
|
|
2480
|
+
tracker.emit(event.type, { batchKey: event.batchKey, batchSize: event.batchSize });
|
|
2481
|
+
});
|
|
2482
|
+
}
|
|
2558
2483
|
let composedFn = wireClient;
|
|
2559
2484
|
if (priorityMiddleware) {
|
|
2560
2485
|
composedFn = priorityMiddleware(composedFn);
|
|
@@ -2578,15 +2503,40 @@ function makeLifecycleClient(config = {}) {
|
|
|
2578
2503
|
if (cacheLayer) {
|
|
2579
2504
|
composedFn = cacheLayer.middleware(composedFn);
|
|
2580
2505
|
}
|
|
2506
|
+
if (batchMiddleware) {
|
|
2507
|
+
composedFn = batchMiddleware(composedFn);
|
|
2508
|
+
}
|
|
2581
2509
|
if (dedupMiddleware) {
|
|
2582
2510
|
composedFn = dedupMiddleware(composedFn);
|
|
2583
2511
|
}
|
|
2512
|
+
let prewarmMgr;
|
|
2513
|
+
const hasPrewarm = config.prewarm !== void 0 && config.prewarm !== false;
|
|
2514
|
+
if (hasPrewarm) {
|
|
2515
|
+
const prewarmConfig = config.prewarm;
|
|
2516
|
+
const prewarmOrigins = prewarmConfig.origins ?? [];
|
|
2517
|
+
if (prewarmOrigins.length > 0) {
|
|
2518
|
+
prewarmMgr = makePrewarmManager({
|
|
2519
|
+
origins: prewarmOrigins,
|
|
2520
|
+
keepAliveDurationMs: prewarmConfig.keepAliveDurationMs,
|
|
2521
|
+
budget: prewarmConfig.budget,
|
|
2522
|
+
probeTimeoutMs: prewarmConfig.probeTimeoutMs,
|
|
2523
|
+
autoRefresh: prewarmConfig.autoRefresh,
|
|
2524
|
+
useClientPool: prewarmConfig.useClientPool,
|
|
2525
|
+
client: prewarmConfig.useClientPool ? wireClient : void 0,
|
|
2526
|
+
onEvent: prewarmConfig.onEvent
|
|
2527
|
+
});
|
|
2528
|
+
}
|
|
2529
|
+
}
|
|
2584
2530
|
return buildLifecycleClient(composedFn, tracker, {
|
|
2585
2531
|
cacheInvalidate: cacheLayer?.invalidate ?? noopInvalidate,
|
|
2586
2532
|
cacheClear: cacheLayer?.clear ?? noopClear,
|
|
2587
|
-
cancelAll: () => cancelControllers(activeControllers),
|
|
2533
|
+
cancelAll: () => cancelControllers(activeControllers, prewarmMgr),
|
|
2534
|
+
shutdown: () => shutdownClient(activeControllers, prewarmMgr, wireClient.shutdown),
|
|
2588
2535
|
activeControllers,
|
|
2589
|
-
|
|
2536
|
+
adaptiveLimiter: wireClient.adaptiveLimiter,
|
|
2537
|
+
queueDepth: priorityMiddleware?.queueDepth,
|
|
2538
|
+
prewarmManager: prewarmMgr,
|
|
2539
|
+
afterResponse: hasPrewarm ? config.prewarm.afterResponse : void 0
|
|
2590
2540
|
});
|
|
2591
2541
|
}
|
|
2592
2542
|
function makeHttpClient(config = {}) {
|
|
@@ -2603,13 +2553,15 @@ function buildLifecycleClient(fn, tracker, internals) {
|
|
|
2603
2553
|
return tracker.snapshot();
|
|
2604
2554
|
};
|
|
2605
2555
|
const withMw = (mw) => {
|
|
2606
|
-
const wrappedFn = mw(fn);
|
|
2556
|
+
const wrappedFn = mw(withLifecycleMetadata(fn, internals));
|
|
2607
2557
|
return buildLifecycleClient(wrappedFn, tracker, internals);
|
|
2608
2558
|
};
|
|
2609
2559
|
const lifecycleClient = Object.assign(client, {
|
|
2610
2560
|
with: withMw,
|
|
2611
2561
|
stats,
|
|
2612
2562
|
cancelAll: internals.cancelAll,
|
|
2563
|
+
shutdown: internals.shutdown,
|
|
2564
|
+
adaptiveLimiter: internals.adaptiveLimiter,
|
|
2613
2565
|
cache: {
|
|
2614
2566
|
invalidate: internals.cacheInvalidate,
|
|
2615
2567
|
clear: internals.cacheClear
|
|
@@ -2617,13 +2569,27 @@ function buildLifecycleClient(fn, tracker, internals) {
|
|
|
2617
2569
|
});
|
|
2618
2570
|
return lifecycleClient;
|
|
2619
2571
|
}
|
|
2620
|
-
function
|
|
2572
|
+
function withLifecycleMetadata(fn, internals) {
|
|
2573
|
+
if (!internals.adaptiveLimiter) return fn;
|
|
2574
|
+
return Object.assign(((req) => fn(req)), {
|
|
2575
|
+
adaptiveLimiter: internals.adaptiveLimiter
|
|
2576
|
+
});
|
|
2577
|
+
}
|
|
2578
|
+
function cancelControllers(activeControllers, prewarmMgr) {
|
|
2621
2579
|
for (const controller of Array.from(activeControllers)) {
|
|
2622
2580
|
try {
|
|
2623
2581
|
controller.abort();
|
|
2624
2582
|
} catch {
|
|
2625
2583
|
}
|
|
2626
2584
|
}
|
|
2585
|
+
if (prewarmMgr) {
|
|
2586
|
+
prewarmMgr.cancelAll();
|
|
2587
|
+
}
|
|
2588
|
+
return asyncSucceed(void 0);
|
|
2589
|
+
}
|
|
2590
|
+
function shutdownClient(activeControllers, prewarmMgr, wireShutdown) {
|
|
2591
|
+
cancelControllers(activeControllers, prewarmMgr);
|
|
2592
|
+
wireShutdown?.();
|
|
2627
2593
|
return asyncSucceed(void 0);
|
|
2628
2594
|
}
|
|
2629
2595
|
function trackRequest(fn, req, tracker, internals) {
|
|
@@ -2660,6 +2626,18 @@ function trackRequest(fn, req, tracker, internals) {
|
|
|
2660
2626
|
internals.activeControllers.delete(controller);
|
|
2661
2627
|
if (exit._tag === "Success") {
|
|
2662
2628
|
tracker.requestCompleted();
|
|
2629
|
+
if (internals.afterResponse && internals.prewarmManager) {
|
|
2630
|
+
try {
|
|
2631
|
+
const originsToWarm = internals.afterResponse(exit.value, req);
|
|
2632
|
+
if (originsToWarm && originsToWarm.length > 0) {
|
|
2633
|
+
for (const origin of originsToWarm) {
|
|
2634
|
+
internals.prewarmManager.warm(origin).catch(() => {
|
|
2635
|
+
});
|
|
2636
|
+
}
|
|
2637
|
+
}
|
|
2638
|
+
} catch {
|
|
2639
|
+
}
|
|
2640
|
+
}
|
|
2663
2641
|
} else {
|
|
2664
2642
|
tracker.requestFailed();
|
|
2665
2643
|
}
|
|
@@ -2773,7 +2751,7 @@ function isNodeEnvironment() {
|
|
|
2773
2751
|
return typeof process !== "undefined" && process.versions != null && process.versions.node != null;
|
|
2774
2752
|
}
|
|
2775
2753
|
|
|
2776
|
-
// src/http/compression/
|
|
2754
|
+
// src/http/compression/decompressor.node.ts
|
|
2777
2755
|
function createNodeDecompressor(zlib2) {
|
|
2778
2756
|
return {
|
|
2779
2757
|
isPassthrough: false,
|
|
@@ -2824,7 +2802,7 @@ function createDecompressor() {
|
|
|
2824
2802
|
|
|
2825
2803
|
// src/http/compression/types.ts
|
|
2826
2804
|
var SUPPORTED_ENCODINGS = ["br", "gzip", "deflate"];
|
|
2827
|
-
function
|
|
2805
|
+
function emptyStats() {
|
|
2828
2806
|
return {
|
|
2829
2807
|
decompressed: { gzip: 0, br: 0, deflate: 0 },
|
|
2830
2808
|
compressedBytes: 0,
|
|
@@ -2874,7 +2852,6 @@ function processResponse(res, decompressor, enabledEncodings, stats) {
|
|
|
2874
2852
|
const encodings = contentEncodingValue.split(",").map((e) => e.trim().toLowerCase());
|
|
2875
2853
|
const reversedEncodings = [...encodings].reverse();
|
|
2876
2854
|
let currentData = Buffer.from(res.bodyText, "latin1");
|
|
2877
|
-
const originalData = currentData;
|
|
2878
2855
|
let decompressedCount = 0;
|
|
2879
2856
|
for (let i = 0; i < reversedEncodings.length; i++) {
|
|
2880
2857
|
const enc = reversedEncodings[i];
|
|
@@ -2944,7 +2921,7 @@ function processResponse(res, decompressor, enabledEncodings, stats) {
|
|
|
2944
2921
|
function makeCompressionMiddleware(config) {
|
|
2945
2922
|
const enabledEncodings = config?.encodings ?? [...SUPPORTED_ENCODINGS];
|
|
2946
2923
|
const decompressor = createDecompressor();
|
|
2947
|
-
const mutableStats =
|
|
2924
|
+
const mutableStats = emptyStats();
|
|
2948
2925
|
const middleware = (next) => {
|
|
2949
2926
|
return (req) => {
|
|
2950
2927
|
const modifiedReq = injectAcceptEncoding(req, enabledEncodings);
|
|
@@ -3174,10 +3151,10 @@ function failEntries(config, key, entries, error) {
|
|
|
3174
3151
|
}
|
|
3175
3152
|
}
|
|
3176
3153
|
function toFetchError(error) {
|
|
3177
|
-
if (
|
|
3154
|
+
if (isHttpError2(error)) return error;
|
|
3178
3155
|
return { _tag: "FetchError", message: error instanceof Error ? error.message : String(error) };
|
|
3179
3156
|
}
|
|
3180
|
-
function
|
|
3157
|
+
function isHttpError2(error) {
|
|
3181
3158
|
if (typeof error !== "object" || error === null || !("_tag" in error)) return false;
|
|
3182
3159
|
const tag = error._tag;
|
|
3183
3160
|
return tag === "Abort" || tag === "BadUrl" || tag === "FetchError" || tag === "Timeout" || tag === "PoolRejected" || tag === "PoolTimeout";
|
|
@@ -3261,7 +3238,7 @@ function runEffect(effect, env, cb) {
|
|
|
3261
3238
|
};
|
|
3262
3239
|
}
|
|
3263
3240
|
|
|
3264
|
-
// src/http/prewarm.ts
|
|
3241
|
+
// src/http/prewarm/legacy.ts
|
|
3265
3242
|
function prewarmConnections(config = {}) {
|
|
3266
3243
|
const fetchImpl = config.fetchImpl ?? globalThis.fetch;
|
|
3267
3244
|
const method = config.method ?? "HEAD";
|
|
@@ -3376,13 +3353,13 @@ function resolveUrl(value, baseUrl) {
|
|
|
3376
3353
|
}
|
|
3377
3354
|
}
|
|
3378
3355
|
function normalizePrewarmError(error) {
|
|
3379
|
-
if (
|
|
3356
|
+
if (isHttpError3(error)) return error;
|
|
3380
3357
|
if (typeof error === "object" && error !== null && error.name === "AbortError") {
|
|
3381
3358
|
return { _tag: "Abort" };
|
|
3382
3359
|
}
|
|
3383
3360
|
return { _tag: "FetchError", message: error instanceof Error ? error.message : String(error) };
|
|
3384
3361
|
}
|
|
3385
|
-
function
|
|
3362
|
+
function isHttpError3(error) {
|
|
3386
3363
|
if (typeof error !== "object" || error === null || !("_tag" in error)) return false;
|
|
3387
3364
|
const tag = error._tag;
|
|
3388
3365
|
return tag === "Abort" || tag === "BadUrl" || tag === "FetchError" || tag === "Timeout" || tag === "PoolRejected" || tag === "PoolTimeout";
|
|
@@ -3394,40 +3371,980 @@ function emit2(config, event) {
|
|
|
3394
3371
|
} catch {
|
|
3395
3372
|
}
|
|
3396
3373
|
}
|
|
3374
|
+
|
|
3375
|
+
// src/http/defaultClient.ts
|
|
3376
|
+
var MINIMAL_PRESET_CONFIG = {
|
|
3377
|
+
timeoutMs: 3e4
|
|
3378
|
+
};
|
|
3379
|
+
var BALANCED_PRESET_CONFIG = {
|
|
3380
|
+
...MINIMAL_PRESET_CONFIG,
|
|
3381
|
+
dedup: {},
|
|
3382
|
+
priority: {
|
|
3383
|
+
concurrency: 32,
|
|
3384
|
+
queueTimeoutMs: 3e4
|
|
3385
|
+
},
|
|
3386
|
+
retry: {
|
|
3387
|
+
maxRetries: 2,
|
|
3388
|
+
baseDelayMs: 100,
|
|
3389
|
+
maxDelayMs: 1e3,
|
|
3390
|
+
maxElapsedMs: 5e3,
|
|
3391
|
+
respectRetryAfter: true
|
|
3392
|
+
},
|
|
3393
|
+
adaptiveLimiter: makeAdaptiveLimiterConfig("balanced")
|
|
3394
|
+
};
|
|
3395
|
+
var DEFAULT_CACHEABLE_METHODS = /* @__PURE__ */ new Set(["GET", "HEAD", "OPTIONS"]);
|
|
3396
|
+
var DEFAULT_PRESET_CONFIG = {
|
|
3397
|
+
...BALANCED_PRESET_CONFIG,
|
|
3398
|
+
cache: {
|
|
3399
|
+
ttlSeconds: 60,
|
|
3400
|
+
maxEntries: 1024,
|
|
3401
|
+
staleWhileRevalidate: true,
|
|
3402
|
+
cachePolicy: (req, res) => ({
|
|
3403
|
+
cacheable: DEFAULT_CACHEABLE_METHODS.has(req.method) && res.status >= 200 && res.status < 400
|
|
3404
|
+
})
|
|
3405
|
+
},
|
|
3406
|
+
priority: {
|
|
3407
|
+
concurrency: 64,
|
|
3408
|
+
queueTimeoutMs: 3e4
|
|
3409
|
+
},
|
|
3410
|
+
retry: {
|
|
3411
|
+
maxRetries: 3,
|
|
3412
|
+
baseDelayMs: 100,
|
|
3413
|
+
maxDelayMs: 2e3,
|
|
3414
|
+
maxElapsedMs: 1e4,
|
|
3415
|
+
respectRetryAfter: true
|
|
3416
|
+
},
|
|
3417
|
+
adaptiveLimiter: makeAdaptiveLimiterConfig("aggressive")
|
|
3418
|
+
};
|
|
3419
|
+
var PRESET_CONFIGS = {
|
|
3420
|
+
minimal: MINIMAL_PRESET_CONFIG,
|
|
3421
|
+
balanced: BALANCED_PRESET_CONFIG,
|
|
3422
|
+
default: DEFAULT_PRESET_CONFIG
|
|
3423
|
+
};
|
|
3424
|
+
var defaultHttpClientPreset = "default";
|
|
3425
|
+
function makeDefaultHttpClient(config = {}) {
|
|
3426
|
+
validateDefaultHttpClientConfig(config);
|
|
3427
|
+
const {
|
|
3428
|
+
preset = defaultHttpClientPreset,
|
|
3429
|
+
compression,
|
|
3430
|
+
middleware = [],
|
|
3431
|
+
...lifecycleOverrides
|
|
3432
|
+
} = config;
|
|
3433
|
+
const lifecycleConfig = mergeLifecycleConfig(PRESET_CONFIGS[preset], lifecycleOverrides);
|
|
3434
|
+
let wire = makeLifecycleClient(lifecycleConfig);
|
|
3435
|
+
const compressionResult = compression === false || compression === void 0 && preset === "minimal" ? void 0 : makeCompressionMiddleware(compression === void 0 ? void 0 : compression);
|
|
3436
|
+
if (compressionResult) {
|
|
3437
|
+
wire = wire.with(compressionResult.middleware);
|
|
3438
|
+
}
|
|
3439
|
+
for (const mw of middleware) {
|
|
3440
|
+
wire = wire.with(mw);
|
|
3441
|
+
}
|
|
3442
|
+
const features = featureSnapshot(lifecycleConfig, compressionResult !== void 0, middleware.length);
|
|
3443
|
+
return buildDefaultClient(wire, {
|
|
3444
|
+
preset,
|
|
3445
|
+
features,
|
|
3446
|
+
compressionStats: compressionResult?.stats
|
|
3447
|
+
});
|
|
3448
|
+
}
|
|
3449
|
+
function buildDefaultClient(wire, meta) {
|
|
3450
|
+
const withPromise = (eff) => withAsyncPromise((e, env) => toPromise(e, env))(eff);
|
|
3451
|
+
const requestRaw = (req) => wire(req);
|
|
3452
|
+
const request = (req) => withPromise(requestRaw(req));
|
|
3453
|
+
const get = (url, init) => request(buildReq("GET", url, init));
|
|
3454
|
+
const post = (url, body, init) => request(buildReq("POST", url, init, body));
|
|
3455
|
+
const getText = (url, init) => {
|
|
3456
|
+
const req = buildReq("GET", url, init);
|
|
3457
|
+
return withPromise(
|
|
3458
|
+
mapTryAsync(requestRaw(req), (w) => toResponse(w, w.bodyText))
|
|
3459
|
+
);
|
|
3460
|
+
};
|
|
3461
|
+
const getJson = ((url, init) => {
|
|
3462
|
+
const req = setHeaderIfMissing("accept", "application/json")(
|
|
3463
|
+
buildReq("GET", url, init)
|
|
3464
|
+
);
|
|
3465
|
+
return withPromise(
|
|
3466
|
+
asyncFlatMap(requestRaw(req), (w) => decodeResponse(w, init?.schema, init?.schemaName))
|
|
3467
|
+
);
|
|
3468
|
+
});
|
|
3469
|
+
const postJson = ((url, bodyObj, init) => {
|
|
3470
|
+
return withPromise(
|
|
3471
|
+
asyncFlatMap(
|
|
3472
|
+
encodeJsonBodyEffect(bodyObj, init?.bodySchema, { schemaName: init?.bodySchemaName }),
|
|
3473
|
+
(bodyText) => {
|
|
3474
|
+
const req = setHeaderIfMissing("content-type", "application/json")(
|
|
3475
|
+
setHeaderIfMissing("accept", "application/json")(
|
|
3476
|
+
buildReq("POST", url, init, bodyText)
|
|
3477
|
+
)
|
|
3478
|
+
);
|
|
3479
|
+
return asyncFlatMap(requestRaw(req), (w) => decodeResponse(w, init?.schema, init?.schemaName));
|
|
3480
|
+
}
|
|
3481
|
+
)
|
|
3482
|
+
);
|
|
3483
|
+
});
|
|
3484
|
+
return {
|
|
3485
|
+
request,
|
|
3486
|
+
get,
|
|
3487
|
+
post,
|
|
3488
|
+
getText,
|
|
3489
|
+
getJson,
|
|
3490
|
+
postJson,
|
|
3491
|
+
with: (mw) => buildDefaultClient(wire.with(mw), {
|
|
3492
|
+
...meta,
|
|
3493
|
+
features: {
|
|
3494
|
+
...meta.features,
|
|
3495
|
+
middleware: meta.features.middleware + 1
|
|
3496
|
+
}
|
|
3497
|
+
}),
|
|
3498
|
+
wire,
|
|
3499
|
+
stats: () => wire.stats(),
|
|
3500
|
+
cache: wire.cache,
|
|
3501
|
+
cancelAll: wire.cancelAll,
|
|
3502
|
+
shutdown: wire.shutdown,
|
|
3503
|
+
preset: meta.preset,
|
|
3504
|
+
features: meta.features,
|
|
3505
|
+
...meta.compressionStats ? {
|
|
3506
|
+
compression: {
|
|
3507
|
+
stats: meta.compressionStats
|
|
3508
|
+
}
|
|
3509
|
+
} : {}
|
|
3510
|
+
};
|
|
3511
|
+
}
|
|
3512
|
+
function buildReq(method, url, init, body) {
|
|
3513
|
+
const { headers, timeoutMs, poolKey, schema: schema2, schemaName, bodySchema, bodySchemaName, ...rest } = init ?? {};
|
|
3514
|
+
const normalizedHeaders = normalizeHeadersInit(headers);
|
|
3515
|
+
const req = {
|
|
3516
|
+
method,
|
|
3517
|
+
url,
|
|
3518
|
+
...body && body.length > 0 ? { body } : {},
|
|
3519
|
+
...typeof timeoutMs === "number" ? { timeoutMs } : {},
|
|
3520
|
+
...typeof poolKey === "string" ? { poolKey } : {},
|
|
3521
|
+
init: rest
|
|
3522
|
+
};
|
|
3523
|
+
return normalizedHeaders ? mergeHeaders(normalizedHeaders)(req) : req;
|
|
3524
|
+
}
|
|
3525
|
+
function toResponse(wire, body) {
|
|
3526
|
+
return {
|
|
3527
|
+
status: wire.status,
|
|
3528
|
+
statusText: wire.statusText,
|
|
3529
|
+
headers: wire.headers,
|
|
3530
|
+
body
|
|
3531
|
+
};
|
|
3532
|
+
}
|
|
3533
|
+
function decodeResponse(wire, schema2, schemaName) {
|
|
3534
|
+
return asyncFlatMap(
|
|
3535
|
+
decodeJsonBodyEffect(wire.bodyText, schema2, { schemaName }),
|
|
3536
|
+
(body) => asyncSucceed(toResponse(wire, body))
|
|
3537
|
+
);
|
|
3538
|
+
}
|
|
3539
|
+
function mergeLifecycleConfig(defaults, overrides) {
|
|
3540
|
+
return {
|
|
3541
|
+
...defaults,
|
|
3542
|
+
...overrides,
|
|
3543
|
+
headers: mergeRecord(defaults.headers, overrides.headers),
|
|
3544
|
+
dedup: mergeLayer(defaults.dedup, overrides.dedup),
|
|
3545
|
+
batch: mergeLayer(defaults.batch, overrides.batch),
|
|
3546
|
+
cache: mergeLayer(defaults.cache, overrides.cache),
|
|
3547
|
+
priority: mergeLayer(defaults.priority, overrides.priority),
|
|
3548
|
+
retry: mergeLayer(defaults.retry, overrides.retry),
|
|
3549
|
+
prewarm: mergeLayer(defaults.prewarm, overrides.prewarm),
|
|
3550
|
+
adaptiveLimiter: mergeAdaptiveLimiterLayer(defaults.adaptiveLimiter, overrides.adaptiveLimiter),
|
|
3551
|
+
pool: mergeLayer(defaults.pool, overrides.pool)
|
|
3552
|
+
};
|
|
3553
|
+
}
|
|
3554
|
+
function mergeRecord(defaults, overrides) {
|
|
3555
|
+
if (!defaults) return overrides;
|
|
3556
|
+
if (!overrides) return defaults;
|
|
3557
|
+
return { ...defaults, ...overrides };
|
|
3558
|
+
}
|
|
3559
|
+
function mergeLayer(defaults, overrides) {
|
|
3560
|
+
if (overrides === void 0) return defaults;
|
|
3561
|
+
if (overrides === false) return false;
|
|
3562
|
+
if (defaults === void 0 || defaults === false) return overrides;
|
|
3563
|
+
return { ...defaults, ...overrides };
|
|
3564
|
+
}
|
|
3565
|
+
function mergeAdaptiveLimiterLayer(defaults, overrides) {
|
|
3566
|
+
if (overrides === void 0) return defaults;
|
|
3567
|
+
if (overrides === false) return false;
|
|
3568
|
+
if (defaults === void 0 || defaults === false) return overrides;
|
|
3569
|
+
if (overrides.preset !== void 0) return overrides;
|
|
3570
|
+
return { ...defaults, ...overrides };
|
|
3571
|
+
}
|
|
3572
|
+
function featureSnapshot(config, compression, middleware) {
|
|
3573
|
+
return Object.freeze({
|
|
3574
|
+
dedup: isEnabled(config.dedup),
|
|
3575
|
+
batch: isEnabled(config.batch),
|
|
3576
|
+
cache: isEnabled(config.cache),
|
|
3577
|
+
priority: isEnabled(config.priority),
|
|
3578
|
+
retry: isEnabled(config.retry),
|
|
3579
|
+
prewarm: isEnabled(config.prewarm),
|
|
3580
|
+
adaptiveLimiter: isEnabled(config.adaptiveLimiter),
|
|
3581
|
+
compression,
|
|
3582
|
+
middleware
|
|
3583
|
+
});
|
|
3584
|
+
}
|
|
3585
|
+
function isEnabled(value) {
|
|
3586
|
+
return value !== void 0 && value !== false;
|
|
3587
|
+
}
|
|
3588
|
+
|
|
3589
|
+
// src/http/builder.ts
|
|
3590
|
+
var DEFAULT_BUILDER_RETRY = {
|
|
3591
|
+
maxRetries: 2,
|
|
3592
|
+
baseDelayMs: 100,
|
|
3593
|
+
maxDelayMs: 1e3
|
|
3594
|
+
};
|
|
3595
|
+
var cloneConfig = (config) => ({
|
|
3596
|
+
...config,
|
|
3597
|
+
headers: config.headers ? { ...config.headers } : void 0,
|
|
3598
|
+
middleware: config.middleware ? [...config.middleware] : void 0
|
|
3599
|
+
});
|
|
3600
|
+
var freezeConfig = (config) => ({
|
|
3601
|
+
...config,
|
|
3602
|
+
headers: config.headers ? { ...config.headers } : void 0,
|
|
3603
|
+
middleware: config.middleware ? [...config.middleware] : void 0
|
|
3604
|
+
});
|
|
3605
|
+
var mergeConfig = (current, next) => ({
|
|
3606
|
+
...current,
|
|
3607
|
+
...next,
|
|
3608
|
+
headers: {
|
|
3609
|
+
...current.headers ?? {},
|
|
3610
|
+
...next.headers ?? {}
|
|
3611
|
+
},
|
|
3612
|
+
middleware: [
|
|
3613
|
+
...current.middleware ?? [],
|
|
3614
|
+
...next.middleware ?? []
|
|
3615
|
+
]
|
|
3616
|
+
});
|
|
3617
|
+
function makeBuilder(config) {
|
|
3618
|
+
const replace = (patch) => makeBuilder({
|
|
3619
|
+
...config,
|
|
3620
|
+
...patch,
|
|
3621
|
+
headers: patch.headers ? { ...patch.headers } : config.headers,
|
|
3622
|
+
middleware: patch.middleware ? [...patch.middleware] : config.middleware
|
|
3623
|
+
});
|
|
3624
|
+
const setLayer = (key, value) => replace({ [key]: value });
|
|
3625
|
+
const middleware = (mw) => makeBuilder({
|
|
3626
|
+
...config,
|
|
3627
|
+
middleware: [...config.middleware ?? [], mw]
|
|
3628
|
+
});
|
|
3629
|
+
return Object.freeze({
|
|
3630
|
+
config: () => freezeConfig(config),
|
|
3631
|
+
baseUrl: (baseUrl) => replace({ baseUrl }),
|
|
3632
|
+
header: (name, value) => makeBuilder({
|
|
3633
|
+
...config,
|
|
3634
|
+
headers: {
|
|
3635
|
+
...config.headers ?? {},
|
|
3636
|
+
[name]: value
|
|
3637
|
+
}
|
|
3638
|
+
}),
|
|
3639
|
+
headers: (headers) => makeBuilder({
|
|
3640
|
+
...config,
|
|
3641
|
+
headers: {
|
|
3642
|
+
...config.headers ?? {},
|
|
3643
|
+
...headers
|
|
3644
|
+
}
|
|
3645
|
+
}),
|
|
3646
|
+
timeoutMs: (timeoutMs) => replace({ timeoutMs }),
|
|
3647
|
+
timeout: (timeoutMs) => replace({ timeoutMs }),
|
|
3648
|
+
preset: (preset) => replace({ preset }),
|
|
3649
|
+
minimal: () => replace({ preset: "minimal" }),
|
|
3650
|
+
balanced: () => replace({ preset: "balanced" }),
|
|
3651
|
+
defaultPreset: () => replace({ preset: "default" }),
|
|
3652
|
+
dedup: (layer = {}) => setLayer("dedup", layer),
|
|
3653
|
+
noDedup: () => setLayer("dedup", false),
|
|
3654
|
+
batch: (layer) => setLayer("batch", layer),
|
|
3655
|
+
noBatch: () => setLayer("batch", false),
|
|
3656
|
+
cache: (layer = {}) => setLayer("cache", layer),
|
|
3657
|
+
noCache: () => setLayer("cache", false),
|
|
3658
|
+
priority: (layer = {}) => setLayer("priority", layer),
|
|
3659
|
+
noPriority: () => setLayer("priority", false),
|
|
3660
|
+
retry: (layer = DEFAULT_BUILDER_RETRY) => setLayer("retry", layer),
|
|
3661
|
+
noRetry: () => setLayer("retry", false),
|
|
3662
|
+
prewarm: (layer = {}) => setLayer("prewarm", layer),
|
|
3663
|
+
noPrewarm: () => setLayer("prewarm", false),
|
|
3664
|
+
adaptiveLimiter: (layer = {}) => setLayer("adaptiveLimiter", layer),
|
|
3665
|
+
adaptiveLimiterPreset: (preset, overrides = {}) => setLayer("adaptiveLimiter", makeAdaptiveLimiterConfig(preset, overrides)),
|
|
3666
|
+
conservativeLimiter: (overrides = {}) => setLayer("adaptiveLimiter", makeAdaptiveLimiterConfig("conservative", overrides)),
|
|
3667
|
+
balancedLimiter: (overrides = {}) => setLayer("adaptiveLimiter", makeAdaptiveLimiterConfig("balanced", overrides)),
|
|
3668
|
+
aggressiveLimiter: (overrides = {}) => setLayer("adaptiveLimiter", makeAdaptiveLimiterConfig("aggressive", overrides)),
|
|
3669
|
+
noAdaptiveLimiter: () => setLayer("adaptiveLimiter", false),
|
|
3670
|
+
pool: (layer = {}) => setLayer("pool", layer),
|
|
3671
|
+
noPool: () => setLayer("pool", false),
|
|
3672
|
+
compression: (layer = {}) => setLayer("compression", layer),
|
|
3673
|
+
noCompression: () => setLayer("compression", false),
|
|
3674
|
+
middleware,
|
|
3675
|
+
use: middleware,
|
|
3676
|
+
configure: (next) => makeBuilder(mergeConfig(config, next)),
|
|
3677
|
+
build: () => makeDefaultHttpClient(freezeConfig(config)),
|
|
3678
|
+
buildWire: () => makeDefaultHttpClient(freezeConfig(config)).wire
|
|
3679
|
+
});
|
|
3680
|
+
}
|
|
3681
|
+
function httpClientBuilder(config = {}) {
|
|
3682
|
+
return makeBuilder(cloneConfig(config));
|
|
3683
|
+
}
|
|
3684
|
+
var makeHttpClientBuilder = httpClientBuilder;
|
|
3685
|
+
var httpBuilder = httpClientBuilder;
|
|
3686
|
+
|
|
3687
|
+
// src/http/server.ts
|
|
3688
|
+
import { createServer } from "http";
|
|
3689
|
+
var DEFAULT_MAX_BODY_BYTES = 1024 * 1024;
|
|
3690
|
+
function route(method, path, optionsOrHandler, maybeHandler) {
|
|
3691
|
+
const options = typeof optionsOrHandler === "function" ? {} : optionsOrHandler;
|
|
3692
|
+
const handler = typeof optionsOrHandler === "function" ? optionsOrHandler : maybeHandler;
|
|
3693
|
+
if (!handler) throw new Error(`Missing handler for HTTP route ${method} ${path}`);
|
|
3694
|
+
return {
|
|
3695
|
+
method: normalizeRouteMethod(method),
|
|
3696
|
+
path: normalizeRoutePath(path),
|
|
3697
|
+
options,
|
|
3698
|
+
handler,
|
|
3699
|
+
match: compileRoutePath(path)
|
|
3700
|
+
};
|
|
3701
|
+
}
|
|
3702
|
+
var httpRoute = route;
|
|
3703
|
+
function makeHttpRouter(routes, options = {}) {
|
|
3704
|
+
const normalizedRoutes = routes.map((item) => ({
|
|
3705
|
+
...item,
|
|
3706
|
+
method: normalizeRouteMethod(item.method),
|
|
3707
|
+
path: normalizeRoutePath(item.path)
|
|
3708
|
+
}));
|
|
3709
|
+
const router = {
|
|
3710
|
+
routes: normalizedRoutes,
|
|
3711
|
+
match: (method, path) => matchRoute(normalizedRoutes, method, path),
|
|
3712
|
+
handle: (request, existingMatch) => {
|
|
3713
|
+
const matched = existingMatch ?? router.match(request.method, request.path);
|
|
3714
|
+
return handleRouteMatch(request, matched, options);
|
|
3715
|
+
},
|
|
3716
|
+
listen: (serverOptions = {}) => nodeHttpServerResource({
|
|
3717
|
+
...serverOptions,
|
|
3718
|
+
router
|
|
3719
|
+
})
|
|
3720
|
+
};
|
|
3721
|
+
return router;
|
|
3722
|
+
}
|
|
3723
|
+
function json(body, init = {}) {
|
|
3724
|
+
return {
|
|
3725
|
+
...init,
|
|
3726
|
+
headers: setHeaderIfMissing2(init.headers, "content-type", "application/json"),
|
|
3727
|
+
body
|
|
3728
|
+
};
|
|
3729
|
+
}
|
|
3730
|
+
function text(body, init = {}) {
|
|
3731
|
+
return {
|
|
3732
|
+
...init,
|
|
3733
|
+
headers: setHeaderIfMissing2(init.headers, "content-type", "text/plain; charset=utf-8"),
|
|
3734
|
+
body
|
|
3735
|
+
};
|
|
3736
|
+
}
|
|
3737
|
+
function empty(status = 204, headers) {
|
|
3738
|
+
return { status, headers };
|
|
3739
|
+
}
|
|
3740
|
+
function makeRuntimeHealthRoute(options = {}) {
|
|
3741
|
+
const { path = "/health", ...healthOptions } = options;
|
|
3742
|
+
return route(
|
|
3743
|
+
"GET",
|
|
3744
|
+
path,
|
|
3745
|
+
() => asyncFlatMap(
|
|
3746
|
+
makeRuntimeHealth(healthOptions),
|
|
3747
|
+
(report) => asyncSucceed(healthReportToServerResponse(report))
|
|
3748
|
+
)
|
|
3749
|
+
);
|
|
3750
|
+
}
|
|
3751
|
+
function makeRuntimeReadinessRoute(options = {}) {
|
|
3752
|
+
return makeRuntimeHealthRoute({
|
|
3753
|
+
...options,
|
|
3754
|
+
path: options.path ?? "/ready"
|
|
3755
|
+
});
|
|
3756
|
+
}
|
|
3757
|
+
var runtimeHealthRoute = makeRuntimeHealthRoute;
|
|
3758
|
+
var runtimeReadinessRoute = makeRuntimeReadinessRoute;
|
|
3759
|
+
function withResponseHeader(name, value) {
|
|
3760
|
+
return (next) => (ctx) => asyncFlatMap(
|
|
3761
|
+
next(ctx),
|
|
3762
|
+
(response) => asyncSucceed({
|
|
3763
|
+
...response,
|
|
3764
|
+
headers: setHeaderIfMissing2(response.headers, name, value)
|
|
3765
|
+
})
|
|
3766
|
+
);
|
|
3767
|
+
}
|
|
3768
|
+
function healthReportToServerResponse(report) {
|
|
3769
|
+
return healthHttpResponseToServerResponse(healthToHttpResponse(report));
|
|
3770
|
+
}
|
|
3771
|
+
function healthHttpResponseToServerResponse(response) {
|
|
3772
|
+
return {
|
|
3773
|
+
status: response.status,
|
|
3774
|
+
headers: response.headers,
|
|
3775
|
+
body: response.body
|
|
3776
|
+
};
|
|
3777
|
+
}
|
|
3778
|
+
function makeNodeHttpServer(options) {
|
|
3779
|
+
return asyncEffect((_env, cb) => {
|
|
3780
|
+
const router = resolveRouter(options.router);
|
|
3781
|
+
const server = createServer((req, res) => {
|
|
3782
|
+
void handleNodeRequest(req, res, router, options);
|
|
3783
|
+
});
|
|
3784
|
+
let settled = false;
|
|
3785
|
+
let closed = false;
|
|
3786
|
+
const close = () => closeNodeServer(server, options);
|
|
3787
|
+
const fail = (error) => {
|
|
3788
|
+
if (settled) {
|
|
3789
|
+
options.onError?.(error);
|
|
3790
|
+
return;
|
|
3791
|
+
}
|
|
3792
|
+
settled = true;
|
|
3793
|
+
cb({
|
|
3794
|
+
_tag: "Failure",
|
|
3795
|
+
cause: {
|
|
3796
|
+
_tag: "Fail",
|
|
3797
|
+
error: {
|
|
3798
|
+
_tag: "ListenError",
|
|
3799
|
+
error,
|
|
3800
|
+
message: error instanceof Error ? error.message : String(error)
|
|
3801
|
+
}
|
|
3802
|
+
}
|
|
3803
|
+
});
|
|
3804
|
+
};
|
|
3805
|
+
server.once("error", fail);
|
|
3806
|
+
server.once("close", () => {
|
|
3807
|
+
closed = true;
|
|
3808
|
+
});
|
|
3809
|
+
server.listen(options.port ?? 0, options.host, () => {
|
|
3810
|
+
if (settled) return;
|
|
3811
|
+
settled = true;
|
|
3812
|
+
server.off("error", fail);
|
|
3813
|
+
server.on("error", (error) => options.onError?.(error));
|
|
3814
|
+
cb({
|
|
3815
|
+
_tag: "Success",
|
|
3816
|
+
value: {
|
|
3817
|
+
server,
|
|
3818
|
+
router,
|
|
3819
|
+
address: () => server.address(),
|
|
3820
|
+
url: () => serverUrl(server),
|
|
3821
|
+
close
|
|
3822
|
+
}
|
|
3823
|
+
});
|
|
3824
|
+
});
|
|
3825
|
+
return () => {
|
|
3826
|
+
if (closed) return;
|
|
3827
|
+
void close().catch((error) => options.onError?.(error));
|
|
3828
|
+
};
|
|
3829
|
+
});
|
|
3830
|
+
}
|
|
3831
|
+
function nodeHttpServerResource(options) {
|
|
3832
|
+
return resource(
|
|
3833
|
+
makeNodeHttpServer(options),
|
|
3834
|
+
(handle) => asyncEffect((_env, cb) => {
|
|
3835
|
+
handle.close().then(() => cb({ _tag: "Success", value: void 0 })).catch(() => cb({ _tag: "Success", value: void 0 }));
|
|
3836
|
+
})
|
|
3837
|
+
);
|
|
3838
|
+
}
|
|
3839
|
+
var makeNodeHttpServerResource = nodeHttpServerResource;
|
|
3840
|
+
var makeHttpServerResource = nodeHttpServerResource;
|
|
3841
|
+
function handleRouteMatch(request, match, options) {
|
|
3842
|
+
if (match._tag === "NotFound") {
|
|
3843
|
+
return asyncSucceed(json({ error: "Not Found" }, { status: 404 }));
|
|
3844
|
+
}
|
|
3845
|
+
if (match._tag === "MethodNotAllowed") {
|
|
3846
|
+
return asyncSucceed(json(
|
|
3847
|
+
{ error: "Method Not Allowed", allowed: match.allowed.filter((method) => method !== "ALL") },
|
|
3848
|
+
{ status: 405, headers: { allow: match.allowed.filter((method) => method !== "ALL").join(", ") } }
|
|
3849
|
+
));
|
|
3850
|
+
}
|
|
3851
|
+
const routeWithParams = match.route;
|
|
3852
|
+
const prepared = prepareRouteContext(request, match.params, routeWithParams);
|
|
3853
|
+
if (!prepared.success) return asyncSucceed(validationErrorResponse(prepared.error));
|
|
3854
|
+
const handler = composeMiddleware(
|
|
3855
|
+
[...options.middleware ?? [], ...routeWithParams.options.middleware ?? []],
|
|
3856
|
+
routeWithParams.handler
|
|
3857
|
+
);
|
|
3858
|
+
let handled;
|
|
3859
|
+
try {
|
|
3860
|
+
handled = handler(prepared.ctx);
|
|
3861
|
+
} catch (error) {
|
|
3862
|
+
return asyncSucceed(handlerErrorResponse(error, options));
|
|
3863
|
+
}
|
|
3864
|
+
return asyncFold(
|
|
3865
|
+
handled,
|
|
3866
|
+
(error) => asyncSucceed(handlerErrorResponse(error, options)),
|
|
3867
|
+
(response) => {
|
|
3868
|
+
const normalized = normalizeServerResponse(response);
|
|
3869
|
+
const validation = validateResponseBody(normalized, routeWithParams);
|
|
3870
|
+
return asyncSucceed(validation.success ? normalized : validationErrorResponse(validation.error));
|
|
3871
|
+
}
|
|
3872
|
+
);
|
|
3873
|
+
}
|
|
3874
|
+
function prepareRouteContext(request, params, routeWithSchemas) {
|
|
3875
|
+
const paramsResult = validatePart(params, routeWithSchemas.options.params, {
|
|
3876
|
+
phase: "request",
|
|
3877
|
+
schemaName: routeWithSchemas.options.paramsSchemaName ?? "params",
|
|
3878
|
+
body: JSON.stringify(params)
|
|
3879
|
+
});
|
|
3880
|
+
if (!paramsResult.success) return paramsResult;
|
|
3881
|
+
const queryResult = validatePart(request.query, routeWithSchemas.options.query, {
|
|
3882
|
+
phase: "request",
|
|
3883
|
+
schemaName: routeWithSchemas.options.querySchemaName ?? "query",
|
|
3884
|
+
body: JSON.stringify(request.query)
|
|
3885
|
+
});
|
|
3886
|
+
if (!queryResult.success) return queryResult;
|
|
3887
|
+
const bodyInput = routeWithSchemas.options.body ? parseJsonRequestBody(request.bodyText) : { success: true, data: request.bodyText.length > 0 ? request.bodyText : void 0 };
|
|
3888
|
+
if (!bodyInput.success) {
|
|
3889
|
+
return {
|
|
3890
|
+
success: false,
|
|
3891
|
+
error: makeValidationError({
|
|
3892
|
+
message: bodyInput.message,
|
|
3893
|
+
body: request.bodyText,
|
|
3894
|
+
phase: "request",
|
|
3895
|
+
schema: routeWithSchemas.options.bodySchemaName ?? "body",
|
|
3896
|
+
issues: [makeSchemaIssue([], "valid JSON", request.bodyText, bodyInput.message)]
|
|
3897
|
+
})
|
|
3898
|
+
};
|
|
3899
|
+
}
|
|
3900
|
+
const bodyResult = validatePart(bodyInput.data, routeWithSchemas.options.body, {
|
|
3901
|
+
phase: "request",
|
|
3902
|
+
schemaName: routeWithSchemas.options.bodySchemaName ?? "body",
|
|
3903
|
+
body: request.bodyText
|
|
3904
|
+
});
|
|
3905
|
+
if (!bodyResult.success) return bodyResult;
|
|
3906
|
+
return {
|
|
3907
|
+
success: true,
|
|
3908
|
+
ctx: {
|
|
3909
|
+
...request,
|
|
3910
|
+
route: routeWithSchemas.path,
|
|
3911
|
+
params: paramsResult.data,
|
|
3912
|
+
query: queryResult.data,
|
|
3913
|
+
body: bodyResult.data
|
|
3914
|
+
}
|
|
3915
|
+
};
|
|
3916
|
+
}
|
|
3917
|
+
function validateResponseBody(response, routeWithSchemas) {
|
|
3918
|
+
const schema2 = routeWithSchemas.options.response;
|
|
3919
|
+
if (!schema2) return { success: true };
|
|
3920
|
+
const result = validateValue(response.body, schema2);
|
|
3921
|
+
if (result.success) return { success: true };
|
|
3922
|
+
return {
|
|
3923
|
+
success: false,
|
|
3924
|
+
error: makeValidationError({
|
|
3925
|
+
message: `HTTP response failed validation: ${formatIssues(result.issues)}`,
|
|
3926
|
+
body: previewJson2(response.body),
|
|
3927
|
+
phase: "response",
|
|
3928
|
+
schema: routeWithSchemas.options.responseSchemaName ?? "response",
|
|
3929
|
+
issues: result.issues
|
|
3930
|
+
})
|
|
3931
|
+
};
|
|
3932
|
+
}
|
|
3933
|
+
function validatePart(input, schema2, options) {
|
|
3934
|
+
if (!schema2) return { success: true, data: input };
|
|
3935
|
+
const result = validateValue(input, schema2);
|
|
3936
|
+
if (result.success) return { success: true, data: result.data };
|
|
3937
|
+
return {
|
|
3938
|
+
success: false,
|
|
3939
|
+
error: makeValidationError({
|
|
3940
|
+
message: `HTTP ${options.schemaName} failed validation: ${formatIssues(result.issues)}`,
|
|
3941
|
+
body: options.body,
|
|
3942
|
+
phase: options.phase,
|
|
3943
|
+
schema: options.schemaName,
|
|
3944
|
+
issues: result.issues
|
|
3945
|
+
})
|
|
3946
|
+
};
|
|
3947
|
+
}
|
|
3948
|
+
function resolveRouter(input) {
|
|
3949
|
+
return isHttpRouter(input) ? input : makeHttpRouter(input);
|
|
3950
|
+
}
|
|
3951
|
+
function isHttpRouter(value) {
|
|
3952
|
+
return !Array.isArray(value) && typeof value === "object" && value !== null && typeof value.match === "function" && typeof value.handle === "function";
|
|
3953
|
+
}
|
|
3954
|
+
function makeValidationError(input) {
|
|
3955
|
+
return {
|
|
3956
|
+
_tag: "ValidationError",
|
|
3957
|
+
message: input.message,
|
|
3958
|
+
body: input.body,
|
|
3959
|
+
phase: input.phase,
|
|
3960
|
+
schema: input.schema,
|
|
3961
|
+
issues: input.issues
|
|
3962
|
+
};
|
|
3963
|
+
}
|
|
3964
|
+
function validationErrorResponse(error) {
|
|
3965
|
+
return json(
|
|
3966
|
+
{
|
|
3967
|
+
error: error.phase === "response" ? "Response validation failed" : "Request validation failed",
|
|
3968
|
+
message: error.message,
|
|
3969
|
+
phase: error.phase,
|
|
3970
|
+
schema: error.schema,
|
|
3971
|
+
issues: error.issues
|
|
3972
|
+
},
|
|
3973
|
+
{ status: error.phase === "response" ? 500 : 400 }
|
|
3974
|
+
);
|
|
3975
|
+
}
|
|
3976
|
+
function handlerErrorResponse(error, options) {
|
|
3977
|
+
return json(
|
|
3978
|
+
{
|
|
3979
|
+
error: "Internal Server Error",
|
|
3980
|
+
...options.includeErrorDetails ? { message: error instanceof Error ? error.message : String(error) } : {}
|
|
3981
|
+
},
|
|
3982
|
+
{ status: 500 }
|
|
3983
|
+
);
|
|
3984
|
+
}
|
|
3985
|
+
async function handleNodeRequest(req, res, router, options) {
|
|
3986
|
+
try {
|
|
3987
|
+
const bodyText = await readNodeRequestBody(req, options.maxBodyBytes ?? DEFAULT_MAX_BODY_BYTES);
|
|
3988
|
+
const serverRequest = nodeRequestToServerRequest(req, bodyText);
|
|
3989
|
+
const match = router.match(serverRequest.method, serverRequest.path);
|
|
3990
|
+
const effect = router.handle(serverRequest, match);
|
|
3991
|
+
const routeLabel = match._tag === "Match" || match._tag === "MethodNotAllowed" ? match.route.path : void 0;
|
|
3992
|
+
const observedInput = {
|
|
3993
|
+
method: serverRequest.method,
|
|
3994
|
+
route: routeLabel,
|
|
3995
|
+
target: serverRequest.target,
|
|
3996
|
+
headers: serverRequest.headers
|
|
3997
|
+
};
|
|
3998
|
+
const response = options.observability ? (await runObservedHttpServerEffect(
|
|
3999
|
+
options.observability,
|
|
4000
|
+
observedInput,
|
|
4001
|
+
effect,
|
|
4002
|
+
{
|
|
4003
|
+
statusCode: (value) => value.status ?? 200,
|
|
4004
|
+
...options.observabilityOptions ?? {}
|
|
4005
|
+
},
|
|
4006
|
+
options.env,
|
|
4007
|
+
options.runtimeOptions
|
|
4008
|
+
)).value : await (options.runtime ?? new Runtime({
|
|
4009
|
+
env: options.env ?? {},
|
|
4010
|
+
...options.runtimeOptions ?? {}
|
|
4011
|
+
})).toPromise(effect);
|
|
4012
|
+
writeNodeResponse(res, response);
|
|
4013
|
+
} catch (error) {
|
|
4014
|
+
options.onError?.(error);
|
|
4015
|
+
writeNodeResponse(res, json({ error: "Internal Server Error" }, { status: 500 }));
|
|
4016
|
+
}
|
|
4017
|
+
}
|
|
4018
|
+
function nodeRequestToServerRequest(req, bodyText) {
|
|
4019
|
+
const headers = normalizeNodeHeaders(req.headers);
|
|
4020
|
+
const target = req.url ?? "/";
|
|
4021
|
+
const parsed = parseRequestUrl(target, headers.host);
|
|
4022
|
+
return {
|
|
4023
|
+
method: (req.method ?? "GET").toUpperCase(),
|
|
4024
|
+
url: parsed.toString(),
|
|
4025
|
+
path: parsed.pathname,
|
|
4026
|
+
target,
|
|
4027
|
+
headers,
|
|
4028
|
+
query: queryToObject(parsed.searchParams),
|
|
4029
|
+
params: {},
|
|
4030
|
+
bodyText,
|
|
4031
|
+
raw: req
|
|
4032
|
+
};
|
|
4033
|
+
}
|
|
4034
|
+
function writeNodeResponse(res, response) {
|
|
4035
|
+
const normalized = normalizeServerResponse(response);
|
|
4036
|
+
const encoded = encodeResponseBody(normalized);
|
|
4037
|
+
res.statusCode = normalized.status ?? 200;
|
|
4038
|
+
for (const [name, value] of Object.entries(encoded.headers)) {
|
|
4039
|
+
res.setHeader(name, value);
|
|
4040
|
+
}
|
|
4041
|
+
if (encoded.body === void 0) {
|
|
4042
|
+
res.end();
|
|
4043
|
+
return;
|
|
4044
|
+
}
|
|
4045
|
+
res.end(encoded.body);
|
|
4046
|
+
}
|
|
4047
|
+
function normalizeServerResponse(response) {
|
|
4048
|
+
if (isServerResponse(response)) {
|
|
4049
|
+
return { status: 200, ...response };
|
|
4050
|
+
}
|
|
4051
|
+
return { status: 200, body: response };
|
|
4052
|
+
}
|
|
4053
|
+
function isServerResponse(value) {
|
|
4054
|
+
return typeof value === "object" && value !== null && ("status" in value || "headers" in value || "body" in value);
|
|
4055
|
+
}
|
|
4056
|
+
function encodeResponseBody(response) {
|
|
4057
|
+
const headers = { ...response.headers ?? {} };
|
|
4058
|
+
const body = response.body;
|
|
4059
|
+
if (body === void 0 || response.status === 204 || response.status === 304) return { headers };
|
|
4060
|
+
if (typeof body === "string") {
|
|
4061
|
+
return {
|
|
4062
|
+
headers: setHeaderIfMissing2(headers, "content-type", "text/plain; charset=utf-8"),
|
|
4063
|
+
body
|
|
4064
|
+
};
|
|
4065
|
+
}
|
|
4066
|
+
if (body instanceof Uint8Array) return { headers, body };
|
|
4067
|
+
if (body instanceof ArrayBuffer) return { headers, body: new Uint8Array(body) };
|
|
4068
|
+
return {
|
|
4069
|
+
headers: setHeaderIfMissing2(headers, "content-type", "application/json"),
|
|
4070
|
+
body: JSON.stringify(body)
|
|
4071
|
+
};
|
|
4072
|
+
}
|
|
4073
|
+
function readNodeRequestBody(req, maxBytes) {
|
|
4074
|
+
return new Promise((resolve, reject) => {
|
|
4075
|
+
const chunks = [];
|
|
4076
|
+
let total = 0;
|
|
4077
|
+
req.on("data", (chunk) => {
|
|
4078
|
+
total += chunk.byteLength;
|
|
4079
|
+
if (total > maxBytes) {
|
|
4080
|
+
reject(new Error(`HTTP request body exceeded ${maxBytes} bytes`));
|
|
4081
|
+
req.destroy();
|
|
4082
|
+
return;
|
|
4083
|
+
}
|
|
4084
|
+
chunks.push(chunk);
|
|
4085
|
+
});
|
|
4086
|
+
req.on("error", reject);
|
|
4087
|
+
req.on("end", () => resolve(Buffer.concat(chunks).toString("utf8")));
|
|
4088
|
+
});
|
|
4089
|
+
}
|
|
4090
|
+
function closeNodeServer(server, options) {
|
|
4091
|
+
return new Promise((resolve, reject) => {
|
|
4092
|
+
const startedAt = Date.now();
|
|
4093
|
+
const schedule = options.shutdownPollSchedule ?? defaultShutdownPollSchedule(options.gracefulShutdownMs ?? 5e3);
|
|
4094
|
+
let state = schedule.initial();
|
|
4095
|
+
let finished = false;
|
|
4096
|
+
const done = (error) => {
|
|
4097
|
+
if (finished) return;
|
|
4098
|
+
finished = true;
|
|
4099
|
+
if (timer) clearTimeout(timer);
|
|
4100
|
+
if (error && error.message !== "Server is not running.") reject(error);
|
|
4101
|
+
else resolve();
|
|
4102
|
+
};
|
|
4103
|
+
let timer;
|
|
4104
|
+
const poll = () => {
|
|
4105
|
+
if (finished) return;
|
|
4106
|
+
if (!server.listening) {
|
|
4107
|
+
done();
|
|
4108
|
+
return;
|
|
4109
|
+
}
|
|
4110
|
+
const [decision, nextState] = schedule.step(state, {
|
|
4111
|
+
listening: server.listening,
|
|
4112
|
+
elapsedMs: Date.now() - startedAt
|
|
4113
|
+
});
|
|
4114
|
+
state = nextState;
|
|
4115
|
+
if (!decision.continue) {
|
|
4116
|
+
server.closeAllConnections?.();
|
|
4117
|
+
done();
|
|
4118
|
+
return;
|
|
4119
|
+
}
|
|
4120
|
+
timer = setTimeout(poll, decision.delayMs);
|
|
4121
|
+
timer.unref?.();
|
|
4122
|
+
};
|
|
4123
|
+
server.close(done);
|
|
4124
|
+
poll();
|
|
4125
|
+
});
|
|
4126
|
+
}
|
|
4127
|
+
function defaultShutdownPollSchedule(maxMs) {
|
|
4128
|
+
const intervalMs = 25;
|
|
4129
|
+
return take(fixed(intervalMs), Math.max(1, Math.ceil(Math.max(0, maxMs) / intervalMs)));
|
|
4130
|
+
}
|
|
4131
|
+
function matchRoute(routes, method, path) {
|
|
4132
|
+
const requestedMethod = method.toUpperCase();
|
|
4133
|
+
const allowed = [];
|
|
4134
|
+
let methodMismatch;
|
|
4135
|
+
for (const candidate of routes) {
|
|
4136
|
+
const params = candidate.match(path);
|
|
4137
|
+
if (!params) continue;
|
|
4138
|
+
if (candidate.method === "ALL" || candidate.method === requestedMethod) {
|
|
4139
|
+
return { _tag: "Match", route: candidate, params };
|
|
4140
|
+
}
|
|
4141
|
+
methodMismatch ??= candidate;
|
|
4142
|
+
allowed.push(candidate.method);
|
|
4143
|
+
}
|
|
4144
|
+
if (methodMismatch) {
|
|
4145
|
+
return { _tag: "MethodNotAllowed", route: methodMismatch, allowed };
|
|
4146
|
+
}
|
|
4147
|
+
return { _tag: "NotFound" };
|
|
4148
|
+
}
|
|
4149
|
+
function composeMiddleware(middleware, handler) {
|
|
4150
|
+
return middleware.reduceRight((next, current) => current(next), handler);
|
|
4151
|
+
}
|
|
4152
|
+
function parseJsonRequestBody(bodyText) {
|
|
4153
|
+
if (bodyText.length === 0) return { success: true, data: void 0 };
|
|
4154
|
+
try {
|
|
4155
|
+
return { success: true, data: JSON.parse(bodyText) };
|
|
4156
|
+
} catch (error) {
|
|
4157
|
+
return {
|
|
4158
|
+
success: false,
|
|
4159
|
+
message: `JSON parse error: ${error instanceof Error ? error.message : String(error)}`
|
|
4160
|
+
};
|
|
4161
|
+
}
|
|
4162
|
+
}
|
|
4163
|
+
function compileRoutePath(path) {
|
|
4164
|
+
const keys = [];
|
|
4165
|
+
const normalized = normalizeRoutePath(path);
|
|
4166
|
+
if (normalized === "/") {
|
|
4167
|
+
return (input) => input === "/" ? {} : void 0;
|
|
4168
|
+
}
|
|
4169
|
+
const source = normalized.split("/").filter(Boolean).map((segment) => {
|
|
4170
|
+
if (segment === "*") {
|
|
4171
|
+
keys.push("*");
|
|
4172
|
+
return "(.*)";
|
|
4173
|
+
}
|
|
4174
|
+
if (segment.startsWith(":")) {
|
|
4175
|
+
keys.push(segment.slice(1));
|
|
4176
|
+
return "([^/]+)";
|
|
4177
|
+
}
|
|
4178
|
+
return escapeRegExp(segment);
|
|
4179
|
+
}).join("/");
|
|
4180
|
+
const regex = new RegExp(`^/${source}/?$`);
|
|
4181
|
+
return (input) => {
|
|
4182
|
+
const match = regex.exec(input);
|
|
4183
|
+
if (!match) return void 0;
|
|
4184
|
+
const params = {};
|
|
4185
|
+
keys.forEach((key, index) => {
|
|
4186
|
+
params[key] = decodePathPart(match[index + 1] ?? "");
|
|
4187
|
+
});
|
|
4188
|
+
return params;
|
|
4189
|
+
};
|
|
4190
|
+
}
|
|
4191
|
+
function normalizeRouteMethod(method) {
|
|
4192
|
+
const upper = method.toUpperCase();
|
|
4193
|
+
return upper === "ALL" ? "ALL" : upper;
|
|
4194
|
+
}
|
|
4195
|
+
function normalizeRoutePath(path) {
|
|
4196
|
+
if (!path || path === "*") return "/";
|
|
4197
|
+
const withSlash = path.startsWith("/") ? path : `/${path}`;
|
|
4198
|
+
return withSlash.length > 1 ? withSlash.replace(/\/+$/, "") : withSlash;
|
|
4199
|
+
}
|
|
4200
|
+
function parseRequestUrl(target, host) {
|
|
4201
|
+
try {
|
|
4202
|
+
return new URL(target, `http://${host ?? "localhost"}`);
|
|
4203
|
+
} catch {
|
|
4204
|
+
return new URL("/", `http://${host ?? "localhost"}`);
|
|
4205
|
+
}
|
|
4206
|
+
}
|
|
4207
|
+
function queryToObject(searchParams) {
|
|
4208
|
+
const out = {};
|
|
4209
|
+
for (const [key, value] of searchParams.entries()) {
|
|
4210
|
+
const current = out[key];
|
|
4211
|
+
if (current === void 0) {
|
|
4212
|
+
out[key] = value;
|
|
4213
|
+
} else if (Array.isArray(current)) {
|
|
4214
|
+
current.push(value);
|
|
4215
|
+
} else {
|
|
4216
|
+
out[key] = [current, value];
|
|
4217
|
+
}
|
|
4218
|
+
}
|
|
4219
|
+
return out;
|
|
4220
|
+
}
|
|
4221
|
+
function normalizeNodeHeaders(headers) {
|
|
4222
|
+
const out = {};
|
|
4223
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
4224
|
+
if (value === void 0) continue;
|
|
4225
|
+
out[key.toLowerCase()] = Array.isArray(value) ? value.join(",") : value;
|
|
4226
|
+
}
|
|
4227
|
+
return out;
|
|
4228
|
+
}
|
|
4229
|
+
function setHeaderIfMissing2(headers, name, value) {
|
|
4230
|
+
const out = { ...headers ?? {} };
|
|
4231
|
+
const existing = Object.keys(out).find((key) => key.toLowerCase() === name.toLowerCase());
|
|
4232
|
+
if (!existing) out[name] = value;
|
|
4233
|
+
return out;
|
|
4234
|
+
}
|
|
4235
|
+
function serverUrl(server) {
|
|
4236
|
+
const address = server.address();
|
|
4237
|
+
if (!address || typeof address === "string") return void 0;
|
|
4238
|
+
const host = address.address === "::" || address.address === "0.0.0.0" ? "127.0.0.1" : address.address;
|
|
4239
|
+
return `http://${host}:${address.port}`;
|
|
4240
|
+
}
|
|
4241
|
+
function previewJson2(value) {
|
|
4242
|
+
try {
|
|
4243
|
+
return JSON.stringify(value);
|
|
4244
|
+
} catch {
|
|
4245
|
+
return "[unserializable]";
|
|
4246
|
+
}
|
|
4247
|
+
}
|
|
4248
|
+
function decodePathPart(value) {
|
|
4249
|
+
try {
|
|
4250
|
+
return decodeURIComponent(value);
|
|
4251
|
+
} catch {
|
|
4252
|
+
return value;
|
|
4253
|
+
}
|
|
4254
|
+
}
|
|
4255
|
+
function escapeRegExp(value) {
|
|
4256
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
4257
|
+
}
|
|
3397
4258
|
export {
|
|
4259
|
+
AdaptiveLimiter,
|
|
4260
|
+
ConfigValidationError,
|
|
3398
4261
|
DEFAULT_CACHE_RELEVANT_HEADERS,
|
|
4262
|
+
EmaComputer,
|
|
3399
4263
|
HttpConcurrencyPool,
|
|
3400
4264
|
LRUCache,
|
|
4265
|
+
LatencyWindow,
|
|
3401
4266
|
LifecycleStatsTracker,
|
|
3402
4267
|
PriorityQueue,
|
|
3403
4268
|
SEPARATOR,
|
|
3404
4269
|
SUPPORTED_ENCODINGS,
|
|
4270
|
+
Schema,
|
|
4271
|
+
SchemaValidationException,
|
|
4272
|
+
adaptiveLimiterPresets,
|
|
3405
4273
|
backoffDelayMs,
|
|
3406
4274
|
clampPriority,
|
|
3407
4275
|
computeCacheKey,
|
|
4276
|
+
computeGradient,
|
|
4277
|
+
computeNewLimit,
|
|
4278
|
+
decodeJsonBody,
|
|
4279
|
+
decodeJsonBodyEffect,
|
|
3408
4280
|
decorate,
|
|
4281
|
+
defaultHttpClientPreset,
|
|
3409
4282
|
defaultRetryOnError,
|
|
3410
4283
|
defaultRetryOnStatus,
|
|
3411
4284
|
defaultRetryableMethods,
|
|
4285
|
+
detectPlatform,
|
|
4286
|
+
empty,
|
|
4287
|
+
encodeJsonBodyEffect,
|
|
4288
|
+
executeProbe,
|
|
4289
|
+
formatHttpError,
|
|
4290
|
+
formatIssues,
|
|
4291
|
+
httpBuilder,
|
|
3412
4292
|
httpClient,
|
|
4293
|
+
httpClientBuilder,
|
|
3413
4294
|
httpClientStream,
|
|
3414
4295
|
httpClientWithMeta,
|
|
4296
|
+
httpRoute,
|
|
4297
|
+
isCircuitBreakerOpen,
|
|
4298
|
+
isHttpError,
|
|
4299
|
+
isKnownHttpError,
|
|
4300
|
+
isSchema,
|
|
4301
|
+
isValidationError,
|
|
4302
|
+
json,
|
|
4303
|
+
makeAdaptiveLimiterConfig,
|
|
4304
|
+
makeBudgetSemaphore,
|
|
3415
4305
|
makeCompressionMiddleware,
|
|
4306
|
+
makeConnectionStateMap,
|
|
4307
|
+
makeDefaultHttpClient,
|
|
3416
4308
|
makeHttp,
|
|
3417
4309
|
makeHttpClient,
|
|
4310
|
+
makeHttpClientBuilder,
|
|
4311
|
+
makeHttpRouter,
|
|
4312
|
+
makeHttpServerResource,
|
|
3418
4313
|
makeHttpStream,
|
|
3419
4314
|
makeLifecycleClient,
|
|
4315
|
+
makeNodeHttpServer,
|
|
4316
|
+
makeNodeHttpServerResource,
|
|
4317
|
+
makePrewarmManager,
|
|
3420
4318
|
makeRequestCompressionMiddleware,
|
|
3421
4319
|
makeResponseCompressionMiddleware,
|
|
4320
|
+
makeRuntimeHealthRoute,
|
|
4321
|
+
makeRuntimeReadinessRoute,
|
|
4322
|
+
makeSchemaIssue,
|
|
4323
|
+
matchHttpError,
|
|
4324
|
+
nodeHttpServerResource,
|
|
3422
4325
|
normalizeHeadersInit,
|
|
3423
4326
|
normalizeRetryBudget,
|
|
3424
4327
|
parseCacheKey,
|
|
4328
|
+
parseConfig,
|
|
3425
4329
|
prewarmConnections,
|
|
3426
4330
|
prewarmHttpConnections,
|
|
4331
|
+
resolveConfig,
|
|
3427
4332
|
resolveHttpPoolKey,
|
|
3428
4333
|
retryAfterMs,
|
|
4334
|
+
route,
|
|
4335
|
+
runtimeHealthRoute,
|
|
4336
|
+
runtimeReadinessRoute,
|
|
4337
|
+
s,
|
|
4338
|
+
schema,
|
|
4339
|
+
text,
|
|
4340
|
+
validateConfig,
|
|
4341
|
+
validateFetchAvailable,
|
|
4342
|
+
validateOrigin,
|
|
4343
|
+
validateValue,
|
|
3429
4344
|
validatedJson,
|
|
4345
|
+
validatedJsonResponse,
|
|
3430
4346
|
withAuth,
|
|
4347
|
+
withBatch,
|
|
3431
4348
|
withCache,
|
|
3432
4349
|
withCircuitBreaker,
|
|
3433
4350
|
withConnectionPrewarming,
|
|
@@ -3436,6 +4353,7 @@ export {
|
|
|
3436
4353
|
withMiddleware,
|
|
3437
4354
|
withPriority,
|
|
3438
4355
|
withRequestBatching,
|
|
4356
|
+
withResponseHeader,
|
|
3439
4357
|
withResponseTransform,
|
|
3440
4358
|
withRetry,
|
|
3441
4359
|
withRetryStream,
|