@fluidframework/sequence 2.42.0 → 2.43.0-343119

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 (72) hide show
  1. package/api-report/sequence.legacy.alpha.api.md +13 -5
  2. package/dist/intervalCollection.d.ts +14 -15
  3. package/dist/intervalCollection.d.ts.map +1 -1
  4. package/dist/intervalCollection.js +109 -104
  5. package/dist/intervalCollection.js.map +1 -1
  6. package/dist/intervalCollectionMap.d.ts +3 -3
  7. package/dist/intervalCollectionMap.d.ts.map +1 -1
  8. package/dist/intervalCollectionMap.js.map +1 -1
  9. package/dist/intervalCollectionMapInterfaces.d.ts +22 -5
  10. package/dist/intervalCollectionMapInterfaces.d.ts.map +1 -1
  11. package/dist/intervalCollectionMapInterfaces.js.map +1 -1
  12. package/dist/intervalIndex/overlappingIntervalsIndex.d.ts +4 -4
  13. package/dist/intervalIndex/overlappingIntervalsIndex.d.ts.map +1 -1
  14. package/dist/intervalIndex/overlappingIntervalsIndex.js.map +1 -1
  15. package/dist/intervals/intervalUtils.d.ts +12 -1
  16. package/dist/intervals/intervalUtils.d.ts.map +1 -1
  17. package/dist/intervals/intervalUtils.js.map +1 -1
  18. package/dist/intervals/sequenceInterval.d.ts +18 -5
  19. package/dist/intervals/sequenceInterval.d.ts.map +1 -1
  20. package/dist/intervals/sequenceInterval.js +2 -1
  21. package/dist/intervals/sequenceInterval.js.map +1 -1
  22. package/dist/packageVersion.d.ts +1 -1
  23. package/dist/packageVersion.d.ts.map +1 -1
  24. package/dist/packageVersion.js +1 -1
  25. package/dist/packageVersion.js.map +1 -1
  26. package/dist/revertibles.d.ts.map +1 -1
  27. package/dist/revertibles.js +6 -3
  28. package/dist/revertibles.js.map +1 -1
  29. package/dist/sequence.d.ts.map +1 -1
  30. package/dist/sequence.js +1 -1
  31. package/dist/sequence.js.map +1 -1
  32. package/lib/intervalCollection.d.ts +14 -15
  33. package/lib/intervalCollection.d.ts.map +1 -1
  34. package/lib/intervalCollection.js +110 -105
  35. package/lib/intervalCollection.js.map +1 -1
  36. package/lib/intervalCollectionMap.d.ts +3 -3
  37. package/lib/intervalCollectionMap.d.ts.map +1 -1
  38. package/lib/intervalCollectionMap.js.map +1 -1
  39. package/lib/intervalCollectionMapInterfaces.d.ts +22 -5
  40. package/lib/intervalCollectionMapInterfaces.d.ts.map +1 -1
  41. package/lib/intervalCollectionMapInterfaces.js.map +1 -1
  42. package/lib/intervalIndex/overlappingIntervalsIndex.d.ts +4 -4
  43. package/lib/intervalIndex/overlappingIntervalsIndex.d.ts.map +1 -1
  44. package/lib/intervalIndex/overlappingIntervalsIndex.js +1 -1
  45. package/lib/intervalIndex/overlappingIntervalsIndex.js.map +1 -1
  46. package/lib/intervals/intervalUtils.d.ts +12 -1
  47. package/lib/intervals/intervalUtils.d.ts.map +1 -1
  48. package/lib/intervals/intervalUtils.js.map +1 -1
  49. package/lib/intervals/sequenceInterval.d.ts +18 -5
  50. package/lib/intervals/sequenceInterval.d.ts.map +1 -1
  51. package/lib/intervals/sequenceInterval.js +2 -1
  52. package/lib/intervals/sequenceInterval.js.map +1 -1
  53. package/lib/packageVersion.d.ts +1 -1
  54. package/lib/packageVersion.d.ts.map +1 -1
  55. package/lib/packageVersion.js +1 -1
  56. package/lib/packageVersion.js.map +1 -1
  57. package/lib/revertibles.d.ts.map +1 -1
  58. package/lib/revertibles.js +6 -3
  59. package/lib/revertibles.js.map +1 -1
  60. package/lib/sequence.d.ts.map +1 -1
  61. package/lib/sequence.js +1 -1
  62. package/lib/sequence.js.map +1 -1
  63. package/package.json +17 -17
  64. package/src/intervalCollection.ts +185 -159
  65. package/src/intervalCollectionMap.ts +4 -11
  66. package/src/intervalCollectionMapInterfaces.ts +25 -6
  67. package/src/intervalIndex/overlappingIntervalsIndex.ts +15 -11
  68. package/src/intervals/intervalUtils.ts +12 -1
  69. package/src/intervals/sequenceInterval.ts +22 -4
  70. package/src/packageVersion.ts +1 -1
  71. package/src/revertibles.ts +7 -6
  72. package/src/sequence.ts +5 -11
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fluidframework/sequence",
3
- "version": "2.42.0",
3
+ "version": "2.43.0-343119",
4
4
  "description": "Distributed sequence",
5
5
  "homepage": "https://fluidframework.com",
6
6
  "repository": {
@@ -91,33 +91,33 @@
91
91
  "temp-directory": "nyc/.nyc_output"
92
92
  },
93
93
  "dependencies": {
94
- "@fluid-internal/client-utils": "~2.42.0",
95
- "@fluidframework/core-interfaces": "~2.42.0",
96
- "@fluidframework/core-utils": "~2.42.0",
97
- "@fluidframework/datastore-definitions": "~2.42.0",
98
- "@fluidframework/driver-definitions": "~2.42.0",
99
- "@fluidframework/merge-tree": "~2.42.0",
100
- "@fluidframework/runtime-definitions": "~2.42.0",
101
- "@fluidframework/runtime-utils": "~2.42.0",
102
- "@fluidframework/shared-object-base": "~2.42.0",
103
- "@fluidframework/telemetry-utils": "~2.42.0",
94
+ "@fluid-internal/client-utils": "2.43.0-343119",
95
+ "@fluidframework/core-interfaces": "2.43.0-343119",
96
+ "@fluidframework/core-utils": "2.43.0-343119",
97
+ "@fluidframework/datastore-definitions": "2.43.0-343119",
98
+ "@fluidframework/driver-definitions": "2.43.0-343119",
99
+ "@fluidframework/merge-tree": "2.43.0-343119",
100
+ "@fluidframework/runtime-definitions": "2.43.0-343119",
101
+ "@fluidframework/runtime-utils": "2.43.0-343119",
102
+ "@fluidframework/shared-object-base": "2.43.0-343119",
103
+ "@fluidframework/telemetry-utils": "2.43.0-343119",
104
104
  "double-ended-queue": "^2.1.0-0",
105
105
  "uuid": "^9.0.0"
106
106
  },
107
107
  "devDependencies": {
108
108
  "@arethetypeswrong/cli": "^0.17.1",
109
109
  "@biomejs/biome": "~1.9.3",
110
- "@fluid-internal/mocha-test-setup": "~2.42.0",
111
- "@fluid-private/stochastic-test-utils": "~2.42.0",
112
- "@fluid-private/test-dds-utils": "~2.42.0",
110
+ "@fluid-internal/mocha-test-setup": "2.43.0-343119",
111
+ "@fluid-private/stochastic-test-utils": "2.43.0-343119",
112
+ "@fluid-private/test-dds-utils": "2.43.0-343119",
113
113
  "@fluid-tools/benchmark": "^0.51.0",
114
114
  "@fluid-tools/build-cli": "^0.55.0",
115
115
  "@fluidframework/build-common": "^2.0.3",
116
116
  "@fluidframework/build-tools": "^0.55.0",
117
- "@fluidframework/container-definitions": "~2.42.0",
117
+ "@fluidframework/container-definitions": "2.43.0-343119",
118
118
  "@fluidframework/eslint-config-fluid": "^5.7.4",
119
- "@fluidframework/sequence-previous": "npm:@fluidframework/sequence@2.41.0",
120
- "@fluidframework/test-runtime-utils": "~2.42.0",
119
+ "@fluidframework/sequence-previous": "npm:@fluidframework/sequence@2.42.0",
120
+ "@fluidframework/test-runtime-utils": "2.43.0-343119",
121
121
  "@microsoft/api-extractor": "7.52.8",
122
122
  "@types/diff": "^3.5.1",
123
123
  "@types/double-ended-queue": "^2.1.0",
@@ -25,14 +25,18 @@ import {
25
25
  endpointPosAndSide,
26
26
  type ISegmentInternal,
27
27
  createLocalReconnectingPerspective,
28
+ DoublyLinkedList,
29
+ type ListNode,
28
30
  } from "@fluidframework/merge-tree/internal";
29
31
  import { LoggingError, UsageError } from "@fluidframework/telemetry-utils/internal";
30
32
  import { v4 as uuid } from "uuid";
31
33
 
32
34
  import {
33
- IMapMessageLocalMetadata,
35
+ IntervalMessageLocalMetadata,
34
36
  SequenceOptions,
35
37
  type IIntervalCollectionTypeOperationValue,
38
+ type IntervalAddLocalMetadata,
39
+ type IntervalChangeLocalMetadata,
36
40
  } from "./intervalCollectionMapInterfaces.js";
37
41
  import {
38
42
  createIdIntervalIndex,
@@ -600,6 +604,9 @@ export interface ISequenceIntervalCollection
600
604
  { start, end, props }: { start?: SequencePlace; end?: SequencePlace; props?: PropertySet },
601
605
  ): SequenceInterval | undefined;
602
606
 
607
+ /**
608
+ * @deprecated This api is not meant or necessary for external consumption and will be removed in subsequent release
609
+ */
603
610
  attachDeserializer(onDeserialize: DeserializeCallback): void;
604
611
  /**
605
612
  * @returns an iterator over all intervals in this collection.
@@ -686,6 +693,56 @@ export interface ISequenceIntervalCollection
686
693
  nextInterval(pos: number): SequenceInterval | undefined;
687
694
  }
688
695
 
696
+ type PendingChanges = Partial<
697
+ Record<
698
+ string,
699
+ {
700
+ /**
701
+ * The local metadatas are stores in order of submission, FIFO.
702
+ * This matches how ops are ordered, and should maintained
703
+ * across, submit, process, resubmit, and rollback.
704
+ */
705
+ local: DoublyLinkedList<IntervalMessageLocalMetadata>;
706
+ /**
707
+ * The endpointChanges are unordered, and are used to determine
708
+ * if any local changes also change the endpoints. The nodes of the
709
+ * list are also stored in the individual change op metadatas, and
710
+ * are removed as those metadatas are handled.
711
+ */
712
+ endpointChanges?: DoublyLinkedList<
713
+ IntervalAddLocalMetadata | IntervalChangeLocalMetadata
714
+ >;
715
+ }
716
+ >
717
+ >;
718
+
719
+ function removeMetadataFromPendingChanges(
720
+ localOpMetadataNode: ListNode<IntervalMessageLocalMetadata> | unknown,
721
+ ): IntervalMessageLocalMetadata {
722
+ const acked = (localOpMetadataNode as ListNode<IntervalMessageLocalMetadata>)?.remove()
723
+ ?.data;
724
+ assert(acked !== undefined, "local change must exist");
725
+ acked.endpointChangesNode?.remove();
726
+ return acked;
727
+ }
728
+
729
+ function clearEmptyPendingEntry(pendingChanges: PendingChanges, id: string) {
730
+ const pending = pendingChanges[id];
731
+ assert(pending !== undefined, "pending must exist for local process");
732
+ if (pending.local.empty) {
733
+ assert(
734
+ pending.endpointChanges?.empty !== false,
735
+ "endpointChanges must be empty if not pending changes",
736
+ );
737
+ // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
738
+ delete pendingChanges[id];
739
+ }
740
+ }
741
+
742
+ function hasEndpointChanges(serialized: SerializedIntervalDelta) {
743
+ return serialized.start !== undefined && serialized.end !== undefined;
744
+ }
745
+
689
746
  /**
690
747
  * {@inheritdoc IIntervalCollection}
691
748
  */
@@ -697,33 +754,37 @@ export class IntervalCollection
697
754
  private localCollection: LocalIntervalCollection | undefined;
698
755
  private onDeserialize: DeserializeCallback | undefined;
699
756
  private client: Client | undefined;
700
- private readonly localSeqToSerializedInterval = new Map<
701
- number,
702
- ISerializedInterval | SerializedIntervalDelta
703
- >();
704
- private readonly localSeqToRebasedInterval = new Map<
705
- number,
706
- ISerializedInterval | SerializedIntervalDelta
707
- >();
708
- private readonly pendingChanges: Map<string, ISerializedIntervalCollectionV1> = new Map<
709
- string,
710
- ISerializedIntervalCollectionV1
711
- >();
757
+
758
+ private readonly pending: PendingChanges = {};
712
759
 
713
760
  public get attached(): boolean {
714
761
  return !!this.localCollection;
715
762
  }
716
763
 
764
+ private readonly submitDelta: (
765
+ op: IIntervalCollectionTypeOperationValue,
766
+ md: IntervalMessageLocalMetadata,
767
+ ) => void;
768
+
717
769
  constructor(
718
- private readonly submitDelta: (
719
- op: IIntervalCollectionTypeOperationValue,
720
- md: IMapMessageLocalMetadata,
721
- ) => void,
770
+ submitDelta: (op: IIntervalCollectionTypeOperationValue, md: unknown) => void,
722
771
  serializedIntervals: ISerializedIntervalCollectionV1 | ISerializedIntervalCollectionV2,
723
772
  private readonly options: Partial<SequenceOptions> = {},
724
773
  ) {
725
774
  super();
726
775
 
776
+ this.submitDelta = (op, md) => {
777
+ const { id } = getSerializedProperties(op.value);
778
+ const pending = (this.pending[id] ??= {
779
+ local: new DoublyLinkedList(),
780
+ });
781
+ if (md.type === "add" || (md.type === "change" && hasEndpointChanges(op.value))) {
782
+ const endpointChanges = (pending.endpointChanges ??= new DoublyLinkedList());
783
+ md.endpointChangesNode = endpointChanges.push(md).last;
784
+ }
785
+ submitDelta(op, pending.local.push(md).last);
786
+ };
787
+
727
788
  this.savedSerializedIntervals = Array.isArray(serializedIntervals)
728
789
  ? serializedIntervals
729
790
  : serializedIntervals.intervals.map((i) =>
@@ -765,14 +826,12 @@ export class IntervalCollection
765
826
  return true;
766
827
  }
767
828
 
768
- public rollback(
769
- op: IIntervalCollectionTypeOperationValue,
770
- localOpMetadata: IMapMessageLocalMetadata,
771
- ) {
772
- const { opName, value } = op;
829
+ public rollback(op: IIntervalCollectionTypeOperationValue, maybeMetadata: unknown) {
830
+ const localOpMetadata = removeMetadataFromPendingChanges(maybeMetadata);
831
+ const { value } = op;
773
832
  const { id, properties } = getSerializedProperties(value);
774
- const { localSeq, previous } = localOpMetadata;
775
- switch (opName) {
833
+ const { type } = localOpMetadata;
834
+ switch (type) {
776
835
  case "add": {
777
836
  const interval = this.getIntervalById(id);
778
837
  if (interval) {
@@ -781,9 +840,8 @@ export class IntervalCollection
781
840
  break;
782
841
  }
783
842
  case "change": {
784
- assert(previous !== undefined, 0xb7c /* must have previous for change */);
785
-
786
- const endpointsChanged = value.start !== undefined && value.end !== undefined;
843
+ const { previous } = localOpMetadata;
844
+ const endpointsChanged = hasEndpointChanges(value);
787
845
  const start = endpointsChanged
788
846
  ? toOptionalSequencePlace(previous.start, previous.startSide)
789
847
  : undefined;
@@ -796,14 +854,10 @@ export class IntervalCollection
796
854
  props: Object.keys(properties).length > 0 ? properties : undefined,
797
855
  rollback: true,
798
856
  });
799
- this.localSeqToSerializedInterval.delete(localSeq);
800
- if (endpointsChanged) {
801
- this.removePendingChange(value);
802
- }
803
857
  break;
804
858
  }
805
859
  case "delete": {
806
- assert(previous !== undefined, 0xb7d /* must have previous for delete */);
860
+ const { previous } = localOpMetadata;
807
861
  this.add({
808
862
  id,
809
863
  start: toSequencePlace(previous.start, previous.startSide),
@@ -814,20 +868,37 @@ export class IntervalCollection
814
868
  break;
815
869
  }
816
870
  default:
817
- unreachableCase(opName);
871
+ unreachableCase(type);
818
872
  }
873
+
874
+ clearEmptyPendingEntry(this.pending, id);
819
875
  }
820
876
 
821
877
  public process(
822
878
  op: IIntervalCollectionTypeOperationValue,
823
879
  local: boolean,
824
880
  message: ISequencedDocumentMessage,
825
- localOpMetadata: IMapMessageLocalMetadata,
881
+ maybeMetadata: unknown,
826
882
  ) {
883
+ const localOpMetadata = local
884
+ ? removeMetadataFromPendingChanges(maybeMetadata)
885
+ : undefined;
886
+
827
887
  const { opName, value } = op;
888
+ assert(
889
+ (local === false && localOpMetadata === undefined) || opName === localOpMetadata?.type,
890
+ "must be same type",
891
+ );
828
892
  switch (opName) {
829
893
  case "add": {
830
- this.ackAdd(value, local, message, localOpMetadata);
894
+ this.ackAdd(
895
+ value,
896
+ local,
897
+ message,
898
+ // this cast is safe because of the above assert which
899
+ // validates the op and metadata types match for local changes
900
+ localOpMetadata as IntervalAddLocalMetadata | undefined,
901
+ );
831
902
  break;
832
903
  }
833
904
 
@@ -837,23 +908,36 @@ export class IntervalCollection
837
908
  }
838
909
 
839
910
  case "change": {
840
- this.ackChange(value, local, message, localOpMetadata);
911
+ this.ackChange(value, local, message);
841
912
  break;
842
913
  }
843
914
  default:
844
915
  unreachableCase(opName);
845
916
  }
917
+
918
+ if (local) {
919
+ const { id } = getSerializedProperties(value);
920
+ clearEmptyPendingEntry(this.pending, id);
921
+ }
846
922
  }
847
923
 
848
924
  public resubmitMessage(
849
925
  op: IIntervalCollectionTypeOperationValue,
850
- localOpMetadata: IMapMessageLocalMetadata,
926
+ maybeMetadata: unknown,
851
927
  ): void {
852
928
  const { opName, value } = op;
929
+
930
+ const localOpMetadata = removeMetadataFromPendingChanges(maybeMetadata);
931
+
853
932
  const rebasedValue =
854
- opName === "delete" ? value : this.rebaseLocalInterval(opName, value, localOpMetadata);
933
+ localOpMetadata.endpointChangesNode === undefined
934
+ ? value
935
+ : this.rebaseLocalInterval(localOpMetadata);
936
+
855
937
  if (rebasedValue === undefined) {
856
- return undefined;
938
+ const { id } = getSerializedProperties(value);
939
+ clearEmptyPendingEntry(this.pending, id);
940
+ return;
857
941
  }
858
942
 
859
943
  this.submitDelta({ opName, value: rebasedValue as any }, localOpMetadata);
@@ -904,28 +988,28 @@ export class IntervalCollection
904
988
  }
905
989
 
906
990
  const { clientId } = this.client.getCollabWindow();
907
- const { segment, offset } = this.client.getContainingSegment(
908
- pos,
909
- {
910
- referenceSequenceNumber: seqNumberFrom,
911
- clientId: this.client.getLongClientId(clientId),
912
- },
913
- localSeq,
914
- );
991
+ const { segment, offset } =
992
+ this.client.getContainingSegment(
993
+ pos,
994
+ {
995
+ referenceSequenceNumber: seqNumberFrom,
996
+ clientId: this.client.getLongClientId(clientId),
997
+ },
998
+ localSeq,
999
+ ) ?? {};
915
1000
 
916
1001
  // if segment is undefined, it slid off the string
917
- assert(segment !== undefined, 0x54e /* No segment found */);
1002
+ assert(segment !== undefined && offset !== undefined, 0x54e /* No segment found */);
918
1003
 
919
- const segoff =
920
- getSlideToSegoff(
921
- { segment, offset },
922
- undefined,
923
- createLocalReconnectingPerspective(this.client.getCurrentSeq(), clientId, localSeq),
924
- this.options.mergeTreeReferencesCanSlideToEndpoint,
925
- ) ?? segment;
1004
+ const segoff = getSlideToSegoff(
1005
+ { segment, offset },
1006
+ undefined,
1007
+ createLocalReconnectingPerspective(this.client.getCurrentSeq(), clientId, localSeq),
1008
+ this.options.mergeTreeReferencesCanSlideToEndpoint,
1009
+ );
926
1010
 
927
1011
  // case happens when rebasing op, but concurrently entire string has been deleted
928
- if (segoff.segment === undefined || segoff.offset === undefined) {
1012
+ if (segoff?.segment === undefined || segoff.offset === undefined) {
929
1013
  return DetachedReferencePosition;
930
1014
  }
931
1015
 
@@ -937,17 +1021,13 @@ export class IntervalCollection
937
1021
  }
938
1022
 
939
1023
  private computeRebasedPositions(
940
- localSeq: number,
1024
+ localOpMetadata: IntervalAddLocalMetadata | IntervalChangeLocalMetadata,
941
1025
  ): ISerializedInterval | SerializedIntervalDelta {
942
1026
  assert(
943
1027
  this.client !== undefined,
944
1028
  0x550 /* Client should be defined when computing rebased position */,
945
1029
  );
946
- const original = this.localSeqToSerializedInterval.get(localSeq);
947
- assert(
948
- original !== undefined,
949
- 0x551 /* Failed to store pending serialized interval info for this localSeq. */,
950
- );
1030
+ const { localSeq, original } = localOpMetadata;
951
1031
  const rebased = { ...original };
952
1032
  const { start, end, sequenceNumber } = original;
953
1033
  if (start !== undefined) {
@@ -972,8 +1052,12 @@ export class IntervalCollection
972
1052
  this.client = client;
973
1053
  if (client) {
974
1054
  client.on("normalize", () => {
975
- for (const localSeq of this.localSeqToSerializedInterval.keys()) {
976
- this.localSeqToRebasedInterval.set(localSeq, this.computeRebasedPositions(localSeq));
1055
+ for (const pending of Object.values(this.pending)) {
1056
+ if (pending?.endpointChanges !== undefined) {
1057
+ for (const local of pending.endpointChanges) {
1058
+ local.data.rebased = this.computeRebasedPositions(local.data);
1059
+ }
1060
+ }
977
1061
  }
978
1062
  });
979
1063
  }
@@ -1124,16 +1208,15 @@ export class IntervalCollection
1124
1208
  const serializedInterval: ISerializedInterval = interval.serialize();
1125
1209
  const localSeq = this.getNextLocalSeq();
1126
1210
  if (this.isCollaborating && rollback !== true) {
1127
- this.localSeqToSerializedInterval.set(localSeq, serializedInterval);
1128
-
1129
1211
  this.submitDelta(
1130
1212
  {
1131
1213
  opName: "add",
1132
1214
  value: serializedInterval,
1133
1215
  },
1134
1216
  {
1217
+ type: "add",
1135
1218
  localSeq,
1136
- intervalId,
1219
+ original: serializedInterval,
1137
1220
  },
1138
1221
  );
1139
1222
  }
@@ -1164,15 +1247,16 @@ export class IntervalCollection
1164
1247
  if (interval) {
1165
1248
  // Local ops get submitted to the server. Remote ops have the deserializer run.
1166
1249
  if (local && rollback !== true) {
1250
+ const value = interval.serialize();
1167
1251
  this.submitDelta(
1168
1252
  {
1169
1253
  opName: "delete",
1170
- value: interval.serialize(),
1254
+ value,
1171
1255
  },
1172
1256
  {
1257
+ type: "delete",
1173
1258
  localSeq: this.getNextLocalSeq(),
1174
- previous: interval.serialize(),
1175
- intervalId: interval.getIntervalId(),
1259
+ previous: value,
1176
1260
  },
1177
1261
  );
1178
1262
  } else {
@@ -1257,19 +1341,19 @@ export class IntervalCollection
1257
1341
  ).serializeDelta({ props, includeEndpoints: changeEndpoints });
1258
1342
  const localSeq = this.getNextLocalSeq();
1259
1343
 
1260
- this.localSeqToSerializedInterval.set(localSeq, serializedInterval);
1261
- this.addPendingChange(id, serializedInterval);
1344
+ const metadata: IntervalChangeLocalMetadata = {
1345
+ type: "change",
1346
+ localSeq,
1347
+ previous: interval.serialize(),
1348
+ original: serializedInterval,
1349
+ };
1262
1350
 
1263
1351
  this.submitDelta(
1264
1352
  {
1265
1353
  opName: "change",
1266
1354
  value: serializedInterval,
1267
1355
  },
1268
- {
1269
- localSeq,
1270
- previous: interval.serialize(),
1271
- intervalId: id,
1272
- },
1356
+ metadata,
1273
1357
  );
1274
1358
  }
1275
1359
  if (deltaProps !== undefined) {
@@ -1298,51 +1382,14 @@ export class IntervalCollection
1298
1382
  return this.client?.getCollabWindow().collaborating ?? false;
1299
1383
  }
1300
1384
 
1301
- private addPendingChange(id: string, serializedInterval: SerializedIntervalDelta) {
1302
- if (!this.isCollaborating) {
1303
- return;
1304
- }
1305
- assert(
1306
- (serializedInterval.start === undefined) === (serializedInterval.end === undefined),
1307
- 0xbb0 /* both start and end must be set or unset */,
1308
- );
1309
- if (serializedInterval.start !== undefined || serializedInterval.end !== undefined) {
1310
- const entries = this.pendingChanges.get(id) ?? [];
1311
- this.pendingChanges.set(id, entries);
1312
- entries.push(serializedInterval as any);
1313
- }
1314
- }
1315
-
1316
- private removePendingChange(serializedInterval: SerializedIntervalDelta) {
1317
- // Change ops always have an ID.
1318
- const { id } = getSerializedProperties(serializedInterval);
1319
- if (serializedInterval.start !== undefined) {
1320
- const entries = this.pendingChanges.get(id);
1321
- if (entries) {
1322
- const pendingChange = entries.shift();
1323
- if (entries.length === 0) {
1324
- this.pendingChanges.delete(id);
1325
- }
1326
- if (
1327
- pendingChange?.start !== serializedInterval.start ||
1328
- pendingChange?.end !== serializedInterval.end
1329
- ) {
1330
- throw new LoggingError("Mismatch in pending changes");
1331
- }
1332
- }
1333
- }
1334
- }
1335
-
1336
- private hasPendingChanges(id: string) {
1337
- const entries = this.pendingChanges.get(id);
1338
- return entries && entries.length !== 0;
1385
+ private hasPendingEndpointChanges(id: string) {
1386
+ return this.pending[id]?.endpointChanges?.empty === false;
1339
1387
  }
1340
1388
 
1341
1389
  public ackChange(
1342
1390
  serializedInterval: SerializedIntervalDelta,
1343
1391
  local: boolean,
1344
1392
  op: ISequencedDocumentMessage,
1345
- localOpMetadata: IMapMessageLocalMetadata | undefined,
1346
1393
  ) {
1347
1394
  if (!this.localCollection) {
1348
1395
  throw new LoggingError("Attach must be called before accessing intervals");
@@ -1355,16 +1402,6 @@ export class IntervalCollection
1355
1402
  assert(id !== undefined, 0x3fe /* id must exist on the interval */);
1356
1403
  const interval: SequenceIntervalClass | undefined = this.getIntervalById(id);
1357
1404
 
1358
- if (local) {
1359
- assert(
1360
- localOpMetadata !== undefined,
1361
- 0x552 /* op metadata should be defined for local op */,
1362
- );
1363
- // This is an ack from the server. Remove the pending change.
1364
- this.localSeqToSerializedInterval.delete(localOpMetadata?.localSeq);
1365
- this.removePendingChange(serializedInterval);
1366
- }
1367
-
1368
1405
  if (!interval) {
1369
1406
  // The interval has been removed locally; no-op.
1370
1407
  return;
@@ -1380,7 +1417,7 @@ export class IntervalCollection
1380
1417
  let start: number | "start" | "end" | undefined;
1381
1418
  let end: number | "start" | "end" | undefined;
1382
1419
  // Track pending start/end independently of one another.
1383
- if (!this.hasPendingChanges(id)) {
1420
+ if (!this.hasPendingEndpointChanges(id)) {
1384
1421
  start = serializedInterval.start;
1385
1422
  end = serializedInterval.end;
1386
1423
  }
@@ -1440,23 +1477,22 @@ export class IntervalCollection
1440
1477
  *
1441
1478
  */
1442
1479
  public rebaseLocalInterval(
1443
- opName: string,
1444
- serializedInterval: SerializedIntervalDelta,
1445
- localOpMetadata: IMapMessageLocalMetadata,
1480
+ localOpMetadata: IntervalAddLocalMetadata | IntervalChangeLocalMetadata,
1446
1481
  ): SerializedIntervalDelta | undefined {
1482
+ const original = localOpMetadata.original;
1447
1483
  if (!this.client) {
1448
1484
  // If there's no associated mergeTree client, the originally submitted op is still correct.
1449
- return serializedInterval;
1485
+ return original;
1450
1486
  }
1451
1487
  if (!this.attached) {
1452
1488
  throw new LoggingError("attachSequence must be called");
1453
1489
  }
1454
1490
 
1455
1491
  const { localSeq } = localOpMetadata;
1456
- const { intervalType, properties, stickiness, startSide, endSide } = serializedInterval;
1457
- const { id } = getSerializedProperties(serializedInterval);
1458
- const { start: startRebased, end: endRebased } =
1459
- this.localSeqToRebasedInterval.get(localSeq) ?? this.computeRebasedPositions(localSeq);
1492
+ const { intervalType, properties, stickiness, startSide, endSide } = original;
1493
+ const { id } = getSerializedProperties(original);
1494
+ const { start: startRebased, end: endRebased } = (localOpMetadata.rebased ??=
1495
+ this.computeRebasedPositions(localOpMetadata));
1460
1496
 
1461
1497
  const localInterval = this.localCollection?.idIntervalIndex.getIntervalById(id);
1462
1498
 
@@ -1471,15 +1507,6 @@ export class IntervalCollection
1471
1507
  endSide,
1472
1508
  };
1473
1509
 
1474
- if (
1475
- opName === "change" &&
1476
- // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -- ?? is not logically equivalent when .hasPendingChangeStart returns false.
1477
- this.hasPendingChanges(id)
1478
- ) {
1479
- this.removePendingChange(serializedInterval);
1480
- this.addPendingChange(id, rebased);
1481
- }
1482
-
1483
1510
  // if the interval slid off the string, rebase the op to be a noop and delete the interval.
1484
1511
  if (
1485
1512
  !this.options.mergeTreeReferencesCanSlideToEndpoint &&
@@ -1509,28 +1536,27 @@ export class IntervalCollection
1509
1536
  private getSlideToSegment(
1510
1537
  lref: LocalReferencePosition,
1511
1538
  slidingPreference: SlidingPreference,
1512
- ): { segment: ISegment | undefined; offset: number | undefined } | undefined {
1539
+ ): { segment: ISegment; offset: number } | undefined {
1513
1540
  if (!this.client) {
1514
1541
  throw new LoggingError("client does not exist");
1515
1542
  }
1516
- const segoff: { segment: ISegmentInternal | undefined; offset: number | undefined } = {
1517
- segment: lref.getSegment(),
1543
+ const segment: ISegmentInternal | undefined = lref.getSegment();
1544
+ if (segment === undefined) {
1545
+ return undefined;
1546
+ }
1547
+ const segoff = {
1548
+ segment,
1518
1549
  offset: lref.getOffset(),
1519
1550
  };
1520
- if (segoff.segment?.localRefs?.has(lref) !== true) {
1551
+ if (segoff.segment.localRefs?.has(lref) !== true) {
1521
1552
  return undefined;
1522
1553
  }
1523
- const newSegoff = getSlideToSegoff(
1554
+ return getSlideToSegoff(
1524
1555
  segoff,
1525
1556
  slidingPreference,
1526
1557
  undefined,
1527
1558
  this.options.mergeTreeReferencesCanSlideToEndpoint,
1528
1559
  );
1529
- const value: { segment: ISegment | undefined; offset: number | undefined } | undefined =
1530
- segoff.segment === newSegoff.segment && segoff.offset === newSegoff.offset
1531
- ? undefined
1532
- : newSegoff;
1533
- return value;
1534
1560
  }
1535
1561
 
1536
1562
  private ackInterval(interval: SequenceIntervalClass, op: ISequencedDocumentMessage): void {
@@ -1551,15 +1577,16 @@ export class IntervalCollection
1551
1577
  );
1552
1578
 
1553
1579
  const id = interval.getIntervalId();
1554
- const hasPendingChange = this.hasPendingChanges(id);
1580
+ const hasPendingChange = this.hasPendingEndpointChanges(id);
1555
1581
 
1556
1582
  if (!hasPendingChange) {
1557
1583
  setSlideOnRemove(interval.start);
1558
1584
  setSlideOnRemove(interval.end);
1559
1585
  }
1560
1586
 
1561
- const needsStartUpdate = newStart !== undefined && !hasPendingChange;
1562
- const needsEndUpdate = newEnd !== undefined && !hasPendingChange;
1587
+ const needsStartUpdate =
1588
+ newStart?.segment !== interval.start.getSegment() && !hasPendingChange;
1589
+ const needsEndUpdate = newEnd?.segment !== interval.end.getSegment() && !hasPendingChange;
1563
1590
 
1564
1591
  if (needsStartUpdate || needsEndUpdate) {
1565
1592
  if (!this.localCollection) {
@@ -1628,7 +1655,7 @@ export class IntervalCollection
1628
1655
  serializedInterval: ISerializedInterval,
1629
1656
  local: boolean,
1630
1657
  op: ISequencedDocumentMessage,
1631
- localOpMetadata: IMapMessageLocalMetadata | undefined,
1658
+ localOpMetadata: IntervalAddLocalMetadata | undefined,
1632
1659
  ) {
1633
1660
  const { id, properties } = getSerializedProperties(serializedInterval);
1634
1661
 
@@ -1637,7 +1664,6 @@ export class IntervalCollection
1637
1664
  localOpMetadata !== undefined,
1638
1665
  0x553 /* op metadata should be defined for local op */,
1639
1666
  );
1640
- this.localSeqToSerializedInterval.delete(localOpMetadata.localSeq);
1641
1667
  const localInterval = this.getIntervalById(id);
1642
1668
  if (localInterval) {
1643
1669
  this.ackInterval(localInterval, op);