@fluidframework/merge-tree 2.4.0-294316 → 2.4.0-297385

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 (147) hide show
  1. package/api-report/merge-tree.legacy.alpha.api.md +25 -5
  2. package/dist/attributionPolicy.d.ts.map +1 -1
  3. package/dist/attributionPolicy.js +10 -3
  4. package/dist/attributionPolicy.js.map +1 -1
  5. package/dist/client.d.ts +14 -4
  6. package/dist/client.d.ts.map +1 -1
  7. package/dist/client.js +97 -9
  8. package/dist/client.js.map +1 -1
  9. package/dist/index.d.ts +2 -2
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/index.js.map +1 -1
  12. package/dist/legacy.d.ts +1 -0
  13. package/dist/localReference.d.ts +1 -1
  14. package/dist/localReference.d.ts.map +1 -1
  15. package/dist/localReference.js.map +1 -1
  16. package/dist/mergeTree.d.ts +22 -8
  17. package/dist/mergeTree.d.ts.map +1 -1
  18. package/dist/mergeTree.js +98 -24
  19. package/dist/mergeTree.js.map +1 -1
  20. package/dist/mergeTreeNodes.d.ts +27 -2
  21. package/dist/mergeTreeNodes.d.ts.map +1 -1
  22. package/dist/mergeTreeNodes.js +2 -18
  23. package/dist/mergeTreeNodes.js.map +1 -1
  24. package/dist/opBuilder.d.ts +15 -1
  25. package/dist/opBuilder.d.ts.map +1 -1
  26. package/dist/opBuilder.js +28 -1
  27. package/dist/opBuilder.js.map +1 -1
  28. package/dist/ops.d.ts +27 -1
  29. package/dist/ops.d.ts.map +1 -1
  30. package/dist/ops.js +1 -0
  31. package/dist/ops.js.map +1 -1
  32. package/dist/revertibles.d.ts.map +1 -1
  33. package/dist/revertibles.js +4 -2
  34. package/dist/revertibles.js.map +1 -1
  35. package/dist/sequencePlace.d.ts +4 -0
  36. package/dist/sequencePlace.d.ts.map +1 -1
  37. package/dist/sequencePlace.js +17 -1
  38. package/dist/sequencePlace.js.map +1 -1
  39. package/dist/snapshotV1.d.ts.map +1 -1
  40. package/dist/snapshotV1.js +0 -2
  41. package/dist/snapshotV1.js.map +1 -1
  42. package/dist/snapshotlegacy.d.ts.map +1 -1
  43. package/dist/snapshotlegacy.js +0 -1
  44. package/dist/snapshotlegacy.js.map +1 -1
  45. package/dist/test/client.annotateMarker.spec.js.map +1 -1
  46. package/dist/test/client.applyMsg.spec.js +3 -3
  47. package/dist/test/client.applyMsg.spec.js.map +1 -1
  48. package/dist/test/client.localReference.spec.js.map +1 -1
  49. package/dist/test/client.rollback.spec.js.map +1 -1
  50. package/dist/test/mergeTree.annotate.spec.js +29 -19
  51. package/dist/test/mergeTree.annotate.spec.js.map +1 -1
  52. package/dist/test/obliterate.concurrent.spec.js +18 -0
  53. package/dist/test/obliterate.concurrent.spec.js.map +1 -1
  54. package/dist/test/obliterate.rangeExpansion.spec.js +109 -53
  55. package/dist/test/obliterate.rangeExpansion.spec.js.map +1 -1
  56. package/dist/test/obliterate.spec.js +2 -2
  57. package/dist/test/obliterate.spec.js.map +1 -1
  58. package/dist/test/reconnectHelper.d.ts +8 -6
  59. package/dist/test/reconnectHelper.d.ts.map +1 -1
  60. package/dist/test/reconnectHelper.js +14 -13
  61. package/dist/test/reconnectHelper.js.map +1 -1
  62. package/dist/test/revertibleFarm.spec.js.map +1 -1
  63. package/dist/test/testClientLogger.d.ts.map +1 -1
  64. package/dist/test/testClientLogger.js +19 -8
  65. package/dist/test/testClientLogger.js.map +1 -1
  66. package/dist/zamboni.js +2 -1
  67. package/dist/zamboni.js.map +1 -1
  68. package/lib/attributionPolicy.d.ts.map +1 -1
  69. package/lib/attributionPolicy.js +10 -3
  70. package/lib/attributionPolicy.js.map +1 -1
  71. package/lib/client.d.ts +14 -4
  72. package/lib/client.d.ts.map +1 -1
  73. package/lib/client.js +98 -10
  74. package/lib/client.js.map +1 -1
  75. package/lib/index.d.ts +2 -2
  76. package/lib/index.d.ts.map +1 -1
  77. package/lib/index.js.map +1 -1
  78. package/lib/legacy.d.ts +1 -0
  79. package/lib/localReference.d.ts +1 -1
  80. package/lib/localReference.d.ts.map +1 -1
  81. package/lib/localReference.js.map +1 -1
  82. package/lib/mergeTree.d.ts +22 -8
  83. package/lib/mergeTree.d.ts.map +1 -1
  84. package/lib/mergeTree.js +100 -26
  85. package/lib/mergeTree.js.map +1 -1
  86. package/lib/mergeTreeNodes.d.ts +27 -2
  87. package/lib/mergeTreeNodes.d.ts.map +1 -1
  88. package/lib/mergeTreeNodes.js +2 -18
  89. package/lib/mergeTreeNodes.js.map +1 -1
  90. package/lib/opBuilder.d.ts +15 -1
  91. package/lib/opBuilder.d.ts.map +1 -1
  92. package/lib/opBuilder.js +26 -0
  93. package/lib/opBuilder.js.map +1 -1
  94. package/lib/ops.d.ts +27 -1
  95. package/lib/ops.d.ts.map +1 -1
  96. package/lib/ops.js +1 -0
  97. package/lib/ops.js.map +1 -1
  98. package/lib/revertibles.d.ts.map +1 -1
  99. package/lib/revertibles.js +5 -3
  100. package/lib/revertibles.js.map +1 -1
  101. package/lib/sequencePlace.d.ts +4 -0
  102. package/lib/sequencePlace.d.ts.map +1 -1
  103. package/lib/sequencePlace.js +15 -0
  104. package/lib/sequencePlace.js.map +1 -1
  105. package/lib/snapshotV1.d.ts.map +1 -1
  106. package/lib/snapshotV1.js +0 -2
  107. package/lib/snapshotV1.js.map +1 -1
  108. package/lib/snapshotlegacy.d.ts.map +1 -1
  109. package/lib/snapshotlegacy.js +0 -1
  110. package/lib/snapshotlegacy.js.map +1 -1
  111. package/lib/test/client.annotateMarker.spec.js.map +1 -1
  112. package/lib/test/client.applyMsg.spec.js +3 -3
  113. package/lib/test/client.applyMsg.spec.js.map +1 -1
  114. package/lib/test/client.localReference.spec.js.map +1 -1
  115. package/lib/test/client.rollback.spec.js.map +1 -1
  116. package/lib/test/mergeTree.annotate.spec.js +29 -19
  117. package/lib/test/mergeTree.annotate.spec.js.map +1 -1
  118. package/lib/test/obliterate.concurrent.spec.js +18 -0
  119. package/lib/test/obliterate.concurrent.spec.js.map +1 -1
  120. package/lib/test/obliterate.rangeExpansion.spec.js +109 -53
  121. package/lib/test/obliterate.rangeExpansion.spec.js.map +1 -1
  122. package/lib/test/obliterate.spec.js +2 -2
  123. package/lib/test/obliterate.spec.js.map +1 -1
  124. package/lib/test/reconnectHelper.d.ts +8 -6
  125. package/lib/test/reconnectHelper.d.ts.map +1 -1
  126. package/lib/test/reconnectHelper.js +15 -14
  127. package/lib/test/reconnectHelper.js.map +1 -1
  128. package/lib/test/revertibleFarm.spec.js.map +1 -1
  129. package/lib/test/testClientLogger.d.ts.map +1 -1
  130. package/lib/test/testClientLogger.js +19 -8
  131. package/lib/test/testClientLogger.js.map +1 -1
  132. package/lib/zamboni.js +2 -1
  133. package/lib/zamboni.js.map +1 -1
  134. package/package.json +31 -18
  135. package/src/attributionPolicy.ts +5 -0
  136. package/src/client.ts +136 -21
  137. package/src/index.ts +2 -0
  138. package/src/localReference.ts +5 -5
  139. package/src/mergeTree.ts +200 -75
  140. package/src/mergeTreeNodes.ts +37 -23
  141. package/src/opBuilder.ts +32 -0
  142. package/src/ops.ts +23 -1
  143. package/src/revertibles.ts +12 -5
  144. package/src/sequencePlace.ts +16 -0
  145. package/src/snapshotV1.ts +0 -2
  146. package/src/snapshotlegacy.ts +0 -1
  147. package/src/zamboni.ts +3 -2
package/src/mergeTree.ts CHANGED
@@ -49,7 +49,6 @@ import {
49
49
  IMergeNode,
50
50
  IMoveInfo,
51
51
  IRemovalInfo,
52
- ISegment,
53
52
  ISegmentAction,
54
53
  ISegmentChanges,
55
54
  ISegmentLeaf,
@@ -63,6 +62,7 @@ import {
63
62
  seqLTE,
64
63
  toMoveInfo,
65
64
  toRemovalInfo,
65
+ type ISegmentInternal,
66
66
  // eslint-disable-next-line import/no-deprecated
67
67
  type ObliterateInfo,
68
68
  } from "./mergeTreeNodes.js";
@@ -80,7 +80,7 @@ import {
80
80
  } from "./ops.js";
81
81
  import { PartialSequenceLengths } from "./partialLengths.js";
82
82
  import { PerspectiveImpl, isSegmentPresent } from "./perspective.js";
83
- import { PropertySet, createMap, extend, extendIfUndefined } from "./properties.js";
83
+ import { PropertySet, clone, createMap, extend, extendIfUndefined } from "./properties.js";
84
84
  import {
85
85
  DetachedReferencePosition,
86
86
  ReferencePosition,
@@ -89,11 +89,14 @@ import {
89
89
  refTypeIncludesFlag,
90
90
  } from "./referencePositions.js";
91
91
  // eslint-disable-next-line import/no-deprecated
92
- import { PropertiesRollback } from "./segmentPropertiesManager.js";
92
+ import { SegmentGroupCollection } from "./segmentGroupCollection.js";
93
+ // eslint-disable-next-line import/no-deprecated
94
+ import { PropertiesManager, PropertiesRollback } from "./segmentPropertiesManager.js";
95
+ import { Side, type InteriorSequencePlace } from "./sequencePlace.js";
93
96
  import { SortedSegmentSet } from "./sortedSegmentSet.js";
94
97
  import { zamboniSegments } from "./zamboni.js";
95
98
 
96
- function markSegmentMoved(seg: ISegment, moveInfo: IMoveInfo): void {
99
+ function markSegmentMoved(seg: ISegmentLeaf, moveInfo: IMoveInfo): void {
97
100
  seg.moveDst = moveInfo.moveDst;
98
101
  seg.movedClientIds = [...moveInfo.movedClientIds];
99
102
  seg.movedSeqs = [moveInfo.movedSeq];
@@ -102,29 +105,29 @@ function markSegmentMoved(seg: ISegment, moveInfo: IMoveInfo): void {
102
105
  seg.wasMovedOnInsert = moveInfo.wasMovedOnInsert;
103
106
  }
104
107
 
105
- function isMoved(segment: ISegment): segment is ISegment & IMoveInfo {
108
+ function isMoved(segment: ISegmentLeaf): segment is ISegmentLeaf & IMoveInfo {
106
109
  return toMoveInfo(segment) !== undefined;
107
110
  }
108
111
 
109
- function isRemoved(segment: ISegment): segment is ISegment & IRemovalInfo {
112
+ function isRemoved(segment: ISegmentLeaf): segment is ISegmentLeaf & IRemovalInfo {
110
113
  return toRemovalInfo(segment) !== undefined;
111
114
  }
112
115
 
113
- function isRemovedAndAcked(segment: ISegment): segment is ISegment & IRemovalInfo {
116
+ function isRemovedAndAcked(segment: ISegmentLeaf): segment is ISegmentLeaf & IRemovalInfo {
114
117
  const removalInfo = toRemovalInfo(segment);
115
118
  return removalInfo !== undefined && removalInfo.removedSeq !== UnassignedSequenceNumber;
116
119
  }
117
120
 
118
- function isMovedAndAcked(segment: ISegment): segment is ISegment & IMoveInfo {
121
+ function isMovedAndAcked(segment: ISegmentLeaf): segment is ISegmentLeaf & IMoveInfo {
119
122
  const moveInfo = toMoveInfo(segment);
120
123
  return moveInfo !== undefined && moveInfo.movedSeq !== UnassignedSequenceNumber;
121
124
  }
122
125
 
123
- function isRemovedAndAckedOrMovedAndAcked(segment: ISegment): boolean {
126
+ function isRemovedAndAckedOrMovedAndAcked(segment: ISegmentLeaf): boolean {
124
127
  return isRemovedAndAcked(segment) || isMovedAndAcked(segment);
125
128
  }
126
129
 
127
- function isRemovedOrMoved(segment: ISegment): boolean {
130
+ function isRemovedOrMoved(segment: ISegmentLeaf): boolean {
128
131
  return isRemoved(segment) || isMoved(segment);
129
132
  }
130
133
 
@@ -197,6 +200,17 @@ export interface IMergeTreeOptions {
197
200
  * @defaultValue `false`
198
201
  */
199
202
  mergeTreeEnableObliterateReconnect?: boolean;
203
+
204
+ /**
205
+ * Enables support for obliterate endpoint expansion.
206
+ * When enabled, obliterate operations can have sidedness specified for their endpoints.
207
+ * If an endpoint is externally anchored
208
+ * (aka the start is after a given position, or the end is before a given position),
209
+ * then concurrent inserts adjacent to the exclusive endpoint of an obliterated range will be included in the obliteration
210
+ *
211
+ * @defaultValue `false`
212
+ */
213
+ mergeTreeEnableSidedObliterate?: boolean;
200
214
  }
201
215
  export function errorIfOptionNotTrue(
202
216
  options: IMergeTreeOptions | undefined,
@@ -210,6 +224,7 @@ export function errorIfOptionNotTrue(
210
224
  /**
211
225
  * @legacy
212
226
  * @alpha
227
+ * @deprecated This functionality was not meant to be exported and will be removed in a future release
213
228
  */
214
229
  export interface IMergeTreeAttributionOptions {
215
230
  /**
@@ -237,6 +252,7 @@ export interface IMergeTreeAttributionOptions {
237
252
  * @sealed
238
253
  * @legacy
239
254
  * @alpha
255
+ * @deprecated This functionality was not meant to be exported and will be removed in a future release
240
256
  */
241
257
  export interface AttributionPolicy {
242
258
  /**
@@ -301,11 +317,11 @@ export function findRootMergeBlock(
301
317
  * @internal
302
318
  */
303
319
  function getSlideToSegment(
304
- segment: ISegment | undefined,
320
+ segment: ISegmentLeaf | undefined,
305
321
  slidingPreference: SlidingPreference = SlidingPreference.FORWARD,
306
- cache?: Map<ISegment, { seg?: ISegment }>,
322
+ cache?: Map<ISegmentLeaf, { seg?: ISegmentLeaf }>,
307
323
  useNewSlidingBehavior: boolean = false,
308
- ): [ISegment | undefined, "start" | "end" | undefined] {
324
+ ): [ISegmentLeaf | undefined, "start" | "end" | undefined] {
309
325
  if (
310
326
  !segment ||
311
327
  !isRemovedAndAckedOrMovedAndAcked(segment) ||
@@ -318,9 +334,9 @@ function getSlideToSegment(
318
334
  if (cachedSegment !== undefined) {
319
335
  return [cachedSegment.seg, undefined];
320
336
  }
321
- const result: { seg?: ISegment } = {};
337
+ const result: { seg?: ISegmentLeaf } = {};
322
338
  cache?.set(segment, result);
323
- const goFurtherToFindSlideToSegment = (seg: ISegment): boolean => {
339
+ const goFurtherToFindSlideToSegment = (seg: ISegmentLeaf): boolean => {
324
340
  if (seg.seq !== UnassignedSequenceNumber && !isRemovedAndAckedOrMovedAndAcked(seg)) {
325
341
  result.seg = seg;
326
342
  return false;
@@ -378,11 +394,11 @@ function getSlideToSegment(
378
394
  * @internal
379
395
  */
380
396
  export function getSlideToSegoff(
381
- segoff: { segment: ISegment | undefined; offset: number | undefined },
397
+ segoff: { segment: ISegmentInternal | undefined; offset: number | undefined },
382
398
  slidingPreference: SlidingPreference = SlidingPreference.FORWARD,
383
399
  useNewSlidingBehavior: boolean = false,
384
400
  ): {
385
- segment: ISegment | undefined;
401
+ segment: ISegmentInternal | undefined;
386
402
  offset: number | undefined;
387
403
  } {
388
404
  if (segoff.segment === undefined) {
@@ -456,7 +472,7 @@ class Obliterates {
456
472
  }
457
473
 
458
474
  // eslint-disable-next-line import/no-deprecated
459
- public findOverlapping(seg: ISegment): Iterable<ObliterateInfo> {
475
+ public findOverlapping(seg: ISegmentLeaf): Iterable<ObliterateInfo> {
460
476
  // eslint-disable-next-line import/no-deprecated
461
477
  const overlapping: ObliterateInfo[] = [];
462
478
  for (const start of this.startOrdered.items) {
@@ -541,7 +557,7 @@ export class MergeTree {
541
557
  * numbers corresponding to un-acked operations give valid results.
542
558
  */
543
559
  public localNetLength(
544
- segment: ISegment,
560
+ segment: ISegmentLeaf,
545
561
  refSeq?: number,
546
562
  localSeq?: number,
547
563
  ): number | undefined {
@@ -617,7 +633,7 @@ export class MergeTree {
617
633
  return index;
618
634
  }
619
635
 
620
- public reloadFromSegments(segments: ISegment[]): void {
636
+ public reloadFromSegments(segments: ISegmentLeaf[]): void {
621
637
  // This code assumes that a later call to `startCollaboration()` will initialize partial lengths.
622
638
  assert(
623
639
  !this.collabWindow.collaborating,
@@ -728,7 +744,7 @@ export class MergeTree {
728
744
  return totalOffset;
729
745
  }
730
746
 
731
- public getContainingSegment<T extends ISegment>(
747
+ public getContainingSegment<T extends ISegmentLeaf>(
732
748
  pos: number,
733
749
  refSeq: number,
734
750
  clientId: number,
@@ -745,7 +761,7 @@ export class MergeTree {
745
761
  let offset: number | undefined;
746
762
 
747
763
  const leaf = (
748
- leafSeg: ISegment,
764
+ leafSeg: ISegmentLeaf,
749
765
  segpos: number,
750
766
  _refSeq: number,
751
767
  _clientId: number,
@@ -779,17 +795,17 @@ export class MergeTree {
779
795
  *
780
796
  * @param segments - An array of (not necessarily contiguous) segments with increasing ordinals.
781
797
  */
782
- private slideAckedRemovedSegmentReferences(segments: ISegment[]): void {
798
+ private slideAckedRemovedSegmentReferences(segments: ISegmentLeaf[]): void {
783
799
  // References are slid in groups to preserve their order.
784
800
  let currentForwardSlideGroup: LocalReferenceCollection[] = [];
785
801
  let currentBackwardSlideGroup: LocalReferenceCollection[] = [];
786
802
 
787
803
  let currentForwardMaybeEndpoint: "start" | "end" | undefined;
788
- let currentForwardSlideDestination: ISegment | undefined;
804
+ let currentForwardSlideDestination: ISegmentLeaf | undefined;
789
805
  let currentForwardSlideIsForward: boolean | undefined;
790
806
 
791
807
  let currentBackwardMaybeEndpoint: "start" | "end" | undefined;
792
- let currentBackwardSlideDestination: ISegment | undefined;
808
+ let currentBackwardSlideDestination: ISegmentLeaf | undefined;
793
809
  let currentBackwardSlideIsForward: boolean | undefined;
794
810
 
795
811
  const slideGroup = (
@@ -848,8 +864,8 @@ export class MergeTree {
848
864
  };
849
865
 
850
866
  const trySlideSegment = (
851
- segment: ISegment,
852
- currentSlideDestination: ISegment | undefined,
867
+ segment: ISegmentLeaf,
868
+ currentSlideDestination: ISegmentLeaf | undefined,
853
869
  currentSlideIsForward: boolean | undefined,
854
870
  currentSlideGroup: LocalReferenceCollection[],
855
871
  pred: (ref: LocalReferencePosition) => boolean,
@@ -857,7 +873,7 @@ export class MergeTree {
857
873
  currentMaybeEndpoint: "start" | "end" | undefined,
858
874
  reassign: (
859
875
  localRefs: LocalReferenceCollection,
860
- slideToSegment: ISegment | undefined,
876
+ slideToSegment: ISegmentLeaf | undefined,
861
877
  slideIsForward: boolean,
862
878
  maybeEndpoint: "start" | "end" | undefined,
863
879
  ) => void,
@@ -902,8 +918,8 @@ export class MergeTree {
902
918
  }
903
919
  };
904
920
 
905
- const forwardSegmentCache = new Map<ISegment, { seg?: ISegment }>();
906
- const backwardSegmentCache = new Map<ISegment, { seg?: ISegment }>();
921
+ const forwardSegmentCache = new Map<ISegmentLeaf, { seg?: ISegmentLeaf }>();
922
+ const backwardSegmentCache = new Map<ISegmentLeaf, { seg?: ISegmentLeaf }>();
907
923
  for (const segment of segments) {
908
924
  assert(
909
925
  isRemovedAndAckedOrMovedAndAcked(segment),
@@ -1234,7 +1250,10 @@ export class MergeTree {
1234
1250
  });
1235
1251
  });
1236
1252
 
1237
- if (opArgs.op.type === MergeTreeDeltaType.OBLITERATE) {
1253
+ if (
1254
+ opArgs.op.type === MergeTreeDeltaType.OBLITERATE ||
1255
+ opArgs.op.type === MergeTreeDeltaType.OBLITERATE_SIDED
1256
+ ) {
1238
1257
  this.obliterates.addOrUpdate(pendingSegmentGroup.obliterateInfo!);
1239
1258
  }
1240
1259
 
@@ -1242,7 +1261,8 @@ export class MergeTree {
1242
1261
  // positions after slide are final
1243
1262
  if (
1244
1263
  opArgs.op.type === MergeTreeDeltaType.REMOVE ||
1245
- opArgs.op.type === MergeTreeDeltaType.OBLITERATE
1264
+ opArgs.op.type === MergeTreeDeltaType.OBLITERATE ||
1265
+ opArgs.op.type === MergeTreeDeltaType.OBLITERATE_SIDED
1246
1266
  ) {
1247
1267
  this.slideAckedRemovedSegmentReferences(pendingSegmentGroup.segments);
1248
1268
  }
@@ -1265,7 +1285,7 @@ export class MergeTree {
1265
1285
  }
1266
1286
 
1267
1287
  private addToPendingList(
1268
- segment: ISegment,
1288
+ segment: ISegmentLeaf,
1269
1289
  // eslint-disable-next-line import/no-deprecated
1270
1290
  segmentGroup?: SegmentGroup,
1271
1291
  localSeq?: number,
@@ -1294,8 +1314,9 @@ export class MergeTree {
1294
1314
  if (previousProps) {
1295
1315
  _segmentGroup.previousProps!.push(previousProps);
1296
1316
  }
1297
-
1298
- segment.segmentGroups.enqueue(_segmentGroup);
1317
+ // eslint-disable-next-line import/no-deprecated
1318
+ const segmentGroups = (segment.segmentGroups ??= new SegmentGroupCollection(segment));
1319
+ segmentGroups.enqueue(_segmentGroup);
1299
1320
  return _segmentGroup;
1300
1321
  }
1301
1322
 
@@ -1344,7 +1365,7 @@ export class MergeTree {
1344
1365
 
1345
1366
  public insertSegments(
1346
1367
  pos: number,
1347
- segments: ISegment[],
1368
+ segments: ISegmentLeaf[],
1348
1369
  refSeq: number,
1349
1370
  clientId: number,
1350
1371
  seq: number,
@@ -1441,7 +1462,7 @@ export class MergeTree {
1441
1462
  };
1442
1463
  // eslint-disable-next-line import/no-deprecated
1443
1464
  let segmentGroup: SegmentGroup;
1444
- const saveIfLocal = (locSegment: ISegment): void => {
1465
+ const saveIfLocal = (locSegment: ISegmentLeaf): void => {
1445
1466
  // Save segment so we can assign sequence number when acked by server
1446
1467
  if (this.collabWindow.collaborating) {
1447
1468
  if (
@@ -1462,7 +1483,7 @@ export class MergeTree {
1462
1483
  }
1463
1484
  };
1464
1485
  const onLeaf = (
1465
- segment: ISegment | undefined,
1486
+ segment: ISegmentLeaf | undefined,
1466
1487
  _pos: number,
1467
1488
  context: InsertContext,
1468
1489
  // Keeping this function within the scope of blockInsert for readability.
@@ -1510,11 +1531,11 @@ export class MergeTree {
1510
1531
  }
1511
1532
 
1512
1533
  this.updateRoot(splitNode);
1513
- saveIfLocal(newSegment);
1514
1534
 
1515
1535
  insertPos += newSegment.cachedLength;
1516
1536
 
1517
1537
  if (!this.options?.mergeTreeEnableObliterate || this.obliterates.empty()) {
1538
+ saveIfLocal(newSegment);
1518
1539
  continue;
1519
1540
  }
1520
1541
 
@@ -1542,13 +1563,13 @@ export class MergeTree {
1542
1563
  movedClientIds.unshift(ob.clientId);
1543
1564
  movedSeqs.unshift(ob.seq);
1544
1565
  } else {
1545
- if (newest === undefined || normalizedNewestSeq < normalizedObSeq) {
1546
- normalizedNewestSeq = normalizedObSeq;
1547
- newest = ob;
1548
- }
1549
1566
  movedClientIds.push(ob.clientId);
1550
1567
  movedSeqs.push(ob.seq);
1551
1568
  }
1569
+ if (newest === undefined || normalizedNewestSeq < normalizedObSeq) {
1570
+ normalizedNewestSeq = normalizedObSeq;
1571
+ newest = ob;
1572
+ }
1552
1573
  }
1553
1574
  }
1554
1575
 
@@ -1575,20 +1596,52 @@ export class MergeTree {
1575
1596
  if (newSegment.parent) {
1576
1597
  this.blockUpdatePathLengths(newSegment.parent, seq, clientId);
1577
1598
  }
1599
+ } else if (oldest && newest?.clientId === clientId) {
1600
+ newSegment.prevObliterateByInserter = newest;
1578
1601
  }
1602
+
1603
+ saveIfLocal(newSegment);
1579
1604
  }
1580
1605
  }
1581
1606
  }
1582
1607
 
1583
1608
  private readonly splitLeafSegment = (
1584
- segment: ISegment | undefined,
1609
+ segment: ISegmentLeaf | undefined,
1585
1610
  pos: number,
1586
1611
  ): ISegmentChanges => {
1587
1612
  if (!(pos > 0 && segment)) {
1588
1613
  return {};
1589
1614
  }
1590
1615
 
1591
- const next = segment.splitAt(pos)!;
1616
+ const next: ISegmentLeaf = segment.splitAt(pos)!;
1617
+
1618
+ if (segment?.segmentGroups) {
1619
+ // eslint-disable-next-line import/no-deprecated
1620
+ next.segmentGroups ??= new SegmentGroupCollection(next);
1621
+ segment.segmentGroups.copyTo(next);
1622
+ }
1623
+
1624
+ if (segment.prevObliterateByInserter) {
1625
+ next.prevObliterateByInserter = segment.prevObliterateByInserter;
1626
+ }
1627
+
1628
+ if (segment.properties) {
1629
+ if (segment.propertyManager === undefined) {
1630
+ next.properties = clone(segment.properties);
1631
+ } else {
1632
+ // eslint-disable-next-line import/no-deprecated
1633
+ next.propertyManager ??= new PropertiesManager();
1634
+ next.properties = segment.propertyManager.copyTo(
1635
+ segment.properties,
1636
+ next.properties,
1637
+ next.propertyManager,
1638
+ );
1639
+ }
1640
+ }
1641
+ if (segment.localRefs) {
1642
+ segment.localRefs.split(pos, next);
1643
+ }
1644
+
1592
1645
  this.mergeTreeMaintenanceCallback?.(
1593
1646
  {
1594
1647
  operation: MergeTreeMaintenanceType.SPLIT,
@@ -1805,6 +1858,7 @@ export class MergeTree {
1805
1858
  clientId: number,
1806
1859
  seq: number,
1807
1860
  opArgs: IMergeTreeDeltaOpArgs,
1861
+
1808
1862
  // eslint-disable-next-line import/no-deprecated
1809
1863
  rollback: PropertiesRollback = PropertiesRollback.None,
1810
1864
  ): void {
@@ -1815,19 +1869,25 @@ export class MergeTree {
1815
1869
  seq === UnassignedSequenceNumber ? ++this.collabWindow.localSeq : undefined;
1816
1870
  // eslint-disable-next-line import/no-deprecated
1817
1871
  let segmentGroup: SegmentGroup | undefined;
1818
- const annotateSegment = (segment: ISegment): boolean => {
1872
+ const annotateSegment = (segment: ISegmentLeaf): boolean => {
1819
1873
  assert(
1820
1874
  !Marker.is(segment) ||
1821
1875
  !(reservedMarkerIdKey in props) ||
1822
1876
  props.markerId === segment.properties?.markerId,
1823
1877
  0x5ad /* Cannot change the markerId of an existing marker */,
1824
1878
  );
1825
- const propertyDeltas = segment.addProperties(
1879
+
1880
+ // eslint-disable-next-line import/no-deprecated
1881
+ const propertyManager = (segment.propertyManager ??= new PropertiesManager());
1882
+ const properties = (segment.properties ??= createMap());
1883
+ const propertyDeltas = propertyManager.addProperties(
1884
+ properties,
1826
1885
  props,
1827
1886
  seq,
1828
1887
  this.collabWindow.collaborating,
1829
1888
  rollback,
1830
1889
  );
1890
+
1831
1891
  if (!isRemovedOrMoved(segment)) {
1832
1892
  deltaSegments.push({ segment, propertyDeltas });
1833
1893
  }
@@ -1866,22 +1926,23 @@ export class MergeTree {
1866
1926
  }
1867
1927
  }
1868
1928
 
1869
- public obliterateRange(
1870
- start: number,
1871
- end: number,
1929
+ private obliterateRangeSided(
1930
+ start: InteriorSequencePlace,
1931
+ end: InteriorSequencePlace,
1872
1932
  refSeq: number,
1873
1933
  clientId: number,
1874
1934
  seq: number,
1875
1935
  overwrite: boolean = false,
1876
1936
  opArgs: IMergeTreeDeltaOpArgs,
1877
1937
  ): void {
1878
- errorIfOptionNotTrue(this.options, "mergeTreeEnableObliterate");
1938
+ const startPos = start.side === Side.Before ? start.pos : start.pos + 1;
1939
+ const endPos = end.side === Side.Before ? end.pos : end.pos + 1;
1879
1940
 
1880
- this.ensureIntervalBoundary(start, refSeq, clientId);
1881
- this.ensureIntervalBoundary(end, refSeq, clientId);
1941
+ this.ensureIntervalBoundary(startPos, refSeq, clientId);
1942
+ this.ensureIntervalBoundary(endPos, refSeq, clientId);
1882
1943
 
1883
1944
  let _overwrite = overwrite;
1884
- const localOverlapWithRefs: ISegment[] = [];
1945
+ const localOverlapWithRefs: ISegmentLeaf[] = [];
1885
1946
  const movedSegments: IMergeTreeSegmentDelta[] = [];
1886
1947
  const localSeq =
1887
1948
  seq === UnassignedSequenceNumber ? ++this.collabWindow.localSeq : undefined;
@@ -1896,8 +1957,8 @@ export class MergeTree {
1896
1957
  segmentGroup: undefined,
1897
1958
  };
1898
1959
 
1899
- const { segment: startSeg } = this.getContainingSegment(start, refSeq, clientId);
1900
- const { segment: endSeg } = this.getContainingSegment(end - 1, refSeq, clientId);
1960
+ const { segment: startSeg } = this.getContainingSegment(start.pos, refSeq, clientId);
1961
+ const { segment: endSeg } = this.getContainingSegment(end.pos, refSeq, clientId);
1901
1962
  assert(
1902
1963
  startSeg !== undefined && endSeg !== undefined,
1903
1964
  0xa3f /* segments cannot be undefined */,
@@ -1905,7 +1966,7 @@ export class MergeTree {
1905
1966
 
1906
1967
  obliterate.start = this.createLocalReferencePosition(
1907
1968
  startSeg,
1908
- 0,
1969
+ start.side === Side.Before ? 0 : Math.max(startSeg.cachedLength - 1, 0),
1909
1970
  ReferenceType.StayOnRemove,
1910
1971
  {
1911
1972
  obliterate,
@@ -1914,20 +1975,53 @@ export class MergeTree {
1914
1975
 
1915
1976
  obliterate.end = this.createLocalReferencePosition(
1916
1977
  endSeg,
1917
- endSeg.cachedLength - 1,
1978
+ end.side === Side.Before ? 0 : Math.max(endSeg.cachedLength - 1, 0),
1918
1979
  ReferenceType.StayOnRemove,
1919
1980
  {
1920
1981
  obliterate,
1921
1982
  },
1922
1983
  );
1923
1984
 
1985
+ // Always create a segment group for obliterate,
1986
+ // even if there are no segments currently in the obliteration range.
1987
+ // Segments may be concurrently inserted into the obliteration range,
1988
+ // at which point they are added to the segment group.
1989
+ obliterate.segmentGroup = {
1990
+ segments: [],
1991
+ localSeq,
1992
+ refSeq: this.collabWindow.currentSeq,
1993
+ obliterateInfo: obliterate,
1994
+ };
1995
+ if (this.collabWindow.collaborating && clientId === this.collabWindow.clientId) {
1996
+ this.pendingSegments.push(obliterate.segmentGroup);
1997
+ }
1998
+ this.obliterates.addOrUpdate(obliterate);
1999
+
1924
2000
  const markMoved = (
1925
- segment: ISegment,
2001
+ segment: ISegmentLeaf,
1926
2002
  pos: number,
1927
2003
  _start: number,
1928
2004
  _end: number,
1929
2005
  ): boolean => {
2006
+ if (
2007
+ (start.side === Side.After && startPos === pos + segment.cachedLength) || // exclusive start segment
2008
+ (end.side === Side.Before &&
2009
+ endPos === pos &&
2010
+ isSegmentPresent(segment, { refSeq, localSeq })) // exclusive end segment
2011
+ ) {
2012
+ // We walk these segments because we want to also walk any concurrently inserted segments between here and the obliterated segments.
2013
+ // These segments are outside of the obliteration range though, so return true to keep walking.
2014
+ return true;
2015
+ }
1930
2016
  const existingMoveInfo = toMoveInfo(segment);
2017
+
2018
+ if (segment.prevObliterateByInserter?.seq === UnassignedSequenceNumber) {
2019
+ // We chose to not obliterate this segment because we are aware of an unacked local obliteration.
2020
+ // The local obliterate has not been sequenced yet, so it is still the newest obliterate we are aware of.
2021
+ // Other clients will also choose not to obliterate this segment because the most recent obliteration has the same clientId
2022
+ return true;
2023
+ }
2024
+
1931
2025
  if (
1932
2026
  clientId !== segment.clientId &&
1933
2027
  segment.seq !== undefined &&
@@ -1978,7 +2072,6 @@ export class MergeTree {
1978
2072
  obliterate.segmentGroup,
1979
2073
  localSeq,
1980
2074
  );
1981
- obliterate.segmentGroup.obliterateInfo ??= obliterate;
1982
2075
  } else {
1983
2076
  if (MergeTree.options.zamboniSegments) {
1984
2077
  this.addToLRUSet(segment, seq);
@@ -2008,14 +2101,12 @@ export class MergeTree {
2008
2101
  markMoved,
2009
2102
  undefined,
2010
2103
  afterMarkMoved,
2011
- start,
2012
- end,
2104
+ start.pos,
2105
+ end.pos + 1, // include the segment containing the end reference
2013
2106
  undefined,
2014
2107
  seq === UnassignedSequenceNumber ? undefined : seq,
2015
2108
  );
2016
2109
 
2017
- this.obliterates.addOrUpdate(obliterate);
2018
-
2019
2110
  this.slideAckedRemovedSegmentReferences(localOverlapWithRefs);
2020
2111
  // opArgs == undefined => test code
2021
2112
  if (movedSegments.length > 0) {
@@ -2041,6 +2132,39 @@ export class MergeTree {
2041
2132
  }
2042
2133
  }
2043
2134
 
2135
+ public obliterateRange(
2136
+ start: number | InteriorSequencePlace,
2137
+ end: number | InteriorSequencePlace,
2138
+ refSeq: number,
2139
+ clientId: number,
2140
+ seq: number,
2141
+ overwrite: boolean = false,
2142
+ opArgs: IMergeTreeDeltaOpArgs,
2143
+ ): void {
2144
+ errorIfOptionNotTrue(this.options, "mergeTreeEnableObliterate");
2145
+ if (this.options?.mergeTreeEnableSidedObliterate) {
2146
+ assert(
2147
+ typeof start === "object" && typeof end === "object",
2148
+ "Start and end must be of type InteriorSequencePlace if mergeTreeEnableSidedObliterate is enabled.",
2149
+ );
2150
+ this.obliterateRangeSided(start, end, refSeq, clientId, seq, overwrite, opArgs);
2151
+ } else {
2152
+ assert(
2153
+ typeof start === "number" && typeof end === "number",
2154
+ "Start and end must be numbers if mergeTreeEnableSidedObliterate is not enabled.",
2155
+ );
2156
+ this.obliterateRangeSided(
2157
+ { pos: start, side: Side.Before },
2158
+ { pos: end - 1, side: Side.After },
2159
+ refSeq,
2160
+ clientId,
2161
+ seq,
2162
+ overwrite,
2163
+ opArgs,
2164
+ );
2165
+ }
2166
+ }
2167
+
2044
2168
  public markRangeRemoved(
2045
2169
  start: number,
2046
2170
  end: number,
@@ -2056,11 +2180,11 @@ export class MergeTree {
2056
2180
  // eslint-disable-next-line import/no-deprecated
2057
2181
  let segmentGroup: SegmentGroup;
2058
2182
  const removedSegments: IMergeTreeSegmentDelta[] = [];
2059
- const localOverlapWithRefs: ISegment[] = [];
2183
+ const localOverlapWithRefs: ISegmentLeaf[] = [];
2060
2184
  const localSeq =
2061
2185
  seq === UnassignedSequenceNumber ? ++this.collabWindow.localSeq : undefined;
2062
2186
  const markRemoved = (
2063
- segment: ISegment,
2187
+ segment: ISegmentLeaf,
2064
2188
  pos: number,
2065
2189
  _start: number,
2066
2190
  _end: number,
@@ -2159,10 +2283,10 @@ export class MergeTree {
2159
2283
  if (pendingSegmentGroup === undefined || pendingSegmentGroup !== localOpMetadata) {
2160
2284
  throw new Error("Rollback op doesn't match last edit");
2161
2285
  }
2162
- // Disabling because a for of loop causes the type of segment to be ISegment, which does not have parent information stored
2286
+ // Disabling because a for of loop causes the type of segment to be ISegmentLeaf, which does not have parent information stored
2163
2287
  // eslint-disable-next-line unicorn/no-array-for-each
2164
2288
  pendingSegmentGroup.segments.forEach((segment: ISegmentLeaf) => {
2165
- const segmentSegmentGroup = segment.segmentGroups?.pop?.();
2289
+ const segmentSegmentGroup = segment?.segmentGroups?.pop?.();
2166
2290
  assert(
2167
2291
  segmentSegmentGroup === pendingSegmentGroup,
2168
2292
  0x3ee /* Unexpected segmentGroup in segment */,
@@ -2212,8 +2336,8 @@ export class MergeTree {
2212
2336
  throw new Error("Rollback op doesn't match last edit");
2213
2337
  }
2214
2338
  let i = 0;
2215
- for (const segment of pendingSegmentGroup.segments) {
2216
- const segmentSegmentGroup = segment.segmentGroups.pop?.();
2339
+ for (const segment of pendingSegmentGroup.segments as ISegmentLeaf[]) {
2340
+ const segmentSegmentGroup = segment?.segmentGroups?.pop?.();
2217
2341
  assert(
2218
2342
  segmentSegmentGroup === pendingSegmentGroup,
2219
2343
  0x3ef /* Unexpected segmentGroup in segment */,
@@ -2244,6 +2368,7 @@ export class MergeTree {
2244
2368
  this.collabWindow.clientId,
2245
2369
  UniversalSequenceNumber,
2246
2370
  { op: annotateOp },
2371
+
2247
2372
  // eslint-disable-next-line import/no-deprecated
2248
2373
  PropertiesRollback.Rollback,
2249
2374
  );
@@ -2258,7 +2383,7 @@ export class MergeTree {
2258
2383
  /**
2259
2384
  * Walk the segments up to the current segment and calculate its position
2260
2385
  */
2261
- private findRollbackPosition(segment: ISegment): number {
2386
+ private findRollbackPosition(segment: ISegmentLeaf): number {
2262
2387
  let segmentPosition = 0;
2263
2388
  walkAllChildSegments(this.root, (seg) => {
2264
2389
  // If we've found the desired segment, terminate the walk and return 'segmentPosition'.
@@ -2361,7 +2486,7 @@ export class MergeTree {
2361
2486
  }
2362
2487
 
2363
2488
  for (
2364
- let segmentToSlide: ListNode<ISegment> | undefined = lastLocalSegment,
2489
+ let segmentToSlide: ListNode<ISegmentLeaf> | undefined = lastLocalSegment,
2365
2490
  nearerSegment = lastLocalSegment?.prev;
2366
2491
  segmentToSlide !== undefined;
2367
2492
  segmentToSlide = nearerSegment, nearerSegment = nearerSegment?.prev
@@ -2399,7 +2524,7 @@ export class MergeTree {
2399
2524
  const newOrder = Array.from(affectedSegments, ({ data }) => data);
2400
2525
  for (const seg of newOrder)
2401
2526
  seg.localRefs?.walkReferences((lref) => lref.callbacks?.beforeSlide?.(lref));
2402
- const perSegmentTrackingGroups = new Map<ISegment, TrackingGroup[]>();
2527
+ const perSegmentTrackingGroups = new Map<ISegmentLeaf, TrackingGroup[]>();
2403
2528
  for (const segment of newOrder) {
2404
2529
  const { trackingCollection } = segment;
2405
2530
  const trackingGroups = [...trackingCollection.trackingGroups];
@@ -2464,7 +2589,7 @@ export class MergeTree {
2464
2589
  * it can fix up its local state to align with what would be expected of the op it resubmits.
2465
2590
  */
2466
2591
  public normalizeSegmentsOnRebase(): void {
2467
- let currentRangeToNormalize = new DoublyLinkedList<ISegment>();
2592
+ let currentRangeToNormalize = new DoublyLinkedList<ISegmentLeaf>();
2468
2593
  let rangeContainsLocalSegs = false;
2469
2594
  let rangeContainsRemoteRemovedSegs = false;
2470
2595
  const normalize = (): void => {
@@ -2487,7 +2612,7 @@ export class MergeTree {
2487
2612
  currentRangeToNormalize.push(seg);
2488
2613
  } else {
2489
2614
  normalize();
2490
- currentRangeToNormalize = new DoublyLinkedList<ISegment>();
2615
+ currentRangeToNormalize = new DoublyLinkedList<ISegmentLeaf>();
2491
2616
  rangeContainsLocalSegs = false;
2492
2617
  rangeContainsRemoteRemovedSegs = false;
2493
2618
  }