kitcn 0.18.0 → 0.20.0

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.
@@ -528,11 +528,19 @@ function getIndexFields(table, index, schema) {
528
528
  fields.push("_id");
529
529
  return fields;
530
530
  }
531
- function getIndexKey(doc, indexFields) {
531
+ /**
532
+ * Read the index key out of a document.
533
+ *
534
+ * Takes field paths already split on `.` rather than the field names: the field
535
+ * list is fixed for the life of a query, so splitting it once at `withIndex`
536
+ * time saves an array allocation per field per document pulled through the
537
+ * stream.
538
+ */
539
+ function getIndexKey(doc, indexFieldPaths) {
532
540
  const key = [];
533
- for (const field of indexFields) {
541
+ for (const path of indexFieldPaths) {
534
542
  let obj = doc;
535
- for (const subfield of field.split(".")) obj = obj[subfield];
543
+ for (const subfield of path) obj = obj[subfield];
536
544
  key.push(obj);
537
545
  }
538
546
  return key;
@@ -729,17 +737,26 @@ var QueryStream = class {
729
737
  }
730
738
  [Symbol.asyncIterator]() {
731
739
  const iterator = this.iterWithKeys()[Symbol.asyncIterator]();
732
- return { async next() {
733
- const result = await iterator.next();
734
- if (result.done) return {
735
- done: true,
736
- value: void 0
737
- };
738
- return {
739
- done: false,
740
- value: result.value[0]
741
- };
742
- } };
740
+ return {
741
+ async next() {
742
+ const result = await iterator.next();
743
+ if (result.done) return {
744
+ done: true,
745
+ value: void 0
746
+ };
747
+ return {
748
+ done: false,
749
+ value: result.value[0]
750
+ };
751
+ },
752
+ async return() {
753
+ await iterator.return?.();
754
+ return {
755
+ done: true,
756
+ value: void 0
757
+ };
758
+ }
759
+ };
743
760
  }
744
761
  };
745
762
  var StreamDatabaseReader = class {
@@ -858,6 +875,7 @@ var OrderedStreamQuery = class extends StreamableQuery {
858
875
  table: this.parent.parent.table,
859
876
  index: this.parent.index,
860
877
  indexFields: this.parent.q.indexFields,
878
+ indexFieldPaths: this.parent.q.indexFieldPaths,
861
879
  order: this.order,
862
880
  bounds: {
863
881
  lowerBound: this.parent.q.lowerBoundIndexKey ?? [],
@@ -876,7 +894,7 @@ var OrderedStreamQuery = class extends StreamableQuery {
876
894
  return db.query(table).withIndex(index, indexRange).order(order);
877
895
  }
878
896
  iterWithKeys() {
879
- const { indexFields } = this.reflect();
897
+ const { indexFieldPaths } = this.reflect();
880
898
  const iterable = this.inner();
881
899
  return { [Symbol.asyncIterator]() {
882
900
  const iterator = iterable[Symbol.asyncIterator]();
@@ -888,7 +906,7 @@ var OrderedStreamQuery = class extends StreamableQuery {
888
906
  };
889
907
  return {
890
908
  done: false,
891
- value: [result.value, getIndexKey(result.value, indexFields)]
909
+ value: [result.value, getIndexKey(result.value, indexFieldPaths)]
892
910
  };
893
911
  } };
894
912
  } };
@@ -950,8 +968,11 @@ var ReflectIndexRange = class {
950
968
  upperBoundInclusive = true;
951
969
  equalityIndexFilter = [];
952
970
  indexFields;
971
+ /** `indexFields`, each split on `.` once, for `getIndexKey`. */
972
+ indexFieldPaths;
953
973
  constructor(indexFields) {
954
974
  this.indexFields = indexFields;
975
+ this.indexFieldPaths = indexFields.map((field) => field.split("."));
955
976
  }
956
977
  eq(field, value) {
957
978
  if (!this.#canLowerBound(field) || !this.#canUpperBound(field)) throw new Error(`Cannot use eq on field '${field}'`);
@@ -1043,16 +1064,24 @@ var ReflectIndexRange = class {
1043
1064
  function mergedStream(streams, orderByIndexFields) {
1044
1065
  return new MergedStream(streams, orderByIndexFields);
1045
1066
  }
1067
+ /**
1068
+ * Marks sources that are already `OrderByStream`s, so `narrow` reuses them
1069
+ * instead of adding a redundant delegation layer per call.
1070
+ *
1071
+ * Module-private on purpose: wrapping is also where the ordering invariant is
1072
+ * checked, so no caller outside this file can skip it.
1073
+ */
1074
+ const PRE_ORDERED_STREAMS = Symbol("preOrderedStreams");
1046
1075
  var MergedStream = class MergedStream extends QueryStream {
1047
1076
  #order;
1048
1077
  #streams;
1049
1078
  #equalityIndexFilter;
1050
1079
  #indexFields;
1051
- constructor(streams, orderByIndexFields) {
1080
+ constructor(streams, orderByIndexFields, preOrdered) {
1052
1081
  super();
1053
1082
  if (streams.length === 0) throw new Error("Cannot union empty array of streams");
1054
1083
  this.#order = allSame(streams.map((stream) => stream.getOrder()), "Cannot merge streams with different orders");
1055
- this.#streams = streams.map((stream) => new OrderByStream(stream, orderByIndexFields));
1084
+ this.#streams = preOrdered === PRE_ORDERED_STREAMS ? streams : streams.map((stream) => new OrderByStream(stream, orderByIndexFields.slice()));
1056
1085
  this.#indexFields = allSame(this.#streams.map((stream) => stream.getIndexFields()), "Cannot merge streams with different index fields. Consider using .orderBy()");
1057
1086
  this.#equalityIndexFilter = commonPrefix(this.#streams.map((stream) => stream.getEqualityIndexFilter()));
1058
1087
  }
@@ -1065,40 +1094,60 @@ var MergedStream = class MergedStream extends QueryStream {
1065
1094
  done: false,
1066
1095
  value: void 0
1067
1096
  }));
1068
- return { async next() {
1069
- await Promise.all(iterators.map(async (iterator, i) => {
1070
- if (!results[i].done && !results[i].value) results[i] = await iterator.next();
1071
- }));
1072
- let minIndexKeyAndIndex;
1073
- for (let i = 0; i < results.length; i++) {
1074
- const result = results[i];
1075
- if (result.done || !result.value) continue;
1076
- const [_, resultIndexKey] = result.value;
1077
- if (minIndexKeyAndIndex === void 0) {
1078
- minIndexKeyAndIndex = [resultIndexKey, i];
1079
- continue;
1097
+ let started = false;
1098
+ let refillIndex;
1099
+ return {
1100
+ async next() {
1101
+ if (started) {
1102
+ if (refillIndex !== void 0) {
1103
+ results[refillIndex] = await iterators[refillIndex].next();
1104
+ refillIndex = void 0;
1105
+ }
1106
+ } else {
1107
+ started = true;
1108
+ await Promise.all(iterators.map(async (iterator, i) => {
1109
+ results[i] = await iterator.next();
1110
+ }));
1080
1111
  }
1081
- const [prevMin, _prevMinIndex] = minIndexKeyAndIndex;
1082
- if (compareKeys({
1083
- value: resultIndexKey,
1084
- kind: "exact"
1085
- }, {
1086
- value: prevMin,
1087
- kind: "exact"
1088
- }) * comparisonInversion < 0) minIndexKeyAndIndex = [resultIndexKey, i];
1112
+ let minIndexKeyAndIndex;
1113
+ for (let i = 0; i < results.length; i++) {
1114
+ const result = results[i];
1115
+ if (result.done || !result.value) continue;
1116
+ const [_, resultIndexKey] = result.value;
1117
+ if (minIndexKeyAndIndex === void 0) {
1118
+ minIndexKeyAndIndex = [resultIndexKey, i];
1119
+ continue;
1120
+ }
1121
+ const [prevMin, _prevMinIndex] = minIndexKeyAndIndex;
1122
+ if (compareKeys({
1123
+ value: resultIndexKey,
1124
+ kind: "exact"
1125
+ }, {
1126
+ value: prevMin,
1127
+ kind: "exact"
1128
+ }) * comparisonInversion < 0) minIndexKeyAndIndex = [resultIndexKey, i];
1129
+ }
1130
+ if (minIndexKeyAndIndex === void 0) return {
1131
+ done: true,
1132
+ value: void 0
1133
+ };
1134
+ const [_, minIndex] = minIndexKeyAndIndex;
1135
+ const result = results[minIndex].value;
1136
+ results[minIndex].value = void 0;
1137
+ refillIndex = minIndex;
1138
+ return {
1139
+ done: false,
1140
+ value: result
1141
+ };
1142
+ },
1143
+ async return() {
1144
+ await Promise.all(iterators.map((iterator) => iterator.return?.()));
1145
+ return {
1146
+ done: true,
1147
+ value: void 0
1148
+ };
1089
1149
  }
1090
- if (minIndexKeyAndIndex === void 0) return {
1091
- done: true,
1092
- value: void 0
1093
- };
1094
- const [_, minIndex] = minIndexKeyAndIndex;
1095
- const result = results[minIndex].value;
1096
- results[minIndex].value = void 0;
1097
- return {
1098
- done: false,
1099
- value: result
1100
- };
1101
- } };
1150
+ };
1102
1151
  } };
1103
1152
  }
1104
1153
  getOrder() {
@@ -1111,7 +1160,7 @@ var MergedStream = class MergedStream extends QueryStream {
1111
1160
  return this.#indexFields;
1112
1161
  }
1113
1162
  narrow(indexBounds) {
1114
- return new MergedStream(this.#streams.map((stream) => stream.narrow(indexBounds)), this.#indexFields);
1163
+ return new MergedStream(this.#streams.map((stream) => stream.narrow(indexBounds)), this.#indexFields, PRE_ORDERED_STREAMS);
1115
1164
  }
1116
1165
  };
1117
1166
  function allSame(values, errorMessage) {
@@ -1159,16 +1208,22 @@ var ConcatStreams = class ConcatStreams extends QueryStream {
1159
1208
  this.#equalityIndexFilter = commonPrefix(streams.map((stream) => stream.getEqualityIndexFilter()));
1160
1209
  }
1161
1210
  iterWithKeys() {
1162
- const iterables = this.#streams.map((stream) => stream.iterWithKeys());
1211
+ const streams = this.#streams;
1163
1212
  const comparisonInversion = this.#order === "asc" ? 1 : -1;
1164
- let previousIndexKey;
1165
1213
  return { [Symbol.asyncIterator]() {
1166
- const iterators = iterables.map((iterable) => iterable[Symbol.asyncIterator]());
1167
- return { async next() {
1168
- while (iterators.length > 0) {
1169
- const result = await iterators[0].next();
1170
- if (result.done) iterators.shift();
1171
- else {
1214
+ let index = 0;
1215
+ let iterator;
1216
+ let previousIndexKey;
1217
+ return {
1218
+ async next() {
1219
+ while (index < streams.length) {
1220
+ iterator ??= streams[index].iterWithKeys()[Symbol.asyncIterator]();
1221
+ const result = await iterator.next();
1222
+ if (result.done) {
1223
+ index++;
1224
+ iterator = void 0;
1225
+ continue;
1226
+ }
1172
1227
  const [_, indexKey] = result.value;
1173
1228
  if (previousIndexKey !== void 0 && compareKeys({
1174
1229
  value: previousIndexKey,
@@ -1180,12 +1235,21 @@ var ConcatStreams = class ConcatStreams extends QueryStream {
1180
1235
  previousIndexKey = indexKey;
1181
1236
  return result;
1182
1237
  }
1238
+ return {
1239
+ done: true,
1240
+ value: void 0
1241
+ };
1242
+ },
1243
+ async return() {
1244
+ await iterator?.return?.();
1245
+ index = streams.length;
1246
+ iterator = void 0;
1247
+ return {
1248
+ done: true,
1249
+ value: void 0
1250
+ };
1183
1251
  }
1184
- return {
1185
- done: true,
1186
- value: void 0
1187
- };
1188
- } };
1252
+ };
1189
1253
  } };
1190
1254
  }
1191
1255
  getOrder() {
@@ -1258,6 +1322,18 @@ var FlatMapStreamIterator = class {
1258
1322
  value: [u, [...this.#currentOuterItem.indexKey, ...indexKey]]
1259
1323
  };
1260
1324
  }
1325
+ async return() {
1326
+ try {
1327
+ await this.#currentOuterItem?.innerIterator.return?.();
1328
+ } finally {
1329
+ this.#currentOuterItem = null;
1330
+ await this.#outerIterator.return?.();
1331
+ }
1332
+ return {
1333
+ done: true,
1334
+ value: void 0
1335
+ };
1336
+ }
1261
1337
  };
1262
1338
  var FlatMapStream = class FlatMapStream extends QueryStream {
1263
1339
  #stream;
@@ -1477,15 +1553,24 @@ var OrderByStream = class OrderByStream extends QueryStream {
1477
1553
  const staticFilter = this.#staticFilter;
1478
1554
  return { [Symbol.asyncIterator]() {
1479
1555
  const iterator = iterable[Symbol.asyncIterator]();
1480
- return { async next() {
1481
- const result = await iterator.next();
1482
- if (result.done) return result;
1483
- const [doc, indexKey] = result.value;
1484
- return {
1485
- done: false,
1486
- value: [doc, indexKey.slice(staticFilter.length)]
1487
- };
1488
- } };
1556
+ return {
1557
+ async next() {
1558
+ const result = await iterator.next();
1559
+ if (result.done) return result;
1560
+ const [doc, indexKey] = result.value;
1561
+ return {
1562
+ done: false,
1563
+ value: [doc, indexKey.slice(staticFilter.length)]
1564
+ };
1565
+ },
1566
+ async return() {
1567
+ await iterator.return?.();
1568
+ return {
1569
+ done: true,
1570
+ value: void 0
1571
+ };
1572
+ }
1573
+ };
1489
1574
  } };
1490
1575
  }
1491
1576
  narrow(indexBounds) {
@@ -1516,36 +1601,57 @@ var DistinctStream = class DistinctStream extends QueryStream {
1516
1601
  iterWithKeys() {
1517
1602
  const stream = this.#stream;
1518
1603
  const distinctIndexFieldsLength = this.#distinctIndexFieldsLength;
1604
+ const order = stream.getOrder();
1605
+ const comparisonInversion = order === "asc" ? 1 : -1;
1519
1606
  return { [Symbol.asyncIterator]() {
1520
- let currentStream = stream;
1521
- let currentIterator = currentStream.iterWithKeys()[Symbol.asyncIterator]();
1522
- return { async next() {
1523
- const result = await currentIterator.next();
1524
- if (result.done) return {
1525
- done: true,
1526
- value: void 0
1527
- };
1528
- const [doc, indexKey] = result.value;
1529
- if (doc === null) return {
1530
- done: false,
1531
- value: [null, indexKey]
1532
- };
1533
- const distinctIndexKey = indexKey.slice(0, distinctIndexFieldsLength);
1534
- if (stream.getOrder() === "asc") currentStream = currentStream.narrow({
1535
- lowerBound: distinctIndexKey,
1536
- lowerBoundInclusive: false,
1537
- upperBound: [],
1538
- upperBoundInclusive: true
1539
- });
1540
- else currentStream = currentStream.narrow({
1541
- lowerBound: [],
1542
- lowerBoundInclusive: true,
1543
- upperBound: distinctIndexKey,
1544
- upperBoundInclusive: false
1545
- });
1546
- currentIterator = currentStream.iterWithKeys()[Symbol.asyncIterator]();
1547
- return result;
1548
- } };
1607
+ let currentIterator = stream.iterWithKeys()[Symbol.asyncIterator]();
1608
+ let previousDistinctIndexKey;
1609
+ return {
1610
+ async next() {
1611
+ const result = await currentIterator.next();
1612
+ if (result.done) return {
1613
+ done: true,
1614
+ value: void 0
1615
+ };
1616
+ const [doc, indexKey] = result.value;
1617
+ if (doc === null) return {
1618
+ done: false,
1619
+ value: [null, indexKey]
1620
+ };
1621
+ const distinctIndexKey = indexKey.slice(0, distinctIndexFieldsLength);
1622
+ if (previousDistinctIndexKey !== void 0 && compareKeys({
1623
+ value: previousDistinctIndexKey,
1624
+ kind: "exact"
1625
+ }, {
1626
+ value: distinctIndexKey,
1627
+ kind: "exact"
1628
+ }) * comparisonInversion >= 0) throw new Error(`DistinctStream in wrong order: ${JSON.stringify(previousDistinctIndexKey)}, ${JSON.stringify(distinctIndexKey)}`);
1629
+ previousDistinctIndexKey = distinctIndexKey;
1630
+ let narrowed;
1631
+ if (order === "asc") narrowed = stream.narrow({
1632
+ lowerBound: distinctIndexKey,
1633
+ lowerBoundInclusive: false,
1634
+ upperBound: [],
1635
+ upperBoundInclusive: true
1636
+ });
1637
+ else narrowed = stream.narrow({
1638
+ lowerBound: [],
1639
+ lowerBoundInclusive: true,
1640
+ upperBound: distinctIndexKey,
1641
+ upperBoundInclusive: false
1642
+ });
1643
+ await currentIterator.return?.();
1644
+ currentIterator = narrowed.iterWithKeys()[Symbol.asyncIterator]();
1645
+ return result;
1646
+ },
1647
+ async return() {
1648
+ await currentIterator.return?.();
1649
+ return {
1650
+ done: true,
1651
+ value: void 0
1652
+ };
1653
+ }
1654
+ };
1549
1655
  } };
1550
1656
  }
1551
1657
  narrow(indexBounds) {
@@ -1512,7 +1512,7 @@ function aggregateNameFromNamespace(namespace) {
1512
1512
  async function insertHandler(ctx, args) {
1513
1513
  const tree = await getOrCreateTree(ctx.db, args.namespace, DEFAULT_MAX_NODE_SIZE, true);
1514
1514
  const summand = args.summand ?? 0;
1515
- const pushUp = await insertIntoNode(ctx, args.namespace, tree.root, {
1515
+ const pushUp = await insertIntoNode(ctx, tree.maxNodeSize, tree.root, {
1516
1516
  k: args.key,
1517
1517
  v: args.value,
1518
1518
  s: summand
@@ -1529,7 +1529,7 @@ async function insertHandler(ctx, args) {
1529
1529
  }
1530
1530
  async function deleteHandler(ctx, args) {
1531
1531
  const tree = await getOrCreateTree(ctx.db, args.namespace, DEFAULT_MAX_NODE_SIZE, true);
1532
- await deleteFromNode(ctx, args.namespace, tree.root, args.key);
1532
+ await deleteFromNode(ctx, tree.maxNodeSize, tree.root, args.key);
1533
1533
  const root = await ctx.db.get(tree.root);
1534
1534
  if (root.items.length === 0 && root.subtrees.length === 1) {
1535
1535
  log(`collapsing root ${root._id} because its only child is ${root.subtrees[0]}`);
@@ -1538,13 +1538,12 @@ async function deleteHandler(ctx, args) {
1538
1538
  await ctx.db.delete(root._id);
1539
1539
  }
1540
1540
  }
1541
- async function MAX_NODE_SIZE(ctx, namespace) {
1542
- return (await mustGetTree(ctx.db, namespace)).maxNodeSize;
1541
+ function assertMaxNodeSize(maxNodeSize) {
1542
+ if (maxNodeSize % 2 !== 0 || maxNodeSize < 4) throw new Error("MAX_NODE_SIZE must be even and at least 4");
1543
1543
  }
1544
- async function MIN_NODE_SIZE(ctx, namespace) {
1545
- const max = await MAX_NODE_SIZE(ctx, namespace);
1546
- if (max % 2 !== 0 || max < 4) throw new Error("MAX_NODE_SIZE must be even and at least 4");
1547
- return max / 2;
1544
+ function minNodeSizeFor(maxNodeSize) {
1545
+ assertMaxNodeSize(maxNodeSize);
1546
+ return maxNodeSize / 2;
1548
1547
  }
1549
1548
  async function aggregateBetweenHandler(ctx, args) {
1550
1549
  const tree = await getTree(ctx.db, args.namespace);
@@ -1554,8 +1553,8 @@ async function aggregateBetweenHandler(ctx, args) {
1554
1553
  };
1555
1554
  return await aggregateBetweenInNode(ctx.db, tree.root, args.k1, args.k2);
1556
1555
  }
1557
- async function filterBetween(db, node, k1, k2) {
1558
- const n = await db.get(node);
1556
+ async function filterBetween(db, node, k1, k2, preloaded) {
1557
+ const n = preloaded ?? await db.get(node);
1559
1558
  const included = [];
1560
1559
  function includeSubtree(i, unboundedRight) {
1561
1560
  const unboundedLeft = k1 === void 0 || included.length > 0;
@@ -1623,7 +1622,7 @@ async function offsetUntilHandler(ctx, args) {
1623
1622
  namespace: args.namespace
1624
1623
  })).count;
1625
1624
  }
1626
- async function deleteFromNode(ctx, namespace, node, key) {
1625
+ async function deleteFromNode(ctx, maxNodeSize, node, key) {
1627
1626
  let n = await ctx.db.get(node);
1628
1627
  let foundItem = null;
1629
1628
  let i = 0;
@@ -1659,13 +1658,13 @@ async function deleteFromNode(ctx, namespace, node, key) {
1659
1658
  code: "DELETE_MISSING_KEY",
1660
1659
  message: `key ${p(key)} not found in node ${n._id}`
1661
1660
  });
1662
- const deleted = await deleteFromNode(ctx, namespace, n.subtrees[i], key);
1661
+ const deleted = await deleteFromNode(ctx, maxNodeSize, n.subtrees[i], key);
1663
1662
  if (!deleted) return null;
1664
1663
  if (!foundItem) foundItem = deleted;
1665
1664
  const newAggregate = n.aggregate && sub(n.aggregate, itemAggregate(deleted));
1666
1665
  if (newAggregate) await ctx.db.patch(node, { aggregate: newAggregate });
1667
1666
  const deficientSubtree = await ctx.db.get(n.subtrees[i]);
1668
- const minNodeSize = await MIN_NODE_SIZE(ctx, namespace);
1667
+ const minNodeSize = minNodeSizeFor(maxNodeSize);
1669
1668
  if (deficientSubtree.items.length < minNodeSize) {
1670
1669
  log(`deficient subtree ${deficientSubtree._id}`);
1671
1670
  if (i > 0) {
@@ -1751,26 +1750,28 @@ async function mergeNodes(db, parent, leftIndex) {
1751
1750
  });
1752
1751
  await db.delete(right._id);
1753
1752
  }
1754
- async function negativeOffsetInNode(db, node, index, k1, k2) {
1755
- const filtered = await filterBetween(db, node, k1, k2);
1753
+ async function negativeOffsetInNode(db, node, index, k1, k2, preloaded) {
1754
+ const filtered = await filterBetween(db, node, k1, k2, preloaded);
1756
1755
  for (const included of filtered.reverse()) if (included.type === "item") {
1757
1756
  if (index === 0) return included.item;
1758
1757
  index -= 1;
1759
1758
  } else {
1760
- const subtreeCount = (await nodeAggregate(db, await db.get(included.subtree))).count;
1761
- if (index < subtreeCount) return await negativeOffsetInNode(db, included.subtree, index);
1759
+ const subtree = await db.get(included.subtree);
1760
+ const subtreeCount = (await nodeAggregate(db, subtree)).count;
1761
+ if (index < subtreeCount) return await negativeOffsetInNode(db, included.subtree, index, void 0, void 0, subtree);
1762
1762
  index -= subtreeCount;
1763
1763
  }
1764
1764
  throw new ConvexError(`negative offset exceeded count by ${index} (in node ${node})`);
1765
1765
  }
1766
- async function atOffsetInNode(db, node, index, k1, k2) {
1767
- const filtered = await filterBetween(db, node, k1, k2);
1766
+ async function atOffsetInNode(db, node, index, k1, k2, preloaded) {
1767
+ const filtered = await filterBetween(db, node, k1, k2, preloaded);
1768
1768
  for (const included of filtered) if (included.type === "item") {
1769
1769
  if (index === 0) return included.item;
1770
1770
  index -= 1;
1771
1771
  } else {
1772
- const subtreeCount = (await nodeAggregate(db, await db.get(included.subtree))).count;
1773
- if (index < subtreeCount) return await atOffsetInNode(db, included.subtree, index);
1772
+ const subtree = await db.get(included.subtree);
1773
+ const subtreeCount = (await nodeAggregate(db, subtree)).count;
1774
+ if (index < subtreeCount) return await atOffsetInNode(db, included.subtree, index, void 0, void 0, subtree);
1774
1775
  index -= subtreeCount;
1775
1776
  }
1776
1777
  throw new ConvexError(`offset exceeded count by ${index} (in node ${node})`);
@@ -1812,7 +1813,7 @@ function accumulate(nums) {
1812
1813
  sum: 0
1813
1814
  });
1814
1815
  }
1815
- async function insertIntoNode(ctx, namespace, node, item) {
1816
+ async function insertIntoNode(ctx, maxNodeSize, node, item) {
1816
1817
  const n = await ctx.db.get(node);
1817
1818
  let i = 0;
1818
1819
  for (; i < n.items.length; i++) {
@@ -1820,31 +1821,41 @@ async function insertIntoNode(ctx, namespace, node, item) {
1820
1821
  if (compare === -1) break;
1821
1822
  if (compare === 0) throw new ConvexError(`key ${p(item.k)} already exists in node ${n._id}`);
1822
1823
  }
1824
+ let nextItems = n.items;
1825
+ let nextSubtrees = n.subtrees;
1823
1826
  if (n.subtrees.length > 0) {
1824
- const pushUp = await insertIntoNode(ctx, namespace, n.subtrees[i], item);
1825
- if (pushUp) await ctx.db.patch(node, {
1826
- items: [
1827
+ const pushUp = await insertIntoNode(ctx, maxNodeSize, n.subtrees[i], item);
1828
+ if (pushUp) {
1829
+ nextItems = [
1827
1830
  ...n.items.slice(0, i),
1828
1831
  pushUp.item,
1829
1832
  ...n.items.slice(i)
1830
- ],
1831
- subtrees: [
1833
+ ];
1834
+ nextSubtrees = [
1832
1835
  ...n.subtrees.slice(0, i),
1833
1836
  pushUp.leftSubtree,
1834
1837
  pushUp.rightSubtree,
1835
1838
  ...n.subtrees.slice(i + 1)
1836
- ]
1837
- });
1838
- } else await ctx.db.patch(node, { items: [
1839
+ ];
1840
+ }
1841
+ } else nextItems = [
1839
1842
  ...n.items.slice(0, i),
1840
1843
  item,
1841
1844
  ...n.items.slice(i)
1842
- ] });
1845
+ ];
1843
1846
  const newAggregate = n.aggregate && add(n.aggregate, itemAggregate(item));
1844
- if (newAggregate) await ctx.db.patch(node, { aggregate: newAggregate });
1845
- const newN = await ctx.db.get(node);
1846
- const maxNodeSize = await MAX_NODE_SIZE(ctx, namespace);
1847
- const minNodeSize = await MIN_NODE_SIZE(ctx, namespace);
1847
+ const nodePatch = {};
1848
+ if (nextItems !== n.items) nodePatch.items = nextItems;
1849
+ if (nextSubtrees !== n.subtrees) nodePatch.subtrees = nextSubtrees;
1850
+ if (newAggregate) nodePatch.aggregate = newAggregate;
1851
+ if (Object.keys(nodePatch).length > 0) await ctx.db.patch(node, nodePatch);
1852
+ const newN = {
1853
+ ...n,
1854
+ items: nextItems,
1855
+ subtrees: nextSubtrees,
1856
+ aggregate: newAggregate ?? n.aggregate
1857
+ };
1858
+ const minNodeSize = minNodeSizeFor(maxNodeSize);
1848
1859
  if (newN.items.length > maxNodeSize) {
1849
1860
  if (newN.items.length !== maxNodeSize + 1 || newN.items.length !== 2 * minNodeSize + 1) throw new Error(`bad ${newN.items.length}`);
1850
1861
  log(`splitting node ${newN._id} at ${newN.items[minNodeSize].k}`);
@@ -1889,6 +1900,7 @@ async function getOrCreateTree(db, namespace, maxNodeSize, rootLazy) {
1889
1900
  const originalTree = await getTree(db, namespace);
1890
1901
  const aggregateName = aggregateNameFromNamespace(namespace);
1891
1902
  if (originalTree) {
1903
+ assertMaxNodeSize(originalTree.maxNodeSize);
1892
1904
  if (originalTree.aggregateName !== aggregateName) {
1893
1905
  await db.patch(originalTree._id, { aggregateName });
1894
1906
  return {
@@ -1906,7 +1918,7 @@ async function getOrCreateTree(db, namespace, maxNodeSize, rootLazy) {
1906
1918
  sum: 0
1907
1919
  }
1908
1920
  });
1909
- const effectiveMaxNodeSize = maxNodeSize ?? await MAX_NODE_SIZE({ db }, void 0) ?? DEFAULT_MAX_NODE_SIZE;
1921
+ const effectiveMaxNodeSize = maxNodeSize ?? (await mustGetTree(db, void 0)).maxNodeSize;
1910
1922
  const effectiveRootLazy = rootLazy ?? await isRootLazy(db, void 0) ?? true;
1911
1923
  const id = await db.insert(AGGREGATE_TREE_TABLE, {
1912
1924
  aggregateName,
@@ -1915,7 +1927,7 @@ async function getOrCreateTree(db, namespace, maxNodeSize, rootLazy) {
1915
1927
  namespace
1916
1928
  });
1917
1929
  const newTree = await db.get(id);
1918
- await MIN_NODE_SIZE({ db }, namespace);
1930
+ assertMaxNodeSize(effectiveMaxNodeSize);
1919
1931
  if (effectiveRootLazy) await db.patch(root, { aggregate: void 0 });
1920
1932
  return newTree;
1921
1933
  }
@@ -1930,6 +1942,19 @@ async function deleteTreeNodes(db, node) {
1930
1942
  for (const subtree of current.subtrees) await deleteTreeNodes(db, subtree);
1931
1943
  await db.delete(node);
1932
1944
  }
1945
+ /**
1946
+ * Deletes up to `limit` namespace trees belonging to one aggregate, returning
1947
+ * true once none are left. Unlike `clearTree` this does not recreate the tree,
1948
+ * so a caller can drain every namespace across several transactions.
1949
+ */
1950
+ async function deleteTreesHandler(ctx, args) {
1951
+ const trees = await ctx.db.query(AGGREGATE_TREE_TABLE).withIndex("by_aggregate_name", (q) => q.eq("aggregateName", args.aggregateName)).take(args.limit);
1952
+ for (const tree of trees) {
1953
+ await deleteTreeNodes(ctx.db, tree.root);
1954
+ await ctx.db.delete(tree._id);
1955
+ }
1956
+ return trees.length === 0;
1957
+ }
1933
1958
  async function clearTree(db, args) {
1934
1959
  const tree = await getTree(db, args.namespace);
1935
1960
  let existingRootLazy = true;
@@ -2319,6 +2344,17 @@ var Aggregate = class {
2319
2344
  rootLazy: opts[0]?.rootLazy
2320
2345
  });
2321
2346
  }
2347
+ /**
2348
+ * Deletes up to `limit` namespace trees without recreating them. Returns true
2349
+ * once this aggregate owns no trees, so callers can drain every namespace
2350
+ * across several mutations instead of walking them all in one.
2351
+ */
2352
+ async deleteTrees(ctx, limit) {
2353
+ return deleteTreesHandler({ db: ctx.db }, {
2354
+ aggregateName: this.aggregateName,
2355
+ limit
2356
+ });
2357
+ }
2322
2358
  async makeRootLazy(ctx, namespace) {
2323
2359
  const tree = await getOrCreateTree(ctx.db, namespaceForArg(this.aggregateName, { namespace }));
2324
2360
  await ctx.db.patch(tree.root, { aggregate: void 0 });
package/dist/watcher.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { a as generateMeta, c as PARSE_SNAPSHOT_SUFFIX, i as resolveConfiguredBackend, n as withLocalCodegenEnv, o as getConvexConfig, r as loadCliConfig, s as logger } from "./local-env-DykABjCt.mjs";
2
+ import { a as generateMeta, c as PARSE_SNAPSHOT_SUFFIX, i as resolveConfiguredBackend, n as withLocalCodegenEnv, o as getConvexConfig, r as loadCliConfig, s as logger } from "./local-env-yFKub75x.mjs";
3
3
  import path from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
5