@fluidframework/sequence 2.0.0-dev.4.1.0.148229 → 2.0.0-dev.4.3.0.157531

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.
@@ -84,9 +84,7 @@ export class Interval {
84
84
  getIntervalId() {
85
85
  var _a;
86
86
  const id = (_a = this.properties) === null || _a === void 0 ? void 0 : _a[reservedIntervalIdKey];
87
- if (id === undefined) {
88
- return undefined;
89
- }
87
+ assert(id !== undefined, 0x5e1 /* interval ID should not be undefined */);
90
88
  return `${id}`;
91
89
  }
92
90
  /**
@@ -228,6 +226,22 @@ export class Interval {
228
226
  * Interval impelmentation whose ends are associated with positions in a mutatable sequence.
229
227
  * As such, when content is inserted into the middle of the interval, the interval expands to
230
228
  * include that content.
229
+ *
230
+ * @remarks - The endpoint's position should be treated exclusively to get reasonable behavior--i.e.
231
+ * an interval referring to "hello" in "hello world" should have a start position of 0 and an end
232
+ * position of 5.
233
+ *
234
+ * To see why, consider what happens if "llo wor" is removed from the string to make "held".
235
+ * The interval's startpoint remains on the "h" (it isn't altered), but the interval's endpoint
236
+ * slides forward to the next unremoved position, which is the "l" in "held".
237
+ * Users would generally expect the interval to now refer to "he" (as it is the subset of content
238
+ * remaining after the removal), hence the "l" should be excluded.
239
+ * If the interval endpoint was treated inclusively, the interval would now refer to "hel", which
240
+ * is undesirable.
241
+ *
242
+ * Since the end of an interval is treated exclusively but cannot be greater than or equal to the
243
+ * length of the associated sequence, application models which leverage interval collections should
244
+ * consider inserting a marker at the end of the sequence to represent the end of the content.
231
245
  */
232
246
  export class SequenceInterval {
233
247
  constructor(client,
@@ -356,9 +370,7 @@ export class SequenceInterval {
356
370
  getIntervalId() {
357
371
  var _a;
358
372
  const id = (_a = this.properties) === null || _a === void 0 ? void 0 : _a[reservedIntervalIdKey];
359
- if (id === undefined) {
360
- return undefined;
361
- }
373
+ assert(id !== undefined, 0x5e2 /* interval ID should not be undefined */);
362
374
  return `${id}`;
363
375
  }
364
376
  /**
@@ -378,7 +390,6 @@ export class SequenceInterval {
378
390
  }
379
391
  /**
380
392
  * @returns whether this interval overlaps two numerical positions.
381
- * @remarks - this is currently strict overlap, which doesn't align with the endpoint treatment of`.overlaps()`
382
393
  */
383
394
  overlapsPos(bstart, bend) {
384
395
  const startPos = this.client.localReferencePositionToPosition(this.start);
@@ -428,7 +439,7 @@ export class SequenceInterval {
428
439
  }
429
440
  }
430
441
  }
431
- function createPositionReferenceFromSegoff(client, segoff, refType, op, localSeq) {
442
+ function createPositionReferenceFromSegoff(client, segoff, refType, op, localSeq, fromSnapshot) {
432
443
  if (segoff.segment) {
433
444
  const ref = client.createLocalReferencePosition(segoff.segment, segoff.offset, refType, undefined);
434
445
  return ref;
@@ -438,7 +449,10 @@ function createPositionReferenceFromSegoff(client, segoff, refType, op, localSeq
438
449
  // - References coming from a remote client (location may have been concurrently removed)
439
450
  // - References being rebased to a new sequence number
440
451
  // (segment they originally referred to may have been removed with no suitable replacement)
441
- if (!op && !localSeq && !refTypeIncludesFlag(refType, ReferenceType.Transient)) {
452
+ if (!op &&
453
+ !localSeq &&
454
+ !fromSnapshot &&
455
+ !refTypeIncludesFlag(refType, ReferenceType.Transient)) {
442
456
  throw new UsageError("Non-transient references need segment");
443
457
  }
444
458
  return createDetachedLocalReferencePosition(refType);
@@ -457,7 +471,7 @@ function createPositionReference(client, pos, refType, op, fromSnapshot, localSe
457
471
  assert((refType & ReferenceType.SlideOnRemove) === 0 || !!fromSnapshot, 0x2f6 /* SlideOnRemove references must be op created */);
458
472
  segoff = client.getContainingSegment(pos, undefined, localSeq);
459
473
  }
460
- return createPositionReferenceFromSegoff(client, segoff, refType, op, localSeq);
474
+ return createPositionReferenceFromSegoff(client, segoff, refType, op, localSeq, fromSnapshot);
461
475
  }
462
476
  export function createSequenceInterval(label, start, end, client, intervalType, op, fromSnapshot) {
463
477
  let beginRefType = ReferenceType.RangeBegin;
@@ -493,82 +507,23 @@ export function createSequenceInterval(label, start, end, client, intervalType,
493
507
  const ival = new SequenceInterval(client, startLref, endLref, intervalType, rangeProp);
494
508
  return ival;
495
509
  }
496
- export function defaultIntervalConflictResolver(a, b) {
497
- a.addPropertySet(b.properties);
498
- return a;
499
- }
500
- export function createIntervalIndex(conflict) {
510
+ export function createIntervalIndex() {
501
511
  const helpers = {
502
512
  compareEnds: compareIntervalEnds,
503
513
  create: createInterval,
504
514
  };
505
515
  const lc = new LocalIntervalCollection(undefined, "", helpers);
506
- if (conflict) {
507
- lc.addConflictResolver(conflict);
508
- }
509
- else {
510
- lc.addConflictResolver(defaultIntervalConflictResolver);
511
- }
512
516
  return lc;
513
517
  }
514
- export class LocalIntervalCollection {
515
- constructor(client, label, helpers,
516
- /** Callback invoked each time one of the endpoints of an interval slides. */
517
- onPositionChange) {
518
+ class OverlappingIntervalsIndex {
519
+ constructor(client, helpers) {
518
520
  this.client = client;
519
- this.label = label;
520
521
  this.helpers = helpers;
521
- this.onPositionChange = onPositionChange;
522
522
  this.intervalTree = new IntervalTree();
523
- this.intervalIdMap = new Map();
524
- // eslint-disable-next-line @typescript-eslint/unbound-method
525
- this.endIntervalTree = new RedBlackTree(helpers.compareEnds);
526
- }
527
- addConflictResolver(conflictResolver) {
528
- this.conflictResolver = conflictResolver;
529
- this.endConflictResolver = (key, currentKey) => {
530
- const ival = conflictResolver(key, currentKey);
531
- return {
532
- data: ival,
533
- key: ival,
534
- };
535
- };
536
523
  }
537
524
  map(fn) {
538
525
  this.intervalTree.map(fn);
539
526
  }
540
- createLegacyId(start, end) {
541
- // Create a non-unique ID based on start and end to be used on intervals that come from legacy clients
542
- // without ID's.
543
- return `${LocalIntervalCollection.legacyIdPrefix}${start}-${end}`;
544
- }
545
- /**
546
- * Validates that a serialized interval has the ID property. Creates an ID
547
- * if one does not already exist
548
- *
549
- * @param serializedInterval - The interval to be checked
550
- * @returns The interval's existing or newly created id
551
- */
552
- ensureSerializedId(serializedInterval) {
553
- var _a;
554
- let id = (_a = serializedInterval.properties) === null || _a === void 0 ? void 0 : _a[reservedIntervalIdKey];
555
- if (id === undefined) {
556
- // An interval came over the wire without an ID, so create a non-unique one based on start/end.
557
- // This will allow all clients to refer to this interval consistently.
558
- id = this.createLegacyId(serializedInterval.start, serializedInterval.end);
559
- const newProps = {
560
- [reservedIntervalIdKey]: id,
561
- };
562
- serializedInterval.properties = addProperties(serializedInterval.properties, newProps);
563
- }
564
- // Make the ID immutable for safety's sake.
565
- Object.defineProperty(serializedInterval.properties, reservedIntervalIdKey, {
566
- configurable: false,
567
- enumerable: true,
568
- writable: false,
569
- });
570
- return id;
571
- }
572
527
  mapUntil(fn) {
573
528
  this.intervalTree.mapUntil(fn);
574
529
  }
@@ -635,7 +590,7 @@ export class LocalIntervalCollection {
635
590
  }
636
591
  /**
637
592
  * @returns an array of all intervals contained in this collection that overlap the range
638
- * `[startPosition, endPosition]`.
593
+ * `[startPosition, endPosition)`.
639
594
  */
640
595
  findOverlappingIntervals(startPosition, endPosition) {
641
596
  if (endPosition < startPosition || this.intervalTree.intervals.isEmpty()) {
@@ -645,6 +600,47 @@ export class LocalIntervalCollection {
645
600
  const overlappingIntervalNodes = this.intervalTree.match(transientInterval);
646
601
  return overlappingIntervalNodes.map((node) => node.key);
647
602
  }
603
+ remove(interval) {
604
+ this.intervalTree.removeExisting(interval);
605
+ }
606
+ add(interval) {
607
+ this.intervalTree.put(interval);
608
+ }
609
+ }
610
+ class IdIntervalIndex {
611
+ constructor() {
612
+ this.intervalIdMap = new Map();
613
+ }
614
+ add(interval) {
615
+ const id = interval.getIntervalId();
616
+ assert(id !== undefined, 0x2c0 /* "ID must be created before adding interval to collection" */);
617
+ // Make the ID immutable.
618
+ Object.defineProperty(interval.properties, reservedIntervalIdKey, {
619
+ configurable: false,
620
+ enumerable: true,
621
+ writable: false,
622
+ });
623
+ this.intervalIdMap.set(id, interval);
624
+ }
625
+ remove(interval) {
626
+ const id = interval.getIntervalId();
627
+ assert(id !== undefined, 0x311 /* expected id to exist on interval */);
628
+ this.intervalIdMap.delete(id);
629
+ }
630
+ getIntervalById(id) {
631
+ return this.intervalIdMap.get(id);
632
+ }
633
+ [Symbol.iterator]() {
634
+ return this.intervalIdMap.values();
635
+ }
636
+ }
637
+ class EndpointIndex {
638
+ constructor(client, helpers) {
639
+ this.client = client;
640
+ this.helpers = helpers;
641
+ // eslint-disable-next-line @typescript-eslint/unbound-method
642
+ this.endIntervalTree = new RedBlackTree(helpers.compareEnds);
643
+ }
648
644
  previousInterval(pos) {
649
645
  const transientInterval = this.helpers.create("transient", pos, pos, this.client, IntervalType.Transient);
650
646
  const rbNode = this.endIntervalTree.floor(transientInterval);
@@ -659,21 +655,70 @@ export class LocalIntervalCollection {
659
655
  return rbNode.data;
660
656
  }
661
657
  }
662
- removeInterval(startPosition, endPosition) {
663
- const transientInterval = this.helpers.create("transient", startPosition, endPosition, this.client, IntervalType.Transient);
664
- this.intervalTree.remove(transientInterval);
665
- this.endIntervalTree.remove(transientInterval);
666
- return transientInterval;
658
+ add(interval) {
659
+ this.endIntervalTree.put(interval, interval);
667
660
  }
668
- removeIntervalFromIndex(interval) {
669
- this.intervalTree.removeExisting(interval);
661
+ remove(interval) {
670
662
  this.endIntervalTree.remove(interval);
671
- const id = interval.getIntervalId();
672
- assert(id !== undefined, 0x311 /* expected id to exist on interval */);
673
- this.intervalIdMap.delete(id);
663
+ }
664
+ }
665
+ export class LocalIntervalCollection {
666
+ constructor(client, label, helpers,
667
+ /** Callback invoked each time one of the endpoints of an interval slides. */
668
+ onPositionChange) {
669
+ this.client = client;
670
+ this.label = label;
671
+ this.helpers = helpers;
672
+ this.onPositionChange = onPositionChange;
673
+ this.overlappingIntervalsIndex = new OverlappingIntervalsIndex(client, helpers);
674
+ this.idIntervalIndex = new IdIntervalIndex();
675
+ this.endIntervalIndex = new EndpointIndex(client, helpers);
676
+ this.indexes = [
677
+ this.overlappingIntervalsIndex,
678
+ this.idIntervalIndex,
679
+ this.endIntervalIndex,
680
+ ];
681
+ }
682
+ createLegacyId(start, end) {
683
+ // Create a non-unique ID based on start and end to be used on intervals that come from legacy clients
684
+ // without ID's.
685
+ return `${LocalIntervalCollection.legacyIdPrefix}${start}-${end}`;
686
+ }
687
+ /**
688
+ * Validates that a serialized interval has the ID property. Creates an ID
689
+ * if one does not already exist
690
+ *
691
+ * @param serializedInterval - The interval to be checked
692
+ * @returns The interval's existing or newly created id
693
+ */
694
+ ensureSerializedId(serializedInterval) {
695
+ var _a;
696
+ let id = (_a = serializedInterval.properties) === null || _a === void 0 ? void 0 : _a[reservedIntervalIdKey];
697
+ if (id === undefined) {
698
+ // Back-compat: 0.39 and earlier did not have IDs on intervals. If an interval from such a client
699
+ // comes over the wire, create a non-unique one based on start/end.
700
+ // This will allow all clients to refer to this interval consistently.
701
+ id = this.createLegacyId(serializedInterval.start, serializedInterval.end);
702
+ const newProps = {
703
+ [reservedIntervalIdKey]: id,
704
+ };
705
+ serializedInterval.properties = addProperties(serializedInterval.properties, newProps);
706
+ }
707
+ // Make the ID immutable for safety's sake.
708
+ Object.defineProperty(serializedInterval.properties, reservedIntervalIdKey, {
709
+ configurable: false,
710
+ enumerable: true,
711
+ writable: false,
712
+ });
713
+ return id;
714
+ }
715
+ removeIntervalFromIndexes(interval) {
716
+ for (const index of this.indexes) {
717
+ index.remove(interval);
718
+ }
674
719
  }
675
720
  removeExistingInterval(interval) {
676
- this.removeIntervalFromIndex(interval);
721
+ this.removeIntervalFromIndexes(interval);
677
722
  this.removeIntervalListeners(interval);
678
723
  }
679
724
  createInterval(start, end, intervalType, op) {
@@ -701,27 +746,16 @@ export class LocalIntervalCollection {
701
746
  interval.end.addProperties({ interval });
702
747
  }
703
748
  }
704
- addIntervalToIndex(interval) {
705
- const id = interval.getIntervalId();
706
- assert(id !== undefined, 0x2c0 /* "ID must be created before adding interval to collection" */);
707
- // Make the ID immutable.
708
- Object.defineProperty(interval.properties, reservedIntervalIdKey, {
709
- configurable: false,
710
- enumerable: true,
711
- writable: false,
712
- });
713
- this.intervalTree.put(interval, this.conflictResolver);
714
- this.endIntervalTree.put(interval, interval, this.endConflictResolver);
715
- this.intervalIdMap.set(id, interval);
749
+ addIntervalToIndexes(interval) {
750
+ for (const index of this.indexes) {
751
+ index.add(interval);
752
+ }
716
753
  }
717
754
  add(interval) {
718
755
  this.linkEndpointsToInterval(interval);
719
- this.addIntervalToIndex(interval);
756
+ this.addIntervalToIndexes(interval);
720
757
  this.addIntervalListeners(interval);
721
758
  }
722
- getIntervalById(id) {
723
- return this.intervalIdMap.get(id);
724
- }
725
759
  changeInterval(interval, start, end, op, localSeq) {
726
760
  const newInterval = interval.modify(this.label, start, end, op, localSeq);
727
761
  if (newInterval) {
@@ -731,10 +765,9 @@ export class LocalIntervalCollection {
731
765
  return newInterval;
732
766
  }
733
767
  serialize() {
734
- const intervals = this.intervalTree.intervals.keys();
735
768
  return {
736
769
  label: this.label,
737
- intervals: intervals.map((interval) => compressInterval(interval.serialize())),
770
+ intervals: Array.from(this.idIntervalIndex, (interval) => compressInterval(interval.serialize())),
738
771
  version: 2,
739
772
  };
740
773
  }
@@ -759,14 +792,14 @@ export class LocalIntervalCollection {
759
792
  previousInterval = interval.clone();
760
793
  previousInterval.start = cloneRef(previousInterval.start);
761
794
  previousInterval.end = cloneRef(previousInterval.end);
762
- this.removeIntervalFromIndex(interval);
795
+ this.removeIntervalFromIndexes(interval);
763
796
  }
764
797
  }, () => {
765
798
  var _a;
766
799
  assert(previousInterval !== undefined, 0x3fa /* Invalid interleaving of before/after slide */);
767
800
  pendingChanges--;
768
801
  if (pendingChanges === 0) {
769
- this.addIntervalToIndex(interval);
802
+ this.addIntervalToIndexes(interval);
770
803
  (_a = this.onPositionChange) === null || _a === void 0 ? void 0 : _a.call(this, interval, previousInterval);
771
804
  previousInterval = undefined;
772
805
  }
@@ -1041,15 +1074,17 @@ export class IntervalCollection extends TypedEventEmitter {
1041
1074
  if (!this.localCollection) {
1042
1075
  throw new LoggingError("attach must be called before accessing intervals");
1043
1076
  }
1044
- return this.localCollection.getIntervalById(id);
1077
+ return this.localCollection.idIntervalIndex.getIntervalById(id);
1045
1078
  }
1046
1079
  /**
1047
1080
  * Creates a new interval and add it to the collection.
1048
- * @param start - interval start position
1049
- * @param end - interval end position
1081
+ * @param start - interval start position (inclusive)
1082
+ * @param end - interval end position (exclusive)
1050
1083
  * @param intervalType - type of the interval. All intervals are SlideOnRemove. Intervals may not be Transient.
1051
1084
  * @param props - properties of the interval
1052
1085
  * @returns - the created interval
1086
+ * @remarks - See documentation on {@link SequenceInterval} for comments on interval endpoint semantics: there are subtleties
1087
+ * with how the current half-open behavior is represented.
1053
1088
  */
1054
1089
  add(start, end, intervalType, props) {
1055
1090
  var _a, _b;
@@ -1106,7 +1141,7 @@ export class IntervalCollection extends TypedEventEmitter {
1106
1141
  if (!this.localCollection) {
1107
1142
  throw new LoggingError("Attach must be called before accessing intervals");
1108
1143
  }
1109
- const interval = this.localCollection.getIntervalById(id);
1144
+ const interval = this.localCollection.idIntervalIndex.getIntervalById(id);
1110
1145
  if (interval) {
1111
1146
  this.deleteExistingInterval(interval, true, undefined);
1112
1147
  }
@@ -1293,14 +1328,19 @@ export class IntervalCollection extends TypedEventEmitter {
1293
1328
  }
1294
1329
  }
1295
1330
  }
1296
- addConflictResolver(conflictResolver) {
1331
+ /**
1332
+ * @deprecated - This functionality was useful when adding two intervals at the same start/end positions resulted
1333
+ * in a conflict. This is no longer the case (as of PR#6407), as interval collections support multiple intervals
1334
+ * at the same location and gives each interval a unique id.
1335
+ *
1336
+ * As such, the conflict resolver is never invoked and unnecessary. This API will be removed in an upcoming release.
1337
+ */
1338
+ addConflictResolver(_) {
1297
1339
  if (!this.localCollection) {
1298
1340
  throw new LoggingError("attachSequence must be called");
1299
1341
  }
1300
- this.localCollection.addConflictResolver(conflictResolver);
1301
1342
  }
1302
1343
  attachDeserializer(onDeserialize) {
1303
- var _a;
1304
1344
  // If no deserializer is specified can skip all processing work
1305
1345
  if (!onDeserialize) {
1306
1346
  return;
@@ -1308,9 +1348,9 @@ export class IntervalCollection extends TypedEventEmitter {
1308
1348
  // Start by storing the callbacks so that any subsequent modifications make use of them
1309
1349
  this.onDeserialize = onDeserialize;
1310
1350
  // Trigger the async prepare work across all values in the collection
1311
- (_a = this.localCollection) === null || _a === void 0 ? void 0 : _a.map((interval) => {
1312
- onDeserialize(interval);
1313
- });
1351
+ if (this.attached) {
1352
+ this.map(onDeserialize);
1353
+ }
1314
1354
  }
1315
1355
  /**
1316
1356
  * Returns new interval after rebasing. If undefined, the interval was
@@ -1331,7 +1371,7 @@ export class IntervalCollection extends TypedEventEmitter {
1331
1371
  const { intervalType, properties } = serializedInterval;
1332
1372
  const { start: startRebased, end: endRebased } = (_a = this.localSeqToRebasedInterval.get(localSeq)) !== null && _a !== void 0 ? _a : this.computeRebasedPositions(localSeq);
1333
1373
  const intervalId = properties === null || properties === void 0 ? void 0 : properties[reservedIntervalIdKey];
1334
- const localInterval = (_b = this.localCollection) === null || _b === void 0 ? void 0 : _b.getIntervalById(intervalId);
1374
+ const localInterval = (_b = this.localCollection) === null || _b === void 0 ? void 0 : _b.idIntervalIndex.getIntervalById(intervalId);
1335
1375
  const rebased = {
1336
1376
  start: startRebased,
1337
1377
  end: endRebased,
@@ -1484,7 +1524,7 @@ export class IntervalCollection extends TypedEventEmitter {
1484
1524
  throw new LoggingError("attach must be called prior to deleting intervals");
1485
1525
  }
1486
1526
  const id = this.localCollection.ensureSerializedId(serializedInterval);
1487
- const interval = this.localCollection.getIntervalById(id);
1527
+ const interval = this.localCollection.idIntervalIndex.getIntervalById(id);
1488
1528
  if (interval) {
1489
1529
  this.deleteExistingInterval(interval, local, op);
1490
1530
  }
@@ -1545,7 +1585,7 @@ export class IntervalCollection extends TypedEventEmitter {
1545
1585
  if (!this.localCollection) {
1546
1586
  return;
1547
1587
  }
1548
- this.localCollection.gatherIterationResults(results, iteratesForward, start, end);
1588
+ this.localCollection.overlappingIntervalsIndex.gatherIterationResults(results, iteratesForward, start, end);
1549
1589
  }
1550
1590
  /**
1551
1591
  * @returns an array of all intervals in this collection that overlap with the interval
@@ -1555,7 +1595,7 @@ export class IntervalCollection extends TypedEventEmitter {
1555
1595
  if (!this.localCollection) {
1556
1596
  throw new LoggingError("attachSequence must be called");
1557
1597
  }
1558
- return this.localCollection.findOverlappingIntervals(startPosition, endPosition);
1598
+ return this.localCollection.overlappingIntervalsIndex.findOverlappingIntervals(startPosition, endPosition);
1559
1599
  }
1560
1600
  /**
1561
1601
  * Applies a function to each interval in this collection.
@@ -1564,19 +1604,21 @@ export class IntervalCollection extends TypedEventEmitter {
1564
1604
  if (!this.localCollection) {
1565
1605
  throw new LoggingError("attachSequence must be called");
1566
1606
  }
1567
- this.localCollection.map(fn);
1607
+ for (const interval of this.localCollection.idIntervalIndex) {
1608
+ fn(interval);
1609
+ }
1568
1610
  }
1569
1611
  previousInterval(pos) {
1570
1612
  if (!this.localCollection) {
1571
1613
  throw new LoggingError("attachSequence must be called");
1572
1614
  }
1573
- return this.localCollection.previousInterval(pos);
1615
+ return this.localCollection.endIntervalIndex.previousInterval(pos);
1574
1616
  }
1575
1617
  nextInterval(pos) {
1576
1618
  if (!this.localCollection) {
1577
1619
  throw new LoggingError("attachSequence must be called");
1578
1620
  }
1579
- return this.localCollection.nextInterval(pos);
1621
+ return this.localCollection.endIntervalIndex.nextInterval(pos);
1580
1622
  }
1581
1623
  }
1582
1624
  /**