brass-runtime 1.13.8 → 1.14.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/dist/agent/cli/main.cjs +43 -43
- package/dist/agent/cli/main.js +2 -2
- package/dist/agent/cli/main.mjs +2 -2
- package/dist/agent/index.cjs +3 -3
- package/dist/agent/index.d.ts +1 -1
- package/dist/agent/index.js +2 -2
- package/dist/agent/index.mjs +2 -2
- package/dist/chunk-4N2JEK4H.mjs +3897 -0
- package/dist/chunk-BKBFSOGT.cjs +3897 -0
- package/dist/{chunk-XNOTJSMZ.mjs → chunk-BMRF4FN6.js} +268 -8
- package/dist/chunk-JT7D6M5H.js +3897 -0
- package/dist/{chunk-3R7ZYRK2.mjs → chunk-MQF7HZ7Y.mjs} +1 -1
- package/dist/chunk-SKVY72E5.cjs +667 -0
- package/dist/{chunk-ATHSSDUF.js → chunk-UWMMYKVK.mjs} +268 -8
- package/dist/{chunk-INZBKOHY.js → chunk-WJESVBWN.js} +1 -1
- package/dist/{chunk-XDINDYNA.cjs → chunk-XTMZTVIT.cjs} +134 -134
- package/dist/{effect-ISvXPLgc.d.ts → effect-DM56H743.d.ts} +191 -21
- package/dist/http/index.cjs +808 -140
- package/dist/http/index.d.ts +181 -8
- package/dist/http/index.js +793 -125
- package/dist/http/index.mjs +793 -125
- package/dist/index.cjs +1785 -137
- package/dist/index.d.ts +979 -36
- package/dist/index.js +1675 -27
- package/dist/index.mjs +1675 -27
- package/dist/stream-Oqe6WeLE.d.ts +173 -0
- package/package.json +1 -1
- package/wasm/pkg/brass_runtime_wasm_engine.d.ts +95 -16
- package/wasm/pkg/brass_runtime_wasm_engine.js +715 -15
- package/wasm/pkg/brass_runtime_wasm_engine_bg.wasm +0 -0
- package/wasm/pkg/brass_runtime_wasm_engine_bg.wasm.d.ts +78 -7
- package/dist/chunk-2P4PD6D7.cjs +0 -2557
- package/dist/chunk-7F2R7A2V.mjs +0 -2557
- package/dist/chunk-L6KKKM66.js +0 -2557
- package/dist/chunk-ZTDK2DLG.cjs +0 -407
- package/dist/stream-BvukHxCv.d.ts +0 -66
package/dist/http/index.mjs
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import {
|
|
2
|
+
makeCircuitBreaker,
|
|
3
|
+
sleep,
|
|
2
4
|
streamFromReadableStream
|
|
3
|
-
} from "../chunk-
|
|
5
|
+
} from "../chunk-UWMMYKVK.mjs";
|
|
4
6
|
import {
|
|
5
7
|
asyncFail,
|
|
6
8
|
asyncFlatMap,
|
|
@@ -8,9 +10,10 @@ import {
|
|
|
8
10
|
asyncSucceed,
|
|
9
11
|
fromPromiseAbortable,
|
|
10
12
|
mapTryAsync,
|
|
13
|
+
resolveWasmModule,
|
|
11
14
|
toPromise,
|
|
12
15
|
withAsyncPromise
|
|
13
|
-
} from "../chunk-
|
|
16
|
+
} from "../chunk-4N2JEK4H.mjs";
|
|
14
17
|
|
|
15
18
|
// src/http/optics/lens.ts
|
|
16
19
|
var Lens = {
|
|
@@ -39,36 +42,389 @@ var mergeHeaders = (extra) => (req) => Lens.over(Request.headers, (h) => ({ ...h
|
|
|
39
42
|
var mergeHeadersUnder = (under) => (req) => Lens.over(Request.headers, (h) => ({ ...under, ...h }))(req);
|
|
40
43
|
var setHeaderIfMissing = (k, v) => (req) => Lens.over(Request.headers, (h) => h[k] ? h : { ...h, [k]: v })(req);
|
|
41
44
|
|
|
42
|
-
// src/http/
|
|
43
|
-
var
|
|
44
|
-
var
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
45
|
+
// src/http/wasmPermitPool.ts
|
|
46
|
+
var DECISION_RUN_NOW = 0;
|
|
47
|
+
var DECISION_QUEUED = 1;
|
|
48
|
+
var WasmHttpPermitPoolBridge = class {
|
|
49
|
+
pool;
|
|
50
|
+
keyCache = /* @__PURE__ */ new Map();
|
|
51
|
+
constructor(Ctor, options) {
|
|
52
|
+
this.pool = new Ctor(options.concurrency, options.maxQueue, toU64(options.queueTimeoutMs));
|
|
48
53
|
}
|
|
49
|
-
|
|
54
|
+
acquire(key, subjectId, nowMs = Date.now()) {
|
|
55
|
+
const keyId = this.internKey(key);
|
|
56
|
+
const decision = this.pool.acquire(subjectId, keyId, toU64(nowMs));
|
|
57
|
+
const permitId = this.pool.last_permit_id();
|
|
58
|
+
if (decision === DECISION_RUN_NOW) return { kind: "run", keyId, permitId };
|
|
59
|
+
if (decision === DECISION_QUEUED) return { kind: "queued", keyId, permitId };
|
|
60
|
+
return { kind: "rejected", keyId, permitId };
|
|
61
|
+
}
|
|
62
|
+
release(keyId, nowMs = Date.now()) {
|
|
63
|
+
const ptr = this.pool.release(keyId, toU64(nowMs));
|
|
64
|
+
return this.readEvents(ptr, this.pool.permit_events_len());
|
|
65
|
+
}
|
|
66
|
+
cancel(permitId) {
|
|
67
|
+
this.pool.cancel(permitId);
|
|
68
|
+
}
|
|
69
|
+
advanceTime(nowMs = Date.now()) {
|
|
70
|
+
const ptr = this.pool.advance_time(toU64(nowMs));
|
|
71
|
+
return this.readEvents(ptr, this.pool.permit_events_len());
|
|
72
|
+
}
|
|
73
|
+
nextDeadlineMs() {
|
|
74
|
+
return this.pool.next_deadline_ms();
|
|
75
|
+
}
|
|
76
|
+
stats() {
|
|
77
|
+
return {
|
|
78
|
+
running: this.pool.metric_u64(0),
|
|
79
|
+
queued: this.pool.metric_u64(1),
|
|
80
|
+
acquired: this.pool.metric_u64(2),
|
|
81
|
+
released: this.pool.metric_u64(3),
|
|
82
|
+
rejected: this.pool.metric_u64(4),
|
|
83
|
+
queueTimeouts: this.pool.metric_u64(5),
|
|
84
|
+
keys: this.pool.metric_u64(6)
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
internKey(key) {
|
|
88
|
+
const normalized = key.trim().slice(0, 160) || "global";
|
|
89
|
+
let id = this.keyCache.get(normalized);
|
|
90
|
+
if (id === void 0) {
|
|
91
|
+
id = this.pool.intern_key(normalized);
|
|
92
|
+
this.keyCache.set(normalized, id);
|
|
93
|
+
}
|
|
94
|
+
return id;
|
|
95
|
+
}
|
|
96
|
+
readEvents(ptr, len) {
|
|
97
|
+
if (ptr === 0 || len <= 1) return [];
|
|
98
|
+
const words = new Uint32Array(this.pool.memory().buffer, ptr, len);
|
|
99
|
+
const count = words[0] >>> 0;
|
|
100
|
+
const out = [];
|
|
101
|
+
for (let i = 0; i < count; i++) {
|
|
102
|
+
const base = 1 + i * 3;
|
|
103
|
+
if (base + 2 >= words.length) break;
|
|
104
|
+
out.push({
|
|
105
|
+
subjectId: words[base] >>> 0,
|
|
106
|
+
permitId: words[base + 1] >>> 0,
|
|
107
|
+
keyId: words[base + 2] >>> 0
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
return out;
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
function makeWasmHttpPermitPool(options) {
|
|
114
|
+
const mod = resolveWasmModule();
|
|
115
|
+
const Ctor = mod?.BrassWasmHttpPermitPool;
|
|
116
|
+
if (!Ctor) throw new Error("brass-runtime wasm HTTP permit pool is not available. Run npm run build:wasm first.");
|
|
117
|
+
return new WasmHttpPermitPoolBridge(Ctor, options);
|
|
118
|
+
}
|
|
119
|
+
function toU64(value) {
|
|
120
|
+
return BigInt(Math.max(0, Math.floor(value)));
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// src/http/pool.ts
|
|
124
|
+
var DEFAULT_CONCURRENCY = 64;
|
|
125
|
+
var DEFAULT_MAX_QUEUE = 256;
|
|
126
|
+
var clampInt = (n, fallback, min) => {
|
|
127
|
+
if (n === void 0 || !Number.isFinite(n)) return fallback;
|
|
128
|
+
return Math.max(min, Math.floor(n));
|
|
50
129
|
};
|
|
51
|
-
var
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
130
|
+
var queueTimeoutError = (key, timeoutMs) => ({
|
|
131
|
+
_tag: "PoolTimeout",
|
|
132
|
+
key,
|
|
133
|
+
timeoutMs,
|
|
134
|
+
message: `HTTP pool '${key}' did not grant a slot within ${timeoutMs}ms`
|
|
135
|
+
});
|
|
136
|
+
var poolRejectedError = (key, maxQueue) => ({
|
|
137
|
+
_tag: "PoolRejected",
|
|
138
|
+
key,
|
|
139
|
+
limit: maxQueue,
|
|
140
|
+
message: `HTTP pool '${key}' queue is full`
|
|
141
|
+
});
|
|
142
|
+
var abortError = () => ({ _tag: "Abort" });
|
|
143
|
+
function resolveHttpPoolEngine(config) {
|
|
144
|
+
if (config.engine !== void 0) {
|
|
145
|
+
if (config.engine === "ts" || config.engine === "wasm") return config.engine;
|
|
146
|
+
throw new Error(`brass-runtime HTTP pool engine must be 'ts' or 'wasm'; received '${String(config.engine)}'`);
|
|
147
|
+
}
|
|
148
|
+
if (config.wasm === true) return "wasm";
|
|
149
|
+
if (config.wasm === false) return "ts";
|
|
150
|
+
return "ts";
|
|
151
|
+
}
|
|
152
|
+
function resolveHttpPoolKey(resolver, req, url) {
|
|
153
|
+
const custom = req.poolKey?.trim();
|
|
154
|
+
if (custom) return custom.slice(0, 160);
|
|
155
|
+
const r = resolver ?? "origin";
|
|
156
|
+
if (typeof r === "function") return r(req, url).trim().slice(0, 160) || "global";
|
|
157
|
+
if (r === "global") return "global";
|
|
158
|
+
if (r === "host") return url.host;
|
|
159
|
+
return url.origin;
|
|
160
|
+
}
|
|
161
|
+
var HttpConcurrencyPool = class {
|
|
162
|
+
states = /* @__PURE__ */ new Map();
|
|
163
|
+
concurrency;
|
|
164
|
+
maxQueue;
|
|
165
|
+
queueTimeoutMs;
|
|
166
|
+
keyResolver;
|
|
167
|
+
wasm;
|
|
168
|
+
wasmWaiters = /* @__PURE__ */ new Map();
|
|
169
|
+
wasmTimer;
|
|
170
|
+
nextSubjectId = 1;
|
|
171
|
+
constructor(config = {}) {
|
|
172
|
+
this.concurrency = clampInt(config.concurrency, DEFAULT_CONCURRENCY, 1);
|
|
173
|
+
this.maxQueue = clampInt(config.maxQueue, DEFAULT_MAX_QUEUE, 0);
|
|
174
|
+
this.queueTimeoutMs = config.queueTimeoutMs !== void 0 && Number.isFinite(config.queueTimeoutMs) ? Math.max(0, Math.floor(config.queueTimeoutMs)) : void 0;
|
|
175
|
+
this.keyResolver = config.key;
|
|
176
|
+
const engine = resolveHttpPoolEngine(config);
|
|
177
|
+
this.wasm = engine === "wasm" ? makeWasmHttpPermitPool({
|
|
178
|
+
concurrency: this.concurrency,
|
|
179
|
+
maxQueue: this.maxQueue,
|
|
180
|
+
queueTimeoutMs: this.queueTimeoutMs ?? 0
|
|
181
|
+
}) : void 0;
|
|
182
|
+
}
|
|
183
|
+
acquire(key, signal) {
|
|
184
|
+
return this.wasm ? this.acquireWasm(key, signal) : this.acquireJs(key, signal);
|
|
185
|
+
}
|
|
186
|
+
stats() {
|
|
187
|
+
const keys = Array.from(this.states.values()).map((state) => ({
|
|
188
|
+
key: state.key,
|
|
189
|
+
running: state.running,
|
|
190
|
+
queued: state.queue.length,
|
|
191
|
+
concurrency: this.concurrency,
|
|
192
|
+
maxQueue: this.maxQueue,
|
|
193
|
+
acquired: state.acquired,
|
|
194
|
+
released: state.released,
|
|
195
|
+
rejected: state.rejected,
|
|
196
|
+
queueTimeouts: state.queueTimeouts,
|
|
197
|
+
abortedWhileQueued: state.abortedWhileQueued
|
|
198
|
+
})).sort((a, b) => b.running + b.queued - (a.running + a.queued) || a.key.localeCompare(b.key));
|
|
199
|
+
return keys.reduce((acc, key) => ({
|
|
200
|
+
running: acc.running + key.running,
|
|
201
|
+
queued: acc.queued + key.queued,
|
|
202
|
+
acquired: acc.acquired + key.acquired,
|
|
203
|
+
released: acc.released + key.released,
|
|
204
|
+
rejected: acc.rejected + key.rejected,
|
|
205
|
+
queueTimeouts: acc.queueTimeouts + key.queueTimeouts,
|
|
206
|
+
abortedWhileQueued: acc.abortedWhileQueued + key.abortedWhileQueued,
|
|
207
|
+
wasm: this.wasm?.stats(),
|
|
208
|
+
keys: acc.keys.concat(key)
|
|
209
|
+
}), {
|
|
210
|
+
running: 0,
|
|
211
|
+
queued: 0,
|
|
212
|
+
acquired: 0,
|
|
213
|
+
released: 0,
|
|
214
|
+
rejected: 0,
|
|
215
|
+
queueTimeouts: 0,
|
|
216
|
+
abortedWhileQueued: 0,
|
|
217
|
+
...this.wasm ? { wasm: this.wasm.stats() } : {},
|
|
218
|
+
keys: []
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
acquireJs(key, signal) {
|
|
222
|
+
const state = this.getState(key);
|
|
223
|
+
if (signal.aborted) return Promise.reject(abortError());
|
|
224
|
+
if (state.running < this.concurrency) {
|
|
225
|
+
state.running++;
|
|
226
|
+
state.acquired++;
|
|
227
|
+
return Promise.resolve(this.makeLease(state));
|
|
228
|
+
}
|
|
229
|
+
if (state.queue.length >= this.maxQueue) {
|
|
230
|
+
state.rejected++;
|
|
231
|
+
return Promise.reject(poolRejectedError(key, this.maxQueue));
|
|
232
|
+
}
|
|
233
|
+
return new Promise((resolve, reject) => {
|
|
234
|
+
const waiter = { signal, resolve, reject };
|
|
235
|
+
const removeWaiter = () => this.removeWaiter(state, waiter);
|
|
236
|
+
const cleanup = () => this.cleanupWaiter(waiter);
|
|
237
|
+
waiter.abort = () => {
|
|
238
|
+
cleanup();
|
|
239
|
+
removeWaiter();
|
|
240
|
+
state.abortedWhileQueued++;
|
|
241
|
+
reject(abortError());
|
|
242
|
+
};
|
|
243
|
+
signal.addEventListener("abort", waiter.abort, { once: true });
|
|
244
|
+
if (this.queueTimeoutMs !== void 0 && this.queueTimeoutMs > 0) {
|
|
245
|
+
waiter.timer = setTimeout(() => {
|
|
246
|
+
cleanup();
|
|
247
|
+
removeWaiter();
|
|
248
|
+
state.queueTimeouts++;
|
|
249
|
+
reject(queueTimeoutError(key, this.queueTimeoutMs));
|
|
250
|
+
}, this.queueTimeoutMs);
|
|
251
|
+
}
|
|
252
|
+
state.queue.push(waiter);
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
acquireWasm(key, signal) {
|
|
256
|
+
const wasm = this.wasm;
|
|
257
|
+
const state = this.getState(key);
|
|
258
|
+
if (signal.aborted) return Promise.reject(abortError());
|
|
259
|
+
const subjectId = this.allocateSubjectId();
|
|
260
|
+
const decision = wasm.acquire(key, subjectId);
|
|
261
|
+
if (decision.kind === "run") {
|
|
262
|
+
state.running++;
|
|
263
|
+
state.acquired++;
|
|
264
|
+
return Promise.resolve(this.makeLease(state, decision.keyId));
|
|
265
|
+
}
|
|
266
|
+
if (decision.kind === "rejected") {
|
|
267
|
+
state.rejected++;
|
|
268
|
+
return Promise.reject(poolRejectedError(key, this.maxQueue));
|
|
269
|
+
}
|
|
270
|
+
return new Promise((resolve, reject) => {
|
|
271
|
+
const waiter = { signal, resolve, reject };
|
|
272
|
+
const removeWaiter = () => this.removeWaiter(state, waiter);
|
|
273
|
+
const cleanup = () => this.cleanupWaiter(waiter);
|
|
274
|
+
waiter.abort = () => {
|
|
275
|
+
cleanup();
|
|
276
|
+
removeWaiter();
|
|
277
|
+
wasm.cancel(decision.permitId);
|
|
278
|
+
this.wasmWaiters.delete(decision.permitId);
|
|
279
|
+
state.abortedWhileQueued++;
|
|
280
|
+
reject(abortError());
|
|
281
|
+
};
|
|
282
|
+
signal.addEventListener("abort", waiter.abort, { once: true });
|
|
283
|
+
state.queue.push(waiter);
|
|
284
|
+
this.wasmWaiters.set(decision.permitId, { waiter, state, keyId: decision.keyId });
|
|
285
|
+
this.scheduleWasmTimeoutPump();
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
getState(key) {
|
|
289
|
+
const k = key.trim().slice(0, 160) || "global";
|
|
290
|
+
const existing = this.states.get(k);
|
|
291
|
+
if (existing) return existing;
|
|
292
|
+
const created = {
|
|
293
|
+
key: k,
|
|
294
|
+
running: 0,
|
|
295
|
+
queue: [],
|
|
296
|
+
acquired: 0,
|
|
297
|
+
released: 0,
|
|
298
|
+
rejected: 0,
|
|
299
|
+
queueTimeouts: 0,
|
|
300
|
+
abortedWhileQueued: 0
|
|
58
301
|
};
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
)
|
|
302
|
+
this.states.set(k, created);
|
|
303
|
+
return created;
|
|
304
|
+
}
|
|
305
|
+
makeLease(state, wasmKeyId) {
|
|
306
|
+
let released = false;
|
|
307
|
+
return {
|
|
308
|
+
key: state.key,
|
|
309
|
+
release: () => {
|
|
310
|
+
if (released) return;
|
|
311
|
+
released = true;
|
|
312
|
+
if (state.running > 0) state.running--;
|
|
313
|
+
state.released++;
|
|
314
|
+
if (this.wasm && wasmKeyId !== void 0) {
|
|
315
|
+
this.handleWasmGrants(this.wasm.release(wasmKeyId));
|
|
316
|
+
this.scheduleWasmTimeoutPump();
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
this.drain(state);
|
|
320
|
+
}
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
drain(state) {
|
|
324
|
+
while (state.running < this.concurrency && state.queue.length > 0) {
|
|
325
|
+
const waiter = state.queue.shift();
|
|
326
|
+
this.cleanupWaiter(waiter);
|
|
327
|
+
if (waiter.signal.aborted) {
|
|
328
|
+
state.abortedWhileQueued++;
|
|
329
|
+
waiter.reject(abortError());
|
|
330
|
+
continue;
|
|
331
|
+
}
|
|
332
|
+
state.running++;
|
|
333
|
+
state.acquired++;
|
|
334
|
+
waiter.resolve(this.makeLease(state));
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
handleWasmGrants(events) {
|
|
338
|
+
for (const event of events) {
|
|
339
|
+
const pending = this.wasmWaiters.get(event.permitId);
|
|
340
|
+
if (!pending) continue;
|
|
341
|
+
this.wasmWaiters.delete(event.permitId);
|
|
342
|
+
this.cleanupWaiter(pending.waiter);
|
|
343
|
+
this.removeWaiter(pending.state, pending.waiter);
|
|
344
|
+
if (pending.waiter.signal.aborted) {
|
|
345
|
+
pending.state.abortedWhileQueued++;
|
|
346
|
+
pending.waiter.reject(abortError());
|
|
347
|
+
continue;
|
|
348
|
+
}
|
|
349
|
+
pending.state.running++;
|
|
350
|
+
pending.state.acquired++;
|
|
351
|
+
pending.waiter.resolve(this.makeLease(pending.state, event.keyId));
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
handleWasmTimeouts(events) {
|
|
355
|
+
for (const event of events) {
|
|
356
|
+
const pending = this.wasmWaiters.get(event.permitId);
|
|
357
|
+
if (!pending) continue;
|
|
358
|
+
this.wasmWaiters.delete(event.permitId);
|
|
359
|
+
this.cleanupWaiter(pending.waiter);
|
|
360
|
+
this.removeWaiter(pending.state, pending.waiter);
|
|
361
|
+
pending.state.queueTimeouts++;
|
|
362
|
+
pending.waiter.reject(queueTimeoutError(pending.state.key, this.queueTimeoutMs ?? 0));
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
scheduleWasmTimeoutPump() {
|
|
366
|
+
if (!this.wasm) return;
|
|
367
|
+
if (this.wasmTimer !== void 0) clearTimeout(this.wasmTimer);
|
|
368
|
+
this.wasmTimer = void 0;
|
|
369
|
+
const next = this.wasm.nextDeadlineMs();
|
|
370
|
+
if (!Number.isFinite(next) || next < 0) return;
|
|
371
|
+
const delay = Math.max(0, Math.min(2 ** 31 - 1, Math.floor(next - Date.now())));
|
|
372
|
+
this.wasmTimer = setTimeout(() => {
|
|
373
|
+
this.wasmTimer = void 0;
|
|
374
|
+
if (!this.wasm) return;
|
|
375
|
+
this.handleWasmTimeouts(this.wasm.advanceTime());
|
|
376
|
+
this.scheduleWasmTimeoutPump();
|
|
377
|
+
}, delay);
|
|
378
|
+
if (typeof this.wasmTimer.unref === "function") this.wasmTimer.unref();
|
|
379
|
+
}
|
|
380
|
+
cleanupWaiter(waiter) {
|
|
381
|
+
if (waiter.timer !== void 0) {
|
|
382
|
+
clearTimeout(waiter.timer);
|
|
383
|
+
waiter.timer = void 0;
|
|
384
|
+
}
|
|
385
|
+
if (waiter.abort) {
|
|
386
|
+
waiter.signal.removeEventListener("abort", waiter.abort);
|
|
387
|
+
waiter.abort = void 0;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
removeWaiter(state, waiter) {
|
|
391
|
+
const idx = state.queue.indexOf(waiter);
|
|
392
|
+
if (idx >= 0) state.queue.splice(idx, 1);
|
|
393
|
+
}
|
|
394
|
+
allocateSubjectId() {
|
|
395
|
+
const id = this.nextSubjectId >>> 0;
|
|
396
|
+
this.nextSubjectId = this.nextSubjectId + 1 >>> 0;
|
|
397
|
+
if (this.nextSubjectId === 0) this.nextSubjectId = 1;
|
|
398
|
+
return id === 0 ? this.allocateSubjectId() : id;
|
|
399
|
+
}
|
|
400
|
+
};
|
|
63
401
|
|
|
64
402
|
// src/http/client.ts
|
|
65
|
-
var
|
|
66
|
-
|
|
67
|
-
|
|
403
|
+
var emptyStats = () => ({
|
|
404
|
+
inFlight: 0,
|
|
405
|
+
started: 0,
|
|
406
|
+
succeeded: 0,
|
|
407
|
+
failed: 0,
|
|
408
|
+
aborted: 0,
|
|
409
|
+
timedOut: 0,
|
|
410
|
+
poolRejected: 0,
|
|
411
|
+
poolTimeouts: 0
|
|
68
412
|
});
|
|
69
|
-
var
|
|
70
|
-
|
|
71
|
-
|
|
413
|
+
var decorate = (run, stats = emptyStats) => Object.assign(((req) => run(req)), {
|
|
414
|
+
with: (mw) => decorate(mw(run), stats),
|
|
415
|
+
stats
|
|
416
|
+
});
|
|
417
|
+
var withMiddleware = (mw) => (c) => decorate(mw(c), c.stats);
|
|
418
|
+
var decorateStream = (run, stats = emptyStats) => Object.assign(((req) => run(req)), { stats });
|
|
419
|
+
var isTaggedHttpError = (e) => {
|
|
420
|
+
if (typeof e !== "object" || e === null || !("_tag" in e)) return false;
|
|
421
|
+
const tag = e._tag;
|
|
422
|
+
return tag === "Abort" || tag === "BadUrl" || tag === "FetchError" || tag === "Timeout" || tag === "PoolRejected" || tag === "PoolTimeout";
|
|
423
|
+
};
|
|
424
|
+
var isAbortError = (e) => typeof e === "object" && e !== null && "name" in e && e.name === "AbortError";
|
|
425
|
+
var normalizeHttpError = (e) => {
|
|
426
|
+
if (isTaggedHttpError(e)) return e;
|
|
427
|
+
if (isAbortError(e)) return { _tag: "Abort" };
|
|
72
428
|
return { _tag: "FetchError", message: String(e) };
|
|
73
429
|
};
|
|
74
430
|
var normalizeHeadersInit = (h) => {
|
|
@@ -90,81 +446,196 @@ var normalizeRequest = (defaultHeaders) => (req0) => {
|
|
|
90
446
|
}
|
|
91
447
|
return req;
|
|
92
448
|
};
|
|
449
|
+
var resolvePositiveTimeout = (value) => {
|
|
450
|
+
if (value === void 0 || !Number.isFinite(value)) return void 0;
|
|
451
|
+
const n = Math.floor(value);
|
|
452
|
+
return n > 0 ? n : void 0;
|
|
453
|
+
};
|
|
454
|
+
var makeHttpStats = (pool) => {
|
|
455
|
+
const stats = {
|
|
456
|
+
inFlight: 0,
|
|
457
|
+
started: 0,
|
|
458
|
+
succeeded: 0,
|
|
459
|
+
failed: 0,
|
|
460
|
+
aborted: 0,
|
|
461
|
+
timedOut: 0,
|
|
462
|
+
poolRejected: 0,
|
|
463
|
+
poolTimeouts: 0
|
|
464
|
+
};
|
|
465
|
+
const onStart = () => {
|
|
466
|
+
stats.inFlight++;
|
|
467
|
+
stats.started++;
|
|
468
|
+
};
|
|
469
|
+
const onFinish = (finish) => {
|
|
470
|
+
if (stats.inFlight > 0) stats.inFlight--;
|
|
471
|
+
stats.lastDurationMs = finish.durationMs;
|
|
472
|
+
if (finish.outcome === "success") {
|
|
473
|
+
stats.succeeded++;
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
if (finish.outcome === "interrupt") {
|
|
477
|
+
stats.aborted++;
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
480
|
+
if (finish.outcome === "timeout") {
|
|
481
|
+
stats.timedOut++;
|
|
482
|
+
return;
|
|
483
|
+
}
|
|
484
|
+
const err = normalizeHttpError(finish.error);
|
|
485
|
+
switch (err._tag) {
|
|
486
|
+
case "Abort":
|
|
487
|
+
stats.aborted++;
|
|
488
|
+
return;
|
|
489
|
+
case "Timeout":
|
|
490
|
+
stats.timedOut++;
|
|
491
|
+
return;
|
|
492
|
+
case "PoolRejected":
|
|
493
|
+
stats.poolRejected++;
|
|
494
|
+
stats.failed++;
|
|
495
|
+
return;
|
|
496
|
+
case "PoolTimeout":
|
|
497
|
+
stats.poolTimeouts++;
|
|
498
|
+
stats.failed++;
|
|
499
|
+
return;
|
|
500
|
+
default:
|
|
501
|
+
stats.failed++;
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
504
|
+
};
|
|
505
|
+
const snapshot = () => ({
|
|
506
|
+
...stats,
|
|
507
|
+
...pool ? { pool: pool.stats() } : {}
|
|
508
|
+
});
|
|
509
|
+
return { onStart, onFinish, snapshot };
|
|
510
|
+
};
|
|
511
|
+
var makePool = (cfg) => cfg.pool === void 0 || cfg.pool === false ? void 0 : new HttpConcurrencyPool(cfg.pool);
|
|
512
|
+
var resolveRequestUrl = (req, baseUrl) => {
|
|
513
|
+
try {
|
|
514
|
+
return new URL(req.url, baseUrl);
|
|
515
|
+
} catch {
|
|
516
|
+
return { _tag: "BadUrl", message: `URL inv\xE1lida: ${req.url}` };
|
|
517
|
+
}
|
|
518
|
+
};
|
|
519
|
+
var headersOf = (res) => {
|
|
520
|
+
const headers = {};
|
|
521
|
+
res.headers.forEach((v, k) => headers[k] = v);
|
|
522
|
+
return headers;
|
|
523
|
+
};
|
|
524
|
+
var fetchLabel = (req, url) => `http:${req.method}:${url.origin}`;
|
|
525
|
+
var timeoutReason = (req, url, timeoutMs) => ({
|
|
526
|
+
_tag: "Timeout",
|
|
527
|
+
timeoutMs,
|
|
528
|
+
phase: "request",
|
|
529
|
+
message: `HTTP ${req.method} ${url.origin} timed out after ${timeoutMs}ms`
|
|
530
|
+
});
|
|
93
531
|
function makeHttpStream(cfg = {}) {
|
|
94
532
|
const baseUrl = cfg.baseUrl ?? "";
|
|
95
533
|
const defaultHeaders = cfg.headers ?? {};
|
|
96
534
|
const normalize = normalizeRequest(defaultHeaders);
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
535
|
+
const pool = makePool(cfg);
|
|
536
|
+
const metrics = makeHttpStats(pool);
|
|
537
|
+
const run = (req0) => {
|
|
538
|
+
const req = normalize(req0);
|
|
539
|
+
const url = resolveRequestUrl(req, baseUrl);
|
|
540
|
+
if (!(url instanceof URL)) return asyncFail(url);
|
|
541
|
+
const timeoutMs = resolvePositiveTimeout(req.timeoutMs ?? cfg.timeoutMs);
|
|
542
|
+
return fromPromiseAbortable(
|
|
543
|
+
async (signal) => {
|
|
544
|
+
let lease;
|
|
545
|
+
try {
|
|
546
|
+
if (pool) {
|
|
547
|
+
const key = resolveHttpPoolKey(pool.keyResolver, req, url);
|
|
548
|
+
lease = await pool.acquire(key, signal);
|
|
549
|
+
}
|
|
550
|
+
const started = performance.now();
|
|
551
|
+
const res = await fetch(url, {
|
|
552
|
+
...req.init ?? {},
|
|
553
|
+
method: req.method,
|
|
554
|
+
headers: Request.headers.get(req),
|
|
555
|
+
body: req.body,
|
|
556
|
+
signal
|
|
557
|
+
});
|
|
558
|
+
const headers = headersOf(res);
|
|
559
|
+
const body = streamFromReadableStream(res.body, normalizeHttpError);
|
|
560
|
+
lease?.release();
|
|
561
|
+
lease = void 0;
|
|
562
|
+
return {
|
|
563
|
+
status: res.status,
|
|
564
|
+
statusText: res.statusText,
|
|
565
|
+
headers,
|
|
566
|
+
body,
|
|
567
|
+
ms: Math.round(performance.now() - started)
|
|
568
|
+
};
|
|
569
|
+
} finally {
|
|
570
|
+
lease?.release();
|
|
571
|
+
}
|
|
572
|
+
},
|
|
573
|
+
normalizeHttpError,
|
|
574
|
+
{
|
|
575
|
+
label: fetchLabel(req, url),
|
|
576
|
+
timeoutMs,
|
|
577
|
+
timeoutReason: timeoutMs ? () => timeoutReason(req, url, timeoutMs) : void 0,
|
|
578
|
+
onStart: metrics.onStart,
|
|
579
|
+
onFinish: metrics.onFinish
|
|
105
580
|
}
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
method: req.method,
|
|
110
|
-
headers: Request.headers.get(req),
|
|
111
|
-
// 👈 optics: headers ya normalizados
|
|
112
|
-
body: req.body,
|
|
113
|
-
signal
|
|
114
|
-
});
|
|
115
|
-
const headers = {};
|
|
116
|
-
res.headers.forEach((v, k) => headers[k] = v);
|
|
117
|
-
const body = streamFromReadableStream(res.body, normalizeHttpError2);
|
|
118
|
-
return {
|
|
119
|
-
status: res.status,
|
|
120
|
-
statusText: res.statusText,
|
|
121
|
-
headers,
|
|
122
|
-
body,
|
|
123
|
-
ms: Math.round(performance.now() - started)
|
|
124
|
-
};
|
|
125
|
-
},
|
|
126
|
-
normalizeHttpError2
|
|
127
|
-
);
|
|
581
|
+
);
|
|
582
|
+
};
|
|
583
|
+
return decorateStream(run, metrics.snapshot);
|
|
128
584
|
}
|
|
129
585
|
function makeHttp(cfg = {}) {
|
|
130
586
|
const baseUrl = cfg.baseUrl ?? "";
|
|
131
587
|
const defaultHeaders = cfg.headers ?? {};
|
|
132
588
|
const normalize = normalizeRequest(defaultHeaders);
|
|
133
|
-
const
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
589
|
+
const pool = makePool(cfg);
|
|
590
|
+
const metrics = makeHttpStats(pool);
|
|
591
|
+
const run = (req0) => {
|
|
592
|
+
const req = normalize(req0);
|
|
593
|
+
const url = resolveRequestUrl(req, baseUrl);
|
|
594
|
+
if (!(url instanceof URL)) return asyncFail(url);
|
|
595
|
+
const timeoutMs = resolvePositiveTimeout(req.timeoutMs ?? cfg.timeoutMs);
|
|
596
|
+
return fromPromiseAbortable(
|
|
597
|
+
async (signal) => {
|
|
598
|
+
let lease;
|
|
599
|
+
try {
|
|
600
|
+
if (pool) {
|
|
601
|
+
const key = resolveHttpPoolKey(pool.keyResolver, req, url);
|
|
602
|
+
lease = await pool.acquire(key, signal);
|
|
603
|
+
}
|
|
604
|
+
const started = performance.now();
|
|
605
|
+
const res = await fetch(url, {
|
|
606
|
+
...req.init ?? {},
|
|
607
|
+
method: req.method,
|
|
608
|
+
headers: Request.headers.get(req),
|
|
609
|
+
body: req.body,
|
|
610
|
+
signal
|
|
611
|
+
});
|
|
612
|
+
const bodyText = await res.text();
|
|
613
|
+
const headers = headersOf(res);
|
|
614
|
+
return {
|
|
615
|
+
status: res.status,
|
|
616
|
+
statusText: res.statusText,
|
|
617
|
+
headers,
|
|
618
|
+
bodyText,
|
|
619
|
+
ms: Math.round(performance.now() - started)
|
|
620
|
+
};
|
|
621
|
+
} finally {
|
|
622
|
+
lease?.release();
|
|
623
|
+
}
|
|
624
|
+
},
|
|
625
|
+
normalizeHttpError,
|
|
626
|
+
{
|
|
627
|
+
label: fetchLabel(req, url),
|
|
628
|
+
timeoutMs,
|
|
629
|
+
timeoutReason: timeoutMs ? () => timeoutReason(req, url, timeoutMs) : void 0,
|
|
630
|
+
onStart: metrics.onStart,
|
|
631
|
+
onFinish: metrics.onFinish
|
|
141
632
|
}
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
method: req.method,
|
|
146
|
-
headers: Request.headers.get(req),
|
|
147
|
-
// 👈 optics
|
|
148
|
-
body: req.body,
|
|
149
|
-
signal
|
|
150
|
-
});
|
|
151
|
-
const bodyText = await res.text();
|
|
152
|
-
const headers = {};
|
|
153
|
-
res.headers.forEach((v, k) => headers[k] = v);
|
|
154
|
-
return {
|
|
155
|
-
status: res.status,
|
|
156
|
-
statusText: res.statusText,
|
|
157
|
-
headers,
|
|
158
|
-
bodyText,
|
|
159
|
-
ms: Math.round(performance.now() - started)
|
|
160
|
-
};
|
|
161
|
-
},
|
|
162
|
-
normalizeHttpError2
|
|
163
|
-
);
|
|
164
|
-
return decorate(run);
|
|
633
|
+
);
|
|
634
|
+
};
|
|
635
|
+
return decorate(run, metrics.snapshot);
|
|
165
636
|
}
|
|
166
637
|
var clamp = (n, min, max) => Math.max(min, Math.min(max, n));
|
|
167
|
-
var defaultRetryOnError = (e) => e._tag === "FetchError";
|
|
638
|
+
var defaultRetryOnError = (e) => e._tag === "FetchError" || e._tag === "Timeout" || e._tag === "PoolTimeout";
|
|
168
639
|
var defaultRetryOnStatus = (s) => s === 408 || s === 429 || s === 500 || s === 502 || s === 503 || s === 504;
|
|
169
640
|
var backoffDelayMs = (attempt, base, cap) => {
|
|
170
641
|
const exp = base * Math.pow(2, attempt);
|
|
@@ -182,31 +653,85 @@ var retryAfterMs = (headers) => {
|
|
|
182
653
|
if (Number.isFinite(t)) return Math.max(0, t - Date.now());
|
|
183
654
|
return void 0;
|
|
184
655
|
};
|
|
185
|
-
var withRetryStream = (p) => (next) =>
|
|
186
|
-
const
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
656
|
+
var withRetryStream = (p) => (next) => {
|
|
657
|
+
const retryOnStatus = p.retryOnStatus ?? defaultRetryOnStatus;
|
|
658
|
+
const retryOnError = p.retryOnError ?? defaultRetryOnError;
|
|
659
|
+
const maxElapsedMs = p.maxElapsedMs !== void 0 && Number.isFinite(p.maxElapsedMs) ? Math.max(0, Math.floor(p.maxElapsedMs)) : void 0;
|
|
660
|
+
const run = (req) => {
|
|
661
|
+
const startedAt = performance.now();
|
|
662
|
+
const remainingBudget = () => maxElapsedMs === void 0 ? Number.POSITIVE_INFINITY : maxElapsedMs - (performance.now() - startedAt);
|
|
663
|
+
const delayWithinBudget = (delayMs) => Math.max(0, Math.min(delayMs, remainingBudget()));
|
|
664
|
+
const loop = (attempt) => asyncFold(
|
|
665
|
+
next(req),
|
|
666
|
+
(e) => {
|
|
667
|
+
if (e._tag === "Abort" || e._tag === "BadUrl" || e._tag === "PoolRejected") return asyncFail(e);
|
|
668
|
+
const canRetry = attempt < p.maxRetries && retryOnError(e) && remainingBudget() > 0;
|
|
669
|
+
if (!canRetry) return asyncFail(e);
|
|
670
|
+
const d = delayWithinBudget(backoffDelayMs(attempt, p.baseDelayMs, p.maxDelayMs));
|
|
671
|
+
if (d <= 0 && maxElapsedMs !== void 0) return asyncFail(e);
|
|
672
|
+
return asyncFlatMap(sleep(d), () => loop(attempt + 1));
|
|
673
|
+
},
|
|
674
|
+
(w) => {
|
|
675
|
+
const canRetry = attempt < p.maxRetries && retryOnStatus(w.status) && remainingBudget() > 0;
|
|
676
|
+
if (!canRetry) return asyncSucceed(w);
|
|
677
|
+
const ra = p.respectRetryAfter === false ? void 0 : retryAfterMs(w.headers);
|
|
678
|
+
const rawDelay = ra === void 0 ? backoffDelayMs(attempt, p.baseDelayMs, p.maxDelayMs) : Math.min(ra, p.maxDelayMs);
|
|
679
|
+
const d = delayWithinBudget(rawDelay);
|
|
680
|
+
if (d <= 0 && maxElapsedMs !== void 0) return asyncSucceed(w);
|
|
681
|
+
return asyncFlatMap(sleep(d), () => loop(attempt + 1));
|
|
682
|
+
}
|
|
683
|
+
);
|
|
684
|
+
return loop(0);
|
|
685
|
+
};
|
|
686
|
+
return decorateStream(run, next.stats);
|
|
687
|
+
};
|
|
688
|
+
|
|
689
|
+
// src/http/retry/wasmRetryPlanner.ts
|
|
690
|
+
var WasmRetryPlannerBridge = class {
|
|
691
|
+
planner;
|
|
692
|
+
constructor(Ctor) {
|
|
693
|
+
this.planner = new Ctor();
|
|
694
|
+
}
|
|
695
|
+
start(options) {
|
|
696
|
+
return this.planner.start(
|
|
697
|
+
options.nowMs,
|
|
698
|
+
options.maxRetries,
|
|
699
|
+
options.baseDelayMs,
|
|
700
|
+
options.maxDelayMs,
|
|
701
|
+
options.maxElapsedMs ?? -1,
|
|
702
|
+
BigInt(this.seed())
|
|
703
|
+
);
|
|
704
|
+
}
|
|
705
|
+
nextDelayMs(retryId, options) {
|
|
706
|
+
const delay = this.planner.next_delay_ms(retryId, options.nowMs, options.retryable, options.retryAfterMs ?? -1);
|
|
707
|
+
return delay < 0 ? void 0 : delay;
|
|
708
|
+
}
|
|
709
|
+
drop(retryId) {
|
|
710
|
+
this.planner.drop_state(retryId);
|
|
711
|
+
}
|
|
712
|
+
stats() {
|
|
713
|
+
return {
|
|
714
|
+
live: this.planner.metric_u64(0),
|
|
715
|
+
planned: this.planner.metric_u64(1),
|
|
716
|
+
exhausted: this.planner.metric_u64(2),
|
|
717
|
+
dropped: this.planner.metric_u64(3)
|
|
718
|
+
};
|
|
719
|
+
}
|
|
720
|
+
seed() {
|
|
721
|
+
return Math.floor(Math.random() * Number.MAX_SAFE_INTEGER);
|
|
722
|
+
}
|
|
723
|
+
};
|
|
724
|
+
function makeWasmRetryPlanner() {
|
|
725
|
+
const mod = resolveWasmModule();
|
|
726
|
+
const Ctor = mod?.BrassWasmRetryPlanner;
|
|
727
|
+
if (!Ctor) throw new Error("brass-runtime wasm retry planner is not available. Run npm run build:wasm first.");
|
|
728
|
+
return new WasmRetryPlannerBridge(Ctor);
|
|
729
|
+
}
|
|
205
730
|
|
|
206
731
|
// src/http/retry/retry.ts
|
|
207
732
|
var defaultRetryableMethods = ["GET", "HEAD", "OPTIONS"];
|
|
208
733
|
var defaultRetryOnStatus2 = (s) => s === 408 || s === 429 || s === 500 || s === 502 || s === 503 || s === 504;
|
|
209
|
-
var defaultRetryOnError2 = (e) => e._tag === "FetchError";
|
|
734
|
+
var defaultRetryOnError2 = (e) => e._tag === "FetchError" || e._tag === "Timeout" || e._tag === "PoolTimeout";
|
|
210
735
|
var clamp2 = (n, min, max) => Math.max(min, Math.min(max, n));
|
|
211
736
|
var backoffDelayMs2 = (attempt, base, cap) => {
|
|
212
737
|
const b = Math.max(0, base);
|
|
@@ -228,32 +753,86 @@ var retryAfterMs2 = (headers) => {
|
|
|
228
753
|
if (Number.isFinite(t)) return Math.max(0, t - Date.now());
|
|
229
754
|
return void 0;
|
|
230
755
|
};
|
|
756
|
+
var normalizeBudget = (ms) => {
|
|
757
|
+
if (ms === void 0 || !Number.isFinite(ms)) return void 0;
|
|
758
|
+
return Math.max(0, Math.floor(ms));
|
|
759
|
+
};
|
|
760
|
+
var resolveRetryEngine = (p) => {
|
|
761
|
+
if (p.engine !== void 0) {
|
|
762
|
+
if (p.engine === "ts" || p.engine === "wasm") return p.engine;
|
|
763
|
+
throw new Error(`brass-runtime retry engine must be 'ts' or 'wasm'; received '${String(p.engine)}'`);
|
|
764
|
+
}
|
|
765
|
+
if (p.wasm === true) return "wasm";
|
|
766
|
+
if (p.wasm === false) return "ts";
|
|
767
|
+
return "ts";
|
|
768
|
+
};
|
|
231
769
|
var withRetry = (p) => (next) => {
|
|
232
770
|
const retryOnMethods = p.retryOnMethods ?? defaultRetryableMethods;
|
|
233
771
|
const retryOnStatus = p.retryOnStatus ?? defaultRetryOnStatus2;
|
|
234
772
|
const retryOnError = p.retryOnError ?? defaultRetryOnError2;
|
|
773
|
+
const maxElapsedMs = normalizeBudget(p.maxElapsedMs);
|
|
774
|
+
const retryEngine = resolveRetryEngine(p);
|
|
775
|
+
const wasmPlanner = retryEngine === "wasm" ? makeWasmRetryPlanner() : void 0;
|
|
235
776
|
const isMethodRetryable = (req) => retryOnMethods.includes(req.method);
|
|
236
|
-
const
|
|
777
|
+
const nextDelay = (retryId, attempt, startedAt, retryable, retryAfter) => {
|
|
778
|
+
if (!retryable) return void 0;
|
|
779
|
+
if (wasmPlanner && retryId !== void 0) {
|
|
780
|
+
return wasmPlanner.nextDelayMs(retryId, {
|
|
781
|
+
nowMs: performance.now(),
|
|
782
|
+
retryable,
|
|
783
|
+
retryAfterMs: retryAfter
|
|
784
|
+
});
|
|
785
|
+
}
|
|
786
|
+
const remainingBudget = maxElapsedMs === void 0 ? Number.POSITIVE_INFINITY : maxElapsedMs - (performance.now() - startedAt);
|
|
787
|
+
if (remainingBudget <= 0) return void 0;
|
|
788
|
+
const rawDelay = retryAfter === void 0 ? backoffDelayMs2(attempt, p.baseDelayMs, p.maxDelayMs) : Math.min(retryAfter, p.maxDelayMs);
|
|
789
|
+
return Math.max(0, Math.min(rawDelay, remainingBudget));
|
|
790
|
+
};
|
|
791
|
+
const dropPlanner = (retryId) => {
|
|
792
|
+
if (retryId !== void 0) wasmPlanner?.drop(retryId);
|
|
793
|
+
};
|
|
794
|
+
const loop = (req, attempt, startedAt, retryId) => {
|
|
237
795
|
if (!isMethodRetryable(req)) return next(req);
|
|
796
|
+
const remainingBudget = () => maxElapsedMs === void 0 ? Number.POSITIVE_INFINITY : maxElapsedMs - (performance.now() - startedAt);
|
|
238
797
|
return asyncFold(
|
|
239
798
|
next(req),
|
|
240
799
|
(e) => {
|
|
241
|
-
if (e._tag === "Abort" || e._tag === "BadUrl"
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
800
|
+
if (e._tag === "Abort" || e._tag === "BadUrl" || e._tag === "PoolRejected") {
|
|
801
|
+
dropPlanner(retryId);
|
|
802
|
+
return asyncFail(e);
|
|
803
|
+
}
|
|
804
|
+
const retryable = attempt < p.maxRetries && retryOnError(e) && remainingBudget() > 0;
|
|
805
|
+
const d = nextDelay(retryId, attempt, startedAt, retryable);
|
|
806
|
+
if (d === void 0 || d <= 0 && maxElapsedMs !== void 0) {
|
|
807
|
+
dropPlanner(retryId);
|
|
808
|
+
return asyncFail(e);
|
|
809
|
+
}
|
|
810
|
+
return asyncFlatMap(sleep(d), () => loop(req, attempt + 1, startedAt, retryId));
|
|
246
811
|
},
|
|
247
812
|
(w) => {
|
|
248
|
-
const
|
|
249
|
-
|
|
250
|
-
const
|
|
251
|
-
|
|
252
|
-
|
|
813
|
+
const retryable = attempt < p.maxRetries && retryOnStatus(w.status) && remainingBudget() > 0;
|
|
814
|
+
const ra = p.respectRetryAfter === false ? void 0 : retryAfterMs2(w.headers);
|
|
815
|
+
const d = nextDelay(retryId, attempt, startedAt, retryable, ra);
|
|
816
|
+
if (d === void 0 || d <= 0 && maxElapsedMs !== void 0) {
|
|
817
|
+
dropPlanner(retryId);
|
|
818
|
+
return asyncSucceed(w);
|
|
819
|
+
}
|
|
820
|
+
return asyncFlatMap(sleep(d), () => loop(req, attempt + 1, startedAt, retryId));
|
|
253
821
|
}
|
|
254
822
|
);
|
|
255
823
|
};
|
|
256
|
-
return (req) =>
|
|
824
|
+
return (req) => {
|
|
825
|
+
if (!isMethodRetryable(req)) return next(req);
|
|
826
|
+
const startedAt = performance.now();
|
|
827
|
+
const retryId = wasmPlanner?.start({
|
|
828
|
+
nowMs: startedAt,
|
|
829
|
+
maxRetries: p.maxRetries,
|
|
830
|
+
baseDelayMs: p.baseDelayMs,
|
|
831
|
+
maxDelayMs: p.maxDelayMs,
|
|
832
|
+
maxElapsedMs
|
|
833
|
+
});
|
|
834
|
+
return loop(req, 0, startedAt, retryId);
|
|
835
|
+
};
|
|
257
836
|
};
|
|
258
837
|
|
|
259
838
|
// src/http/httpClient.ts
|
|
@@ -269,9 +848,11 @@ var createHttpCore = (cfg = {}) => {
|
|
|
269
848
|
const withPromise = (eff) => withAsyncPromise((e, env) => toPromise(e, env))(eff);
|
|
270
849
|
const requestRaw = (req) => wire(req);
|
|
271
850
|
const splitInit = (init) => {
|
|
272
|
-
const { headers, ...rest } = init ?? {};
|
|
851
|
+
const { headers, timeoutMs, poolKey, ...rest } = init ?? {};
|
|
273
852
|
return {
|
|
274
853
|
headers: normalizeHeadersInit(headers),
|
|
854
|
+
timeoutMs: typeof timeoutMs === "number" ? timeoutMs : void 0,
|
|
855
|
+
poolKey: typeof poolKey === "string" ? poolKey : void 0,
|
|
275
856
|
init: rest
|
|
276
857
|
};
|
|
277
858
|
};
|
|
@@ -282,6 +863,8 @@ var createHttpCore = (cfg = {}) => {
|
|
|
282
863
|
method,
|
|
283
864
|
url,
|
|
284
865
|
...body && body.length > 0 ? { body } : {},
|
|
866
|
+
...s.timeoutMs !== void 0 ? { timeoutMs: s.timeoutMs } : {},
|
|
867
|
+
...s.poolKey !== void 0 ? { poolKey: s.poolKey } : {},
|
|
285
868
|
init: s.init
|
|
286
869
|
};
|
|
287
870
|
return applyInitHeaders(s.headers)(req);
|
|
@@ -337,7 +920,8 @@ function httpClient(cfg = {}) {
|
|
|
337
920
|
postJson,
|
|
338
921
|
with: (mw) => make(wire.with(mw)),
|
|
339
922
|
withRetry: (p) => make(wire.with(withRetry(p))),
|
|
340
|
-
wire
|
|
923
|
+
wire,
|
|
924
|
+
stats: () => wire.stats()
|
|
341
925
|
};
|
|
342
926
|
};
|
|
343
927
|
return make(core.wire);
|
|
@@ -435,12 +1019,92 @@ function httpClientStream(cfg = {}) {
|
|
|
435
1019
|
get: getStream,
|
|
436
1020
|
with: (mw) => make(mw(w)),
|
|
437
1021
|
withRetry: (p) => make(withRetryStream(p)(w)),
|
|
438
|
-
wire: w
|
|
1022
|
+
wire: w,
|
|
1023
|
+
stats: () => w.stats()
|
|
439
1024
|
};
|
|
440
1025
|
};
|
|
441
1026
|
return make(wire);
|
|
442
1027
|
}
|
|
1028
|
+
|
|
1029
|
+
// src/http/circuitBreaker.ts
|
|
1030
|
+
function withCircuitBreaker(config = {}) {
|
|
1031
|
+
if (config.perOrigin) {
|
|
1032
|
+
const breakers = /* @__PURE__ */ new Map();
|
|
1033
|
+
const getBreaker = (url) => {
|
|
1034
|
+
try {
|
|
1035
|
+
const origin = new URL(url).origin;
|
|
1036
|
+
if (!breakers.has(origin)) {
|
|
1037
|
+
breakers.set(origin, makeCircuitBreaker(config));
|
|
1038
|
+
}
|
|
1039
|
+
return breakers.get(origin);
|
|
1040
|
+
} catch {
|
|
1041
|
+
if (!breakers.has("__global__")) {
|
|
1042
|
+
breakers.set("__global__", makeCircuitBreaker(config));
|
|
1043
|
+
}
|
|
1044
|
+
return breakers.get("__global__");
|
|
1045
|
+
}
|
|
1046
|
+
};
|
|
1047
|
+
return (next) => (req) => {
|
|
1048
|
+
const breaker2 = getBreaker(req.url);
|
|
1049
|
+
return breaker2.protect(next(req));
|
|
1050
|
+
};
|
|
1051
|
+
}
|
|
1052
|
+
const breaker = makeCircuitBreaker({
|
|
1053
|
+
...config,
|
|
1054
|
+
isFailure: config.isFailure ?? ((e) => {
|
|
1055
|
+
const err = e;
|
|
1056
|
+
return err._tag !== "BadUrl" && err._tag !== "Abort";
|
|
1057
|
+
})
|
|
1058
|
+
});
|
|
1059
|
+
return (next) => (req) => {
|
|
1060
|
+
return breaker.protect(next(req));
|
|
1061
|
+
};
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
// src/http/tracing.ts
|
|
1065
|
+
function withTracing(tracer) {
|
|
1066
|
+
return (next) => (req) => {
|
|
1067
|
+
return tracer.span(
|
|
1068
|
+
`HTTP ${req.method} ${req.url}`,
|
|
1069
|
+
next(req),
|
|
1070
|
+
{
|
|
1071
|
+
"http.method": req.method,
|
|
1072
|
+
"http.url": req.url,
|
|
1073
|
+
...req.headers?.["content-type"] ? { "http.content_type": req.headers["content-type"] } : {}
|
|
1074
|
+
}
|
|
1075
|
+
);
|
|
1076
|
+
};
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
// src/http/validation.ts
|
|
1080
|
+
function validatedJson(client, validator) {
|
|
1081
|
+
return (req) => asyncFold(
|
|
1082
|
+
client(req),
|
|
1083
|
+
(error) => asyncFail(error),
|
|
1084
|
+
(response) => {
|
|
1085
|
+
try {
|
|
1086
|
+
const parsed = JSON.parse(response.bodyText);
|
|
1087
|
+
const result = validator(parsed);
|
|
1088
|
+
if (result.success) {
|
|
1089
|
+
return asyncSucceed(result.data);
|
|
1090
|
+
}
|
|
1091
|
+
return asyncFail({
|
|
1092
|
+
_tag: "ValidationError",
|
|
1093
|
+
message: result.error,
|
|
1094
|
+
body: response.bodyText
|
|
1095
|
+
});
|
|
1096
|
+
} catch (e) {
|
|
1097
|
+
return asyncFail({
|
|
1098
|
+
_tag: "ValidationError",
|
|
1099
|
+
message: `JSON parse error: ${String(e)}`,
|
|
1100
|
+
body: response.bodyText
|
|
1101
|
+
});
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
);
|
|
1105
|
+
}
|
|
443
1106
|
export {
|
|
1107
|
+
HttpConcurrencyPool,
|
|
444
1108
|
decorate,
|
|
445
1109
|
httpClient,
|
|
446
1110
|
httpClientStream,
|
|
@@ -448,6 +1112,10 @@ export {
|
|
|
448
1112
|
makeHttp,
|
|
449
1113
|
makeHttpStream,
|
|
450
1114
|
normalizeHeadersInit,
|
|
1115
|
+
resolveHttpPoolKey,
|
|
1116
|
+
validatedJson,
|
|
1117
|
+
withCircuitBreaker,
|
|
451
1118
|
withMiddleware,
|
|
452
|
-
withRetryStream
|
|
1119
|
+
withRetryStream,
|
|
1120
|
+
withTracing
|
|
453
1121
|
};
|