spice-js 2.7.30 → 2.7.31

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 {
@@ -659,7 +730,7 @@ class SpiceModel {
659
730
  // ⚡ Profiling: use track() for proper async context forking
660
731
  var p = (_this6$_ctx = _this6[_ctx]) == null ? void 0 : _this6$_ctx.profiler;
661
732
  var doGetMulti = /*#__PURE__*/function () {
662
- var _ref9 = _asyncToGenerator(function* () {
733
+ var _ref0 = _asyncToGenerator(function* () {
663
734
  if (!args) {
664
735
  args = {};
665
736
  }
@@ -701,7 +772,7 @@ class SpiceModel {
701
772
  return results;
702
773
  });
703
774
  return function doGetMulti() {
704
- return _ref9.apply(this, arguments);
775
+ return _ref0.apply(this, arguments);
705
776
  };
706
777
  }();
707
778
  try {
@@ -757,7 +828,7 @@ class SpiceModel {
757
828
  // Profiling: use track() for proper async context forking
758
829
  var p = (_this8$_ctx = _this8[_ctx]) == null ? void 0 : _this8$_ctx.profiler;
759
830
  var doUpdate = /*#__PURE__*/function () {
760
- var _ref0 = _asyncToGenerator(function* () {
831
+ var _ref1 = _asyncToGenerator(function* () {
761
832
  _this8.updated_at = new SDate().now();
762
833
  var results = yield _this8.database.get(args.id);
763
834
  var item_exist = yield _this8.exist(results);
@@ -801,7 +872,7 @@ class SpiceModel {
801
872
  });
802
873
  });
803
874
  return function doUpdate() {
804
- return _ref0.apply(this, arguments);
875
+ return _ref1.apply(this, arguments);
805
876
  };
806
877
  }();
807
878
  try {
@@ -827,7 +898,7 @@ class SpiceModel {
827
898
  // Profiling: use track() for proper async context forking
828
899
  var p = (_this9$_ctx = _this9[_ctx]) == null ? void 0 : _this9$_ctx.profiler;
829
900
  var doCreate = /*#__PURE__*/function () {
830
- var _ref10 = _asyncToGenerator(function* () {
901
+ var _ref11 = _asyncToGenerator(function* () {
831
902
  var form;
832
903
  _this9.created_at = new SDate().now();
833
904
  args = _.defaults(args, {
@@ -873,7 +944,7 @@ class SpiceModel {
873
944
  });
874
945
  });
875
946
  return function doCreate() {
876
- return _ref10.apply(this, arguments);
947
+ return _ref11.apply(this, arguments);
877
948
  };
878
949
  }();
879
950
  try {
@@ -911,7 +982,7 @@ class SpiceModel {
911
982
  // Profiling: use track() for proper async context forking
912
983
  var p = (_this1$_ctx = _this1[_ctx]) == null ? void 0 : _this1$_ctx.profiler;
913
984
  var doDelete = /*#__PURE__*/function () {
914
- var _ref12 = _asyncToGenerator(function* () {
985
+ var _ref13 = _asyncToGenerator(function* () {
915
986
  var item_exist = yield _this1.exist(args.id);
916
987
  if (!item_exist) {
917
988
  throw new Error(_this1.type + " does not exist.");
@@ -922,7 +993,7 @@ class SpiceModel {
922
993
  }
923
994
  var delete_response = {};
924
995
  var dbOperation = /*#__PURE__*/function () {
925
- var _ref13 = _asyncToGenerator(function* () {
996
+ var _ref14 = _asyncToGenerator(function* () {
926
997
  if (args.hard) {
927
998
  delete_response = yield _this1.database.delete(args.id);
928
999
  _this1.setMonitor();
@@ -933,7 +1004,7 @@ class SpiceModel {
933
1004
  }
934
1005
  });
935
1006
  return function dbOperation() {
936
- return _ref13.apply(this, arguments);
1007
+ return _ref14.apply(this, arguments);
937
1008
  };
938
1009
  }();
939
1010
  if (p) {
@@ -949,7 +1020,7 @@ class SpiceModel {
949
1020
  return {};
950
1021
  });
951
1022
  return function doDelete() {
952
- return _ref12.apply(this, arguments);
1023
+ return _ref13.apply(this, arguments);
953
1024
  };
954
1025
  }();
955
1026
  try {
@@ -1139,12 +1210,12 @@ class SpiceModel {
1139
1210
  }
1140
1211
  createJoinSection(mappedNestings) {
1141
1212
  if (!mappedNestings || mappedNestings.length === 0) return "";
1142
- return mappedNestings.map(_ref14 => {
1213
+ return mappedNestings.map(_ref15 => {
1143
1214
  var {
1144
1215
  alias,
1145
1216
  reference,
1146
1217
  is_array
1147
- } = _ref14;
1218
+ } = _ref15;
1148
1219
  var keyspace = (0, _fix.fixCollection)(reference);
1149
1220
  if (is_array === true) {
1150
1221
  return "LEFT NEST `" + keyspace + "` AS `" + alias + "` ON KEYS `" + this.type + "`.`" + alias + "`";
@@ -1289,7 +1360,7 @@ class SpiceModel {
1289
1360
  // Profiling: use track() for proper async context forking
1290
1361
  var p = (_this10$_ctx = _this10[_ctx]) == null ? void 0 : _this10$_ctx.profiler;
1291
1362
  var doList = /*#__PURE__*/function () {
1292
- var _ref15 = _asyncToGenerator(function* () {
1363
+ var _ref16 = _asyncToGenerator(function* () {
1293
1364
  var _args1, _args10;
1294
1365
  _this10.applyMappingDepthArgs(args);
1295
1366
 
@@ -1368,7 +1439,7 @@ class SpiceModel {
1368
1439
  return results;
1369
1440
  });
1370
1441
  return function doList() {
1371
- return _ref15.apply(this, arguments);
1442
+ return _ref16.apply(this, arguments);
1372
1443
  };
1373
1444
  }();
1374
1445
  try {
@@ -1422,7 +1493,7 @@ class SpiceModel {
1422
1493
  // Profiling: use track() for proper async context forking
1423
1494
  var p = (_this11$_ctx = _this11[_ctx]) == null ? void 0 : _this11$_ctx.profiler;
1424
1495
  var doFetch = /*#__PURE__*/function () {
1425
- var _ref16 = _asyncToGenerator(function* () {
1496
+ var _ref17 = _asyncToGenerator(function* () {
1426
1497
  if (args.is_custom_query === "true" && args.ids.length > 0) {
1427
1498
  console.log("Fetching results from database", _this11.type, args, query, mappedNestings);
1428
1499
  return yield _this11.database.query(query);
@@ -1435,7 +1506,7 @@ class SpiceModel {
1435
1506
  }
1436
1507
  });
1437
1508
  return function doFetch() {
1438
- return _ref16.apply(this, arguments);
1509
+ return _ref17.apply(this, arguments);
1439
1510
  };
1440
1511
  }();
1441
1512
  if (p) {
@@ -1444,12 +1515,12 @@ class SpiceModel {
1444
1515
  return yield doFetch();
1445
1516
  })();
1446
1517
  }
1447
- addHook(_ref17) {
1518
+ addHook(_ref18) {
1448
1519
  var {
1449
1520
  operation,
1450
1521
  when,
1451
1522
  execute
1452
- } = _ref17;
1523
+ } = _ref18;
1453
1524
  this[_hooks][operation][when].push(execute);
1454
1525
  }
1455
1526
  run_hook(data, op, when, old_data) {
@@ -1572,7 +1643,7 @@ class SpiceModel {
1572
1643
 
1573
1644
  // ⚡ Wrap in profiler track() to ensure proper async context for child operations
1574
1645
  var fetchRelated = /*#__PURE__*/function () {
1575
- var _ref18 = _asyncToGenerator(function* () {
1646
+ var _ref19 = _asyncToGenerator(function* () {
1576
1647
  return yield Promise.allSettled(_.map(classes, obj => {
1577
1648
  var objInstance = new obj(_extends({}, _this13[_args], {
1578
1649
  skip_cache: _this13[_skip_cache],
@@ -1580,6 +1651,8 @@ class SpiceModel {
1580
1651
  mapping_dept: _this13[_mapping_dept],
1581
1652
  mapping_dept_scalar: _this13[_mapping_dept_scalar],
1582
1653
  mapping_dept_array: _this13[_mapping_dept_array],
1654
+ mapping_concurrency: _this13[_mapping_concurrency],
1655
+ skip_mapped_serialization: _this13[_skip_mapped_serialization],
1583
1656
  mapping_dept_exempt: _this13[_mapping_dept_exempt],
1584
1657
  _columns: columnArray.join(","),
1585
1658
  _current_path: childPath
@@ -1587,12 +1660,13 @@ class SpiceModel {
1587
1660
  return objInstance.getMulti({
1588
1661
  skip_hooks: true,
1589
1662
  ids: ids,
1590
- columns: columnArray
1663
+ columns: columnArray,
1664
+ skip_read_serialize: _this13[_skip_mapped_serialization]
1591
1665
  });
1592
1666
  }));
1593
1667
  });
1594
1668
  return function fetchRelated() {
1595
- return _ref18.apply(this, arguments);
1669
+ return _ref19.apply(this, arguments);
1596
1670
  };
1597
1671
  }();
1598
1672
  var returned_all;
@@ -1693,7 +1767,7 @@ class SpiceModel {
1693
1767
 
1694
1768
  // ⚡ Wrap in profiler track() to ensure proper async context for child operations
1695
1769
  var fetchRelated = /*#__PURE__*/function () {
1696
- var _ref19 = _asyncToGenerator(function* () {
1770
+ var _ref20 = _asyncToGenerator(function* () {
1697
1771
  return yield Promise.allSettled(_.map(classes, obj => {
1698
1772
  return new obj(_extends({}, _this14[_args], {
1699
1773
  skip_cache: _this14[_skip_cache],
@@ -1701,18 +1775,21 @@ class SpiceModel {
1701
1775
  mapping_dept: _this14[_mapping_dept],
1702
1776
  mapping_dept_scalar: _this14[_mapping_dept_scalar],
1703
1777
  mapping_dept_array: _this14[_mapping_dept_array],
1778
+ mapping_concurrency: _this14[_mapping_concurrency],
1779
+ skip_mapped_serialization: _this14[_skip_mapped_serialization],
1704
1780
  mapping_dept_exempt: _this14[_mapping_dept_exempt],
1705
1781
  _columns: columnArray.join(","),
1706
1782
  _current_path: childPath
1707
1783
  })).getMulti({
1708
1784
  skip_hooks: true,
1709
1785
  ids: ids,
1710
- columns: columnArray
1786
+ columns: columnArray,
1787
+ skip_read_serialize: _this14[_skip_mapped_serialization]
1711
1788
  });
1712
1789
  }));
1713
1790
  });
1714
1791
  return function fetchRelated() {
1715
- return _ref19.apply(this, arguments);
1792
+ return _ref20.apply(this, arguments);
1716
1793
  };
1717
1794
  }();
1718
1795
  var returned_all;
@@ -1760,12 +1837,22 @@ class SpiceModel {
1760
1837
  return original_is_array ? data : data[0];
1761
1838
  })();
1762
1839
  }
1763
- addModifier(_ref20) {
1840
+ addModifier(_ref21) {
1764
1841
  var {
1765
1842
  when,
1766
- execute
1767
- } = _ref20;
1843
+ execute,
1844
+ kind,
1845
+ source_property,
1846
+ store_property
1847
+ } = _ref21;
1768
1848
  if (this[_serializers][when]) {
1849
+ if (kind) {
1850
+ execute.spiceModifier = {
1851
+ kind,
1852
+ source_property,
1853
+ store_property
1854
+ };
1855
+ }
1769
1856
  this[_serializers][when]["modifiers"].push(execute);
1770
1857
  }
1771
1858
  }
@@ -1833,6 +1920,9 @@ class SpiceModel {
1833
1920
  {
1834
1921
  _this15.addModifier({
1835
1922
  when: properties[i].map.when || "read",
1923
+ kind: "relationship_map",
1924
+ source_property: i,
1925
+ store_property: properties[i].map.destination || i,
1836
1926
  execute: function () {
1837
1927
  var _execute = _asyncToGenerator(function* (data, old_data, ctx, type, args, mapping_dept, level, columns, serializationType) {
1838
1928
  // Skip if columns are specified but this field isn't included
@@ -1841,7 +1931,7 @@ class SpiceModel {
1841
1931
  }
1842
1932
  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
1933
  });
1844
- function execute(_x, _x2, _x3, _x4, _x5, _x6, _x7, _x8, _x9) {
1934
+ function execute(_x4, _x5, _x6, _x7, _x8, _x9, _x0, _x1, _x10) {
1845
1935
  return _execute.apply(this, arguments);
1846
1936
  }
1847
1937
  return execute;
@@ -1854,6 +1944,9 @@ class SpiceModel {
1854
1944
  {
1855
1945
  _this15.addModifier({
1856
1946
  when: properties[i].map.when || "read",
1947
+ kind: "relationship_map",
1948
+ source_property: i,
1949
+ store_property: properties[i].map.destination || i,
1857
1950
  execute: function () {
1858
1951
  var _execute2 = _asyncToGenerator(function* (data, old_data, ctx, type, args, mapping_dept, level, columns, serializationType) {
1859
1952
  // Skip if columns are specified but this field isn't included
@@ -1862,7 +1955,7 @@ class SpiceModel {
1862
1955
  }
1863
1956
  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
1957
  });
1865
- function execute(_x0, _x1, _x10, _x11, _x12, _x13, _x14, _x15, _x16) {
1958
+ function execute(_x11, _x12, _x13, _x14, _x15, _x16, _x17, _x18, _x19) {
1866
1959
  return _execute2.apply(this, arguments);
1867
1960
  }
1868
1961
  return execute;
@@ -1894,7 +1987,7 @@ class SpiceModel {
1894
1987
  // Profiling: use track() for proper async context forking
1895
1988
  var p = (_this16$_ctx = _this16[_ctx]) == null ? void 0 : _this16$_ctx.profiler;
1896
1989
  var doSerialize = /*#__PURE__*/function () {
1897
- var _ref21 = _asyncToGenerator(function* () {
1990
+ var _ref22 = _asyncToGenerator(function* () {
1898
1991
  var _this16$_serializers, _this16$_serializers$;
1899
1992
  // Early exit if serialization should not run.
1900
1993
  if (!_this16.shouldSerializerRun(args, type)) {
@@ -1909,19 +2002,117 @@ class SpiceModel {
1909
2002
 
1910
2003
  // Cache the modifiers lookup for the specified type.
1911
2004
  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");
2005
+ var invokeModifier = /*#__PURE__*/function () {
2006
+ var _ref23 = _asyncToGenerator(function* (modifier, index, inputData) {
2007
+ try {
2008
+ var result = yield modifier(inputData, old_data, _this16[_ctx], _this16.type, args, _this16[_mapping_dept], _this16[_level], _this16[_columns], type);
2009
+ if (result !== undefined) {
2010
+ return {
2011
+ hasValue: true,
2012
+ value: result
2013
+ };
2014
+ }
2015
+ console.warn("Modifier #" + index + " for type=" + _this16.type + " returned undefined, keeping previous data");
2016
+ } catch (error) {
2017
+ console.error("Modifier error in do_serialize (type=" + _this16.type + ", modifier #" + index + "):", error instanceof Error ? error.stack : "Non-Error thrown: " + JSON.stringify(error));
2018
+ }
2019
+ return {
2020
+ hasValue: false
2021
+ };
2022
+ });
2023
+ return function invokeModifier(_x20, _x21, _x22) {
2024
+ return _ref23.apply(this, arguments);
2025
+ };
2026
+ }();
2027
+ var modifierIndex = 0;
2028
+ var _loop2 = function* _loop2() {
2029
+ var _modifier$spiceModifi;
2030
+ var modifier = modifiers[modifierIndex];
2031
+ 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";
2032
+ if (!canRunRelationshipMapsConcurrently) {
2033
+ var result = yield invokeModifier(modifier, modifierIndex, data);
2034
+ if (result.hasValue) data = result.value;
2035
+ modifierIndex += 1;
2036
+ return "continue";
2037
+ }
2038
+ var group = [];
2039
+ var destinations = new Set();
2040
+ while (modifierIndex < modifiers.length) {
2041
+ var candidate = modifiers[modifierIndex];
2042
+ var metadata = candidate == null ? void 0 : candidate.spiceModifier;
2043
+ if ((metadata == null ? void 0 : metadata.kind) !== "relationship_map") break;
2044
+ if (metadata.store_property && destinations.has(metadata.store_property)) {
2045
+ break;
2046
+ }
2047
+ if (metadata.store_property) {
2048
+ destinations.add(metadata.store_property);
1921
2049
  }
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));
2050
+ group.push({
2051
+ modifier: candidate,
2052
+ index: modifierIndex,
2053
+ metadata
2054
+ });
2055
+ modifierIndex += 1;
1924
2056
  }
2057
+ var groupQueuedAt = process.hrtime.bigint();
2058
+ var activeRequestMappings = 0;
2059
+ var executeGroup = /*#__PURE__*/function () {
2060
+ var _ref24 = _asyncToGenerator(function* () {
2061
+ return yield runWithConcurrency(group, _this16[_mapping_concurrency], /*#__PURE__*/function () {
2062
+ var _ref25 = _asyncToGenerator(function* (entry) {
2063
+ var requestQueueWaitMs = Number(process.hrtime.bigint() - groupQueuedAt) / 1e6;
2064
+ var globalSlot = yield acquireGlobalMappingSlot();
2065
+ activeRequestMappings += 1;
2066
+ var profileMetadata = {
2067
+ source_property: entry.metadata.source_property,
2068
+ store_property: entry.metadata.store_property,
2069
+ request_queue_wait_ms: Math.round(requestQueueWaitMs * 100) / 100,
2070
+ global_queue_wait_ms: Math.round(globalSlot.waitMs * 100) / 100,
2071
+ active_request_mappings: activeRequestMappings,
2072
+ request_limit: _this16[_mapping_concurrency],
2073
+ active_global_mappings: globalSlot.active,
2074
+ global_limit: GLOBAL_MAPPING_CONCURRENCY
2075
+ };
2076
+ try {
2077
+ var execute = /*#__PURE__*/function () {
2078
+ var _ref26 = _asyncToGenerator(function* () {
2079
+ return yield invokeModifier(entry.modifier, entry.index, data);
2080
+ });
2081
+ return function execute() {
2082
+ return _ref26.apply(this, arguments);
2083
+ };
2084
+ }();
2085
+ if (p) {
2086
+ return yield p.track(_this16.type + ".map.concurrent." + entry.metadata.source_property, execute, profileMetadata);
2087
+ }
2088
+ return yield execute();
2089
+ } finally {
2090
+ activeRequestMappings -= 1;
2091
+ globalSlot.release();
2092
+ }
2093
+ });
2094
+ return function (_x23) {
2095
+ return _ref25.apply(this, arguments);
2096
+ };
2097
+ }());
2098
+ });
2099
+ return function executeGroup() {
2100
+ return _ref24.apply(this, arguments);
2101
+ };
2102
+ }();
2103
+ var groupMetadata = {
2104
+ map_count: group.length,
2105
+ request_limit: _this16[_mapping_concurrency],
2106
+ global_limit: GLOBAL_MAPPING_CONCURRENCY
2107
+ };
2108
+ var groupResults = p ? yield p.track(_this16.type + ".map.concurrent", executeGroup, groupMetadata) : yield executeGroup();
2109
+ for (var _result of groupResults) {
2110
+ if (_result == null ? void 0 : _result.hasValue) data = _result.value;
2111
+ }
2112
+ };
2113
+ while (modifierIndex < modifiers.length) {
2114
+ var _ret = yield* _loop2();
2115
+ if (_ret === "continue") continue;
1925
2116
  }
1926
2117
 
1927
2118
  // Ensure data is always an array for consistent processing.
@@ -1962,7 +2153,7 @@ class SpiceModel {
1962
2153
  return originalIsArray ? data : data[0];
1963
2154
  });
1964
2155
  return function doSerialize() {
1965
- return _ref21.apply(this, arguments);
2156
+ return _ref22.apply(this, arguments);
1966
2157
  };
1967
2158
  }();
1968
2159
  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.31",
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
  }
@@ -1815,6 +1886,8 @@ export default class SpiceModel {
1815
1886
  mapping_dept: this[_mapping_dept],
1816
1887
  mapping_dept_scalar: this[_mapping_dept_scalar],
1817
1888
  mapping_dept_array: this[_mapping_dept_array],
1889
+ mapping_concurrency: this[_mapping_concurrency],
1890
+ skip_mapped_serialization: this[_skip_mapped_serialization],
1818
1891
  mapping_dept_exempt: this[_mapping_dept_exempt],
1819
1892
  _columns: columnArray.join(","),
1820
1893
  _current_path: childPath,
@@ -1824,6 +1897,7 @@ export default class SpiceModel {
1824
1897
  skip_hooks: true,
1825
1898
  ids: ids,
1826
1899
  columns: columnArray,
1900
+ skip_read_serialize: this[_skip_mapped_serialization],
1827
1901
  });
1828
1902
  }),
1829
1903
  );
@@ -1990,6 +2064,8 @@ export default class SpiceModel {
1990
2064
  mapping_dept: this[_mapping_dept],
1991
2065
  mapping_dept_scalar: this[_mapping_dept_scalar],
1992
2066
  mapping_dept_array: this[_mapping_dept_array],
2067
+ mapping_concurrency: this[_mapping_concurrency],
2068
+ skip_mapped_serialization: this[_skip_mapped_serialization],
1993
2069
  mapping_dept_exempt: this[_mapping_dept_exempt],
1994
2070
  _columns: columnArray.join(","),
1995
2071
  _current_path: childPath,
@@ -1997,6 +2073,7 @@ export default class SpiceModel {
1997
2073
  skip_hooks: true,
1998
2074
  ids: ids,
1999
2075
  columns: columnArray,
2076
+ skip_read_serialize: this[_skip_mapped_serialization],
2000
2077
  });
2001
2078
  }),
2002
2079
  );
@@ -2070,8 +2147,21 @@ export default class SpiceModel {
2070
2147
  return original_is_array ? data : data[0];
2071
2148
  }
2072
2149
 
2073
- addModifier({ when, execute }) {
2150
+ addModifier({
2151
+ when,
2152
+ execute,
2153
+ kind,
2154
+ source_property,
2155
+ store_property,
2156
+ }) {
2074
2157
  if (this[_serializers][when]) {
2158
+ if (kind) {
2159
+ execute.spiceModifier = {
2160
+ kind,
2161
+ source_property,
2162
+ store_property,
2163
+ };
2164
+ }
2075
2165
  this[_serializers][when]["modifiers"].push(execute);
2076
2166
  }
2077
2167
  }
@@ -2145,6 +2235,9 @@ export default class SpiceModel {
2145
2235
  case "string": {
2146
2236
  this.addModifier({
2147
2237
  when: properties[i].map.when || "read",
2238
+ kind: "relationship_map",
2239
+ source_property: i,
2240
+ store_property: properties[i].map.destination || i,
2148
2241
  execute: async (
2149
2242
  data,
2150
2243
  old_data,
@@ -2182,6 +2275,9 @@ export default class SpiceModel {
2182
2275
  case "array": {
2183
2276
  this.addModifier({
2184
2277
  when: properties[i].map.when || "read",
2278
+ kind: "relationship_map",
2279
+ source_property: i,
2280
+ store_property: properties[i].map.destination || i,
2185
2281
  execute: async (
2186
2282
  data,
2187
2283
  old_data,
@@ -2245,11 +2341,10 @@ export default class SpiceModel {
2245
2341
 
2246
2342
  // Cache the modifiers lookup for the specified type.
2247
2343
  const modifiers = this[_serializers]?.[type]?.modifiers || [];
2248
- for (let i = 0; i < modifiers.length; i++) {
2249
- const modifier = modifiers[i];
2344
+ const invokeModifier = async (modifier, index, inputData) => {
2250
2345
  try {
2251
2346
  const result = await modifier(
2252
- data,
2347
+ inputData,
2253
2348
  old_data,
2254
2349
  this[_ctx],
2255
2350
  this.type,
@@ -2259,22 +2354,117 @@ export default class SpiceModel {
2259
2354
  this[_columns],
2260
2355
  type,
2261
2356
  );
2262
- // Guard against modifiers that return undefined
2263
2357
  if (result !== undefined) {
2264
- data = result;
2265
- } else {
2266
- console.warn(
2267
- `Modifier #${i} for type=${this.type} returned undefined, keeping previous data`,
2268
- );
2358
+ return { hasValue: true, value: result };
2269
2359
  }
2360
+ console.warn(
2361
+ `Modifier #${index} for type=${this.type} returned undefined, keeping previous data`,
2362
+ );
2270
2363
  } catch (error) {
2271
2364
  console.error(
2272
- `Modifier error in do_serialize (type=${this.type}, modifier #${i}):`,
2365
+ `Modifier error in do_serialize (type=${this.type}, modifier #${index}):`,
2273
2366
  error instanceof Error ?
2274
2367
  error.stack
2275
2368
  : `Non-Error thrown: ${JSON.stringify(error)}`,
2276
2369
  );
2277
2370
  }
2371
+ return { hasValue: false };
2372
+ };
2373
+
2374
+ let modifierIndex = 0;
2375
+ while (modifierIndex < modifiers.length) {
2376
+ const modifier = modifiers[modifierIndex];
2377
+ const canRunRelationshipMapsConcurrently =
2378
+ type === "read" &&
2379
+ this[_level] === 0 &&
2380
+ this[_mapping_concurrency] > 1 &&
2381
+ modifier?.spiceModifier?.kind === "relationship_map";
2382
+
2383
+ if (!canRunRelationshipMapsConcurrently) {
2384
+ const result = await invokeModifier(modifier, modifierIndex, data);
2385
+ if (result.hasValue) data = result.value;
2386
+ modifierIndex += 1;
2387
+ continue;
2388
+ }
2389
+
2390
+ const group = [];
2391
+ const destinations = new Set();
2392
+ while (modifierIndex < modifiers.length) {
2393
+ const candidate = modifiers[modifierIndex];
2394
+ const metadata = candidate?.spiceModifier;
2395
+ if (metadata?.kind !== "relationship_map") break;
2396
+ if (
2397
+ metadata.store_property &&
2398
+ destinations.has(metadata.store_property)
2399
+ ) {
2400
+ break;
2401
+ }
2402
+ if (metadata.store_property) {
2403
+ destinations.add(metadata.store_property);
2404
+ }
2405
+ group.push({ modifier: candidate, index: modifierIndex, metadata });
2406
+ modifierIndex += 1;
2407
+ }
2408
+
2409
+ const groupQueuedAt = process.hrtime.bigint();
2410
+ let activeRequestMappings = 0;
2411
+ const executeGroup = async () =>
2412
+ await runWithConcurrency(
2413
+ group,
2414
+ this[_mapping_concurrency],
2415
+ async (entry) => {
2416
+ const requestQueueWaitMs =
2417
+ Number(process.hrtime.bigint() - groupQueuedAt) / 1e6;
2418
+ const globalSlot = await acquireGlobalMappingSlot();
2419
+ activeRequestMappings += 1;
2420
+ const profileMetadata = {
2421
+ source_property: entry.metadata.source_property,
2422
+ store_property: entry.metadata.store_property,
2423
+ request_queue_wait_ms:
2424
+ Math.round(requestQueueWaitMs * 100) / 100,
2425
+ global_queue_wait_ms:
2426
+ Math.round(globalSlot.waitMs * 100) / 100,
2427
+ active_request_mappings: activeRequestMappings,
2428
+ request_limit: this[_mapping_concurrency],
2429
+ active_global_mappings: globalSlot.active,
2430
+ global_limit: GLOBAL_MAPPING_CONCURRENCY,
2431
+ };
2432
+
2433
+ try {
2434
+ const execute = async () =>
2435
+ await invokeModifier(entry.modifier, entry.index, data);
2436
+ if (p) {
2437
+ return await p.track(
2438
+ `${this.type}.map.concurrent.${entry.metadata.source_property}`,
2439
+ execute,
2440
+ profileMetadata,
2441
+ );
2442
+ }
2443
+ return await execute();
2444
+ } finally {
2445
+ activeRequestMappings -= 1;
2446
+ globalSlot.release();
2447
+ }
2448
+ },
2449
+ );
2450
+
2451
+ const groupMetadata = {
2452
+ map_count: group.length,
2453
+ request_limit: this[_mapping_concurrency],
2454
+ global_limit: GLOBAL_MAPPING_CONCURRENCY,
2455
+ };
2456
+ const groupResults =
2457
+ p ?
2458
+ await p.track(
2459
+ `${this.type}.map.concurrent`,
2460
+ executeGroup,
2461
+ groupMetadata,
2462
+ )
2463
+ : await executeGroup();
2464
+
2465
+ for (const result of groupResults) {
2466
+ if (result?.hasValue) data = result.value;
2467
+ }
2278
2468
  }
2279
2469
 
2280
2470
  // 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