@fluidframework/merge-tree 2.23.0-323641 → 2.23.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 (43) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/dist/mergeTree.d.ts.map +1 -1
  3. package/dist/mergeTree.js +47 -15
  4. package/dist/mergeTree.js.map +1 -1
  5. package/dist/mergeTreeNodes.d.ts.map +1 -1
  6. package/dist/mergeTreeNodes.js +0 -2
  7. package/dist/mergeTreeNodes.js.map +1 -1
  8. package/dist/partialLengths.d.ts.map +1 -1
  9. package/dist/partialLengths.js +3 -4
  10. package/dist/partialLengths.js.map +1 -1
  11. package/dist/segmentInfos.d.ts +10 -18
  12. package/dist/segmentInfos.d.ts.map +1 -1
  13. package/dist/segmentInfos.js +22 -3
  14. package/dist/segmentInfos.js.map +1 -1
  15. package/dist/snapshotLoader.d.ts.map +1 -1
  16. package/dist/snapshotLoader.js +0 -2
  17. package/dist/snapshotLoader.js.map +1 -1
  18. package/dist/test/obliterate.concurrent.spec.js +53 -1
  19. package/dist/test/obliterate.concurrent.spec.js.map +1 -1
  20. package/lib/mergeTree.d.ts.map +1 -1
  21. package/lib/mergeTree.js +48 -16
  22. package/lib/mergeTree.js.map +1 -1
  23. package/lib/mergeTreeNodes.d.ts.map +1 -1
  24. package/lib/mergeTreeNodes.js +0 -2
  25. package/lib/mergeTreeNodes.js.map +1 -1
  26. package/lib/partialLengths.d.ts.map +1 -1
  27. package/lib/partialLengths.js +4 -5
  28. package/lib/partialLengths.js.map +1 -1
  29. package/lib/segmentInfos.d.ts +10 -18
  30. package/lib/segmentInfos.d.ts.map +1 -1
  31. package/lib/segmentInfos.js +20 -2
  32. package/lib/segmentInfos.js.map +1 -1
  33. package/lib/snapshotLoader.d.ts.map +1 -1
  34. package/lib/snapshotLoader.js +0 -2
  35. package/lib/snapshotLoader.js.map +1 -1
  36. package/lib/test/obliterate.concurrent.spec.js +53 -1
  37. package/lib/test/obliterate.concurrent.spec.js.map +1 -1
  38. package/package.json +17 -17
  39. package/src/mergeTree.ts +57 -15
  40. package/src/mergeTreeNodes.ts +0 -2
  41. package/src/partialLengths.ts +12 -5
  42. package/src/segmentInfos.ts +23 -21
  43. package/src/snapshotLoader.ts +0 -2
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fluidframework/merge-tree",
3
- "version": "2.23.0-323641",
3
+ "version": "2.23.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.23.0-323641",
85
- "@fluidframework/container-definitions": "2.23.0-323641",
86
- "@fluidframework/core-interfaces": "2.23.0-323641",
87
- "@fluidframework/core-utils": "2.23.0-323641",
88
- "@fluidframework/datastore-definitions": "2.23.0-323641",
89
- "@fluidframework/driver-definitions": "2.23.0-323641",
90
- "@fluidframework/runtime-definitions": "2.23.0-323641",
91
- "@fluidframework/runtime-utils": "2.23.0-323641",
92
- "@fluidframework/shared-object-base": "2.23.0-323641",
93
- "@fluidframework/telemetry-utils": "2.23.0-323641"
84
+ "@fluid-internal/client-utils": "~2.23.0",
85
+ "@fluidframework/container-definitions": "~2.23.0",
86
+ "@fluidframework/core-interfaces": "~2.23.0",
87
+ "@fluidframework/core-utils": "~2.23.0",
88
+ "@fluidframework/datastore-definitions": "~2.23.0",
89
+ "@fluidframework/driver-definitions": "~2.23.0",
90
+ "@fluidframework/runtime-definitions": "~2.23.0",
91
+ "@fluidframework/runtime-utils": "~2.23.0",
92
+ "@fluidframework/shared-object-base": "~2.23.0",
93
+ "@fluidframework/telemetry-utils": "~2.23.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.23.0-323641",
99
- "@fluid-private/stochastic-test-utils": "2.23.0-323641",
100
- "@fluid-private/test-pairwise-generator": "2.23.0-323641",
98
+ "@fluid-internal/mocha-test-setup": "~2.23.0",
99
+ "@fluid-private/stochastic-test-utils": "~2.23.0",
100
+ "@fluid-private/test-pairwise-generator": "~2.23.0",
101
101
  "@fluid-tools/benchmark": "^0.50.0",
102
- "@fluid-tools/build-cli": "^0.53.0",
102
+ "@fluid-tools/build-cli": "^0.54.0",
103
103
  "@fluidframework/build-common": "^2.0.3",
104
- "@fluidframework/build-tools": "^0.53.0",
104
+ "@fluidframework/build-tools": "^0.54.0",
105
105
  "@fluidframework/eslint-config-fluid": "^5.7.3",
106
106
  "@fluidframework/merge-tree-previous": "npm:@fluidframework/merge-tree@2.22.0",
107
- "@fluidframework/test-runtime-utils": "2.23.0-323641",
107
+ "@fluidframework/test-runtime-utils": "~2.23.0",
108
108
  "@microsoft/api-extractor": "7.47.8",
109
109
  "@types/diff": "^3.5.1",
110
110
  "@types/mocha": "^10.0.10",
package/src/mergeTree.ts CHANGED
@@ -95,6 +95,7 @@ import {
95
95
  removeRemovalInfo,
96
96
  toMoveInfo,
97
97
  toRemovalInfo,
98
+ wasMovedOnInsert,
98
99
  type IInsertionInfo,
99
100
  type IMoveInfo,
100
101
  type IRemovalInfo,
@@ -1083,7 +1084,14 @@ export class MergeTree {
1083
1084
  if (!this.collabWindow.collaborating || this.collabWindow.clientId === clientId) {
1084
1085
  if (node.isLeaf()) {
1085
1086
  return this.localNetLength(node, refSeq, localSeq);
1086
- } else if (localSeq === undefined) {
1087
+ } else if (
1088
+ localSeq === undefined ||
1089
+ // All changes are visible. Small note on why we allow refSeq >= this.collabWindow.currentSeq rather than just equality:
1090
+ // merge-tree eventing occurs before the collab window is updated to account for whatever op it is processing, and we want
1091
+ // to support resolving positions from within the event handler which account for that op. e.g. undo-redo relies on this
1092
+ // behavior with local references.
1093
+ (localSeq === this.collabWindow.localSeq && refSeq >= this.collabWindow.currentSeq)
1094
+ ) {
1087
1095
  // Local client sees all segments, even when collaborating
1088
1096
  return node.cachedLength;
1089
1097
  } else {
@@ -1178,6 +1186,8 @@ export class MergeTree {
1178
1186
  */
1179
1187
  public referencePositionToLocalPosition(
1180
1188
  refPos: ReferencePosition,
1189
+ // Note: this is not `this.collabWindow.currentSeq` because we want to support resolving local reference positions to positions
1190
+ // from within event handlers, and the collab window's sequence numbers are not updated in time in all of those cases.
1181
1191
  refSeq = Number.MAX_SAFE_INTEGER,
1182
1192
  clientId = this.collabWindow.clientId,
1183
1193
  localSeq: number | undefined = this.collabWindow.localSeq,
@@ -1611,6 +1621,8 @@ export class MergeTree {
1611
1621
  let normalizedNewestSeq: number = 0;
1612
1622
  const movedClientIds: number[] = [];
1613
1623
  const movedSeqs: number[] = [];
1624
+ let newestAcked: ObliterateInfo | undefined;
1625
+ let oldestUnacked: ObliterateInfo | undefined;
1614
1626
  for (const ob of this.obliterates.findOverlapping(newSegment)) {
1615
1627
  // compute a normalized seq that takes into account local seqs
1616
1628
  // but is still comparable to remote seqs to keep the checks below easy
@@ -1641,6 +1653,23 @@ export class MergeTree {
1641
1653
  normalizedNewestSeq = normalizedObSeq;
1642
1654
  newest = ob;
1643
1655
  }
1656
+
1657
+ if (
1658
+ ob.seq !== UnassignedSequenceNumber &&
1659
+ (newestAcked === undefined || newestAcked.seq < ob.seq)
1660
+ ) {
1661
+ newestAcked = ob;
1662
+ }
1663
+
1664
+ if (
1665
+ ob.seq === UnassignedSequenceNumber &&
1666
+ (oldestUnacked === undefined || oldestUnacked.localSeq! > ob.localSeq!)
1667
+ ) {
1668
+ // There can be one local obliterate surrounding a segment if a client repeatedly obliterates
1669
+ // a region (ex: in the text ABCDEFG, obliterate D, then obliterate CE, then BF). In this case,
1670
+ // the first one that's applied will be the one that actually removes the segment.
1671
+ oldestUnacked = ob;
1672
+ }
1644
1673
  }
1645
1674
  }
1646
1675
 
@@ -1649,23 +1678,39 @@ export class MergeTree {
1649
1678
  // by the same client that's inserting this segment, we let them insert into this range and therefore don't
1650
1679
  // mark it obliterated.
1651
1680
  if (oldest && newest?.clientId !== clientId) {
1652
- const moveInfo: IMoveInfo = {
1653
- movedClientIds,
1654
- movedSeq: oldest.seq,
1655
- movedSeqs,
1656
- localMovedSeq: oldest.localSeq,
1657
- wasMovedOnInsert: oldest.seq !== UnassignedSequenceNumber,
1658
- };
1681
+ let moveInfo: IMoveInfo;
1682
+ if (newestAcked === newest || newestAcked?.clientId !== clientId) {
1683
+ moveInfo = {
1684
+ movedClientIds,
1685
+ movedSeq: oldest.seq,
1686
+ movedSeqs,
1687
+ localMovedSeq: oldestUnacked?.localSeq,
1688
+ };
1689
+ } else {
1690
+ assert(
1691
+ oldestUnacked !== undefined,
1692
+ 0xb55 /* Expected local obliterate to be defined if newestAcked is not equal to newest */,
1693
+ );
1694
+ // There's a pending local obliterate for this range, so it will be marked as obliterated by us. However,
1695
+ // all other clients are under the impression that the most recent acked obliterate won the right to insert
1696
+ // in this range.
1697
+ moveInfo = {
1698
+ movedClientIds: [oldestUnacked.clientId],
1699
+ movedSeq: oldestUnacked.seq,
1700
+ movedSeqs: [oldestUnacked.seq],
1701
+ localMovedSeq: oldestUnacked.localSeq,
1702
+ };
1703
+ }
1659
1704
 
1660
1705
  overwriteInfo(newSegment, moveInfo);
1661
1706
 
1662
1707
  if (moveInfo.localMovedSeq !== undefined) {
1663
1708
  assert(
1664
- oldest.segmentGroup !== undefined,
1709
+ oldestUnacked?.segmentGroup !== undefined,
1665
1710
  0x86c /* expected segment group to exist */,
1666
1711
  );
1667
1712
 
1668
- this.addToPendingList(newSegment, oldest.segmentGroup);
1713
+ this.addToPendingList(newSegment, oldestUnacked?.segmentGroup);
1669
1714
  }
1670
1715
 
1671
1716
  if (newSegment.parent) {
@@ -2104,8 +2149,6 @@ export class MergeTree {
2104
2149
  movedSeq: seq,
2105
2150
  localMovedSeq: localSeq,
2106
2151
  movedSeqs: [seq],
2107
- wasMovedOnInsert:
2108
- segment.seq === UnassignedSequenceNumber && seq !== UnassignedSequenceNumber,
2109
2152
  });
2110
2153
 
2111
2154
  const existingRemoval = toRemovalInfo(movedSeg);
@@ -2121,16 +2164,15 @@ export class MergeTree {
2121
2164
  }
2122
2165
  } else {
2123
2166
  if (existingMoveInfo.movedSeq === UnassignedSequenceNumber) {
2124
- // Should not need explicit set here, but this should be implied:
2125
2167
  assert(
2126
- !existingMoveInfo.wasMovedOnInsert,
2168
+ !wasMovedOnInsert(segment),
2127
2169
  0xab4 /* Local obliterate cannot have removed a segment as soon as it was inserted */,
2128
2170
  );
2129
2171
  assert(
2130
2172
  seq !== UnassignedSequenceNumber,
2131
2173
  0xab5 /* Cannot obliterate the same segment locally twice */,
2132
2174
  );
2133
- existingMoveInfo.wasMovedOnInsert = segment.seq === UnassignedSequenceNumber;
2175
+
2134
2176
  // we moved this locally, but someone else moved it first
2135
2177
  // so put them at the head of the list
2136
2178
  // The list isn't ordered, but we keep the first move at the head
@@ -375,7 +375,6 @@ export abstract class BaseSegment implements ISegment {
375
375
  overwriteInfo<IMoveInfo>(seg, {
376
376
  movedSeq: this.movedSeq,
377
377
  movedSeqs: [...this.movedSeqs],
378
- wasMovedOnInsert: this.wasMovedOnInsert,
379
378
  movedClientIds: [...this.movedClientIds],
380
379
  });
381
380
  }
@@ -441,7 +440,6 @@ export abstract class BaseSegment implements ISegment {
441
440
  movedSeq: this.movedSeq,
442
441
  movedSeqs: [...this.movedSeqs],
443
442
  localMovedSeq: this.localMovedSeq,
444
- wasMovedOnInsert: this.wasMovedOnInsert,
445
443
  });
446
444
  }
447
445
 
@@ -14,7 +14,12 @@ import {
14
14
  seqLTE,
15
15
  type MergeBlock,
16
16
  } from "./mergeTreeNodes.js";
17
- import { toRemovalInfo, toMoveInfo, assertInserted } from "./segmentInfos.js";
17
+ import {
18
+ toRemovalInfo,
19
+ toMoveInfo,
20
+ assertInserted,
21
+ wasMovedOnInsert,
22
+ } from "./segmentInfos.js";
18
23
  import { SortedSet } from "./sortedSet.js";
19
24
 
20
25
  class PartialSequenceLengthsSet extends SortedSet<PartialSequenceLength> {
@@ -472,8 +477,7 @@ export class PartialSequenceLengths {
472
477
  if (child.isLeaf()) {
473
478
  // Leaf segment
474
479
  const segment = child;
475
- const moveInfo = toMoveInfo(segment);
476
- if (moveInfo?.wasMovedOnInsert) {
480
+ if (wasMovedOnInsert(segment)) {
477
481
  PartialSequenceLengths.accountForMoveOnInsert(
478
482
  combinedPartialLengths,
479
483
  segment,
@@ -511,7 +515,10 @@ export class PartialSequenceLengths {
511
515
  ): void {
512
516
  assertInserted(segment);
513
517
  const moveInfo = toMoveInfo(segment);
514
- assert(moveInfo?.wasMovedOnInsert === true, 0xab7 /* Segment was not moved on insert */);
518
+ assert(
519
+ moveInfo !== undefined && wasMovedOnInsert(segment),
520
+ 0xab7 /* Segment was not moved on insert */,
521
+ );
515
522
  if (moveInfo.movedSeq <= collabWindow.minSeq) {
516
523
  // This segment was obliterated as soon as it was inserted, and everyone was aware of the obliterate.
517
524
  // Thus every single client treats this segment as length 0 from every perspective, and no adjustments
@@ -843,7 +850,7 @@ export class PartialSequenceLengths {
843
850
  segment.seq !== undefined &&
844
851
  moveInfo &&
845
852
  moveInfo.movedSeq < segment.seq &&
846
- moveInfo.wasMovedOnInsert
853
+ wasMovedOnInsert(segment)
847
854
  ) {
848
855
  this.addClientAdjustment(clientId, moveInfo.movedSeq, segment.cachedLength);
849
856
  failIncrementalPropagation = true;
@@ -5,6 +5,7 @@
5
5
 
6
6
  import { assert, isObject } from "@fluidframework/core-utils/internal";
7
7
 
8
+ import { UnassignedSequenceNumber } from "./constants.js";
8
9
  import { ISegmentInternal, ISegmentPrivate, MergeBlock } from "./mergeTreeNodes.js";
9
10
  import type { ReferencePosition } from "./referencePositions.js";
10
11
 
@@ -296,31 +297,12 @@ export interface IMoveInfo {
296
297
  * list have all issued concurrent ops to move the segment.
297
298
  */
298
299
  movedClientIds: number[];
299
-
300
- /**
301
- * If this segment was inserted into a concurrently moved range and
302
- * the move op was sequenced before the insertion op. In this case,
303
- * the segment is visible only to the inserting client
304
- *
305
- * `wasMovedOnInsert` only applies for acked obliterates. That is, if
306
- * a segment inserted by a remote client is moved on insertion by a local
307
- * and unacked obliterate, we do not consider it as having been moved
308
- * on insert
309
- *
310
- * If a segment is moved on insertion, its length is only ever visible to
311
- * the client that inserted the segment. This is relevant in partial length
312
- * calculations
313
- *
314
- * @privateRemarks
315
- * TODO:AB#29553: This property is not persisted in the summary, but it should be.
316
- */
317
- wasMovedOnInsert: boolean;
318
300
  }
301
+
319
302
  export const toMoveInfo = (segmentLike: unknown): IMoveInfo | undefined =>
320
303
  hasProp(segmentLike, "movedClientIds", "array") &&
321
304
  hasProp(segmentLike, "movedSeq", "number") &&
322
- hasProp(segmentLike, "movedSeqs", "array") &&
323
- hasProp(segmentLike, "wasMovedOnInsert", "boolean")
305
+ hasProp(segmentLike, "movedSeqs", "array")
324
306
  ? segmentLike
325
307
  : undefined;
326
308
 
@@ -334,6 +316,26 @@ export const toMoveInfo = (segmentLike: unknown): IMoveInfo | undefined =>
334
316
  export const isMoved = (segmentLike: unknown): segmentLike is IMoveInfo =>
335
317
  toMoveInfo(segmentLike) !== undefined;
336
318
 
319
+ /**
320
+ * Returns whether this segment was marked moved as soon as its insertion was acked.
321
+ *
322
+ * This can happen when an an insert occurs concurrent to an obliterate over the range the segment was inserted into,
323
+ * and the obliterate was sequenced first.
324
+ *
325
+ * When this happens, the segment is only ever visible to the client that inserted the segment
326
+ * (and only until that client has seen the obliterate which removed their segment).
327
+ */
328
+ export function wasMovedOnInsert(segment: IInsertionInfo & ISegmentPrivate): boolean {
329
+ const moveInfo = toMoveInfo(segment);
330
+ const movedSeq = moveInfo?.movedSeq;
331
+ if (movedSeq === undefined || movedSeq === UnassignedSequenceNumber) {
332
+ return false;
333
+ }
334
+
335
+ const insertSeq = segment.seq;
336
+ return insertSeq === UnassignedSequenceNumber || insertSeq > movedSeq;
337
+ }
338
+
337
339
  /**
338
340
  * Asserts that the segment has move info. Usage of this function should not produce a user facing error.
339
341
  *
@@ -140,8 +140,6 @@ export class SnapshotLoader {
140
140
  movedClientIds: spec.movedClientIds.map((id) =>
141
141
  this.client.getOrAddShortClientId(id),
142
142
  ),
143
- // TODO:AB#29553: This property should be derived from segment data, not hard-coded.
144
- wasMovedOnInsert: false,
145
143
  });
146
144
  }
147
145