@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.
- package/CHANGELOG.md +4 -0
- package/dist/mergeTree.d.ts.map +1 -1
- package/dist/mergeTree.js +47 -15
- package/dist/mergeTree.js.map +1 -1
- package/dist/mergeTreeNodes.d.ts.map +1 -1
- package/dist/mergeTreeNodes.js +0 -2
- package/dist/mergeTreeNodes.js.map +1 -1
- package/dist/partialLengths.d.ts.map +1 -1
- package/dist/partialLengths.js +3 -4
- package/dist/partialLengths.js.map +1 -1
- package/dist/segmentInfos.d.ts +10 -18
- package/dist/segmentInfos.d.ts.map +1 -1
- package/dist/segmentInfos.js +22 -3
- package/dist/segmentInfos.js.map +1 -1
- package/dist/snapshotLoader.d.ts.map +1 -1
- package/dist/snapshotLoader.js +0 -2
- package/dist/snapshotLoader.js.map +1 -1
- package/dist/test/obliterate.concurrent.spec.js +53 -1
- package/dist/test/obliterate.concurrent.spec.js.map +1 -1
- package/lib/mergeTree.d.ts.map +1 -1
- package/lib/mergeTree.js +48 -16
- package/lib/mergeTree.js.map +1 -1
- package/lib/mergeTreeNodes.d.ts.map +1 -1
- package/lib/mergeTreeNodes.js +0 -2
- package/lib/mergeTreeNodes.js.map +1 -1
- package/lib/partialLengths.d.ts.map +1 -1
- package/lib/partialLengths.js +4 -5
- package/lib/partialLengths.js.map +1 -1
- package/lib/segmentInfos.d.ts +10 -18
- package/lib/segmentInfos.d.ts.map +1 -1
- package/lib/segmentInfos.js +20 -2
- package/lib/segmentInfos.js.map +1 -1
- package/lib/snapshotLoader.d.ts.map +1 -1
- package/lib/snapshotLoader.js +0 -2
- package/lib/snapshotLoader.js.map +1 -1
- package/lib/test/obliterate.concurrent.spec.js +53 -1
- package/lib/test/obliterate.concurrent.spec.js.map +1 -1
- package/package.json +17 -17
- package/src/mergeTree.ts +57 -15
- package/src/mergeTreeNodes.ts +0 -2
- package/src/partialLengths.ts +12 -5
- package/src/segmentInfos.ts +23 -21
- 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
|
|
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
|
|
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
|
|
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
|
|
99
|
-
"@fluid-private/stochastic-test-utils": "2.23.0
|
|
100
|
-
"@fluid-private/test-pairwise-generator": "2.23.0
|
|
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.
|
|
102
|
+
"@fluid-tools/build-cli": "^0.54.0",
|
|
103
103
|
"@fluidframework/build-common": "^2.0.3",
|
|
104
|
-
"@fluidframework/build-tools": "^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
|
|
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 (
|
|
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
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
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
|
-
|
|
1709
|
+
oldestUnacked?.segmentGroup !== undefined,
|
|
1665
1710
|
0x86c /* expected segment group to exist */,
|
|
1666
1711
|
);
|
|
1667
1712
|
|
|
1668
|
-
this.addToPendingList(newSegment,
|
|
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
|
-
!
|
|
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
|
-
|
|
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
|
package/src/mergeTreeNodes.ts
CHANGED
|
@@ -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
|
|
package/src/partialLengths.ts
CHANGED
|
@@ -14,7 +14,12 @@ import {
|
|
|
14
14
|
seqLTE,
|
|
15
15
|
type MergeBlock,
|
|
16
16
|
} from "./mergeTreeNodes.js";
|
|
17
|
-
import {
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
853
|
+
wasMovedOnInsert(segment)
|
|
847
854
|
) {
|
|
848
855
|
this.addClientAdjustment(clientId, moveInfo.movedSeq, segment.cachedLength);
|
|
849
856
|
failIncrementalPropagation = true;
|
package/src/segmentInfos.ts
CHANGED
|
@@ -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
|
*
|
package/src/snapshotLoader.ts
CHANGED
|
@@ -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
|
|