@fluidframework/sequence 0.59.4001 → 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 (68) hide show
  1. package/.eslintrc.js +1 -1
  2. package/README.md +18 -6
  3. package/dist/defaultMap.d.ts +2 -6
  4. package/dist/defaultMap.d.ts.map +1 -1
  5. package/dist/defaultMap.js +27 -37
  6. package/dist/defaultMap.js.map +1 -1
  7. package/dist/defaultMapInterfaces.d.ts +24 -3
  8. package/dist/defaultMapInterfaces.d.ts.map +1 -1
  9. package/dist/defaultMapInterfaces.js.map +1 -1
  10. package/dist/index.d.ts +2 -2
  11. package/dist/index.d.ts.map +1 -1
  12. package/dist/index.js.map +1 -1
  13. package/dist/intervalCollection.d.ts +93 -8
  14. package/dist/intervalCollection.d.ts.map +1 -1
  15. package/dist/intervalCollection.js +434 -178
  16. package/dist/intervalCollection.js.map +1 -1
  17. package/dist/packageVersion.d.ts +1 -1
  18. package/dist/packageVersion.d.ts.map +1 -1
  19. package/dist/packageVersion.js +1 -1
  20. package/dist/packageVersion.js.map +1 -1
  21. package/dist/sequence.d.ts +6 -7
  22. package/dist/sequence.d.ts.map +1 -1
  23. package/dist/sequence.js +17 -21
  24. package/dist/sequence.js.map +1 -1
  25. package/dist/sharedIntervalCollection.d.ts.map +1 -1
  26. package/dist/sharedIntervalCollection.js +2 -2
  27. package/dist/sharedIntervalCollection.js.map +1 -1
  28. package/dist/sharedSequence.js.map +1 -1
  29. package/dist/sparsematrix.js +2 -2
  30. package/dist/sparsematrix.js.map +1 -1
  31. package/lib/defaultMap.d.ts +2 -6
  32. package/lib/defaultMap.d.ts.map +1 -1
  33. package/lib/defaultMap.js +27 -37
  34. package/lib/defaultMap.js.map +1 -1
  35. package/lib/defaultMapInterfaces.d.ts +24 -3
  36. package/lib/defaultMapInterfaces.d.ts.map +1 -1
  37. package/lib/defaultMapInterfaces.js.map +1 -1
  38. package/lib/index.d.ts +2 -2
  39. package/lib/index.d.ts.map +1 -1
  40. package/lib/index.js.map +1 -1
  41. package/lib/intervalCollection.d.ts +93 -8
  42. package/lib/intervalCollection.d.ts.map +1 -1
  43. package/lib/intervalCollection.js +435 -179
  44. package/lib/intervalCollection.js.map +1 -1
  45. package/lib/packageVersion.d.ts +1 -1
  46. package/lib/packageVersion.d.ts.map +1 -1
  47. package/lib/packageVersion.js +1 -1
  48. package/lib/packageVersion.js.map +1 -1
  49. package/lib/sequence.d.ts +6 -7
  50. package/lib/sequence.d.ts.map +1 -1
  51. package/lib/sequence.js +18 -22
  52. package/lib/sequence.js.map +1 -1
  53. package/lib/sharedIntervalCollection.d.ts.map +1 -1
  54. package/lib/sharedIntervalCollection.js +2 -2
  55. package/lib/sharedIntervalCollection.js.map +1 -1
  56. package/lib/sharedSequence.js.map +1 -1
  57. package/lib/sparsematrix.js +2 -2
  58. package/lib/sparsematrix.js.map +1 -1
  59. package/package.json +23 -45
  60. package/src/defaultMap.ts +39 -41
  61. package/src/defaultMapInterfaces.ts +28 -3
  62. package/src/index.ts +3 -0
  63. package/src/intervalCollection.ts +575 -211
  64. package/src/packageVersion.ts +1 -1
  65. package/src/sequence.ts +36 -38
  66. package/src/sharedIntervalCollection.ts +4 -3
  67. package/src/sharedSequence.ts +1 -1
  68. package/src/sparsematrix.ts +2 -2
@@ -7,6 +7,7 @@
7
7
 
8
8
  import { assert, TypedEventEmitter } from "@fluidframework/common-utils";
9
9
  import { IEvent } from "@fluidframework/common-definitions";
10
+ import { UsageError } from "@fluidframework/container-utils";
10
11
  import {
11
12
  addProperties,
12
13
  Client,
@@ -17,25 +18,45 @@ import {
17
18
  IntervalConflictResolver,
18
19
  IntervalNode,
19
20
  IntervalTree,
21
+ ISegment,
20
22
  LocalReference,
21
23
  MergeTreeDeltaType,
22
24
  PropertiesManager,
23
25
  PropertySet,
24
26
  RedBlackTree,
25
27
  ReferenceType,
28
+ refTypeIncludesFlag,
26
29
  reservedRangeLabelsKey,
27
30
  UnassignedSequenceNumber,
28
31
  } from "@fluidframework/merge-tree";
29
32
  import { ISequencedDocumentMessage } from "@fluidframework/protocol-definitions";
33
+ import { LoggingError } from "@fluidframework/telemetry-utils";
30
34
  import { v4 as uuid } from "uuid";
31
- import { IValueFactory, IValueOpEmitter, IValueOperation, IValueType } from "./defaultMapInterfaces";
35
+ import {
36
+ IMapMessageLocalMetadata,
37
+ IValueFactory,
38
+ IValueOpEmitter,
39
+ IValueOperation,
40
+ IValueType,
41
+ IValueTypeOperationValue,
42
+ } from "./defaultMapInterfaces";
32
43
 
33
44
  const reservedIntervalIdKey = "intervalId";
34
45
 
35
46
  export enum IntervalType {
36
47
  Simple = 0x0,
37
48
  Nest = 0x1,
38
- SlideOnRemove = 0x2,
49
+ /**
50
+ * SlideOnRemove indicates that the ends of the interval will slide if the segment
51
+ * they reference is removed and acked.
52
+ * See `packages\dds\merge-tree\REFERENCEPOSITIONS.md` for details
53
+ * SlideOnRemove is the default interval behavior and does not need to be specified.
54
+ */
55
+ SlideOnRemove = 0x2, // SlideOnRemove is default behavior - all intervals are SlideOnRemove
56
+ /**
57
+ * @internal
58
+ * A temporary interval, used internally
59
+ */
39
60
  Transient = 0x4,
40
61
  }
41
62
 
@@ -47,6 +68,55 @@ export interface ISerializedInterval {
47
68
  properties?: PropertySet;
48
69
  }
49
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
+
50
120
  export interface ISerializableInterval extends IInterval {
51
121
  properties: PropertySet;
52
122
  propertyManager: PropertiesManager;
@@ -69,7 +139,11 @@ export class Interval implements ISerializableInterval {
69
139
  constructor(
70
140
  public start: number,
71
141
  public end: number,
72
- props?: PropertySet) {
142
+ props?: PropertySet,
143
+ ) {
144
+ this.propertyManager = new PropertiesManager();
145
+ this.properties = {};
146
+
73
147
  if (props) {
74
148
  this.addProperties(props);
75
149
  }
@@ -94,7 +168,7 @@ export class Interval implements ISerializableInterval {
94
168
  this.auxProps.push(props);
95
169
  }
96
170
 
97
- public serialize(client: Client) {
171
+ public serialize(client: Client): ISerializedInterval {
98
172
  let seq = 0;
99
173
  if (client) {
100
174
  seq = client.getCurrentSeq();
@@ -168,12 +242,7 @@ export class Interval implements ISerializableInterval {
168
242
  op?: ICombiningOp,
169
243
  ): PropertySet | undefined {
170
244
  if (newProps) {
171
- if (!this.propertyManager) {
172
- this.propertyManager = new PropertiesManager();
173
- }
174
- if (!this.properties) {
175
- this.properties = createMap<any>();
176
- }
245
+ this.initializeProperties();
177
246
  return this.propertyManager.addProperties(this.properties, newProps, op, seq, collaborating);
178
247
  }
179
248
  }
@@ -185,7 +254,21 @@ export class Interval implements ISerializableInterval {
185
254
  // Return undefined to indicate that no change is necessary.
186
255
  return;
187
256
  }
188
- 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
+ }
189
272
  }
190
273
  }
191
274
 
@@ -197,13 +280,49 @@ export class SequenceInterval implements ISerializableInterval {
197
280
  public start: LocalReference,
198
281
  public end: LocalReference,
199
282
  public intervalType: IntervalType,
200
- props?: PropertySet) {
283
+ props?: PropertySet,
284
+ ) {
285
+ this.propertyManager = new PropertiesManager();
286
+ this.properties = {};
287
+
201
288
  if (props) {
202
289
  this.addProperties(props);
203
290
  }
204
291
  }
205
292
 
206
- 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 {
207
326
  const startPosition = this.start.toPosition();
208
327
  const endPosition = this.end.toPosition();
209
328
  const serializedInterval: ISerializedInterval = {
@@ -212,9 +331,11 @@ export class SequenceInterval implements ISerializableInterval {
212
331
  sequenceNumber: client.getCurrentSeq(),
213
332
  start: startPosition,
214
333
  };
334
+
215
335
  if (this.properties) {
216
336
  serializedInterval.properties = this.properties;
217
337
  }
338
+
218
339
  return serializedInterval;
219
340
  }
220
341
 
@@ -277,12 +398,7 @@ export class SequenceInterval implements ISerializableInterval {
277
398
  seq?: number,
278
399
  op?: ICombiningOp,
279
400
  ): PropertySet | undefined {
280
- if (!this.propertyManager) {
281
- this.propertyManager = new PropertiesManager();
282
- }
283
- if (!this.properties) {
284
- this.properties = createMap<any>();
285
- }
401
+ this.initializeProperties();
286
402
  return this.propertyManager.addProperties(this.properties, newProps, op, seq, collab);
287
403
  }
288
404
 
@@ -293,21 +409,62 @@ export class SequenceInterval implements ISerializableInterval {
293
409
  }
294
410
 
295
411
  public modify(label: string, start: number, end: number, op?: ISequencedDocumentMessage) {
296
- const startPos = start ?? this.start.toPosition();
297
- 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
+ };
298
420
 
299
- if (this.start.toPosition() === startPos && this.end.toPosition() === endPos) {
300
- // Return undefined to indicate that no change is necessary.
301
- 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);
302
425
  }
303
426
 
304
- const newInterval =
305
- createSequenceInterval(label, startPos, endPos, this.start.getClient(), this.intervalType, op);
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);
431
+ }
432
+
433
+ startRef.pairedRef = endRef;
434
+ endRef.pairedRef = startRef;
435
+
436
+ const newInterval = new SequenceInterval(startRef, endRef, this.intervalType);
306
437
  if (this.properties) {
307
- newInterval.addProperties(this.properties);
438
+ newInterval.initializeProperties();
439
+ this.propertyManager.copyTo(this.properties, newInterval.properties, newInterval.propertyManager);
308
440
  }
309
441
  return newInterval;
310
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
+ }
452
+ }
453
+
454
+ function createPositionReferenceFromSegoff(
455
+ client: Client,
456
+ segoff: { segment: ISegment | undefined; offset: number | undefined; },
457
+ refType: ReferenceType,
458
+ op?: ISequencedDocumentMessage): LocalReference {
459
+ if (segoff.segment) {
460
+ const ref = client.createLocalReferencePosition(segoff.segment, segoff.offset, refType, undefined);
461
+ return ref as LocalReference;
462
+ } else {
463
+ if (!op && !refTypeIncludesFlag(refType, ReferenceType.Transient)) {
464
+ throw new UsageError("Non-transient references need segment");
465
+ }
466
+ return new LocalReference(client, undefined, 0, refType);
467
+ }
311
468
  }
312
469
 
313
470
  function createPositionReference(
@@ -315,15 +472,16 @@ function createPositionReference(
315
472
  pos: number,
316
473
  refType: ReferenceType,
317
474
  op?: ISequencedDocumentMessage): LocalReference {
318
- const segoff = client.getContainingSegment(pos, op);
319
- if (segoff?.segment) {
320
- const lref = new LocalReference(client, segoff.segment, segoff.offset, refType);
321
- if (refType !== ReferenceType.Transient) {
322
- client.addLocalReference(lref);
323
- }
324
- return lref;
475
+ let segoff;
476
+ if (op) {
477
+ assert((refType & ReferenceType.SlideOnRemove) !== 0, 0x2f5 /* op create references must be SlideOnRemove */);
478
+ segoff = client.getContainingSegment(pos, op);
479
+ segoff = client.getSlideToSegment(segoff);
480
+ } else {
481
+ assert((refType & ReferenceType.SlideOnRemove) === 0, 0x2f6 /* SlideOnRemove references must be op created */);
482
+ segoff = client.getContainingSegment(pos);
325
483
  }
326
- return new LocalReference(client, undefined);
484
+ return createPositionReferenceFromSegoff(client, segoff, refType, op);
327
485
  }
328
486
 
329
487
  function createSequenceInterval(
@@ -331,38 +489,42 @@ function createSequenceInterval(
331
489
  start: number,
332
490
  end: number,
333
491
  client: Client,
334
- intervalType: IntervalType,
492
+ intervalType?: IntervalType,
335
493
  op?: ISequencedDocumentMessage): SequenceInterval {
336
494
  let beginRefType = ReferenceType.RangeBegin;
337
495
  let endRefType = ReferenceType.RangeEnd;
338
- if (intervalType === IntervalType.Nest) {
339
- beginRefType = ReferenceType.NestBegin;
340
- endRefType = ReferenceType.NestEnd;
341
- } else if (intervalType === IntervalType.Transient) {
496
+ if (intervalType === IntervalType.Transient) {
342
497
  beginRefType = ReferenceType.Transient;
343
498
  endRefType = ReferenceType.Transient;
344
- }
345
-
346
- // TODO: Should SlideOnRemove be the default behavior?
347
- if (intervalType & IntervalType.SlideOnRemove) {
348
- beginRefType |= ReferenceType.SlideOnRemove;
349
- endRefType |= ReferenceType.SlideOnRemove;
499
+ } else {
500
+ if (intervalType === IntervalType.Nest) {
501
+ beginRefType = ReferenceType.NestBegin;
502
+ endRefType = ReferenceType.NestEnd;
503
+ }
504
+ // All non-transient interval references must eventually be SlideOnRemove
505
+ // To ensure eventual consistency, they must start as StayOnRemove when
506
+ // pending (created locally and creation op is not acked)
507
+ if (op) {
508
+ beginRefType |= ReferenceType.SlideOnRemove;
509
+ endRefType |= ReferenceType.SlideOnRemove;
510
+ } else {
511
+ beginRefType |= ReferenceType.StayOnRemove;
512
+ endRefType |= ReferenceType.StayOnRemove;
513
+ }
350
514
  }
351
515
 
352
516
  const startLref = createPositionReference(client, start, beginRefType, op);
353
517
  const endLref = createPositionReference(client, end, endRefType, op);
354
- if (startLref && endLref) {
355
- startLref.pairedRef = endLref;
356
- endLref.pairedRef = startLref;
357
- const rangeProp = {
358
- [reservedRangeLabelsKey]: [label],
359
- };
360
- startLref.addProperties(rangeProp);
361
- endLref.addProperties(rangeProp);
518
+ startLref.pairedRef = endLref;
519
+ endLref.pairedRef = startLref;
520
+ const rangeProp = {
521
+ [reservedRangeLabelsKey]: [label],
522
+ };
523
+ startLref.addProperties(rangeProp);
524
+ endLref.addProperties(rangeProp);
362
525
 
363
- const ival = new SequenceInterval(startLref, endLref, intervalType, rangeProp);
364
- return ival;
365
- }
526
+ const ival = new SequenceInterval(startLref, endLref, intervalType, rangeProp);
527
+ return ival;
366
528
  }
367
529
 
368
530
  export function defaultIntervalConflictResolver(a: Interval, b: Interval) {
@@ -387,8 +549,9 @@ export function createIntervalIndex(conflict?: IntervalConflictResolver<Interval
387
549
  export class LocalIntervalCollection<TInterval extends ISerializableInterval> {
388
550
  private readonly intervalTree = new IntervalTree<TInterval>();
389
551
  private readonly endIntervalTree: RedBlackTree<TInterval, TInterval>;
390
- private conflictResolver: IntervalConflictResolver<TInterval>;
391
- 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;
392
555
 
393
556
  private static readonly legacyIdPrefix = "legacy";
394
557
 
@@ -396,6 +559,8 @@ export class LocalIntervalCollection<TInterval extends ISerializableInterval> {
396
559
  private readonly client: Client,
397
560
  private readonly label: string,
398
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,
399
564
  ) {
400
565
  // eslint-disable-next-line @typescript-eslint/unbound-method
401
566
  this.endIntervalTree = new RedBlackTree<TInterval, TInterval>(helpers.compareEnds);
@@ -405,7 +570,7 @@ export class LocalIntervalCollection<TInterval extends ISerializableInterval> {
405
570
  this.conflictResolver = conflictResolver;
406
571
  this.endConflictResolver =
407
572
  (key: TInterval, currentKey: TInterval) => {
408
- const ival = this.conflictResolver(key, currentKey);
573
+ const ival = conflictResolver(key, currentKey);
409
574
  return {
410
575
  data: ival,
411
576
  key: ival,
@@ -423,12 +588,21 @@ export class LocalIntervalCollection<TInterval extends ISerializableInterval> {
423
588
  return `${LocalIntervalCollection.legacyIdPrefix}${start}-${end}`;
424
589
  }
425
590
 
426
- public ensureSerializedId(serializedInterval: ISerializedInterval) {
427
- 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) {
428
601
  // An interval came over the wire without an ID, so create a non-unique one based on start/end.
429
602
  // This will allow all clients to refer to this interval consistently.
603
+ id = this.createLegacyId(serializedInterval.start, serializedInterval.end);
430
604
  const newProps = {
431
- [reservedIntervalIdKey]: this.createLegacyId(serializedInterval.start, serializedInterval.end),
605
+ [reservedIntervalIdKey]: id,
432
606
  };
433
607
  serializedInterval.properties = addProperties(serializedInterval.properties, newProps);
434
608
  }
@@ -438,6 +612,8 @@ export class LocalIntervalCollection<TInterval extends ISerializableInterval> {
438
612
  enumerable: true,
439
613
  writable: false,
440
614
  });
615
+
616
+ return id;
441
617
  }
442
618
 
443
619
  public mapUntil(fn: (interval: TInterval) => boolean) {
@@ -520,20 +696,19 @@ export class LocalIntervalCollection<TInterval extends ISerializableInterval> {
520
696
  }
521
697
 
522
698
  public findOverlappingIntervals(startPosition: number, endPosition: number) {
523
- if (!this.intervalTree.intervals.isEmpty()) {
524
- const transientInterval =
525
- this.helpers.create(
526
- "transient",
527
- startPosition,
528
- endPosition,
529
- this.client,
530
- IntervalType.Transient);
531
-
532
- const overlappingIntervalNodes = this.intervalTree.match(transientInterval);
533
- return overlappingIntervalNodes.map((node) => node.key);
534
- } else {
699
+ if (endPosition < startPosition || this.intervalTree.intervals.isEmpty()) {
535
700
  return [];
536
701
  }
702
+ const transientInterval =
703
+ this.helpers.create(
704
+ "transient",
705
+ startPosition,
706
+ endPosition,
707
+ this.client,
708
+ IntervalType.Transient);
709
+
710
+ const overlappingIntervalNodes = this.intervalTree.match(transientInterval);
711
+ return overlappingIntervalNodes.map((node) => node.key);
537
712
  }
538
713
 
539
714
  public previousInterval(pos: number) {
@@ -562,9 +737,20 @@ export class LocalIntervalCollection<TInterval extends ISerializableInterval> {
562
737
  return transientInterval;
563
738
  }
564
739
 
565
- public removeExistingInterval(interval: TInterval) {
740
+ private removeIntervalFromIndex(interval: TInterval) {
566
741
  this.intervalTree.removeExisting(interval);
567
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);
568
754
  }
569
755
 
570
756
  public createInterval(
@@ -598,9 +784,9 @@ export class LocalIntervalCollection<TInterval extends ISerializableInterval> {
598
784
  return interval;
599
785
  }
600
786
 
601
- public add(interval: TInterval) {
602
- assert(Object.prototype.hasOwnProperty.call(interval.properties, reservedIntervalIdKey),
603
- 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" */);
604
790
  // Make the ID immutable.
605
791
  Object.defineProperty(interval.properties, reservedIntervalIdKey, {
606
792
  configurable: false,
@@ -609,18 +795,16 @@ export class LocalIntervalCollection<TInterval extends ISerializableInterval> {
609
795
  });
610
796
  this.intervalTree.put(interval, this.conflictResolver);
611
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);
612
804
  }
613
805
 
614
806
  public getIntervalById(id: string) {
615
- let result: TInterval | undefined;
616
- this.mapUntil((interval: TInterval) => {
617
- if (interval.getIntervalId() === id) {
618
- result = interval;
619
- return false;
620
- }
621
- return true;
622
- });
623
- return result;
807
+ return this.intervalIdMap.get(id);
624
808
  }
625
809
 
626
810
  public changeInterval(interval: TInterval, start: number, end: number, op?: ISequencedDocumentMessage) {
@@ -632,10 +816,33 @@ export class LocalIntervalCollection<TInterval extends ISerializableInterval> {
632
816
  return newInterval;
633
817
  }
634
818
 
635
- public serialize() {
819
+ public serialize(): ISerializedIntervalCollectionV2 {
636
820
  const client = this.client;
637
821
  const intervals = this.intervalTree.intervals.keys();
638
- 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
+ }
639
846
  }
640
847
  }
641
848
 
@@ -645,7 +852,7 @@ class SequenceIntervalCollectionFactory
645
852
  implements IValueFactory<IntervalCollection<SequenceInterval>> {
646
853
  public load(
647
854
  emitter: IValueOpEmitter,
648
- raw: ISerializedInterval[] = [],
855
+ raw: ISerializedInterval[] | ISerializedIntervalCollectionV2 = [],
649
856
  ): IntervalCollection<SequenceInterval> {
650
857
  const helpers: IIntervalHelpers<SequenceInterval> = {
651
858
  compareEnds: compareSequenceIntervalEnds,
@@ -654,7 +861,7 @@ class SequenceIntervalCollectionFactory
654
861
  return new IntervalCollection<SequenceInterval>(helpers, true, emitter, raw);
655
862
  }
656
863
 
657
- public store(value: IntervalCollection<SequenceInterval>): ISerializedInterval[] {
864
+ public store(value: IntervalCollection<SequenceInterval>): ISerializedIntervalCollectionV2 {
658
865
  return value.serializeInternal();
659
866
  }
660
867
  }
@@ -678,49 +885,27 @@ export class SequenceIntervalCollectionValueType
678
885
  private static readonly _factory: IValueFactory<IntervalCollection<SequenceInterval>> =
679
886
  new SequenceIntervalCollectionFactory();
680
887
 
681
- private static readonly _ops: Map<string, IValueOperation<IntervalCollection<SequenceInterval>>> =
682
- new Map<string, IValueOperation<IntervalCollection<SequenceInterval>>>(
683
- [[
684
- "add",
685
- {
686
- process: (value, params, local, op) => {
687
- value.ackAdd(params, local, op);
688
- },
689
- },
690
- ],
691
- [
692
- "delete",
693
- {
694
- process: (value, params, local, op) => {
695
- value.ackDelete(params, local, op);
696
- },
697
- },
698
- ],
699
- [
700
- "change",
701
- {
702
- process: (value, params, local, op) => {
703
- value.ackChange(params, local, op);
704
- },
705
- },
706
- ]]);
888
+ private static readonly _ops = makeOpsMap<SequenceInterval>();
707
889
  }
708
890
 
709
891
  const compareIntervalEnds = (a: Interval, b: Interval) => a.end - b.end;
710
892
 
711
893
  function createInterval(label: string, start: number, end: number, client: Client): Interval {
712
- let rangeProp: PropertySet;
713
- if (label && (label.length > 0)) {
714
- rangeProp = {
715
- [reservedRangeLabelsKey]: [label],
716
- };
894
+ const rangeProp: PropertySet = {};
895
+
896
+ if (label && label.length > 0) {
897
+ rangeProp[reservedRangeLabelsKey] = [label];
717
898
  }
899
+
718
900
  return new Interval(start, end, rangeProp);
719
901
  }
720
902
 
721
903
  class IntervalCollectionFactory
722
904
  implements IValueFactory<IntervalCollection<Interval>> {
723
- public load(emitter: IValueOpEmitter, raw: ISerializedInterval[] = []): IntervalCollection<Interval> {
905
+ public load(
906
+ emitter: IValueOpEmitter,
907
+ raw: ISerializedInterval[] | ISerializedIntervalCollectionV2 = [],
908
+ ): IntervalCollection<Interval> {
724
909
  const helpers: IIntervalHelpers<Interval> = {
725
910
  compareEnds: compareIntervalEnds,
726
911
  create: createInterval,
@@ -730,7 +915,7 @@ class IntervalCollectionFactory
730
915
  return collection;
731
916
  }
732
917
 
733
- public store(value: IntervalCollection<Interval>): ISerializedInterval[] {
918
+ public store(value: IntervalCollection<Interval>): ISerializedIntervalCollectionV2 {
734
919
  return value.serializeInternal();
735
920
  }
736
921
  }
@@ -753,32 +938,52 @@ export class IntervalCollectionValueType
753
938
 
754
939
  private static readonly _factory: IValueFactory<IntervalCollection<Interval>> =
755
940
  new IntervalCollectionFactory();
756
- private static readonly _ops: Map<string, IValueOperation<IntervalCollection<Interval>>> =
757
- new Map<string, IValueOperation<IntervalCollection<Interval>>>(
758
- [[
759
- "add",
760
- {
761
- process: (value, params, local, op) => {
762
- value.ackAdd(params, local, op);
763
- },
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);
962
+ },
963
+ rebase,
964
+ },
965
+ ],
966
+ [
967
+ "delete",
968
+ {
969
+ process: (collection, params, local, op) => {
970
+ collection.ackDelete(params, local, op);
764
971
  },
765
- ],
766
- [
767
- "delete",
768
- {
769
- process: (value, params, local, op) => {
770
- value.ackDelete(params, local, op);
771
- },
972
+ rebase: (collection, op, localOpMetadata) => {
973
+ // Deletion of intervals is based on id, so requires no rebasing.
974
+ return { rebasedOp: op, rebasedLocalOpMetadata: localOpMetadata };
772
975
  },
773
- ],
774
- [
775
- "change",
776
- {
777
- process: (value, params, local, op) => {
778
- value.ackChange(params, local, op);
779
- },
976
+ },
977
+ ],
978
+ [
979
+ "change",
980
+ {
981
+ process: (collection, params, local, op) => {
982
+ collection.ackChange(params, local, op);
780
983
  },
781
- ]]);
984
+ rebase,
985
+ },
986
+ ]]);
782
987
  }
783
988
 
784
989
  export type DeserializeCallback = (properties: PropertySet) => void;
@@ -815,7 +1020,18 @@ export class IntervalCollectionIterator<TInterval extends ISerializableInterval>
815
1020
  }
816
1021
 
817
1022
  export interface IIntervalCollectionEvent<TInterval extends ISerializableInterval> extends IEvent {
818
- (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",
819
1035
  listener: (interval: TInterval, local: boolean, op: ISequencedDocumentMessage) => void);
820
1036
  (event: "propertyChanged", listener: (interval: TInterval, propertyArgs: PropertySet) => void);
821
1037
  }
@@ -824,34 +1040,49 @@ export class IntervalCollection<TInterval extends ISerializableInterval>
824
1040
  extends TypedEventEmitter<IIntervalCollectionEvent<TInterval>> {
825
1041
  private savedSerializedIntervals?: ISerializedInterval[];
826
1042
  private localCollection: LocalIntervalCollection<TInterval>;
827
- private onDeserialize: DeserializeCallback;
1043
+ private onDeserialize: DeserializeCallback | undefined;
828
1044
  private client: Client;
829
- private pendingChangesStart: Map<string, ISerializedInterval[]>;
830
- 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[]>();
831
1047
 
832
1048
  public get attached(): boolean {
833
1049
  return !!this.localCollection;
834
1050
  }
835
1051
 
836
- 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,
837
1056
  private readonly emitter: IValueOpEmitter,
838
- serializedIntervals: ISerializedInterval[]) {
1057
+ serializedIntervals: ISerializedInterval[] | ISerializedIntervalCollectionV2,
1058
+ ) {
839
1059
  super();
840
- 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
+ }
841
1067
  }
842
1068
 
843
1069
  public attachGraph(client: Client, label: string) {
844
1070
  if (this.attached) {
845
- throw new Error("Only supports one Sequence attach");
1071
+ throw new LoggingError("Only supports one Sequence attach");
846
1072
  }
847
1073
 
848
1074
  if ((client === undefined) && (this.requiresClient)) {
849
- throw new Error("Client required for this collection");
1075
+ throw new LoggingError("Client required for this collection");
850
1076
  }
851
1077
 
852
1078
  // Instantiate the local interval collection based on the saved intervals
853
1079
  this.client = client;
854
- 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
+ );
855
1086
  if (this.savedSerializedIntervals) {
856
1087
  for (const serializedInterval of this.savedSerializedIntervals) {
857
1088
  this.localCollection.ensureSerializedId(serializedInterval);
@@ -865,13 +1096,28 @@ export class IntervalCollection<TInterval extends ISerializableInterval>
865
1096
  this.savedSerializedIntervals = undefined;
866
1097
  }
867
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
+
868
1106
  public getIntervalById(id: string) {
869
1107
  if (!this.attached) {
870
- throw new Error("attach must be called before accessing intervals");
1108
+ throw new LoggingError("attach must be called before accessing intervals");
871
1109
  }
872
1110
  return this.localCollection.getIntervalById(id);
873
1111
  }
874
1112
 
1113
+ /**
1114
+ * Create a new interval and add it to the collection
1115
+ * @param start - interval start position
1116
+ * @param end - interval end position
1117
+ * @param intervalType - type of the interval. All intervals are SlideOnRemove. Intervals may not be Transient.
1118
+ * @param props - properties of the interval
1119
+ * @returns - the created interval
1120
+ */
875
1121
  public add(
876
1122
  start: number,
877
1123
  end: number,
@@ -879,7 +1125,10 @@ export class IntervalCollection<TInterval extends ISerializableInterval>
879
1125
  props?: PropertySet,
880
1126
  ) {
881
1127
  if (!this.attached) {
882
- throw new Error("attach must be called prior to adding intervals");
1128
+ throw new LoggingError("attach must be called prior to adding intervals");
1129
+ }
1130
+ if (intervalType & IntervalType.Transient) {
1131
+ throw new LoggingError("Can not add transient intervals");
883
1132
  }
884
1133
 
885
1134
  const interval: TInterval = this.localCollection.addInterval(start, end, intervalType, props);
@@ -893,7 +1142,7 @@ export class IntervalCollection<TInterval extends ISerializableInterval>
893
1142
  start,
894
1143
  };
895
1144
  // Local ops get submitted to the server. Remote ops have the deserializer run.
896
- this.emitter.emit("add", undefined, serializedInterval);
1145
+ this.emitter.emit("add", undefined, serializedInterval, { localSeq: this.getNextLocalSeq() });
897
1146
  }
898
1147
 
899
1148
  this.emit("addInterval", interval, true, undefined);
@@ -901,13 +1150,19 @@ export class IntervalCollection<TInterval extends ISerializableInterval>
901
1150
  return interval;
902
1151
  }
903
1152
 
904
- private deleteExistingInterval(interval: TInterval, local: boolean, op: ISequencedDocumentMessage) {
1153
+ private deleteExistingInterval(interval: TInterval, local: boolean, op?: ISequencedDocumentMessage) {
905
1154
  // The given interval is known to exist in the collection.
906
1155
  this.localCollection.removeExistingInterval(interval);
1156
+
907
1157
  if (interval) {
908
1158
  // Local ops get submitted to the server. Remote ops have the deserializer run.
909
1159
  if (local) {
910
- 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
+ );
911
1166
  } else {
912
1167
  if (this.onDeserialize) {
913
1168
  this.onDeserialize(interval);
@@ -928,13 +1183,13 @@ export class IntervalCollection<TInterval extends ISerializableInterval>
928
1183
 
929
1184
  public changeProperties(id: string, props: PropertySet) {
930
1185
  if (!this.attached) {
931
- throw new Error("Attach must be called before accessing intervals");
1186
+ throw new LoggingError("Attach must be called before accessing intervals");
932
1187
  }
933
1188
  if (typeof (id) !== "string") {
934
- 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");
935
1190
  }
936
1191
  if (!props) {
937
- throw new Error("changeProperties should be called with a property set");
1192
+ throw new LoggingError("changeProperties should be called with a property set");
938
1193
  }
939
1194
 
940
1195
  const interval = this.getIntervalById(id);
@@ -942,12 +1197,15 @@ export class IntervalCollection<TInterval extends ISerializableInterval>
942
1197
  // Pass Unassigned as the sequence number to indicate that this is a local op that is waiting for an ack.
943
1198
  const deltaProps = interval.addProperties(props, true, UnassignedSequenceNumber);
944
1199
  const serializedInterval: ISerializedInterval = interval.serialize(this.client);
945
- // Emit a change op that will only change properties. Add the ID to the property bag provided by the caller.
946
- serializedInterval.start = undefined;
947
- 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
+
948
1206
  serializedInterval.properties = props;
949
1207
  serializedInterval.properties[reservedIntervalIdKey] = interval.getIntervalId();
950
- this.emitter.emit("change", undefined, serializedInterval);
1208
+ this.emitter.emit("change", undefined, serializedInterval, { localSeq: this.getNextLocalSeq() });
951
1209
  this.emit("propertyChanged", interval, deltaProps);
952
1210
  }
953
1211
  this.emit("changeInterval", interval, true, undefined);
@@ -955,42 +1213,39 @@ export class IntervalCollection<TInterval extends ISerializableInterval>
955
1213
 
956
1214
  public change(id: string, start?: number, end?: number): TInterval | undefined {
957
1215
  if (!this.attached) {
958
- throw new Error("Attach must be called before accessing intervals");
1216
+ throw new LoggingError("Attach must be called before accessing intervals");
959
1217
  }
1218
+
1219
+ // Force id to be a string.
960
1220
  if (typeof (id) !== "string") {
961
- 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");
962
1222
  }
963
1223
 
964
- // Force id to be a string.
965
1224
  const interval = this.getIntervalById(id);
966
1225
  if (interval) {
967
- this.localCollection.changeInterval(interval, start, end);
1226
+ const newInterval = this.localCollection.changeInterval(interval, start, end);
968
1227
  const serializedInterval: ISerializedInterval = interval.serialize(this.client);
969
1228
  serializedInterval.start = start;
970
1229
  serializedInterval.end = end;
971
1230
  // Emit a property bag containing only the ID, as we don't intend for this op to change any properties.
972
1231
  serializedInterval.properties =
973
- {
974
- [reservedIntervalIdKey]: interval.getIntervalId(),
975
- };
976
- this.emitter.emit("change", undefined, serializedInterval);
1232
+ {
1233
+ [reservedIntervalIdKey]: interval.getIntervalId(),
1234
+ };
1235
+ this.emitter.emit("change", undefined, serializedInterval, { localSeq: this.getNextLocalSeq() });
977
1236
  this.addPendingChange(id, serializedInterval);
1237
+ this.emit("changeInterval", newInterval, true, undefined);
1238
+ return newInterval;
978
1239
  }
979
- this.emit("changeInterval", interval, true, undefined);
980
- return interval;
1240
+ // No interval to change
1241
+ return undefined;
981
1242
  }
982
1243
 
983
1244
  private addPendingChange(id: string, serializedInterval: ISerializedInterval) {
984
1245
  if (serializedInterval.start !== undefined) {
985
- if (!this.pendingChangesStart) {
986
- this.pendingChangesStart = new Map<string, ISerializedInterval[]>();
987
- }
988
1246
  this.addPendingChangeHelper(id, this.pendingChangesStart, serializedInterval);
989
1247
  }
990
1248
  if (serializedInterval.end !== undefined) {
991
- if (!this.pendingChangesEnd) {
992
- this.pendingChangesEnd = new Map<string, ISerializedInterval[]>();
993
- }
994
1249
  this.addPendingChangeHelper(id, this.pendingChangesEnd, serializedInterval);
995
1250
  }
996
1251
  }
@@ -1000,7 +1255,7 @@ export class IntervalCollection<TInterval extends ISerializableInterval>
1000
1255
  pendingChanges: Map<string, ISerializedInterval[]>,
1001
1256
  serializedInterval: ISerializedInterval,
1002
1257
  ) {
1003
- let entries: ISerializedInterval[] = pendingChanges.get(id);
1258
+ let entries: ISerializedInterval[] | undefined = pendingChanges.get(id);
1004
1259
  if (!entries) {
1005
1260
  entries = [];
1006
1261
  pendingChanges.set(id, entries);
@@ -1010,7 +1265,7 @@ export class IntervalCollection<TInterval extends ISerializableInterval>
1010
1265
 
1011
1266
  private removePendingChange(serializedInterval: ISerializedInterval) {
1012
1267
  // Change ops always have an ID.
1013
- const id: string = serializedInterval.properties[reservedIntervalIdKey];
1268
+ const id: string = serializedInterval.properties?.[reservedIntervalIdKey];
1014
1269
  if (serializedInterval.start !== undefined) {
1015
1270
  this.removePendingChangeHelper(id, this.pendingChangesStart, serializedInterval);
1016
1271
  }
@@ -1024,26 +1279,26 @@ export class IntervalCollection<TInterval extends ISerializableInterval>
1024
1279
  pendingChanges: Map<string, ISerializedInterval[]>,
1025
1280
  serializedInterval: ISerializedInterval,
1026
1281
  ) {
1027
- const entries = pendingChanges?.get(id);
1282
+ const entries = pendingChanges.get(id);
1028
1283
  if (entries) {
1029
1284
  const pendingChange = entries.shift();
1030
1285
  if (entries.length === 0) {
1031
1286
  pendingChanges.delete(id);
1032
1287
  }
1033
- if (pendingChange.start !== serializedInterval.start ||
1034
- pendingChange.end !== serializedInterval.end) {
1035
- 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");
1036
1291
  }
1037
1292
  }
1038
1293
  }
1039
1294
 
1040
1295
  private hasPendingChangeStart(id: string) {
1041
- const entries = this.pendingChangesStart?.get(id);
1296
+ const entries = this.pendingChangesStart.get(id);
1042
1297
  return entries && entries.length !== 0;
1043
1298
  }
1044
1299
 
1045
1300
  private hasPendingChangeEnd(id: string) {
1046
- const entries = this.pendingChangesEnd?.get(id);
1301
+ const entries = this.pendingChangesEnd.get(id);
1047
1302
  return entries && entries.length !== 0;
1048
1303
  }
1049
1304
 
@@ -1055,7 +1310,7 @@ export class IntervalCollection<TInterval extends ISerializableInterval>
1055
1310
  /** @internal */
1056
1311
  public ackChange(serializedInterval: ISerializedInterval, local: boolean, op: ISequencedDocumentMessage) {
1057
1312
  if (!this.attached) {
1058
- throw new Error("Attach must be called before accessing intervals");
1313
+ throw new LoggingError("Attach must be called before accessing intervals");
1059
1314
  }
1060
1315
 
1061
1316
  let interval: TInterval | undefined;
@@ -1063,15 +1318,17 @@ export class IntervalCollection<TInterval extends ISerializableInterval>
1063
1318
  if (local) {
1064
1319
  // This is an ack from the server. Remove the pending change.
1065
1320
  this.removePendingChange(serializedInterval);
1066
- const id: string = serializedInterval.properties[reservedIntervalIdKey];
1321
+ const id: string = serializedInterval.properties?.[reservedIntervalIdKey];
1067
1322
  interval = this.getIntervalById(id);
1068
1323
  if (interval) {
1069
1324
  // Let the propertyManager prune its pending change-properties set.
1070
1325
  interval.propertyManager?.ackPendingProperties(
1071
1326
  {
1072
1327
  type: MergeTreeDeltaType.ANNOTATE,
1073
- props: serializedInterval.properties,
1328
+ props: serializedInterval.properties ?? {},
1074
1329
  });
1330
+
1331
+ this.ackInterval(interval, op);
1075
1332
  }
1076
1333
  } else {
1077
1334
  // If there are pending changes with this ID, don't apply the remote start/end change, as the local ack
@@ -1110,7 +1367,7 @@ export class IntervalCollection<TInterval extends ISerializableInterval>
1110
1367
 
1111
1368
  public addConflictResolver(conflictResolver: IntervalConflictResolver<TInterval>): void {
1112
1369
  if (!this.attached) {
1113
- throw new Error("attachSequence must be called");
1370
+ throw new LoggingError("attachSequence must be called");
1114
1371
  }
1115
1372
  this.localCollection.addConflictResolver(conflictResolver);
1116
1373
  }
@@ -1126,10 +1383,111 @@ export class IntervalCollection<TInterval extends ISerializableInterval>
1126
1383
 
1127
1384
  // Trigger the async prepare work across all values in the collection
1128
1385
  this.localCollection.map((interval) => {
1129
- this.onDeserialize(interval);
1386
+ onDeserialize(interval);
1130
1387
  });
1131
1388
  }
1132
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
+
1421
+ private getSlideToSegment(lref: LocalReference) {
1422
+ const segoff = { segment: lref.segment, offset: lref.offset };
1423
+ const newSegoff = this.client.getSlideToSegment(segoff);
1424
+ const value: { segment: ISegment | undefined; offset: number | undefined; } | undefined
1425
+ = (segoff.segment === newSegoff.segment && segoff.offset === newSegoff.offset) ? undefined : newSegoff;
1426
+ return value;
1427
+ }
1428
+
1429
+ private setSlideOnRemove(lref: LocalReference) {
1430
+ let refType = lref.refType;
1431
+ refType = refType & ~ReferenceType.StayOnRemove;
1432
+ refType = refType | ReferenceType.SlideOnRemove;
1433
+ lref.refType = refType;
1434
+ }
1435
+
1436
+ private ackInterval(interval: TInterval, op: ISequencedDocumentMessage) {
1437
+ // in current usage, interval is always a SequenceInterval
1438
+ if (!(interval instanceof SequenceInterval)) {
1439
+ return;
1440
+ }
1441
+
1442
+ if (!refTypeIncludesFlag(interval.start, ReferenceType.StayOnRemove) &&
1443
+ !refTypeIncludesFlag(interval.end, ReferenceType.StayOnRemove)) {
1444
+ return;
1445
+ }
1446
+
1447
+ const newStart = this.getSlideToSegment(interval.start);
1448
+ const newEnd = this.getSlideToSegment(interval.end);
1449
+
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.
1469
+ this.localCollection.removeExistingInterval(interval);
1470
+
1471
+ if (needsStartUpdate) {
1472
+ const props = interval.start.properties;
1473
+ this.client.removeLocalReferencePosition(interval.start);
1474
+ interval.start = createPositionReferenceFromSegoff(this.client, newStart, interval.start.refType, op);
1475
+ if (props) {
1476
+ interval.start.addProperties(props);
1477
+ }
1478
+ }
1479
+ if (needsEndUpdate) {
1480
+ const props = interval.end.properties;
1481
+ this.client.removeLocalReferencePosition(interval.end);
1482
+ interval.end = createPositionReferenceFromSegoff(this.client, newEnd, interval.end.refType, op);
1483
+ if (props) {
1484
+ interval.end.addProperties(props);
1485
+ }
1486
+ }
1487
+ this.localCollection.add(interval);
1488
+ }
1489
+ }
1490
+
1133
1491
  /** @deprecated - use ackAdd */
1134
1492
  public addInternal(
1135
1493
  serializedInterval: ISerializedInterval,
@@ -1144,13 +1502,16 @@ export class IntervalCollection<TInterval extends ISerializableInterval>
1144
1502
  local: boolean,
1145
1503
  op: ISequencedDocumentMessage) {
1146
1504
  if (local) {
1147
- // Local ops were applied when the message was created and there's no "pending add"
1148
- // state to bookkeep
1505
+ const id: string = serializedInterval.properties?.[reservedIntervalIdKey];
1506
+ const localInterval = this.getIntervalById(id);
1507
+ if (localInterval) {
1508
+ this.ackInterval(localInterval, op);
1509
+ }
1149
1510
  return;
1150
1511
  }
1151
1512
 
1152
1513
  if (!this.attached) {
1153
- throw new Error("attachSequence must be called");
1514
+ throw new LoggingError("attachSequence must be called");
1154
1515
  }
1155
1516
 
1156
1517
  this.localCollection.ensureSerializedId(serializedInterval);
@@ -1194,19 +1555,22 @@ export class IntervalCollection<TInterval extends ISerializableInterval>
1194
1555
  }
1195
1556
 
1196
1557
  if (!this.attached) {
1197
- throw new Error("attach must be called prior to deleting intervals");
1558
+ throw new LoggingError("attach must be called prior to deleting intervals");
1198
1559
  }
1199
1560
 
1200
- this.localCollection.ensureSerializedId(serializedInterval);
1201
- const interval = this.localCollection.getIntervalById(serializedInterval.properties[reservedIntervalIdKey]);
1561
+ const id = this.localCollection.ensureSerializedId(serializedInterval);
1562
+ const interval = this.localCollection.getIntervalById(id);
1202
1563
  if (interval) {
1203
1564
  this.deleteExistingInterval(interval, local, op);
1204
1565
  }
1205
1566
  }
1206
1567
 
1207
- public serializeInternal() {
1568
+ /**
1569
+ * @internal
1570
+ */
1571
+ public serializeInternal(): ISerializedIntervalCollectionV2 {
1208
1572
  if (!this.attached) {
1209
- throw new Error("attachSequence must be called");
1573
+ throw new LoggingError("attachSequence must be called");
1210
1574
  }
1211
1575
 
1212
1576
  return this.localCollection.serialize();
@@ -1251,7 +1615,7 @@ export class IntervalCollection<TInterval extends ISerializableInterval>
1251
1615
 
1252
1616
  public findOverlappingIntervals(startPosition: number, endPosition: number): TInterval[] {
1253
1617
  if (!this.attached) {
1254
- throw new Error("attachSequence must be called");
1618
+ throw new LoggingError("attachSequence must be called");
1255
1619
  }
1256
1620
 
1257
1621
  return this.localCollection.findOverlappingIntervals(startPosition, endPosition);
@@ -1259,7 +1623,7 @@ export class IntervalCollection<TInterval extends ISerializableInterval>
1259
1623
 
1260
1624
  public map(fn: (interval: TInterval) => void) {
1261
1625
  if (!this.attached) {
1262
- throw new Error("attachSequence must be called");
1626
+ throw new LoggingError("attachSequence must be called");
1263
1627
  }
1264
1628
 
1265
1629
  this.localCollection.map(fn);
@@ -1267,7 +1631,7 @@ export class IntervalCollection<TInterval extends ISerializableInterval>
1267
1631
 
1268
1632
  public previousInterval(pos: number): TInterval {
1269
1633
  if (!this.attached) {
1270
- throw new Error("attachSequence must be called");
1634
+ throw new LoggingError("attachSequence must be called");
1271
1635
  }
1272
1636
 
1273
1637
  return this.localCollection.previousInterval(pos);
@@ -1275,7 +1639,7 @@ export class IntervalCollection<TInterval extends ISerializableInterval>
1275
1639
 
1276
1640
  public nextInterval(pos: number): TInterval {
1277
1641
  if (!this.attached) {
1278
- throw new Error("attachSequence must be called");
1642
+ throw new LoggingError("attachSequence must be called");
1279
1643
  }
1280
1644
 
1281
1645
  return this.localCollection.nextInterval(pos);