@fluidframework/sequence 2.0.0-internal.6.3.3 → 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 (191) hide show
  1. package/CHANGELOG.md +64 -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 +2 -2
  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 +140 -22
  15. package/dist/intervalCollection.d.ts.map +1 -1
  16. package/dist/intervalCollection.js +146 -49
  17. package/dist/intervalCollection.js.map +1 -1
  18. package/dist/intervalIndex/endpointInRangeIndex.d.ts +13 -3
  19. package/dist/intervalIndex/endpointInRangeIndex.d.ts.map +1 -1
  20. package/dist/intervalIndex/endpointInRangeIndex.js +9 -6
  21. package/dist/intervalIndex/endpointInRangeIndex.js.map +1 -1
  22. package/dist/intervalIndex/endpointIndex.d.ts +13 -2
  23. package/dist/intervalIndex/endpointIndex.d.ts.map +1 -1
  24. package/dist/intervalIndex/endpointIndex.js +7 -5
  25. package/dist/intervalIndex/endpointIndex.js.map +1 -1
  26. package/dist/intervalIndex/idIntervalIndex.js.map +1 -1
  27. package/dist/intervalIndex/index.d.ts +4 -4
  28. package/dist/intervalIndex/index.d.ts.map +1 -1
  29. package/dist/intervalIndex/index.js +5 -1
  30. package/dist/intervalIndex/index.js.map +1 -1
  31. package/dist/intervalIndex/intervalIndex.d.ts +2 -2
  32. package/dist/intervalIndex/intervalIndex.js.map +1 -1
  33. package/dist/intervalIndex/overlappingIntervalsIndex.d.ts +8 -6
  34. package/dist/intervalIndex/overlappingIntervalsIndex.d.ts.map +1 -1
  35. package/dist/intervalIndex/overlappingIntervalsIndex.js +11 -4
  36. package/dist/intervalIndex/overlappingIntervalsIndex.js.map +1 -1
  37. package/dist/intervalIndex/overlappingSequenceIntervalsIndex.d.ts +2 -2
  38. package/dist/intervalIndex/overlappingSequenceIntervalsIndex.d.ts.map +1 -1
  39. package/dist/intervalIndex/overlappingSequenceIntervalsIndex.js +3 -1
  40. package/dist/intervalIndex/overlappingSequenceIntervalsIndex.js.map +1 -1
  41. package/dist/intervalIndex/sequenceIntervalIndexes.d.ts +1 -1
  42. package/dist/intervalIndex/sequenceIntervalIndexes.d.ts.map +1 -1
  43. package/dist/intervalIndex/sequenceIntervalIndexes.js.map +1 -1
  44. package/dist/intervalIndex/startpointInRangeIndex.d.ts +13 -3
  45. package/dist/intervalIndex/startpointInRangeIndex.d.ts.map +1 -1
  46. package/dist/intervalIndex/startpointInRangeIndex.js +9 -8
  47. package/dist/intervalIndex/startpointInRangeIndex.js.map +1 -1
  48. package/dist/intervalTree.d.ts +1 -1
  49. package/dist/intervalTree.d.ts.map +1 -1
  50. package/dist/intervals/interval.d.ts +4 -3
  51. package/dist/intervals/interval.d.ts.map +1 -1
  52. package/dist/intervals/interval.js +14 -6
  53. package/dist/intervals/interval.js.map +1 -1
  54. package/dist/intervals/intervalUtils.d.ts +42 -20
  55. package/dist/intervals/intervalUtils.d.ts.map +1 -1
  56. package/dist/intervals/intervalUtils.js +12 -10
  57. package/dist/intervals/intervalUtils.js.map +1 -1
  58. package/dist/intervals/sequenceInterval.d.ts +30 -17
  59. package/dist/intervals/sequenceInterval.d.ts.map +1 -1
  60. package/dist/intervals/sequenceInterval.js +124 -45
  61. package/dist/intervals/sequenceInterval.js.map +1 -1
  62. package/dist/packageVersion.d.ts +1 -1
  63. package/dist/packageVersion.js +1 -1
  64. package/dist/packageVersion.js.map +1 -1
  65. package/dist/revertibles.d.ts +3 -15
  66. package/dist/revertibles.d.ts.map +1 -1
  67. package/dist/revertibles.js +6 -17
  68. package/dist/revertibles.js.map +1 -1
  69. package/dist/sequence.d.ts +3 -2
  70. package/dist/sequence.d.ts.map +1 -1
  71. package/dist/sequence.js +46 -45
  72. package/dist/sequence.js.map +1 -1
  73. package/dist/sequenceDeltaEvent.d.ts +8 -3
  74. package/dist/sequenceDeltaEvent.d.ts.map +1 -1
  75. package/dist/sequenceDeltaEvent.js.map +1 -1
  76. package/dist/sequenceFactory.js +1 -1
  77. package/dist/sequenceFactory.js.map +1 -1
  78. package/dist/sharedIntervalCollection.js +9 -9
  79. package/dist/sharedIntervalCollection.js.map +1 -1
  80. package/dist/sharedSequence.js +6 -6
  81. package/dist/sharedSequence.js.map +1 -1
  82. package/dist/sharedString.d.ts +1 -1
  83. package/dist/sharedString.d.ts.map +1 -1
  84. package/dist/sharedString.js +7 -6
  85. package/dist/sharedString.js.map +1 -1
  86. package/dist/tsdoc-metadata.json +1 -1
  87. package/lib/defaultMap.d.ts +1 -1
  88. package/lib/defaultMap.d.ts.map +1 -1
  89. package/lib/defaultMap.js +6 -6
  90. package/lib/defaultMap.js.map +1 -1
  91. package/lib/defaultMapInterfaces.d.ts +21 -2
  92. package/lib/defaultMapInterfaces.d.ts.map +1 -1
  93. package/lib/defaultMapInterfaces.js.map +1 -1
  94. package/lib/index.d.ts +2 -2
  95. package/lib/index.d.ts.map +1 -1
  96. package/lib/index.js +1 -1
  97. package/lib/index.js.map +1 -1
  98. package/lib/intervalCollection.d.ts +140 -22
  99. package/lib/intervalCollection.d.ts.map +1 -1
  100. package/lib/intervalCollection.js +144 -50
  101. package/lib/intervalCollection.js.map +1 -1
  102. package/lib/intervalIndex/endpointInRangeIndex.d.ts +13 -3
  103. package/lib/intervalIndex/endpointInRangeIndex.d.ts.map +1 -1
  104. package/lib/intervalIndex/endpointInRangeIndex.js +9 -7
  105. package/lib/intervalIndex/endpointInRangeIndex.js.map +1 -1
  106. package/lib/intervalIndex/endpointIndex.d.ts +13 -2
  107. package/lib/intervalIndex/endpointIndex.d.ts.map +1 -1
  108. package/lib/intervalIndex/endpointIndex.js +7 -6
  109. package/lib/intervalIndex/endpointIndex.js.map +1 -1
  110. package/lib/intervalIndex/idIntervalIndex.js.map +1 -1
  111. package/lib/intervalIndex/index.d.ts +4 -4
  112. package/lib/intervalIndex/index.d.ts.map +1 -1
  113. package/lib/intervalIndex/index.js +4 -4
  114. package/lib/intervalIndex/index.js.map +1 -1
  115. package/lib/intervalIndex/intervalIndex.d.ts +2 -2
  116. package/lib/intervalIndex/intervalIndex.js.map +1 -1
  117. package/lib/intervalIndex/overlappingIntervalsIndex.d.ts +8 -6
  118. package/lib/intervalIndex/overlappingIntervalsIndex.d.ts.map +1 -1
  119. package/lib/intervalIndex/overlappingIntervalsIndex.js +12 -5
  120. package/lib/intervalIndex/overlappingIntervalsIndex.js.map +1 -1
  121. package/lib/intervalIndex/overlappingSequenceIntervalsIndex.d.ts +2 -2
  122. package/lib/intervalIndex/overlappingSequenceIntervalsIndex.d.ts.map +1 -1
  123. package/lib/intervalIndex/overlappingSequenceIntervalsIndex.js +3 -1
  124. package/lib/intervalIndex/overlappingSequenceIntervalsIndex.js.map +1 -1
  125. package/lib/intervalIndex/sequenceIntervalIndexes.d.ts +1 -1
  126. package/lib/intervalIndex/sequenceIntervalIndexes.d.ts.map +1 -1
  127. package/lib/intervalIndex/sequenceIntervalIndexes.js.map +1 -1
  128. package/lib/intervalIndex/startpointInRangeIndex.d.ts +13 -3
  129. package/lib/intervalIndex/startpointInRangeIndex.d.ts.map +1 -1
  130. package/lib/intervalIndex/startpointInRangeIndex.js +9 -9
  131. package/lib/intervalIndex/startpointInRangeIndex.js.map +1 -1
  132. package/lib/intervalTree.d.ts +1 -1
  133. package/lib/intervalTree.d.ts.map +1 -1
  134. package/lib/intervals/interval.d.ts +4 -3
  135. package/lib/intervals/interval.d.ts.map +1 -1
  136. package/lib/intervals/interval.js +14 -6
  137. package/lib/intervals/interval.js.map +1 -1
  138. package/lib/intervals/intervalUtils.d.ts +42 -20
  139. package/lib/intervals/intervalUtils.d.ts.map +1 -1
  140. package/lib/intervals/intervalUtils.js +8 -6
  141. package/lib/intervals/intervalUtils.js.map +1 -1
  142. package/lib/intervals/sequenceInterval.d.ts +30 -17
  143. package/lib/intervals/sequenceInterval.d.ts.map +1 -1
  144. package/lib/intervals/sequenceInterval.js +125 -44
  145. package/lib/intervals/sequenceInterval.js.map +1 -1
  146. package/lib/packageVersion.d.ts +1 -1
  147. package/lib/packageVersion.js +1 -1
  148. package/lib/packageVersion.js.map +1 -1
  149. package/lib/revertibles.d.ts +3 -15
  150. package/lib/revertibles.d.ts.map +1 -1
  151. package/lib/revertibles.js +6 -17
  152. package/lib/revertibles.js.map +1 -1
  153. package/lib/sequence.d.ts +3 -2
  154. package/lib/sequence.d.ts.map +1 -1
  155. package/lib/sequence.js +46 -45
  156. package/lib/sequence.js.map +1 -1
  157. package/lib/sequenceDeltaEvent.d.ts +8 -3
  158. package/lib/sequenceDeltaEvent.d.ts.map +1 -1
  159. package/lib/sequenceDeltaEvent.js.map +1 -1
  160. package/lib/sequenceFactory.js +1 -1
  161. package/lib/sequenceFactory.js.map +1 -1
  162. package/lib/sharedIntervalCollection.js +9 -9
  163. package/lib/sharedIntervalCollection.js.map +1 -1
  164. package/lib/sharedSequence.js +6 -6
  165. package/lib/sharedSequence.js.map +1 -1
  166. package/lib/sharedString.d.ts +1 -1
  167. package/lib/sharedString.d.ts.map +1 -1
  168. package/lib/sharedString.js +7 -6
  169. package/lib/sharedString.js.map +1 -1
  170. package/package.json +49 -23
  171. package/src/defaultMapInterfaces.ts +21 -2
  172. package/src/index.ts +4 -1
  173. package/src/intervalCollection.ts +347 -84
  174. package/src/intervalIndex/endpointInRangeIndex.ts +19 -11
  175. package/src/intervalIndex/endpointIndex.ts +16 -9
  176. package/src/intervalIndex/idIntervalIndex.ts +1 -1
  177. package/src/intervalIndex/index.ts +12 -3
  178. package/src/intervalIndex/intervalIndex.ts +2 -2
  179. package/src/intervalIndex/overlappingIntervalsIndex.ts +31 -15
  180. package/src/intervalIndex/overlappingSequenceIntervalsIndex.ts +4 -1
  181. package/src/intervalIndex/sequenceIntervalIndexes.ts +1 -1
  182. package/src/intervalIndex/startpointInRangeIndex.ts +19 -17
  183. package/src/intervals/interval.ts +30 -8
  184. package/src/intervals/intervalUtils.ts +51 -28
  185. package/src/intervals/sequenceInterval.ts +197 -49
  186. package/src/packageVersion.ts +1 -1
  187. package/src/revertibles.ts +8 -33
  188. package/src/sequence.ts +5 -2
  189. package/src/sequenceDeltaEvent.ts +11 -3
  190. package/src/sequenceFactory.ts +1 -1
  191. package/src/sharedString.ts +2 -1
@@ -4,6 +4,7 @@
4
4
  */
5
5
 
6
6
  /* eslint-disable no-bitwise */
7
+ /* eslint-disable import/no-deprecated */
7
8
 
8
9
  import { TypedEventEmitter } from "@fluid-internal/client-utils";
9
10
  import { assert } from "@fluidframework/core-utils";
@@ -22,6 +23,8 @@ import {
22
23
  reservedRangeLabelsKey,
23
24
  UnassignedSequenceNumber,
24
25
  DetachedReferencePosition,
26
+ UniversalSequenceNumber,
27
+ SlidingPreference,
25
28
  } from "@fluidframework/merge-tree";
26
29
  import { ISequencedDocumentMessage } from "@fluidframework/protocol-definitions";
27
30
  import { LoggingError, UsageError } from "@fluidframework/telemetry-utils";
@@ -53,15 +56,61 @@ import {
53
56
  createInterval,
54
57
  } from "./intervals";
55
58
  import {
59
+ EndpointIndex,
56
60
  IEndpointIndex,
57
61
  IIdIntervalIndex,
58
62
  IOverlappingIntervalsIndex,
59
63
  IntervalIndex,
60
- createEndpointIndex,
64
+ OverlappingIntervalsIndex,
61
65
  createIdIntervalIndex,
62
- createOverlappingIntervalsIndex,
63
66
  } from "./intervalIndex";
64
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
+
65
114
  const reservedIntervalIdKey = "intervalId";
66
115
 
67
116
  export interface ISerializedIntervalCollectionV2 {
@@ -70,6 +119,13 @@ export interface ISerializedIntervalCollectionV2 {
70
119
  intervals: CompressedSerializedInterval[];
71
120
  }
72
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
+
73
129
  /**
74
130
  * Decompress an interval after loading a summary from JSON. The exact format
75
131
  * of this compression is unspecified and subject to change
@@ -78,13 +134,17 @@ function decompressInterval(
78
134
  interval: CompressedSerializedInterval,
79
135
  label?: string,
80
136
  ): ISerializedInterval {
137
+ const stickiness = interval[5] ?? IntervalStickiness.END;
138
+ const { startSide, endSide } = sidesFromStickiness(stickiness);
81
139
  return {
82
140
  start: interval[0],
83
141
  end: interval[1],
84
142
  sequenceNumber: interval[2],
85
143
  intervalType: interval[3],
86
144
  properties: { ...interval[4], [reservedRangeLabelsKey]: [label] },
87
- stickiness: interval[5],
145
+ stickiness,
146
+ startSide,
147
+ endSide,
88
148
  };
89
149
  }
90
150
 
@@ -95,7 +155,7 @@ function decompressInterval(
95
155
  function compressInterval(interval: ISerializedInterval): CompressedSerializedInterval {
96
156
  const { start, end, sequenceNumber, intervalType, properties } = interval;
97
157
 
98
- const base: CompressedSerializedInterval = [
158
+ let base: CompressedSerializedInterval = [
99
159
  start,
100
160
  end,
101
161
  sequenceNumber,
@@ -106,18 +166,69 @@ function compressInterval(interval: ISerializedInterval): CompressedSerializedIn
106
166
  ];
107
167
 
108
168
  if (interval.stickiness !== undefined && interval.stickiness !== IntervalStickiness.END) {
109
- base.push(interval.stickiness);
169
+ // reassignment to make it easier for typescript to reason about types
170
+ base = [...base, interval.stickiness];
110
171
  }
111
172
 
112
173
  return base;
113
174
  }
114
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
+
115
227
  export function createIntervalIndex() {
116
228
  const helpers: IIntervalHelpers<Interval> = {
117
- compareEnds: (a: Interval, b: Interval) => a.end - b.end,
118
229
  create: createInterval,
119
230
  };
120
- const lc = new LocalIntervalCollection<Interval>(undefined as any as Client, "", helpers);
231
+ const lc = new LocalIntervalCollection<Interval>(undefined as any as Client, "", helpers, {});
121
232
  return lc;
122
233
  }
123
234
 
@@ -132,15 +243,16 @@ export class LocalIntervalCollection<TInterval extends ISerializableInterval> {
132
243
  private readonly client: Client,
133
244
  private readonly label: string,
134
245
  private readonly helpers: IIntervalHelpers<TInterval>,
246
+ private readonly options: Partial<SequenceOptions>,
135
247
  /** Callback invoked each time one of the endpoints of an interval slides. */
136
248
  private readonly onPositionChange?: (
137
249
  interval: TInterval,
138
250
  previousInterval: TInterval,
139
251
  ) => void,
140
252
  ) {
141
- this.overlappingIntervalsIndex = createOverlappingIntervalsIndex(client, helpers);
253
+ this.overlappingIntervalsIndex = new OverlappingIntervalsIndex(client, helpers);
142
254
  this.idIntervalIndex = createIdIntervalIndex<TInterval>();
143
- this.endIntervalIndex = createEndpointIndex(client, helpers);
255
+ this.endIntervalIndex = new EndpointIndex(client, helpers);
144
256
  this.indexes = new Set([
145
257
  this.overlappingIntervalsIndex,
146
258
  this.idIntervalIndex,
@@ -148,7 +260,7 @@ export class LocalIntervalCollection<TInterval extends ISerializableInterval> {
148
260
  ]);
149
261
  }
150
262
 
151
- public createLegacyId(start: number, end: number): string {
263
+ public createLegacyId(start: number | "start" | "end", end: number | "start" | "end"): string {
152
264
  // Create a non-unique ID based on start and end to be used on intervals that come from legacy clients
153
265
  // without ID's.
154
266
  return `${LocalIntervalCollection.legacyIdPrefix}${start}-${end}`;
@@ -203,11 +315,10 @@ export class LocalIntervalCollection<TInterval extends ISerializableInterval> {
203
315
  }
204
316
 
205
317
  public createInterval(
206
- start: number,
207
- end: number,
318
+ start: SequencePlace,
319
+ end: SequencePlace,
208
320
  intervalType: IntervalType,
209
321
  op?: ISequencedDocumentMessage,
210
- stickiness: IntervalStickiness = IntervalStickiness.END,
211
322
  ): TInterval {
212
323
  return this.helpers.create(
213
324
  this.label,
@@ -217,19 +328,18 @@ export class LocalIntervalCollection<TInterval extends ISerializableInterval> {
217
328
  intervalType,
218
329
  op,
219
330
  undefined,
220
- stickiness,
331
+ this.options.mergeTreeReferencesCanSlideToEndpoint,
221
332
  );
222
333
  }
223
334
 
224
335
  public addInterval(
225
- start: number,
226
- end: number,
336
+ start: SequencePlace,
337
+ end: SequencePlace,
227
338
  intervalType: IntervalType,
228
339
  props?: PropertySet,
229
340
  op?: ISequencedDocumentMessage,
230
- stickiness: IntervalStickiness = IntervalStickiness.END,
231
341
  ) {
232
- const interval: TInterval = this.createInterval(start, end, intervalType, op, stickiness);
342
+ const interval: TInterval = this.createInterval(start, end, intervalType, op);
233
343
  if (interval) {
234
344
  if (!interval.properties) {
235
345
  interval.properties = createMap<any>();
@@ -276,14 +386,19 @@ export class LocalIntervalCollection<TInterval extends ISerializableInterval> {
276
386
 
277
387
  public changeInterval(
278
388
  interval: TInterval,
279
- start: number | undefined,
280
- end: number | undefined,
389
+ start: SequencePlace | undefined,
390
+ end: SequencePlace | undefined,
281
391
  op?: ISequencedDocumentMessage,
282
392
  localSeq?: number,
283
393
  ) {
284
- const newInterval = interval.modify(this.label, start, end, op, localSeq) as
285
- | TInterval
286
- | 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;
287
402
  if (newInterval) {
288
403
  this.removeExistingInterval(interval);
289
404
  this.add(newInterval);
@@ -317,6 +432,7 @@ export class LocalIntervalCollection<TInterval extends ISerializableInterval> {
317
432
  ReferenceType.Transient,
318
433
  ref.properties,
319
434
  ref.slidingPreference,
435
+ ref.canSlideToEndpoint,
320
436
  );
321
437
  };
322
438
  if (interval instanceof SequenceInterval) {
@@ -410,7 +526,6 @@ class IntervalCollectionFactory implements IValueFactory<IntervalCollection<Inte
410
526
  options?: Partial<SequenceOptions>,
411
527
  ): IntervalCollection<Interval> {
412
528
  const helpers: IIntervalHelpers<Interval> = {
413
- compareEnds: (a: Interval, b: Interval) => a.end - b.end,
414
529
  create: createInterval,
415
530
  };
416
531
  const collection = new IntervalCollection<Interval>(helpers, false, emitter, raw, options);
@@ -612,7 +727,7 @@ export interface IIntervalCollection<TInterval extends ISerializableInterval>
612
727
  * All intervals which are part of this collection will be added to the index, and the index will automatically
613
728
  * be updated when this collection updates due to local or remote changes.
614
729
  *
615
- * @remarks - After attaching an index to an interval collection, applications should typically store this
730
+ * @remarks After attaching an index to an interval collection, applications should typically store this
616
731
  * index somewhere in their in-memory data model for future reference and querying.
617
732
  */
618
733
  attachIndex(index: IntervalIndex<TInterval>): void;
@@ -621,7 +736,7 @@ export interface IIntervalCollection<TInterval extends ISerializableInterval>
621
736
  * All intervals which are part of this collection will be removed from the index, and updates to this collection
622
737
  * due to local or remote changes will no longer incur updates to the index.
623
738
  *
624
- * @returns - Return false if the target index cannot be found in the indexes, otherwise remove all intervals in the index and return true
739
+ * @returns `false` if the target index cannot be found in the indexes, otherwise remove all intervals in the index and return `true`.
625
740
  */
626
741
  detachIndex(index: IntervalIndex<TInterval>): boolean;
627
742
  /**
@@ -631,21 +746,82 @@ export interface IIntervalCollection<TInterval extends ISerializableInterval>
631
746
  getIntervalById(id: string): TInterval | undefined;
632
747
  /**
633
748
  * Creates a new interval and add it to the collection.
634
- * @param start - interval start position (inclusive)
635
- * @param end - interval end position (exclusive)
636
- * @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.
637
753
  * @param props - properties of the interval
638
- * @param stickiness - {@link (IntervalStickiness:type)} to apply to the added interval.
639
- * @returns - the created interval
640
- * @remarks - See documentation on {@link SequenceInterval} for comments on interval endpoint semantics: there are subtleties
641
- * with how the current half-open behavior is represented.
754
+ * @returns The created interval
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
642
819
  */
643
820
  add(
644
- start: number,
645
- end: number,
821
+ start: SequencePlace,
822
+ end: SequencePlace,
646
823
  intervalType: IntervalType,
647
824
  props?: PropertySet,
648
- stickiness?: IntervalStickiness,
649
825
  ): TInterval;
650
826
  /**
651
827
  * Removes an interval from the collection.
@@ -663,11 +839,11 @@ export interface IIntervalCollection<TInterval extends ISerializableInterval>
663
839
  /**
664
840
  * Changes the endpoints of an existing interval.
665
841
  * @param id - Id of the interval to change
666
- * @param start - New start value, if defined. `undefined` signifies this endpoint should be left unchanged.
667
- * @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.
668
844
  * @returns the interval that was changed, if it existed in the collection.
669
845
  */
670
- change(id: string, start?: number, end?: number): TInterval | undefined;
846
+ change(id: string, start: SequencePlace, end: SequencePlace): TInterval | undefined;
671
847
 
672
848
  attachDeserializer(onDeserialize: DeserializeCallback): void;
673
849
  /**
@@ -810,13 +986,18 @@ export class IntervalCollection<TInterval extends ISerializableInterval>
810
986
  }
811
987
 
812
988
  private rebasePositionWithSegmentSlide(
813
- pos: number,
989
+ pos: number | "start" | "end",
814
990
  seqNumberFrom: number,
815
991
  localSeq: number,
816
- ): number | undefined {
992
+ ): number | "start" | "end" | undefined {
817
993
  if (!this.client) {
818
994
  throw new LoggingError("mergeTree client must exist");
819
995
  }
996
+
997
+ if (pos === "start" || pos === "end") {
998
+ return pos;
999
+ }
1000
+
820
1001
  const { clientId } = this.client.getCollabWindow();
821
1002
  const { segment, offset } = this.client.getContainingSegment(
822
1003
  pos,
@@ -830,7 +1011,12 @@ export class IntervalCollection<TInterval extends ISerializableInterval>
830
1011
  // if segment is undefined, it slid off the string
831
1012
  assert(segment !== undefined, 0x54e /* No segment found */);
832
1013
 
833
- const segoff = getSlideToSegoff({ segment, offset }) ?? segment;
1014
+ const segoff =
1015
+ getSlideToSegoff(
1016
+ { segment, offset },
1017
+ undefined,
1018
+ this.options.mergeTreeReferencesCanSlideToEndpoint,
1019
+ ) ?? segment;
834
1020
 
835
1021
  // case happens when rebasing op, but concurrently entire string has been deleted
836
1022
  if (segoff.segment === undefined || segoff.offset === undefined) {
@@ -894,12 +1080,28 @@ export class IntervalCollection<TInterval extends ISerializableInterval>
894
1080
  client,
895
1081
  label,
896
1082
  this.helpers,
1083
+ this.options,
897
1084
  (interval, previousInterval) => this.emitChange(interval, previousInterval, true, true),
898
1085
  );
899
1086
  if (this.savedSerializedIntervals) {
900
1087
  for (const serializedInterval of this.savedSerializedIntervals) {
901
1088
  this.localCollection.ensureSerializedId(serializedInterval);
902
- 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;
903
1105
  const interval = this.helpers.create(
904
1106
  label,
905
1107
  start,
@@ -908,7 +1110,7 @@ export class IntervalCollection<TInterval extends ISerializableInterval>
908
1110
  intervalType,
909
1111
  undefined,
910
1112
  true,
911
- stickiness,
1113
+ this.options.mergeTreeReferencesCanSlideToEndpoint,
912
1114
  );
913
1115
  if (properties) {
914
1116
  interval.addProperties(properties);
@@ -965,15 +1167,25 @@ export class IntervalCollection<TInterval extends ISerializableInterval>
965
1167
  return this.localCollection.idIntervalIndex.getIntervalById(id);
966
1168
  }
967
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
+
968
1181
  /**
969
1182
  * {@inheritdoc IIntervalCollection.add}
970
1183
  */
971
1184
  public add(
972
- start: number,
973
- end: number,
1185
+ start: SequencePlace,
1186
+ end: SequencePlace,
974
1187
  intervalType: IntervalType,
975
1188
  props?: PropertySet,
976
- stickiness: IntervalStickiness = IntervalStickiness.END,
977
1189
  ): TInterval {
978
1190
  if (!this.localCollection) {
979
1191
  throw new LoggingError("attach must be called prior to adding intervals");
@@ -981,29 +1193,42 @@ export class IntervalCollection<TInterval extends ISerializableInterval>
981
1193
  if (intervalType & IntervalType.Transient) {
982
1194
  throw new LoggingError("Can not add transient intervals");
983
1195
  }
984
- if (stickiness !== IntervalStickiness.END && !this.options.intervalStickinessEnabled) {
985
- throw new UsageError(
986
- "attempted to set interval stickiness without enabling `intervalStickinessEnabled` feature flag",
987
- );
988
- }
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);
989
1210
 
990
1211
  const interval: TInterval = this.localCollection.addInterval(
991
- start,
992
- end,
1212
+ toSequencePlace(startPos, startSide),
1213
+ toSequencePlace(endPos, endSide),
993
1214
  intervalType,
994
1215
  props,
995
- undefined,
996
- stickiness,
997
1216
  );
998
1217
 
999
1218
  if (interval) {
1000
- const serializedInterval = {
1001
- end,
1219
+ if (!this.isCollaborating && interval instanceof SequenceInterval) {
1220
+ setSlideOnRemove(interval.start);
1221
+ setSlideOnRemove(interval.end);
1222
+ }
1223
+ const serializedInterval: ISerializedInterval = {
1224
+ start: startPos,
1225
+ end: endPos,
1002
1226
  intervalType,
1003
1227
  properties: interval.properties,
1004
1228
  sequenceNumber: this.client?.getCurrentSeq() ?? 0,
1005
- start,
1006
1229
  stickiness,
1230
+ startSide,
1231
+ endSide,
1007
1232
  };
1008
1233
  const localSeq = this.getNextLocalSeq();
1009
1234
  this.localSeqToSerializedInterval.set(localSeq, serializedInterval);
@@ -1080,8 +1305,11 @@ export class IntervalCollection<TInterval extends ISerializableInterval>
1080
1305
 
1081
1306
  const interval = this.getIntervalById(id);
1082
1307
  if (interval) {
1083
- // Pass Unassigned as the sequence number to indicate that this is a local op that is waiting for an ack.
1084
- const deltaProps = interval.addProperties(props, true, UnassignedSequenceNumber);
1308
+ const deltaProps = interval.addProperties(
1309
+ props,
1310
+ true,
1311
+ this.isCollaborating ? UnassignedSequenceNumber : UniversalSequenceNumber,
1312
+ );
1085
1313
  const serializedInterval: ISerializedInterval = interval.serialize();
1086
1314
 
1087
1315
  // Emit a change op that will only change properties. Add the ID to
@@ -1101,7 +1329,7 @@ export class IntervalCollection<TInterval extends ISerializableInterval>
1101
1329
  /**
1102
1330
  * {@inheritdoc IIntervalCollection.change}
1103
1331
  */
1104
- public change(id: string, start?: number, end?: number): TInterval | undefined {
1332
+ public change(id: string, start: SequencePlace, end: SequencePlace): TInterval | undefined {
1105
1333
  if (!this.localCollection) {
1106
1334
  throw new LoggingError("Attach must be called before accessing intervals");
1107
1335
  }
@@ -1117,9 +1345,18 @@ export class IntervalCollection<TInterval extends ISerializableInterval>
1117
1345
  if (!newInterval) {
1118
1346
  return undefined;
1119
1347
  }
1348
+ if (!this.isCollaborating && newInterval instanceof SequenceInterval) {
1349
+ setSlideOnRemove(newInterval.start);
1350
+ setSlideOnRemove(newInterval.end);
1351
+ }
1120
1352
  const serializedInterval: SerializedIntervalDelta = interval.serialize();
1121
- serializedInterval.start = start;
1122
- 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;
1123
1360
  // Emit a property bag containing only the ID, as we don't intend for this op to change any properties.
1124
1361
  serializedInterval.properties = {
1125
1362
  [reservedIntervalIdKey]: interval.getIntervalId(),
@@ -1135,7 +1372,14 @@ export class IntervalCollection<TInterval extends ISerializableInterval>
1135
1372
  return undefined;
1136
1373
  }
1137
1374
 
1375
+ private get isCollaborating(): boolean {
1376
+ return this.client?.getCollabWindow().collaborating ?? false;
1377
+ }
1378
+
1138
1379
  private addPendingChange(id: string, serializedInterval: SerializedIntervalDelta) {
1380
+ if (!this.isCollaborating) {
1381
+ return;
1382
+ }
1139
1383
  if (serializedInterval.start !== undefined) {
1140
1384
  this.addPendingChangeHelper(id, this.pendingChangesStart, serializedInterval);
1141
1385
  }
@@ -1241,8 +1485,8 @@ export class IntervalCollection<TInterval extends ISerializableInterval>
1241
1485
  } else {
1242
1486
  // If there are pending changes with this ID, don't apply the remote start/end change, as the local ack
1243
1487
  // should be the winning change.
1244
- let start: number | undefined;
1245
- let end: number | undefined;
1488
+ let start: number | "start" | "end" | undefined;
1489
+ let end: number | "start" | "end" | undefined;
1246
1490
  // Track pending start/end independently of one another.
1247
1491
  if (!this.hasPendingChangeStart(id)) {
1248
1492
  start = serializedInterval.start;
@@ -1256,7 +1500,12 @@ export class IntervalCollection<TInterval extends ISerializableInterval>
1256
1500
  // If changeInterval gives us a new interval, work with that one. Otherwise keep working with
1257
1501
  // the one we originally found in the tree.
1258
1502
  newInterval =
1259
- 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;
1260
1509
  }
1261
1510
  const deltaProps = newInterval.addProperties(newProps, true, op.sequenceNumber);
1262
1511
  if (this.onDeserialize) {
@@ -1312,7 +1561,7 @@ export class IntervalCollection<TInterval extends ISerializableInterval>
1312
1561
  throw new LoggingError("attachSequence must be called");
1313
1562
  }
1314
1563
 
1315
- const { intervalType, properties } = serializedInterval;
1564
+ const { intervalType, properties, stickiness, startSide, endSide } = serializedInterval;
1316
1565
 
1317
1566
  const { start: startRebased, end: endRebased } =
1318
1567
  this.localSeqToRebasedInterval.get(localSeq) ?? this.computeRebasedPositions(localSeq);
@@ -1326,6 +1575,9 @@ export class IntervalCollection<TInterval extends ISerializableInterval>
1326
1575
  intervalType,
1327
1576
  sequenceNumber: this.client?.getCurrentSeq() ?? 0,
1328
1577
  properties,
1578
+ stickiness,
1579
+ startSide,
1580
+ endSide,
1329
1581
  };
1330
1582
 
1331
1583
  if (
@@ -1357,8 +1609,8 @@ export class IntervalCollection<TInterval extends ISerializableInterval>
1357
1609
  // updates the local client's state to be consistent with the emitted op.
1358
1610
  this.localCollection?.changeInterval(
1359
1611
  localInterval,
1360
- startRebased,
1361
- endRebased,
1612
+ toOptionalSequencePlace(startRebased, startSide),
1613
+ toOptionalSequencePlace(endRebased, endSide),
1362
1614
  undefined,
1363
1615
  localSeq,
1364
1616
  );
@@ -1377,7 +1629,11 @@ export class IntervalCollection<TInterval extends ISerializableInterval>
1377
1629
  if (segoff.segment?.localRefs?.has(lref) !== true) {
1378
1630
  return undefined;
1379
1631
  }
1380
- const newSegoff = getSlideToSegoff(segoff);
1632
+ const newSegoff = getSlideToSegoff(
1633
+ segoff,
1634
+ undefined,
1635
+ this.options.mergeTreeReferencesCanSlideToEndpoint,
1636
+ );
1381
1637
  const value: { segment: ISegment | undefined; offset: number | undefined } | undefined =
1382
1638
  segoff.segment === newSegoff.segment && segoff.offset === newSegoff.offset
1383
1639
  ? undefined
@@ -1385,14 +1641,7 @@ export class IntervalCollection<TInterval extends ISerializableInterval>
1385
1641
  return value;
1386
1642
  }
1387
1643
 
1388
- private setSlideOnRemove(lref: LocalReferencePosition) {
1389
- let refType = lref.refType;
1390
- refType = refType & ~ReferenceType.StayOnRemove;
1391
- refType = refType | ReferenceType.SlideOnRemove;
1392
- lref.refType = refType;
1393
- }
1394
-
1395
- private ackInterval(interval: TInterval, op: ISequencedDocumentMessage) {
1644
+ private ackInterval(interval: TInterval, op: ISequencedDocumentMessage): void {
1396
1645
  // Only SequenceIntervals need potential sliding
1397
1646
  if (!(interval instanceof SequenceInterval)) {
1398
1647
  return;
@@ -1413,11 +1662,11 @@ export class IntervalCollection<TInterval extends ISerializableInterval>
1413
1662
  const hasPendingEndChange = this.hasPendingChangeEnd(id);
1414
1663
 
1415
1664
  if (!hasPendingStartChange) {
1416
- this.setSlideOnRemove(interval.start);
1665
+ setSlideOnRemove(interval.start);
1417
1666
  }
1418
1667
 
1419
1668
  if (!hasPendingEndChange) {
1420
- this.setSlideOnRemove(interval.end);
1669
+ setSlideOnRemove(interval.end);
1421
1670
  }
1422
1671
 
1423
1672
  const needsStartUpdate = newStart !== undefined && !hasPendingStartChange;
@@ -1446,7 +1695,11 @@ export class IntervalCollection<TInterval extends ISerializableInterval>
1446
1695
  newStart,
1447
1696
  interval.start.refType,
1448
1697
  op,
1698
+ undefined,
1699
+ undefined,
1449
1700
  startReferenceSlidingPreference(interval.stickiness),
1701
+ startReferenceSlidingPreference(interval.stickiness) ===
1702
+ SlidingPreference.BACKWARD,
1450
1703
  );
1451
1704
  if (props) {
1452
1705
  interval.start.addProperties(props);
@@ -1464,7 +1717,11 @@ export class IntervalCollection<TInterval extends ISerializableInterval>
1464
1717
  newEnd,
1465
1718
  interval.end.refType,
1466
1719
  op,
1720
+ undefined,
1721
+ undefined,
1467
1722
  endReferenceSlidingPreference(interval.stickiness),
1723
+ endReferenceSlidingPreference(interval.stickiness) ===
1724
+ SlidingPreference.FORWARD,
1468
1725
  );
1469
1726
  if (props) {
1470
1727
  interval.end.addProperties(props);
@@ -1508,12 +1765,11 @@ export class IntervalCollection<TInterval extends ISerializableInterval>
1508
1765
  this.localCollection.ensureSerializedId(serializedInterval);
1509
1766
 
1510
1767
  const interval: TInterval = this.localCollection.addInterval(
1511
- serializedInterval.start,
1512
- serializedInterval.end,
1768
+ toSequencePlace(serializedInterval.start, serializedInterval.startSide ?? Side.Before),
1769
+ toSequencePlace(serializedInterval.end, serializedInterval.endSide ?? Side.Before),
1513
1770
  serializedInterval.intervalType,
1514
1771
  serializedInterval.properties,
1515
1772
  op,
1516
- serializedInterval.stickiness,
1517
1773
  );
1518
1774
 
1519
1775
  if (interval) {
@@ -1691,6 +1947,13 @@ export class IntervalCollection<TInterval extends ISerializableInterval>
1691
1947
  }
1692
1948
  }
1693
1949
 
1950
+ function setSlideOnRemove(lref: LocalReferencePosition) {
1951
+ let refType = lref.refType;
1952
+ refType = refType & ~ReferenceType.StayOnRemove;
1953
+ refType = refType | ReferenceType.SlideOnRemove;
1954
+ lref.refType = refType;
1955
+ }
1956
+
1694
1957
  /**
1695
1958
  * Information that identifies an interval within a `Sequence`.
1696
1959
  */