@fluidframework/sequence 2.41.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 (75) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/api-report/sequence.legacy.alpha.api.md +13 -5
  3. package/dist/intervalCollection.d.ts +14 -19
  4. package/dist/intervalCollection.d.ts.map +1 -1
  5. package/dist/intervalCollection.js +113 -128
  6. package/dist/intervalCollection.js.map +1 -1
  7. package/dist/intervalCollectionMap.d.ts +3 -3
  8. package/dist/intervalCollectionMap.d.ts.map +1 -1
  9. package/dist/intervalCollectionMap.js.map +1 -1
  10. package/dist/intervalCollectionMapInterfaces.d.ts +22 -4
  11. package/dist/intervalCollectionMapInterfaces.d.ts.map +1 -1
  12. package/dist/intervalCollectionMapInterfaces.js.map +1 -1
  13. package/dist/intervalIndex/overlappingIntervalsIndex.d.ts +4 -4
  14. package/dist/intervalIndex/overlappingIntervalsIndex.d.ts.map +1 -1
  15. package/dist/intervalIndex/overlappingIntervalsIndex.js.map +1 -1
  16. package/dist/intervals/intervalUtils.d.ts +12 -1
  17. package/dist/intervals/intervalUtils.d.ts.map +1 -1
  18. package/dist/intervals/intervalUtils.js.map +1 -1
  19. package/dist/intervals/sequenceInterval.d.ts +18 -5
  20. package/dist/intervals/sequenceInterval.d.ts.map +1 -1
  21. package/dist/intervals/sequenceInterval.js +2 -1
  22. package/dist/intervals/sequenceInterval.js.map +1 -1
  23. package/dist/packageVersion.d.ts +1 -1
  24. package/dist/packageVersion.d.ts.map +1 -1
  25. package/dist/packageVersion.js +1 -1
  26. package/dist/packageVersion.js.map +1 -1
  27. package/dist/revertibles.d.ts.map +1 -1
  28. package/dist/revertibles.js +6 -3
  29. package/dist/revertibles.js.map +1 -1
  30. package/dist/sequence.d.ts +2 -1
  31. package/dist/sequence.d.ts.map +1 -1
  32. package/dist/sequence.js +6 -3
  33. package/dist/sequence.js.map +1 -1
  34. package/lib/intervalCollection.d.ts +14 -19
  35. package/lib/intervalCollection.d.ts.map +1 -1
  36. package/lib/intervalCollection.js +114 -129
  37. package/lib/intervalCollection.js.map +1 -1
  38. package/lib/intervalCollectionMap.d.ts +3 -3
  39. package/lib/intervalCollectionMap.d.ts.map +1 -1
  40. package/lib/intervalCollectionMap.js.map +1 -1
  41. package/lib/intervalCollectionMapInterfaces.d.ts +22 -4
  42. package/lib/intervalCollectionMapInterfaces.d.ts.map +1 -1
  43. package/lib/intervalCollectionMapInterfaces.js.map +1 -1
  44. package/lib/intervalIndex/overlappingIntervalsIndex.d.ts +4 -4
  45. package/lib/intervalIndex/overlappingIntervalsIndex.d.ts.map +1 -1
  46. package/lib/intervalIndex/overlappingIntervalsIndex.js +1 -1
  47. package/lib/intervalIndex/overlappingIntervalsIndex.js.map +1 -1
  48. package/lib/intervals/intervalUtils.d.ts +12 -1
  49. package/lib/intervals/intervalUtils.d.ts.map +1 -1
  50. package/lib/intervals/intervalUtils.js.map +1 -1
  51. package/lib/intervals/sequenceInterval.d.ts +18 -5
  52. package/lib/intervals/sequenceInterval.d.ts.map +1 -1
  53. package/lib/intervals/sequenceInterval.js +2 -1
  54. package/lib/intervals/sequenceInterval.js.map +1 -1
  55. package/lib/packageVersion.d.ts +1 -1
  56. package/lib/packageVersion.d.ts.map +1 -1
  57. package/lib/packageVersion.js +1 -1
  58. package/lib/packageVersion.js.map +1 -1
  59. package/lib/revertibles.d.ts.map +1 -1
  60. package/lib/revertibles.js +6 -3
  61. package/lib/revertibles.js.map +1 -1
  62. package/lib/sequence.d.ts +2 -1
  63. package/lib/sequence.d.ts.map +1 -1
  64. package/lib/sequence.js +6 -3
  65. package/lib/sequence.js.map +1 -1
  66. package/package.json +17 -17
  67. package/src/intervalCollection.ts +191 -195
  68. package/src/intervalCollectionMap.ts +4 -11
  69. package/src/intervalCollectionMapInterfaces.ts +25 -5
  70. package/src/intervalIndex/overlappingIntervalsIndex.ts +15 -11
  71. package/src/intervals/intervalUtils.ts +12 -1
  72. package/src/intervals/sequenceInterval.ts +22 -4
  73. package/src/packageVersion.ts +1 -1
  74. package/src/revertibles.ts +7 -6
  75. package/src/sequence.ts +11 -13
@@ -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,37 +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 pendingChangesStart: Map<string, ISerializedIntervalCollectionV1> = new Map<
709
- string,
710
- ISerializedIntervalCollectionV1
711
- >();
712
- private readonly pendingChangesEnd: Map<string, ISerializedIntervalCollectionV1> = new Map<
713
- string,
714
- ISerializedIntervalCollectionV1
715
- >();
757
+
758
+ private readonly pending: PendingChanges = {};
716
759
 
717
760
  public get attached(): boolean {
718
761
  return !!this.localCollection;
719
762
  }
720
763
 
764
+ private readonly submitDelta: (
765
+ op: IIntervalCollectionTypeOperationValue,
766
+ md: IntervalMessageLocalMetadata,
767
+ ) => void;
768
+
721
769
  constructor(
722
- private readonly submitDelta: (
723
- op: IIntervalCollectionTypeOperationValue,
724
- md: IMapMessageLocalMetadata,
725
- ) => void,
770
+ submitDelta: (op: IIntervalCollectionTypeOperationValue, md: unknown) => void,
726
771
  serializedIntervals: ISerializedIntervalCollectionV1 | ISerializedIntervalCollectionV2,
727
772
  private readonly options: Partial<SequenceOptions> = {},
728
773
  ) {
729
774
  super();
730
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
+
731
788
  this.savedSerializedIntervals = Array.isArray(serializedIntervals)
732
789
  ? serializedIntervals
733
790
  : serializedIntervals.intervals.map((i) =>
@@ -769,14 +826,12 @@ export class IntervalCollection
769
826
  return true;
770
827
  }
771
828
 
772
- public rollback(
773
- op: IIntervalCollectionTypeOperationValue,
774
- localOpMetadata: IMapMessageLocalMetadata,
775
- ) {
776
- const { opName, value } = op;
829
+ public rollback(op: IIntervalCollectionTypeOperationValue, maybeMetadata: unknown) {
830
+ const localOpMetadata = removeMetadataFromPendingChanges(maybeMetadata);
831
+ const { value } = op;
777
832
  const { id, properties } = getSerializedProperties(value);
778
- const { localSeq, previous } = localOpMetadata;
779
- switch (opName) {
833
+ const { type } = localOpMetadata;
834
+ switch (type) {
780
835
  case "add": {
781
836
  const interval = this.getIntervalById(id);
782
837
  if (interval) {
@@ -785,9 +840,8 @@ export class IntervalCollection
785
840
  break;
786
841
  }
787
842
  case "change": {
788
- assert(previous !== undefined, 0xb7c /* must have previous for change */);
789
-
790
- const endpointsChanged = value.start !== undefined && value.end !== undefined;
843
+ const { previous } = localOpMetadata;
844
+ const endpointsChanged = hasEndpointChanges(value);
791
845
  const start = endpointsChanged
792
846
  ? toOptionalSequencePlace(previous.start, previous.startSide)
793
847
  : undefined;
@@ -800,14 +854,10 @@ export class IntervalCollection
800
854
  props: Object.keys(properties).length > 0 ? properties : undefined,
801
855
  rollback: true,
802
856
  });
803
- this.localSeqToSerializedInterval.delete(localSeq);
804
- if (endpointsChanged) {
805
- this.removePendingChange(value);
806
- }
807
857
  break;
808
858
  }
809
859
  case "delete": {
810
- assert(previous !== undefined, 0xb7d /* must have previous for delete */);
860
+ const { previous } = localOpMetadata;
811
861
  this.add({
812
862
  id,
813
863
  start: toSequencePlace(previous.start, previous.startSide),
@@ -818,20 +868,37 @@ export class IntervalCollection
818
868
  break;
819
869
  }
820
870
  default:
821
- unreachableCase(opName);
871
+ unreachableCase(type);
822
872
  }
873
+
874
+ clearEmptyPendingEntry(this.pending, id);
823
875
  }
824
876
 
825
877
  public process(
826
878
  op: IIntervalCollectionTypeOperationValue,
827
879
  local: boolean,
828
880
  message: ISequencedDocumentMessage,
829
- localOpMetadata: IMapMessageLocalMetadata,
881
+ maybeMetadata: unknown,
830
882
  ) {
883
+ const localOpMetadata = local
884
+ ? removeMetadataFromPendingChanges(maybeMetadata)
885
+ : undefined;
886
+
831
887
  const { opName, value } = op;
888
+ assert(
889
+ (local === false && localOpMetadata === undefined) || opName === localOpMetadata?.type,
890
+ "must be same type",
891
+ );
832
892
  switch (opName) {
833
893
  case "add": {
834
- 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
+ );
835
902
  break;
836
903
  }
837
904
 
@@ -841,24 +908,36 @@ export class IntervalCollection
841
908
  }
842
909
 
843
910
  case "change": {
844
- this.ackChange(value, local, message, localOpMetadata);
911
+ this.ackChange(value, local, message);
845
912
  break;
846
913
  }
847
914
  default:
848
915
  unreachableCase(opName);
849
916
  }
917
+
918
+ if (local) {
919
+ const { id } = getSerializedProperties(value);
920
+ clearEmptyPendingEntry(this.pending, id);
921
+ }
850
922
  }
851
923
 
852
924
  public resubmitMessage(
853
925
  op: IIntervalCollectionTypeOperationValue,
854
- localOpMetadata: IMapMessageLocalMetadata,
926
+ maybeMetadata: unknown,
855
927
  ): void {
856
928
  const { opName, value } = op;
857
- const { localSeq } = localOpMetadata;
929
+
930
+ const localOpMetadata = removeMetadataFromPendingChanges(maybeMetadata);
931
+
858
932
  const rebasedValue =
859
- opName === "delete" ? value : this.rebaseLocalInterval(opName, value, localSeq);
933
+ localOpMetadata.endpointChangesNode === undefined
934
+ ? value
935
+ : this.rebaseLocalInterval(localOpMetadata);
936
+
860
937
  if (rebasedValue === undefined) {
861
- return undefined;
938
+ const { id } = getSerializedProperties(value);
939
+ clearEmptyPendingEntry(this.pending, id);
940
+ return;
862
941
  }
863
942
 
864
943
  this.submitDelta({ opName, value: rebasedValue as any }, localOpMetadata);
@@ -909,28 +988,28 @@ export class IntervalCollection
909
988
  }
910
989
 
911
990
  const { clientId } = this.client.getCollabWindow();
912
- const { segment, offset } = this.client.getContainingSegment(
913
- pos,
914
- {
915
- referenceSequenceNumber: seqNumberFrom,
916
- clientId: this.client.getLongClientId(clientId),
917
- },
918
- localSeq,
919
- );
991
+ const { segment, offset } =
992
+ this.client.getContainingSegment(
993
+ pos,
994
+ {
995
+ referenceSequenceNumber: seqNumberFrom,
996
+ clientId: this.client.getLongClientId(clientId),
997
+ },
998
+ localSeq,
999
+ ) ?? {};
920
1000
 
921
1001
  // if segment is undefined, it slid off the string
922
- assert(segment !== undefined, 0x54e /* No segment found */);
1002
+ assert(segment !== undefined && offset !== undefined, 0x54e /* No segment found */);
923
1003
 
924
- const segoff =
925
- getSlideToSegoff(
926
- { segment, offset },
927
- undefined,
928
- createLocalReconnectingPerspective(this.client.getCurrentSeq(), clientId, localSeq),
929
- this.options.mergeTreeReferencesCanSlideToEndpoint,
930
- ) ?? segment;
1004
+ const segoff = getSlideToSegoff(
1005
+ { segment, offset },
1006
+ undefined,
1007
+ createLocalReconnectingPerspective(this.client.getCurrentSeq(), clientId, localSeq),
1008
+ this.options.mergeTreeReferencesCanSlideToEndpoint,
1009
+ );
931
1010
 
932
1011
  // case happens when rebasing op, but concurrently entire string has been deleted
933
- if (segoff.segment === undefined || segoff.offset === undefined) {
1012
+ if (segoff?.segment === undefined || segoff.offset === undefined) {
934
1013
  return DetachedReferencePosition;
935
1014
  }
936
1015
 
@@ -942,17 +1021,13 @@ export class IntervalCollection
942
1021
  }
943
1022
 
944
1023
  private computeRebasedPositions(
945
- localSeq: number,
1024
+ localOpMetadata: IntervalAddLocalMetadata | IntervalChangeLocalMetadata,
946
1025
  ): ISerializedInterval | SerializedIntervalDelta {
947
1026
  assert(
948
1027
  this.client !== undefined,
949
1028
  0x550 /* Client should be defined when computing rebased position */,
950
1029
  );
951
- const original = this.localSeqToSerializedInterval.get(localSeq);
952
- assert(
953
- original !== undefined,
954
- 0x551 /* Failed to store pending serialized interval info for this localSeq. */,
955
- );
1030
+ const { localSeq, original } = localOpMetadata;
956
1031
  const rebased = { ...original };
957
1032
  const { start, end, sequenceNumber } = original;
958
1033
  if (start !== undefined) {
@@ -977,8 +1052,12 @@ export class IntervalCollection
977
1052
  this.client = client;
978
1053
  if (client) {
979
1054
  client.on("normalize", () => {
980
- for (const localSeq of this.localSeqToSerializedInterval.keys()) {
981
- 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
+ }
982
1061
  }
983
1062
  });
984
1063
  }
@@ -1110,8 +1189,10 @@ export class IntervalCollection
1110
1189
 
1111
1190
  this.assertStickinessEnabled(start, end);
1112
1191
 
1192
+ const intervalId = id ?? uuid();
1193
+
1113
1194
  const interval: SequenceIntervalClass = this.localCollection.addInterval(
1114
- id ?? uuid(),
1195
+ intervalId,
1115
1196
  toSequencePlace(startPos, startSide),
1116
1197
  toSequencePlace(endPos, endSide),
1117
1198
  props,
@@ -1127,15 +1208,15 @@ export class IntervalCollection
1127
1208
  const serializedInterval: ISerializedInterval = interval.serialize();
1128
1209
  const localSeq = this.getNextLocalSeq();
1129
1210
  if (this.isCollaborating && rollback !== true) {
1130
- this.localSeqToSerializedInterval.set(localSeq, serializedInterval);
1131
-
1132
1211
  this.submitDelta(
1133
1212
  {
1134
1213
  opName: "add",
1135
1214
  value: serializedInterval,
1136
1215
  },
1137
1216
  {
1217
+ type: "add",
1138
1218
  localSeq,
1219
+ original: serializedInterval,
1139
1220
  },
1140
1221
  );
1141
1222
  }
@@ -1166,14 +1247,16 @@ export class IntervalCollection
1166
1247
  if (interval) {
1167
1248
  // Local ops get submitted to the server. Remote ops have the deserializer run.
1168
1249
  if (local && rollback !== true) {
1250
+ const value = interval.serialize();
1169
1251
  this.submitDelta(
1170
1252
  {
1171
1253
  opName: "delete",
1172
- value: interval.serialize(),
1254
+ value,
1173
1255
  },
1174
1256
  {
1257
+ type: "delete",
1175
1258
  localSeq: this.getNextLocalSeq(),
1176
- previous: interval.serialize(),
1259
+ previous: value,
1177
1260
  },
1178
1261
  );
1179
1262
  } else {
@@ -1258,18 +1341,19 @@ export class IntervalCollection
1258
1341
  ).serializeDelta({ props, includeEndpoints: changeEndpoints });
1259
1342
  const localSeq = this.getNextLocalSeq();
1260
1343
 
1261
- this.localSeqToSerializedInterval.set(localSeq, serializedInterval);
1262
- this.addPendingChange(id, serializedInterval);
1344
+ const metadata: IntervalChangeLocalMetadata = {
1345
+ type: "change",
1346
+ localSeq,
1347
+ previous: interval.serialize(),
1348
+ original: serializedInterval,
1349
+ };
1263
1350
 
1264
1351
  this.submitDelta(
1265
1352
  {
1266
1353
  opName: "change",
1267
1354
  value: serializedInterval,
1268
1355
  },
1269
- {
1270
- localSeq,
1271
- previous: interval.serialize(),
1272
- },
1356
+ metadata,
1273
1357
  );
1274
1358
  }
1275
1359
  if (deltaProps !== undefined) {
@@ -1298,98 +1382,26 @@ 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
- if (serializedInterval.start !== undefined) {
1306
- this.addPendingChangeHelper(id, this.pendingChangesStart, serializedInterval);
1307
- }
1308
- if (serializedInterval.end !== undefined) {
1309
- this.addPendingChangeHelper(id, this.pendingChangesEnd, serializedInterval);
1310
- }
1311
- }
1312
-
1313
- private addPendingChangeHelper(
1314
- id: string,
1315
- pendingChanges: Map<string, SerializedIntervalDelta[]>,
1316
- serializedInterval: SerializedIntervalDelta,
1317
- ) {
1318
- let entries: SerializedIntervalDelta[] | undefined = pendingChanges.get(id);
1319
- if (!entries) {
1320
- entries = [];
1321
- pendingChanges.set(id, entries);
1322
- }
1323
- entries.push(serializedInterval);
1324
- }
1325
-
1326
- private removePendingChange(serializedInterval: SerializedIntervalDelta) {
1327
- // Change ops always have an ID.
1328
- const { id } = getSerializedProperties(serializedInterval);
1329
- if (serializedInterval.start !== undefined) {
1330
- this.removePendingChangeHelper(id, this.pendingChangesStart, serializedInterval);
1331
- }
1332
- if (serializedInterval.end !== undefined) {
1333
- this.removePendingChangeHelper(id, this.pendingChangesEnd, serializedInterval);
1334
- }
1335
- }
1336
-
1337
- private removePendingChangeHelper(
1338
- id: string,
1339
- pendingChanges: Map<string, SerializedIntervalDelta[]>,
1340
- serializedInterval: SerializedIntervalDelta,
1341
- ) {
1342
- const entries = pendingChanges.get(id);
1343
- if (entries) {
1344
- const pendingChange = entries.shift();
1345
- if (entries.length === 0) {
1346
- pendingChanges.delete(id);
1347
- }
1348
- if (
1349
- pendingChange?.start !== serializedInterval.start ||
1350
- pendingChange?.end !== serializedInterval.end
1351
- ) {
1352
- throw new LoggingError("Mismatch in pending changes");
1353
- }
1354
- }
1355
- }
1356
-
1357
- private hasPendingChangeStart(id: string) {
1358
- const entries = this.pendingChangesStart.get(id);
1359
- return entries && entries.length !== 0;
1360
- }
1361
-
1362
- private hasPendingChangeEnd(id: string) {
1363
- const entries = this.pendingChangesEnd.get(id);
1364
- return entries && entries.length !== 0;
1385
+ private hasPendingEndpointChanges(id: string) {
1386
+ return this.pending[id]?.endpointChanges?.empty === false;
1365
1387
  }
1366
1388
 
1367
1389
  public ackChange(
1368
1390
  serializedInterval: SerializedIntervalDelta,
1369
1391
  local: boolean,
1370
1392
  op: ISequencedDocumentMessage,
1371
- localOpMetadata: IMapMessageLocalMetadata | undefined,
1372
1393
  ) {
1373
1394
  if (!this.localCollection) {
1374
1395
  throw new LoggingError("Attach must be called before accessing intervals");
1375
1396
  }
1376
1397
 
1377
- if (local) {
1378
- assert(
1379
- localOpMetadata !== undefined,
1380
- 0x552 /* op metadata should be defined for local op */,
1381
- );
1382
- this.localSeqToSerializedInterval.delete(localOpMetadata?.localSeq);
1383
- // This is an ack from the server. Remove the pending change.
1384
- this.removePendingChange(serializedInterval);
1385
- }
1386
-
1387
1398
  // Note that the ID is in the property bag only to allow us to find the interval.
1388
1399
  // This API cannot change the ID, and writing to the ID property will result in an exception. So we
1389
1400
  // strip it out of the properties here.
1390
1401
  const { id, properties } = getSerializedProperties(serializedInterval);
1391
1402
  assert(id !== undefined, 0x3fe /* id must exist on the interval */);
1392
1403
  const interval: SequenceIntervalClass | undefined = this.getIntervalById(id);
1404
+
1393
1405
  if (!interval) {
1394
1406
  // The interval has been removed locally; no-op.
1395
1407
  return;
@@ -1405,10 +1417,8 @@ export class IntervalCollection
1405
1417
  let start: number | "start" | "end" | undefined;
1406
1418
  let end: number | "start" | "end" | undefined;
1407
1419
  // Track pending start/end independently of one another.
1408
- if (!this.hasPendingChangeStart(id)) {
1420
+ if (!this.hasPendingEndpointChanges(id)) {
1409
1421
  start = serializedInterval.start;
1410
- }
1411
- if (!this.hasPendingChangeEnd(id)) {
1412
1422
  end = serializedInterval.end;
1413
1423
  }
1414
1424
 
@@ -1467,22 +1477,22 @@ export class IntervalCollection
1467
1477
  *
1468
1478
  */
1469
1479
  public rebaseLocalInterval(
1470
- opName: string,
1471
- serializedInterval: SerializedIntervalDelta,
1472
- localSeq: number,
1480
+ localOpMetadata: IntervalAddLocalMetadata | IntervalChangeLocalMetadata,
1473
1481
  ): SerializedIntervalDelta | undefined {
1482
+ const original = localOpMetadata.original;
1474
1483
  if (!this.client) {
1475
1484
  // If there's no associated mergeTree client, the originally submitted op is still correct.
1476
- return serializedInterval;
1485
+ return original;
1477
1486
  }
1478
1487
  if (!this.attached) {
1479
1488
  throw new LoggingError("attachSequence must be called");
1480
1489
  }
1481
1490
 
1482
- const { intervalType, properties, stickiness, startSide, endSide } = serializedInterval;
1483
- const { id } = getSerializedProperties(serializedInterval);
1484
- const { start: startRebased, end: endRebased } =
1485
- this.localSeqToRebasedInterval.get(localSeq) ?? this.computeRebasedPositions(localSeq);
1491
+ const { localSeq } = localOpMetadata;
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));
1486
1496
 
1487
1497
  const localInterval = this.localCollection?.idIntervalIndex.getIntervalById(id);
1488
1498
 
@@ -1497,15 +1507,6 @@ export class IntervalCollection
1497
1507
  endSide,
1498
1508
  };
1499
1509
 
1500
- if (
1501
- opName === "change" &&
1502
- // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -- ?? is not logically equivalent when .hasPendingChangeStart returns false.
1503
- (this.hasPendingChangeStart(id) || this.hasPendingChangeEnd(id))
1504
- ) {
1505
- this.removePendingChange(serializedInterval);
1506
- this.addPendingChange(id, rebased);
1507
- }
1508
-
1509
1510
  // if the interval slid off the string, rebase the op to be a noop and delete the interval.
1510
1511
  if (
1511
1512
  !this.options.mergeTreeReferencesCanSlideToEndpoint &&
@@ -1535,28 +1536,27 @@ export class IntervalCollection
1535
1536
  private getSlideToSegment(
1536
1537
  lref: LocalReferencePosition,
1537
1538
  slidingPreference: SlidingPreference,
1538
- ): { segment: ISegment | undefined; offset: number | undefined } | undefined {
1539
+ ): { segment: ISegment; offset: number } | undefined {
1539
1540
  if (!this.client) {
1540
1541
  throw new LoggingError("client does not exist");
1541
1542
  }
1542
- const segoff: { segment: ISegmentInternal | undefined; offset: number | undefined } = {
1543
- segment: lref.getSegment(),
1543
+ const segment: ISegmentInternal | undefined = lref.getSegment();
1544
+ if (segment === undefined) {
1545
+ return undefined;
1546
+ }
1547
+ const segoff = {
1548
+ segment,
1544
1549
  offset: lref.getOffset(),
1545
1550
  };
1546
- if (segoff.segment?.localRefs?.has(lref) !== true) {
1551
+ if (segoff.segment.localRefs?.has(lref) !== true) {
1547
1552
  return undefined;
1548
1553
  }
1549
- const newSegoff = getSlideToSegoff(
1554
+ return getSlideToSegoff(
1550
1555
  segoff,
1551
1556
  slidingPreference,
1552
1557
  undefined,
1553
1558
  this.options.mergeTreeReferencesCanSlideToEndpoint,
1554
1559
  );
1555
- const value: { segment: ISegment | undefined; offset: number | undefined } | undefined =
1556
- segoff.segment === newSegoff.segment && segoff.offset === newSegoff.offset
1557
- ? undefined
1558
- : newSegoff;
1559
- return value;
1560
1560
  }
1561
1561
 
1562
1562
  private ackInterval(interval: SequenceIntervalClass, op: ISequencedDocumentMessage): void {
@@ -1577,19 +1577,16 @@ export class IntervalCollection
1577
1577
  );
1578
1578
 
1579
1579
  const id = interval.getIntervalId();
1580
- const hasPendingStartChange = this.hasPendingChangeStart(id);
1581
- const hasPendingEndChange = this.hasPendingChangeEnd(id);
1580
+ const hasPendingChange = this.hasPendingEndpointChanges(id);
1582
1581
 
1583
- if (!hasPendingStartChange) {
1582
+ if (!hasPendingChange) {
1584
1583
  setSlideOnRemove(interval.start);
1585
- }
1586
-
1587
- if (!hasPendingEndChange) {
1588
1584
  setSlideOnRemove(interval.end);
1589
1585
  }
1590
1586
 
1591
- const needsStartUpdate = newStart !== undefined && !hasPendingStartChange;
1592
- const needsEndUpdate = newEnd !== undefined && !hasPendingEndChange;
1587
+ const needsStartUpdate =
1588
+ newStart?.segment !== interval.start.getSegment() && !hasPendingChange;
1589
+ const needsEndUpdate = newEnd?.segment !== interval.end.getSegment() && !hasPendingChange;
1593
1590
 
1594
1591
  if (needsStartUpdate || needsEndUpdate) {
1595
1592
  if (!this.localCollection) {
@@ -1658,7 +1655,7 @@ export class IntervalCollection
1658
1655
  serializedInterval: ISerializedInterval,
1659
1656
  local: boolean,
1660
1657
  op: ISequencedDocumentMessage,
1661
- localOpMetadata: IMapMessageLocalMetadata | undefined,
1658
+ localOpMetadata: IntervalAddLocalMetadata | undefined,
1662
1659
  ) {
1663
1660
  const { id, properties } = getSerializedProperties(serializedInterval);
1664
1661
 
@@ -1667,7 +1664,6 @@ export class IntervalCollection
1667
1664
  localOpMetadata !== undefined,
1668
1665
  0x553 /* op metadata should be defined for local op */,
1669
1666
  );
1670
- this.localSeqToSerializedInterval.delete(localOpMetadata.localSeq);
1671
1667
  const localInterval = this.getIntervalById(id);
1672
1668
  if (localInterval) {
1673
1669
  this.ackInterval(localInterval, op);