@fluidframework/merge-tree 2.31.0 → 2.32.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 (137) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/dist/client.d.ts +7 -1
  3. package/dist/client.d.ts.map +1 -1
  4. package/dist/client.js +153 -44
  5. package/dist/client.js.map +1 -1
  6. package/dist/index.d.ts +1 -1
  7. package/dist/index.d.ts.map +1 -1
  8. package/dist/index.js +3 -1
  9. package/dist/index.js.map +1 -1
  10. package/dist/mergeTree.d.ts +17 -5
  11. package/dist/mergeTree.d.ts.map +1 -1
  12. package/dist/mergeTree.js +188 -79
  13. package/dist/mergeTree.js.map +1 -1
  14. package/dist/mergeTreeNodes.d.ts +16 -18
  15. package/dist/mergeTreeNodes.d.ts.map +1 -1
  16. package/dist/mergeTreeNodes.js +6 -0
  17. package/dist/mergeTreeNodes.js.map +1 -1
  18. package/dist/perspective.d.ts +9 -0
  19. package/dist/perspective.d.ts.map +1 -1
  20. package/dist/perspective.js +14 -1
  21. package/dist/perspective.js.map +1 -1
  22. package/dist/segmentInfos.d.ts +32 -4
  23. package/dist/segmentInfos.d.ts.map +1 -1
  24. package/dist/segmentInfos.js +3 -1
  25. package/dist/segmentInfos.js.map +1 -1
  26. package/dist/sortedSegmentSet.d.ts +1 -0
  27. package/dist/sortedSegmentSet.d.ts.map +1 -1
  28. package/dist/sortedSegmentSet.js +3 -0
  29. package/dist/sortedSegmentSet.js.map +1 -1
  30. package/dist/test/beastTest.spec.js +5 -5
  31. package/dist/test/beastTest.spec.js.map +1 -1
  32. package/dist/test/client.localReference.spec.js +3 -3
  33. package/dist/test/client.localReference.spec.js.map +1 -1
  34. package/dist/test/client.rollback.spec.js +17 -0
  35. package/dist/test/client.rollback.spec.js.map +1 -1
  36. package/dist/test/clientTestHelper.d.ts +100 -0
  37. package/dist/test/clientTestHelper.d.ts.map +1 -0
  38. package/dist/test/clientTestHelper.js +196 -0
  39. package/dist/test/clientTestHelper.js.map +1 -0
  40. package/dist/test/mergeTree.annotate.spec.js +12 -12
  41. package/dist/test/mergeTree.annotate.spec.js.map +1 -1
  42. package/dist/test/mergeTree.markRangeRemoved.deltaCallback.spec.js +1 -1
  43. package/dist/test/mergeTree.markRangeRemoved.deltaCallback.spec.js.map +1 -1
  44. package/dist/test/obliterate.concurrent.spec.js +93 -90
  45. package/dist/test/obliterate.concurrent.spec.js.map +1 -1
  46. package/dist/test/obliterate.deltaCallback.spec.js +121 -116
  47. package/dist/test/obliterate.deltaCallback.spec.js.map +1 -1
  48. package/dist/test/obliterate.rangeExpansion.spec.js +29 -79
  49. package/dist/test/obliterate.rangeExpansion.spec.js.map +1 -1
  50. package/dist/test/obliterate.reconnect.spec.js +235 -58
  51. package/dist/test/obliterate.reconnect.spec.js.map +1 -1
  52. package/dist/test/testClient.js +1 -1
  53. package/dist/test/testClient.js.map +1 -1
  54. package/dist/test/testUtils.d.ts +13 -0
  55. package/dist/test/testUtils.d.ts.map +1 -1
  56. package/dist/test/testUtils.js +22 -1
  57. package/dist/test/testUtils.js.map +1 -1
  58. package/lib/client.d.ts +7 -1
  59. package/lib/client.d.ts.map +1 -1
  60. package/lib/client.js +155 -46
  61. package/lib/client.js.map +1 -1
  62. package/lib/index.d.ts +1 -1
  63. package/lib/index.d.ts.map +1 -1
  64. package/lib/index.js +1 -0
  65. package/lib/index.js.map +1 -1
  66. package/lib/mergeTree.d.ts +17 -5
  67. package/lib/mergeTree.d.ts.map +1 -1
  68. package/lib/mergeTree.js +192 -83
  69. package/lib/mergeTree.js.map +1 -1
  70. package/lib/mergeTreeNodes.d.ts +16 -18
  71. package/lib/mergeTreeNodes.d.ts.map +1 -1
  72. package/lib/mergeTreeNodes.js +7 -1
  73. package/lib/mergeTreeNodes.js.map +1 -1
  74. package/lib/perspective.d.ts +9 -0
  75. package/lib/perspective.d.ts.map +1 -1
  76. package/lib/perspective.js +12 -0
  77. package/lib/perspective.js.map +1 -1
  78. package/lib/segmentInfos.d.ts +32 -4
  79. package/lib/segmentInfos.d.ts.map +1 -1
  80. package/lib/segmentInfos.js +2 -1
  81. package/lib/segmentInfos.js.map +1 -1
  82. package/lib/sortedSegmentSet.d.ts +1 -0
  83. package/lib/sortedSegmentSet.d.ts.map +1 -1
  84. package/lib/sortedSegmentSet.js +3 -0
  85. package/lib/sortedSegmentSet.js.map +1 -1
  86. package/lib/test/beastTest.spec.js +5 -5
  87. package/lib/test/beastTest.spec.js.map +1 -1
  88. package/lib/test/client.localReference.spec.js +3 -3
  89. package/lib/test/client.localReference.spec.js.map +1 -1
  90. package/lib/test/client.rollback.spec.js +18 -1
  91. package/lib/test/client.rollback.spec.js.map +1 -1
  92. package/lib/test/clientTestHelper.d.ts +100 -0
  93. package/lib/test/clientTestHelper.d.ts.map +1 -0
  94. package/lib/test/clientTestHelper.js +192 -0
  95. package/lib/test/clientTestHelper.js.map +1 -0
  96. package/lib/test/mergeTree.annotate.spec.js +12 -12
  97. package/lib/test/mergeTree.annotate.spec.js.map +1 -1
  98. package/lib/test/mergeTree.markRangeRemoved.deltaCallback.spec.js +1 -1
  99. package/lib/test/mergeTree.markRangeRemoved.deltaCallback.spec.js.map +1 -1
  100. package/lib/test/obliterate.concurrent.spec.js +93 -90
  101. package/lib/test/obliterate.concurrent.spec.js.map +1 -1
  102. package/lib/test/obliterate.deltaCallback.spec.js +121 -116
  103. package/lib/test/obliterate.deltaCallback.spec.js.map +1 -1
  104. package/lib/test/obliterate.rangeExpansion.spec.js +1 -51
  105. package/lib/test/obliterate.rangeExpansion.spec.js.map +1 -1
  106. package/lib/test/obliterate.reconnect.spec.js +236 -59
  107. package/lib/test/obliterate.reconnect.spec.js.map +1 -1
  108. package/lib/test/testClient.js +1 -1
  109. package/lib/test/testClient.js.map +1 -1
  110. package/lib/test/testUtils.d.ts +13 -0
  111. package/lib/test/testUtils.d.ts.map +1 -1
  112. package/lib/test/testUtils.js +20 -0
  113. package/lib/test/testUtils.js.map +1 -1
  114. package/package.json +19 -18
  115. package/src/client.ts +286 -55
  116. package/src/index.ts +1 -1
  117. package/src/mergeTree.ts +265 -98
  118. package/src/mergeTreeNodes.ts +24 -18
  119. package/src/perspective.ts +21 -0
  120. package/src/segmentInfos.ts +48 -6
  121. package/src/sortedSegmentSet.ts +4 -0
  122. package/dist/test/partialSyncHelper.d.ts +0 -42
  123. package/dist/test/partialSyncHelper.d.ts.map +0 -1
  124. package/dist/test/partialSyncHelper.js +0 -96
  125. package/dist/test/partialSyncHelper.js.map +0 -1
  126. package/dist/test/reconnectHelper.d.ts +0 -50
  127. package/dist/test/reconnectHelper.d.ts.map +0 -1
  128. package/dist/test/reconnectHelper.js +0 -106
  129. package/dist/test/reconnectHelper.js.map +0 -1
  130. package/lib/test/partialSyncHelper.d.ts +0 -42
  131. package/lib/test/partialSyncHelper.d.ts.map +0 -1
  132. package/lib/test/partialSyncHelper.js +0 -92
  133. package/lib/test/partialSyncHelper.js.map +0 -1
  134. package/lib/test/reconnectHelper.d.ts +0 -50
  135. package/lib/test/reconnectHelper.d.ts.map +0 -1
  136. package/lib/test/reconnectHelper.js +0 -102
  137. package/lib/test/reconnectHelper.js.map +0 -1
package/src/mergeTree.ts CHANGED
@@ -16,7 +16,6 @@ import {
16
16
  NonCollabClient,
17
17
  TreeMaintenanceSequenceNumber,
18
18
  UnassignedSequenceNumber,
19
- UniversalSequenceNumber,
20
19
  } from "./constants.js";
21
20
  import { EndOfTreeSegment, StartOfTreeSegment } from "./endOfTreeSegment.js";
22
21
  import {
@@ -24,7 +23,6 @@ import {
24
23
  LocalReferencePosition,
25
24
  SlidingPreference,
26
25
  anyLocalReferencePosition,
27
- createDetachedLocalReferencePosition,
28
26
  filterLocalReferencePositions,
29
27
  } from "./localReference.js";
30
28
  import {
@@ -64,7 +62,7 @@ import {
64
62
  type ISegmentPrivate,
65
63
  type ObliterateInfo,
66
64
  } from "./mergeTreeNodes.js";
67
- import type { TrackingGroup } from "./mergeTreeTracking.js";
65
+ import { UnorderedTrackingGroup, type TrackingGroup } from "./mergeTreeTracking.js";
68
66
  import {
69
67
  createAnnotateRangeOp,
70
68
  createInsertSegmentOp,
@@ -83,6 +81,7 @@ import {
83
81
  type Perspective,
84
82
  LocalDefaultPerspective,
85
83
  RemoteObliteratePerspective,
84
+ allAckedChangesPerspective,
86
85
  } from "./perspective.js";
87
86
  import { PropertySet, createMap, extend, extendIfUndefined } from "./properties.js";
88
87
  import {
@@ -95,6 +94,8 @@ import {
95
94
  import { SegmentGroupCollection } from "./segmentGroupCollection.js";
96
95
  import {
97
96
  assertRemoved,
97
+ ISegmentInsideObliterateInfo,
98
+ isInsideObliterate,
98
99
  isMergeNodeInfo,
99
100
  isRemoved,
100
101
  overwriteInfo,
@@ -197,14 +198,6 @@ function ackSegment(
197
198
  type: op.type === MergeTreeDeltaType.REMOVE ? "setRemove" : "sliceRemove",
198
199
  };
199
200
  segment.removes[segment.removes.length - 1] = removeStamp;
200
-
201
- const { obliterateInfo } = segmentGroup;
202
- const hasObliterateInfo = obliterateInfo !== undefined;
203
- const isObliterate = op.type !== MergeTreeDeltaType.REMOVE;
204
- assert(hasObliterateInfo === isObliterate, 0xa40 /* must have obliterate info */);
205
- if (hasObliterateInfo) {
206
- obliterateInfo.stamp = removeStamp as SliceRemoveOperationStamp;
207
- }
208
201
  break;
209
202
  }
210
203
 
@@ -400,10 +393,15 @@ export function findRootMergeBlock(
400
393
  function getSlideToSegment(
401
394
  segment: ISegmentLeaf | undefined,
402
395
  slidingPreference: SlidingPreference = SlidingPreference.FORWARD,
396
+ perspective: Perspective,
403
397
  cache?: Map<ISegmentLeaf, { seg?: ISegmentLeaf }>,
404
398
  useNewSlidingBehavior: boolean = false,
405
399
  ): [ISegmentLeaf | undefined, "start" | "end" | undefined] {
406
- if (!segment || !isRemovedAndAcked(segment) || segment.endpointType !== undefined) {
400
+ if (
401
+ !segment ||
402
+ perspective.isSegmentPresent(segment) ||
403
+ segment.endpointType !== undefined
404
+ ) {
407
405
  return [segment, undefined];
408
406
  }
409
407
 
@@ -414,7 +412,7 @@ function getSlideToSegment(
414
412
  const result: { seg?: ISegmentLeaf } = {};
415
413
  cache?.set(segment, result);
416
414
  const goFurtherToFindSlideToSegment = (seg: ISegmentLeaf): boolean => {
417
- if (opstampUtils.isAcked(seg.insert) && !isRemovedAndAcked(seg)) {
415
+ if (perspective.isSegmentPresent(seg)) {
418
416
  result.seg = seg;
419
417
  return false;
420
418
  }
@@ -473,6 +471,7 @@ function getSlideToSegment(
473
471
  export function getSlideToSegoff(
474
472
  segoff: { segment: ISegmentInternal | undefined; offset: number | undefined },
475
473
  slidingPreference: SlidingPreference = SlidingPreference.FORWARD,
474
+ perspective: Perspective = allAckedChangesPerspective,
476
475
  useNewSlidingBehavior: boolean = false,
477
476
  ): {
478
477
  segment: ISegmentInternal | undefined;
@@ -484,6 +483,7 @@ export function getSlideToSegoff(
484
483
  const [segment, _] = getSlideToSegment(
485
484
  segoff.segment,
486
485
  slidingPreference,
486
+ perspective,
487
487
  undefined,
488
488
  useNewSlidingBehavior,
489
489
  );
@@ -535,6 +535,10 @@ class Obliterates {
535
535
  }
536
536
  }
537
537
 
538
+ public onNormalize(): void {
539
+ this.startOrdered.onSortOrderChange();
540
+ }
541
+
538
542
  public addOrUpdate(obliterateInfo: ObliterateInfo): void {
539
543
  const {
540
544
  stamp: { seq },
@@ -567,6 +571,48 @@ class Obliterates {
567
571
  }
568
572
  return overlapping;
569
573
  }
574
+
575
+ /**
576
+ * Remove a local obliterate from this data structure.
577
+ * @privateRemarks
578
+ * This data structure could support removing non-local obliterates if we wanted it to, but when adding support for that
579
+ * we should reconsider the indexing structure for seq ordered obliterates (right now it would be an O(# obliterates) operation)
580
+ */
581
+ public removeLocalObliterate(obliterateInfo: ObliterateInfo): void {
582
+ assert(
583
+ obliterateInfo.stamp.seq === UnassignedSequenceNumber,
584
+ 0xb6e /* Expected local obliterate */,
585
+ );
586
+ this.startOrdered.remove(obliterateInfo.start);
587
+ }
588
+
589
+ /**
590
+ * Returns an iterator over the `ObliterateInfo` for all obliterates in the collab window. Obliterates are not guaranteed to be ordered.
591
+ * The iterator is not guaranteed to be valid over edits to the set of obliterates.
592
+ */
593
+ public [Symbol.iterator](): IterableIterator<ObliterateInfo> {
594
+ let index = 0;
595
+ const { items: starts } = this.startOrdered;
596
+ const iterator: IterableIterator<ObliterateInfo> = {
597
+ next(): IteratorResult<ObliterateInfo> {
598
+ if (index < starts.length) {
599
+ const start = starts[index++];
600
+ const info = start.properties?.obliterate as ObliterateInfo;
601
+ assert(
602
+ info?.start !== undefined && info?.end !== undefined,
603
+ 0xb6f /* Expected obliterateInfo endpoint to map to its obliterate */,
604
+ );
605
+ return { value: info, done: false };
606
+ }
607
+
608
+ return { value: undefined, done: true };
609
+ },
610
+ [Symbol.iterator]() {
611
+ return this;
612
+ },
613
+ };
614
+ return iterator;
615
+ }
570
616
  }
571
617
 
572
618
  interface InsertResult {
@@ -631,6 +677,16 @@ export class MergeTree {
631
677
  this.attributionPolicy = options?.attribution?.policyFactory?.();
632
678
  }
633
679
 
680
+ public rebaseObliterateTo(
681
+ existing: ObliterateInfo,
682
+ newObliterate: ObliterateInfo | undefined,
683
+ ): void {
684
+ this.obliterates.removeLocalObliterate(existing);
685
+ if (newObliterate !== undefined) {
686
+ this.obliterates.addOrUpdate(newObliterate);
687
+ }
688
+ }
689
+
634
690
  private _root: IRootMergeBlock;
635
691
  public get root(): IRootMergeBlock {
636
692
  return this._root;
@@ -927,6 +983,7 @@ export class MergeTree {
927
983
  const [slideToSegment, maybeEndpoint] = getSlideToSegment(
928
984
  segment,
929
985
  slidingPreference,
986
+ allAckedChangesPerspective,
930
987
  slidingPreference === SlidingPreference.FORWARD
931
988
  ? forwardSegmentCache
932
989
  : backwardSegmentCache,
@@ -1248,10 +1305,9 @@ export class MergeTree {
1248
1305
  }
1249
1306
 
1250
1307
  /**
1251
- * Assign sequence number to existing segment; update partial lengths to reflect the change
1252
- * @param seq - sequence number given by server to pending segment
1308
+ * Assign sequence number to existing segments affected by an op; update partial lengths to reflect the change
1253
1309
  */
1254
- public ackPendingSegment(opArgs: IMergeTreeDeltaOpArgs): void {
1310
+ public ackOp(opArgs: IMergeTreeDeltaOpArgs): void {
1255
1311
  const seq = opArgs.sequencedMessage!.sequenceNumber;
1256
1312
  const stamp: OperationStamp = {
1257
1313
  seq,
@@ -1261,9 +1317,33 @@ export class MergeTree {
1261
1317
  const nodesToUpdate: MergeBlock[] = [];
1262
1318
  let overwrite = false;
1263
1319
  if (pendingSegmentGroup !== undefined) {
1320
+ const { obliterateInfo, segments } = pendingSegmentGroup;
1321
+ const hasObliterateInfo = obliterateInfo !== undefined;
1322
+ const isObliterate =
1323
+ opArgs.op.type === MergeTreeDeltaType.OBLITERATE ||
1324
+ opArgs.op.type === MergeTreeDeltaType.OBLITERATE_SIDED;
1325
+ assert(hasObliterateInfo === isObliterate, 0xa40 /* must have obliterate info */);
1326
+ if (hasObliterateInfo) {
1327
+ obliterateInfo.stamp = { ...stamp, type: "sliceRemove" };
1328
+ this.obliterates.addOrUpdate(obliterateInfo);
1329
+ assert(
1330
+ obliterateInfo.tiebreakTrackingGroup !== undefined,
1331
+ 0xb70 /* obliterateInfo should have a tiebreak tracking group on ack */,
1332
+ );
1333
+ for (const segment of obliterateInfo.tiebreakTrackingGroup.tracked) {
1334
+ segment.trackingCollection.unlink(obliterateInfo.tiebreakTrackingGroup);
1335
+ assert(
1336
+ isSegmentLeaf(segment) && isInsideObliterate(segment),
1337
+ 0xb71 /* Expected segment leaf inside obliterate */,
1338
+ );
1339
+ segment.insertionRefSeqStamp = undefined;
1340
+ }
1341
+ obliterateInfo.tiebreakTrackingGroup = undefined;
1342
+ }
1343
+
1264
1344
  const deltaSegments: IMergeTreeSegmentDelta[] = [];
1265
1345
  const overlappingRemoves: boolean[] = [];
1266
- pendingSegmentGroup.segments.map((pendingSegment: ISegmentLeaf) => {
1346
+ segments.map((pendingSegment: ISegmentLeaf) => {
1267
1347
  const overlappingRemove = !ackSegment(
1268
1348
  pendingSegment,
1269
1349
  pendingSegmentGroup,
@@ -1285,11 +1365,6 @@ export class MergeTree {
1285
1365
  });
1286
1366
  });
1287
1367
 
1288
- if (pendingSegmentGroup.obliterateInfo !== undefined) {
1289
- pendingSegmentGroup.obliterateInfo.stamp = { type: "sliceRemove", ...stamp };
1290
- this.obliterates.addOrUpdate(pendingSegmentGroup.obliterateInfo);
1291
- }
1292
-
1293
1368
  // Perform slides after all segments have been acked, so that
1294
1369
  // positions after slide are final
1295
1370
  if (
@@ -1297,7 +1372,7 @@ export class MergeTree {
1297
1372
  opArgs.op.type === MergeTreeDeltaType.OBLITERATE ||
1298
1373
  opArgs.op.type === MergeTreeDeltaType.OBLITERATE_SIDED
1299
1374
  ) {
1300
- this.slideAckedRemovedSegmentReferences(pendingSegmentGroup.segments);
1375
+ this.slideAckedRemovedSegmentReferences(segments);
1301
1376
  }
1302
1377
 
1303
1378
  this.mergeTreeMaintenanceCallback?.(
@@ -1326,6 +1401,10 @@ export class MergeTree {
1326
1401
  ): SegmentGroup {
1327
1402
  let _segmentGroup = segmentGroup;
1328
1403
  if (_segmentGroup === undefined) {
1404
+ assert(
1405
+ localSeq !== undefined,
1406
+ 0xb72 /* Local seq should be passed when creating new segment group */,
1407
+ );
1329
1408
  _segmentGroup = {
1330
1409
  segments: [],
1331
1410
  localSeq,
@@ -1550,16 +1629,17 @@ export class MergeTree {
1550
1629
  continue;
1551
1630
  }
1552
1631
 
1632
+ const refSeqStamp: OperationStamp = {
1633
+ seq: perspective.refSeq,
1634
+ clientId: stamp.clientId,
1635
+ };
1636
+
1553
1637
  const overlappingAckedObliterates: RemoveOperationStamp[] = [];
1554
1638
  let oldest: ObliterateInfo | undefined;
1555
1639
  let newest: ObliterateInfo | undefined;
1556
1640
  let newestAcked: ObliterateInfo | undefined;
1557
1641
  let oldestUnacked: ObliterateInfo | undefined;
1558
- const refSeqStamp: OperationStamp = {
1559
- seq: perspective.refSeq,
1560
- clientId: stamp.clientId,
1561
- localSeq: stamp.localSeq,
1562
- };
1642
+
1563
1643
  for (const ob of this.obliterates.findOverlapping(newSegment)) {
1564
1644
  if (opstampUtils.greaterThan(ob.stamp, refSeqStamp)) {
1565
1645
  // Any obliterate from the same client that's inserting this segment cannot cause the segment to be marked as
@@ -1601,7 +1681,19 @@ export class MergeTree {
1601
1681
  }
1602
1682
  }
1603
1683
 
1604
- newSegment.obliteratePrecedingInsertion = newest;
1684
+ overwriteInfo<ISegmentInsideObliterateInfo>(newSegment, {
1685
+ obliteratePrecedingInsertion: newest,
1686
+ });
1687
+ if (newest !== undefined && opstampUtils.isLocal(newest.stamp)) {
1688
+ assert(
1689
+ newest?.tiebreakTrackingGroup !== undefined,
1690
+ 0xb73 /* Expected local obliterateinfo to have tiebreak group */,
1691
+ );
1692
+ newest.tiebreakTrackingGroup.link(newSegment);
1693
+ overwriteInfo<ISegmentInsideObliterateInfo>(newSegment, {
1694
+ insertionRefSeqStamp: refSeqStamp,
1695
+ });
1696
+ }
1605
1697
  // See doc comment on obliteratePrecedingInsertion for more details: if the newest obliterate was performed
1606
1698
  // by the same client that's inserting this segment, we let them insert into this range and therefore don't
1607
1699
  // mark it obliterated.
@@ -1642,6 +1734,22 @@ export class MergeTree {
1642
1734
  }
1643
1735
  }
1644
1736
 
1737
+ private computeObliteratePrecedingInsertion(
1738
+ segment: ISegmentLeaf,
1739
+ refSeqStamp: OperationStamp,
1740
+ ): ObliterateInfo | undefined {
1741
+ let newest: ObliterateInfo | undefined;
1742
+ for (const ob of this.obliterates.findOverlapping(segment)) {
1743
+ if (
1744
+ opstampUtils.greaterThan(ob.stamp, refSeqStamp) &&
1745
+ (newest === undefined || opstampUtils.greaterThan(ob.stamp, newest.stamp))
1746
+ ) {
1747
+ newest = ob;
1748
+ }
1749
+ }
1750
+ return newest;
1751
+ }
1752
+
1645
1753
  private readonly splitLeafSegment = (
1646
1754
  segment: ISegmentLeaf | undefined,
1647
1755
  pos: number,
@@ -1658,13 +1766,8 @@ export class MergeTree {
1658
1766
  segment.segmentGroups.copyTo(next.segmentGroups);
1659
1767
  }
1660
1768
 
1661
- if (segment.obliteratePrecedingInsertion) {
1662
- next.obliteratePrecedingInsertion = segment.obliteratePrecedingInsertion;
1663
- }
1664
1769
  copyPropertiesAndManager(segment, next);
1665
- if (segment.localRefs) {
1666
- segment.localRefs.split(pos, next);
1667
- }
1770
+ segment.localRefs?.split(pos, next);
1668
1771
 
1669
1772
  this.mergeTreeMaintenanceCallback?.(
1670
1773
  {
@@ -1712,7 +1815,14 @@ export class MergeTree {
1712
1815
  stamp: OperationStamp,
1713
1816
  context: InsertContext,
1714
1817
  ): void {
1715
- const { remainder } = this.insertRecursive(this.root, pos, perspective, stamp, context);
1818
+ const { remainder } = this.insertRecursive(
1819
+ this.root,
1820
+ pos,
1821
+ perspective,
1822
+ stamp,
1823
+ context,
1824
+ true,
1825
+ );
1716
1826
  if (remainder !== undefined) {
1717
1827
  this.updateRoot(remainder);
1718
1828
  }
@@ -1724,7 +1834,7 @@ export class MergeTree {
1724
1834
  perspective: Perspective,
1725
1835
  stamp: OperationStamp,
1726
1836
  context: InsertContext,
1727
- isLastChildBlock: boolean = true,
1837
+ isLastBlock: boolean,
1728
1838
  ): InsertResult {
1729
1839
  let _pos: number = pos;
1730
1840
 
@@ -1736,10 +1846,12 @@ export class MergeTree {
1736
1846
  let hadChanges = false;
1737
1847
  for (childIndex = 0; childIndex < block.childCount; childIndex++) {
1738
1848
  child = children[childIndex];
1739
- // ensure we walk down the far edge of the tree, even if all sub-tree is eligible for zamboni
1740
- const isLastNonLeafBlock =
1741
- isLastChildBlock && !child.isLeaf() && childIndex === block.childCount - 1;
1742
- const len = this.nodeLength(child, perspective) ?? (isLastChildBlock ? 0 : undefined);
1849
+ // removed blocks below the min seq will have an undefined length, and be skipped
1850
+ // however if it is the last block in the layer of the tree we don't want to skip it, so we correctly
1851
+ // walk down the far edge of the tree.
1852
+ const isLastChildOfLastBlock = isLastBlock && childIndex === block.childCount - 1;
1853
+ const len =
1854
+ this.nodeLength(child, perspective) ?? (isLastChildOfLastBlock ? 0 : undefined);
1743
1855
 
1744
1856
  if (len === undefined) {
1745
1857
  // if the seg len is undefined, the segment
@@ -1767,15 +1879,14 @@ export class MergeTree {
1767
1879
  return { remainder: undefined, hadChanges };
1768
1880
  }
1769
1881
  } else {
1770
- const childBlock = child;
1771
1882
  // Internal node
1772
1883
  const insertResult = this.insertRecursive(
1773
- childBlock,
1884
+ child,
1774
1885
  _pos,
1775
1886
  perspective,
1776
1887
  stamp,
1777
1888
  context,
1778
- isLastNonLeafBlock,
1889
+ isLastChildOfLastBlock,
1779
1890
  );
1780
1891
  hadChanges ||= insertResult.hadChanges;
1781
1892
  if (insertResult.remainder === undefined) {
@@ -1971,50 +2082,51 @@ export class MergeTree {
1971
2082
  const localOverlapWithRefs: ISegmentLeaf[] = [];
1972
2083
  const removedSegments: SegmentWithInfo<IHasRemovalInfo, ISegmentLeaf>[] = [];
1973
2084
 
2085
+ const createRefFromSequencePlace = (
2086
+ place: InteriorSequencePlace,
2087
+ ): LocalReferencePosition => {
2088
+ const { segment: placeSeg, offset: placeOffset } = this.getContainingSegment(
2089
+ place.pos,
2090
+ perspective,
2091
+ );
2092
+ assert(
2093
+ isSegmentLeaf(placeSeg) && placeOffset !== undefined,
2094
+ 0xa3f /* segments cannot be undefined */,
2095
+ );
2096
+ return this.createLocalReferencePosition(
2097
+ placeSeg,
2098
+ placeOffset,
2099
+ ReferenceType.StayOnRemove,
2100
+ undefined,
2101
+ );
2102
+ };
2103
+
1974
2104
  const obliterate: ObliterateInfo = {
1975
- start: createDetachedLocalReferencePosition(undefined),
1976
- end: createDetachedLocalReferencePosition(undefined),
2105
+ start: createRefFromSequencePlace(start),
2106
+ startSide: start.side,
2107
+ end: createRefFromSequencePlace(end),
2108
+ endSide: end.side,
1977
2109
  refSeq: perspective.refSeq,
1978
2110
  stamp,
1979
2111
  segmentGroup: undefined,
2112
+ tiebreakTrackingGroup: undefined,
1980
2113
  };
2114
+ // Link references back to this obliterate info
2115
+ obliterate.start.addProperties({ obliterate });
2116
+ obliterate.end.addProperties({ obliterate });
1981
2117
 
1982
- const { segment: startSeg } = this.getContainingSegment(start.pos, perspective);
1983
- const { segment: endSeg } = this.getContainingSegment(end.pos, perspective);
1984
- assert(
1985
- isSegmentLeaf(startSeg) && isSegmentLeaf(endSeg),
1986
- 0xa3f /* segments cannot be undefined */,
1987
- );
1988
-
1989
- obliterate.start = this.createLocalReferencePosition(
1990
- startSeg,
1991
- start.side === Side.Before ? 0 : Math.max(startSeg.cachedLength - 1, 0),
1992
- ReferenceType.StayOnRemove,
1993
- {
1994
- obliterate,
1995
- },
1996
- );
1997
-
1998
- obliterate.end = this.createLocalReferencePosition(
1999
- endSeg,
2000
- end.side === Side.Before ? 0 : Math.max(endSeg.cachedLength - 1, 0),
2001
- ReferenceType.StayOnRemove,
2002
- {
2003
- obliterate,
2004
- },
2005
- );
2006
-
2007
- // Always create a segment group for obliterate,
2008
- // even if there are no segments currently in the obliteration range.
2009
- // Segments may be concurrently inserted into the obliteration range,
2010
- // at which point they are added to the segment group.
2011
- obliterate.segmentGroup = {
2012
- segments: [],
2013
- localSeq: stamp.localSeq,
2014
- refSeq: this.collabWindow.currentSeq,
2015
- obliterateInfo: obliterate,
2016
- };
2017
2118
  if (this.collabWindow.collaborating && stamp.clientId === this.collabWindow.clientId) {
2119
+ // Always create a segment group for local obliterates,
2120
+ // even if there are no segments currently in the obliteration range.
2121
+ // Segments may be concurrently inserted into the obliteration range,
2122
+ // at which point they are added to the segment group.
2123
+ obliterate.segmentGroup = {
2124
+ segments: [],
2125
+ localSeq: stamp.localSeq,
2126
+ refSeq: this.collabWindow.currentSeq,
2127
+ obliterateInfo: obliterate,
2128
+ };
2129
+ obliterate.tiebreakTrackingGroup = new UnorderedTrackingGroup();
2018
2130
  this.pendingSegments.push(obliterate.segmentGroup);
2019
2131
  }
2020
2132
  this.obliterates.addOrUpdate(obliterate);
@@ -2036,6 +2148,7 @@ export class MergeTree {
2036
2148
  // will win against the obliterate we're processing, hence the early exit.
2037
2149
  if (
2038
2150
  opstampUtils.isLocal(segment.insert) &&
2151
+ isInsideObliterate(segment) &&
2039
2152
  segment.obliteratePrecedingInsertion?.stamp.seq === UnassignedSequenceNumber &&
2040
2153
  opstampUtils.isAcked(stamp)
2041
2154
  ) {
@@ -2263,6 +2376,10 @@ export class MergeTree {
2263
2376
  * Revert an unacked local op
2264
2377
  */
2265
2378
  public rollback(op: IMergeTreeDeltaOp, localOpMetadata: SegmentGroup): void {
2379
+ const rollbackStamp: OperationStamp = {
2380
+ seq: TreeMaintenanceSequenceNumber,
2381
+ clientId: NonCollabClient,
2382
+ };
2266
2383
  if (op.type === MergeTreeDeltaType.REMOVE) {
2267
2384
  const pendingSegmentGroup = this.pendingSegments.pop()?.data;
2268
2385
  if (pendingSegmentGroup === undefined || pendingSegmentGroup !== localOpMetadata) {
@@ -2283,17 +2400,11 @@ export class MergeTree {
2283
2400
  segment.removes[0].type === "setRemove",
2284
2401
  0x39d /* Rollback segment removedClientId does not match local client */,
2285
2402
  );
2286
- let updateNode: MergeBlock | undefined = segment.parent;
2287
2403
  // This also removes obliterates, but that should be ok as we can only remove a segment once.
2288
2404
  // If we were able to remove it locally, that also means there are no remote removals (since rollback is synchronous).
2289
2405
  removeRemovalInfo(segment);
2290
2406
 
2291
- for (updateNode; updateNode !== undefined; updateNode = updateNode.parent) {
2292
- this.blockUpdateLength(updateNode, {
2293
- seq: UnassignedSequenceNumber,
2294
- clientId: this.collabWindow.clientId,
2295
- });
2296
- }
2407
+ this.blockUpdatePathLengths(segment.parent, rollbackStamp);
2297
2408
 
2298
2409
  // Note: optional chaining short-circuits:
2299
2410
  // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Optional_chaining#short-circuiting
@@ -2332,14 +2443,12 @@ export class MergeTree {
2332
2443
  if (op.type === MergeTreeDeltaType.INSERT) {
2333
2444
  segment.insert = {
2334
2445
  type: "insert",
2335
- seq: UniversalSequenceNumber,
2336
- clientId: this.collabWindow.clientId,
2446
+ ...rollbackStamp,
2337
2447
  };
2338
2448
  const removeOp = createRemoveRangeOp(start, start + segment.cachedLength);
2339
2449
  const removeStamp: SetRemoveOperationStamp = {
2340
2450
  type: "setRemove",
2341
- seq: UniversalSequenceNumber,
2342
- clientId: this.collabWindow.clientId,
2451
+ ...rollbackStamp,
2343
2452
  };
2344
2453
  this.markRangeRemoved(
2345
2454
  start,
@@ -2351,16 +2460,13 @@ export class MergeTree {
2351
2460
  } /* op.type === MergeTreeDeltaType.ANNOTATE */ else {
2352
2461
  const props = pendingSegmentGroup.previousProps![i];
2353
2462
  const annotateOp = createAnnotateRangeOp(start, start + segment.cachedLength, props);
2354
- const annotateStamp: OperationStamp = {
2355
- seq: UniversalSequenceNumber,
2356
- clientId: this.collabWindow.clientId,
2357
- };
2463
+
2358
2464
  this.annotateRange(
2359
2465
  start,
2360
2466
  start + segment.cachedLength,
2361
2467
  { props },
2362
2468
  this.localPerspective,
2363
- annotateStamp,
2469
+ rollbackStamp,
2364
2470
  { op: annotateOp, rollback: true },
2365
2471
  );
2366
2472
  i++;
@@ -2457,7 +2563,17 @@ export class MergeTree {
2457
2563
  return segRef;
2458
2564
  }
2459
2565
 
2460
- // Segments should either be removed remotely, removed locally, or inserted locally
2566
+ /**
2567
+ * Segments should either be removed remotely, removed locally, or inserted locally
2568
+ *
2569
+ * See description of {@link normalizeSegmentsOnRebase}.
2570
+ *
2571
+ * This normalizes a block of adjacent segments whose positions have collapsed between the time of the original submission and now
2572
+ * such that removed segments come after ones that still exist.
2573
+ *
2574
+ * TODO:AB#34898: It looks like this method has some bugs, search code for this tag for an example test that demonstrates
2575
+ * segment normalization yielding an order that remote clients wouldn't have seen.
2576
+ */
2461
2577
  private normalizeAdjacentSegments(affectedSegments: DoublyLinkedList<ISegmentLeaf>): void {
2462
2578
  // Eagerly demand this since we're about to shift elements in the list around
2463
2579
  const currentOrder = Array.from(affectedSegments, ({ data: seg }) => ({
@@ -2612,6 +2728,57 @@ export class MergeTree {
2612
2728
  });
2613
2729
 
2614
2730
  normalize();
2731
+ this.obliterates.onNormalize();
2732
+ const segmentTiebreakChanges = new Set<{
2733
+ segment: ISegmentLeaf;
2734
+ old: ObliterateInfo;
2735
+ new: ObliterateInfo | undefined;
2736
+ }>();
2737
+ for (const info of this.obliterates) {
2738
+ if (info.tiebreakTrackingGroup !== undefined) {
2739
+ for (const segment of info.tiebreakTrackingGroup.tracked) {
2740
+ // Recompute previous obliterate
2741
+ assert(
2742
+ isSegmentLeaf(segment) &&
2743
+ isInsideObliterate(segment) &&
2744
+ segment.insertionRefSeqStamp !== undefined,
2745
+ 0xb74 /* Expected segment leaf inside obliterate with insertionRefSeqStamp */,
2746
+ );
2747
+ // This may have changed as a result of segments shuffling: outstanding local obliterates that previously surrounded a segment may no longer surround it.
2748
+ const newObliteratePrecedingInsertion = this.computeObliteratePrecedingInsertion(
2749
+ segment,
2750
+ segment.insertionRefSeqStamp,
2751
+ );
2752
+
2753
+ if (newObliteratePrecedingInsertion !== info) {
2754
+ segmentTiebreakChanges.add({
2755
+ segment,
2756
+ old: info,
2757
+ new: newObliteratePrecedingInsertion,
2758
+ });
2759
+ }
2760
+ }
2761
+ }
2762
+ }
2763
+
2764
+ for (const { segment, old, new: newInfo } of segmentTiebreakChanges) {
2765
+ // Update tiebreak tracking groups on old/new segment as well as `ISegmentInsideObliterateInfo` state
2766
+ // which we only keep around as long as obliterates are in flight.
2767
+ old.tiebreakTrackingGroup?.unlink(segment);
2768
+ if (newInfo?.tiebreakTrackingGroup === undefined) {
2769
+ // Segment is either no longer inside any obliterate or only inside acked obliterates.
2770
+ overwriteInfo<ISegmentInsideObliterateInfo>(segment, {
2771
+ obliteratePrecedingInsertion: newInfo,
2772
+ insertionRefSeqStamp: undefined,
2773
+ });
2774
+ } else {
2775
+ overwriteInfo<ISegmentInsideObliterateInfo>(segment, {
2776
+ obliteratePrecedingInsertion: newInfo,
2777
+ });
2778
+
2779
+ newInfo.tiebreakTrackingGroup.link(segment);
2780
+ }
2781
+ }
2615
2782
  }
2616
2783
  private blockUpdate(block: MergeBlock): void {
2617
2784
  let len: number | undefined;