@fluidframework/merge-tree 2.41.0 → 2.42.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/dist/client.d.ts +6 -3
  3. package/dist/client.d.ts.map +1 -1
  4. package/dist/client.js +71 -25
  5. package/dist/client.js.map +1 -1
  6. package/dist/constants.d.ts +5 -0
  7. package/dist/constants.d.ts.map +1 -1
  8. package/dist/constants.js +6 -1
  9. package/dist/constants.js.map +1 -1
  10. package/dist/mergeTree.js +1 -1
  11. package/dist/mergeTree.js.map +1 -1
  12. package/dist/perspective.d.ts +15 -0
  13. package/dist/perspective.d.ts.map +1 -1
  14. package/dist/perspective.js +25 -1
  15. package/dist/perspective.js.map +1 -1
  16. package/dist/stamps.d.ts +1 -0
  17. package/dist/stamps.d.ts.map +1 -1
  18. package/dist/stamps.js +5 -1
  19. package/dist/stamps.js.map +1 -1
  20. package/dist/test/client.applyMsg.spec.js +4 -4
  21. package/dist/test/client.applyMsg.spec.js.map +1 -1
  22. package/dist/test/client.applyStashedOpFarm.spec.d.ts.map +1 -1
  23. package/dist/test/client.applyStashedOpFarm.spec.js +3 -3
  24. package/dist/test/client.applyStashedOpFarm.spec.js.map +1 -1
  25. package/dist/test/client.reconnectFarm.spec.js +1 -1
  26. package/dist/test/client.reconnectFarm.spec.js.map +1 -1
  27. package/dist/test/client.searchForMarker.spec.js +2 -2
  28. package/dist/test/client.searchForMarker.spec.js.map +1 -1
  29. package/dist/test/clientTestHelper.js +1 -1
  30. package/dist/test/clientTestHelper.js.map +1 -1
  31. package/dist/test/resetPendingSegmentsToOp.spec.js +10 -10
  32. package/dist/test/resetPendingSegmentsToOp.spec.js.map +1 -1
  33. package/lib/client.d.ts +6 -3
  34. package/lib/client.d.ts.map +1 -1
  35. package/lib/client.js +73 -27
  36. package/lib/client.js.map +1 -1
  37. package/lib/constants.d.ts +5 -0
  38. package/lib/constants.d.ts.map +1 -1
  39. package/lib/constants.js +5 -0
  40. package/lib/constants.js.map +1 -1
  41. package/lib/mergeTree.js +1 -1
  42. package/lib/mergeTree.js.map +1 -1
  43. package/lib/perspective.d.ts +15 -0
  44. package/lib/perspective.d.ts.map +1 -1
  45. package/lib/perspective.js +23 -0
  46. package/lib/perspective.js.map +1 -1
  47. package/lib/stamps.d.ts +1 -0
  48. package/lib/stamps.d.ts.map +1 -1
  49. package/lib/stamps.js +4 -1
  50. package/lib/stamps.js.map +1 -1
  51. package/lib/test/client.applyMsg.spec.js +4 -4
  52. package/lib/test/client.applyMsg.spec.js.map +1 -1
  53. package/lib/test/client.applyStashedOpFarm.spec.d.ts.map +1 -1
  54. package/lib/test/client.applyStashedOpFarm.spec.js +3 -3
  55. package/lib/test/client.applyStashedOpFarm.spec.js.map +1 -1
  56. package/lib/test/client.reconnectFarm.spec.js +1 -1
  57. package/lib/test/client.reconnectFarm.spec.js.map +1 -1
  58. package/lib/test/client.searchForMarker.spec.js +2 -2
  59. package/lib/test/client.searchForMarker.spec.js.map +1 -1
  60. package/lib/test/clientTestHelper.js +1 -1
  61. package/lib/test/clientTestHelper.js.map +1 -1
  62. package/lib/test/resetPendingSegmentsToOp.spec.js +10 -10
  63. package/lib/test/resetPendingSegmentsToOp.spec.js.map +1 -1
  64. package/package.json +16 -16
  65. package/src/client.ts +95 -30
  66. package/src/constants.ts +6 -0
  67. package/src/mergeTree.ts +1 -1
  68. package/src/perspective.ts +26 -0
  69. package/src/stamps.ts +9 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fluidframework/merge-tree",
3
- "version": "2.41.0",
3
+ "version": "2.42.0",
4
4
  "description": "Merge tree",
5
5
  "homepage": "https://fluidframework.com",
6
6
  "repository": {
@@ -81,30 +81,30 @@
81
81
  "temp-directory": "nyc/.nyc_output"
82
82
  },
83
83
  "dependencies": {
84
- "@fluid-internal/client-utils": "~2.41.0",
85
- "@fluidframework/container-definitions": "~2.41.0",
86
- "@fluidframework/core-interfaces": "~2.41.0",
87
- "@fluidframework/core-utils": "~2.41.0",
88
- "@fluidframework/datastore-definitions": "~2.41.0",
89
- "@fluidframework/driver-definitions": "~2.41.0",
90
- "@fluidframework/runtime-definitions": "~2.41.0",
91
- "@fluidframework/runtime-utils": "~2.41.0",
92
- "@fluidframework/shared-object-base": "~2.41.0",
93
- "@fluidframework/telemetry-utils": "~2.41.0"
84
+ "@fluid-internal/client-utils": "~2.42.0",
85
+ "@fluidframework/container-definitions": "~2.42.0",
86
+ "@fluidframework/core-interfaces": "~2.42.0",
87
+ "@fluidframework/core-utils": "~2.42.0",
88
+ "@fluidframework/datastore-definitions": "~2.42.0",
89
+ "@fluidframework/driver-definitions": "~2.42.0",
90
+ "@fluidframework/runtime-definitions": "~2.42.0",
91
+ "@fluidframework/runtime-utils": "~2.42.0",
92
+ "@fluidframework/shared-object-base": "~2.42.0",
93
+ "@fluidframework/telemetry-utils": "~2.42.0"
94
94
  },
95
95
  "devDependencies": {
96
96
  "@arethetypeswrong/cli": "^0.17.1",
97
97
  "@biomejs/biome": "~1.9.3",
98
- "@fluid-internal/mocha-test-setup": "~2.41.0",
99
- "@fluid-private/stochastic-test-utils": "~2.41.0",
100
- "@fluid-private/test-pairwise-generator": "~2.41.0",
98
+ "@fluid-internal/mocha-test-setup": "~2.42.0",
99
+ "@fluid-private/stochastic-test-utils": "~2.42.0",
100
+ "@fluid-private/test-pairwise-generator": "~2.42.0",
101
101
  "@fluid-tools/benchmark": "^0.51.0",
102
102
  "@fluid-tools/build-cli": "^0.55.0",
103
103
  "@fluidframework/build-common": "^2.0.3",
104
104
  "@fluidframework/build-tools": "^0.55.0",
105
105
  "@fluidframework/eslint-config-fluid": "^5.7.4",
106
- "@fluidframework/merge-tree-previous": "npm:@fluidframework/merge-tree@2.40.0",
107
- "@fluidframework/test-runtime-utils": "~2.41.0",
106
+ "@fluidframework/merge-tree-previous": "npm:@fluidframework/merge-tree@2.41.0",
107
+ "@fluidframework/test-runtime-utils": "~2.42.0",
108
108
  "@microsoft/api-extractor": "7.52.8",
109
109
  "@types/diff": "^3.5.1",
110
110
  "@types/mocha": "^10.0.10",
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;
@@ -898,16 +907,17 @@ export class Client extends TypedEventEmitter<IClientEvents> {
898
907
  return { segment: newSegment, offset: newOffset, side: newSide };
899
908
  }
900
909
 
901
- private computeNewObliterateEndpoints(obliterateInfo: ObliterateInfo): {
910
+ private computeNewObliterateEndpoints(
911
+ obliterateInfo: ObliterateInfo,
912
+ squash: boolean,
913
+ ): {
902
914
  start: RebasedObliterateEndpoint;
903
915
  end: RebasedObliterateEndpoint;
904
916
  } {
905
917
  const { currentSeq, clientId } = this.getCollabWindow();
906
- const reconnectingPerspective = new LocalReconnectingPerspective(
907
- currentSeq,
908
- clientId,
909
- obliterateInfo.stamp.localSeq! - 1,
910
- );
918
+ const reconnectingPerspective = new (
919
+ squash ? LocalSquashPerspective : LocalReconnectingPerspective
920
+ )(currentSeq, clientId, obliterateInfo.stamp.localSeq! - 1);
911
921
 
912
922
  const newStart = this.rebaseSidedLocalReference(
913
923
  obliterateInfo.start,
@@ -932,6 +942,7 @@ export class Client extends TypedEventEmitter<IClientEvents> {
932
942
  resetOp: IMergeTreeDeltaOp,
933
943
 
934
944
  segmentGroup: SegmentGroup,
945
+ squash: boolean,
935
946
  ): IMergeTreeDeltaOp[] {
936
947
  assert(!!segmentGroup, 0x033 /* "Segment group undefined" */);
937
948
  const NACKedSegmentGroup = this.pendingRebase?.shift()?.data;
@@ -991,13 +1002,20 @@ export class Client extends TypedEventEmitter<IClientEvents> {
991
1002
  );
992
1003
  const lastRemove = segment.removes[segment.removes.length - 1];
993
1004
  assert(
994
- lastRemove.type === "sliceRemove" && lastRemove.localSeq === segmentGroup.localSeq,
995
- 0xb67 /* Last remove should be the obliterate that is being resubmitted. */,
1005
+ (lastRemove.type === "sliceRemove" &&
1006
+ lastRemove.localSeq === segmentGroup.localSeq) ||
1007
+ opstampUtils.isSquashedOp(lastRemove),
1008
+ 0xbad /* Last remove should be the obliterate that is being resubmitted. */,
996
1009
  );
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();
1010
+
1011
+ // The original obliterate affected this segment, but it has since been removed.
1012
+ // This can happen when a concurrent obliterate also removed the segment, as well as when the segment was
1013
+ // only locally inserted and its insertion was squashed upon reconnecting.
1014
+ // In the concurrent removal case (where we didn't avoid sending the segment's insertion in the first place due
1015
+ // to squashing), we adjust the metadata on that segment to reflect the fact that this obliterate no longer removes it.
1016
+ if (!opstampUtils.isSquashedOp(lastRemove)) {
1017
+ segment.removes.pop();
1018
+ }
1001
1019
  }
1002
1020
 
1003
1021
  this._mergeTree.rebaseObliterateTo(obliterateInfo, undefined);
@@ -1047,10 +1065,11 @@ export class Client extends TypedEventEmitter<IClientEvents> {
1047
1065
  0x035 /* "Segment group not in segment pending queue" */,
1048
1066
  );
1049
1067
  if (
1050
- (segment.ordinal > newStartSegment.ordinal &&
1068
+ !isRemovedAndAcked(segment) &&
1069
+ ((segment.ordinal > newStartSegment.ordinal &&
1051
1070
  segment.ordinal < newEndSegment.ordinal) ||
1052
- (segment === newStartSegment && newStartSide === Side.Before) ||
1053
- (segment === newEndSegment && newEndSide === Side.After)
1071
+ (segment === newStartSegment && newStartSide === Side.Before) ||
1072
+ (segment === newEndSegment && newEndSide === Side.After))
1054
1073
  ) {
1055
1074
  segment.segmentGroups.enqueue(newObliterate.segmentGroup);
1056
1075
  } else {
@@ -1060,12 +1079,17 @@ export class Client extends TypedEventEmitter<IClientEvents> {
1060
1079
  );
1061
1080
  const lastRemove = segment.removes[segment.removes.length - 1];
1062
1081
  assert(
1063
- lastRemove.type === "sliceRemove" && lastRemove.localSeq === segmentGroup.localSeq,
1064
- 0xb6a /* Last remove should be the obliterate that is being resubmitted. */,
1082
+ (lastRemove.type === "sliceRemove" &&
1083
+ lastRemove.localSeq === segmentGroup.localSeq) ||
1084
+ opstampUtils.isSquashedOp(lastRemove),
1085
+ 0xbae /* Last remove should be the obliterate that is being resubmitted. */,
1065
1086
  );
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();
1087
+
1088
+ if (!opstampUtils.isSquashedOp(lastRemove)) {
1089
+ // The original obliterate affected this segment, but it has since been removed and it's impossible to apply the
1090
+ // local obliterate so that is so. We adjust the metadata on that segment now.
1091
+ segment.removes.pop();
1092
+ }
1069
1093
  }
1070
1094
  }
1071
1095
 
@@ -1154,13 +1178,25 @@ export class Client extends TypedEventEmitter<IClientEvents> {
1154
1178
  }
1155
1179
 
1156
1180
  case MergeTreeDeltaType.INSERT: {
1181
+ if (isInserted(segment) && opstampUtils.isSquashedOp(segment.insert)) {
1182
+ break;
1183
+ }
1157
1184
  assert(
1158
1185
  isInserted(segment) && opstampUtils.isLocal(segment.insert),
1159
1186
  0x037 /* "Segment already has assigned sequence number" */,
1160
1187
  );
1161
1188
  const removeInfo = toRemovalInfo(segment);
1162
1189
 
1163
- if (removeInfo !== undefined && opstampUtils.isAcked(removeInfo.removes[0])) {
1190
+ const unusedStamp: OperationStamp = { seq: 0, clientId: 0 };
1191
+ if (removeInfo !== undefined && squash) {
1192
+ assert(
1193
+ removeInfo.removes.length === 1 ||
1194
+ opstampUtils.isAcked(removeInfo.removes[removeInfo.removes.length - 2]),
1195
+ 0xbaf /* Expected only one local remove */,
1196
+ );
1197
+ this.squashInsertion(segment);
1198
+ break;
1199
+ } else if (removeInfo !== undefined && opstampUtils.isAcked(removeInfo.removes[0])) {
1164
1200
  assert(
1165
1201
  removeInfo.removes[0].type === "sliceRemove",
1166
1202
  0xb5c /* Remove on insertion must be caused by obliterate. */,
@@ -1181,6 +1217,7 @@ export class Client extends TypedEventEmitter<IClientEvents> {
1181
1217
  clientId: NonCollabClient,
1182
1218
  },
1183
1219
  });
1220
+ this._mergeTree.blockUpdatePathLengths(segment.parent, unusedStamp, true);
1184
1221
  break;
1185
1222
  }
1186
1223
 
@@ -1360,13 +1397,40 @@ export class Client extends TypedEventEmitter<IClientEvents> {
1360
1397
  { start: RebasedObliterateEndpoint; end: RebasedObliterateEndpoint }
1361
1398
  > = new Map();
1362
1399
 
1400
+ private squashInsertion(segment: ISegmentLeaf): void {
1401
+ overwriteInfo<IHasInsertionInfo & IHasRemovalInfo>(segment, {
1402
+ insert: {
1403
+ type: "insert",
1404
+ seq: UniversalSequenceNumber,
1405
+ localSeq: undefined,
1406
+ clientId: SquashClient,
1407
+ },
1408
+ removes: [
1409
+ {
1410
+ type: "setRemove",
1411
+ seq: UniversalSequenceNumber,
1412
+ localSeq: undefined,
1413
+ clientId: SquashClient,
1414
+ },
1415
+ ],
1416
+ });
1417
+
1418
+ this._mergeTree.blockUpdatePathLengths(segment.parent, { seq: 0, clientId: 0 }, true);
1419
+ }
1420
+
1363
1421
  /**
1364
1422
  * Given a pending operation and segment group, regenerate the op, so it
1365
1423
  * can be resubmitted
1366
1424
  * @param resetOp - The op to reset
1367
1425
  * @param segmentGroup - The segment group associated with the op
1426
+ * @param squash - whether intermediate states should be squashed. See `IDeltaHandler.reSubmit`'s squash parameter
1427
+ * documentation for more details.
1368
1428
  */
1369
- public regeneratePendingOp(resetOp: IMergeTreeOp, localOpMetadata: unknown): IMergeTreeOp {
1429
+ public regeneratePendingOp(
1430
+ resetOp: IMergeTreeOp,
1431
+ localOpMetadata: unknown,
1432
+ squash: boolean,
1433
+ ): IMergeTreeOp {
1370
1434
  const segmentGroup = localOpMetadata as SegmentGroup | SegmentGroup[];
1371
1435
  if (this.pendingRebase === undefined || this.pendingRebase.empty) {
1372
1436
  let firstGroup: SegmentGroup;
@@ -1396,13 +1460,14 @@ export class Client extends TypedEventEmitter<IClientEvents> {
1396
1460
  collabWindow.currentSeq !== this.lastNormalization.refSeq ||
1397
1461
  collabWindow.localSeq !== this.lastNormalization.localRefSeq
1398
1462
  ) {
1463
+ const allPendingSegments = [...this._mergeTree.pendingSegments, ...this.pendingRebase];
1399
1464
  // Compute obliterate endpoint destinations before segments are normalized.
1400
1465
  // Segment normalization can affect what should be the semantically correct segments for the endpoints to be placed on.
1401
1466
  this.cachedObliterateRebases.clear();
1402
- for (const group of [...this._mergeTree.pendingSegments, ...this.pendingRebase]) {
1467
+ for (const group of allPendingSegments) {
1403
1468
  const { obliterateInfo } = group.data;
1404
1469
  if (obliterateInfo !== undefined) {
1405
- const { start, end } = this.computeNewObliterateEndpoints(obliterateInfo);
1470
+ const { start, end } = this.computeNewObliterateEndpoints(obliterateInfo, squash);
1406
1471
  const { localSeq } = obliterateInfo.stamp;
1407
1472
  assert(localSeq !== undefined, 0xb6d /* Local seq must be defined */);
1408
1473
  this.cachedObliterateRebases.set(localSeq, { start, end });
@@ -1426,7 +1491,7 @@ export class Client extends TypedEventEmitter<IClientEvents> {
1426
1491
  );
1427
1492
 
1428
1493
  for (let i = 0; i < resetOp.ops.length; i++) {
1429
- opList.push(...this.resetPendingDeltaToOps(resetOp.ops[i], segmentGroup[i]));
1494
+ opList.push(...this.resetPendingDeltaToOps(resetOp.ops[i], segmentGroup[i], squash));
1430
1495
  }
1431
1496
  } else {
1432
1497
  // A group op containing a single op will pass a direct reference to 'segmentGroup'
@@ -1435,7 +1500,7 @@ export class Client extends TypedEventEmitter<IClientEvents> {
1435
1500
  resetOp.ops.length === 1,
1436
1501
  0x03b /* "Number of ops in 'resetOp' must match the number of segment groups provided." */,
1437
1502
  );
1438
- opList.push(...this.resetPendingDeltaToOps(resetOp.ops[0], segmentGroup));
1503
+ opList.push(...this.resetPendingDeltaToOps(resetOp.ops[0], segmentGroup, squash));
1439
1504
  }
1440
1505
  } else {
1441
1506
  assert(
@@ -1446,7 +1511,7 @@ export class Client extends TypedEventEmitter<IClientEvents> {
1446
1511
  !Array.isArray(segmentGroup),
1447
1512
  0x03d /* "segmentGroup is array rather than singleton!" */,
1448
1513
  );
1449
- opList.push(...this.resetPendingDeltaToOps(resetOp, segmentGroup));
1514
+ opList.push(...this.resetPendingDeltaToOps(resetOp, segmentGroup, squash));
1450
1515
  }
1451
1516
 
1452
1517
  return opList.length === 1 ? opList[0] : createGroupOp(...opList);
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/mergeTree.ts CHANGED
@@ -1270,7 +1270,7 @@ export class MergeTree {
1270
1270
  segment,
1271
1271
  (node) => {
1272
1272
  if (node.isLeaf()) {
1273
- if (Marker.is(node) && refHasTileLabel(node, markerLabel)) {
1273
+ if (!isRemoved(node) && Marker.is(node) && refHasTileLabel(node, markerLabel)) {
1274
1274
  foundMarker = node;
1275
1275
  }
1276
1276
  } else {
@@ -117,6 +117,32 @@ export class LocalReconnectingPerspective extends PerspectiveBase implements Per
117
117
  }
118
118
  }
119
119
 
120
+ /**
121
+ * This perspective is used when rebasing obliterate endpoints to find the segment to slide to when squash is enabled.
122
+ *
123
+ * TODO:AB#39357: This class would not be necessary if obliterate rebasing occurred as resubmit was called rather than
124
+ * precomputed before segment normalization. It also adds more dependencies on all ops being resubmitted (the squash
125
+ * parameter coming from rebasing an obliterate does not necessarily align with an inserted segment), which is not
126
+ * fully correct.
127
+ */
128
+ export class LocalSquashPerspective extends LocalReconnectingPerspective {
129
+ public constructor(
130
+ readonly refSeq: number,
131
+ readonly clientId: number,
132
+ readonly localSeq: number,
133
+ ) {
134
+ super(refSeq, clientId, localSeq);
135
+ }
136
+
137
+ public override isSegmentPresent(seg: ISegment): boolean {
138
+ // Avoid sliding to segments whose insertion will be squashed.
139
+ if (isInserted(seg) && opstampUtils.isLocal(seg.insert) && isRemoved(seg)) {
140
+ return false;
141
+ }
142
+ return super.isSegmentPresent(seg);
143
+ }
144
+ }
145
+
120
146
  /**
121
147
  * A perspective which includes edits which were either:
122
148
  * - acked and at or before some reference sequence number
package/src/stamps.ts CHANGED
@@ -3,7 +3,11 @@
3
3
  * Licensed under the MIT License.
4
4
  */
5
5
 
6
- import { UnassignedSequenceNumber } from "./constants.js";
6
+ import {
7
+ SquashClient,
8
+ UnassignedSequenceNumber,
9
+ UniversalSequenceNumber,
10
+ } from "./constants.js";
7
11
 
8
12
  /**
9
13
  * A stamp that identifies provenance of an operation performed on the MergeTree.
@@ -122,6 +126,10 @@ export function isLocal(a: OperationStamp): boolean {
122
126
  return a.seq === UnassignedSequenceNumber;
123
127
  }
124
128
 
129
+ export function isSquashedOp(a: OperationStamp): boolean {
130
+ return a.clientId === SquashClient && a.seq === UniversalSequenceNumber;
131
+ }
132
+
125
133
  export function isAcked(a: OperationStamp): boolean {
126
134
  return a.seq !== UnassignedSequenceNumber;
127
135
  }