@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/dist/mergeTree.js CHANGED
@@ -39,6 +39,7 @@ const localReference_js_1 = require("./localReference.js");
39
39
  const mergeTreeDeltaCallback_js_1 = require("./mergeTreeDeltaCallback.js");
40
40
  const mergeTreeNodeWalk_js_1 = require("./mergeTreeNodeWalk.js");
41
41
  const mergeTreeNodes_js_1 = require("./mergeTreeNodes.js");
42
+ const mergeTreeTracking_js_1 = require("./mergeTreeTracking.js");
42
43
  const opBuilder_js_1 = require("./opBuilder.js");
43
44
  const ops_js_1 = require("./ops.js");
44
45
  const partialLengths_js_1 = require("./partialLengths.js");
@@ -101,13 +102,6 @@ function ackSegment(segment, segmentGroup, opArgs, stamp) {
101
102
  type: op.type === ops_js_1.MergeTreeDeltaType.REMOVE ? "setRemove" : "sliceRemove",
102
103
  };
103
104
  segment.removes[segment.removes.length - 1] = removeStamp;
104
- const { obliterateInfo } = segmentGroup;
105
- const hasObliterateInfo = obliterateInfo !== undefined;
106
- const isObliterate = op.type !== ops_js_1.MergeTreeDeltaType.REMOVE;
107
- (0, internal_1.assert)(hasObliterateInfo === isObliterate, 0xa40 /* must have obliterate info */);
108
- if (hasObliterateInfo) {
109
- obliterateInfo.stamp = removeStamp;
110
- }
111
105
  break;
112
106
  }
113
107
  default: {
@@ -146,8 +140,10 @@ exports.findRootMergeBlock = findRootMergeBlock;
146
140
  * This can reduce the number of times the tree needs to be scanned if a range containing many
147
141
  * SlideOnRemove references is removed.
148
142
  */
149
- function getSlideToSegment(segment, slidingPreference = localReference_js_1.SlidingPreference.FORWARD, cache, useNewSlidingBehavior = false) {
150
- if (!segment || !isRemovedAndAcked(segment) || segment.endpointType !== undefined) {
143
+ function getSlideToSegment(segment, slidingPreference = localReference_js_1.SlidingPreference.FORWARD, perspective, cache, useNewSlidingBehavior = false) {
144
+ if (!segment ||
145
+ perspective.isSegmentPresent(segment) ||
146
+ segment.endpointType !== undefined) {
151
147
  return [segment, undefined];
152
148
  }
153
149
  const cachedSegment = cache?.get(segment);
@@ -157,7 +153,7 @@ function getSlideToSegment(segment, slidingPreference = localReference_js_1.Slid
157
153
  const result = {};
158
154
  cache?.set(segment, result);
159
155
  const goFurtherToFindSlideToSegment = (seg) => {
160
- if (opstampUtils.isAcked(seg.insert) && !isRemovedAndAcked(seg)) {
156
+ if (perspective.isSegmentPresent(seg)) {
161
157
  result.seg = seg;
162
158
  return false;
163
159
  }
@@ -208,11 +204,11 @@ function getSlideToSegment(segment, slidingPreference = localReference_js_1.Slid
208
204
  * @returns segment and offset to slide the reference to
209
205
  * @internal
210
206
  */
211
- function getSlideToSegoff(segoff, slidingPreference = localReference_js_1.SlidingPreference.FORWARD, useNewSlidingBehavior = false) {
207
+ function getSlideToSegoff(segoff, slidingPreference = localReference_js_1.SlidingPreference.FORWARD, perspective = perspective_js_1.allAckedChangesPerspective, useNewSlidingBehavior = false) {
212
208
  if (!(0, mergeTreeNodes_js_1.isSegmentLeaf)(segoff.segment)) {
213
209
  return segoff;
214
210
  }
215
- const [segment, _] = getSlideToSegment(segoff.segment, slidingPreference, undefined, useNewSlidingBehavior);
211
+ const [segment, _] = getSlideToSegment(segoff.segment, slidingPreference, perspective, undefined, useNewSlidingBehavior);
216
212
  if (segment === segoff.segment) {
217
213
  return segoff;
218
214
  }
@@ -254,6 +250,9 @@ class Obliterates {
254
250
  this.mergeTree.removeLocalReferencePosition(ob.data.end);
255
251
  }
256
252
  }
253
+ onNormalize() {
254
+ this.startOrdered.onSortOrderChange();
255
+ }
257
256
  addOrUpdate(obliterateInfo) {
258
257
  const { stamp: { seq }, start, } = obliterateInfo;
259
258
  if (seq !== constants_js_1.UnassignedSequenceNumber) {
@@ -282,6 +281,39 @@ class Obliterates {
282
281
  }
283
282
  return overlapping;
284
283
  }
284
+ /**
285
+ * Remove a local obliterate from this data structure.
286
+ * @privateRemarks
287
+ * This data structure could support removing non-local obliterates if we wanted it to, but when adding support for that
288
+ * we should reconsider the indexing structure for seq ordered obliterates (right now it would be an O(# obliterates) operation)
289
+ */
290
+ removeLocalObliterate(obliterateInfo) {
291
+ (0, internal_1.assert)(obliterateInfo.stamp.seq === constants_js_1.UnassignedSequenceNumber, 0xb6e /* Expected local obliterate */);
292
+ this.startOrdered.remove(obliterateInfo.start);
293
+ }
294
+ /**
295
+ * Returns an iterator over the `ObliterateInfo` for all obliterates in the collab window. Obliterates are not guaranteed to be ordered.
296
+ * The iterator is not guaranteed to be valid over edits to the set of obliterates.
297
+ */
298
+ [Symbol.iterator]() {
299
+ let index = 0;
300
+ const { items: starts } = this.startOrdered;
301
+ const iterator = {
302
+ next() {
303
+ if (index < starts.length) {
304
+ const start = starts[index++];
305
+ const info = start.properties?.obliterate;
306
+ (0, internal_1.assert)(info?.start !== undefined && info?.end !== undefined, 0xb6f /* Expected obliterateInfo endpoint to map to its obliterate */);
307
+ return { value: info, done: false };
308
+ }
309
+ return { value: undefined, done: true };
310
+ },
311
+ [Symbol.iterator]() {
312
+ return this;
313
+ },
314
+ };
315
+ return iterator;
316
+ }
285
317
  }
286
318
  /**
287
319
  * @internal
@@ -317,13 +349,8 @@ class MergeTree {
317
349
  next.segmentGroups ??= new segmentGroupCollection_js_1.SegmentGroupCollection(next);
318
350
  segment.segmentGroups.copyTo(next.segmentGroups);
319
351
  }
320
- if (segment.obliteratePrecedingInsertion) {
321
- next.obliteratePrecedingInsertion = segment.obliteratePrecedingInsertion;
322
- }
323
352
  (0, segmentPropertiesManager_js_1.copyPropertiesAndManager)(segment, next);
324
- if (segment.localRefs) {
325
- segment.localRefs.split(pos, next);
326
- }
353
+ segment.localRefs?.split(pos, next);
327
354
  this.mergeTreeMaintenanceCallback?.({
328
355
  operation: mergeTreeDeltaCallback_js_1.MergeTreeMaintenanceType.SPLIT,
329
356
  deltaSegments: [{ segment }, { segment: next }],
@@ -336,6 +363,12 @@ class MergeTree {
336
363
  this._root.mergeTree = this;
337
364
  this.attributionPolicy = options?.attribution?.policyFactory?.();
338
365
  }
366
+ rebaseObliterateTo(existing, newObliterate) {
367
+ this.obliterates.removeLocalObliterate(existing);
368
+ if (newObliterate !== undefined) {
369
+ this.obliterates.addOrUpdate(newObliterate);
370
+ }
371
+ }
339
372
  get root() {
340
373
  return this._root;
341
374
  }
@@ -549,7 +582,7 @@ class MergeTree {
549
582
  if (!segment.localRefs || !(0, localReference_js_1.anyLocalReferencePosition)(segment.localRefs, pred)) {
550
583
  return;
551
584
  }
552
- const [slideToSegment, maybeEndpoint] = getSlideToSegment(segment, slidingPreference, slidingPreference === localReference_js_1.SlidingPreference.FORWARD
585
+ const [slideToSegment, maybeEndpoint] = getSlideToSegment(segment, slidingPreference, perspective_js_1.allAckedChangesPerspective, slidingPreference === localReference_js_1.SlidingPreference.FORWARD
553
586
  ? forwardSegmentCache
554
587
  : backwardSegmentCache, this.options?.mergeTreeReferencesCanSlideToEndpoint);
555
588
  const slideIsForward = slideToSegment === undefined ? false : slideToSegment.ordinal > segment.ordinal;
@@ -750,10 +783,9 @@ class MergeTree {
750
783
  this.nodeUpdateLengthNewStructure(this.root);
751
784
  }
752
785
  /**
753
- * Assign sequence number to existing segment; update partial lengths to reflect the change
754
- * @param seq - sequence number given by server to pending segment
786
+ * Assign sequence number to existing segments affected by an op; update partial lengths to reflect the change
755
787
  */
756
- ackPendingSegment(opArgs) {
788
+ ackOp(opArgs) {
757
789
  const seq = opArgs.sequencedMessage.sequenceNumber;
758
790
  const stamp = {
759
791
  seq,
@@ -763,9 +795,25 @@ class MergeTree {
763
795
  const nodesToUpdate = [];
764
796
  let overwrite = false;
765
797
  if (pendingSegmentGroup !== undefined) {
798
+ const { obliterateInfo, segments } = pendingSegmentGroup;
799
+ const hasObliterateInfo = obliterateInfo !== undefined;
800
+ const isObliterate = opArgs.op.type === ops_js_1.MergeTreeDeltaType.OBLITERATE ||
801
+ opArgs.op.type === ops_js_1.MergeTreeDeltaType.OBLITERATE_SIDED;
802
+ (0, internal_1.assert)(hasObliterateInfo === isObliterate, 0xa40 /* must have obliterate info */);
803
+ if (hasObliterateInfo) {
804
+ obliterateInfo.stamp = { ...stamp, type: "sliceRemove" };
805
+ this.obliterates.addOrUpdate(obliterateInfo);
806
+ (0, internal_1.assert)(obliterateInfo.tiebreakTrackingGroup !== undefined, 0xb70 /* obliterateInfo should have a tiebreak tracking group on ack */);
807
+ for (const segment of obliterateInfo.tiebreakTrackingGroup.tracked) {
808
+ segment.trackingCollection.unlink(obliterateInfo.tiebreakTrackingGroup);
809
+ (0, internal_1.assert)((0, mergeTreeNodes_js_1.isSegmentLeaf)(segment) && (0, segmentInfos_js_1.isInsideObliterate)(segment), 0xb71 /* Expected segment leaf inside obliterate */);
810
+ segment.insertionRefSeqStamp = undefined;
811
+ }
812
+ obliterateInfo.tiebreakTrackingGroup = undefined;
813
+ }
766
814
  const deltaSegments = [];
767
815
  const overlappingRemoves = [];
768
- pendingSegmentGroup.segments.map((pendingSegment) => {
816
+ segments.map((pendingSegment) => {
769
817
  const overlappingRemove = !ackSegment(pendingSegment, pendingSegmentGroup, opArgs, stamp);
770
818
  overwrite ||= overlappingRemove;
771
819
  overlappingRemoves.push(overlappingRemove);
@@ -779,16 +827,12 @@ class MergeTree {
779
827
  segment: pendingSegment,
780
828
  });
781
829
  });
782
- if (pendingSegmentGroup.obliterateInfo !== undefined) {
783
- pendingSegmentGroup.obliterateInfo.stamp = { type: "sliceRemove", ...stamp };
784
- this.obliterates.addOrUpdate(pendingSegmentGroup.obliterateInfo);
785
- }
786
830
  // Perform slides after all segments have been acked, so that
787
831
  // positions after slide are final
788
832
  if (opArgs.op.type === ops_js_1.MergeTreeDeltaType.REMOVE ||
789
833
  opArgs.op.type === ops_js_1.MergeTreeDeltaType.OBLITERATE ||
790
834
  opArgs.op.type === ops_js_1.MergeTreeDeltaType.OBLITERATE_SIDED) {
791
- this.slideAckedRemovedSegmentReferences(pendingSegmentGroup.segments);
835
+ this.slideAckedRemovedSegmentReferences(segments);
792
836
  }
793
837
  this.mergeTreeMaintenanceCallback?.({
794
838
  deltaSegments,
@@ -805,6 +849,7 @@ class MergeTree {
805
849
  addToPendingList(segment, segmentGroup, localSeq, previousProps) {
806
850
  let _segmentGroup = segmentGroup;
807
851
  if (_segmentGroup === undefined) {
852
+ (0, internal_1.assert)(localSeq !== undefined, 0xb72 /* Local seq should be passed when creating new segment group */);
808
853
  _segmentGroup = {
809
854
  segments: [],
810
855
  localSeq,
@@ -982,16 +1027,15 @@ class MergeTree {
982
1027
  saveIfLocal(newSegment);
983
1028
  continue;
984
1029
  }
1030
+ const refSeqStamp = {
1031
+ seq: perspective.refSeq,
1032
+ clientId: stamp.clientId,
1033
+ };
985
1034
  const overlappingAckedObliterates = [];
986
1035
  let oldest;
987
1036
  let newest;
988
1037
  let newestAcked;
989
1038
  let oldestUnacked;
990
- const refSeqStamp = {
991
- seq: perspective.refSeq,
992
- clientId: stamp.clientId,
993
- localSeq: stamp.localSeq,
994
- };
995
1039
  for (const ob of this.obliterates.findOverlapping(newSegment)) {
996
1040
  if (opstampUtils.greaterThan(ob.stamp, refSeqStamp)) {
997
1041
  // Any obliterate from the same client that's inserting this segment cannot cause the segment to be marked as
@@ -1024,7 +1068,16 @@ class MergeTree {
1024
1068
  }
1025
1069
  }
1026
1070
  }
1027
- newSegment.obliteratePrecedingInsertion = newest;
1071
+ (0, segmentInfos_js_1.overwriteInfo)(newSegment, {
1072
+ obliteratePrecedingInsertion: newest,
1073
+ });
1074
+ if (newest !== undefined && opstampUtils.isLocal(newest.stamp)) {
1075
+ (0, internal_1.assert)(newest?.tiebreakTrackingGroup !== undefined, 0xb73 /* Expected local obliterateinfo to have tiebreak group */);
1076
+ newest.tiebreakTrackingGroup.link(newSegment);
1077
+ (0, segmentInfos_js_1.overwriteInfo)(newSegment, {
1078
+ insertionRefSeqStamp: refSeqStamp,
1079
+ });
1080
+ }
1028
1081
  // See doc comment on obliteratePrecedingInsertion for more details: if the newest obliterate was performed
1029
1082
  // by the same client that's inserting this segment, we let them insert into this range and therefore don't
1030
1083
  // mark it obliterated.
@@ -1055,6 +1108,16 @@ class MergeTree {
1055
1108
  saveIfLocal(newSegment);
1056
1109
  }
1057
1110
  }
1111
+ computeObliteratePrecedingInsertion(segment, refSeqStamp) {
1112
+ let newest;
1113
+ for (const ob of this.obliterates.findOverlapping(segment)) {
1114
+ if (opstampUtils.greaterThan(ob.stamp, refSeqStamp) &&
1115
+ (newest === undefined || opstampUtils.greaterThan(ob.stamp, newest.stamp))) {
1116
+ newest = ob;
1117
+ }
1118
+ }
1119
+ return newest;
1120
+ }
1058
1121
  ensureIntervalBoundary(pos, perspective) {
1059
1122
  this.insertingWalk(pos, perspective, {
1060
1123
  seq: constants_js_1.TreeMaintenanceSequenceNumber,
@@ -1077,12 +1140,12 @@ class MergeTree {
1077
1140
  }
1078
1141
  }
1079
1142
  insertingWalk(pos, perspective, stamp, context) {
1080
- const { remainder } = this.insertRecursive(this.root, pos, perspective, stamp, context);
1143
+ const { remainder } = this.insertRecursive(this.root, pos, perspective, stamp, context, true);
1081
1144
  if (remainder !== undefined) {
1082
1145
  this.updateRoot(remainder);
1083
1146
  }
1084
1147
  }
1085
- insertRecursive(block, pos, perspective, stamp, context, isLastChildBlock = true) {
1148
+ insertRecursive(block, pos, perspective, stamp, context, isLastBlock) {
1086
1149
  let _pos = pos;
1087
1150
  const children = block.children;
1088
1151
  let childIndex;
@@ -1092,9 +1155,11 @@ class MergeTree {
1092
1155
  let hadChanges = false;
1093
1156
  for (childIndex = 0; childIndex < block.childCount; childIndex++) {
1094
1157
  child = children[childIndex];
1095
- // ensure we walk down the far edge of the tree, even if all sub-tree is eligible for zamboni
1096
- const isLastNonLeafBlock = isLastChildBlock && !child.isLeaf() && childIndex === block.childCount - 1;
1097
- const len = this.nodeLength(child, perspective) ?? (isLastChildBlock ? 0 : undefined);
1158
+ // removed blocks below the min seq will have an undefined length, and be skipped
1159
+ // however if it is the last block in the layer of the tree we don't want to skip it, so we correctly
1160
+ // walk down the far edge of the tree.
1161
+ const isLastChildOfLastBlock = isLastBlock && childIndex === block.childCount - 1;
1162
+ const len = this.nodeLength(child, perspective) ?? (isLastChildOfLastBlock ? 0 : undefined);
1098
1163
  if (len === undefined) {
1099
1164
  // if the seg len is undefined, the segment
1100
1165
  // will be removed, so should just be skipped for now
@@ -1121,9 +1186,8 @@ class MergeTree {
1121
1186
  }
1122
1187
  }
1123
1188
  else {
1124
- const childBlock = child;
1125
1189
  // Internal node
1126
- const insertResult = this.insertRecursive(childBlock, _pos, perspective, stamp, context, isLastNonLeafBlock);
1190
+ const insertResult = this.insertRecursive(child, _pos, perspective, stamp, context, isLastChildOfLastBlock);
1127
1191
  hadChanges ||= insertResult.hadChanges;
1128
1192
  if (insertResult.remainder === undefined) {
1129
1193
  if (insertResult.hadChanges) {
@@ -1270,33 +1334,36 @@ class MergeTree {
1270
1334
  let _overwrite = false;
1271
1335
  const localOverlapWithRefs = [];
1272
1336
  const removedSegments = [];
1337
+ const createRefFromSequencePlace = (place) => {
1338
+ const { segment: placeSeg, offset: placeOffset } = this.getContainingSegment(place.pos, perspective);
1339
+ (0, internal_1.assert)((0, mergeTreeNodes_js_1.isSegmentLeaf)(placeSeg) && placeOffset !== undefined, 0xa3f /* segments cannot be undefined */);
1340
+ return this.createLocalReferencePosition(placeSeg, placeOffset, ops_js_1.ReferenceType.StayOnRemove, undefined);
1341
+ };
1273
1342
  const obliterate = {
1274
- start: (0, localReference_js_1.createDetachedLocalReferencePosition)(undefined),
1275
- end: (0, localReference_js_1.createDetachedLocalReferencePosition)(undefined),
1343
+ start: createRefFromSequencePlace(start),
1344
+ startSide: start.side,
1345
+ end: createRefFromSequencePlace(end),
1346
+ endSide: end.side,
1276
1347
  refSeq: perspective.refSeq,
1277
1348
  stamp,
1278
1349
  segmentGroup: undefined,
1350
+ tiebreakTrackingGroup: undefined,
1279
1351
  };
1280
- const { segment: startSeg } = this.getContainingSegment(start.pos, perspective);
1281
- const { segment: endSeg } = this.getContainingSegment(end.pos, perspective);
1282
- (0, internal_1.assert)((0, mergeTreeNodes_js_1.isSegmentLeaf)(startSeg) && (0, mergeTreeNodes_js_1.isSegmentLeaf)(endSeg), 0xa3f /* segments cannot be undefined */);
1283
- obliterate.start = this.createLocalReferencePosition(startSeg, start.side === sequencePlace_js_1.Side.Before ? 0 : Math.max(startSeg.cachedLength - 1, 0), ops_js_1.ReferenceType.StayOnRemove, {
1284
- obliterate,
1285
- });
1286
- obliterate.end = this.createLocalReferencePosition(endSeg, end.side === sequencePlace_js_1.Side.Before ? 0 : Math.max(endSeg.cachedLength - 1, 0), ops_js_1.ReferenceType.StayOnRemove, {
1287
- obliterate,
1288
- });
1289
- // Always create a segment group for obliterate,
1290
- // even if there are no segments currently in the obliteration range.
1291
- // Segments may be concurrently inserted into the obliteration range,
1292
- // at which point they are added to the segment group.
1293
- obliterate.segmentGroup = {
1294
- segments: [],
1295
- localSeq: stamp.localSeq,
1296
- refSeq: this.collabWindow.currentSeq,
1297
- obliterateInfo: obliterate,
1298
- };
1352
+ // Link references back to this obliterate info
1353
+ obliterate.start.addProperties({ obliterate });
1354
+ obliterate.end.addProperties({ obliterate });
1299
1355
  if (this.collabWindow.collaborating && stamp.clientId === this.collabWindow.clientId) {
1356
+ // Always create a segment group for local obliterates,
1357
+ // even if there are no segments currently in the obliteration range.
1358
+ // Segments may be concurrently inserted into the obliteration range,
1359
+ // at which point they are added to the segment group.
1360
+ obliterate.segmentGroup = {
1361
+ segments: [],
1362
+ localSeq: stamp.localSeq,
1363
+ refSeq: this.collabWindow.currentSeq,
1364
+ obliterateInfo: obliterate,
1365
+ };
1366
+ obliterate.tiebreakTrackingGroup = new mergeTreeTracking_js_1.UnorderedTrackingGroup();
1300
1367
  this.pendingSegments.push(obliterate.segmentGroup);
1301
1368
  }
1302
1369
  this.obliterates.addOrUpdate(obliterate);
@@ -1314,6 +1381,7 @@ class MergeTree {
1314
1381
  // Specifically, we want to avoid marking a local-only segment as obliterated when we know one of our own local obliterates
1315
1382
  // will win against the obliterate we're processing, hence the early exit.
1316
1383
  if (opstampUtils.isLocal(segment.insert) &&
1384
+ (0, segmentInfos_js_1.isInsideObliterate)(segment) &&
1317
1385
  segment.obliteratePrecedingInsertion?.stamp.seq === constants_js_1.UnassignedSequenceNumber &&
1318
1386
  opstampUtils.isAcked(stamp)) {
1319
1387
  // We chose to not obliterate this segment because we are aware of an unacked local obliteration.
@@ -1479,6 +1547,10 @@ class MergeTree {
1479
1547
  * Revert an unacked local op
1480
1548
  */
1481
1549
  rollback(op, localOpMetadata) {
1550
+ const rollbackStamp = {
1551
+ seq: constants_js_1.TreeMaintenanceSequenceNumber,
1552
+ clientId: constants_js_1.NonCollabClient,
1553
+ };
1482
1554
  if (op.type === ops_js_1.MergeTreeDeltaType.REMOVE) {
1483
1555
  const pendingSegmentGroup = this.pendingSegments.pop()?.data;
1484
1556
  if (pendingSegmentGroup === undefined || pendingSegmentGroup !== localOpMetadata) {
@@ -1492,16 +1564,10 @@ class MergeTree {
1492
1564
  (0, internal_1.assert)((0, segmentInfos_js_1.isRemoved)(segment) &&
1493
1565
  segment.removes[0].clientId === this.collabWindow.clientId &&
1494
1566
  segment.removes[0].type === "setRemove", 0x39d /* Rollback segment removedClientId does not match local client */);
1495
- let updateNode = segment.parent;
1496
1567
  // This also removes obliterates, but that should be ok as we can only remove a segment once.
1497
1568
  // If we were able to remove it locally, that also means there are no remote removals (since rollback is synchronous).
1498
1569
  (0, segmentInfos_js_1.removeRemovalInfo)(segment);
1499
- for (updateNode; updateNode !== undefined; updateNode = updateNode.parent) {
1500
- this.blockUpdateLength(updateNode, {
1501
- seq: constants_js_1.UnassignedSequenceNumber,
1502
- clientId: this.collabWindow.clientId,
1503
- });
1504
- }
1570
+ this.blockUpdatePathLengths(segment.parent, rollbackStamp);
1505
1571
  // Note: optional chaining short-circuits:
1506
1572
  // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Optional_chaining#short-circuiting
1507
1573
  this.mergeTreeDeltaCallback?.({
@@ -1529,25 +1595,19 @@ class MergeTree {
1529
1595
  if (op.type === ops_js_1.MergeTreeDeltaType.INSERT) {
1530
1596
  segment.insert = {
1531
1597
  type: "insert",
1532
- seq: constants_js_1.UniversalSequenceNumber,
1533
- clientId: this.collabWindow.clientId,
1598
+ ...rollbackStamp,
1534
1599
  };
1535
1600
  const removeOp = (0, opBuilder_js_1.createRemoveRangeOp)(start, start + segment.cachedLength);
1536
1601
  const removeStamp = {
1537
1602
  type: "setRemove",
1538
- seq: constants_js_1.UniversalSequenceNumber,
1539
- clientId: this.collabWindow.clientId,
1603
+ ...rollbackStamp,
1540
1604
  };
1541
1605
  this.markRangeRemoved(start, start + segment.cachedLength, this.localPerspective, removeStamp, { op: removeOp, rollback: true });
1542
1606
  } /* op.type === MergeTreeDeltaType.ANNOTATE */
1543
1607
  else {
1544
1608
  const props = pendingSegmentGroup.previousProps[i];
1545
1609
  const annotateOp = (0, opBuilder_js_1.createAnnotateRangeOp)(start, start + segment.cachedLength, props);
1546
- const annotateStamp = {
1547
- seq: constants_js_1.UniversalSequenceNumber,
1548
- clientId: this.collabWindow.clientId,
1549
- };
1550
- this.annotateRange(start, start + segment.cachedLength, { props }, this.localPerspective, annotateStamp, { op: annotateOp, rollback: true });
1610
+ this.annotateRange(start, start + segment.cachedLength, { props }, this.localPerspective, rollbackStamp, { op: annotateOp, rollback: true });
1551
1611
  i++;
1552
1612
  }
1553
1613
  }
@@ -1608,7 +1668,17 @@ class MergeTree {
1608
1668
  const segRef = localRefs.createLocalRef(offset, refType, properties, slidingPreference, canSlideToEndpoint);
1609
1669
  return segRef;
1610
1670
  }
1611
- // Segments should either be removed remotely, removed locally, or inserted locally
1671
+ /**
1672
+ * Segments should either be removed remotely, removed locally, or inserted locally
1673
+ *
1674
+ * See description of {@link normalizeSegmentsOnRebase}.
1675
+ *
1676
+ * This normalizes a block of adjacent segments whose positions have collapsed between the time of the original submission and now
1677
+ * such that removed segments come after ones that still exist.
1678
+ *
1679
+ * TODO:AB#34898: It looks like this method has some bugs, search code for this tag for an example test that demonstrates
1680
+ * segment normalization yielding an order that remote clients wouldn't have seen.
1681
+ */
1612
1682
  normalizeAdjacentSegments(affectedSegments) {
1613
1683
  // Eagerly demand this since we're about to shift elements in the list around
1614
1684
  const currentOrder = Array.from(affectedSegments, ({ data: seg }) => ({
@@ -1743,6 +1813,45 @@ class MergeTree {
1743
1813
  return true;
1744
1814
  });
1745
1815
  normalize();
1816
+ this.obliterates.onNormalize();
1817
+ const segmentTiebreakChanges = new Set();
1818
+ for (const info of this.obliterates) {
1819
+ if (info.tiebreakTrackingGroup !== undefined) {
1820
+ for (const segment of info.tiebreakTrackingGroup.tracked) {
1821
+ // Recompute previous obliterate
1822
+ (0, internal_1.assert)((0, mergeTreeNodes_js_1.isSegmentLeaf)(segment) &&
1823
+ (0, segmentInfos_js_1.isInsideObliterate)(segment) &&
1824
+ segment.insertionRefSeqStamp !== undefined, 0xb74 /* Expected segment leaf inside obliterate with insertionRefSeqStamp */);
1825
+ // This may have changed as a result of segments shuffling: outstanding local obliterates that previously surrounded a segment may no longer surround it.
1826
+ const newObliteratePrecedingInsertion = this.computeObliteratePrecedingInsertion(segment, segment.insertionRefSeqStamp);
1827
+ if (newObliteratePrecedingInsertion !== info) {
1828
+ segmentTiebreakChanges.add({
1829
+ segment,
1830
+ old: info,
1831
+ new: newObliteratePrecedingInsertion,
1832
+ });
1833
+ }
1834
+ }
1835
+ }
1836
+ }
1837
+ for (const { segment, old, new: newInfo } of segmentTiebreakChanges) {
1838
+ // Update tiebreak tracking groups on old/new segment as well as `ISegmentInsideObliterateInfo` state
1839
+ // which we only keep around as long as obliterates are in flight.
1840
+ old.tiebreakTrackingGroup?.unlink(segment);
1841
+ if (newInfo?.tiebreakTrackingGroup === undefined) {
1842
+ // Segment is either no longer inside any obliterate or only inside acked obliterates.
1843
+ (0, segmentInfos_js_1.overwriteInfo)(segment, {
1844
+ obliteratePrecedingInsertion: newInfo,
1845
+ insertionRefSeqStamp: undefined,
1846
+ });
1847
+ }
1848
+ else {
1849
+ (0, segmentInfos_js_1.overwriteInfo)(segment, {
1850
+ obliteratePrecedingInsertion: newInfo,
1851
+ });
1852
+ newInfo.tiebreakTrackingGroup.link(segment);
1853
+ }
1854
+ }
1746
1855
  }
1747
1856
  blockUpdate(block) {
1748
1857
  let len;