@blamejs/core 0.4.12 → 0.4.14

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,8 @@ upgrading across more than a few patches at a time.
8
8
 
9
9
  ## v0.4.x
10
10
 
11
+ - **0.4.13** (2026-04-30) — b.db: streaming query results
12
+ - **0.4.12** (2026-04-30) — b.log: multi-sink output with per-sink level filtering
11
13
  - **0.4.11** (2026-04-30) — b.cache: bytes-cap eviction, sliding TTL, tag invalidation
12
14
  - **0.4.10** (2026-04-30) — bodyParser multipart: fileFilter + per-field maxBytes/mimeTypes
13
15
  - **0.4.9** (2026-04-30) — Origin-Agent-Cluster + DNS-Prefetch-Control headers; b.auth.lockout primitive
package/lib/db-query.js CHANGED
@@ -184,6 +184,37 @@ class Query {
184
184
  return out;
185
185
  }
186
186
 
187
+ // Streaming counterpart to all(). Each row is auto-unsealed against
188
+ // the bound table's sealedFields registration before it lands in the
189
+ // operator's pipeline. For large result sets (audit exports, backup
190
+ // table dumps) this avoids materializing the full rowset in memory.
191
+ stream() {
192
+ var sql = "SELECT " + this._projection() + ' FROM "' + this._table + '"' +
193
+ this._whereClause() + this._orderLimitOffset();
194
+ var stmt = this._db.prepare(sql);
195
+ var table = this._table;
196
+ var iter;
197
+ var Readable = require("node:stream").Readable;
198
+ try { iter = stmt.iterate.apply(stmt, this._whereParams); }
199
+ catch (e) {
200
+ var r = new Readable({ objectMode: true, read: function () {} });
201
+ setImmediate(function () { r.destroy(e); });
202
+ return r;
203
+ }
204
+ return new Readable({
205
+ objectMode: true,
206
+ read: function () {
207
+ try {
208
+ var step = iter.next();
209
+ if (step.done) { this.push(null); return; }
210
+ this.push(cryptoField.unsealRow(table, step.value));
211
+ } catch (e) {
212
+ this.destroy(e);
213
+ }
214
+ },
215
+ });
216
+ }
217
+
187
218
  count() {
188
219
  var sql = 'SELECT COUNT(*) AS n FROM "' + this._table + '"' + this._whereClause();
189
220
  var stmt = this._db.prepare(sql);
package/lib/db.js CHANGED
@@ -32,6 +32,8 @@
32
32
  *
33
33
  * db.from(tableName) → Query (chainable)
34
34
  * db.prepare(sql) → SQLite Statement (raw escape hatch)
35
+ * db.stream(sql, ...params, opts?) → Readable (object-mode rows;
36
+ * opts.table enables auto-unseal)
35
37
  * db.runSql(sql) → raw SQL execution (DDL, BEGIN/COMMIT)
36
38
  * db.transaction(function (db) {…}) → wraps in BEGIN/COMMIT/ROLLBACK
37
39
  * db.hashFor(table, field, value) → derived-hash lookup helper
@@ -746,6 +748,66 @@ function prepare(sql) {
746
748
  return database.prepare(sql);
747
749
  }
748
750
 
751
+ // stream — Readable in object mode that yields rows as node:sqlite's
752
+ // iterate() produces them. Unlike all(), the engine doesn't materialize
753
+ // the result set in memory before the first row arrives, so audit
754
+ // exports / backup table dumps / large reports can process millions of
755
+ // rows without OOM pressure.
756
+ //
757
+ // Optional opts.table enables auto-unseal of sealed columns via the
758
+ // table's registered cryptoField schema. Raw / aggregate queries omit
759
+ // it. Mid-iteration prepare()-bound errors propagate as 'error' events.
760
+ function stream(sql) {
761
+ _requireInit();
762
+ var opts = null;
763
+ var params;
764
+ // Last arg may be a plain {table?} options object; everything else
765
+ // is a SQL parameter binding. node:sqlite accepts numbers, strings,
766
+ // bigints, Buffers, and null — plain objects can only be opts.
767
+ var args = Array.prototype.slice.call(arguments, 1);
768
+ if (args.length > 0) {
769
+ var last = args[args.length - 1];
770
+ var isOptsShape = last !== null && typeof last === "object" &&
771
+ !Buffer.isBuffer(last) && !Array.isArray(last) &&
772
+ typeof last.length !== "number"; // exclude TypedArray-shapes
773
+ if (isOptsShape) {
774
+ opts = last;
775
+ params = args.slice(0, -1);
776
+ } else {
777
+ params = args;
778
+ }
779
+ } else {
780
+ params = [];
781
+ }
782
+ var table = opts && typeof opts.table === "string" ? opts.table : null;
783
+ var unseal = table ? cryptoField : null;
784
+
785
+ var Readable = require("node:stream").Readable;
786
+ var stmt;
787
+ var iter;
788
+ try {
789
+ stmt = database.prepare(sql);
790
+ iter = stmt.iterate.apply(stmt, params);
791
+ } catch (e) {
792
+ var r = new Readable({ objectMode: true, read: function () {} });
793
+ setImmediate(function () { r.destroy(e); });
794
+ return r;
795
+ }
796
+ return new Readable({
797
+ objectMode: true,
798
+ read: function () {
799
+ try {
800
+ var step = iter.next();
801
+ if (step.done) { this.push(null); return; }
802
+ var row = step.value;
803
+ this.push(unseal ? unseal.unsealRow(table, row) : row);
804
+ } catch (e) {
805
+ this.destroy(e);
806
+ }
807
+ },
808
+ });
809
+ }
810
+
749
811
  function execRaw(sql) {
750
812
  _requireInit();
751
813
  return runSql(database, sql);
@@ -946,6 +1008,7 @@ module.exports = {
946
1008
  init: init,
947
1009
  from: from,
948
1010
  prepare: prepare,
1011
+ stream: stream,
949
1012
  runSql: execRaw,
950
1013
  // SQLite multi-statement helper alias matching the node:sqlite
951
1014
  // module's shape. Operator migration / seeder files that received
package/lib/i18n.js CHANGED
@@ -385,12 +385,32 @@ function create(opts) {
385
385
  opts = opts || {};
386
386
  validateOpts(opts, [
387
387
  "defaultLocale", "locales", "fallbackLocale",
388
- "translations", "dir",
389
- "interpolation", "missingKey", "rtlLanguages",
388
+ "translations", "dir", "eagerLocales", "lazyLoad",
389
+ "interpolation", "missingKey", "onMissingKey", "rtlLanguages",
390
390
  "observability", "clock",
391
391
  ], "b.i18n");
392
392
  _validateCreateOpts(opts);
393
393
 
394
+ if (opts.lazyLoad === true && opts.translations) {
395
+ throw _err("BAD_OPT", "i18n.create: lazyLoad: true requires dir-based loading; " +
396
+ "translations: { ... } is inline-only and already complete at create time");
397
+ }
398
+ if (opts.eagerLocales !== undefined) {
399
+ if (!Array.isArray(opts.eagerLocales)) {
400
+ throw _err("BAD_OPT", "i18n.create: eagerLocales must be an array of BCP 47 tags");
401
+ }
402
+ for (var ei = 0; ei < opts.eagerLocales.length; ei++) {
403
+ _validateLocale("i18n.create: eagerLocales[" + ei + "]", opts.eagerLocales[ei]);
404
+ if (opts.locales.indexOf(opts.eagerLocales[ei]) === -1) {
405
+ throw _err("BAD_OPT", "i18n.create: eagerLocales[" + ei + "] '" +
406
+ opts.eagerLocales[ei] + "' must be in locales array");
407
+ }
408
+ }
409
+ }
410
+ if (opts.onMissingKey !== undefined && typeof opts.onMissingKey !== "function") {
411
+ throw _err("BAD_OPT", "i18n.create: onMissingKey must be a function (key, locale)");
412
+ }
413
+
394
414
  var defaultLocale = opts.defaultLocale;
395
415
  var locales = opts.locales.slice();
396
416
  var fallbackLocale = (opts.fallbackLocale === null) ? null
@@ -401,22 +421,54 @@ function create(opts) {
401
421
  var operatorObs = opts.observability || null;
402
422
 
403
423
  // Translations: either inline object or loaded from dir at create.
424
+ // With lazyLoad, only eager locales hit disk now; the rest load on
425
+ // first lookup that resolves to them.
404
426
  var translations;
427
+ var lazyLoadEnabled = false;
428
+ var lazyLoadDir = null;
429
+ var loadedSet = new Set();
405
430
  if (opts.dir) {
406
- translations = _loadFromDir(opts.dir, locales);
431
+ if (opts.lazyLoad === true) {
432
+ lazyLoadEnabled = true;
433
+ lazyLoadDir = opts.dir;
434
+ var eager = Array.isArray(opts.eagerLocales) && opts.eagerLocales.length > 0
435
+ ? opts.eagerLocales
436
+ : [defaultLocale];
437
+ translations = _loadFromDir(opts.dir, eager);
438
+ for (var ei2 = 0; ei2 < eager.length; ei2++) loadedSet.add(eager[ei2]);
439
+ } else {
440
+ translations = _loadFromDir(opts.dir, locales);
441
+ for (var ei3 = 0; ei3 < locales.length; ei3++) loadedSet.add(locales[ei3]);
442
+ }
407
443
  } else if (opts.translations) {
408
444
  translations = opts.translations;
445
+ for (var ei4 = 0; ei4 < locales.length; ei4++) {
446
+ if (translations[locales[ei4]]) loadedSet.add(locales[ei4]);
447
+ }
409
448
  } else {
410
449
  translations = {};
411
450
  }
451
+ var onMissingKey = opts.onMissingKey || null;
412
452
  // Validate translation trees up-front so plural-shape errors surface
413
- // at boot, not at the first request that hits the broken key.
453
+ // at boot, not at the first request that hits the broken key. Lazy
454
+ // locales validate on first load.
414
455
  for (var li = 0; li < locales.length; li++) {
415
456
  var loc = locales[li];
416
457
  if (translations[loc]) {
417
458
  _validateTranslationTree(loc, translations[loc], "");
418
459
  }
419
460
  }
461
+
462
+ function _ensureLocaleLoaded(locale) {
463
+ if (loadedSet.has(locale)) return;
464
+ if (!lazyLoadEnabled || !lazyLoadDir) return; // not configured for lazy
465
+ if (!localesSet.has(locale)) return; // unknown locale; lookup falls through
466
+ var loaded = _loadFromDir(lazyLoadDir, [locale]);
467
+ translations[locale] = loaded[locale];
468
+ _validateTranslationTree(locale, translations[locale], "");
469
+ loadedSet.add(locale);
470
+ _emitObs("i18n.lazyLoad", { locale: locale });
471
+ }
420
472
  var localesSet = new Set(locales);
421
473
  var currentLocale = defaultLocale;
422
474
 
@@ -493,6 +545,7 @@ function create(opts) {
493
545
  var chain = _localeChain(locale);
494
546
  for (var i = 0; i < chain.length; i++) {
495
547
  var loc = chain[i];
548
+ _ensureLocaleLoaded(loc);
496
549
  if (!translations[loc]) continue;
497
550
  var v = _resolveKey(translations[loc], key);
498
551
  if (v !== undefined) {
@@ -502,8 +555,20 @@ function create(opts) {
502
555
  return null;
503
556
  }
504
557
 
505
- function _selectPlural(node, count, locale) {
506
- var rules = _pluralRulesFor(locale);
558
+ // Ordinal-plural rules cache — separate from cardinal because Intl.PluralRules
559
+ // is type-fixed at construction.
560
+ var ordinalRulesByLocale = {};
561
+ function _ordinalRulesFor(locale) {
562
+ var r = ordinalRulesByLocale[locale];
563
+ if (!r) {
564
+ r = new Intl.PluralRules(locale, { type: "ordinal" });
565
+ ordinalRulesByLocale[locale] = r;
566
+ }
567
+ return r;
568
+ }
569
+
570
+ function _selectPlural(node, count, locale, ordinal) {
571
+ var rules = ordinal ? _ordinalRulesFor(locale) : _pluralRulesFor(locale);
507
572
  var category = rules.select(count);
508
573
  if (typeof node[category] === "string") return node[category];
509
574
  // Fallback within the entry: "other" was validated mandatory at load.
@@ -520,6 +585,10 @@ function create(opts) {
520
585
 
521
586
  if (!found) {
522
587
  _emitObs("i18n.missing", { locale: locale, key: key });
588
+ if (onMissingKey) {
589
+ try { onMissingKey(key, locale); }
590
+ catch (_e) { /* hook is best-effort; never break the request */ }
591
+ }
523
592
  if (callerOpts.default !== undefined) return callerOpts.default;
524
593
  if (typeof missingKeyPolicy === "function") {
525
594
  return missingKeyPolicy(key, locale);
@@ -540,7 +609,7 @@ function create(opts) {
540
609
  raw = found.value;
541
610
  } else if (_isPluralShape(found.value)) {
542
611
  var count = (vars && typeof vars.count === "number") ? vars.count : 0;
543
- raw = _selectPlural(found.value, count, found.foundIn);
612
+ raw = _selectPlural(found.value, count, found.foundIn, callerOpts.ordinal === true);
544
613
  } else {
545
614
  // Operator stored a nested tree at this key but called t() against
546
615
  // the namespace. Return the key-path as a missing-key signal.
@@ -560,6 +629,20 @@ function create(opts) {
560
629
  return t(key, merged, callerOpts);
561
630
  }
562
631
 
632
+ // to — ordinal-plural counterpart of tn. Selects from the entry using
633
+ // Intl.PluralRules({ type: "ordinal" }), so English keys
634
+ // { one: "{count}st", two: "{count}nd", few: "{count}rd", other: "{count}th" }
635
+ // resolve as "1st", "2nd", "3rd", "4th", "21st", etc.
636
+ function to(key, count, vars, callerOpts) {
637
+ if (typeof count !== "number" || !isFinite(count)) {
638
+ throw _err("BAD_INPUT", "i18n.to: count must be a finite number, got " +
639
+ (typeof count) + " " + JSON.stringify(count));
640
+ }
641
+ var merged = vars ? Object.assign({}, vars, { count: count }) : { count: count };
642
+ var withOrdinal = Object.assign({}, callerOpts || {}, { ordinal: true });
643
+ return t(key, merged, withOrdinal);
644
+ }
645
+
563
646
  function has(key, callerOpts) {
564
647
  callerOpts = callerOpts || {};
565
648
  if (typeof key !== "string" || key.length === 0) return false;
@@ -727,6 +810,11 @@ function create(opts) {
727
810
  if (c.locale === undefined) c.locale = resolvedLocale;
728
811
  return tn(key, count, vars, c);
729
812
  }
813
+ function reqTo(key, count, vars, callerOpts) {
814
+ var c = callerOpts ? Object.assign({}, callerOpts) : {};
815
+ if (c.locale === undefined) c.locale = resolvedLocale;
816
+ return to(key, count, vars, c);
817
+ }
730
818
  function reqDir() {
731
819
  return dir({ locale: resolvedLocale });
732
820
  }
@@ -734,12 +822,14 @@ function create(opts) {
734
822
  req.locale = resolvedLocale;
735
823
  req.t = reqT;
736
824
  req.tn = reqTn;
825
+ req.to = reqTo;
737
826
  req.dir = reqDir;
738
827
  if (res && typeof res === "object") {
739
828
  if (!res.locals) res.locals = {};
740
829
  res.locals.locale = resolvedLocale;
741
830
  res.locals.t = reqT;
742
831
  res.locals.tn = reqTn;
832
+ res.locals.to = reqTo;
743
833
  res.locals.dir = reqDir();
744
834
  }
745
835
  } catch (_e) {
@@ -753,6 +843,7 @@ function create(opts) {
753
843
  return {
754
844
  t: t,
755
845
  tn: tn,
846
+ to: to,
756
847
  has: has,
757
848
  formatNumber: formatNumber,
758
849
  formatDate: formatDate,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/core",
3
- "version": "0.4.12",
3
+ "version": "0.4.14",
4
4
  "description": "The Node framework that owns its stack.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "blamejs contributors",