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.
- package/README.md +18 -1
- package/build/commonjs/id_list.d.ts +56 -20
- package/build/commonjs/id_list.js +207 -107
- package/build/commonjs/id_list.js.map +1 -1
- package/build/commonjs/internal/leaf_map.d.ts +25 -0
- package/build/commonjs/internal/leaf_map.js +54 -0
- package/build/commonjs/internal/leaf_map.js.map +1 -0
- package/build/commonjs/internal/seq_map.d.ts +20 -0
- package/build/commonjs/internal/seq_map.js +40 -0
- package/build/commonjs/internal/seq_map.js.map +1 -0
- package/build/commonjs/vendor/functional-red-black-tree.d.ts +8 -0
- package/build/commonjs/vendor/functional-red-black-tree.js +911 -0
- package/build/commonjs/vendor/functional-red-black-tree.js.map +1 -0
- package/build/esm/id_list.d.ts +56 -20
- package/build/esm/id_list.js +206 -105
- package/build/esm/id_list.js.map +1 -1
- package/build/esm/internal/leaf_map.d.ts +25 -0
- package/build/esm/internal/leaf_map.js +47 -0
- package/build/esm/internal/leaf_map.js.map +1 -0
- package/build/esm/internal/seq_map.d.ts +20 -0
- package/build/esm/internal/seq_map.js +32 -0
- package/build/esm/internal/seq_map.js.map +1 -0
- package/build/esm/vendor/functional-red-black-tree.d.ts +8 -0
- package/build/esm/vendor/functional-red-black-tree.js +911 -0
- package/build/esm/vendor/functional-red-black-tree.js.map +1 -0
- package/package.json +10 -5
- package/src/id_list.ts +306 -109
- package/src/internal/leaf_map.ts +57 -0
- package/src/internal/seq_map.ts +48 -0
- package/src/vendor/functional-red-black-tree.d.ts +177 -0
- 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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
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
|
|
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 (
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
472
|
-
|
|
473
|
-
|
|
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
|
-
|
|
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
|
|
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(
|
|
696
|
-
// leaves one-by-one, which would take O(
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
1088
|
+
return new InnerNodeInner(
|
|
1089
|
+
getAndBumpNextSeq(parentSeqsMut),
|
|
1090
|
+
newParents,
|
|
1091
|
+
parentSeqsMut
|
|
1092
|
+
);
|
|
960
1093
|
} else {
|
|
961
|
-
return replaceNode(
|
|
1094
|
+
return replaceNode(
|
|
1095
|
+
located,
|
|
1096
|
+
root,
|
|
1097
|
+
leafMapMut,
|
|
1098
|
+
parentSeqsMut,
|
|
1099
|
+
newParents,
|
|
1100
|
+
i + 1
|
|
1101
|
+
);
|
|
962
1102
|
}
|
|
963
1103
|
} else {
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
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(
|
|
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
|
-
*
|
|
1129
|
-
*
|
|
1130
|
-
*
|
|
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(
|
|
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(
|
|
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
|
+
}
|