articulated 0.1.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 +53 -26
- package/build/commonjs/id.d.ts +3 -6
- package/build/commonjs/id.js.map +1 -1
- package/build/commonjs/id_list.d.ts +152 -52
- package/build/commonjs/id_list.js +845 -186
- package/build/commonjs/id_list.js.map +1 -1
- package/build/commonjs/index.d.ts +1 -1
- package/build/commonjs/index.js +4 -1
- package/build/commonjs/index.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/commonjs/saved_id_list.d.ts +5 -5
- package/build/esm/id.d.ts +3 -6
- package/build/esm/id.js.map +1 -1
- package/build/esm/id_list.d.ts +152 -52
- package/build/esm/id_list.js +842 -185
- package/build/esm/id_list.js.map +1 -1
- package/build/esm/index.d.ts +1 -1
- package/build/esm/index.js +1 -1
- package/build/esm/index.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/build/esm/saved_id_list.d.ts +5 -5
- package/package.json +13 -2
- package/src/id.ts +3 -6
- package/src/id_list.ts +1066 -191
- package/src/index.ts +1 -1
- package/src/internal/leaf_map.ts +59 -0
- package/src/internal/seq_map.ts +50 -0
- package/src/saved_id_list.ts +5 -5
package/src/id_list.ts
CHANGED
|
@@ -1,13 +1,146 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { SparseIndices } from "sparse-array-rled";
|
|
2
|
+
import { ElementId } from "./id";
|
|
3
|
+
import { LeafMap, MutableLeafMap } from "./internal/leaf_map";
|
|
4
|
+
import { getAndBumpNextSeq, MutableSeqMap, SeqMap } from "./internal/seq_map";
|
|
2
5
|
import { SavedIdList } from "./saved_id_list";
|
|
3
6
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
+
// Most exports are only for tests. See index.ts for public exports.
|
|
8
|
+
|
|
9
|
+
/*
|
|
10
|
+
IdList implementation using a modified B+Tree.
|
|
11
|
+
|
|
12
|
+
See tests/id_list_simple.ts for a simpler implementation with the same API but
|
|
13
|
+
impractical efficiency (linear time ops; one object in memory per id).
|
|
14
|
+
The fuzz tests compare that implementation to this one.
|
|
15
|
+
|
|
16
|
+
The B+Tree is unusual in that it has no keys, only values (= ids). The order on the values
|
|
17
|
+
is determined "by fiat" using insertAfter/insertBefore instead of using sorted keys.
|
|
18
|
+
|
|
19
|
+
The leaves in the B+Tree are not individual ids; instead, each leaf is a compressed representation of a groups of ids
|
|
20
|
+
with the same bunchId and sequential counters. Each leaf also contains a `present`
|
|
21
|
+
field to track which of its ids are deleted.
|
|
22
|
+
(Unlike in a SavedIdList, we do not separate adjacent ids with different isDeleted statuses.)
|
|
23
|
+
|
|
24
|
+
Note that it is possible for adjacent leaves to be mergeable (i.e., they could be one leaf) but not merged.
|
|
25
|
+
This happens if you insert the middle ids later (e.g., 0, 2, 1).
|
|
26
|
+
It has a slight perf penalty that goes away once you reload.
|
|
27
|
+
Note that save() needs to work around this possibility - see pushSaveItem.
|
|
28
|
+
|
|
29
|
+
The B+Tree also stores two statistics about each subtree: its size (# of present ids)
|
|
30
|
+
and its knownSize (# of known ids). These allow indexed access in log time.
|
|
31
|
+
|
|
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.
|
|
40
|
+
*/
|
|
41
|
+
|
|
42
|
+
export interface LeafNode {
|
|
43
|
+
readonly bunchId: string;
|
|
44
|
+
readonly startCounter: number;
|
|
45
|
+
readonly count: number;
|
|
46
|
+
/**
|
|
47
|
+
* The present counter values in this leaf node.
|
|
48
|
+
*
|
|
49
|
+
* Note that it is indexed by counter, not by (counter - this.startCounter).
|
|
50
|
+
*/
|
|
51
|
+
readonly present: SparseIndices;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* An inner node with inner-node children.
|
|
56
|
+
*/
|
|
57
|
+
export class InnerNodeInner {
|
|
58
|
+
readonly size: number;
|
|
59
|
+
readonly knownSize: number;
|
|
60
|
+
|
|
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
|
+
) {
|
|
75
|
+
let size = 0;
|
|
76
|
+
let knownSize = 0;
|
|
77
|
+
for (const child of children) {
|
|
78
|
+
size += child.size;
|
|
79
|
+
knownSize += child.knownSize;
|
|
80
|
+
if (parentSeqsMut) {
|
|
81
|
+
parentSeqsMut.value = parentSeqsMut.value.set(child.seq, seq);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
this.size = size;
|
|
85
|
+
this.knownSize = knownSize;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* An inner node with leaf children.
|
|
91
|
+
*/
|
|
92
|
+
export class InnerNodeLeaf {
|
|
93
|
+
readonly size: number;
|
|
94
|
+
readonly knownSize: number;
|
|
95
|
+
|
|
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
|
+
) {
|
|
109
|
+
let size = 0;
|
|
110
|
+
let knownSize = 0;
|
|
111
|
+
for (const child of children) {
|
|
112
|
+
size += child.present.count();
|
|
113
|
+
knownSize += child.count;
|
|
114
|
+
if (leafMapMut) {
|
|
115
|
+
leafMapMut.value = leafMapMut.value.set(child, seq);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
this.size = size;
|
|
119
|
+
this.knownSize = knownSize;
|
|
120
|
+
}
|
|
7
121
|
}
|
|
8
122
|
|
|
123
|
+
export type InnerNode = InnerNodeInner | InnerNodeLeaf;
|
|
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
|
+
|
|
9
131
|
/**
|
|
10
|
-
*
|
|
132
|
+
* The B+Tree's branching factor, i.e., the max number of children of a node.
|
|
133
|
+
*
|
|
134
|
+
* Note that our B+Tree has no keys - in particular, no keys in internal nodes.
|
|
135
|
+
*
|
|
136
|
+
* Wiki B+Tree: "B+ trees can also be used for data stored in RAM.
|
|
137
|
+
* In this case a reasonable choice for block size would be the size of [the] processor's cache line."
|
|
138
|
+
* (64 byte cache line) / (8 byte pointer) = 8.
|
|
139
|
+
*/
|
|
140
|
+
export const M = 8;
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* A list of ElementIds, as a persistent (immutable) data structure.
|
|
11
144
|
*
|
|
12
145
|
* An IdList helps you assign a unique immutable id to each element of a list, such
|
|
13
146
|
* as a todo-list or a text document (= list of characters). That way, you can keep track
|
|
@@ -16,11 +149,14 @@ interface ListElement {
|
|
|
16
149
|
*
|
|
17
150
|
* Any id that has been inserted into an IdList remains **known** to that list indefinitely,
|
|
18
151
|
* allowing you to reference it in insertAfter/insertBefore operations. Calling {@link delete}
|
|
19
|
-
* merely marks an id as deleted (not present); it
|
|
152
|
+
* merely marks an id as deleted (= not present); a deleted id does not count towards the length of the list or index-based accessors, but it does remain in memory as a "tombstone".
|
|
20
153
|
* This is useful in collaborative settings, since another user might instruct you to
|
|
21
154
|
* call `insertAfter(before, newId)` when you have already deleted `before` locally.
|
|
22
|
-
*
|
|
23
|
-
*
|
|
155
|
+
*
|
|
156
|
+
* To enable easy and efficient rollbacks, such as in a
|
|
157
|
+
* [server reconciliation](https://mattweidner.com/2024/06/04/server-architectures.html#1-server-reconciliation)
|
|
158
|
+
* architecture, IdList is a persistent (immutable) data structure. Mutating methods
|
|
159
|
+
* return a new IdList, sharing memory with the old IdList where possible.
|
|
24
160
|
*
|
|
25
161
|
* See {@link ElementId} for advice on generating ElementIds. IdList is optimized for
|
|
26
162
|
* the case where sequential ElementIds often have the same bunchId and sequential counters.
|
|
@@ -28,30 +164,77 @@ interface ListElement {
|
|
|
28
164
|
* cause such ids to be separated, partially deleted, or even reordered.
|
|
29
165
|
*/
|
|
30
166
|
export class IdList {
|
|
31
|
-
|
|
32
|
-
|
|
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
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Internal - construct an IdList using a static method (e.g. `IdList.new`).
|
|
176
|
+
*/
|
|
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
|
+
}
|
|
33
189
|
|
|
34
190
|
/**
|
|
35
191
|
* Constructs an empty list.
|
|
36
192
|
*
|
|
37
|
-
* To begin with a non-empty list, use {@link IdList.from}
|
|
193
|
+
* To begin with a non-empty list, use {@link IdList.from}, {@link IdList.fromIds},
|
|
194
|
+
* or {@link IdList.load}.
|
|
38
195
|
*/
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
196
|
+
static new() {
|
|
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
|
+
);
|
|
42
204
|
}
|
|
43
205
|
|
|
44
206
|
/**
|
|
45
207
|
* Constructs a list with the given known ids and their isDeleted status, in list order.
|
|
46
208
|
*/
|
|
47
|
-
static from(
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
209
|
+
static from(
|
|
210
|
+
knownIds: Iterable<{ id: ElementId; isDeleted: boolean }>
|
|
211
|
+
): IdList {
|
|
212
|
+
// Convert knownIds to a saved state and load that.
|
|
213
|
+
const savedState: SavedIdList = [];
|
|
214
|
+
|
|
215
|
+
for (const { id, isDeleted } of knownIds) {
|
|
216
|
+
if (savedState.length !== 0) {
|
|
217
|
+
const current = savedState.at(-1)!;
|
|
218
|
+
if (
|
|
219
|
+
id.bunchId === current.bunchId &&
|
|
220
|
+
id.counter === current.startCounter + current.count &&
|
|
221
|
+
isDeleted === current.isDeleted
|
|
222
|
+
) {
|
|
223
|
+
// @ts-expect-error Mutating for convenience; no aliasing to worry about.
|
|
224
|
+
current.count++;
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
savedState.push({
|
|
230
|
+
bunchId: id.bunchId,
|
|
231
|
+
startCounter: id.counter,
|
|
232
|
+
count: 1,
|
|
233
|
+
isDeleted,
|
|
234
|
+
});
|
|
53
235
|
}
|
|
54
|
-
|
|
236
|
+
|
|
237
|
+
return IdList.load(savedState);
|
|
55
238
|
}
|
|
56
239
|
|
|
57
240
|
/**
|
|
@@ -61,17 +244,18 @@ export class IdList {
|
|
|
61
244
|
* specify known-but-deleted ids. That way, you can reference the known-but-deleted ids
|
|
62
245
|
* in future insertAfter/insertBefore operations.
|
|
63
246
|
*/
|
|
64
|
-
static fromIds(ids: Iterable<ElementId>) {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
return list;
|
|
247
|
+
static fromIds(ids: Iterable<ElementId>): IdList {
|
|
248
|
+
return this.from(
|
|
249
|
+
(function* () {
|
|
250
|
+
for (const id of ids) yield { id, isDeleted: false };
|
|
251
|
+
})()
|
|
252
|
+
);
|
|
71
253
|
}
|
|
72
254
|
|
|
73
255
|
/**
|
|
74
256
|
* Inserts `newId` immediately after the given id (`before`), which may be deleted.
|
|
257
|
+
* A new IdList is returned and the current list remains unchanged.
|
|
258
|
+
*
|
|
75
259
|
* All ids to the right of `before` are shifted one index to the right, in the manner
|
|
76
260
|
* of [Array.splice](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/splice).
|
|
77
261
|
*
|
|
@@ -81,30 +265,111 @@ export class IdList {
|
|
|
81
265
|
* @param count Provide this to bulk-insert `count` ids from left-to-right,
|
|
82
266
|
* starting with newId and proceeding with the same bunchId and sequential counters.
|
|
83
267
|
* @throws If `before` is not known.
|
|
84
|
-
* @throws If
|
|
268
|
+
* @throws If any inserted id is already known.
|
|
85
269
|
*/
|
|
86
|
-
insertAfter(before: ElementId | null, newId: ElementId, count = 1) {
|
|
87
|
-
if (
|
|
88
|
-
throw new Error(
|
|
270
|
+
insertAfter(before: ElementId | null, newId: ElementId, count = 1): IdList {
|
|
271
|
+
if (!(Number.isSafeInteger(newId.counter) && newId.counter >= 0)) {
|
|
272
|
+
throw new Error(`Invalid counter: ${newId.counter}`);
|
|
273
|
+
}
|
|
274
|
+
if (!(Number.isSafeInteger(count) && count >= 0)) {
|
|
275
|
+
throw new Error(`Invalid count: ${count}`);
|
|
276
|
+
}
|
|
277
|
+
if (this.isAnyKnown(newId, count)) {
|
|
278
|
+
throw new Error("An inserted id is already known");
|
|
89
279
|
}
|
|
90
280
|
|
|
91
|
-
let index: number;
|
|
92
281
|
if (before === null) {
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
282
|
+
if (count === 0) return this;
|
|
283
|
+
|
|
284
|
+
if (this.root.children.length === 0) {
|
|
285
|
+
// Insert the first leaf as a child of root.
|
|
286
|
+
const present = SparseIndices.new();
|
|
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 };
|
|
296
|
+
return new IdList(
|
|
297
|
+
new InnerNodeLeaf(this.root.seq, [leaf], leafMapMut),
|
|
298
|
+
leafMapMut.value,
|
|
299
|
+
this.parentSeqs
|
|
300
|
+
);
|
|
301
|
+
} else {
|
|
302
|
+
// Insert before the first known id.
|
|
303
|
+
return this.insertBefore(firstId(this.root), newId, count);
|
|
99
304
|
}
|
|
100
305
|
}
|
|
101
306
|
|
|
102
|
-
|
|
103
|
-
|
|
307
|
+
const located = this.locate(before);
|
|
308
|
+
if (located === null) {
|
|
309
|
+
throw new Error("before is not known");
|
|
310
|
+
}
|
|
311
|
+
if (count === 0) return this;
|
|
312
|
+
const leaf = located[0].node;
|
|
313
|
+
|
|
314
|
+
if (before.counter === leaf.startCounter + leaf.count - 1) {
|
|
315
|
+
// before is leaf's last id: we insert directly after leaf.
|
|
316
|
+
if (
|
|
317
|
+
leaf.bunchId === newId.bunchId &&
|
|
318
|
+
leaf.startCounter + leaf.count === newId.counter
|
|
319
|
+
) {
|
|
320
|
+
// Extending leaf forwards.
|
|
321
|
+
const present = leaf.present.clone();
|
|
322
|
+
present.set(newId.counter, count);
|
|
323
|
+
return this.replaceLeaf(located, {
|
|
324
|
+
...leaf,
|
|
325
|
+
count: leaf.count + count,
|
|
326
|
+
present,
|
|
327
|
+
});
|
|
328
|
+
} else {
|
|
329
|
+
const present = SparseIndices.new();
|
|
330
|
+
present.set(newId.counter, count);
|
|
331
|
+
return this.replaceLeaf(located, leaf, {
|
|
332
|
+
bunchId: newId.bunchId,
|
|
333
|
+
startCounter: newId.counter,
|
|
334
|
+
count,
|
|
335
|
+
present,
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
} else {
|
|
339
|
+
// before is not leaf's last id: we need to split leaf and insert there.
|
|
340
|
+
const newPresent = SparseIndices.new();
|
|
341
|
+
newPresent.set(newId.counter, count);
|
|
342
|
+
const [leftPresent, rightPresent] = splitPresent(
|
|
343
|
+
leaf.present,
|
|
344
|
+
before.counter + 1
|
|
345
|
+
);
|
|
346
|
+
return this.replaceLeaf(
|
|
347
|
+
located,
|
|
348
|
+
{
|
|
349
|
+
...leaf,
|
|
350
|
+
count: before.counter + 1 - leaf.startCounter,
|
|
351
|
+
present: leftPresent,
|
|
352
|
+
},
|
|
353
|
+
{
|
|
354
|
+
bunchId: newId.bunchId,
|
|
355
|
+
startCounter: newId.counter,
|
|
356
|
+
count,
|
|
357
|
+
present: newPresent,
|
|
358
|
+
},
|
|
359
|
+
{
|
|
360
|
+
...leaf,
|
|
361
|
+
startCounter: before.counter + 1,
|
|
362
|
+
count: leaf.count - (before.counter + 1 - leaf.startCounter),
|
|
363
|
+
present: rightPresent,
|
|
364
|
+
}
|
|
365
|
+
);
|
|
366
|
+
}
|
|
104
367
|
}
|
|
105
368
|
|
|
106
369
|
/**
|
|
107
370
|
* Inserts `newId` immediately before the given id (`after`), which may be deleted.
|
|
371
|
+
* A new IdList is returned and the current list remains unchanged.
|
|
372
|
+
*
|
|
108
373
|
* All ids to the right of `after`, plus `after` itself, are shifted one index to the right, in the manner
|
|
109
374
|
* of [Array.splice](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/splice).
|
|
110
375
|
*
|
|
@@ -117,80 +382,218 @@ export class IdList {
|
|
|
117
382
|
* @throws If `after` is not known.
|
|
118
383
|
* @throws If `newId` is already known.
|
|
119
384
|
*/
|
|
120
|
-
insertBefore(after: ElementId | null, newId: ElementId, count = 1) {
|
|
121
|
-
if (
|
|
122
|
-
throw new Error(
|
|
385
|
+
insertBefore(after: ElementId | null, newId: ElementId, count = 1): IdList {
|
|
386
|
+
if (!(Number.isSafeInteger(newId.counter) && newId.counter >= 0)) {
|
|
387
|
+
throw new Error(`Invalid counter: ${newId.counter}`);
|
|
388
|
+
}
|
|
389
|
+
if (!(Number.isSafeInteger(count) && count >= 0)) {
|
|
390
|
+
throw new Error(`Invalid count: ${count}`);
|
|
391
|
+
}
|
|
392
|
+
if (this.isAnyKnown(newId, count)) {
|
|
393
|
+
throw new Error("An inserted id is already known");
|
|
123
394
|
}
|
|
124
395
|
|
|
125
|
-
let index: number;
|
|
126
396
|
if (after === null) {
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
397
|
+
if (count === 0) return this;
|
|
398
|
+
|
|
399
|
+
// Insert after the last known id, or at the beginning if empty.
|
|
400
|
+
return this.insertAfter(
|
|
401
|
+
this.root.knownSize === 0 ? null : lastId(this.root),
|
|
402
|
+
newId,
|
|
403
|
+
count
|
|
404
|
+
);
|
|
133
405
|
}
|
|
134
406
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
407
|
+
const located = this.locate(after);
|
|
408
|
+
if (located === null) {
|
|
409
|
+
throw new Error("after is not known");
|
|
410
|
+
}
|
|
411
|
+
if (count === 0) return this;
|
|
412
|
+
const leaf = located[0].node;
|
|
139
413
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
414
|
+
if (after.counter === leaf.startCounter) {
|
|
415
|
+
// after is leaf's first id: we insert directly before leaf.
|
|
416
|
+
if (
|
|
417
|
+
leaf.bunchId === newId.bunchId &&
|
|
418
|
+
leaf.startCounter === newId.counter + count
|
|
419
|
+
) {
|
|
420
|
+
// Extending leaf backwards.
|
|
421
|
+
const present = leaf.present.clone();
|
|
422
|
+
present.set(newId.counter, count);
|
|
423
|
+
return this.replaceLeaf(located, {
|
|
424
|
+
...leaf,
|
|
425
|
+
startCounter: leaf.startCounter - count,
|
|
426
|
+
count: leaf.count + count,
|
|
427
|
+
present,
|
|
428
|
+
});
|
|
429
|
+
} else {
|
|
430
|
+
const present = SparseIndices.new();
|
|
431
|
+
present.set(newId.counter, count);
|
|
432
|
+
return this.replaceLeaf(
|
|
433
|
+
located,
|
|
434
|
+
{
|
|
435
|
+
bunchId: newId.bunchId,
|
|
436
|
+
startCounter: newId.counter,
|
|
437
|
+
count,
|
|
438
|
+
present,
|
|
439
|
+
},
|
|
440
|
+
leaf
|
|
441
|
+
);
|
|
442
|
+
}
|
|
443
|
+
} else {
|
|
444
|
+
// after is not leaf's first id: we need to split leaf and insert there.
|
|
445
|
+
const present = SparseIndices.new();
|
|
446
|
+
present.set(newId.counter, count);
|
|
447
|
+
const [leftPresent, rightPresent] = splitPresent(
|
|
448
|
+
leaf.present,
|
|
449
|
+
after.counter
|
|
450
|
+
);
|
|
451
|
+
return this.replaceLeaf(
|
|
452
|
+
located,
|
|
453
|
+
{
|
|
454
|
+
...leaf,
|
|
455
|
+
count: after.counter - leaf.startCounter,
|
|
456
|
+
present: leftPresent,
|
|
457
|
+
},
|
|
458
|
+
{
|
|
459
|
+
bunchId: newId.bunchId,
|
|
460
|
+
startCounter: newId.counter,
|
|
461
|
+
count,
|
|
462
|
+
present,
|
|
463
|
+
},
|
|
464
|
+
{
|
|
465
|
+
...leaf,
|
|
466
|
+
startCounter: after.counter,
|
|
467
|
+
count: leaf.count - (after.counter - leaf.startCounter),
|
|
468
|
+
present: rightPresent,
|
|
469
|
+
}
|
|
470
|
+
);
|
|
154
471
|
}
|
|
155
472
|
}
|
|
156
473
|
|
|
157
474
|
/**
|
|
158
|
-
* Marks `id` as deleted from this list.
|
|
475
|
+
* Marks `id` as deleted from this list.
|
|
476
|
+
* A new IdList is returned and the current list remains unchanged.
|
|
159
477
|
*
|
|
478
|
+
* Once deleted, `id` does not count towards the length of the list or index-based accessors.
|
|
479
|
+
* However, it remains known (a "tombstone").
|
|
160
480
|
* Because `id` is still known, you can reference it in future insertAfter/insertBefore
|
|
161
481
|
* operations, including ones sent concurrently by other devices.
|
|
162
|
-
*
|
|
163
|
-
*
|
|
164
|
-
* For an exact inverse to `insertAfter(-, id)` or `insertBefore(-, id)`
|
|
165
|
-
* that makes `id` no longer known, see {@link uninsert}.
|
|
482
|
+
* This does have a memory cost, but it is compressed in common cases.
|
|
166
483
|
*
|
|
167
|
-
* If `id` is already deleted or not known, this method does nothing.
|
|
484
|
+
* If `id` is already deleted or is not known, this method does nothing.
|
|
168
485
|
*/
|
|
169
486
|
delete(id: ElementId) {
|
|
170
|
-
const
|
|
171
|
-
if (
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
487
|
+
const located = this.locate(id);
|
|
488
|
+
if (located === null) return this;
|
|
489
|
+
|
|
490
|
+
const leaf = located[0].node;
|
|
491
|
+
if (!leaf.present.has(id.counter)) return this;
|
|
492
|
+
|
|
493
|
+
const newPresent = leaf.present.clone();
|
|
494
|
+
newPresent.delete(id.counter);
|
|
495
|
+
|
|
496
|
+
return this.replaceLeaf(located, { ...leaf, present: newPresent });
|
|
175
497
|
}
|
|
176
498
|
|
|
177
499
|
/**
|
|
178
500
|
* Un-marks `id` as deleted from this list, making it present again.
|
|
179
|
-
*
|
|
501
|
+
* A new IdList is returned and the current list remains unchanged.
|
|
502
|
+
*
|
|
503
|
+
* This method is an exact inverse to {@link delete}.
|
|
180
504
|
*
|
|
181
505
|
* If `id` is already present, this method does nothing.
|
|
182
506
|
*
|
|
183
507
|
* @throws If `id` is not known.
|
|
184
508
|
*/
|
|
185
509
|
undelete(id: ElementId) {
|
|
186
|
-
const
|
|
187
|
-
if (
|
|
510
|
+
const located = this.locate(id);
|
|
511
|
+
if (located === null) {
|
|
188
512
|
throw new Error("id is not known");
|
|
189
513
|
}
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
514
|
+
|
|
515
|
+
const leaf = located[0].node;
|
|
516
|
+
if (leaf.present.has(id.counter)) return this;
|
|
517
|
+
|
|
518
|
+
const newPresent = leaf.present.clone();
|
|
519
|
+
newPresent.set(id.counter);
|
|
520
|
+
|
|
521
|
+
return this.replaceLeaf(located, { ...leaf, present: newPresent });
|
|
522
|
+
}
|
|
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;
|
|
193
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
|
+
|
|
578
|
+
/**
|
|
579
|
+
* Replaces the leaf at the given path with newLeaves.
|
|
580
|
+
* Returns a proper (sufficiently balanced) B+Tree with updated sizes.
|
|
581
|
+
*
|
|
582
|
+
* newLeaves.length must be in [1, M].
|
|
583
|
+
*/
|
|
584
|
+
private replaceLeaf(located: Located, ...newLeaves: LeafNode[]): IdList {
|
|
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);
|
|
194
597
|
}
|
|
195
598
|
|
|
196
599
|
// Accessors
|
|
@@ -203,9 +606,13 @@ export class IdList {
|
|
|
203
606
|
* Compare to {@link isKnown}.
|
|
204
607
|
*/
|
|
205
608
|
has(id: ElementId): boolean {
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
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;
|
|
209
616
|
}
|
|
210
617
|
|
|
211
618
|
/**
|
|
@@ -214,7 +621,49 @@ export class IdList {
|
|
|
214
621
|
* Compare to {@link has}.
|
|
215
622
|
*/
|
|
216
623
|
isKnown(id: ElementId): boolean {
|
|
217
|
-
|
|
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;
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
/**
|
|
661
|
+
* The length of the list, counting only present ids.
|
|
662
|
+
*
|
|
663
|
+
* To include known but deleted ids, use `this.knownIds.length`.
|
|
664
|
+
*/
|
|
665
|
+
get length() {
|
|
666
|
+
return this.root.size;
|
|
218
667
|
}
|
|
219
668
|
|
|
220
669
|
/**
|
|
@@ -228,14 +677,36 @@ export class IdList {
|
|
|
228
677
|
}
|
|
229
678
|
|
|
230
679
|
let remaining = index;
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
680
|
+
let curParent = this.root;
|
|
681
|
+
// eslint-disable-next-line no-constant-condition
|
|
682
|
+
recurse: while (true) {
|
|
683
|
+
if (curParent instanceof InnerNodeInner) {
|
|
684
|
+
for (const child of curParent.children) {
|
|
685
|
+
if (remaining < child.size) {
|
|
686
|
+
// Recurse.
|
|
687
|
+
curParent = child;
|
|
688
|
+
continue recurse;
|
|
689
|
+
} else {
|
|
690
|
+
remaining -= child.size;
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
} else {
|
|
694
|
+
for (const child of curParent.children) {
|
|
695
|
+
const childSize = child.present.count();
|
|
696
|
+
if (remaining < childSize) {
|
|
697
|
+
// Found it.
|
|
698
|
+
return {
|
|
699
|
+
bunchId: child.bunchId,
|
|
700
|
+
counter: child.present.indexOfCount(remaining),
|
|
701
|
+
};
|
|
702
|
+
} else {
|
|
703
|
+
remaining -= childSize;
|
|
704
|
+
}
|
|
705
|
+
}
|
|
235
706
|
}
|
|
236
|
-
}
|
|
237
707
|
|
|
238
|
-
|
|
708
|
+
throw new Error("Internal error");
|
|
709
|
+
}
|
|
239
710
|
}
|
|
240
711
|
|
|
241
712
|
/**
|
|
@@ -250,36 +721,47 @@ export class IdList {
|
|
|
250
721
|
* @throws If `id` is not known.
|
|
251
722
|
*/
|
|
252
723
|
indexOf(id: ElementId, bias: "none" | "left" | "right" = "none"): number {
|
|
724
|
+
const located = this.locate(id);
|
|
725
|
+
if (located === null) throw new Error("id is not known");
|
|
726
|
+
|
|
253
727
|
/**
|
|
254
728
|
* The number of present ids less than id.
|
|
255
729
|
* Equivalently, the index id would have if present.
|
|
256
730
|
*/
|
|
257
731
|
let index = 0;
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
return index - 1;
|
|
267
|
-
case "right":
|
|
268
|
-
return index;
|
|
269
|
-
}
|
|
270
|
-
} else return index;
|
|
732
|
+
|
|
733
|
+
// Lesser siblings of parent, grandparent, etc.
|
|
734
|
+
for (let i = 1; i < located.length; i++) {
|
|
735
|
+
const parent = (
|
|
736
|
+
i === located.length - 1 ? this.root : located[i + 1].node
|
|
737
|
+
) as InnerNodeInner;
|
|
738
|
+
for (let c = 0; c < located[i].indexInParent; c++) {
|
|
739
|
+
index += parent.children[c].size;
|
|
271
740
|
}
|
|
272
|
-
if (!elt.isDeleted) index++;
|
|
273
741
|
}
|
|
274
742
|
|
|
275
|
-
|
|
276
|
-
|
|
743
|
+
// Siblings of id's leaf.
|
|
744
|
+
const leafParent = (
|
|
745
|
+
located.length === 1 ? this.root : located[1].node
|
|
746
|
+
) as InnerNodeLeaf;
|
|
747
|
+
for (let c = 0; c < located[0].indexInParent; c++) {
|
|
748
|
+
index += leafParent.children[c].present.count();
|
|
749
|
+
}
|
|
277
750
|
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
751
|
+
// id's index within leaf.
|
|
752
|
+
const [count, has] = located[0].node.present._countHas(id.counter);
|
|
753
|
+
index += count;
|
|
754
|
+
if (has) return index;
|
|
755
|
+
else {
|
|
756
|
+
switch (bias) {
|
|
757
|
+
case "none":
|
|
758
|
+
return -1;
|
|
759
|
+
case "left":
|
|
760
|
+
return index - 1;
|
|
761
|
+
case "right":
|
|
762
|
+
return index;
|
|
763
|
+
}
|
|
764
|
+
}
|
|
283
765
|
}
|
|
284
766
|
|
|
285
767
|
// Iterators and views
|
|
@@ -287,10 +769,8 @@ export class IdList {
|
|
|
287
769
|
/**
|
|
288
770
|
* Iterates over all present ids in the list.
|
|
289
771
|
*/
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
if (!elt.isDeleted) yield elt.id;
|
|
293
|
-
}
|
|
772
|
+
[Symbol.iterator](): IterableIterator<ElementId> {
|
|
773
|
+
return iterateNode(this.root, false);
|
|
294
774
|
}
|
|
295
775
|
|
|
296
776
|
/**
|
|
@@ -303,26 +783,22 @@ export class IdList {
|
|
|
303
783
|
/**
|
|
304
784
|
* Iterates over all __known__ ids in the list, indicating which are deleted.
|
|
305
785
|
*/
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
* Returns an independent copy of this list, including known but deleted ids.
|
|
312
|
-
*/
|
|
313
|
-
clone(): IdList {
|
|
314
|
-
return IdList.from(this.state);
|
|
786
|
+
valuesWithIsDeleted(): IterableIterator<{
|
|
787
|
+
id: ElementId;
|
|
788
|
+
isDeleted: boolean;
|
|
789
|
+
}> {
|
|
790
|
+
return iterateNodeWithIsDeleted(this.root);
|
|
315
791
|
}
|
|
316
792
|
|
|
317
793
|
private _knownIds?: KnownIdView;
|
|
318
794
|
|
|
319
795
|
/**
|
|
320
|
-
* A
|
|
321
|
-
* That is, it ignores
|
|
796
|
+
* A view of this list that treats all known ids as present.
|
|
797
|
+
* That is, it ignores is-deleted status when computing list indices or iterating.
|
|
322
798
|
*/
|
|
323
799
|
get knownIds(): KnownIdView {
|
|
324
800
|
if (this._knownIds === undefined) {
|
|
325
|
-
this._knownIds = new KnownIdView(this, this.
|
|
801
|
+
this._knownIds = new KnownIdView(this, this.root);
|
|
326
802
|
}
|
|
327
803
|
return this._knownIds;
|
|
328
804
|
}
|
|
@@ -336,66 +812,98 @@ export class IdList {
|
|
|
336
812
|
* See {@link SavedIdList} for a description of the save format.
|
|
337
813
|
*/
|
|
338
814
|
save(): SavedIdList {
|
|
339
|
-
const
|
|
815
|
+
const acc: SavedIdList = [];
|
|
816
|
+
saveNode(this.root, acc);
|
|
817
|
+
return acc;
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
/**
|
|
821
|
+
* Loads a saved state returned by {@link save}.
|
|
822
|
+
*/
|
|
823
|
+
static load(savedState: SavedIdList) {
|
|
824
|
+
// 1. Determine the leaves in list order.
|
|
340
825
|
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
826
|
+
const leaves: LeafNode[] = [];
|
|
827
|
+
for (let i = 0; i < savedState.length; i++) {
|
|
828
|
+
const item = savedState[i];
|
|
829
|
+
|
|
830
|
+
if (!(Number.isSafeInteger(item.count) && item.count >= 0)) {
|
|
831
|
+
throw new Error(`Invalid count: ${item.count}`);
|
|
832
|
+
}
|
|
833
|
+
if (
|
|
834
|
+
!(Number.isSafeInteger(item.startCounter) && item.startCounter >= 0)
|
|
835
|
+
) {
|
|
836
|
+
throw new Error(`Invalid startCounter: ${item.startCounter}`);
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
if (item.count === 0) continue;
|
|
840
|
+
|
|
841
|
+
if (leaves.length !== 0) {
|
|
842
|
+
const lastLeaf = leaves.at(-1)!;
|
|
344
843
|
if (
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
isDeleted === current.isDeleted
|
|
844
|
+
item.bunchId === lastLeaf.bunchId &&
|
|
845
|
+
item.startCounter === lastLeaf.startCounter + lastLeaf.count
|
|
348
846
|
) {
|
|
349
|
-
|
|
847
|
+
// Extend lastLeaf.
|
|
848
|
+
// Okay to mutate in-place since we haven't referenced it anywhere else yet.
|
|
849
|
+
// @ts-expect-error Mutate in place
|
|
850
|
+
lastLeaf.count += item.count;
|
|
851
|
+
if (!item.isDeleted) {
|
|
852
|
+
lastLeaf.present.set(item.startCounter, item.count);
|
|
853
|
+
}
|
|
350
854
|
continue;
|
|
351
855
|
}
|
|
352
856
|
}
|
|
353
857
|
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
858
|
+
// If we get to here, we need a new leaf.
|
|
859
|
+
const present = SparseIndices.new();
|
|
860
|
+
if (!item.isDeleted) present.set(item.startCounter, item.count);
|
|
861
|
+
leaves.push({
|
|
862
|
+
bunchId: item.bunchId,
|
|
863
|
+
startCounter: item.startCounter,
|
|
864
|
+
count: item.count,
|
|
865
|
+
present,
|
|
359
866
|
});
|
|
360
867
|
}
|
|
361
868
|
|
|
362
|
-
|
|
363
|
-
|
|
869
|
+
// 2. Create a B+Tree with the given leaves.
|
|
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)).
|
|
364
873
|
|
|
365
|
-
|
|
366
|
-
* Loads a saved state returned by {@link save}, __overwriting__ the current state of this list.
|
|
367
|
-
*/
|
|
368
|
-
load(savedState: SavedIdList) {
|
|
369
|
-
this.state.length = 0;
|
|
370
|
-
this._length = 0;
|
|
874
|
+
if (leaves.length === 0) return IdList.new();
|
|
371
875
|
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
}
|
|
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.
|
|
376
879
|
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
880
|
+
const leafMapMut = { value: LeafMap.new() };
|
|
881
|
+
const parentSeqsMut = { value: SeqMap.new() };
|
|
882
|
+
|
|
883
|
+
// Depth of the B+Tree (number of non-root nodes on any path from a leaf to the root).
|
|
884
|
+
// A full B+Tree of depth d has between [M^{d-1} + 1, M^d] leaves.
|
|
885
|
+
const depth =
|
|
886
|
+
leaves.length === 1
|
|
887
|
+
? 1
|
|
888
|
+
: Math.ceil(Math.log(leaves.length) / Math.log(M));
|
|
889
|
+
const root = buildTree(leaves, leafMapMut, parentSeqsMut, 0, depth);
|
|
890
|
+
return new IdList(root, leafMapMut.value, parentSeqsMut.value);
|
|
385
891
|
}
|
|
386
892
|
}
|
|
387
893
|
|
|
388
894
|
/**
|
|
389
|
-
* A
|
|
390
|
-
* That is, this class ignores the underlying list's
|
|
895
|
+
* A view of an IdList that treats all known ids as present.
|
|
896
|
+
* That is, this class ignores the underlying list's is-deleted status when computing list indices.
|
|
897
|
+
* Access using {@link IdList.knownIds}.
|
|
391
898
|
*
|
|
392
|
-
* To mutate,
|
|
899
|
+
* Like IdList, KnownIdView is immutable. To mutate, use a mutating method on the original IdList
|
|
900
|
+
* and access the returned list's `knownIds`.
|
|
393
901
|
*/
|
|
394
902
|
export class KnownIdView {
|
|
395
903
|
/**
|
|
396
904
|
* Internal use only. Use {@link IdList.knownIds} instead.
|
|
397
905
|
*/
|
|
398
|
-
constructor(readonly list: IdList, private readonly
|
|
906
|
+
constructor(readonly list: IdList, private readonly root: InnerNode) {}
|
|
399
907
|
|
|
400
908
|
// Mutators are omitted - mutate this.list instead.
|
|
401
909
|
|
|
@@ -413,14 +921,73 @@ export class KnownIdView {
|
|
|
413
921
|
throw new Error(`Index out of bounds: ${index} (length: ${this.length}`);
|
|
414
922
|
}
|
|
415
923
|
|
|
416
|
-
|
|
924
|
+
let remaining = index;
|
|
925
|
+
let curParent = this.root;
|
|
926
|
+
// eslint-disable-next-line no-constant-condition
|
|
927
|
+
recurse: while (true) {
|
|
928
|
+
if (curParent instanceof InnerNodeInner) {
|
|
929
|
+
for (const child of curParent.children) {
|
|
930
|
+
if (remaining < child.knownSize) {
|
|
931
|
+
// Recurse.
|
|
932
|
+
curParent = child;
|
|
933
|
+
continue recurse;
|
|
934
|
+
} else {
|
|
935
|
+
remaining -= child.knownSize;
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
} else {
|
|
939
|
+
for (const child of curParent.children) {
|
|
940
|
+
if (remaining < child.count) {
|
|
941
|
+
// Found it.
|
|
942
|
+
return {
|
|
943
|
+
bunchId: child.bunchId,
|
|
944
|
+
counter: child.startCounter + remaining,
|
|
945
|
+
};
|
|
946
|
+
} else {
|
|
947
|
+
remaining -= child.count;
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
throw new Error("Internal error");
|
|
953
|
+
}
|
|
417
954
|
}
|
|
418
955
|
|
|
419
956
|
/**
|
|
420
957
|
* Returns the index of `id` in this view, or -1 if it is not known.
|
|
421
958
|
*/
|
|
422
959
|
indexOf(id: ElementId): number {
|
|
423
|
-
|
|
960
|
+
// @ts-expect-error Ignore private
|
|
961
|
+
const located = this.list.locate(id);
|
|
962
|
+
if (located === null) throw new Error("id is not known");
|
|
963
|
+
|
|
964
|
+
/**
|
|
965
|
+
* The number of present ids less than id.
|
|
966
|
+
* Equivalently, the index id would have if present.
|
|
967
|
+
*/
|
|
968
|
+
let index = 0;
|
|
969
|
+
|
|
970
|
+
// Lesser siblings of parent, grandparent, etc.
|
|
971
|
+
for (let i = 1; i < located.length; i++) {
|
|
972
|
+
const parent = (
|
|
973
|
+
i === located.length - 1 ? this.root : located[i + 1].node
|
|
974
|
+
) as InnerNodeInner;
|
|
975
|
+
for (let c = 0; c < located[i].indexInParent; c++) {
|
|
976
|
+
index += parent.children[c].knownSize;
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
// Siblings of id's leaf.
|
|
981
|
+
const leafParent = (
|
|
982
|
+
located.length === 1 ? this.root : located[1].node
|
|
983
|
+
) as InnerNodeLeaf;
|
|
984
|
+
for (let c = 0; c < located[0].indexInParent; c++) {
|
|
985
|
+
const child = leafParent.children[c];
|
|
986
|
+
index += child.count;
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
// id's index with leaf.
|
|
990
|
+
return index + (id.counter - located[0].node.startCounter);
|
|
424
991
|
}
|
|
425
992
|
|
|
426
993
|
/**
|
|
@@ -429,7 +996,7 @@ export class KnownIdView {
|
|
|
429
996
|
* Equivalently, the number of known ids in `this.list`.
|
|
430
997
|
*/
|
|
431
998
|
get length(): number {
|
|
432
|
-
return this.
|
|
999
|
+
return this.root.knownSize;
|
|
433
1000
|
}
|
|
434
1001
|
|
|
435
1002
|
// Iterators
|
|
@@ -437,10 +1004,8 @@ export class KnownIdView {
|
|
|
437
1004
|
/**
|
|
438
1005
|
* Iterates over all ids in this view, i.e., all known ids in `this.list`.
|
|
439
1006
|
*/
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
yield elt.id;
|
|
443
|
-
}
|
|
1007
|
+
[Symbol.iterator](): IterableIterator<ElementId> {
|
|
1008
|
+
return iterateNode(this.root, true);
|
|
444
1009
|
}
|
|
445
1010
|
|
|
446
1011
|
/**
|
|
@@ -451,21 +1016,331 @@ export class KnownIdView {
|
|
|
451
1016
|
}
|
|
452
1017
|
}
|
|
453
1018
|
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
1019
|
+
/**
|
|
1020
|
+
* Returns the first (leftmost) known ElementId in node's subtree.
|
|
1021
|
+
*/
|
|
1022
|
+
function firstId(node: InnerNode): ElementId {
|
|
1023
|
+
let currentInner = node;
|
|
1024
|
+
while (!(currentInner instanceof InnerNodeLeaf)) {
|
|
1025
|
+
currentInner = currentInner.children[0];
|
|
461
1026
|
}
|
|
1027
|
+
const firstLeaf = currentInner.children[0];
|
|
1028
|
+
return {
|
|
1029
|
+
bunchId: firstLeaf.bunchId,
|
|
1030
|
+
counter: firstLeaf.startCounter,
|
|
1031
|
+
};
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
/**
|
|
1035
|
+
* Returns the last (rightmost) known ElementId in node's subtree.
|
|
1036
|
+
*/
|
|
1037
|
+
function lastId(node: InnerNode): ElementId {
|
|
1038
|
+
let currentInner = node;
|
|
1039
|
+
while (!(currentInner instanceof InnerNodeLeaf)) {
|
|
1040
|
+
currentInner = currentInner.children.at(-1)!;
|
|
1041
|
+
}
|
|
1042
|
+
const lastLeaf = currentInner.children.at(-1)!;
|
|
1043
|
+
return {
|
|
1044
|
+
bunchId: lastLeaf.bunchId,
|
|
1045
|
+
counter: lastLeaf.startCounter + lastLeaf.count - 1,
|
|
1046
|
+
};
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
/**
|
|
1050
|
+
* Replace located[i].node with newNodes.
|
|
1051
|
+
*
|
|
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).
|
|
1056
|
+
*/
|
|
1057
|
+
function replaceNode(
|
|
1058
|
+
located: Located,
|
|
1059
|
+
root: InnerNode,
|
|
1060
|
+
leafMapMut: MutableLeafMap,
|
|
1061
|
+
parentSeqsMut: MutableSeqMap,
|
|
1062
|
+
newNodes: InnerNode[] | LeafNode[],
|
|
1063
|
+
i: number
|
|
1064
|
+
): InnerNode {
|
|
1065
|
+
const parent =
|
|
1066
|
+
i === located.length - 1 ? root : (located[i + 1].node as InnerNode);
|
|
1067
|
+
const indexInParent = located[i].indexInParent;
|
|
1068
|
+
// Copy-on-write version of parent.children.splice(indexInParent, 1, ...newNodes)
|
|
1069
|
+
const newChildren = parent.children
|
|
1070
|
+
.slice(0, indexInParent)
|
|
1071
|
+
.concat(newNodes, parent.children.slice(indexInParent + 1));
|
|
1072
|
+
|
|
1073
|
+
if (newChildren.length > M) {
|
|
1074
|
+
// Split the parent to maintain BTree property (# children <= M).
|
|
1075
|
+
// Treat the right parent as "new", getting a new seq.
|
|
1076
|
+
const split = Math.ceil(newChildren.length / 2);
|
|
1077
|
+
const seqs = [parent.seq, getAndBumpNextSeq(parentSeqsMut)];
|
|
1078
|
+
const newParents = [
|
|
1079
|
+
newChildren.slice(0, split),
|
|
1080
|
+
newChildren.slice(split),
|
|
1081
|
+
].map((children, j) =>
|
|
1082
|
+
i === 0
|
|
1083
|
+
? new InnerNodeLeaf(seqs[j], children as LeafNode[], leafMapMut)
|
|
1084
|
+
: new InnerNodeInner(seqs[j], children as InnerNode[], parentSeqsMut)
|
|
1085
|
+
);
|
|
1086
|
+
if (i === located.length - 1) {
|
|
1087
|
+
// newParents replace root. We need a new root to hold them.
|
|
1088
|
+
return new InnerNodeInner(
|
|
1089
|
+
getAndBumpNextSeq(parentSeqsMut),
|
|
1090
|
+
newParents,
|
|
1091
|
+
parentSeqsMut
|
|
1092
|
+
);
|
|
1093
|
+
} else {
|
|
1094
|
+
return replaceNode(
|
|
1095
|
+
located,
|
|
1096
|
+
root,
|
|
1097
|
+
leafMapMut,
|
|
1098
|
+
parentSeqsMut,
|
|
1099
|
+
newParents,
|
|
1100
|
+
i + 1
|
|
1101
|
+
);
|
|
1102
|
+
}
|
|
1103
|
+
} else {
|
|
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
|
+
|
|
1139
|
+
if (i === located.length - 1) {
|
|
1140
|
+
// Replaces root.
|
|
1141
|
+
return newParent;
|
|
1142
|
+
} else {
|
|
1143
|
+
return replaceNode(
|
|
1144
|
+
located,
|
|
1145
|
+
root,
|
|
1146
|
+
leafMapMut,
|
|
1147
|
+
parentSeqsMut,
|
|
1148
|
+
[newParent],
|
|
1149
|
+
i + 1
|
|
1150
|
+
);
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
/**
|
|
1156
|
+
* Splits present into two SparseIndices at the given counter.
|
|
1157
|
+
*/
|
|
1158
|
+
function splitPresent(
|
|
1159
|
+
present: SparseIndices,
|
|
1160
|
+
splitCounter: number
|
|
1161
|
+
): [leftPresent: SparseIndices, rightPresent: SparseIndices] {
|
|
1162
|
+
const leftPresent = SparseIndices.new();
|
|
1163
|
+
const rightPresent = SparseIndices.new();
|
|
1164
|
+
const leafSlicer = present.newSlicer();
|
|
1165
|
+
for (const [index, count] of leafSlicer.nextSlice(splitCounter)) {
|
|
1166
|
+
leftPresent.set(index, count);
|
|
1167
|
+
}
|
|
1168
|
+
for (const [index, count] of leafSlicer.nextSlice(null)) {
|
|
1169
|
+
rightPresent.set(index, count);
|
|
1170
|
+
}
|
|
1171
|
+
return [leftPresent, rightPresent];
|
|
1172
|
+
}
|
|
462
1173
|
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
1174
|
+
function* iterateNode(
|
|
1175
|
+
node: InnerNode,
|
|
1176
|
+
includeDeleted: boolean
|
|
1177
|
+
): IterableIterator<ElementId> {
|
|
1178
|
+
if (node instanceof InnerNodeInner) {
|
|
1179
|
+
for (const child of node.children) {
|
|
1180
|
+
yield* iterateNode(child, includeDeleted);
|
|
1181
|
+
}
|
|
1182
|
+
} else {
|
|
1183
|
+
for (const child of node.children) {
|
|
1184
|
+
if (includeDeleted) {
|
|
1185
|
+
for (let i = 0; i < child.count; i++) {
|
|
1186
|
+
yield { bunchId: child.bunchId, counter: child.startCounter + i };
|
|
1187
|
+
}
|
|
1188
|
+
} else {
|
|
1189
|
+
for (const counter of child.present.keys()) {
|
|
1190
|
+
yield { bunchId: child.bunchId, counter };
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
function* iterateNodeWithIsDeleted(
|
|
1198
|
+
node: InnerNode
|
|
1199
|
+
): IterableIterator<{ id: ElementId; isDeleted: boolean }> {
|
|
1200
|
+
if (node instanceof InnerNodeInner) {
|
|
1201
|
+
for (const child of node.children) {
|
|
1202
|
+
yield* iterateNodeWithIsDeleted(child);
|
|
1203
|
+
}
|
|
1204
|
+
} else {
|
|
1205
|
+
for (const child of node.children) {
|
|
1206
|
+
let nextIndex = child.startCounter;
|
|
1207
|
+
for (const index of child.present.keys()) {
|
|
1208
|
+
while (nextIndex < index) {
|
|
1209
|
+
yield {
|
|
1210
|
+
id: { bunchId: child.bunchId, counter: nextIndex },
|
|
1211
|
+
isDeleted: true,
|
|
1212
|
+
};
|
|
1213
|
+
nextIndex++;
|
|
1214
|
+
}
|
|
1215
|
+
yield {
|
|
1216
|
+
id: { bunchId: child.bunchId, counter: index },
|
|
1217
|
+
isDeleted: false,
|
|
1218
|
+
};
|
|
1219
|
+
nextIndex++;
|
|
1220
|
+
}
|
|
1221
|
+
while (nextIndex < child.startCounter + child.count) {
|
|
1222
|
+
yield {
|
|
1223
|
+
id: { bunchId: child.bunchId, counter: nextIndex },
|
|
1224
|
+
isDeleted: true,
|
|
1225
|
+
};
|
|
1226
|
+
nextIndex++;
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
/**
|
|
1233
|
+
* Updates acc to account for node's subtree, as part of a depth-first search
|
|
1234
|
+
* in list order.
|
|
1235
|
+
*/
|
|
1236
|
+
function saveNode(node: InnerNode, acc: SavedIdList) {
|
|
1237
|
+
if (node instanceof InnerNodeInner) {
|
|
1238
|
+
for (const child of node.children) {
|
|
1239
|
+
saveNode(child, acc);
|
|
1240
|
+
}
|
|
1241
|
+
} else {
|
|
1242
|
+
for (const child of node.children) {
|
|
1243
|
+
let nextIndex = child.startCounter;
|
|
1244
|
+
for (const [index, count] of child.present.items()) {
|
|
1245
|
+
if (nextIndex < index) {
|
|
1246
|
+
// Need a deleted item.
|
|
1247
|
+
pushSaveItem(acc, {
|
|
1248
|
+
bunchId: child.bunchId,
|
|
1249
|
+
startCounter: nextIndex,
|
|
1250
|
+
count: index - nextIndex,
|
|
1251
|
+
isDeleted: true,
|
|
1252
|
+
});
|
|
1253
|
+
}
|
|
1254
|
+
pushSaveItem(acc, {
|
|
1255
|
+
bunchId: child.bunchId,
|
|
1256
|
+
startCounter: index,
|
|
1257
|
+
count,
|
|
1258
|
+
isDeleted: false,
|
|
1259
|
+
});
|
|
1260
|
+
nextIndex = index + count;
|
|
1261
|
+
}
|
|
1262
|
+
if (nextIndex < child.startCounter + child.count) {
|
|
1263
|
+
pushSaveItem(acc, {
|
|
1264
|
+
bunchId: child.bunchId,
|
|
1265
|
+
startCounter: nextIndex,
|
|
1266
|
+
count: child.startCounter + child.count - nextIndex,
|
|
1267
|
+
isDeleted: true,
|
|
1268
|
+
});
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
/**
|
|
1275
|
+
* Pushes a save item onto acc, combing it with the previous item if possible.
|
|
1276
|
+
*
|
|
1277
|
+
* This function is necessary because we don't guarantee that adjacent leaves are fully merged.
|
|
1278
|
+
* Specifically, if you insert a bunch's ids with counter values (0, 2, 1)
|
|
1279
|
+
* in that order, then counter 1 will extend one of the existing leaves
|
|
1280
|
+
* but not merge with the other leaf.
|
|
1281
|
+
*
|
|
1282
|
+
* This situation won't appear in typical usage, and its perf penalty
|
|
1283
|
+
* will go away once you reload. Thus we tolerate it instead of figuring out
|
|
1284
|
+
* how to delete leaves from a B+Tree.
|
|
1285
|
+
*/
|
|
1286
|
+
function pushSaveItem(acc: SavedIdList, item: SavedIdList[number]) {
|
|
1287
|
+
if (acc.length > 0) {
|
|
1288
|
+
const previous = acc.at(-1)!;
|
|
1289
|
+
if (
|
|
1290
|
+
previous.isDeleted === item.isDeleted &&
|
|
1291
|
+
previous.bunchId === item.bunchId &&
|
|
1292
|
+
previous.startCounter + previous.count === item.startCounter
|
|
1293
|
+
) {
|
|
1294
|
+
// Combine items.
|
|
1295
|
+
// @ts-expect-error Mutating for convenience; no aliasing to worry about.
|
|
1296
|
+
previous.count += item.count;
|
|
1297
|
+
return;
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
acc.push(item);
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
/**
|
|
1304
|
+
* Builds a tree with the given leaves. Used by IdList.load.
|
|
1305
|
+
*
|
|
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.
|
|
1313
|
+
*/
|
|
1314
|
+
function buildTree(
|
|
1315
|
+
leaves: LeafNode[],
|
|
1316
|
+
leafMapMut: MutableLeafMap,
|
|
1317
|
+
parentSeqsMut: MutableSeqMap,
|
|
1318
|
+
startIndex: number,
|
|
1319
|
+
depthRemaining: number
|
|
1320
|
+
): InnerNode {
|
|
1321
|
+
const parentSeq = getAndBumpNextSeq(parentSeqsMut);
|
|
1322
|
+
if (depthRemaining === 1) {
|
|
1323
|
+
return new InnerNodeLeaf(
|
|
1324
|
+
parentSeq,
|
|
1325
|
+
leaves.slice(startIndex, startIndex + M),
|
|
1326
|
+
leafMapMut
|
|
1327
|
+
);
|
|
1328
|
+
} else {
|
|
1329
|
+
const children: InnerNode[] = [];
|
|
1330
|
+
const childLeafCount = Math.pow(M, depthRemaining - 1);
|
|
1331
|
+
for (let i = 0; i < M; i++) {
|
|
1332
|
+
const childStartIndex = startIndex + i * childLeafCount;
|
|
1333
|
+
if (childStartIndex >= leaves.length) break;
|
|
1334
|
+
children.push(
|
|
1335
|
+
buildTree(
|
|
1336
|
+
leaves,
|
|
1337
|
+
leafMapMut,
|
|
1338
|
+
parentSeqsMut,
|
|
1339
|
+
childStartIndex,
|
|
1340
|
+
depthRemaining - 1
|
|
1341
|
+
)
|
|
1342
|
+
);
|
|
1343
|
+
}
|
|
1344
|
+
return new InnerNodeInner(parentSeq, children, parentSeqsMut);
|
|
469
1345
|
}
|
|
470
|
-
return ans;
|
|
471
1346
|
}
|