articulated 0.2.0 → 0.4.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.
Files changed (31) hide show
  1. package/README.md +18 -1
  2. package/build/commonjs/id_list.d.ts +56 -20
  3. package/build/commonjs/id_list.js +207 -107
  4. package/build/commonjs/id_list.js.map +1 -1
  5. package/build/commonjs/internal/leaf_map.d.ts +25 -0
  6. package/build/commonjs/internal/leaf_map.js +54 -0
  7. package/build/commonjs/internal/leaf_map.js.map +1 -0
  8. package/build/commonjs/internal/seq_map.d.ts +20 -0
  9. package/build/commonjs/internal/seq_map.js +40 -0
  10. package/build/commonjs/internal/seq_map.js.map +1 -0
  11. package/build/commonjs/vendor/functional-red-black-tree.d.ts +8 -0
  12. package/build/commonjs/vendor/functional-red-black-tree.js +911 -0
  13. package/build/commonjs/vendor/functional-red-black-tree.js.map +1 -0
  14. package/build/esm/id_list.d.ts +56 -20
  15. package/build/esm/id_list.js +206 -105
  16. package/build/esm/id_list.js.map +1 -1
  17. package/build/esm/internal/leaf_map.d.ts +25 -0
  18. package/build/esm/internal/leaf_map.js +47 -0
  19. package/build/esm/internal/leaf_map.js.map +1 -0
  20. package/build/esm/internal/seq_map.d.ts +20 -0
  21. package/build/esm/internal/seq_map.js +32 -0
  22. package/build/esm/internal/seq_map.js.map +1 -0
  23. package/build/esm/vendor/functional-red-black-tree.d.ts +8 -0
  24. package/build/esm/vendor/functional-red-black-tree.js +911 -0
  25. package/build/esm/vendor/functional-red-black-tree.js.map +1 -0
  26. package/package.json +10 -5
  27. package/src/id_list.ts +306 -109
  28. package/src/internal/leaf_map.ts +57 -0
  29. package/src/internal/seq_map.ts +48 -0
  30. package/src/vendor/functional-red-black-tree.d.ts +177 -0
  31. package/src/vendor/functional-red-black-tree.js +938 -0
package/src/id_list.ts CHANGED
@@ -1,5 +1,7 @@
1
1
  import { SparseIndices } from "sparse-array-rled";
2
2
  import { ElementId } from "./id";
3
+ import { LeafMap, MutableLeafMap } from "./internal/leaf_map";
4
+ import { getAndBumpNextSeq, MutableSeqMap, SeqMap } from "./internal/seq_map";
3
5
  import { SavedIdList } from "./saved_id_list";
4
6
 
5
7
  // Most exports are only for tests. See index.ts for public exports.
@@ -28,6 +30,13 @@ import { SavedIdList } from "./saved_id_list";
28
30
  and its knownSize (# of known ids). These allow indexed access in log time.
29
31
 
30
32
  Unlike some B+Trees, we do not store a linked list of leaves. Iteration instead uses a depth-first search.
33
+
34
+ Finally, we also store a "bottom-up" view of the B+Tree, in order to quickly find the leaf or
35
+ tree path corresponding to an ElementId. Each inner node is assigned a unique sequence number
36
+ (seq), and we store a persistent map from each leaf to its parent's seq (leafMap)
37
+ and from each inner node's seq to its parent's seq (parentSeqs). Because leafMap is sorted
38
+ by (LeafNode.bunchId, LeafNode.startCounter), we can also use it to lookup the leaf corresponding
39
+ to an ElementId, e.g., for IdList.has.
31
40
  */
32
41
 
33
42
  export interface LeafNode {
@@ -49,12 +58,28 @@ export class InnerNodeInner {
49
58
  readonly size: number;
50
59
  readonly knownSize: number;
51
60
 
52
- constructor(readonly children: readonly InnerNode[]) {
61
+ constructor(
62
+ /**
63
+ * A unique identifer for this node within its IdTree.
64
+ */
65
+ readonly seq: number,
66
+ readonly children: readonly InnerNode[],
67
+ /**
68
+ * We add entries for the children to this map, overwriting any existing parentSeqs.
69
+ *
70
+ * Pass null to skip when you are doing it yourself. Regardless, you need to
71
+ * delete any outdated entries yourself.
72
+ */
73
+ parentSeqsMut: MutableSeqMap | null
74
+ ) {
53
75
  let size = 0;
54
76
  let knownSize = 0;
55
77
  for (const child of children) {
56
78
  size += child.size;
57
79
  knownSize += child.knownSize;
80
+ if (parentSeqsMut) {
81
+ parentSeqsMut.value = parentSeqsMut.value.set(child.seq, seq);
82
+ }
58
83
  }
59
84
  this.size = size;
60
85
  this.knownSize = knownSize;
@@ -68,12 +93,27 @@ export class InnerNodeLeaf {
68
93
  readonly size: number;
69
94
  readonly knownSize: number;
70
95
 
71
- constructor(readonly children: readonly LeafNode[]) {
96
+ constructor(
97
+ /**
98
+ * A unique identifer for this node within its IdTree.
99
+ */
100
+ readonly seq: number,
101
+ readonly children: readonly LeafNode[],
102
+ /**
103
+ * We add entries for the children to this map, overwriting any existing parentSeqs.
104
+ *
105
+ * Pass null to skip when you are doing it yourself.
106
+ */
107
+ leafMapMut: MutableLeafMap | null
108
+ ) {
72
109
  let size = 0;
73
110
  let knownSize = 0;
74
111
  for (const child of children) {
75
112
  size += child.present.count();
76
113
  knownSize += child.count;
114
+ if (leafMapMut) {
115
+ leafMapMut.value = leafMapMut.value.set(child, seq);
116
+ }
77
117
  }
78
118
  this.size = size;
79
119
  this.knownSize = knownSize;
@@ -82,6 +122,12 @@ export class InnerNodeLeaf {
82
122
 
83
123
  export type InnerNode = InnerNodeInner | InnerNodeLeaf;
84
124
 
125
+ type Located = [
126
+ { node: LeafNode; indexInParent: number },
127
+ // Index 1 will be an InnerNodeLeaf if it exists.
128
+ ...{ node: InnerNode; indexInParent: number }[]
129
+ ];
130
+
85
131
  /**
86
132
  * The B+Tree's branching factor, i.e., the max number of children of a node.
87
133
  *
@@ -118,10 +164,28 @@ export const M = 8;
118
164
  * cause such ids to be separated, partially deleted, or even reordered.
119
165
  */
120
166
  export class IdList {
167
+ /**
168
+ * A persistent map from each InnerNode's seq to its parent node's seq.
169
+ *
170
+ * We map the root's seq to 0 (in our constructor).
171
+ */
172
+ private readonly parentSeqs: SeqMap;
173
+
121
174
  /**
122
175
  * Internal - construct an IdList using a static method (e.g. `IdList.new`).
123
176
  */
124
- private constructor(private readonly root: InnerNode) {}
177
+ private constructor(
178
+ private readonly root: InnerNode,
179
+ /**
180
+ * A persistent sorted map from each leaf to its parent node's seq.
181
+ *
182
+ * Besides parentSeqs, we also use this to lookup leaves by ElementId.
183
+ */
184
+ private readonly leafMap: LeafMap,
185
+ parentSeqs: SeqMap
186
+ ) {
187
+ this.parentSeqs = parentSeqs.set(root.seq, 0);
188
+ }
125
189
 
126
190
  /**
127
191
  * Constructs an empty list.
@@ -130,7 +194,13 @@ export class IdList {
130
194
  * or {@link IdList.load}.
131
195
  */
132
196
  static new() {
133
- return new this(new InnerNodeLeaf([]));
197
+ const leafMapMut = { value: LeafMap.new() };
198
+ const parentSeqsMut = { value: SeqMap.new() };
199
+ return new this(
200
+ new InnerNodeLeaf(getAndBumpNextSeq(parentSeqsMut), [], leafMapMut),
201
+ leafMapMut.value,
202
+ parentSeqsMut.value
203
+ );
134
204
  }
135
205
 
136
206
  /**
@@ -204,7 +274,7 @@ export class IdList {
204
274
  if (!(Number.isSafeInteger(count) && count >= 0)) {
205
275
  throw new Error(`Invalid count: ${count}`);
206
276
  }
207
- if (count !== 0 && isAnyKnown(newId, count, this.root)) {
277
+ if (this.isAnyKnown(newId, count)) {
208
278
  throw new Error("An inserted id is already known");
209
279
  }
210
280
 
@@ -215,15 +285,18 @@ export class IdList {
215
285
  // Insert the first leaf as a child of root.
216
286
  const present = SparseIndices.new();
217
287
  present.set(newId.counter, count);
288
+ const leaf: LeafNode = {
289
+ bunchId: newId.bunchId,
290
+ startCounter: newId.counter,
291
+ count,
292
+ present,
293
+ };
294
+
295
+ const leafMapMut = { value: this.leafMap };
218
296
  return new IdList(
219
- new InnerNodeLeaf([
220
- {
221
- bunchId: newId.bunchId,
222
- startCounter: newId.counter,
223
- count,
224
- present,
225
- },
226
- ])
297
+ new InnerNodeLeaf(this.root.seq, [leaf], leafMapMut),
298
+ leafMapMut.value,
299
+ this.parentSeqs
227
300
  );
228
301
  } else {
229
302
  // Insert before the first known id.
@@ -231,7 +304,7 @@ export class IdList {
231
304
  }
232
305
  }
233
306
 
234
- const located = locate(before, this.root);
307
+ const located = this.locate(before);
235
308
  if (located === null) {
236
309
  throw new Error("before is not known");
237
310
  }
@@ -316,7 +389,7 @@ export class IdList {
316
389
  if (!(Number.isSafeInteger(count) && count >= 0)) {
317
390
  throw new Error(`Invalid count: ${count}`);
318
391
  }
319
- if (count !== 0 && isAnyKnown(newId, count, this.root)) {
392
+ if (this.isAnyKnown(newId, count)) {
320
393
  throw new Error("An inserted id is already known");
321
394
  }
322
395
 
@@ -331,7 +404,7 @@ export class IdList {
331
404
  );
332
405
  }
333
406
 
334
- const located = locate(after, this.root);
407
+ const located = this.locate(after);
335
408
  if (located === null) {
336
409
  throw new Error("after is not known");
337
410
  }
@@ -411,7 +484,7 @@ export class IdList {
411
484
  * If `id` is already deleted or is not known, this method does nothing.
412
485
  */
413
486
  delete(id: ElementId) {
414
- const located = locate(id, this.root);
487
+ const located = this.locate(id);
415
488
  if (located === null) return this;
416
489
 
417
490
  const leaf = located[0].node;
@@ -434,7 +507,7 @@ export class IdList {
434
507
  * @throws If `id` is not known.
435
508
  */
436
509
  undelete(id: ElementId) {
437
- const located = locate(id, this.root);
510
+ const located = this.locate(id);
438
511
  if (located === null) {
439
512
  throw new Error("id is not known");
440
513
  }
@@ -448,6 +521,60 @@ export class IdList {
448
521
  return this.replaceLeaf(located, { ...leaf, present: newPresent });
449
522
  }
450
523
 
524
+ /**
525
+ * Returns the path from id's leaf node to the root, or null if id is not found.
526
+ *
527
+ * The path contains each node and its index in its parent's node, starting with id's
528
+ * LeafNode and ending at a child of the root.
529
+ */
530
+ private locate(id: ElementId): Located | null {
531
+ // Find the leaf containing id, if any.
532
+ const [leaf, parentSeq] = this.leafMap.getLeaf(id.bunchId, id.counter);
533
+ if (leaf === undefined) return null;
534
+ if (
535
+ !(
536
+ leaf.bunchId === id.bunchId &&
537
+ leaf.startCounter <= id.counter &&
538
+ id.counter < leaf.startCounter + leaf.count
539
+ )
540
+ ) {
541
+ return null;
542
+ }
543
+
544
+ // Find the seqs on the path (leaf, root].
545
+ const innerSeqs: number[] = [];
546
+ let curSeq = parentSeq;
547
+ while (curSeq !== 0) {
548
+ innerSeqs.push(curSeq);
549
+ curSeq = this.parentSeqs.get(curSeq);
550
+ }
551
+
552
+ // Find the nodes and indexInParent's on the path (root, leaf),
553
+ // using seqs to find the appropriate child of each node.
554
+ const innerNodes: { node: InnerNode; indexInParent: number }[] = [];
555
+ let curParent = this.root;
556
+ // Start at the root child's seq and proceed to the leaf parent's seq.
557
+ for (let i = innerSeqs.length - 2; i >= 0; i--) {
558
+ const children = (curParent as InnerNodeInner).children;
559
+ const childIndex = children.findIndex(
560
+ (child) => child.seq === innerSeqs[i]
561
+ );
562
+ if (childIndex === -1) throw new Error("Internal error");
563
+ const child = children[childIndex];
564
+
565
+ innerNodes.push({ node: child, indexInParent: childIndex });
566
+ curParent = child;
567
+ }
568
+
569
+ // Now curParent is the leaf's parent. Find leaf in its children and return.
570
+ const leafChildIndex = (curParent as InnerNodeLeaf).children.indexOf(leaf);
571
+ if (leafChildIndex === -1) throw new Error("Internal error");
572
+ return [
573
+ { node: leaf, indexInParent: leafChildIndex },
574
+ ...innerNodes.reverse(),
575
+ ];
576
+ }
577
+
451
578
  /**
452
579
  * Replaces the leaf at the given path with newLeaves.
453
580
  * Returns a proper (sufficiently balanced) B+Tree with updated sizes.
@@ -455,7 +582,18 @@ export class IdList {
455
582
  * newLeaves.length must be in [1, M].
456
583
  */
457
584
  private replaceLeaf(located: Located, ...newLeaves: LeafNode[]): IdList {
458
- return new IdList(replaceNode(located, this.root, newLeaves, 0));
585
+ const leafMapMut = { value: this.leafMap };
586
+ const parentSeqsMut = { value: this.parentSeqs };
587
+
588
+ const newRoot = replaceNode(
589
+ located,
590
+ this.root,
591
+ leafMapMut,
592
+ parentSeqsMut,
593
+ newLeaves,
594
+ 0
595
+ );
596
+ return new IdList(newRoot, leafMapMut.value, parentSeqsMut.value);
459
597
  }
460
598
 
461
599
  // Accessors
@@ -468,9 +606,13 @@ export class IdList {
468
606
  * Compare to {@link isKnown}.
469
607
  */
470
608
  has(id: ElementId): boolean {
471
- const located = locate(id, this.root);
472
- if (located === null) return false;
473
- return located[0].node.present.has(id.counter);
609
+ // Find the LeafNode that would contain id if known.
610
+ const [leaf] = this.leafMap.getLeaf(id.bunchId, id.counter);
611
+ if (leaf && leaf.bunchId === id.bunchId) {
612
+ return leaf.present.has(id.counter);
613
+ }
614
+
615
+ return false;
474
616
  }
475
617
 
476
618
  /**
@@ -479,7 +621,40 @@ export class IdList {
479
621
  * Compare to {@link has}.
480
622
  */
481
623
  isKnown(id: ElementId): boolean {
482
- return locate(id, this.root) !== null;
624
+ // Find the LeafNode that would contain id if known.
625
+ const [leaf] = this.leafMap.getLeaf(id.bunchId, id.counter);
626
+ if (leaf && leaf.bunchId === id.bunchId) {
627
+ return (
628
+ leaf.startCounter <= id.counter &&
629
+ id.counter < leaf.startCounter + leaf.count
630
+ );
631
+ }
632
+
633
+ return false;
634
+ }
635
+
636
+ // TODO: Make public?
637
+ /**
638
+ * Returns true if any of the given bulk ids are known.
639
+ */
640
+ private isAnyKnown(id: ElementId, count: number): boolean {
641
+ if (count === 0) return false;
642
+
643
+ // Find the leaf containing the last id, or the previous leaf.
644
+ // If any leaf knows any of the ids, this leaf must know an id too.
645
+ const [leaf] = this.leafMap.getLeaf(id.bunchId, id.counter + count - 1);
646
+
647
+ if (leaf && leaf.bunchId === id.bunchId) {
648
+ // Test if there is any overlap between the leaf's counter range [a, b]
649
+ // and the bulk ids' counter range [c, d].
650
+ const a = leaf.startCounter;
651
+ const b = leaf.startCounter + leaf.count - 1;
652
+ const c = id.counter;
653
+ const d = id.counter + count - 1;
654
+ return a <= d && c <= b;
655
+ }
656
+
657
+ return false;
483
658
  }
484
659
 
485
660
  /**
@@ -546,7 +721,7 @@ export class IdList {
546
721
  * @throws If `id` is not known.
547
722
  */
548
723
  indexOf(id: ElementId, bias: "none" | "left" | "right" = "none"): number {
549
- const located = locate(id, this.root);
724
+ const located = this.locate(id);
550
725
  if (located === null) throw new Error("id is not known");
551
726
 
552
727
  /**
@@ -646,7 +821,7 @@ export class IdList {
646
821
  * Loads a saved state returned by {@link save}.
647
822
  */
648
823
  static load(savedState: SavedIdList) {
649
- // 1. Determine the leaves.
824
+ // 1. Determine the leaves in list order.
650
825
 
651
826
  const leaves: LeafNode[] = [];
652
827
  for (let i = 0; i < savedState.length; i++) {
@@ -692,18 +867,27 @@ export class IdList {
692
867
  }
693
868
 
694
869
  // 2. Create a B+Tree with the given leaves.
695
- // We do a "direct" balanced construction that takes O(n) time, instead of inserting
696
- // leaves one-by-one, which would take O(n log(n)) time.
870
+ // We do a "direct" balanced construction that takes O(L) time, instead of inserting
871
+ // leaves one-by-one, which would take O(L log(L)) time.
872
+ // However, constructing the sorted leafMap brings the overall runtime to O(L log(L)).
697
873
 
698
874
  if (leaves.length === 0) return IdList.new();
699
875
 
876
+ // TODO: Test the aux data structures after loading.
877
+ // E.g. reload and then call checkAll again.
878
+ // Also should do insertions to test splitting of the full tree.
879
+
880
+ const leafMapMut = { value: LeafMap.new() };
881
+ const parentSeqsMut = { value: SeqMap.new() };
882
+
700
883
  // Depth of the B+Tree (number of non-root nodes on any path from a leaf to the root).
701
- // A fully balanced B+Tree of depth d has between [M^{d-1} + 1, M^d] leaves.
884
+ // A full B+Tree of depth d has between [M^{d-1} + 1, M^d] leaves.
702
885
  const depth =
703
886
  leaves.length === 1
704
887
  ? 1
705
888
  : Math.ceil(Math.log(leaves.length) / Math.log(M));
706
- return new IdList(buildTree(leaves, 0, depth));
889
+ const root = buildTree(leaves, leafMapMut, parentSeqsMut, 0, depth);
890
+ return new IdList(root, leafMapMut.value, parentSeqsMut.value);
707
891
  }
708
892
  }
709
893
 
@@ -773,7 +957,8 @@ export class KnownIdView {
773
957
  * Returns the index of `id` in this view, or -1 if it is not known.
774
958
  */
775
959
  indexOf(id: ElementId): number {
776
- const located = locate(id, this.root);
960
+ // @ts-expect-error Ignore private
961
+ const located = this.list.locate(id);
777
962
  if (located === null) throw new Error("id is not known");
778
963
 
779
964
  /**
@@ -861,77 +1046,19 @@ function lastId(node: InnerNode): ElementId {
861
1046
  };
862
1047
  }
863
1048
 
864
- type Located = [
865
- { node: LeafNode; indexInParent: number },
866
- // Index 1 will be an InnerNodeLeaf if it exists.
867
- ...{ node: InnerNode; indexInParent: number }[]
868
- ];
869
-
870
- /**
871
- * Returns the path from id's leaf node to the root, or null if id is not found.
872
- *
873
- * The path contains each node and its index in its parent's node, starting with id's
874
- * LeafNode and ending at a child of the root.
875
- */
876
- export function locate(id: ElementId, node: InnerNode): Located | null {
877
- if (node instanceof InnerNodeInner) {
878
- for (let i = 0; i < node.children.length; i++) {
879
- const child = node.children[i];
880
- const childLocated = locate(id, child);
881
- if (childLocated !== null) {
882
- childLocated.push({ node: child, indexInParent: i });
883
- return childLocated;
884
- }
885
- }
886
- } else {
887
- for (let i = 0; i < node.children.length; i++) {
888
- const child = node.children[i];
889
- if (
890
- child.bunchId === id.bunchId &&
891
- child.startCounter <= id.counter &&
892
- id.counter < child.startCounter + child.count
893
- ) {
894
- return [{ node: child, indexInParent: i }];
895
- }
896
- }
897
- }
898
- return null;
899
- }
900
-
901
- /**
902
- * Returns true if any of the given bulk ids are known within node's subtree.
903
- *
904
- * Assumes count > 0.
905
- */
906
- function isAnyKnown(id: ElementId, count: number, node: InnerNode): boolean {
907
- if (node instanceof InnerNodeInner) {
908
- for (const child of node.children) {
909
- if (isAnyKnown(id, count, child)) return true;
910
- }
911
- } else {
912
- for (const child of node.children) {
913
- if (child.bunchId === id.bunchId) {
914
- // Test if there is any overlap between the child's counter range [a, b]
915
- // and the bulk id's counter range [c, d].
916
- const a = child.startCounter;
917
- const b = child.startCounter + child.count - 1;
918
- const c = id.counter;
919
- const d = id.counter + count - 1;
920
- if (a <= d && c <= b) return true;
921
- }
922
- }
923
- }
924
- return false;
925
- }
926
-
927
1049
  /**
928
1050
  * Replace located[i].node with newNodes.
929
1051
  *
930
1052
  * newNodes.length must be in [1, M].
1053
+ *
1054
+ * The returned node's descendants are recorded in leafMapMut and parentSeqsMut,
1055
+ * but the node itself is not (since we don't know its parent here).
931
1056
  */
932
1057
  function replaceNode(
933
1058
  located: Located,
934
1059
  root: InnerNode,
1060
+ leafMapMut: MutableLeafMap,
1061
+ parentSeqsMut: MutableSeqMap,
935
1062
  newNodes: InnerNode[] | LeafNode[],
936
1063
  i: number
937
1064
  ): InnerNode {
@@ -945,31 +1072,82 @@ function replaceNode(
945
1072
 
946
1073
  if (newChildren.length > M) {
947
1074
  // Split the parent to maintain BTree property (# children <= M).
1075
+ // Treat the right parent as "new", getting a new seq.
948
1076
  const split = Math.ceil(newChildren.length / 2);
1077
+ const seqs = [parent.seq, getAndBumpNextSeq(parentSeqsMut)];
949
1078
  const newParents = [
950
1079
  newChildren.slice(0, split),
951
1080
  newChildren.slice(split),
952
- ].map((children) =>
1081
+ ].map((children, j) =>
953
1082
  i === 0
954
- ? new InnerNodeLeaf(children as LeafNode[])
955
- : new InnerNodeInner(children as InnerNode[])
1083
+ ? new InnerNodeLeaf(seqs[j], children as LeafNode[], leafMapMut)
1084
+ : new InnerNodeInner(seqs[j], children as InnerNode[], parentSeqsMut)
956
1085
  );
957
1086
  if (i === located.length - 1) {
958
1087
  // newParents replace root. We need a new root to hold them.
959
- return new InnerNodeInner(newParents);
1088
+ return new InnerNodeInner(
1089
+ getAndBumpNextSeq(parentSeqsMut),
1090
+ newParents,
1091
+ parentSeqsMut
1092
+ );
960
1093
  } else {
961
- return replaceNode(located, root, newParents, i + 1);
1094
+ return replaceNode(
1095
+ located,
1096
+ root,
1097
+ leafMapMut,
1098
+ parentSeqsMut,
1099
+ newParents,
1100
+ i + 1
1101
+ );
962
1102
  }
963
1103
  } else {
964
- const newParent =
965
- i === 0
966
- ? new InnerNodeLeaf(newChildren as LeafNode[])
967
- : new InnerNodeInner(newChildren as InnerNode[]);
1104
+ // "Replace" parent, reusing its seq.
1105
+ // To avoid doing newChildren.length sets every time (which makes replaceLeaf
1106
+ // do >=(M/2)*log(L) total sets, even when none were necessary),
1107
+ // we bypass the InnerNode constructors' leafMap/parentSeq operations,
1108
+ // instead doing them ourselves only on the changed children.
1109
+ let newParent: InnerNode;
1110
+ if (i === 0) {
1111
+ newParent = new InnerNodeLeaf(
1112
+ parent.seq,
1113
+ newChildren as LeafNode[],
1114
+ null
1115
+ );
1116
+ // Important to delete the replaced leaf's entry, so that it doesn't corrupt by-ElementId searches.
1117
+ leafMapMut.value = leafMapMut.value.delete(located[0].node);
1118
+ for (const newNode of newNodes as LeafNode[]) {
1119
+ leafMapMut.value = leafMapMut.value.set(newNode, parent.seq);
1120
+ }
1121
+ } else {
1122
+ newParent = new InnerNodeInner(
1123
+ parent.seq,
1124
+ newChildren as InnerNode[],
1125
+ null
1126
+ );
1127
+ for (const newNode of newNodes as InnerNode[]) {
1128
+ if (newNode.seq !== (located[i].node as InnerNode).seq) {
1129
+ parentSeqsMut.value = parentSeqsMut.value.set(
1130
+ newNode.seq,
1131
+ parent.seq
1132
+ );
1133
+ }
1134
+ }
1135
+ // If the replaced node isn't represented in newNodes (i.e., same seq is not reused),
1136
+ // we could delete its entry to save memory, but it is not necessary.
1137
+ }
1138
+
968
1139
  if (i === located.length - 1) {
969
1140
  // Replaces root.
970
1141
  return newParent;
971
1142
  } else {
972
- return replaceNode(located, root, [newParent], i + 1);
1143
+ return replaceNode(
1144
+ located,
1145
+ root,
1146
+ leafMapMut,
1147
+ parentSeqsMut,
1148
+ [newParent],
1149
+ i + 1
1150
+ );
973
1151
  }
974
1152
  }
975
1153
  }
@@ -1125,25 +1303,44 @@ function pushSaveItem(acc: SavedIdList, item: SavedIdList[number]) {
1125
1303
  /**
1126
1304
  * Builds a tree with the given leaves. Used by IdList.load.
1127
1305
  *
1128
- * In contrast to inserting the leaves one-by-one, this function balances the
1129
- * tree, with full inner nodes (M children) whenever possible,
1130
- * and runs in O(L) time instead of O(L log(L)).
1306
+ * The returned node's descendants are recorded in leafMapMut and parentSeqsMut,
1307
+ * but not the node itself (since we don't know its parent here).
1308
+ *
1309
+ * In contrast to inserting the leaves one-by-one, this function fills nodes
1310
+ * with M children whenever possible,
1311
+ * and the B+Tree parts run in O(L) time instead of O(L log(L)).
1312
+ * However, the overall runtime is O(L log(L)) from constructing the sorted leafMap.
1131
1313
  */
1132
1314
  function buildTree(
1133
1315
  leaves: LeafNode[],
1316
+ leafMapMut: MutableLeafMap,
1317
+ parentSeqsMut: MutableSeqMap,
1134
1318
  startIndex: number,
1135
1319
  depthRemaining: number
1136
1320
  ): InnerNode {
1321
+ const parentSeq = getAndBumpNextSeq(parentSeqsMut);
1137
1322
  if (depthRemaining === 1) {
1138
- return new InnerNodeLeaf(leaves.slice(startIndex, startIndex + M));
1323
+ return new InnerNodeLeaf(
1324
+ parentSeq,
1325
+ leaves.slice(startIndex, startIndex + M),
1326
+ leafMapMut
1327
+ );
1139
1328
  } else {
1140
1329
  const children: InnerNode[] = [];
1141
1330
  const childLeafCount = Math.pow(M, depthRemaining - 1);
1142
1331
  for (let i = 0; i < M; i++) {
1143
1332
  const childStartIndex = startIndex + i * childLeafCount;
1144
1333
  if (childStartIndex >= leaves.length) break;
1145
- children.push(buildTree(leaves, childStartIndex, depthRemaining - 1));
1334
+ children.push(
1335
+ buildTree(
1336
+ leaves,
1337
+ leafMapMut,
1338
+ parentSeqsMut,
1339
+ childStartIndex,
1340
+ depthRemaining - 1
1341
+ )
1342
+ );
1146
1343
  }
1147
- return new InnerNodeInner(children);
1344
+ return new InnerNodeInner(parentSeq, children, parentSeqsMut);
1148
1345
  }
1149
1346
  }
@@ -0,0 +1,57 @@
1
+ import createRBTree, { Tree } from "../vendor/functional-red-black-tree";
2
+ import type { LeafNode } from "../id_list";
3
+
4
+ /**
5
+ * A persistent sorted map from each LeafNode to its parent's seq.
6
+ *
7
+ * Leaves are sorted by their first ElementId.
8
+ * This lets you quickly look up the LeafNode containing an ElementId,
9
+ * even though the LeafNode might start at a lower counter.
10
+ */
11
+ export class LeafMap {
12
+ private constructor(private readonly tree: Tree<LeafNode, number>) {}
13
+
14
+ static new() {
15
+ return new this(createRBTree(compareLeaves));
16
+ }
17
+
18
+ /**
19
+ * Returns the greatest leaf whose first id is <= the given id,
20
+ * or undefined if none exists. Also returns the associated seq (or -1 if not found).
21
+ *
22
+ * The returned leaf might not actually contain the given id.
23
+ */
24
+ getLeaf(
25
+ bunchId: string,
26
+ counter: number
27
+ ): [leaf: LeafNode | undefined, seq: number] {
28
+ const iter = this.tree.le({ bunchId, startCounter: counter } as LeafNode);
29
+ return [iter.key, iter.value ?? -1];
30
+ }
31
+
32
+ set(leaf: LeafNode, seq: number): LeafMap {
33
+ return new LeafMap(this.tree.set(leaf, seq));
34
+ }
35
+
36
+ delete(leaf: LeafNode): LeafMap {
37
+ return new LeafMap(this.tree.remove(leaf));
38
+ }
39
+ }
40
+
41
+ /**
42
+ * Sort function for LeafNodes in LeafMap.
43
+ *
44
+ * Sorting by startCounters lets us quickly look up the LeafNode containing an ElementId,
45
+ * even though the LeafNode might start at a lower counter.
46
+ */
47
+ function compareLeaves(a: LeafNode, b: LeafNode) {
48
+ if (a.bunchId === b.bunchId) {
49
+ return a.startCounter - b.startCounter;
50
+ } else {
51
+ return a.bunchId > b.bunchId ? 1 : -1;
52
+ }
53
+ }
54
+
55
+ export interface MutableLeafMap {
56
+ value: LeafMap;
57
+ }