@fluidframework/merge-tree 2.4.0-294316 → 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 (98) hide show
  1. package/api-report/merge-tree.legacy.alpha.api.md +26 -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 +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 -1
  14. package/dist/mergeTree.d.ts.map +1 -1
  15. package/dist/mergeTree.js +65 -19
  16. package/dist/mergeTree.js.map +1 -1
  17. package/dist/mergeTreeNodes.d.ts +6 -0
  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.rangeExpansion.spec.js +109 -53
  36. package/dist/test/obliterate.rangeExpansion.spec.js.map +1 -1
  37. package/dist/test/obliterate.spec.js +2 -2
  38. package/dist/test/obliterate.spec.js.map +1 -1
  39. package/dist/test/reconnectHelper.d.ts +8 -6
  40. package/dist/test/reconnectHelper.d.ts.map +1 -1
  41. package/dist/test/reconnectHelper.js +14 -13
  42. package/dist/test/reconnectHelper.js.map +1 -1
  43. package/dist/test/testClientLogger.d.ts.map +1 -1
  44. package/dist/test/testClientLogger.js +19 -8
  45. package/dist/test/testClientLogger.js.map +1 -1
  46. package/lib/attributionPolicy.d.ts.map +1 -1
  47. package/lib/attributionPolicy.js +10 -3
  48. package/lib/attributionPolicy.js.map +1 -1
  49. package/lib/client.d.ts +14 -4
  50. package/lib/client.d.ts.map +1 -1
  51. package/lib/client.js +98 -10
  52. package/lib/client.js.map +1 -1
  53. package/lib/index.d.ts +1 -1
  54. package/lib/index.d.ts.map +1 -1
  55. package/lib/index.js.map +1 -1
  56. package/lib/legacy.d.ts +1 -0
  57. package/lib/mergeTree.d.ts +15 -1
  58. package/lib/mergeTree.d.ts.map +1 -1
  59. package/lib/mergeTree.js +65 -19
  60. package/lib/mergeTree.js.map +1 -1
  61. package/lib/mergeTreeNodes.d.ts +6 -0
  62. package/lib/mergeTreeNodes.d.ts.map +1 -1
  63. package/lib/mergeTreeNodes.js +2 -1
  64. package/lib/mergeTreeNodes.js.map +1 -1
  65. package/lib/opBuilder.d.ts +15 -1
  66. package/lib/opBuilder.d.ts.map +1 -1
  67. package/lib/opBuilder.js +26 -0
  68. package/lib/opBuilder.js.map +1 -1
  69. package/lib/ops.d.ts +27 -1
  70. package/lib/ops.d.ts.map +1 -1
  71. package/lib/ops.js +1 -0
  72. package/lib/ops.js.map +1 -1
  73. package/lib/sequencePlace.d.ts +4 -0
  74. package/lib/sequencePlace.d.ts.map +1 -1
  75. package/lib/sequencePlace.js +15 -0
  76. package/lib/sequencePlace.js.map +1 -1
  77. package/lib/test/obliterate.concurrent.spec.js +18 -0
  78. package/lib/test/obliterate.concurrent.spec.js.map +1 -1
  79. package/lib/test/obliterate.rangeExpansion.spec.js +109 -53
  80. package/lib/test/obliterate.rangeExpansion.spec.js.map +1 -1
  81. package/lib/test/obliterate.spec.js +2 -2
  82. package/lib/test/obliterate.spec.js.map +1 -1
  83. package/lib/test/reconnectHelper.d.ts +8 -6
  84. package/lib/test/reconnectHelper.d.ts.map +1 -1
  85. package/lib/test/reconnectHelper.js +15 -14
  86. package/lib/test/reconnectHelper.js.map +1 -1
  87. package/lib/test/testClientLogger.d.ts.map +1 -1
  88. package/lib/test/testClientLogger.js +19 -8
  89. package/lib/test/testClientLogger.js.map +1 -1
  90. package/package.json +29 -16
  91. package/src/attributionPolicy.ts +5 -0
  92. package/src/client.ts +136 -21
  93. package/src/index.ts +1 -0
  94. package/src/mergeTree.ts +108 -22
  95. package/src/mergeTreeNodes.ts +9 -1
  96. package/src/opBuilder.ts +32 -0
  97. package/src/ops.ts +23 -1
  98. 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,15 +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
263
  public obliterateRangeLocal(
259
- start: number,
260
- end: number,
264
+ start: number | InteriorSequencePlace,
265
+ end: number | InteriorSequencePlace,
266
+ // eslint-disable-next-line import/no-deprecated
267
+ ): IMergeTreeObliterateMsg | IMergeTreeObliterateSidedMsg {
261
268
  // eslint-disable-next-line import/no-deprecated
262
- ): IMergeTreeObliterateMsg {
263
- const obliterateOp = createObliterateRangeOp(start, end);
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
+ }
264
279
  this.applyObliterateRangeOp({ op: obliterateOp });
265
280
  return obliterateOp;
266
281
  }
@@ -472,22 +487,39 @@ export class Client extends TypedEventEmitter<IClientEvents> {
472
487
 
473
488
  private applyObliterateRangeOp(opArgs: IMergeTreeDeltaOpArgs): void {
474
489
  assert(
475
- opArgs.op.type === MergeTreeDeltaType.OBLITERATE,
490
+ opArgs.op.type === MergeTreeDeltaType.OBLITERATE ||
491
+ opArgs.op.type === MergeTreeDeltaType.OBLITERATE_SIDED,
476
492
  0x866 /* Unexpected op type on range obliterate! */,
477
493
  );
478
494
  const op = opArgs.op;
479
495
  const clientArgs = this.getClientSequenceArgs(opArgs);
480
- const range = this.getValidOpRange(op, clientArgs);
481
-
482
- this._mergeTree.obliterateRange(
483
- range.start,
484
- range.end,
485
- clientArgs.referenceSequenceNumber,
486
- clientArgs.clientId,
487
- clientArgs.sequenceNumber,
488
- false,
489
- opArgs,
490
- );
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
+ }
491
523
  }
492
524
 
493
525
  /**
@@ -565,6 +597,82 @@ export class Client extends TypedEventEmitter<IClientEvents> {
565
597
  );
566
598
  }
567
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
+
568
676
  /**
569
677
  * Returns a valid range for the op, or undefined
570
678
  * @param op - The op to generate the range for
@@ -614,7 +722,6 @@ export class Client extends TypedEventEmitter<IClientEvents> {
614
722
  invalidPositions.push("start");
615
723
  }
616
724
  // Validate end if not insert, or insert has end
617
- //
618
725
  if (
619
726
  (op.type !== MergeTreeDeltaType.INSERT || end !== undefined) &&
620
727
  (end === undefined || end <= start!)
@@ -892,7 +999,12 @@ export class Client extends TypedEventEmitter<IClientEvents> {
892
999
 
893
1000
  const first = opList[0];
894
1001
 
895
- 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
+ ) {
896
1008
  first.pos2 += newOp.pos2! - newOp.pos1!;
897
1009
  } else {
898
1010
  opList.push(newOp);
@@ -939,7 +1051,8 @@ export class Client extends TypedEventEmitter<IClientEvents> {
939
1051
  this.applyAnnotateRangeOp(opArgs);
940
1052
  break;
941
1053
  }
942
- case MergeTreeDeltaType.OBLITERATE: {
1054
+ case MergeTreeDeltaType.OBLITERATE:
1055
+ case MergeTreeDeltaType.OBLITERATE_SIDED: {
943
1056
  this.applyObliterateRangeOp(opArgs);
944
1057
  break;
945
1058
  }
@@ -973,6 +1086,7 @@ export class Client extends TypedEventEmitter<IClientEvents> {
973
1086
  this.applyAnnotateRangeOp({ op });
974
1087
  break;
975
1088
  }
1089
+ case MergeTreeDeltaType.OBLITERATE_SIDED:
976
1090
  case MergeTreeDeltaType.OBLITERATE: {
977
1091
  this.applyObliterateRangeOp({ op });
978
1092
  break;
@@ -1205,6 +1319,7 @@ export class Client extends TypedEventEmitter<IClientEvents> {
1205
1319
  this.applyRemoveRangeOp(opArgs);
1206
1320
  break;
1207
1321
  }
1322
+ case MergeTreeDeltaType.OBLITERATE_SIDED:
1208
1323
  case MergeTreeDeltaType.OBLITERATE: {
1209
1324
  this.applyObliterateRangeOp(opArgs);
1210
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,6 +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 { Side, type InteriorSequencePlace } from "./sequencePlace.js";
93
94
  import { SortedSegmentSet } from "./sortedSegmentSet.js";
94
95
  import { zamboniSegments } from "./zamboni.js";
95
96
 
@@ -197,6 +198,17 @@ export interface IMergeTreeOptions {
197
198
  * @defaultValue `false`
198
199
  */
199
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;
200
212
  }
201
213
  export function errorIfOptionNotTrue(
202
214
  options: IMergeTreeOptions | undefined,
@@ -210,6 +222,7 @@ export function errorIfOptionNotTrue(
210
222
  /**
211
223
  * @legacy
212
224
  * @alpha
225
+ * @deprecated This functionality was not meant to be exported and will be removed in a future release
213
226
  */
214
227
  export interface IMergeTreeAttributionOptions {
215
228
  /**
@@ -237,6 +250,7 @@ export interface IMergeTreeAttributionOptions {
237
250
  * @sealed
238
251
  * @legacy
239
252
  * @alpha
253
+ * @deprecated This functionality was not meant to be exported and will be removed in a future release
240
254
  */
241
255
  export interface AttributionPolicy {
242
256
  /**
@@ -1234,7 +1248,10 @@ export class MergeTree {
1234
1248
  });
1235
1249
  });
1236
1250
 
1237
- if (opArgs.op.type === MergeTreeDeltaType.OBLITERATE) {
1251
+ if (
1252
+ opArgs.op.type === MergeTreeDeltaType.OBLITERATE ||
1253
+ opArgs.op.type === MergeTreeDeltaType.OBLITERATE_SIDED
1254
+ ) {
1238
1255
  this.obliterates.addOrUpdate(pendingSegmentGroup.obliterateInfo!);
1239
1256
  }
1240
1257
 
@@ -1242,7 +1259,8 @@ export class MergeTree {
1242
1259
  // positions after slide are final
1243
1260
  if (
1244
1261
  opArgs.op.type === MergeTreeDeltaType.REMOVE ||
1245
- opArgs.op.type === MergeTreeDeltaType.OBLITERATE
1262
+ opArgs.op.type === MergeTreeDeltaType.OBLITERATE ||
1263
+ opArgs.op.type === MergeTreeDeltaType.OBLITERATE_SIDED
1246
1264
  ) {
1247
1265
  this.slideAckedRemovedSegmentReferences(pendingSegmentGroup.segments);
1248
1266
  }
@@ -1510,11 +1528,11 @@ export class MergeTree {
1510
1528
  }
1511
1529
 
1512
1530
  this.updateRoot(splitNode);
1513
- saveIfLocal(newSegment);
1514
1531
 
1515
1532
  insertPos += newSegment.cachedLength;
1516
1533
 
1517
1534
  if (!this.options?.mergeTreeEnableObliterate || this.obliterates.empty()) {
1535
+ saveIfLocal(newSegment);
1518
1536
  continue;
1519
1537
  }
1520
1538
 
@@ -1542,13 +1560,13 @@ export class MergeTree {
1542
1560
  movedClientIds.unshift(ob.clientId);
1543
1561
  movedSeqs.unshift(ob.seq);
1544
1562
  } else {
1545
- if (newest === undefined || normalizedNewestSeq < normalizedObSeq) {
1546
- normalizedNewestSeq = normalizedObSeq;
1547
- newest = ob;
1548
- }
1549
1563
  movedClientIds.push(ob.clientId);
1550
1564
  movedSeqs.push(ob.seq);
1551
1565
  }
1566
+ if (newest === undefined || normalizedNewestSeq < normalizedObSeq) {
1567
+ normalizedNewestSeq = normalizedObSeq;
1568
+ newest = ob;
1569
+ }
1552
1570
  }
1553
1571
  }
1554
1572
 
@@ -1575,7 +1593,11 @@ export class MergeTree {
1575
1593
  if (newSegment.parent) {
1576
1594
  this.blockUpdatePathLengths(newSegment.parent, seq, clientId);
1577
1595
  }
1596
+ } else if (oldest && newest?.clientId === clientId) {
1597
+ newSegment.prevObliterateByInserter = newest;
1578
1598
  }
1599
+
1600
+ saveIfLocal(newSegment);
1579
1601
  }
1580
1602
  }
1581
1603
  }
@@ -1866,19 +1888,20 @@ export class MergeTree {
1866
1888
  }
1867
1889
  }
1868
1890
 
1869
- public obliterateRange(
1870
- start: number,
1871
- end: number,
1891
+ private obliterateRangeSided(
1892
+ start: InteriorSequencePlace,
1893
+ end: InteriorSequencePlace,
1872
1894
  refSeq: number,
1873
1895
  clientId: number,
1874
1896
  seq: number,
1875
1897
  overwrite: boolean = false,
1876
1898
  opArgs: IMergeTreeDeltaOpArgs,
1877
1899
  ): void {
1878
- errorIfOptionNotTrue(this.options, "mergeTreeEnableObliterate");
1900
+ const startPos = start.side === Side.Before ? start.pos : start.pos + 1;
1901
+ const endPos = end.side === Side.Before ? end.pos : end.pos + 1;
1879
1902
 
1880
- this.ensureIntervalBoundary(start, refSeq, clientId);
1881
- this.ensureIntervalBoundary(end, refSeq, clientId);
1903
+ this.ensureIntervalBoundary(startPos, refSeq, clientId);
1904
+ this.ensureIntervalBoundary(endPos, refSeq, clientId);
1882
1905
 
1883
1906
  let _overwrite = overwrite;
1884
1907
  const localOverlapWithRefs: ISegment[] = [];
@@ -1896,8 +1919,8 @@ export class MergeTree {
1896
1919
  segmentGroup: undefined,
1897
1920
  };
1898
1921
 
1899
- const { segment: startSeg } = this.getContainingSegment(start, refSeq, clientId);
1900
- const { segment: endSeg } = this.getContainingSegment(end - 1, refSeq, clientId);
1922
+ const { segment: startSeg } = this.getContainingSegment(start.pos, refSeq, clientId);
1923
+ const { segment: endSeg } = this.getContainingSegment(end.pos, refSeq, clientId);
1901
1924
  assert(
1902
1925
  startSeg !== undefined && endSeg !== undefined,
1903
1926
  0xa3f /* segments cannot be undefined */,
@@ -1905,7 +1928,7 @@ export class MergeTree {
1905
1928
 
1906
1929
  obliterate.start = this.createLocalReferencePosition(
1907
1930
  startSeg,
1908
- 0,
1931
+ start.side === Side.Before ? 0 : Math.max(startSeg.cachedLength - 1, 0),
1909
1932
  ReferenceType.StayOnRemove,
1910
1933
  {
1911
1934
  obliterate,
@@ -1914,20 +1937,53 @@ export class MergeTree {
1914
1937
 
1915
1938
  obliterate.end = this.createLocalReferencePosition(
1916
1939
  endSeg,
1917
- endSeg.cachedLength - 1,
1940
+ end.side === Side.Before ? 0 : Math.max(endSeg.cachedLength - 1, 0),
1918
1941
  ReferenceType.StayOnRemove,
1919
1942
  {
1920
1943
  obliterate,
1921
1944
  },
1922
1945
  );
1923
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
+
1924
1962
  const markMoved = (
1925
1963
  segment: ISegment,
1926
1964
  pos: number,
1927
1965
  _start: number,
1928
1966
  _end: number,
1929
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
+ }
1930
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
+
1931
1987
  if (
1932
1988
  clientId !== segment.clientId &&
1933
1989
  segment.seq !== undefined &&
@@ -1978,7 +2034,6 @@ export class MergeTree {
1978
2034
  obliterate.segmentGroup,
1979
2035
  localSeq,
1980
2036
  );
1981
- obliterate.segmentGroup.obliterateInfo ??= obliterate;
1982
2037
  } else {
1983
2038
  if (MergeTree.options.zamboniSegments) {
1984
2039
  this.addToLRUSet(segment, seq);
@@ -2008,14 +2063,12 @@ export class MergeTree {
2008
2063
  markMoved,
2009
2064
  undefined,
2010
2065
  afterMarkMoved,
2011
- start,
2012
- end,
2066
+ start.pos,
2067
+ end.pos + 1, // include the segment containing the end reference
2013
2068
  undefined,
2014
2069
  seq === UnassignedSequenceNumber ? undefined : seq,
2015
2070
  );
2016
2071
 
2017
- this.obliterates.addOrUpdate(obliterate);
2018
-
2019
2072
  this.slideAckedRemovedSegmentReferences(localOverlapWithRefs);
2020
2073
  // opArgs == undefined => test code
2021
2074
  if (movedSegments.length > 0) {
@@ -2041,6 +2094,39 @@ export class MergeTree {
2041
2094
  }
2042
2095
  }
2043
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
+
2044
2130
  public markRangeRemoved(
2045
2131
  start: number,
2046
2132
  end: number,
@@ -160,6 +160,13 @@ export interface IMoveInfo {
160
160
  * calculations
161
161
  */
162
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;
163
170
  }
164
171
 
165
172
  export function toMoveInfo(maybe: Partial<IMoveInfo> | undefined): IMoveInfo | undefined {
@@ -661,7 +668,8 @@ export abstract class BaseSegment implements ISegment {
661
668
  return false;
662
669
  }
663
670
 
664
- case MergeTreeDeltaType.OBLITERATE: {
671
+ case MergeTreeDeltaType.OBLITERATE:
672
+ case MergeTreeDeltaType.OBLITERATE_SIDED: {
665
673
  const moveInfo: IMoveInfo | undefined = toMoveInfo(this);
666
674
  assert(moveInfo !== undefined, 0x86e /* On obliterate ack, missing move info! */);
667
675
  const obliterateInfo = segmentGroup.obliterateInfo;
package/src/opBuilder.ts CHANGED
@@ -14,8 +14,10 @@ import {
14
14
  IMergeTreeObliterateMsg,
15
15
  IMergeTreeRemoveMsg,
16
16
  MergeTreeDeltaType,
17
+ type IMergeTreeObliterateSidedMsg,
17
18
  } from "./ops.js";
18
19
  import { PropertySet } from "./properties.js";
20
+ import { normalizePlace, Side, type SequencePlace } from "./sequencePlace.js";
19
21
 
20
22
  /**
21
23
  * Creates the op for annotating the markers with the provided properties
@@ -97,6 +99,36 @@ export function createObliterateRangeOp(start: number, end: number): IMergeTreeO
97
99
  };
98
100
  }
99
101
 
102
+ /**
103
+ * Creates the op to obliterate a range
104
+ *
105
+ * @param start - The start of the range to obliterate.
106
+ * If a number is provided, the range will start before that index.
107
+ * @param end - The end of the range to obliterate.
108
+ * If a number is provided, the range will end after that index -1.
109
+ * This preserves the previous behavior of not expanding obliteration ranges at the endpoints
110
+ * for uses which predate the availability of endpoint expansion.
111
+ *
112
+ * @internal
113
+ */
114
+ export function createObliterateRangeOpSided(
115
+ start: SequencePlace,
116
+ end: SequencePlace,
117
+ ): IMergeTreeObliterateSidedMsg {
118
+ const startPlace = normalizePlace(start);
119
+ // If a number is provided, default to after the previous index.
120
+ // This preserves the behavior of obliterate prior to the introduction of endpoint expansion.
121
+ const endPlace =
122
+ typeof end === "number"
123
+ ? { pos: end - 1, side: Side.After } // default to inclusive bounds
124
+ : normalizePlace(end);
125
+ return {
126
+ type: MergeTreeDeltaType.OBLITERATE_SIDED,
127
+ pos1: { pos: startPlace.pos, before: startPlace.side === Side.Before },
128
+ pos2: { pos: endPlace.pos, before: endPlace.side === Side.Before },
129
+ };
130
+ }
131
+
100
132
  /**
101
133
  * Creates an op for inserting a segment at the specified position.
102
134
  *
package/src/ops.ts CHANGED
@@ -70,6 +70,7 @@ export const MergeTreeDeltaType = {
70
70
  */
71
71
  GROUP: 3,
72
72
  OBLITERATE: 4,
73
+ OBLITERATE_SIDED: 5,
73
74
  } as const;
74
75
 
75
76
  /**
@@ -162,6 +163,26 @@ export interface IMergeTreeObliterateMsg extends IMergeTreeDelta {
162
163
  relativePos2?: never;
163
164
  }
164
165
 
166
+ /**
167
+ * @legacy
168
+ * @alpha
169
+ */
170
+ export interface IMergeTreeObliterateSidedMsg extends IMergeTreeDelta {
171
+ type: typeof MergeTreeDeltaType.OBLITERATE_SIDED;
172
+ pos1: { pos: number; before: boolean };
173
+ /**
174
+ * This field is currently unused, but we keep it around to make the union
175
+ * type of all merge-tree messages have the same fields
176
+ */
177
+ relativePos1?: never;
178
+ pos2: { pos: number; before: boolean };
179
+ /**
180
+ * This field is currently unused, but we keep it around to make the union
181
+ * type of all merge-tree messages have the same fields
182
+ */
183
+ relativePos2?: never;
184
+ }
185
+
165
186
  /**
166
187
  * @legacy
167
188
  * @alpha
@@ -206,7 +227,8 @@ export type IMergeTreeDeltaOp =
206
227
  | IMergeTreeInsertMsg
207
228
  | IMergeTreeRemoveMsg
208
229
  | IMergeTreeAnnotateMsg
209
- | IMergeTreeObliterateMsg;
230
+ | IMergeTreeObliterateMsg
231
+ | IMergeTreeObliterateSidedMsg;
210
232
 
211
233
  /**
212
234
  * @legacy
@@ -87,3 +87,19 @@ export function endpointPosAndSide(
87
87
  endPos,
88
88
  };
89
89
  }
90
+
91
+ /**
92
+ * Returns the given place in InteriorSequencePlace form.
93
+ */
94
+ export function normalizePlace(place: SequencePlace): InteriorSequencePlace {
95
+ if (typeof place === "number") {
96
+ return { pos: place, side: Side.Before };
97
+ }
98
+ if (place === "start") {
99
+ return { pos: -1, side: Side.After };
100
+ }
101
+ if (place === "end") {
102
+ return { pos: -1, side: Side.Before };
103
+ }
104
+ return place;
105
+ }