@fluidframework/merge-tree 2.2.0 → 2.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 (168) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/api-report/merge-tree.legacy.alpha.api.md +20 -0
  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 -4
  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 +1 -30
  17. package/dist/mergeTree.d.ts.map +1 -1
  18. package/dist/mergeTree.js +131 -167
  19. package/dist/mergeTree.js.map +1 -1
  20. package/dist/mergeTreeNodes.d.ts +16 -1
  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.spec.js +55 -0
  65. package/dist/test/obliterate.spec.js.map +1 -1
  66. package/dist/test/reconnectHelper.d.ts +0 -1
  67. package/dist/test/reconnectHelper.d.ts.map +1 -1
  68. package/dist/test/reconnectHelper.js +1 -1
  69. package/dist/test/reconnectHelper.js.map +1 -1
  70. package/dist/test/testClientLogger.d.ts.map +1 -1
  71. package/dist/test/testClientLogger.js +17 -7
  72. package/dist/test/testClientLogger.js.map +1 -1
  73. package/dist/zamboni.d.ts.map +1 -1
  74. package/dist/zamboni.js +0 -4
  75. package/dist/zamboni.js.map +1 -1
  76. package/lib/attributionCollection.d.ts.map +1 -1
  77. package/lib/attributionCollection.js +1 -29
  78. package/lib/attributionCollection.js.map +1 -1
  79. package/lib/client.d.ts.map +1 -1
  80. package/lib/client.js +3 -4
  81. package/lib/client.js.map +1 -1
  82. package/lib/index.d.ts +1 -1
  83. package/lib/index.d.ts.map +1 -1
  84. package/lib/index.js.map +1 -1
  85. package/lib/legacy.d.ts +1 -0
  86. package/lib/localReference.d.ts.map +1 -1
  87. package/lib/localReference.js +0 -2
  88. package/lib/localReference.js.map +1 -1
  89. package/lib/mergeTree.d.ts +1 -30
  90. package/lib/mergeTree.d.ts.map +1 -1
  91. package/lib/mergeTree.js +132 -168
  92. package/lib/mergeTree.js.map +1 -1
  93. package/lib/mergeTreeNodes.d.ts +16 -1
  94. package/lib/mergeTreeNodes.d.ts.map +1 -1
  95. package/lib/mergeTreeNodes.js +5 -2
  96. package/lib/mergeTreeNodes.js.map +1 -1
  97. package/lib/partialLengths.d.ts.map +1 -1
  98. package/lib/partialLengths.js +8 -54
  99. package/lib/partialLengths.js.map +1 -1
  100. package/lib/properties.d.ts.map +1 -1
  101. package/lib/properties.js +0 -2
  102. package/lib/properties.js.map +1 -1
  103. package/lib/revertibles.d.ts.map +1 -1
  104. package/lib/revertibles.js +0 -14
  105. package/lib/revertibles.js.map +1 -1
  106. package/lib/segmentGroupCollection.d.ts.map +1 -1
  107. package/lib/segmentGroupCollection.js +0 -2
  108. package/lib/segmentGroupCollection.js.map +1 -1
  109. package/lib/segmentPropertiesManager.d.ts.map +1 -1
  110. package/lib/segmentPropertiesManager.js +1 -3
  111. package/lib/segmentPropertiesManager.js.map +1 -1
  112. package/lib/snapshotLoader.d.ts.map +1 -1
  113. package/lib/snapshotLoader.js +1 -4
  114. package/lib/snapshotLoader.js.map +1 -1
  115. package/lib/snapshotV1.d.ts.map +1 -1
  116. package/lib/snapshotV1.js +1 -11
  117. package/lib/snapshotV1.js.map +1 -1
  118. package/lib/snapshotlegacy.d.ts.map +1 -1
  119. package/lib/snapshotlegacy.js +0 -1
  120. package/lib/snapshotlegacy.js.map +1 -1
  121. package/lib/sortedSegmentSet.d.ts +0 -1
  122. package/lib/sortedSegmentSet.d.ts.map +1 -1
  123. package/lib/sortedSegmentSet.js +1 -9
  124. package/lib/sortedSegmentSet.js.map +1 -1
  125. package/lib/sortedSet.d.ts.map +1 -1
  126. package/lib/sortedSet.js +0 -4
  127. package/lib/sortedSet.js.map +1 -1
  128. package/lib/test/client.conflictFarm.spec.d.ts.map +1 -1
  129. package/lib/test/client.conflictFarm.spec.js +37 -28
  130. package/lib/test/client.conflictFarm.spec.js.map +1 -1
  131. package/lib/test/client.replay.spec.js +1 -1
  132. package/lib/test/client.replay.spec.js.map +1 -1
  133. package/lib/test/mergeTreeOperationRunner.d.ts +2 -1
  134. package/lib/test/mergeTreeOperationRunner.d.ts.map +1 -1
  135. package/lib/test/mergeTreeOperationRunner.js +30 -12
  136. package/lib/test/mergeTreeOperationRunner.js.map +1 -1
  137. package/lib/test/obliterate.spec.js +55 -0
  138. package/lib/test/obliterate.spec.js.map +1 -1
  139. package/lib/test/reconnectHelper.d.ts +0 -1
  140. package/lib/test/reconnectHelper.d.ts.map +1 -1
  141. package/lib/test/reconnectHelper.js +1 -1
  142. package/lib/test/reconnectHelper.js.map +1 -1
  143. package/lib/test/testClientLogger.d.ts.map +1 -1
  144. package/lib/test/testClientLogger.js +18 -8
  145. package/lib/test/testClientLogger.js.map +1 -1
  146. package/lib/tsdoc-metadata.json +1 -1
  147. package/lib/zamboni.d.ts.map +1 -1
  148. package/lib/zamboni.js +0 -4
  149. package/lib/zamboni.js.map +1 -1
  150. package/package.json +22 -21
  151. package/src/attributionCollection.ts +14 -42
  152. package/src/client.ts +8 -9
  153. package/src/index.ts +1 -0
  154. package/src/localReference.ts +1 -3
  155. package/src/mergeTree.ts +185 -208
  156. package/src/mergeTreeNodes.ts +22 -3
  157. package/src/partialLengths.ts +23 -68
  158. package/src/properties.ts +1 -3
  159. package/src/revertibles.ts +7 -21
  160. package/src/segmentGroupCollection.ts +1 -3
  161. package/src/segmentPropertiesManager.ts +0 -1
  162. package/src/snapshotLoader.ts +2 -4
  163. package/src/snapshotV1.ts +5 -15
  164. package/src/snapshotlegacy.ts +1 -2
  165. package/src/sortedSegmentSet.ts +3 -10
  166. package/src/sortedSet.ts +2 -6
  167. package/src/zamboni.ts +4 -8
  168. 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 {
@@ -88,15 +91,9 @@ import {
88
91
  // eslint-disable-next-line import/no-deprecated
89
92
  import { PropertiesRollback } from "./segmentPropertiesManager.js";
90
93
  import { endpointPosAndSide, type SequencePlace } from "./sequencePlace.js";
94
+ import { SortedSegmentSet } from "./sortedSegmentSet.js";
91
95
  import { zamboniSegments } from "./zamboni.js";
92
96
 
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
97
  function markSegmentMoved(seg: ISegment, moveInfo: IMoveInfo): void {
101
98
  seg.moveDst = moveInfo.moveDst;
102
99
  seg.movedClientIds = [...moveInfo.movedClientIds];
@@ -414,6 +411,71 @@ const forwardPred = (ref: LocalReferencePosition): boolean =>
414
411
  const backwardPred = (ref: LocalReferencePosition): boolean =>
415
412
  ref.slidingPreference === SlidingPreference.BACKWARD;
416
413
 
414
+ class Obliterates {
415
+ /**
416
+ * Array containing the all move operations within the
417
+ * collab window.
418
+ *
419
+ * The moves are stored in sequence order which accelerates clean up in setMinSeq
420
+ *
421
+ * See https://github.com/microsoft/FluidFramework/blob/main/packages/dds/merge-tree/docs/Obliterate.md#remote-perspective
422
+ * for additional context
423
+ */
424
+ // eslint-disable-next-line import/no-deprecated
425
+ private readonly seqOrdered = new DoublyLinkedList<ObliterateInfo>();
426
+
427
+ /**
428
+ * This contains a sorted lists of all obliterate starts
429
+ * and is used to accelerate finding overlapping obliterates
430
+ * as well as determining if there are any obliterates at all.
431
+ */
432
+ private readonly startOrdered = new SortedSegmentSet<LocalReferencePosition>();
433
+
434
+ constructor(private readonly mergeTree: MergeTree) {}
435
+
436
+ public setMinSeq(minSeq: number): void {
437
+ // eslint-disable-next-line @typescript-eslint/no-non-null-asserted-optional-chain
438
+ while (!this.seqOrdered.empty && this.seqOrdered.first?.data.seq! <= minSeq) {
439
+ const ob = this.seqOrdered.shift()!;
440
+ this.startOrdered.remove(ob.data.start);
441
+ this.mergeTree.removeLocalReferencePosition(ob.data.start);
442
+ this.mergeTree.removeLocalReferencePosition(ob.data.end);
443
+ }
444
+ }
445
+
446
+ // eslint-disable-next-line import/no-deprecated
447
+ public addOrUpdate(obliterateInfo: ObliterateInfo): void {
448
+ const { seq, start } = obliterateInfo;
449
+ if (seq !== UnassignedSequenceNumber) {
450
+ this.seqOrdered.push(obliterateInfo);
451
+ }
452
+ this.startOrdered.addOrUpdate(start);
453
+ }
454
+
455
+ public empty(): boolean {
456
+ return this.startOrdered.size === 0;
457
+ }
458
+
459
+ // eslint-disable-next-line import/no-deprecated
460
+ public findOverlapping(seg: ISegment): Iterable<ObliterateInfo> {
461
+ // eslint-disable-next-line import/no-deprecated
462
+ const overlapping: ObliterateInfo[] = [];
463
+ for (const start of this.startOrdered.items) {
464
+ if (start.getSegment()!.ordinal <= seg.ordinal) {
465
+ // eslint-disable-next-line import/no-deprecated
466
+ const ob = start.properties?.obliterate as ObliterateInfo;
467
+ if (ob.end.getSegment()!.ordinal >= seg.ordinal) {
468
+ overlapping.push(ob);
469
+ }
470
+ } else {
471
+ // the start is past the seg, so exit
472
+ break;
473
+ }
474
+ }
475
+ return overlapping;
476
+ }
477
+ }
478
+
417
479
  /**
418
480
  * @internal
419
481
  */
@@ -448,38 +510,7 @@ export class MergeTree {
448
510
  public mergeTreeDeltaCallback?: MergeTreeDeltaCallback;
449
511
  public mergeTreeMaintenanceCallback?: MergeTreeMaintenanceCallback;
450
512
 
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();
513
+ private readonly obliterates = new Obliterates(this);
483
514
 
484
515
  public constructor(public options?: IMergeTreeOptions) {
485
516
  this._root = this.makeBlock(0);
@@ -617,8 +648,7 @@ export class MergeTree {
617
648
  childIndex++, nodeIndex++ // Advance to next child & node
618
649
  ) {
619
650
  // Insert the next node into the current block
620
- // TODO Non null asserting, why is this not null?
621
- this.addNode(block, nodes[nodeIndex]!);
651
+ this.addNode(block, nodes[nodeIndex]);
622
652
  }
623
653
 
624
654
  // Calculate this block's info. Previously this was inlined into the above loop as a micro-optimization,
@@ -628,8 +658,7 @@ export class MergeTree {
628
658
  }
629
659
 
630
660
  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.
661
+ ? blocks[0] // ...then we're done. Return the root.
633
662
  : buildMergeBlock(blocks); // ...otherwise recursively build the next layer above blocks.
634
663
  };
635
664
  if (segments.length > 0) {
@@ -688,8 +717,7 @@ export class MergeTree {
688
717
  while (parent) {
689
718
  const children = parent.children;
690
719
  for (let childIndex = 0; childIndex < parent.childCount; childIndex++) {
691
- // TODO Non null asserting, why is this not null?
692
- const child = children[childIndex]!;
720
+ const child = children[childIndex];
693
721
  if ((!!prevParent && child === prevParent) || child === node) {
694
722
  break;
695
723
  }
@@ -1052,8 +1080,7 @@ export class MergeTree {
1052
1080
 
1053
1081
  if (minSeq > this.collabWindow.minSeq) {
1054
1082
  this.collabWindow.minSeq = minSeq;
1055
- const firstMoveSeqIdx = this.moveSeqs.findIndex((seq) => seq >= minSeq);
1056
- this.moveSeqs = firstMoveSeqIdx === -1 ? [] : this.moveSeqs.slice(firstMoveSeqIdx);
1083
+ this.obliterates.setMinSeq(minSeq);
1057
1084
  if (MergeTree.options.zamboniSegments) {
1058
1085
  zamboniSegments(this);
1059
1086
  }
@@ -1192,42 +1219,10 @@ export class MergeTree {
1192
1219
  const deltaSegments: IMergeTreeSegmentDelta[] = [];
1193
1220
  const overlappingRemoves: boolean[] = [];
1194
1221
  pendingSegmentGroup.segments.map((pendingSegment: ISegmentLeaf) => {
1195
- const localMovedSeq = pendingSegment.localMovedSeq;
1196
1222
  const overlappingRemove = !pendingSegment.ack(pendingSegmentGroup, opArgs);
1197
1223
 
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
1224
  overwrite = overlappingRemove || overwrite;
1221
1225
 
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
1226
  overlappingRemoves.push(overlappingRemove);
1232
1227
  if (MergeTree.options.zamboniSegments) {
1233
1228
  this.addToLRUSet(pendingSegment, seq);
@@ -1240,6 +1235,10 @@ export class MergeTree {
1240
1235
  });
1241
1236
  });
1242
1237
 
1238
+ if (opArgs.op.type === MergeTreeDeltaType.OBLITERATE) {
1239
+ this.obliterates.addOrUpdate(pendingSegmentGroup.obliterateInfo!);
1240
+ }
1241
+
1243
1242
  // Perform slides after all segments have been acked, so that
1244
1243
  // positions after slide are final
1245
1244
  if (
@@ -1516,115 +1515,62 @@ export class MergeTree {
1516
1515
 
1517
1516
  insertPos += newSegment.cachedLength;
1518
1517
 
1519
- if (!this.options?.mergeTreeEnableObliterate) {
1520
- continue;
1521
- }
1522
-
1523
- let moveUpperBound = Number.POSITIVE_INFINITY;
1524
- const smallestSeqMoveOp = this.getSmallestSeqMoveOp();
1525
-
1526
- if (smallestSeqMoveOp === undefined) {
1518
+ if (!this.options?.mergeTreeEnableObliterate || this.obliterates.empty()) {
1527
1519
  continue;
1528
1520
  }
1529
1521
 
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;
1522
+ // eslint-disable-next-line import/no-deprecated
1523
+ let oldest: ObliterateInfo | undefined;
1524
+ let normalizedOldestSeq: number = 0;
1525
+ // eslint-disable-next-line import/no-deprecated
1526
+ let newest: ObliterateInfo | undefined;
1527
+ let normalizedNewestSeq: number = 0;
1528
+ const movedClientIds: number[] = [];
1529
+ const movedSeqs: number[] = [];
1530
+ for (const ob of this.obliterates.findOverlapping(newSegment)) {
1531
+ // compute a normalized seq that takes into account local seqs
1532
+ // but is still comparable to remote seqs to keep the checks below easy
1533
+ // REMOTE SEQUENCE NUMBERS LOCAL SEQUENCE NUMBERS
1534
+ // [0, 1, 2, 3, ..., 100, ..., 1000, ..., (MAX - MaxLocalSeq), L1, L2, L3, L4, ..., L100, ..., L1000, ...(MAX)]
1535
+ const normalizedObSeq =
1536
+ ob.seq === UnassignedSequenceNumber
1537
+ ? Number.MAX_SAFE_INTEGER - this.collabWindow.localSeq + ob.localSeq!
1538
+ : ob.seq;
1539
+ if (normalizedObSeq > refSeq) {
1540
+ if (oldest === undefined || normalizedOldestSeq > normalizedObSeq) {
1541
+ normalizedOldestSeq = normalizedObSeq;
1542
+ oldest = ob;
1543
+ movedClientIds.unshift(ob.clientId);
1544
+ movedSeqs.unshift(ob.seq);
1545
+ } else {
1546
+ if (newest === undefined || normalizedNewestSeq < normalizedObSeq) {
1547
+ normalizedNewestSeq = normalizedObSeq;
1548
+ newest = ob;
1549
+ }
1550
+ movedClientIds.push(ob.clientId);
1551
+ movedSeqs.push(ob.seq);
1585
1552
  }
1586
1553
  }
1554
+ }
1587
1555
 
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 = {
1556
+ if (oldest && newest?.clientId !== clientId) {
1557
+ const moveInfo: IMoveInfo = {
1610
1558
  movedClientIds,
1611
- movedSeq: _movedSeq ?? UnassignedSequenceNumber,
1612
- movedSeqs: _movedSeq === undefined ? [UnassignedSequenceNumber] : [_movedSeq],
1613
- localMovedSeq: _localMovedSeq,
1614
- wasMovedOnInsert: (_movedSeq ?? -1) !== UnassignedSequenceNumber,
1559
+ movedSeq: oldest.seq,
1560
+ movedSeqs,
1561
+ localMovedSeq: oldest.localSeq,
1562
+ wasMovedOnInsert: oldest.seq !== UnassignedSequenceNumber,
1615
1563
  };
1616
1564
 
1617
1565
  markSegmentMoved(newSegment, moveInfo);
1618
1566
 
1619
1567
  if (moveInfo.localMovedSeq !== undefined) {
1620
- const movedSegmentGroup = this.locallyMovedSegments.get(moveInfo.localMovedSeq);
1621
-
1622
1568
  assert(
1623
- movedSegmentGroup !== undefined,
1569
+ oldest.segmentGroup !== undefined,
1624
1570
  0x86c /* expected segment group to exist */,
1625
1571
  );
1626
1572
 
1627
- this.addToPendingList(newSegment, movedSegmentGroup, localSeq);
1573
+ this.addToPendingList(newSegment, oldest.segmentGroup);
1628
1574
  }
1629
1575
 
1630
1576
  if (newSegment.parent) {
@@ -1701,10 +1647,6 @@ export class MergeTree {
1701
1647
  }
1702
1648
  }
1703
1649
 
1704
- private getSmallestSeqMoveOp(): number | undefined {
1705
- return this.moveSeqs[0] ?? (this.localMoveSeqs.size > 0 ? -1 : undefined);
1706
- }
1707
-
1708
1650
  private insertingWalk(
1709
1651
  block: MergeBlock,
1710
1652
  pos: number | "start" | "end",
@@ -1729,8 +1671,7 @@ export class MergeTree {
1729
1671
  let newNode: IMergeNode | undefined;
1730
1672
  let fromSplit: MergeBlock | undefined;
1731
1673
  for (childIndex = 0; childIndex < block.childCount; childIndex++) {
1732
- // TODO Non null asserting, why is this not null?
1733
- child = children[childIndex]!;
1674
+ child = children[childIndex];
1734
1675
  // ensure we walk down the far edge of the tree, even if all sub-tree is eligible for zamboni
1735
1676
  const isLastNonLeafBlock =
1736
1677
  isLastChildBlock && !child.isLeaf() && childIndex === block.childCount - 1;
@@ -1801,10 +1742,8 @@ export class MergeTree {
1801
1742
  }
1802
1743
  if (newNode) {
1803
1744
  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;
1745
+ block.children[i] = block.children[i - 1];
1746
+ block.children[i].index = i;
1808
1747
  }
1809
1748
  block.assignChild(newNode, childIndex, false);
1810
1749
  block.childCount++;
@@ -1841,8 +1780,7 @@ export class MergeTree {
1841
1780
  // Update ordinals to reflect lowered child count
1842
1781
  this.nodeUpdateOrdinals(node);
1843
1782
  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);
1783
+ newNode.assignChild(node.children[halfCount + i], i, false);
1846
1784
  node.children[halfCount + i] = undefined!;
1847
1785
  }
1848
1786
  this.nodeUpdateLengthNewStructure(node);
@@ -1852,8 +1790,7 @@ export class MergeTree {
1852
1790
 
1853
1791
  public nodeUpdateOrdinals(block: MergeBlock): void {
1854
1792
  for (let i = 0; i < block.childCount; i++) {
1855
- // TODO Non null asserting, why is this not null?
1856
- const child = block.children[i]!;
1793
+ const child = block.children[i];
1857
1794
  block.setOrdinal(child, i);
1858
1795
  if (!child.isLeaf()) {
1859
1796
  this.nodeUpdateOrdinals(child);
@@ -1972,13 +1909,53 @@ export class MergeTree {
1972
1909
  const movedSegments: IMergeTreeSegmentDelta[] = [];
1973
1910
  const localSeq =
1974
1911
  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
1912
  // eslint-disable-next-line import/no-deprecated
1981
- let segmentGroup: SegmentGroup;
1913
+ const obliterate: ObliterateInfo = {
1914
+ clientId,
1915
+ end: createDetachedLocalReferencePosition(undefined),
1916
+ refSeq,
1917
+ seq,
1918
+ start: createDetachedLocalReferencePosition(undefined),
1919
+ localSeq,
1920
+ segmentGroup: undefined,
1921
+ };
1922
+ const normalizedStartPos = startPos === "start" || startPos === undefined ? 0 : startPos;
1923
+ const normalizedEndPos =
1924
+ endPos === "end" || endPos === undefined ? this.getLength(refSeq, clientId) : endPos;
1925
+
1926
+ const { segment: startSeg } = this.getContainingSegment(
1927
+ normalizedStartPos,
1928
+ refSeq,
1929
+ clientId,
1930
+ );
1931
+ const { segment: endSeg } = this.getContainingSegment(
1932
+ normalizedEndPos - 1,
1933
+ refSeq,
1934
+ clientId,
1935
+ );
1936
+ assert(
1937
+ startSeg !== undefined && endSeg !== undefined,
1938
+ 0xa3f /* segments cannot be undefined */,
1939
+ );
1940
+
1941
+ obliterate.start = this.createLocalReferencePosition(
1942
+ startSeg,
1943
+ 0,
1944
+ ReferenceType.StayOnRemove,
1945
+ {
1946
+ obliterate,
1947
+ },
1948
+ );
1949
+
1950
+ obliterate.end = this.createLocalReferencePosition(
1951
+ endSeg,
1952
+ endSeg.cachedLength - 1,
1953
+ ReferenceType.StayOnRemove,
1954
+ {
1955
+ obliterate,
1956
+ },
1957
+ );
1958
+
1982
1959
  const markMoved = (
1983
1960
  segment: ISegment,
1984
1961
  pos: number,
@@ -1986,9 +1963,6 @@ export class MergeTree {
1986
1963
  _end: number,
1987
1964
  ): boolean => {
1988
1965
  const existingMoveInfo = toMoveInfo(segment);
1989
- if (startSide) segment.startSide = startSide;
1990
- if (endSide) segment.endSide = endSide;
1991
-
1992
1966
  if (
1993
1967
  clientId !== segment.clientId &&
1994
1968
  segment.seq !== undefined &&
@@ -2034,7 +2008,12 @@ export class MergeTree {
2034
2008
  segment.movedSeq === UnassignedSequenceNumber &&
2035
2009
  clientId === this.collabWindow.clientId
2036
2010
  ) {
2037
- segmentGroup = this.addToPendingList(segment, segmentGroup, localSeq);
2011
+ obliterate.segmentGroup = this.addToPendingList(
2012
+ segment,
2013
+ obliterate.segmentGroup,
2014
+ localSeq,
2015
+ );
2016
+ obliterate.segmentGroup.obliterateInfo ??= obliterate;
2038
2017
  } else {
2039
2018
  if (MergeTree.options.zamboniSegments) {
2040
2019
  this.addToLRUSet(segment, seq);
@@ -2070,6 +2049,8 @@ export class MergeTree {
2070
2049
  seq === UnassignedSequenceNumber ? undefined : seq,
2071
2050
  );
2072
2051
 
2052
+ this.obliterates.addOrUpdate(obliterate);
2053
+
2073
2054
  this.slideAckedRemovedSegmentReferences(localOverlapWithRefs);
2074
2055
  // opArgs == undefined => test code
2075
2056
  if (movedSegments.length > 0) {
@@ -2079,10 +2060,6 @@ export class MergeTree {
2079
2060
  });
2080
2061
  }
2081
2062
 
2082
- if (segmentGroup! && localSeq !== undefined) {
2083
- this.locallyMovedSegments.set(localSeq, segmentGroup);
2084
- }
2085
-
2086
2063
  // these events are newly removed
2087
2064
  // so we slide after eventing in case the consumer wants to make reference
2088
2065
  // changes at remove time, like add a ref to track undo redo.
@@ -2292,8 +2269,7 @@ export class MergeTree {
2292
2269
  { op: removeOp },
2293
2270
  );
2294
2271
  } /* op.type === MergeTreeDeltaType.ANNOTATE */ else {
2295
- // TODO Non null asserting, why is this not null?
2296
- const props = pendingSegmentGroup.previousProps![i]!;
2272
+ const props = pendingSegmentGroup.previousProps![i];
2297
2273
  const annotateOp = createAnnotateRangeOp(start, start + segment.cachedLength, props);
2298
2274
  this.annotateRange(
2299
2275
  start,
@@ -2366,7 +2342,10 @@ export class MergeTree {
2366
2342
  _segment !== "start" &&
2367
2343
  _segment !== "end" &&
2368
2344
  isRemovedAndAckedOrMovedAndAcked(_segment) &&
2369
- !refTypeIncludesFlag(refType, ReferenceType.SlideOnRemove | ReferenceType.Transient) &&
2345
+ !refTypeIncludesFlag(
2346
+ refType,
2347
+ ReferenceType.SlideOnRemove | ReferenceType.Transient | ReferenceType.StayOnRemove,
2348
+ ) &&
2370
2349
  _segment.endpointType === undefined
2371
2350
  ) {
2372
2351
  throw new UsageError(
@@ -2466,9 +2445,8 @@ export class MergeTree {
2466
2445
  }
2467
2446
 
2468
2447
  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]!;
2448
+ const seg = newOrder[i];
2449
+ const { parent, index, ordinal } = currentOrder[i];
2472
2450
  parent?.assignChild(seg, index, false);
2473
2451
  seg.ordinal = ordinal;
2474
2452
  }
@@ -2561,8 +2539,7 @@ export class MergeTree {
2561
2539
  const leftmostTiles = createMap<Marker>();
2562
2540
 
2563
2541
  for (let i = 0; i < block.childCount; i++) {
2564
- // TODO Non null asserting, why is this not null?
2565
- const node = block.children[i]!;
2542
+ const node = block.children[i];
2566
2543
  const nodeLength = nodeTotalLength(this, node);
2567
2544
  if (nodeLength !== undefined) {
2568
2545
  len ??= 0;
@@ -14,7 +14,7 @@ import {
14
14
  UnassignedSequenceNumber,
15
15
  UniversalSequenceNumber,
16
16
  } from "./constants.js";
17
- import { LocalReferenceCollection } from "./localReference.js";
17
+ import { LocalReferenceCollection, type LocalReferencePosition } from "./localReference.js";
18
18
  import { IMergeTreeDeltaOpArgs } from "./mergeTreeDeltaCallback.js";
19
19
  import { TrackingGroupCollection } from "./mergeTreeTracking.js";
20
20
  import { IJSONSegment, IMarkerDef, MergeTreeDeltaType, ReferenceType } from "./ops.js";
@@ -393,6 +393,21 @@ export interface SegmentActions<TClientData> {
393
393
  post?: BlockAction<TClientData>;
394
394
  }
395
395
 
396
+ /**
397
+ * @deprecated This functionality was not meant to be exported and will be removed in a future release
398
+ * @legacy
399
+ * @alpha
400
+ */
401
+ export interface ObliterateInfo {
402
+ start: LocalReferencePosition;
403
+ end: LocalReferencePosition;
404
+ refSeq: number;
405
+ clientId: number;
406
+ seq: number;
407
+ localSeq: number | undefined;
408
+ segmentGroup: SegmentGroup | undefined;
409
+ }
410
+
396
411
  /**
397
412
  * @deprecated This functionality was not meant to be exported and will be removed in a future release
398
413
  * @legacy
@@ -403,6 +418,7 @@ export interface SegmentGroup {
403
418
  previousProps?: PropertySet[];
404
419
  localSeq?: number;
405
420
  refSeq: number;
421
+ obliterateInfo?: ObliterateInfo;
406
422
  }
407
423
 
408
424
  /**
@@ -657,10 +673,13 @@ export abstract class BaseSegment implements ISegment {
657
673
  case MergeTreeDeltaType.OBLITERATE: {
658
674
  const moveInfo: IMoveInfo | undefined = toMoveInfo(this);
659
675
  assert(moveInfo !== undefined, 0x86e /* On obliterate ack, missing move info! */);
660
- this.localMovedSeq = undefined;
676
+ const obliterateInfo = segmentGroup.obliterateInfo;
677
+ assert(obliterateInfo !== undefined, 0xa40 /* must have obliterate info */);
678
+ this.localMovedSeq = obliterateInfo.localSeq = undefined;
661
679
  const seqIdx = moveInfo.movedSeqs.indexOf(UnassignedSequenceNumber);
662
680
  assert(seqIdx !== -1, 0x86f /* expected movedSeqs to contain unacked seq */);
663
- moveInfo.movedSeqs[seqIdx] = opArgs.sequencedMessage!.sequenceNumber;
681
+ moveInfo.movedSeqs[seqIdx] = obliterateInfo.seq =
682
+ opArgs.sequencedMessage!.sequenceNumber;
664
683
 
665
684
  if (moveInfo.movedSeq === UnassignedSequenceNumber) {
666
685
  moveInfo.movedSeq = opArgs.sequencedMessage!.sequenceNumber;