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