@fluidframework/sequence 1.0.1 → 1.1.0-75972

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 (67) hide show
  1. package/README.md +18 -6
  2. package/dist/defaultMap.d.ts +2 -6
  3. package/dist/defaultMap.d.ts.map +1 -1
  4. package/dist/defaultMap.js +27 -37
  5. package/dist/defaultMap.js.map +1 -1
  6. package/dist/defaultMapInterfaces.d.ts +24 -3
  7. package/dist/defaultMapInterfaces.d.ts.map +1 -1
  8. package/dist/defaultMapInterfaces.js.map +1 -1
  9. package/dist/index.d.ts +2 -2
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/index.js.map +1 -1
  12. package/dist/intervalCollection.d.ts +72 -8
  13. package/dist/intervalCollection.d.ts.map +1 -1
  14. package/dist/intervalCollection.js +325 -155
  15. package/dist/intervalCollection.js.map +1 -1
  16. package/dist/packageVersion.d.ts +1 -1
  17. package/dist/packageVersion.d.ts.map +1 -1
  18. package/dist/packageVersion.js +1 -1
  19. package/dist/packageVersion.js.map +1 -1
  20. package/dist/sequence.d.ts +4 -5
  21. package/dist/sequence.d.ts.map +1 -1
  22. package/dist/sequence.js +11 -15
  23. package/dist/sequence.js.map +1 -1
  24. package/dist/sharedIntervalCollection.d.ts.map +1 -1
  25. package/dist/sharedIntervalCollection.js +1 -1
  26. package/dist/sharedIntervalCollection.js.map +1 -1
  27. package/dist/sharedSequence.js.map +1 -1
  28. package/dist/sparsematrix.js +2 -2
  29. package/dist/sparsematrix.js.map +1 -1
  30. package/lib/defaultMap.d.ts +2 -6
  31. package/lib/defaultMap.d.ts.map +1 -1
  32. package/lib/defaultMap.js +27 -37
  33. package/lib/defaultMap.js.map +1 -1
  34. package/lib/defaultMapInterfaces.d.ts +24 -3
  35. package/lib/defaultMapInterfaces.d.ts.map +1 -1
  36. package/lib/defaultMapInterfaces.js.map +1 -1
  37. package/lib/index.d.ts +2 -2
  38. package/lib/index.d.ts.map +1 -1
  39. package/lib/index.js.map +1 -1
  40. package/lib/intervalCollection.d.ts +72 -8
  41. package/lib/intervalCollection.d.ts.map +1 -1
  42. package/lib/intervalCollection.js +325 -155
  43. package/lib/intervalCollection.js.map +1 -1
  44. package/lib/packageVersion.d.ts +1 -1
  45. package/lib/packageVersion.d.ts.map +1 -1
  46. package/lib/packageVersion.js +1 -1
  47. package/lib/packageVersion.js.map +1 -1
  48. package/lib/sequence.d.ts +4 -5
  49. package/lib/sequence.d.ts.map +1 -1
  50. package/lib/sequence.js +11 -15
  51. package/lib/sequence.js.map +1 -1
  52. package/lib/sharedIntervalCollection.d.ts.map +1 -1
  53. package/lib/sharedIntervalCollection.js +1 -1
  54. package/lib/sharedIntervalCollection.js.map +1 -1
  55. package/lib/sharedSequence.js.map +1 -1
  56. package/lib/sparsematrix.js +2 -2
  57. package/lib/sparsematrix.js.map +1 -1
  58. package/package.json +19 -43
  59. package/src/defaultMap.ts +39 -41
  60. package/src/defaultMapInterfaces.ts +28 -3
  61. package/src/index.ts +3 -0
  62. package/src/intervalCollection.ts +447 -181
  63. package/src/packageVersion.ts +1 -1
  64. package/src/sequence.ts +17 -21
  65. package/src/sharedIntervalCollection.ts +3 -2
  66. package/src/sharedSequence.ts +1 -1
  67. package/src/sparsematrix.ts +2 -2
@@ -30,8 +30,16 @@ import {
30
30
  UnassignedSequenceNumber,
31
31
  } from "@fluidframework/merge-tree";
32
32
  import { ISequencedDocumentMessage } from "@fluidframework/protocol-definitions";
33
+ import { LoggingError } from "@fluidframework/telemetry-utils";
33
34
  import { v4 as uuid } from "uuid";
34
- import { IValueFactory, IValueOpEmitter, IValueOperation, IValueType } from "./defaultMapInterfaces";
35
+ import {
36
+ IMapMessageLocalMetadata,
37
+ IValueFactory,
38
+ IValueOpEmitter,
39
+ IValueOperation,
40
+ IValueType,
41
+ IValueTypeOperationValue,
42
+ } from "./defaultMapInterfaces";
35
43
 
36
44
  const reservedIntervalIdKey = "intervalId";
37
45
 
@@ -60,6 +68,55 @@ export interface ISerializedInterval {
60
68
  properties?: PropertySet;
61
69
  }
62
70
 
71
+ /**
72
+ * A size optimization to avoid redundantly storing keys when serializing intervals
73
+ * as JSON. Intervals are of the format:
74
+ *
75
+ * [start, end, sequenceNumber, intervalType, properties]
76
+ */
77
+ export type CompressedSerializedInterval = [number, number, number, IntervalType, PropertySet];
78
+
79
+ /**
80
+ * @internal
81
+ */
82
+ export interface ISerializedIntervalCollectionV2 {
83
+ label: string;
84
+ version: 2;
85
+ intervals: CompressedSerializedInterval[];
86
+ }
87
+
88
+ /**
89
+ * Decompress an interval after loading a summary from JSON. The exact format
90
+ * of this compression is unspecified and subject to change
91
+ */
92
+ function decompressInterval(interval: CompressedSerializedInterval, label?: string): ISerializedInterval {
93
+ return {
94
+ start: interval[0],
95
+ end: interval[1],
96
+ sequenceNumber: interval[2],
97
+ intervalType: interval[3],
98
+ properties: { ...interval[4], [reservedRangeLabelsKey]: label },
99
+ };
100
+ }
101
+
102
+ /**
103
+ * Compress an interval prior to serialization as JSON. The exact format of this
104
+ * compression is unspecified and subject to change
105
+ */
106
+ function compressInterval(interval: ISerializedInterval): CompressedSerializedInterval {
107
+ const { start, end, sequenceNumber, intervalType, properties } = interval;
108
+
109
+ return [
110
+ start,
111
+ end,
112
+ sequenceNumber,
113
+ intervalType,
114
+ // remove the `referenceRangeLabels` property as it is already stored
115
+ // in the `label` field of the summary
116
+ { ...properties, [reservedRangeLabelsKey]: undefined },
117
+ ];
118
+ }
119
+
63
120
  export interface ISerializableInterval extends IInterval {
64
121
  properties: PropertySet;
65
122
  propertyManager: PropertiesManager;
@@ -82,7 +139,11 @@ export class Interval implements ISerializableInterval {
82
139
  constructor(
83
140
  public start: number,
84
141
  public end: number,
85
- props?: PropertySet) {
142
+ props?: PropertySet,
143
+ ) {
144
+ this.propertyManager = new PropertiesManager();
145
+ this.properties = {};
146
+
86
147
  if (props) {
87
148
  this.addProperties(props);
88
149
  }
@@ -107,7 +168,7 @@ export class Interval implements ISerializableInterval {
107
168
  this.auxProps.push(props);
108
169
  }
109
170
 
110
- public serialize(client: Client) {
171
+ public serialize(client: Client): ISerializedInterval {
111
172
  let seq = 0;
112
173
  if (client) {
113
174
  seq = client.getCurrentSeq();
@@ -181,12 +242,7 @@ export class Interval implements ISerializableInterval {
181
242
  op?: ICombiningOp,
182
243
  ): PropertySet | undefined {
183
244
  if (newProps) {
184
- if (!this.propertyManager) {
185
- this.propertyManager = new PropertiesManager();
186
- }
187
- if (!this.properties) {
188
- this.properties = createMap<any>();
189
- }
245
+ this.initializeProperties();
190
246
  return this.propertyManager.addProperties(this.properties, newProps, op, seq, collaborating);
191
247
  }
192
248
  }
@@ -198,7 +254,21 @@ export class Interval implements ISerializableInterval {
198
254
  // Return undefined to indicate that no change is necessary.
199
255
  return;
200
256
  }
201
- return new Interval(startPos, endPos, this.properties);
257
+ const newInterval = new Interval(startPos, endPos);
258
+ if (this.properties) {
259
+ newInterval.initializeProperties();
260
+ this.propertyManager.copyTo(this.properties, newInterval.properties, newInterval.propertyManager);
261
+ }
262
+ return newInterval;
263
+ }
264
+
265
+ private initializeProperties(): void {
266
+ if (!this.propertyManager) {
267
+ this.propertyManager = new PropertiesManager();
268
+ }
269
+ if (!this.properties) {
270
+ this.properties = createMap<any>();
271
+ }
202
272
  }
203
273
  }
204
274
 
@@ -210,13 +280,49 @@ export class SequenceInterval implements ISerializableInterval {
210
280
  public start: LocalReference,
211
281
  public end: LocalReference,
212
282
  public intervalType: IntervalType,
213
- props?: PropertySet) {
283
+ props?: PropertySet,
284
+ ) {
285
+ this.propertyManager = new PropertiesManager();
286
+ this.properties = {};
287
+
214
288
  if (props) {
215
289
  this.addProperties(props);
216
290
  }
217
291
  }
218
292
 
219
- public serialize(client: Client) {
293
+ private callbacks?: Record<"beforePositionChange" | "afterPositionChange", () => void>;
294
+
295
+ /**
296
+ * @internal
297
+ * Subscribes to position change events on this interval if there are no current listeners.
298
+ */
299
+ public addPositionChangeListeners(beforePositionChange: () => void, afterPositionChange: () => void): void {
300
+ if (this.callbacks === undefined) {
301
+ this.callbacks = {
302
+ beforePositionChange,
303
+ afterPositionChange,
304
+ };
305
+
306
+ const startCbs = this.start.callbacks ??= {};
307
+ const endCbs = this.end.callbacks ??= {};
308
+ startCbs.beforeSlide = endCbs.beforeSlide = beforePositionChange;
309
+ startCbs.afterSlide = endCbs.afterSlide = afterPositionChange;
310
+ }
311
+ }
312
+
313
+ /**
314
+ * @internal
315
+ * Removes the currently subscribed position change listeners.
316
+ */
317
+ public removePositionChangeListeners(): void {
318
+ if (this.callbacks) {
319
+ this.callbacks = undefined;
320
+ this.start.callbacks = undefined;
321
+ this.end.callbacks = undefined;
322
+ }
323
+ }
324
+
325
+ public serialize(client: Client): ISerializedInterval {
220
326
  const startPosition = this.start.toPosition();
221
327
  const endPosition = this.end.toPosition();
222
328
  const serializedInterval: ISerializedInterval = {
@@ -225,9 +331,11 @@ export class SequenceInterval implements ISerializableInterval {
225
331
  sequenceNumber: client.getCurrentSeq(),
226
332
  start: startPosition,
227
333
  };
334
+
228
335
  if (this.properties) {
229
336
  serializedInterval.properties = this.properties;
230
337
  }
338
+
231
339
  return serializedInterval;
232
340
  }
233
341
 
@@ -290,12 +398,7 @@ export class SequenceInterval implements ISerializableInterval {
290
398
  seq?: number,
291
399
  op?: ICombiningOp,
292
400
  ): PropertySet | undefined {
293
- if (!this.propertyManager) {
294
- this.propertyManager = new PropertiesManager();
295
- }
296
- if (!this.properties) {
297
- this.properties = createMap<any>();
298
- }
401
+ this.initializeProperties();
299
402
  return this.propertyManager.addProperties(this.properties, newProps, op, seq, collab);
300
403
  }
301
404
 
@@ -306,21 +409,46 @@ export class SequenceInterval implements ISerializableInterval {
306
409
  }
307
410
 
308
411
  public modify(label: string, start: number, end: number, op?: ISequencedDocumentMessage) {
309
- const startPos = start ?? this.start.toPosition();
310
- const endPos = end ?? this.end.toPosition();
412
+ const getRefType = (baseType: ReferenceType): ReferenceType => {
413
+ let refType = baseType;
414
+ if (op === undefined) {
415
+ refType &= ~ReferenceType.SlideOnRemove;
416
+ refType |= ReferenceType.StayOnRemove;
417
+ }
418
+ return refType;
419
+ };
311
420
 
312
- if (this.start.toPosition() === startPos && this.end.toPosition() === endPos) {
313
- // Return undefined to indicate that no change is necessary.
314
- return;
421
+ let startRef = this.start;
422
+ if (start !== undefined) {
423
+ startRef = createPositionReference(this.start.getClient(), start, getRefType(this.start.refType), op);
424
+ startRef.addProperties(this.start.properties);
425
+ }
426
+
427
+ let endRef = this.end;
428
+ if (end !== undefined) {
429
+ endRef = createPositionReference(this.end.getClient(), end, getRefType(this.end.refType), op);
430
+ endRef.addProperties(this.end.properties);
315
431
  }
316
432
 
317
- const newInterval =
318
- createSequenceInterval(label, startPos, endPos, this.start.getClient(), this.intervalType, op);
433
+ startRef.pairedRef = endRef;
434
+ endRef.pairedRef = startRef;
435
+
436
+ const newInterval = new SequenceInterval(startRef, endRef, this.intervalType);
319
437
  if (this.properties) {
320
- newInterval.addProperties(this.properties);
438
+ newInterval.initializeProperties();
439
+ this.propertyManager.copyTo(this.properties, newInterval.properties, newInterval.propertyManager);
321
440
  }
322
441
  return newInterval;
323
442
  }
443
+
444
+ private initializeProperties(): void {
445
+ if (!this.propertyManager) {
446
+ this.propertyManager = new PropertiesManager();
447
+ }
448
+ if (!this.properties) {
449
+ this.properties = createMap<any>();
450
+ }
451
+ }
324
452
  }
325
453
 
326
454
  function createPositionReferenceFromSegoff(
@@ -421,8 +549,9 @@ export function createIntervalIndex(conflict?: IntervalConflictResolver<Interval
421
549
  export class LocalIntervalCollection<TInterval extends ISerializableInterval> {
422
550
  private readonly intervalTree = new IntervalTree<TInterval>();
423
551
  private readonly endIntervalTree: RedBlackTree<TInterval, TInterval>;
424
- private conflictResolver: IntervalConflictResolver<TInterval>;
425
- private endConflictResolver: ConflictAction<TInterval, TInterval>;
552
+ private readonly intervalIdMap: Map<string, TInterval> = new Map();
553
+ private conflictResolver: IntervalConflictResolver<TInterval> | undefined;
554
+ private endConflictResolver: ConflictAction<TInterval, TInterval> | undefined;
426
555
 
427
556
  private static readonly legacyIdPrefix = "legacy";
428
557
 
@@ -430,6 +559,8 @@ export class LocalIntervalCollection<TInterval extends ISerializableInterval> {
430
559
  private readonly client: Client,
431
560
  private readonly label: string,
432
561
  private readonly helpers: IIntervalHelpers<TInterval>,
562
+ /** Callback invoked each time one of the endpoints of an interval slides. */
563
+ private readonly onPositionChange?: (interval: TInterval) => void,
433
564
  ) {
434
565
  // eslint-disable-next-line @typescript-eslint/unbound-method
435
566
  this.endIntervalTree = new RedBlackTree<TInterval, TInterval>(helpers.compareEnds);
@@ -439,7 +570,7 @@ export class LocalIntervalCollection<TInterval extends ISerializableInterval> {
439
570
  this.conflictResolver = conflictResolver;
440
571
  this.endConflictResolver =
441
572
  (key: TInterval, currentKey: TInterval) => {
442
- const ival = this.conflictResolver(key, currentKey);
573
+ const ival = conflictResolver(key, currentKey);
443
574
  return {
444
575
  data: ival,
445
576
  key: ival,
@@ -457,12 +588,21 @@ export class LocalIntervalCollection<TInterval extends ISerializableInterval> {
457
588
  return `${LocalIntervalCollection.legacyIdPrefix}${start}-${end}`;
458
589
  }
459
590
 
460
- public ensureSerializedId(serializedInterval: ISerializedInterval) {
461
- if (serializedInterval.properties?.[reservedIntervalIdKey] === undefined) {
591
+ /**
592
+ * Validates that a serialized interval has the ID property. Creates an ID
593
+ * if one does not already exist
594
+ *
595
+ * @param serializedInterval - The interval to be checked
596
+ * @returns The interval's existing or newly created id
597
+ */
598
+ public ensureSerializedId(serializedInterval: ISerializedInterval): string {
599
+ let id: string | undefined = serializedInterval.properties?.[reservedIntervalIdKey];
600
+ if (id === undefined) {
462
601
  // An interval came over the wire without an ID, so create a non-unique one based on start/end.
463
602
  // This will allow all clients to refer to this interval consistently.
603
+ id = this.createLegacyId(serializedInterval.start, serializedInterval.end);
464
604
  const newProps = {
465
- [reservedIntervalIdKey]: this.createLegacyId(serializedInterval.start, serializedInterval.end),
605
+ [reservedIntervalIdKey]: id,
466
606
  };
467
607
  serializedInterval.properties = addProperties(serializedInterval.properties, newProps);
468
608
  }
@@ -472,6 +612,8 @@ export class LocalIntervalCollection<TInterval extends ISerializableInterval> {
472
612
  enumerable: true,
473
613
  writable: false,
474
614
  });
615
+
616
+ return id;
475
617
  }
476
618
 
477
619
  public mapUntil(fn: (interval: TInterval) => boolean) {
@@ -595,9 +737,20 @@ export class LocalIntervalCollection<TInterval extends ISerializableInterval> {
595
737
  return transientInterval;
596
738
  }
597
739
 
598
- public removeExistingInterval(interval: TInterval) {
740
+ private removeIntervalFromIndex(interval: TInterval) {
599
741
  this.intervalTree.removeExisting(interval);
600
742
  this.endIntervalTree.remove(interval);
743
+
744
+ const id = interval.getIntervalId();
745
+
746
+ assert(id !== undefined, 0x311 /* expected id to exist on interval */);
747
+
748
+ this.intervalIdMap.delete(id);
749
+ }
750
+
751
+ public removeExistingInterval(interval: TInterval) {
752
+ this.removeIntervalFromIndex(interval);
753
+ this.removeIntervalListeners(interval);
601
754
  }
602
755
 
603
756
  public createInterval(
@@ -631,9 +784,9 @@ export class LocalIntervalCollection<TInterval extends ISerializableInterval> {
631
784
  return interval;
632
785
  }
633
786
 
634
- public add(interval: TInterval) {
635
- assert(Object.prototype.hasOwnProperty.call(interval.properties, reservedIntervalIdKey),
636
- 0x2c0 /* "ID must be created before adding interval to collection" */);
787
+ private addIntervalToIndex(interval: TInterval) {
788
+ const id = interval.getIntervalId();
789
+ assert(id !== undefined, 0x2c0 /* "ID must be created before adding interval to collection" */);
637
790
  // Make the ID immutable.
638
791
  Object.defineProperty(interval.properties, reservedIntervalIdKey, {
639
792
  configurable: false,
@@ -642,18 +795,16 @@ export class LocalIntervalCollection<TInterval extends ISerializableInterval> {
642
795
  });
643
796
  this.intervalTree.put(interval, this.conflictResolver);
644
797
  this.endIntervalTree.put(interval, interval, this.endConflictResolver);
798
+ this.intervalIdMap.set(id, interval);
799
+ }
800
+
801
+ public add(interval: TInterval) {
802
+ this.addIntervalToIndex(interval);
803
+ this.addIntervalListeners(interval);
645
804
  }
646
805
 
647
806
  public getIntervalById(id: string) {
648
- let result: TInterval | undefined;
649
- this.mapUntil((interval: TInterval) => {
650
- if (interval.getIntervalId() === id) {
651
- result = interval;
652
- return false;
653
- }
654
- return true;
655
- });
656
- return result;
807
+ return this.intervalIdMap.get(id);
657
808
  }
658
809
 
659
810
  public changeInterval(interval: TInterval, start: number, end: number, op?: ISequencedDocumentMessage) {
@@ -665,10 +816,33 @@ export class LocalIntervalCollection<TInterval extends ISerializableInterval> {
665
816
  return newInterval;
666
817
  }
667
818
 
668
- public serialize() {
819
+ public serialize(): ISerializedIntervalCollectionV2 {
669
820
  const client = this.client;
670
821
  const intervals = this.intervalTree.intervals.keys();
671
- return intervals.map((interval) => interval.serialize(client));
822
+
823
+ return {
824
+ label: this.label,
825
+ intervals: intervals.map((interval) => compressInterval(interval.serialize(client))),
826
+ version: 2,
827
+ };
828
+ }
829
+
830
+ private addIntervalListeners(interval: TInterval) {
831
+ if (interval instanceof SequenceInterval) {
832
+ interval.addPositionChangeListeners(
833
+ () => this.removeIntervalFromIndex(interval),
834
+ () => {
835
+ this.addIntervalToIndex(interval);
836
+ this.onPositionChange?.(interval);
837
+ },
838
+ );
839
+ }
840
+ }
841
+
842
+ private removeIntervalListeners(interval: TInterval) {
843
+ if (interval instanceof SequenceInterval) {
844
+ interval.removePositionChangeListeners();
845
+ }
672
846
  }
673
847
  }
674
848
 
@@ -678,7 +852,7 @@ class SequenceIntervalCollectionFactory
678
852
  implements IValueFactory<IntervalCollection<SequenceInterval>> {
679
853
  public load(
680
854
  emitter: IValueOpEmitter,
681
- raw: ISerializedInterval[] = [],
855
+ raw: ISerializedInterval[] | ISerializedIntervalCollectionV2 = [],
682
856
  ): IntervalCollection<SequenceInterval> {
683
857
  const helpers: IIntervalHelpers<SequenceInterval> = {
684
858
  compareEnds: compareSequenceIntervalEnds,
@@ -687,7 +861,7 @@ class SequenceIntervalCollectionFactory
687
861
  return new IntervalCollection<SequenceInterval>(helpers, true, emitter, raw);
688
862
  }
689
863
 
690
- public store(value: IntervalCollection<SequenceInterval>): ISerializedInterval[] {
864
+ public store(value: IntervalCollection<SequenceInterval>): ISerializedIntervalCollectionV2 {
691
865
  return value.serializeInternal();
692
866
  }
693
867
  }
@@ -711,49 +885,27 @@ export class SequenceIntervalCollectionValueType
711
885
  private static readonly _factory: IValueFactory<IntervalCollection<SequenceInterval>> =
712
886
  new SequenceIntervalCollectionFactory();
713
887
 
714
- private static readonly _ops: Map<string, IValueOperation<IntervalCollection<SequenceInterval>>> =
715
- new Map<string, IValueOperation<IntervalCollection<SequenceInterval>>>(
716
- [[
717
- "add",
718
- {
719
- process: (value, params, local, op) => {
720
- value.ackAdd(params, local, op);
721
- },
722
- },
723
- ],
724
- [
725
- "delete",
726
- {
727
- process: (value, params, local, op) => {
728
- value.ackDelete(params, local, op);
729
- },
730
- },
731
- ],
732
- [
733
- "change",
734
- {
735
- process: (value, params, local, op) => {
736
- value.ackChange(params, local, op);
737
- },
738
- },
739
- ]]);
888
+ private static readonly _ops = makeOpsMap<SequenceInterval>();
740
889
  }
741
890
 
742
891
  const compareIntervalEnds = (a: Interval, b: Interval) => a.end - b.end;
743
892
 
744
893
  function createInterval(label: string, start: number, end: number, client: Client): Interval {
745
- let rangeProp: PropertySet;
746
- if (label && (label.length > 0)) {
747
- rangeProp = {
748
- [reservedRangeLabelsKey]: [label],
749
- };
894
+ const rangeProp: PropertySet = {};
895
+
896
+ if (label && label.length > 0) {
897
+ rangeProp[reservedRangeLabelsKey] = [label];
750
898
  }
899
+
751
900
  return new Interval(start, end, rangeProp);
752
901
  }
753
902
 
754
903
  class IntervalCollectionFactory
755
904
  implements IValueFactory<IntervalCollection<Interval>> {
756
- public load(emitter: IValueOpEmitter, raw: ISerializedInterval[] = []): IntervalCollection<Interval> {
905
+ public load(
906
+ emitter: IValueOpEmitter,
907
+ raw: ISerializedInterval[] | ISerializedIntervalCollectionV2 = [],
908
+ ): IntervalCollection<Interval> {
757
909
  const helpers: IIntervalHelpers<Interval> = {
758
910
  compareEnds: compareIntervalEnds,
759
911
  create: createInterval,
@@ -763,7 +915,7 @@ class IntervalCollectionFactory
763
915
  return collection;
764
916
  }
765
917
 
766
- public store(value: IntervalCollection<Interval>): ISerializedInterval[] {
918
+ public store(value: IntervalCollection<Interval>): ISerializedIntervalCollectionV2 {
767
919
  return value.serializeInternal();
768
920
  }
769
921
  }
@@ -786,32 +938,52 @@ export class IntervalCollectionValueType
786
938
 
787
939
  private static readonly _factory: IValueFactory<IntervalCollection<Interval>> =
788
940
  new IntervalCollectionFactory();
789
- private static readonly _ops: Map<string, IValueOperation<IntervalCollection<Interval>>> =
790
- new Map<string, IValueOperation<IntervalCollection<Interval>>>(
791
- [[
792
- "add",
793
- {
794
- process: (value, params, local, op) => {
795
- value.ackAdd(params, local, op);
796
- },
941
+ private static readonly _ops = makeOpsMap<Interval>();
942
+ }
943
+
944
+ function makeOpsMap<T extends ISerializableInterval>(): Map<string, IValueOperation<IntervalCollection<T>>> {
945
+ const rebase = (
946
+ collection: IntervalCollection<T>,
947
+ op: IValueTypeOperationValue,
948
+ localOpMetadata: IMapMessageLocalMetadata,
949
+ ) => {
950
+ const { localSeq } = localOpMetadata;
951
+ const rebasedValue = collection.rebaseLocalInterval(op.opName, op.value, localSeq);
952
+ const rebasedOp = { ...op, value: rebasedValue };
953
+ return { rebasedOp, rebasedLocalOpMetadata: localOpMetadata };
954
+ };
955
+
956
+ return new Map<string, IValueOperation<IntervalCollection<T>>>(
957
+ [[
958
+ "add",
959
+ {
960
+ process: (collection, params, local, op) => {
961
+ collection.ackAdd(params, local, op);
797
962
  },
798
- ],
799
- [
800
- "delete",
801
- {
802
- process: (value, params, local, op) => {
803
- value.ackDelete(params, local, op);
804
- },
963
+ rebase,
964
+ },
965
+ ],
966
+ [
967
+ "delete",
968
+ {
969
+ process: (collection, params, local, op) => {
970
+ collection.ackDelete(params, local, op);
805
971
  },
806
- ],
807
- [
808
- "change",
809
- {
810
- process: (value, params, local, op) => {
811
- value.ackChange(params, local, op);
812
- },
972
+ rebase: (collection, op, localOpMetadata) => {
973
+ // Deletion of intervals is based on id, so requires no rebasing.
974
+ return { rebasedOp: op, rebasedLocalOpMetadata: localOpMetadata };
813
975
  },
814
- ]]);
976
+ },
977
+ ],
978
+ [
979
+ "change",
980
+ {
981
+ process: (collection, params, local, op) => {
982
+ collection.ackChange(params, local, op);
983
+ },
984
+ rebase,
985
+ },
986
+ ]]);
815
987
  }
816
988
 
817
989
  export type DeserializeCallback = (properties: PropertySet) => void;
@@ -848,7 +1020,18 @@ export class IntervalCollectionIterator<TInterval extends ISerializableInterval>
848
1020
  }
849
1021
 
850
1022
  export interface IIntervalCollectionEvent<TInterval extends ISerializableInterval> extends IEvent {
851
- (event: "addInterval" | "changeInterval" | "deleteInterval",
1023
+ /**
1024
+ * This event is invoked whenever the properties or endpoints of an interval may have changed.
1025
+ * This can happen on:
1026
+ * - endpoint modification (local or remote)
1027
+ * - ack of an endpoint modification
1028
+ * - property change (local or remote)
1029
+ * - position change due to segment sliding (will always appear as a local change)
1030
+ * The `interval` argument reflects the new values.
1031
+ */
1032
+ (event: "changeInterval",
1033
+ listener: (interval: TInterval, local: boolean, op: ISequencedDocumentMessage | undefined) => void);
1034
+ (event: "addInterval" | "deleteInterval",
852
1035
  listener: (interval: TInterval, local: boolean, op: ISequencedDocumentMessage) => void);
853
1036
  (event: "propertyChanged", listener: (interval: TInterval, propertyArgs: PropertySet) => void);
854
1037
  }
@@ -857,34 +1040,49 @@ export class IntervalCollection<TInterval extends ISerializableInterval>
857
1040
  extends TypedEventEmitter<IIntervalCollectionEvent<TInterval>> {
858
1041
  private savedSerializedIntervals?: ISerializedInterval[];
859
1042
  private localCollection: LocalIntervalCollection<TInterval>;
860
- private onDeserialize: DeserializeCallback;
1043
+ private onDeserialize: DeserializeCallback | undefined;
861
1044
  private client: Client;
862
- private pendingChangesStart: Map<string, ISerializedInterval[]>;
863
- private pendingChangesEnd: Map<string, ISerializedInterval[]>;
1045
+ private readonly pendingChangesStart: Map<string, ISerializedInterval[]> = new Map<string, ISerializedInterval[]>();
1046
+ private readonly pendingChangesEnd: Map<string, ISerializedInterval[]> = new Map<string, ISerializedInterval[]>();
864
1047
 
865
1048
  public get attached(): boolean {
866
1049
  return !!this.localCollection;
867
1050
  }
868
1051
 
869
- constructor(private readonly helpers: IIntervalHelpers<TInterval>, private readonly requiresClient: boolean,
1052
+ /** @internal */
1053
+ constructor(
1054
+ private readonly helpers: IIntervalHelpers<TInterval>,
1055
+ private readonly requiresClient: boolean,
870
1056
  private readonly emitter: IValueOpEmitter,
871
- serializedIntervals: ISerializedInterval[]) {
1057
+ serializedIntervals: ISerializedInterval[] | ISerializedIntervalCollectionV2,
1058
+ ) {
872
1059
  super();
873
- this.savedSerializedIntervals = serializedIntervals;
1060
+
1061
+ if (Array.isArray(serializedIntervals)) {
1062
+ this.savedSerializedIntervals = serializedIntervals;
1063
+ } else {
1064
+ this.savedSerializedIntervals =
1065
+ serializedIntervals.intervals.map((i) => decompressInterval(i, serializedIntervals.label));
1066
+ }
874
1067
  }
875
1068
 
876
1069
  public attachGraph(client: Client, label: string) {
877
1070
  if (this.attached) {
878
- throw new Error("Only supports one Sequence attach");
1071
+ throw new LoggingError("Only supports one Sequence attach");
879
1072
  }
880
1073
 
881
1074
  if ((client === undefined) && (this.requiresClient)) {
882
- throw new Error("Client required for this collection");
1075
+ throw new LoggingError("Client required for this collection");
883
1076
  }
884
1077
 
885
1078
  // Instantiate the local interval collection based on the saved intervals
886
1079
  this.client = client;
887
- this.localCollection = new LocalIntervalCollection<TInterval>(client, label, this.helpers);
1080
+ this.localCollection = new LocalIntervalCollection<TInterval>(
1081
+ client,
1082
+ label,
1083
+ this.helpers,
1084
+ (interval) => this.emit("changeInterval", interval, true, undefined),
1085
+ );
888
1086
  if (this.savedSerializedIntervals) {
889
1087
  for (const serializedInterval of this.savedSerializedIntervals) {
890
1088
  this.localCollection.ensureSerializedId(serializedInterval);
@@ -898,9 +1096,16 @@ export class IntervalCollection<TInterval extends ISerializableInterval>
898
1096
  this.savedSerializedIntervals = undefined;
899
1097
  }
900
1098
 
1099
+ /**
1100
+ * Gets the next local sequence number, modifying this client's collab window in doing so.
1101
+ */
1102
+ private getNextLocalSeq(): number {
1103
+ return ++this.client.getCollabWindow().localSeq;
1104
+ }
1105
+
901
1106
  public getIntervalById(id: string) {
902
1107
  if (!this.attached) {
903
- throw new Error("attach must be called before accessing intervals");
1108
+ throw new LoggingError("attach must be called before accessing intervals");
904
1109
  }
905
1110
  return this.localCollection.getIntervalById(id);
906
1111
  }
@@ -920,10 +1125,10 @@ export class IntervalCollection<TInterval extends ISerializableInterval>
920
1125
  props?: PropertySet,
921
1126
  ) {
922
1127
  if (!this.attached) {
923
- throw new Error("attach must be called prior to adding intervals");
1128
+ throw new LoggingError("attach must be called prior to adding intervals");
924
1129
  }
925
1130
  if (intervalType & IntervalType.Transient) {
926
- throw new Error("Can not add transient intervals");
1131
+ throw new LoggingError("Can not add transient intervals");
927
1132
  }
928
1133
 
929
1134
  const interval: TInterval = this.localCollection.addInterval(start, end, intervalType, props);
@@ -937,7 +1142,7 @@ export class IntervalCollection<TInterval extends ISerializableInterval>
937
1142
  start,
938
1143
  };
939
1144
  // Local ops get submitted to the server. Remote ops have the deserializer run.
940
- this.emitter.emit("add", undefined, serializedInterval);
1145
+ this.emitter.emit("add", undefined, serializedInterval, { localSeq: this.getNextLocalSeq() });
941
1146
  }
942
1147
 
943
1148
  this.emit("addInterval", interval, true, undefined);
@@ -945,13 +1150,19 @@ export class IntervalCollection<TInterval extends ISerializableInterval>
945
1150
  return interval;
946
1151
  }
947
1152
 
948
- private deleteExistingInterval(interval: TInterval, local: boolean, op: ISequencedDocumentMessage) {
1153
+ private deleteExistingInterval(interval: TInterval, local: boolean, op?: ISequencedDocumentMessage) {
949
1154
  // The given interval is known to exist in the collection.
950
1155
  this.localCollection.removeExistingInterval(interval);
1156
+
951
1157
  if (interval) {
952
1158
  // Local ops get submitted to the server. Remote ops have the deserializer run.
953
1159
  if (local) {
954
- this.emitter.emit("delete", undefined, interval.serialize(this.client));
1160
+ this.emitter.emit(
1161
+ "delete",
1162
+ undefined,
1163
+ interval.serialize(this.client),
1164
+ { localSeq: this.getNextLocalSeq() },
1165
+ );
955
1166
  } else {
956
1167
  if (this.onDeserialize) {
957
1168
  this.onDeserialize(interval);
@@ -972,13 +1183,13 @@ export class IntervalCollection<TInterval extends ISerializableInterval>
972
1183
 
973
1184
  public changeProperties(id: string, props: PropertySet) {
974
1185
  if (!this.attached) {
975
- throw new Error("Attach must be called before accessing intervals");
1186
+ throw new LoggingError("Attach must be called before accessing intervals");
976
1187
  }
977
1188
  if (typeof (id) !== "string") {
978
- throw new Error("Change API requires an ID that is a string");
1189
+ throw new LoggingError("Change API requires an ID that is a string");
979
1190
  }
980
1191
  if (!props) {
981
- throw new Error("changeProperties should be called with a property set");
1192
+ throw new LoggingError("changeProperties should be called with a property set");
982
1193
  }
983
1194
 
984
1195
  const interval = this.getIntervalById(id);
@@ -986,12 +1197,15 @@ export class IntervalCollection<TInterval extends ISerializableInterval>
986
1197
  // Pass Unassigned as the sequence number to indicate that this is a local op that is waiting for an ack.
987
1198
  const deltaProps = interval.addProperties(props, true, UnassignedSequenceNumber);
988
1199
  const serializedInterval: ISerializedInterval = interval.serialize(this.client);
989
- // Emit a change op that will only change properties. Add the ID to the property bag provided by the caller.
990
- serializedInterval.start = undefined;
991
- serializedInterval.end = undefined;
1200
+
1201
+ // Emit a change op that will only change properties. Add the ID to
1202
+ // the property bag provided by the caller.
1203
+ serializedInterval.start = undefined as any;
1204
+ serializedInterval.end = undefined as any;
1205
+
992
1206
  serializedInterval.properties = props;
993
1207
  serializedInterval.properties[reservedIntervalIdKey] = interval.getIntervalId();
994
- this.emitter.emit("change", undefined, serializedInterval);
1208
+ this.emitter.emit("change", undefined, serializedInterval, { localSeq: this.getNextLocalSeq() });
995
1209
  this.emit("propertyChanged", interval, deltaProps);
996
1210
  }
997
1211
  this.emit("changeInterval", interval, true, undefined);
@@ -999,42 +1213,39 @@ export class IntervalCollection<TInterval extends ISerializableInterval>
999
1213
 
1000
1214
  public change(id: string, start?: number, end?: number): TInterval | undefined {
1001
1215
  if (!this.attached) {
1002
- throw new Error("Attach must be called before accessing intervals");
1216
+ throw new LoggingError("Attach must be called before accessing intervals");
1003
1217
  }
1218
+
1219
+ // Force id to be a string.
1004
1220
  if (typeof (id) !== "string") {
1005
- throw new Error("Change API requires an ID that is a string");
1221
+ throw new LoggingError("Change API requires an ID that is a string");
1006
1222
  }
1007
1223
 
1008
- // Force id to be a string.
1009
1224
  const interval = this.getIntervalById(id);
1010
1225
  if (interval) {
1011
- this.localCollection.changeInterval(interval, start, end);
1226
+ const newInterval = this.localCollection.changeInterval(interval, start, end);
1012
1227
  const serializedInterval: ISerializedInterval = interval.serialize(this.client);
1013
1228
  serializedInterval.start = start;
1014
1229
  serializedInterval.end = end;
1015
1230
  // Emit a property bag containing only the ID, as we don't intend for this op to change any properties.
1016
1231
  serializedInterval.properties =
1017
- {
1018
- [reservedIntervalIdKey]: interval.getIntervalId(),
1019
- };
1020
- this.emitter.emit("change", undefined, serializedInterval);
1232
+ {
1233
+ [reservedIntervalIdKey]: interval.getIntervalId(),
1234
+ };
1235
+ this.emitter.emit("change", undefined, serializedInterval, { localSeq: this.getNextLocalSeq() });
1021
1236
  this.addPendingChange(id, serializedInterval);
1237
+ this.emit("changeInterval", newInterval, true, undefined);
1238
+ return newInterval;
1022
1239
  }
1023
- this.emit("changeInterval", interval, true, undefined);
1024
- return interval;
1240
+ // No interval to change
1241
+ return undefined;
1025
1242
  }
1026
1243
 
1027
1244
  private addPendingChange(id: string, serializedInterval: ISerializedInterval) {
1028
1245
  if (serializedInterval.start !== undefined) {
1029
- if (!this.pendingChangesStart) {
1030
- this.pendingChangesStart = new Map<string, ISerializedInterval[]>();
1031
- }
1032
1246
  this.addPendingChangeHelper(id, this.pendingChangesStart, serializedInterval);
1033
1247
  }
1034
1248
  if (serializedInterval.end !== undefined) {
1035
- if (!this.pendingChangesEnd) {
1036
- this.pendingChangesEnd = new Map<string, ISerializedInterval[]>();
1037
- }
1038
1249
  this.addPendingChangeHelper(id, this.pendingChangesEnd, serializedInterval);
1039
1250
  }
1040
1251
  }
@@ -1044,7 +1255,7 @@ export class IntervalCollection<TInterval extends ISerializableInterval>
1044
1255
  pendingChanges: Map<string, ISerializedInterval[]>,
1045
1256
  serializedInterval: ISerializedInterval,
1046
1257
  ) {
1047
- let entries: ISerializedInterval[] = pendingChanges.get(id);
1258
+ let entries: ISerializedInterval[] | undefined = pendingChanges.get(id);
1048
1259
  if (!entries) {
1049
1260
  entries = [];
1050
1261
  pendingChanges.set(id, entries);
@@ -1054,7 +1265,7 @@ export class IntervalCollection<TInterval extends ISerializableInterval>
1054
1265
 
1055
1266
  private removePendingChange(serializedInterval: ISerializedInterval) {
1056
1267
  // Change ops always have an ID.
1057
- const id: string = serializedInterval.properties[reservedIntervalIdKey];
1268
+ const id: string = serializedInterval.properties?.[reservedIntervalIdKey];
1058
1269
  if (serializedInterval.start !== undefined) {
1059
1270
  this.removePendingChangeHelper(id, this.pendingChangesStart, serializedInterval);
1060
1271
  }
@@ -1068,26 +1279,26 @@ export class IntervalCollection<TInterval extends ISerializableInterval>
1068
1279
  pendingChanges: Map<string, ISerializedInterval[]>,
1069
1280
  serializedInterval: ISerializedInterval,
1070
1281
  ) {
1071
- const entries = pendingChanges?.get(id);
1282
+ const entries = pendingChanges.get(id);
1072
1283
  if (entries) {
1073
1284
  const pendingChange = entries.shift();
1074
1285
  if (entries.length === 0) {
1075
1286
  pendingChanges.delete(id);
1076
1287
  }
1077
- if (pendingChange.start !== serializedInterval.start ||
1078
- pendingChange.end !== serializedInterval.end) {
1079
- throw new Error("Mismatch in pending changes");
1288
+ if (pendingChange?.start !== serializedInterval.start ||
1289
+ pendingChange?.end !== serializedInterval.end) {
1290
+ throw new LoggingError("Mismatch in pending changes");
1080
1291
  }
1081
1292
  }
1082
1293
  }
1083
1294
 
1084
1295
  private hasPendingChangeStart(id: string) {
1085
- const entries = this.pendingChangesStart?.get(id);
1296
+ const entries = this.pendingChangesStart.get(id);
1086
1297
  return entries && entries.length !== 0;
1087
1298
  }
1088
1299
 
1089
1300
  private hasPendingChangeEnd(id: string) {
1090
- const entries = this.pendingChangesEnd?.get(id);
1301
+ const entries = this.pendingChangesEnd.get(id);
1091
1302
  return entries && entries.length !== 0;
1092
1303
  }
1093
1304
 
@@ -1099,7 +1310,7 @@ export class IntervalCollection<TInterval extends ISerializableInterval>
1099
1310
  /** @internal */
1100
1311
  public ackChange(serializedInterval: ISerializedInterval, local: boolean, op: ISequencedDocumentMessage) {
1101
1312
  if (!this.attached) {
1102
- throw new Error("Attach must be called before accessing intervals");
1313
+ throw new LoggingError("Attach must be called before accessing intervals");
1103
1314
  }
1104
1315
 
1105
1316
  let interval: TInterval | undefined;
@@ -1107,15 +1318,14 @@ export class IntervalCollection<TInterval extends ISerializableInterval>
1107
1318
  if (local) {
1108
1319
  // This is an ack from the server. Remove the pending change.
1109
1320
  this.removePendingChange(serializedInterval);
1110
- const id: string = serializedInterval.properties[reservedIntervalIdKey];
1111
- // Could store the interval in the localOpMetadata to avoid the getIntervalById call
1321
+ const id: string = serializedInterval.properties?.[reservedIntervalIdKey];
1112
1322
  interval = this.getIntervalById(id);
1113
1323
  if (interval) {
1114
1324
  // Let the propertyManager prune its pending change-properties set.
1115
1325
  interval.propertyManager?.ackPendingProperties(
1116
1326
  {
1117
1327
  type: MergeTreeDeltaType.ANNOTATE,
1118
- props: serializedInterval.properties,
1328
+ props: serializedInterval.properties ?? {},
1119
1329
  });
1120
1330
 
1121
1331
  this.ackInterval(interval, op);
@@ -1157,7 +1367,7 @@ export class IntervalCollection<TInterval extends ISerializableInterval>
1157
1367
 
1158
1368
  public addConflictResolver(conflictResolver: IntervalConflictResolver<TInterval>): void {
1159
1369
  if (!this.attached) {
1160
- throw new Error("attachSequence must be called");
1370
+ throw new LoggingError("attachSequence must be called");
1161
1371
  }
1162
1372
  this.localCollection.addConflictResolver(conflictResolver);
1163
1373
  }
@@ -1173,15 +1383,46 @@ export class IntervalCollection<TInterval extends ISerializableInterval>
1173
1383
 
1174
1384
  // Trigger the async prepare work across all values in the collection
1175
1385
  this.localCollection.map((interval) => {
1176
- this.onDeserialize(interval);
1386
+ onDeserialize(interval);
1177
1387
  });
1178
1388
  }
1179
1389
 
1390
+ /** @internal */
1391
+ public rebaseLocalInterval(
1392
+ opName: string,
1393
+ serializedInterval: ISerializedInterval,
1394
+ localSeq: number,
1395
+ ) {
1396
+ if (!this.attached) {
1397
+ throw new LoggingError("attachSequence must be called");
1398
+ }
1399
+
1400
+ const { start, end, intervalType, properties, sequenceNumber } = serializedInterval;
1401
+ const startRebased = start === undefined ? undefined :
1402
+ this.client.rebasePosition(start, sequenceNumber, localSeq);
1403
+ const endRebased = end === undefined ? undefined :
1404
+ this.client.rebasePosition(end, sequenceNumber, localSeq);
1405
+
1406
+ const intervalId = properties?.[reservedIntervalIdKey];
1407
+ const rebased: ISerializedInterval = {
1408
+ start: startRebased,
1409
+ end: endRebased,
1410
+ intervalType,
1411
+ sequenceNumber: this.client?.getCurrentSeq() ?? 0,
1412
+ properties,
1413
+ };
1414
+ if (opName === "change" && (this.hasPendingChangeStart(intervalId) || this.hasPendingChangeEnd(intervalId))) {
1415
+ this.removePendingChange(serializedInterval);
1416
+ this.addPendingChange(intervalId, rebased);
1417
+ }
1418
+ return rebased;
1419
+ }
1420
+
1180
1421
  private getSlideToSegment(lref: LocalReference) {
1181
1422
  const segoff = { segment: lref.segment, offset: lref.offset };
1182
1423
  const newSegoff = this.client.getSlideToSegment(segoff);
1183
1424
  const value: { segment: ISegment | undefined; offset: number | undefined; } | undefined
1184
- = (segoff === newSegoff) ? undefined : newSegoff;
1425
+ = (segoff.segment === newSegoff.segment && segoff.offset === newSegoff.offset) ? undefined : newSegoff;
1185
1426
  return value;
1186
1427
  }
1187
1428
 
@@ -1198,27 +1439,50 @@ export class IntervalCollection<TInterval extends ISerializableInterval>
1198
1439
  return;
1199
1440
  }
1200
1441
 
1201
- if (!refTypeIncludesFlag(interval.start, ReferenceType.StayOnRemove)) {
1442
+ if (!refTypeIncludesFlag(interval.start, ReferenceType.StayOnRemove) &&
1443
+ !refTypeIncludesFlag(interval.end, ReferenceType.StayOnRemove)) {
1202
1444
  return;
1203
1445
  }
1204
- assert(refTypeIncludesFlag(interval.end, ReferenceType.StayOnRemove),
1205
- 0x2f7 /* start and end must both be StayOnRemove */);
1446
+
1206
1447
  const newStart = this.getSlideToSegment(interval.start);
1207
1448
  const newEnd = this.getSlideToSegment(interval.end);
1208
- this.setSlideOnRemove(interval.start);
1209
- this.setSlideOnRemove(interval.end);
1210
1449
 
1211
- if (newStart || newEnd) {
1450
+ const id = interval.properties[reservedIntervalIdKey];
1451
+ const hasPendingStartChange = this.hasPendingChangeStart(id);
1452
+ const hasPendingEndChange = this.hasPendingChangeEnd(id);
1453
+
1454
+ if (!hasPendingStartChange) {
1455
+ this.setSlideOnRemove(interval.start);
1456
+ }
1457
+
1458
+ if (!hasPendingEndChange) {
1459
+ this.setSlideOnRemove(interval.end);
1460
+ }
1461
+
1462
+ const needsStartUpdate = newStart !== undefined && !hasPendingStartChange;
1463
+ const needsEndUpdate = newEnd !== undefined && !hasPendingEndChange;
1464
+
1465
+ if (needsStartUpdate || needsEndUpdate) {
1466
+ // In this case, where we change the start or end of an interval,
1467
+ // it is necessary to remove and re-add the interval listeners.
1468
+ // This ensures that the correct listeners are added to the ReferencePosition.
1212
1469
  this.localCollection.removeExistingInterval(interval);
1213
- if (newStart) {
1470
+
1471
+ if (needsStartUpdate) {
1214
1472
  const props = interval.start.properties;
1473
+ this.client.removeLocalReferencePosition(interval.start);
1215
1474
  interval.start = createPositionReferenceFromSegoff(this.client, newStart, interval.start.refType, op);
1216
- interval.start.addProperties(props);
1475
+ if (props) {
1476
+ interval.start.addProperties(props);
1477
+ }
1217
1478
  }
1218
- if (newEnd) {
1479
+ if (needsEndUpdate) {
1219
1480
  const props = interval.end.properties;
1481
+ this.client.removeLocalReferencePosition(interval.end);
1220
1482
  interval.end = createPositionReferenceFromSegoff(this.client, newEnd, interval.end.refType, op);
1221
- interval.end.addProperties(props);
1483
+ if (props) {
1484
+ interval.end.addProperties(props);
1485
+ }
1222
1486
  }
1223
1487
  this.localCollection.add(interval);
1224
1488
  }
@@ -1238,8 +1502,7 @@ export class IntervalCollection<TInterval extends ISerializableInterval>
1238
1502
  local: boolean,
1239
1503
  op: ISequencedDocumentMessage) {
1240
1504
  if (local) {
1241
- const id: string = serializedInterval.properties[reservedIntervalIdKey];
1242
- // Could store the interval in the localOpMetadata to avoid the getIntervalById call
1505
+ const id: string = serializedInterval.properties?.[reservedIntervalIdKey];
1243
1506
  const localInterval = this.getIntervalById(id);
1244
1507
  if (localInterval) {
1245
1508
  this.ackInterval(localInterval, op);
@@ -1248,7 +1511,7 @@ export class IntervalCollection<TInterval extends ISerializableInterval>
1248
1511
  }
1249
1512
 
1250
1513
  if (!this.attached) {
1251
- throw new Error("attachSequence must be called");
1514
+ throw new LoggingError("attachSequence must be called");
1252
1515
  }
1253
1516
 
1254
1517
  this.localCollection.ensureSerializedId(serializedInterval);
@@ -1292,19 +1555,22 @@ export class IntervalCollection<TInterval extends ISerializableInterval>
1292
1555
  }
1293
1556
 
1294
1557
  if (!this.attached) {
1295
- throw new Error("attach must be called prior to deleting intervals");
1558
+ throw new LoggingError("attach must be called prior to deleting intervals");
1296
1559
  }
1297
1560
 
1298
- this.localCollection.ensureSerializedId(serializedInterval);
1299
- const interval = this.localCollection.getIntervalById(serializedInterval.properties[reservedIntervalIdKey]);
1561
+ const id = this.localCollection.ensureSerializedId(serializedInterval);
1562
+ const interval = this.localCollection.getIntervalById(id);
1300
1563
  if (interval) {
1301
1564
  this.deleteExistingInterval(interval, local, op);
1302
1565
  }
1303
1566
  }
1304
1567
 
1305
- public serializeInternal() {
1568
+ /**
1569
+ * @internal
1570
+ */
1571
+ public serializeInternal(): ISerializedIntervalCollectionV2 {
1306
1572
  if (!this.attached) {
1307
- throw new Error("attachSequence must be called");
1573
+ throw new LoggingError("attachSequence must be called");
1308
1574
  }
1309
1575
 
1310
1576
  return this.localCollection.serialize();
@@ -1349,7 +1615,7 @@ export class IntervalCollection<TInterval extends ISerializableInterval>
1349
1615
 
1350
1616
  public findOverlappingIntervals(startPosition: number, endPosition: number): TInterval[] {
1351
1617
  if (!this.attached) {
1352
- throw new Error("attachSequence must be called");
1618
+ throw new LoggingError("attachSequence must be called");
1353
1619
  }
1354
1620
 
1355
1621
  return this.localCollection.findOverlappingIntervals(startPosition, endPosition);
@@ -1357,7 +1623,7 @@ export class IntervalCollection<TInterval extends ISerializableInterval>
1357
1623
 
1358
1624
  public map(fn: (interval: TInterval) => void) {
1359
1625
  if (!this.attached) {
1360
- throw new Error("attachSequence must be called");
1626
+ throw new LoggingError("attachSequence must be called");
1361
1627
  }
1362
1628
 
1363
1629
  this.localCollection.map(fn);
@@ -1365,7 +1631,7 @@ export class IntervalCollection<TInterval extends ISerializableInterval>
1365
1631
 
1366
1632
  public previousInterval(pos: number): TInterval {
1367
1633
  if (!this.attached) {
1368
- throw new Error("attachSequence must be called");
1634
+ throw new LoggingError("attachSequence must be called");
1369
1635
  }
1370
1636
 
1371
1637
  return this.localCollection.previousInterval(pos);
@@ -1373,7 +1639,7 @@ export class IntervalCollection<TInterval extends ISerializableInterval>
1373
1639
 
1374
1640
  public nextInterval(pos: number): TInterval {
1375
1641
  if (!this.attached) {
1376
- throw new Error("attachSequence must be called");
1642
+ throw new LoggingError("attachSequence must be called");
1377
1643
  }
1378
1644
 
1379
1645
  return this.localCollection.nextInterval(pos);