spice-js 2.7.30 → 2.7.32

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.
@@ -7,9 +7,9 @@ var _ResourceLifecycleTriggered = _interopRequireDefault(require("../events/even
7
7
  var _Security = require("../utility/Security");
8
8
  var _fix = require("../utility/fix");
9
9
  function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
10
+ function _extends() { return _extends = Object.assign ? Object.assign.bind() : function (n) { for (var e = 1; e < arguments.length; e++) { var t = arguments[e]; for (var r in t) { ({}).hasOwnProperty.call(t, r) && (n[r] = t[r]); } } return n; }, _extends.apply(null, arguments); }
10
11
  function asyncGeneratorStep(n, t, e, r, o, a, c) { try { var i = n[a](c), u = i.value; } catch (n) { return void e(n); } i.done ? t(u) : Promise.resolve(u).then(r, o); }
11
12
  function _asyncToGenerator(n) { return function () { var t = this, e = arguments; return new Promise(function (r, o) { var a = n.apply(t, e); function _next(n) { asyncGeneratorStep(a, r, o, _next, _throw, "next", n); } function _throw(n) { asyncGeneratorStep(a, r, o, _next, _throw, "throw", n); } _next(void 0); }); }; }
12
- function _extends() { return _extends = Object.assign ? Object.assign.bind() : function (n) { for (var e = 1; e < arguments.length; e++) { var t = arguments[e]; for (var r in t) { ({}).hasOwnProperty.call(t, r) && (n[r] = t[r]); } } return n; }, _extends.apply(null, arguments); }
13
13
  var co = require("co");
14
14
  var regeneratorRuntime = require("regenerator-runtime");
15
15
  var SDate = require("sonover-date"),
@@ -29,11 +29,74 @@ var SDate = require("sonover-date"),
29
29
  _mapping_dept = Symbol(),
30
30
  _mapping_dept_scalar = Symbol(),
31
31
  _mapping_dept_array = Symbol(),
32
+ _mapping_concurrency = Symbol(),
33
+ _skip_mapped_serialization = Symbol(),
32
34
  _mapping_dept_exempt = Symbol(),
33
35
  _current_path = Symbol();
34
-
35
- //const _type = Symbol("type");
36
-
36
+ var GLOBAL_MAPPING_CONCURRENCY = Math.max(1, Number(process.env.SPICE_MAPPING_GLOBAL_CONCURRENCY) || 32);
37
+ var activeGlobalMappings = 0;
38
+ var globalMappingWaiters = [];
39
+ function normalizeConcurrency(value, fallback) {
40
+ if (fallback === void 0) {
41
+ fallback = 1;
42
+ }
43
+ var parsed = Number(value);
44
+ return Number.isFinite(parsed) && parsed > 0 ? Math.floor(parsed) : fallback;
45
+ }
46
+ function acquireGlobalMappingSlot() {
47
+ return _acquireGlobalMappingSlot.apply(this, arguments);
48
+ }
49
+ function _acquireGlobalMappingSlot() {
50
+ _acquireGlobalMappingSlot = _asyncToGenerator(function* () {
51
+ var queuedAt = process.hrtime.bigint();
52
+ if (activeGlobalMappings < GLOBAL_MAPPING_CONCURRENCY) {
53
+ activeGlobalMappings += 1;
54
+ } else {
55
+ yield new Promise(resolve => globalMappingWaiters.push(resolve));
56
+ }
57
+ return {
58
+ active: activeGlobalMappings,
59
+ waitMs: Number(process.hrtime.bigint() - queuedAt) / 1e6,
60
+ release() {
61
+ var next = globalMappingWaiters.shift();
62
+ if (next) {
63
+ next();
64
+ } else {
65
+ activeGlobalMappings -= 1;
66
+ }
67
+ }
68
+ };
69
+ });
70
+ return _acquireGlobalMappingSlot.apply(this, arguments);
71
+ }
72
+ function runWithConcurrency(_x, _x2, _x3) {
73
+ return _runWithConcurrency.apply(this, arguments);
74
+ } //const _type = Symbol("type");
75
+ function _runWithConcurrency() {
76
+ _runWithConcurrency = _asyncToGenerator(function* (items, concurrency, worker) {
77
+ var results = new Array(items.length);
78
+ var nextIndex = 0;
79
+ function runWorker() {
80
+ return _runWorker.apply(this, arguments);
81
+ }
82
+ function _runWorker() {
83
+ _runWorker = _asyncToGenerator(function* () {
84
+ while (nextIndex < items.length) {
85
+ var index = nextIndex;
86
+ nextIndex += 1;
87
+ results[index] = yield worker(items[index], index);
88
+ }
89
+ });
90
+ return _runWorker.apply(this, arguments);
91
+ }
92
+ var workerCount = Math.min(concurrency, items.length);
93
+ yield Promise.all(Array.from({
94
+ length: workerCount
95
+ }, () => runWorker()));
96
+ return results;
97
+ });
98
+ return _runWithConcurrency.apply(this, arguments);
99
+ }
37
100
  var that;
38
101
  if (!Promise.allSettled) {
39
102
  Promise.allSettled = promises => Promise.all(promises.map((promise, i) => promise.then(value => ({
@@ -50,7 +113,7 @@ class SpiceModel {
50
113
  args = {};
51
114
  }
52
115
  try {
53
- var _args2, _modelArgs$ctx, _modelArgs$ctx$state, _ref, _ref2, _modelArgs$mapping_de, _ref3, _ref4, _modelArgs$mapping_de2, _args$args, _args3, _args3$args, _args4, _args4$args, _args5, _args5$args, _args6, _args6$args, _args7, _args7$args;
116
+ var _args2, _modelArgs$ctx, _modelArgs$ctx$state, _ref, _ref2, _modelArgs$mapping_de, _ref3, _ref4, _modelArgs$mapping_de2, _modelArgs$mapping_co, _ref5, _modelArgs$skip_mappe, _args$args, _args3, _args3$args, _args4, _args4$args, _args5, _args5$args, _args6, _args6$args, _args7, _args7$args;
54
117
  var modelArgs = ((_args2 = args) == null ? void 0 : _args2.args) || {};
55
118
  var requestProfile = (modelArgs == null ? void 0 : (_modelArgs$ctx = modelArgs.ctx) == null ? void 0 : (_modelArgs$ctx$state = _modelArgs$ctx.state) == null ? void 0 : _modelArgs$ctx$state.read_profile) || {};
56
119
  var defaultMappingDept = process.env.MAPPING_DEPT || 3;
@@ -60,6 +123,8 @@ class SpiceModel {
60
123
  this[_mapping_dept] = legacyMappingDept != null ? legacyMappingDept : defaultMappingDept;
61
124
  this[_mapping_dept_scalar] = (_ref = (_ref2 = (_modelArgs$mapping_de = modelArgs.mapping_dept_scalar) != null ? _modelArgs$mapping_de : legacyMappingDept) != null ? _ref2 : requestProfile.mapping_dept_scalar) != null ? _ref : defaultMappingDept;
62
125
  this[_mapping_dept_array] = (_ref3 = (_ref4 = (_modelArgs$mapping_de2 = modelArgs.mapping_dept_array) != null ? _modelArgs$mapping_de2 : legacyMappingDept) != null ? _ref4 : requestProfile.mapping_dept_array) != null ? _ref3 : defaultMappingDept;
126
+ this[_mapping_concurrency] = normalizeConcurrency((_modelArgs$mapping_co = modelArgs.mapping_concurrency) != null ? _modelArgs$mapping_co : requestProfile.mapping_concurrency);
127
+ this[_skip_mapped_serialization] = (_ref5 = (_modelArgs$skip_mappe = modelArgs.skip_mapped_serialization) != null ? _modelArgs$skip_mappe : requestProfile.skip_mapped_serialization) != null ? _ref5 : false;
63
128
  this[_mapping_dept_exempt] = modelArgs.mapping_dept_exempt || [];
64
129
  this.type = "";
65
130
  this.collection = args.connection;
@@ -233,6 +298,12 @@ class SpiceModel {
233
298
  if (args.mapping_dept_array !== undefined && args.mapping_dept_array !== null) {
234
299
  this[_mapping_dept_array] = args.mapping_dept_array;
235
300
  }
301
+ if (args.skip_mapped_serialization !== undefined) {
302
+ this[_skip_mapped_serialization] = args.skip_mapped_serialization;
303
+ }
304
+ if (args.mapping_concurrency !== undefined) {
305
+ this[_mapping_concurrency] = normalizeConcurrency(args.mapping_concurrency, this[_mapping_concurrency]);
306
+ }
236
307
  if (args.mapping_dept_exempt) {
237
308
  this[_mapping_dept_exempt] = args.mapping_dept_exempt;
238
309
  }
@@ -278,13 +349,13 @@ class SpiceModel {
278
349
  return [];
279
350
  })();
280
351
  }
281
- createValidationString(_ref5) {
352
+ createValidationString(_ref6) {
282
353
  var {
283
354
  property_name,
284
355
  props,
285
356
  operation,
286
357
  complete
287
- } = _ref5;
358
+ } = _ref6;
288
359
  var operationExempt = {
289
360
  PUT: ["required", "unique"]
290
361
  };
@@ -515,7 +586,7 @@ class SpiceModel {
515
586
  // Profiling: use track() for proper async context forking
516
587
  var p = (_this4$_ctx = _this4[_ctx]) == null ? void 0 : _this4$_ctx.profiler;
517
588
  var doGet = /*#__PURE__*/function () {
518
- var _ref6 = _asyncToGenerator(function* () {
589
+ var _ref7 = _asyncToGenerator(function* () {
519
590
  if (!args) {
520
591
  args = {};
521
592
  }
@@ -624,7 +695,7 @@ class SpiceModel {
624
695
  }
625
696
  });
626
697
  return function doGet() {
627
- return _ref6.apply(this, arguments);
698
+ return _ref7.apply(this, arguments);
628
699
  };
629
700
  }();
630
701
  try {
@@ -641,17 +712,44 @@ class SpiceModel {
641
712
  }
642
713
  })();
643
714
  }
644
- query(query, scope) {
715
+ query(query, scope, options) {
645
716
  var _this5 = this;
646
717
  return _asyncToGenerator(function* () {
647
718
  try {
648
- var results = yield _this5.database.query(query, scope);
719
+ var results = yield _this5.database.query(query, scope, options);
649
720
  return results;
650
721
  } catch (error) {
651
722
  throw error;
652
723
  }
653
724
  })();
654
725
  }
726
+ getQueryOptions(args) {
727
+ if (args === void 0) {
728
+ args = {};
729
+ }
730
+ var scanConsistency = args.scan_consistency;
731
+
732
+ // Backward compatibility for callers that already use statement_consistent.
733
+ // The adapter accepts the SDK-style value as well as these snake_case keys.
734
+ if (scanConsistency === undefined && args.statement_consistent !== undefined) {
735
+ var legacyValue = args.statement_consistent;
736
+ var isRequestPlus = legacyValue === true || typeof legacyValue === "string" && ["true", "request_plus"].includes(legacyValue.toLowerCase());
737
+ scanConsistency = isRequestPlus ? "request_plus" : "not_bounded";
738
+ }
739
+ return {
740
+ scan_consistency: scanConsistency,
741
+ consistent_with: args.consistent_with,
742
+ scan_wait: args.scan_wait
743
+ };
744
+ }
745
+ requiresFreshQuery(args) {
746
+ if (args === void 0) {
747
+ args = {};
748
+ }
749
+ var options = this.getQueryOptions(args);
750
+ var scanConsistency = options.scan_consistency;
751
+ return options.consistent_with !== undefined || scanConsistency === true || typeof scanConsistency === "string" && ["true", "request_plus"].includes(scanConsistency.toLowerCase());
752
+ }
655
753
  getMulti(args) {
656
754
  var _this6 = this;
657
755
  return _asyncToGenerator(function* () {
@@ -659,7 +757,7 @@ class SpiceModel {
659
757
  // ⚡ Profiling: use track() for proper async context forking
660
758
  var p = (_this6$_ctx = _this6[_ctx]) == null ? void 0 : _this6$_ctx.profiler;
661
759
  var doGetMulti = /*#__PURE__*/function () {
662
- var _ref9 = _asyncToGenerator(function* () {
760
+ var _ref0 = _asyncToGenerator(function* () {
663
761
  if (!args) {
664
762
  args = {};
665
763
  }
@@ -701,7 +799,7 @@ class SpiceModel {
701
799
  return results;
702
800
  });
703
801
  return function doGetMulti() {
704
- return _ref9.apply(this, arguments);
802
+ return _ref0.apply(this, arguments);
705
803
  };
706
804
  }();
707
805
  try {
@@ -757,7 +855,7 @@ class SpiceModel {
757
855
  // Profiling: use track() for proper async context forking
758
856
  var p = (_this8$_ctx = _this8[_ctx]) == null ? void 0 : _this8$_ctx.profiler;
759
857
  var doUpdate = /*#__PURE__*/function () {
760
- var _ref0 = _asyncToGenerator(function* () {
858
+ var _ref1 = _asyncToGenerator(function* () {
761
859
  _this8.updated_at = new SDate().now();
762
860
  var results = yield _this8.database.get(args.id);
763
861
  var item_exist = yield _this8.exist(results);
@@ -801,7 +899,7 @@ class SpiceModel {
801
899
  });
802
900
  });
803
901
  return function doUpdate() {
804
- return _ref0.apply(this, arguments);
902
+ return _ref1.apply(this, arguments);
805
903
  };
806
904
  }();
807
905
  try {
@@ -827,7 +925,7 @@ class SpiceModel {
827
925
  // Profiling: use track() for proper async context forking
828
926
  var p = (_this9$_ctx = _this9[_ctx]) == null ? void 0 : _this9$_ctx.profiler;
829
927
  var doCreate = /*#__PURE__*/function () {
830
- var _ref10 = _asyncToGenerator(function* () {
928
+ var _ref11 = _asyncToGenerator(function* () {
831
929
  var form;
832
930
  _this9.created_at = new SDate().now();
833
931
  args = _.defaults(args, {
@@ -873,7 +971,7 @@ class SpiceModel {
873
971
  });
874
972
  });
875
973
  return function doCreate() {
876
- return _ref10.apply(this, arguments);
974
+ return _ref11.apply(this, arguments);
877
975
  };
878
976
  }();
879
977
  try {
@@ -911,7 +1009,7 @@ class SpiceModel {
911
1009
  // Profiling: use track() for proper async context forking
912
1010
  var p = (_this1$_ctx = _this1[_ctx]) == null ? void 0 : _this1$_ctx.profiler;
913
1011
  var doDelete = /*#__PURE__*/function () {
914
- var _ref12 = _asyncToGenerator(function* () {
1012
+ var _ref13 = _asyncToGenerator(function* () {
915
1013
  var item_exist = yield _this1.exist(args.id);
916
1014
  if (!item_exist) {
917
1015
  throw new Error(_this1.type + " does not exist.");
@@ -922,7 +1020,7 @@ class SpiceModel {
922
1020
  }
923
1021
  var delete_response = {};
924
1022
  var dbOperation = /*#__PURE__*/function () {
925
- var _ref13 = _asyncToGenerator(function* () {
1023
+ var _ref14 = _asyncToGenerator(function* () {
926
1024
  if (args.hard) {
927
1025
  delete_response = yield _this1.database.delete(args.id);
928
1026
  _this1.setMonitor();
@@ -933,7 +1031,7 @@ class SpiceModel {
933
1031
  }
934
1032
  });
935
1033
  return function dbOperation() {
936
- return _ref13.apply(this, arguments);
1034
+ return _ref14.apply(this, arguments);
937
1035
  };
938
1036
  }();
939
1037
  if (p) {
@@ -949,7 +1047,7 @@ class SpiceModel {
949
1047
  return {};
950
1048
  });
951
1049
  return function doDelete() {
952
- return _ref12.apply(this, arguments);
1050
+ return _ref13.apply(this, arguments);
953
1051
  };
954
1052
  }();
955
1053
  try {
@@ -1139,12 +1237,12 @@ class SpiceModel {
1139
1237
  }
1140
1238
  createJoinSection(mappedNestings) {
1141
1239
  if (!mappedNestings || mappedNestings.length === 0) return "";
1142
- return mappedNestings.map(_ref14 => {
1240
+ return mappedNestings.map(_ref15 => {
1143
1241
  var {
1144
1242
  alias,
1145
1243
  reference,
1146
1244
  is_array
1147
- } = _ref14;
1245
+ } = _ref15;
1148
1246
  var keyspace = (0, _fix.fixCollection)(reference);
1149
1247
  if (is_array === true) {
1150
1248
  return "LEFT NEST `" + keyspace + "` AS `" + alias + "` ON KEYS `" + this.type + "`.`" + alias + "`";
@@ -1289,7 +1387,7 @@ class SpiceModel {
1289
1387
  // Profiling: use track() for proper async context forking
1290
1388
  var p = (_this10$_ctx = _this10[_ctx]) == null ? void 0 : _this10$_ctx.profiler;
1291
1389
  var doList = /*#__PURE__*/function () {
1292
- var _ref15 = _asyncToGenerator(function* () {
1390
+ var _ref16 = _asyncToGenerator(function* () {
1293
1391
  var _args1, _args10;
1294
1392
  _this10.applyMappingDepthArgs(args);
1295
1393
 
@@ -1338,9 +1436,9 @@ class SpiceModel {
1338
1436
  if (args.skip_hooks !== true) {
1339
1437
  yield _this10.run_hook(_this10, "list", "before");
1340
1438
  }
1341
- var cacheKey = "list::" + _this10.type + "::" + args._join + "::" + query + "::" + args.limit + "::" + args.offset + "::" + args.sort + "::" + args.do_count + "::" + args.statement_consistent + "::" + args.columns + "::" + args.is_full_text + "::" + args.is_custom_query;
1439
+ var cacheKey = "list::" + _this10.type + "::" + args._join + "::" + query + "::" + args.limit + "::" + args.offset + "::" + args.sort + "::" + args.do_count + "::" + args.query_scope + "::" + args.scan_consistency + "::" + args.statement_consistent + "::" + args.scan_wait + "::" + args.columns + "::" + args.is_full_text + "::" + args.is_custom_query;
1342
1440
  var results;
1343
- if (_this10.shouldUseCache(_this10.type)) {
1441
+ if (_this10.shouldUseCache(_this10.type) && !_this10.requiresFreshQuery(args)) {
1344
1442
  var cached = yield _this10.getCacheProviderObject(_this10.type).get(cacheKey);
1345
1443
  results = cached == null ? void 0 : cached.value;
1346
1444
  var isEmpty = (cached == null ? void 0 : cached.value) === undefined;
@@ -1368,7 +1466,7 @@ class SpiceModel {
1368
1466
  return results;
1369
1467
  });
1370
1468
  return function doList() {
1371
- return _ref15.apply(this, arguments);
1469
+ return _ref16.apply(this, arguments);
1372
1470
  };
1373
1471
  }();
1374
1472
  try {
@@ -1422,20 +1520,21 @@ class SpiceModel {
1422
1520
  // Profiling: use track() for proper async context forking
1423
1521
  var p = (_this11$_ctx = _this11[_ctx]) == null ? void 0 : _this11$_ctx.profiler;
1424
1522
  var doFetch = /*#__PURE__*/function () {
1425
- var _ref16 = _asyncToGenerator(function* () {
1523
+ var _ref17 = _asyncToGenerator(function* () {
1524
+ var queryOptions = _this11.getQueryOptions(args);
1426
1525
  if (args.is_custom_query === "true" && args.ids.length > 0) {
1427
1526
  console.log("Fetching results from database", _this11.type, args, query, mappedNestings);
1428
- return yield _this11.database.query(query);
1527
+ return yield _this11.database.query(query, args.query_scope, queryOptions);
1429
1528
  } else if (args.is_full_text === "true") {
1430
1529
  return yield _this11.database.full_text_search(_this11.type, query || "", args.limit, args.offset, args._join);
1431
1530
  } else {
1432
1531
  var correctedColumns = _this11.correctColumns(args.columns, mappedNestings);
1433
- var result = yield _this11.database.search(_this11.type, correctedColumns || args.columns || "", query || "", args.limit, args.offset, args.sort, args.do_count, args.statement_consistent, args._join);
1532
+ var result = yield _this11.database.search(_this11.type, correctedColumns || args.columns || "", query || "", args.limit, args.offset, args.sort, args.do_count, args.query_scope, args._join, queryOptions);
1434
1533
  return result;
1435
1534
  }
1436
1535
  });
1437
1536
  return function doFetch() {
1438
- return _ref16.apply(this, arguments);
1537
+ return _ref17.apply(this, arguments);
1439
1538
  };
1440
1539
  }();
1441
1540
  if (p) {
@@ -1444,12 +1543,12 @@ class SpiceModel {
1444
1543
  return yield doFetch();
1445
1544
  })();
1446
1545
  }
1447
- addHook(_ref17) {
1546
+ addHook(_ref18) {
1448
1547
  var {
1449
1548
  operation,
1450
1549
  when,
1451
1550
  execute
1452
- } = _ref17;
1551
+ } = _ref18;
1453
1552
  this[_hooks][operation][when].push(execute);
1454
1553
  }
1455
1554
  run_hook(data, op, when, old_data) {
@@ -1572,7 +1671,7 @@ class SpiceModel {
1572
1671
 
1573
1672
  // ⚡ Wrap in profiler track() to ensure proper async context for child operations
1574
1673
  var fetchRelated = /*#__PURE__*/function () {
1575
- var _ref18 = _asyncToGenerator(function* () {
1674
+ var _ref19 = _asyncToGenerator(function* () {
1576
1675
  return yield Promise.allSettled(_.map(classes, obj => {
1577
1676
  var objInstance = new obj(_extends({}, _this13[_args], {
1578
1677
  skip_cache: _this13[_skip_cache],
@@ -1580,6 +1679,8 @@ class SpiceModel {
1580
1679
  mapping_dept: _this13[_mapping_dept],
1581
1680
  mapping_dept_scalar: _this13[_mapping_dept_scalar],
1582
1681
  mapping_dept_array: _this13[_mapping_dept_array],
1682
+ mapping_concurrency: _this13[_mapping_concurrency],
1683
+ skip_mapped_serialization: _this13[_skip_mapped_serialization],
1583
1684
  mapping_dept_exempt: _this13[_mapping_dept_exempt],
1584
1685
  _columns: columnArray.join(","),
1585
1686
  _current_path: childPath
@@ -1587,12 +1688,13 @@ class SpiceModel {
1587
1688
  return objInstance.getMulti({
1588
1689
  skip_hooks: true,
1589
1690
  ids: ids,
1590
- columns: columnArray
1691
+ columns: columnArray,
1692
+ skip_read_serialize: _this13[_skip_mapped_serialization]
1591
1693
  });
1592
1694
  }));
1593
1695
  });
1594
1696
  return function fetchRelated() {
1595
- return _ref18.apply(this, arguments);
1697
+ return _ref19.apply(this, arguments);
1596
1698
  };
1597
1699
  }();
1598
1700
  var returned_all;
@@ -1693,7 +1795,7 @@ class SpiceModel {
1693
1795
 
1694
1796
  // ⚡ Wrap in profiler track() to ensure proper async context for child operations
1695
1797
  var fetchRelated = /*#__PURE__*/function () {
1696
- var _ref19 = _asyncToGenerator(function* () {
1798
+ var _ref20 = _asyncToGenerator(function* () {
1697
1799
  return yield Promise.allSettled(_.map(classes, obj => {
1698
1800
  return new obj(_extends({}, _this14[_args], {
1699
1801
  skip_cache: _this14[_skip_cache],
@@ -1701,18 +1803,21 @@ class SpiceModel {
1701
1803
  mapping_dept: _this14[_mapping_dept],
1702
1804
  mapping_dept_scalar: _this14[_mapping_dept_scalar],
1703
1805
  mapping_dept_array: _this14[_mapping_dept_array],
1806
+ mapping_concurrency: _this14[_mapping_concurrency],
1807
+ skip_mapped_serialization: _this14[_skip_mapped_serialization],
1704
1808
  mapping_dept_exempt: _this14[_mapping_dept_exempt],
1705
1809
  _columns: columnArray.join(","),
1706
1810
  _current_path: childPath
1707
1811
  })).getMulti({
1708
1812
  skip_hooks: true,
1709
1813
  ids: ids,
1710
- columns: columnArray
1814
+ columns: columnArray,
1815
+ skip_read_serialize: _this14[_skip_mapped_serialization]
1711
1816
  });
1712
1817
  }));
1713
1818
  });
1714
1819
  return function fetchRelated() {
1715
- return _ref19.apply(this, arguments);
1820
+ return _ref20.apply(this, arguments);
1716
1821
  };
1717
1822
  }();
1718
1823
  var returned_all;
@@ -1760,12 +1865,22 @@ class SpiceModel {
1760
1865
  return original_is_array ? data : data[0];
1761
1866
  })();
1762
1867
  }
1763
- addModifier(_ref20) {
1868
+ addModifier(_ref21) {
1764
1869
  var {
1765
1870
  when,
1766
- execute
1767
- } = _ref20;
1871
+ execute,
1872
+ kind,
1873
+ source_property,
1874
+ store_property
1875
+ } = _ref21;
1768
1876
  if (this[_serializers][when]) {
1877
+ if (kind) {
1878
+ execute.spiceModifier = {
1879
+ kind,
1880
+ source_property,
1881
+ store_property
1882
+ };
1883
+ }
1769
1884
  this[_serializers][when]["modifiers"].push(execute);
1770
1885
  }
1771
1886
  }
@@ -1833,6 +1948,9 @@ class SpiceModel {
1833
1948
  {
1834
1949
  _this15.addModifier({
1835
1950
  when: properties[i].map.when || "read",
1951
+ kind: "relationship_map",
1952
+ source_property: i,
1953
+ store_property: properties[i].map.destination || i,
1836
1954
  execute: function () {
1837
1955
  var _execute = _asyncToGenerator(function* (data, old_data, ctx, type, args, mapping_dept, level, columns, serializationType) {
1838
1956
  // Skip if columns are specified but this field isn't included
@@ -1841,7 +1959,7 @@ class SpiceModel {
1841
1959
  }
1842
1960
  return yield _this15.mapToObject(data, _.isString(properties[i].map.reference) ? spice.models[properties[i].map.reference] : properties[i].map.reference, i, properties[i].map.destination || i, properties[i], args, serializationType, mapping_dept, level, columns);
1843
1961
  });
1844
- function execute(_x, _x2, _x3, _x4, _x5, _x6, _x7, _x8, _x9) {
1962
+ function execute(_x4, _x5, _x6, _x7, _x8, _x9, _x0, _x1, _x10) {
1845
1963
  return _execute.apply(this, arguments);
1846
1964
  }
1847
1965
  return execute;
@@ -1854,6 +1972,9 @@ class SpiceModel {
1854
1972
  {
1855
1973
  _this15.addModifier({
1856
1974
  when: properties[i].map.when || "read",
1975
+ kind: "relationship_map",
1976
+ source_property: i,
1977
+ store_property: properties[i].map.destination || i,
1857
1978
  execute: function () {
1858
1979
  var _execute2 = _asyncToGenerator(function* (data, old_data, ctx, type, args, mapping_dept, level, columns, serializationType) {
1859
1980
  // Skip if columns are specified but this field isn't included
@@ -1862,7 +1983,7 @@ class SpiceModel {
1862
1983
  }
1863
1984
  return yield _this15.mapToObjectArray(data, _.isString(properties[i].map.reference) ? spice.models[properties[i].map.reference] : properties[i].map.reference, i, properties[i].map.destination || i, properties[i], args, serializationType, mapping_dept, level, columns);
1864
1985
  });
1865
- function execute(_x0, _x1, _x10, _x11, _x12, _x13, _x14, _x15, _x16) {
1986
+ function execute(_x11, _x12, _x13, _x14, _x15, _x16, _x17, _x18, _x19) {
1866
1987
  return _execute2.apply(this, arguments);
1867
1988
  }
1868
1989
  return execute;
@@ -1894,7 +2015,7 @@ class SpiceModel {
1894
2015
  // Profiling: use track() for proper async context forking
1895
2016
  var p = (_this16$_ctx = _this16[_ctx]) == null ? void 0 : _this16$_ctx.profiler;
1896
2017
  var doSerialize = /*#__PURE__*/function () {
1897
- var _ref21 = _asyncToGenerator(function* () {
2018
+ var _ref22 = _asyncToGenerator(function* () {
1898
2019
  var _this16$_serializers, _this16$_serializers$;
1899
2020
  // Early exit if serialization should not run.
1900
2021
  if (!_this16.shouldSerializerRun(args, type)) {
@@ -1909,19 +2030,117 @@ class SpiceModel {
1909
2030
 
1910
2031
  // Cache the modifiers lookup for the specified type.
1911
2032
  var modifiers = ((_this16$_serializers = _this16[_serializers]) == null ? void 0 : (_this16$_serializers$ = _this16$_serializers[type]) == null ? void 0 : _this16$_serializers$.modifiers) || [];
1912
- for (var i = 0; i < modifiers.length; i++) {
1913
- var modifier = modifiers[i];
1914
- try {
1915
- var result = yield modifier(data, old_data, _this16[_ctx], _this16.type, args, _this16[_mapping_dept], _this16[_level], _this16[_columns], type);
1916
- // Guard against modifiers that return undefined
1917
- if (result !== undefined) {
1918
- data = result;
1919
- } else {
1920
- console.warn("Modifier #" + i + " for type=" + _this16.type + " returned undefined, keeping previous data");
2033
+ var invokeModifier = /*#__PURE__*/function () {
2034
+ var _ref23 = _asyncToGenerator(function* (modifier, index, inputData) {
2035
+ try {
2036
+ var result = yield modifier(inputData, old_data, _this16[_ctx], _this16.type, args, _this16[_mapping_dept], _this16[_level], _this16[_columns], type);
2037
+ if (result !== undefined) {
2038
+ return {
2039
+ hasValue: true,
2040
+ value: result
2041
+ };
2042
+ }
2043
+ console.warn("Modifier #" + index + " for type=" + _this16.type + " returned undefined, keeping previous data");
2044
+ } catch (error) {
2045
+ console.error("Modifier error in do_serialize (type=" + _this16.type + ", modifier #" + index + "):", error instanceof Error ? error.stack : "Non-Error thrown: " + JSON.stringify(error));
1921
2046
  }
1922
- } catch (error) {
1923
- console.error("Modifier error in do_serialize (type=" + _this16.type + ", modifier #" + i + "):", error instanceof Error ? error.stack : "Non-Error thrown: " + JSON.stringify(error));
2047
+ return {
2048
+ hasValue: false
2049
+ };
2050
+ });
2051
+ return function invokeModifier(_x20, _x21, _x22) {
2052
+ return _ref23.apply(this, arguments);
2053
+ };
2054
+ }();
2055
+ var modifierIndex = 0;
2056
+ var _loop2 = function* _loop2() {
2057
+ var _modifier$spiceModifi;
2058
+ var modifier = modifiers[modifierIndex];
2059
+ var canRunRelationshipMapsConcurrently = type === "read" && _this16[_level] === 0 && _this16[_mapping_concurrency] > 1 && (modifier == null ? void 0 : (_modifier$spiceModifi = modifier.spiceModifier) == null ? void 0 : _modifier$spiceModifi.kind) === "relationship_map";
2060
+ if (!canRunRelationshipMapsConcurrently) {
2061
+ var result = yield invokeModifier(modifier, modifierIndex, data);
2062
+ if (result.hasValue) data = result.value;
2063
+ modifierIndex += 1;
2064
+ return "continue";
1924
2065
  }
2066
+ var group = [];
2067
+ var destinations = new Set();
2068
+ while (modifierIndex < modifiers.length) {
2069
+ var candidate = modifiers[modifierIndex];
2070
+ var metadata = candidate == null ? void 0 : candidate.spiceModifier;
2071
+ if ((metadata == null ? void 0 : metadata.kind) !== "relationship_map") break;
2072
+ if (metadata.store_property && destinations.has(metadata.store_property)) {
2073
+ break;
2074
+ }
2075
+ if (metadata.store_property) {
2076
+ destinations.add(metadata.store_property);
2077
+ }
2078
+ group.push({
2079
+ modifier: candidate,
2080
+ index: modifierIndex,
2081
+ metadata
2082
+ });
2083
+ modifierIndex += 1;
2084
+ }
2085
+ var groupQueuedAt = process.hrtime.bigint();
2086
+ var activeRequestMappings = 0;
2087
+ var executeGroup = /*#__PURE__*/function () {
2088
+ var _ref24 = _asyncToGenerator(function* () {
2089
+ return yield runWithConcurrency(group, _this16[_mapping_concurrency], /*#__PURE__*/function () {
2090
+ var _ref25 = _asyncToGenerator(function* (entry) {
2091
+ var requestQueueWaitMs = Number(process.hrtime.bigint() - groupQueuedAt) / 1e6;
2092
+ var globalSlot = yield acquireGlobalMappingSlot();
2093
+ activeRequestMappings += 1;
2094
+ var profileMetadata = {
2095
+ source_property: entry.metadata.source_property,
2096
+ store_property: entry.metadata.store_property,
2097
+ request_queue_wait_ms: Math.round(requestQueueWaitMs * 100) / 100,
2098
+ global_queue_wait_ms: Math.round(globalSlot.waitMs * 100) / 100,
2099
+ active_request_mappings: activeRequestMappings,
2100
+ request_limit: _this16[_mapping_concurrency],
2101
+ active_global_mappings: globalSlot.active,
2102
+ global_limit: GLOBAL_MAPPING_CONCURRENCY
2103
+ };
2104
+ try {
2105
+ var execute = /*#__PURE__*/function () {
2106
+ var _ref26 = _asyncToGenerator(function* () {
2107
+ return yield invokeModifier(entry.modifier, entry.index, data);
2108
+ });
2109
+ return function execute() {
2110
+ return _ref26.apply(this, arguments);
2111
+ };
2112
+ }();
2113
+ if (p) {
2114
+ return yield p.track(_this16.type + ".map.concurrent." + entry.metadata.source_property, execute, profileMetadata);
2115
+ }
2116
+ return yield execute();
2117
+ } finally {
2118
+ activeRequestMappings -= 1;
2119
+ globalSlot.release();
2120
+ }
2121
+ });
2122
+ return function (_x23) {
2123
+ return _ref25.apply(this, arguments);
2124
+ };
2125
+ }());
2126
+ });
2127
+ return function executeGroup() {
2128
+ return _ref24.apply(this, arguments);
2129
+ };
2130
+ }();
2131
+ var groupMetadata = {
2132
+ map_count: group.length,
2133
+ request_limit: _this16[_mapping_concurrency],
2134
+ global_limit: GLOBAL_MAPPING_CONCURRENCY
2135
+ };
2136
+ var groupResults = p ? yield p.track(_this16.type + ".map.concurrent", executeGroup, groupMetadata) : yield executeGroup();
2137
+ for (var _result of groupResults) {
2138
+ if (_result == null ? void 0 : _result.hasValue) data = _result.value;
2139
+ }
2140
+ };
2141
+ while (modifierIndex < modifiers.length) {
2142
+ var _ret = yield* _loop2();
2143
+ if (_ret === "continue") continue;
1925
2144
  }
1926
2145
 
1927
2146
  // Ensure data is always an array for consistent processing.
@@ -1962,7 +2181,7 @@ class SpiceModel {
1962
2181
  return originalIsArray ? data : data[0];
1963
2182
  });
1964
2183
  return function doSerialize() {
1965
- return _ref21.apply(this, arguments);
2184
+ return _ref22.apply(this, arguments);
1966
2185
  };
1967
2186
  }();
1968
2187
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spice-js",
3
- "version": "2.7.30",
3
+ "version": "2.7.32",
4
4
  "description": "spice",
5
5
  "main": "build/index.js",
6
6
  "repository": {
@@ -24,9 +24,64 @@ var SDate = require("sonover-date"),
24
24
  _mapping_dept = Symbol(),
25
25
  _mapping_dept_scalar = Symbol(),
26
26
  _mapping_dept_array = Symbol(),
27
+ _mapping_concurrency = Symbol(),
28
+ _skip_mapped_serialization = Symbol(),
27
29
  _mapping_dept_exempt = Symbol(),
28
30
  _current_path = Symbol();
29
31
 
32
+ const GLOBAL_MAPPING_CONCURRENCY = Math.max(
33
+ 1,
34
+ Number(process.env.SPICE_MAPPING_GLOBAL_CONCURRENCY) || 32,
35
+ );
36
+ let activeGlobalMappings = 0;
37
+ const globalMappingWaiters = [];
38
+
39
+ function normalizeConcurrency(value, fallback = 1) {
40
+ const parsed = Number(value);
41
+ return Number.isFinite(parsed) && parsed > 0 ? Math.floor(parsed) : fallback;
42
+ }
43
+
44
+ async function acquireGlobalMappingSlot() {
45
+ const queuedAt = process.hrtime.bigint();
46
+ if (activeGlobalMappings < GLOBAL_MAPPING_CONCURRENCY) {
47
+ activeGlobalMappings += 1;
48
+ } else {
49
+ await new Promise((resolve) => globalMappingWaiters.push(resolve));
50
+ }
51
+
52
+ return {
53
+ active: activeGlobalMappings,
54
+ waitMs: Number(process.hrtime.bigint() - queuedAt) / 1e6,
55
+ release() {
56
+ const next = globalMappingWaiters.shift();
57
+ if (next) {
58
+ next();
59
+ } else {
60
+ activeGlobalMappings -= 1;
61
+ }
62
+ },
63
+ };
64
+ }
65
+
66
+ async function runWithConcurrency(items, concurrency, worker) {
67
+ const results = new Array(items.length);
68
+ let nextIndex = 0;
69
+
70
+ async function runWorker() {
71
+ while (nextIndex < items.length) {
72
+ const index = nextIndex;
73
+ nextIndex += 1;
74
+ results[index] = await worker(items[index], index);
75
+ }
76
+ }
77
+
78
+ const workerCount = Math.min(concurrency, items.length);
79
+ await Promise.all(
80
+ Array.from({ length: workerCount }, () => runWorker()),
81
+ );
82
+ return results;
83
+ }
84
+
30
85
  //const _type = Symbol("type");
31
86
 
32
87
  let that;
@@ -71,6 +126,13 @@ export default class SpiceModel {
71
126
  legacyMappingDept ??
72
127
  requestProfile.mapping_dept_array ??
73
128
  defaultMappingDept;
129
+ this[_mapping_concurrency] = normalizeConcurrency(
130
+ modelArgs.mapping_concurrency ?? requestProfile.mapping_concurrency,
131
+ );
132
+ this[_skip_mapped_serialization] =
133
+ modelArgs.skip_mapped_serialization ??
134
+ requestProfile.skip_mapped_serialization ??
135
+ false;
74
136
  this[_mapping_dept_exempt] = modelArgs.mapping_dept_exempt || [];
75
137
  this.type = "";
76
138
  this.collection = args.connection;
@@ -277,6 +339,15 @@ export default class SpiceModel {
277
339
  ) {
278
340
  this[_mapping_dept_array] = args.mapping_dept_array;
279
341
  }
342
+ if (args.skip_mapped_serialization !== undefined) {
343
+ this[_skip_mapped_serialization] = args.skip_mapped_serialization;
344
+ }
345
+ if (args.mapping_concurrency !== undefined) {
346
+ this[_mapping_concurrency] = normalizeConcurrency(
347
+ args.mapping_concurrency,
348
+ this[_mapping_concurrency],
349
+ );
350
+ }
280
351
  if (args.mapping_dept_exempt) {
281
352
  this[_mapping_dept_exempt] = args.mapping_dept_exempt;
282
353
  }
@@ -712,15 +783,47 @@ export default class SpiceModel {
712
783
  }
713
784
  }
714
785
 
715
- async query(query, scope) {
786
+ async query(query, scope, options) {
716
787
  try {
717
- let results = await this.database.query(query, scope);
788
+ let results = await this.database.query(query, scope, options);
718
789
  return results;
719
790
  } catch (error) {
720
791
  throw error;
721
792
  }
722
793
  }
723
794
 
795
+ getQueryOptions(args = {}) {
796
+ let scanConsistency = args.scan_consistency;
797
+
798
+ // Backward compatibility for callers that already use statement_consistent.
799
+ // The adapter accepts the SDK-style value as well as these snake_case keys.
800
+ if (scanConsistency === undefined && args.statement_consistent !== undefined) {
801
+ const legacyValue = args.statement_consistent;
802
+ const isRequestPlus =
803
+ legacyValue === true ||
804
+ (typeof legacyValue === "string" &&
805
+ ["true", "request_plus"].includes(legacyValue.toLowerCase()));
806
+ scanConsistency = isRequestPlus ? "request_plus" : "not_bounded";
807
+ }
808
+
809
+ return {
810
+ scan_consistency: scanConsistency,
811
+ consistent_with: args.consistent_with,
812
+ scan_wait: args.scan_wait,
813
+ };
814
+ }
815
+
816
+ requiresFreshQuery(args = {}) {
817
+ const options = this.getQueryOptions(args);
818
+ const scanConsistency = options.scan_consistency;
819
+ return (
820
+ options.consistent_with !== undefined ||
821
+ scanConsistency === true ||
822
+ (typeof scanConsistency === "string" &&
823
+ ["true", "request_plus"].includes(scanConsistency.toLowerCase()))
824
+ );
825
+ }
826
+
724
827
  async getMulti(args) {
725
828
  // ⚡ Profiling: use track() for proper async context forking
726
829
  const p = this[_ctx]?.profiler;
@@ -1498,10 +1601,10 @@ export default class SpiceModel {
1498
1601
  await this.run_hook(this, "list", "before");
1499
1602
  }
1500
1603
 
1501
- const cacheKey = `list::${this.type}::${args._join}::${query}::${args.limit}::${args.offset}::${args.sort}::${args.do_count}::${args.statement_consistent}::${args.columns}::${args.is_full_text}::${args.is_custom_query}`;
1604
+ const cacheKey = `list::${this.type}::${args._join}::${query}::${args.limit}::${args.offset}::${args.sort}::${args.do_count}::${args.query_scope}::${args.scan_consistency}::${args.statement_consistent}::${args.scan_wait}::${args.columns}::${args.is_full_text}::${args.is_custom_query}`;
1502
1605
  let results;
1503
1606
 
1504
- if (this.shouldUseCache(this.type)) {
1607
+ if (this.shouldUseCache(this.type) && !this.requiresFreshQuery(args)) {
1505
1608
  const cached = await this.getCacheProviderObject(this.type).get(
1506
1609
  cacheKey,
1507
1610
  );
@@ -1600,6 +1703,7 @@ export default class SpiceModel {
1600
1703
  const p = this[_ctx]?.profiler;
1601
1704
 
1602
1705
  const doFetch = async () => {
1706
+ const queryOptions = this.getQueryOptions(args);
1603
1707
  if (args.is_custom_query === "true" && args.ids.length > 0) {
1604
1708
  console.log(
1605
1709
  "Fetching results from database",
@@ -1608,7 +1712,7 @@ export default class SpiceModel {
1608
1712
  query,
1609
1713
  mappedNestings,
1610
1714
  );
1611
- return await this.database.query(query);
1715
+ return await this.database.query(query, args.query_scope, queryOptions);
1612
1716
  } else if (args.is_full_text === "true") {
1613
1717
  return await this.database.full_text_search(
1614
1718
  this.type,
@@ -1630,8 +1734,9 @@ export default class SpiceModel {
1630
1734
  args.offset,
1631
1735
  args.sort,
1632
1736
  args.do_count,
1633
- args.statement_consistent,
1737
+ args.query_scope,
1634
1738
  args._join,
1739
+ queryOptions,
1635
1740
  );
1636
1741
  return result;
1637
1742
  }
@@ -1815,6 +1920,8 @@ export default class SpiceModel {
1815
1920
  mapping_dept: this[_mapping_dept],
1816
1921
  mapping_dept_scalar: this[_mapping_dept_scalar],
1817
1922
  mapping_dept_array: this[_mapping_dept_array],
1923
+ mapping_concurrency: this[_mapping_concurrency],
1924
+ skip_mapped_serialization: this[_skip_mapped_serialization],
1818
1925
  mapping_dept_exempt: this[_mapping_dept_exempt],
1819
1926
  _columns: columnArray.join(","),
1820
1927
  _current_path: childPath,
@@ -1824,6 +1931,7 @@ export default class SpiceModel {
1824
1931
  skip_hooks: true,
1825
1932
  ids: ids,
1826
1933
  columns: columnArray,
1934
+ skip_read_serialize: this[_skip_mapped_serialization],
1827
1935
  });
1828
1936
  }),
1829
1937
  );
@@ -1990,6 +2098,8 @@ export default class SpiceModel {
1990
2098
  mapping_dept: this[_mapping_dept],
1991
2099
  mapping_dept_scalar: this[_mapping_dept_scalar],
1992
2100
  mapping_dept_array: this[_mapping_dept_array],
2101
+ mapping_concurrency: this[_mapping_concurrency],
2102
+ skip_mapped_serialization: this[_skip_mapped_serialization],
1993
2103
  mapping_dept_exempt: this[_mapping_dept_exempt],
1994
2104
  _columns: columnArray.join(","),
1995
2105
  _current_path: childPath,
@@ -1997,6 +2107,7 @@ export default class SpiceModel {
1997
2107
  skip_hooks: true,
1998
2108
  ids: ids,
1999
2109
  columns: columnArray,
2110
+ skip_read_serialize: this[_skip_mapped_serialization],
2000
2111
  });
2001
2112
  }),
2002
2113
  );
@@ -2070,8 +2181,21 @@ export default class SpiceModel {
2070
2181
  return original_is_array ? data : data[0];
2071
2182
  }
2072
2183
 
2073
- addModifier({ when, execute }) {
2184
+ addModifier({
2185
+ when,
2186
+ execute,
2187
+ kind,
2188
+ source_property,
2189
+ store_property,
2190
+ }) {
2074
2191
  if (this[_serializers][when]) {
2192
+ if (kind) {
2193
+ execute.spiceModifier = {
2194
+ kind,
2195
+ source_property,
2196
+ store_property,
2197
+ };
2198
+ }
2075
2199
  this[_serializers][when]["modifiers"].push(execute);
2076
2200
  }
2077
2201
  }
@@ -2145,6 +2269,9 @@ export default class SpiceModel {
2145
2269
  case "string": {
2146
2270
  this.addModifier({
2147
2271
  when: properties[i].map.when || "read",
2272
+ kind: "relationship_map",
2273
+ source_property: i,
2274
+ store_property: properties[i].map.destination || i,
2148
2275
  execute: async (
2149
2276
  data,
2150
2277
  old_data,
@@ -2182,6 +2309,9 @@ export default class SpiceModel {
2182
2309
  case "array": {
2183
2310
  this.addModifier({
2184
2311
  when: properties[i].map.when || "read",
2312
+ kind: "relationship_map",
2313
+ source_property: i,
2314
+ store_property: properties[i].map.destination || i,
2185
2315
  execute: async (
2186
2316
  data,
2187
2317
  old_data,
@@ -2245,11 +2375,10 @@ export default class SpiceModel {
2245
2375
 
2246
2376
  // Cache the modifiers lookup for the specified type.
2247
2377
  const modifiers = this[_serializers]?.[type]?.modifiers || [];
2248
- for (let i = 0; i < modifiers.length; i++) {
2249
- const modifier = modifiers[i];
2378
+ const invokeModifier = async (modifier, index, inputData) => {
2250
2379
  try {
2251
2380
  const result = await modifier(
2252
- data,
2381
+ inputData,
2253
2382
  old_data,
2254
2383
  this[_ctx],
2255
2384
  this.type,
@@ -2259,22 +2388,117 @@ export default class SpiceModel {
2259
2388
  this[_columns],
2260
2389
  type,
2261
2390
  );
2262
- // Guard against modifiers that return undefined
2263
2391
  if (result !== undefined) {
2264
- data = result;
2265
- } else {
2266
- console.warn(
2267
- `Modifier #${i} for type=${this.type} returned undefined, keeping previous data`,
2268
- );
2392
+ return { hasValue: true, value: result };
2269
2393
  }
2394
+ console.warn(
2395
+ `Modifier #${index} for type=${this.type} returned undefined, keeping previous data`,
2396
+ );
2270
2397
  } catch (error) {
2271
2398
  console.error(
2272
- `Modifier error in do_serialize (type=${this.type}, modifier #${i}):`,
2399
+ `Modifier error in do_serialize (type=${this.type}, modifier #${index}):`,
2273
2400
  error instanceof Error ?
2274
2401
  error.stack
2275
2402
  : `Non-Error thrown: ${JSON.stringify(error)}`,
2276
2403
  );
2277
2404
  }
2405
+ return { hasValue: false };
2406
+ };
2407
+
2408
+ let modifierIndex = 0;
2409
+ while (modifierIndex < modifiers.length) {
2410
+ const modifier = modifiers[modifierIndex];
2411
+ const canRunRelationshipMapsConcurrently =
2412
+ type === "read" &&
2413
+ this[_level] === 0 &&
2414
+ this[_mapping_concurrency] > 1 &&
2415
+ modifier?.spiceModifier?.kind === "relationship_map";
2416
+
2417
+ if (!canRunRelationshipMapsConcurrently) {
2418
+ const result = await invokeModifier(modifier, modifierIndex, data);
2419
+ if (result.hasValue) data = result.value;
2420
+ modifierIndex += 1;
2421
+ continue;
2422
+ }
2423
+
2424
+ const group = [];
2425
+ const destinations = new Set();
2426
+ while (modifierIndex < modifiers.length) {
2427
+ const candidate = modifiers[modifierIndex];
2428
+ const metadata = candidate?.spiceModifier;
2429
+ if (metadata?.kind !== "relationship_map") break;
2430
+ if (
2431
+ metadata.store_property &&
2432
+ destinations.has(metadata.store_property)
2433
+ ) {
2434
+ break;
2435
+ }
2436
+ if (metadata.store_property) {
2437
+ destinations.add(metadata.store_property);
2438
+ }
2439
+ group.push({ modifier: candidate, index: modifierIndex, metadata });
2440
+ modifierIndex += 1;
2441
+ }
2442
+
2443
+ const groupQueuedAt = process.hrtime.bigint();
2444
+ let activeRequestMappings = 0;
2445
+ const executeGroup = async () =>
2446
+ await runWithConcurrency(
2447
+ group,
2448
+ this[_mapping_concurrency],
2449
+ async (entry) => {
2450
+ const requestQueueWaitMs =
2451
+ Number(process.hrtime.bigint() - groupQueuedAt) / 1e6;
2452
+ const globalSlot = await acquireGlobalMappingSlot();
2453
+ activeRequestMappings += 1;
2454
+ const profileMetadata = {
2455
+ source_property: entry.metadata.source_property,
2456
+ store_property: entry.metadata.store_property,
2457
+ request_queue_wait_ms:
2458
+ Math.round(requestQueueWaitMs * 100) / 100,
2459
+ global_queue_wait_ms:
2460
+ Math.round(globalSlot.waitMs * 100) / 100,
2461
+ active_request_mappings: activeRequestMappings,
2462
+ request_limit: this[_mapping_concurrency],
2463
+ active_global_mappings: globalSlot.active,
2464
+ global_limit: GLOBAL_MAPPING_CONCURRENCY,
2465
+ };
2466
+
2467
+ try {
2468
+ const execute = async () =>
2469
+ await invokeModifier(entry.modifier, entry.index, data);
2470
+ if (p) {
2471
+ return await p.track(
2472
+ `${this.type}.map.concurrent.${entry.metadata.source_property}`,
2473
+ execute,
2474
+ profileMetadata,
2475
+ );
2476
+ }
2477
+ return await execute();
2478
+ } finally {
2479
+ activeRequestMappings -= 1;
2480
+ globalSlot.release();
2481
+ }
2482
+ },
2483
+ );
2484
+
2485
+ const groupMetadata = {
2486
+ map_count: group.length,
2487
+ request_limit: this[_mapping_concurrency],
2488
+ global_limit: GLOBAL_MAPPING_CONCURRENCY,
2489
+ };
2490
+ const groupResults =
2491
+ p ?
2492
+ await p.track(
2493
+ `${this.type}.map.concurrent`,
2494
+ executeGroup,
2495
+ groupMetadata,
2496
+ )
2497
+ : await executeGroup();
2498
+
2499
+ for (const result of groupResults) {
2500
+ if (result?.hasValue) data = result.value;
2501
+ }
2278
2502
  }
2279
2503
 
2280
2504
  // Ensure data is always an array for consistent processing.
@@ -7,7 +7,9 @@ describe('SpiceModel type-specific mapping depth', () => {
7
7
  capture.constructorArgs.push(args);
8
8
  }
9
9
 
10
- async getMulti({ ids }) {
10
+ async getMulti(args) {
11
+ const { ids } = args;
12
+ capture.getMultiArgs.push(args);
11
13
  capture.getMultiIds.push(ids);
12
14
  return records.filter((record) => ids.includes(record.id));
13
15
  }
@@ -15,8 +17,16 @@ describe('SpiceModel type-specific mapping depth', () => {
15
17
  }
16
18
 
17
19
  function createMappedModel(args = {}) {
18
- const scalarCapture = { constructorArgs: [], getMultiIds: [] };
19
- const arrayCapture = { constructorArgs: [], getMultiIds: [] };
20
+ const scalarCapture = {
21
+ constructorArgs: [],
22
+ getMultiArgs: [],
23
+ getMultiIds: [],
24
+ };
25
+ const arrayCapture = {
26
+ constructorArgs: [],
27
+ getMultiArgs: [],
28
+ getMultiIds: [],
29
+ };
20
30
  const ScalarModel = createRelatedModel(
21
31
  [{ id: 'group-1', name: 'Administrators' }],
22
32
  scalarCapture
@@ -51,6 +61,41 @@ describe('SpiceModel type-specific mapping depth', () => {
51
61
  return { model, scalarCapture, arrayCapture };
52
62
  }
53
63
 
64
+ function createConcurrencyModel(mappingConcurrency, extraArgs = {}) {
65
+ const stats = { active: 0, maxActive: 0 };
66
+ class RelatedModel {
67
+ async getMulti({ ids }) {
68
+ stats.active += 1;
69
+ stats.maxActive = Math.max(stats.maxActive, stats.active);
70
+ await new Promise((resolve) => setImmediate(resolve));
71
+ stats.active -= 1;
72
+ return ids.map((id) => ({ id, name: id }));
73
+ }
74
+ }
75
+
76
+ const props = {};
77
+ const record = { id: 'parent-concurrent', type: 'parent' };
78
+ for (let index = 1; index <= 3; index += 1) {
79
+ const field = `relationship_${index}`;
80
+ props[field] = {
81
+ type: 'string',
82
+ map: { type: 'model', reference: RelatedModel }
83
+ };
84
+ record[field] = `related-${index}`;
85
+ }
86
+
87
+ const model = createTestModel({
88
+ type: 'parent',
89
+ args: {
90
+ ...extraArgs,
91
+ ...(mappingConcurrency ? { mapping_concurrency: mappingConcurrency } : {})
92
+ },
93
+ props
94
+ });
95
+ seedDatabase(model, record);
96
+ return { model, stats };
97
+ }
98
+
54
99
  test('uses scalar depth 1 and array depth 3 from the request profile', async () => {
55
100
  const ctx = {
56
101
  state: {
@@ -127,7 +172,8 @@ describe('SpiceModel type-specific mapping depth', () => {
127
172
  test('propagates both depths to related model instances', async () => {
128
173
  const { model, arrayCapture } = createMappedModel({
129
174
  mapping_dept_scalar: 1,
130
- mapping_dept_array: 3
175
+ mapping_dept_array: 3,
176
+ mapping_concurrency: 6
131
177
  });
132
178
 
133
179
  await model.getMulti({ ids: ['parent-1'] });
@@ -136,13 +182,105 @@ describe('SpiceModel type-specific mapping depth', () => {
136
182
  expect.objectContaining({
137
183
  mapping_dept_scalar: 1,
138
184
  mapping_dept_array: 3,
185
+ mapping_concurrency: 6,
139
186
  _level: 1
140
187
  })
141
188
  );
142
189
  });
143
190
 
191
+ test('runs independent relationship maps with bounded concurrency', async () => {
192
+ const { model, stats } = createConcurrencyModel(2);
193
+
194
+ const result = await model.get({ id: 'parent-concurrent' });
195
+
196
+ expect(stats.maxActive).toBe(2);
197
+ expect(result.relationship_1).toEqual(
198
+ expect.objectContaining({ id: 'related-1' })
199
+ );
200
+ expect(result.relationship_3).toEqual(
201
+ expect.objectContaining({ id: 'related-3' })
202
+ );
203
+ });
204
+
205
+ test('keeps relationship maps sequential when concurrency is not enabled', async () => {
206
+ const { model, stats } = createConcurrencyModel();
207
+
208
+ await model.get({ id: 'parent-concurrent' });
209
+
210
+ expect(stats.maxActive).toBe(1);
211
+ });
212
+
213
+ test('keeps nested relationship maps sequential to avoid recursive slot use', async () => {
214
+ const { model, stats } = createConcurrencyModel(2, { _level: 1 });
215
+
216
+ await model.get({ id: 'parent-concurrent' });
217
+
218
+ expect(stats.maxActive).toBe(1);
219
+ });
220
+
221
+ test('profiles concurrent groups and mapping slot pressure', async () => {
222
+ const calls = [];
223
+ const profiler = {
224
+ track: jest.fn(async (name, execute, metadata) => {
225
+ calls.push({ name, metadata });
226
+ return await execute();
227
+ })
228
+ };
229
+ const { model } = createConcurrencyModel(2, { ctx: { profiler } });
230
+
231
+ await model.get({ id: 'parent-concurrent' });
232
+
233
+ expect(calls).toEqual(
234
+ expect.arrayContaining([
235
+ expect.objectContaining({
236
+ name: 'parent.map.concurrent',
237
+ metadata: expect.objectContaining({
238
+ map_count: 3,
239
+ request_limit: 2
240
+ })
241
+ }),
242
+ expect.objectContaining({
243
+ name: 'parent.map.concurrent.relationship_1',
244
+ metadata: expect.objectContaining({
245
+ request_limit: 2,
246
+ active_request_mappings: expect.any(Number),
247
+ active_global_mappings: expect.any(Number),
248
+ request_queue_wait_ms: expect.any(Number),
249
+ global_queue_wait_ms: expect.any(Number)
250
+ })
251
+ })
252
+ ])
253
+ );
254
+ });
255
+
256
+ test('skips serialization for mapped child reads when requested by the profile', async () => {
257
+ const ctx = {
258
+ state: {
259
+ read_profile: {
260
+ mapping_dept_scalar: 2,
261
+ mapping_dept_array: 3,
262
+ skip_mapped_serialization: true
263
+ }
264
+ }
265
+ };
266
+ const { model, scalarCapture, arrayCapture } = createMappedModel({ ctx });
267
+
268
+ const result = await model.get({ id: 'parent-1' });
269
+
270
+ expect(result.group).toEqual(
271
+ expect.objectContaining({ id: 'group-1', name: 'Administrators' })
272
+ );
273
+ expect(result.children).toHaveLength(2);
274
+ expect(scalarCapture.getMultiArgs[0]).toEqual(
275
+ expect.objectContaining({ skip_hooks: true, skip_read_serialize: true })
276
+ );
277
+ expect(arrayCapture.getMultiArgs[0]).toEqual(
278
+ expect.objectContaining({ skip_hooks: true, skip_read_serialize: true })
279
+ );
280
+ });
281
+
144
282
  test('does not apply the request read profile to write serialization', async () => {
145
- const capture = { constructorArgs: [], getMultiIds: [] };
283
+ const capture = { constructorArgs: [], getMultiArgs: [], getMultiIds: [] };
146
284
  const RelatedModel = createRelatedModel(
147
285
  [{ id: 'group-1', name: 'Administrators' }],
148
286
  capture
@@ -0,0 +1,114 @@
1
+ const { createTestModel } = require("../helpers/modelFactory");
2
+
3
+ describe("SpiceModel query consistency", () => {
4
+ test("keeps query scope separate from query consistency", async () => {
5
+ const model = createTestModel({ type: "application" });
6
+ model.database.search = jest.fn(async () => ({ data: [], total: null }));
7
+
8
+ await model.fetchResults(
9
+ {
10
+ ids: [],
11
+ query_scope: "cluster",
12
+ scan_consistency: "request_plus",
13
+ scan_wait: 2500,
14
+ _join: "",
15
+ },
16
+ "",
17
+ [],
18
+ );
19
+
20
+ expect(model.database.search).toHaveBeenCalledWith(
21
+ "application",
22
+ "",
23
+ "",
24
+ undefined,
25
+ undefined,
26
+ undefined,
27
+ undefined,
28
+ "cluster",
29
+ "",
30
+ {
31
+ scan_consistency: "request_plus",
32
+ consistent_with: undefined,
33
+ scan_wait: 2500,
34
+ },
35
+ );
36
+ });
37
+
38
+ test.each([true, "true", "request_plus"])(
39
+ "maps legacy statement_consistent=%s to request_plus",
40
+ (legacyValue) => {
41
+ const model = createTestModel({ type: "application" });
42
+
43
+ expect(
44
+ model.getQueryOptions({ statement_consistent: legacyValue }),
45
+ ).toEqual(
46
+ expect.objectContaining({ scan_consistency: "request_plus" }),
47
+ );
48
+ },
49
+ );
50
+
51
+ test.each([false, "false"])(
52
+ "maps legacy statement_consistent=%s to not_bounded",
53
+ (legacyValue) => {
54
+ const model = createTestModel({ type: "application" });
55
+
56
+ expect(
57
+ model.getQueryOptions({ statement_consistent: legacyValue }),
58
+ ).toEqual(
59
+ expect.objectContaining({ scan_consistency: "not_bounded" }),
60
+ );
61
+ },
62
+ );
63
+
64
+ test("explicit scan_consistency takes precedence over the legacy flag", () => {
65
+ const model = createTestModel({ type: "application" });
66
+
67
+ expect(
68
+ model.getQueryOptions({
69
+ scan_consistency: "not_bounded",
70
+ statement_consistent: true,
71
+ }),
72
+ ).toEqual(expect.objectContaining({ scan_consistency: "not_bounded" }));
73
+ });
74
+
75
+ test("strongly consistent reads bypass the model query cache", () => {
76
+ const model = createTestModel({ type: "application" });
77
+
78
+ expect(model.requiresFreshQuery({ scan_consistency: "request_plus" })).toBe(
79
+ true,
80
+ );
81
+ expect(model.requiresFreshQuery({ statement_consistent: true })).toBe(true);
82
+ expect(model.requiresFreshQuery({ consistent_with: {} })).toBe(true);
83
+ expect(model.requiresFreshQuery({ scan_consistency: "not_bounded" })).toBe(
84
+ false,
85
+ );
86
+ });
87
+
88
+ test("custom queries receive the same query options", async () => {
89
+ const model = createTestModel({ type: "application" });
90
+ model.database.query = jest.fn(async () => ({ rows: [] }));
91
+ const mutationState = { token: "test-token" };
92
+
93
+ await model.fetchResults(
94
+ {
95
+ ids: ["application-1"],
96
+ is_custom_query: "true",
97
+ query_scope: "scope",
98
+ consistent_with: mutationState,
99
+ },
100
+ "SELECT 1",
101
+ [],
102
+ );
103
+
104
+ expect(model.database.query).toHaveBeenCalledWith(
105
+ "SELECT 1",
106
+ "scope",
107
+ {
108
+ scan_consistency: undefined,
109
+ consistent_with: mutationState,
110
+ scan_wait: undefined,
111
+ },
112
+ );
113
+ });
114
+ });