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.
@@ -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 id_1 = require("./id");
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
- * A list of ElementIds.
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 remains in memory as a "tombstone".
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
- * If that is not a concern and you truly want to make an id no longer known, instead
19
- * call {@link uninsert}.
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} or {@link IdList.fromIds}.
83
+ * To begin with a non-empty list, use {@link IdList.from}, {@link IdList.fromIds},
84
+ * or {@link IdList.load}.
31
85
  */
32
- constructor() {
33
- this.state = [];
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(state) {
40
- const list = new IdList();
41
- for (const { id, isDeleted } of state) {
42
- // Clone to prevent aliasing.
43
- list.state.push({ id, isDeleted });
44
- if (!isDeleted)
45
- list._length++;
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 list;
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
- const list = new IdList();
58
- for (const id of ids) {
59
- list.state.push({ id, isDeleted: false });
60
- list._length++;
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 `newId` is already known.
141
+ * @throws If any inserted id is already known.
76
142
  */
77
143
  insertAfter(before, newId, count = 1) {
78
- if (this.isKnown(newId)) {
79
- throw new Error("newId is already known");
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
- // -1 so index + 1 is 0: insert at the beginning of the list.
84
- index = -1;
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
- else {
87
- index = this.state.findIndex((elt) => (0, id_1.equalsId)(elt.id, before));
88
- if (index === -1) {
89
- throw new Error("before is not known");
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
- this.state.splice(index + 1, 0, ...expandElements(newId, false, count));
93
- this._length += count;
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 (this.isKnown(newId)) {
111
- throw new Error("newId is already known");
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
- index = this.state.length;
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
- else {
118
- index = this.state.findIndex((elt) => (0, id_1.equalsId)(elt.id, after));
119
- if (index === -1) {
120
- throw new Error("after is not known");
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
- // We insert the bunch from left-to-right even though it's insertBefore.
124
- this.state.splice(index, 0, ...expandElements(newId, false, count));
125
- this._length += count;
126
- }
127
- /**
128
- * Un-inserts `id` from the list, making it no longer known or present in this list.
129
- *
130
- * Typically, you instead want to call {@link delete}, which marks `id` as deleted while
131
- * it remains known. That way, you can reference `id` in future insertAfter/insertBefore
132
- * operations, including ones sent concurrently by other devices.
133
- *
134
- * If `id` is already not known, this method does nothing.
135
- */
136
- uninsert(id) {
137
- const index = this.state.findIndex((elt) => (0, id_1.equalsId)(elt.id, id));
138
- if (index !== -1) {
139
- this.state.splice(index, 1);
140
- this._length--;
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. The id remains known (a "tombstone").
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
- * However, it does occupy space in memory (compressed in common cases).
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 elt = this.state.find((elt) => (0, id_1.equalsId)(elt.id, id));
157
- if (elt !== undefined && !elt.isDeleted) {
158
- elt.isDeleted = true;
159
- this._length--;
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
- * This is an exact inverse to {@link delete}.
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 elt = this.state.find((elt) => (0, id_1.equalsId)(elt.id, id));
172
- if (elt === undefined) {
347
+ const located = locate(id, this.root);
348
+ if (located === null) {
173
349
  throw new Error("id is not known");
174
350
  }
175
- if (elt.isDeleted) {
176
- elt.isDeleted = false;
177
- this._length++;
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 elt = this.state.find((elt) => (0, id_1.equalsId)(elt.id, id));
190
- if (elt === undefined)
376
+ const located = locate(id, this.root);
377
+ if (located === null)
191
378
  return false;
192
- return !elt.isDeleted;
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 this.state.some((elt) => (0, id_1.equalsId)(elt.id, id));
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
- for (const elt of this.state) {
213
- if (!elt.isDeleted) {
214
- if (remaining === 0)
215
- return elt.id;
216
- remaining--;
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
- for (const elt of this.state) {
239
- if ((0, id_1.equalsId)(elt.id, id)) {
240
- // Found it.
241
- if (elt.isDeleted) {
242
- switch (bias) {
243
- case "none":
244
- return -1;
245
- case "left":
246
- return index - 1;
247
- case "right":
248
- return index;
249
- }
250
- }
251
- else
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
- *[Symbol.iterator]() {
270
- for (const elt of this.state) {
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
- valuesWithDeleted() {
285
- return this.state.values();
504
+ valuesWithIsDeleted() {
505
+ return iterateNodeWithIsDeleted(this.root);
286
506
  }
287
507
  /**
288
- * Returns an independent copy of this list, including known but deleted ids.
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.state);
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 ans = [];
312
- for (const { id, isDeleted } of this.state) {
313
- if (ans.length !== 0) {
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}, __overwriting__ the current state of this list.
530
+ * Loads a saved state returned by {@link save}.
333
531
  */
334
- load(savedState) {
335
- this.state.length = 0;
336
- this._length = 0;
337
- for (const { bunchId, startCounter, count, isDeleted } of savedState) {
338
- if (!(Number.isSafeInteger(count) && count >= 0)) {
339
- throw new Error(`Invalid length: ${count}`);
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
- for (let i = 0; i < count; i++) {
342
- this.state.push({
343
- id: { bunchId, counter: startCounter + i },
344
- isDeleted,
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
- if (!isDeleted)
348
- this._length += count;
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 live-updating view of an IdList that treats all known ids as present.
355
- * That is, this class ignores the underlying list's isDeleted status when computing list indices.
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, call methods on the original IdList (`this.list`).
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, state) {
596
+ constructor(list, root) {
364
597
  this.list = list;
365
- this.state = state;
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
- return this.state[index].id;
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
- return this.state.findIndex((elt) => (0, id_1.equalsId)(elt.id, id));
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.state.length;
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
- *[Symbol.iterator]() {
401
- for (const elt of this.state) {
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
- function expandElements(startId, isDeleted, count) {
414
- if (!(Number.isSafeInteger(count) && count >= 0)) {
415
- throw new Error(`Invalid count: ${count}`);
416
- }
417
- const ans = [];
418
- for (let i = 0; i < count; i++) {
419
- ans.push({
420
- id: { bunchId: startId.bunchId, counter: startId.counter + i },
421
- isDeleted,
422
- });
423
- }
424
- return ans;
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