@fluidframework/sequence 2.50.0-345060 → 2.50.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/dist/intervalCollection.d.ts +4 -5
  3. package/dist/intervalCollection.d.ts.map +1 -1
  4. package/dist/intervalCollection.js +91 -95
  5. package/dist/intervalCollection.js.map +1 -1
  6. package/dist/intervalCollectionMapInterfaces.d.ts +1 -11
  7. package/dist/intervalCollectionMapInterfaces.d.ts.map +1 -1
  8. package/dist/intervalCollectionMapInterfaces.js.map +1 -1
  9. package/dist/intervals/sequenceInterval.d.ts +7 -3
  10. package/dist/intervals/sequenceInterval.d.ts.map +1 -1
  11. package/dist/intervals/sequenceInterval.js +72 -30
  12. package/dist/intervals/sequenceInterval.js.map +1 -1
  13. package/dist/packageVersion.d.ts +1 -1
  14. package/dist/packageVersion.d.ts.map +1 -1
  15. package/dist/packageVersion.js +1 -1
  16. package/dist/packageVersion.js.map +1 -1
  17. package/lib/intervalCollection.d.ts +4 -5
  18. package/lib/intervalCollection.d.ts.map +1 -1
  19. package/lib/intervalCollection.js +91 -95
  20. package/lib/intervalCollection.js.map +1 -1
  21. package/lib/intervalCollectionMapInterfaces.d.ts +1 -11
  22. package/lib/intervalCollectionMapInterfaces.d.ts.map +1 -1
  23. package/lib/intervalCollectionMapInterfaces.js.map +1 -1
  24. package/lib/intervals/sequenceInterval.d.ts +7 -3
  25. package/lib/intervals/sequenceInterval.d.ts.map +1 -1
  26. package/lib/intervals/sequenceInterval.js +74 -32
  27. package/lib/intervals/sequenceInterval.js.map +1 -1
  28. package/lib/packageVersion.d.ts +1 -1
  29. package/lib/packageVersion.d.ts.map +1 -1
  30. package/lib/packageVersion.js +1 -1
  31. package/lib/packageVersion.js.map +1 -1
  32. package/package.json +16 -16
  33. package/src/intervalCollection.ts +103 -103
  34. package/src/intervalCollectionMapInterfaces.ts +1 -9
  35. package/src/intervals/sequenceInterval.ts +88 -32
  36. package/src/packageVersion.ts +1 -1
@@ -197,7 +197,6 @@ export class LocalIntervalCollection {
197
197
  end: SequencePlace,
198
198
  props?: PropertySet,
199
199
  op?: ISequencedDocumentMessage,
200
- rollback?: boolean,
201
200
  ) {
202
201
  // This check is intended to prevent scenarios where a random interval is created and then
203
202
  // inserted into a collection. The aim is to ensure that the collection is created first
@@ -221,7 +220,7 @@ export class LocalIntervalCollection {
221
220
  undefined,
222
221
  this.options.mergeTreeReferencesCanSlideToEndpoint,
223
222
  props,
224
- rollback,
223
+ false,
225
224
  );
226
225
 
227
226
  this.add(interval);
@@ -693,6 +692,7 @@ type PendingChanges = Partial<
693
692
  endpointChanges?: DoublyLinkedList<
694
693
  IntervalAddLocalMetadata | IntervalChangeLocalMetadata
695
694
  >;
695
+ consensus: SequenceIntervalClass | undefined;
696
696
  }
697
697
  >
698
698
  >;
@@ -709,8 +709,7 @@ function removeMetadataFromPendingChanges(
709
709
 
710
710
  function clearEmptyPendingEntry(pendingChanges: PendingChanges, id: string) {
711
711
  const pending = pendingChanges[id];
712
- assert(pending !== undefined, 0xbbf /* pending must exist for local process */);
713
- if (pending.local.empty) {
712
+ if (pending !== undefined && pending.local.empty) {
714
713
  assert(
715
714
  pending.endpointChanges?.empty !== false,
716
715
  0xbc0 /* endpointChanges must be empty if not pending changes */,
@@ -718,6 +717,7 @@ function clearEmptyPendingEntry(pendingChanges: PendingChanges, id: string) {
718
717
  // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
719
718
  delete pendingChanges[id];
720
719
  }
720
+ return pending;
721
721
  }
722
722
 
723
723
  function hasEndpointChanges(
@@ -747,6 +747,7 @@ export class IntervalCollection
747
747
  private readonly submitDelta: (
748
748
  op: IIntervalCollectionTypeOperationValue,
749
749
  md: IntervalMessageLocalMetadata,
750
+ consensus?: SequenceIntervalClass,
750
751
  ) => void;
751
752
 
752
753
  constructor(
@@ -756,15 +757,15 @@ export class IntervalCollection
756
757
  ) {
757
758
  super();
758
759
 
759
- this.submitDelta = (op, md) => {
760
+ this.submitDelta = (op, md, consensus) => {
760
761
  const { id } = getSerializedProperties(op.value);
761
762
  const pending = (this.pending[id] ??= {
762
763
  local: new DoublyLinkedList(),
764
+ consensus,
763
765
  });
764
766
  if (md.type === "add" || (md.type === "change" && hasEndpointChanges(op.value))) {
765
767
  const endpointChanges = (pending.endpointChanges ??= new DoublyLinkedList());
766
768
  md.endpointChangesNode = endpointChanges.push(md).last;
767
- md.rebased = undefined;
768
769
  }
769
770
  submitDelta(op, pending.local.push(md).last);
770
771
  };
@@ -814,48 +815,45 @@ export class IntervalCollection
814
815
  const localOpMetadata = removeMetadataFromPendingChanges(maybeMetadata);
815
816
  const { value } = op;
816
817
  const { id, properties } = getSerializedProperties(value);
817
- const { type } = localOpMetadata;
818
+ const pending = clearEmptyPendingEntry(this.pending, id);
819
+ const previous = pending?.local.empty
820
+ ? pending.consensus
821
+ : pending?.local.last?.data.interval;
822
+ const { type, interval } = localOpMetadata;
818
823
  switch (type) {
819
824
  case "add": {
820
- const interval = this.getIntervalById(id);
821
- if (interval) {
822
- this.deleteExistingInterval({ interval, local: true, rollback: true });
823
- }
825
+ this.deleteExistingInterval({ interval, local: true, rollback: true });
826
+ interval.dispose();
824
827
  break;
825
828
  }
826
829
  case "change": {
827
- const { previous } = localOpMetadata;
828
- const endpointsChanged = hasEndpointChanges(value);
829
- const start = endpointsChanged
830
- ? toOptionalSequencePlace(previous.start, previous.startSide)
831
- : undefined;
832
- const end = endpointsChanged
833
- ? toOptionalSequencePlace(previous.end, previous.endSide)
830
+ const changeProperties = Object.keys(properties).length > 0;
831
+ const deltaProps = changeProperties
832
+ ? interval.changeProperties(properties, undefined, true)
834
833
  : undefined;
835
- this.change(id, {
836
- start,
837
- end,
838
- props: Object.keys(properties).length > 0 ? properties : undefined,
839
- rollback: true,
840
- });
834
+ if (localOpMetadata.endpointChangesNode !== undefined) {
835
+ this.localCollection?.removeExistingInterval(interval);
836
+ assert(previous !== undefined, 0xbd2 /* must have existed to change */);
837
+ this.localCollection?.add(previous);
838
+ this.emitChange(previous, interval, true, true);
839
+ }
840
+ if (previous !== interval) {
841
+ interval.dispose();
842
+ }
843
+ if (changeProperties) {
844
+ this.emit("propertyChanged", previous, deltaProps, true, undefined);
845
+ }
841
846
  break;
842
847
  }
843
848
  case "delete": {
844
- const { previous } = localOpMetadata;
845
- this.add({
846
- id,
847
- start: toSequencePlace(previous.start, previous.startSide),
848
- end: toSequencePlace(previous.end, previous.endSide),
849
- props: Object.keys(properties).length > 0 ? properties : undefined,
850
- rollback: true,
851
- });
849
+ assert(previous !== undefined, 0xbd3 /* must have existed to delete */);
850
+ this.localCollection?.add(previous);
851
+ this.emit("addInterval", previous, true, undefined);
852
852
  break;
853
853
  }
854
854
  default:
855
855
  unreachableCase(type);
856
856
  }
857
-
858
- clearEmptyPendingEntry(this.pending, id);
859
857
  }
860
858
 
861
859
  public process(
@@ -869,13 +867,15 @@ export class IntervalCollection
869
867
  : undefined;
870
868
 
871
869
  const { opName, value } = op;
870
+ const { id } = getSerializedProperties(value);
872
871
  assert(
873
872
  (local === false && localOpMetadata === undefined) || opName === localOpMetadata?.type,
874
873
  0xbc1 /* must be same type */,
875
874
  );
875
+ let newConsensus = localOpMetadata?.interval;
876
876
  switch (opName) {
877
877
  case "add": {
878
- this.ackAdd(
878
+ newConsensus = this.ackAdd(
879
879
  value,
880
880
  local,
881
881
  message,
@@ -892,16 +892,22 @@ export class IntervalCollection
892
892
  }
893
893
 
894
894
  case "change": {
895
- this.ackChange(value, local, message);
895
+ newConsensus = this.ackChange(
896
+ value,
897
+ local,
898
+ message, // this cast is safe because of the above assert which
899
+ // validates the op and metadata types match for local changes
900
+ localOpMetadata as IntervalChangeLocalMetadata | undefined,
901
+ );
896
902
  break;
897
903
  }
898
904
  default:
899
905
  unreachableCase(opName);
900
906
  }
907
+ const pending = clearEmptyPendingEntry(this.pending, id);
901
908
 
902
- if (local) {
903
- const { id } = getSerializedProperties(value);
904
- clearEmptyPendingEntry(this.pending, id);
909
+ if (pending !== undefined) {
910
+ pending.consensus = newConsensus;
905
911
  }
906
912
  }
907
913
 
@@ -1040,7 +1046,7 @@ export class IntervalCollection
1040
1046
  for (const pending of Object.values(this.pending)) {
1041
1047
  if (pending?.endpointChanges !== undefined) {
1042
1048
  for (const local of pending.endpointChanges) {
1043
- local.data.rebased = this.computeRebasedPositions(local.data, squash);
1049
+ this.rebaseLocalInterval(local.data.interval.serialize(), local.data, squash);
1044
1050
  }
1045
1051
  }
1046
1052
  }
@@ -1150,13 +1156,11 @@ export class IntervalCollection
1150
1156
  start,
1151
1157
  end,
1152
1158
  props,
1153
- rollback,
1154
1159
  }: {
1155
1160
  id?: string;
1156
1161
  start: SequencePlace;
1157
1162
  end: SequencePlace;
1158
1163
  props?: PropertySet;
1159
- rollback?: boolean;
1160
1164
  }): SequenceIntervalClass {
1161
1165
  if (!this.localCollection) {
1162
1166
  throw new LoggingError("attach must be called prior to adding intervals");
@@ -1182,7 +1186,6 @@ export class IntervalCollection
1182
1186
  toSequencePlace(endPos, endSide),
1183
1187
  props,
1184
1188
  undefined,
1185
- rollback,
1186
1189
  );
1187
1190
 
1188
1191
  if (interval) {
@@ -1192,7 +1195,7 @@ export class IntervalCollection
1192
1195
  }
1193
1196
  const serializedInterval: ISerializedInterval = interval.serialize();
1194
1197
  const localSeq = this.getNextLocalSeq();
1195
- if (this.isCollaborating && rollback !== true) {
1198
+ if (this.isCollaborating) {
1196
1199
  this.submitDelta(
1197
1200
  {
1198
1201
  opName: "add",
@@ -1241,8 +1244,8 @@ export class IntervalCollection
1241
1244
  {
1242
1245
  type: "delete",
1243
1246
  localSeq: this.getNextLocalSeq(),
1244
- previous: value,
1245
1247
  },
1248
+ interval,
1246
1249
  );
1247
1250
  } else {
1248
1251
  if (this.onDeserialize) {
@@ -1261,7 +1264,7 @@ export class IntervalCollection
1261
1264
  if (!this.localCollection) {
1262
1265
  throw new LoggingError("Attach must be called before accessing intervals");
1263
1266
  }
1264
- const interval = this.localCollection.idIntervalIndex.getIntervalById(id);
1267
+ const interval = this.getIntervalById(id);
1265
1268
  if (interval) {
1266
1269
  this.deleteExistingInterval({ interval, local: true });
1267
1270
  }
@@ -1329,7 +1332,6 @@ export class IntervalCollection
1329
1332
  const metadata: IntervalChangeLocalMetadata = {
1330
1333
  type: "change",
1331
1334
  localSeq,
1332
- previous: interval.serialize(),
1333
1335
  interval: newInterval ?? interval,
1334
1336
  };
1335
1337
 
@@ -1339,6 +1341,7 @@ export class IntervalCollection
1339
1341
  value: serializedInterval,
1340
1342
  },
1341
1343
  metadata,
1344
+ interval,
1342
1345
  );
1343
1346
  }
1344
1347
  if (deltaProps !== undefined) {
@@ -1377,6 +1380,7 @@ export class IntervalCollection
1377
1380
  serializedInterval: SerializedIntervalDelta,
1378
1381
  local: boolean,
1379
1382
  op: ISequencedDocumentMessage,
1383
+ localOpMetadata: IntervalChangeLocalMetadata | undefined,
1380
1384
  ) {
1381
1385
  if (!this.localCollection) {
1382
1386
  throw new LoggingError("Attach must be called before accessing intervals");
@@ -1387,55 +1391,51 @@ export class IntervalCollection
1387
1391
  // strip it out of the properties here.
1388
1392
  const { id, properties } = getSerializedProperties(serializedInterval);
1389
1393
  assert(id !== undefined, 0x3fe /* id must exist on the interval */);
1390
- const interval: SequenceIntervalClass | undefined = this.getIntervalById(id);
1391
-
1392
- if (!interval) {
1393
- // The interval has been removed locally; no-op.
1394
- return;
1395
- }
1396
1394
 
1397
1395
  if (local) {
1396
+ assert(localOpMetadata !== undefined, 0xbd4 /* local must have metadata */);
1397
+ const { interval } = localOpMetadata;
1398
1398
  interval.ackPropertiesChange(properties, op);
1399
1399
 
1400
1400
  this.ackInterval(interval, op);
1401
+ return interval;
1401
1402
  } else {
1402
- // If there are pending changes with this ID, don't apply the remote start/end change, as the local ack
1403
- // should be the winning change.
1404
- let start: number | "start" | "end" | undefined;
1405
- let end: number | "start" | "end" | undefined;
1406
- // Track pending start/end independently of one another.
1407
- if (!this.hasPendingEndpointChanges(id)) {
1408
- start = serializedInterval.start;
1409
- end = serializedInterval.end;
1410
- }
1403
+ const latestInterval = this.getIntervalById(id);
1404
+ const intervalToChange: SequenceIntervalClass | undefined =
1405
+ this.pending[id]?.consensus ?? latestInterval;
1411
1406
 
1412
- let newInterval = interval;
1413
- if (start !== undefined || end !== undefined) {
1414
- // If changeInterval gives us a new interval, work with that one. Otherwise keep working with
1415
- // the one we originally found in the tree.
1416
- newInterval =
1417
- this.localCollection.changeInterval(
1418
- interval,
1419
- toOptionalSequencePlace(start, serializedInterval.startSide ?? Side.Before),
1420
- toOptionalSequencePlace(end, serializedInterval.endSide ?? Side.Before),
1421
- op,
1422
- ) ?? interval;
1423
- }
1424
- const deltaProps = newInterval.changeProperties(properties, op);
1407
+ const isLatestInterval = intervalToChange === latestInterval;
1425
1408
 
1426
- if (this.onDeserialize) {
1427
- this.onDeserialize(newInterval);
1409
+ if (!intervalToChange) {
1410
+ return intervalToChange;
1428
1411
  }
1429
1412
 
1430
- if (newInterval !== interval) {
1431
- this.emitChange(newInterval, interval, local, false, op);
1413
+ const deltaProps = intervalToChange.changeProperties(properties, op);
1414
+
1415
+ let newInterval = intervalToChange;
1416
+ if (hasEndpointChanges(serializedInterval)) {
1417
+ const { start, end, startSide, endSide } = serializedInterval;
1418
+ newInterval = intervalToChange.modify(
1419
+ "",
1420
+ toOptionalSequencePlace(start, startSide ?? Side.Before),
1421
+ toOptionalSequencePlace(end, endSide ?? Side.Before),
1422
+ op,
1423
+ );
1424
+ if (isLatestInterval) {
1425
+ this.localCollection.removeExistingInterval(intervalToChange);
1426
+ this.localCollection.add(newInterval);
1427
+ this.emitChange(newInterval, intervalToChange, local, false, op);
1428
+ if (this.onDeserialize) {
1429
+ this.onDeserialize(newInterval);
1430
+ }
1431
+ }
1432
1432
  }
1433
1433
 
1434
- const changedProperties = Object.keys(properties).length > 0;
1435
- if (changedProperties) {
1436
- this.emit("propertyChanged", interval, deltaProps, local, op);
1437
- this.emit("changed", interval, deltaProps, undefined, local, false);
1434
+ if (deltaProps !== undefined && Object.keys(deltaProps).length > 0) {
1435
+ this.emit("propertyChanged", latestInterval, deltaProps, local, op);
1436
+ this.emit("changed", latestInterval, deltaProps, undefined, local, false);
1438
1437
  }
1438
+ return newInterval;
1439
1439
  }
1440
1440
  }
1441
1441
 
@@ -1478,11 +1478,9 @@ export class IntervalCollection
1478
1478
 
1479
1479
  const { localSeq, interval } = localOpMetadata;
1480
1480
  const { id } = getSerializedProperties(original);
1481
- const rebasedEndpoint = (localOpMetadata.rebased ??= this.computeRebasedPositions(
1482
- localOpMetadata,
1483
- squash,
1484
- ));
1485
- const localInterval = this.localCollection.idIntervalIndex.getIntervalById(id);
1481
+
1482
+ const rebasedEndpoint = this.computeRebasedPositions(localOpMetadata, squash);
1483
+ const localInterval = this.getIntervalById(id);
1486
1484
 
1487
1485
  // if the interval slid off the string, rebase the op to be a noop and delete the interval.
1488
1486
  if (rebasedEndpoint === "detached") {
@@ -1511,8 +1509,7 @@ export class IntervalCollection
1511
1509
  this.localCollection.add(interval);
1512
1510
  this.emitChange(interval, old, true, true);
1513
1511
  }
1514
- this.client.removeLocalReferencePosition(old.start);
1515
- this.client.removeLocalReferencePosition(old.end);
1512
+ old.dispose();
1516
1513
  }
1517
1514
 
1518
1515
  return {
@@ -1583,10 +1580,14 @@ export class IntervalCollection
1583
1580
  // `interval`'s endpoints will get modified in-place, so clone it prior to doing so for event emission.
1584
1581
  const oldInterval = interval.clone();
1585
1582
 
1586
- // In this case, where we change the start or end of an interval,
1587
- // it is necessary to remove and re-add the interval listeners.
1588
- // This ensures that the correct listeners are added to the LocalReferencePosition.
1589
- this.localCollection.removeExistingInterval(interval);
1583
+ const isLatestInterval = this.getIntervalById(id) === interval;
1584
+
1585
+ if (isLatestInterval) {
1586
+ // In this case, where we change the start or end of an interval,
1587
+ // it is necessary to remove and re-add the interval listeners.
1588
+ // This ensures that the correct listeners are added to the LocalReferencePosition.
1589
+ this.localCollection.removeExistingInterval(interval);
1590
+ }
1590
1591
  if (!this.client) {
1591
1592
  throw new LoggingError("client does not exist");
1592
1593
  }
@@ -1629,8 +1630,10 @@ export class IntervalCollection
1629
1630
  oldInterval.end.refType = ReferenceType.Transient;
1630
1631
  oldSeg?.localRefs?.addLocalRef(oldInterval.end, oldInterval.end.getOffset());
1631
1632
  }
1632
- this.localCollection.add(interval);
1633
- this.emitChange(interval, oldInterval, true, true, op);
1633
+ if (isLatestInterval) {
1634
+ this.localCollection.add(interval);
1635
+ this.emitChange(interval, oldInterval, true, true, op);
1636
+ }
1634
1637
  }
1635
1638
  }
1636
1639
 
@@ -1639,7 +1642,7 @@ export class IntervalCollection
1639
1642
  local: boolean,
1640
1643
  op: ISequencedDocumentMessage,
1641
1644
  localOpMetadata: IntervalAddLocalMetadata | undefined,
1642
- ) {
1645
+ ): SequenceIntervalClass {
1643
1646
  const { id, properties } = getSerializedProperties(serializedInterval);
1644
1647
 
1645
1648
  if (local) {
@@ -1647,11 +1650,8 @@ export class IntervalCollection
1647
1650
  localOpMetadata !== undefined,
1648
1651
  0x553 /* op metadata should be defined for local op */,
1649
1652
  );
1650
- const localInterval = this.getIntervalById(id);
1651
- if (localInterval) {
1652
- this.ackInterval(localInterval, op);
1653
- }
1654
- return;
1653
+ this.ackInterval(localOpMetadata.interval, op);
1654
+ return localOpMetadata.interval;
1655
1655
  }
1656
1656
 
1657
1657
  if (!this.localCollection) {
@@ -1694,7 +1694,7 @@ export class IntervalCollection
1694
1694
  }
1695
1695
 
1696
1696
  const { id } = getSerializedProperties(serializedInterval);
1697
- const interval = this.localCollection.idIntervalIndex.getIntervalById(id);
1697
+ const interval = this.getIntervalById(id);
1698
1698
  if (interval) {
1699
1699
  this.deleteExistingInterval({ interval, local, op });
1700
1700
  }
@@ -5,7 +5,7 @@
5
5
 
6
6
  import type { ListNode } from "@fluidframework/core-utils/internal";
7
7
  import { ISequencedDocumentMessage } from "@fluidframework/driver-definitions/internal";
8
- import { IMergeTreeOptions, type ISegmentInternal } from "@fluidframework/merge-tree/internal";
8
+ import { IMergeTreeOptions } from "@fluidframework/merge-tree/internal";
9
9
 
10
10
  import type {
11
11
  IntervalCollection,
@@ -23,25 +23,17 @@ export interface IntervalAddLocalMetadata {
23
23
  type: typeof IntervalDeltaOpType.ADD;
24
24
  localSeq: number;
25
25
  endpointChangesNode?: ListNode<IntervalAddLocalMetadata | IntervalChangeLocalMetadata>;
26
- rebased?:
27
- | Record<"start" | "end", { segment: ISegmentInternal; offset: number }>
28
- | "detached";
29
26
  interval: SequenceIntervalClass;
30
27
  }
31
28
  export interface IntervalChangeLocalMetadata {
32
29
  type: typeof IntervalDeltaOpType.CHANGE;
33
30
  localSeq: number;
34
- previous: ISerializedInterval;
35
31
  endpointChangesNode?: ListNode<IntervalChangeLocalMetadata | IntervalChangeLocalMetadata>;
36
- rebased?:
37
- | Record<"start" | "end", { segment: ISegmentInternal; offset: number }>
38
- | "detached";
39
32
  interval: SequenceIntervalClass;
40
33
  }
41
34
  export interface IntervalDeleteLocalMetadata {
42
35
  type: typeof IntervalDeltaOpType.DELETE;
43
36
  localSeq: number;
44
- previous: ISerializedInterval;
45
37
  endpointChangesNode?: undefined;
46
38
  interval?: undefined;
47
39
  }