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