@fluidframework/merge-tree 2.3.1 → 2.4.0-297027

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 (118) hide show
  1. package/api-report/merge-tree.legacy.alpha.api.md +26 -7
  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 -10
  8. package/dist/client.js.map +1 -1
  9. package/dist/index.d.ts +1 -1
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/index.js.map +1 -1
  12. package/dist/legacy.d.ts +1 -0
  13. package/dist/mergeTree.d.ts +15 -2
  14. package/dist/mergeTree.d.ts.map +1 -1
  15. package/dist/mergeTree.js +68 -48
  16. package/dist/mergeTree.js.map +1 -1
  17. package/dist/mergeTreeNodes.d.ts +6 -9
  18. package/dist/mergeTreeNodes.d.ts.map +1 -1
  19. package/dist/mergeTreeNodes.js +2 -1
  20. package/dist/mergeTreeNodes.js.map +1 -1
  21. package/dist/opBuilder.d.ts +15 -1
  22. package/dist/opBuilder.d.ts.map +1 -1
  23. package/dist/opBuilder.js +28 -1
  24. package/dist/opBuilder.js.map +1 -1
  25. package/dist/ops.d.ts +27 -1
  26. package/dist/ops.d.ts.map +1 -1
  27. package/dist/ops.js +1 -0
  28. package/dist/ops.js.map +1 -1
  29. package/dist/sequencePlace.d.ts +4 -0
  30. package/dist/sequencePlace.d.ts.map +1 -1
  31. package/dist/sequencePlace.js +17 -1
  32. package/dist/sequencePlace.js.map +1 -1
  33. package/dist/test/obliterate.concurrent.spec.js +18 -0
  34. package/dist/test/obliterate.concurrent.spec.js.map +1 -1
  35. package/dist/test/obliterate.partialLength.spec.js +8 -4
  36. package/dist/test/obliterate.partialLength.spec.js.map +1 -1
  37. package/dist/test/obliterate.rangeExpansion.spec.js +109 -53
  38. package/dist/test/obliterate.rangeExpansion.spec.js.map +1 -1
  39. package/dist/test/obliterate.spec.js +14 -8
  40. package/dist/test/obliterate.spec.js.map +1 -1
  41. package/dist/test/reconnectHelper.d.ts +8 -6
  42. package/dist/test/reconnectHelper.d.ts.map +1 -1
  43. package/dist/test/reconnectHelper.js +14 -13
  44. package/dist/test/reconnectHelper.js.map +1 -1
  45. package/dist/test/testClient.d.ts +1 -11
  46. package/dist/test/testClient.d.ts.map +1 -1
  47. package/dist/test/testClient.js +0 -3
  48. package/dist/test/testClient.js.map +1 -1
  49. package/dist/test/testClientLogger.d.ts.map +1 -1
  50. package/dist/test/testClientLogger.js +19 -8
  51. package/dist/test/testClientLogger.js.map +1 -1
  52. package/dist/test/testUtils.d.ts +10 -0
  53. package/dist/test/testUtils.d.ts.map +1 -1
  54. package/dist/test/testUtils.js +5 -1
  55. package/dist/test/testUtils.js.map +1 -1
  56. package/lib/attributionPolicy.d.ts.map +1 -1
  57. package/lib/attributionPolicy.js +10 -3
  58. package/lib/attributionPolicy.js.map +1 -1
  59. package/lib/client.d.ts +14 -4
  60. package/lib/client.d.ts.map +1 -1
  61. package/lib/client.js +98 -11
  62. package/lib/client.js.map +1 -1
  63. package/lib/index.d.ts +1 -1
  64. package/lib/index.d.ts.map +1 -1
  65. package/lib/index.js.map +1 -1
  66. package/lib/legacy.d.ts +1 -0
  67. package/lib/mergeTree.d.ts +15 -2
  68. package/lib/mergeTree.d.ts.map +1 -1
  69. package/lib/mergeTree.js +69 -49
  70. package/lib/mergeTree.js.map +1 -1
  71. package/lib/mergeTreeNodes.d.ts +6 -9
  72. package/lib/mergeTreeNodes.d.ts.map +1 -1
  73. package/lib/mergeTreeNodes.js +2 -1
  74. package/lib/mergeTreeNodes.js.map +1 -1
  75. package/lib/opBuilder.d.ts +15 -1
  76. package/lib/opBuilder.d.ts.map +1 -1
  77. package/lib/opBuilder.js +26 -0
  78. package/lib/opBuilder.js.map +1 -1
  79. package/lib/ops.d.ts +27 -1
  80. package/lib/ops.d.ts.map +1 -1
  81. package/lib/ops.js +1 -0
  82. package/lib/ops.js.map +1 -1
  83. package/lib/sequencePlace.d.ts +4 -0
  84. package/lib/sequencePlace.d.ts.map +1 -1
  85. package/lib/sequencePlace.js +15 -0
  86. package/lib/sequencePlace.js.map +1 -1
  87. package/lib/test/obliterate.concurrent.spec.js +18 -0
  88. package/lib/test/obliterate.concurrent.spec.js.map +1 -1
  89. package/lib/test/obliterate.partialLength.spec.js +9 -5
  90. package/lib/test/obliterate.partialLength.spec.js.map +1 -1
  91. package/lib/test/obliterate.rangeExpansion.spec.js +109 -53
  92. package/lib/test/obliterate.rangeExpansion.spec.js.map +1 -1
  93. package/lib/test/obliterate.spec.js +15 -9
  94. package/lib/test/obliterate.spec.js.map +1 -1
  95. package/lib/test/reconnectHelper.d.ts +8 -6
  96. package/lib/test/reconnectHelper.d.ts.map +1 -1
  97. package/lib/test/reconnectHelper.js +15 -14
  98. package/lib/test/reconnectHelper.js.map +1 -1
  99. package/lib/test/testClient.d.ts +1 -11
  100. package/lib/test/testClient.d.ts.map +1 -1
  101. package/lib/test/testClient.js +0 -3
  102. package/lib/test/testClient.js.map +1 -1
  103. package/lib/test/testClientLogger.d.ts.map +1 -1
  104. package/lib/test/testClientLogger.js +19 -8
  105. package/lib/test/testClientLogger.js.map +1 -1
  106. package/lib/test/testUtils.d.ts +10 -0
  107. package/lib/test/testUtils.d.ts.map +1 -1
  108. package/lib/test/testUtils.js +3 -0
  109. package/lib/test/testUtils.js.map +1 -1
  110. package/package.json +30 -17
  111. package/src/attributionPolicy.ts +5 -0
  112. package/src/client.ts +138 -20
  113. package/src/index.ts +1 -0
  114. package/src/mergeTree.ts +116 -77
  115. package/src/mergeTreeNodes.ts +9 -10
  116. package/src/opBuilder.ts +32 -0
  117. package/src/ops.ts +23 -1
  118. package/src/sequencePlace.ts +16 -0
package/src/client.ts CHANGED
@@ -56,6 +56,7 @@ import {
56
56
  createGroupOp,
57
57
  createInsertSegmentOp,
58
58
  createObliterateRangeOp,
59
+ createObliterateRangeOpSided,
59
60
  createRemoveRangeOp,
60
61
  } from "./opBuilder.js";
61
62
  import {
@@ -72,9 +73,11 @@ import {
72
73
  IRelativePosition,
73
74
  MergeTreeDeltaType,
74
75
  ReferenceType,
76
+ type IMergeTreeObliterateSidedMsg,
75
77
  } from "./ops.js";
76
78
  import { PropertySet } from "./properties.js";
77
79
  import { DetachedReferencePosition, ReferencePosition } from "./referencePositions.js";
80
+ import { Side, type InteriorSequencePlace } from "./sequencePlace.js";
78
81
  import { SnapshotLoader } from "./snapshotLoader.js";
79
82
  import { SnapshotV1 } from "./snapshotV1.js";
80
83
  import { SnapshotLegacy } from "./snapshotlegacy.js";
@@ -99,6 +102,7 @@ export interface IIntegerRange {
99
102
  * they need for rebasing their ops on reconnection.
100
103
  * @legacy
101
104
  * @alpha
105
+ * @deprecated This functionality was not meant to be exported and will be removed in a future release
102
106
  */
103
107
  export interface IClientEvents {
104
108
  (event: "normalize", listener: (target: IEventThisPlaceHolder) => void): void;
@@ -252,12 +256,26 @@ export class Client extends TypedEventEmitter<IClientEvents> {
252
256
  * Obliterates the range. This is similar to removing the range, but also
253
257
  * includes any concurrently inserted content.
254
258
  *
255
- * @param start - The inclusive start of the range to obliterate
256
- * @param end - The exclusive end of the range to obliterate
259
+ * @param start - The start of the range to obliterate. Inclusive is side is Before (default).
260
+ * @param end - The end of the range to obliterate. Exclusive is side is After
261
+ * (default is to be after the last included character, but number index is exclusive).
257
262
  */
258
- // eslint-disable-next-line import/no-deprecated
259
- public obliterateRangeLocal(start: number, end: number): IMergeTreeObliterateMsg {
260
- const obliterateOp = createObliterateRangeOp(start, end);
263
+ public obliterateRangeLocal(
264
+ start: number | InteriorSequencePlace,
265
+ end: number | InteriorSequencePlace,
266
+ // eslint-disable-next-line import/no-deprecated
267
+ ): IMergeTreeObliterateMsg | IMergeTreeObliterateSidedMsg {
268
+ // eslint-disable-next-line import/no-deprecated
269
+ let obliterateOp: IMergeTreeObliterateMsg | IMergeTreeObliterateSidedMsg;
270
+ if (this._mergeTree.options?.mergeTreeEnableSidedObliterate) {
271
+ obliterateOp = createObliterateRangeOpSided(start, end);
272
+ } else {
273
+ assert(
274
+ typeof start === "number" && typeof end === "number",
275
+ "Start and end must be numbers if mergeTreeEnableSidedObliterate is not enabled.",
276
+ );
277
+ obliterateOp = createObliterateRangeOp(start, end);
278
+ }
261
279
  this.applyObliterateRangeOp({ op: obliterateOp });
262
280
  return obliterateOp;
263
281
  }
@@ -469,22 +487,39 @@ export class Client extends TypedEventEmitter<IClientEvents> {
469
487
 
470
488
  private applyObliterateRangeOp(opArgs: IMergeTreeDeltaOpArgs): void {
471
489
  assert(
472
- opArgs.op.type === MergeTreeDeltaType.OBLITERATE,
490
+ opArgs.op.type === MergeTreeDeltaType.OBLITERATE ||
491
+ opArgs.op.type === MergeTreeDeltaType.OBLITERATE_SIDED,
473
492
  0x866 /* Unexpected op type on range obliterate! */,
474
493
  );
475
494
  const op = opArgs.op;
476
495
  const clientArgs = this.getClientSequenceArgs(opArgs);
477
- const range = this.getValidOpRange(op, clientArgs);
478
-
479
- this._mergeTree.obliterateRange(
480
- range.start,
481
- range.end,
482
- clientArgs.referenceSequenceNumber,
483
- clientArgs.clientId,
484
- clientArgs.sequenceNumber,
485
- false,
486
- opArgs,
487
- );
496
+ if (this._mergeTree.options?.mergeTreeEnableSidedObliterate) {
497
+ const { start, end } = this.getValidSidedRange(op, clientArgs);
498
+ this._mergeTree.obliterateRange(
499
+ start,
500
+ end,
501
+ clientArgs.referenceSequenceNumber,
502
+ clientArgs.clientId,
503
+ clientArgs.sequenceNumber,
504
+ false,
505
+ opArgs,
506
+ );
507
+ } else {
508
+ assert(
509
+ op.type === MergeTreeDeltaType.OBLITERATE,
510
+ "Unexpected sided obliterate while mergeTreeEnableSidedObliterate is disabled",
511
+ );
512
+ const range = this.getValidOpRange(op, clientArgs);
513
+ this._mergeTree.obliterateRange(
514
+ range.start,
515
+ range.end,
516
+ clientArgs.referenceSequenceNumber,
517
+ clientArgs.clientId,
518
+ clientArgs.sequenceNumber,
519
+ false,
520
+ opArgs,
521
+ );
522
+ }
488
523
  }
489
524
 
490
525
  /**
@@ -562,6 +597,82 @@ export class Client extends TypedEventEmitter<IClientEvents> {
562
597
  );
563
598
  }
564
599
 
600
+ /**
601
+ * Returns a valid range for the op, or throws if the range is invalid
602
+ * @param op - The op to generate the range for
603
+ * @param clientArgs - The client args for the op
604
+ * @throws LoggingError if the range is invalid
605
+ */
606
+ private getValidSidedRange(
607
+ // eslint-disable-next-line import/no-deprecated
608
+ op: IMergeTreeObliterateSidedMsg | IMergeTreeObliterateMsg,
609
+ clientArgs: IMergeTreeClientSequenceArgs,
610
+ ): {
611
+ start: InteriorSequencePlace;
612
+ end: InteriorSequencePlace;
613
+ } {
614
+ const invalidPositions: string[] = [];
615
+ let start: InteriorSequencePlace | undefined;
616
+ let end: InteriorSequencePlace | undefined;
617
+ if (op.pos1 === undefined) {
618
+ invalidPositions.push("start");
619
+ } else {
620
+ start =
621
+ typeof op.pos1 === "object"
622
+ ? { pos: op.pos1.pos, side: op.pos1.before ? Side.Before : Side.After }
623
+ : { pos: op.pos1, side: Side.Before };
624
+ }
625
+ if (op.pos2 === undefined) {
626
+ invalidPositions.push("end");
627
+ } else {
628
+ end =
629
+ typeof op.pos2 === "object"
630
+ ? { pos: op.pos2.pos, side: op.pos2.before ? Side.Before : Side.After }
631
+ : { pos: op.pos2 - 1, side: Side.After };
632
+ }
633
+
634
+ // Validate if local op
635
+ if (clientArgs.clientId === this.getClientId()) {
636
+ const length = this._mergeTree.getLength(
637
+ this.getCollabWindow().currentSeq,
638
+ this.getClientId(),
639
+ );
640
+ if (start !== undefined && (start.pos >= length || start.pos < 0)) {
641
+ // start out of bounds
642
+ invalidPositions.push("start");
643
+ }
644
+ if (end !== undefined && (end.pos >= length || end.pos < 0)) {
645
+ invalidPositions.push("end");
646
+ }
647
+ if (
648
+ start !== undefined &&
649
+ end !== undefined &&
650
+ (start.pos > end.pos ||
651
+ (start.pos === end.pos && start.side !== end.side && start.side === Side.After))
652
+ ) {
653
+ // end is before start
654
+ invalidPositions.push("inverted");
655
+ }
656
+ if (invalidPositions.length > 0) {
657
+ throw new LoggingError("InvalidRange", {
658
+ usageError: true,
659
+ invalidPositions: invalidPositions.toString(),
660
+ length,
661
+ opType: op.type,
662
+ opPos1Relative: op.relativePos1 !== undefined,
663
+ opPos2Relative: op.relativePos2 !== undefined,
664
+ opPos1: JSON.stringify(op.pos1),
665
+ opPos2: JSON.stringify(op.pos2),
666
+ start: JSON.stringify(start),
667
+ end: JSON.stringify(end),
668
+ });
669
+ }
670
+ }
671
+
672
+ assert(start !== undefined && end !== undefined, "Missing start or end of range");
673
+ return { start, end };
674
+ }
675
+
565
676
  /**
566
677
  * Returns a valid range for the op, or undefined
567
678
  * @param op - The op to generate the range for
@@ -611,7 +722,6 @@ export class Client extends TypedEventEmitter<IClientEvents> {
611
722
  invalidPositions.push("start");
612
723
  }
613
724
  // Validate end if not insert, or insert has end
614
- //
615
725
  if (
616
726
  (op.type !== MergeTreeDeltaType.INSERT || end !== undefined) &&
617
727
  (end === undefined || end <= start!)
@@ -889,7 +999,12 @@ export class Client extends TypedEventEmitter<IClientEvents> {
889
999
 
890
1000
  const first = opList[0];
891
1001
 
892
- if (!!first && first.pos2 !== undefined) {
1002
+ if (
1003
+ !!first &&
1004
+ first.pos2 !== undefined &&
1005
+ first.type !== MergeTreeDeltaType.OBLITERATE_SIDED &&
1006
+ newOp.type !== MergeTreeDeltaType.OBLITERATE_SIDED
1007
+ ) {
893
1008
  first.pos2 += newOp.pos2! - newOp.pos1!;
894
1009
  } else {
895
1010
  opList.push(newOp);
@@ -936,7 +1051,8 @@ export class Client extends TypedEventEmitter<IClientEvents> {
936
1051
  this.applyAnnotateRangeOp(opArgs);
937
1052
  break;
938
1053
  }
939
- case MergeTreeDeltaType.OBLITERATE: {
1054
+ case MergeTreeDeltaType.OBLITERATE:
1055
+ case MergeTreeDeltaType.OBLITERATE_SIDED: {
940
1056
  this.applyObliterateRangeOp(opArgs);
941
1057
  break;
942
1058
  }
@@ -970,6 +1086,7 @@ export class Client extends TypedEventEmitter<IClientEvents> {
970
1086
  this.applyAnnotateRangeOp({ op });
971
1087
  break;
972
1088
  }
1089
+ case MergeTreeDeltaType.OBLITERATE_SIDED:
973
1090
  case MergeTreeDeltaType.OBLITERATE: {
974
1091
  this.applyObliterateRangeOp({ op });
975
1092
  break;
@@ -1202,6 +1319,7 @@ export class Client extends TypedEventEmitter<IClientEvents> {
1202
1319
  this.applyRemoveRangeOp(opArgs);
1203
1320
  break;
1204
1321
  }
1322
+ case MergeTreeDeltaType.OBLITERATE_SIDED:
1205
1323
  case MergeTreeDeltaType.OBLITERATE: {
1206
1324
  this.applyObliterateRangeOp(opArgs);
1207
1325
  break;
package/src/index.ts CHANGED
@@ -100,6 +100,7 @@ export {
100
100
  MergeTreeDeltaType,
101
101
  ReferenceType,
102
102
  IMergeTreeObliterateMsg,
103
+ IMergeTreeObliterateSidedMsg,
103
104
  } from "./ops.js";
104
105
  export {
105
106
  addProperties,
package/src/mergeTree.ts CHANGED
@@ -90,7 +90,7 @@ import {
90
90
  } from "./referencePositions.js";
91
91
  // eslint-disable-next-line import/no-deprecated
92
92
  import { PropertiesRollback } from "./segmentPropertiesManager.js";
93
- import { endpointPosAndSide, type SequencePlace } from "./sequencePlace.js";
93
+ import { Side, type InteriorSequencePlace } from "./sequencePlace.js";
94
94
  import { SortedSegmentSet } from "./sortedSegmentSet.js";
95
95
  import { zamboniSegments } from "./zamboni.js";
96
96
 
@@ -198,6 +198,17 @@ export interface IMergeTreeOptions {
198
198
  * @defaultValue `false`
199
199
  */
200
200
  mergeTreeEnableObliterateReconnect?: boolean;
201
+
202
+ /**
203
+ * Enables support for obliterate endpoint expansion.
204
+ * When enabled, obliterate operations can have sidedness specified for their endpoints.
205
+ * If an endpoint is externally anchored
206
+ * (aka the start is after a given position, or the end is before a given position),
207
+ * then concurrent inserts adjacent to the exclusive endpoint of an obliterated range will be included in the obliteration
208
+ *
209
+ * @defaultValue `false`
210
+ */
211
+ mergeTreeEnableSidedObliterate?: boolean;
201
212
  }
202
213
  export function errorIfOptionNotTrue(
203
214
  options: IMergeTreeOptions | undefined,
@@ -211,6 +222,7 @@ export function errorIfOptionNotTrue(
211
222
  /**
212
223
  * @legacy
213
224
  * @alpha
225
+ * @deprecated This functionality was not meant to be exported and will be removed in a future release
214
226
  */
215
227
  export interface IMergeTreeAttributionOptions {
216
228
  /**
@@ -238,6 +250,7 @@ export interface IMergeTreeAttributionOptions {
238
250
  * @sealed
239
251
  * @legacy
240
252
  * @alpha
253
+ * @deprecated This functionality was not meant to be exported and will be removed in a future release
241
254
  */
242
255
  export interface AttributionPolicy {
243
256
  /**
@@ -1235,7 +1248,10 @@ export class MergeTree {
1235
1248
  });
1236
1249
  });
1237
1250
 
1238
- if (opArgs.op.type === MergeTreeDeltaType.OBLITERATE) {
1251
+ if (
1252
+ opArgs.op.type === MergeTreeDeltaType.OBLITERATE ||
1253
+ opArgs.op.type === MergeTreeDeltaType.OBLITERATE_SIDED
1254
+ ) {
1239
1255
  this.obliterates.addOrUpdate(pendingSegmentGroup.obliterateInfo!);
1240
1256
  }
1241
1257
 
@@ -1243,7 +1259,8 @@ export class MergeTree {
1243
1259
  // positions after slide are final
1244
1260
  if (
1245
1261
  opArgs.op.type === MergeTreeDeltaType.REMOVE ||
1246
- opArgs.op.type === MergeTreeDeltaType.OBLITERATE
1262
+ opArgs.op.type === MergeTreeDeltaType.OBLITERATE ||
1263
+ opArgs.op.type === MergeTreeDeltaType.OBLITERATE_SIDED
1247
1264
  ) {
1248
1265
  this.slideAckedRemovedSegmentReferences(pendingSegmentGroup.segments);
1249
1266
  }
@@ -1511,11 +1528,11 @@ export class MergeTree {
1511
1528
  }
1512
1529
 
1513
1530
  this.updateRoot(splitNode);
1514
- saveIfLocal(newSegment);
1515
1531
 
1516
1532
  insertPos += newSegment.cachedLength;
1517
1533
 
1518
1534
  if (!this.options?.mergeTreeEnableObliterate || this.obliterates.empty()) {
1535
+ saveIfLocal(newSegment);
1519
1536
  continue;
1520
1537
  }
1521
1538
 
@@ -1543,13 +1560,13 @@ export class MergeTree {
1543
1560
  movedClientIds.unshift(ob.clientId);
1544
1561
  movedSeqs.unshift(ob.seq);
1545
1562
  } else {
1546
- if (newest === undefined || normalizedNewestSeq < normalizedObSeq) {
1547
- normalizedNewestSeq = normalizedObSeq;
1548
- newest = ob;
1549
- }
1550
1563
  movedClientIds.push(ob.clientId);
1551
1564
  movedSeqs.push(ob.seq);
1552
1565
  }
1566
+ if (newest === undefined || normalizedNewestSeq < normalizedObSeq) {
1567
+ normalizedNewestSeq = normalizedObSeq;
1568
+ newest = ob;
1569
+ }
1553
1570
  }
1554
1571
  }
1555
1572
 
@@ -1576,7 +1593,11 @@ export class MergeTree {
1576
1593
  if (newSegment.parent) {
1577
1594
  this.blockUpdatePathLengths(newSegment.parent, seq, clientId);
1578
1595
  }
1596
+ } else if (oldest && newest?.clientId === clientId) {
1597
+ newSegment.prevObliterateByInserter = newest;
1579
1598
  }
1599
+
1600
+ saveIfLocal(newSegment);
1580
1601
  }
1581
1602
  }
1582
1603
  }
@@ -1601,11 +1622,7 @@ export class MergeTree {
1601
1622
  return { next };
1602
1623
  };
1603
1624
 
1604
- private ensureIntervalBoundary(
1605
- pos: number | "start" | "end",
1606
- refSeq: number,
1607
- clientId: number,
1608
- ): void {
1625
+ private ensureIntervalBoundary(pos: number, refSeq: number, clientId: number): void {
1609
1626
  const splitNode = this.insertingWalk(
1610
1627
  this.root,
1611
1628
  pos,
@@ -1649,21 +1666,14 @@ export class MergeTree {
1649
1666
 
1650
1667
  private insertingWalk(
1651
1668
  block: MergeBlock,
1652
- pos: number | "start" | "end",
1669
+ pos: number,
1653
1670
  refSeq: number,
1654
1671
  clientId: number,
1655
1672
  seq: number,
1656
1673
  context: InsertContext,
1657
1674
  isLastChildBlock: boolean = true,
1658
1675
  ): MergeBlock | undefined {
1659
- let _pos: number;
1660
- if (pos === "start") {
1661
- _pos = 0;
1662
- } else if (pos === "end") {
1663
- _pos = this.root.mergeTree?.getLength(refSeq, clientId) ?? 0;
1664
- } else {
1665
- _pos = pos;
1666
- }
1676
+ let _pos: number = pos;
1667
1677
 
1668
1678
  const children = block.children;
1669
1679
  let childIndex: number;
@@ -1878,28 +1888,17 @@ export class MergeTree {
1878
1888
  }
1879
1889
  }
1880
1890
 
1881
- public obliterateRange(
1882
- start: SequencePlace,
1883
- end: SequencePlace,
1891
+ private obliterateRangeSided(
1892
+ start: InteriorSequencePlace,
1893
+ end: InteriorSequencePlace,
1884
1894
  refSeq: number,
1885
1895
  clientId: number,
1886
1896
  seq: number,
1887
1897
  overwrite: boolean = false,
1888
1898
  opArgs: IMergeTreeDeltaOpArgs,
1889
1899
  ): void {
1890
- errorIfOptionNotTrue(this.options, "mergeTreeEnableObliterate");
1891
-
1892
- const { startPos, startSide, endPos, endSide } = endpointPosAndSide(start, end);
1893
-
1894
- assert(
1895
- startPos !== undefined &&
1896
- endPos !== undefined &&
1897
- startSide !== undefined &&
1898
- endSide !== undefined &&
1899
- startPos !== "end" &&
1900
- endPos !== "start",
1901
- 0x9e2 /* start and end cannot be undefined because they were not passed in as undefined */,
1902
- );
1900
+ const startPos = start.side === Side.Before ? start.pos : start.pos + 1;
1901
+ const endPos = end.side === Side.Before ? end.pos : end.pos + 1;
1903
1902
 
1904
1903
  this.ensureIntervalBoundary(startPos, refSeq, clientId);
1905
1904
  this.ensureIntervalBoundary(endPos, refSeq, clientId);
@@ -1919,20 +1918,9 @@ export class MergeTree {
1919
1918
  localSeq,
1920
1919
  segmentGroup: undefined,
1921
1920
  };
1922
- const normalizedStartPos = startPos === "start" || startPos === undefined ? 0 : startPos;
1923
- const normalizedEndPos =
1924
- endPos === "end" || endPos === undefined ? this.getLength(refSeq, clientId) : endPos;
1925
1921
 
1926
- const { segment: startSeg } = this.getContainingSegment(
1927
- normalizedStartPos,
1928
- refSeq,
1929
- clientId,
1930
- );
1931
- const { segment: endSeg } = this.getContainingSegment(
1932
- normalizedEndPos - 1,
1933
- refSeq,
1934
- clientId,
1935
- );
1922
+ const { segment: startSeg } = this.getContainingSegment(start.pos, refSeq, clientId);
1923
+ const { segment: endSeg } = this.getContainingSegment(end.pos, refSeq, clientId);
1936
1924
  assert(
1937
1925
  startSeg !== undefined && endSeg !== undefined,
1938
1926
  0xa3f /* segments cannot be undefined */,
@@ -1940,7 +1928,7 @@ export class MergeTree {
1940
1928
 
1941
1929
  obliterate.start = this.createLocalReferencePosition(
1942
1930
  startSeg,
1943
- 0,
1931
+ start.side === Side.Before ? 0 : Math.max(startSeg.cachedLength - 1, 0),
1944
1932
  ReferenceType.StayOnRemove,
1945
1933
  {
1946
1934
  obliterate,
@@ -1949,20 +1937,53 @@ export class MergeTree {
1949
1937
 
1950
1938
  obliterate.end = this.createLocalReferencePosition(
1951
1939
  endSeg,
1952
- endSeg.cachedLength - 1,
1940
+ end.side === Side.Before ? 0 : Math.max(endSeg.cachedLength - 1, 0),
1953
1941
  ReferenceType.StayOnRemove,
1954
1942
  {
1955
1943
  obliterate,
1956
1944
  },
1957
1945
  );
1958
1946
 
1947
+ // Always create a segment group for obliterate,
1948
+ // even if there are no segments currently in the obliteration range.
1949
+ // Segments may be concurrently inserted into the obliteration range,
1950
+ // at which point they are added to the segment group.
1951
+ obliterate.segmentGroup = {
1952
+ segments: [],
1953
+ localSeq,
1954
+ refSeq: this.collabWindow.currentSeq,
1955
+ obliterateInfo: obliterate,
1956
+ };
1957
+ if (this.collabWindow.collaborating && clientId === this.collabWindow.clientId) {
1958
+ this.pendingSegments.push(obliterate.segmentGroup);
1959
+ }
1960
+ this.obliterates.addOrUpdate(obliterate);
1961
+
1959
1962
  const markMoved = (
1960
1963
  segment: ISegment,
1961
1964
  pos: number,
1962
1965
  _start: number,
1963
1966
  _end: number,
1964
1967
  ): boolean => {
1968
+ if (
1969
+ (start.side === Side.After && startPos === pos + segment.cachedLength) || // exclusive start segment
1970
+ (end.side === Side.Before &&
1971
+ endPos === pos &&
1972
+ isSegmentPresent(segment, { refSeq, localSeq })) // exclusive end segment
1973
+ ) {
1974
+ // We walk these segments because we want to also walk any concurrently inserted segments between here and the obliterated segments.
1975
+ // These segments are outside of the obliteration range though, so return true to keep walking.
1976
+ return true;
1977
+ }
1965
1978
  const existingMoveInfo = toMoveInfo(segment);
1979
+
1980
+ if (segment.prevObliterateByInserter?.seq === UnassignedSequenceNumber) {
1981
+ // We chose to not obliterate this segment because we are aware of an unacked local obliteration.
1982
+ // The local obliterate has not been sequenced yet, so it is still the newest obliterate we are aware of.
1983
+ // Other clients will also choose not to obliterate this segment because the most recent obliteration has the same clientId
1984
+ return true;
1985
+ }
1986
+
1966
1987
  if (
1967
1988
  clientId !== segment.clientId &&
1968
1989
  segment.seq !== undefined &&
@@ -2013,7 +2034,6 @@ export class MergeTree {
2013
2034
  obliterate.segmentGroup,
2014
2035
  localSeq,
2015
2036
  );
2016
- obliterate.segmentGroup.obliterateInfo ??= obliterate;
2017
2037
  } else {
2018
2038
  if (MergeTree.options.zamboniSegments) {
2019
2039
  this.addToLRUSet(segment, seq);
@@ -2043,14 +2063,12 @@ export class MergeTree {
2043
2063
  markMoved,
2044
2064
  undefined,
2045
2065
  afterMarkMoved,
2046
- start,
2047
- end,
2066
+ start.pos,
2067
+ end.pos + 1, // include the segment containing the end reference
2048
2068
  undefined,
2049
2069
  seq === UnassignedSequenceNumber ? undefined : seq,
2050
2070
  );
2051
2071
 
2052
- this.obliterates.addOrUpdate(obliterate);
2053
-
2054
2072
  this.slideAckedRemovedSegmentReferences(localOverlapWithRefs);
2055
2073
  // opArgs == undefined => test code
2056
2074
  if (movedSegments.length > 0) {
@@ -2076,6 +2094,39 @@ export class MergeTree {
2076
2094
  }
2077
2095
  }
2078
2096
 
2097
+ public obliterateRange(
2098
+ start: number | InteriorSequencePlace,
2099
+ end: number | InteriorSequencePlace,
2100
+ refSeq: number,
2101
+ clientId: number,
2102
+ seq: number,
2103
+ overwrite: boolean = false,
2104
+ opArgs: IMergeTreeDeltaOpArgs,
2105
+ ): void {
2106
+ errorIfOptionNotTrue(this.options, "mergeTreeEnableObliterate");
2107
+ if (this.options?.mergeTreeEnableSidedObliterate) {
2108
+ assert(
2109
+ typeof start === "object" && typeof end === "object",
2110
+ "Start and end must be of type InteriorSequencePlace if mergeTreeEnableSidedObliterate is enabled.",
2111
+ );
2112
+ this.obliterateRangeSided(start, end, refSeq, clientId, seq, overwrite, opArgs);
2113
+ } else {
2114
+ assert(
2115
+ typeof start === "number" && typeof end === "number",
2116
+ "Start and end must be numbers if mergeTreeEnableSidedObliterate is not enabled.",
2117
+ );
2118
+ this.obliterateRangeSided(
2119
+ { pos: start, side: Side.Before },
2120
+ { pos: end - 1, side: Side.After },
2121
+ refSeq,
2122
+ clientId,
2123
+ seq,
2124
+ overwrite,
2125
+ opArgs,
2126
+ );
2127
+ }
2128
+ }
2129
+
2079
2130
  public markRangeRemoved(
2080
2131
  start: number,
2081
2132
  end: number,
@@ -2684,28 +2735,18 @@ export class MergeTree {
2684
2735
  leaf: ISegmentAction<TClientData>,
2685
2736
  accum: TClientData,
2686
2737
  post?: BlockAction<TClientData>,
2687
- start: SequencePlace = 0,
2688
- end?: SequencePlace,
2738
+ start: number = 0,
2739
+ end?: number,
2689
2740
  localSeq?: number,
2690
2741
  visibilitySeq: number = refSeq,
2691
2742
  ): void {
2692
- const maybeEndPos = end ?? this.nodeLength(this.root, refSeq, clientId, localSeq) ?? 0;
2693
- if (maybeEndPos === start) {
2743
+ const endPos = end ?? this.nodeLength(this.root, refSeq, clientId, localSeq) ?? 0;
2744
+ if (endPos === start) {
2694
2745
  return;
2695
2746
  }
2696
2747
 
2697
2748
  let pos = 0;
2698
- let { startPos, endPos } = endpointPosAndSide(start, end);
2699
2749
 
2700
- startPos = startPos === "start" || startPos === undefined ? 0 : startPos;
2701
- endPos =
2702
- endPos === "end" || endPos === undefined
2703
- ? this.root.mergeTree?.getLength(refSeq, clientId) ?? 0
2704
- : endPos;
2705
- assert(
2706
- startPos !== "end" && endPos !== "start",
2707
- 0x9e3 /* start cannot be 'end' and end cannot be 'start' */,
2708
- );
2709
2750
  depthFirstNodeWalk(
2710
2751
  this.root,
2711
2752
  this.root.children[0],
@@ -2733,15 +2774,13 @@ export class MergeTree {
2733
2774
 
2734
2775
  const nextPos = pos + lenAtRefSeq;
2735
2776
  // start is beyond the current node, so we can skip it
2736
- if (typeof startPos === "number" && startPos >= nextPos) {
2777
+ if (start >= nextPos) {
2737
2778
  pos = nextPos;
2738
2779
  return NodeAction.Skip;
2739
2780
  }
2740
2781
 
2741
2782
  if (node.isLeaf()) {
2742
- if (
2743
- leaf(node, pos, refSeq, clientId, startPos - pos, endPos - pos, accum) === false
2744
- ) {
2783
+ if (leaf(node, pos, refSeq, clientId, start - pos, endPos - pos, accum) === false) {
2745
2784
  return NodeAction.Exit;
2746
2785
  }
2747
2786
  pos = nextPos;
@@ -2751,7 +2790,7 @@ export class MergeTree {
2751
2790
  post === undefined
2752
2791
  ? undefined
2753
2792
  : (block): boolean =>
2754
- post(block, pos, refSeq, clientId, startPos - pos, endPos - pos, accum),
2793
+ post(block, pos, refSeq, clientId, start - pos, endPos - pos, accum),
2755
2794
  );
2756
2795
  }
2757
2796
  }
@@ -30,7 +30,6 @@ import {
30
30
  import { SegmentGroupCollection } from "./segmentGroupCollection.js";
31
31
  // eslint-disable-next-line import/no-deprecated
32
32
  import { PropertiesManager, PropertiesRollback } from "./segmentPropertiesManager.js";
33
- import { Side } from "./sequencePlace.js";
34
33
 
35
34
  /**
36
35
  * Common properties for a node in a merge tree.
@@ -161,6 +160,13 @@ export interface IMoveInfo {
161
160
  * calculations
162
161
  */
163
162
  wasMovedOnInsert: boolean;
163
+
164
+ /**
165
+ * If a segment is inserted into an obliterated range,
166
+ * but the newest obliteration of that range was by the inserting client,
167
+ * then the segment is not obliterated because it is aware of the latest obliteration.
168
+ */
169
+ prevObliterateByInserter?: ObliterateInfo;
164
170
  }
165
171
 
166
172
  export function toMoveInfo(maybe: Partial<IMoveInfo> | undefined): IMoveInfo | undefined {
@@ -263,14 +269,6 @@ export interface ISegment extends IMergeNodeCommon, Partial<IRemovalInfo>, Parti
263
269
  * Properties that have been added to this segment via annotation.
264
270
  */
265
271
  properties?: PropertySet;
266
- /**
267
- * Stores side information passed to obliterate for the start of a range.
268
- */
269
- startSide?: Side.Before | Side.After;
270
- /**
271
- * Stores side information passed to obliterate for the end of a range.
272
- */
273
- endSide?: Side.Before | Side.After;
274
272
 
275
273
  /**
276
274
  * Add properties to this segment via annotation.
@@ -670,7 +668,8 @@ export abstract class BaseSegment implements ISegment {
670
668
  return false;
671
669
  }
672
670
 
673
- case MergeTreeDeltaType.OBLITERATE: {
671
+ case MergeTreeDeltaType.OBLITERATE:
672
+ case MergeTreeDeltaType.OBLITERATE_SIDED: {
674
673
  const moveInfo: IMoveInfo | undefined = toMoveInfo(this);
675
674
  assert(moveInfo !== undefined, 0x86e /* On obliterate ack, missing move info! */);
676
675
  const obliterateInfo = segmentGroup.obliterateInfo;