@blamejs/core 0.4.15 → 0.4.16

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
@@ -8,6 +8,7 @@ upgrading across more than a few patches at a time.
8
8
 
9
9
  ## v0.4.x
10
10
 
11
+ - **0.4.15** (2026-04-30) — b.httpClient: redirect-following + outbound multipart
11
12
  - **0.4.14** (2026-04-30) — b.i18n: lazy locales + ordinal plurals + onMissingKey hook
12
13
  - **0.4.13** (2026-04-30) — b.db: streaming query results
13
14
  - **0.4.12** (2026-04-30) — b.log: multi-sink output with per-sink level filtering
@@ -375,6 +375,50 @@ function request(opts) {
375
375
  return Promise.reject(_makeError(opts && opts.errorClass, "BAD_ARG", "url is required", true));
376
376
  }
377
377
 
378
+ // Validate before/after shapes early — Tier-A throw if the operator
379
+ // passed something un-callable so the bug surfaces at the call site
380
+ // rather than inside the request loop.
381
+ if (opts.before !== undefined) {
382
+ if (!Array.isArray(opts.before) || !opts.before.every(function (f) { return typeof f === "function"; })) {
383
+ return Promise.reject(_makeError(opts.errorClass, "BAD_ARG",
384
+ "before must be an array of functions", true));
385
+ }
386
+ }
387
+ if (opts.after !== undefined) {
388
+ if (!Array.isArray(opts.after) || !opts.after.every(function (f) { return typeof f === "function"; })) {
389
+ return Promise.reject(_makeError(opts.errorClass, "BAD_ARG",
390
+ "after must be an array of functions", true));
391
+ }
392
+ }
393
+ if (opts.onUploadProgress !== undefined && typeof opts.onUploadProgress !== "function") {
394
+ return Promise.reject(_makeError(opts.errorClass, "BAD_ARG",
395
+ "onUploadProgress must be a function", true));
396
+ }
397
+ if (opts.onDownloadProgress !== undefined && typeof opts.onDownloadProgress !== "function") {
398
+ return Promise.reject(_makeError(opts.errorClass, "BAD_ARG",
399
+ "onDownloadProgress must be a function", true));
400
+ }
401
+
402
+ // before interceptors — run in array order. Each may return a modified
403
+ // opts object (or return nothing to leave the running opts as-is).
404
+ // Caller-set defaults / observability / auth header injection lands
405
+ // here. Synchronous to keep the request hot path simple; async
406
+ // pre-flight work (e.g. token refresh) belongs in the route handler
407
+ // before httpClient.request is even called.
408
+ if (Array.isArray(opts.before) && opts.before.length > 0) {
409
+ var working = opts;
410
+ for (var bi = 0; bi < opts.before.length; bi++) {
411
+ var ret;
412
+ try { ret = opts.before[bi](working); }
413
+ catch (e) {
414
+ return Promise.reject(_makeError(opts.errorClass, "BEFORE_THREW",
415
+ "before[" + bi + "] threw: " + ((e && e.message) || String(e)), true));
416
+ }
417
+ if (ret && typeof ret === "object") working = ret;
418
+ }
419
+ opts = working;
420
+ }
421
+
378
422
  // Multipart shorthand: { multipart: { fields, files } } expands to
379
423
  // body + Content-Type with the boundary parameter. Mutually exclusive
380
424
  // with caller-supplied body / Content-Type.
@@ -413,11 +457,23 @@ function request(opts) {
413
457
  "maxRedirects must be a non-negative integer or null", true));
414
458
  }
415
459
  }
460
+ var afterChain = (Array.isArray(opts.after) && opts.after.length > 0) ? opts.after : null;
461
+ function _runAfter(finalOpts, res) {
462
+ if (!afterChain) return res;
463
+ for (var ai = 0; ai < afterChain.length; ai++) {
464
+ try { afterChain[ai](finalOpts, res); }
465
+ catch (_e) { /* after hooks are best-effort — never break the response */ }
466
+ }
467
+ return res;
468
+ }
469
+
416
470
  if (maxRedirects === null || maxRedirects === 0) {
417
- return _requestSingle(opts);
471
+ return _requestSingle(opts).then(function (res) { return _runAfter(opts, res); });
418
472
  }
419
473
 
420
- return _requestWithRedirects(opts, maxRedirects);
474
+ return _requestWithRedirects(opts, maxRedirects).then(function (boxed) {
475
+ return _runAfter(boxed.finalOpts, boxed.res);
476
+ });
421
477
  }
422
478
 
423
479
  function _requestWithRedirects(opts, hopsLeft) {
@@ -431,9 +487,11 @@ function _requestWithRedirects(opts, hopsLeft) {
431
487
  var current = Object.assign({}, opts, { _resolveOnRedirect: true });
432
488
  function _follow() {
433
489
  return _requestSingle(current).then(function (res) {
434
- if (!REDIRECT_STATUSES.has(res.statusCode) || hopsLeft <= 0) return res;
490
+ if (!REDIRECT_STATUSES.has(res.statusCode) || hopsLeft <= 0) {
491
+ return { finalOpts: current, res: res };
492
+ }
435
493
  var loc = res.headers && (res.headers.location || res.headers.Location);
436
- if (!loc) return res; // 3xx with no Location — operator handles
494
+ if (!loc) return { finalOpts: current, res: res }; // 3xx with no Location — operator handles
437
495
  hopsLeft -= 1;
438
496
 
439
497
  // Resolve relative Location against the just-fetched URL (the URL
@@ -562,9 +620,26 @@ function _requestH1(transport, u, opts) {
562
620
  function _resolve(value) { if (!settled) { settled = true; resolve(value); } }
563
621
  function _reject(err) { if (!settled) { settled = true; reject(err); } }
564
622
 
623
+ var onUploadProgress = typeof opts.onUploadProgress === "function" ? opts.onUploadProgress : null;
624
+ var onDownloadProgress = typeof opts.onDownloadProgress === "function" ? opts.onDownloadProgress : null;
625
+
565
626
  var req = transport.lib.request(reqOpts, function (res) {
566
627
  if (observer) observer("response:headers", { statusCode: res.statusCode, headers: res.headers });
567
628
 
629
+ // Download total: Content-Length when present, null otherwise.
630
+ var dlTotal = null;
631
+ if (res.headers && typeof res.headers["content-length"] === "string") {
632
+ var cl = parseInt(res.headers["content-length"], 10);
633
+ if (!isNaN(cl) && cl >= 0) dlTotal = cl;
634
+ }
635
+ var dlLoaded = 0;
636
+ function _emitDownload(chunkBytes) {
637
+ if (!onDownloadProgress) return;
638
+ dlLoaded += chunkBytes;
639
+ try { onDownloadProgress({ loaded: dlLoaded, total: dlTotal }); }
640
+ catch (_e) { /* progress hooks are best-effort */ }
641
+ }
642
+
568
643
  if (responseMode === "stream") {
569
644
  if (res.statusCode >= 400) {
570
645
  res.resume();
@@ -572,6 +647,17 @@ function _requestH1(transport, u, opts) {
572
647
  "HTTP " + res.statusCode + " " + (res.statusMessage || ""),
573
648
  _isPermanentStatus(res.statusCode), res.statusCode));
574
649
  }
650
+ if (onDownloadProgress) {
651
+ // Wrap the stream so chunks emit progress to the operator.
652
+ // The framework's contract is to hand back the response stream
653
+ // unmodified; fix-up via a passthrough keeps that contract while
654
+ // observing the chunk sizes.
655
+ var passthrough = new (require("node:stream").PassThrough)();
656
+ res.on("data", function (chunk) { _emitDownload(chunk.length); passthrough.write(chunk); });
657
+ res.on("end", function () { passthrough.end(); });
658
+ res.on("error", function (e) { passthrough.destroy(e); });
659
+ return _resolve({ statusCode: res.statusCode, headers: res.headers, body: passthrough });
660
+ }
575
661
  return _resolve({ statusCode: res.statusCode, headers: res.headers, body: res });
576
662
  }
577
663
 
@@ -586,7 +672,9 @@ function _requestH1(transport, u, opts) {
586
672
  req.destroy();
587
673
  _reject(_makeError(opts.errorClass, "RESPONSE_TOO_LARGE",
588
674
  "response body exceeds " + maxResponseBytes + " bytes", true));
675
+ return;
589
676
  }
677
+ _emitDownload(chunk.length);
590
678
  });
591
679
  res.on("end", function () {
592
680
  if (capExceeded) return;
@@ -639,17 +727,47 @@ function _requestH1(transport, u, opts) {
639
727
  signal.addEventListener("abort", onAbort, { once: true });
640
728
  }
641
729
 
730
+ // Upload progress: emit { loaded, total } as body bytes go to the
731
+ // wire. Buffer / string bodies are sliced into chunks ourselves so
732
+ // operators see incremental progress; Readable bodies emit on each
733
+ // 'data' event from the source stream.
734
+ var ulTotal = null;
735
+ if (Buffer.isBuffer(opts.body)) ulTotal = opts.body.length;
736
+ else if (typeof opts.body === "string") ulTotal = Buffer.byteLength(opts.body, "utf8");
737
+ var ulLoaded = 0;
738
+ function _emitUpload(chunkBytes) {
739
+ if (!onUploadProgress) return;
740
+ ulLoaded += chunkBytes;
741
+ try { onUploadProgress({ loaded: ulLoaded, total: ulTotal }); }
742
+ catch (_e) { /* progress hooks are best-effort */ }
743
+ }
744
+
642
745
  if (opts.body && typeof opts.body.pipe === "function") {
746
+ if (onUploadProgress) {
747
+ opts.body.on("data", function (c) { _emitUpload(c.length); });
748
+ }
643
749
  opts.body.on("error", function (e) {
644
750
  try { req.destroy(); } catch (_) {}
645
751
  _reject(_makeError(opts.errorClass, "REQ_BODY_ERROR",
646
752
  "request body stream error: " + e.message, false));
647
753
  });
648
754
  opts.body.pipe(req);
649
- } else if (Buffer.isBuffer(opts.body)) {
650
- req.end(opts.body);
651
- } else if (typeof opts.body === "string") {
652
- req.end(Buffer.from(opts.body, "utf8"));
755
+ } else if (Buffer.isBuffer(opts.body) || typeof opts.body === "string") {
756
+ var bodyBuf = Buffer.isBuffer(opts.body) ? opts.body : Buffer.from(opts.body, "utf8");
757
+ if (onUploadProgress) {
758
+ // Chunked write so progress reports land before req.end().
759
+ var CHUNK = 64 * 1024;
760
+ var off = 0;
761
+ while (off < bodyBuf.length) {
762
+ var slice = bodyBuf.slice(off, Math.min(off + CHUNK, bodyBuf.length));
763
+ req.write(slice);
764
+ _emitUpload(slice.length);
765
+ off += slice.length;
766
+ }
767
+ req.end();
768
+ } else {
769
+ req.end(bodyBuf);
770
+ }
653
771
  } else {
654
772
  req.end();
655
773
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/core",
3
- "version": "0.4.15",
3
+ "version": "0.4.16",
4
4
  "description": "The Node framework that owns its stack.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "blamejs contributors",