brass-runtime 1.19.1 → 1.19.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,39 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.18.3 - HTTP Pool/Timeout Fast Path
4
+
5
+ ### Performance
6
+
7
+ - Connected the non-streaming HTTP wire client to the existing `runPoolTransport`
8
+ fast path whenever a request uses `pool`, `adaptiveLimiter`, or `timeoutMs`.
9
+ The uncontended pool path now uses synchronous `tryAcquireSync` before falling
10
+ back to queued async acquire, avoiding the generic `fromPromiseAbortable`
11
+ wrapper and extra promise/microtask boundaries on hot BFF/proxy paths.
12
+ - Improved local HTTP overhead benchmark results for the proxy effect transport
13
+ path with 30k calls, 5k warmup, concurrency 32:
14
+ - `default-proxy-effect-timeout`: p99 `1.915ms` -> `1.559ms`,
15
+ throughput `70.9k/s` -> `101.5k/s`.
16
+ - `default-proxy-effect-pool`: p99 `1.564ms` -> `1.276ms`,
17
+ throughput `79.2k/s` -> `115.7k/s`.
18
+ - `default-proxy-effect-timeout-pool`: p99 `2.014ms` -> `1.209ms`,
19
+ throughput `70.2k/s` -> `103.1k/s`.
20
+
21
+ ### Fixed
22
+
23
+ - Preserved real host-request cancellation on the pool/timeout fast path by
24
+ passing transports a request-scoped `AbortController` signal. Promise
25
+ transports such as Axios can now observe aborts on cancellation and timeout
26
+ while still benefiting from the pool fast path.
27
+ - Added regression coverage ensuring uncontended pool transports run during
28
+ effect registration and that cancellation aborts the signal passed to the
29
+ transport.
30
+
31
+ ### Validation
32
+
33
+ - `npm run test:types`
34
+ - `npm test -- src/http/__tests__`
35
+ - `BRASS_HTTP_OVERHEAD_CALLS=30000 BRASS_HTTP_OVERHEAD_WARMUP_CALLS=5000 BRASS_HTTP_OVERHEAD_CONCURRENCY=32 BRASS_HTTP_OVERHEAD_VARIANTS=default-proxy-effect-transport,default-proxy-effect-timeout,default-proxy-effect-pool,default-proxy-effect-timeout-pool npm run benchmark:http:overhead`
36
+
3
37
  ## 1.18.2 - Release Metadata Alignment
4
38
 
5
39
  ### Fixed
@@ -12,6 +12,8 @@ import {
12
12
  } from "./chunk-XRRC5BBM.mjs";
13
13
  import {
14
14
  fromPromiseAbortable,
15
+ recordAbortablePromiseFinish,
16
+ recordAbortablePromiseStart,
15
17
  resolveWasmModule,
16
18
  toPromise
17
19
  } from "./chunk-BFNBU47C.mjs";
@@ -3182,6 +3184,275 @@ var noopSignal = (() => {
3182
3184
  globalThis.__brassNoopSignal = s;
3183
3185
  return s;
3184
3186
  })();
3187
+ var PoolRequestState = class {
3188
+ constructor(req, url, transport, metrics, pool, adaptiveLimiter, timerWheel, timeoutMs, env, cb, previousSignal, controller, signal, label, startedAt) {
3189
+ this.req = req;
3190
+ this.url = url;
3191
+ this.transport = transport;
3192
+ this.metrics = metrics;
3193
+ this.pool = pool;
3194
+ this.adaptiveLimiter = adaptiveLimiter;
3195
+ this.timerWheel = timerWheel;
3196
+ this.timeoutMs = timeoutMs;
3197
+ this.env = env;
3198
+ this.cb = cb;
3199
+ this.previousSignal = previousSignal;
3200
+ this.controller = controller;
3201
+ this.signal = signal;
3202
+ this.label = label;
3203
+ this.startedAt = startedAt;
3204
+ }
3205
+ req;
3206
+ url;
3207
+ transport;
3208
+ metrics;
3209
+ pool;
3210
+ adaptiveLimiter;
3211
+ timerWheel;
3212
+ timeoutMs;
3213
+ env;
3214
+ cb;
3215
+ previousSignal;
3216
+ controller;
3217
+ signal;
3218
+ label;
3219
+ startedAt;
3220
+ done = false;
3221
+ cancelInner = void 0;
3222
+ timerHandle = void 0;
3223
+ timeoutHandle = void 0;
3224
+ lease = void 0;
3225
+ finish(outcome, exit, error) {
3226
+ if (this.done) return;
3227
+ this.done = true;
3228
+ if (this.timerHandle) {
3229
+ this.timerHandle.cancel();
3230
+ this.timerHandle = void 0;
3231
+ }
3232
+ if (this.timeoutHandle !== void 0) {
3233
+ clearTimeout(this.timeoutHandle);
3234
+ this.timeoutHandle = void 0;
3235
+ }
3236
+ if (this.previousSignal) this.previousSignal.removeEventListener("abort", this.boundAbortFromPrevious);
3237
+ this.cancelInner = void 0;
3238
+ recordAbortablePromiseFinish(this.label, outcome);
3239
+ this.metrics.onFinish({
3240
+ label: this.label,
3241
+ outcome,
3242
+ durationMs: Math.round(performance.now() - this.startedAt),
3243
+ error
3244
+ });
3245
+ this.cb(exit);
3246
+ }
3247
+ finishFailure(error, outcome = "failure") {
3248
+ this.finish(outcome, { _tag: "Failure", cause: Cause.fail(error) }, error);
3249
+ }
3250
+ abortCurrent(reason) {
3251
+ if (!this.controller) return;
3252
+ try {
3253
+ this.controller.abort(reason);
3254
+ } catch {
3255
+ this.controller.abort();
3256
+ }
3257
+ }
3258
+ /**
3259
+ * Bound reference for addEventListener/removeEventListener.
3260
+ * Only allocated when previousSignal is present (not on the hot path).
3261
+ */
3262
+ boundAbortFromPrevious = void 0;
3263
+ abortFromPrevious() {
3264
+ const error = this.previousSignal ? abortErrorForSignal(this.previousSignal) : { _tag: "Abort" };
3265
+ this.abortCurrent(this.previousSignal?.reason);
3266
+ const cancel = this.cancelInner;
3267
+ this.cancelInner = void 0;
3268
+ if (this.lease) {
3269
+ releaseFailure(this.lease, this.adaptiveLimiter);
3270
+ this.lease = void 0;
3271
+ }
3272
+ queueMicrotask(() => {
3273
+ this.finishFailure(error);
3274
+ cancel?.();
3275
+ });
3276
+ }
3277
+ runTransportInner() {
3278
+ const { transport, req, url, signal, env } = this;
3279
+ try {
3280
+ const effect = transport({ request: req, url, signal });
3281
+ if (effect._tag === "Succeed") {
3282
+ if (this.lease) releaseSuccess(this.lease, this.adaptiveLimiter, effect.value);
3283
+ this.finish("success", { _tag: "Success", value: effect.value });
3284
+ } else if (effect._tag === "Fail") {
3285
+ if (this.lease) releaseFailure(this.lease, this.adaptiveLimiter);
3286
+ this.finishFailure(effect.error);
3287
+ } else if (effect._tag === "Async") {
3288
+ try {
3289
+ const cancel = effect.register(env, (exit) => {
3290
+ if (this.done) return;
3291
+ this.cancelInner = void 0;
3292
+ if (exit._tag === "Success") {
3293
+ if (this.lease) releaseSuccess(this.lease, this.adaptiveLimiter, exit.value);
3294
+ this.finish("success", exit);
3295
+ } else {
3296
+ if (this.lease) releaseFailure(this.lease, this.adaptiveLimiter);
3297
+ this.finish("failure", exit, exitError(exit));
3298
+ }
3299
+ });
3300
+ if (!this.done) this.cancelInner = typeof cancel === "function" ? cancel : void 0;
3301
+ } catch (error) {
3302
+ if (this.lease) releaseFailure(this.lease, this.adaptiveLimiter);
3303
+ this.finishFailure(normalizeHttpError(error));
3304
+ }
3305
+ } else {
3306
+ const innerCancel = registerHttpEffect(
3307
+ effect,
3308
+ env,
3309
+ (exit) => {
3310
+ if (this.done) return;
3311
+ this.cancelInner = void 0;
3312
+ if (exit._tag === "Success") {
3313
+ if (this.lease) releaseSuccess(this.lease, this.adaptiveLimiter, exit.value);
3314
+ this.finish("success", exit);
3315
+ } else {
3316
+ if (this.lease) releaseFailure(this.lease, this.adaptiveLimiter);
3317
+ this.finish("failure", exit, exitError(exit));
3318
+ }
3319
+ }
3320
+ );
3321
+ if (!this.done) this.cancelInner = innerCancel;
3322
+ }
3323
+ } catch (error) {
3324
+ if (this.lease) releaseFailure(this.lease, this.adaptiveLimiter);
3325
+ this.finishFailure(normalizeHttpError(error));
3326
+ }
3327
+ }
3328
+ };
3329
+ var runPoolTransport = (req, url, transport, metrics, pool, adaptiveLimiter, timerWheel, timeoutMs) => ({
3330
+ _tag: "Async",
3331
+ register: (env, cb) => {
3332
+ const previousSignal = req.init?.signal;
3333
+ const controller = new AbortController();
3334
+ const signal = controller.signal;
3335
+ const label = fetchLabel(req, url);
3336
+ const startedAt = performance.now();
3337
+ const state = new PoolRequestState(
3338
+ req,
3339
+ url,
3340
+ transport,
3341
+ metrics,
3342
+ pool,
3343
+ adaptiveLimiter,
3344
+ timerWheel,
3345
+ timeoutMs,
3346
+ env,
3347
+ cb,
3348
+ previousSignal,
3349
+ controller,
3350
+ signal,
3351
+ label,
3352
+ startedAt
3353
+ );
3354
+ recordAbortablePromiseStart(label);
3355
+ metrics.onStart();
3356
+ if (previousSignal) {
3357
+ state.boundAbortFromPrevious = () => state.abortFromPrevious();
3358
+ if (previousSignal.aborted) {
3359
+ state.abortFromPrevious();
3360
+ return () => void 0;
3361
+ }
3362
+ previousSignal.addEventListener("abort", state.boundAbortFromPrevious, ONCE_OPTIONS);
3363
+ }
3364
+ if (timeoutMs !== void 0 && timeoutMs > 0) {
3365
+ const onTimeout = () => {
3366
+ const reason = timeoutReason(req, url, timeoutMs);
3367
+ if (state.lease) {
3368
+ releaseFailure(state.lease, adaptiveLimiter);
3369
+ state.lease = void 0;
3370
+ }
3371
+ const cancel = state.cancelInner;
3372
+ state.cancelInner = void 0;
3373
+ state.finishFailure(reason, "timeout");
3374
+ state.abortCurrent(reason);
3375
+ cancel?.();
3376
+ };
3377
+ if (timerWheel) {
3378
+ state.timerHandle = timerWheel.schedule(timeoutMs, onTimeout, startedAt);
3379
+ } else {
3380
+ state.timeoutHandle = setTimeout(onTimeout, timeoutMs);
3381
+ }
3382
+ }
3383
+ if (adaptiveLimiter) {
3384
+ const key = resolveHttpPoolKey(adaptiveLimiter.keyResolver, req, url);
3385
+ const syncLease = adaptiveLimiter.tryAcquireSync(key, signal);
3386
+ if (syncLease) {
3387
+ state.lease = syncLease;
3388
+ } else {
3389
+ adaptiveLimiter.acquire(key, signal, { priority: requestPriority(req) }).then(
3390
+ (asyncLease) => {
3391
+ if (state.done) {
3392
+ asyncLease.release(0);
3393
+ return;
3394
+ }
3395
+ state.lease = asyncLease;
3396
+ state.runTransportInner();
3397
+ },
3398
+ (err) => {
3399
+ if (state.done) return;
3400
+ state.finishFailure(normalizeHttpError(err));
3401
+ }
3402
+ );
3403
+ return () => {
3404
+ if (state.done) return;
3405
+ const cancel = state.cancelInner;
3406
+ state.cancelInner = void 0;
3407
+ state.abortCurrent();
3408
+ if (state.lease) releaseFailure(state.lease, adaptiveLimiter);
3409
+ state.finish("interrupt", { _tag: "Failure", cause: Cause.interrupt() });
3410
+ cancel?.();
3411
+ };
3412
+ }
3413
+ } else if (pool) {
3414
+ const key = resolveHttpPoolKey(pool.keyResolver, req, url);
3415
+ const syncLease = pool.tryAcquireSync(key, signal);
3416
+ if (syncLease) {
3417
+ state.lease = syncLease;
3418
+ } else {
3419
+ pool.acquire(key, signal).then(
3420
+ (asyncLease) => {
3421
+ if (state.done) {
3422
+ asyncLease.release();
3423
+ return;
3424
+ }
3425
+ state.lease = asyncLease;
3426
+ state.runTransportInner();
3427
+ },
3428
+ (err) => {
3429
+ if (state.done) return;
3430
+ state.finishFailure(normalizeHttpError(err));
3431
+ }
3432
+ );
3433
+ return () => {
3434
+ if (state.done) return;
3435
+ const cancel = state.cancelInner;
3436
+ state.cancelInner = void 0;
3437
+ state.abortCurrent();
3438
+ if (state.lease) releaseFailure(state.lease, void 0);
3439
+ state.finish("interrupt", { _tag: "Failure", cause: Cause.interrupt() });
3440
+ cancel?.();
3441
+ };
3442
+ }
3443
+ }
3444
+ state.runTransportInner();
3445
+ return () => {
3446
+ if (state.done) return;
3447
+ const cancel = state.cancelInner;
3448
+ state.cancelInner = void 0;
3449
+ state.abortCurrent();
3450
+ if (state.lease) releaseFailure(state.lease, adaptiveLimiter);
3451
+ state.finish("interrupt", { _tag: "Failure", cause: Cause.interrupt() });
3452
+ cancel?.();
3453
+ };
3454
+ }
3455
+ });
3185
3456
  function makeHttp(cfg = {}) {
3186
3457
  validateMakeHttpConfig(cfg);
3187
3458
  const baseUrl = cfg.baseUrl ?? "";
@@ -3200,42 +3471,7 @@ function makeHttp(cfg = {}) {
3200
3471
  if (!adaptiveLimiter && !pool && timeoutMs === void 0) {
3201
3472
  return runDirectTransport(req, url, transport, metrics);
3202
3473
  }
3203
- return fromPromiseAbortable(
3204
- async (signal, env) => {
3205
- let lease;
3206
- const linkedSignal = linkAbortSignals(signal, req.init?.signal);
3207
- try {
3208
- if (linkedSignal.signal.aborted) throw abortErrorForSignal(linkedSignal.signal);
3209
- if (adaptiveLimiter) {
3210
- const key = resolveHttpPoolKey(adaptiveLimiter.keyResolver, req, url);
3211
- lease = await adaptiveLimiter.acquire(key, linkedSignal.signal, { priority: requestPriority(req) });
3212
- } else if (pool) {
3213
- const key = resolveHttpPoolKey(pool.keyResolver, req, url);
3214
- lease = await pool.acquire(key, linkedSignal.signal);
3215
- }
3216
- const response = await runTransportEffect(
3217
- transport({ request: req, url, signal: linkedSignal.signal }),
3218
- env,
3219
- linkedSignal.signal
3220
- );
3221
- lease = releaseSuccess(lease, adaptiveLimiter, response);
3222
- return response;
3223
- } finally {
3224
- linkedSignal.cleanup();
3225
- if (lease) {
3226
- releaseFailure(lease, adaptiveLimiter);
3227
- }
3228
- }
3229
- },
3230
- normalizeHttpError,
3231
- {
3232
- label: fetchLabel(req, url),
3233
- timeoutMs,
3234
- timeoutReason: timeoutMs ? () => timeoutReason(req, url, timeoutMs) : void 0,
3235
- onStart: metrics.onStart,
3236
- onFinish: metrics.onFinish
3237
- }
3238
- );
3474
+ return runPoolTransport(req, url, transport, metrics, pool, adaptiveLimiter, void 0, timeoutMs);
3239
3475
  };
3240
3476
  const metadata = {};
3241
3477
  if (adaptiveLimiter) metadata.adaptiveLimiter = adaptiveLimiter;
@@ -4,7 +4,7 @@ import {
4
4
  } from "./chunk-RZQK32C6.js";
5
5
  import {
6
6
  withHttpObservability
7
- } from "./chunk-XJSWDU2S.js";
7
+ } from "./chunk-BHX4MD35.js";
8
8
  import {
9
9
  makeObservability
10
10
  } from "./chunk-TAPB4NN5.js";
@@ -14,7 +14,7 @@ import {
14
14
  import {
15
15
  makeDefaultHttpClient,
16
16
  makeHttp
17
- } from "./chunk-IHXF6NQG.js";
17
+ } from "./chunk-HKLZJ5UK.js";
18
18
  import {
19
19
  Runtime,
20
20
  Scheduler
@@ -20,7 +20,7 @@ import {
20
20
  httpErrorStatus,
21
21
  isRetryableHttpError,
22
22
  makeDefaultHttpClient
23
- } from "./chunk-IHXF6NQG.js";
23
+ } from "./chunk-HKLZJ5UK.js";
24
24
  import {
25
25
  registerHttpEffect
26
26
  } from "./chunk-TRM4JUZQ.js";