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