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.
Files changed (36) hide show
  1. package/dist/agent/cli/main.cjs +43 -43
  2. package/dist/agent/cli/main.js +2 -2
  3. package/dist/agent/cli/main.mjs +2 -2
  4. package/dist/agent/index.cjs +3 -3
  5. package/dist/agent/index.d.ts +1 -1
  6. package/dist/agent/index.js +2 -2
  7. package/dist/agent/index.mjs +2 -2
  8. package/dist/chunk-4N2JEK4H.mjs +3897 -0
  9. package/dist/chunk-BKBFSOGT.cjs +3897 -0
  10. package/dist/{chunk-XNOTJSMZ.mjs → chunk-BMRF4FN6.js} +268 -8
  11. package/dist/chunk-JT7D6M5H.js +3897 -0
  12. package/dist/{chunk-3R7ZYRK2.mjs → chunk-MQF7HZ7Y.mjs} +1 -1
  13. package/dist/chunk-SKVY72E5.cjs +667 -0
  14. package/dist/{chunk-ATHSSDUF.js → chunk-UWMMYKVK.mjs} +268 -8
  15. package/dist/{chunk-INZBKOHY.js → chunk-WJESVBWN.js} +1 -1
  16. package/dist/{chunk-XDINDYNA.cjs → chunk-XTMZTVIT.cjs} +134 -134
  17. package/dist/{effect-ISvXPLgc.d.ts → effect-DM56H743.d.ts} +191 -21
  18. package/dist/http/index.cjs +808 -140
  19. package/dist/http/index.d.ts +181 -8
  20. package/dist/http/index.js +793 -125
  21. package/dist/http/index.mjs +793 -125
  22. package/dist/index.cjs +1785 -137
  23. package/dist/index.d.ts +979 -36
  24. package/dist/index.js +1675 -27
  25. package/dist/index.mjs +1675 -27
  26. package/dist/stream-Oqe6WeLE.d.ts +173 -0
  27. package/package.json +1 -1
  28. package/wasm/pkg/brass_runtime_wasm_engine.d.ts +95 -16
  29. package/wasm/pkg/brass_runtime_wasm_engine.js +715 -15
  30. package/wasm/pkg/brass_runtime_wasm_engine_bg.wasm +0 -0
  31. package/wasm/pkg/brass_runtime_wasm_engine_bg.wasm.d.ts +78 -7
  32. package/dist/chunk-2P4PD6D7.cjs +0 -2557
  33. package/dist/chunk-7F2R7A2V.mjs +0 -2557
  34. package/dist/chunk-L6KKKM66.js +0 -2557
  35. package/dist/chunk-ZTDK2DLG.cjs +0 -407
  36. package/dist/stream-BvukHxCv.d.ts +0 -66
@@ -1,6 +1,8 @@
1
1
  import {
2
+ makeCircuitBreaker,
3
+ sleep,
2
4
  streamFromReadableStream
3
- } from "../chunk-XNOTJSMZ.mjs";
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-7F2R7A2V.mjs";
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/sleep.ts
43
- var isHttpError = (e) => typeof e === "object" && e !== null && "_tag" in e;
44
- var normalizeHttpError = (e) => {
45
- if (isHttpError(e)) return e;
46
- if (typeof e === "object" && e !== null && e.name === "AbortError") {
47
- return { _tag: "Abort" };
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
- return { _tag: "FetchError", message: String(e) };
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 sleepMs = (ms) => fromPromiseAbortable(
52
- (signal) => new Promise((resolve, reject) => {
53
- if (signal.aborted) return reject({ _tag: "Abort" });
54
- const id = setTimeout(resolve, ms);
55
- const onAbort = () => {
56
- clearTimeout(id);
57
- reject({ _tag: "Abort" });
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
- signal.addEventListener("abort", onAbort, { once: true });
60
- }),
61
- normalizeHttpError
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 withMiddleware = (mw) => (c) => decorate(mw(c));
66
- var decorate = (run) => Object.assign(((req) => run(req)), {
67
- with: (mw) => decorate(mw(run))
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 normalizeHttpError2 = (e) => {
70
- if (e instanceof DOMException && e.name === "AbortError") return { _tag: "Abort" };
71
- if (typeof e === "object" && e && "_tag" in e) return e;
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
- return (req0) => fromPromiseAbortable(
98
- async (signal) => {
99
- const req = normalize(req0);
100
- let url;
101
- try {
102
- url = new URL(req.url, baseUrl);
103
- } catch {
104
- throw { _tag: "BadUrl", message: `URL inv\xE1lida: ${req.url}` };
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
- const started = performance.now();
107
- const res = await fetch(url, {
108
- ...req.init ?? {},
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 run = (req0) => fromPromiseAbortable(
134
- async (signal) => {
135
- const req = normalize(req0);
136
- let url;
137
- try {
138
- url = new URL(req.url, baseUrl);
139
- } catch {
140
- throw { _tag: "BadUrl", message: `URL inv\xE1lida: ${req.url}` };
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
- const started = performance.now();
143
- const res = await fetch(url, {
144
- ...req.init ?? {},
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) => ((req) => {
186
- const loop = (attempt) => asyncFold(
187
- next(req),
188
- (e) => {
189
- if (e._tag === "Abort" || e._tag === "BadUrl") return asyncFail(e);
190
- const canRetry = attempt < p.maxRetries && (p.retryOnError ?? defaultRetryOnError)(e);
191
- if (!canRetry) return asyncFail(e);
192
- const d = backoffDelayMs(attempt, p.baseDelayMs, p.maxDelayMs);
193
- return asyncFlatMap(sleepMs(d), () => loop(attempt + 1));
194
- },
195
- (w) => {
196
- const canRetry = attempt < p.maxRetries && (p.retryOnStatus ?? defaultRetryOnStatus)(w.status);
197
- if (!canRetry) return asyncSucceed(w);
198
- const ra = retryAfterMs(w.headers);
199
- const d = ra ?? backoffDelayMs(attempt, p.baseDelayMs, p.maxDelayMs);
200
- return asyncFlatMap(sleepMs(d), () => loop(attempt + 1));
201
- }
202
- );
203
- return loop(0);
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 loop = (req, attempt) => {
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") return asyncFail(e);
242
- const canRetry = attempt < p.maxRetries && retryOnError(e);
243
- if (!canRetry) return asyncFail(e);
244
- const d = backoffDelayMs2(attempt, p.baseDelayMs, p.maxDelayMs);
245
- return asyncFlatMap(sleepMs(d), () => loop(req, attempt + 1));
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 canRetry = attempt < p.maxRetries && retryOnStatus(w.status);
249
- if (!canRetry) return asyncSucceed(w);
250
- const ra = retryAfterMs2(w.headers);
251
- const d = ra ?? backoffDelayMs2(attempt, p.baseDelayMs, p.maxDelayMs);
252
- return asyncFlatMap(sleepMs(d), () => loop(req, attempt + 1));
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) => loop(req, 0);
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
  };