@fluidframework/sequence 2.50.0-345060 → 2.51.0-347100

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 (34) 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 +94 -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.js +1 -1
  15. package/dist/packageVersion.js.map +1 -1
  16. package/lib/intervalCollection.d.ts +4 -5
  17. package/lib/intervalCollection.d.ts.map +1 -1
  18. package/lib/intervalCollection.js +94 -95
  19. package/lib/intervalCollection.js.map +1 -1
  20. package/lib/intervalCollectionMapInterfaces.d.ts +1 -11
  21. package/lib/intervalCollectionMapInterfaces.d.ts.map +1 -1
  22. package/lib/intervalCollectionMapInterfaces.js.map +1 -1
  23. package/lib/intervals/sequenceInterval.d.ts +7 -3
  24. package/lib/intervals/sequenceInterval.d.ts.map +1 -1
  25. package/lib/intervals/sequenceInterval.js +74 -32
  26. package/lib/intervals/sequenceInterval.js.map +1 -1
  27. package/lib/packageVersion.d.ts +1 -1
  28. package/lib/packageVersion.js +1 -1
  29. package/lib/packageVersion.js.map +1 -1
  30. package/package.json +18 -19
  31. package/src/intervalCollection.ts +106 -103
  32. package/src/intervalCollectionMapInterfaces.ts +1 -9
  33. package/src/intervals/sequenceInterval.ts +88 -32
  34. 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,48 @@ 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)
830
+ const changeProperties = Object.keys(properties).length > 0;
831
+ const deltaProps = changeProperties
832
+ ? interval.changeProperties(properties, undefined, true)
831
833
  : undefined;
832
- const end = endpointsChanged
833
- ? toOptionalSequencePlace(previous.end, previous.endSide)
834
- : 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
+ if (previous !== undefined) {
836
+ this.localCollection?.removeExistingInterval(interval);
837
+ this.localCollection?.add(previous);
838
+ this.emitChange(previous, interval, true, true);
839
+ }
840
+ if (previous !== interval) {
841
+ interval.dispose();
842
+ }
843
+ }
844
+ if (changeProperties) {
845
+ this.emit("propertyChanged", previous, deltaProps, true, undefined);
846
+ }
841
847
  break;
842
848
  }
843
849
  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
- });
850
+ // a remote could delete the same interval, so it may not exist to re-add
851
+ if (previous !== undefined) {
852
+ this.localCollection?.add(previous);
853
+ this.emit("addInterval", previous, true, undefined);
854
+ }
852
855
  break;
853
856
  }
854
857
  default:
855
858
  unreachableCase(type);
856
859
  }
857
-
858
- clearEmptyPendingEntry(this.pending, id);
859
860
  }
860
861
 
861
862
  public process(
@@ -869,13 +870,15 @@ export class IntervalCollection
869
870
  : undefined;
870
871
 
871
872
  const { opName, value } = op;
873
+ const { id } = getSerializedProperties(value);
872
874
  assert(
873
875
  (local === false && localOpMetadata === undefined) || opName === localOpMetadata?.type,
874
876
  0xbc1 /* must be same type */,
875
877
  );
878
+ let newConsensus = localOpMetadata?.interval;
876
879
  switch (opName) {
877
880
  case "add": {
878
- this.ackAdd(
881
+ newConsensus = this.ackAdd(
879
882
  value,
880
883
  local,
881
884
  message,
@@ -892,16 +895,22 @@ export class IntervalCollection
892
895
  }
893
896
 
894
897
  case "change": {
895
- this.ackChange(value, local, message);
898
+ newConsensus = this.ackChange(
899
+ value,
900
+ local,
901
+ message, // this cast is safe because of the above assert which
902
+ // validates the op and metadata types match for local changes
903
+ localOpMetadata as IntervalChangeLocalMetadata | undefined,
904
+ );
896
905
  break;
897
906
  }
898
907
  default:
899
908
  unreachableCase(opName);
900
909
  }
910
+ const pending = clearEmptyPendingEntry(this.pending, id);
901
911
 
902
- if (local) {
903
- const { id } = getSerializedProperties(value);
904
- clearEmptyPendingEntry(this.pending, id);
912
+ if (pending !== undefined) {
913
+ pending.consensus = newConsensus;
905
914
  }
906
915
  }
907
916
 
@@ -1040,7 +1049,7 @@ export class IntervalCollection
1040
1049
  for (const pending of Object.values(this.pending)) {
1041
1050
  if (pending?.endpointChanges !== undefined) {
1042
1051
  for (const local of pending.endpointChanges) {
1043
- local.data.rebased = this.computeRebasedPositions(local.data, squash);
1052
+ this.rebaseLocalInterval(local.data.interval.serialize(), local.data, squash);
1044
1053
  }
1045
1054
  }
1046
1055
  }
@@ -1150,13 +1159,11 @@ export class IntervalCollection
1150
1159
  start,
1151
1160
  end,
1152
1161
  props,
1153
- rollback,
1154
1162
  }: {
1155
1163
  id?: string;
1156
1164
  start: SequencePlace;
1157
1165
  end: SequencePlace;
1158
1166
  props?: PropertySet;
1159
- rollback?: boolean;
1160
1167
  }): SequenceIntervalClass {
1161
1168
  if (!this.localCollection) {
1162
1169
  throw new LoggingError("attach must be called prior to adding intervals");
@@ -1182,7 +1189,6 @@ export class IntervalCollection
1182
1189
  toSequencePlace(endPos, endSide),
1183
1190
  props,
1184
1191
  undefined,
1185
- rollback,
1186
1192
  );
1187
1193
 
1188
1194
  if (interval) {
@@ -1192,7 +1198,7 @@ export class IntervalCollection
1192
1198
  }
1193
1199
  const serializedInterval: ISerializedInterval = interval.serialize();
1194
1200
  const localSeq = this.getNextLocalSeq();
1195
- if (this.isCollaborating && rollback !== true) {
1201
+ if (this.isCollaborating) {
1196
1202
  this.submitDelta(
1197
1203
  {
1198
1204
  opName: "add",
@@ -1241,8 +1247,8 @@ export class IntervalCollection
1241
1247
  {
1242
1248
  type: "delete",
1243
1249
  localSeq: this.getNextLocalSeq(),
1244
- previous: value,
1245
1250
  },
1251
+ interval,
1246
1252
  );
1247
1253
  } else {
1248
1254
  if (this.onDeserialize) {
@@ -1261,7 +1267,7 @@ export class IntervalCollection
1261
1267
  if (!this.localCollection) {
1262
1268
  throw new LoggingError("Attach must be called before accessing intervals");
1263
1269
  }
1264
- const interval = this.localCollection.idIntervalIndex.getIntervalById(id);
1270
+ const interval = this.getIntervalById(id);
1265
1271
  if (interval) {
1266
1272
  this.deleteExistingInterval({ interval, local: true });
1267
1273
  }
@@ -1329,7 +1335,6 @@ export class IntervalCollection
1329
1335
  const metadata: IntervalChangeLocalMetadata = {
1330
1336
  type: "change",
1331
1337
  localSeq,
1332
- previous: interval.serialize(),
1333
1338
  interval: newInterval ?? interval,
1334
1339
  };
1335
1340
 
@@ -1339,6 +1344,7 @@ export class IntervalCollection
1339
1344
  value: serializedInterval,
1340
1345
  },
1341
1346
  metadata,
1347
+ interval,
1342
1348
  );
1343
1349
  }
1344
1350
  if (deltaProps !== undefined) {
@@ -1377,6 +1383,7 @@ export class IntervalCollection
1377
1383
  serializedInterval: SerializedIntervalDelta,
1378
1384
  local: boolean,
1379
1385
  op: ISequencedDocumentMessage,
1386
+ localOpMetadata: IntervalChangeLocalMetadata | undefined,
1380
1387
  ) {
1381
1388
  if (!this.localCollection) {
1382
1389
  throw new LoggingError("Attach must be called before accessing intervals");
@@ -1387,55 +1394,51 @@ export class IntervalCollection
1387
1394
  // strip it out of the properties here.
1388
1395
  const { id, properties } = getSerializedProperties(serializedInterval);
1389
1396
  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
1397
 
1397
1398
  if (local) {
1399
+ assert(localOpMetadata !== undefined, 0xbd4 /* local must have metadata */);
1400
+ const { interval } = localOpMetadata;
1398
1401
  interval.ackPropertiesChange(properties, op);
1399
1402
 
1400
1403
  this.ackInterval(interval, op);
1404
+ return interval;
1401
1405
  } 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
- }
1406
+ const latestInterval = this.getIntervalById(id);
1407
+ const intervalToChange: SequenceIntervalClass | undefined =
1408
+ this.pending[id]?.consensus ?? latestInterval;
1411
1409
 
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);
1410
+ const isLatestInterval = intervalToChange === latestInterval;
1425
1411
 
1426
- if (this.onDeserialize) {
1427
- this.onDeserialize(newInterval);
1412
+ if (!intervalToChange) {
1413
+ return intervalToChange;
1428
1414
  }
1429
1415
 
1430
- if (newInterval !== interval) {
1431
- this.emitChange(newInterval, interval, local, false, op);
1416
+ const deltaProps = intervalToChange.changeProperties(properties, op);
1417
+
1418
+ let newInterval = intervalToChange;
1419
+ if (hasEndpointChanges(serializedInterval)) {
1420
+ const { start, end, startSide, endSide } = serializedInterval;
1421
+ newInterval = intervalToChange.modify(
1422
+ "",
1423
+ toOptionalSequencePlace(start, startSide ?? Side.Before),
1424
+ toOptionalSequencePlace(end, endSide ?? Side.Before),
1425
+ op,
1426
+ );
1427
+ if (isLatestInterval) {
1428
+ this.localCollection.removeExistingInterval(intervalToChange);
1429
+ this.localCollection.add(newInterval);
1430
+ this.emitChange(newInterval, intervalToChange, local, false, op);
1431
+ if (this.onDeserialize) {
1432
+ this.onDeserialize(newInterval);
1433
+ }
1434
+ }
1432
1435
  }
1433
1436
 
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);
1437
+ if (deltaProps !== undefined && Object.keys(deltaProps).length > 0) {
1438
+ this.emit("propertyChanged", latestInterval, deltaProps, local, op);
1439
+ this.emit("changed", latestInterval, deltaProps, undefined, local, false);
1438
1440
  }
1441
+ return newInterval;
1439
1442
  }
1440
1443
  }
1441
1444
 
@@ -1478,11 +1481,9 @@ export class IntervalCollection
1478
1481
 
1479
1482
  const { localSeq, interval } = localOpMetadata;
1480
1483
  const { id } = getSerializedProperties(original);
1481
- const rebasedEndpoint = (localOpMetadata.rebased ??= this.computeRebasedPositions(
1482
- localOpMetadata,
1483
- squash,
1484
- ));
1485
- const localInterval = this.localCollection.idIntervalIndex.getIntervalById(id);
1484
+
1485
+ const rebasedEndpoint = this.computeRebasedPositions(localOpMetadata, squash);
1486
+ const localInterval = this.getIntervalById(id);
1486
1487
 
1487
1488
  // if the interval slid off the string, rebase the op to be a noop and delete the interval.
1488
1489
  if (rebasedEndpoint === "detached") {
@@ -1511,8 +1512,7 @@ export class IntervalCollection
1511
1512
  this.localCollection.add(interval);
1512
1513
  this.emitChange(interval, old, true, true);
1513
1514
  }
1514
- this.client.removeLocalReferencePosition(old.start);
1515
- this.client.removeLocalReferencePosition(old.end);
1515
+ old.dispose();
1516
1516
  }
1517
1517
 
1518
1518
  return {
@@ -1583,10 +1583,14 @@ export class IntervalCollection
1583
1583
  // `interval`'s endpoints will get modified in-place, so clone it prior to doing so for event emission.
1584
1584
  const oldInterval = interval.clone();
1585
1585
 
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);
1586
+ const isLatestInterval = this.getIntervalById(id) === interval;
1587
+
1588
+ if (isLatestInterval) {
1589
+ // In this case, where we change the start or end of an interval,
1590
+ // it is necessary to remove and re-add the interval listeners.
1591
+ // This ensures that the correct listeners are added to the LocalReferencePosition.
1592
+ this.localCollection.removeExistingInterval(interval);
1593
+ }
1590
1594
  if (!this.client) {
1591
1595
  throw new LoggingError("client does not exist");
1592
1596
  }
@@ -1629,8 +1633,10 @@ export class IntervalCollection
1629
1633
  oldInterval.end.refType = ReferenceType.Transient;
1630
1634
  oldSeg?.localRefs?.addLocalRef(oldInterval.end, oldInterval.end.getOffset());
1631
1635
  }
1632
- this.localCollection.add(interval);
1633
- this.emitChange(interval, oldInterval, true, true, op);
1636
+ if (isLatestInterval) {
1637
+ this.localCollection.add(interval);
1638
+ this.emitChange(interval, oldInterval, true, true, op);
1639
+ }
1634
1640
  }
1635
1641
  }
1636
1642
 
@@ -1639,7 +1645,7 @@ export class IntervalCollection
1639
1645
  local: boolean,
1640
1646
  op: ISequencedDocumentMessage,
1641
1647
  localOpMetadata: IntervalAddLocalMetadata | undefined,
1642
- ) {
1648
+ ): SequenceIntervalClass {
1643
1649
  const { id, properties } = getSerializedProperties(serializedInterval);
1644
1650
 
1645
1651
  if (local) {
@@ -1647,11 +1653,8 @@ export class IntervalCollection
1647
1653
  localOpMetadata !== undefined,
1648
1654
  0x553 /* op metadata should be defined for local op */,
1649
1655
  );
1650
- const localInterval = this.getIntervalById(id);
1651
- if (localInterval) {
1652
- this.ackInterval(localInterval, op);
1653
- }
1654
- return;
1656
+ this.ackInterval(localOpMetadata.interval, op);
1657
+ return localOpMetadata.interval;
1655
1658
  }
1656
1659
 
1657
1660
  if (!this.localCollection) {
@@ -1694,7 +1697,7 @@ export class IntervalCollection
1694
1697
  }
1695
1698
 
1696
1699
  const { id } = getSerializedProperties(serializedInterval);
1697
- const interval = this.localCollection.idIntervalIndex.getIntervalById(id);
1700
+ const interval = this.getIntervalById(id);
1698
1701
  if (interval) {
1699
1702
  this.deleteExistingInterval({ interval, local, op });
1700
1703
  }
@@ -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
  }