@fluidframework/sequence 2.0.0-dev.4.4.0.162574 → 2.0.0-dev.5.3.2.178189

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 (107) hide show
  1. package/CHANGELOG.md +48 -0
  2. package/dist/defaultMap.d.ts +3 -2
  3. package/dist/defaultMap.d.ts.map +1 -1
  4. package/dist/defaultMap.js +4 -3
  5. package/dist/defaultMap.js.map +1 -1
  6. package/dist/defaultMapInterfaces.d.ts +12 -1
  7. package/dist/defaultMapInterfaces.d.ts.map +1 -1
  8. package/dist/defaultMapInterfaces.js.map +1 -1
  9. package/dist/index.d.ts +4 -2
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/index.js +17 -3
  12. package/dist/index.js.map +1 -1
  13. package/dist/intervalCollection.d.ts +240 -78
  14. package/dist/intervalCollection.d.ts.map +1 -1
  15. package/dist/intervalCollection.js +313 -190
  16. package/dist/intervalCollection.js.map +1 -1
  17. package/dist/intervalIndex/index.d.ts +8 -0
  18. package/dist/intervalIndex/index.d.ts.map +1 -0
  19. package/dist/intervalIndex/index.js +12 -0
  20. package/dist/intervalIndex/index.js.map +1 -0
  21. package/dist/intervalIndex/overlappingIntervalsIndex.d.ts +32 -0
  22. package/dist/intervalIndex/overlappingIntervalsIndex.d.ts.map +1 -0
  23. package/dist/intervalIndex/overlappingIntervalsIndex.js +103 -0
  24. package/dist/intervalIndex/overlappingIntervalsIndex.js.map +1 -0
  25. package/dist/intervalIndex/overlappingSequenceIntervalsIndex.d.ts +8 -0
  26. package/dist/intervalIndex/overlappingSequenceIntervalsIndex.d.ts.map +1 -0
  27. package/dist/intervalIndex/overlappingSequenceIntervalsIndex.js +33 -0
  28. package/dist/intervalIndex/overlappingSequenceIntervalsIndex.js.map +1 -0
  29. package/dist/intervalIndex/sequenceIntervalIndexes.d.ts +33 -0
  30. package/dist/intervalIndex/sequenceIntervalIndexes.d.ts.map +1 -0
  31. package/dist/intervalIndex/sequenceIntervalIndexes.js +7 -0
  32. package/dist/intervalIndex/sequenceIntervalIndexes.js.map +1 -0
  33. package/dist/packageVersion.d.ts +1 -1
  34. package/dist/packageVersion.js +1 -1
  35. package/dist/packageVersion.js.map +1 -1
  36. package/dist/revertibles.d.ts +104 -0
  37. package/dist/revertibles.d.ts.map +1 -0
  38. package/dist/revertibles.js +414 -0
  39. package/dist/revertibles.js.map +1 -0
  40. package/dist/sequence.d.ts +4 -4
  41. package/dist/sequence.d.ts.map +1 -1
  42. package/dist/sequence.js +3 -3
  43. package/dist/sequence.js.map +1 -1
  44. package/dist/sharedIntervalCollection.d.ts +3 -3
  45. package/dist/sharedIntervalCollection.d.ts.map +1 -1
  46. package/dist/sharedIntervalCollection.js +1 -1
  47. package/dist/sharedIntervalCollection.js.map +1 -1
  48. package/dist/tsdoc-metadata.json +11 -0
  49. package/lib/defaultMap.d.ts +3 -2
  50. package/lib/defaultMap.d.ts.map +1 -1
  51. package/lib/defaultMap.js +4 -3
  52. package/lib/defaultMap.js.map +1 -1
  53. package/lib/defaultMapInterfaces.d.ts +12 -1
  54. package/lib/defaultMapInterfaces.d.ts.map +1 -1
  55. package/lib/defaultMapInterfaces.js.map +1 -1
  56. package/lib/index.d.ts +4 -2
  57. package/lib/index.d.ts.map +1 -1
  58. package/lib/index.js +3 -1
  59. package/lib/index.js.map +1 -1
  60. package/lib/intervalCollection.d.ts +240 -78
  61. package/lib/intervalCollection.d.ts.map +1 -1
  62. package/lib/intervalCollection.js +310 -190
  63. package/lib/intervalCollection.js.map +1 -1
  64. package/lib/intervalIndex/index.d.ts +8 -0
  65. package/lib/intervalIndex/index.d.ts.map +1 -0
  66. package/lib/intervalIndex/index.js +7 -0
  67. package/lib/intervalIndex/index.js.map +1 -0
  68. package/lib/intervalIndex/overlappingIntervalsIndex.d.ts +32 -0
  69. package/lib/intervalIndex/overlappingIntervalsIndex.d.ts.map +1 -0
  70. package/lib/intervalIndex/overlappingIntervalsIndex.js +98 -0
  71. package/lib/intervalIndex/overlappingIntervalsIndex.js.map +1 -0
  72. package/lib/intervalIndex/overlappingSequenceIntervalsIndex.d.ts +8 -0
  73. package/lib/intervalIndex/overlappingSequenceIntervalsIndex.d.ts.map +1 -0
  74. package/lib/intervalIndex/overlappingSequenceIntervalsIndex.js +29 -0
  75. package/lib/intervalIndex/overlappingSequenceIntervalsIndex.js.map +1 -0
  76. package/lib/intervalIndex/sequenceIntervalIndexes.d.ts +33 -0
  77. package/lib/intervalIndex/sequenceIntervalIndexes.d.ts.map +1 -0
  78. package/lib/intervalIndex/sequenceIntervalIndexes.js +6 -0
  79. package/lib/intervalIndex/sequenceIntervalIndexes.js.map +1 -0
  80. package/lib/packageVersion.d.ts +1 -1
  81. package/lib/packageVersion.js +1 -1
  82. package/lib/packageVersion.js.map +1 -1
  83. package/lib/revertibles.d.ts +104 -0
  84. package/lib/revertibles.d.ts.map +1 -0
  85. package/lib/revertibles.js +404 -0
  86. package/lib/revertibles.js.map +1 -0
  87. package/lib/sequence.d.ts +4 -4
  88. package/lib/sequence.d.ts.map +1 -1
  89. package/lib/sequence.js +3 -3
  90. package/lib/sequence.js.map +1 -1
  91. package/lib/sharedIntervalCollection.d.ts +3 -3
  92. package/lib/sharedIntervalCollection.d.ts.map +1 -1
  93. package/lib/sharedIntervalCollection.js +1 -1
  94. package/lib/sharedIntervalCollection.js.map +1 -1
  95. package/package.json +22 -24
  96. package/src/defaultMap.ts +4 -1
  97. package/src/defaultMapInterfaces.ts +13 -1
  98. package/src/index.ts +27 -5
  99. package/src/intervalCollection.ts +660 -216
  100. package/src/intervalIndex/index.ts +11 -0
  101. package/src/intervalIndex/overlappingIntervalsIndex.ts +166 -0
  102. package/src/intervalIndex/overlappingSequenceIntervalsIndex.ts +71 -0
  103. package/src/intervalIndex/sequenceIntervalIndexes.ts +32 -0
  104. package/src/packageVersion.ts +1 -1
  105. package/src/revertibles.ts +626 -0
  106. package/src/sequence.ts +12 -2
  107. package/src/sharedIntervalCollection.ts +4 -2
@@ -13,6 +13,7 @@ import {
13
13
  Client,
14
14
  compareReferencePositions,
15
15
  createMap,
16
+ getSlideToSegoff,
16
17
  ICombiningOp,
17
18
  ISegment,
18
19
  MergeTreeDeltaType,
@@ -28,6 +29,8 @@ import {
28
29
  maxReferencePosition,
29
30
  createDetachedLocalReferencePosition,
30
31
  DetachedReferencePosition,
32
+ SlidingPreference,
33
+ PropertyAction,
31
34
  } from "@fluidframework/merge-tree";
32
35
  import { ISequencedDocumentMessage } from "@fluidframework/protocol-definitions";
33
36
  import { LoggingError } from "@fluidframework/telemetry-utils";
@@ -39,11 +42,25 @@ import {
39
42
  IValueOperation,
40
43
  IValueType,
41
44
  IValueTypeOperationValue,
45
+ SequenceOptions,
42
46
  } from "./defaultMapInterfaces";
43
- import { IInterval, IntervalConflictResolver, IntervalTree, IntervalNode } from "./intervalTree";
47
+ import { IInterval, IntervalConflictResolver } from "./intervalTree";
48
+ import { IOverlappingIntervalsIndex, createOverlappingIntervalsIndex } from "./intervalIndex";
44
49
 
45
50
  const reservedIntervalIdKey = "intervalId";
46
51
 
52
+ /**
53
+ * Values are used in persisted formats (ops) and revertibles.
54
+ * @alpha
55
+ */
56
+ export const IntervalOpType = {
57
+ ADD: "add",
58
+ DELETE: "delete",
59
+ CHANGE: "change",
60
+ PROPERTY_CHANGED: "propertyChanged",
61
+ POSITION_REMOVE: "positionRemove",
62
+ } as const;
63
+
47
64
  export enum IntervalType {
48
65
  Simple = 0x0,
49
66
  Nest = 0x1,
@@ -51,7 +68,7 @@ export enum IntervalType {
51
68
  /**
52
69
  * SlideOnRemove indicates that the ends of the interval will slide if the segment
53
70
  * they reference is removed and acked.
54
- * See `packages\dds\merge-tree\REFERENCEPOSITIONS.md` for details
71
+ * See `packages\dds\merge-tree\docs\REFERENCEPOSITIONS.md` for details
55
72
  * SlideOnRemove is the default interval behavior and does not need to be specified.
56
73
  */
57
74
  SlideOnRemove = 0x2, // SlideOnRemove is default behavior - all intervals are SlideOnRemove
@@ -83,6 +100,7 @@ export interface ISerializedInterval {
83
100
  end: number;
84
101
  /** Interval type to create */
85
102
  intervalType: IntervalType;
103
+ stickiness?: IntervalStickiness;
86
104
  /** Any properties the interval has */
87
105
  properties?: PropertySet;
88
106
  }
@@ -101,13 +119,12 @@ export type SerializedIntervalDelta = Omit<ISerializedInterval, "start" | "end"
101
119
  *
102
120
  * Intervals are of the format:
103
121
  *
104
- * [start, end, sequenceNumber, intervalType, properties]
122
+ * [start, end, sequenceNumber, intervalType, properties, stickiness?]
105
123
  */
106
- export type CompressedSerializedInterval = [number, number, number, IntervalType, PropertySet];
124
+ export type CompressedSerializedInterval =
125
+ | [number, number, number, IntervalType, PropertySet, IntervalStickiness]
126
+ | [number, number, number, IntervalType, PropertySet];
107
127
 
108
- /**
109
- * @internal
110
- */
111
128
  export interface ISerializedIntervalCollectionV2 {
112
129
  label: string;
113
130
  version: 2;
@@ -128,6 +145,7 @@ function decompressInterval(
128
145
  sequenceNumber: interval[2],
129
146
  intervalType: interval[3],
130
147
  properties: { ...interval[4], [reservedRangeLabelsKey]: [label] },
148
+ stickiness: interval[5],
131
149
  };
132
150
  }
133
151
 
@@ -138,7 +156,7 @@ function decompressInterval(
138
156
  function compressInterval(interval: ISerializedInterval): CompressedSerializedInterval {
139
157
  const { start, end, sequenceNumber, intervalType, properties } = interval;
140
158
 
141
- return [
159
+ const base: CompressedSerializedInterval = [
142
160
  start,
143
161
  end,
144
162
  sequenceNumber,
@@ -147,6 +165,26 @@ function compressInterval(interval: ISerializedInterval): CompressedSerializedIn
147
165
  // in the `label` field of the summary
148
166
  { ...properties, [reservedRangeLabelsKey]: undefined },
149
167
  ];
168
+
169
+ if (interval.stickiness !== undefined && interval.stickiness !== IntervalStickiness.END) {
170
+ base.push(interval.stickiness);
171
+ }
172
+
173
+ return base;
174
+ }
175
+
176
+ function startReferenceSlidingPreference(stickiness: IntervalStickiness): SlidingPreference {
177
+ // if any start stickiness, prefer sliding backwards
178
+ return (stickiness & IntervalStickiness.START) !== 0
179
+ ? SlidingPreference.BACKWARD
180
+ : SlidingPreference.FORWARD;
181
+ }
182
+
183
+ function endReferenceSlidingPreference(stickiness: IntervalStickiness): SlidingPreference {
184
+ // if any end stickiness, prefer sliding forwards
185
+ return (stickiness & IntervalStickiness.END) !== 0
186
+ ? SlidingPreference.FORWARD
187
+ : SlidingPreference.BACKWARD;
150
188
  }
151
189
 
152
190
  export interface ISerializableInterval extends IInterval {
@@ -172,14 +210,18 @@ export interface ISerializableInterval extends IInterval {
172
210
  getIntervalId(): string | undefined;
173
211
  }
174
212
 
213
+ /**
214
+ * @sealed
215
+ */
175
216
  export interface IIntervalHelpers<TInterval extends ISerializableInterval> {
176
217
  compareEnds(a: TInterval, b: TInterval): number;
218
+ compareStarts?(a: TInterval, b: TInterval): number;
177
219
  /**
178
220
  *
179
221
  * @param label - label of the interval collection this interval is being added to. This parameter is
180
222
  * irrelevant for transient intervals.
181
223
  * @param start - numerical start position of the interval
182
- * @param end - numberical end position of the interval
224
+ * @param end - numerical end position of the interval
183
225
  * @param client - client creating the interval
184
226
  * @param intervalType - Type of interval to create. Default is SlideOnRemove
185
227
  * @param op - If this create came from a remote client, op that created it. Default is undefined (i.e. local)
@@ -193,9 +235,50 @@ export interface IIntervalHelpers<TInterval extends ISerializableInterval> {
193
235
  intervalType: IntervalType,
194
236
  op?: ISequencedDocumentMessage,
195
237
  fromSnapshot?: boolean,
238
+ stickiness?: IntervalStickiness,
196
239
  ): TInterval;
197
240
  }
198
241
 
242
+ /**
243
+ * Determines how an interval should expand when segments are inserted adjacent
244
+ * to the range it spans
245
+ *
246
+ * Note that interval stickiness is currently an experimental feature and must
247
+ * be explicitly enabled with the `intervalStickinessEnabled` flag
248
+ */
249
+ export const IntervalStickiness = {
250
+ /**
251
+ * Interval does not expand to include adjacent segments
252
+ */
253
+ NONE: 0b00,
254
+
255
+ /**
256
+ * Interval expands to include segments inserted adjacent to the start
257
+ */
258
+ START: 0b01,
259
+
260
+ /**
261
+ * Interval expands to include segments inserted adjacent to the end
262
+ *
263
+ * This is the default stickiness
264
+ */
265
+ END: 0b10,
266
+
267
+ /**
268
+ * Interval expands to include all segments inserted adjacent to it
269
+ */
270
+ FULL: 0b11,
271
+ } as const;
272
+
273
+ /**
274
+ * Determines how an interval should expand when segments are inserted adjacent
275
+ * to the range it spans
276
+ *
277
+ * Note that interval stickiness is currently an experimental feature and must
278
+ * be explicitly enabled with the `intervalStickinessEnabled` flag
279
+ */
280
+ export type IntervalStickiness = typeof IntervalStickiness[keyof typeof IntervalStickiness];
281
+
199
282
  /**
200
283
  * Serializable interval whose endpoints are plain-old numbers.
201
284
  */
@@ -240,7 +323,7 @@ export class Interval implements ISerializableInterval {
240
323
  * Adds an auxiliary set of properties to this interval.
241
324
  * These properties can be recovered using `getAdditionalPropertySets`
242
325
  * @param props - set of properties to add
243
- * @remarks - This gets called as part of the default conflict resolver for `IntervalCollection<Interval>`
326
+ * @remarks - This gets called as part of the default conflict resolver for `IIntervalCollection<Interval>`
244
327
  * (i.e. non-sequence-based interval collections). However, the additional properties don't get serialized.
245
328
  * This functionality seems half-baked.
246
329
  */
@@ -394,7 +477,7 @@ export class Interval implements ISerializableInterval {
394
477
  }
395
478
 
396
479
  /**
397
- * Interval impelmentation whose ends are associated with positions in a mutatable sequence.
480
+ * Interval implementation whose ends are associated with positions in a mutatable sequence.
398
481
  * As such, when content is inserted into the middle of the interval, the interval expands to
399
482
  * include that content.
400
483
  *
@@ -439,6 +522,7 @@ export class SequenceInterval implements ISerializableInterval {
439
522
  public end: LocalReferencePosition,
440
523
  public intervalType: IntervalType,
441
524
  props?: PropertySet,
525
+ public readonly stickiness: IntervalStickiness = IntervalStickiness.END,
442
526
  ) {
443
527
  this.propertyManager = new PropertiesManager();
444
528
  this.properties = {};
@@ -500,6 +584,9 @@ export class SequenceInterval implements ISerializableInterval {
500
584
  if (this.properties) {
501
585
  serializedInterval.properties = this.properties;
502
586
  }
587
+ if (this.stickiness !== IntervalStickiness.END) {
588
+ serializedInterval.stickiness = this.stickiness;
589
+ }
503
590
 
504
591
  return serializedInterval;
505
592
  }
@@ -514,6 +601,7 @@ export class SequenceInterval implements ISerializableInterval {
514
601
  this.end,
515
602
  this.intervalType,
516
603
  this.properties,
604
+ this.stickiness,
517
605
  );
518
606
  }
519
607
 
@@ -621,6 +709,7 @@ export class SequenceInterval implements ISerializableInterval {
621
709
  end: number,
622
710
  op?: ISequencedDocumentMessage,
623
711
  localSeq?: number,
712
+ stickiness: IntervalStickiness = IntervalStickiness.END,
624
713
  ) {
625
714
  const getRefType = (baseType: ReferenceType): ReferenceType => {
626
715
  let refType = baseType;
@@ -640,6 +729,7 @@ export class SequenceInterval implements ISerializableInterval {
640
729
  op,
641
730
  undefined,
642
731
  localSeq,
732
+ startReferenceSlidingPreference(stickiness),
643
733
  );
644
734
  if (this.start.properties) {
645
735
  startRef.addProperties(this.start.properties);
@@ -655,6 +745,7 @@ export class SequenceInterval implements ISerializableInterval {
655
745
  op,
656
746
  undefined,
657
747
  localSeq,
748
+ endReferenceSlidingPreference(stickiness),
658
749
  );
659
750
  if (this.end.properties) {
660
751
  endRef.addProperties(this.end.properties);
@@ -683,13 +774,14 @@ export class SequenceInterval implements ISerializableInterval {
683
774
  }
684
775
  }
685
776
 
686
- function createPositionReferenceFromSegoff(
777
+ export function createPositionReferenceFromSegoff(
687
778
  client: Client,
688
779
  segoff: { segment: ISegment | undefined; offset: number | undefined },
689
780
  refType: ReferenceType,
690
781
  op?: ISequencedDocumentMessage,
691
782
  localSeq?: number,
692
783
  fromSnapshot?: boolean,
784
+ slidingPreference?: SlidingPreference,
693
785
  ): LocalReferencePosition {
694
786
  if (segoff.segment) {
695
787
  const ref = client.createLocalReferencePosition(
@@ -697,6 +789,7 @@ function createPositionReferenceFromSegoff(
697
789
  segoff.offset,
698
790
  refType,
699
791
  undefined,
792
+ slidingPreference,
700
793
  );
701
794
  return ref;
702
795
  }
@@ -725,6 +818,7 @@ function createPositionReference(
725
818
  op?: ISequencedDocumentMessage,
726
819
  fromSnapshot?: boolean,
727
820
  localSeq?: number,
821
+ slidingPreference?: SlidingPreference,
728
822
  ): LocalReferencePosition {
729
823
  let segoff;
730
824
  if (op) {
@@ -736,7 +830,7 @@ function createPositionReference(
736
830
  referenceSequenceNumber: op.referenceSequenceNumber,
737
831
  clientId: op.clientId,
738
832
  });
739
- segoff = client.getSlideToSegment(segoff);
833
+ segoff = getSlideToSegoff(segoff);
740
834
  } else {
741
835
  assert(
742
836
  (refType & ReferenceType.SlideOnRemove) === 0 || !!fromSnapshot,
@@ -744,7 +838,16 @@ function createPositionReference(
744
838
  );
745
839
  segoff = client.getContainingSegment(pos, undefined, localSeq);
746
840
  }
747
- return createPositionReferenceFromSegoff(client, segoff, refType, op, localSeq, fromSnapshot);
841
+
842
+ return createPositionReferenceFromSegoff(
843
+ client,
844
+ segoff,
845
+ refType,
846
+ op,
847
+ localSeq,
848
+ fromSnapshot,
849
+ slidingPreference,
850
+ );
748
851
  }
749
852
 
750
853
  export function createSequenceInterval(
@@ -755,6 +858,7 @@ export function createSequenceInterval(
755
858
  intervalType: IntervalType,
756
859
  op?: ISequencedDocumentMessage,
757
860
  fromSnapshot?: boolean,
861
+ stickiness: IntervalStickiness = IntervalStickiness.END,
758
862
  ): SequenceInterval {
759
863
  let beginRefType = ReferenceType.RangeBegin;
760
864
  let endRefType = ReferenceType.RangeEnd;
@@ -778,15 +882,40 @@ export function createSequenceInterval(
778
882
  }
779
883
  }
780
884
 
781
- const startLref = createPositionReference(client, start, beginRefType, op, fromSnapshot);
782
- const endLref = createPositionReference(client, end, endRefType, op, fromSnapshot);
885
+ const startLref = createPositionReference(
886
+ client,
887
+ start,
888
+ beginRefType,
889
+ op,
890
+ fromSnapshot,
891
+ undefined,
892
+ startReferenceSlidingPreference(stickiness),
893
+ );
894
+
895
+ const endLref = createPositionReference(
896
+ client,
897
+ end,
898
+ endRefType,
899
+ op,
900
+ fromSnapshot,
901
+ undefined,
902
+ endReferenceSlidingPreference(stickiness),
903
+ );
904
+
783
905
  const rangeProp = {
784
906
  [reservedRangeLabelsKey]: [label],
785
907
  };
786
908
  startLref.addProperties(rangeProp);
787
909
  endLref.addProperties(rangeProp);
788
910
 
789
- const ival = new SequenceInterval(client, startLref, endLref, intervalType, rangeProp);
911
+ const ival = new SequenceInterval(
912
+ client,
913
+ startLref,
914
+ endLref,
915
+ intervalType,
916
+ rangeProp,
917
+ stickiness,
918
+ );
790
919
  return ival;
791
920
  }
792
921
 
@@ -824,135 +953,6 @@ export interface IntervalIndex<TInterval extends ISerializableInterval> {
824
953
  remove(interval: TInterval): void;
825
954
  }
826
955
 
827
- class OverlappingIntervalsIndex<TInterval extends ISerializableInterval>
828
- implements IntervalIndex<TInterval>
829
- {
830
- private readonly intervalTree = new IntervalTree<TInterval>();
831
-
832
- constructor(
833
- private readonly client: Client,
834
- private readonly helpers: IIntervalHelpers<TInterval>,
835
- ) {}
836
-
837
- public map(fn: (interval: TInterval) => void) {
838
- this.intervalTree.map(fn);
839
- }
840
-
841
- public mapUntil(fn: (interval: TInterval) => boolean) {
842
- this.intervalTree.mapUntil(fn);
843
- }
844
-
845
- public gatherIterationResults(
846
- results: TInterval[],
847
- iteratesForward: boolean,
848
- start?: number,
849
- end?: number,
850
- ) {
851
- if (this.intervalTree.intervals.isEmpty()) {
852
- return;
853
- }
854
-
855
- if (start === undefined && end === undefined) {
856
- // No start/end provided. Gather the whole tree in the specified order.
857
- if (iteratesForward) {
858
- this.intervalTree.map((interval: TInterval) => {
859
- results.push(interval);
860
- });
861
- } else {
862
- this.intervalTree.mapBackward((interval: TInterval) => {
863
- results.push(interval);
864
- });
865
- }
866
- } else {
867
- const transientInterval: TInterval = this.helpers.create(
868
- "transient",
869
- start,
870
- end,
871
- this.client,
872
- IntervalType.Transient,
873
- );
874
-
875
- if (start === undefined) {
876
- // Only end position provided. Since the tree is not sorted by end position,
877
- // walk the whole tree in the specified order, gathering intervals that match the end.
878
- if (iteratesForward) {
879
- this.intervalTree.map((interval: TInterval) => {
880
- if (transientInterval.compareEnd(interval) === 0) {
881
- results.push(interval);
882
- }
883
- });
884
- } else {
885
- this.intervalTree.mapBackward((interval: TInterval) => {
886
- if (transientInterval.compareEnd(interval) === 0) {
887
- results.push(interval);
888
- }
889
- });
890
- }
891
- } else {
892
- // Start and (possibly) end provided. Walk the subtrees that may contain
893
- // this start position.
894
- const compareFn =
895
- end === undefined
896
- ? (node: IntervalNode<TInterval>) => {
897
- return transientInterval.compareStart(node.key);
898
- }
899
- : (node: IntervalNode<TInterval>) => {
900
- return transientInterval.compare(node.key);
901
- };
902
- const continueLeftFn = (cmpResult: number) => cmpResult <= 0;
903
- const continueRightFn = (cmpResult: number) => cmpResult >= 0;
904
- const actionFn = (node: IntervalNode<TInterval>) => {
905
- results.push(node.key);
906
- };
907
-
908
- if (iteratesForward) {
909
- this.intervalTree.intervals.walkExactMatchesForward(
910
- compareFn,
911
- actionFn,
912
- continueLeftFn,
913
- continueRightFn,
914
- );
915
- } else {
916
- this.intervalTree.intervals.walkExactMatchesBackward(
917
- compareFn,
918
- actionFn,
919
- continueLeftFn,
920
- continueRightFn,
921
- );
922
- }
923
- }
924
- }
925
- }
926
-
927
- /**
928
- * @returns an array of all intervals contained in this collection that overlap the range
929
- * `[startPosition, endPosition)`.
930
- */
931
- public findOverlappingIntervals(startPosition: number, endPosition: number) {
932
- if (endPosition < startPosition || this.intervalTree.intervals.isEmpty()) {
933
- return [];
934
- }
935
- const transientInterval = this.helpers.create(
936
- "transient",
937
- startPosition,
938
- endPosition,
939
- this.client,
940
- IntervalType.Transient,
941
- );
942
-
943
- const overlappingIntervalNodes = this.intervalTree.match(transientInterval);
944
- return overlappingIntervalNodes.map((node) => node.key);
945
- }
946
-
947
- public remove(interval: TInterval) {
948
- this.intervalTree.removeExisting(interval);
949
- }
950
-
951
- public add(interval: TInterval) {
952
- this.intervalTree.put(interval);
953
- }
954
- }
955
-
956
956
  class IdIntervalIndex<TInterval extends ISerializableInterval>
957
957
  implements IntervalIndex<TInterval>, Iterable<TInterval>
958
958
  {
@@ -1036,12 +1036,229 @@ class EndpointIndex<TInterval extends ISerializableInterval> implements Interval
1036
1036
  }
1037
1037
  }
1038
1038
 
1039
+ /**
1040
+ * Collection of intervals.
1041
+ *
1042
+ * Provide additional APIs to support efficiently querying a collection of intervals whose endpoints fall within a specified range.
1043
+ */
1044
+ export interface IEndpointInRangeIndex<TInterval extends ISerializableInterval>
1045
+ extends IntervalIndex<TInterval> {
1046
+ /**
1047
+ * @returns an array of all intervals contained in this collection whose endpoints locate in the range [start, end] (includes both ends)
1048
+ */
1049
+ findIntervalsWithEndpointInRange(start: number, end: number);
1050
+ }
1051
+
1052
+ /**
1053
+ * Collection of intervals.
1054
+ *
1055
+ * Provide additional APIs to support efficiently querying a collection of intervals whose startpoints fall within a specified range.
1056
+ */
1057
+ export interface IStartpointInRangeIndex<TInterval extends ISerializableInterval>
1058
+ extends IntervalIndex<TInterval> {
1059
+ /**
1060
+ * @returns an array of all intervals contained in this collection whose startpoints locate in the range [start, end] (includes both ends)
1061
+ */
1062
+ findIntervalsWithStartpointInRange(start: number, end: number);
1063
+ }
1064
+
1065
+ /**
1066
+ * Interface for intervals that have comparison override properties.
1067
+ */
1068
+ const forceCompare = Symbol();
1069
+
1070
+ interface HasComparisonOverride {
1071
+ [forceCompare]: number;
1072
+ }
1073
+
1074
+ /**
1075
+ * Compares two objects based on their comparison override properties.
1076
+ * @returns A number indicating the order of the intervals (negative for a is lower than b, 0 for tie, positive for a is greater than b).
1077
+ */
1078
+ function compareOverrideables(
1079
+ a: Partial<HasComparisonOverride>,
1080
+ b: Partial<HasComparisonOverride>,
1081
+ ): number {
1082
+ const forceCompareA = a[forceCompare] ?? 0;
1083
+ const forceCompareB = b[forceCompare] ?? 0;
1084
+
1085
+ return forceCompareA - forceCompareB;
1086
+ }
1087
+
1088
+ class EndpointInRangeIndex<TInterval extends ISerializableInterval>
1089
+ implements IEndpointInRangeIndex<TInterval>
1090
+ {
1091
+ private readonly intervalTree;
1092
+
1093
+ constructor(
1094
+ private readonly helpers: IIntervalHelpers<TInterval>,
1095
+ private readonly client: Client,
1096
+ ) {
1097
+ this.intervalTree = new RedBlackTree<TInterval, TInterval>((a: TInterval, b: TInterval) => {
1098
+ const compareEndsResult = helpers.compareEnds(a, b);
1099
+ if (compareEndsResult !== 0) {
1100
+ return compareEndsResult;
1101
+ }
1102
+
1103
+ const overrideablesComparison = compareOverrideables(
1104
+ a as Partial<HasComparisonOverride>,
1105
+ b as Partial<HasComparisonOverride>,
1106
+ );
1107
+ if (overrideablesComparison !== 0) {
1108
+ return overrideablesComparison;
1109
+ }
1110
+
1111
+ const aId = a.getIntervalId();
1112
+ const bId = b.getIntervalId();
1113
+ if (aId !== undefined && bId !== undefined) {
1114
+ return aId.localeCompare(bId);
1115
+ }
1116
+ return 0;
1117
+ });
1118
+ }
1119
+
1120
+ public add(interval: TInterval): void {
1121
+ this.intervalTree.put(interval, interval);
1122
+ }
1123
+
1124
+ public remove(interval: TInterval): void {
1125
+ this.intervalTree.remove(interval);
1126
+ }
1127
+
1128
+ public findIntervalsWithEndpointInRange(start: number, end: number) {
1129
+ if (start <= 0 || start > end || this.intervalTree.isEmpty()) {
1130
+ return [];
1131
+ }
1132
+ const results: TInterval[] = [];
1133
+ const action: PropertyAction<TInterval, TInterval> = (node) => {
1134
+ results.push(node.data);
1135
+ return true;
1136
+ };
1137
+
1138
+ const transientStartInterval = this.helpers.create(
1139
+ "transient",
1140
+ start,
1141
+ start,
1142
+ this.client,
1143
+ IntervalType.Transient,
1144
+ );
1145
+
1146
+ const transientEndInterval = this.helpers.create(
1147
+ "transient",
1148
+ end,
1149
+ end,
1150
+ this.client,
1151
+ IntervalType.Transient,
1152
+ );
1153
+
1154
+ // Add comparison overrides to the transient intervals
1155
+ (transientStartInterval as Partial<HasComparisonOverride>)[forceCompare] = -1;
1156
+ (transientEndInterval as Partial<HasComparisonOverride>)[forceCompare] = 1;
1157
+
1158
+ this.intervalTree.mapRange(action, results, transientStartInterval, transientEndInterval);
1159
+ return results;
1160
+ }
1161
+ }
1162
+
1163
+ class StartpointInRangeIndex<TInterval extends ISerializableInterval>
1164
+ implements IStartpointInRangeIndex<TInterval>
1165
+ {
1166
+ private readonly intervalTree;
1167
+
1168
+ constructor(
1169
+ private readonly helpers: IIntervalHelpers<TInterval>,
1170
+ private readonly client: Client,
1171
+ ) {
1172
+ this.intervalTree = new RedBlackTree<TInterval, TInterval>((a: TInterval, b: TInterval) => {
1173
+ assert(
1174
+ typeof helpers.compareStarts === "function",
1175
+ 0x6d1 /* compareStarts does not exist in the helpers */,
1176
+ );
1177
+
1178
+ const compareStartsResult = helpers.compareStarts(a, b);
1179
+ if (compareStartsResult !== 0) {
1180
+ return compareStartsResult;
1181
+ }
1182
+
1183
+ const overrideablesComparison = compareOverrideables(
1184
+ a as Partial<HasComparisonOverride>,
1185
+ b as Partial<HasComparisonOverride>,
1186
+ );
1187
+ if (overrideablesComparison !== 0) {
1188
+ return overrideablesComparison;
1189
+ }
1190
+ const aId = a.getIntervalId();
1191
+ const bId = b.getIntervalId();
1192
+ if (aId !== undefined && bId !== undefined) {
1193
+ return aId.localeCompare(bId);
1194
+ }
1195
+ return 0;
1196
+ });
1197
+ }
1198
+
1199
+ public add(interval: TInterval): void {
1200
+ this.intervalTree.put(interval, interval);
1201
+ }
1202
+
1203
+ public remove(interval: TInterval): void {
1204
+ this.intervalTree.remove(interval);
1205
+ }
1206
+
1207
+ public findIntervalsWithStartpointInRange(start: number, end: number) {
1208
+ if (start <= 0 || start > end || this.intervalTree.isEmpty()) {
1209
+ return [];
1210
+ }
1211
+ const results: TInterval[] = [];
1212
+ const action: PropertyAction<TInterval, TInterval> = (node) => {
1213
+ results.push(node.data);
1214
+ return true;
1215
+ };
1216
+
1217
+ const transientStartInterval = this.helpers.create(
1218
+ "transient",
1219
+ start,
1220
+ start,
1221
+ this.client,
1222
+ IntervalType.Transient,
1223
+ );
1224
+
1225
+ const transientEndInterval = this.helpers.create(
1226
+ "transient",
1227
+ end,
1228
+ end,
1229
+ this.client,
1230
+ IntervalType.Transient,
1231
+ );
1232
+
1233
+ // Add comparison overrides to the transient intervals
1234
+ (transientStartInterval as Partial<HasComparisonOverride>)[forceCompare] = -1;
1235
+ (transientEndInterval as Partial<HasComparisonOverride>)[forceCompare] = 1;
1236
+
1237
+ this.intervalTree.mapRange(action, results, transientStartInterval, transientEndInterval);
1238
+ return results;
1239
+ }
1240
+ }
1241
+
1242
+ export function createEndpointInRangeIndex<TInterval extends ISerializableInterval>(
1243
+ helpers: IIntervalHelpers<TInterval>,
1244
+ client: Client,
1245
+ ): IEndpointInRangeIndex<TInterval> {
1246
+ return new EndpointInRangeIndex<TInterval>(helpers, client);
1247
+ }
1248
+
1249
+ export function createStartpointInRangeIndex<TInterval extends ISerializableInterval>(
1250
+ helpers: IIntervalHelpers<TInterval>,
1251
+ client: Client,
1252
+ ): IStartpointInRangeIndex<TInterval> {
1253
+ return new StartpointInRangeIndex<TInterval>(helpers, client);
1254
+ }
1255
+
1039
1256
  export class LocalIntervalCollection<TInterval extends ISerializableInterval> {
1040
1257
  private static readonly legacyIdPrefix = "legacy";
1041
- public readonly overlappingIntervalsIndex: OverlappingIntervalsIndex<TInterval>;
1258
+ public readonly overlappingIntervalsIndex: IOverlappingIntervalsIndex<TInterval>;
1042
1259
  public readonly idIntervalIndex: IdIntervalIndex<TInterval>;
1043
1260
  public readonly endIntervalIndex: EndpointIndex<TInterval>;
1044
- private readonly indexes: IntervalIndex<TInterval>[];
1261
+ private readonly indexes: Set<IntervalIndex<TInterval>>;
1045
1262
 
1046
1263
  constructor(
1047
1264
  private readonly client: Client,
@@ -1053,14 +1270,14 @@ export class LocalIntervalCollection<TInterval extends ISerializableInterval> {
1053
1270
  previousInterval: TInterval,
1054
1271
  ) => void,
1055
1272
  ) {
1056
- this.overlappingIntervalsIndex = new OverlappingIntervalsIndex(client, helpers);
1273
+ this.overlappingIntervalsIndex = createOverlappingIntervalsIndex(client, helpers);
1057
1274
  this.idIntervalIndex = new IdIntervalIndex();
1058
1275
  this.endIntervalIndex = new EndpointIndex(client, helpers);
1059
- this.indexes = [
1276
+ this.indexes = new Set([
1060
1277
  this.overlappingIntervalsIndex,
1061
1278
  this.idIntervalIndex,
1062
1279
  this.endIntervalIndex,
1063
- ];
1280
+ ]);
1064
1281
  }
1065
1282
 
1066
1283
  public createLegacyId(start: number, end: number): string {
@@ -1104,6 +1321,14 @@ export class LocalIntervalCollection<TInterval extends ISerializableInterval> {
1104
1321
  }
1105
1322
  }
1106
1323
 
1324
+ public appendIndex(index: IntervalIndex<TInterval>) {
1325
+ this.indexes.add(index);
1326
+ }
1327
+
1328
+ public removeIndex(index: IntervalIndex<TInterval>): boolean {
1329
+ return this.indexes.delete(index);
1330
+ }
1331
+
1107
1332
  public removeExistingInterval(interval: TInterval) {
1108
1333
  this.removeIntervalFromIndexes(interval);
1109
1334
  this.removeIntervalListeners(interval);
@@ -1114,8 +1339,18 @@ export class LocalIntervalCollection<TInterval extends ISerializableInterval> {
1114
1339
  end: number,
1115
1340
  intervalType: IntervalType,
1116
1341
  op?: ISequencedDocumentMessage,
1342
+ stickiness: IntervalStickiness = IntervalStickiness.END,
1117
1343
  ): TInterval {
1118
- return this.helpers.create(this.label, start, end, this.client, intervalType, op);
1344
+ return this.helpers.create(
1345
+ this.label,
1346
+ start,
1347
+ end,
1348
+ this.client,
1349
+ intervalType,
1350
+ op,
1351
+ undefined,
1352
+ stickiness,
1353
+ );
1119
1354
  }
1120
1355
 
1121
1356
  public addInterval(
@@ -1124,14 +1359,26 @@ export class LocalIntervalCollection<TInterval extends ISerializableInterval> {
1124
1359
  intervalType: IntervalType,
1125
1360
  props?: PropertySet,
1126
1361
  op?: ISequencedDocumentMessage,
1362
+ stickiness: IntervalStickiness = IntervalStickiness.END,
1127
1363
  ) {
1128
- const interval: TInterval = this.createInterval(start, end, intervalType, op);
1364
+ const interval: TInterval = this.createInterval(start, end, intervalType, op, stickiness);
1129
1365
  if (interval) {
1130
1366
  if (!interval.properties) {
1131
1367
  interval.properties = createMap<any>();
1132
1368
  }
1133
1369
 
1134
1370
  if (props) {
1371
+ // This check is intended to prevent scenarios where a random interval is created and then
1372
+ // inserted into a collection. The aim is to ensure that the collection is created first
1373
+ // then the user can create/add intervals based on the collection
1374
+ if (
1375
+ props[reservedRangeLabelsKey] !== undefined &&
1376
+ props[reservedRangeLabelsKey][0] !== this.label
1377
+ ) {
1378
+ throw new LoggingError(
1379
+ "Adding an interval that belongs to another interval collection is not permitted",
1380
+ );
1381
+ }
1135
1382
  interval.addProperties(props);
1136
1383
  }
1137
1384
  interval.properties[reservedIntervalIdKey] ??= uuid();
@@ -1201,6 +1448,7 @@ export class LocalIntervalCollection<TInterval extends ISerializableInterval> {
1201
1448
  ref.getOffset(),
1202
1449
  ReferenceType.Transient,
1203
1450
  ref.properties,
1451
+ ref.slidingPreference,
1204
1452
  );
1205
1453
  };
1206
1454
  if (interval instanceof SequenceInterval) {
@@ -1243,18 +1491,36 @@ export class LocalIntervalCollection<TInterval extends ISerializableInterval> {
1243
1491
  export const compareSequenceIntervalEnds = (a: SequenceInterval, b: SequenceInterval): number =>
1244
1492
  compareReferencePositions(a.end, b.end);
1245
1493
 
1494
+ export const compareSequenceIntervalStarts = (a: SequenceInterval, b: SequenceInterval): number =>
1495
+ compareReferencePositions(a.start, b.start);
1496
+
1497
+ export const sequenceIntervalHelpers: IIntervalHelpers<SequenceInterval> = {
1498
+ compareEnds: compareSequenceIntervalEnds,
1499
+ compareStarts: compareSequenceIntervalStarts,
1500
+ create: createSequenceInterval,
1501
+ };
1502
+
1503
+ export const intervalHelpers: IIntervalHelpers<Interval> = {
1504
+ compareEnds: (a: Interval, b: Interval) => a.end - b.end,
1505
+ compareStarts: (a: Interval, b: Interval) => a.start - b.start,
1506
+ create: createInterval,
1507
+ };
1508
+
1246
1509
  class SequenceIntervalCollectionFactory
1247
1510
  implements IValueFactory<IntervalCollection<SequenceInterval>>
1248
1511
  {
1249
1512
  public load(
1250
1513
  emitter: IValueOpEmitter,
1251
1514
  raw: ISerializedInterval[] | ISerializedIntervalCollectionV2 = [],
1515
+ options?: Partial<SequenceOptions>,
1252
1516
  ): IntervalCollection<SequenceInterval> {
1253
- const helpers: IIntervalHelpers<SequenceInterval> = {
1254
- compareEnds: compareSequenceIntervalEnds,
1255
- create: createSequenceInterval,
1256
- };
1257
- return new IntervalCollection<SequenceInterval>(helpers, true, emitter, raw);
1517
+ return new IntervalCollection<SequenceInterval>(
1518
+ sequenceIntervalHelpers,
1519
+ true,
1520
+ emitter,
1521
+ raw,
1522
+ options,
1523
+ );
1258
1524
  }
1259
1525
 
1260
1526
  public store(
@@ -1289,7 +1555,15 @@ export class SequenceIntervalCollectionValueType
1289
1555
 
1290
1556
  const compareIntervalEnds = (a: Interval, b: Interval) => a.end - b.end;
1291
1557
 
1292
- function createInterval(label: string, start: number, end: number, client: Client): Interval {
1558
+ function createInterval(
1559
+ label: string,
1560
+ start: number,
1561
+ end: number,
1562
+ client: Client,
1563
+ intervalType?: IntervalType,
1564
+ op?: ISequencedDocumentMessage,
1565
+ fromSnapshot?: boolean,
1566
+ ): Interval {
1293
1567
  const rangeProp: PropertySet = {};
1294
1568
 
1295
1569
  if (label && label.length > 0) {
@@ -1303,12 +1577,13 @@ class IntervalCollectionFactory implements IValueFactory<IntervalCollection<Inte
1303
1577
  public load(
1304
1578
  emitter: IValueOpEmitter,
1305
1579
  raw: ISerializedInterval[] | ISerializedIntervalCollectionV2 = [],
1580
+ options?: Partial<SequenceOptions>,
1306
1581
  ): IntervalCollection<Interval> {
1307
1582
  const helpers: IIntervalHelpers<Interval> = {
1308
1583
  compareEnds: compareIntervalEnds,
1309
1584
  create: createInterval,
1310
1585
  };
1311
- const collection = new IntervalCollection<Interval>(helpers, false, emitter, raw);
1586
+ const collection = new IntervalCollection<Interval>(helpers, false, emitter, raw, options);
1312
1587
  collection.attachGraph(undefined as any as Client, "");
1313
1588
  return collection;
1314
1589
  }
@@ -1355,7 +1630,7 @@ export function makeOpsMap<T extends ISerializableInterval>(): Map<
1355
1630
 
1356
1631
  return new Map<string, IValueOperation<IntervalCollection<T>>>([
1357
1632
  [
1358
- "add",
1633
+ IntervalOpType.ADD,
1359
1634
  {
1360
1635
  process: (collection, params, local, op, localOpMetadata) => {
1361
1636
  // if params is undefined, the interval was deleted during
@@ -1370,7 +1645,7 @@ export function makeOpsMap<T extends ISerializableInterval>(): Map<
1370
1645
  },
1371
1646
  ],
1372
1647
  [
1373
- "delete",
1648
+ IntervalOpType.DELETE,
1374
1649
  {
1375
1650
  process: (collection, params, local, op) => {
1376
1651
  assert(op !== undefined, 0x3fc /* op should exist here */);
@@ -1383,7 +1658,7 @@ export function makeOpsMap<T extends ISerializableInterval>(): Map<
1383
1658
  },
1384
1659
  ],
1385
1660
  [
1386
- "change",
1661
+ IntervalOpType.CHANGE,
1387
1662
  {
1388
1663
  process: (collection, params, local, op, localOpMetadata) => {
1389
1664
  // if params is undefined, the interval was deleted during
@@ -1402,7 +1677,7 @@ export function makeOpsMap<T extends ISerializableInterval>(): Map<
1402
1677
 
1403
1678
  export type DeserializeCallback = (properties: PropertySet) => void;
1404
1679
 
1405
- export class IntervalCollectionIterator<TInterval extends ISerializableInterval>
1680
+ class IntervalCollectionIterator<TInterval extends ISerializableInterval>
1406
1681
  implements Iterator<TInterval>
1407
1682
  {
1408
1683
  private readonly results: TInterval[];
@@ -1450,6 +1725,7 @@ export interface IIntervalCollectionEvent<TInterval extends ISerializableInterva
1450
1725
  * endpoints. These references should be used for position information only.
1451
1726
  * `local` reflects whether the change originated locally.
1452
1727
  * `op` is defined if and only if the server has acked this change.
1728
+ * `slide` is true if the change is due to sliding on removal of position
1453
1729
  */
1454
1730
  (
1455
1731
  event: "changeInterval",
@@ -1458,6 +1734,7 @@ export interface IIntervalCollectionEvent<TInterval extends ISerializableInterva
1458
1734
  previousInterval: TInterval,
1459
1735
  local: boolean,
1460
1736
  op: ISequencedDocumentMessage | undefined,
1737
+ slide: boolean,
1461
1738
  ) => void,
1462
1739
  );
1463
1740
  /**
@@ -1495,14 +1772,137 @@ export interface IIntervalCollectionEvent<TInterval extends ISerializableInterva
1495
1772
 
1496
1773
  /**
1497
1774
  * Collection of intervals that supports addition, modification, removal, and efficient spatial querying.
1498
- * This class is not a DDS in its own right, but emits events on mutating operations such that it's possible to
1499
- * integrate into a DDS.
1500
- * This aligns with its usage in `SharedSegmentSequence`, which allows associating intervals to positions in the
1501
- * sequence DDS which are broadcast to all other clients in an eventually consistent fashion.
1775
+ * Changes to this collection will be incur updates on collaborating clients (i.e. they are not local-only).
1502
1776
  */
1503
- export class IntervalCollection<TInterval extends ISerializableInterval> extends TypedEventEmitter<
1504
- IIntervalCollectionEvent<TInterval>
1505
- > {
1777
+ export interface IIntervalCollection<TInterval extends ISerializableInterval>
1778
+ extends TypedEventEmitter<IIntervalCollectionEvent<TInterval>> {
1779
+ readonly attached: boolean;
1780
+ /**
1781
+ * Attaches an index to this collection.
1782
+ * All intervals which are part of this collection will be added to the index, and the index will automatically
1783
+ * be updated when this collection updates due to local or remote changes.
1784
+ *
1785
+ * @remarks - After attaching an index to an interval collection, applications should typically store this
1786
+ * index somewhere in their in-memory data model for future reference and querying.
1787
+ */
1788
+ attachIndex(index: IntervalIndex<TInterval>): void;
1789
+ /**
1790
+ * Detaches an index from this collection.
1791
+ * All intervals which are part of this collection will be removed from the index, and updates to this collection
1792
+ * due to local or remote changes will no longer incur updates to the index.
1793
+ *
1794
+ * @returns - Return false if the target index cannot be found in the indexes, otherwise remove all intervals in the index and return true
1795
+ */
1796
+ detachIndex(index: IntervalIndex<TInterval>): boolean;
1797
+ /**
1798
+ * @returns the interval in this collection that has the provided `id`.
1799
+ * If no interval in the collection has this `id`, returns `undefined`.
1800
+ */
1801
+ getIntervalById(id: string): TInterval | undefined;
1802
+ /**
1803
+ * Creates a new interval and add it to the collection.
1804
+ * @param start - interval start position (inclusive)
1805
+ * @param end - interval end position (exclusive)
1806
+ * @param intervalType - type of the interval. All intervals are SlideOnRemove. Intervals may not be Transient.
1807
+ * @param props - properties of the interval
1808
+ * @param stickiness - {@link (IntervalStickiness:type)} to apply to the added interval.
1809
+ * @returns - the created interval
1810
+ * @remarks - See documentation on {@link SequenceInterval} for comments on interval endpoint semantics: there are subtleties
1811
+ * with how the current half-open behavior is represented.
1812
+ */
1813
+ add(
1814
+ start: number,
1815
+ end: number,
1816
+ intervalType: IntervalType,
1817
+ props?: PropertySet,
1818
+ stickiness?: IntervalStickiness,
1819
+ ): TInterval;
1820
+ /**
1821
+ * Removes an interval from the collection.
1822
+ * @param id - Id of the interval to remove
1823
+ * @returns the removed interval
1824
+ */
1825
+ removeIntervalById(id: string): TInterval | undefined;
1826
+ /**
1827
+ * Changes the properties on an existing interval.
1828
+ * @param id - Id of the interval whose properties should be changed
1829
+ * @param props - Property set to apply to the interval. Shallow merging is used between any existing properties
1830
+ * and `prop`, i.e. the interval will end up with a property object equivalent to `{ ...oldProps, ...props }`.
1831
+ */
1832
+ changeProperties(id: string, props: PropertySet);
1833
+ /**
1834
+ * Changes the endpoints of an existing interval.
1835
+ * @param id - Id of the interval to change
1836
+ * @param start - New start value, if defined. `undefined` signifies this endpoint should be left unchanged.
1837
+ * @param end - New end value, if defined. `undefined` signifies this endpoint should be left unchanged.
1838
+ * @returns the interval that was changed, if it existed in the collection.
1839
+ */
1840
+ change(id: string, start?: number, end?: number): TInterval | undefined;
1841
+
1842
+ attachDeserializer(onDeserialize: DeserializeCallback): void;
1843
+ /**
1844
+ * @returns an iterator over all intervals in this collection.
1845
+ */
1846
+ [Symbol.iterator](): Iterator<TInterval>;
1847
+
1848
+ /**
1849
+ * @returns a forward iterator over all intervals in this collection with start point equal to `startPosition`.
1850
+ */
1851
+ CreateForwardIteratorWithStartPosition(startPosition: number): Iterator<TInterval>;
1852
+
1853
+ /**
1854
+ * @returns a backward iterator over all intervals in this collection with start point equal to `startPosition`.
1855
+ */
1856
+ CreateBackwardIteratorWithStartPosition(startPosition: number): Iterator<TInterval>;
1857
+
1858
+ /**
1859
+ * @returns a forward iterator over all intervals in this collection with end point equal to `endPosition`.
1860
+ */
1861
+ CreateForwardIteratorWithEndPosition(endPosition: number): Iterator<TInterval>;
1862
+
1863
+ /**
1864
+ * @returns a backward iterator over all intervals in this collection with end point equal to `endPosition`.
1865
+ */
1866
+ CreateBackwardIteratorWithEndPosition(endPosition: number): Iterator<TInterval>;
1867
+
1868
+ /**
1869
+ * Gathers iteration results that optionally match a start/end criteria into the provided array.
1870
+ * @param results - Array to gather the results into. In lieu of a return value, this array will be populated with
1871
+ * intervals matching the query upon edit.
1872
+ * @param iteratesForward - whether or not iteration should be in the forward direction
1873
+ * @param start - If provided, only match intervals whose start point is equal to `start`.
1874
+ * @param end - If provided, only match intervals whose end point is equal to `end`.
1875
+ */
1876
+ gatherIterationResults(
1877
+ results: TInterval[],
1878
+ iteratesForward: boolean,
1879
+ start?: number,
1880
+ end?: number,
1881
+ ): void;
1882
+
1883
+ /**
1884
+ * @returns an array of all intervals in this collection that overlap with the interval
1885
+ * `[startPosition, endPosition]`.
1886
+ */
1887
+ findOverlappingIntervals(startPosition: number, endPosition: number): TInterval[];
1888
+
1889
+ /**
1890
+ * Applies a function to each interval in this collection.
1891
+ */
1892
+ map(fn: (interval: TInterval) => void): void;
1893
+
1894
+ previousInterval(pos: number): TInterval | undefined;
1895
+
1896
+ nextInterval(pos: number): TInterval | undefined;
1897
+ }
1898
+
1899
+ /**
1900
+ * {@inheritdoc IIntervalCollection}
1901
+ */
1902
+ export class IntervalCollection<TInterval extends ISerializableInterval>
1903
+ extends TypedEventEmitter<IIntervalCollectionEvent<TInterval>>
1904
+ implements IIntervalCollection<TInterval>
1905
+ {
1506
1906
  private savedSerializedIntervals?: ISerializedInterval[];
1507
1907
  private localCollection: LocalIntervalCollection<TInterval> | undefined;
1508
1908
  private onDeserialize: DeserializeCallback | undefined;
@@ -1534,6 +1934,7 @@ export class IntervalCollection<TInterval extends ISerializableInterval> extends
1534
1934
  private readonly requiresClient: boolean,
1535
1935
  private readonly emitter: IValueOpEmitter,
1536
1936
  serializedIntervals: ISerializedInterval[] | ISerializedIntervalCollectionV2,
1937
+ private readonly options: Partial<SequenceOptions> = {},
1537
1938
  ) {
1538
1939
  super();
1539
1940
 
@@ -1544,6 +1945,40 @@ export class IntervalCollection<TInterval extends ISerializableInterval> extends
1544
1945
  );
1545
1946
  }
1546
1947
 
1948
+ /**
1949
+ * {@inheritdoc IIntervalCollection.attachIndex}
1950
+ */
1951
+ public attachIndex(index: IntervalIndex<TInterval>): void {
1952
+ if (!this.attached) {
1953
+ throw new LoggingError("The local interval collection must exist");
1954
+ }
1955
+ for (const interval of this) {
1956
+ index.add(interval);
1957
+ }
1958
+
1959
+ this.localCollection?.appendIndex(index);
1960
+ }
1961
+
1962
+ /**
1963
+ * {@inheritdoc IIntervalCollection.detachIndex}
1964
+ */
1965
+ public detachIndex(index: IntervalIndex<TInterval>): boolean {
1966
+ if (!this.attached) {
1967
+ throw new LoggingError("The local interval collection must exist");
1968
+ }
1969
+
1970
+ // Avoid removing intervals if the index does not exist
1971
+ if (!this.localCollection?.removeIndex(index)) {
1972
+ return false;
1973
+ }
1974
+
1975
+ for (const interval of this) {
1976
+ index.remove(interval);
1977
+ }
1978
+
1979
+ return true;
1980
+ }
1981
+
1547
1982
  private rebasePositionWithSegmentSlide(
1548
1983
  pos: number,
1549
1984
  seqNumberFrom: number,
@@ -1565,7 +2000,7 @@ export class IntervalCollection<TInterval extends ISerializableInterval> extends
1565
2000
  // if segment is undefined, it slid off the string
1566
2001
  assert(segment !== undefined, 0x54e /* No segment found */);
1567
2002
 
1568
- const segoff = this.client.getSlideToSegment({ segment, offset }) ?? segment;
2003
+ const segoff = getSlideToSegoff({ segment, offset }) ?? segment;
1569
2004
 
1570
2005
  // case happens when rebasing op, but concurrently entire string has been deleted
1571
2006
  if (segoff.segment === undefined || segoff.offset === undefined) {
@@ -1629,12 +2064,12 @@ export class IntervalCollection<TInterval extends ISerializableInterval> extends
1629
2064
  client,
1630
2065
  label,
1631
2066
  this.helpers,
1632
- (interval, previousInterval) => this.emitChange(interval, previousInterval, true),
2067
+ (interval, previousInterval) => this.emitChange(interval, previousInterval, true, true),
1633
2068
  );
1634
2069
  if (this.savedSerializedIntervals) {
1635
2070
  for (const serializedInterval of this.savedSerializedIntervals) {
1636
2071
  this.localCollection.ensureSerializedId(serializedInterval);
1637
- const { start, end, intervalType, properties } = serializedInterval;
2072
+ const { start, end, intervalType, properties, stickiness } = serializedInterval;
1638
2073
  const interval = this.helpers.create(
1639
2074
  label,
1640
2075
  start,
@@ -1643,6 +2078,7 @@ export class IntervalCollection<TInterval extends ISerializableInterval> extends
1643
2078
  intervalType,
1644
2079
  undefined,
1645
2080
  true,
2081
+ stickiness,
1646
2082
  );
1647
2083
  if (properties) {
1648
2084
  interval.addProperties(properties);
@@ -1668,6 +2104,7 @@ export class IntervalCollection<TInterval extends ISerializableInterval> extends
1668
2104
  interval: TInterval,
1669
2105
  previousInterval: TInterval,
1670
2106
  local: boolean,
2107
+ slide: boolean,
1671
2108
  op?: ISequencedDocumentMessage,
1672
2109
  ): void {
1673
2110
  // Temporarily make references transient so that positional queries work (non-transient refs
@@ -1680,17 +2117,16 @@ export class IntervalCollection<TInterval extends ISerializableInterval> extends
1680
2117
  endRefType = previousInterval.end.refType;
1681
2118
  previousInterval.start.refType = ReferenceType.Transient;
1682
2119
  previousInterval.end.refType = ReferenceType.Transient;
1683
- this.emit("changeInterval", interval, previousInterval, local, op);
2120
+ this.emit("changeInterval", interval, previousInterval, local, op, slide);
1684
2121
  previousInterval.start.refType = startRefType;
1685
2122
  previousInterval.end.refType = endRefType;
1686
2123
  } else {
1687
- this.emit("changeInterval", interval, previousInterval, local, op);
2124
+ this.emit("changeInterval", interval, previousInterval, local, op, slide);
1688
2125
  }
1689
2126
  }
1690
2127
 
1691
2128
  /**
1692
- * @returns the interval in this collection that has the provided `id`.
1693
- * If no interval in the collection has this `id`, returns `undefined`.
2129
+ * {@inheritdoc IIntervalCollection.getIntervalById}
1694
2130
  */
1695
2131
  public getIntervalById(id: string) {
1696
2132
  if (!this.localCollection) {
@@ -1700,20 +2136,14 @@ export class IntervalCollection<TInterval extends ISerializableInterval> extends
1700
2136
  }
1701
2137
 
1702
2138
  /**
1703
- * Creates a new interval and add it to the collection.
1704
- * @param start - interval start position (inclusive)
1705
- * @param end - interval end position (exclusive)
1706
- * @param intervalType - type of the interval. All intervals are SlideOnRemove. Intervals may not be Transient.
1707
- * @param props - properties of the interval
1708
- * @returns - the created interval
1709
- * @remarks - See documentation on {@link SequenceInterval} for comments on interval endpoint semantics: there are subtleties
1710
- * with how the current half-open behavior is represented.
2139
+ * {@inheritdoc IIntervalCollection.add}
1711
2140
  */
1712
2141
  public add(
1713
2142
  start: number,
1714
2143
  end: number,
1715
2144
  intervalType: IntervalType,
1716
2145
  props?: PropertySet,
2146
+ stickiness: IntervalStickiness = IntervalStickiness.END,
1717
2147
  ): TInterval {
1718
2148
  if (!this.localCollection) {
1719
2149
  throw new LoggingError("attach must be called prior to adding intervals");
@@ -1721,12 +2151,19 @@ export class IntervalCollection<TInterval extends ISerializableInterval> extends
1721
2151
  if (intervalType & IntervalType.Transient) {
1722
2152
  throw new LoggingError("Can not add transient intervals");
1723
2153
  }
2154
+ if (stickiness !== IntervalStickiness.END && !this.options.intervalStickinessEnabled) {
2155
+ throw new UsageError(
2156
+ "attempted to set interval stickiness without enabling `intervalStickinessEnabled` feature flag",
2157
+ );
2158
+ }
1724
2159
 
1725
2160
  const interval: TInterval = this.localCollection.addInterval(
1726
2161
  start,
1727
2162
  end,
1728
2163
  intervalType,
1729
2164
  props,
2165
+ undefined,
2166
+ stickiness,
1730
2167
  );
1731
2168
 
1732
2169
  if (interval) {
@@ -1736,6 +2173,7 @@ export class IntervalCollection<TInterval extends ISerializableInterval> extends
1736
2173
  properties: interval.properties,
1737
2174
  sequenceNumber: this.client?.getCurrentSeq() ?? 0,
1738
2175
  start,
2176
+ stickiness,
1739
2177
  };
1740
2178
  const localSeq = this.getNextLocalSeq();
1741
2179
  this.localSeqToSerializedInterval.set(localSeq, serializedInterval);
@@ -1776,9 +2214,7 @@ export class IntervalCollection<TInterval extends ISerializableInterval> extends
1776
2214
  }
1777
2215
 
1778
2216
  /**
1779
- * Removes an interval from the collection.
1780
- * @param id - Id of the interval to remove
1781
- * @returns the removed interval
2217
+ * {@inheritdoc IIntervalCollection.removeIntervalById}
1782
2218
  */
1783
2219
  public removeIntervalById(id: string) {
1784
2220
  if (!this.localCollection) {
@@ -1792,10 +2228,7 @@ export class IntervalCollection<TInterval extends ISerializableInterval> extends
1792
2228
  }
1793
2229
 
1794
2230
  /**
1795
- * Changes the properties on an existing interval.
1796
- * @param id - Id of the interval whose properties should be changed
1797
- * @param props - Property set to apply to the interval. Shallow merging is used between any existing properties
1798
- * and `prop`, i.e. the interval will end up with a property object equivalent to `{ ...oldProps, ...props }`.
2231
+ * {@inheritdoc IIntervalCollection.changeProperties}
1799
2232
  */
1800
2233
  public changeProperties(id: string, props: PropertySet) {
1801
2234
  if (!this.attached) {
@@ -1807,6 +2240,13 @@ export class IntervalCollection<TInterval extends ISerializableInterval> extends
1807
2240
  if (!props) {
1808
2241
  throw new LoggingError("changeProperties should be called with a property set");
1809
2242
  }
2243
+ // prevent the overwriting of an interval label, it should remain unchanged
2244
+ // once it has been inserted into the collection.
2245
+ if (props[reservedRangeLabelsKey] !== undefined) {
2246
+ throw new LoggingError(
2247
+ "The label property should not be modified once inserted to the collection",
2248
+ );
2249
+ }
1810
2250
 
1811
2251
  const interval = this.getIntervalById(id);
1812
2252
  if (interval) {
@@ -1829,11 +2269,7 @@ export class IntervalCollection<TInterval extends ISerializableInterval> extends
1829
2269
  }
1830
2270
 
1831
2271
  /**
1832
- * Changes the endpoints of an existing interval.
1833
- * @param id - Id of the interval to change
1834
- * @param start - New start value, if defined. `undefined` signifies this endpoint should be left unchanged.
1835
- * @param end - New end value, if defined. `undefined` signifies this endpoint should be left unchanged.
1836
- * @returns the interval that was changed, if it existed in the collection.
2272
+ * {@inheritdoc IIntervalCollection.change}
1837
2273
  */
1838
2274
  public change(id: string, start?: number, end?: number): TInterval | undefined {
1839
2275
  if (!this.localCollection) {
@@ -1862,7 +2298,7 @@ export class IntervalCollection<TInterval extends ISerializableInterval> extends
1862
2298
  this.localSeqToSerializedInterval.set(localSeq, serializedInterval);
1863
2299
  this.emitter.emit("change", undefined, serializedInterval, { localSeq });
1864
2300
  this.addPendingChange(id, serializedInterval);
1865
- this.emitChange(newInterval, interval, true);
2301
+ this.emitChange(newInterval, interval, true, false);
1866
2302
  return newInterval;
1867
2303
  }
1868
2304
  // No interval to change
@@ -1998,7 +2434,7 @@ export class IntervalCollection<TInterval extends ISerializableInterval> extends
1998
2434
  }
1999
2435
 
2000
2436
  if (newInterval !== interval) {
2001
- this.emitChange(newInterval, interval, local, op);
2437
+ this.emitChange(newInterval, interval, local, false, op);
2002
2438
  }
2003
2439
 
2004
2440
  const changedProperties = Object.keys(newProps).length > 0;
@@ -2021,6 +2457,9 @@ export class IntervalCollection<TInterval extends ISerializableInterval> extends
2021
2457
  }
2022
2458
  }
2023
2459
 
2460
+ /**
2461
+ * {@inheritdoc IIntervalCollection.attachDeserializer}
2462
+ */
2024
2463
  public attachDeserializer(onDeserialize: DeserializeCallback): void {
2025
2464
  // If no deserializer is specified can skip all processing work
2026
2465
  if (!onDeserialize) {
@@ -2111,7 +2550,9 @@ export class IntervalCollection<TInterval extends ISerializableInterval> extends
2111
2550
  return rebased;
2112
2551
  }
2113
2552
 
2114
- private getSlideToSegment(lref: LocalReferencePosition) {
2553
+ private getSlideToSegment(
2554
+ lref: LocalReferencePosition,
2555
+ ): { segment: ISegment | undefined; offset: number | undefined } | undefined {
2115
2556
  if (!this.client) {
2116
2557
  throw new LoggingError("client does not exist");
2117
2558
  }
@@ -2119,7 +2560,7 @@ export class IntervalCollection<TInterval extends ISerializableInterval> extends
2119
2560
  if (segoff.segment?.localRefs?.has(lref) !== true) {
2120
2561
  return undefined;
2121
2562
  }
2122
- const newSegoff = this.client.getSlideToSegment(segoff);
2563
+ const newSegoff = getSlideToSegoff(segoff);
2123
2564
  const value: { segment: ISegment | undefined; offset: number | undefined } | undefined =
2124
2565
  segoff.segment === newSegoff.segment && segoff.offset === newSegoff.offset
2125
2566
  ? undefined
@@ -2188,6 +2629,7 @@ export class IntervalCollection<TInterval extends ISerializableInterval> extends
2188
2629
  newStart,
2189
2630
  interval.start.refType,
2190
2631
  op,
2632
+ startReferenceSlidingPreference(interval.stickiness),
2191
2633
  );
2192
2634
  if (props) {
2193
2635
  interval.start.addProperties(props);
@@ -2205,6 +2647,7 @@ export class IntervalCollection<TInterval extends ISerializableInterval> extends
2205
2647
  newEnd,
2206
2648
  interval.end.refType,
2207
2649
  op,
2650
+ endReferenceSlidingPreference(interval.stickiness),
2208
2651
  );
2209
2652
  if (props) {
2210
2653
  interval.end.addProperties(props);
@@ -2216,7 +2659,7 @@ export class IntervalCollection<TInterval extends ISerializableInterval> extends
2216
2659
  oldSeg?.localRefs?.addLocalRef(oldInterval.end, oldInterval.end.getOffset());
2217
2660
  }
2218
2661
  this.localCollection.add(interval);
2219
- this.emitChange(interval, oldInterval as TInterval, true, op);
2662
+ this.emitChange(interval, oldInterval as TInterval, true, true, op);
2220
2663
  }
2221
2664
  }
2222
2665
 
@@ -2253,6 +2696,7 @@ export class IntervalCollection<TInterval extends ISerializableInterval> extends
2253
2696
  serializedInterval.intervalType,
2254
2697
  serializedInterval.properties,
2255
2698
  op,
2699
+ serializedInterval.stickiness,
2256
2700
  );
2257
2701
 
2258
2702
  if (interval) {
@@ -2310,7 +2754,7 @@ export class IntervalCollection<TInterval extends ISerializableInterval> extends
2310
2754
  }
2311
2755
 
2312
2756
  /**
2313
- * @returns a forward iterator over all intervals in this collection with start point equal to `startPosition`.
2757
+ * {@inheritdoc IIntervalCollection.CreateForwardIteratorWithStartPosition}
2314
2758
  */
2315
2759
  public CreateForwardIteratorWithStartPosition(
2316
2760
  startPosition: number,
@@ -2320,7 +2764,7 @@ export class IntervalCollection<TInterval extends ISerializableInterval> extends
2320
2764
  }
2321
2765
 
2322
2766
  /**
2323
- * @returns a backward iterator over all intervals in this collection with start point equal to `startPosition`.
2767
+ * {@inheritdoc IIntervalCollection.CreateBackwardIteratorWithStartPosition}
2324
2768
  */
2325
2769
  public CreateBackwardIteratorWithStartPosition(
2326
2770
  startPosition: number,
@@ -2330,7 +2774,7 @@ export class IntervalCollection<TInterval extends ISerializableInterval> extends
2330
2774
  }
2331
2775
 
2332
2776
  /**
2333
- * @returns a forward iterator over all intervals in this collection with end point equal to `endPosition`.
2777
+ * {@inheritdoc IIntervalCollection.CreateForwardIteratorWithEndPosition}
2334
2778
  */
2335
2779
  public CreateForwardIteratorWithEndPosition(
2336
2780
  endPosition: number,
@@ -2345,7 +2789,7 @@ export class IntervalCollection<TInterval extends ISerializableInterval> extends
2345
2789
  }
2346
2790
 
2347
2791
  /**
2348
- * @returns a backward iterator over all intervals in this collection with end point equal to `endPosition`.
2792
+ * {@inheritdoc IIntervalCollection.CreateBackwardIteratorWithEndPosition}
2349
2793
  */
2350
2794
  public CreateBackwardIteratorWithEndPosition(
2351
2795
  endPosition: number,
@@ -2360,12 +2804,7 @@ export class IntervalCollection<TInterval extends ISerializableInterval> extends
2360
2804
  }
2361
2805
 
2362
2806
  /**
2363
- * Gathers iteration results that optionally match a start/end criteria into the provided array.
2364
- * @param results - Array to gather the results into. In lieu of a return value, this array will be populated with
2365
- * intervals matching the query upon edit.
2366
- * @param iteratesForward - whether or not iteration should be in the forward direction
2367
- * @param start - If provided, only match intervals whose start point is equal to `start`.
2368
- * @param end - If provided, only match intervals whose end point is equal to `end`.
2807
+ * {@inheritdoc IIntervalCollection.gatherIterationResults}
2369
2808
  */
2370
2809
  public gatherIterationResults(
2371
2810
  results: TInterval[],
@@ -2386,8 +2825,7 @@ export class IntervalCollection<TInterval extends ISerializableInterval> extends
2386
2825
  }
2387
2826
 
2388
2827
  /**
2389
- * @returns an array of all intervals in this collection that overlap with the interval
2390
- * `[startPosition, endPosition]`.
2828
+ * {@inheritdoc IIntervalCollection.findOverlappingIntervals}
2391
2829
  */
2392
2830
  public findOverlappingIntervals(startPosition: number, endPosition: number): TInterval[] {
2393
2831
  if (!this.localCollection) {
@@ -2401,7 +2839,7 @@ export class IntervalCollection<TInterval extends ISerializableInterval> extends
2401
2839
  }
2402
2840
 
2403
2841
  /**
2404
- * Applies a function to each interval in this collection.
2842
+ * {@inheritdoc IIntervalCollection.map}
2405
2843
  */
2406
2844
  public map(fn: (interval: TInterval) => void) {
2407
2845
  if (!this.localCollection) {
@@ -2413,6 +2851,9 @@ export class IntervalCollection<TInterval extends ISerializableInterval> extends
2413
2851
  }
2414
2852
  }
2415
2853
 
2854
+ /**
2855
+ * {@inheritdoc IIntervalCollection.previousInterval}
2856
+ */
2416
2857
  public previousInterval(pos: number): TInterval | undefined {
2417
2858
  if (!this.localCollection) {
2418
2859
  throw new LoggingError("attachSequence must be called");
@@ -2421,6 +2862,9 @@ export class IntervalCollection<TInterval extends ISerializableInterval> extends
2421
2862
  return this.localCollection.endIntervalIndex.previousInterval(pos);
2422
2863
  }
2423
2864
 
2865
+ /**
2866
+ * {@inheritdoc IIntervalCollection.nextInterval}
2867
+ */
2424
2868
  public nextInterval(pos: number): TInterval | undefined {
2425
2869
  if (!this.localCollection) {
2426
2870
  throw new LoggingError("attachSequence must be called");