articulated 0.2.0 → 0.3.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 +56 -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 +42 -0
- package/build/commonjs/internal/seq_map.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 +49 -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 +34 -0
- package/build/esm/internal/seq_map.js.map +1 -0
- package/package.json +11 -5
- package/src/id_list.ts +306 -109
- package/src/internal/leaf_map.ts +59 -0
- package/src/internal/seq_map.ts +50 -0
package/README.md
CHANGED
|
@@ -109,7 +109,24 @@ let newList = IdList.load(savedState);
|
|
|
109
109
|
|
|
110
110
|
IdList stores its state as a modified [B+Tree](https://en.wikipedia.org/wiki/B%2B_tree), described at the top of [its source code](./src/id_list.ts). Each leaf in the B+Tree represents multiple ElementIds (sharing a bunchId and sequential counters) in a compressed way; for normal collaborative text editing, expect 10-20 ElementIds per leaf.
|
|
111
111
|
|
|
112
|
-
|
|
112
|
+
To speed up searches, we also maintain a "bottom-up" tree that maps from each node to a sequence number identifying its parent. (Using sequence numbers instead of pointers is necessary for persistence.) The map is implemented using persistent balanced trees from [functional-red-black-tree](https://www.npmjs.com/package/functional-red-black-tree).
|
|
113
|
+
|
|
114
|
+
Asymptotic runtimes are given in terms of the number of leaves `L` and the maximum "fragmentation" of a leaf `F`, which is the number of times its ElementIds alternate between deleted vs present.
|
|
115
|
+
|
|
116
|
+
- insertAfter, insertBefore: `O(log^2(L) + F)`.
|
|
117
|
+
- The bottleneck is finding the B+Tree path of the before/after ElementId. This requires `O(log(L))` lookups in the bottom-up tree's map, each of which takes `O(log(L))` time. See the implementation of `IdList.locate`.
|
|
118
|
+
- delete, undelete: `O(log^2(L) + F)`.
|
|
119
|
+
- indexOf: `O(log^2(L) + F)`.
|
|
120
|
+
- Bottleneck is locating the id.
|
|
121
|
+
- at: `O(log(L) + F)`.
|
|
122
|
+
- Simple B+Tree search.
|
|
123
|
+
- has, isKnown: `O(log(L) + F)`
|
|
124
|
+
- Part of the bottom-up tree is a sorted map with leaf keys; due to the sort, we can also use that map to look up the leaf corresponding to an ElementId, in `O(log(L))` time.
|
|
125
|
+
- length: `O(1)`.
|
|
126
|
+
- Cached.
|
|
127
|
+
- save: `O(S + L)`, where `S <= L * F` is the saved state's length.
|
|
128
|
+
- load: `O(S * log(S))`
|
|
129
|
+
- The bottleneck is constructing the bottom-up tree: specifically, the map from each leaf to its parent's sequence number (`leafMap`). That map is itself a sorted tree, hence takes `O(L * log(L))` time to construct, and `L <= S`.
|
|
113
130
|
|
|
114
131
|
If you want to get a sense of what IdList is or how to implement your own version, consider reading the source code for [IdListSimple](./test/id_list_simple.ts), which behaves identically to IdList. It is short (<300 SLOC) and direct, using an array and `Array.splice`. The downside is that IdListSimple does not compress ElementIds, and all of its operations take `O(# ids)` time. We use it as a known-good implementation in our fuzz tests.
|
|
115
132
|
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { SparseIndices } from "sparse-array-rled";
|
|
2
2
|
import { ElementId } from "./id";
|
|
3
|
+
import { MutableLeafMap } from "./internal/leaf_map";
|
|
4
|
+
import { MutableSeqMap } from "./internal/seq_map";
|
|
3
5
|
import { SavedIdList } from "./saved_id_list";
|
|
4
6
|
export interface LeafNode {
|
|
5
7
|
readonly bunchId: string;
|
|
@@ -16,19 +18,48 @@ export interface LeafNode {
|
|
|
16
18
|
* An inner node with inner-node children.
|
|
17
19
|
*/
|
|
18
20
|
export declare class InnerNodeInner {
|
|
21
|
+
/**
|
|
22
|
+
* A unique identifer for this node within its IdTree.
|
|
23
|
+
*/
|
|
24
|
+
readonly seq: number;
|
|
19
25
|
readonly children: readonly InnerNode[];
|
|
20
26
|
readonly size: number;
|
|
21
27
|
readonly knownSize: number;
|
|
22
|
-
constructor(
|
|
28
|
+
constructor(
|
|
29
|
+
/**
|
|
30
|
+
* A unique identifer for this node within its IdTree.
|
|
31
|
+
*/
|
|
32
|
+
seq: number, children: readonly InnerNode[],
|
|
33
|
+
/**
|
|
34
|
+
* We add entries for the children to this map, overwriting any existing parentSeqs.
|
|
35
|
+
*
|
|
36
|
+
* Pass null to skip when you are doing it yourself. Regardless, you need to
|
|
37
|
+
* delete any outdated entries yourself.
|
|
38
|
+
*/
|
|
39
|
+
parentSeqsMut: MutableSeqMap | null);
|
|
23
40
|
}
|
|
24
41
|
/**
|
|
25
42
|
* An inner node with leaf children.
|
|
26
43
|
*/
|
|
27
44
|
export declare class InnerNodeLeaf {
|
|
45
|
+
/**
|
|
46
|
+
* A unique identifer for this node within its IdTree.
|
|
47
|
+
*/
|
|
48
|
+
readonly seq: number;
|
|
28
49
|
readonly children: readonly LeafNode[];
|
|
29
50
|
readonly size: number;
|
|
30
51
|
readonly knownSize: number;
|
|
31
|
-
constructor(
|
|
52
|
+
constructor(
|
|
53
|
+
/**
|
|
54
|
+
* A unique identifer for this node within its IdTree.
|
|
55
|
+
*/
|
|
56
|
+
seq: number, children: readonly LeafNode[],
|
|
57
|
+
/**
|
|
58
|
+
* We add entries for the children to this map, overwriting any existing parentSeqs.
|
|
59
|
+
*
|
|
60
|
+
* Pass null to skip when you are doing it yourself.
|
|
61
|
+
*/
|
|
62
|
+
leafMapMut: MutableLeafMap | null);
|
|
32
63
|
}
|
|
33
64
|
export type InnerNode = InnerNodeInner | InnerNodeLeaf;
|
|
34
65
|
/**
|
|
@@ -67,6 +98,18 @@ export declare const M = 8;
|
|
|
67
98
|
*/
|
|
68
99
|
export declare class IdList {
|
|
69
100
|
private readonly root;
|
|
101
|
+
/**
|
|
102
|
+
* A persistent sorted map from each leaf to its parent node's seq.
|
|
103
|
+
*
|
|
104
|
+
* Besides parentSeqs, we also use this to lookup leaves by ElementId.
|
|
105
|
+
*/
|
|
106
|
+
private readonly leafMap;
|
|
107
|
+
/**
|
|
108
|
+
* A persistent map from each InnerNode's seq to its parent node's seq.
|
|
109
|
+
*
|
|
110
|
+
* We map the root's seq to 0 (in our constructor).
|
|
111
|
+
*/
|
|
112
|
+
private readonly parentSeqs;
|
|
70
113
|
/**
|
|
71
114
|
* Internal - construct an IdList using a static method (e.g. `IdList.new`).
|
|
72
115
|
*/
|
|
@@ -150,6 +193,13 @@ export declare class IdList {
|
|
|
150
193
|
* @throws If `id` is not known.
|
|
151
194
|
*/
|
|
152
195
|
undelete(id: ElementId): IdList;
|
|
196
|
+
/**
|
|
197
|
+
* Returns the path from id's leaf node to the root, or null if id is not found.
|
|
198
|
+
*
|
|
199
|
+
* The path contains each node and its index in its parent's node, starting with id's
|
|
200
|
+
* LeafNode and ending at a child of the root.
|
|
201
|
+
*/
|
|
202
|
+
private locate;
|
|
153
203
|
/**
|
|
154
204
|
* Replaces the leaf at the given path with newLeaves.
|
|
155
205
|
* Returns a proper (sufficiently balanced) B+Tree with updated sizes.
|
|
@@ -171,6 +221,10 @@ export declare class IdList {
|
|
|
171
221
|
* Compare to {@link has}.
|
|
172
222
|
*/
|
|
173
223
|
isKnown(id: ElementId): boolean;
|
|
224
|
+
/**
|
|
225
|
+
* Returns true if any of the given bulk ids are known.
|
|
226
|
+
*/
|
|
227
|
+
private isAnyKnown;
|
|
174
228
|
/**
|
|
175
229
|
* The length of the list, counting only present ids.
|
|
176
230
|
*
|
|
@@ -270,21 +324,3 @@ export declare class KnownIdView {
|
|
|
270
324
|
*/
|
|
271
325
|
values(): IterableIterator<ElementId>;
|
|
272
326
|
}
|
|
273
|
-
type Located = [
|
|
274
|
-
{
|
|
275
|
-
node: LeafNode;
|
|
276
|
-
indexInParent: number;
|
|
277
|
-
},
|
|
278
|
-
...{
|
|
279
|
-
node: InnerNode;
|
|
280
|
-
indexInParent: number;
|
|
281
|
-
}[]
|
|
282
|
-
];
|
|
283
|
-
/**
|
|
284
|
-
* Returns the path from id's leaf node to the root, or null if id is not found.
|
|
285
|
-
*
|
|
286
|
-
* The path contains each node and its index in its parent's node, starting with id's
|
|
287
|
-
* LeafNode and ending at a child of the root.
|
|
288
|
-
*/
|
|
289
|
-
export declare function locate(id: ElementId, node: InnerNode): Located | null;
|
|
290
|
-
export {};
|
|
@@ -1,18 +1,35 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.
|
|
3
|
+
exports.KnownIdView = exports.IdList = exports.M = exports.InnerNodeLeaf = exports.InnerNodeInner = void 0;
|
|
4
4
|
const sparse_array_rled_1 = require("sparse-array-rled");
|
|
5
|
+
const leaf_map_1 = require("./internal/leaf_map");
|
|
6
|
+
const seq_map_1 = require("./internal/seq_map");
|
|
5
7
|
/**
|
|
6
8
|
* An inner node with inner-node children.
|
|
7
9
|
*/
|
|
8
10
|
class InnerNodeInner {
|
|
9
|
-
constructor(
|
|
11
|
+
constructor(
|
|
12
|
+
/**
|
|
13
|
+
* A unique identifer for this node within its IdTree.
|
|
14
|
+
*/
|
|
15
|
+
seq, children,
|
|
16
|
+
/**
|
|
17
|
+
* We add entries for the children to this map, overwriting any existing parentSeqs.
|
|
18
|
+
*
|
|
19
|
+
* Pass null to skip when you are doing it yourself. Regardless, you need to
|
|
20
|
+
* delete any outdated entries yourself.
|
|
21
|
+
*/
|
|
22
|
+
parentSeqsMut) {
|
|
23
|
+
this.seq = seq;
|
|
10
24
|
this.children = children;
|
|
11
25
|
let size = 0;
|
|
12
26
|
let knownSize = 0;
|
|
13
27
|
for (const child of children) {
|
|
14
28
|
size += child.size;
|
|
15
29
|
knownSize += child.knownSize;
|
|
30
|
+
if (parentSeqsMut) {
|
|
31
|
+
parentSeqsMut.value = parentSeqsMut.value.set(child.seq, seq);
|
|
32
|
+
}
|
|
16
33
|
}
|
|
17
34
|
this.size = size;
|
|
18
35
|
this.knownSize = knownSize;
|
|
@@ -23,13 +40,27 @@ exports.InnerNodeInner = InnerNodeInner;
|
|
|
23
40
|
* An inner node with leaf children.
|
|
24
41
|
*/
|
|
25
42
|
class InnerNodeLeaf {
|
|
26
|
-
constructor(
|
|
43
|
+
constructor(
|
|
44
|
+
/**
|
|
45
|
+
* A unique identifer for this node within its IdTree.
|
|
46
|
+
*/
|
|
47
|
+
seq, children,
|
|
48
|
+
/**
|
|
49
|
+
* We add entries for the children to this map, overwriting any existing parentSeqs.
|
|
50
|
+
*
|
|
51
|
+
* Pass null to skip when you are doing it yourself.
|
|
52
|
+
*/
|
|
53
|
+
leafMapMut) {
|
|
54
|
+
this.seq = seq;
|
|
27
55
|
this.children = children;
|
|
28
56
|
let size = 0;
|
|
29
57
|
let knownSize = 0;
|
|
30
58
|
for (const child of children) {
|
|
31
59
|
size += child.present.count();
|
|
32
60
|
knownSize += child.count;
|
|
61
|
+
if (leafMapMut) {
|
|
62
|
+
leafMapMut.value = leafMapMut.value.set(child, seq);
|
|
63
|
+
}
|
|
33
64
|
}
|
|
34
65
|
this.size = size;
|
|
35
66
|
this.knownSize = knownSize;
|
|
@@ -74,8 +105,16 @@ class IdList {
|
|
|
74
105
|
/**
|
|
75
106
|
* Internal - construct an IdList using a static method (e.g. `IdList.new`).
|
|
76
107
|
*/
|
|
77
|
-
constructor(root
|
|
108
|
+
constructor(root,
|
|
109
|
+
/**
|
|
110
|
+
* A persistent sorted map from each leaf to its parent node's seq.
|
|
111
|
+
*
|
|
112
|
+
* Besides parentSeqs, we also use this to lookup leaves by ElementId.
|
|
113
|
+
*/
|
|
114
|
+
leafMap, parentSeqs) {
|
|
78
115
|
this.root = root;
|
|
116
|
+
this.leafMap = leafMap;
|
|
117
|
+
this.parentSeqs = parentSeqs.set(root.seq, 0);
|
|
79
118
|
}
|
|
80
119
|
/**
|
|
81
120
|
* Constructs an empty list.
|
|
@@ -84,7 +123,9 @@ class IdList {
|
|
|
84
123
|
* or {@link IdList.load}.
|
|
85
124
|
*/
|
|
86
125
|
static new() {
|
|
87
|
-
|
|
126
|
+
const leafMapMut = { value: leaf_map_1.LeafMap.new() };
|
|
127
|
+
const parentSeqsMut = { value: seq_map_1.SeqMap.new() };
|
|
128
|
+
return new this(new InnerNodeLeaf((0, seq_map_1.getAndBumpNextSeq)(parentSeqsMut), [], leafMapMut), leafMapMut.value, parentSeqsMut.value);
|
|
88
129
|
}
|
|
89
130
|
/**
|
|
90
131
|
* Constructs a list with the given known ids and their isDeleted status, in list order.
|
|
@@ -147,7 +188,7 @@ class IdList {
|
|
|
147
188
|
if (!(Number.isSafeInteger(count) && count >= 0)) {
|
|
148
189
|
throw new Error(`Invalid count: ${count}`);
|
|
149
190
|
}
|
|
150
|
-
if (
|
|
191
|
+
if (this.isAnyKnown(newId, count)) {
|
|
151
192
|
throw new Error("An inserted id is already known");
|
|
152
193
|
}
|
|
153
194
|
if (before === null) {
|
|
@@ -157,21 +198,21 @@ class IdList {
|
|
|
157
198
|
// Insert the first leaf as a child of root.
|
|
158
199
|
const present = sparse_array_rled_1.SparseIndices.new();
|
|
159
200
|
present.set(newId.counter, count);
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
]));
|
|
201
|
+
const leaf = {
|
|
202
|
+
bunchId: newId.bunchId,
|
|
203
|
+
startCounter: newId.counter,
|
|
204
|
+
count,
|
|
205
|
+
present,
|
|
206
|
+
};
|
|
207
|
+
const leafMapMut = { value: this.leafMap };
|
|
208
|
+
return new IdList(new InnerNodeLeaf(this.root.seq, [leaf], leafMapMut), leafMapMut.value, this.parentSeqs);
|
|
168
209
|
}
|
|
169
210
|
else {
|
|
170
211
|
// Insert before the first known id.
|
|
171
212
|
return this.insertBefore(firstId(this.root), newId, count);
|
|
172
213
|
}
|
|
173
214
|
}
|
|
174
|
-
const located = locate(before
|
|
215
|
+
const located = this.locate(before);
|
|
175
216
|
if (located === null) {
|
|
176
217
|
throw new Error("before is not known");
|
|
177
218
|
}
|
|
@@ -247,7 +288,7 @@ class IdList {
|
|
|
247
288
|
if (!(Number.isSafeInteger(count) && count >= 0)) {
|
|
248
289
|
throw new Error(`Invalid count: ${count}`);
|
|
249
290
|
}
|
|
250
|
-
if (
|
|
291
|
+
if (this.isAnyKnown(newId, count)) {
|
|
251
292
|
throw new Error("An inserted id is already known");
|
|
252
293
|
}
|
|
253
294
|
if (after === null) {
|
|
@@ -256,7 +297,7 @@ class IdList {
|
|
|
256
297
|
// Insert after the last known id, or at the beginning if empty.
|
|
257
298
|
return this.insertAfter(this.root.knownSize === 0 ? null : lastId(this.root), newId, count);
|
|
258
299
|
}
|
|
259
|
-
const located = locate(after
|
|
300
|
+
const located = this.locate(after);
|
|
260
301
|
if (located === null) {
|
|
261
302
|
throw new Error("after is not known");
|
|
262
303
|
}
|
|
@@ -323,7 +364,7 @@ class IdList {
|
|
|
323
364
|
* If `id` is already deleted or is not known, this method does nothing.
|
|
324
365
|
*/
|
|
325
366
|
delete(id) {
|
|
326
|
-
const located = locate(id
|
|
367
|
+
const located = this.locate(id);
|
|
327
368
|
if (located === null)
|
|
328
369
|
return this;
|
|
329
370
|
const leaf = located[0].node;
|
|
@@ -344,7 +385,7 @@ class IdList {
|
|
|
344
385
|
* @throws If `id` is not known.
|
|
345
386
|
*/
|
|
346
387
|
undelete(id) {
|
|
347
|
-
const located = locate(id
|
|
388
|
+
const located = this.locate(id);
|
|
348
389
|
if (located === null) {
|
|
349
390
|
throw new Error("id is not known");
|
|
350
391
|
}
|
|
@@ -355,6 +396,52 @@ class IdList {
|
|
|
355
396
|
newPresent.set(id.counter);
|
|
356
397
|
return this.replaceLeaf(located, { ...leaf, present: newPresent });
|
|
357
398
|
}
|
|
399
|
+
/**
|
|
400
|
+
* Returns the path from id's leaf node to the root, or null if id is not found.
|
|
401
|
+
*
|
|
402
|
+
* The path contains each node and its index in its parent's node, starting with id's
|
|
403
|
+
* LeafNode and ending at a child of the root.
|
|
404
|
+
*/
|
|
405
|
+
locate(id) {
|
|
406
|
+
// Find the leaf containing id, if any.
|
|
407
|
+
const [leaf, parentSeq] = this.leafMap.getLeaf(id.bunchId, id.counter);
|
|
408
|
+
if (leaf === undefined)
|
|
409
|
+
return null;
|
|
410
|
+
if (!(leaf.bunchId === id.bunchId &&
|
|
411
|
+
leaf.startCounter <= id.counter &&
|
|
412
|
+
id.counter < leaf.startCounter + leaf.count)) {
|
|
413
|
+
return null;
|
|
414
|
+
}
|
|
415
|
+
// Find the seqs on the path (leaf, root].
|
|
416
|
+
const innerSeqs = [];
|
|
417
|
+
let curSeq = parentSeq;
|
|
418
|
+
while (curSeq !== 0) {
|
|
419
|
+
innerSeqs.push(curSeq);
|
|
420
|
+
curSeq = this.parentSeqs.get(curSeq);
|
|
421
|
+
}
|
|
422
|
+
// Find the nodes and indexInParent's on the path (root, leaf),
|
|
423
|
+
// using seqs to find the appropriate child of each node.
|
|
424
|
+
const innerNodes = [];
|
|
425
|
+
let curParent = this.root;
|
|
426
|
+
// Start at the root child's seq and proceed to the leaf parent's seq.
|
|
427
|
+
for (let i = innerSeqs.length - 2; i >= 0; i--) {
|
|
428
|
+
const children = curParent.children;
|
|
429
|
+
const childIndex = children.findIndex((child) => child.seq === innerSeqs[i]);
|
|
430
|
+
if (childIndex === -1)
|
|
431
|
+
throw new Error("Internal error");
|
|
432
|
+
const child = children[childIndex];
|
|
433
|
+
innerNodes.push({ node: child, indexInParent: childIndex });
|
|
434
|
+
curParent = child;
|
|
435
|
+
}
|
|
436
|
+
// Now curParent is the leaf's parent. Find leaf in its children and return.
|
|
437
|
+
const leafChildIndex = curParent.children.indexOf(leaf);
|
|
438
|
+
if (leafChildIndex === -1)
|
|
439
|
+
throw new Error("Internal error");
|
|
440
|
+
return [
|
|
441
|
+
{ node: leaf, indexInParent: leafChildIndex },
|
|
442
|
+
...innerNodes.reverse(),
|
|
443
|
+
];
|
|
444
|
+
}
|
|
358
445
|
/**
|
|
359
446
|
* Replaces the leaf at the given path with newLeaves.
|
|
360
447
|
* Returns a proper (sufficiently balanced) B+Tree with updated sizes.
|
|
@@ -362,7 +449,10 @@ class IdList {
|
|
|
362
449
|
* newLeaves.length must be in [1, M].
|
|
363
450
|
*/
|
|
364
451
|
replaceLeaf(located, ...newLeaves) {
|
|
365
|
-
|
|
452
|
+
const leafMapMut = { value: this.leafMap };
|
|
453
|
+
const parentSeqsMut = { value: this.parentSeqs };
|
|
454
|
+
const newRoot = replaceNode(located, this.root, leafMapMut, parentSeqsMut, newLeaves, 0);
|
|
455
|
+
return new IdList(newRoot, leafMapMut.value, parentSeqsMut.value);
|
|
366
456
|
}
|
|
367
457
|
// Accessors
|
|
368
458
|
/**
|
|
@@ -373,10 +463,12 @@ class IdList {
|
|
|
373
463
|
* Compare to {@link isKnown}.
|
|
374
464
|
*/
|
|
375
465
|
has(id) {
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
466
|
+
// Find the LeafNode that would contain id if known.
|
|
467
|
+
const [leaf] = this.leafMap.getLeaf(id.bunchId, id.counter);
|
|
468
|
+
if (leaf && leaf.bunchId === id.bunchId) {
|
|
469
|
+
return leaf.present.has(id.counter);
|
|
470
|
+
}
|
|
471
|
+
return false;
|
|
380
472
|
}
|
|
381
473
|
/**
|
|
382
474
|
* Returns whether id is known to this list.
|
|
@@ -384,7 +476,34 @@ class IdList {
|
|
|
384
476
|
* Compare to {@link has}.
|
|
385
477
|
*/
|
|
386
478
|
isKnown(id) {
|
|
387
|
-
|
|
479
|
+
// Find the LeafNode that would contain id if known.
|
|
480
|
+
const [leaf] = this.leafMap.getLeaf(id.bunchId, id.counter);
|
|
481
|
+
if (leaf && leaf.bunchId === id.bunchId) {
|
|
482
|
+
return (leaf.startCounter <= id.counter &&
|
|
483
|
+
id.counter < leaf.startCounter + leaf.count);
|
|
484
|
+
}
|
|
485
|
+
return false;
|
|
486
|
+
}
|
|
487
|
+
// TODO: Make public?
|
|
488
|
+
/**
|
|
489
|
+
* Returns true if any of the given bulk ids are known.
|
|
490
|
+
*/
|
|
491
|
+
isAnyKnown(id, count) {
|
|
492
|
+
if (count === 0)
|
|
493
|
+
return false;
|
|
494
|
+
// Find the leaf containing the last id, or the previous leaf.
|
|
495
|
+
// If any leaf knows any of the ids, this leaf must know an id too.
|
|
496
|
+
const [leaf] = this.leafMap.getLeaf(id.bunchId, id.counter + count - 1);
|
|
497
|
+
if (leaf && leaf.bunchId === id.bunchId) {
|
|
498
|
+
// Test if there is any overlap between the leaf's counter range [a, b]
|
|
499
|
+
// and the bulk ids' counter range [c, d].
|
|
500
|
+
const a = leaf.startCounter;
|
|
501
|
+
const b = leaf.startCounter + leaf.count - 1;
|
|
502
|
+
const c = id.counter;
|
|
503
|
+
const d = id.counter + count - 1;
|
|
504
|
+
return a <= d && c <= b;
|
|
505
|
+
}
|
|
506
|
+
return false;
|
|
388
507
|
}
|
|
389
508
|
/**
|
|
390
509
|
* The length of the list, counting only present ids.
|
|
@@ -449,7 +568,7 @@ class IdList {
|
|
|
449
568
|
* @throws If `id` is not known.
|
|
450
569
|
*/
|
|
451
570
|
indexOf(id, bias = "none") {
|
|
452
|
-
const located = locate(id
|
|
571
|
+
const located = this.locate(id);
|
|
453
572
|
if (located === null)
|
|
454
573
|
throw new Error("id is not known");
|
|
455
574
|
/**
|
|
@@ -530,7 +649,7 @@ class IdList {
|
|
|
530
649
|
* Loads a saved state returned by {@link save}.
|
|
531
650
|
*/
|
|
532
651
|
static load(savedState) {
|
|
533
|
-
// 1. Determine the leaves.
|
|
652
|
+
// 1. Determine the leaves in list order.
|
|
534
653
|
const leaves = [];
|
|
535
654
|
for (let i = 0; i < savedState.length; i++) {
|
|
536
655
|
const item = savedState[i];
|
|
@@ -568,16 +687,23 @@ class IdList {
|
|
|
568
687
|
});
|
|
569
688
|
}
|
|
570
689
|
// 2. Create a B+Tree with the given leaves.
|
|
571
|
-
// We do a "direct" balanced construction that takes O(
|
|
572
|
-
// leaves one-by-one, which would take O(
|
|
690
|
+
// We do a "direct" balanced construction that takes O(L) time, instead of inserting
|
|
691
|
+
// leaves one-by-one, which would take O(L log(L)) time.
|
|
692
|
+
// However, constructing the sorted leafMap brings the overall runtime to O(L log(L)).
|
|
573
693
|
if (leaves.length === 0)
|
|
574
694
|
return IdList.new();
|
|
695
|
+
// TODO: Test the aux data structures after loading.
|
|
696
|
+
// E.g. reload and then call checkAll again.
|
|
697
|
+
// Also should do insertions to test splitting of the full tree.
|
|
698
|
+
const leafMapMut = { value: leaf_map_1.LeafMap.new() };
|
|
699
|
+
const parentSeqsMut = { value: seq_map_1.SeqMap.new() };
|
|
575
700
|
// Depth of the B+Tree (number of non-root nodes on any path from a leaf to the root).
|
|
576
|
-
// A
|
|
701
|
+
// A full B+Tree of depth d has between [M^{d-1} + 1, M^d] leaves.
|
|
577
702
|
const depth = leaves.length === 1
|
|
578
703
|
? 1
|
|
579
704
|
: Math.ceil(Math.log(leaves.length) / Math.log(exports.M));
|
|
580
|
-
|
|
705
|
+
const root = buildTree(leaves, leafMapMut, parentSeqsMut, 0, depth);
|
|
706
|
+
return new IdList(root, leafMapMut.value, parentSeqsMut.value);
|
|
581
707
|
}
|
|
582
708
|
}
|
|
583
709
|
exports.IdList = IdList;
|
|
@@ -647,7 +773,8 @@ class KnownIdView {
|
|
|
647
773
|
* Returns the index of `id` in this view, or -1 if it is not known.
|
|
648
774
|
*/
|
|
649
775
|
indexOf(id) {
|
|
650
|
-
|
|
776
|
+
// @ts-expect-error Ignore private
|
|
777
|
+
const located = this.list.locate(id);
|
|
651
778
|
if (located === null)
|
|
652
779
|
throw new Error("id is not known");
|
|
653
780
|
/**
|
|
@@ -722,70 +849,15 @@ function lastId(node) {
|
|
|
722
849
|
counter: lastLeaf.startCounter + lastLeaf.count - 1,
|
|
723
850
|
};
|
|
724
851
|
}
|
|
725
|
-
/**
|
|
726
|
-
* Returns the path from id's leaf node to the root, or null if id is not found.
|
|
727
|
-
*
|
|
728
|
-
* The path contains each node and its index in its parent's node, starting with id's
|
|
729
|
-
* LeafNode and ending at a child of the root.
|
|
730
|
-
*/
|
|
731
|
-
function locate(id, node) {
|
|
732
|
-
if (node instanceof InnerNodeInner) {
|
|
733
|
-
for (let i = 0; i < node.children.length; i++) {
|
|
734
|
-
const child = node.children[i];
|
|
735
|
-
const childLocated = locate(id, child);
|
|
736
|
-
if (childLocated !== null) {
|
|
737
|
-
childLocated.push({ node: child, indexInParent: i });
|
|
738
|
-
return childLocated;
|
|
739
|
-
}
|
|
740
|
-
}
|
|
741
|
-
}
|
|
742
|
-
else {
|
|
743
|
-
for (let i = 0; i < node.children.length; i++) {
|
|
744
|
-
const child = node.children[i];
|
|
745
|
-
if (child.bunchId === id.bunchId &&
|
|
746
|
-
child.startCounter <= id.counter &&
|
|
747
|
-
id.counter < child.startCounter + child.count) {
|
|
748
|
-
return [{ node: child, indexInParent: i }];
|
|
749
|
-
}
|
|
750
|
-
}
|
|
751
|
-
}
|
|
752
|
-
return null;
|
|
753
|
-
}
|
|
754
|
-
exports.locate = locate;
|
|
755
|
-
/**
|
|
756
|
-
* Returns true if any of the given bulk ids are known within node's subtree.
|
|
757
|
-
*
|
|
758
|
-
* Assumes count > 0.
|
|
759
|
-
*/
|
|
760
|
-
function isAnyKnown(id, count, node) {
|
|
761
|
-
if (node instanceof InnerNodeInner) {
|
|
762
|
-
for (const child of node.children) {
|
|
763
|
-
if (isAnyKnown(id, count, child))
|
|
764
|
-
return true;
|
|
765
|
-
}
|
|
766
|
-
}
|
|
767
|
-
else {
|
|
768
|
-
for (const child of node.children) {
|
|
769
|
-
if (child.bunchId === id.bunchId) {
|
|
770
|
-
// Test if there is any overlap between the child's counter range [a, b]
|
|
771
|
-
// and the bulk id's counter range [c, d].
|
|
772
|
-
const a = child.startCounter;
|
|
773
|
-
const b = child.startCounter + child.count - 1;
|
|
774
|
-
const c = id.counter;
|
|
775
|
-
const d = id.counter + count - 1;
|
|
776
|
-
if (a <= d && c <= b)
|
|
777
|
-
return true;
|
|
778
|
-
}
|
|
779
|
-
}
|
|
780
|
-
}
|
|
781
|
-
return false;
|
|
782
|
-
}
|
|
783
852
|
/**
|
|
784
853
|
* Replace located[i].node with newNodes.
|
|
785
854
|
*
|
|
786
855
|
* newNodes.length must be in [1, M].
|
|
856
|
+
*
|
|
857
|
+
* The returned node's descendants are recorded in leafMapMut and parentSeqsMut,
|
|
858
|
+
* but the node itself is not (since we don't know its parent here).
|
|
787
859
|
*/
|
|
788
|
-
function replaceNode(located, root, newNodes, i) {
|
|
860
|
+
function replaceNode(located, root, leafMapMut, parentSeqsMut, newNodes, i) {
|
|
789
861
|
const parent = i === located.length - 1 ? root : located[i + 1].node;
|
|
790
862
|
const indexInParent = located[i].indexInParent;
|
|
791
863
|
// Copy-on-write version of parent.children.splice(indexInParent, 1, ...newNodes)
|
|
@@ -794,31 +866,54 @@ function replaceNode(located, root, newNodes, i) {
|
|
|
794
866
|
.concat(newNodes, parent.children.slice(indexInParent + 1));
|
|
795
867
|
if (newChildren.length > exports.M) {
|
|
796
868
|
// Split the parent to maintain BTree property (# children <= M).
|
|
869
|
+
// Treat the right parent as "new", getting a new seq.
|
|
797
870
|
const split = Math.ceil(newChildren.length / 2);
|
|
871
|
+
const seqs = [parent.seq, (0, seq_map_1.getAndBumpNextSeq)(parentSeqsMut)];
|
|
798
872
|
const newParents = [
|
|
799
873
|
newChildren.slice(0, split),
|
|
800
874
|
newChildren.slice(split),
|
|
801
|
-
].map((children) => i === 0
|
|
802
|
-
? new InnerNodeLeaf(children)
|
|
803
|
-
: new InnerNodeInner(children));
|
|
875
|
+
].map((children, j) => i === 0
|
|
876
|
+
? new InnerNodeLeaf(seqs[j], children, leafMapMut)
|
|
877
|
+
: new InnerNodeInner(seqs[j], children, parentSeqsMut));
|
|
804
878
|
if (i === located.length - 1) {
|
|
805
879
|
// newParents replace root. We need a new root to hold them.
|
|
806
|
-
return new InnerNodeInner(newParents);
|
|
880
|
+
return new InnerNodeInner((0, seq_map_1.getAndBumpNextSeq)(parentSeqsMut), newParents, parentSeqsMut);
|
|
807
881
|
}
|
|
808
882
|
else {
|
|
809
|
-
return replaceNode(located, root, newParents, i + 1);
|
|
883
|
+
return replaceNode(located, root, leafMapMut, parentSeqsMut, newParents, i + 1);
|
|
810
884
|
}
|
|
811
885
|
}
|
|
812
886
|
else {
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
887
|
+
// "Replace" parent, reusing its seq.
|
|
888
|
+
// To avoid doing newChildren.length sets every time (which makes replaceLeaf
|
|
889
|
+
// do >=(M/2)*log(L) total sets, even when none were necessary),
|
|
890
|
+
// we bypass the InnerNode constructors' leafMap/parentSeq operations,
|
|
891
|
+
// instead doing them ourselves only on the changed children.
|
|
892
|
+
let newParent;
|
|
893
|
+
if (i === 0) {
|
|
894
|
+
newParent = new InnerNodeLeaf(parent.seq, newChildren, null);
|
|
895
|
+
// Important to delete the replaced leaf's entry, so that it doesn't corrupt by-ElementId searches.
|
|
896
|
+
leafMapMut.value = leafMapMut.value.delete(located[0].node);
|
|
897
|
+
for (const newNode of newNodes) {
|
|
898
|
+
leafMapMut.value = leafMapMut.value.set(newNode, parent.seq);
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
else {
|
|
902
|
+
newParent = new InnerNodeInner(parent.seq, newChildren, null);
|
|
903
|
+
for (const newNode of newNodes) {
|
|
904
|
+
if (newNode.seq !== located[i].node.seq) {
|
|
905
|
+
parentSeqsMut.value = parentSeqsMut.value.set(newNode.seq, parent.seq);
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
// If the replaced node isn't represented in newNodes (i.e., same seq is not reused),
|
|
909
|
+
// we could delete its entry to save memory, but it is not necessary.
|
|
910
|
+
}
|
|
816
911
|
if (i === located.length - 1) {
|
|
817
912
|
// Replaces root.
|
|
818
913
|
return newParent;
|
|
819
914
|
}
|
|
820
915
|
else {
|
|
821
|
-
return replaceNode(located, root, [newParent], i + 1);
|
|
916
|
+
return replaceNode(located, root, leafMapMut, parentSeqsMut, [newParent], i + 1);
|
|
822
917
|
}
|
|
823
918
|
}
|
|
824
919
|
}
|
|
@@ -962,13 +1057,18 @@ function pushSaveItem(acc, item) {
|
|
|
962
1057
|
/**
|
|
963
1058
|
* Builds a tree with the given leaves. Used by IdList.load.
|
|
964
1059
|
*
|
|
965
|
-
*
|
|
966
|
-
*
|
|
967
|
-
*
|
|
1060
|
+
* The returned node's descendants are recorded in leafMapMut and parentSeqsMut,
|
|
1061
|
+
* but not the node itself (since we don't know its parent here).
|
|
1062
|
+
*
|
|
1063
|
+
* In contrast to inserting the leaves one-by-one, this function fills nodes
|
|
1064
|
+
* with M children whenever possible,
|
|
1065
|
+
* and the B+Tree parts run in O(L) time instead of O(L log(L)).
|
|
1066
|
+
* However, the overall runtime is O(L log(L)) from constructing the sorted leafMap.
|
|
968
1067
|
*/
|
|
969
|
-
function buildTree(leaves, startIndex, depthRemaining) {
|
|
1068
|
+
function buildTree(leaves, leafMapMut, parentSeqsMut, startIndex, depthRemaining) {
|
|
1069
|
+
const parentSeq = (0, seq_map_1.getAndBumpNextSeq)(parentSeqsMut);
|
|
970
1070
|
if (depthRemaining === 1) {
|
|
971
|
-
return new InnerNodeLeaf(leaves.slice(startIndex, startIndex + exports.M));
|
|
1071
|
+
return new InnerNodeLeaf(parentSeq, leaves.slice(startIndex, startIndex + exports.M), leafMapMut);
|
|
972
1072
|
}
|
|
973
1073
|
else {
|
|
974
1074
|
const children = [];
|
|
@@ -977,9 +1077,9 @@ function buildTree(leaves, startIndex, depthRemaining) {
|
|
|
977
1077
|
const childStartIndex = startIndex + i * childLeafCount;
|
|
978
1078
|
if (childStartIndex >= leaves.length)
|
|
979
1079
|
break;
|
|
980
|
-
children.push(buildTree(leaves, childStartIndex, depthRemaining - 1));
|
|
1080
|
+
children.push(buildTree(leaves, leafMapMut, parentSeqsMut, childStartIndex, depthRemaining - 1));
|
|
981
1081
|
}
|
|
982
|
-
return new InnerNodeInner(children);
|
|
1082
|
+
return new InnerNodeInner(parentSeq, children, parentSeqsMut);
|
|
983
1083
|
}
|
|
984
1084
|
}
|
|
985
1085
|
//# sourceMappingURL=id_list.js.map
|