@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 +1 -0
- package/lib/http-client.js +126 -8
- package/package.json +1 -1
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
|
package/lib/http-client.js
CHANGED
|
@@ -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)
|
|
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;
|
|
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
|
-
|
|
651
|
-
|
|
652
|
-
|
|
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
|
}
|