@fluidframework/merge-tree 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 (154) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/dist/client.d.ts +9 -6
  3. package/dist/client.d.ts.map +1 -1
  4. package/dist/client.js +79 -36
  5. package/dist/client.js.map +1 -1
  6. package/dist/collections/list.d.ts +95 -5
  7. package/dist/collections/list.d.ts.map +1 -1
  8. package/dist/collections/list.js +67 -5
  9. package/dist/collections/list.js.map +1 -1
  10. package/dist/constants.d.ts +5 -0
  11. package/dist/constants.d.ts.map +1 -1
  12. package/dist/constants.js +6 -1
  13. package/dist/constants.js.map +1 -1
  14. package/dist/index.d.ts +1 -1
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js +3 -2
  17. package/dist/index.js.map +1 -1
  18. package/dist/mergeTree.d.ts +9 -9
  19. package/dist/mergeTree.d.ts.map +1 -1
  20. package/dist/mergeTree.js +15 -7
  21. package/dist/mergeTree.js.map +1 -1
  22. package/dist/perspective.d.ts +15 -0
  23. package/dist/perspective.d.ts.map +1 -1
  24. package/dist/perspective.js +25 -1
  25. package/dist/perspective.js.map +1 -1
  26. package/dist/revertibles.js +1 -1
  27. package/dist/revertibles.js.map +1 -1
  28. package/dist/stamps.d.ts +1 -0
  29. package/dist/stamps.d.ts.map +1 -1
  30. package/dist/stamps.js +5 -1
  31. package/dist/stamps.js.map +1 -1
  32. package/dist/test/beastTest.spec.js +1 -1
  33. package/dist/test/beastTest.spec.js.map +1 -1
  34. package/dist/test/client.annotateMarker.spec.js +1 -1
  35. package/dist/test/client.annotateMarker.spec.js.map +1 -1
  36. package/dist/test/client.applyMsg.spec.js +25 -25
  37. package/dist/test/client.applyMsg.spec.js.map +1 -1
  38. package/dist/test/client.applyStashedOpFarm.spec.d.ts.map +1 -1
  39. package/dist/test/client.applyStashedOpFarm.spec.js +3 -3
  40. package/dist/test/client.applyStashedOpFarm.spec.js.map +1 -1
  41. package/dist/test/client.attributionFarm.spec.js +1 -1
  42. package/dist/test/client.attributionFarm.spec.js.map +1 -1
  43. package/dist/test/client.getPosition.spec.js +1 -0
  44. package/dist/test/client.getPosition.spec.js.map +1 -1
  45. package/dist/test/client.localReference.spec.js +62 -48
  46. package/dist/test/client.localReference.spec.js.map +1 -1
  47. package/dist/test/client.localReferenceFarm.spec.js +1 -0
  48. package/dist/test/client.localReferenceFarm.spec.js.map +1 -1
  49. package/dist/test/client.reconnectFarm.spec.js +1 -1
  50. package/dist/test/client.reconnectFarm.spec.js.map +1 -1
  51. package/dist/test/client.rollback.spec.js +8 -6
  52. package/dist/test/client.rollback.spec.js.map +1 -1
  53. package/dist/test/client.searchForMarker.spec.js +2 -2
  54. package/dist/test/client.searchForMarker.spec.js.map +1 -1
  55. package/dist/test/clientTestHelper.js +1 -1
  56. package/dist/test/clientTestHelper.js.map +1 -1
  57. package/dist/test/mergeTree.annotate.spec.js +25 -25
  58. package/dist/test/mergeTree.annotate.spec.js.map +1 -1
  59. package/dist/test/mergeTree.markRangeRemoved.spec.js +1 -1
  60. package/dist/test/mergeTree.markRangeRemoved.spec.js.map +1 -1
  61. package/dist/test/obliterate.spec.js +4 -4
  62. package/dist/test/obliterate.spec.js.map +1 -1
  63. package/dist/test/resetPendingSegmentsToOp.spec.js +14 -14
  64. package/dist/test/resetPendingSegmentsToOp.spec.js.map +1 -1
  65. package/dist/test/snapshot.utils.js +1 -1
  66. package/dist/test/snapshot.utils.js.map +1 -1
  67. package/dist/test/sortedSegmentSet.spec.js +3 -3
  68. package/dist/test/sortedSegmentSet.spec.js.map +1 -1
  69. package/dist/test/testClient.js +3 -3
  70. package/dist/test/testClient.js.map +1 -1
  71. package/dist/test/tracking.spec.js +7 -7
  72. package/dist/test/tracking.spec.js.map +1 -1
  73. package/dist/test/wordUnitTests.spec.js.map +1 -1
  74. package/lib/client.d.ts +9 -6
  75. package/lib/client.d.ts.map +1 -1
  76. package/lib/client.js +81 -38
  77. package/lib/client.js.map +1 -1
  78. package/lib/collections/list.d.ts +95 -5
  79. package/lib/collections/list.d.ts.map +1 -1
  80. package/lib/collections/list.js +67 -5
  81. package/lib/collections/list.js.map +1 -1
  82. package/lib/constants.d.ts +5 -0
  83. package/lib/constants.d.ts.map +1 -1
  84. package/lib/constants.js +5 -0
  85. package/lib/constants.js.map +1 -1
  86. package/lib/index.d.ts +1 -1
  87. package/lib/index.d.ts.map +1 -1
  88. package/lib/index.js +1 -1
  89. package/lib/index.js.map +1 -1
  90. package/lib/mergeTree.d.ts +9 -9
  91. package/lib/mergeTree.d.ts.map +1 -1
  92. package/lib/mergeTree.js +15 -7
  93. package/lib/mergeTree.js.map +1 -1
  94. package/lib/perspective.d.ts +15 -0
  95. package/lib/perspective.d.ts.map +1 -1
  96. package/lib/perspective.js +23 -0
  97. package/lib/perspective.js.map +1 -1
  98. package/lib/revertibles.js +1 -1
  99. package/lib/revertibles.js.map +1 -1
  100. package/lib/stamps.d.ts +1 -0
  101. package/lib/stamps.d.ts.map +1 -1
  102. package/lib/stamps.js +4 -1
  103. package/lib/stamps.js.map +1 -1
  104. package/lib/test/beastTest.spec.js +1 -1
  105. package/lib/test/beastTest.spec.js.map +1 -1
  106. package/lib/test/client.annotateMarker.spec.js +1 -1
  107. package/lib/test/client.annotateMarker.spec.js.map +1 -1
  108. package/lib/test/client.applyMsg.spec.js +25 -25
  109. package/lib/test/client.applyMsg.spec.js.map +1 -1
  110. package/lib/test/client.applyStashedOpFarm.spec.d.ts.map +1 -1
  111. package/lib/test/client.applyStashedOpFarm.spec.js +3 -3
  112. package/lib/test/client.applyStashedOpFarm.spec.js.map +1 -1
  113. package/lib/test/client.attributionFarm.spec.js +1 -1
  114. package/lib/test/client.attributionFarm.spec.js.map +1 -1
  115. package/lib/test/client.getPosition.spec.js +1 -0
  116. package/lib/test/client.getPosition.spec.js.map +1 -1
  117. package/lib/test/client.localReference.spec.js +62 -48
  118. package/lib/test/client.localReference.spec.js.map +1 -1
  119. package/lib/test/client.localReferenceFarm.spec.js +1 -0
  120. package/lib/test/client.localReferenceFarm.spec.js.map +1 -1
  121. package/lib/test/client.reconnectFarm.spec.js +1 -1
  122. package/lib/test/client.reconnectFarm.spec.js.map +1 -1
  123. package/lib/test/client.rollback.spec.js +8 -6
  124. package/lib/test/client.rollback.spec.js.map +1 -1
  125. package/lib/test/client.searchForMarker.spec.js +2 -2
  126. package/lib/test/client.searchForMarker.spec.js.map +1 -1
  127. package/lib/test/clientTestHelper.js +1 -1
  128. package/lib/test/clientTestHelper.js.map +1 -1
  129. package/lib/test/mergeTree.annotate.spec.js +25 -25
  130. package/lib/test/mergeTree.annotate.spec.js.map +1 -1
  131. package/lib/test/mergeTree.markRangeRemoved.spec.js +1 -1
  132. package/lib/test/mergeTree.markRangeRemoved.spec.js.map +1 -1
  133. package/lib/test/obliterate.spec.js +4 -4
  134. package/lib/test/obliterate.spec.js.map +1 -1
  135. package/lib/test/resetPendingSegmentsToOp.spec.js +14 -14
  136. package/lib/test/resetPendingSegmentsToOp.spec.js.map +1 -1
  137. package/lib/test/snapshot.utils.js +1 -1
  138. package/lib/test/snapshot.utils.js.map +1 -1
  139. package/lib/test/sortedSegmentSet.spec.js +3 -3
  140. package/lib/test/sortedSegmentSet.spec.js.map +1 -1
  141. package/lib/test/testClient.js +3 -3
  142. package/lib/test/testClient.js.map +1 -1
  143. package/lib/test/tracking.spec.js +7 -7
  144. package/lib/test/tracking.spec.js.map +1 -1
  145. package/lib/test/wordUnitTests.spec.js.map +1 -1
  146. package/package.json +16 -16
  147. package/src/client.ts +117 -50
  148. package/src/collections/list.ts +101 -5
  149. package/src/constants.ts +6 -0
  150. package/src/index.ts +3 -0
  151. package/src/mergeTree.ts +30 -24
  152. package/src/perspective.ts +26 -0
  153. package/src/revertibles.ts +1 -1
  154. package/src/stamps.ts +9 -1
package/src/client.ts CHANGED
@@ -27,7 +27,7 @@ import {
27
27
 
28
28
  import { MergeTreeTextHelper, type IMergeTreeTextHelper } from "./MergeTreeTextHelper.js";
29
29
  import { DoublyLinkedList, RedBlackTree } from "./collections/index.js";
30
- import { NonCollabClient, UniversalSequenceNumber } from "./constants.js";
30
+ import { NonCollabClient, SquashClient, UniversalSequenceNumber } from "./constants.js";
31
31
  import { LocalReferencePosition, SlidingPreference } from "./localReference.js";
32
32
  import {
33
33
  MergeTree,
@@ -84,6 +84,7 @@ import {
84
84
  } from "./ops.js";
85
85
  import {
86
86
  LocalReconnectingPerspective,
87
+ LocalSquashPerspective,
87
88
  PriorPerspective,
88
89
  type Perspective,
89
90
  } from "./perspective.js";
@@ -95,6 +96,7 @@ import {
95
96
  overwriteInfo,
96
97
  toRemovalInfo,
97
98
  type IHasInsertionInfo,
99
+ type IHasRemovalInfo,
98
100
  } from "./segmentInfos.js";
99
101
  import { Side, type InteriorSequencePlace } from "./sequencePlace.js";
100
102
  import { SnapshotLoader } from "./snapshotLoader.js";
@@ -363,6 +365,7 @@ export class Client extends TypedEventEmitter<IClientEvents> {
363
365
  end: number | undefined,
364
366
  accum: TClientData,
365
367
  splitRange?: boolean,
368
+ perspective?: Pick<ISequencedDocumentMessage, "clientId" | "referenceSequenceNumber">,
366
369
  ): void;
367
370
  public walkSegments(
368
371
  handler: ISegmentAction<undefined>,
@@ -370,6 +373,7 @@ export class Client extends TypedEventEmitter<IClientEvents> {
370
373
  end?: number,
371
374
  accum?: undefined,
372
375
  splitRange?: boolean,
376
+ perspective?: Pick<ISequencedDocumentMessage, "clientId" | "referenceSequenceNumber">,
373
377
  ): void;
374
378
  public walkSegments<TClientData>(
375
379
  handler: ISegmentAction<TClientData>,
@@ -377,10 +381,13 @@ export class Client extends TypedEventEmitter<IClientEvents> {
377
381
  end: number | undefined,
378
382
  accum: TClientData,
379
383
  splitRange: boolean = false,
384
+ perspective?: Pick<ISequencedDocumentMessage, "clientId" | "referenceSequenceNumber">,
380
385
  ): void {
381
386
  this._mergeTree.mapRange(
382
387
  handler,
383
- this._mergeTree.localPerspective,
388
+ perspective === undefined
389
+ ? this.getCollabWindow().localPerspective
390
+ : this.getOperationPerspective(perspective),
384
391
  accum,
385
392
  start,
386
393
  end,
@@ -556,7 +563,9 @@ export class Client extends TypedEventEmitter<IClientEvents> {
556
563
  }
557
564
 
558
565
  private getOperationPerspective(
559
- sequencedMessage: ISequencedDocumentMessage | undefined,
566
+ sequencedMessage:
567
+ | Pick<ISequencedDocumentMessage, "clientId" | "referenceSequenceNumber">
568
+ | undefined,
560
569
  ): Perspective {
561
570
  if (!sequencedMessage) {
562
571
  return this._mergeTree.localPerspective;
@@ -869,17 +878,20 @@ export class Client extends TypedEventEmitter<IClientEvents> {
869
878
  const useNewSlidingBehavior = true;
870
879
  // Destructuring segment + offset is convenient and segment is reassigned
871
880
  // eslint-disable-next-line prefer-const
872
- let { segment: newSegment, offset: newOffset } = getSlideToSegoff(
881
+ const segOff = getSlideToSegoff(
873
882
  { segment: oldSegment, offset: oldOffset },
874
883
  slidePreference,
875
884
  reconnectingPerspective,
876
885
  useNewSlidingBehavior,
877
886
  );
878
887
 
879
- newSegment ??=
880
- slidePreference === SlidingPreference.FORWARD
881
- ? this._mergeTree.endOfTree
882
- : this._mergeTree.startOfTree;
888
+ const { segment: newSegment, offset: newOffset } = segOff ?? {
889
+ segment:
890
+ slidePreference === SlidingPreference.FORWARD
891
+ ? this._mergeTree.endOfTree
892
+ : this._mergeTree.startOfTree,
893
+ offset: 0,
894
+ };
883
895
 
884
896
  assert(
885
897
  isSegmentLeaf(newSegment) && newOffset !== undefined,
@@ -898,16 +910,17 @@ export class Client extends TypedEventEmitter<IClientEvents> {
898
910
  return { segment: newSegment, offset: newOffset, side: newSide };
899
911
  }
900
912
 
901
- private computeNewObliterateEndpoints(obliterateInfo: ObliterateInfo): {
913
+ private computeNewObliterateEndpoints(
914
+ obliterateInfo: ObliterateInfo,
915
+ squash: boolean,
916
+ ): {
902
917
  start: RebasedObliterateEndpoint;
903
918
  end: RebasedObliterateEndpoint;
904
919
  } {
905
920
  const { currentSeq, clientId } = this.getCollabWindow();
906
- const reconnectingPerspective = new LocalReconnectingPerspective(
907
- currentSeq,
908
- clientId,
909
- obliterateInfo.stamp.localSeq! - 1,
910
- );
921
+ const reconnectingPerspective = new (
922
+ squash ? LocalSquashPerspective : LocalReconnectingPerspective
923
+ )(currentSeq, clientId, obliterateInfo.stamp.localSeq! - 1);
911
924
 
912
925
  const newStart = this.rebaseSidedLocalReference(
913
926
  obliterateInfo.start,
@@ -932,6 +945,7 @@ export class Client extends TypedEventEmitter<IClientEvents> {
932
945
  resetOp: IMergeTreeDeltaOp,
933
946
 
934
947
  segmentGroup: SegmentGroup,
948
+ squash: boolean,
935
949
  ): IMergeTreeDeltaOp[] {
936
950
  assert(!!segmentGroup, 0x033 /* "Segment group undefined" */);
937
951
  const NACKedSegmentGroup = this.pendingRebase?.shift()?.data;
@@ -991,13 +1005,20 @@ export class Client extends TypedEventEmitter<IClientEvents> {
991
1005
  );
992
1006
  const lastRemove = segment.removes[segment.removes.length - 1];
993
1007
  assert(
994
- lastRemove.type === "sliceRemove" && lastRemove.localSeq === segmentGroup.localSeq,
995
- 0xb67 /* Last remove should be the obliterate that is being resubmitted. */,
1008
+ (lastRemove.type === "sliceRemove" &&
1009
+ lastRemove.localSeq === segmentGroup.localSeq) ||
1010
+ opstampUtils.isSquashedOp(lastRemove),
1011
+ 0xbad /* Last remove should be the obliterate that is being resubmitted. */,
996
1012
  );
997
- // The original obliterate affected this segment, but it has since been removed and overlapping removes
998
- // are only possible when they are concurrent. We adjust the metadata on that segment now to reflect
999
- // the fact that the obliterate no longer affects it.
1000
- segment.removes.pop();
1013
+
1014
+ // The original obliterate affected this segment, but it has since been removed.
1015
+ // This can happen when a concurrent obliterate also removed the segment, as well as when the segment was
1016
+ // only locally inserted and its insertion was squashed upon reconnecting.
1017
+ // In the concurrent removal case (where we didn't avoid sending the segment's insertion in the first place due
1018
+ // to squashing), we adjust the metadata on that segment to reflect the fact that this obliterate no longer removes it.
1019
+ if (!opstampUtils.isSquashedOp(lastRemove)) {
1020
+ segment.removes.pop();
1021
+ }
1001
1022
  }
1002
1023
 
1003
1024
  this._mergeTree.rebaseObliterateTo(obliterateInfo, undefined);
@@ -1047,10 +1068,11 @@ export class Client extends TypedEventEmitter<IClientEvents> {
1047
1068
  0x035 /* "Segment group not in segment pending queue" */,
1048
1069
  );
1049
1070
  if (
1050
- (segment.ordinal > newStartSegment.ordinal &&
1071
+ !isRemovedAndAcked(segment) &&
1072
+ ((segment.ordinal > newStartSegment.ordinal &&
1051
1073
  segment.ordinal < newEndSegment.ordinal) ||
1052
- (segment === newStartSegment && newStartSide === Side.Before) ||
1053
- (segment === newEndSegment && newEndSide === Side.After)
1074
+ (segment === newStartSegment && newStartSide === Side.Before) ||
1075
+ (segment === newEndSegment && newEndSide === Side.After))
1054
1076
  ) {
1055
1077
  segment.segmentGroups.enqueue(newObliterate.segmentGroup);
1056
1078
  } else {
@@ -1060,12 +1082,17 @@ export class Client extends TypedEventEmitter<IClientEvents> {
1060
1082
  );
1061
1083
  const lastRemove = segment.removes[segment.removes.length - 1];
1062
1084
  assert(
1063
- lastRemove.type === "sliceRemove" && lastRemove.localSeq === segmentGroup.localSeq,
1064
- 0xb6a /* Last remove should be the obliterate that is being resubmitted. */,
1085
+ (lastRemove.type === "sliceRemove" &&
1086
+ lastRemove.localSeq === segmentGroup.localSeq) ||
1087
+ opstampUtils.isSquashedOp(lastRemove),
1088
+ 0xbae /* Last remove should be the obliterate that is being resubmitted. */,
1065
1089
  );
1066
- // The original obliterate affected this segment, but it has since been removed and it's impossible to apply the
1067
- // local obliterate so that is so. We adjust the metadata on that segment now.
1068
- segment.removes.pop();
1090
+
1091
+ if (!opstampUtils.isSquashedOp(lastRemove)) {
1092
+ // The original obliterate affected this segment, but it has since been removed and it's impossible to apply the
1093
+ // local obliterate so that is so. We adjust the metadata on that segment now.
1094
+ segment.removes.pop();
1095
+ }
1069
1096
  }
1070
1097
  }
1071
1098
 
@@ -1154,13 +1181,25 @@ export class Client extends TypedEventEmitter<IClientEvents> {
1154
1181
  }
1155
1182
 
1156
1183
  case MergeTreeDeltaType.INSERT: {
1184
+ if (isInserted(segment) && opstampUtils.isSquashedOp(segment.insert)) {
1185
+ break;
1186
+ }
1157
1187
  assert(
1158
1188
  isInserted(segment) && opstampUtils.isLocal(segment.insert),
1159
1189
  0x037 /* "Segment already has assigned sequence number" */,
1160
1190
  );
1161
1191
  const removeInfo = toRemovalInfo(segment);
1162
1192
 
1163
- if (removeInfo !== undefined && opstampUtils.isAcked(removeInfo.removes[0])) {
1193
+ const unusedStamp: OperationStamp = { seq: 0, clientId: 0 };
1194
+ if (removeInfo !== undefined && squash) {
1195
+ assert(
1196
+ removeInfo.removes.length === 1 ||
1197
+ opstampUtils.isAcked(removeInfo.removes[removeInfo.removes.length - 2]),
1198
+ 0xbaf /* Expected only one local remove */,
1199
+ );
1200
+ this.squashInsertion(segment);
1201
+ break;
1202
+ } else if (removeInfo !== undefined && opstampUtils.isAcked(removeInfo.removes[0])) {
1164
1203
  assert(
1165
1204
  removeInfo.removes[0].type === "sliceRemove",
1166
1205
  0xb5c /* Remove on insertion must be caused by obliterate. */,
@@ -1181,6 +1220,7 @@ export class Client extends TypedEventEmitter<IClientEvents> {
1181
1220
  clientId: NonCollabClient,
1182
1221
  },
1183
1222
  });
1223
+ this._mergeTree.blockUpdatePathLengths(segment.parent, unusedStamp, true);
1184
1224
  break;
1185
1225
  }
1186
1226
 
@@ -1360,13 +1400,40 @@ export class Client extends TypedEventEmitter<IClientEvents> {
1360
1400
  { start: RebasedObliterateEndpoint; end: RebasedObliterateEndpoint }
1361
1401
  > = new Map();
1362
1402
 
1403
+ private squashInsertion(segment: ISegmentLeaf): void {
1404
+ overwriteInfo<IHasInsertionInfo & IHasRemovalInfo>(segment, {
1405
+ insert: {
1406
+ type: "insert",
1407
+ seq: UniversalSequenceNumber,
1408
+ localSeq: undefined,
1409
+ clientId: SquashClient,
1410
+ },
1411
+ removes: [
1412
+ {
1413
+ type: "setRemove",
1414
+ seq: UniversalSequenceNumber,
1415
+ localSeq: undefined,
1416
+ clientId: SquashClient,
1417
+ },
1418
+ ],
1419
+ });
1420
+
1421
+ this._mergeTree.blockUpdatePathLengths(segment.parent, { seq: 0, clientId: 0 }, true);
1422
+ }
1423
+
1363
1424
  /**
1364
1425
  * Given a pending operation and segment group, regenerate the op, so it
1365
1426
  * can be resubmitted
1366
1427
  * @param resetOp - The op to reset
1367
1428
  * @param segmentGroup - The segment group associated with the op
1429
+ * @param squash - whether intermediate states should be squashed. See `IDeltaHandler.reSubmit`'s squash parameter
1430
+ * documentation for more details.
1368
1431
  */
1369
- public regeneratePendingOp(resetOp: IMergeTreeOp, localOpMetadata: unknown): IMergeTreeOp {
1432
+ public regeneratePendingOp(
1433
+ resetOp: IMergeTreeOp,
1434
+ localOpMetadata: unknown,
1435
+ squash: boolean,
1436
+ ): IMergeTreeOp {
1370
1437
  const segmentGroup = localOpMetadata as SegmentGroup | SegmentGroup[];
1371
1438
  if (this.pendingRebase === undefined || this.pendingRebase.empty) {
1372
1439
  let firstGroup: SegmentGroup;
@@ -1396,13 +1463,14 @@ export class Client extends TypedEventEmitter<IClientEvents> {
1396
1463
  collabWindow.currentSeq !== this.lastNormalization.refSeq ||
1397
1464
  collabWindow.localSeq !== this.lastNormalization.localRefSeq
1398
1465
  ) {
1466
+ const allPendingSegments = [...this._mergeTree.pendingSegments, ...this.pendingRebase];
1399
1467
  // Compute obliterate endpoint destinations before segments are normalized.
1400
1468
  // Segment normalization can affect what should be the semantically correct segments for the endpoints to be placed on.
1401
1469
  this.cachedObliterateRebases.clear();
1402
- for (const group of [...this._mergeTree.pendingSegments, ...this.pendingRebase]) {
1470
+ for (const group of allPendingSegments) {
1403
1471
  const { obliterateInfo } = group.data;
1404
1472
  if (obliterateInfo !== undefined) {
1405
- const { start, end } = this.computeNewObliterateEndpoints(obliterateInfo);
1473
+ const { start, end } = this.computeNewObliterateEndpoints(obliterateInfo, squash);
1406
1474
  const { localSeq } = obliterateInfo.stamp;
1407
1475
  assert(localSeq !== undefined, 0xb6d /* Local seq must be defined */);
1408
1476
  this.cachedObliterateRebases.set(localSeq, { start, end });
@@ -1426,7 +1494,7 @@ export class Client extends TypedEventEmitter<IClientEvents> {
1426
1494
  );
1427
1495
 
1428
1496
  for (let i = 0; i < resetOp.ops.length; i++) {
1429
- opList.push(...this.resetPendingDeltaToOps(resetOp.ops[i], segmentGroup[i]));
1497
+ opList.push(...this.resetPendingDeltaToOps(resetOp.ops[i], segmentGroup[i], squash));
1430
1498
  }
1431
1499
  } else {
1432
1500
  // A group op containing a single op will pass a direct reference to 'segmentGroup'
@@ -1435,7 +1503,7 @@ export class Client extends TypedEventEmitter<IClientEvents> {
1435
1503
  resetOp.ops.length === 1,
1436
1504
  0x03b /* "Number of ops in 'resetOp' must match the number of segment groups provided." */,
1437
1505
  );
1438
- opList.push(...this.resetPendingDeltaToOps(resetOp.ops[0], segmentGroup));
1506
+ opList.push(...this.resetPendingDeltaToOps(resetOp.ops[0], segmentGroup, squash));
1439
1507
  }
1440
1508
  } else {
1441
1509
  assert(
@@ -1446,7 +1514,7 @@ export class Client extends TypedEventEmitter<IClientEvents> {
1446
1514
  !Array.isArray(segmentGroup),
1447
1515
  0x03d /* "segmentGroup is array rather than singleton!" */,
1448
1516
  );
1449
- opList.push(...this.resetPendingDeltaToOps(resetOp, segmentGroup));
1517
+ opList.push(...this.resetPendingDeltaToOps(resetOp, segmentGroup, squash));
1450
1518
  }
1451
1519
 
1452
1520
  return opList.length === 1 ? opList[0] : createGroupOp(...opList);
@@ -1547,10 +1615,12 @@ export class Client extends TypedEventEmitter<IClientEvents> {
1547
1615
  pos: number,
1548
1616
  sequenceArgs?: Pick<ISequencedDocumentMessage, "referenceSequenceNumber" | "clientId">,
1549
1617
  localSeq?: number,
1550
- ): {
1551
- segment: T | undefined;
1552
- offset: number | undefined;
1553
- } {
1618
+ ):
1619
+ | {
1620
+ segment: T;
1621
+ offset: number;
1622
+ }
1623
+ | undefined {
1554
1624
  let perspective: Perspective;
1555
1625
  const clientId =
1556
1626
  sequenceArgs === undefined
@@ -1565,20 +1635,17 @@ export class Client extends TypedEventEmitter<IClientEvents> {
1565
1635
  perspective = new PriorPerspective(refSeq, clientId);
1566
1636
  }
1567
1637
 
1568
- return this._mergeTree.getContainingSegment(pos, perspective) as {
1569
- segment: T | undefined;
1570
- offset: number | undefined;
1571
- };
1638
+ return this._mergeTree.getContainingSegment(pos, perspective) as
1639
+ | {
1640
+ segment: T;
1641
+ offset: number;
1642
+ }
1643
+ | undefined;
1572
1644
  }
1573
1645
 
1574
1646
  getPropertiesAtPosition(pos: number): PropertySet | undefined {
1575
- let propertiesAtPosition: PropertySet | undefined;
1576
1647
  const segoff = this.getContainingSegment(pos);
1577
- const seg = segoff.segment;
1578
- if (seg) {
1579
- propertiesAtPosition = seg.properties;
1580
- }
1581
- return propertiesAtPosition;
1648
+ return segoff?.segment?.properties;
1582
1649
  }
1583
1650
 
1584
1651
  getRangeExtentsOfPosition(pos: number): {
@@ -1589,7 +1656,7 @@ export class Client extends TypedEventEmitter<IClientEvents> {
1589
1656
  let posAfterEnd: number | undefined;
1590
1657
 
1591
1658
  const segoff = this.getContainingSegment(pos);
1592
- const seg = segoff.segment;
1659
+ const seg = segoff?.segment;
1593
1660
  if (seg) {
1594
1661
  posStart = this.getPosition(seg);
1595
1662
  posAfterEnd = posStart + seg.cachedLength;
@@ -5,15 +5,46 @@
5
5
 
6
6
  import { UsageError } from "@fluidframework/telemetry-utils/internal";
7
7
 
8
+ /**
9
+ * Represents a node in a doubly linked list.
10
+ * @internal
11
+ */
8
12
  export interface ListNode<T> {
13
+ /**
14
+ * The list this node belongs to, or undefined if not attached.
15
+ */
9
16
  readonly list: DoublyLinkedList<T> | undefined;
17
+ /**
18
+ * The data value stored in this node.
19
+ */
10
20
  readonly data: T;
21
+ /**
22
+ * The next node in the list, or undefined if this is the last node.
23
+ */
11
24
  readonly next: ListNode<T> | undefined;
25
+ /**
26
+ * The previous node in the list, or undefined if this is the first node.
27
+ */
12
28
  readonly prev: ListNode<T> | undefined;
29
+ /**
30
+ * Removes this node from its list.
31
+ * @returns The removed node, or undefined if not in a list.
32
+ */
33
+ remove(): ListNode<T> | undefined;
13
34
  }
14
35
 
36
+ /**
37
+ * Represents a range of nodes in a doubly linked list.
38
+ * @internal
39
+ */
15
40
  export interface ListNodeRange<T> {
41
+ /**
42
+ * The first node in the range.
43
+ */
16
44
  first: ListNode<T>;
45
+ /**
46
+ * The last node in the range.
47
+ */
17
48
  last: ListNode<T>;
18
49
  }
19
50
 
@@ -50,6 +81,9 @@ class DataNode<T> extends HeadNode<T> implements ListNode<T> {
50
81
  super(undefined);
51
82
  this.headNode = headNode;
52
83
  }
84
+ remove(): ListNode<T> | undefined {
85
+ return this.list?.remove(this);
86
+ }
53
87
  }
54
88
 
55
89
  function insertAfter<T>(node: DataNode<T> | HeadNode<T>, items: T[]): ListNodeRange<T> {
@@ -80,6 +114,11 @@ function insertAfter<T>(node: DataNode<T> | HeadNode<T>, items: T[]): ListNodeRa
80
114
  return newRange;
81
115
  }
82
116
 
117
+ /**
118
+ * A doubly linked list implementation with array-like methods and node access.
119
+ * @typeParam T - The type of data stored in the list nodes.
120
+ * @internal
121
+ */
83
122
  export class DoublyLinkedList<T>
84
123
  implements
85
124
  Iterable<ListNode<T>>,
@@ -87,12 +126,21 @@ export class DoublyLinkedList<T>
87
126
  // try to match array signature and semantics where possible
88
127
  Pick<ListNode<T>[], "pop" | "shift" | "length" | "includes">
89
128
  {
129
+ /**
130
+ * Creates a new doubly linked list optionally initialized with values.
131
+ * @param values - Optional iterable of values to populate the list.
132
+ */
90
133
  constructor(values?: Iterable<T>) {
91
134
  if (values !== undefined) {
92
135
  this.push(...values);
93
136
  }
94
137
  }
95
138
 
139
+ /**
140
+ * Finds the first node matching the predicate.
141
+ * @param predicate - Function to test each node.
142
+ * @returns The first matching node, or undefined if none found.
143
+ */
96
144
  find(
97
145
  predicate: (value: ListNode<T>, obj: DoublyLinkedList<T>) => unknown,
98
146
  ): ListNode<T> | undefined {
@@ -106,6 +154,10 @@ export class DoublyLinkedList<T>
106
154
  return found;
107
155
  }
108
156
 
157
+ /**
158
+ * Returns an iterable that maps each node to a new value.
159
+ * @param callbackfn - Function to produce a new value for each node.
160
+ */
109
161
  map<U>(callbackfn: (value: ListNode<T>) => U): Iterable<U> {
110
162
  let node = this.first;
111
163
  const iterator: IterableIterator<U> = {
@@ -124,6 +176,12 @@ export class DoublyLinkedList<T>
124
176
  return iterator;
125
177
  }
126
178
 
179
+ /**
180
+ * Inserts items after the specified node.
181
+ * @param preceding - The node to insert after.
182
+ * @param items - Items to insert.
183
+ * @returns The range of newly inserted nodes.
184
+ */
127
185
  insertAfter(preceding: ListNode<T>, ...items: T[]): ListNodeRange<T> {
128
186
  if (!this._includes(preceding)) {
129
187
  throw new Error("preceding not in list");
@@ -132,10 +190,19 @@ export class DoublyLinkedList<T>
132
190
  return insertAfter(preceding, items);
133
191
  }
134
192
 
193
+ /**
194
+ * Removes and returns the last node in the list.
195
+ * @returns The removed node, or undefined if the list is empty.
196
+ */
135
197
  pop(): ListNode<T> | undefined {
136
198
  return this.remove(this.last);
137
199
  }
138
200
 
201
+ /**
202
+ * Appends items to the end of the list.
203
+ * @param items - Items to append.
204
+ * @returns The range of newly inserted nodes.
205
+ */
139
206
  push(...items: T[]): ListNodeRange<T> {
140
207
  this._len += items.length;
141
208
  const start = this.headNode._prev;
@@ -143,14 +210,17 @@ export class DoublyLinkedList<T>
143
210
  }
144
211
 
145
212
  /**
146
- * Remove and return the first element
213
+ * Removes and returns the first node in the list.
214
+ * @returns The removed node, or undefined if the list is empty.
147
215
  */
148
216
  shift(): ListNode<T> | undefined {
149
217
  return this.remove(this.first);
150
218
  }
151
219
 
152
220
  /**
153
- * Insert `items` at start of list
221
+ * Inserts items at the start of the list.
222
+ * @param items - Items to insert.
223
+ * @returns The range of newly inserted nodes.
154
224
  */
155
225
  unshift(...items: T[]): ListNodeRange<T> {
156
226
  this._len += items.length;
@@ -158,9 +228,10 @@ export class DoublyLinkedList<T>
158
228
  }
159
229
 
160
230
  /**
161
- * Remove nodes starting at `start` until either the `end` node is reached
162
- * or until `count` nodes have been removed. Returns the removed nodes as
163
- * a separate linked list
231
+ * Removes nodes starting at `start` until `end` or `count` is reached.
232
+ * @param start - The node to start removing from.
233
+ * @param countOrEnd - The number of nodes to remove or the end node.
234
+ * @returns A new list containing the removed nodes.
164
235
  */
165
236
  splice(start: ListNode<T>, countOrEnd?: ListNode<T> | number): DoublyLinkedList<T> {
166
237
  const newList = new DoublyLinkedList<T>();
@@ -187,6 +258,11 @@ export class DoublyLinkedList<T>
187
258
  return newList;
188
259
  }
189
260
 
261
+ /**
262
+ * Checks if the node is in this list.
263
+ * @param node - The node to check.
264
+ * @returns True if the node is in the list.
265
+ */
190
266
  public includes(node: ListNode<T> | undefined): node is ListNode<T> {
191
267
  return this._includes(node);
192
268
  }
@@ -207,6 +283,11 @@ export class DoublyLinkedList<T>
207
283
  return undefined;
208
284
  }
209
285
 
286
+ /**
287
+ * Removes the specified node from the list.
288
+ * @param node - The node to remove.
289
+ * @returns The removed node, or undefined if not in the list.
290
+ */
210
291
  public remove(node: ListNode<T> | undefined): ListNode<T> | undefined {
211
292
  return this._remove(node);
212
293
  }
@@ -231,16 +312,31 @@ export class DoublyLinkedList<T>
231
312
 
232
313
  private _len: number = 0;
233
314
  private readonly headNode: HeadNode<T> | DataNode<T> = new HeadNode(this);
315
+
316
+ /**
317
+ * The number of nodes in the list.
318
+ */
234
319
  public get length(): number {
235
320
  return this._len;
236
321
  }
322
+
323
+ /**
324
+ * Whether the list is empty.
325
+ */
237
326
  public get empty(): boolean {
238
327
  return this._len === 0;
239
328
  }
329
+
330
+ /**
331
+ * The first node in the list, or undefined if empty.
332
+ */
240
333
  public get first(): ListNode<T> | undefined {
241
334
  return this.headNode.next;
242
335
  }
243
336
 
337
+ /**
338
+ * The last node in the list, or undefined if empty.
339
+ */
244
340
  public get last(): ListNode<T> | undefined {
245
341
  return this.headNode.prev;
246
342
  }
package/src/constants.ts CHANGED
@@ -34,3 +34,9 @@ export const LocalClientId = -1;
34
34
  * @internal
35
35
  */
36
36
  export const NonCollabClient = -2;
37
+
38
+ /**
39
+ * Used as the client id for operations that were squashed upon resubmission and should therefore
40
+ * never be seen by other clients.
41
+ */
42
+ export const SquashClient = -3;
package/src/index.ts CHANGED
@@ -30,6 +30,9 @@ export {
30
30
  RBNodeActions,
31
31
  RedBlackTree,
32
32
  SortedDictionary,
33
+ DoublyLinkedList,
34
+ ListNode,
35
+ ListNodeRange,
33
36
  } from "./collections/index.js";
34
37
  export { UnassignedSequenceNumber, UniversalSequenceNumber } from "./constants.js";
35
38
  export {