@fluidframework/sequence 2.0.0-internal.6.4.0 → 2.0.0-internal.7.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (137) hide show
  1. package/CHANGELOG.md +60 -0
  2. package/README.md +130 -0
  3. package/dist/defaultMap.d.ts +1 -1
  4. package/dist/defaultMap.d.ts.map +1 -1
  5. package/dist/defaultMap.js +6 -6
  6. package/dist/defaultMap.js.map +1 -1
  7. package/dist/defaultMapInterfaces.d.ts +21 -2
  8. package/dist/defaultMapInterfaces.d.ts.map +1 -1
  9. package/dist/defaultMapInterfaces.js.map +1 -1
  10. package/dist/index.d.ts +1 -1
  11. package/dist/index.d.ts.map +1 -1
  12. package/dist/index.js +2 -1
  13. package/dist/index.js.map +1 -1
  14. package/dist/intervalCollection.d.ts +136 -18
  15. package/dist/intervalCollection.d.ts.map +1 -1
  16. package/dist/intervalCollection.js +120 -37
  17. package/dist/intervalCollection.js.map +1 -1
  18. package/dist/intervalIndex/endpointInRangeIndex.js +1 -1
  19. package/dist/intervalIndex/endpointInRangeIndex.js.map +1 -1
  20. package/dist/intervalIndex/endpointIndex.d.ts.map +1 -1
  21. package/dist/intervalIndex/endpointIndex.js +1 -2
  22. package/dist/intervalIndex/endpointIndex.js.map +1 -1
  23. package/dist/intervalIndex/overlappingIntervalsIndex.d.ts +5 -4
  24. package/dist/intervalIndex/overlappingIntervalsIndex.d.ts.map +1 -1
  25. package/dist/intervalIndex/overlappingIntervalsIndex.js +7 -2
  26. package/dist/intervalIndex/overlappingIntervalsIndex.js.map +1 -1
  27. package/dist/intervalIndex/startpointInRangeIndex.d.ts.map +1 -1
  28. package/dist/intervalIndex/startpointInRangeIndex.js +1 -3
  29. package/dist/intervalIndex/startpointInRangeIndex.js.map +1 -1
  30. package/dist/intervalTree.d.ts +1 -1
  31. package/dist/intervalTree.d.ts.map +1 -1
  32. package/dist/intervals/interval.d.ts +3 -2
  33. package/dist/intervals/interval.d.ts.map +1 -1
  34. package/dist/intervals/interval.js +12 -5
  35. package/dist/intervals/interval.js.map +1 -1
  36. package/dist/intervals/intervalUtils.d.ts +39 -18
  37. package/dist/intervals/intervalUtils.d.ts.map +1 -1
  38. package/dist/intervals/intervalUtils.js +12 -10
  39. package/dist/intervals/intervalUtils.js.map +1 -1
  40. package/dist/intervals/sequenceInterval.d.ts +23 -13
  41. package/dist/intervals/sequenceInterval.d.ts.map +1 -1
  42. package/dist/intervals/sequenceInterval.js +117 -42
  43. package/dist/intervals/sequenceInterval.js.map +1 -1
  44. package/dist/packageVersion.d.ts +1 -1
  45. package/dist/packageVersion.js +1 -1
  46. package/dist/packageVersion.js.map +1 -1
  47. package/dist/revertibles.d.ts +3 -15
  48. package/dist/revertibles.d.ts.map +1 -1
  49. package/dist/revertibles.js +6 -17
  50. package/dist/revertibles.js.map +1 -1
  51. package/dist/sequence.d.ts +1 -1
  52. package/dist/sequence.d.ts.map +1 -1
  53. package/dist/sequence.js +43 -43
  54. package/dist/sequence.js.map +1 -1
  55. package/dist/sharedIntervalCollection.js +9 -9
  56. package/dist/sharedIntervalCollection.js.map +1 -1
  57. package/dist/sharedSequence.js +6 -6
  58. package/dist/sharedSequence.js.map +1 -1
  59. package/dist/sharedString.d.ts +1 -1
  60. package/dist/sharedString.d.ts.map +1 -1
  61. package/dist/sharedString.js +5 -5
  62. package/dist/sharedString.js.map +1 -1
  63. package/dist/tsdoc-metadata.json +1 -1
  64. package/lib/defaultMap.d.ts +1 -1
  65. package/lib/defaultMap.d.ts.map +1 -1
  66. package/lib/defaultMap.js +6 -6
  67. package/lib/defaultMap.js.map +1 -1
  68. package/lib/defaultMapInterfaces.d.ts +21 -2
  69. package/lib/defaultMapInterfaces.d.ts.map +1 -1
  70. package/lib/defaultMapInterfaces.js.map +1 -1
  71. package/lib/index.d.ts +1 -1
  72. package/lib/index.d.ts.map +1 -1
  73. package/lib/index.js +1 -1
  74. package/lib/index.js.map +1 -1
  75. package/lib/intervalCollection.d.ts +136 -18
  76. package/lib/intervalCollection.d.ts.map +1 -1
  77. package/lib/intervalCollection.js +117 -37
  78. package/lib/intervalCollection.js.map +1 -1
  79. package/lib/intervalIndex/endpointInRangeIndex.js +1 -1
  80. package/lib/intervalIndex/endpointInRangeIndex.js.map +1 -1
  81. package/lib/intervalIndex/endpointIndex.d.ts.map +1 -1
  82. package/lib/intervalIndex/endpointIndex.js +1 -2
  83. package/lib/intervalIndex/endpointIndex.js.map +1 -1
  84. package/lib/intervalIndex/overlappingIntervalsIndex.d.ts +5 -4
  85. package/lib/intervalIndex/overlappingIntervalsIndex.d.ts.map +1 -1
  86. package/lib/intervalIndex/overlappingIntervalsIndex.js +7 -2
  87. package/lib/intervalIndex/overlappingIntervalsIndex.js.map +1 -1
  88. package/lib/intervalIndex/startpointInRangeIndex.d.ts.map +1 -1
  89. package/lib/intervalIndex/startpointInRangeIndex.js +1 -3
  90. package/lib/intervalIndex/startpointInRangeIndex.js.map +1 -1
  91. package/lib/intervalTree.d.ts +1 -1
  92. package/lib/intervalTree.d.ts.map +1 -1
  93. package/lib/intervals/interval.d.ts +3 -2
  94. package/lib/intervals/interval.d.ts.map +1 -1
  95. package/lib/intervals/interval.js +12 -5
  96. package/lib/intervals/interval.js.map +1 -1
  97. package/lib/intervals/intervalUtils.d.ts +39 -18
  98. package/lib/intervals/intervalUtils.d.ts.map +1 -1
  99. package/lib/intervals/intervalUtils.js +8 -6
  100. package/lib/intervals/intervalUtils.js.map +1 -1
  101. package/lib/intervals/sequenceInterval.d.ts +23 -13
  102. package/lib/intervals/sequenceInterval.d.ts.map +1 -1
  103. package/lib/intervals/sequenceInterval.js +118 -41
  104. package/lib/intervals/sequenceInterval.js.map +1 -1
  105. package/lib/packageVersion.d.ts +1 -1
  106. package/lib/packageVersion.js +1 -1
  107. package/lib/packageVersion.js.map +1 -1
  108. package/lib/revertibles.d.ts +3 -15
  109. package/lib/revertibles.d.ts.map +1 -1
  110. package/lib/revertibles.js +6 -17
  111. package/lib/revertibles.js.map +1 -1
  112. package/lib/sequence.d.ts +1 -1
  113. package/lib/sequence.d.ts.map +1 -1
  114. package/lib/sequence.js +43 -43
  115. package/lib/sequence.js.map +1 -1
  116. package/lib/sharedIntervalCollection.js +9 -9
  117. package/lib/sharedIntervalCollection.js.map +1 -1
  118. package/lib/sharedSequence.js +6 -6
  119. package/lib/sharedSequence.js.map +1 -1
  120. package/lib/sharedString.d.ts +1 -1
  121. package/lib/sharedString.d.ts.map +1 -1
  122. package/lib/sharedString.js +5 -5
  123. package/lib/sharedString.js.map +1 -1
  124. package/package.json +48 -20
  125. package/src/defaultMapInterfaces.ts +21 -2
  126. package/src/index.ts +3 -0
  127. package/src/intervalCollection.ts +309 -66
  128. package/src/intervalIndex/endpointInRangeIndex.ts +1 -1
  129. package/src/intervalIndex/endpointIndex.ts +1 -2
  130. package/src/intervalIndex/overlappingIntervalsIndex.ts +17 -9
  131. package/src/intervalIndex/startpointInRangeIndex.ts +1 -7
  132. package/src/intervals/interval.ts +28 -7
  133. package/src/intervals/intervalUtils.ts +47 -26
  134. package/src/intervals/sequenceInterval.ts +190 -46
  135. package/src/packageVersion.ts +1 -1
  136. package/src/revertibles.ts +8 -33
  137. package/src/sequence.ts +2 -0
@@ -24,6 +24,7 @@ import {
24
24
  UnassignedSequenceNumber,
25
25
  DetachedReferencePosition,
26
26
  UniversalSequenceNumber,
27
+ SlidingPreference,
27
28
  } from "@fluidframework/merge-tree";
28
29
  import { ISequencedDocumentMessage } from "@fluidframework/protocol-definitions";
29
30
  import { LoggingError, UsageError } from "@fluidframework/telemetry-utils";
@@ -64,6 +65,52 @@ import {
64
65
  createIdIntervalIndex,
65
66
  } from "./intervalIndex";
66
67
 
68
+ /**
69
+ * Defines a position and side relative to a character in a sequence.
70
+ *
71
+ * For this purpose, sequences look like:
72
+ *
73
+ * `{start} - {character 0} - {character 1} - ... - {character N} - {end}`
74
+ *
75
+ * Each `{value}` in the diagram is a character within a sequence.
76
+ * Each `-` in the above diagram is a position where text could be inserted.
77
+ * Each position between a `{value}` and a `-` is a `SequencePlace`.
78
+ *
79
+ * The special endpoints `{start}` and `{end}` refer to positions outside the
80
+ * contents of the string.
81
+ *
82
+ * This gives us 2N + 2 possible positions to refer to within a string, where N
83
+ * is the number of characters.
84
+ *
85
+ * If the position is specified with a bare number, the side defaults to
86
+ * `Side.Before`.
87
+ *
88
+ * If a SequencePlace is the endpoint of a range (e.g. start/end of an interval or search range),
89
+ * the Side value means it is exclusive if it is nearer to the other position and inclusive if it is farther.
90
+ * E.g. the start of a range with Side.After is exclusive of the character at the position.
91
+ */
92
+ export type SequencePlace = number | "start" | "end" | InteriorSequencePlace;
93
+
94
+ /**
95
+ * A sequence place that does not refer to the special endpoint segments.
96
+ *
97
+ * See {@link SequencePlace} for additional context.
98
+ */
99
+ export interface InteriorSequencePlace {
100
+ pos: number;
101
+ side: Side;
102
+ }
103
+
104
+ /**
105
+ * Defines a side relative to a character in a sequence.
106
+ *
107
+ * @remarks See {@link SequencePlace} for additional context on usage.
108
+ */
109
+ export enum Side {
110
+ Before = 0,
111
+ After = 1,
112
+ }
113
+
67
114
  const reservedIntervalIdKey = "intervalId";
68
115
 
69
116
  export interface ISerializedIntervalCollectionV2 {
@@ -72,6 +119,13 @@ export interface ISerializedIntervalCollectionV2 {
72
119
  intervals: CompressedSerializedInterval[];
73
120
  }
74
121
 
122
+ export function sidesFromStickiness(stickiness: IntervalStickiness) {
123
+ const startSide = (stickiness & IntervalStickiness.START) !== 0 ? Side.After : Side.Before;
124
+ const endSide = (stickiness & IntervalStickiness.END) !== 0 ? Side.Before : Side.After;
125
+
126
+ return { startSide, endSide };
127
+ }
128
+
75
129
  /**
76
130
  * Decompress an interval after loading a summary from JSON. The exact format
77
131
  * of this compression is unspecified and subject to change
@@ -80,13 +134,17 @@ function decompressInterval(
80
134
  interval: CompressedSerializedInterval,
81
135
  label?: string,
82
136
  ): ISerializedInterval {
137
+ const stickiness = interval[5] ?? IntervalStickiness.END;
138
+ const { startSide, endSide } = sidesFromStickiness(stickiness);
83
139
  return {
84
140
  start: interval[0],
85
141
  end: interval[1],
86
142
  sequenceNumber: interval[2],
87
143
  intervalType: interval[3],
88
144
  properties: { ...interval[4], [reservedRangeLabelsKey]: [label] },
89
- stickiness: interval[5],
145
+ stickiness,
146
+ startSide,
147
+ endSide,
90
148
  };
91
149
  }
92
150
 
@@ -97,7 +155,7 @@ function decompressInterval(
97
155
  function compressInterval(interval: ISerializedInterval): CompressedSerializedInterval {
98
156
  const { start, end, sequenceNumber, intervalType, properties } = interval;
99
157
 
100
- const base: CompressedSerializedInterval = [
158
+ let base: CompressedSerializedInterval = [
101
159
  start,
102
160
  end,
103
161
  sequenceNumber,
@@ -108,18 +166,69 @@ function compressInterval(interval: ISerializedInterval): CompressedSerializedIn
108
166
  ];
109
167
 
110
168
  if (interval.stickiness !== undefined && interval.stickiness !== IntervalStickiness.END) {
111
- base.push(interval.stickiness);
169
+ // reassignment to make it easier for typescript to reason about types
170
+ base = [...base, interval.stickiness];
112
171
  }
113
172
 
114
173
  return base;
115
174
  }
116
175
 
176
+ export function endpointPosAndSide(
177
+ start: SequencePlace | undefined,
178
+ end: SequencePlace | undefined,
179
+ ) {
180
+ const startIsPlainEndpoint = typeof start === "number" || start === "start" || start === "end";
181
+ const endIsPlainEndpoint = typeof end === "number" || end === "start" || end === "end";
182
+
183
+ const startSide = startIsPlainEndpoint ? Side.Before : start?.side;
184
+ const endSide = endIsPlainEndpoint ? Side.Before : end?.side;
185
+
186
+ const startPos = startIsPlainEndpoint ? start : start?.pos;
187
+ const endPos = endIsPlainEndpoint ? end : end?.pos;
188
+
189
+ return {
190
+ startSide,
191
+ endSide,
192
+ startPos,
193
+ endPos,
194
+ };
195
+ }
196
+
197
+ function toSequencePlace(pos: number | "start" | "end", side: Side): SequencePlace {
198
+ return typeof pos === "number" ? { pos, side } : pos;
199
+ }
200
+
201
+ function toOptionalSequencePlace(
202
+ pos: number | "start" | "end" | undefined,
203
+ side: Side = Side.Before,
204
+ ): SequencePlace | undefined {
205
+ return typeof pos === "number" ? { pos, side } : pos;
206
+ }
207
+
208
+ export function computeStickinessFromSide(
209
+ startPos: number | "start" | "end" | undefined = -1,
210
+ startSide: Side = Side.Before,
211
+ endPos: number | "start" | "end" | undefined = -1,
212
+ endSide: Side = Side.Before,
213
+ ): IntervalStickiness {
214
+ let stickiness: IntervalStickiness = IntervalStickiness.NONE;
215
+
216
+ if (startSide === Side.After || startPos === "start") {
217
+ stickiness |= IntervalStickiness.START;
218
+ }
219
+
220
+ if (endSide === Side.Before || endPos === "end") {
221
+ stickiness |= IntervalStickiness.END;
222
+ }
223
+
224
+ return stickiness as IntervalStickiness;
225
+ }
226
+
117
227
  export function createIntervalIndex() {
118
228
  const helpers: IIntervalHelpers<Interval> = {
119
- compareEnds: (a: Interval, b: Interval) => a.end - b.end,
120
229
  create: createInterval,
121
230
  };
122
- const lc = new LocalIntervalCollection<Interval>(undefined as any as Client, "", helpers);
231
+ const lc = new LocalIntervalCollection<Interval>(undefined as any as Client, "", helpers, {});
123
232
  return lc;
124
233
  }
125
234
 
@@ -134,6 +243,7 @@ export class LocalIntervalCollection<TInterval extends ISerializableInterval> {
134
243
  private readonly client: Client,
135
244
  private readonly label: string,
136
245
  private readonly helpers: IIntervalHelpers<TInterval>,
246
+ private readonly options: Partial<SequenceOptions>,
137
247
  /** Callback invoked each time one of the endpoints of an interval slides. */
138
248
  private readonly onPositionChange?: (
139
249
  interval: TInterval,
@@ -150,7 +260,7 @@ export class LocalIntervalCollection<TInterval extends ISerializableInterval> {
150
260
  ]);
151
261
  }
152
262
 
153
- public createLegacyId(start: number, end: number): string {
263
+ public createLegacyId(start: number | "start" | "end", end: number | "start" | "end"): string {
154
264
  // Create a non-unique ID based on start and end to be used on intervals that come from legacy clients
155
265
  // without ID's.
156
266
  return `${LocalIntervalCollection.legacyIdPrefix}${start}-${end}`;
@@ -205,11 +315,10 @@ export class LocalIntervalCollection<TInterval extends ISerializableInterval> {
205
315
  }
206
316
 
207
317
  public createInterval(
208
- start: number,
209
- end: number,
318
+ start: SequencePlace,
319
+ end: SequencePlace,
210
320
  intervalType: IntervalType,
211
321
  op?: ISequencedDocumentMessage,
212
- stickiness: IntervalStickiness = IntervalStickiness.END,
213
322
  ): TInterval {
214
323
  return this.helpers.create(
215
324
  this.label,
@@ -219,19 +328,18 @@ export class LocalIntervalCollection<TInterval extends ISerializableInterval> {
219
328
  intervalType,
220
329
  op,
221
330
  undefined,
222
- stickiness,
331
+ this.options.mergeTreeReferencesCanSlideToEndpoint,
223
332
  );
224
333
  }
225
334
 
226
335
  public addInterval(
227
- start: number,
228
- end: number,
336
+ start: SequencePlace,
337
+ end: SequencePlace,
229
338
  intervalType: IntervalType,
230
339
  props?: PropertySet,
231
340
  op?: ISequencedDocumentMessage,
232
- stickiness: IntervalStickiness = IntervalStickiness.END,
233
341
  ) {
234
- const interval: TInterval = this.createInterval(start, end, intervalType, op, stickiness);
342
+ const interval: TInterval = this.createInterval(start, end, intervalType, op);
235
343
  if (interval) {
236
344
  if (!interval.properties) {
237
345
  interval.properties = createMap<any>();
@@ -278,14 +386,19 @@ export class LocalIntervalCollection<TInterval extends ISerializableInterval> {
278
386
 
279
387
  public changeInterval(
280
388
  interval: TInterval,
281
- start: number | undefined,
282
- end: number | undefined,
389
+ start: SequencePlace | undefined,
390
+ end: SequencePlace | undefined,
283
391
  op?: ISequencedDocumentMessage,
284
392
  localSeq?: number,
285
393
  ) {
286
- const newInterval = interval.modify(this.label, start, end, op, localSeq) as
287
- | TInterval
288
- | undefined;
394
+ const newInterval = interval.modify(
395
+ this.label,
396
+ start,
397
+ end,
398
+ op,
399
+ localSeq,
400
+ this.options.mergeTreeReferencesCanSlideToEndpoint,
401
+ ) as TInterval | undefined;
289
402
  if (newInterval) {
290
403
  this.removeExistingInterval(interval);
291
404
  this.add(newInterval);
@@ -319,6 +432,7 @@ export class LocalIntervalCollection<TInterval extends ISerializableInterval> {
319
432
  ReferenceType.Transient,
320
433
  ref.properties,
321
434
  ref.slidingPreference,
435
+ ref.canSlideToEndpoint,
322
436
  );
323
437
  };
324
438
  if (interval instanceof SequenceInterval) {
@@ -412,7 +526,6 @@ class IntervalCollectionFactory implements IValueFactory<IntervalCollection<Inte
412
526
  options?: Partial<SequenceOptions>,
413
527
  ): IntervalCollection<Interval> {
414
528
  const helpers: IIntervalHelpers<Interval> = {
415
- compareEnds: (a: Interval, b: Interval) => a.end - b.end,
416
529
  create: createInterval,
417
530
  };
418
531
  const collection = new IntervalCollection<Interval>(helpers, false, emitter, raw, options);
@@ -633,21 +746,82 @@ export interface IIntervalCollection<TInterval extends ISerializableInterval>
633
746
  getIntervalById(id: string): TInterval | undefined;
634
747
  /**
635
748
  * Creates a new interval and add it to the collection.
636
- * @param start - interval start position (inclusive)
637
- * @param end - interval end position (exclusive)
638
- * @param intervalType - type of the interval. All intervals are SlideOnRemove. Intervals may not be Transient.
749
+ * @param start - interval start position
750
+ * @param end - interval end position
751
+ * @param intervalType - type of the interval. All intervals are SlideOnRemove.
752
+ * Intervals may not be Transient.
639
753
  * @param props - properties of the interval
640
- * @param stickiness - {@link (IntervalStickiness:type)} to apply to the added interval.
641
754
  * @returns The created interval
642
- * @remarks See documentation on {@link SequenceInterval} for comments on interval endpoint semantics: there are subtleties
643
- * with how the current half-open behavior is represented.
755
+ * @remarks See documentation on {@link SequenceInterval} for comments on
756
+ * interval endpoint semantics: there are subtleties with how the current
757
+ * half-open behavior is represented.
758
+ *
759
+ * Note that intervals may behave unexpectedly if the entire contents
760
+ * of the string are deleted. In this case, it is possible for one endpoint
761
+ * of the interval to become detached, while the other remains on the string.
762
+ *
763
+ * By adjusting the `side` and `pos` values of the `start` and `end` parameters,
764
+ * it is possible to control whether the interval expands to include content
765
+ * inserted at its start or end.
766
+ *
767
+ * See {@link SequencePlace} for more details on the model.
768
+ *
769
+ * @example
770
+ *
771
+ * Given the string "ABCD":
772
+ *
773
+ *```typescript
774
+ * // Refers to "BC". If any content is inserted before B or after C, this
775
+ * // interval will include that content
776
+ * //
777
+ * // Picture:
778
+ * // \{start\} - A[- B - C -]D - \{end\}
779
+ * // \{start\} - A - B - C - D - \{end\}
780
+ * collection.add(\{ pos: 0, side: Side.After \}, \{ pos: 3, side: Side.Before \}, IntervalType.SlideOnRemove);
781
+ * // Equivalent to specifying the same positions and Side.Before.
782
+ * // Refers to "ABC". Content inserted after C will be included in the
783
+ * // interval, but content inserted before A will not.
784
+ * // \{start\} -[A - B - C -]D - \{end\}
785
+ * // \{start\} - A - B - C - D - \{end\}
786
+ * collection.add(0, 3, IntervalType.SlideOnRemove);
787
+ *```
788
+ *
789
+ * In the case of the first example, if text is deleted,
790
+ *
791
+ * ```typescript
792
+ * // Delete the character "B"
793
+ * string.removeRange(1, 2);
794
+ * ```
795
+ *
796
+ * The start point of the interval will slide to the position immediately
797
+ * before "C", and the same will be true.
798
+ *
799
+ * ```
800
+ * \{start\} - A[- C -]D - \{end\}
801
+ * ```
802
+ *
803
+ * In this case, text inserted immediately before "C" would be included in
804
+ * the interval.
805
+ *
806
+ * ```typescript
807
+ * string.insertText(1, "EFG");
808
+ * ```
809
+ *
810
+ * With the string now being,
811
+ *
812
+ * ```
813
+ * \{start\} - A[- E - F - G - C -]D - \{end\}
814
+ * ```
815
+ *
816
+ * @privateRemarks TODO: ADO:5205 the above comment regarding behavior in
817
+ * the case that the entire interval has been deleted should be resolved at
818
+ * the same time as this ticket
644
819
  */
645
820
  add(
646
- start: number,
647
- end: number,
821
+ start: SequencePlace,
822
+ end: SequencePlace,
648
823
  intervalType: IntervalType,
649
824
  props?: PropertySet,
650
- stickiness?: IntervalStickiness,
651
825
  ): TInterval;
652
826
  /**
653
827
  * Removes an interval from the collection.
@@ -665,11 +839,11 @@ export interface IIntervalCollection<TInterval extends ISerializableInterval>
665
839
  /**
666
840
  * Changes the endpoints of an existing interval.
667
841
  * @param id - Id of the interval to change
668
- * @param start - New start value, if defined. `undefined` signifies this endpoint should be left unchanged.
669
- * @param end - New end value, if defined. `undefined` signifies this endpoint should be left unchanged.
842
+ * @param start - New start value. This can be the existing position to keep it unchanged.
843
+ * @param end - New end value. This can be the existing position to keep it unchanged.
670
844
  * @returns the interval that was changed, if it existed in the collection.
671
845
  */
672
- change(id: string, start?: number, end?: number): TInterval | undefined;
846
+ change(id: string, start: SequencePlace, end: SequencePlace): TInterval | undefined;
673
847
 
674
848
  attachDeserializer(onDeserialize: DeserializeCallback): void;
675
849
  /**
@@ -812,13 +986,18 @@ export class IntervalCollection<TInterval extends ISerializableInterval>
812
986
  }
813
987
 
814
988
  private rebasePositionWithSegmentSlide(
815
- pos: number,
989
+ pos: number | "start" | "end",
816
990
  seqNumberFrom: number,
817
991
  localSeq: number,
818
- ): number | undefined {
992
+ ): number | "start" | "end" | undefined {
819
993
  if (!this.client) {
820
994
  throw new LoggingError("mergeTree client must exist");
821
995
  }
996
+
997
+ if (pos === "start" || pos === "end") {
998
+ return pos;
999
+ }
1000
+
822
1001
  const { clientId } = this.client.getCollabWindow();
823
1002
  const { segment, offset } = this.client.getContainingSegment(
824
1003
  pos,
@@ -832,7 +1011,12 @@ export class IntervalCollection<TInterval extends ISerializableInterval>
832
1011
  // if segment is undefined, it slid off the string
833
1012
  assert(segment !== undefined, 0x54e /* No segment found */);
834
1013
 
835
- const segoff = getSlideToSegoff({ segment, offset }) ?? segment;
1014
+ const segoff =
1015
+ getSlideToSegoff(
1016
+ { segment, offset },
1017
+ undefined,
1018
+ this.options.mergeTreeReferencesCanSlideToEndpoint,
1019
+ ) ?? segment;
836
1020
 
837
1021
  // case happens when rebasing op, but concurrently entire string has been deleted
838
1022
  if (segoff.segment === undefined || segoff.offset === undefined) {
@@ -896,12 +1080,28 @@ export class IntervalCollection<TInterval extends ISerializableInterval>
896
1080
  client,
897
1081
  label,
898
1082
  this.helpers,
1083
+ this.options,
899
1084
  (interval, previousInterval) => this.emitChange(interval, previousInterval, true, true),
900
1085
  );
901
1086
  if (this.savedSerializedIntervals) {
902
1087
  for (const serializedInterval of this.savedSerializedIntervals) {
903
1088
  this.localCollection.ensureSerializedId(serializedInterval);
904
- const { start, end, intervalType, properties, stickiness } = serializedInterval;
1089
+ const {
1090
+ start: startPos,
1091
+ end: endPos,
1092
+ intervalType,
1093
+ properties,
1094
+ startSide,
1095
+ endSide,
1096
+ } = serializedInterval;
1097
+ const start =
1098
+ typeof startPos === "number" && startSide !== undefined
1099
+ ? { pos: startPos, side: startSide }
1100
+ : startPos;
1101
+ const end =
1102
+ typeof endPos === "number" && endSide !== undefined
1103
+ ? { pos: endPos, side: endSide }
1104
+ : endPos;
905
1105
  const interval = this.helpers.create(
906
1106
  label,
907
1107
  start,
@@ -910,7 +1110,7 @@ export class IntervalCollection<TInterval extends ISerializableInterval>
910
1110
  intervalType,
911
1111
  undefined,
912
1112
  true,
913
- stickiness,
1113
+ this.options.mergeTreeReferencesCanSlideToEndpoint,
914
1114
  );
915
1115
  if (properties) {
916
1116
  interval.addProperties(properties);
@@ -967,15 +1167,25 @@ export class IntervalCollection<TInterval extends ISerializableInterval>
967
1167
  return this.localCollection.idIntervalIndex.getIntervalById(id);
968
1168
  }
969
1169
 
1170
+ private assertStickinessEnabled(start: SequencePlace, end: SequencePlace) {
1171
+ if (
1172
+ !(typeof start === "number" && typeof end === "number") &&
1173
+ !this.options.intervalStickinessEnabled
1174
+ ) {
1175
+ throw new UsageError(
1176
+ "attempted to set interval stickiness without enabling `intervalStickinessEnabled` feature flag",
1177
+ );
1178
+ }
1179
+ }
1180
+
970
1181
  /**
971
1182
  * {@inheritdoc IIntervalCollection.add}
972
1183
  */
973
1184
  public add(
974
- start: number,
975
- end: number,
1185
+ start: SequencePlace,
1186
+ end: SequencePlace,
976
1187
  intervalType: IntervalType,
977
1188
  props?: PropertySet,
978
- stickiness: IntervalStickiness = IntervalStickiness.END,
979
1189
  ): TInterval {
980
1190
  if (!this.localCollection) {
981
1191
  throw new LoggingError("attach must be called prior to adding intervals");
@@ -983,19 +1193,26 @@ export class IntervalCollection<TInterval extends ISerializableInterval>
983
1193
  if (intervalType & IntervalType.Transient) {
984
1194
  throw new LoggingError("Can not add transient intervals");
985
1195
  }
986
- if (stickiness !== IntervalStickiness.END && !this.options.intervalStickinessEnabled) {
987
- throw new UsageError(
988
- "attempted to set interval stickiness without enabling `intervalStickinessEnabled` feature flag",
989
- );
990
- }
1196
+
1197
+ const { startSide, endSide, startPos, endPos } = endpointPosAndSide(start, end);
1198
+
1199
+ assert(
1200
+ startPos !== undefined &&
1201
+ endPos !== undefined &&
1202
+ startSide !== undefined &&
1203
+ endSide !== undefined,
1204
+ 0x793 /* start and end cannot be undefined because they were not passed in as undefined */,
1205
+ );
1206
+
1207
+ const stickiness = computeStickinessFromSide(startPos, startSide, endPos, endSide);
1208
+
1209
+ this.assertStickinessEnabled(start, end);
991
1210
 
992
1211
  const interval: TInterval = this.localCollection.addInterval(
993
- start,
994
- end,
1212
+ toSequencePlace(startPos, startSide),
1213
+ toSequencePlace(endPos, endSide),
995
1214
  intervalType,
996
1215
  props,
997
- undefined,
998
- stickiness,
999
1216
  );
1000
1217
 
1001
1218
  if (interval) {
@@ -1003,13 +1220,15 @@ export class IntervalCollection<TInterval extends ISerializableInterval>
1003
1220
  setSlideOnRemove(interval.start);
1004
1221
  setSlideOnRemove(interval.end);
1005
1222
  }
1006
- const serializedInterval = {
1007
- end,
1223
+ const serializedInterval: ISerializedInterval = {
1224
+ start: startPos,
1225
+ end: endPos,
1008
1226
  intervalType,
1009
1227
  properties: interval.properties,
1010
1228
  sequenceNumber: this.client?.getCurrentSeq() ?? 0,
1011
- start,
1012
1229
  stickiness,
1230
+ startSide,
1231
+ endSide,
1013
1232
  };
1014
1233
  const localSeq = this.getNextLocalSeq();
1015
1234
  this.localSeqToSerializedInterval.set(localSeq, serializedInterval);
@@ -1110,7 +1329,7 @@ export class IntervalCollection<TInterval extends ISerializableInterval>
1110
1329
  /**
1111
1330
  * {@inheritdoc IIntervalCollection.change}
1112
1331
  */
1113
- public change(id: string, start?: number, end?: number): TInterval | undefined {
1332
+ public change(id: string, start: SequencePlace, end: SequencePlace): TInterval | undefined {
1114
1333
  if (!this.localCollection) {
1115
1334
  throw new LoggingError("Attach must be called before accessing intervals");
1116
1335
  }
@@ -1131,8 +1350,13 @@ export class IntervalCollection<TInterval extends ISerializableInterval>
1131
1350
  setSlideOnRemove(newInterval.end);
1132
1351
  }
1133
1352
  const serializedInterval: SerializedIntervalDelta = interval.serialize();
1134
- serializedInterval.start = start;
1135
- serializedInterval.end = end;
1353
+ const { startPos, startSide, endPos, endSide } = endpointPosAndSide(start, end);
1354
+ const stickiness = computeStickinessFromSide(startPos, startSide, endPos, endSide);
1355
+ serializedInterval.start = startPos;
1356
+ serializedInterval.end = endPos;
1357
+ serializedInterval.startSide = startSide;
1358
+ serializedInterval.endSide = endSide;
1359
+ serializedInterval.stickiness = stickiness;
1136
1360
  // Emit a property bag containing only the ID, as we don't intend for this op to change any properties.
1137
1361
  serializedInterval.properties = {
1138
1362
  [reservedIntervalIdKey]: interval.getIntervalId(),
@@ -1261,8 +1485,8 @@ export class IntervalCollection<TInterval extends ISerializableInterval>
1261
1485
  } else {
1262
1486
  // If there are pending changes with this ID, don't apply the remote start/end change, as the local ack
1263
1487
  // should be the winning change.
1264
- let start: number | undefined;
1265
- let end: number | undefined;
1488
+ let start: number | "start" | "end" | undefined;
1489
+ let end: number | "start" | "end" | undefined;
1266
1490
  // Track pending start/end independently of one another.
1267
1491
  if (!this.hasPendingChangeStart(id)) {
1268
1492
  start = serializedInterval.start;
@@ -1276,7 +1500,12 @@ export class IntervalCollection<TInterval extends ISerializableInterval>
1276
1500
  // If changeInterval gives us a new interval, work with that one. Otherwise keep working with
1277
1501
  // the one we originally found in the tree.
1278
1502
  newInterval =
1279
- this.localCollection.changeInterval(interval, start, end, op) ?? interval;
1503
+ this.localCollection.changeInterval(
1504
+ interval,
1505
+ toOptionalSequencePlace(start, serializedInterval.startSide),
1506
+ toOptionalSequencePlace(end, serializedInterval.endSide),
1507
+ op,
1508
+ ) ?? interval;
1280
1509
  }
1281
1510
  const deltaProps = newInterval.addProperties(newProps, true, op.sequenceNumber);
1282
1511
  if (this.onDeserialize) {
@@ -1332,7 +1561,7 @@ export class IntervalCollection<TInterval extends ISerializableInterval>
1332
1561
  throw new LoggingError("attachSequence must be called");
1333
1562
  }
1334
1563
 
1335
- const { intervalType, properties } = serializedInterval;
1564
+ const { intervalType, properties, stickiness, startSide, endSide } = serializedInterval;
1336
1565
 
1337
1566
  const { start: startRebased, end: endRebased } =
1338
1567
  this.localSeqToRebasedInterval.get(localSeq) ?? this.computeRebasedPositions(localSeq);
@@ -1346,6 +1575,9 @@ export class IntervalCollection<TInterval extends ISerializableInterval>
1346
1575
  intervalType,
1347
1576
  sequenceNumber: this.client?.getCurrentSeq() ?? 0,
1348
1577
  properties,
1578
+ stickiness,
1579
+ startSide,
1580
+ endSide,
1349
1581
  };
1350
1582
 
1351
1583
  if (
@@ -1377,8 +1609,8 @@ export class IntervalCollection<TInterval extends ISerializableInterval>
1377
1609
  // updates the local client's state to be consistent with the emitted op.
1378
1610
  this.localCollection?.changeInterval(
1379
1611
  localInterval,
1380
- startRebased,
1381
- endRebased,
1612
+ toOptionalSequencePlace(startRebased, startSide),
1613
+ toOptionalSequencePlace(endRebased, endSide),
1382
1614
  undefined,
1383
1615
  localSeq,
1384
1616
  );
@@ -1397,7 +1629,11 @@ export class IntervalCollection<TInterval extends ISerializableInterval>
1397
1629
  if (segoff.segment?.localRefs?.has(lref) !== true) {
1398
1630
  return undefined;
1399
1631
  }
1400
- const newSegoff = getSlideToSegoff(segoff);
1632
+ const newSegoff = getSlideToSegoff(
1633
+ segoff,
1634
+ undefined,
1635
+ this.options.mergeTreeReferencesCanSlideToEndpoint,
1636
+ );
1401
1637
  const value: { segment: ISegment | undefined; offset: number | undefined } | undefined =
1402
1638
  segoff.segment === newSegoff.segment && segoff.offset === newSegoff.offset
1403
1639
  ? undefined
@@ -1405,7 +1641,7 @@ export class IntervalCollection<TInterval extends ISerializableInterval>
1405
1641
  return value;
1406
1642
  }
1407
1643
 
1408
- private ackInterval(interval: TInterval, op: ISequencedDocumentMessage) {
1644
+ private ackInterval(interval: TInterval, op: ISequencedDocumentMessage): void {
1409
1645
  // Only SequenceIntervals need potential sliding
1410
1646
  if (!(interval instanceof SequenceInterval)) {
1411
1647
  return;
@@ -1459,7 +1695,11 @@ export class IntervalCollection<TInterval extends ISerializableInterval>
1459
1695
  newStart,
1460
1696
  interval.start.refType,
1461
1697
  op,
1698
+ undefined,
1699
+ undefined,
1462
1700
  startReferenceSlidingPreference(interval.stickiness),
1701
+ startReferenceSlidingPreference(interval.stickiness) ===
1702
+ SlidingPreference.BACKWARD,
1463
1703
  );
1464
1704
  if (props) {
1465
1705
  interval.start.addProperties(props);
@@ -1477,7 +1717,11 @@ export class IntervalCollection<TInterval extends ISerializableInterval>
1477
1717
  newEnd,
1478
1718
  interval.end.refType,
1479
1719
  op,
1720
+ undefined,
1721
+ undefined,
1480
1722
  endReferenceSlidingPreference(interval.stickiness),
1723
+ endReferenceSlidingPreference(interval.stickiness) ===
1724
+ SlidingPreference.FORWARD,
1481
1725
  );
1482
1726
  if (props) {
1483
1727
  interval.end.addProperties(props);
@@ -1521,12 +1765,11 @@ export class IntervalCollection<TInterval extends ISerializableInterval>
1521
1765
  this.localCollection.ensureSerializedId(serializedInterval);
1522
1766
 
1523
1767
  const interval: TInterval = this.localCollection.addInterval(
1524
- serializedInterval.start,
1525
- serializedInterval.end,
1768
+ toSequencePlace(serializedInterval.start, serializedInterval.startSide ?? Side.Before),
1769
+ toSequencePlace(serializedInterval.end, serializedInterval.endSide ?? Side.Before),
1526
1770
  serializedInterval.intervalType,
1527
1771
  serializedInterval.properties,
1528
1772
  op,
1529
- serializedInterval.stickiness,
1530
1773
  );
1531
1774
 
1532
1775
  if (interval) {
@@ -39,7 +39,7 @@ export class EndpointInRangeIndex<TInterval extends ISerializableInterval>
39
39
  private readonly helpers: IIntervalHelpers<TInterval>,
40
40
  ) {
41
41
  this.intervalTree = new RedBlackTree<TInterval, TInterval>((a: TInterval, b: TInterval) => {
42
- const compareEndsResult = helpers.compareEnds(a, b);
42
+ const compareEndsResult = a.compareEnd(b);
43
43
  if (compareEndsResult !== 0) {
44
44
  return compareEndsResult;
45
45
  }
@@ -39,8 +39,7 @@ export class EndpointIndex<TInterval extends ISerializableInterval>
39
39
  private readonly client: Client,
40
40
  private readonly helpers: IIntervalHelpers<TInterval>,
41
41
  ) {
42
- // eslint-disable-next-line @typescript-eslint/unbound-method
43
- this.endIntervalTree = new RedBlackTree<TInterval, TInterval>(helpers.compareEnds);
42
+ this.endIntervalTree = new RedBlackTree<TInterval, TInterval>((a, b) => a.compareEnd(b));
44
43
  }
45
44
 
46
45
  public previousInterval(pos: number): TInterval | undefined {