@fluidframework/merge-tree 2.30.0 → 2.31.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 +403 -399
- package/api-report/merge-tree.legacy.alpha.api.md +1 -0
- package/dist/MergeTreeTextHelper.d.ts +9 -3
- package/dist/MergeTreeTextHelper.d.ts.map +1 -1
- package/dist/MergeTreeTextHelper.js +5 -5
- package/dist/MergeTreeTextHelper.js.map +1 -1
- package/dist/client.d.ts +7 -13
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +136 -110
- package/dist/client.js.map +1 -1
- package/dist/endOfTreeSegment.d.ts +12 -8
- package/dist/endOfTreeSegment.d.ts.map +1 -1
- package/dist/endOfTreeSegment.js +2 -4
- package/dist/endOfTreeSegment.js.map +1 -1
- package/dist/index.d.ts +6 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -3
- package/dist/index.js.map +1 -1
- package/dist/mergeTree.d.ts +37 -23
- package/dist/mergeTree.d.ts.map +1 -1
- package/dist/mergeTree.js +400 -483
- package/dist/mergeTree.js.map +1 -1
- package/dist/mergeTreeDeltaCallback.d.ts +4 -8
- package/dist/mergeTreeDeltaCallback.d.ts.map +1 -1
- package/dist/mergeTreeDeltaCallback.js.map +1 -1
- package/dist/mergeTreeNodes.d.ts +32 -10
- package/dist/mergeTreeNodes.d.ts.map +1 -1
- package/dist/mergeTreeNodes.js +43 -28
- package/dist/mergeTreeNodes.js.map +1 -1
- package/dist/partialLengths.d.ts +2 -2
- package/dist/partialLengths.d.ts.map +1 -1
- package/dist/partialLengths.js +181 -109
- package/dist/partialLengths.js.map +1 -1
- package/dist/perspective.d.ts +8 -27
- package/dist/perspective.d.ts.map +1 -1
- package/dist/perspective.js +7 -67
- package/dist/perspective.js.map +1 -1
- package/dist/revertibles.d.ts.map +1 -1
- package/dist/revertibles.js +2 -2
- package/dist/revertibles.js.map +1 -1
- package/dist/segmentInfos.d.ts +20 -106
- package/dist/segmentInfos.d.ts.map +1 -1
- package/dist/segmentInfos.js +28 -42
- package/dist/segmentInfos.js.map +1 -1
- package/dist/segmentPropertiesManager.d.ts +1 -14
- package/dist/segmentPropertiesManager.d.ts.map +1 -1
- package/dist/segmentPropertiesManager.js +3 -17
- package/dist/segmentPropertiesManager.js.map +1 -1
- package/dist/snapshotLoader.d.ts.map +1 -1
- package/dist/snapshotLoader.js +62 -19
- package/dist/snapshotLoader.js.map +1 -1
- package/dist/snapshotV1.d.ts.map +1 -1
- package/dist/snapshotV1.js +55 -24
- package/dist/snapshotV1.js.map +1 -1
- package/dist/snapshotlegacy.d.ts.map +1 -1
- package/dist/snapshotlegacy.js +6 -9
- package/dist/snapshotlegacy.js.map +1 -1
- package/dist/stamps.d.ts +1 -1
- package/dist/stamps.js +1 -1
- package/dist/stamps.js.map +1 -1
- package/dist/test/Insertion.perf.spec.js +6 -51
- package/dist/test/Insertion.perf.spec.js.map +1 -1
- package/dist/test/PartialLengths.perf.spec.js +18 -25
- package/dist/test/PartialLengths.perf.spec.js.map +1 -1
- package/dist/test/Removal.perf.spec.js +13 -41
- package/dist/test/Removal.perf.spec.js.map +1 -1
- package/dist/test/beastTest.spec.d.ts.map +1 -1
- package/dist/test/beastTest.spec.js +41 -66
- package/dist/test/beastTest.spec.js.map +1 -1
- package/dist/test/client.annotateMarker.spec.js +1 -11
- package/dist/test/client.annotateMarker.spec.js.map +1 -1
- package/dist/test/client.applyMsg.spec.js +14 -14
- package/dist/test/client.applyMsg.spec.js.map +1 -1
- package/dist/test/client.getPosition.spec.js +1 -1
- package/dist/test/client.getPosition.spec.js.map +1 -1
- package/dist/test/client.localReference.spec.js +1 -1
- package/dist/test/client.localReference.spec.js.map +1 -1
- package/dist/test/client.rollback.spec.js +49 -58
- package/dist/test/client.rollback.spec.js.map +1 -1
- package/dist/test/client.rollbackFarm.spec.js +1 -1
- package/dist/test/client.rollbackFarm.spec.js.map +1 -1
- package/dist/test/client.searchForMarker.spec.js +4 -21
- package/dist/test/client.searchForMarker.spec.js.map +1 -1
- package/dist/test/index.d.ts +2 -2
- package/dist/test/index.d.ts.map +1 -1
- package/dist/test/index.js +2 -6
- package/dist/test/index.js.map +1 -1
- package/dist/test/mergeTree.annotate.deltaCallback.spec.js +14 -59
- package/dist/test/mergeTree.annotate.deltaCallback.spec.js.map +1 -1
- package/dist/test/mergeTree.annotate.spec.js +47 -63
- package/dist/test/mergeTree.annotate.spec.js.map +1 -1
- package/dist/test/mergeTree.insert.deltaCallback.spec.js +9 -62
- package/dist/test/mergeTree.insert.deltaCallback.spec.js.map +1 -1
- package/dist/test/mergeTree.insertingWalk.spec.js +59 -125
- package/dist/test/mergeTree.insertingWalk.spec.js.map +1 -1
- package/dist/test/mergeTree.markRangeRemoved.deltaCallback.spec.js +12 -93
- package/dist/test/mergeTree.markRangeRemoved.deltaCallback.spec.js.map +1 -1
- package/dist/test/mergeTree.markRangeRemoved.spec.js +10 -7
- package/dist/test/mergeTree.markRangeRemoved.spec.js.map +1 -1
- package/dist/test/mergeTree.walk.spec.js +2 -14
- package/dist/test/mergeTree.walk.spec.js.map +1 -1
- package/dist/test/mergeTreeOperationRunner.js +2 -2
- package/dist/test/mergeTreeOperationRunner.js.map +1 -1
- package/dist/test/obliterate.concurrent.spec.js +18 -23
- package/dist/test/obliterate.concurrent.spec.js.map +1 -1
- package/dist/test/obliterate.partialLength.spec.js +166 -136
- package/dist/test/obliterate.partialLength.spec.js.map +1 -1
- package/dist/test/obliterate.spec.js +16 -126
- package/dist/test/obliterate.spec.js.map +1 -1
- package/dist/test/partialLength.spec.js +28 -196
- package/dist/test/partialLength.spec.js.map +1 -1
- package/dist/test/perspective.spec.js +34 -0
- package/dist/test/perspective.spec.js.map +1 -1
- package/dist/test/propertyManager.spec.js +1 -1
- package/dist/test/propertyManager.spec.js.map +1 -1
- package/dist/test/resetPendingSegmentsToOp.spec.js +0 -2
- package/dist/test/resetPendingSegmentsToOp.spec.js.map +1 -1
- package/dist/test/segmentGroupCollection.spec.js +10 -4
- package/dist/test/segmentGroupCollection.spec.js.map +1 -1
- package/dist/test/testClient.d.ts +1 -0
- package/dist/test/testClient.d.ts.map +1 -1
- package/dist/test/testClient.js +16 -26
- package/dist/test/testClient.js.map +1 -1
- package/dist/test/testClientLogger.d.ts.map +1 -1
- package/dist/test/testClientLogger.js +3 -10
- package/dist/test/testClientLogger.js.map +1 -1
- package/dist/test/testServer.d.ts +2 -1
- package/dist/test/testServer.d.ts.map +1 -1
- package/dist/test/testServer.js +7 -5
- package/dist/test/testServer.js.map +1 -1
- package/dist/test/testUtils.d.ts +36 -56
- package/dist/test/testUtils.d.ts.map +1 -1
- package/dist/test/testUtils.js +68 -77
- package/dist/test/testUtils.js.map +1 -1
- package/dist/test/text.d.ts +2 -2
- package/dist/test/text.d.ts.map +1 -1
- package/dist/test/text.js +5 -2
- package/dist/test/text.js.map +1 -1
- package/dist/textSegment.d.ts +0 -6
- package/dist/textSegment.d.ts.map +1 -1
- package/dist/textSegment.js.map +1 -1
- package/dist/zamboni.d.ts.map +1 -1
- package/dist/zamboni.js +53 -26
- package/dist/zamboni.js.map +1 -1
- package/lib/MergeTreeTextHelper.d.ts +9 -3
- package/lib/MergeTreeTextHelper.d.ts.map +1 -1
- package/lib/MergeTreeTextHelper.js +5 -5
- package/lib/MergeTreeTextHelper.js.map +1 -1
- package/lib/client.d.ts +7 -13
- package/lib/client.d.ts.map +1 -1
- package/lib/client.js +117 -116
- package/lib/client.js.map +1 -1
- package/lib/endOfTreeSegment.d.ts +12 -8
- package/lib/endOfTreeSegment.d.ts.map +1 -1
- package/lib/endOfTreeSegment.js +2 -4
- package/lib/endOfTreeSegment.js.map +1 -1
- package/lib/index.d.ts +6 -3
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +1 -1
- package/lib/index.js.map +1 -1
- package/lib/mergeTree.d.ts +37 -23
- package/lib/mergeTree.d.ts.map +1 -1
- package/lib/mergeTree.js +381 -488
- package/lib/mergeTree.js.map +1 -1
- package/lib/mergeTreeDeltaCallback.d.ts +4 -8
- package/lib/mergeTreeDeltaCallback.d.ts.map +1 -1
- package/lib/mergeTreeDeltaCallback.js.map +1 -1
- package/lib/mergeTreeNodes.d.ts +32 -10
- package/lib/mergeTreeNodes.d.ts.map +1 -1
- package/lib/mergeTreeNodes.js +42 -29
- package/lib/mergeTreeNodes.js.map +1 -1
- package/lib/partialLengths.d.ts +2 -2
- package/lib/partialLengths.d.ts.map +1 -1
- package/lib/partialLengths.js +160 -111
- package/lib/partialLengths.js.map +1 -1
- package/lib/perspective.d.ts +8 -27
- package/lib/perspective.d.ts.map +1 -1
- package/lib/perspective.js +8 -68
- package/lib/perspective.js.map +1 -1
- package/lib/revertibles.d.ts.map +1 -1
- package/lib/revertibles.js +2 -2
- package/lib/revertibles.js.map +1 -1
- package/lib/segmentInfos.d.ts +20 -106
- package/lib/segmentInfos.d.ts.map +1 -1
- package/lib/segmentInfos.js +26 -37
- package/lib/segmentInfos.js.map +1 -1
- package/lib/segmentPropertiesManager.d.ts +1 -14
- package/lib/segmentPropertiesManager.d.ts.map +1 -1
- package/lib/segmentPropertiesManager.js +2 -16
- package/lib/segmentPropertiesManager.js.map +1 -1
- package/lib/snapshotLoader.d.ts.map +1 -1
- package/lib/snapshotLoader.js +39 -19
- package/lib/snapshotLoader.js.map +1 -1
- package/lib/snapshotV1.d.ts.map +1 -1
- package/lib/snapshotV1.js +34 -26
- package/lib/snapshotV1.js.map +1 -1
- package/lib/snapshotlegacy.d.ts.map +1 -1
- package/lib/snapshotlegacy.js +7 -10
- package/lib/snapshotlegacy.js.map +1 -1
- package/lib/stamps.d.ts +1 -1
- package/lib/stamps.js +1 -1
- package/lib/stamps.js.map +1 -1
- package/lib/test/Insertion.perf.spec.js +6 -51
- package/lib/test/Insertion.perf.spec.js.map +1 -1
- package/lib/test/PartialLengths.perf.spec.js +18 -25
- package/lib/test/PartialLengths.perf.spec.js.map +1 -1
- package/lib/test/Removal.perf.spec.js +13 -41
- package/lib/test/Removal.perf.spec.js.map +1 -1
- package/lib/test/beastTest.spec.d.ts.map +1 -1
- package/lib/test/beastTest.spec.js +42 -67
- package/lib/test/beastTest.spec.js.map +1 -1
- package/lib/test/client.annotateMarker.spec.js +1 -11
- package/lib/test/client.annotateMarker.spec.js.map +1 -1
- package/lib/test/client.applyMsg.spec.js +14 -14
- package/lib/test/client.applyMsg.spec.js.map +1 -1
- package/lib/test/client.getPosition.spec.js +1 -1
- package/lib/test/client.getPosition.spec.js.map +1 -1
- package/lib/test/client.localReference.spec.js +1 -1
- package/lib/test/client.localReference.spec.js.map +1 -1
- package/lib/test/client.rollback.spec.js +50 -59
- package/lib/test/client.rollback.spec.js.map +1 -1
- package/lib/test/client.rollbackFarm.spec.js +1 -1
- package/lib/test/client.rollbackFarm.spec.js.map +1 -1
- package/lib/test/client.searchForMarker.spec.js +4 -21
- package/lib/test/client.searchForMarker.spec.js.map +1 -1
- package/lib/test/index.d.ts +2 -2
- package/lib/test/index.d.ts.map +1 -1
- package/lib/test/index.js +1 -1
- package/lib/test/index.js.map +1 -1
- package/lib/test/mergeTree.annotate.deltaCallback.spec.js +15 -60
- package/lib/test/mergeTree.annotate.deltaCallback.spec.js.map +1 -1
- package/lib/test/mergeTree.annotate.spec.js +48 -64
- package/lib/test/mergeTree.annotate.spec.js.map +1 -1
- package/lib/test/mergeTree.insert.deltaCallback.spec.js +10 -63
- package/lib/test/mergeTree.insert.deltaCallback.spec.js.map +1 -1
- package/lib/test/mergeTree.insertingWalk.spec.js +61 -127
- package/lib/test/mergeTree.insertingWalk.spec.js.map +1 -1
- package/lib/test/mergeTree.markRangeRemoved.deltaCallback.spec.js +13 -94
- package/lib/test/mergeTree.markRangeRemoved.deltaCallback.spec.js.map +1 -1
- package/lib/test/mergeTree.markRangeRemoved.spec.js +10 -7
- package/lib/test/mergeTree.markRangeRemoved.spec.js.map +1 -1
- package/lib/test/mergeTree.walk.spec.js +2 -14
- package/lib/test/mergeTree.walk.spec.js.map +1 -1
- package/lib/test/mergeTreeOperationRunner.js +3 -3
- package/lib/test/mergeTreeOperationRunner.js.map +1 -1
- package/lib/test/obliterate.concurrent.spec.js +18 -23
- package/lib/test/obliterate.concurrent.spec.js.map +1 -1
- package/lib/test/obliterate.partialLength.spec.js +167 -137
- package/lib/test/obliterate.partialLength.spec.js.map +1 -1
- package/lib/test/obliterate.spec.js +17 -127
- package/lib/test/obliterate.spec.js.map +1 -1
- package/lib/test/partialLength.spec.js +29 -197
- package/lib/test/partialLength.spec.js.map +1 -1
- package/lib/test/perspective.spec.js +34 -0
- package/lib/test/perspective.spec.js.map +1 -1
- package/lib/test/propertyManager.spec.js +2 -2
- package/lib/test/propertyManager.spec.js.map +1 -1
- package/lib/test/resetPendingSegmentsToOp.spec.js +0 -2
- package/lib/test/resetPendingSegmentsToOp.spec.js.map +1 -1
- package/lib/test/segmentGroupCollection.spec.js +10 -4
- package/lib/test/segmentGroupCollection.spec.js.map +1 -1
- package/lib/test/testClient.d.ts +1 -0
- package/lib/test/testClient.d.ts.map +1 -1
- package/lib/test/testClient.js +18 -28
- package/lib/test/testClient.js.map +1 -1
- package/lib/test/testClientLogger.d.ts.map +1 -1
- package/lib/test/testClientLogger.js +3 -10
- package/lib/test/testClientLogger.js.map +1 -1
- package/lib/test/testServer.d.ts +2 -1
- package/lib/test/testServer.d.ts.map +1 -1
- package/lib/test/testServer.js +7 -5
- package/lib/test/testServer.js.map +1 -1
- package/lib/test/testUtils.d.ts +36 -56
- package/lib/test/testUtils.d.ts.map +1 -1
- package/lib/test/testUtils.js +66 -48
- package/lib/test/testUtils.js.map +1 -1
- package/lib/test/text.d.ts +2 -2
- package/lib/test/text.d.ts.map +1 -1
- package/lib/test/text.js +6 -3
- package/lib/test/text.js.map +1 -1
- package/lib/textSegment.d.ts +0 -6
- package/lib/textSegment.d.ts.map +1 -1
- package/lib/textSegment.js.map +1 -1
- package/lib/tsdoc-metadata.json +1 -1
- package/lib/zamboni.d.ts.map +1 -1
- package/lib/zamboni.js +32 -28
- package/lib/zamboni.js.map +1 -1
- package/package.json +17 -20
- package/src/MergeTreeTextHelper.ts +17 -12
- package/src/client.ts +141 -197
- package/src/endOfTreeSegment.ts +11 -8
- package/src/index.ts +4 -3
- package/src/mergeTree.ts +482 -633
- package/src/mergeTreeDeltaCallback.ts +4 -8
- package/src/mergeTreeNodes.ts +66 -45
- package/src/partialLengths.ts +181 -137
- package/src/perspective.ts +17 -95
- package/src/revertibles.ts +2 -7
- package/src/segmentInfos.ts +48 -141
- package/src/segmentPropertiesManager.ts +2 -16
- package/src/snapshotLoader.ts +62 -30
- package/src/snapshotV1.ts +36 -28
- package/src/snapshotlegacy.ts +7 -16
- package/src/stamps.ts +1 -1
- package/src/textSegment.ts +0 -13
- package/src/zamboni.ts +38 -32
- package/tsconfig.json +1 -0
- package/prettier.config.cjs +0 -8
package/lib/mergeTree.js
CHANGED
|
@@ -11,89 +11,83 @@ import { NonCollabClient, TreeMaintenanceSequenceNumber, UnassignedSequenceNumbe
|
|
|
11
11
|
import { EndOfTreeSegment, StartOfTreeSegment } from "./endOfTreeSegment.js";
|
|
12
12
|
import { LocalReferenceCollection, SlidingPreference, anyLocalReferencePosition, createDetachedLocalReferencePosition, filterLocalReferencePositions, } from "./localReference.js";
|
|
13
13
|
import { MergeTreeMaintenanceType, } from "./mergeTreeDeltaCallback.js";
|
|
14
|
-
import { NodeAction, backwardExcursion, depthFirstNodeWalk, forwardExcursion, walkAllChildSegments, } from "./mergeTreeNodeWalk.js";
|
|
15
|
-
import { CollaborationWindow, Marker, MaxNodesInBlock, MergeBlock, assertSegmentLeaf, assignChild, isSegmentLeaf, reservedMarkerIdKey,
|
|
14
|
+
import { LeafAction, NodeAction, backwardExcursion, depthFirstNodeWalk, forwardExcursion, walkAllChildSegments, } from "./mergeTreeNodeWalk.js";
|
|
15
|
+
import { CollaborationWindow, Marker, MaxNodesInBlock, MergeBlock, assertSegmentLeaf, assignChild, getMinSeqPerspective, getMinSeqStamp, isSegmentLeaf, reservedMarkerIdKey, } from "./mergeTreeNodes.js";
|
|
16
16
|
import { createAnnotateRangeOp, createInsertSegmentOp, createRemoveRangeOp, } from "./opBuilder.js";
|
|
17
17
|
import { MergeTreeDeltaType, ReferenceType, } from "./ops.js";
|
|
18
18
|
import { PartialSequenceLengths } from "./partialLengths.js";
|
|
19
|
-
import {
|
|
19
|
+
import { PriorPerspective, LocalReconnectingPerspective, LocalDefaultPerspective, RemoteObliteratePerspective, } from "./perspective.js";
|
|
20
20
|
import { createMap, extend, extendIfUndefined } from "./properties.js";
|
|
21
21
|
import { DetachedReferencePosition, refGetTileLabels, refHasTileLabel, refTypeIncludesFlag, } from "./referencePositions.js";
|
|
22
22
|
import { SegmentGroupCollection } from "./segmentGroupCollection.js";
|
|
23
|
-
import {
|
|
24
|
-
import { copyPropertiesAndManager, PropertiesManager,
|
|
23
|
+
import { assertRemoved, isMergeNodeInfo, isRemoved, overwriteInfo, removeRemovalInfo, toRemovalInfo, } from "./segmentInfos.js";
|
|
24
|
+
import { copyPropertiesAndManager, PropertiesManager, } from "./segmentPropertiesManager.js";
|
|
25
25
|
import { Side } from "./sequencePlace.js";
|
|
26
26
|
import { SortedSegmentSet } from "./sortedSegmentSet.js";
|
|
27
|
+
import * as opstampUtils from "./stamps.js";
|
|
27
28
|
import { zamboniSegments } from "./zamboni.js";
|
|
28
|
-
function isRemovedAndAcked(segment) {
|
|
29
|
+
export function isRemovedAndAcked(segment) {
|
|
29
30
|
const removalInfo = toRemovalInfo(segment);
|
|
30
|
-
return removalInfo !== undefined && removalInfo.
|
|
31
|
-
}
|
|
32
|
-
function isMovedAndAcked(segment) {
|
|
33
|
-
const moveInfo = toMoveInfo(segment);
|
|
34
|
-
return moveInfo !== undefined && moveInfo.movedSeq !== UnassignedSequenceNumber;
|
|
35
|
-
}
|
|
36
|
-
function isRemovedAndAckedOrMovedAndAcked(segment) {
|
|
37
|
-
return isRemovedAndAcked(segment) || isMovedAndAcked(segment);
|
|
38
|
-
}
|
|
39
|
-
function isRemovedOrMoved(segment) {
|
|
40
|
-
return isRemoved(segment) || isMoved(segment);
|
|
31
|
+
return removalInfo !== undefined && opstampUtils.isAcked(removalInfo.removes[0]);
|
|
41
32
|
}
|
|
42
33
|
function nodeTotalLength(mergeTree, node) {
|
|
43
34
|
if (!node.isLeaf()) {
|
|
44
35
|
return node.cachedLength;
|
|
45
36
|
}
|
|
46
|
-
return mergeTree.
|
|
37
|
+
return mergeTree.leafLength(node);
|
|
47
38
|
}
|
|
48
39
|
const LRUSegmentComparer = {
|
|
49
40
|
min: { maxSeq: -2 },
|
|
50
41
|
compare: (a, b) => a.maxSeq - b.maxSeq,
|
|
51
42
|
};
|
|
52
|
-
function ackSegment(segment, segmentGroup, opArgs) {
|
|
43
|
+
function ackSegment(segment, segmentGroup, opArgs, stamp) {
|
|
53
44
|
const currentSegmentGroup = segment.segmentGroups?.dequeue();
|
|
54
45
|
assert(currentSegmentGroup === segmentGroup, 0x043 /* "On ack, unexpected segmentGroup!" */);
|
|
55
46
|
assert(opArgs.sequencedMessage !== undefined, 0xa6e /* must have sequencedMessage */);
|
|
56
47
|
const { op, sequencedMessage: { sequenceNumber, minimumSequenceNumber }, } = opArgs;
|
|
48
|
+
let allowIncrementalPartialLengthsUpdate = true;
|
|
57
49
|
switch (op.type) {
|
|
58
50
|
case MergeTreeDeltaType.ANNOTATE: {
|
|
59
51
|
assert(!!segment.propertyManager, 0x044 /* "On annotate ack, missing segment property manager!" */);
|
|
60
52
|
segment.propertyManager.ack(sequenceNumber, minimumSequenceNumber, op);
|
|
61
|
-
|
|
53
|
+
break;
|
|
62
54
|
}
|
|
63
55
|
case MergeTreeDeltaType.INSERT: {
|
|
64
|
-
assert(segment.
|
|
65
|
-
segment.
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
assertRemoved(segment);
|
|
71
|
-
segment.localRemovedSeq = undefined;
|
|
72
|
-
if (segment.removedSeq === UnassignedSequenceNumber) {
|
|
73
|
-
segment.removedSeq = sequenceNumber;
|
|
74
|
-
return true;
|
|
75
|
-
}
|
|
76
|
-
return false;
|
|
56
|
+
assert(opstampUtils.isLocal(segment.insert), 0x045 /* "On insert, seq number already assigned!" */);
|
|
57
|
+
segment.insert = {
|
|
58
|
+
...stamp,
|
|
59
|
+
type: "insert",
|
|
60
|
+
};
|
|
61
|
+
break;
|
|
77
62
|
}
|
|
63
|
+
case MergeTreeDeltaType.REMOVE:
|
|
78
64
|
case MergeTreeDeltaType.OBLITERATE:
|
|
79
65
|
case MergeTreeDeltaType.OBLITERATE_SIDED: {
|
|
80
|
-
|
|
81
|
-
const
|
|
82
|
-
assert(
|
|
83
|
-
segment.
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
66
|
+
assertRemoved(segment);
|
|
67
|
+
const latestRemove = segment.removes[segment.removes.length - 1];
|
|
68
|
+
assert(opstampUtils.isLocal(latestRemove), 0xb5d /* Expected last remove to be unacked */);
|
|
69
|
+
assert(segment.removes.length === 1 ||
|
|
70
|
+
opstampUtils.isAcked(segment.removes[segment.removes.length - 2]), 0xb5e /* Expected prior remove to be acked */);
|
|
71
|
+
allowIncrementalPartialLengthsUpdate = segment.removes.length === 1;
|
|
72
|
+
const removeStamp = {
|
|
73
|
+
...stamp,
|
|
74
|
+
type: op.type === MergeTreeDeltaType.REMOVE ? "setRemove" : "sliceRemove",
|
|
75
|
+
};
|
|
76
|
+
segment.removes[segment.removes.length - 1] = removeStamp;
|
|
77
|
+
const { obliterateInfo } = segmentGroup;
|
|
78
|
+
const hasObliterateInfo = obliterateInfo !== undefined;
|
|
79
|
+
const isObliterate = op.type !== MergeTreeDeltaType.REMOVE;
|
|
80
|
+
assert(hasObliterateInfo === isObliterate, 0xa40 /* must have obliterate info */);
|
|
81
|
+
if (hasObliterateInfo) {
|
|
82
|
+
obliterateInfo.stamp = removeStamp;
|
|
90
83
|
}
|
|
91
|
-
|
|
84
|
+
break;
|
|
92
85
|
}
|
|
93
86
|
default: {
|
|
94
87
|
throw new Error(`${op.type} is in unrecognized operation type`);
|
|
95
88
|
}
|
|
96
89
|
}
|
|
90
|
+
return allowIncrementalPartialLengthsUpdate;
|
|
97
91
|
}
|
|
98
92
|
export function errorIfOptionNotTrue(options, option) {
|
|
99
93
|
if (options?.[option] !== true) {
|
|
@@ -124,9 +118,7 @@ export function findRootMergeBlock(segmentOrNode) {
|
|
|
124
118
|
* SlideOnRemove references is removed.
|
|
125
119
|
*/
|
|
126
120
|
function getSlideToSegment(segment, slidingPreference = SlidingPreference.FORWARD, cache, useNewSlidingBehavior = false) {
|
|
127
|
-
if (!segment ||
|
|
128
|
-
!isRemovedAndAckedOrMovedAndAcked(segment) ||
|
|
129
|
-
segment.endpointType !== undefined) {
|
|
121
|
+
if (!segment || !isRemovedAndAcked(segment) || segment.endpointType !== undefined) {
|
|
130
122
|
return [segment, undefined];
|
|
131
123
|
}
|
|
132
124
|
const cachedSegment = cache?.get(segment);
|
|
@@ -136,13 +128,12 @@ function getSlideToSegment(segment, slidingPreference = SlidingPreference.FORWAR
|
|
|
136
128
|
const result = {};
|
|
137
129
|
cache?.set(segment, result);
|
|
138
130
|
const goFurtherToFindSlideToSegment = (seg) => {
|
|
139
|
-
if (seg.
|
|
131
|
+
if (opstampUtils.isAcked(seg.insert) && !isRemovedAndAcked(seg)) {
|
|
140
132
|
result.seg = seg;
|
|
141
133
|
return false;
|
|
142
134
|
}
|
|
143
135
|
if (cache !== undefined &&
|
|
144
|
-
|
|
145
|
-
toMoveInfo(seg)?.movedSeq === toMoveInfo(segment)?.movedSeq)) {
|
|
136
|
+
toRemovalInfo(seg)?.removes[0].seq === toRemovalInfo(segment)?.removes[0].seq) {
|
|
146
137
|
cache.set(seg, result);
|
|
147
138
|
}
|
|
148
139
|
return true;
|
|
@@ -208,10 +199,10 @@ class Obliterates {
|
|
|
208
199
|
constructor(mergeTree) {
|
|
209
200
|
this.mergeTree = mergeTree;
|
|
210
201
|
/**
|
|
211
|
-
* Array containing the all
|
|
202
|
+
* Array containing the all obliterate operations within the
|
|
212
203
|
* collab window.
|
|
213
204
|
*
|
|
214
|
-
* The
|
|
205
|
+
* The obliterates are stored in sequence order which accelerates clean up in setMinSeq
|
|
215
206
|
*
|
|
216
207
|
* See https://github.com/microsoft/FluidFramework/blob/main/packages/dds/merge-tree/docs/Obliterate.md#remote-perspective
|
|
217
208
|
* for additional context
|
|
@@ -226,7 +217,7 @@ class Obliterates {
|
|
|
226
217
|
}
|
|
227
218
|
setMinSeq(minSeq) {
|
|
228
219
|
// eslint-disable-next-line @typescript-eslint/no-non-null-asserted-optional-chain
|
|
229
|
-
while (!this.seqOrdered.empty && this.seqOrdered.first?.data.seq <= minSeq) {
|
|
220
|
+
while (!this.seqOrdered.empty && this.seqOrdered.first?.data.stamp.seq <= minSeq) {
|
|
230
221
|
const ob = this.seqOrdered.shift();
|
|
231
222
|
this.startOrdered.remove(ob.data.start);
|
|
232
223
|
this.mergeTree.removeLocalReferencePosition(ob.data.start);
|
|
@@ -234,7 +225,7 @@ class Obliterates {
|
|
|
234
225
|
}
|
|
235
226
|
}
|
|
236
227
|
addOrUpdate(obliterateInfo) {
|
|
237
|
-
const { seq, start } = obliterateInfo;
|
|
228
|
+
const { stamp: { seq }, start, } = obliterateInfo;
|
|
238
229
|
if (seq !== UnassignedSequenceNumber) {
|
|
239
230
|
this.seqOrdered.push(obliterateInfo);
|
|
240
231
|
}
|
|
@@ -266,12 +257,14 @@ class Obliterates {
|
|
|
266
257
|
* @internal
|
|
267
258
|
*/
|
|
268
259
|
export class MergeTree {
|
|
260
|
+
get localPerspective() {
|
|
261
|
+
return this.collabWindow.localPerspective;
|
|
262
|
+
}
|
|
269
263
|
constructor(options) {
|
|
270
264
|
this.options = options;
|
|
271
265
|
this.collabWindow = new CollaborationWindow();
|
|
272
266
|
this.pendingSegments = new DoublyLinkedList();
|
|
273
267
|
this.segmentsToScour = new Heap(LRUSegmentComparer);
|
|
274
|
-
this.localPerspective = new LocalDefaultPerspective(this.collabWindow.clientId);
|
|
275
268
|
/**
|
|
276
269
|
* Whether or not all blocks in the mergeTree currently have information about local partial lengths computed.
|
|
277
270
|
* This information is only necessary on reconnect, and otherwise costly to bookkeep.
|
|
@@ -326,61 +319,20 @@ export class MergeTree {
|
|
|
326
319
|
return block;
|
|
327
320
|
}
|
|
328
321
|
/**
|
|
329
|
-
* Compute the net length of this segment from
|
|
330
|
-
* @
|
|
331
|
-
*
|
|
332
|
-
* default is to consider the local client's current perspective. Only local sequence
|
|
333
|
-
* numbers corresponding to un-acked operations give valid results.
|
|
322
|
+
* Compute the net length of this segment leaf from some perspective.
|
|
323
|
+
* @returns - Undefined if the segment has been removed and its removal is common knowledge to all collaborators (and therefore
|
|
324
|
+
* may not even be present on clients that have loaded from a summary beyond this point). Otherwise, the length of the segment.
|
|
334
325
|
*/
|
|
335
|
-
|
|
326
|
+
leafLength(segment, perspective = this.localPerspective) {
|
|
336
327
|
const removalInfo = toRemovalInfo(segment);
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
}
|
|
344
|
-
// this segment removed and outside the collab window which means it is zamboni eligible
|
|
345
|
-
// this also means the segment could not exist, so we should not consider it
|
|
346
|
-
// when making decisions about conflict resolutions
|
|
347
|
-
return undefined;
|
|
348
|
-
}
|
|
349
|
-
else {
|
|
350
|
-
return segment.cachedLength;
|
|
351
|
-
}
|
|
352
|
-
}
|
|
353
|
-
assert(refSeq !== undefined, 0x398 /* localSeq provided for local length without refSeq */);
|
|
354
|
-
assert(segment.seq !== undefined, 0x399 /* segment with no seq in mergeTree */);
|
|
355
|
-
const { seq } = segment;
|
|
356
|
-
const { removedSeq, localRemovedSeq } = removalInfo ?? {};
|
|
357
|
-
const { movedSeq, localMovedSeq } = moveInfo ?? {};
|
|
358
|
-
if (seq === UnassignedSequenceNumber) {
|
|
359
|
-
assert(segment.localSeq !== undefined, 0x39a /* unacked segment with undefined localSeq */);
|
|
360
|
-
// inserted locally, still un-acked
|
|
361
|
-
if (segment.localSeq > localSeq ||
|
|
362
|
-
(localRemovedSeq !== undefined && localRemovedSeq <= localSeq) ||
|
|
363
|
-
(localMovedSeq !== undefined && localMovedSeq <= localSeq)) {
|
|
364
|
-
return 0;
|
|
365
|
-
}
|
|
366
|
-
const { cachedLength } = segment;
|
|
367
|
-
return cachedLength;
|
|
368
|
-
}
|
|
369
|
-
else {
|
|
370
|
-
// inserted remotely
|
|
371
|
-
if (seq > refSeq ||
|
|
372
|
-
(removedSeq !== undefined &&
|
|
373
|
-
removedSeq !== UnassignedSequenceNumber &&
|
|
374
|
-
removedSeq <= refSeq) ||
|
|
375
|
-
(movedSeq !== undefined &&
|
|
376
|
-
movedSeq !== UnassignedSequenceNumber &&
|
|
377
|
-
movedSeq <= refSeq) ||
|
|
378
|
-
(localRemovedSeq !== undefined && localRemovedSeq <= localSeq) ||
|
|
379
|
-
(localMovedSeq !== undefined && localMovedSeq <= localSeq)) {
|
|
380
|
-
return 0;
|
|
381
|
-
}
|
|
382
|
-
return segment.cachedLength;
|
|
328
|
+
if (removalInfo &&
|
|
329
|
+
getMinSeqPerspective(this.collabWindow).hasOccurred(removalInfo.removes[0])) {
|
|
330
|
+
// this segment's removal has already moved outside the collab window which means it is zamboni eligible
|
|
331
|
+
// this also means the segment could be completely absent from other client's in-memory merge trees,
|
|
332
|
+
// so we should not consider it when making decisions about conflict resolutions
|
|
333
|
+
return undefined;
|
|
383
334
|
}
|
|
335
|
+
return perspective.isSegmentPresent(segment) ? segment.cachedLength : 0;
|
|
384
336
|
}
|
|
385
337
|
unlinkMarker(marker) {
|
|
386
338
|
const id = marker.getId();
|
|
@@ -438,6 +390,7 @@ export class MergeTree {
|
|
|
438
390
|
this.collabWindow.minSeq = minSeq;
|
|
439
391
|
this.collabWindow.collaborating = true;
|
|
440
392
|
this.collabWindow.currentSeq = currentSeq;
|
|
393
|
+
this.collabWindow.localPerspective = new LocalDefaultPerspective(localClientId);
|
|
441
394
|
this.nodeUpdateLengthNewStructure(this.root, true);
|
|
442
395
|
}
|
|
443
396
|
addToLRUSet(leaf, seq) {
|
|
@@ -450,8 +403,8 @@ export class MergeTree {
|
|
|
450
403
|
this.segmentsToScour.add({ segment: leaf, maxSeq: seq });
|
|
451
404
|
}
|
|
452
405
|
}
|
|
453
|
-
getLength(
|
|
454
|
-
return this.
|
|
406
|
+
getLength(perspective) {
|
|
407
|
+
return this.nodeLength(this.root, perspective) ?? 0;
|
|
455
408
|
}
|
|
456
409
|
/**
|
|
457
410
|
* Returns the current length of the MergeTree for the local client.
|
|
@@ -459,7 +412,7 @@ export class MergeTree {
|
|
|
459
412
|
get length() {
|
|
460
413
|
return this.root.cachedLength;
|
|
461
414
|
}
|
|
462
|
-
getPosition(node,
|
|
415
|
+
getPosition(node, perspective) {
|
|
463
416
|
if (node.isLeaf() && node.endpointType === "start") {
|
|
464
417
|
return 0;
|
|
465
418
|
}
|
|
@@ -473,15 +426,16 @@ export class MergeTree {
|
|
|
473
426
|
if ((!!prevParent && child === prevParent) || child === node) {
|
|
474
427
|
break;
|
|
475
428
|
}
|
|
476
|
-
totalOffset += this.nodeLength(child,
|
|
429
|
+
totalOffset += this.nodeLength(child, perspective) ?? 0;
|
|
477
430
|
}
|
|
478
431
|
prevParent = parent;
|
|
479
432
|
parent = parent.parent;
|
|
480
433
|
}
|
|
481
434
|
return totalOffset;
|
|
482
435
|
}
|
|
483
|
-
getContainingSegment(pos,
|
|
484
|
-
assert(localSeq === undefined ||
|
|
436
|
+
getContainingSegment(pos, perspective) {
|
|
437
|
+
assert(perspective.localSeq === undefined ||
|
|
438
|
+
perspective.clientId === this.collabWindow.clientId, 0x39b /* localSeq provided for non-local client */);
|
|
485
439
|
let segment;
|
|
486
440
|
let offset;
|
|
487
441
|
const leaf = (leafSeg, _, start) => {
|
|
@@ -489,7 +443,7 @@ export class MergeTree {
|
|
|
489
443
|
offset = start;
|
|
490
444
|
return false;
|
|
491
445
|
};
|
|
492
|
-
this.nodeMap(
|
|
446
|
+
this.nodeMap(perspective, leaf, undefined, pos, pos + 1);
|
|
493
447
|
return { segment, offset };
|
|
494
448
|
}
|
|
495
449
|
/**
|
|
@@ -582,7 +536,7 @@ export class MergeTree {
|
|
|
582
536
|
const forwardSegmentCache = new Map();
|
|
583
537
|
const backwardSegmentCache = new Map();
|
|
584
538
|
for (const segment of segments) {
|
|
585
|
-
assert(
|
|
539
|
+
assert(isRemovedAndAcked(segment), 0x2f1 /* slideReferences from a segment which has not been removed and acked */);
|
|
586
540
|
if (segment.localRefs === undefined || segment.localRefs.empty) {
|
|
587
541
|
continue;
|
|
588
542
|
}
|
|
@@ -602,11 +556,6 @@ export class MergeTree {
|
|
|
602
556
|
slideGroup(currentForwardSlideDestination, currentForwardSlideIsForward, currentForwardSlideGroup, forwardPred, currentForwardMaybeEndpoint);
|
|
603
557
|
slideGroup(currentBackwardSlideDestination, currentBackwardSlideIsForward, currentBackwardSlideGroup, backwardPred, currentBackwardMaybeEndpoint);
|
|
604
558
|
}
|
|
605
|
-
blockLength(node, refSeq, clientId) {
|
|
606
|
-
return this.collabWindow.collaborating && clientId !== this.collabWindow.clientId
|
|
607
|
-
? node.partialLengths.getPartialLength(refSeq, clientId)
|
|
608
|
-
: (node.cachedLength ?? 0);
|
|
609
|
-
}
|
|
610
559
|
/**
|
|
611
560
|
* Compute local partial length information
|
|
612
561
|
*
|
|
@@ -624,62 +573,27 @@ export class MergeTree {
|
|
|
624
573
|
this.root.partialLengths = PartialSequenceLengths.combine(this.root, rebaseCollabWindow, true, true);
|
|
625
574
|
this.localPartialsComputed = true;
|
|
626
575
|
}
|
|
627
|
-
nodeLength(node,
|
|
628
|
-
if (
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
}
|
|
649
|
-
else {
|
|
650
|
-
// Sequence number within window
|
|
651
|
-
if (node.isLeaf()) {
|
|
652
|
-
const segment = node;
|
|
653
|
-
const removalInfo = toRemovalInfo(segment);
|
|
654
|
-
const moveInfo = toMoveInfo(segment);
|
|
655
|
-
if (removalInfo !== undefined) {
|
|
656
|
-
if (seqLTE(removalInfo.removedSeq, this.collabWindow.minSeq)) {
|
|
657
|
-
return undefined;
|
|
658
|
-
}
|
|
659
|
-
if (seqLTE(removalInfo.removedSeq, refSeq) ||
|
|
660
|
-
removalInfo.removedClientIds.includes(clientId)) {
|
|
661
|
-
return 0;
|
|
662
|
-
}
|
|
663
|
-
}
|
|
664
|
-
if (moveInfo !== undefined) {
|
|
665
|
-
if (seqLTE(moveInfo.movedSeq, this.collabWindow.minSeq)) {
|
|
666
|
-
return undefined;
|
|
667
|
-
}
|
|
668
|
-
if (seqLTE(moveInfo.movedSeq, refSeq) ||
|
|
669
|
-
moveInfo.movedClientIds.includes(clientId)) {
|
|
670
|
-
return 0;
|
|
671
|
-
}
|
|
672
|
-
}
|
|
673
|
-
return seqLTE(node.seq ?? 0, refSeq) || segment.clientId === clientId
|
|
674
|
-
? segment.cachedLength
|
|
675
|
-
: 0;
|
|
676
|
-
}
|
|
677
|
-
else {
|
|
678
|
-
const partialLen = node.partialLengths.getPartialLength(refSeq, clientId);
|
|
679
|
-
PartialSequenceLengths.options.verifyExpected?.(this, node, refSeq, clientId);
|
|
680
|
-
return partialLen;
|
|
681
|
-
}
|
|
682
|
-
}
|
|
576
|
+
nodeLength(node, perspective) {
|
|
577
|
+
if (node.isLeaf()) {
|
|
578
|
+
return this.leafLength(node, perspective);
|
|
579
|
+
}
|
|
580
|
+
const { refSeq, clientId, localSeq } = perspective;
|
|
581
|
+
const isLocalPerspective = !this.collabWindow.collaborating || this.collabWindow.clientId === clientId;
|
|
582
|
+
if (isLocalPerspective &&
|
|
583
|
+
(localSeq === undefined ||
|
|
584
|
+
(localSeq === this.collabWindow.localSeq && refSeq >= this.collabWindow.currentSeq))) {
|
|
585
|
+
// All changes are visible. Small note on why we allow refSeq >= this.collabWindow.currentSeq rather than just equality:
|
|
586
|
+
// merge-tree eventing occurs before the collab window is updated to account for whatever op it is processing, and we want
|
|
587
|
+
// to support resolving positions from within the event handler which account for that op. e.g. undo-redo relies on this
|
|
588
|
+
// behavior with local references.
|
|
589
|
+
return node.cachedLength;
|
|
590
|
+
}
|
|
591
|
+
if (localSeq !== undefined) {
|
|
592
|
+
this.computeLocalPartials(refSeq);
|
|
593
|
+
}
|
|
594
|
+
const length = node.partialLengths.getPartialLength(refSeq, clientId, localSeq);
|
|
595
|
+
PartialSequenceLengths.options.verifyExpected?.(this, node, refSeq, clientId, localSeq);
|
|
596
|
+
return length;
|
|
683
597
|
}
|
|
684
598
|
setMinSeq(minSeq) {
|
|
685
599
|
assert(minSeq <= this.collabWindow.currentSeq, 0x04e /* "Trying to set minSeq above currentSeq of collab window!" */);
|
|
@@ -705,44 +619,61 @@ export class MergeTree {
|
|
|
705
619
|
referencePositionToLocalPosition(refPos,
|
|
706
620
|
// Note: this is not `this.collabWindow.currentSeq` because we want to support resolving local reference positions to positions
|
|
707
621
|
// from within event handlers, and the collab window's sequence numbers are not updated in time in all of those cases.
|
|
708
|
-
refSeq = Number.MAX_SAFE_INTEGER, clientId = this.collabWindow.clientId, localSeq =
|
|
622
|
+
refSeq = Number.MAX_SAFE_INTEGER, clientId = this.collabWindow.clientId, localSeq = undefined) {
|
|
709
623
|
const perspective = clientId === this.collabWindow.clientId
|
|
710
624
|
? localSeq === undefined
|
|
711
625
|
? this.localPerspective
|
|
712
626
|
: new LocalReconnectingPerspective(refSeq, clientId, localSeq)
|
|
713
627
|
: new PriorPerspective(refSeq, clientId);
|
|
714
628
|
const seg = refPos.getSegment();
|
|
715
|
-
if (!isSegmentLeaf(seg)) {
|
|
629
|
+
if (seg === undefined || !isSegmentLeaf(seg)) {
|
|
716
630
|
// We have no idea where this reference is, because it refers to a segment which is not in the tree.
|
|
717
631
|
return DetachedReferencePosition;
|
|
718
632
|
}
|
|
719
633
|
if (refPos.isLeaf()) {
|
|
720
|
-
return this.getPosition(seg,
|
|
634
|
+
return this.getPosition(seg, perspective);
|
|
721
635
|
}
|
|
722
636
|
if (refTypeIncludesFlag(refPos, ReferenceType.Transient) || seg.localRefs?.has(refPos)) {
|
|
723
637
|
if (seg !== this.startOfTree &&
|
|
724
638
|
seg !== this.endOfTree &&
|
|
725
639
|
!perspective.isSegmentPresent(seg)) {
|
|
726
640
|
const forward = refPos.slidingPreference === SlidingPreference.FORWARD;
|
|
727
|
-
const moveInfo = toMoveInfo(seg);
|
|
728
641
|
const removeInfo = toRemovalInfo(seg);
|
|
729
|
-
const
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
const slideLocalSeq = moveInfo?.localMovedSeq ?? removeInfo?.localRemovedSeq;
|
|
735
|
-
const slidePerspective = slideLocalSeq === undefined
|
|
642
|
+
const firstRemove = removeInfo?.removes[0];
|
|
643
|
+
const slideSeq = firstRemove !== undefined && opstampUtils.isAcked(firstRemove)
|
|
644
|
+
? firstRemove.seq
|
|
645
|
+
: refSeq;
|
|
646
|
+
const slidePerspective = firstRemove?.localSeq === undefined
|
|
736
647
|
? new PriorPerspective(slideSeq, this.collabWindow.clientId)
|
|
737
|
-
: new LocalReconnectingPerspective(slideSeq, this.collabWindow.clientId,
|
|
738
|
-
const slidSegment =
|
|
739
|
-
return (this.getPosition(slidSegment,
|
|
648
|
+
: new LocalReconnectingPerspective(slideSeq, this.collabWindow.clientId, firstRemove.localSeq);
|
|
649
|
+
const slidSegment = this.nextSegment(slidePerspective, seg, forward);
|
|
650
|
+
return (this.getPosition(slidSegment, perspective) +
|
|
740
651
|
(forward ? 0 : slidSegment.cachedLength === 0 ? 0 : slidSegment.cachedLength - 1));
|
|
741
652
|
}
|
|
742
|
-
return this.getPosition(seg,
|
|
653
|
+
return this.getPosition(seg, perspective) + refPos.getOffset();
|
|
743
654
|
}
|
|
744
655
|
return DetachedReferencePosition;
|
|
745
656
|
}
|
|
657
|
+
/**
|
|
658
|
+
* Returns the immediately adjacent segment in the specified direction from this perspective.
|
|
659
|
+
* There may actually be multiple segments between the given segment and the returned segment,
|
|
660
|
+
* but they were either inserted after this perspective, or have been removed before this perspective.
|
|
661
|
+
*
|
|
662
|
+
* @param segment - The segment to start from.
|
|
663
|
+
* @param forward - The direction to search.
|
|
664
|
+
* @returns the next segment in the specified direction, or the start or end of the tree if there is no next segment.
|
|
665
|
+
*/
|
|
666
|
+
nextSegment(perspective, segment, forward = true) {
|
|
667
|
+
let next;
|
|
668
|
+
const action = (seg) => {
|
|
669
|
+
if (perspective.isSegmentPresent(seg)) {
|
|
670
|
+
next = seg;
|
|
671
|
+
return LeafAction.Exit;
|
|
672
|
+
}
|
|
673
|
+
};
|
|
674
|
+
(forward ? forwardExcursion : backwardExcursion)(segment, action);
|
|
675
|
+
return next ?? (forward ? this.endOfTree : this.startOfTree);
|
|
676
|
+
}
|
|
746
677
|
/**
|
|
747
678
|
* Finds the nearest reference with ReferenceType.Tile to `startPos` in the direction dictated by `forwards`.
|
|
748
679
|
* Uses depthFirstNodeWalk in addition to block-accelerated functionality. The search position will be included in
|
|
@@ -755,9 +686,9 @@ export class MergeTree {
|
|
|
755
686
|
* @param markerLabel - Label of the marker to search for
|
|
756
687
|
* @param forwards - Whether the string should be searched in the forward or backward direction
|
|
757
688
|
*/
|
|
758
|
-
searchForMarker(startPos,
|
|
689
|
+
searchForMarker(startPos, markerLabel, forwards = true) {
|
|
759
690
|
let foundMarker;
|
|
760
|
-
const { segment } = this.getContainingSegment(startPos,
|
|
691
|
+
const { segment } = this.getContainingSegment(startPos, this.localPerspective);
|
|
761
692
|
if (!isSegmentLeaf(segment)) {
|
|
762
693
|
return undefined;
|
|
763
694
|
}
|
|
@@ -781,14 +712,12 @@ export class MergeTree {
|
|
|
781
712
|
return foundMarker;
|
|
782
713
|
}
|
|
783
714
|
updateRoot(splitNode) {
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
this.nodeUpdateLengthNewStructure(this.root);
|
|
791
|
-
}
|
|
715
|
+
const newRoot = this.makeBlock(2);
|
|
716
|
+
assignChild(newRoot, this.root, 0, false);
|
|
717
|
+
assignChild(newRoot, splitNode, 1, false);
|
|
718
|
+
this.root = newRoot;
|
|
719
|
+
this.nodeUpdateOrdinals(this.root);
|
|
720
|
+
this.nodeUpdateLengthNewStructure(this.root);
|
|
792
721
|
}
|
|
793
722
|
/**
|
|
794
723
|
* Assign sequence number to existing segment; update partial lengths to reflect the change
|
|
@@ -796,6 +725,10 @@ export class MergeTree {
|
|
|
796
725
|
*/
|
|
797
726
|
ackPendingSegment(opArgs) {
|
|
798
727
|
const seq = opArgs.sequencedMessage.sequenceNumber;
|
|
728
|
+
const stamp = {
|
|
729
|
+
seq,
|
|
730
|
+
clientId: this.collabWindow.clientId,
|
|
731
|
+
};
|
|
799
732
|
const pendingSegmentGroup = this.pendingSegments.shift()?.data;
|
|
800
733
|
const nodesToUpdate = [];
|
|
801
734
|
let overwrite = false;
|
|
@@ -803,8 +736,8 @@ export class MergeTree {
|
|
|
803
736
|
const deltaSegments = [];
|
|
804
737
|
const overlappingRemoves = [];
|
|
805
738
|
pendingSegmentGroup.segments.map((pendingSegment) => {
|
|
806
|
-
const overlappingRemove = !ackSegment(pendingSegment, pendingSegmentGroup, opArgs);
|
|
807
|
-
overwrite ||= overlappingRemove
|
|
739
|
+
const overlappingRemove = !ackSegment(pendingSegment, pendingSegmentGroup, opArgs, stamp);
|
|
740
|
+
overwrite ||= overlappingRemove;
|
|
808
741
|
overlappingRemoves.push(overlappingRemove);
|
|
809
742
|
if (MergeTree.options.zamboniSegments) {
|
|
810
743
|
this.addToLRUSet(pendingSegment, seq);
|
|
@@ -817,7 +750,7 @@ export class MergeTree {
|
|
|
817
750
|
});
|
|
818
751
|
});
|
|
819
752
|
if (pendingSegmentGroup.obliterateInfo !== undefined) {
|
|
820
|
-
pendingSegmentGroup.obliterateInfo.
|
|
753
|
+
pendingSegmentGroup.obliterateInfo.stamp = { type: "sliceRemove", ...stamp };
|
|
821
754
|
this.obliterates.addOrUpdate(pendingSegmentGroup.obliterateInfo);
|
|
822
755
|
}
|
|
823
756
|
// Perform slides after all segments have been acked, so that
|
|
@@ -831,9 +764,8 @@ export class MergeTree {
|
|
|
831
764
|
deltaSegments,
|
|
832
765
|
operation: MergeTreeMaintenanceType.ACKNOWLEDGED,
|
|
833
766
|
}, opArgs);
|
|
834
|
-
const clientId = this.collabWindow.clientId;
|
|
835
767
|
for (const node of nodesToUpdate) {
|
|
836
|
-
this.blockUpdatePathLengths(node,
|
|
768
|
+
this.blockUpdatePathLengths(node, stamp, overwrite);
|
|
837
769
|
}
|
|
838
770
|
}
|
|
839
771
|
if (MergeTree.options.zamboniSegments) {
|
|
@@ -867,11 +799,7 @@ export class MergeTree {
|
|
|
867
799
|
// TODO: error checking
|
|
868
800
|
getMarkerFromId(id) {
|
|
869
801
|
const marker = this.idToMarker.get(id);
|
|
870
|
-
return marker === undefined ||
|
|
871
|
-
isRemoved(marker) ||
|
|
872
|
-
(isMoved(marker) && marker.moveDst === undefined)
|
|
873
|
-
? undefined
|
|
874
|
-
: marker;
|
|
802
|
+
return marker === undefined || isRemoved(marker) ? undefined : marker;
|
|
875
803
|
}
|
|
876
804
|
/**
|
|
877
805
|
* Given a position specified relative to a marker id, lookup the marker
|
|
@@ -880,14 +808,14 @@ export class MergeTree {
|
|
|
880
808
|
* @param refseq - The reference sequence number at which to compute the position.
|
|
881
809
|
* @param clientId - The client id with which to compute the position.
|
|
882
810
|
*/
|
|
883
|
-
posFromRelativePos(relativePos,
|
|
811
|
+
posFromRelativePos(relativePos, perspective) {
|
|
884
812
|
let pos = -1;
|
|
885
813
|
let marker;
|
|
886
814
|
if (relativePos.id) {
|
|
887
815
|
marker = this.getMarkerFromId(relativePos.id);
|
|
888
816
|
}
|
|
889
817
|
if (isSegmentLeaf(marker)) {
|
|
890
|
-
pos = this.getPosition(marker,
|
|
818
|
+
pos = this.getPosition(marker, perspective);
|
|
891
819
|
if (relativePos.before) {
|
|
892
820
|
if (relativePos.offset !== undefined) {
|
|
893
821
|
pos -= relativePos.offset;
|
|
@@ -902,14 +830,14 @@ export class MergeTree {
|
|
|
902
830
|
}
|
|
903
831
|
return pos;
|
|
904
832
|
}
|
|
905
|
-
insertSegments(pos, segments,
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
this.blockInsert(pos,
|
|
833
|
+
insertSegments(pos, segments, perspective, stampArg, opArgs) {
|
|
834
|
+
const stamp = { ...stampArg, type: "insert" };
|
|
835
|
+
this.ensureIntervalBoundary(pos, perspective);
|
|
836
|
+
this.blockInsert(pos, perspective, stamp, segments);
|
|
909
837
|
// opArgs == undefined => loading snapshot or test code
|
|
910
838
|
if (opArgs !== undefined) {
|
|
911
839
|
const deltaSegments = segments
|
|
912
|
-
.filter((segment) => !
|
|
840
|
+
.filter((segment) => !isRemoved(segment))
|
|
913
841
|
.map((segment) => ({ segment }));
|
|
914
842
|
if (deltaSegments.length > 0) {
|
|
915
843
|
this.mergeTreeDeltaCallback?.(opArgs, {
|
|
@@ -920,7 +848,7 @@ export class MergeTree {
|
|
|
920
848
|
}
|
|
921
849
|
if (this.collabWindow.collaborating &&
|
|
922
850
|
MergeTree.options.zamboniSegments &&
|
|
923
|
-
|
|
851
|
+
opstampUtils.isAcked(stamp)) {
|
|
924
852
|
zamboniSegments(this);
|
|
925
853
|
}
|
|
926
854
|
}
|
|
@@ -942,19 +870,19 @@ export class MergeTree {
|
|
|
942
870
|
if (remoteClientRefSeq < this.collabWindow.minSeq) {
|
|
943
871
|
return undefined;
|
|
944
872
|
}
|
|
945
|
-
const
|
|
946
|
-
const
|
|
873
|
+
const remotePerspective = new PriorPerspective(remoteClientRefSeq, remoteClientId);
|
|
874
|
+
const segmentInfo = this.getContainingSegment(remoteClientPosition, remotePerspective);
|
|
947
875
|
if (isSegmentLeaf(segmentInfo?.segment)) {
|
|
948
|
-
const segmentPosition = this.getPosition(segmentInfo.segment,
|
|
876
|
+
const segmentPosition = this.getPosition(segmentInfo.segment, this.localPerspective);
|
|
949
877
|
return segmentPosition + segmentInfo.offset;
|
|
950
878
|
}
|
|
951
879
|
else {
|
|
952
|
-
if (remoteClientPosition === this.getLength(
|
|
953
|
-
return this.getLength(
|
|
880
|
+
if (remoteClientPosition === this.getLength(remotePerspective)) {
|
|
881
|
+
return this.getLength(this.localPerspective);
|
|
954
882
|
}
|
|
955
883
|
}
|
|
956
884
|
}
|
|
957
|
-
blockInsert(pos,
|
|
885
|
+
blockInsert(pos, perspective, stamp, newSegments) {
|
|
958
886
|
// Keeping this function within the scope of blockInsert for readability.
|
|
959
887
|
// eslint-disable-next-line unicorn/consistent-function-scoping
|
|
960
888
|
const continueFrom = (node) => {
|
|
@@ -969,16 +897,16 @@ export class MergeTree {
|
|
|
969
897
|
const saveIfLocal = (locSegment) => {
|
|
970
898
|
// Save segment so we can assign sequence number when acked by server
|
|
971
899
|
if (this.collabWindow.collaborating) {
|
|
972
|
-
if (locSegment.
|
|
973
|
-
clientId === this.collabWindow.clientId) {
|
|
974
|
-
segmentGroup = this.addToPendingList(locSegment, segmentGroup, localSeq);
|
|
900
|
+
if (opstampUtils.isLocal(locSegment.insert) &&
|
|
901
|
+
stamp.clientId === this.collabWindow.clientId) {
|
|
902
|
+
segmentGroup = this.addToPendingList(locSegment, segmentGroup, stamp.localSeq);
|
|
975
903
|
}
|
|
976
904
|
// LocSegment.seq === 0 when coming from SharedSegmentSequence.loadBody()
|
|
977
905
|
// In all other cases this has to be true (checked by addToLRUSet):
|
|
978
906
|
// locSegment.seq > this.collabWindow.currentSeq
|
|
979
|
-
else if (
|
|
980
|
-
|
|
981
|
-
this.addToLRUSet(locSegment, locSegment.seq);
|
|
907
|
+
else if (MergeTree.options.zamboniSegments &&
|
|
908
|
+
opstampUtils.greaterThan(locSegment.insert, getMinSeqStamp(this.collabWindow))) {
|
|
909
|
+
this.addToLRUSet(locSegment, locSegment.insert.seq);
|
|
982
910
|
}
|
|
983
911
|
}
|
|
984
912
|
};
|
|
@@ -994,85 +922,71 @@ export class MergeTree {
|
|
|
994
922
|
}
|
|
995
923
|
return segmentChanges;
|
|
996
924
|
};
|
|
997
|
-
const insertInfo = {
|
|
998
|
-
clientId,
|
|
999
|
-
seq,
|
|
1000
|
-
localSeq,
|
|
1001
|
-
};
|
|
1002
925
|
// TODO: build tree from segs and insert all at once
|
|
1003
926
|
let insertPos = pos;
|
|
1004
927
|
for (const newSegment of newSegments
|
|
1005
928
|
.filter((s) => s.cachedLength > 0)
|
|
1006
|
-
.map((s) => overwriteInfo(s,
|
|
929
|
+
.map((s) => overwriteInfo(s, { insert: stamp }))) {
|
|
1007
930
|
if (Marker.is(newSegment)) {
|
|
1008
931
|
const markerId = newSegment.getId();
|
|
1009
932
|
if (markerId) {
|
|
1010
933
|
this.idToMarker.set(markerId, newSegment);
|
|
1011
934
|
}
|
|
1012
935
|
}
|
|
1013
|
-
|
|
936
|
+
this.insertingWalk(insertPos, perspective, stamp, {
|
|
1014
937
|
leaf: onLeaf,
|
|
1015
938
|
candidateSegment: newSegment,
|
|
1016
939
|
continuePredicate: continueFrom,
|
|
1017
940
|
});
|
|
1018
941
|
if (!isSegmentLeaf(newSegment)) {
|
|
1019
942
|
// Indicates an attempt to insert past the end of the merge-tree's content.
|
|
1020
|
-
const errorConstructor = localSeq === undefined ? DataProcessingError : UsageError;
|
|
943
|
+
const errorConstructor = stamp.localSeq === undefined ? DataProcessingError : UsageError;
|
|
1021
944
|
throw new errorConstructor("MergeTree insert failed", {
|
|
1022
945
|
currentSeq: this.collabWindow.currentSeq,
|
|
1023
946
|
minSeq: this.collabWindow.minSeq,
|
|
1024
|
-
segSeq:
|
|
947
|
+
segSeq: stamp.seq,
|
|
1025
948
|
});
|
|
1026
949
|
}
|
|
1027
|
-
this.updateRoot(splitNode);
|
|
1028
950
|
insertPos += newSegment.cachedLength;
|
|
1029
951
|
if (!this.options?.mergeTreeEnableObliterate || this.obliterates.empty()) {
|
|
1030
952
|
saveIfLocal(newSegment);
|
|
1031
953
|
continue;
|
|
1032
954
|
}
|
|
955
|
+
const overlappingAckedObliterates = [];
|
|
1033
956
|
let oldest;
|
|
1034
|
-
let normalizedOldestSeq = 0;
|
|
1035
957
|
let newest;
|
|
1036
|
-
let normalizedNewestSeq = 0;
|
|
1037
|
-
const movedClientIds = [];
|
|
1038
|
-
const movedSeqs = [];
|
|
1039
958
|
let newestAcked;
|
|
1040
959
|
let oldestUnacked;
|
|
960
|
+
const refSeqStamp = {
|
|
961
|
+
seq: perspective.refSeq,
|
|
962
|
+
clientId: stamp.clientId,
|
|
963
|
+
localSeq: stamp.localSeq,
|
|
964
|
+
};
|
|
1041
965
|
for (const ob of this.obliterates.findOverlapping(newSegment)) {
|
|
1042
|
-
|
|
1043
|
-
// but is still comparable to remote seqs to keep the checks below easy
|
|
1044
|
-
// REMOTE SEQUENCE NUMBERS LOCAL SEQUENCE NUMBERS
|
|
1045
|
-
// [0, 1, 2, 3, ..., 100, ..., 1000, ..., (MAX - MaxLocalSeq), L1, L2, L3, L4, ..., L100, ..., L1000, ...(MAX)]
|
|
1046
|
-
const normalizedObSeq = ob.seq === UnassignedSequenceNumber
|
|
1047
|
-
? Number.MAX_SAFE_INTEGER - this.collabWindow.localSeq + ob.localSeq
|
|
1048
|
-
: ob.seq;
|
|
1049
|
-
if (normalizedObSeq > refSeq) {
|
|
966
|
+
if (opstampUtils.greaterThan(ob.stamp, refSeqStamp)) {
|
|
1050
967
|
// Any obliterate from the same client that's inserting this segment cannot cause the segment to be marked as
|
|
1051
968
|
// obliterated (since that client must have performed the obliterate before this insertion).
|
|
1052
969
|
// We still need to consider such obliterates when determining the winning obliterate for the insertion point,
|
|
1053
970
|
// see `obliteratePrecedingInsertion` docs.
|
|
1054
|
-
if (clientId !== ob.clientId) {
|
|
1055
|
-
if (
|
|
1056
|
-
|
|
1057
|
-
oldest = ob;
|
|
1058
|
-
movedClientIds.unshift(ob.clientId);
|
|
1059
|
-
movedSeqs.unshift(ob.seq);
|
|
971
|
+
if (stamp.clientId !== ob.stamp.clientId) {
|
|
972
|
+
if (opstampUtils.isAcked(ob.stamp)) {
|
|
973
|
+
overlappingAckedObliterates.push(ob.stamp);
|
|
1060
974
|
}
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
movedSeqs.push(ob.seq);
|
|
975
|
+
if (oldest === undefined || opstampUtils.lessThan(ob.stamp, oldest.stamp)) {
|
|
976
|
+
oldest = ob;
|
|
1064
977
|
}
|
|
1065
978
|
}
|
|
1066
|
-
if (newest === undefined ||
|
|
1067
|
-
normalizedNewestSeq = normalizedObSeq;
|
|
979
|
+
if (newest === undefined || opstampUtils.greaterThan(ob.stamp, newest.stamp)) {
|
|
1068
980
|
newest = ob;
|
|
1069
981
|
}
|
|
1070
|
-
if (ob.
|
|
1071
|
-
(newestAcked === undefined ||
|
|
982
|
+
if (opstampUtils.isAcked(ob.stamp) &&
|
|
983
|
+
(newestAcked === undefined ||
|
|
984
|
+
opstampUtils.greaterThan(ob.stamp, newestAcked.stamp))) {
|
|
1072
985
|
newestAcked = ob;
|
|
1073
986
|
}
|
|
1074
|
-
if (ob.
|
|
1075
|
-
(oldestUnacked === undefined ||
|
|
987
|
+
if (opstampUtils.isLocal(ob.stamp) &&
|
|
988
|
+
(oldestUnacked === undefined ||
|
|
989
|
+
opstampUtils.greaterThan(oldestUnacked.stamp, ob.stamp))) {
|
|
1076
990
|
// There can be one local obliterate surrounding a segment if a client repeatedly obliterates
|
|
1077
991
|
// a region (ex: in the text ABCDEFG, obliterate D, then obliterate CE, then BF). In this case,
|
|
1078
992
|
// the first one that's applied will be the one that actually removes the segment.
|
|
@@ -1084,32 +998,20 @@ export class MergeTree {
|
|
|
1084
998
|
// See doc comment on obliteratePrecedingInsertion for more details: if the newest obliterate was performed
|
|
1085
999
|
// by the same client that's inserting this segment, we let them insert into this range and therefore don't
|
|
1086
1000
|
// mark it obliterated.
|
|
1087
|
-
if (oldest && newest?.clientId !== clientId) {
|
|
1088
|
-
|
|
1089
|
-
if (newestAcked === newest || newestAcked?.clientId !== clientId) {
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
localMovedSeq: oldestUnacked?.localSeq,
|
|
1095
|
-
};
|
|
1096
|
-
}
|
|
1097
|
-
else {
|
|
1098
|
-
assert(oldestUnacked !== undefined, 0xb55 /* Expected local obliterate to be defined if newestAcked is not equal to newest */);
|
|
1099
|
-
// There's a pending local obliterate for this range, so it will be marked as obliterated by us. However,
|
|
1100
|
-
// all other clients are under the impression that the most recent acked obliterate won the right to insert
|
|
1101
|
-
// in this range.
|
|
1102
|
-
moveInfo = {
|
|
1103
|
-
movedClientIds: [oldestUnacked.clientId],
|
|
1104
|
-
movedSeq: oldestUnacked.seq,
|
|
1105
|
-
movedSeqs: [oldestUnacked.seq],
|
|
1106
|
-
localMovedSeq: oldestUnacked.localSeq,
|
|
1107
|
-
};
|
|
1001
|
+
if (oldest && newest?.stamp.clientId !== stamp.clientId) {
|
|
1002
|
+
const removeInfo = { removes: [] };
|
|
1003
|
+
if (newestAcked === newest || newestAcked?.stamp.clientId !== stamp.clientId) {
|
|
1004
|
+
removeInfo.removes = overlappingAckedObliterates;
|
|
1005
|
+
// Because we found these by looking at overlapping obliterates, they are not necessarily currently sorted by seq.
|
|
1006
|
+
// Address that now.
|
|
1007
|
+
removeInfo.removes.sort(opstampUtils.compare);
|
|
1108
1008
|
}
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1009
|
+
// Note that we don't need to worry about preserving any existing remove information since the segment is new.
|
|
1010
|
+
overwriteInfo(newSegment, removeInfo);
|
|
1011
|
+
if (oldestUnacked !== undefined) {
|
|
1012
|
+
removeInfo.removes.push(oldestUnacked.stamp);
|
|
1013
|
+
assert(oldestUnacked.segmentGroup !== undefined, 0x86c /* expected segment group to exist */);
|
|
1014
|
+
this.addToPendingList(newSegment, oldestUnacked.segmentGroup);
|
|
1113
1015
|
}
|
|
1114
1016
|
if (newSegment.parent) {
|
|
1115
1017
|
// The incremental update codepath in theory can handle most cases where segments are obliterated upon insertion,
|
|
@@ -1117,90 +1019,95 @@ export class MergeTree {
|
|
|
1117
1019
|
// lengths inside the inserting walk, we'd be at risk of double-counting the insertion in any case if we allow
|
|
1118
1020
|
// incremental updates here.
|
|
1119
1021
|
const newStructure = true;
|
|
1120
|
-
this.blockUpdatePathLengths(newSegment.parent,
|
|
1022
|
+
this.blockUpdatePathLengths(newSegment.parent, removeInfo.removes[0], newStructure);
|
|
1121
1023
|
}
|
|
1122
1024
|
}
|
|
1123
1025
|
saveIfLocal(newSegment);
|
|
1124
1026
|
}
|
|
1125
1027
|
}
|
|
1126
|
-
ensureIntervalBoundary(pos,
|
|
1127
|
-
|
|
1128
|
-
|
|
1028
|
+
ensureIntervalBoundary(pos, perspective) {
|
|
1029
|
+
this.insertingWalk(pos, perspective, {
|
|
1030
|
+
seq: TreeMaintenanceSequenceNumber,
|
|
1031
|
+
clientId: perspective.clientId,
|
|
1032
|
+
}, { leaf: this.splitLeafSegment });
|
|
1129
1033
|
}
|
|
1130
1034
|
// Assume called only when pos == len
|
|
1131
|
-
breakTie(pos, node,
|
|
1035
|
+
breakTie(pos, node, insertStamp) {
|
|
1132
1036
|
if (node.isLeaf()) {
|
|
1133
1037
|
if (pos !== 0) {
|
|
1134
1038
|
return false;
|
|
1135
1039
|
}
|
|
1136
|
-
|
|
1137
|
-
// if the new seg is local (UnassignedSequenceNumber) give it the highest possible
|
|
1138
|
-
// seq for comparison, as it will get a seq higher than any other seq once sequences
|
|
1139
|
-
// if the current seg is local (UnassignedSequenceNumber) give it the second highest
|
|
1140
|
-
// possible seq, as the highest is reserved for the previous.
|
|
1141
|
-
const newSeq = seq === UnassignedSequenceNumber ? Number.MAX_SAFE_INTEGER : seq;
|
|
1142
|
-
const segSeq = node.seq === UnassignedSequenceNumber ? Number.MAX_SAFE_INTEGER - 1 : (node.seq ?? 0);
|
|
1143
|
-
return (newSeq > segSeq ||
|
|
1144
|
-
(isMoved(node) && node.movedSeq !== UnassignedSequenceNumber && node.movedSeq > seq) ||
|
|
1040
|
+
return (opstampUtils.greaterThan(insertStamp, node.insert) ||
|
|
1145
1041
|
(isRemoved(node) &&
|
|
1146
|
-
node.
|
|
1147
|
-
node.
|
|
1042
|
+
opstampUtils.isAcked(node.removes[0]) &&
|
|
1043
|
+
opstampUtils.greaterThan(node.removes[0], insertStamp)));
|
|
1148
1044
|
}
|
|
1149
1045
|
else {
|
|
1150
1046
|
return true;
|
|
1151
1047
|
}
|
|
1152
1048
|
}
|
|
1153
|
-
insertingWalk(
|
|
1049
|
+
insertingWalk(pos, perspective, stamp, context) {
|
|
1050
|
+
const { remainder } = this.insertRecursive(this.root, pos, perspective, stamp, context);
|
|
1051
|
+
if (remainder !== undefined) {
|
|
1052
|
+
this.updateRoot(remainder);
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
insertRecursive(block, pos, perspective, stamp, context, isLastChildBlock = true) {
|
|
1154
1056
|
let _pos = pos;
|
|
1155
1057
|
const children = block.children;
|
|
1156
1058
|
let childIndex;
|
|
1157
1059
|
let child;
|
|
1158
1060
|
let newNode;
|
|
1159
1061
|
let fromSplit;
|
|
1062
|
+
let hadChanges = false;
|
|
1160
1063
|
for (childIndex = 0; childIndex < block.childCount; childIndex++) {
|
|
1161
1064
|
child = children[childIndex];
|
|
1162
1065
|
// ensure we walk down the far edge of the tree, even if all sub-tree is eligible for zamboni
|
|
1163
1066
|
const isLastNonLeafBlock = isLastChildBlock && !child.isLeaf() && childIndex === block.childCount - 1;
|
|
1164
|
-
const len = this.nodeLength(child,
|
|
1067
|
+
const len = this.nodeLength(child, perspective) ?? (isLastChildBlock ? 0 : undefined);
|
|
1165
1068
|
if (len === undefined) {
|
|
1166
1069
|
// if the seg len is undefined, the segment
|
|
1167
1070
|
// will be removed, so should just be skipped for now
|
|
1168
1071
|
continue;
|
|
1169
1072
|
}
|
|
1170
1073
|
assert(len >= 0, 0x4bc /* Length should not be negative */);
|
|
1171
|
-
if (_pos < len || (_pos === len && this.breakTie(_pos, child,
|
|
1074
|
+
if (_pos < len || (_pos === len && this.breakTie(_pos, child, stamp))) {
|
|
1172
1075
|
// Found entry containing pos
|
|
1173
1076
|
if (child.isLeaf()) {
|
|
1174
1077
|
const segment = child;
|
|
1175
1078
|
const segmentChanges = context.leaf(segment, _pos, context);
|
|
1176
1079
|
if (segmentChanges.replaceCurrent) {
|
|
1080
|
+
hadChanges = true;
|
|
1177
1081
|
assignChild(block, segmentChanges.replaceCurrent, childIndex, false);
|
|
1178
1082
|
segmentChanges.replaceCurrent.ordinal = child.ordinal;
|
|
1179
1083
|
}
|
|
1180
1084
|
if (segmentChanges.next) {
|
|
1085
|
+
hadChanges = true;
|
|
1181
1086
|
newNode = segmentChanges.next;
|
|
1182
1087
|
childIndex++; // Insert after
|
|
1183
1088
|
}
|
|
1184
1089
|
else {
|
|
1185
|
-
|
|
1186
|
-
return undefined;
|
|
1090
|
+
return { remainder: undefined, hadChanges };
|
|
1187
1091
|
}
|
|
1188
1092
|
}
|
|
1189
1093
|
else {
|
|
1190
1094
|
const childBlock = child;
|
|
1191
1095
|
// Internal node
|
|
1192
|
-
const
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1096
|
+
const insertResult = this.insertRecursive(childBlock, _pos, perspective, stamp, context, isLastNonLeafBlock);
|
|
1097
|
+
hadChanges ||= insertResult.hadChanges;
|
|
1098
|
+
if (insertResult.remainder === undefined) {
|
|
1099
|
+
if (insertResult.hadChanges) {
|
|
1100
|
+
this.blockUpdateLength(block, stamp);
|
|
1101
|
+
}
|
|
1102
|
+
return insertResult;
|
|
1196
1103
|
}
|
|
1197
|
-
else if (
|
|
1104
|
+
else if (insertResult.remainder === MergeTree.theUnfinishedNode) {
|
|
1198
1105
|
_pos -= len; // Act as if shifted segment
|
|
1199
1106
|
continue;
|
|
1200
1107
|
}
|
|
1201
1108
|
else {
|
|
1202
|
-
newNode =
|
|
1203
|
-
fromSplit =
|
|
1109
|
+
newNode = insertResult.remainder;
|
|
1110
|
+
fromSplit = insertResult.remainder;
|
|
1204
1111
|
childIndex++; // Insert after
|
|
1205
1112
|
}
|
|
1206
1113
|
}
|
|
@@ -1212,7 +1119,7 @@ export class MergeTree {
|
|
|
1212
1119
|
}
|
|
1213
1120
|
if (!newNode && _pos === 0) {
|
|
1214
1121
|
if (context.continuePredicate?.(block)) {
|
|
1215
|
-
return MergeTree.theUnfinishedNode;
|
|
1122
|
+
return { remainder: MergeTree.theUnfinishedNode, hadChanges };
|
|
1216
1123
|
}
|
|
1217
1124
|
else {
|
|
1218
1125
|
const segmentChanges = context.leaf(undefined, _pos, context);
|
|
@@ -1221,6 +1128,7 @@ export class MergeTree {
|
|
|
1221
1128
|
}
|
|
1222
1129
|
}
|
|
1223
1130
|
if (newNode) {
|
|
1131
|
+
hadChanges = true;
|
|
1224
1132
|
for (let i = block.childCount; i > childIndex; i--) {
|
|
1225
1133
|
block.children[i] = block.children[i - 1];
|
|
1226
1134
|
block.children[i].index = i;
|
|
@@ -1232,19 +1140,19 @@ export class MergeTree {
|
|
|
1232
1140
|
if (fromSplit) {
|
|
1233
1141
|
this.nodeUpdateOrdinals(fromSplit);
|
|
1234
1142
|
}
|
|
1235
|
-
this.blockUpdateLength(block,
|
|
1236
|
-
return undefined;
|
|
1143
|
+
this.blockUpdateLength(block, stamp);
|
|
1144
|
+
return { remainder: undefined, hadChanges };
|
|
1237
1145
|
}
|
|
1238
1146
|
else {
|
|
1239
1147
|
// Don't update ordinals because higher block will do it
|
|
1240
1148
|
const newNodeFromSplit = this.split(block);
|
|
1241
|
-
PartialSequenceLengths.options.verifyExpected?.(this, block, refSeq, clientId);
|
|
1242
|
-
PartialSequenceLengths.options.verifyExpected?.(this, newNodeFromSplit, refSeq, clientId);
|
|
1243
|
-
return newNodeFromSplit;
|
|
1149
|
+
PartialSequenceLengths.options.verifyExpected?.(this, block, perspective.refSeq, stamp.clientId);
|
|
1150
|
+
PartialSequenceLengths.options.verifyExpected?.(this, newNodeFromSplit, perspective.refSeq, stamp.clientId);
|
|
1151
|
+
return { remainder: newNodeFromSplit, hadChanges };
|
|
1244
1152
|
}
|
|
1245
1153
|
}
|
|
1246
1154
|
else {
|
|
1247
|
-
return undefined;
|
|
1155
|
+
return { remainder: undefined, hadChanges };
|
|
1248
1156
|
}
|
|
1249
1157
|
}
|
|
1250
1158
|
split(node) {
|
|
@@ -1279,16 +1187,14 @@ export class MergeTree {
|
|
|
1279
1187
|
* @param clientId - The id of the client making the annotate
|
|
1280
1188
|
* @param seq - The sequence number of the annotate operation
|
|
1281
1189
|
* @param opArgs - The op args for the annotate op. this is passed to the merge tree callback if there is one
|
|
1282
|
-
* @param rollback - Whether this is for a local rollback and what kind
|
|
1283
1190
|
*/
|
|
1284
|
-
annotateRange(start, end, propsOrAdjust,
|
|
1191
|
+
annotateRange(start, end, propsOrAdjust, perspective, stamp, opArgs) {
|
|
1285
1192
|
if (propsOrAdjust.adjust !== undefined) {
|
|
1286
1193
|
errorIfOptionNotTrue(this.options, "mergeTreeEnableAnnotateAdjust");
|
|
1287
1194
|
}
|
|
1288
|
-
this.ensureIntervalBoundary(start,
|
|
1289
|
-
this.ensureIntervalBoundary(end,
|
|
1195
|
+
this.ensureIntervalBoundary(start, perspective);
|
|
1196
|
+
this.ensureIntervalBoundary(end, perspective);
|
|
1290
1197
|
const deltaSegments = [];
|
|
1291
|
-
const localSeq = seq === UnassignedSequenceNumber ? ++this.collabWindow.localSeq : undefined;
|
|
1292
1198
|
let segmentGroup;
|
|
1293
1199
|
const opObj = propsOrAdjust.props ?? propsOrAdjust.adjust;
|
|
1294
1200
|
const annotateSegment = (segment) => {
|
|
@@ -1296,23 +1202,23 @@ export class MergeTree {
|
|
|
1296
1202
|
!(reservedMarkerIdKey in opObj) ||
|
|
1297
1203
|
opObj.markerId === segment.properties?.markerId, 0x5ad /* Cannot change the markerId of an existing marker */);
|
|
1298
1204
|
const propertyManager = (segment.propertyManager ??= new PropertiesManager());
|
|
1299
|
-
const propertyDeltas = propertyManager.handleProperties(propsOrAdjust, segment, seq, this.collabWindow.minSeq, this.collabWindow.collaborating, rollback);
|
|
1300
|
-
if (!
|
|
1205
|
+
const propertyDeltas = propertyManager.handleProperties(propsOrAdjust, segment, stamp.seq, this.collabWindow.minSeq, this.collabWindow.collaborating, opArgs?.rollback === true);
|
|
1206
|
+
if (!isRemoved(segment)) {
|
|
1301
1207
|
deltaSegments.push({ segment, propertyDeltas });
|
|
1302
1208
|
}
|
|
1303
1209
|
if (this.collabWindow.collaborating) {
|
|
1304
|
-
if (
|
|
1305
|
-
segmentGroup = this.addToPendingList(segment, segmentGroup, localSeq, propertyDeltas);
|
|
1210
|
+
if (opstampUtils.isLocal(stamp)) {
|
|
1211
|
+
segmentGroup = this.addToPendingList(segment, segmentGroup, stamp.localSeq, propertyDeltas);
|
|
1306
1212
|
}
|
|
1307
1213
|
else {
|
|
1308
1214
|
if (MergeTree.options.zamboniSegments) {
|
|
1309
|
-
this.addToLRUSet(segment, seq);
|
|
1215
|
+
this.addToLRUSet(segment, stamp.seq);
|
|
1310
1216
|
}
|
|
1311
1217
|
}
|
|
1312
1218
|
}
|
|
1313
1219
|
return true;
|
|
1314
1220
|
};
|
|
1315
|
-
this.nodeMap(
|
|
1221
|
+
this.nodeMap(perspective, annotateSegment, undefined, start, end);
|
|
1316
1222
|
// OpArgs == undefined => test code
|
|
1317
1223
|
if (deltaSegments.length > 0) {
|
|
1318
1224
|
this.mergeTreeDeltaCallback?.(opArgs, {
|
|
@@ -1321,34 +1227,28 @@ export class MergeTree {
|
|
|
1321
1227
|
});
|
|
1322
1228
|
}
|
|
1323
1229
|
if (this.collabWindow.collaborating &&
|
|
1324
|
-
|
|
1230
|
+
opstampUtils.isAcked(stamp) &&
|
|
1325
1231
|
MergeTree.options.zamboniSegments) {
|
|
1326
1232
|
zamboniSegments(this);
|
|
1327
1233
|
}
|
|
1328
1234
|
}
|
|
1329
|
-
obliterateRangeSided(start, end,
|
|
1235
|
+
obliterateRangeSided(start, end, perspective, stamp, opArgs) {
|
|
1330
1236
|
const startPos = start.side === Side.Before ? start.pos : start.pos + 1;
|
|
1331
1237
|
const endPos = end.side === Side.Before ? end.pos : end.pos + 1;
|
|
1332
|
-
this.ensureIntervalBoundary(startPos,
|
|
1333
|
-
this.ensureIntervalBoundary(endPos,
|
|
1238
|
+
this.ensureIntervalBoundary(startPos, perspective);
|
|
1239
|
+
this.ensureIntervalBoundary(endPos, perspective);
|
|
1334
1240
|
let _overwrite = false;
|
|
1335
1241
|
const localOverlapWithRefs = [];
|
|
1336
|
-
const
|
|
1337
|
-
const localSeq = seq === UnassignedSequenceNumber ? ++this.collabWindow.localSeq : undefined;
|
|
1338
|
-
const perspective = seq === UnassignedSequenceNumber
|
|
1339
|
-
? this.localPerspective
|
|
1340
|
-
: new PriorPerspective(refSeq, clientId);
|
|
1242
|
+
const removedSegments = [];
|
|
1341
1243
|
const obliterate = {
|
|
1342
|
-
clientId,
|
|
1343
|
-
end: createDetachedLocalReferencePosition(undefined),
|
|
1344
|
-
refSeq,
|
|
1345
|
-
seq,
|
|
1346
1244
|
start: createDetachedLocalReferencePosition(undefined),
|
|
1347
|
-
|
|
1245
|
+
end: createDetachedLocalReferencePosition(undefined),
|
|
1246
|
+
refSeq: perspective.refSeq,
|
|
1247
|
+
stamp,
|
|
1348
1248
|
segmentGroup: undefined,
|
|
1349
1249
|
};
|
|
1350
|
-
const { segment: startSeg } = this.getContainingSegment(start.pos,
|
|
1351
|
-
const { segment: endSeg } = this.getContainingSegment(end.pos,
|
|
1250
|
+
const { segment: startSeg } = this.getContainingSegment(start.pos, perspective);
|
|
1251
|
+
const { segment: endSeg } = this.getContainingSegment(end.pos, perspective);
|
|
1352
1252
|
assert(isSegmentLeaf(startSeg) && isSegmentLeaf(endSeg), 0xa3f /* segments cannot be undefined */);
|
|
1353
1253
|
obliterate.start = this.createLocalReferencePosition(startSeg, start.side === Side.Before ? 0 : Math.max(startSeg.cachedLength - 1, 0), ReferenceType.StayOnRemove, {
|
|
1354
1254
|
obliterate,
|
|
@@ -1362,15 +1262,15 @@ export class MergeTree {
|
|
|
1362
1262
|
// at which point they are added to the segment group.
|
|
1363
1263
|
obliterate.segmentGroup = {
|
|
1364
1264
|
segments: [],
|
|
1365
|
-
localSeq,
|
|
1265
|
+
localSeq: stamp.localSeq,
|
|
1366
1266
|
refSeq: this.collabWindow.currentSeq,
|
|
1367
1267
|
obliterateInfo: obliterate,
|
|
1368
1268
|
};
|
|
1369
|
-
if (this.collabWindow.collaborating && clientId === this.collabWindow.clientId) {
|
|
1269
|
+
if (this.collabWindow.collaborating && stamp.clientId === this.collabWindow.clientId) {
|
|
1370
1270
|
this.pendingSegments.push(obliterate.segmentGroup);
|
|
1371
1271
|
}
|
|
1372
1272
|
this.obliterates.addOrUpdate(obliterate);
|
|
1373
|
-
const
|
|
1273
|
+
const markRemoved = (segment, pos) => {
|
|
1374
1274
|
if ((start.side === Side.After && startPos === pos + segment.cachedLength) || // exclusive start segment
|
|
1375
1275
|
(end.side === Side.Before && endPos === pos && perspective.isSegmentPresent(segment)) // exclusive end segment
|
|
1376
1276
|
) {
|
|
@@ -1378,173 +1278,136 @@ export class MergeTree {
|
|
|
1378
1278
|
// These segments are outside of the obliteration range though, so return true to keep walking.
|
|
1379
1279
|
return true;
|
|
1380
1280
|
}
|
|
1381
|
-
const
|
|
1281
|
+
const existingRemoveInfo = toRemovalInfo(segment);
|
|
1382
1282
|
// The "last-to-obliterate-gets-to-insert" policy described by the doc comment on `obliteratePrecedingInsertion`
|
|
1383
1283
|
// is mostly handled by logic at insertion time, but we need a small bit of handling here.
|
|
1384
1284
|
// Specifically, we want to avoid marking a local-only segment as obliterated when we know one of our own local obliterates
|
|
1385
1285
|
// will win against the obliterate we're processing, hence the early exit.
|
|
1386
|
-
if (segment.
|
|
1387
|
-
segment.obliteratePrecedingInsertion?.seq === UnassignedSequenceNumber &&
|
|
1388
|
-
|
|
1286
|
+
if (opstampUtils.isLocal(segment.insert) &&
|
|
1287
|
+
segment.obliteratePrecedingInsertion?.stamp.seq === UnassignedSequenceNumber &&
|
|
1288
|
+
opstampUtils.isAcked(stamp)) {
|
|
1389
1289
|
// We chose to not obliterate this segment because we are aware of an unacked local obliteration.
|
|
1390
1290
|
// The local obliterate has not been sequenced yet, so it is still the newest obliterate we are aware of.
|
|
1391
1291
|
// Other clients will also choose not to obliterate this segment because the most recent obliteration has the same clientId
|
|
1392
1292
|
return true;
|
|
1393
1293
|
}
|
|
1394
1294
|
// Partial lengths incrementality is not supported for overlapping obliterate/removes.
|
|
1395
|
-
_overwrite ||=
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1295
|
+
_overwrite ||= existingRemoveInfo !== undefined;
|
|
1296
|
+
// - Record the segment as removed
|
|
1297
|
+
// - If this was the first thing to remove the segment from the local view, add it to removedSegments
|
|
1298
|
+
// - Otherwise, if it was the first thing to remove the segment from the acked view, add it to localOverlapWithRefs (so we can slide them)
|
|
1299
|
+
if (existingRemoveInfo === undefined) {
|
|
1300
|
+
const removed = overwriteInfo(segment, {
|
|
1301
|
+
removes: [stamp],
|
|
1402
1302
|
});
|
|
1403
|
-
|
|
1404
|
-
if (existingRemoval === undefined) {
|
|
1405
|
-
movedSegments.push(movedSeg);
|
|
1406
|
-
}
|
|
1407
|
-
else if (existingRemoval.removedSeq === UnassignedSequenceNumber &&
|
|
1408
|
-
segment.localRefs?.empty === false) {
|
|
1409
|
-
// We removed this locally already so we don't need to event it again, but it might have references
|
|
1410
|
-
// that need sliding now that a move may have been acked.
|
|
1411
|
-
localOverlapWithRefs.push(segment);
|
|
1412
|
-
}
|
|
1303
|
+
removedSegments.push(removed);
|
|
1413
1304
|
}
|
|
1414
1305
|
else {
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
// The list isn't ordered, but we keep the first move at the head
|
|
1421
|
-
// for partialLengths bookkeeping purposes
|
|
1422
|
-
existingMoveInfo.movedClientIds.unshift(clientId);
|
|
1423
|
-
existingMoveInfo.movedSeq = seq;
|
|
1424
|
-
existingMoveInfo.movedSeqs.unshift(seq);
|
|
1425
|
-
if (segment.localRefs?.empty === false) {
|
|
1426
|
-
localOverlapWithRefs.push(segment);
|
|
1427
|
-
}
|
|
1428
|
-
}
|
|
1429
|
-
else {
|
|
1430
|
-
// Do not replace earlier sequence number for move
|
|
1431
|
-
existingMoveInfo.movedClientIds.push(clientId);
|
|
1432
|
-
existingMoveInfo.movedSeqs.push(seq);
|
|
1306
|
+
// The segment has already been removed, so we don't need to add it to removedSegments. However,
|
|
1307
|
+
// if it's only been removed locally, we still need to slide any references that may exist on it.
|
|
1308
|
+
if (!opstampUtils.hasAnyAckedOperation(existingRemoveInfo.removes) &&
|
|
1309
|
+
segment.localRefs?.empty === false) {
|
|
1310
|
+
localOverlapWithRefs.push(segment);
|
|
1433
1311
|
}
|
|
1312
|
+
opstampUtils.spliceIntoList(existingRemoveInfo.removes, stamp);
|
|
1434
1313
|
}
|
|
1435
|
-
|
|
1436
|
-
// Save segment so can assign
|
|
1314
|
+
assertRemoved(segment);
|
|
1315
|
+
// Save segment so can assign sequence number when acked by server
|
|
1437
1316
|
if (this.collabWindow.collaborating) {
|
|
1438
|
-
if (segment.
|
|
1439
|
-
clientId === this.collabWindow.clientId) {
|
|
1440
|
-
obliterate.segmentGroup = this.addToPendingList(segment, obliterate.segmentGroup, localSeq);
|
|
1317
|
+
if (opstampUtils.isLocal(segment.removes[0]) &&
|
|
1318
|
+
stamp.clientId === this.collabWindow.clientId) {
|
|
1319
|
+
obliterate.segmentGroup = this.addToPendingList(segment, obliterate.segmentGroup, stamp.localSeq);
|
|
1441
1320
|
}
|
|
1442
1321
|
else {
|
|
1443
1322
|
if (MergeTree.options.zamboniSegments) {
|
|
1444
|
-
this.addToLRUSet(segment, seq);
|
|
1323
|
+
this.addToLRUSet(segment, stamp.seq);
|
|
1445
1324
|
}
|
|
1446
1325
|
}
|
|
1447
1326
|
}
|
|
1448
1327
|
return true;
|
|
1449
1328
|
};
|
|
1450
|
-
const
|
|
1329
|
+
const afterMarkRemoved = (node) => {
|
|
1451
1330
|
if (_overwrite) {
|
|
1452
1331
|
this.nodeUpdateLengthNewStructure(node);
|
|
1453
1332
|
}
|
|
1454
1333
|
else {
|
|
1455
|
-
this.blockUpdateLength(node,
|
|
1334
|
+
this.blockUpdateLength(node, stamp);
|
|
1456
1335
|
}
|
|
1457
1336
|
return true;
|
|
1458
1337
|
};
|
|
1459
|
-
this.nodeMap(
|
|
1460
|
-
|
|
1338
|
+
this.nodeMap(perspective, markRemoved, afterMarkRemoved, start.pos, end.pos + 1, // include the segment containing the end reference
|
|
1339
|
+
// Use a visibilityPerspective which includes all segments (including local ones) which are in the obliteration range.
|
|
1340
|
+
// This ensures that concurrently inserted segments will also be marked obliterated.
|
|
1341
|
+
opstampUtils.isLocal(stamp)
|
|
1342
|
+
? perspective
|
|
1343
|
+
: new RemoteObliteratePerspective(stamp.clientId));
|
|
1461
1344
|
this.slideAckedRemovedSegmentReferences(localOverlapWithRefs);
|
|
1462
1345
|
// opArgs == undefined => test code
|
|
1463
1346
|
if (start.pos !== end.pos || start.side !== end.side) {
|
|
1464
1347
|
this.mergeTreeDeltaCallback?.(opArgs, {
|
|
1465
1348
|
operation: MergeTreeDeltaType.OBLITERATE,
|
|
1466
|
-
deltaSegments:
|
|
1349
|
+
deltaSegments: removedSegments.map((segment) => ({ segment })),
|
|
1467
1350
|
});
|
|
1468
1351
|
}
|
|
1469
1352
|
// these events are newly removed
|
|
1470
1353
|
// so we slide after eventing in case the consumer wants to make reference
|
|
1471
1354
|
// changes at remove time, like add a ref to track undo redo.
|
|
1472
|
-
if (!this.collabWindow.collaborating || clientId !== this.collabWindow.clientId) {
|
|
1473
|
-
this.slideAckedRemovedSegmentReferences(
|
|
1355
|
+
if (!this.collabWindow.collaborating || stamp.clientId !== this.collabWindow.clientId) {
|
|
1356
|
+
this.slideAckedRemovedSegmentReferences(removedSegments);
|
|
1474
1357
|
}
|
|
1475
1358
|
if (this.collabWindow.collaborating &&
|
|
1476
|
-
|
|
1359
|
+
opstampUtils.isAcked(stamp) &&
|
|
1477
1360
|
MergeTree.options.zamboniSegments) {
|
|
1478
1361
|
zamboniSegments(this);
|
|
1479
1362
|
}
|
|
1480
1363
|
}
|
|
1481
|
-
obliterateRange(start, end,
|
|
1364
|
+
obliterateRange(start, end, perspective, stampArg, opArgs) {
|
|
1482
1365
|
errorIfOptionNotTrue(this.options, "mergeTreeEnableObliterate");
|
|
1366
|
+
const stamp = { ...stampArg, type: "sliceRemove" };
|
|
1483
1367
|
if (this.options?.mergeTreeEnableSidedObliterate) {
|
|
1484
1368
|
assert(typeof start === "object" && typeof end === "object", 0xa45 /* Start and end must be of type InteriorSequencePlace if mergeTreeEnableSidedObliterate is enabled. */);
|
|
1485
|
-
this.obliterateRangeSided(start, end,
|
|
1369
|
+
this.obliterateRangeSided(start, end, perspective, stamp, opArgs);
|
|
1486
1370
|
}
|
|
1487
1371
|
else {
|
|
1488
1372
|
assert(typeof start === "number" && typeof end === "number", 0xa46 /* Start and end must be numbers if mergeTreeEnableSidedObliterate is not enabled. */);
|
|
1489
|
-
this.obliterateRangeSided({ pos: start, side: Side.Before }, { pos: end - 1, side: Side.After },
|
|
1373
|
+
this.obliterateRangeSided({ pos: start, side: Side.Before }, { pos: end - 1, side: Side.After }, perspective, stamp, opArgs);
|
|
1490
1374
|
}
|
|
1491
1375
|
}
|
|
1492
|
-
markRangeRemoved(start, end,
|
|
1376
|
+
markRangeRemoved(start, end, perspective, stampArg, opArgs) {
|
|
1493
1377
|
let _overwrite = false;
|
|
1494
|
-
|
|
1495
|
-
this.ensureIntervalBoundary(
|
|
1378
|
+
const stamp = { ...stampArg, type: "setRemove" };
|
|
1379
|
+
this.ensureIntervalBoundary(start, perspective);
|
|
1380
|
+
this.ensureIntervalBoundary(end, perspective);
|
|
1496
1381
|
let segmentGroup;
|
|
1497
1382
|
const removedSegments = [];
|
|
1498
1383
|
const localOverlapWithRefs = [];
|
|
1499
|
-
const localSeq = seq === UnassignedSequenceNumber ? ++this.collabWindow.localSeq : undefined;
|
|
1500
1384
|
const markRemoved = (segment, pos, _start, _end) => {
|
|
1501
1385
|
const existingRemovalInfo = toRemovalInfo(segment);
|
|
1502
1386
|
// Partial lengths incrementality is not supported for overlapping obliterate/removes.
|
|
1503
|
-
_overwrite ||= existingRemovalInfo !== undefined
|
|
1387
|
+
_overwrite ||= existingRemovalInfo !== undefined;
|
|
1504
1388
|
if (existingRemovalInfo === undefined) {
|
|
1505
1389
|
const removed = overwriteInfo(segment, {
|
|
1506
|
-
|
|
1507
|
-
removedSeq: seq,
|
|
1508
|
-
localRemovedSeq: localSeq,
|
|
1390
|
+
removes: [stamp],
|
|
1509
1391
|
});
|
|
1510
|
-
|
|
1511
|
-
if (existingMoveInfo === undefined) {
|
|
1512
|
-
removedSegments.push(removed);
|
|
1513
|
-
}
|
|
1514
|
-
else if (existingMoveInfo.movedSeq === UnassignedSequenceNumber &&
|
|
1515
|
-
segment.localRefs?.empty === false) {
|
|
1516
|
-
// We moved this locally already so we don't need to event it again, but it might have references
|
|
1517
|
-
// that need sliding now that a remove may have been acked.
|
|
1518
|
-
localOverlapWithRefs.push(segment);
|
|
1519
|
-
}
|
|
1392
|
+
removedSegments.push(removed);
|
|
1520
1393
|
}
|
|
1521
1394
|
else {
|
|
1522
|
-
if (existingRemovalInfo.
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
// The list isn't ordered, but we keep the first removal at the head
|
|
1526
|
-
// for partialLengths bookkeeping purposes
|
|
1527
|
-
existingRemovalInfo.removedClientIds.unshift(clientId);
|
|
1528
|
-
existingRemovalInfo.removedSeq = seq;
|
|
1529
|
-
if (segment.localRefs?.empty === false) {
|
|
1530
|
-
localOverlapWithRefs.push(segment);
|
|
1531
|
-
}
|
|
1532
|
-
}
|
|
1533
|
-
else {
|
|
1534
|
-
// Do not replace earlier sequence number for remove
|
|
1535
|
-
existingRemovalInfo.removedClientIds.push(clientId);
|
|
1395
|
+
if (!opstampUtils.hasAnyAckedOperation(existingRemovalInfo.removes) &&
|
|
1396
|
+
segment.localRefs?.empty === false) {
|
|
1397
|
+
localOverlapWithRefs.push(segment);
|
|
1536
1398
|
}
|
|
1399
|
+
opstampUtils.spliceIntoList(existingRemovalInfo.removes, stamp);
|
|
1537
1400
|
}
|
|
1538
1401
|
assertRemoved(segment);
|
|
1539
1402
|
// Save segment so we can assign removed sequence number when acked by server
|
|
1540
1403
|
if (this.collabWindow.collaborating) {
|
|
1541
|
-
if (segment.
|
|
1542
|
-
clientId === this.collabWindow.clientId) {
|
|
1543
|
-
segmentGroup = this.addToPendingList(segment, segmentGroup, localSeq);
|
|
1404
|
+
if (opstampUtils.isLocal(segment.removes[0]) &&
|
|
1405
|
+
stamp.clientId === this.collabWindow.clientId) {
|
|
1406
|
+
segmentGroup = this.addToPendingList(segment, segmentGroup, stamp.localSeq);
|
|
1544
1407
|
}
|
|
1545
1408
|
else {
|
|
1546
1409
|
if (MergeTree.options.zamboniSegments) {
|
|
1547
|
-
this.addToLRUSet(segment, seq);
|
|
1410
|
+
this.addToLRUSet(segment, stamp.seq);
|
|
1548
1411
|
}
|
|
1549
1412
|
}
|
|
1550
1413
|
}
|
|
@@ -1555,11 +1418,11 @@ export class MergeTree {
|
|
|
1555
1418
|
this.nodeUpdateLengthNewStructure(node);
|
|
1556
1419
|
}
|
|
1557
1420
|
else {
|
|
1558
|
-
this.blockUpdateLength(node,
|
|
1421
|
+
this.blockUpdateLength(node, stamp);
|
|
1559
1422
|
}
|
|
1560
1423
|
return true;
|
|
1561
1424
|
};
|
|
1562
|
-
this.nodeMap(
|
|
1425
|
+
this.nodeMap(perspective, markRemoved, afterMarkRemoved, start, end);
|
|
1563
1426
|
// these segments are already viewed as being removed locally and are not event-ed
|
|
1564
1427
|
// so can slide non-StayOnRemove refs immediately
|
|
1565
1428
|
this.slideAckedRemovedSegmentReferences(localOverlapWithRefs);
|
|
@@ -1573,11 +1436,11 @@ export class MergeTree {
|
|
|
1573
1436
|
// these events are newly removed
|
|
1574
1437
|
// so we slide after eventing in case the consumer wants to make reference
|
|
1575
1438
|
// changes at remove time, like add a ref to track undo redo.
|
|
1576
|
-
if (!this.collabWindow.collaborating || clientId !== this.collabWindow.clientId) {
|
|
1439
|
+
if (!this.collabWindow.collaborating || stamp.clientId !== this.collabWindow.clientId) {
|
|
1577
1440
|
this.slideAckedRemovedSegmentReferences(removedSegments);
|
|
1578
1441
|
}
|
|
1579
1442
|
if (this.collabWindow.collaborating &&
|
|
1580
|
-
|
|
1443
|
+
opstampUtils.isAcked(stamp) &&
|
|
1581
1444
|
MergeTree.options.zamboniSegments) {
|
|
1582
1445
|
zamboniSegments(this);
|
|
1583
1446
|
}
|
|
@@ -1596,15 +1459,25 @@ export class MergeTree {
|
|
|
1596
1459
|
pendingSegmentGroup.segments.forEach((segment) => {
|
|
1597
1460
|
const segmentSegmentGroup = segment?.segmentGroups?.pop();
|
|
1598
1461
|
assert(segmentSegmentGroup === pendingSegmentGroup, 0x3ee /* Unexpected segmentGroup in segment */);
|
|
1599
|
-
assert(isRemoved(segment) &&
|
|
1462
|
+
assert(isRemoved(segment) &&
|
|
1463
|
+
segment.removes[0].clientId === this.collabWindow.clientId &&
|
|
1464
|
+
segment.removes[0].type === "setRemove", 0x39d /* Rollback segment removedClientId does not match local client */);
|
|
1600
1465
|
let updateNode = segment.parent;
|
|
1466
|
+
// This also removes obliterates, but that should be ok as we can only remove a segment once.
|
|
1467
|
+
// If we were able to remove it locally, that also means there are no remote removals (since rollback is synchronous).
|
|
1601
1468
|
removeRemovalInfo(segment);
|
|
1602
1469
|
for (updateNode; updateNode !== undefined; updateNode = updateNode.parent) {
|
|
1603
|
-
this.blockUpdateLength(updateNode,
|
|
1470
|
+
this.blockUpdateLength(updateNode, {
|
|
1471
|
+
seq: UnassignedSequenceNumber,
|
|
1472
|
+
clientId: this.collabWindow.clientId,
|
|
1473
|
+
});
|
|
1604
1474
|
}
|
|
1605
1475
|
// Note: optional chaining short-circuits:
|
|
1606
1476
|
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Optional_chaining#short-circuiting
|
|
1607
|
-
this.mergeTreeDeltaCallback?.({
|
|
1477
|
+
this.mergeTreeDeltaCallback?.({
|
|
1478
|
+
op: createInsertSegmentOp(this.findRollbackPosition(segment), segment),
|
|
1479
|
+
rollback: true,
|
|
1480
|
+
}, {
|
|
1608
1481
|
operation: MergeTreeDeltaType.INSERT,
|
|
1609
1482
|
deltaSegments: [{ segment }],
|
|
1610
1483
|
});
|
|
@@ -1624,15 +1497,27 @@ export class MergeTree {
|
|
|
1624
1497
|
assert(segmentSegmentGroup === pendingSegmentGroup, 0x3ef /* Unexpected segmentGroup in segment */);
|
|
1625
1498
|
const start = this.findRollbackPosition(segment);
|
|
1626
1499
|
if (op.type === MergeTreeDeltaType.INSERT) {
|
|
1627
|
-
segment.
|
|
1628
|
-
|
|
1500
|
+
segment.insert = {
|
|
1501
|
+
type: "insert",
|
|
1502
|
+
seq: UniversalSequenceNumber,
|
|
1503
|
+
clientId: this.collabWindow.clientId,
|
|
1504
|
+
};
|
|
1629
1505
|
const removeOp = createRemoveRangeOp(start, start + segment.cachedLength);
|
|
1630
|
-
|
|
1506
|
+
const removeStamp = {
|
|
1507
|
+
type: "setRemove",
|
|
1508
|
+
seq: UniversalSequenceNumber,
|
|
1509
|
+
clientId: this.collabWindow.clientId,
|
|
1510
|
+
};
|
|
1511
|
+
this.markRangeRemoved(start, start + segment.cachedLength, this.localPerspective, removeStamp, { op: removeOp, rollback: true });
|
|
1631
1512
|
} /* op.type === MergeTreeDeltaType.ANNOTATE */
|
|
1632
1513
|
else {
|
|
1633
1514
|
const props = pendingSegmentGroup.previousProps[i];
|
|
1634
1515
|
const annotateOp = createAnnotateRangeOp(start, start + segment.cachedLength, props);
|
|
1635
|
-
|
|
1516
|
+
const annotateStamp = {
|
|
1517
|
+
seq: UniversalSequenceNumber,
|
|
1518
|
+
clientId: this.collabWindow.clientId,
|
|
1519
|
+
};
|
|
1520
|
+
this.annotateRange(start, start + segment.cachedLength, { props }, this.localPerspective, annotateStamp, { op: annotateOp, rollback: true });
|
|
1636
1521
|
i++;
|
|
1637
1522
|
}
|
|
1638
1523
|
}
|
|
@@ -1673,7 +1558,7 @@ export class MergeTree {
|
|
|
1673
1558
|
createLocalReferencePosition(_segment, offset, refType, properties, slidingPreference, canSlideToEndpoint) {
|
|
1674
1559
|
if (_segment !== "start" &&
|
|
1675
1560
|
_segment !== "end" &&
|
|
1676
|
-
|
|
1561
|
+
isRemovedAndAcked(_segment) &&
|
|
1677
1562
|
!refTypeIncludesFlag(refType, ReferenceType.SlideOnRemove | ReferenceType.Transient | ReferenceType.StayOnRemove) &&
|
|
1678
1563
|
_segment.endpointType === undefined) {
|
|
1679
1564
|
throw new UsageError("Can only create SlideOnRemove or Transient local reference position on a removed or obliterated segment");
|
|
@@ -1717,15 +1602,15 @@ export class MergeTree {
|
|
|
1717
1602
|
affectedSegments.insertAfter(lastLocalSegment, segmentToSlide.data);
|
|
1718
1603
|
}
|
|
1719
1604
|
else if (isRemoved(segmentToSlide.data)) {
|
|
1720
|
-
assert(segmentToSlide.data.
|
|
1605
|
+
assert(segmentToSlide.data.removes[0].seq !== undefined, 0x54d /* Removed segment that hasnt had its removal acked should be locally removed */);
|
|
1721
1606
|
// Slide each locally removed item past all segments that have localSeq > lremoveItem.localSeq
|
|
1722
1607
|
// but not past remotely removed segments;
|
|
1723
1608
|
let cur = segmentToSlide;
|
|
1724
1609
|
let scan = cur.next;
|
|
1725
1610
|
while (scan !== undefined &&
|
|
1726
1611
|
!isRemovedAndAcked(scan.data) &&
|
|
1727
|
-
scan.data.localSeq !== undefined &&
|
|
1728
|
-
scan.data.
|
|
1612
|
+
scan.data.insert.localSeq !== undefined &&
|
|
1613
|
+
opstampUtils.greaterThan(scan.data.insert, segmentToSlide.data.removes[0])) {
|
|
1729
1614
|
cur = scan;
|
|
1730
1615
|
scan = scan.next;
|
|
1731
1616
|
}
|
|
@@ -1810,11 +1695,11 @@ export class MergeTree {
|
|
|
1810
1695
|
}
|
|
1811
1696
|
};
|
|
1812
1697
|
walkAllChildSegments(this.root, (seg) => {
|
|
1813
|
-
if (isRemoved(seg) || seg.
|
|
1698
|
+
if (isRemoved(seg) || opstampUtils.isLocal(seg.insert)) {
|
|
1814
1699
|
if (isRemovedAndAcked(seg)) {
|
|
1815
1700
|
rangeContainsRemoteRemovedSegs = true;
|
|
1816
1701
|
}
|
|
1817
|
-
if (seg.
|
|
1702
|
+
if (opstampUtils.isLocal(seg.insert)) {
|
|
1818
1703
|
rangeContainsLocalSegs = true;
|
|
1819
1704
|
}
|
|
1820
1705
|
currentRangeToNormalize.push(seg);
|
|
@@ -1842,7 +1727,7 @@ export class MergeTree {
|
|
|
1842
1727
|
}
|
|
1843
1728
|
if (node.isLeaf()) {
|
|
1844
1729
|
const segment = node;
|
|
1845
|
-
if ((this.
|
|
1730
|
+
if ((this.leafLength(segment) ?? 0) > 0 && Marker.is(segment)) {
|
|
1846
1731
|
const markerId = segment.getId();
|
|
1847
1732
|
// Also in insertMarker but need for reload segs case
|
|
1848
1733
|
// can add option for this only from reload segs
|
|
@@ -1872,33 +1757,33 @@ export class MergeTree {
|
|
|
1872
1757
|
block.rightmostTiles = rightmostTiles;
|
|
1873
1758
|
block.cachedLength = len;
|
|
1874
1759
|
}
|
|
1875
|
-
blockUpdatePathLengths(startBlock,
|
|
1760
|
+
blockUpdatePathLengths(startBlock, stamp, newStructure = false) {
|
|
1876
1761
|
let block = startBlock;
|
|
1877
1762
|
while (block !== undefined) {
|
|
1878
1763
|
if (newStructure) {
|
|
1879
1764
|
this.nodeUpdateLengthNewStructure(block);
|
|
1880
1765
|
}
|
|
1881
1766
|
else {
|
|
1882
|
-
this.blockUpdateLength(block,
|
|
1767
|
+
this.blockUpdateLength(block, stamp);
|
|
1883
1768
|
}
|
|
1884
1769
|
block = block.parent;
|
|
1885
1770
|
}
|
|
1886
1771
|
}
|
|
1887
|
-
blockUpdateLength(node,
|
|
1772
|
+
blockUpdateLength(node, stamp) {
|
|
1888
1773
|
this.blockUpdate(node);
|
|
1889
1774
|
this.localPartialsComputed = false;
|
|
1890
1775
|
if (this.collabWindow.collaborating &&
|
|
1891
|
-
|
|
1892
|
-
seq !== TreeMaintenanceSequenceNumber) {
|
|
1776
|
+
opstampUtils.isAcked(stamp) &&
|
|
1777
|
+
stamp.seq !== TreeMaintenanceSequenceNumber) {
|
|
1893
1778
|
if (node.partialLengths !== undefined &&
|
|
1894
1779
|
MergeTree.options.incrementalUpdate &&
|
|
1895
|
-
clientId !== NonCollabClient) {
|
|
1896
|
-
node.partialLengths.update(node, seq, clientId, this.collabWindow);
|
|
1780
|
+
stamp.clientId !== NonCollabClient) {
|
|
1781
|
+
node.partialLengths.update(node, stamp.seq, stamp.clientId, this.collabWindow);
|
|
1897
1782
|
}
|
|
1898
1783
|
else {
|
|
1899
1784
|
node.partialLengths = PartialSequenceLengths.combine(node, this.collabWindow);
|
|
1900
1785
|
}
|
|
1901
|
-
PartialSequenceLengths.options.verifyExpected?.(this, node, seq, clientId);
|
|
1786
|
+
PartialSequenceLengths.options.verifyExpected?.(this, node, stamp.seq, stamp.clientId);
|
|
1902
1787
|
}
|
|
1903
1788
|
}
|
|
1904
1789
|
/**
|
|
@@ -1908,16 +1793,16 @@ export class MergeTree {
|
|
|
1908
1793
|
*
|
|
1909
1794
|
* See `this.nodeMap` for additional documentation
|
|
1910
1795
|
*/
|
|
1911
|
-
mapRange(handler,
|
|
1796
|
+
mapRange(handler, perspective, accum, start, end, splitRange = false, visibilityPerspective = perspective) {
|
|
1912
1797
|
if (splitRange) {
|
|
1913
1798
|
if (start) {
|
|
1914
|
-
this.ensureIntervalBoundary(start,
|
|
1799
|
+
this.ensureIntervalBoundary(start, perspective);
|
|
1915
1800
|
}
|
|
1916
1801
|
if (end) {
|
|
1917
|
-
this.ensureIntervalBoundary(end,
|
|
1802
|
+
this.ensureIntervalBoundary(end, perspective);
|
|
1918
1803
|
}
|
|
1919
1804
|
}
|
|
1920
|
-
this.nodeMap(
|
|
1805
|
+
this.nodeMap(perspective, (seg, pos, _start, _end) => handler(seg, pos, perspective.refSeq, perspective.clientId, _start, _end, accum), undefined, start, end, visibilityPerspective);
|
|
1921
1806
|
}
|
|
1922
1807
|
/**
|
|
1923
1808
|
* Map over all visible segments in a given range
|
|
@@ -1942,8 +1827,8 @@ export class MergeTree {
|
|
|
1942
1827
|
* but it will not count as a segment within the range. That is, it will be
|
|
1943
1828
|
* ignored for the purposes of tracking when traversal should end.
|
|
1944
1829
|
*/
|
|
1945
|
-
nodeMap(
|
|
1946
|
-
const endPos = end ?? this.nodeLength(this.root,
|
|
1830
|
+
nodeMap(perspective, leaf, post, start = 0, end, visibilityPerspective = perspective) {
|
|
1831
|
+
const endPos = end ?? this.nodeLength(this.root, perspective) ?? 0;
|
|
1947
1832
|
if (endPos === start) {
|
|
1948
1833
|
return;
|
|
1949
1834
|
}
|
|
@@ -1952,12 +1837,15 @@ export class MergeTree {
|
|
|
1952
1837
|
if (endPos <= pos) {
|
|
1953
1838
|
return NodeAction.Exit;
|
|
1954
1839
|
}
|
|
1955
|
-
const len = this.nodeLength(node,
|
|
1956
|
-
const lenAtRefSeq = (
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1840
|
+
const len = this.nodeLength(node, visibilityPerspective);
|
|
1841
|
+
const lenAtRefSeq = (visibilityPerspective === perspective ? len : this.nodeLength(node, perspective)) ??
|
|
1842
|
+
0;
|
|
1843
|
+
// NOTE: This code ensures that obliterates have a chance to mark segments which have been inserted locally
|
|
1844
|
+
// as also having been obliterated on the local client. With the introduction of RemoteObliteratePerspective,
|
|
1845
|
+
// it's feasible we could remove it if the `nodeLength` calculation also respects that perspective for blocks
|
|
1846
|
+
// and not just leaves.
|
|
1847
|
+
const isUnackedAndInObliterate = visibilityPerspective !== perspective &&
|
|
1848
|
+
(!node.isLeaf() || opstampUtils.isLocal(node.insert));
|
|
1961
1849
|
if ((len === undefined && lenAtRefSeq === 0) ||
|
|
1962
1850
|
(len === 0 && !isUnackedAndInObliterate && lenAtRefSeq === 0)) {
|
|
1963
1851
|
return NodeAction.Skip;
|
|
@@ -1982,5 +1870,10 @@ MergeTree.options = {
|
|
|
1982
1870
|
insertAfterRemovedSegs: true,
|
|
1983
1871
|
zamboniSegments: true,
|
|
1984
1872
|
};
|
|
1873
|
+
/**
|
|
1874
|
+
* A sentinel value that indicates an inserting walk should continue to the next block sibling.
|
|
1875
|
+
* This can occur for example when tie-break forces insertion of a segment past an entire block (and
|
|
1876
|
+
* the inserting walk first recurses into the block before realizing that).
|
|
1877
|
+
*/
|
|
1985
1878
|
MergeTree.theUnfinishedNode = { childCount: -1 };
|
|
1986
1879
|
//# sourceMappingURL=mergeTree.js.map
|