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/build/esm/id_list.js
CHANGED
|
@@ -1,6 +1,79 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { SparseIndices } from "sparse-array-rled";
|
|
2
|
+
import { LeafMap } from "./internal/leaf_map";
|
|
3
|
+
import { getAndBumpNextSeq, SeqMap } from "./internal/seq_map";
|
|
2
4
|
/**
|
|
3
|
-
*
|
|
5
|
+
* An inner node with inner-node children.
|
|
6
|
+
*/
|
|
7
|
+
export class InnerNodeInner {
|
|
8
|
+
constructor(
|
|
9
|
+
/**
|
|
10
|
+
* A unique identifer for this node within its IdTree.
|
|
11
|
+
*/
|
|
12
|
+
seq, children,
|
|
13
|
+
/**
|
|
14
|
+
* We add entries for the children to this map, overwriting any existing parentSeqs.
|
|
15
|
+
*
|
|
16
|
+
* Pass null to skip when you are doing it yourself. Regardless, you need to
|
|
17
|
+
* delete any outdated entries yourself.
|
|
18
|
+
*/
|
|
19
|
+
parentSeqsMut) {
|
|
20
|
+
this.seq = seq;
|
|
21
|
+
this.children = children;
|
|
22
|
+
let size = 0;
|
|
23
|
+
let knownSize = 0;
|
|
24
|
+
for (const child of children) {
|
|
25
|
+
size += child.size;
|
|
26
|
+
knownSize += child.knownSize;
|
|
27
|
+
if (parentSeqsMut) {
|
|
28
|
+
parentSeqsMut.value = parentSeqsMut.value.set(child.seq, seq);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
this.size = size;
|
|
32
|
+
this.knownSize = knownSize;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* An inner node with leaf children.
|
|
37
|
+
*/
|
|
38
|
+
export class InnerNodeLeaf {
|
|
39
|
+
constructor(
|
|
40
|
+
/**
|
|
41
|
+
* A unique identifer for this node within its IdTree.
|
|
42
|
+
*/
|
|
43
|
+
seq, children,
|
|
44
|
+
/**
|
|
45
|
+
* We add entries for the children to this map, overwriting any existing parentSeqs.
|
|
46
|
+
*
|
|
47
|
+
* Pass null to skip when you are doing it yourself.
|
|
48
|
+
*/
|
|
49
|
+
leafMapMut) {
|
|
50
|
+
this.seq = seq;
|
|
51
|
+
this.children = children;
|
|
52
|
+
let size = 0;
|
|
53
|
+
let knownSize = 0;
|
|
54
|
+
for (const child of children) {
|
|
55
|
+
size += child.present.count();
|
|
56
|
+
knownSize += child.count;
|
|
57
|
+
if (leafMapMut) {
|
|
58
|
+
leafMapMut.value = leafMapMut.value.set(child, seq);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
this.size = size;
|
|
62
|
+
this.knownSize = knownSize;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* The B+Tree's branching factor, i.e., the max number of children of a node.
|
|
67
|
+
*
|
|
68
|
+
* Note that our B+Tree has no keys - in particular, no keys in internal nodes.
|
|
69
|
+
*
|
|
70
|
+
* Wiki B+Tree: "B+ trees can also be used for data stored in RAM.
|
|
71
|
+
* In this case a reasonable choice for block size would be the size of [the] processor's cache line."
|
|
72
|
+
* (64 byte cache line) / (8 byte pointer) = 8.
|
|
73
|
+
*/
|
|
74
|
+
export const M = 8;
|
|
75
|
+
/**
|
|
76
|
+
* A list of ElementIds, as a persistent (immutable) data structure.
|
|
4
77
|
*
|
|
5
78
|
* An IdList helps you assign a unique immutable id to each element of a list, such
|
|
6
79
|
* as a todo-list or a text document (= list of characters). That way, you can keep track
|
|
@@ -9,11 +82,14 @@ import { equalsId } from "./id";
|
|
|
9
82
|
*
|
|
10
83
|
* Any id that has been inserted into an IdList remains **known** to that list indefinitely,
|
|
11
84
|
* allowing you to reference it in insertAfter/insertBefore operations. Calling {@link delete}
|
|
12
|
-
* merely marks an id as deleted (not present); it
|
|
85
|
+
* 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".
|
|
13
86
|
* This is useful in collaborative settings, since another user might instruct you to
|
|
14
87
|
* call `insertAfter(before, newId)` when you have already deleted `before` locally.
|
|
15
|
-
*
|
|
16
|
-
*
|
|
88
|
+
*
|
|
89
|
+
* To enable easy and efficient rollbacks, such as in a
|
|
90
|
+
* [server reconciliation](https://mattweidner.com/2024/06/04/server-architectures.html#1-server-reconciliation)
|
|
91
|
+
* architecture, IdList is a persistent (immutable) data structure. Mutating methods
|
|
92
|
+
* return a new IdList, sharing memory with the old IdList where possible.
|
|
17
93
|
*
|
|
18
94
|
* See {@link ElementId} for advice on generating ElementIds. IdList is optimized for
|
|
19
95
|
* the case where sequential ElementIds often have the same bunchId and sequential counters.
|
|
@@ -21,27 +97,56 @@ import { equalsId } from "./id";
|
|
|
21
97
|
* cause such ids to be separated, partially deleted, or even reordered.
|
|
22
98
|
*/
|
|
23
99
|
export class IdList {
|
|
100
|
+
/**
|
|
101
|
+
* Internal - construct an IdList using a static method (e.g. `IdList.new`).
|
|
102
|
+
*/
|
|
103
|
+
constructor(root,
|
|
104
|
+
/**
|
|
105
|
+
* A persistent sorted map from each leaf to its parent node's seq.
|
|
106
|
+
*
|
|
107
|
+
* Besides parentSeqs, we also use this to lookup leaves by ElementId.
|
|
108
|
+
*/
|
|
109
|
+
leafMap, parentSeqs) {
|
|
110
|
+
this.root = root;
|
|
111
|
+
this.leafMap = leafMap;
|
|
112
|
+
this.parentSeqs = parentSeqs.set(root.seq, 0);
|
|
113
|
+
}
|
|
24
114
|
/**
|
|
25
115
|
* Constructs an empty list.
|
|
26
116
|
*
|
|
27
|
-
* To begin with a non-empty list, use {@link IdList.from}
|
|
117
|
+
* To begin with a non-empty list, use {@link IdList.from}, {@link IdList.fromIds},
|
|
118
|
+
* or {@link IdList.load}.
|
|
28
119
|
*/
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
120
|
+
static new() {
|
|
121
|
+
const leafMapMut = { value: LeafMap.new() };
|
|
122
|
+
const parentSeqsMut = { value: SeqMap.new() };
|
|
123
|
+
return new this(new InnerNodeLeaf(getAndBumpNextSeq(parentSeqsMut), [], leafMapMut), leafMapMut.value, parentSeqsMut.value);
|
|
32
124
|
}
|
|
33
125
|
/**
|
|
34
126
|
* Constructs a list with the given known ids and their isDeleted status, in list order.
|
|
35
127
|
*/
|
|
36
|
-
static from(
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
128
|
+
static from(knownIds) {
|
|
129
|
+
// Convert knownIds to a saved state and load that.
|
|
130
|
+
const savedState = [];
|
|
131
|
+
for (const { id, isDeleted } of knownIds) {
|
|
132
|
+
if (savedState.length !== 0) {
|
|
133
|
+
const current = savedState.at(-1);
|
|
134
|
+
if (id.bunchId === current.bunchId &&
|
|
135
|
+
id.counter === current.startCounter + current.count &&
|
|
136
|
+
isDeleted === current.isDeleted) {
|
|
137
|
+
// @ts-expect-error Mutating for convenience; no aliasing to worry about.
|
|
138
|
+
current.count++;
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
savedState.push({
|
|
143
|
+
bunchId: id.bunchId,
|
|
144
|
+
startCounter: id.counter,
|
|
145
|
+
count: 1,
|
|
146
|
+
isDeleted,
|
|
147
|
+
});
|
|
43
148
|
}
|
|
44
|
-
return
|
|
149
|
+
return IdList.load(savedState);
|
|
45
150
|
}
|
|
46
151
|
/**
|
|
47
152
|
* Constructs a list with the given present ids.
|
|
@@ -51,15 +156,15 @@ export class IdList {
|
|
|
51
156
|
* in future insertAfter/insertBefore operations.
|
|
52
157
|
*/
|
|
53
158
|
static fromIds(ids) {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
}
|
|
59
|
-
return list;
|
|
159
|
+
return this.from((function* () {
|
|
160
|
+
for (const id of ids)
|
|
161
|
+
yield { id, isDeleted: false };
|
|
162
|
+
})());
|
|
60
163
|
}
|
|
61
164
|
/**
|
|
62
165
|
* Inserts `newId` immediately after the given id (`before`), which may be deleted.
|
|
166
|
+
* A new IdList is returned and the current list remains unchanged.
|
|
167
|
+
*
|
|
63
168
|
* All ids to the right of `before` are shifted one index to the right, in the manner
|
|
64
169
|
* of [Array.splice](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/splice).
|
|
65
170
|
*
|
|
@@ -69,28 +174,96 @@ export class IdList {
|
|
|
69
174
|
* @param count Provide this to bulk-insert `count` ids from left-to-right,
|
|
70
175
|
* starting with newId and proceeding with the same bunchId and sequential counters.
|
|
71
176
|
* @throws If `before` is not known.
|
|
72
|
-
* @throws If
|
|
177
|
+
* @throws If any inserted id is already known.
|
|
73
178
|
*/
|
|
74
179
|
insertAfter(before, newId, count = 1) {
|
|
75
|
-
if (
|
|
76
|
-
throw new Error(
|
|
180
|
+
if (!(Number.isSafeInteger(newId.counter) && newId.counter >= 0)) {
|
|
181
|
+
throw new Error(`Invalid counter: ${newId.counter}`);
|
|
182
|
+
}
|
|
183
|
+
if (!(Number.isSafeInteger(count) && count >= 0)) {
|
|
184
|
+
throw new Error(`Invalid count: ${count}`);
|
|
185
|
+
}
|
|
186
|
+
if (this.isAnyKnown(newId, count)) {
|
|
187
|
+
throw new Error("An inserted id is already known");
|
|
77
188
|
}
|
|
78
|
-
let index;
|
|
79
189
|
if (before === null) {
|
|
80
|
-
|
|
81
|
-
|
|
190
|
+
if (count === 0)
|
|
191
|
+
return this;
|
|
192
|
+
if (this.root.children.length === 0) {
|
|
193
|
+
// Insert the first leaf as a child of root.
|
|
194
|
+
const present = SparseIndices.new();
|
|
195
|
+
present.set(newId.counter, count);
|
|
196
|
+
const leaf = {
|
|
197
|
+
bunchId: newId.bunchId,
|
|
198
|
+
startCounter: newId.counter,
|
|
199
|
+
count,
|
|
200
|
+
present,
|
|
201
|
+
};
|
|
202
|
+
const leafMapMut = { value: this.leafMap };
|
|
203
|
+
return new IdList(new InnerNodeLeaf(this.root.seq, [leaf], leafMapMut), leafMapMut.value, this.parentSeqs);
|
|
204
|
+
}
|
|
205
|
+
else {
|
|
206
|
+
// Insert before the first known id.
|
|
207
|
+
return this.insertBefore(firstId(this.root), newId, count);
|
|
208
|
+
}
|
|
82
209
|
}
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
210
|
+
const located = this.locate(before);
|
|
211
|
+
if (located === null) {
|
|
212
|
+
throw new Error("before is not known");
|
|
213
|
+
}
|
|
214
|
+
if (count === 0)
|
|
215
|
+
return this;
|
|
216
|
+
const leaf = located[0].node;
|
|
217
|
+
if (before.counter === leaf.startCounter + leaf.count - 1) {
|
|
218
|
+
// before is leaf's last id: we insert directly after leaf.
|
|
219
|
+
if (leaf.bunchId === newId.bunchId &&
|
|
220
|
+
leaf.startCounter + leaf.count === newId.counter) {
|
|
221
|
+
// Extending leaf forwards.
|
|
222
|
+
const present = leaf.present.clone();
|
|
223
|
+
present.set(newId.counter, count);
|
|
224
|
+
return this.replaceLeaf(located, {
|
|
225
|
+
...leaf,
|
|
226
|
+
count: leaf.count + count,
|
|
227
|
+
present,
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
else {
|
|
231
|
+
const present = SparseIndices.new();
|
|
232
|
+
present.set(newId.counter, count);
|
|
233
|
+
return this.replaceLeaf(located, leaf, {
|
|
234
|
+
bunchId: newId.bunchId,
|
|
235
|
+
startCounter: newId.counter,
|
|
236
|
+
count,
|
|
237
|
+
present,
|
|
238
|
+
});
|
|
87
239
|
}
|
|
88
240
|
}
|
|
89
|
-
|
|
90
|
-
|
|
241
|
+
else {
|
|
242
|
+
// before is not leaf's last id: we need to split leaf and insert there.
|
|
243
|
+
const newPresent = SparseIndices.new();
|
|
244
|
+
newPresent.set(newId.counter, count);
|
|
245
|
+
const [leftPresent, rightPresent] = splitPresent(leaf.present, before.counter + 1);
|
|
246
|
+
return this.replaceLeaf(located, {
|
|
247
|
+
...leaf,
|
|
248
|
+
count: before.counter + 1 - leaf.startCounter,
|
|
249
|
+
present: leftPresent,
|
|
250
|
+
}, {
|
|
251
|
+
bunchId: newId.bunchId,
|
|
252
|
+
startCounter: newId.counter,
|
|
253
|
+
count,
|
|
254
|
+
present: newPresent,
|
|
255
|
+
}, {
|
|
256
|
+
...leaf,
|
|
257
|
+
startCounter: before.counter + 1,
|
|
258
|
+
count: leaf.count - (before.counter + 1 - leaf.startCounter),
|
|
259
|
+
present: rightPresent,
|
|
260
|
+
});
|
|
261
|
+
}
|
|
91
262
|
}
|
|
92
263
|
/**
|
|
93
264
|
* Inserts `newId` immediately before the given id (`after`), which may be deleted.
|
|
265
|
+
* A new IdList is returned and the current list remains unchanged.
|
|
266
|
+
*
|
|
94
267
|
* All ids to the right of `after`, plus `after` itself, are shifted one index to the right, in the manner
|
|
95
268
|
* of [Array.splice](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/splice).
|
|
96
269
|
*
|
|
@@ -104,75 +277,177 @@ export class IdList {
|
|
|
104
277
|
* @throws If `newId` is already known.
|
|
105
278
|
*/
|
|
106
279
|
insertBefore(after, newId, count = 1) {
|
|
107
|
-
if (
|
|
108
|
-
throw new Error(
|
|
280
|
+
if (!(Number.isSafeInteger(newId.counter) && newId.counter >= 0)) {
|
|
281
|
+
throw new Error(`Invalid counter: ${newId.counter}`);
|
|
282
|
+
}
|
|
283
|
+
if (!(Number.isSafeInteger(count) && count >= 0)) {
|
|
284
|
+
throw new Error(`Invalid count: ${count}`);
|
|
285
|
+
}
|
|
286
|
+
if (this.isAnyKnown(newId, count)) {
|
|
287
|
+
throw new Error("An inserted id is already known");
|
|
109
288
|
}
|
|
110
|
-
let index;
|
|
111
289
|
if (after === null) {
|
|
112
|
-
|
|
290
|
+
if (count === 0)
|
|
291
|
+
return this;
|
|
292
|
+
// Insert after the last known id, or at the beginning if empty.
|
|
293
|
+
return this.insertAfter(this.root.knownSize === 0 ? null : lastId(this.root), newId, count);
|
|
113
294
|
}
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
295
|
+
const located = this.locate(after);
|
|
296
|
+
if (located === null) {
|
|
297
|
+
throw new Error("after is not known");
|
|
298
|
+
}
|
|
299
|
+
if (count === 0)
|
|
300
|
+
return this;
|
|
301
|
+
const leaf = located[0].node;
|
|
302
|
+
if (after.counter === leaf.startCounter) {
|
|
303
|
+
// after is leaf's first id: we insert directly before leaf.
|
|
304
|
+
if (leaf.bunchId === newId.bunchId &&
|
|
305
|
+
leaf.startCounter === newId.counter + count) {
|
|
306
|
+
// Extending leaf backwards.
|
|
307
|
+
const present = leaf.present.clone();
|
|
308
|
+
present.set(newId.counter, count);
|
|
309
|
+
return this.replaceLeaf(located, {
|
|
310
|
+
...leaf,
|
|
311
|
+
startCounter: leaf.startCounter - count,
|
|
312
|
+
count: leaf.count + count,
|
|
313
|
+
present,
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
else {
|
|
317
|
+
const present = SparseIndices.new();
|
|
318
|
+
present.set(newId.counter, count);
|
|
319
|
+
return this.replaceLeaf(located, {
|
|
320
|
+
bunchId: newId.bunchId,
|
|
321
|
+
startCounter: newId.counter,
|
|
322
|
+
count,
|
|
323
|
+
present,
|
|
324
|
+
}, leaf);
|
|
118
325
|
}
|
|
119
326
|
}
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
327
|
+
else {
|
|
328
|
+
// after is not leaf's first id: we need to split leaf and insert there.
|
|
329
|
+
const present = SparseIndices.new();
|
|
330
|
+
present.set(newId.counter, count);
|
|
331
|
+
const [leftPresent, rightPresent] = splitPresent(leaf.present, after.counter);
|
|
332
|
+
return this.replaceLeaf(located, {
|
|
333
|
+
...leaf,
|
|
334
|
+
count: after.counter - leaf.startCounter,
|
|
335
|
+
present: leftPresent,
|
|
336
|
+
}, {
|
|
337
|
+
bunchId: newId.bunchId,
|
|
338
|
+
startCounter: newId.counter,
|
|
339
|
+
count,
|
|
340
|
+
present,
|
|
341
|
+
}, {
|
|
342
|
+
...leaf,
|
|
343
|
+
startCounter: after.counter,
|
|
344
|
+
count: leaf.count - (after.counter - leaf.startCounter),
|
|
345
|
+
present: rightPresent,
|
|
346
|
+
});
|
|
138
347
|
}
|
|
139
348
|
}
|
|
140
349
|
/**
|
|
141
|
-
* Marks `id` as deleted from this list.
|
|
350
|
+
* Marks `id` as deleted from this list.
|
|
351
|
+
* A new IdList is returned and the current list remains unchanged.
|
|
142
352
|
*
|
|
353
|
+
* Once deleted, `id` does not count towards the length of the list or index-based accessors.
|
|
354
|
+
* However, it remains known (a "tombstone").
|
|
143
355
|
* Because `id` is still known, you can reference it in future insertAfter/insertBefore
|
|
144
356
|
* operations, including ones sent concurrently by other devices.
|
|
145
|
-
*
|
|
357
|
+
* This does have a memory cost, but it is compressed in common cases.
|
|
146
358
|
*
|
|
147
|
-
*
|
|
148
|
-
* that makes `id` no longer known, see {@link uninsert}.
|
|
149
|
-
*
|
|
150
|
-
* If `id` is already deleted or not known, this method does nothing.
|
|
359
|
+
* If `id` is already deleted or is not known, this method does nothing.
|
|
151
360
|
*/
|
|
152
361
|
delete(id) {
|
|
153
|
-
const
|
|
154
|
-
if (
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
362
|
+
const located = this.locate(id);
|
|
363
|
+
if (located === null)
|
|
364
|
+
return this;
|
|
365
|
+
const leaf = located[0].node;
|
|
366
|
+
if (!leaf.present.has(id.counter))
|
|
367
|
+
return this;
|
|
368
|
+
const newPresent = leaf.present.clone();
|
|
369
|
+
newPresent.delete(id.counter);
|
|
370
|
+
return this.replaceLeaf(located, { ...leaf, present: newPresent });
|
|
158
371
|
}
|
|
159
372
|
/**
|
|
160
373
|
* Un-marks `id` as deleted from this list, making it present again.
|
|
161
|
-
*
|
|
374
|
+
* A new IdList is returned and the current list remains unchanged.
|
|
375
|
+
*
|
|
376
|
+
* This method is an exact inverse to {@link delete}.
|
|
162
377
|
*
|
|
163
378
|
* If `id` is already present, this method does nothing.
|
|
164
379
|
*
|
|
165
380
|
* @throws If `id` is not known.
|
|
166
381
|
*/
|
|
167
382
|
undelete(id) {
|
|
168
|
-
const
|
|
169
|
-
if (
|
|
383
|
+
const located = this.locate(id);
|
|
384
|
+
if (located === null) {
|
|
170
385
|
throw new Error("id is not known");
|
|
171
386
|
}
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
this
|
|
387
|
+
const leaf = located[0].node;
|
|
388
|
+
if (leaf.present.has(id.counter))
|
|
389
|
+
return this;
|
|
390
|
+
const newPresent = leaf.present.clone();
|
|
391
|
+
newPresent.set(id.counter);
|
|
392
|
+
return this.replaceLeaf(located, { ...leaf, present: newPresent });
|
|
393
|
+
}
|
|
394
|
+
/**
|
|
395
|
+
* Returns the path from id's leaf node to the root, or null if id is not found.
|
|
396
|
+
*
|
|
397
|
+
* The path contains each node and its index in its parent's node, starting with id's
|
|
398
|
+
* LeafNode and ending at a child of the root.
|
|
399
|
+
*/
|
|
400
|
+
locate(id) {
|
|
401
|
+
// Find the leaf containing id, if any.
|
|
402
|
+
const [leaf, parentSeq] = this.leafMap.getLeaf(id.bunchId, id.counter);
|
|
403
|
+
if (leaf === undefined)
|
|
404
|
+
return null;
|
|
405
|
+
if (!(leaf.bunchId === id.bunchId &&
|
|
406
|
+
leaf.startCounter <= id.counter &&
|
|
407
|
+
id.counter < leaf.startCounter + leaf.count)) {
|
|
408
|
+
return null;
|
|
409
|
+
}
|
|
410
|
+
// Find the seqs on the path (leaf, root].
|
|
411
|
+
const innerSeqs = [];
|
|
412
|
+
let curSeq = parentSeq;
|
|
413
|
+
while (curSeq !== 0) {
|
|
414
|
+
innerSeqs.push(curSeq);
|
|
415
|
+
curSeq = this.parentSeqs.get(curSeq);
|
|
175
416
|
}
|
|
417
|
+
// Find the nodes and indexInParent's on the path (root, leaf),
|
|
418
|
+
// using seqs to find the appropriate child of each node.
|
|
419
|
+
const innerNodes = [];
|
|
420
|
+
let curParent = this.root;
|
|
421
|
+
// Start at the root child's seq and proceed to the leaf parent's seq.
|
|
422
|
+
for (let i = innerSeqs.length - 2; i >= 0; i--) {
|
|
423
|
+
const children = curParent.children;
|
|
424
|
+
const childIndex = children.findIndex((child) => child.seq === innerSeqs[i]);
|
|
425
|
+
if (childIndex === -1)
|
|
426
|
+
throw new Error("Internal error");
|
|
427
|
+
const child = children[childIndex];
|
|
428
|
+
innerNodes.push({ node: child, indexInParent: childIndex });
|
|
429
|
+
curParent = child;
|
|
430
|
+
}
|
|
431
|
+
// Now curParent is the leaf's parent. Find leaf in its children and return.
|
|
432
|
+
const leafChildIndex = curParent.children.indexOf(leaf);
|
|
433
|
+
if (leafChildIndex === -1)
|
|
434
|
+
throw new Error("Internal error");
|
|
435
|
+
return [
|
|
436
|
+
{ node: leaf, indexInParent: leafChildIndex },
|
|
437
|
+
...innerNodes.reverse(),
|
|
438
|
+
];
|
|
439
|
+
}
|
|
440
|
+
/**
|
|
441
|
+
* Replaces the leaf at the given path with newLeaves.
|
|
442
|
+
* Returns a proper (sufficiently balanced) B+Tree with updated sizes.
|
|
443
|
+
*
|
|
444
|
+
* newLeaves.length must be in [1, M].
|
|
445
|
+
*/
|
|
446
|
+
replaceLeaf(located, ...newLeaves) {
|
|
447
|
+
const leafMapMut = { value: this.leafMap };
|
|
448
|
+
const parentSeqsMut = { value: this.parentSeqs };
|
|
449
|
+
const newRoot = replaceNode(located, this.root, leafMapMut, parentSeqsMut, newLeaves, 0);
|
|
450
|
+
return new IdList(newRoot, leafMapMut.value, parentSeqsMut.value);
|
|
176
451
|
}
|
|
177
452
|
// Accessors
|
|
178
453
|
/**
|
|
@@ -183,10 +458,12 @@ export class IdList {
|
|
|
183
458
|
* Compare to {@link isKnown}.
|
|
184
459
|
*/
|
|
185
460
|
has(id) {
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
461
|
+
// Find the LeafNode that would contain id if known.
|
|
462
|
+
const [leaf] = this.leafMap.getLeaf(id.bunchId, id.counter);
|
|
463
|
+
if (leaf && leaf.bunchId === id.bunchId) {
|
|
464
|
+
return leaf.present.has(id.counter);
|
|
465
|
+
}
|
|
466
|
+
return false;
|
|
190
467
|
}
|
|
191
468
|
/**
|
|
192
469
|
* Returns whether id is known to this list.
|
|
@@ -194,7 +471,42 @@ export class IdList {
|
|
|
194
471
|
* Compare to {@link has}.
|
|
195
472
|
*/
|
|
196
473
|
isKnown(id) {
|
|
197
|
-
|
|
474
|
+
// Find the LeafNode that would contain id if known.
|
|
475
|
+
const [leaf] = this.leafMap.getLeaf(id.bunchId, id.counter);
|
|
476
|
+
if (leaf && leaf.bunchId === id.bunchId) {
|
|
477
|
+
return (leaf.startCounter <= id.counter &&
|
|
478
|
+
id.counter < leaf.startCounter + leaf.count);
|
|
479
|
+
}
|
|
480
|
+
return false;
|
|
481
|
+
}
|
|
482
|
+
// TODO: Make public?
|
|
483
|
+
/**
|
|
484
|
+
* Returns true if any of the given bulk ids are known.
|
|
485
|
+
*/
|
|
486
|
+
isAnyKnown(id, count) {
|
|
487
|
+
if (count === 0)
|
|
488
|
+
return false;
|
|
489
|
+
// Find the leaf containing the last id, or the previous leaf.
|
|
490
|
+
// If any leaf knows any of the ids, this leaf must know an id too.
|
|
491
|
+
const [leaf] = this.leafMap.getLeaf(id.bunchId, id.counter + count - 1);
|
|
492
|
+
if (leaf && leaf.bunchId === id.bunchId) {
|
|
493
|
+
// Test if there is any overlap between the leaf's counter range [a, b]
|
|
494
|
+
// and the bulk ids' counter range [c, d].
|
|
495
|
+
const a = leaf.startCounter;
|
|
496
|
+
const b = leaf.startCounter + leaf.count - 1;
|
|
497
|
+
const c = id.counter;
|
|
498
|
+
const d = id.counter + count - 1;
|
|
499
|
+
return a <= d && c <= b;
|
|
500
|
+
}
|
|
501
|
+
return false;
|
|
502
|
+
}
|
|
503
|
+
/**
|
|
504
|
+
* The length of the list, counting only present ids.
|
|
505
|
+
*
|
|
506
|
+
* To include known but deleted ids, use `this.knownIds.length`.
|
|
507
|
+
*/
|
|
508
|
+
get length() {
|
|
509
|
+
return this.root.size;
|
|
198
510
|
}
|
|
199
511
|
/**
|
|
200
512
|
* Returns the id at the given index in the list.
|
|
@@ -206,14 +518,38 @@ export class IdList {
|
|
|
206
518
|
throw new Error(`Index out of bounds: ${index} (length: ${this.length}`);
|
|
207
519
|
}
|
|
208
520
|
let remaining = index;
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
521
|
+
let curParent = this.root;
|
|
522
|
+
// eslint-disable-next-line no-constant-condition
|
|
523
|
+
recurse: while (true) {
|
|
524
|
+
if (curParent instanceof InnerNodeInner) {
|
|
525
|
+
for (const child of curParent.children) {
|
|
526
|
+
if (remaining < child.size) {
|
|
527
|
+
// Recurse.
|
|
528
|
+
curParent = child;
|
|
529
|
+
continue recurse;
|
|
530
|
+
}
|
|
531
|
+
else {
|
|
532
|
+
remaining -= child.size;
|
|
533
|
+
}
|
|
534
|
+
}
|
|
214
535
|
}
|
|
536
|
+
else {
|
|
537
|
+
for (const child of curParent.children) {
|
|
538
|
+
const childSize = child.present.count();
|
|
539
|
+
if (remaining < childSize) {
|
|
540
|
+
// Found it.
|
|
541
|
+
return {
|
|
542
|
+
bunchId: child.bunchId,
|
|
543
|
+
counter: child.present.indexOfCount(remaining),
|
|
544
|
+
};
|
|
545
|
+
}
|
|
546
|
+
else {
|
|
547
|
+
remaining -= childSize;
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
throw new Error("Internal error");
|
|
215
552
|
}
|
|
216
|
-
throw new Error("Internal error");
|
|
217
553
|
}
|
|
218
554
|
/**
|
|
219
555
|
* Returns the index of `id` in the list.
|
|
@@ -227,47 +563,48 @@ export class IdList {
|
|
|
227
563
|
* @throws If `id` is not known.
|
|
228
564
|
*/
|
|
229
565
|
indexOf(id, bias = "none") {
|
|
566
|
+
const located = this.locate(id);
|
|
567
|
+
if (located === null)
|
|
568
|
+
throw new Error("id is not known");
|
|
230
569
|
/**
|
|
231
570
|
* The number of present ids less than id.
|
|
232
571
|
* Equivalently, the index id would have if present.
|
|
233
572
|
*/
|
|
234
573
|
let index = 0;
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
574
|
+
// Lesser siblings of parent, grandparent, etc.
|
|
575
|
+
for (let i = 1; i < located.length; i++) {
|
|
576
|
+
const parent = (i === located.length - 1 ? this.root : located[i + 1].node);
|
|
577
|
+
for (let c = 0; c < located[i].indexInParent; c++) {
|
|
578
|
+
index += parent.children[c].size;
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
// Siblings of id's leaf.
|
|
582
|
+
const leafParent = (located.length === 1 ? this.root : located[1].node);
|
|
583
|
+
for (let c = 0; c < located[0].indexInParent; c++) {
|
|
584
|
+
index += leafParent.children[c].present.count();
|
|
585
|
+
}
|
|
586
|
+
// id's index within leaf.
|
|
587
|
+
const [count, has] = located[0].node.present._countHas(id.counter);
|
|
588
|
+
index += count;
|
|
589
|
+
if (has)
|
|
590
|
+
return index;
|
|
591
|
+
else {
|
|
592
|
+
switch (bias) {
|
|
593
|
+
case "none":
|
|
594
|
+
return -1;
|
|
595
|
+
case "left":
|
|
596
|
+
return index - 1;
|
|
597
|
+
case "right":
|
|
249
598
|
return index;
|
|
250
599
|
}
|
|
251
|
-
if (!elt.isDeleted)
|
|
252
|
-
index++;
|
|
253
600
|
}
|
|
254
|
-
throw new Error("id is not known");
|
|
255
|
-
}
|
|
256
|
-
/**
|
|
257
|
-
* The length of the list.
|
|
258
|
-
*/
|
|
259
|
-
get length() {
|
|
260
|
-
return this._length;
|
|
261
601
|
}
|
|
262
602
|
// Iterators and views
|
|
263
603
|
/**
|
|
264
604
|
* Iterates over all present ids in the list.
|
|
265
605
|
*/
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
if (!elt.isDeleted)
|
|
269
|
-
yield elt.id;
|
|
270
|
-
}
|
|
606
|
+
[Symbol.iterator]() {
|
|
607
|
+
return iterateNode(this.root, false);
|
|
271
608
|
}
|
|
272
609
|
/**
|
|
273
610
|
* Iterates over all present ids in the list.
|
|
@@ -278,22 +615,16 @@ export class IdList {
|
|
|
278
615
|
/**
|
|
279
616
|
* Iterates over all __known__ ids in the list, indicating which are deleted.
|
|
280
617
|
*/
|
|
281
|
-
|
|
282
|
-
return this.
|
|
283
|
-
}
|
|
284
|
-
/**
|
|
285
|
-
* Returns an independent copy of this list, including known but deleted ids.
|
|
286
|
-
*/
|
|
287
|
-
clone() {
|
|
288
|
-
return IdList.from(this.state);
|
|
618
|
+
valuesWithIsDeleted() {
|
|
619
|
+
return iterateNodeWithIsDeleted(this.root);
|
|
289
620
|
}
|
|
290
621
|
/**
|
|
291
|
-
* A
|
|
292
|
-
* That is, it ignores
|
|
622
|
+
* A view of this list that treats all known ids as present.
|
|
623
|
+
* That is, it ignores is-deleted status when computing list indices or iterating.
|
|
293
624
|
*/
|
|
294
625
|
get knownIds() {
|
|
295
626
|
if (this._knownIds === undefined) {
|
|
296
|
-
this._knownIds = new KnownIdView(this, this.
|
|
627
|
+
this._knownIds = new KnownIdView(this, this.root);
|
|
297
628
|
}
|
|
298
629
|
return this._knownIds;
|
|
299
630
|
}
|
|
@@ -305,60 +636,86 @@ export class IdList {
|
|
|
305
636
|
* See {@link SavedIdList} for a description of the save format.
|
|
306
637
|
*/
|
|
307
638
|
save() {
|
|
308
|
-
const
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
const current = ans[ans.length - 1];
|
|
312
|
-
if (id.bunchId === current.bunchId &&
|
|
313
|
-
id.counter === current.startCounter + current.count &&
|
|
314
|
-
isDeleted === current.isDeleted) {
|
|
315
|
-
current.count++;
|
|
316
|
-
continue;
|
|
317
|
-
}
|
|
318
|
-
}
|
|
319
|
-
ans.push({
|
|
320
|
-
bunchId: id.bunchId,
|
|
321
|
-
startCounter: id.counter,
|
|
322
|
-
count: 1,
|
|
323
|
-
isDeleted,
|
|
324
|
-
});
|
|
325
|
-
}
|
|
326
|
-
return ans;
|
|
639
|
+
const acc = [];
|
|
640
|
+
saveNode(this.root, acc);
|
|
641
|
+
return acc;
|
|
327
642
|
}
|
|
328
643
|
/**
|
|
329
|
-
* Loads a saved state returned by {@link save}
|
|
644
|
+
* Loads a saved state returned by {@link save}.
|
|
330
645
|
*/
|
|
331
|
-
load(savedState) {
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
for (
|
|
335
|
-
|
|
336
|
-
|
|
646
|
+
static load(savedState) {
|
|
647
|
+
// 1. Determine the leaves in list order.
|
|
648
|
+
const leaves = [];
|
|
649
|
+
for (let i = 0; i < savedState.length; i++) {
|
|
650
|
+
const item = savedState[i];
|
|
651
|
+
if (!(Number.isSafeInteger(item.count) && item.count >= 0)) {
|
|
652
|
+
throw new Error(`Invalid count: ${item.count}`);
|
|
337
653
|
}
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
654
|
+
if (!(Number.isSafeInteger(item.startCounter) && item.startCounter >= 0)) {
|
|
655
|
+
throw new Error(`Invalid startCounter: ${item.startCounter}`);
|
|
656
|
+
}
|
|
657
|
+
if (item.count === 0)
|
|
658
|
+
continue;
|
|
659
|
+
if (leaves.length !== 0) {
|
|
660
|
+
const lastLeaf = leaves.at(-1);
|
|
661
|
+
if (item.bunchId === lastLeaf.bunchId &&
|
|
662
|
+
item.startCounter === lastLeaf.startCounter + lastLeaf.count) {
|
|
663
|
+
// Extend lastLeaf.
|
|
664
|
+
// Okay to mutate in-place since we haven't referenced it anywhere else yet.
|
|
665
|
+
// @ts-expect-error Mutate in place
|
|
666
|
+
lastLeaf.count += item.count;
|
|
667
|
+
if (!item.isDeleted) {
|
|
668
|
+
lastLeaf.present.set(item.startCounter, item.count);
|
|
669
|
+
}
|
|
670
|
+
continue;
|
|
671
|
+
}
|
|
343
672
|
}
|
|
344
|
-
|
|
345
|
-
|
|
673
|
+
// If we get to here, we need a new leaf.
|
|
674
|
+
const present = SparseIndices.new();
|
|
675
|
+
if (!item.isDeleted)
|
|
676
|
+
present.set(item.startCounter, item.count);
|
|
677
|
+
leaves.push({
|
|
678
|
+
bunchId: item.bunchId,
|
|
679
|
+
startCounter: item.startCounter,
|
|
680
|
+
count: item.count,
|
|
681
|
+
present,
|
|
682
|
+
});
|
|
346
683
|
}
|
|
684
|
+
// 2. Create a B+Tree with the given leaves.
|
|
685
|
+
// We do a "direct" balanced construction that takes O(L) time, instead of inserting
|
|
686
|
+
// leaves one-by-one, which would take O(L log(L)) time.
|
|
687
|
+
// However, constructing the sorted leafMap brings the overall runtime to O(L log(L)).
|
|
688
|
+
if (leaves.length === 0)
|
|
689
|
+
return IdList.new();
|
|
690
|
+
// TODO: Test the aux data structures after loading.
|
|
691
|
+
// E.g. reload and then call checkAll again.
|
|
692
|
+
// Also should do insertions to test splitting of the full tree.
|
|
693
|
+
const leafMapMut = { value: LeafMap.new() };
|
|
694
|
+
const parentSeqsMut = { value: SeqMap.new() };
|
|
695
|
+
// Depth of the B+Tree (number of non-root nodes on any path from a leaf to the root).
|
|
696
|
+
// A full B+Tree of depth d has between [M^{d-1} + 1, M^d] leaves.
|
|
697
|
+
const depth = leaves.length === 1
|
|
698
|
+
? 1
|
|
699
|
+
: Math.ceil(Math.log(leaves.length) / Math.log(M));
|
|
700
|
+
const root = buildTree(leaves, leafMapMut, parentSeqsMut, 0, depth);
|
|
701
|
+
return new IdList(root, leafMapMut.value, parentSeqsMut.value);
|
|
347
702
|
}
|
|
348
703
|
}
|
|
349
704
|
/**
|
|
350
|
-
* A
|
|
351
|
-
* That is, this class ignores the underlying list's
|
|
705
|
+
* A view of an IdList that treats all known ids as present.
|
|
706
|
+
* That is, this class ignores the underlying list's is-deleted status when computing list indices.
|
|
707
|
+
* Access using {@link IdList.knownIds}.
|
|
352
708
|
*
|
|
353
|
-
* To mutate,
|
|
709
|
+
* Like IdList, KnownIdView is immutable. To mutate, use a mutating method on the original IdList
|
|
710
|
+
* and access the returned list's `knownIds`.
|
|
354
711
|
*/
|
|
355
712
|
export class KnownIdView {
|
|
356
713
|
/**
|
|
357
714
|
* Internal use only. Use {@link IdList.knownIds} instead.
|
|
358
715
|
*/
|
|
359
|
-
constructor(list,
|
|
716
|
+
constructor(list, root) {
|
|
360
717
|
this.list = list;
|
|
361
|
-
this.
|
|
718
|
+
this.root = root;
|
|
362
719
|
}
|
|
363
720
|
// Mutators are omitted - mutate this.list instead.
|
|
364
721
|
// Accessors
|
|
@@ -373,13 +730,67 @@ export class KnownIdView {
|
|
|
373
730
|
if (!(Number.isSafeInteger(index) && 0 <= index && index < this.length)) {
|
|
374
731
|
throw new Error(`Index out of bounds: ${index} (length: ${this.length}`);
|
|
375
732
|
}
|
|
376
|
-
|
|
733
|
+
let remaining = index;
|
|
734
|
+
let curParent = this.root;
|
|
735
|
+
// eslint-disable-next-line no-constant-condition
|
|
736
|
+
recurse: while (true) {
|
|
737
|
+
if (curParent instanceof InnerNodeInner) {
|
|
738
|
+
for (const child of curParent.children) {
|
|
739
|
+
if (remaining < child.knownSize) {
|
|
740
|
+
// Recurse.
|
|
741
|
+
curParent = child;
|
|
742
|
+
continue recurse;
|
|
743
|
+
}
|
|
744
|
+
else {
|
|
745
|
+
remaining -= child.knownSize;
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
else {
|
|
750
|
+
for (const child of curParent.children) {
|
|
751
|
+
if (remaining < child.count) {
|
|
752
|
+
// Found it.
|
|
753
|
+
return {
|
|
754
|
+
bunchId: child.bunchId,
|
|
755
|
+
counter: child.startCounter + remaining,
|
|
756
|
+
};
|
|
757
|
+
}
|
|
758
|
+
else {
|
|
759
|
+
remaining -= child.count;
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
throw new Error("Internal error");
|
|
764
|
+
}
|
|
377
765
|
}
|
|
378
766
|
/**
|
|
379
767
|
* Returns the index of `id` in this view, or -1 if it is not known.
|
|
380
768
|
*/
|
|
381
769
|
indexOf(id) {
|
|
382
|
-
|
|
770
|
+
// @ts-expect-error Ignore private
|
|
771
|
+
const located = this.list.locate(id);
|
|
772
|
+
if (located === null)
|
|
773
|
+
throw new Error("id is not known");
|
|
774
|
+
/**
|
|
775
|
+
* The number of present ids less than id.
|
|
776
|
+
* Equivalently, the index id would have if present.
|
|
777
|
+
*/
|
|
778
|
+
let index = 0;
|
|
779
|
+
// Lesser siblings of parent, grandparent, etc.
|
|
780
|
+
for (let i = 1; i < located.length; i++) {
|
|
781
|
+
const parent = (i === located.length - 1 ? this.root : located[i + 1].node);
|
|
782
|
+
for (let c = 0; c < located[i].indexInParent; c++) {
|
|
783
|
+
index += parent.children[c].knownSize;
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
// Siblings of id's leaf.
|
|
787
|
+
const leafParent = (located.length === 1 ? this.root : located[1].node);
|
|
788
|
+
for (let c = 0; c < located[0].indexInParent; c++) {
|
|
789
|
+
const child = leafParent.children[c];
|
|
790
|
+
index += child.count;
|
|
791
|
+
}
|
|
792
|
+
// id's index with leaf.
|
|
793
|
+
return index + (id.counter - located[0].node.startCounter);
|
|
383
794
|
}
|
|
384
795
|
/**
|
|
385
796
|
* The length of this view.
|
|
@@ -387,16 +798,14 @@ export class KnownIdView {
|
|
|
387
798
|
* Equivalently, the number of known ids in `this.list`.
|
|
388
799
|
*/
|
|
389
800
|
get length() {
|
|
390
|
-
return this.
|
|
801
|
+
return this.root.knownSize;
|
|
391
802
|
}
|
|
392
803
|
// Iterators
|
|
393
804
|
/**
|
|
394
805
|
* Iterates over all ids in this view, i.e., all known ids in `this.list`.
|
|
395
806
|
*/
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
yield elt.id;
|
|
399
|
-
}
|
|
807
|
+
[Symbol.iterator]() {
|
|
808
|
+
return iterateNode(this.root, true);
|
|
400
809
|
}
|
|
401
810
|
/**
|
|
402
811
|
* Iterates over all ids in this view, i.e., all known ids in `this.list`.
|
|
@@ -405,17 +814,265 @@ export class KnownIdView {
|
|
|
405
814
|
return this[Symbol.iterator]();
|
|
406
815
|
}
|
|
407
816
|
}
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
817
|
+
/**
|
|
818
|
+
* Returns the first (leftmost) known ElementId in node's subtree.
|
|
819
|
+
*/
|
|
820
|
+
function firstId(node) {
|
|
821
|
+
let currentInner = node;
|
|
822
|
+
while (!(currentInner instanceof InnerNodeLeaf)) {
|
|
823
|
+
currentInner = currentInner.children[0];
|
|
824
|
+
}
|
|
825
|
+
const firstLeaf = currentInner.children[0];
|
|
826
|
+
return {
|
|
827
|
+
bunchId: firstLeaf.bunchId,
|
|
828
|
+
counter: firstLeaf.startCounter,
|
|
829
|
+
};
|
|
830
|
+
}
|
|
831
|
+
/**
|
|
832
|
+
* Returns the last (rightmost) known ElementId in node's subtree.
|
|
833
|
+
*/
|
|
834
|
+
function lastId(node) {
|
|
835
|
+
let currentInner = node;
|
|
836
|
+
while (!(currentInner instanceof InnerNodeLeaf)) {
|
|
837
|
+
currentInner = currentInner.children.at(-1);
|
|
838
|
+
}
|
|
839
|
+
const lastLeaf = currentInner.children.at(-1);
|
|
840
|
+
return {
|
|
841
|
+
bunchId: lastLeaf.bunchId,
|
|
842
|
+
counter: lastLeaf.startCounter + lastLeaf.count - 1,
|
|
843
|
+
};
|
|
844
|
+
}
|
|
845
|
+
/**
|
|
846
|
+
* Replace located[i].node with newNodes.
|
|
847
|
+
*
|
|
848
|
+
* newNodes.length must be in [1, M].
|
|
849
|
+
*
|
|
850
|
+
* The returned node's descendants are recorded in leafMapMut and parentSeqsMut,
|
|
851
|
+
* but the node itself is not (since we don't know its parent here).
|
|
852
|
+
*/
|
|
853
|
+
function replaceNode(located, root, leafMapMut, parentSeqsMut, newNodes, i) {
|
|
854
|
+
const parent = i === located.length - 1 ? root : located[i + 1].node;
|
|
855
|
+
const indexInParent = located[i].indexInParent;
|
|
856
|
+
// Copy-on-write version of parent.children.splice(indexInParent, 1, ...newNodes)
|
|
857
|
+
const newChildren = parent.children
|
|
858
|
+
.slice(0, indexInParent)
|
|
859
|
+
.concat(newNodes, parent.children.slice(indexInParent + 1));
|
|
860
|
+
if (newChildren.length > M) {
|
|
861
|
+
// Split the parent to maintain BTree property (# children <= M).
|
|
862
|
+
// Treat the right parent as "new", getting a new seq.
|
|
863
|
+
const split = Math.ceil(newChildren.length / 2);
|
|
864
|
+
const seqs = [parent.seq, getAndBumpNextSeq(parentSeqsMut)];
|
|
865
|
+
const newParents = [
|
|
866
|
+
newChildren.slice(0, split),
|
|
867
|
+
newChildren.slice(split),
|
|
868
|
+
].map((children, j) => i === 0
|
|
869
|
+
? new InnerNodeLeaf(seqs[j], children, leafMapMut)
|
|
870
|
+
: new InnerNodeInner(seqs[j], children, parentSeqsMut));
|
|
871
|
+
if (i === located.length - 1) {
|
|
872
|
+
// newParents replace root. We need a new root to hold them.
|
|
873
|
+
return new InnerNodeInner(getAndBumpNextSeq(parentSeqsMut), newParents, parentSeqsMut);
|
|
874
|
+
}
|
|
875
|
+
else {
|
|
876
|
+
return replaceNode(located, root, leafMapMut, parentSeqsMut, newParents, i + 1);
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
else {
|
|
880
|
+
// "Replace" parent, reusing its seq.
|
|
881
|
+
// To avoid doing newChildren.length sets every time (which makes replaceLeaf
|
|
882
|
+
// do >=(M/2)*log(L) total sets, even when none were necessary),
|
|
883
|
+
// we bypass the InnerNode constructors' leafMap/parentSeq operations,
|
|
884
|
+
// instead doing them ourselves only on the changed children.
|
|
885
|
+
let newParent;
|
|
886
|
+
if (i === 0) {
|
|
887
|
+
newParent = new InnerNodeLeaf(parent.seq, newChildren, null);
|
|
888
|
+
// Important to delete the replaced leaf's entry, so that it doesn't corrupt by-ElementId searches.
|
|
889
|
+
leafMapMut.value = leafMapMut.value.delete(located[0].node);
|
|
890
|
+
for (const newNode of newNodes) {
|
|
891
|
+
leafMapMut.value = leafMapMut.value.set(newNode, parent.seq);
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
else {
|
|
895
|
+
newParent = new InnerNodeInner(parent.seq, newChildren, null);
|
|
896
|
+
for (const newNode of newNodes) {
|
|
897
|
+
if (newNode.seq !== located[i].node.seq) {
|
|
898
|
+
parentSeqsMut.value = parentSeqsMut.value.set(newNode.seq, parent.seq);
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
// If the replaced node isn't represented in newNodes (i.e., same seq is not reused),
|
|
902
|
+
// we could delete its entry to save memory, but it is not necessary.
|
|
903
|
+
}
|
|
904
|
+
if (i === located.length - 1) {
|
|
905
|
+
// Replaces root.
|
|
906
|
+
return newParent;
|
|
907
|
+
}
|
|
908
|
+
else {
|
|
909
|
+
return replaceNode(located, root, leafMapMut, parentSeqsMut, [newParent], i + 1);
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
/**
|
|
914
|
+
* Splits present into two SparseIndices at the given counter.
|
|
915
|
+
*/
|
|
916
|
+
function splitPresent(present, splitCounter) {
|
|
917
|
+
const leftPresent = SparseIndices.new();
|
|
918
|
+
const rightPresent = SparseIndices.new();
|
|
919
|
+
const leafSlicer = present.newSlicer();
|
|
920
|
+
for (const [index, count] of leafSlicer.nextSlice(splitCounter)) {
|
|
921
|
+
leftPresent.set(index, count);
|
|
922
|
+
}
|
|
923
|
+
for (const [index, count] of leafSlicer.nextSlice(null)) {
|
|
924
|
+
rightPresent.set(index, count);
|
|
925
|
+
}
|
|
926
|
+
return [leftPresent, rightPresent];
|
|
927
|
+
}
|
|
928
|
+
function* iterateNode(node, includeDeleted) {
|
|
929
|
+
if (node instanceof InnerNodeInner) {
|
|
930
|
+
for (const child of node.children) {
|
|
931
|
+
yield* iterateNode(child, includeDeleted);
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
else {
|
|
935
|
+
for (const child of node.children) {
|
|
936
|
+
if (includeDeleted) {
|
|
937
|
+
for (let i = 0; i < child.count; i++) {
|
|
938
|
+
yield { bunchId: child.bunchId, counter: child.startCounter + i };
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
else {
|
|
942
|
+
for (const counter of child.present.keys()) {
|
|
943
|
+
yield { bunchId: child.bunchId, counter };
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
function* iterateNodeWithIsDeleted(node) {
|
|
950
|
+
if (node instanceof InnerNodeInner) {
|
|
951
|
+
for (const child of node.children) {
|
|
952
|
+
yield* iterateNodeWithIsDeleted(child);
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
else {
|
|
956
|
+
for (const child of node.children) {
|
|
957
|
+
let nextIndex = child.startCounter;
|
|
958
|
+
for (const index of child.present.keys()) {
|
|
959
|
+
while (nextIndex < index) {
|
|
960
|
+
yield {
|
|
961
|
+
id: { bunchId: child.bunchId, counter: nextIndex },
|
|
962
|
+
isDeleted: true,
|
|
963
|
+
};
|
|
964
|
+
nextIndex++;
|
|
965
|
+
}
|
|
966
|
+
yield {
|
|
967
|
+
id: { bunchId: child.bunchId, counter: index },
|
|
968
|
+
isDeleted: false,
|
|
969
|
+
};
|
|
970
|
+
nextIndex++;
|
|
971
|
+
}
|
|
972
|
+
while (nextIndex < child.startCounter + child.count) {
|
|
973
|
+
yield {
|
|
974
|
+
id: { bunchId: child.bunchId, counter: nextIndex },
|
|
975
|
+
isDeleted: true,
|
|
976
|
+
};
|
|
977
|
+
nextIndex++;
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
/**
|
|
983
|
+
* Updates acc to account for node's subtree, as part of a depth-first search
|
|
984
|
+
* in list order.
|
|
985
|
+
*/
|
|
986
|
+
function saveNode(node, acc) {
|
|
987
|
+
if (node instanceof InnerNodeInner) {
|
|
988
|
+
for (const child of node.children) {
|
|
989
|
+
saveNode(child, acc);
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
else {
|
|
993
|
+
for (const child of node.children) {
|
|
994
|
+
let nextIndex = child.startCounter;
|
|
995
|
+
for (const [index, count] of child.present.items()) {
|
|
996
|
+
if (nextIndex < index) {
|
|
997
|
+
// Need a deleted item.
|
|
998
|
+
pushSaveItem(acc, {
|
|
999
|
+
bunchId: child.bunchId,
|
|
1000
|
+
startCounter: nextIndex,
|
|
1001
|
+
count: index - nextIndex,
|
|
1002
|
+
isDeleted: true,
|
|
1003
|
+
});
|
|
1004
|
+
}
|
|
1005
|
+
pushSaveItem(acc, {
|
|
1006
|
+
bunchId: child.bunchId,
|
|
1007
|
+
startCounter: index,
|
|
1008
|
+
count,
|
|
1009
|
+
isDeleted: false,
|
|
1010
|
+
});
|
|
1011
|
+
nextIndex = index + count;
|
|
1012
|
+
}
|
|
1013
|
+
if (nextIndex < child.startCounter + child.count) {
|
|
1014
|
+
pushSaveItem(acc, {
|
|
1015
|
+
bunchId: child.bunchId,
|
|
1016
|
+
startCounter: nextIndex,
|
|
1017
|
+
count: child.startCounter + child.count - nextIndex,
|
|
1018
|
+
isDeleted: true,
|
|
1019
|
+
});
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
/**
|
|
1025
|
+
* Pushes a save item onto acc, combing it with the previous item if possible.
|
|
1026
|
+
*
|
|
1027
|
+
* This function is necessary because we don't guarantee that adjacent leaves are fully merged.
|
|
1028
|
+
* Specifically, if you insert a bunch's ids with counter values (0, 2, 1)
|
|
1029
|
+
* in that order, then counter 1 will extend one of the existing leaves
|
|
1030
|
+
* but not merge with the other leaf.
|
|
1031
|
+
*
|
|
1032
|
+
* This situation won't appear in typical usage, and its perf penalty
|
|
1033
|
+
* will go away once you reload. Thus we tolerate it instead of figuring out
|
|
1034
|
+
* how to delete leaves from a B+Tree.
|
|
1035
|
+
*/
|
|
1036
|
+
function pushSaveItem(acc, item) {
|
|
1037
|
+
if (acc.length > 0) {
|
|
1038
|
+
const previous = acc.at(-1);
|
|
1039
|
+
if (previous.isDeleted === item.isDeleted &&
|
|
1040
|
+
previous.bunchId === item.bunchId &&
|
|
1041
|
+
previous.startCounter + previous.count === item.startCounter) {
|
|
1042
|
+
// Combine items.
|
|
1043
|
+
// @ts-expect-error Mutating for convenience; no aliasing to worry about.
|
|
1044
|
+
previous.count += item.count;
|
|
1045
|
+
return;
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
acc.push(item);
|
|
1049
|
+
}
|
|
1050
|
+
/**
|
|
1051
|
+
* Builds a tree with the given leaves. Used by IdList.load.
|
|
1052
|
+
*
|
|
1053
|
+
* The returned node's descendants are recorded in leafMapMut and parentSeqsMut,
|
|
1054
|
+
* but not the node itself (since we don't know its parent here).
|
|
1055
|
+
*
|
|
1056
|
+
* In contrast to inserting the leaves one-by-one, this function fills nodes
|
|
1057
|
+
* with M children whenever possible,
|
|
1058
|
+
* and the B+Tree parts run in O(L) time instead of O(L log(L)).
|
|
1059
|
+
* However, the overall runtime is O(L log(L)) from constructing the sorted leafMap.
|
|
1060
|
+
*/
|
|
1061
|
+
function buildTree(leaves, leafMapMut, parentSeqsMut, startIndex, depthRemaining) {
|
|
1062
|
+
const parentSeq = getAndBumpNextSeq(parentSeqsMut);
|
|
1063
|
+
if (depthRemaining === 1) {
|
|
1064
|
+
return new InnerNodeLeaf(parentSeq, leaves.slice(startIndex, startIndex + M), leafMapMut);
|
|
1065
|
+
}
|
|
1066
|
+
else {
|
|
1067
|
+
const children = [];
|
|
1068
|
+
const childLeafCount = Math.pow(M, depthRemaining - 1);
|
|
1069
|
+
for (let i = 0; i < M; i++) {
|
|
1070
|
+
const childStartIndex = startIndex + i * childLeafCount;
|
|
1071
|
+
if (childStartIndex >= leaves.length)
|
|
1072
|
+
break;
|
|
1073
|
+
children.push(buildTree(leaves, leafMapMut, parentSeqsMut, childStartIndex, depthRemaining - 1));
|
|
1074
|
+
}
|
|
1075
|
+
return new InnerNodeInner(parentSeq, children, parentSeqsMut);
|
|
1076
|
+
}
|
|
420
1077
|
}
|
|
421
1078
|
//# sourceMappingURL=id_list.js.map
|