@fluidframework/merge-tree 2.3.0-288113 → 2.4.0-294316

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 (188) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/api-report/merge-tree.legacy.alpha.api.md +20 -2
  3. package/dist/attributionCollection.d.ts.map +1 -1
  4. package/dist/attributionCollection.js +1 -29
  5. package/dist/attributionCollection.js.map +1 -1
  6. package/dist/client.d.ts.map +1 -1
  7. package/dist/client.js +3 -5
  8. package/dist/client.js.map +1 -1
  9. package/dist/index.d.ts +1 -1
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/index.js.map +1 -1
  12. package/dist/legacy.d.ts +1 -0
  13. package/dist/localReference.d.ts.map +1 -1
  14. package/dist/localReference.js +0 -2
  15. package/dist/localReference.js.map +1 -1
  16. package/dist/mergeTree.d.ts +2 -32
  17. package/dist/mergeTree.d.ts.map +1 -1
  18. package/dist/mergeTree.js +137 -199
  19. package/dist/mergeTree.js.map +1 -1
  20. package/dist/mergeTreeNodes.d.ts +16 -10
  21. package/dist/mergeTreeNodes.d.ts.map +1 -1
  22. package/dist/mergeTreeNodes.js +5 -2
  23. package/dist/mergeTreeNodes.js.map +1 -1
  24. package/dist/partialLengths.d.ts.map +1 -1
  25. package/dist/partialLengths.js +8 -54
  26. package/dist/partialLengths.js.map +1 -1
  27. package/dist/properties.d.ts.map +1 -1
  28. package/dist/properties.js +0 -2
  29. package/dist/properties.js.map +1 -1
  30. package/dist/revertibles.d.ts.map +1 -1
  31. package/dist/revertibles.js +0 -14
  32. package/dist/revertibles.js.map +1 -1
  33. package/dist/segmentGroupCollection.d.ts.map +1 -1
  34. package/dist/segmentGroupCollection.js +0 -2
  35. package/dist/segmentGroupCollection.js.map +1 -1
  36. package/dist/segmentPropertiesManager.d.ts.map +1 -1
  37. package/dist/segmentPropertiesManager.js +1 -3
  38. package/dist/segmentPropertiesManager.js.map +1 -1
  39. package/dist/snapshotLoader.d.ts.map +1 -1
  40. package/dist/snapshotLoader.js +1 -4
  41. package/dist/snapshotLoader.js.map +1 -1
  42. package/dist/snapshotV1.d.ts.map +1 -1
  43. package/dist/snapshotV1.js +1 -11
  44. package/dist/snapshotV1.js.map +1 -1
  45. package/dist/snapshotlegacy.d.ts.map +1 -1
  46. package/dist/snapshotlegacy.js +0 -1
  47. package/dist/snapshotlegacy.js.map +1 -1
  48. package/dist/sortedSegmentSet.d.ts +0 -1
  49. package/dist/sortedSegmentSet.d.ts.map +1 -1
  50. package/dist/sortedSegmentSet.js +1 -9
  51. package/dist/sortedSegmentSet.js.map +1 -1
  52. package/dist/sortedSet.d.ts.map +1 -1
  53. package/dist/sortedSet.js +0 -4
  54. package/dist/sortedSet.js.map +1 -1
  55. package/dist/test/client.conflictFarm.spec.d.ts.map +1 -1
  56. package/dist/test/client.conflictFarm.spec.js +36 -27
  57. package/dist/test/client.conflictFarm.spec.js.map +1 -1
  58. package/dist/test/client.replay.spec.js +1 -1
  59. package/dist/test/client.replay.spec.js.map +1 -1
  60. package/dist/test/mergeTreeOperationRunner.d.ts +2 -1
  61. package/dist/test/mergeTreeOperationRunner.d.ts.map +1 -1
  62. package/dist/test/mergeTreeOperationRunner.js +29 -11
  63. package/dist/test/mergeTreeOperationRunner.js.map +1 -1
  64. package/dist/test/obliterate.partialLength.spec.js +8 -4
  65. package/dist/test/obliterate.partialLength.spec.js.map +1 -1
  66. package/dist/test/obliterate.spec.js +66 -5
  67. package/dist/test/obliterate.spec.js.map +1 -1
  68. package/dist/test/reconnectHelper.d.ts +0 -1
  69. package/dist/test/reconnectHelper.d.ts.map +1 -1
  70. package/dist/test/reconnectHelper.js +1 -1
  71. package/dist/test/reconnectHelper.js.map +1 -1
  72. package/dist/test/testClient.d.ts +1 -11
  73. package/dist/test/testClient.d.ts.map +1 -1
  74. package/dist/test/testClient.js +0 -3
  75. package/dist/test/testClient.js.map +1 -1
  76. package/dist/test/testClientLogger.d.ts.map +1 -1
  77. package/dist/test/testClientLogger.js +17 -7
  78. package/dist/test/testClientLogger.js.map +1 -1
  79. package/dist/test/testUtils.d.ts +10 -0
  80. package/dist/test/testUtils.d.ts.map +1 -1
  81. package/dist/test/testUtils.js +5 -1
  82. package/dist/test/testUtils.js.map +1 -1
  83. package/dist/zamboni.d.ts.map +1 -1
  84. package/dist/zamboni.js +0 -4
  85. package/dist/zamboni.js.map +1 -1
  86. package/lib/attributionCollection.d.ts.map +1 -1
  87. package/lib/attributionCollection.js +1 -29
  88. package/lib/attributionCollection.js.map +1 -1
  89. package/lib/client.d.ts.map +1 -1
  90. package/lib/client.js +3 -5
  91. package/lib/client.js.map +1 -1
  92. package/lib/index.d.ts +1 -1
  93. package/lib/index.d.ts.map +1 -1
  94. package/lib/index.js.map +1 -1
  95. package/lib/legacy.d.ts +1 -0
  96. package/lib/localReference.d.ts.map +1 -1
  97. package/lib/localReference.js +0 -2
  98. package/lib/localReference.js.map +1 -1
  99. package/lib/mergeTree.d.ts +2 -32
  100. package/lib/mergeTree.d.ts.map +1 -1
  101. package/lib/mergeTree.js +138 -200
  102. package/lib/mergeTree.js.map +1 -1
  103. package/lib/mergeTreeNodes.d.ts +16 -10
  104. package/lib/mergeTreeNodes.d.ts.map +1 -1
  105. package/lib/mergeTreeNodes.js +5 -2
  106. package/lib/mergeTreeNodes.js.map +1 -1
  107. package/lib/partialLengths.d.ts.map +1 -1
  108. package/lib/partialLengths.js +8 -54
  109. package/lib/partialLengths.js.map +1 -1
  110. package/lib/properties.d.ts.map +1 -1
  111. package/lib/properties.js +0 -2
  112. package/lib/properties.js.map +1 -1
  113. package/lib/revertibles.d.ts.map +1 -1
  114. package/lib/revertibles.js +0 -14
  115. package/lib/revertibles.js.map +1 -1
  116. package/lib/segmentGroupCollection.d.ts.map +1 -1
  117. package/lib/segmentGroupCollection.js +0 -2
  118. package/lib/segmentGroupCollection.js.map +1 -1
  119. package/lib/segmentPropertiesManager.d.ts.map +1 -1
  120. package/lib/segmentPropertiesManager.js +1 -3
  121. package/lib/segmentPropertiesManager.js.map +1 -1
  122. package/lib/snapshotLoader.d.ts.map +1 -1
  123. package/lib/snapshotLoader.js +1 -4
  124. package/lib/snapshotLoader.js.map +1 -1
  125. package/lib/snapshotV1.d.ts.map +1 -1
  126. package/lib/snapshotV1.js +1 -11
  127. package/lib/snapshotV1.js.map +1 -1
  128. package/lib/snapshotlegacy.d.ts.map +1 -1
  129. package/lib/snapshotlegacy.js +0 -1
  130. package/lib/snapshotlegacy.js.map +1 -1
  131. package/lib/sortedSegmentSet.d.ts +0 -1
  132. package/lib/sortedSegmentSet.d.ts.map +1 -1
  133. package/lib/sortedSegmentSet.js +1 -9
  134. package/lib/sortedSegmentSet.js.map +1 -1
  135. package/lib/sortedSet.d.ts.map +1 -1
  136. package/lib/sortedSet.js +0 -4
  137. package/lib/sortedSet.js.map +1 -1
  138. package/lib/test/client.conflictFarm.spec.d.ts.map +1 -1
  139. package/lib/test/client.conflictFarm.spec.js +37 -28
  140. package/lib/test/client.conflictFarm.spec.js.map +1 -1
  141. package/lib/test/client.replay.spec.js +1 -1
  142. package/lib/test/client.replay.spec.js.map +1 -1
  143. package/lib/test/mergeTreeOperationRunner.d.ts +2 -1
  144. package/lib/test/mergeTreeOperationRunner.d.ts.map +1 -1
  145. package/lib/test/mergeTreeOperationRunner.js +30 -12
  146. package/lib/test/mergeTreeOperationRunner.js.map +1 -1
  147. package/lib/test/obliterate.partialLength.spec.js +9 -5
  148. package/lib/test/obliterate.partialLength.spec.js.map +1 -1
  149. package/lib/test/obliterate.spec.js +67 -6
  150. package/lib/test/obliterate.spec.js.map +1 -1
  151. package/lib/test/reconnectHelper.d.ts +0 -1
  152. package/lib/test/reconnectHelper.d.ts.map +1 -1
  153. package/lib/test/reconnectHelper.js +1 -1
  154. package/lib/test/reconnectHelper.js.map +1 -1
  155. package/lib/test/testClient.d.ts +1 -11
  156. package/lib/test/testClient.d.ts.map +1 -1
  157. package/lib/test/testClient.js +0 -3
  158. package/lib/test/testClient.js.map +1 -1
  159. package/lib/test/testClientLogger.d.ts.map +1 -1
  160. package/lib/test/testClientLogger.js +18 -8
  161. package/lib/test/testClientLogger.js.map +1 -1
  162. package/lib/test/testUtils.d.ts +10 -0
  163. package/lib/test/testUtils.d.ts.map +1 -1
  164. package/lib/test/testUtils.js +3 -0
  165. package/lib/test/testUtils.js.map +1 -1
  166. package/lib/tsdoc-metadata.json +1 -1
  167. package/lib/zamboni.d.ts.map +1 -1
  168. package/lib/zamboni.js +0 -4
  169. package/lib/zamboni.js.map +1 -1
  170. package/package.json +22 -21
  171. package/src/attributionCollection.ts +14 -42
  172. package/src/client.ts +13 -11
  173. package/src/index.ts +1 -0
  174. package/src/localReference.ts +1 -3
  175. package/src/mergeTree.ts +188 -258
  176. package/src/mergeTreeNodes.ts +22 -12
  177. package/src/partialLengths.ts +23 -68
  178. package/src/properties.ts +1 -3
  179. package/src/revertibles.ts +7 -21
  180. package/src/segmentGroupCollection.ts +1 -3
  181. package/src/segmentPropertiesManager.ts +0 -1
  182. package/src/snapshotLoader.ts +2 -4
  183. package/src/snapshotV1.ts +5 -15
  184. package/src/snapshotlegacy.ts +1 -2
  185. package/src/sortedSegmentSet.ts +3 -10
  186. package/src/sortedSet.ts +2 -6
  187. package/src/zamboni.ts +4 -8
  188. package/tsconfig.json +1 -0
package/src/mergeTree.ts CHANGED
@@ -25,6 +25,7 @@ import {
25
25
  LocalReferencePosition,
26
26
  SlidingPreference,
27
27
  anyLocalReferencePosition,
28
+ createDetachedLocalReferencePosition,
28
29
  filterLocalReferencePositions,
29
30
  } from "./localReference.js";
30
31
  import {
@@ -62,6 +63,8 @@ import {
62
63
  seqLTE,
63
64
  toMoveInfo,
64
65
  toRemovalInfo,
66
+ // eslint-disable-next-line import/no-deprecated
67
+ type ObliterateInfo,
65
68
  } from "./mergeTreeNodes.js";
66
69
  import type { TrackingGroup } from "./mergeTreeTracking.js";
67
70
  import {
@@ -87,16 +90,9 @@ import {
87
90
  } from "./referencePositions.js";
88
91
  // eslint-disable-next-line import/no-deprecated
89
92
  import { PropertiesRollback } from "./segmentPropertiesManager.js";
90
- import { endpointPosAndSide, type SequencePlace } from "./sequencePlace.js";
93
+ import { SortedSegmentSet } from "./sortedSegmentSet.js";
91
94
  import { zamboniSegments } from "./zamboni.js";
92
95
 
93
- function wasRemovedAfter(seg: ISegment, seq: number): boolean {
94
- return (
95
- seg.removedSeq !== UnassignedSequenceNumber &&
96
- (seg.removedSeq === undefined || seg.removedSeq > seq)
97
- );
98
- }
99
-
100
96
  function markSegmentMoved(seg: ISegment, moveInfo: IMoveInfo): void {
101
97
  seg.moveDst = moveInfo.moveDst;
102
98
  seg.movedClientIds = [...moveInfo.movedClientIds];
@@ -414,6 +410,71 @@ const forwardPred = (ref: LocalReferencePosition): boolean =>
414
410
  const backwardPred = (ref: LocalReferencePosition): boolean =>
415
411
  ref.slidingPreference === SlidingPreference.BACKWARD;
416
412
 
413
+ class Obliterates {
414
+ /**
415
+ * Array containing the all move operations within the
416
+ * collab window.
417
+ *
418
+ * The moves are stored in sequence order which accelerates clean up in setMinSeq
419
+ *
420
+ * See https://github.com/microsoft/FluidFramework/blob/main/packages/dds/merge-tree/docs/Obliterate.md#remote-perspective
421
+ * for additional context
422
+ */
423
+ // eslint-disable-next-line import/no-deprecated
424
+ private readonly seqOrdered = new DoublyLinkedList<ObliterateInfo>();
425
+
426
+ /**
427
+ * This contains a sorted lists of all obliterate starts
428
+ * and is used to accelerate finding overlapping obliterates
429
+ * as well as determining if there are any obliterates at all.
430
+ */
431
+ private readonly startOrdered = new SortedSegmentSet<LocalReferencePosition>();
432
+
433
+ constructor(private readonly mergeTree: MergeTree) {}
434
+
435
+ public setMinSeq(minSeq: number): void {
436
+ // eslint-disable-next-line @typescript-eslint/no-non-null-asserted-optional-chain
437
+ while (!this.seqOrdered.empty && this.seqOrdered.first?.data.seq! <= minSeq) {
438
+ const ob = this.seqOrdered.shift()!;
439
+ this.startOrdered.remove(ob.data.start);
440
+ this.mergeTree.removeLocalReferencePosition(ob.data.start);
441
+ this.mergeTree.removeLocalReferencePosition(ob.data.end);
442
+ }
443
+ }
444
+
445
+ // eslint-disable-next-line import/no-deprecated
446
+ public addOrUpdate(obliterateInfo: ObliterateInfo): void {
447
+ const { seq, start } = obliterateInfo;
448
+ if (seq !== UnassignedSequenceNumber) {
449
+ this.seqOrdered.push(obliterateInfo);
450
+ }
451
+ this.startOrdered.addOrUpdate(start);
452
+ }
453
+
454
+ public empty(): boolean {
455
+ return this.startOrdered.size === 0;
456
+ }
457
+
458
+ // eslint-disable-next-line import/no-deprecated
459
+ public findOverlapping(seg: ISegment): Iterable<ObliterateInfo> {
460
+ // eslint-disable-next-line import/no-deprecated
461
+ const overlapping: ObliterateInfo[] = [];
462
+ for (const start of this.startOrdered.items) {
463
+ if (start.getSegment()!.ordinal <= seg.ordinal) {
464
+ // eslint-disable-next-line import/no-deprecated
465
+ const ob = start.properties?.obliterate as ObliterateInfo;
466
+ if (ob.end.getSegment()!.ordinal >= seg.ordinal) {
467
+ overlapping.push(ob);
468
+ }
469
+ } else {
470
+ // the start is past the seg, so exit
471
+ break;
472
+ }
473
+ }
474
+ return overlapping;
475
+ }
476
+ }
477
+
417
478
  /**
418
479
  * @internal
419
480
  */
@@ -448,38 +509,7 @@ export class MergeTree {
448
509
  public mergeTreeDeltaCallback?: MergeTreeDeltaCallback;
449
510
  public mergeTreeMaintenanceCallback?: MergeTreeMaintenanceCallback;
450
511
 
451
- /**
452
- * Array containing the sequence number of all move operations within the
453
- * collab window
454
- *
455
- * When a segment is inserted, we must traverse to the left and right of it
456
- * to determine whether the segment was inserted into an obliterated range.
457
- * By keeping track of all move seqs, we can significantly reduce the search
458
- * space we must traverse.
459
- *
460
- * Sequence numbers in `moveSeqs` are sorted to accelerate bookkeeping.
461
- *
462
- * See https://github.com/microsoft/FluidFramework/blob/main/packages/dds/merge-tree/docs/Obliterate.md#remote-perspective
463
- * for additional context
464
- */
465
- private moveSeqs: number[] = [];
466
-
467
- /**
468
- * Similar to moveSeqs, but tracks local moves. These are not the move
469
- * operations within the collab window, but rather local moves that have
470
- * not been acked.
471
- */
472
- private readonly localMoveSeqs: Set<number> = new Set();
473
-
474
- /**
475
- * Groups of segments moved by local moves/obliterates
476
- *
477
- * When a local obliterate is acked, we must also ack segments that were
478
- * concurrently obliterated on insert. We check this segment group to find
479
- * such segments
480
- */
481
- // eslint-disable-next-line import/no-deprecated
482
- private readonly locallyMovedSegments: Map<number, SegmentGroup> = new Map();
512
+ private readonly obliterates = new Obliterates(this);
483
513
 
484
514
  public constructor(public options?: IMergeTreeOptions) {
485
515
  this._root = this.makeBlock(0);
@@ -617,8 +647,7 @@ export class MergeTree {
617
647
  childIndex++, nodeIndex++ // Advance to next child & node
618
648
  ) {
619
649
  // Insert the next node into the current block
620
- // TODO Non null asserting, why is this not null?
621
- this.addNode(block, nodes[nodeIndex]!);
650
+ this.addNode(block, nodes[nodeIndex]);
622
651
  }
623
652
 
624
653
  // Calculate this block's info. Previously this was inlined into the above loop as a micro-optimization,
@@ -628,8 +657,7 @@ export class MergeTree {
628
657
  }
629
658
 
630
659
  return blocks.length === 1 // If there is only one block at this layer...
631
- ? // Non null asserting here because of the length check above
632
- blocks[0]! // ...then we're done. Return the root.
660
+ ? blocks[0] // ...then we're done. Return the root.
633
661
  : buildMergeBlock(blocks); // ...otherwise recursively build the next layer above blocks.
634
662
  };
635
663
  if (segments.length > 0) {
@@ -688,8 +716,7 @@ export class MergeTree {
688
716
  while (parent) {
689
717
  const children = parent.children;
690
718
  for (let childIndex = 0; childIndex < parent.childCount; childIndex++) {
691
- // TODO Non null asserting, why is this not null?
692
- const child = children[childIndex]!;
719
+ const child = children[childIndex];
693
720
  if ((!!prevParent && child === prevParent) || child === node) {
694
721
  break;
695
722
  }
@@ -1052,8 +1079,7 @@ export class MergeTree {
1052
1079
 
1053
1080
  if (minSeq > this.collabWindow.minSeq) {
1054
1081
  this.collabWindow.minSeq = minSeq;
1055
- const firstMoveSeqIdx = this.moveSeqs.findIndex((seq) => seq >= minSeq);
1056
- this.moveSeqs = firstMoveSeqIdx === -1 ? [] : this.moveSeqs.slice(firstMoveSeqIdx);
1082
+ this.obliterates.setMinSeq(minSeq);
1057
1083
  if (MergeTree.options.zamboniSegments) {
1058
1084
  zamboniSegments(this);
1059
1085
  }
@@ -1192,42 +1218,10 @@ export class MergeTree {
1192
1218
  const deltaSegments: IMergeTreeSegmentDelta[] = [];
1193
1219
  const overlappingRemoves: boolean[] = [];
1194
1220
  pendingSegmentGroup.segments.map((pendingSegment: ISegmentLeaf) => {
1195
- const localMovedSeq = pendingSegment.localMovedSeq;
1196
1221
  const overlappingRemove = !pendingSegment.ack(pendingSegmentGroup, opArgs);
1197
1222
 
1198
- if (opArgs.op.type === MergeTreeDeltaType.OBLITERATE && localMovedSeq !== undefined) {
1199
- const locallyMovedSegments = this.locallyMovedSegments.get(localMovedSeq);
1200
-
1201
- if (locallyMovedSegments) {
1202
- // Disabling because a for of loop causes the type of segment to be ISegment, which does not have parent information stored
1203
- // eslint-disable-next-line unicorn/no-array-for-each
1204
- locallyMovedSegments.segments.forEach((segment: ISegmentLeaf) => {
1205
- segment.localMovedSeq = undefined;
1206
-
1207
- if (!nodesToUpdate.includes(segment.parent!)) {
1208
- nodesToUpdate.push(segment.parent!);
1209
- }
1210
-
1211
- if (segment.movedSeq === UnassignedSequenceNumber) {
1212
- segment.movedSeq = seq;
1213
- }
1214
- });
1215
-
1216
- this.locallyMovedSegments.delete(localMovedSeq);
1217
- }
1218
- }
1219
-
1220
1223
  overwrite = overlappingRemove || overwrite;
1221
1224
 
1222
- if (opArgs.op.type === MergeTreeDeltaType.OBLITERATE) {
1223
- if (seq !== this.moveSeqs[this.moveSeqs.length - 1]) {
1224
- this.moveSeqs.push(seq);
1225
- }
1226
- if (localMovedSeq !== undefined) {
1227
- this.localMoveSeqs.delete(localMovedSeq);
1228
- }
1229
- }
1230
-
1231
1225
  overlappingRemoves.push(overlappingRemove);
1232
1226
  if (MergeTree.options.zamboniSegments) {
1233
1227
  this.addToLRUSet(pendingSegment, seq);
@@ -1240,6 +1234,10 @@ export class MergeTree {
1240
1234
  });
1241
1235
  });
1242
1236
 
1237
+ if (opArgs.op.type === MergeTreeDeltaType.OBLITERATE) {
1238
+ this.obliterates.addOrUpdate(pendingSegmentGroup.obliterateInfo!);
1239
+ }
1240
+
1243
1241
  // Perform slides after all segments have been acked, so that
1244
1242
  // positions after slide are final
1245
1243
  if (
@@ -1516,115 +1514,62 @@ export class MergeTree {
1516
1514
 
1517
1515
  insertPos += newSegment.cachedLength;
1518
1516
 
1519
- if (!this.options?.mergeTreeEnableObliterate) {
1517
+ if (!this.options?.mergeTreeEnableObliterate || this.obliterates.empty()) {
1520
1518
  continue;
1521
1519
  }
1522
1520
 
1523
- let moveUpperBound = Number.POSITIVE_INFINITY;
1524
- const smallestSeqMoveOp = this.getSmallestSeqMoveOp();
1525
-
1526
- if (smallestSeqMoveOp === undefined) {
1527
- continue;
1528
- }
1529
-
1530
- const leftAckedSegments: Record<number, ISegment> = {};
1531
- const leftLocalSegments: Record<number, ISegment> = {};
1532
-
1533
- let _localMovedSeq: number | undefined;
1534
- let _movedSeq: number | undefined;
1535
- let movedClientIds: number[] | undefined;
1536
-
1537
- const findLeftMovedSegment = (seg: ISegment): boolean => {
1538
- const movedSeqs = seg.movedSeqs?.filter((movedSeq) => movedSeq >= refSeq) ?? [];
1539
- const localMovedSeqs = seg.localMovedSeq ? [seg.localMovedSeq] : [];
1540
- for (const movedSeq of movedSeqs) {
1541
- leftAckedSegments[movedSeq] = seg;
1542
- }
1543
-
1544
- for (const localMovedSeq of localMovedSeqs) {
1545
- leftLocalSegments[localMovedSeq] = seg;
1546
- }
1547
-
1548
- if ((seg.movedSeqs?.length ?? 0) > 0 || localMovedSeqs.length > 0) {
1549
- return true;
1550
- }
1551
-
1552
- if (!isRemoved(seg) || wasRemovedAfter(seg, moveUpperBound)) {
1553
- moveUpperBound = Math.min(moveUpperBound, seg.seq ?? Number.POSITIVE_INFINITY);
1554
- }
1555
- // If we've reached a segment that existed before any of our in-collab-window move ops
1556
- // happened, no need to continue.
1557
- return moveUpperBound >= smallestSeqMoveOp;
1558
- };
1559
-
1560
- const findRightMovedSegment = (seg: ISegment): boolean => {
1561
- const movedSeqs = seg.movedSeqs?.filter((movedSeq) => movedSeq >= refSeq) ?? [];
1562
- const localMovedSeqs = seg.localMovedSeq ? [seg.localMovedSeq] : [];
1563
-
1564
- for (const movedSeq of movedSeqs) {
1565
- const left = leftAckedSegments[movedSeq];
1566
- if (left) {
1567
- _movedSeq = movedSeq;
1568
- const clientIdIdx = left.movedSeqs?.indexOf(movedSeq) ?? -1;
1569
- const movedClientId = left.movedClientIds?.[clientIdIdx];
1570
- assert(movedClientId !== undefined, 0x869 /* expected client id to exist */);
1571
- movedClientIds = [movedClientId];
1572
- return false;
1573
- }
1574
- }
1575
-
1576
- for (const localMovedSeq of localMovedSeqs) {
1577
- const left = leftLocalSegments[localMovedSeq];
1578
- if (left) {
1579
- _localMovedSeq = localMovedSeq;
1580
- const clientIdIdx = left.movedSeqs?.indexOf(UnassignedSequenceNumber) ?? -1;
1581
- const movedClientId = left.movedClientIds?.[clientIdIdx];
1582
- assert(movedClientId !== undefined, 0x86a /* expected client id to exist */);
1583
- movedClientIds = [movedClientId];
1584
- return false;
1521
+ // eslint-disable-next-line import/no-deprecated
1522
+ let oldest: ObliterateInfo | undefined;
1523
+ let normalizedOldestSeq: number = 0;
1524
+ // eslint-disable-next-line import/no-deprecated
1525
+ let newest: ObliterateInfo | undefined;
1526
+ let normalizedNewestSeq: number = 0;
1527
+ const movedClientIds: number[] = [];
1528
+ const movedSeqs: number[] = [];
1529
+ for (const ob of this.obliterates.findOverlapping(newSegment)) {
1530
+ // compute a normalized seq that takes into account local seqs
1531
+ // but is still comparable to remote seqs to keep the checks below easy
1532
+ // REMOTE SEQUENCE NUMBERS LOCAL SEQUENCE NUMBERS
1533
+ // [0, 1, 2, 3, ..., 100, ..., 1000, ..., (MAX - MaxLocalSeq), L1, L2, L3, L4, ..., L100, ..., L1000, ...(MAX)]
1534
+ const normalizedObSeq =
1535
+ ob.seq === UnassignedSequenceNumber
1536
+ ? Number.MAX_SAFE_INTEGER - this.collabWindow.localSeq + ob.localSeq!
1537
+ : ob.seq;
1538
+ if (normalizedObSeq > refSeq) {
1539
+ if (oldest === undefined || normalizedOldestSeq > normalizedObSeq) {
1540
+ normalizedOldestSeq = normalizedObSeq;
1541
+ oldest = ob;
1542
+ movedClientIds.unshift(ob.clientId);
1543
+ movedSeqs.unshift(ob.seq);
1544
+ } else {
1545
+ if (newest === undefined || normalizedNewestSeq < normalizedObSeq) {
1546
+ normalizedNewestSeq = normalizedObSeq;
1547
+ newest = ob;
1548
+ }
1549
+ movedClientIds.push(ob.clientId);
1550
+ movedSeqs.push(ob.seq);
1585
1551
  }
1586
1552
  }
1553
+ }
1587
1554
 
1588
- if ((seg.movedSeqs?.length ?? 0) || localMovedSeqs.length > 0) {
1589
- return true;
1590
- }
1591
-
1592
- if (!isRemoved(seg) || wasRemovedAfter(seg, moveUpperBound)) {
1593
- moveUpperBound = Math.min(moveUpperBound, seg.seq ?? Number.POSITIVE_INFINITY);
1594
- }
1595
- // If we've reached a segment that existed before any of our in-collab-window move ops
1596
- // happened, no need to continue.
1597
- return moveUpperBound >= smallestSeqMoveOp;
1598
- };
1599
-
1600
- backwardExcursion(newSegment, findLeftMovedSegment);
1601
- moveUpperBound = Number.POSITIVE_INFINITY;
1602
- forwardExcursion(newSegment, findRightMovedSegment);
1603
-
1604
- if (_localMovedSeq !== undefined || _movedSeq !== undefined) {
1605
- assert(
1606
- movedClientIds !== undefined,
1607
- 0x86b /* movedClientIds should be set if local/moved seq is set */,
1608
- );
1609
- const moveInfo = {
1555
+ if (oldest && newest?.clientId !== clientId) {
1556
+ const moveInfo: IMoveInfo = {
1610
1557
  movedClientIds,
1611
- movedSeq: _movedSeq ?? UnassignedSequenceNumber,
1612
- movedSeqs: _movedSeq === undefined ? [UnassignedSequenceNumber] : [_movedSeq],
1613
- localMovedSeq: _localMovedSeq,
1614
- wasMovedOnInsert: (_movedSeq ?? -1) !== UnassignedSequenceNumber,
1558
+ movedSeq: oldest.seq,
1559
+ movedSeqs,
1560
+ localMovedSeq: oldest.localSeq,
1561
+ wasMovedOnInsert: oldest.seq !== UnassignedSequenceNumber,
1615
1562
  };
1616
1563
 
1617
1564
  markSegmentMoved(newSegment, moveInfo);
1618
1565
 
1619
1566
  if (moveInfo.localMovedSeq !== undefined) {
1620
- const movedSegmentGroup = this.locallyMovedSegments.get(moveInfo.localMovedSeq);
1621
-
1622
1567
  assert(
1623
- movedSegmentGroup !== undefined,
1568
+ oldest.segmentGroup !== undefined,
1624
1569
  0x86c /* expected segment group to exist */,
1625
1570
  );
1626
1571
 
1627
- this.addToPendingList(newSegment, movedSegmentGroup, localSeq);
1572
+ this.addToPendingList(newSegment, oldest.segmentGroup);
1628
1573
  }
1629
1574
 
1630
1575
  if (newSegment.parent) {
@@ -1655,11 +1600,7 @@ export class MergeTree {
1655
1600
  return { next };
1656
1601
  };
1657
1602
 
1658
- private ensureIntervalBoundary(
1659
- pos: number | "start" | "end",
1660
- refSeq: number,
1661
- clientId: number,
1662
- ): void {
1603
+ private ensureIntervalBoundary(pos: number, refSeq: number, clientId: number): void {
1663
1604
  const splitNode = this.insertingWalk(
1664
1605
  this.root,
1665
1606
  pos,
@@ -1701,27 +1642,16 @@ export class MergeTree {
1701
1642
  }
1702
1643
  }
1703
1644
 
1704
- private getSmallestSeqMoveOp(): number | undefined {
1705
- return this.moveSeqs[0] ?? (this.localMoveSeqs.size > 0 ? -1 : undefined);
1706
- }
1707
-
1708
1645
  private insertingWalk(
1709
1646
  block: MergeBlock,
1710
- pos: number | "start" | "end",
1647
+ pos: number,
1711
1648
  refSeq: number,
1712
1649
  clientId: number,
1713
1650
  seq: number,
1714
1651
  context: InsertContext,
1715
1652
  isLastChildBlock: boolean = true,
1716
1653
  ): MergeBlock | undefined {
1717
- let _pos: number;
1718
- if (pos === "start") {
1719
- _pos = 0;
1720
- } else if (pos === "end") {
1721
- _pos = this.root.mergeTree?.getLength(refSeq, clientId) ?? 0;
1722
- } else {
1723
- _pos = pos;
1724
- }
1654
+ let _pos: number = pos;
1725
1655
 
1726
1656
  const children = block.children;
1727
1657
  let childIndex: number;
@@ -1729,8 +1659,7 @@ export class MergeTree {
1729
1659
  let newNode: IMergeNode | undefined;
1730
1660
  let fromSplit: MergeBlock | undefined;
1731
1661
  for (childIndex = 0; childIndex < block.childCount; childIndex++) {
1732
- // TODO Non null asserting, why is this not null?
1733
- child = children[childIndex]!;
1662
+ child = children[childIndex];
1734
1663
  // ensure we walk down the far edge of the tree, even if all sub-tree is eligible for zamboni
1735
1664
  const isLastNonLeafBlock =
1736
1665
  isLastChildBlock && !child.isLeaf() && childIndex === block.childCount - 1;
@@ -1801,10 +1730,8 @@ export class MergeTree {
1801
1730
  }
1802
1731
  if (newNode) {
1803
1732
  for (let i = block.childCount; i > childIndex; i--) {
1804
- // TODO Non null asserting, why is this not null?
1805
- block.children[i] = block.children[i - 1]!;
1806
- // TODO Non null asserting, why is this not null?
1807
- block.children[i]!.index = i;
1733
+ block.children[i] = block.children[i - 1];
1734
+ block.children[i].index = i;
1808
1735
  }
1809
1736
  block.assignChild(newNode, childIndex, false);
1810
1737
  block.childCount++;
@@ -1841,8 +1768,7 @@ export class MergeTree {
1841
1768
  // Update ordinals to reflect lowered child count
1842
1769
  this.nodeUpdateOrdinals(node);
1843
1770
  for (let i = 0; i < halfCount; i++) {
1844
- // TODO Non null asserting, why is this not null?
1845
- newNode.assignChild(node.children[halfCount + i]!, i, false);
1771
+ newNode.assignChild(node.children[halfCount + i], i, false);
1846
1772
  node.children[halfCount + i] = undefined!;
1847
1773
  }
1848
1774
  this.nodeUpdateLengthNewStructure(node);
@@ -1852,8 +1778,7 @@ export class MergeTree {
1852
1778
 
1853
1779
  public nodeUpdateOrdinals(block: MergeBlock): void {
1854
1780
  for (let i = 0; i < block.childCount; i++) {
1855
- // TODO Non null asserting, why is this not null?
1856
- const child = block.children[i]!;
1781
+ const child = block.children[i];
1857
1782
  block.setOrdinal(child, i);
1858
1783
  if (!child.isLeaf()) {
1859
1784
  this.nodeUpdateOrdinals(child);
@@ -1942,8 +1867,8 @@ export class MergeTree {
1942
1867
  }
1943
1868
 
1944
1869
  public obliterateRange(
1945
- start: SequencePlace,
1946
- end: SequencePlace,
1870
+ start: number,
1871
+ end: number,
1947
1872
  refSeq: number,
1948
1873
  clientId: number,
1949
1874
  seq: number,
@@ -1952,33 +1877,50 @@ export class MergeTree {
1952
1877
  ): void {
1953
1878
  errorIfOptionNotTrue(this.options, "mergeTreeEnableObliterate");
1954
1879
 
1955
- const { startPos, startSide, endPos, endSide } = endpointPosAndSide(start, end);
1956
-
1957
- assert(
1958
- startPos !== undefined &&
1959
- endPos !== undefined &&
1960
- startSide !== undefined &&
1961
- endSide !== undefined &&
1962
- startPos !== "end" &&
1963
- endPos !== "start",
1964
- 0x9e2 /* start and end cannot be undefined because they were not passed in as undefined */,
1965
- );
1966
-
1967
- this.ensureIntervalBoundary(startPos, refSeq, clientId);
1968
- this.ensureIntervalBoundary(endPos, refSeq, clientId);
1880
+ this.ensureIntervalBoundary(start, refSeq, clientId);
1881
+ this.ensureIntervalBoundary(end, refSeq, clientId);
1969
1882
 
1970
1883
  let _overwrite = overwrite;
1971
1884
  const localOverlapWithRefs: ISegment[] = [];
1972
1885
  const movedSegments: IMergeTreeSegmentDelta[] = [];
1973
1886
  const localSeq =
1974
1887
  seq === UnassignedSequenceNumber ? ++this.collabWindow.localSeq : undefined;
1975
- if (seq !== UnassignedSequenceNumber && seq !== this.moveSeqs[this.moveSeqs.length - 1]) {
1976
- this.moveSeqs.push(seq);
1977
- } else if (seq === UnassignedSequenceNumber && localSeq !== undefined) {
1978
- this.localMoveSeqs.add(localSeq);
1979
- }
1980
1888
  // eslint-disable-next-line import/no-deprecated
1981
- let segmentGroup: SegmentGroup;
1889
+ const obliterate: ObliterateInfo = {
1890
+ clientId,
1891
+ end: createDetachedLocalReferencePosition(undefined),
1892
+ refSeq,
1893
+ seq,
1894
+ start: createDetachedLocalReferencePosition(undefined),
1895
+ localSeq,
1896
+ segmentGroup: undefined,
1897
+ };
1898
+
1899
+ const { segment: startSeg } = this.getContainingSegment(start, refSeq, clientId);
1900
+ const { segment: endSeg } = this.getContainingSegment(end - 1, refSeq, clientId);
1901
+ assert(
1902
+ startSeg !== undefined && endSeg !== undefined,
1903
+ 0xa3f /* segments cannot be undefined */,
1904
+ );
1905
+
1906
+ obliterate.start = this.createLocalReferencePosition(
1907
+ startSeg,
1908
+ 0,
1909
+ ReferenceType.StayOnRemove,
1910
+ {
1911
+ obliterate,
1912
+ },
1913
+ );
1914
+
1915
+ obliterate.end = this.createLocalReferencePosition(
1916
+ endSeg,
1917
+ endSeg.cachedLength - 1,
1918
+ ReferenceType.StayOnRemove,
1919
+ {
1920
+ obliterate,
1921
+ },
1922
+ );
1923
+
1982
1924
  const markMoved = (
1983
1925
  segment: ISegment,
1984
1926
  pos: number,
@@ -1986,9 +1928,6 @@ export class MergeTree {
1986
1928
  _end: number,
1987
1929
  ): boolean => {
1988
1930
  const existingMoveInfo = toMoveInfo(segment);
1989
- if (startSide) segment.startSide = startSide;
1990
- if (endSide) segment.endSide = endSide;
1991
-
1992
1931
  if (
1993
1932
  clientId !== segment.clientId &&
1994
1933
  segment.seq !== undefined &&
@@ -2034,7 +1973,12 @@ export class MergeTree {
2034
1973
  segment.movedSeq === UnassignedSequenceNumber &&
2035
1974
  clientId === this.collabWindow.clientId
2036
1975
  ) {
2037
- segmentGroup = this.addToPendingList(segment, segmentGroup, localSeq);
1976
+ obliterate.segmentGroup = this.addToPendingList(
1977
+ segment,
1978
+ obliterate.segmentGroup,
1979
+ localSeq,
1980
+ );
1981
+ obliterate.segmentGroup.obliterateInfo ??= obliterate;
2038
1982
  } else {
2039
1983
  if (MergeTree.options.zamboniSegments) {
2040
1984
  this.addToLRUSet(segment, seq);
@@ -2070,6 +2014,8 @@ export class MergeTree {
2070
2014
  seq === UnassignedSequenceNumber ? undefined : seq,
2071
2015
  );
2072
2016
 
2017
+ this.obliterates.addOrUpdate(obliterate);
2018
+
2073
2019
  this.slideAckedRemovedSegmentReferences(localOverlapWithRefs);
2074
2020
  // opArgs == undefined => test code
2075
2021
  if (movedSegments.length > 0) {
@@ -2079,10 +2025,6 @@ export class MergeTree {
2079
2025
  });
2080
2026
  }
2081
2027
 
2082
- if (segmentGroup! && localSeq !== undefined) {
2083
- this.locallyMovedSegments.set(localSeq, segmentGroup);
2084
- }
2085
-
2086
2028
  // these events are newly removed
2087
2029
  // so we slide after eventing in case the consumer wants to make reference
2088
2030
  // changes at remove time, like add a ref to track undo redo.
@@ -2292,8 +2234,7 @@ export class MergeTree {
2292
2234
  { op: removeOp },
2293
2235
  );
2294
2236
  } /* op.type === MergeTreeDeltaType.ANNOTATE */ else {
2295
- // TODO Non null asserting, why is this not null?
2296
- const props = pendingSegmentGroup.previousProps![i]!;
2237
+ const props = pendingSegmentGroup.previousProps![i];
2297
2238
  const annotateOp = createAnnotateRangeOp(start, start + segment.cachedLength, props);
2298
2239
  this.annotateRange(
2299
2240
  start,
@@ -2366,7 +2307,10 @@ export class MergeTree {
2366
2307
  _segment !== "start" &&
2367
2308
  _segment !== "end" &&
2368
2309
  isRemovedAndAckedOrMovedAndAcked(_segment) &&
2369
- !refTypeIncludesFlag(refType, ReferenceType.SlideOnRemove | ReferenceType.Transient) &&
2310
+ !refTypeIncludesFlag(
2311
+ refType,
2312
+ ReferenceType.SlideOnRemove | ReferenceType.Transient | ReferenceType.StayOnRemove,
2313
+ ) &&
2370
2314
  _segment.endpointType === undefined
2371
2315
  ) {
2372
2316
  throw new UsageError(
@@ -2466,9 +2410,8 @@ export class MergeTree {
2466
2410
  }
2467
2411
 
2468
2412
  for (let i = 0; i < newOrder.length; i++) {
2469
- // TODO Non null asserting, why is this not null?
2470
- const seg = newOrder[i]!;
2471
- const { parent, index, ordinal } = currentOrder[i]!;
2413
+ const seg = newOrder[i];
2414
+ const { parent, index, ordinal } = currentOrder[i];
2472
2415
  parent?.assignChild(seg, index, false);
2473
2416
  seg.ordinal = ordinal;
2474
2417
  }
@@ -2561,8 +2504,7 @@ export class MergeTree {
2561
2504
  const leftmostTiles = createMap<Marker>();
2562
2505
 
2563
2506
  for (let i = 0; i < block.childCount; i++) {
2564
- // TODO Non null asserting, why is this not null?
2565
- const node = block.children[i]!;
2507
+ const node = block.children[i];
2566
2508
  const nodeLength = nodeTotalLength(this, node);
2567
2509
  if (nodeLength !== undefined) {
2568
2510
  len ??= 0;
@@ -2707,28 +2649,18 @@ export class MergeTree {
2707
2649
  leaf: ISegmentAction<TClientData>,
2708
2650
  accum: TClientData,
2709
2651
  post?: BlockAction<TClientData>,
2710
- start: SequencePlace = 0,
2711
- end?: SequencePlace,
2652
+ start: number = 0,
2653
+ end?: number,
2712
2654
  localSeq?: number,
2713
2655
  visibilitySeq: number = refSeq,
2714
2656
  ): void {
2715
- const maybeEndPos = end ?? this.nodeLength(this.root, refSeq, clientId, localSeq) ?? 0;
2716
- if (maybeEndPos === start) {
2657
+ const endPos = end ?? this.nodeLength(this.root, refSeq, clientId, localSeq) ?? 0;
2658
+ if (endPos === start) {
2717
2659
  return;
2718
2660
  }
2719
2661
 
2720
2662
  let pos = 0;
2721
- let { startPos, endPos } = endpointPosAndSide(start, end);
2722
2663
 
2723
- startPos = startPos === "start" || startPos === undefined ? 0 : startPos;
2724
- endPos =
2725
- endPos === "end" || endPos === undefined
2726
- ? this.root.mergeTree?.getLength(refSeq, clientId) ?? 0
2727
- : endPos;
2728
- assert(
2729
- startPos !== "end" && endPos !== "start",
2730
- 0x9e3 /* start cannot be 'end' and end cannot be 'start' */,
2731
- );
2732
2664
  depthFirstNodeWalk(
2733
2665
  this.root,
2734
2666
  this.root.children[0],
@@ -2756,15 +2688,13 @@ export class MergeTree {
2756
2688
 
2757
2689
  const nextPos = pos + lenAtRefSeq;
2758
2690
  // start is beyond the current node, so we can skip it
2759
- if (typeof startPos === "number" && startPos >= nextPos) {
2691
+ if (start >= nextPos) {
2760
2692
  pos = nextPos;
2761
2693
  return NodeAction.Skip;
2762
2694
  }
2763
2695
 
2764
2696
  if (node.isLeaf()) {
2765
- if (
2766
- leaf(node, pos, refSeq, clientId, startPos - pos, endPos - pos, accum) === false
2767
- ) {
2697
+ if (leaf(node, pos, refSeq, clientId, start - pos, endPos - pos, accum) === false) {
2768
2698
  return NodeAction.Exit;
2769
2699
  }
2770
2700
  pos = nextPos;
@@ -2774,7 +2704,7 @@ export class MergeTree {
2774
2704
  post === undefined
2775
2705
  ? undefined
2776
2706
  : (block): boolean =>
2777
- post(block, pos, refSeq, clientId, startPos - pos, endPos - pos, accum),
2707
+ post(block, pos, refSeq, clientId, start - pos, endPos - pos, accum),
2778
2708
  );
2779
2709
  }
2780
2710
  }