@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/lib/mergeTree.js CHANGED
@@ -7,20 +7,21 @@
7
7
  import { assert, Heap } from "@fluidframework/core-utils/internal";
8
8
  import { DataProcessingError, UsageError } from "@fluidframework/telemetry-utils/internal";
9
9
  import { DoublyLinkedList } from "./collections/index.js";
10
- import { NonCollabClient, TreeMaintenanceSequenceNumber, UnassignedSequenceNumber, UniversalSequenceNumber, } from "./constants.js";
10
+ import { NonCollabClient, TreeMaintenanceSequenceNumber, UnassignedSequenceNumber, } from "./constants.js";
11
11
  import { EndOfTreeSegment, StartOfTreeSegment } from "./endOfTreeSegment.js";
12
- import { LocalReferenceCollection, SlidingPreference, anyLocalReferencePosition, createDetachedLocalReferencePosition, filterLocalReferencePositions, } from "./localReference.js";
12
+ import { LocalReferenceCollection, SlidingPreference, anyLocalReferencePosition, filterLocalReferencePositions, } from "./localReference.js";
13
13
  import { MergeTreeMaintenanceType, } from "./mergeTreeDeltaCallback.js";
14
14
  import { LeafAction, NodeAction, backwardExcursion, depthFirstNodeWalk, forwardExcursion, walkAllChildSegments, } from "./mergeTreeNodeWalk.js";
15
15
  import { CollaborationWindow, Marker, MaxNodesInBlock, MergeBlock, assertSegmentLeaf, assignChild, getMinSeqPerspective, getMinSeqStamp, isSegmentLeaf, reservedMarkerIdKey, } from "./mergeTreeNodes.js";
16
+ import { UnorderedTrackingGroup } from "./mergeTreeTracking.js";
16
17
  import { createAnnotateRangeOp, createInsertSegmentOp, createRemoveRangeOp, } from "./opBuilder.js";
17
18
  import { MergeTreeDeltaType, ReferenceType, } from "./ops.js";
18
19
  import { PartialSequenceLengths } from "./partialLengths.js";
19
- import { PriorPerspective, LocalReconnectingPerspective, LocalDefaultPerspective, RemoteObliteratePerspective, } from "./perspective.js";
20
+ import { PriorPerspective, LocalReconnectingPerspective, LocalDefaultPerspective, RemoteObliteratePerspective, allAckedChangesPerspective, } from "./perspective.js";
20
21
  import { createMap, extend, extendIfUndefined } from "./properties.js";
21
22
  import { DetachedReferencePosition, refGetTileLabels, refHasTileLabel, refTypeIncludesFlag, } from "./referencePositions.js";
22
23
  import { SegmentGroupCollection } from "./segmentGroupCollection.js";
23
- import { assertRemoved, isMergeNodeInfo, isRemoved, overwriteInfo, removeRemovalInfo, toRemovalInfo, } from "./segmentInfos.js";
24
+ import { assertRemoved, isInsideObliterate, isMergeNodeInfo, isRemoved, overwriteInfo, removeRemovalInfo, toRemovalInfo, } from "./segmentInfos.js";
24
25
  import { copyPropertiesAndManager, PropertiesManager, } from "./segmentPropertiesManager.js";
25
26
  import { Side } from "./sequencePlace.js";
26
27
  import { SortedSegmentSet } from "./sortedSegmentSet.js";
@@ -74,13 +75,6 @@ function ackSegment(segment, segmentGroup, opArgs, stamp) {
74
75
  type: op.type === MergeTreeDeltaType.REMOVE ? "setRemove" : "sliceRemove",
75
76
  };
76
77
  segment.removes[segment.removes.length - 1] = removeStamp;
77
- const { obliterateInfo } = segmentGroup;
78
- const hasObliterateInfo = obliterateInfo !== undefined;
79
- const isObliterate = op.type !== MergeTreeDeltaType.REMOVE;
80
- assert(hasObliterateInfo === isObliterate, 0xa40 /* must have obliterate info */);
81
- if (hasObliterateInfo) {
82
- obliterateInfo.stamp = removeStamp;
83
- }
84
78
  break;
85
79
  }
86
80
  default: {
@@ -117,8 +111,10 @@ export function findRootMergeBlock(segmentOrNode) {
117
111
  * This can reduce the number of times the tree needs to be scanned if a range containing many
118
112
  * SlideOnRemove references is removed.
119
113
  */
120
- function getSlideToSegment(segment, slidingPreference = SlidingPreference.FORWARD, cache, useNewSlidingBehavior = false) {
121
- if (!segment || !isRemovedAndAcked(segment) || segment.endpointType !== undefined) {
114
+ function getSlideToSegment(segment, slidingPreference = SlidingPreference.FORWARD, perspective, cache, useNewSlidingBehavior = false) {
115
+ if (!segment ||
116
+ perspective.isSegmentPresent(segment) ||
117
+ segment.endpointType !== undefined) {
122
118
  return [segment, undefined];
123
119
  }
124
120
  const cachedSegment = cache?.get(segment);
@@ -128,7 +124,7 @@ function getSlideToSegment(segment, slidingPreference = SlidingPreference.FORWAR
128
124
  const result = {};
129
125
  cache?.set(segment, result);
130
126
  const goFurtherToFindSlideToSegment = (seg) => {
131
- if (opstampUtils.isAcked(seg.insert) && !isRemovedAndAcked(seg)) {
127
+ if (perspective.isSegmentPresent(seg)) {
132
128
  result.seg = seg;
133
129
  return false;
134
130
  }
@@ -179,11 +175,11 @@ function getSlideToSegment(segment, slidingPreference = SlidingPreference.FORWAR
179
175
  * @returns segment and offset to slide the reference to
180
176
  * @internal
181
177
  */
182
- export function getSlideToSegoff(segoff, slidingPreference = SlidingPreference.FORWARD, useNewSlidingBehavior = false) {
178
+ export function getSlideToSegoff(segoff, slidingPreference = SlidingPreference.FORWARD, perspective = allAckedChangesPerspective, useNewSlidingBehavior = false) {
183
179
  if (!isSegmentLeaf(segoff.segment)) {
184
180
  return segoff;
185
181
  }
186
- const [segment, _] = getSlideToSegment(segoff.segment, slidingPreference, undefined, useNewSlidingBehavior);
182
+ const [segment, _] = getSlideToSegment(segoff.segment, slidingPreference, perspective, undefined, useNewSlidingBehavior);
187
183
  if (segment === segoff.segment) {
188
184
  return segoff;
189
185
  }
@@ -224,6 +220,9 @@ class Obliterates {
224
220
  this.mergeTree.removeLocalReferencePosition(ob.data.end);
225
221
  }
226
222
  }
223
+ onNormalize() {
224
+ this.startOrdered.onSortOrderChange();
225
+ }
227
226
  addOrUpdate(obliterateInfo) {
228
227
  const { stamp: { seq }, start, } = obliterateInfo;
229
228
  if (seq !== UnassignedSequenceNumber) {
@@ -252,6 +251,39 @@ class Obliterates {
252
251
  }
253
252
  return overlapping;
254
253
  }
254
+ /**
255
+ * Remove a local obliterate from this data structure.
256
+ * @privateRemarks
257
+ * This data structure could support removing non-local obliterates if we wanted it to, but when adding support for that
258
+ * we should reconsider the indexing structure for seq ordered obliterates (right now it would be an O(# obliterates) operation)
259
+ */
260
+ removeLocalObliterate(obliterateInfo) {
261
+ assert(obliterateInfo.stamp.seq === UnassignedSequenceNumber, 0xb6e /* Expected local obliterate */);
262
+ this.startOrdered.remove(obliterateInfo.start);
263
+ }
264
+ /**
265
+ * Returns an iterator over the `ObliterateInfo` for all obliterates in the collab window. Obliterates are not guaranteed to be ordered.
266
+ * The iterator is not guaranteed to be valid over edits to the set of obliterates.
267
+ */
268
+ [Symbol.iterator]() {
269
+ let index = 0;
270
+ const { items: starts } = this.startOrdered;
271
+ const iterator = {
272
+ next() {
273
+ if (index < starts.length) {
274
+ const start = starts[index++];
275
+ const info = start.properties?.obliterate;
276
+ assert(info?.start !== undefined && info?.end !== undefined, 0xb6f /* Expected obliterateInfo endpoint to map to its obliterate */);
277
+ return { value: info, done: false };
278
+ }
279
+ return { value: undefined, done: true };
280
+ },
281
+ [Symbol.iterator]() {
282
+ return this;
283
+ },
284
+ };
285
+ return iterator;
286
+ }
255
287
  }
256
288
  /**
257
289
  * @internal
@@ -287,13 +319,8 @@ export class MergeTree {
287
319
  next.segmentGroups ??= new SegmentGroupCollection(next);
288
320
  segment.segmentGroups.copyTo(next.segmentGroups);
289
321
  }
290
- if (segment.obliteratePrecedingInsertion) {
291
- next.obliteratePrecedingInsertion = segment.obliteratePrecedingInsertion;
292
- }
293
322
  copyPropertiesAndManager(segment, next);
294
- if (segment.localRefs) {
295
- segment.localRefs.split(pos, next);
296
- }
323
+ segment.localRefs?.split(pos, next);
297
324
  this.mergeTreeMaintenanceCallback?.({
298
325
  operation: MergeTreeMaintenanceType.SPLIT,
299
326
  deltaSegments: [{ segment }, { segment: next }],
@@ -306,6 +333,12 @@ export class MergeTree {
306
333
  this._root.mergeTree = this;
307
334
  this.attributionPolicy = options?.attribution?.policyFactory?.();
308
335
  }
336
+ rebaseObliterateTo(existing, newObliterate) {
337
+ this.obliterates.removeLocalObliterate(existing);
338
+ if (newObliterate !== undefined) {
339
+ this.obliterates.addOrUpdate(newObliterate);
340
+ }
341
+ }
309
342
  get root() {
310
343
  return this._root;
311
344
  }
@@ -519,7 +552,7 @@ export class MergeTree {
519
552
  if (!segment.localRefs || !anyLocalReferencePosition(segment.localRefs, pred)) {
520
553
  return;
521
554
  }
522
- const [slideToSegment, maybeEndpoint] = getSlideToSegment(segment, slidingPreference, slidingPreference === SlidingPreference.FORWARD
555
+ const [slideToSegment, maybeEndpoint] = getSlideToSegment(segment, slidingPreference, allAckedChangesPerspective, slidingPreference === SlidingPreference.FORWARD
523
556
  ? forwardSegmentCache
524
557
  : backwardSegmentCache, this.options?.mergeTreeReferencesCanSlideToEndpoint);
525
558
  const slideIsForward = slideToSegment === undefined ? false : slideToSegment.ordinal > segment.ordinal;
@@ -720,10 +753,9 @@ export class MergeTree {
720
753
  this.nodeUpdateLengthNewStructure(this.root);
721
754
  }
722
755
  /**
723
- * Assign sequence number to existing segment; update partial lengths to reflect the change
724
- * @param seq - sequence number given by server to pending segment
756
+ * Assign sequence number to existing segments affected by an op; update partial lengths to reflect the change
725
757
  */
726
- ackPendingSegment(opArgs) {
758
+ ackOp(opArgs) {
727
759
  const seq = opArgs.sequencedMessage.sequenceNumber;
728
760
  const stamp = {
729
761
  seq,
@@ -733,9 +765,25 @@ export class MergeTree {
733
765
  const nodesToUpdate = [];
734
766
  let overwrite = false;
735
767
  if (pendingSegmentGroup !== undefined) {
768
+ const { obliterateInfo, segments } = pendingSegmentGroup;
769
+ const hasObliterateInfo = obliterateInfo !== undefined;
770
+ const isObliterate = opArgs.op.type === MergeTreeDeltaType.OBLITERATE ||
771
+ opArgs.op.type === MergeTreeDeltaType.OBLITERATE_SIDED;
772
+ assert(hasObliterateInfo === isObliterate, 0xa40 /* must have obliterate info */);
773
+ if (hasObliterateInfo) {
774
+ obliterateInfo.stamp = { ...stamp, type: "sliceRemove" };
775
+ this.obliterates.addOrUpdate(obliterateInfo);
776
+ assert(obliterateInfo.tiebreakTrackingGroup !== undefined, 0xb70 /* obliterateInfo should have a tiebreak tracking group on ack */);
777
+ for (const segment of obliterateInfo.tiebreakTrackingGroup.tracked) {
778
+ segment.trackingCollection.unlink(obliterateInfo.tiebreakTrackingGroup);
779
+ assert(isSegmentLeaf(segment) && isInsideObliterate(segment), 0xb71 /* Expected segment leaf inside obliterate */);
780
+ segment.insertionRefSeqStamp = undefined;
781
+ }
782
+ obliterateInfo.tiebreakTrackingGroup = undefined;
783
+ }
736
784
  const deltaSegments = [];
737
785
  const overlappingRemoves = [];
738
- pendingSegmentGroup.segments.map((pendingSegment) => {
786
+ segments.map((pendingSegment) => {
739
787
  const overlappingRemove = !ackSegment(pendingSegment, pendingSegmentGroup, opArgs, stamp);
740
788
  overwrite ||= overlappingRemove;
741
789
  overlappingRemoves.push(overlappingRemove);
@@ -749,16 +797,12 @@ export class MergeTree {
749
797
  segment: pendingSegment,
750
798
  });
751
799
  });
752
- if (pendingSegmentGroup.obliterateInfo !== undefined) {
753
- pendingSegmentGroup.obliterateInfo.stamp = { type: "sliceRemove", ...stamp };
754
- this.obliterates.addOrUpdate(pendingSegmentGroup.obliterateInfo);
755
- }
756
800
  // Perform slides after all segments have been acked, so that
757
801
  // positions after slide are final
758
802
  if (opArgs.op.type === MergeTreeDeltaType.REMOVE ||
759
803
  opArgs.op.type === MergeTreeDeltaType.OBLITERATE ||
760
804
  opArgs.op.type === MergeTreeDeltaType.OBLITERATE_SIDED) {
761
- this.slideAckedRemovedSegmentReferences(pendingSegmentGroup.segments);
805
+ this.slideAckedRemovedSegmentReferences(segments);
762
806
  }
763
807
  this.mergeTreeMaintenanceCallback?.({
764
808
  deltaSegments,
@@ -775,6 +819,7 @@ export class MergeTree {
775
819
  addToPendingList(segment, segmentGroup, localSeq, previousProps) {
776
820
  let _segmentGroup = segmentGroup;
777
821
  if (_segmentGroup === undefined) {
822
+ assert(localSeq !== undefined, 0xb72 /* Local seq should be passed when creating new segment group */);
778
823
  _segmentGroup = {
779
824
  segments: [],
780
825
  localSeq,
@@ -952,16 +997,15 @@ export class MergeTree {
952
997
  saveIfLocal(newSegment);
953
998
  continue;
954
999
  }
1000
+ const refSeqStamp = {
1001
+ seq: perspective.refSeq,
1002
+ clientId: stamp.clientId,
1003
+ };
955
1004
  const overlappingAckedObliterates = [];
956
1005
  let oldest;
957
1006
  let newest;
958
1007
  let newestAcked;
959
1008
  let oldestUnacked;
960
- const refSeqStamp = {
961
- seq: perspective.refSeq,
962
- clientId: stamp.clientId,
963
- localSeq: stamp.localSeq,
964
- };
965
1009
  for (const ob of this.obliterates.findOverlapping(newSegment)) {
966
1010
  if (opstampUtils.greaterThan(ob.stamp, refSeqStamp)) {
967
1011
  // Any obliterate from the same client that's inserting this segment cannot cause the segment to be marked as
@@ -994,7 +1038,16 @@ export class MergeTree {
994
1038
  }
995
1039
  }
996
1040
  }
997
- newSegment.obliteratePrecedingInsertion = newest;
1041
+ overwriteInfo(newSegment, {
1042
+ obliteratePrecedingInsertion: newest,
1043
+ });
1044
+ if (newest !== undefined && opstampUtils.isLocal(newest.stamp)) {
1045
+ assert(newest?.tiebreakTrackingGroup !== undefined, 0xb73 /* Expected local obliterateinfo to have tiebreak group */);
1046
+ newest.tiebreakTrackingGroup.link(newSegment);
1047
+ overwriteInfo(newSegment, {
1048
+ insertionRefSeqStamp: refSeqStamp,
1049
+ });
1050
+ }
998
1051
  // See doc comment on obliteratePrecedingInsertion for more details: if the newest obliterate was performed
999
1052
  // by the same client that's inserting this segment, we let them insert into this range and therefore don't
1000
1053
  // mark it obliterated.
@@ -1025,6 +1078,16 @@ export class MergeTree {
1025
1078
  saveIfLocal(newSegment);
1026
1079
  }
1027
1080
  }
1081
+ computeObliteratePrecedingInsertion(segment, refSeqStamp) {
1082
+ let newest;
1083
+ for (const ob of this.obliterates.findOverlapping(segment)) {
1084
+ if (opstampUtils.greaterThan(ob.stamp, refSeqStamp) &&
1085
+ (newest === undefined || opstampUtils.greaterThan(ob.stamp, newest.stamp))) {
1086
+ newest = ob;
1087
+ }
1088
+ }
1089
+ return newest;
1090
+ }
1028
1091
  ensureIntervalBoundary(pos, perspective) {
1029
1092
  this.insertingWalk(pos, perspective, {
1030
1093
  seq: TreeMaintenanceSequenceNumber,
@@ -1047,12 +1110,12 @@ export class MergeTree {
1047
1110
  }
1048
1111
  }
1049
1112
  insertingWalk(pos, perspective, stamp, context) {
1050
- const { remainder } = this.insertRecursive(this.root, pos, perspective, stamp, context);
1113
+ const { remainder } = this.insertRecursive(this.root, pos, perspective, stamp, context, true);
1051
1114
  if (remainder !== undefined) {
1052
1115
  this.updateRoot(remainder);
1053
1116
  }
1054
1117
  }
1055
- insertRecursive(block, pos, perspective, stamp, context, isLastChildBlock = true) {
1118
+ insertRecursive(block, pos, perspective, stamp, context, isLastBlock) {
1056
1119
  let _pos = pos;
1057
1120
  const children = block.children;
1058
1121
  let childIndex;
@@ -1062,9 +1125,11 @@ export class MergeTree {
1062
1125
  let hadChanges = false;
1063
1126
  for (childIndex = 0; childIndex < block.childCount; childIndex++) {
1064
1127
  child = children[childIndex];
1065
- // ensure we walk down the far edge of the tree, even if all sub-tree is eligible for zamboni
1066
- const isLastNonLeafBlock = isLastChildBlock && !child.isLeaf() && childIndex === block.childCount - 1;
1067
- const len = this.nodeLength(child, perspective) ?? (isLastChildBlock ? 0 : undefined);
1128
+ // removed blocks below the min seq will have an undefined length, and be skipped
1129
+ // however if it is the last block in the layer of the tree we don't want to skip it, so we correctly
1130
+ // walk down the far edge of the tree.
1131
+ const isLastChildOfLastBlock = isLastBlock && childIndex === block.childCount - 1;
1132
+ const len = this.nodeLength(child, perspective) ?? (isLastChildOfLastBlock ? 0 : undefined);
1068
1133
  if (len === undefined) {
1069
1134
  // if the seg len is undefined, the segment
1070
1135
  // will be removed, so should just be skipped for now
@@ -1091,9 +1156,8 @@ export class MergeTree {
1091
1156
  }
1092
1157
  }
1093
1158
  else {
1094
- const childBlock = child;
1095
1159
  // Internal node
1096
- const insertResult = this.insertRecursive(childBlock, _pos, perspective, stamp, context, isLastNonLeafBlock);
1160
+ const insertResult = this.insertRecursive(child, _pos, perspective, stamp, context, isLastChildOfLastBlock);
1097
1161
  hadChanges ||= insertResult.hadChanges;
1098
1162
  if (insertResult.remainder === undefined) {
1099
1163
  if (insertResult.hadChanges) {
@@ -1240,33 +1304,36 @@ export class MergeTree {
1240
1304
  let _overwrite = false;
1241
1305
  const localOverlapWithRefs = [];
1242
1306
  const removedSegments = [];
1307
+ const createRefFromSequencePlace = (place) => {
1308
+ const { segment: placeSeg, offset: placeOffset } = this.getContainingSegment(place.pos, perspective);
1309
+ assert(isSegmentLeaf(placeSeg) && placeOffset !== undefined, 0xa3f /* segments cannot be undefined */);
1310
+ return this.createLocalReferencePosition(placeSeg, placeOffset, ReferenceType.StayOnRemove, undefined);
1311
+ };
1243
1312
  const obliterate = {
1244
- start: createDetachedLocalReferencePosition(undefined),
1245
- end: createDetachedLocalReferencePosition(undefined),
1313
+ start: createRefFromSequencePlace(start),
1314
+ startSide: start.side,
1315
+ end: createRefFromSequencePlace(end),
1316
+ endSide: end.side,
1246
1317
  refSeq: perspective.refSeq,
1247
1318
  stamp,
1248
1319
  segmentGroup: undefined,
1320
+ tiebreakTrackingGroup: undefined,
1249
1321
  };
1250
- const { segment: startSeg } = this.getContainingSegment(start.pos, perspective);
1251
- const { segment: endSeg } = this.getContainingSegment(end.pos, perspective);
1252
- assert(isSegmentLeaf(startSeg) && isSegmentLeaf(endSeg), 0xa3f /* segments cannot be undefined */);
1253
- obliterate.start = this.createLocalReferencePosition(startSeg, start.side === Side.Before ? 0 : Math.max(startSeg.cachedLength - 1, 0), ReferenceType.StayOnRemove, {
1254
- obliterate,
1255
- });
1256
- obliterate.end = this.createLocalReferencePosition(endSeg, end.side === Side.Before ? 0 : Math.max(endSeg.cachedLength - 1, 0), ReferenceType.StayOnRemove, {
1257
- obliterate,
1258
- });
1259
- // Always create a segment group for obliterate,
1260
- // even if there are no segments currently in the obliteration range.
1261
- // Segments may be concurrently inserted into the obliteration range,
1262
- // at which point they are added to the segment group.
1263
- obliterate.segmentGroup = {
1264
- segments: [],
1265
- localSeq: stamp.localSeq,
1266
- refSeq: this.collabWindow.currentSeq,
1267
- obliterateInfo: obliterate,
1268
- };
1322
+ // Link references back to this obliterate info
1323
+ obliterate.start.addProperties({ obliterate });
1324
+ obliterate.end.addProperties({ obliterate });
1269
1325
  if (this.collabWindow.collaborating && stamp.clientId === this.collabWindow.clientId) {
1326
+ // Always create a segment group for local obliterates,
1327
+ // even if there are no segments currently in the obliteration range.
1328
+ // Segments may be concurrently inserted into the obliteration range,
1329
+ // at which point they are added to the segment group.
1330
+ obliterate.segmentGroup = {
1331
+ segments: [],
1332
+ localSeq: stamp.localSeq,
1333
+ refSeq: this.collabWindow.currentSeq,
1334
+ obliterateInfo: obliterate,
1335
+ };
1336
+ obliterate.tiebreakTrackingGroup = new UnorderedTrackingGroup();
1270
1337
  this.pendingSegments.push(obliterate.segmentGroup);
1271
1338
  }
1272
1339
  this.obliterates.addOrUpdate(obliterate);
@@ -1284,6 +1351,7 @@ export class MergeTree {
1284
1351
  // Specifically, we want to avoid marking a local-only segment as obliterated when we know one of our own local obliterates
1285
1352
  // will win against the obliterate we're processing, hence the early exit.
1286
1353
  if (opstampUtils.isLocal(segment.insert) &&
1354
+ isInsideObliterate(segment) &&
1287
1355
  segment.obliteratePrecedingInsertion?.stamp.seq === UnassignedSequenceNumber &&
1288
1356
  opstampUtils.isAcked(stamp)) {
1289
1357
  // We chose to not obliterate this segment because we are aware of an unacked local obliteration.
@@ -1449,6 +1517,10 @@ export class MergeTree {
1449
1517
  * Revert an unacked local op
1450
1518
  */
1451
1519
  rollback(op, localOpMetadata) {
1520
+ const rollbackStamp = {
1521
+ seq: TreeMaintenanceSequenceNumber,
1522
+ clientId: NonCollabClient,
1523
+ };
1452
1524
  if (op.type === MergeTreeDeltaType.REMOVE) {
1453
1525
  const pendingSegmentGroup = this.pendingSegments.pop()?.data;
1454
1526
  if (pendingSegmentGroup === undefined || pendingSegmentGroup !== localOpMetadata) {
@@ -1462,16 +1534,10 @@ export class MergeTree {
1462
1534
  assert(isRemoved(segment) &&
1463
1535
  segment.removes[0].clientId === this.collabWindow.clientId &&
1464
1536
  segment.removes[0].type === "setRemove", 0x39d /* Rollback segment removedClientId does not match local client */);
1465
- let updateNode = segment.parent;
1466
1537
  // This also removes obliterates, but that should be ok as we can only remove a segment once.
1467
1538
  // If we were able to remove it locally, that also means there are no remote removals (since rollback is synchronous).
1468
1539
  removeRemovalInfo(segment);
1469
- for (updateNode; updateNode !== undefined; updateNode = updateNode.parent) {
1470
- this.blockUpdateLength(updateNode, {
1471
- seq: UnassignedSequenceNumber,
1472
- clientId: this.collabWindow.clientId,
1473
- });
1474
- }
1540
+ this.blockUpdatePathLengths(segment.parent, rollbackStamp);
1475
1541
  // Note: optional chaining short-circuits:
1476
1542
  // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Optional_chaining#short-circuiting
1477
1543
  this.mergeTreeDeltaCallback?.({
@@ -1499,25 +1565,19 @@ export class MergeTree {
1499
1565
  if (op.type === MergeTreeDeltaType.INSERT) {
1500
1566
  segment.insert = {
1501
1567
  type: "insert",
1502
- seq: UniversalSequenceNumber,
1503
- clientId: this.collabWindow.clientId,
1568
+ ...rollbackStamp,
1504
1569
  };
1505
1570
  const removeOp = createRemoveRangeOp(start, start + segment.cachedLength);
1506
1571
  const removeStamp = {
1507
1572
  type: "setRemove",
1508
- seq: UniversalSequenceNumber,
1509
- clientId: this.collabWindow.clientId,
1573
+ ...rollbackStamp,
1510
1574
  };
1511
1575
  this.markRangeRemoved(start, start + segment.cachedLength, this.localPerspective, removeStamp, { op: removeOp, rollback: true });
1512
1576
  } /* op.type === MergeTreeDeltaType.ANNOTATE */
1513
1577
  else {
1514
1578
  const props = pendingSegmentGroup.previousProps[i];
1515
1579
  const annotateOp = createAnnotateRangeOp(start, start + segment.cachedLength, props);
1516
- const annotateStamp = {
1517
- seq: UniversalSequenceNumber,
1518
- clientId: this.collabWindow.clientId,
1519
- };
1520
- this.annotateRange(start, start + segment.cachedLength, { props }, this.localPerspective, annotateStamp, { op: annotateOp, rollback: true });
1580
+ this.annotateRange(start, start + segment.cachedLength, { props }, this.localPerspective, rollbackStamp, { op: annotateOp, rollback: true });
1521
1581
  i++;
1522
1582
  }
1523
1583
  }
@@ -1578,7 +1638,17 @@ export class MergeTree {
1578
1638
  const segRef = localRefs.createLocalRef(offset, refType, properties, slidingPreference, canSlideToEndpoint);
1579
1639
  return segRef;
1580
1640
  }
1581
- // Segments should either be removed remotely, removed locally, or inserted locally
1641
+ /**
1642
+ * Segments should either be removed remotely, removed locally, or inserted locally
1643
+ *
1644
+ * See description of {@link normalizeSegmentsOnRebase}.
1645
+ *
1646
+ * This normalizes a block of adjacent segments whose positions have collapsed between the time of the original submission and now
1647
+ * such that removed segments come after ones that still exist.
1648
+ *
1649
+ * TODO:AB#34898: It looks like this method has some bugs, search code for this tag for an example test that demonstrates
1650
+ * segment normalization yielding an order that remote clients wouldn't have seen.
1651
+ */
1582
1652
  normalizeAdjacentSegments(affectedSegments) {
1583
1653
  // Eagerly demand this since we're about to shift elements in the list around
1584
1654
  const currentOrder = Array.from(affectedSegments, ({ data: seg }) => ({
@@ -1713,6 +1783,45 @@ export class MergeTree {
1713
1783
  return true;
1714
1784
  });
1715
1785
  normalize();
1786
+ this.obliterates.onNormalize();
1787
+ const segmentTiebreakChanges = new Set();
1788
+ for (const info of this.obliterates) {
1789
+ if (info.tiebreakTrackingGroup !== undefined) {
1790
+ for (const segment of info.tiebreakTrackingGroup.tracked) {
1791
+ // Recompute previous obliterate
1792
+ assert(isSegmentLeaf(segment) &&
1793
+ isInsideObliterate(segment) &&
1794
+ segment.insertionRefSeqStamp !== undefined, 0xb74 /* Expected segment leaf inside obliterate with insertionRefSeqStamp */);
1795
+ // This may have changed as a result of segments shuffling: outstanding local obliterates that previously surrounded a segment may no longer surround it.
1796
+ const newObliteratePrecedingInsertion = this.computeObliteratePrecedingInsertion(segment, segment.insertionRefSeqStamp);
1797
+ if (newObliteratePrecedingInsertion !== info) {
1798
+ segmentTiebreakChanges.add({
1799
+ segment,
1800
+ old: info,
1801
+ new: newObliteratePrecedingInsertion,
1802
+ });
1803
+ }
1804
+ }
1805
+ }
1806
+ }
1807
+ for (const { segment, old, new: newInfo } of segmentTiebreakChanges) {
1808
+ // Update tiebreak tracking groups on old/new segment as well as `ISegmentInsideObliterateInfo` state
1809
+ // which we only keep around as long as obliterates are in flight.
1810
+ old.tiebreakTrackingGroup?.unlink(segment);
1811
+ if (newInfo?.tiebreakTrackingGroup === undefined) {
1812
+ // Segment is either no longer inside any obliterate or only inside acked obliterates.
1813
+ overwriteInfo(segment, {
1814
+ obliteratePrecedingInsertion: newInfo,
1815
+ insertionRefSeqStamp: undefined,
1816
+ });
1817
+ }
1818
+ else {
1819
+ overwriteInfo(segment, {
1820
+ obliteratePrecedingInsertion: newInfo,
1821
+ });
1822
+ newInfo.tiebreakTrackingGroup.link(segment);
1823
+ }
1824
+ }
1716
1825
  }
1717
1826
  blockUpdate(block) {
1718
1827
  let len;