@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/src/mergeTree.ts
CHANGED
|
@@ -35,6 +35,7 @@ import {
|
|
|
35
35
|
MergeTreeMaintenanceType,
|
|
36
36
|
} from "./mergeTreeDeltaCallback.js";
|
|
37
37
|
import {
|
|
38
|
+
LeafAction,
|
|
38
39
|
NodeAction,
|
|
39
40
|
backwardExcursion,
|
|
40
41
|
depthFirstNodeWalk,
|
|
@@ -53,9 +54,10 @@ import {
|
|
|
53
54
|
SegmentGroup,
|
|
54
55
|
assertSegmentLeaf,
|
|
55
56
|
assignChild,
|
|
57
|
+
getMinSeqPerspective,
|
|
58
|
+
getMinSeqStamp,
|
|
56
59
|
isSegmentLeaf,
|
|
57
60
|
reservedMarkerIdKey,
|
|
58
|
-
seqLTE,
|
|
59
61
|
type IMergeNodeBuilder,
|
|
60
62
|
type ISegmentInternal,
|
|
61
63
|
type ISegmentLeaf,
|
|
@@ -76,10 +78,11 @@ import {
|
|
|
76
78
|
} from "./ops.js";
|
|
77
79
|
import { PartialSequenceLengths } from "./partialLengths.js";
|
|
78
80
|
import {
|
|
79
|
-
LocalDefaultPerspective,
|
|
80
|
-
LocalReconnectingPerspective,
|
|
81
81
|
PriorPerspective,
|
|
82
|
+
LocalReconnectingPerspective,
|
|
82
83
|
type Perspective,
|
|
84
|
+
LocalDefaultPerspective,
|
|
85
|
+
RemoteObliteratePerspective,
|
|
83
86
|
} from "./perspective.js";
|
|
84
87
|
import { PropertySet, createMap, extend, extendIfUndefined } from "./properties.js";
|
|
85
88
|
import {
|
|
@@ -91,54 +94,45 @@ import {
|
|
|
91
94
|
} from "./referencePositions.js";
|
|
92
95
|
import { SegmentGroupCollection } from "./segmentGroupCollection.js";
|
|
93
96
|
import {
|
|
94
|
-
assertMoved,
|
|
95
97
|
assertRemoved,
|
|
96
98
|
isMergeNodeInfo,
|
|
97
|
-
isMoved,
|
|
98
99
|
isRemoved,
|
|
99
100
|
overwriteInfo,
|
|
100
101
|
removeRemovalInfo,
|
|
101
|
-
toMoveInfo,
|
|
102
102
|
toRemovalInfo,
|
|
103
|
-
|
|
104
|
-
type
|
|
105
|
-
type IMoveInfo,
|
|
106
|
-
type IRemovalInfo,
|
|
103
|
+
type IHasInsertionInfo,
|
|
104
|
+
type IHasRemovalInfo,
|
|
107
105
|
type SegmentWithInfo,
|
|
108
106
|
} from "./segmentInfos.js";
|
|
109
107
|
import {
|
|
110
108
|
copyPropertiesAndManager,
|
|
111
109
|
PropertiesManager,
|
|
112
|
-
PropertiesRollback,
|
|
113
110
|
type PropsOrAdjust,
|
|
114
111
|
} from "./segmentPropertiesManager.js";
|
|
115
112
|
import { Side, type InteriorSequencePlace } from "./sequencePlace.js";
|
|
116
113
|
import { SortedSegmentSet } from "./sortedSegmentSet.js";
|
|
114
|
+
import type {
|
|
115
|
+
OperationStamp,
|
|
116
|
+
InsertOperationStamp,
|
|
117
|
+
RemoveOperationStamp,
|
|
118
|
+
SetRemoveOperationStamp,
|
|
119
|
+
SliceRemoveOperationStamp,
|
|
120
|
+
} from "./stamps.js";
|
|
121
|
+
import * as opstampUtils from "./stamps.js";
|
|
117
122
|
import { zamboniSegments } from "./zamboni.js";
|
|
118
123
|
|
|
119
|
-
function isRemovedAndAcked(
|
|
124
|
+
export function isRemovedAndAcked(
|
|
125
|
+
segment: ISegmentPrivate,
|
|
126
|
+
): segment is ISegmentLeaf & IHasRemovalInfo {
|
|
120
127
|
const removalInfo = toRemovalInfo(segment);
|
|
121
|
-
return removalInfo !== undefined && removalInfo.
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
function isMovedAndAcked(segment: ISegmentPrivate): segment is ISegmentLeaf & IMoveInfo {
|
|
125
|
-
const moveInfo = toMoveInfo(segment);
|
|
126
|
-
return moveInfo !== undefined && moveInfo.movedSeq !== UnassignedSequenceNumber;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
function isRemovedAndAckedOrMovedAndAcked(segment: ISegmentPrivate): boolean {
|
|
130
|
-
return isRemovedAndAcked(segment) || isMovedAndAcked(segment);
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
function isRemovedOrMoved(segment: ISegmentLeaf): boolean {
|
|
134
|
-
return isRemoved(segment) || isMoved(segment);
|
|
128
|
+
return removalInfo !== undefined && opstampUtils.isAcked(removalInfo.removes[0]);
|
|
135
129
|
}
|
|
136
130
|
|
|
137
131
|
function nodeTotalLength(mergeTree: MergeTree, node: IMergeNode): number | undefined {
|
|
138
132
|
if (!node.isLeaf()) {
|
|
139
133
|
return node.cachedLength;
|
|
140
134
|
}
|
|
141
|
-
return mergeTree.
|
|
135
|
+
return mergeTree.leafLength(node);
|
|
142
136
|
}
|
|
143
137
|
|
|
144
138
|
const LRUSegmentComparer: IComparer<LRUSegment> = {
|
|
@@ -150,6 +144,7 @@ function ackSegment(
|
|
|
150
144
|
segment: ISegmentLeaf,
|
|
151
145
|
segmentGroup: SegmentGroup,
|
|
152
146
|
opArgs: IMergeTreeDeltaOpArgs,
|
|
147
|
+
stamp: OperationStamp,
|
|
153
148
|
): boolean {
|
|
154
149
|
const currentSegmentGroup = segment.segmentGroups?.dequeue();
|
|
155
150
|
assert(currentSegmentGroup === segmentGroup, 0x043 /* "On ack, unexpected segmentGroup!" */);
|
|
@@ -158,6 +153,7 @@ function ackSegment(
|
|
|
158
153
|
op,
|
|
159
154
|
sequencedMessage: { sequenceNumber, minimumSequenceNumber },
|
|
160
155
|
} = opArgs;
|
|
156
|
+
let allowIncrementalPartialLengthsUpdate = true;
|
|
161
157
|
switch (op.type) {
|
|
162
158
|
case MergeTreeDeltaType.ANNOTATE: {
|
|
163
159
|
assert(
|
|
@@ -165,51 +161,59 @@ function ackSegment(
|
|
|
165
161
|
0x044 /* "On annotate ack, missing segment property manager!" */,
|
|
166
162
|
);
|
|
167
163
|
segment.propertyManager.ack(sequenceNumber, minimumSequenceNumber, op);
|
|
168
|
-
|
|
164
|
+
break;
|
|
169
165
|
}
|
|
170
166
|
|
|
171
167
|
case MergeTreeDeltaType.INSERT: {
|
|
172
168
|
assert(
|
|
173
|
-
segment.
|
|
169
|
+
opstampUtils.isLocal(segment.insert),
|
|
174
170
|
0x045 /* "On insert, seq number already assigned!" */,
|
|
175
171
|
);
|
|
176
|
-
segment.seq = sequenceNumber;
|
|
177
|
-
segment.localSeq = undefined;
|
|
178
|
-
return true;
|
|
179
|
-
}
|
|
180
172
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
return true;
|
|
187
|
-
}
|
|
188
|
-
return false;
|
|
173
|
+
segment.insert = {
|
|
174
|
+
...stamp,
|
|
175
|
+
type: "insert",
|
|
176
|
+
};
|
|
177
|
+
break;
|
|
189
178
|
}
|
|
190
|
-
|
|
179
|
+
case MergeTreeDeltaType.REMOVE:
|
|
191
180
|
case MergeTreeDeltaType.OBLITERATE:
|
|
192
181
|
case MergeTreeDeltaType.OBLITERATE_SIDED: {
|
|
193
|
-
|
|
194
|
-
const
|
|
195
|
-
assert(
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
}
|
|
182
|
+
assertRemoved(segment);
|
|
183
|
+
const latestRemove = segment.removes[segment.removes.length - 1];
|
|
184
|
+
assert(
|
|
185
|
+
opstampUtils.isLocal(latestRemove),
|
|
186
|
+
0xb5d /* Expected last remove to be unacked */,
|
|
187
|
+
);
|
|
188
|
+
assert(
|
|
189
|
+
segment.removes.length === 1 ||
|
|
190
|
+
opstampUtils.isAcked(segment.removes[segment.removes.length - 2]),
|
|
191
|
+
0xb5e /* Expected prior remove to be acked */,
|
|
192
|
+
);
|
|
205
193
|
|
|
206
|
-
|
|
194
|
+
allowIncrementalPartialLengthsUpdate = segment.removes.length === 1;
|
|
195
|
+
const removeStamp: RemoveOperationStamp = {
|
|
196
|
+
...stamp,
|
|
197
|
+
type: op.type === MergeTreeDeltaType.REMOVE ? "setRemove" : "sliceRemove",
|
|
198
|
+
};
|
|
199
|
+
segment.removes[segment.removes.length - 1] = removeStamp;
|
|
200
|
+
|
|
201
|
+
const { obliterateInfo } = segmentGroup;
|
|
202
|
+
const hasObliterateInfo = obliterateInfo !== undefined;
|
|
203
|
+
const isObliterate = op.type !== MergeTreeDeltaType.REMOVE;
|
|
204
|
+
assert(hasObliterateInfo === isObliterate, 0xa40 /* must have obliterate info */);
|
|
205
|
+
if (hasObliterateInfo) {
|
|
206
|
+
obliterateInfo.stamp = removeStamp as SliceRemoveOperationStamp;
|
|
207
|
+
}
|
|
208
|
+
break;
|
|
207
209
|
}
|
|
208
210
|
|
|
209
211
|
default: {
|
|
210
212
|
throw new Error(`${op.type} is in unrecognized operation type`);
|
|
211
213
|
}
|
|
212
214
|
}
|
|
215
|
+
|
|
216
|
+
return allowIncrementalPartialLengthsUpdate;
|
|
213
217
|
}
|
|
214
218
|
|
|
215
219
|
/**
|
|
@@ -399,11 +403,7 @@ function getSlideToSegment(
|
|
|
399
403
|
cache?: Map<ISegmentLeaf, { seg?: ISegmentLeaf }>,
|
|
400
404
|
useNewSlidingBehavior: boolean = false,
|
|
401
405
|
): [ISegmentLeaf | undefined, "start" | "end" | undefined] {
|
|
402
|
-
if (
|
|
403
|
-
!segment ||
|
|
404
|
-
!isRemovedAndAckedOrMovedAndAcked(segment) ||
|
|
405
|
-
segment.endpointType !== undefined
|
|
406
|
-
) {
|
|
406
|
+
if (!segment || !isRemovedAndAcked(segment) || segment.endpointType !== undefined) {
|
|
407
407
|
return [segment, undefined];
|
|
408
408
|
}
|
|
409
409
|
|
|
@@ -414,14 +414,13 @@ function getSlideToSegment(
|
|
|
414
414
|
const result: { seg?: ISegmentLeaf } = {};
|
|
415
415
|
cache?.set(segment, result);
|
|
416
416
|
const goFurtherToFindSlideToSegment = (seg: ISegmentLeaf): boolean => {
|
|
417
|
-
if (seg.
|
|
417
|
+
if (opstampUtils.isAcked(seg.insert) && !isRemovedAndAcked(seg)) {
|
|
418
418
|
result.seg = seg;
|
|
419
419
|
return false;
|
|
420
420
|
}
|
|
421
421
|
if (
|
|
422
422
|
cache !== undefined &&
|
|
423
|
-
|
|
424
|
-
toMoveInfo(seg)?.movedSeq === toMoveInfo(segment)?.movedSeq)
|
|
423
|
+
toRemovalInfo(seg)?.removes[0].seq === toRemovalInfo(segment)?.removes[0].seq
|
|
425
424
|
) {
|
|
426
425
|
cache.set(seg, result);
|
|
427
426
|
}
|
|
@@ -506,10 +505,10 @@ const backwardPred = (ref: LocalReferencePosition): boolean =>
|
|
|
506
505
|
|
|
507
506
|
class Obliterates {
|
|
508
507
|
/**
|
|
509
|
-
* Array containing the all
|
|
508
|
+
* Array containing the all obliterate operations within the
|
|
510
509
|
* collab window.
|
|
511
510
|
*
|
|
512
|
-
* The
|
|
511
|
+
* The obliterates are stored in sequence order which accelerates clean up in setMinSeq
|
|
513
512
|
*
|
|
514
513
|
* See https://github.com/microsoft/FluidFramework/blob/main/packages/dds/merge-tree/docs/Obliterate.md#remote-perspective
|
|
515
514
|
* for additional context
|
|
@@ -528,7 +527,7 @@ class Obliterates {
|
|
|
528
527
|
|
|
529
528
|
public setMinSeq(minSeq: number): void {
|
|
530
529
|
// eslint-disable-next-line @typescript-eslint/no-non-null-asserted-optional-chain
|
|
531
|
-
while (!this.seqOrdered.empty && this.seqOrdered.first?.data.seq! <= minSeq) {
|
|
530
|
+
while (!this.seqOrdered.empty && this.seqOrdered.first?.data.stamp.seq! <= minSeq) {
|
|
532
531
|
const ob = this.seqOrdered.shift()!;
|
|
533
532
|
this.startOrdered.remove(ob.data.start);
|
|
534
533
|
this.mergeTree.removeLocalReferencePosition(ob.data.start);
|
|
@@ -537,7 +536,10 @@ class Obliterates {
|
|
|
537
536
|
}
|
|
538
537
|
|
|
539
538
|
public addOrUpdate(obliterateInfo: ObliterateInfo): void {
|
|
540
|
-
const {
|
|
539
|
+
const {
|
|
540
|
+
stamp: { seq },
|
|
541
|
+
start,
|
|
542
|
+
} = obliterateInfo;
|
|
541
543
|
if (seq !== UnassignedSequenceNumber) {
|
|
542
544
|
this.seqOrdered.push(obliterateInfo);
|
|
543
545
|
}
|
|
@@ -567,6 +569,17 @@ class Obliterates {
|
|
|
567
569
|
}
|
|
568
570
|
}
|
|
569
571
|
|
|
572
|
+
interface InsertResult {
|
|
573
|
+
/**
|
|
574
|
+
* If the insertion necessitated rebalancing, this field contains a `MergeBlock` that should be inserted after the block that `insertRecursive` was called on.
|
|
575
|
+
*/
|
|
576
|
+
remainder: MergeBlock | undefined;
|
|
577
|
+
/**
|
|
578
|
+
* Whether the insert changed anything (including recursive changes) in the subtree of the block that `insertRecursive` was called on.
|
|
579
|
+
*/
|
|
580
|
+
hadChanges: boolean;
|
|
581
|
+
}
|
|
582
|
+
|
|
570
583
|
/**
|
|
571
584
|
* @internal
|
|
572
585
|
*/
|
|
@@ -577,6 +590,11 @@ export class MergeTree {
|
|
|
577
590
|
zamboniSegments: true,
|
|
578
591
|
};
|
|
579
592
|
|
|
593
|
+
/**
|
|
594
|
+
* A sentinel value that indicates an inserting walk should continue to the next block sibling.
|
|
595
|
+
* This can occur for example when tie-break forces insertion of a segment past an entire block (and
|
|
596
|
+
* the inserting walk first recurses into the block before realizing that).
|
|
597
|
+
*/
|
|
580
598
|
private static readonly theUnfinishedNode = { childCount: -1 } as unknown as MergeBlock;
|
|
581
599
|
|
|
582
600
|
public readonly collabWindow = new CollaborationWindow();
|
|
@@ -587,9 +605,9 @@ export class MergeTree {
|
|
|
587
605
|
|
|
588
606
|
public readonly attributionPolicy: AttributionPolicy | undefined;
|
|
589
607
|
|
|
590
|
-
public localPerspective: Perspective
|
|
591
|
-
this.collabWindow.
|
|
592
|
-
|
|
608
|
+
public get localPerspective(): Perspective {
|
|
609
|
+
return this.collabWindow.localPerspective;
|
|
610
|
+
}
|
|
593
611
|
|
|
594
612
|
/**
|
|
595
613
|
* Whether or not all blocks in the mergeTree currently have information about local partial lengths computed.
|
|
@@ -630,76 +648,26 @@ export class MergeTree {
|
|
|
630
648
|
}
|
|
631
649
|
|
|
632
650
|
/**
|
|
633
|
-
* Compute the net length of this segment from
|
|
634
|
-
* @
|
|
635
|
-
*
|
|
636
|
-
* default is to consider the local client's current perspective. Only local sequence
|
|
637
|
-
* numbers corresponding to un-acked operations give valid results.
|
|
651
|
+
* Compute the net length of this segment leaf from some perspective.
|
|
652
|
+
* @returns - Undefined if the segment has been removed and its removal is common knowledge to all collaborators (and therefore
|
|
653
|
+
* may not even be present on clients that have loaded from a summary beyond this point). Otherwise, the length of the segment.
|
|
638
654
|
*/
|
|
639
|
-
public
|
|
655
|
+
public leafLength(
|
|
640
656
|
segment: ISegmentLeaf,
|
|
641
|
-
|
|
642
|
-
localSeq?: number,
|
|
657
|
+
perspective: Perspective = this.localPerspective,
|
|
643
658
|
): number | undefined {
|
|
644
659
|
const removalInfo = toRemovalInfo(segment);
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
}
|
|
654
|
-
// this segment removed and outside the collab window which means it is zamboni eligible
|
|
655
|
-
// this also means the segment could not exist, so we should not consider it
|
|
656
|
-
// when making decisions about conflict resolutions
|
|
657
|
-
return undefined;
|
|
658
|
-
} else {
|
|
659
|
-
return segment.cachedLength;
|
|
660
|
-
}
|
|
660
|
+
if (
|
|
661
|
+
removalInfo &&
|
|
662
|
+
getMinSeqPerspective(this.collabWindow).hasOccurred(removalInfo.removes[0])
|
|
663
|
+
) {
|
|
664
|
+
// this segment's removal has already moved outside the collab window which means it is zamboni eligible
|
|
665
|
+
// this also means the segment could be completely absent from other client's in-memory merge trees,
|
|
666
|
+
// so we should not consider it when making decisions about conflict resolutions
|
|
667
|
+
return undefined;
|
|
661
668
|
}
|
|
662
669
|
|
|
663
|
-
|
|
664
|
-
refSeq !== undefined,
|
|
665
|
-
0x398 /* localSeq provided for local length without refSeq */,
|
|
666
|
-
);
|
|
667
|
-
assert(segment.seq !== undefined, 0x399 /* segment with no seq in mergeTree */);
|
|
668
|
-
const { seq } = segment;
|
|
669
|
-
const { removedSeq, localRemovedSeq } = removalInfo ?? {};
|
|
670
|
-
const { movedSeq, localMovedSeq } = moveInfo ?? {};
|
|
671
|
-
if (seq === UnassignedSequenceNumber) {
|
|
672
|
-
assert(
|
|
673
|
-
segment.localSeq !== undefined,
|
|
674
|
-
0x39a /* unacked segment with undefined localSeq */,
|
|
675
|
-
);
|
|
676
|
-
// inserted locally, still un-acked
|
|
677
|
-
if (
|
|
678
|
-
segment.localSeq > localSeq ||
|
|
679
|
-
(localRemovedSeq !== undefined && localRemovedSeq <= localSeq) ||
|
|
680
|
-
(localMovedSeq !== undefined && localMovedSeq <= localSeq)
|
|
681
|
-
) {
|
|
682
|
-
return 0;
|
|
683
|
-
}
|
|
684
|
-
const { cachedLength } = segment;
|
|
685
|
-
return cachedLength;
|
|
686
|
-
} else {
|
|
687
|
-
// inserted remotely
|
|
688
|
-
if (
|
|
689
|
-
seq > refSeq ||
|
|
690
|
-
(removedSeq !== undefined &&
|
|
691
|
-
removedSeq !== UnassignedSequenceNumber &&
|
|
692
|
-
removedSeq <= refSeq) ||
|
|
693
|
-
(movedSeq !== undefined &&
|
|
694
|
-
movedSeq !== UnassignedSequenceNumber &&
|
|
695
|
-
movedSeq <= refSeq) ||
|
|
696
|
-
(localRemovedSeq !== undefined && localRemovedSeq <= localSeq) ||
|
|
697
|
-
(localMovedSeq !== undefined && localMovedSeq <= localSeq)
|
|
698
|
-
) {
|
|
699
|
-
return 0;
|
|
700
|
-
}
|
|
701
|
-
return segment.cachedLength;
|
|
702
|
-
}
|
|
670
|
+
return perspective.isSegmentPresent(segment) ? segment.cachedLength : 0;
|
|
703
671
|
}
|
|
704
672
|
|
|
705
673
|
public unlinkMarker(marker: Marker): void {
|
|
@@ -715,7 +683,7 @@ export class MergeTree {
|
|
|
715
683
|
return index;
|
|
716
684
|
}
|
|
717
685
|
|
|
718
|
-
public reloadFromSegments(segments: SegmentWithInfo<
|
|
686
|
+
public reloadFromSegments(segments: SegmentWithInfo<IHasInsertionInfo>[]): void {
|
|
719
687
|
// This code assumes that a later call to `startCollaboration()` will initialize partial lengths.
|
|
720
688
|
assert(
|
|
721
689
|
!this.collabWindow.collaborating,
|
|
@@ -772,6 +740,7 @@ export class MergeTree {
|
|
|
772
740
|
this.collabWindow.minSeq = minSeq;
|
|
773
741
|
this.collabWindow.collaborating = true;
|
|
774
742
|
this.collabWindow.currentSeq = currentSeq;
|
|
743
|
+
this.collabWindow.localPerspective = new LocalDefaultPerspective(localClientId);
|
|
775
744
|
this.nodeUpdateLengthNewStructure(this.root, true);
|
|
776
745
|
}
|
|
777
746
|
|
|
@@ -787,8 +756,8 @@ export class MergeTree {
|
|
|
787
756
|
}
|
|
788
757
|
}
|
|
789
758
|
|
|
790
|
-
public getLength(
|
|
791
|
-
return this.
|
|
759
|
+
public getLength(perspective: Perspective): number {
|
|
760
|
+
return this.nodeLength(this.root, perspective) ?? 0;
|
|
792
761
|
}
|
|
793
762
|
|
|
794
763
|
/**
|
|
@@ -798,12 +767,7 @@ export class MergeTree {
|
|
|
798
767
|
return this.root.cachedLength;
|
|
799
768
|
}
|
|
800
769
|
|
|
801
|
-
public getPosition(
|
|
802
|
-
node: IMergeNode,
|
|
803
|
-
refSeq: number,
|
|
804
|
-
clientId: number,
|
|
805
|
-
localSeq?: number,
|
|
806
|
-
): number {
|
|
770
|
+
public getPosition(node: IMergeNode, perspective: Perspective): number {
|
|
807
771
|
if (node.isLeaf() && node.endpointType === "start") {
|
|
808
772
|
return 0;
|
|
809
773
|
}
|
|
@@ -818,7 +782,7 @@ export class MergeTree {
|
|
|
818
782
|
if ((!!prevParent && child === prevParent) || child === node) {
|
|
819
783
|
break;
|
|
820
784
|
}
|
|
821
|
-
totalOffset += this.nodeLength(child,
|
|
785
|
+
totalOffset += this.nodeLength(child, perspective) ?? 0;
|
|
822
786
|
}
|
|
823
787
|
prevParent = parent;
|
|
824
788
|
parent = parent.parent;
|
|
@@ -828,17 +792,17 @@ export class MergeTree {
|
|
|
828
792
|
|
|
829
793
|
public getContainingSegment(
|
|
830
794
|
pos: number,
|
|
831
|
-
|
|
832
|
-
clientId: number,
|
|
833
|
-
localSeq?: number,
|
|
795
|
+
perspective: Perspective,
|
|
834
796
|
): {
|
|
835
797
|
segment: ISegmentLeaf | undefined;
|
|
836
798
|
offset: number | undefined;
|
|
837
799
|
} {
|
|
838
800
|
assert(
|
|
839
|
-
localSeq === undefined ||
|
|
801
|
+
perspective.localSeq === undefined ||
|
|
802
|
+
perspective.clientId === this.collabWindow.clientId,
|
|
840
803
|
0x39b /* localSeq provided for non-local client */,
|
|
841
804
|
);
|
|
805
|
+
|
|
842
806
|
let segment: ISegmentLeaf | undefined;
|
|
843
807
|
let offset: number | undefined;
|
|
844
808
|
|
|
@@ -847,7 +811,7 @@ export class MergeTree {
|
|
|
847
811
|
offset = start;
|
|
848
812
|
return false;
|
|
849
813
|
};
|
|
850
|
-
this.nodeMap(
|
|
814
|
+
this.nodeMap(perspective, leaf, undefined, pos, pos + 1);
|
|
851
815
|
return { segment, offset };
|
|
852
816
|
}
|
|
853
817
|
|
|
@@ -998,7 +962,7 @@ export class MergeTree {
|
|
|
998
962
|
const backwardSegmentCache = new Map<ISegmentLeaf, { seg?: ISegmentLeaf }>();
|
|
999
963
|
for (const segment of segments) {
|
|
1000
964
|
assert(
|
|
1001
|
-
|
|
965
|
+
isRemovedAndAcked(segment),
|
|
1002
966
|
0x2f1 /* slideReferences from a segment which has not been removed and acked */,
|
|
1003
967
|
);
|
|
1004
968
|
if (segment.localRefs === undefined || segment.localRefs.empty) {
|
|
@@ -1054,12 +1018,6 @@ export class MergeTree {
|
|
|
1054
1018
|
);
|
|
1055
1019
|
}
|
|
1056
1020
|
|
|
1057
|
-
private blockLength(node: MergeBlock, refSeq: number, clientId: number): number {
|
|
1058
|
-
return this.collabWindow.collaborating && clientId !== this.collabWindow.clientId
|
|
1059
|
-
? node.partialLengths!.getPartialLength(refSeq, clientId)
|
|
1060
|
-
: (node.cachedLength ?? 0);
|
|
1061
|
-
}
|
|
1062
|
-
|
|
1063
1021
|
/**
|
|
1064
1022
|
* Compute local partial length information
|
|
1065
1023
|
*
|
|
@@ -1084,83 +1042,35 @@ export class MergeTree {
|
|
|
1084
1042
|
this.localPartialsComputed = true;
|
|
1085
1043
|
}
|
|
1086
1044
|
|
|
1087
|
-
private nodeLength(
|
|
1088
|
-
node
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
localSeq?: number,
|
|
1092
|
-
): number | undefined {
|
|
1093
|
-
if (!this.collabWindow.collaborating || this.collabWindow.clientId === clientId) {
|
|
1094
|
-
if (node.isLeaf()) {
|
|
1095
|
-
return this.localNetLength(node, refSeq, localSeq);
|
|
1096
|
-
} else if (
|
|
1097
|
-
localSeq === undefined ||
|
|
1098
|
-
// All changes are visible. Small note on why we allow refSeq >= this.collabWindow.currentSeq rather than just equality:
|
|
1099
|
-
// merge-tree eventing occurs before the collab window is updated to account for whatever op it is processing, and we want
|
|
1100
|
-
// to support resolving positions from within the event handler which account for that op. e.g. undo-redo relies on this
|
|
1101
|
-
// behavior with local references.
|
|
1102
|
-
(localSeq === this.collabWindow.localSeq && refSeq >= this.collabWindow.currentSeq)
|
|
1103
|
-
) {
|
|
1104
|
-
// Local client sees all segments, even when collaborating
|
|
1105
|
-
return node.cachedLength;
|
|
1106
|
-
} else {
|
|
1107
|
-
this.computeLocalPartials(refSeq);
|
|
1108
|
-
|
|
1109
|
-
// Local client should see all segments except those after localSeq.
|
|
1110
|
-
const partialLen = node.partialLengths!.getPartialLength(refSeq, clientId, localSeq);
|
|
1111
|
-
|
|
1112
|
-
PartialSequenceLengths.options.verifyExpected?.(
|
|
1113
|
-
this,
|
|
1114
|
-
node,
|
|
1115
|
-
refSeq,
|
|
1116
|
-
clientId,
|
|
1117
|
-
localSeq,
|
|
1118
|
-
);
|
|
1119
|
-
|
|
1120
|
-
return partialLen;
|
|
1121
|
-
}
|
|
1122
|
-
} else {
|
|
1123
|
-
// Sequence number within window
|
|
1124
|
-
if (node.isLeaf()) {
|
|
1125
|
-
const segment = node;
|
|
1126
|
-
const removalInfo = toRemovalInfo(segment);
|
|
1127
|
-
const moveInfo = toMoveInfo(segment);
|
|
1045
|
+
private nodeLength(node: IMergeNode, perspective: Perspective): number | undefined {
|
|
1046
|
+
if (node.isLeaf()) {
|
|
1047
|
+
return this.leafLength(node, perspective);
|
|
1048
|
+
}
|
|
1128
1049
|
|
|
1129
|
-
|
|
1130
|
-
if (seqLTE(removalInfo.removedSeq, this.collabWindow.minSeq)) {
|
|
1131
|
-
return undefined;
|
|
1132
|
-
}
|
|
1133
|
-
if (
|
|
1134
|
-
seqLTE(removalInfo.removedSeq, refSeq) ||
|
|
1135
|
-
removalInfo.removedClientIds.includes(clientId)
|
|
1136
|
-
) {
|
|
1137
|
-
return 0;
|
|
1138
|
-
}
|
|
1139
|
-
}
|
|
1050
|
+
const { refSeq, clientId, localSeq } = perspective;
|
|
1140
1051
|
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1052
|
+
const isLocalPerspective =
|
|
1053
|
+
!this.collabWindow.collaborating || this.collabWindow.clientId === clientId;
|
|
1054
|
+
if (
|
|
1055
|
+
isLocalPerspective &&
|
|
1056
|
+
(localSeq === undefined ||
|
|
1057
|
+
(localSeq === this.collabWindow.localSeq && refSeq >= this.collabWindow.currentSeq))
|
|
1058
|
+
) {
|
|
1059
|
+
// All changes are visible. Small note on why we allow refSeq >= this.collabWindow.currentSeq rather than just equality:
|
|
1060
|
+
// merge-tree eventing occurs before the collab window is updated to account for whatever op it is processing, and we want
|
|
1061
|
+
// to support resolving positions from within the event handler which account for that op. e.g. undo-redo relies on this
|
|
1062
|
+
// behavior with local references.
|
|
1063
|
+
return node.cachedLength;
|
|
1064
|
+
}
|
|
1152
1065
|
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
} else {
|
|
1157
|
-
const partialLen = node.partialLengths!.getPartialLength(refSeq, clientId);
|
|
1066
|
+
if (localSeq !== undefined) {
|
|
1067
|
+
this.computeLocalPartials(refSeq);
|
|
1068
|
+
}
|
|
1158
1069
|
|
|
1159
|
-
|
|
1070
|
+
const length = node.partialLengths!.getPartialLength(refSeq, clientId, localSeq);
|
|
1160
1071
|
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
}
|
|
1072
|
+
PartialSequenceLengths.options.verifyExpected?.(this, node, refSeq, clientId, localSeq);
|
|
1073
|
+
return length;
|
|
1164
1074
|
}
|
|
1165
1075
|
|
|
1166
1076
|
public setMinSeq(minSeq: number): void {
|
|
@@ -1199,7 +1109,7 @@ export class MergeTree {
|
|
|
1199
1109
|
// from within event handlers, and the collab window's sequence numbers are not updated in time in all of those cases.
|
|
1200
1110
|
refSeq = Number.MAX_SAFE_INTEGER,
|
|
1201
1111
|
clientId = this.collabWindow.clientId,
|
|
1202
|
-
localSeq: number | undefined =
|
|
1112
|
+
localSeq: number | undefined = undefined,
|
|
1203
1113
|
): number {
|
|
1204
1114
|
const perspective =
|
|
1205
1115
|
clientId === this.collabWindow.clientId
|
|
@@ -1208,12 +1118,12 @@ export class MergeTree {
|
|
|
1208
1118
|
: new LocalReconnectingPerspective(refSeq, clientId, localSeq)
|
|
1209
1119
|
: new PriorPerspective(refSeq, clientId);
|
|
1210
1120
|
const seg = refPos.getSegment();
|
|
1211
|
-
if (!isSegmentLeaf(seg)) {
|
|
1121
|
+
if (seg === undefined || !isSegmentLeaf(seg)) {
|
|
1212
1122
|
// We have no idea where this reference is, because it refers to a segment which is not in the tree.
|
|
1213
1123
|
return DetachedReferencePosition;
|
|
1214
1124
|
}
|
|
1215
1125
|
if (refPos.isLeaf()) {
|
|
1216
|
-
return this.getPosition(seg,
|
|
1126
|
+
return this.getPosition(seg, perspective);
|
|
1217
1127
|
}
|
|
1218
1128
|
if (refTypeIncludesFlag(refPos, ReferenceType.Transient) || seg.localRefs?.has(refPos)) {
|
|
1219
1129
|
if (
|
|
@@ -1222,34 +1132,58 @@ export class MergeTree {
|
|
|
1222
1132
|
!perspective.isSegmentPresent(seg)
|
|
1223
1133
|
) {
|
|
1224
1134
|
const forward = refPos.slidingPreference === SlidingPreference.FORWARD;
|
|
1225
|
-
const moveInfo = toMoveInfo(seg);
|
|
1226
1135
|
const removeInfo = toRemovalInfo(seg);
|
|
1136
|
+
const firstRemove = removeInfo?.removes[0];
|
|
1227
1137
|
const slideSeq =
|
|
1228
|
-
|
|
1229
|
-
?
|
|
1230
|
-
:
|
|
1231
|
-
|
|
1232
|
-
: refSeq;
|
|
1233
|
-
const slideLocalSeq = moveInfo?.localMovedSeq ?? removeInfo?.localRemovedSeq;
|
|
1138
|
+
firstRemove !== undefined && opstampUtils.isAcked(firstRemove)
|
|
1139
|
+
? firstRemove.seq
|
|
1140
|
+
: refSeq;
|
|
1141
|
+
|
|
1234
1142
|
const slidePerspective =
|
|
1235
|
-
|
|
1143
|
+
firstRemove?.localSeq === undefined
|
|
1236
1144
|
? new PriorPerspective(slideSeq, this.collabWindow.clientId)
|
|
1237
1145
|
: new LocalReconnectingPerspective(
|
|
1238
1146
|
slideSeq,
|
|
1239
1147
|
this.collabWindow.clientId,
|
|
1240
|
-
|
|
1148
|
+
firstRemove.localSeq,
|
|
1241
1149
|
);
|
|
1242
|
-
|
|
1150
|
+
|
|
1151
|
+
const slidSegment = this.nextSegment(slidePerspective, seg, forward);
|
|
1243
1152
|
return (
|
|
1244
|
-
this.getPosition(slidSegment,
|
|
1153
|
+
this.getPosition(slidSegment, perspective) +
|
|
1245
1154
|
(forward ? 0 : slidSegment.cachedLength === 0 ? 0 : slidSegment.cachedLength - 1)
|
|
1246
1155
|
);
|
|
1247
1156
|
}
|
|
1248
|
-
return this.getPosition(seg,
|
|
1157
|
+
return this.getPosition(seg, perspective) + refPos.getOffset();
|
|
1249
1158
|
}
|
|
1250
1159
|
return DetachedReferencePosition;
|
|
1251
1160
|
}
|
|
1252
1161
|
|
|
1162
|
+
/**
|
|
1163
|
+
* Returns the immediately adjacent segment in the specified direction from this perspective.
|
|
1164
|
+
* There may actually be multiple segments between the given segment and the returned segment,
|
|
1165
|
+
* but they were either inserted after this perspective, or have been removed before this perspective.
|
|
1166
|
+
*
|
|
1167
|
+
* @param segment - The segment to start from.
|
|
1168
|
+
* @param forward - The direction to search.
|
|
1169
|
+
* @returns the next segment in the specified direction, or the start or end of the tree if there is no next segment.
|
|
1170
|
+
*/
|
|
1171
|
+
private nextSegment(
|
|
1172
|
+
perspective: Perspective,
|
|
1173
|
+
segment: ISegmentLeaf,
|
|
1174
|
+
forward: boolean = true,
|
|
1175
|
+
): ISegmentLeaf {
|
|
1176
|
+
let next: ISegmentLeaf | undefined;
|
|
1177
|
+
const action = (seg: ISegmentLeaf): boolean | undefined => {
|
|
1178
|
+
if (perspective.isSegmentPresent(seg)) {
|
|
1179
|
+
next = seg;
|
|
1180
|
+
return LeafAction.Exit;
|
|
1181
|
+
}
|
|
1182
|
+
};
|
|
1183
|
+
(forward ? forwardExcursion : backwardExcursion)(segment, action);
|
|
1184
|
+
return next ?? (forward ? this.endOfTree : this.startOfTree);
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1253
1187
|
/**
|
|
1254
1188
|
* Finds the nearest reference with ReferenceType.Tile to `startPos` in the direction dictated by `forwards`.
|
|
1255
1189
|
* Uses depthFirstNodeWalk in addition to block-accelerated functionality. The search position will be included in
|
|
@@ -1264,13 +1198,12 @@ export class MergeTree {
|
|
|
1264
1198
|
*/
|
|
1265
1199
|
public searchForMarker(
|
|
1266
1200
|
startPos: number,
|
|
1267
|
-
clientId: number,
|
|
1268
1201
|
markerLabel: string,
|
|
1269
1202
|
forwards = true,
|
|
1270
1203
|
): Marker | undefined {
|
|
1271
1204
|
let foundMarker: Marker | undefined;
|
|
1272
1205
|
|
|
1273
|
-
const { segment } = this.getContainingSegment(startPos,
|
|
1206
|
+
const { segment } = this.getContainingSegment(startPos, this.localPerspective);
|
|
1274
1207
|
if (!isSegmentLeaf(segment)) {
|
|
1275
1208
|
return undefined;
|
|
1276
1209
|
}
|
|
@@ -1305,15 +1238,13 @@ export class MergeTree {
|
|
|
1305
1238
|
return foundMarker;
|
|
1306
1239
|
}
|
|
1307
1240
|
|
|
1308
|
-
private updateRoot(splitNode: MergeBlock
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
this.nodeUpdateLengthNewStructure(this.root);
|
|
1316
|
-
}
|
|
1241
|
+
private updateRoot(splitNode: MergeBlock): void {
|
|
1242
|
+
const newRoot = this.makeBlock(2);
|
|
1243
|
+
assignChild(newRoot, this.root, 0, false);
|
|
1244
|
+
assignChild(newRoot, splitNode, 1, false);
|
|
1245
|
+
this.root = newRoot;
|
|
1246
|
+
this.nodeUpdateOrdinals(this.root);
|
|
1247
|
+
this.nodeUpdateLengthNewStructure(this.root);
|
|
1317
1248
|
}
|
|
1318
1249
|
|
|
1319
1250
|
/**
|
|
@@ -1322,6 +1253,10 @@ export class MergeTree {
|
|
|
1322
1253
|
*/
|
|
1323
1254
|
public ackPendingSegment(opArgs: IMergeTreeDeltaOpArgs): void {
|
|
1324
1255
|
const seq = opArgs.sequencedMessage!.sequenceNumber;
|
|
1256
|
+
const stamp: OperationStamp = {
|
|
1257
|
+
seq,
|
|
1258
|
+
clientId: this.collabWindow.clientId,
|
|
1259
|
+
};
|
|
1325
1260
|
const pendingSegmentGroup = this.pendingSegments.shift()?.data;
|
|
1326
1261
|
const nodesToUpdate: MergeBlock[] = [];
|
|
1327
1262
|
let overwrite = false;
|
|
@@ -1329,9 +1264,14 @@ export class MergeTree {
|
|
|
1329
1264
|
const deltaSegments: IMergeTreeSegmentDelta[] = [];
|
|
1330
1265
|
const overlappingRemoves: boolean[] = [];
|
|
1331
1266
|
pendingSegmentGroup.segments.map((pendingSegment: ISegmentLeaf) => {
|
|
1332
|
-
const overlappingRemove = !ackSegment(
|
|
1267
|
+
const overlappingRemove = !ackSegment(
|
|
1268
|
+
pendingSegment,
|
|
1269
|
+
pendingSegmentGroup,
|
|
1270
|
+
opArgs,
|
|
1271
|
+
stamp,
|
|
1272
|
+
);
|
|
1333
1273
|
|
|
1334
|
-
overwrite ||= overlappingRemove
|
|
1274
|
+
overwrite ||= overlappingRemove;
|
|
1335
1275
|
|
|
1336
1276
|
overlappingRemoves.push(overlappingRemove);
|
|
1337
1277
|
if (MergeTree.options.zamboniSegments) {
|
|
@@ -1346,7 +1286,7 @@ export class MergeTree {
|
|
|
1346
1286
|
});
|
|
1347
1287
|
|
|
1348
1288
|
if (pendingSegmentGroup.obliterateInfo !== undefined) {
|
|
1349
|
-
pendingSegmentGroup.obliterateInfo.
|
|
1289
|
+
pendingSegmentGroup.obliterateInfo.stamp = { type: "sliceRemove", ...stamp };
|
|
1350
1290
|
this.obliterates.addOrUpdate(pendingSegmentGroup.obliterateInfo);
|
|
1351
1291
|
}
|
|
1352
1292
|
|
|
@@ -1367,9 +1307,9 @@ export class MergeTree {
|
|
|
1367
1307
|
},
|
|
1368
1308
|
opArgs,
|
|
1369
1309
|
);
|
|
1370
|
-
|
|
1310
|
+
|
|
1371
1311
|
for (const node of nodesToUpdate) {
|
|
1372
|
-
this.blockUpdatePathLengths(node,
|
|
1312
|
+
this.blockUpdatePathLengths(node, stamp, overwrite);
|
|
1373
1313
|
}
|
|
1374
1314
|
}
|
|
1375
1315
|
if (MergeTree.options.zamboniSegments) {
|
|
@@ -1415,11 +1355,7 @@ export class MergeTree {
|
|
|
1415
1355
|
// TODO: error checking
|
|
1416
1356
|
public getMarkerFromId(id: string): Marker | undefined {
|
|
1417
1357
|
const marker = this.idToMarker.get(id);
|
|
1418
|
-
return marker === undefined ||
|
|
1419
|
-
isRemoved(marker) ||
|
|
1420
|
-
(isMoved(marker) && marker.moveDst === undefined)
|
|
1421
|
-
? undefined
|
|
1422
|
-
: marker;
|
|
1358
|
+
return marker === undefined || isRemoved(marker) ? undefined : marker;
|
|
1423
1359
|
}
|
|
1424
1360
|
|
|
1425
1361
|
/**
|
|
@@ -1429,18 +1365,14 @@ export class MergeTree {
|
|
|
1429
1365
|
* @param refseq - The reference sequence number at which to compute the position.
|
|
1430
1366
|
* @param clientId - The client id with which to compute the position.
|
|
1431
1367
|
*/
|
|
1432
|
-
public posFromRelativePos(
|
|
1433
|
-
relativePos: IRelativePosition,
|
|
1434
|
-
refseq = this.collabWindow.currentSeq,
|
|
1435
|
-
clientId = this.collabWindow.clientId,
|
|
1436
|
-
): number {
|
|
1368
|
+
public posFromRelativePos(relativePos: IRelativePosition, perspective: Perspective): number {
|
|
1437
1369
|
let pos = -1;
|
|
1438
1370
|
let marker: Marker | undefined;
|
|
1439
1371
|
if (relativePos.id) {
|
|
1440
1372
|
marker = this.getMarkerFromId(relativePos.id);
|
|
1441
1373
|
}
|
|
1442
1374
|
if (isSegmentLeaf(marker)) {
|
|
1443
|
-
pos = this.getPosition(marker,
|
|
1375
|
+
pos = this.getPosition(marker, perspective);
|
|
1444
1376
|
if (relativePos.before) {
|
|
1445
1377
|
if (relativePos.offset !== undefined) {
|
|
1446
1378
|
pos -= relativePos.offset;
|
|
@@ -1458,22 +1390,19 @@ export class MergeTree {
|
|
|
1458
1390
|
public insertSegments(
|
|
1459
1391
|
pos: number,
|
|
1460
1392
|
segments: ISegmentPrivate[],
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
seq: number,
|
|
1393
|
+
perspective: Perspective,
|
|
1394
|
+
stampArg: OperationStamp,
|
|
1464
1395
|
opArgs: IMergeTreeDeltaOpArgs | undefined,
|
|
1465
1396
|
): void {
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
const localSeq =
|
|
1469
|
-
seq === UnassignedSequenceNumber ? ++this.collabWindow.localSeq : undefined;
|
|
1397
|
+
const stamp: InsertOperationStamp = { ...stampArg, type: "insert" };
|
|
1398
|
+
this.ensureIntervalBoundary(pos, perspective);
|
|
1470
1399
|
|
|
1471
|
-
this.blockInsert(pos,
|
|
1400
|
+
this.blockInsert(pos, perspective, stamp, segments);
|
|
1472
1401
|
|
|
1473
1402
|
// opArgs == undefined => loading snapshot or test code
|
|
1474
1403
|
if (opArgs !== undefined) {
|
|
1475
1404
|
const deltaSegments = segments
|
|
1476
|
-
.filter((segment) => !
|
|
1405
|
+
.filter((segment) => !isRemoved(segment))
|
|
1477
1406
|
.map((segment) => ({ segment }));
|
|
1478
1407
|
|
|
1479
1408
|
if (deltaSegments.length > 0) {
|
|
@@ -1487,7 +1416,7 @@ export class MergeTree {
|
|
|
1487
1416
|
if (
|
|
1488
1417
|
this.collabWindow.collaborating &&
|
|
1489
1418
|
MergeTree.options.zamboniSegments &&
|
|
1490
|
-
|
|
1419
|
+
opstampUtils.isAcked(stamp)
|
|
1491
1420
|
) {
|
|
1492
1421
|
zamboniSegments(this);
|
|
1493
1422
|
}
|
|
@@ -1516,30 +1445,23 @@ export class MergeTree {
|
|
|
1516
1445
|
return undefined;
|
|
1517
1446
|
}
|
|
1518
1447
|
|
|
1519
|
-
const
|
|
1520
|
-
|
|
1521
|
-
remoteClientRefSeq,
|
|
1522
|
-
remoteClientId,
|
|
1523
|
-
);
|
|
1524
|
-
|
|
1525
|
-
const { currentSeq, clientId } = this.collabWindow;
|
|
1448
|
+
const remotePerspective = new PriorPerspective(remoteClientRefSeq, remoteClientId);
|
|
1449
|
+
const segmentInfo = this.getContainingSegment(remoteClientPosition, remotePerspective);
|
|
1526
1450
|
|
|
1527
1451
|
if (isSegmentLeaf(segmentInfo?.segment)) {
|
|
1528
|
-
const segmentPosition = this.getPosition(segmentInfo.segment,
|
|
1452
|
+
const segmentPosition = this.getPosition(segmentInfo.segment, this.localPerspective);
|
|
1529
1453
|
return segmentPosition + segmentInfo.offset!;
|
|
1530
1454
|
} else {
|
|
1531
|
-
if (remoteClientPosition === this.getLength(
|
|
1532
|
-
return this.getLength(
|
|
1455
|
+
if (remoteClientPosition === this.getLength(remotePerspective)) {
|
|
1456
|
+
return this.getLength(this.localPerspective);
|
|
1533
1457
|
}
|
|
1534
1458
|
}
|
|
1535
1459
|
}
|
|
1536
1460
|
|
|
1537
1461
|
private blockInsert<T extends ISegmentPrivate>(
|
|
1538
1462
|
pos: number,
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
seq: number,
|
|
1542
|
-
localSeq: number | undefined,
|
|
1463
|
+
perspective: Perspective,
|
|
1464
|
+
stamp: InsertOperationStamp,
|
|
1543
1465
|
newSegments: T[],
|
|
1544
1466
|
): void {
|
|
1545
1467
|
// Keeping this function within the scope of blockInsert for readability.
|
|
@@ -1558,19 +1480,19 @@ export class MergeTree {
|
|
|
1558
1480
|
// Save segment so we can assign sequence number when acked by server
|
|
1559
1481
|
if (this.collabWindow.collaborating) {
|
|
1560
1482
|
if (
|
|
1561
|
-
locSegment.
|
|
1562
|
-
clientId === this.collabWindow.clientId
|
|
1483
|
+
opstampUtils.isLocal(locSegment.insert) &&
|
|
1484
|
+
stamp.clientId === this.collabWindow.clientId
|
|
1563
1485
|
) {
|
|
1564
|
-
segmentGroup = this.addToPendingList(locSegment, segmentGroup, localSeq);
|
|
1486
|
+
segmentGroup = this.addToPendingList(locSegment, segmentGroup, stamp.localSeq);
|
|
1565
1487
|
}
|
|
1566
1488
|
// LocSegment.seq === 0 when coming from SharedSegmentSequence.loadBody()
|
|
1567
1489
|
// In all other cases this has to be true (checked by addToLRUSet):
|
|
1568
1490
|
// locSegment.seq > this.collabWindow.currentSeq
|
|
1569
1491
|
else if (
|
|
1570
|
-
|
|
1571
|
-
|
|
1492
|
+
MergeTree.options.zamboniSegments &&
|
|
1493
|
+
opstampUtils.greaterThan(locSegment.insert, getMinSeqStamp(this.collabWindow))
|
|
1572
1494
|
) {
|
|
1573
|
-
this.addToLRUSet(locSegment, locSegment.seq);
|
|
1495
|
+
this.addToLRUSet(locSegment, locSegment.insert.seq);
|
|
1574
1496
|
}
|
|
1575
1497
|
}
|
|
1576
1498
|
};
|
|
@@ -1592,16 +1514,11 @@ export class MergeTree {
|
|
|
1592
1514
|
return segmentChanges;
|
|
1593
1515
|
};
|
|
1594
1516
|
|
|
1595
|
-
const insertInfo: IInsertionInfo = {
|
|
1596
|
-
clientId,
|
|
1597
|
-
seq,
|
|
1598
|
-
localSeq,
|
|
1599
|
-
};
|
|
1600
1517
|
// TODO: build tree from segs and insert all at once
|
|
1601
1518
|
let insertPos = pos;
|
|
1602
1519
|
for (const newSegment of newSegments
|
|
1603
1520
|
.filter((s) => s.cachedLength > 0)
|
|
1604
|
-
.map((s) => overwriteInfo(s,
|
|
1521
|
+
.map((s) => overwriteInfo(s, { insert: stamp }))) {
|
|
1605
1522
|
if (Marker.is(newSegment)) {
|
|
1606
1523
|
const markerId = newSegment.getId();
|
|
1607
1524
|
if (markerId) {
|
|
@@ -1609,7 +1526,7 @@ export class MergeTree {
|
|
|
1609
1526
|
}
|
|
1610
1527
|
}
|
|
1611
1528
|
|
|
1612
|
-
|
|
1529
|
+
this.insertingWalk(insertPos, perspective, stamp, {
|
|
1613
1530
|
leaf: onLeaf,
|
|
1614
1531
|
candidateSegment: newSegment,
|
|
1615
1532
|
continuePredicate: continueFrom,
|
|
@@ -1617,16 +1534,15 @@ export class MergeTree {
|
|
|
1617
1534
|
|
|
1618
1535
|
if (!isSegmentLeaf(newSegment)) {
|
|
1619
1536
|
// Indicates an attempt to insert past the end of the merge-tree's content.
|
|
1620
|
-
const errorConstructor =
|
|
1537
|
+
const errorConstructor =
|
|
1538
|
+
stamp.localSeq === undefined ? DataProcessingError : UsageError;
|
|
1621
1539
|
throw new errorConstructor("MergeTree insert failed", {
|
|
1622
1540
|
currentSeq: this.collabWindow.currentSeq,
|
|
1623
1541
|
minSeq: this.collabWindow.minSeq,
|
|
1624
|
-
segSeq:
|
|
1542
|
+
segSeq: stamp.seq,
|
|
1625
1543
|
});
|
|
1626
1544
|
}
|
|
1627
1545
|
|
|
1628
|
-
this.updateRoot(splitNode);
|
|
1629
|
-
|
|
1630
1546
|
insertPos += newSegment.cachedLength;
|
|
1631
1547
|
|
|
1632
1548
|
if (!this.options?.mergeTreeEnableObliterate || this.obliterates.empty()) {
|
|
@@ -1634,55 +1550,48 @@ export class MergeTree {
|
|
|
1634
1550
|
continue;
|
|
1635
1551
|
}
|
|
1636
1552
|
|
|
1553
|
+
const overlappingAckedObliterates: RemoveOperationStamp[] = [];
|
|
1637
1554
|
let oldest: ObliterateInfo | undefined;
|
|
1638
|
-
let normalizedOldestSeq: number = 0;
|
|
1639
1555
|
let newest: ObliterateInfo | undefined;
|
|
1640
|
-
let normalizedNewestSeq: number = 0;
|
|
1641
|
-
const movedClientIds: number[] = [];
|
|
1642
|
-
const movedSeqs: number[] = [];
|
|
1643
1556
|
let newestAcked: ObliterateInfo | undefined;
|
|
1644
1557
|
let oldestUnacked: ObliterateInfo | undefined;
|
|
1558
|
+
const refSeqStamp: OperationStamp = {
|
|
1559
|
+
seq: perspective.refSeq,
|
|
1560
|
+
clientId: stamp.clientId,
|
|
1561
|
+
localSeq: stamp.localSeq,
|
|
1562
|
+
};
|
|
1645
1563
|
for (const ob of this.obliterates.findOverlapping(newSegment)) {
|
|
1646
|
-
|
|
1647
|
-
// but is still comparable to remote seqs to keep the checks below easy
|
|
1648
|
-
// REMOTE SEQUENCE NUMBERS LOCAL SEQUENCE NUMBERS
|
|
1649
|
-
// [0, 1, 2, 3, ..., 100, ..., 1000, ..., (MAX - MaxLocalSeq), L1, L2, L3, L4, ..., L100, ..., L1000, ...(MAX)]
|
|
1650
|
-
const normalizedObSeq =
|
|
1651
|
-
ob.seq === UnassignedSequenceNumber
|
|
1652
|
-
? Number.MAX_SAFE_INTEGER - this.collabWindow.localSeq + ob.localSeq!
|
|
1653
|
-
: ob.seq;
|
|
1654
|
-
if (normalizedObSeq > refSeq) {
|
|
1564
|
+
if (opstampUtils.greaterThan(ob.stamp, refSeqStamp)) {
|
|
1655
1565
|
// Any obliterate from the same client that's inserting this segment cannot cause the segment to be marked as
|
|
1656
1566
|
// obliterated (since that client must have performed the obliterate before this insertion).
|
|
1657
1567
|
// We still need to consider such obliterates when determining the winning obliterate for the insertion point,
|
|
1658
1568
|
// see `obliteratePrecedingInsertion` docs.
|
|
1659
|
-
if (clientId !== ob.clientId) {
|
|
1660
|
-
if (
|
|
1661
|
-
|
|
1569
|
+
if (stamp.clientId !== ob.stamp.clientId) {
|
|
1570
|
+
if (opstampUtils.isAcked(ob.stamp)) {
|
|
1571
|
+
overlappingAckedObliterates.push(ob.stamp);
|
|
1572
|
+
}
|
|
1573
|
+
|
|
1574
|
+
if (oldest === undefined || opstampUtils.lessThan(ob.stamp, oldest.stamp)) {
|
|
1662
1575
|
oldest = ob;
|
|
1663
|
-
movedClientIds.unshift(ob.clientId);
|
|
1664
|
-
movedSeqs.unshift(ob.seq);
|
|
1665
|
-
} else {
|
|
1666
|
-
movedClientIds.push(ob.clientId);
|
|
1667
|
-
movedSeqs.push(ob.seq);
|
|
1668
1576
|
}
|
|
1669
1577
|
}
|
|
1670
1578
|
|
|
1671
|
-
if (newest === undefined ||
|
|
1672
|
-
normalizedNewestSeq = normalizedObSeq;
|
|
1579
|
+
if (newest === undefined || opstampUtils.greaterThan(ob.stamp, newest.stamp)) {
|
|
1673
1580
|
newest = ob;
|
|
1674
1581
|
}
|
|
1675
1582
|
|
|
1676
1583
|
if (
|
|
1677
|
-
ob.
|
|
1678
|
-
(newestAcked === undefined ||
|
|
1584
|
+
opstampUtils.isAcked(ob.stamp) &&
|
|
1585
|
+
(newestAcked === undefined ||
|
|
1586
|
+
opstampUtils.greaterThan(ob.stamp, newestAcked.stamp))
|
|
1679
1587
|
) {
|
|
1680
1588
|
newestAcked = ob;
|
|
1681
1589
|
}
|
|
1682
1590
|
|
|
1683
1591
|
if (
|
|
1684
|
-
ob.
|
|
1685
|
-
(oldestUnacked === undefined ||
|
|
1592
|
+
opstampUtils.isLocal(ob.stamp) &&
|
|
1593
|
+
(oldestUnacked === undefined ||
|
|
1594
|
+
opstampUtils.greaterThan(oldestUnacked.stamp, ob.stamp))
|
|
1686
1595
|
) {
|
|
1687
1596
|
// There can be one local obliterate surrounding a segment if a client repeatedly obliterates
|
|
1688
1597
|
// a region (ex: in the text ABCDEFG, obliterate D, then obliterate CE, then BF). In this case,
|
|
@@ -1696,40 +1605,27 @@ export class MergeTree {
|
|
|
1696
1605
|
// See doc comment on obliteratePrecedingInsertion for more details: if the newest obliterate was performed
|
|
1697
1606
|
// by the same client that's inserting this segment, we let them insert into this range and therefore don't
|
|
1698
1607
|
// mark it obliterated.
|
|
1699
|
-
if (oldest && newest?.clientId !== clientId) {
|
|
1700
|
-
|
|
1701
|
-
if (newestAcked === newest || newestAcked?.clientId !== clientId) {
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
localMovedSeq: oldestUnacked?.localSeq,
|
|
1707
|
-
};
|
|
1708
|
-
} else {
|
|
1709
|
-
assert(
|
|
1710
|
-
oldestUnacked !== undefined,
|
|
1711
|
-
0xb55 /* Expected local obliterate to be defined if newestAcked is not equal to newest */,
|
|
1712
|
-
);
|
|
1713
|
-
// There's a pending local obliterate for this range, so it will be marked as obliterated by us. However,
|
|
1714
|
-
// all other clients are under the impression that the most recent acked obliterate won the right to insert
|
|
1715
|
-
// in this range.
|
|
1716
|
-
moveInfo = {
|
|
1717
|
-
movedClientIds: [oldestUnacked.clientId],
|
|
1718
|
-
movedSeq: oldestUnacked.seq,
|
|
1719
|
-
movedSeqs: [oldestUnacked.seq],
|
|
1720
|
-
localMovedSeq: oldestUnacked.localSeq,
|
|
1721
|
-
};
|
|
1608
|
+
if (oldest && newest?.stamp.clientId !== stamp.clientId) {
|
|
1609
|
+
const removeInfo: IHasRemovalInfo = { removes: [] };
|
|
1610
|
+
if (newestAcked === newest || newestAcked?.stamp.clientId !== stamp.clientId) {
|
|
1611
|
+
removeInfo.removes = overlappingAckedObliterates;
|
|
1612
|
+
// Because we found these by looking at overlapping obliterates, they are not necessarily currently sorted by seq.
|
|
1613
|
+
// Address that now.
|
|
1614
|
+
removeInfo.removes.sort(opstampUtils.compare);
|
|
1722
1615
|
}
|
|
1723
1616
|
|
|
1724
|
-
|
|
1617
|
+
// Note that we don't need to worry about preserving any existing remove information since the segment is new.
|
|
1618
|
+
overwriteInfo(newSegment, removeInfo);
|
|
1619
|
+
|
|
1620
|
+
if (oldestUnacked !== undefined) {
|
|
1621
|
+
removeInfo.removes.push(oldestUnacked.stamp);
|
|
1725
1622
|
|
|
1726
|
-
if (moveInfo.localMovedSeq !== undefined) {
|
|
1727
1623
|
assert(
|
|
1728
|
-
oldestUnacked
|
|
1624
|
+
oldestUnacked.segmentGroup !== undefined,
|
|
1729
1625
|
0x86c /* expected segment group to exist */,
|
|
1730
1626
|
);
|
|
1731
1627
|
|
|
1732
|
-
this.addToPendingList(newSegment, oldestUnacked
|
|
1628
|
+
this.addToPendingList(newSegment, oldestUnacked.segmentGroup);
|
|
1733
1629
|
}
|
|
1734
1630
|
|
|
1735
1631
|
if (newSegment.parent) {
|
|
@@ -1738,12 +1634,7 @@ export class MergeTree {
|
|
|
1738
1634
|
// lengths inside the inserting walk, we'd be at risk of double-counting the insertion in any case if we allow
|
|
1739
1635
|
// incremental updates here.
|
|
1740
1636
|
const newStructure = true;
|
|
1741
|
-
this.blockUpdatePathLengths(
|
|
1742
|
-
newSegment.parent,
|
|
1743
|
-
moveInfo.movedSeq,
|
|
1744
|
-
clientId,
|
|
1745
|
-
newStructure,
|
|
1746
|
-
);
|
|
1637
|
+
this.blockUpdatePathLengths(newSegment.parent, removeInfo.removes[0], newStructure);
|
|
1747
1638
|
}
|
|
1748
1639
|
}
|
|
1749
1640
|
|
|
@@ -1786,55 +1677,55 @@ export class MergeTree {
|
|
|
1786
1677
|
return { next };
|
|
1787
1678
|
};
|
|
1788
1679
|
|
|
1789
|
-
private ensureIntervalBoundary(pos: number,
|
|
1790
|
-
|
|
1791
|
-
this.root,
|
|
1680
|
+
private ensureIntervalBoundary(pos: number, perspective: Perspective): void {
|
|
1681
|
+
this.insertingWalk(
|
|
1792
1682
|
pos,
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1683
|
+
perspective,
|
|
1684
|
+
{
|
|
1685
|
+
seq: TreeMaintenanceSequenceNumber,
|
|
1686
|
+
clientId: perspective.clientId,
|
|
1687
|
+
},
|
|
1796
1688
|
{ leaf: this.splitLeafSegment },
|
|
1797
1689
|
);
|
|
1798
|
-
this.updateRoot(splitNode);
|
|
1799
1690
|
}
|
|
1800
1691
|
|
|
1801
1692
|
// Assume called only when pos == len
|
|
1802
|
-
private breakTie(pos: number, node: IMergeNode,
|
|
1693
|
+
private breakTie(pos: number, node: IMergeNode, insertStamp: OperationStamp): boolean {
|
|
1803
1694
|
if (node.isLeaf()) {
|
|
1804
1695
|
if (pos !== 0) {
|
|
1805
1696
|
return false;
|
|
1806
1697
|
}
|
|
1807
1698
|
|
|
1808
|
-
// normalize the seq numbers
|
|
1809
|
-
// if the new seg is local (UnassignedSequenceNumber) give it the highest possible
|
|
1810
|
-
// seq for comparison, as it will get a seq higher than any other seq once sequences
|
|
1811
|
-
// if the current seg is local (UnassignedSequenceNumber) give it the second highest
|
|
1812
|
-
// possible seq, as the highest is reserved for the previous.
|
|
1813
|
-
const newSeq = seq === UnassignedSequenceNumber ? Number.MAX_SAFE_INTEGER : seq;
|
|
1814
|
-
const segSeq =
|
|
1815
|
-
node.seq === UnassignedSequenceNumber ? Number.MAX_SAFE_INTEGER - 1 : (node.seq ?? 0);
|
|
1816
|
-
|
|
1817
1699
|
return (
|
|
1818
|
-
|
|
1819
|
-
(isMoved(node) && node.movedSeq !== UnassignedSequenceNumber && node.movedSeq > seq) ||
|
|
1700
|
+
opstampUtils.greaterThan(insertStamp, node.insert) ||
|
|
1820
1701
|
(isRemoved(node) &&
|
|
1821
|
-
node.
|
|
1822
|
-
node.
|
|
1702
|
+
opstampUtils.isAcked(node.removes[0]) &&
|
|
1703
|
+
opstampUtils.greaterThan(node.removes[0], insertStamp))
|
|
1823
1704
|
);
|
|
1824
1705
|
} else {
|
|
1825
1706
|
return true;
|
|
1826
1707
|
}
|
|
1827
1708
|
}
|
|
1828
|
-
|
|
1829
1709
|
private insertingWalk(
|
|
1710
|
+
pos: number,
|
|
1711
|
+
perspective: Perspective,
|
|
1712
|
+
stamp: OperationStamp,
|
|
1713
|
+
context: InsertContext,
|
|
1714
|
+
): void {
|
|
1715
|
+
const { remainder } = this.insertRecursive(this.root, pos, perspective, stamp, context);
|
|
1716
|
+
if (remainder !== undefined) {
|
|
1717
|
+
this.updateRoot(remainder);
|
|
1718
|
+
}
|
|
1719
|
+
}
|
|
1720
|
+
|
|
1721
|
+
private insertRecursive(
|
|
1830
1722
|
block: MergeBlock,
|
|
1831
1723
|
pos: number,
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
seq: number,
|
|
1724
|
+
perspective: Perspective,
|
|
1725
|
+
stamp: OperationStamp,
|
|
1835
1726
|
context: InsertContext,
|
|
1836
1727
|
isLastChildBlock: boolean = true,
|
|
1837
|
-
):
|
|
1728
|
+
): InsertResult {
|
|
1838
1729
|
let _pos: number = pos;
|
|
1839
1730
|
|
|
1840
1731
|
const children = block.children;
|
|
@@ -1842,13 +1733,13 @@ export class MergeTree {
|
|
|
1842
1733
|
let child: IMergeNode;
|
|
1843
1734
|
let newNode: IMergeNodeBuilder | undefined;
|
|
1844
1735
|
let fromSplit: MergeBlock | undefined;
|
|
1736
|
+
let hadChanges = false;
|
|
1845
1737
|
for (childIndex = 0; childIndex < block.childCount; childIndex++) {
|
|
1846
1738
|
child = children[childIndex];
|
|
1847
1739
|
// ensure we walk down the far edge of the tree, even if all sub-tree is eligible for zamboni
|
|
1848
1740
|
const isLastNonLeafBlock =
|
|
1849
1741
|
isLastChildBlock && !child.isLeaf() && childIndex === block.childCount - 1;
|
|
1850
|
-
const len =
|
|
1851
|
-
this.nodeLength(child, refSeq, clientId) ?? (isLastChildBlock ? 0 : undefined);
|
|
1742
|
+
const len = this.nodeLength(child, perspective) ?? (isLastChildBlock ? 0 : undefined);
|
|
1852
1743
|
|
|
1853
1744
|
if (len === undefined) {
|
|
1854
1745
|
// if the seg len is undefined, the segment
|
|
@@ -1858,43 +1749,46 @@ export class MergeTree {
|
|
|
1858
1749
|
|
|
1859
1750
|
assert(len >= 0, 0x4bc /* Length should not be negative */);
|
|
1860
1751
|
|
|
1861
|
-
if (_pos < len || (_pos === len && this.breakTie(_pos, child,
|
|
1752
|
+
if (_pos < len || (_pos === len && this.breakTie(_pos, child, stamp))) {
|
|
1862
1753
|
// Found entry containing pos
|
|
1863
1754
|
if (child.isLeaf()) {
|
|
1864
1755
|
const segment = child;
|
|
1865
1756
|
const segmentChanges = context.leaf(segment, _pos, context);
|
|
1866
1757
|
if (segmentChanges.replaceCurrent) {
|
|
1758
|
+
hadChanges = true;
|
|
1867
1759
|
assignChild(block, segmentChanges.replaceCurrent, childIndex, false);
|
|
1868
1760
|
segmentChanges.replaceCurrent.ordinal = child.ordinal;
|
|
1869
1761
|
}
|
|
1870
1762
|
if (segmentChanges.next) {
|
|
1763
|
+
hadChanges = true;
|
|
1871
1764
|
newNode = segmentChanges.next;
|
|
1872
1765
|
childIndex++; // Insert after
|
|
1873
1766
|
} else {
|
|
1874
|
-
|
|
1875
|
-
return undefined;
|
|
1767
|
+
return { remainder: undefined, hadChanges };
|
|
1876
1768
|
}
|
|
1877
1769
|
} else {
|
|
1878
1770
|
const childBlock = child;
|
|
1879
1771
|
// Internal node
|
|
1880
|
-
const
|
|
1772
|
+
const insertResult = this.insertRecursive(
|
|
1881
1773
|
childBlock,
|
|
1882
1774
|
_pos,
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
seq,
|
|
1775
|
+
perspective,
|
|
1776
|
+
stamp,
|
|
1886
1777
|
context,
|
|
1887
1778
|
isLastNonLeafBlock,
|
|
1888
1779
|
);
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1780
|
+
hadChanges ||= insertResult.hadChanges;
|
|
1781
|
+
if (insertResult.remainder === undefined) {
|
|
1782
|
+
if (insertResult.hadChanges) {
|
|
1783
|
+
this.blockUpdateLength(block, stamp);
|
|
1784
|
+
}
|
|
1785
|
+
return insertResult;
|
|
1786
|
+
} else if (insertResult.remainder === MergeTree.theUnfinishedNode) {
|
|
1893
1787
|
_pos -= len; // Act as if shifted segment
|
|
1894
1788
|
continue;
|
|
1895
1789
|
} else {
|
|
1896
|
-
newNode =
|
|
1897
|
-
fromSplit =
|
|
1790
|
+
newNode = insertResult.remainder;
|
|
1791
|
+
fromSplit = insertResult.remainder;
|
|
1898
1792
|
childIndex++; // Insert after
|
|
1899
1793
|
}
|
|
1900
1794
|
}
|
|
@@ -1905,7 +1799,7 @@ export class MergeTree {
|
|
|
1905
1799
|
}
|
|
1906
1800
|
if (!newNode && _pos === 0) {
|
|
1907
1801
|
if (context.continuePredicate?.(block)) {
|
|
1908
|
-
return MergeTree.theUnfinishedNode;
|
|
1802
|
+
return { remainder: MergeTree.theUnfinishedNode, hadChanges };
|
|
1909
1803
|
} else {
|
|
1910
1804
|
const segmentChanges = context.leaf(undefined, _pos, context);
|
|
1911
1805
|
newNode = segmentChanges.next;
|
|
@@ -1913,6 +1807,7 @@ export class MergeTree {
|
|
|
1913
1807
|
}
|
|
1914
1808
|
}
|
|
1915
1809
|
if (newNode) {
|
|
1810
|
+
hadChanges = true;
|
|
1916
1811
|
for (let i = block.childCount; i > childIndex; i--) {
|
|
1917
1812
|
block.children[i] = block.children[i - 1];
|
|
1918
1813
|
block.children[i].index = i;
|
|
@@ -1924,24 +1819,29 @@ export class MergeTree {
|
|
|
1924
1819
|
if (fromSplit) {
|
|
1925
1820
|
this.nodeUpdateOrdinals(fromSplit);
|
|
1926
1821
|
}
|
|
1927
|
-
this.blockUpdateLength(block,
|
|
1928
|
-
return undefined;
|
|
1822
|
+
this.blockUpdateLength(block, stamp);
|
|
1823
|
+
return { remainder: undefined, hadChanges };
|
|
1929
1824
|
} else {
|
|
1930
1825
|
// Don't update ordinals because higher block will do it
|
|
1931
1826
|
const newNodeFromSplit = this.split(block);
|
|
1932
1827
|
|
|
1933
|
-
PartialSequenceLengths.options.verifyExpected?.(
|
|
1828
|
+
PartialSequenceLengths.options.verifyExpected?.(
|
|
1829
|
+
this,
|
|
1830
|
+
block,
|
|
1831
|
+
perspective.refSeq,
|
|
1832
|
+
stamp.clientId,
|
|
1833
|
+
);
|
|
1934
1834
|
PartialSequenceLengths.options.verifyExpected?.(
|
|
1935
1835
|
this,
|
|
1936
1836
|
newNodeFromSplit,
|
|
1937
|
-
refSeq,
|
|
1938
|
-
clientId,
|
|
1837
|
+
perspective.refSeq,
|
|
1838
|
+
stamp.clientId,
|
|
1939
1839
|
);
|
|
1940
1840
|
|
|
1941
|
-
return newNodeFromSplit;
|
|
1841
|
+
return { remainder: newNodeFromSplit, hadChanges };
|
|
1942
1842
|
}
|
|
1943
1843
|
} else {
|
|
1944
|
-
return undefined;
|
|
1844
|
+
return { remainder: undefined, hadChanges };
|
|
1945
1845
|
}
|
|
1946
1846
|
}
|
|
1947
1847
|
|
|
@@ -1979,28 +1879,22 @@ export class MergeTree {
|
|
|
1979
1879
|
* @param clientId - The id of the client making the annotate
|
|
1980
1880
|
* @param seq - The sequence number of the annotate operation
|
|
1981
1881
|
* @param opArgs - The op args for the annotate op. this is passed to the merge tree callback if there is one
|
|
1982
|
-
* @param rollback - Whether this is for a local rollback and what kind
|
|
1983
1882
|
*/
|
|
1984
1883
|
public annotateRange(
|
|
1985
1884
|
start: number,
|
|
1986
1885
|
end: number,
|
|
1987
1886
|
propsOrAdjust: PropsOrAdjust,
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
seq: number,
|
|
1887
|
+
perspective: Perspective,
|
|
1888
|
+
stamp: OperationStamp,
|
|
1991
1889
|
opArgs: IMergeTreeDeltaOpArgs,
|
|
1992
|
-
|
|
1993
|
-
rollback: PropertiesRollback = PropertiesRollback.None,
|
|
1994
1890
|
): void {
|
|
1995
1891
|
if (propsOrAdjust.adjust !== undefined) {
|
|
1996
1892
|
errorIfOptionNotTrue(this.options, "mergeTreeEnableAnnotateAdjust");
|
|
1997
1893
|
}
|
|
1998
1894
|
|
|
1999
|
-
this.ensureIntervalBoundary(start,
|
|
2000
|
-
this.ensureIntervalBoundary(end,
|
|
1895
|
+
this.ensureIntervalBoundary(start, perspective);
|
|
1896
|
+
this.ensureIntervalBoundary(end, perspective);
|
|
2001
1897
|
const deltaSegments: IMergeTreeSegmentDelta[] = [];
|
|
2002
|
-
const localSeq =
|
|
2003
|
-
seq === UnassignedSequenceNumber ? ++this.collabWindow.localSeq : undefined;
|
|
2004
1898
|
|
|
2005
1899
|
let segmentGroup: SegmentGroup | undefined;
|
|
2006
1900
|
const opObj = propsOrAdjust.props ?? propsOrAdjust.adjust;
|
|
@@ -2016,33 +1910,33 @@ export class MergeTree {
|
|
|
2016
1910
|
const propertyDeltas = propertyManager.handleProperties(
|
|
2017
1911
|
propsOrAdjust,
|
|
2018
1912
|
segment,
|
|
2019
|
-
seq,
|
|
1913
|
+
stamp.seq,
|
|
2020
1914
|
this.collabWindow.minSeq,
|
|
2021
1915
|
this.collabWindow.collaborating,
|
|
2022
|
-
rollback,
|
|
1916
|
+
opArgs?.rollback === true,
|
|
2023
1917
|
);
|
|
2024
1918
|
|
|
2025
|
-
if (!
|
|
1919
|
+
if (!isRemoved(segment)) {
|
|
2026
1920
|
deltaSegments.push({ segment, propertyDeltas });
|
|
2027
1921
|
}
|
|
2028
1922
|
if (this.collabWindow.collaborating) {
|
|
2029
|
-
if (
|
|
1923
|
+
if (opstampUtils.isLocal(stamp)) {
|
|
2030
1924
|
segmentGroup = this.addToPendingList(
|
|
2031
1925
|
segment,
|
|
2032
1926
|
segmentGroup,
|
|
2033
|
-
localSeq,
|
|
1927
|
+
stamp.localSeq,
|
|
2034
1928
|
propertyDeltas,
|
|
2035
1929
|
);
|
|
2036
1930
|
} else {
|
|
2037
1931
|
if (MergeTree.options.zamboniSegments) {
|
|
2038
|
-
this.addToLRUSet(segment, seq);
|
|
1932
|
+
this.addToLRUSet(segment, stamp.seq);
|
|
2039
1933
|
}
|
|
2040
1934
|
}
|
|
2041
1935
|
}
|
|
2042
1936
|
return true;
|
|
2043
1937
|
};
|
|
2044
1938
|
|
|
2045
|
-
this.nodeMap(
|
|
1939
|
+
this.nodeMap(perspective, annotateSegment, undefined, start, end);
|
|
2046
1940
|
|
|
2047
1941
|
// OpArgs == undefined => test code
|
|
2048
1942
|
if (deltaSegments.length > 0) {
|
|
@@ -2053,7 +1947,7 @@ export class MergeTree {
|
|
|
2053
1947
|
}
|
|
2054
1948
|
if (
|
|
2055
1949
|
this.collabWindow.collaborating &&
|
|
2056
|
-
|
|
1950
|
+
opstampUtils.isAcked(stamp) &&
|
|
2057
1951
|
MergeTree.options.zamboniSegments
|
|
2058
1952
|
) {
|
|
2059
1953
|
zamboniSegments(this);
|
|
@@ -2063,40 +1957,30 @@ export class MergeTree {
|
|
|
2063
1957
|
private obliterateRangeSided(
|
|
2064
1958
|
start: InteriorSequencePlace,
|
|
2065
1959
|
end: InteriorSequencePlace,
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
seq: number,
|
|
1960
|
+
perspective: Perspective,
|
|
1961
|
+
stamp: SliceRemoveOperationStamp,
|
|
2069
1962
|
opArgs: IMergeTreeDeltaOpArgs,
|
|
2070
1963
|
): void {
|
|
2071
1964
|
const startPos = start.side === Side.Before ? start.pos : start.pos + 1;
|
|
2072
1965
|
const endPos = end.side === Side.Before ? end.pos : end.pos + 1;
|
|
2073
1966
|
|
|
2074
|
-
this.ensureIntervalBoundary(startPos,
|
|
2075
|
-
this.ensureIntervalBoundary(endPos,
|
|
1967
|
+
this.ensureIntervalBoundary(startPos, perspective);
|
|
1968
|
+
this.ensureIntervalBoundary(endPos, perspective);
|
|
2076
1969
|
|
|
2077
1970
|
let _overwrite = false;
|
|
2078
1971
|
const localOverlapWithRefs: ISegmentLeaf[] = [];
|
|
2079
|
-
const
|
|
2080
|
-
const localSeq =
|
|
2081
|
-
seq === UnassignedSequenceNumber ? ++this.collabWindow.localSeq : undefined;
|
|
2082
|
-
|
|
2083
|
-
const perspective =
|
|
2084
|
-
seq === UnassignedSequenceNumber
|
|
2085
|
-
? this.localPerspective
|
|
2086
|
-
: new PriorPerspective(refSeq, clientId);
|
|
1972
|
+
const removedSegments: SegmentWithInfo<IHasRemovalInfo, ISegmentLeaf>[] = [];
|
|
2087
1973
|
|
|
2088
1974
|
const obliterate: ObliterateInfo = {
|
|
2089
|
-
clientId,
|
|
2090
|
-
end: createDetachedLocalReferencePosition(undefined),
|
|
2091
|
-
refSeq,
|
|
2092
|
-
seq,
|
|
2093
1975
|
start: createDetachedLocalReferencePosition(undefined),
|
|
2094
|
-
|
|
1976
|
+
end: createDetachedLocalReferencePosition(undefined),
|
|
1977
|
+
refSeq: perspective.refSeq,
|
|
1978
|
+
stamp,
|
|
2095
1979
|
segmentGroup: undefined,
|
|
2096
1980
|
};
|
|
2097
1981
|
|
|
2098
|
-
const { segment: startSeg } = this.getContainingSegment(start.pos,
|
|
2099
|
-
const { segment: endSeg } = this.getContainingSegment(end.pos,
|
|
1982
|
+
const { segment: startSeg } = this.getContainingSegment(start.pos, perspective);
|
|
1983
|
+
const { segment: endSeg } = this.getContainingSegment(end.pos, perspective);
|
|
2100
1984
|
assert(
|
|
2101
1985
|
isSegmentLeaf(startSeg) && isSegmentLeaf(endSeg),
|
|
2102
1986
|
0xa3f /* segments cannot be undefined */,
|
|
@@ -2126,16 +2010,16 @@ export class MergeTree {
|
|
|
2126
2010
|
// at which point they are added to the segment group.
|
|
2127
2011
|
obliterate.segmentGroup = {
|
|
2128
2012
|
segments: [],
|
|
2129
|
-
localSeq,
|
|
2013
|
+
localSeq: stamp.localSeq,
|
|
2130
2014
|
refSeq: this.collabWindow.currentSeq,
|
|
2131
2015
|
obliterateInfo: obliterate,
|
|
2132
2016
|
};
|
|
2133
|
-
if (this.collabWindow.collaborating && clientId === this.collabWindow.clientId) {
|
|
2017
|
+
if (this.collabWindow.collaborating && stamp.clientId === this.collabWindow.clientId) {
|
|
2134
2018
|
this.pendingSegments.push(obliterate.segmentGroup);
|
|
2135
2019
|
}
|
|
2136
2020
|
this.obliterates.addOrUpdate(obliterate);
|
|
2137
2021
|
|
|
2138
|
-
const
|
|
2022
|
+
const markRemoved = (segment: ISegmentLeaf, pos: number): boolean => {
|
|
2139
2023
|
if (
|
|
2140
2024
|
(start.side === Side.After && startPos === pos + segment.cachedLength) || // exclusive start segment
|
|
2141
2025
|
(end.side === Side.Before && endPos === pos && perspective.isSegmentPresent(segment)) // exclusive end segment
|
|
@@ -2144,16 +2028,16 @@ export class MergeTree {
|
|
|
2144
2028
|
// These segments are outside of the obliteration range though, so return true to keep walking.
|
|
2145
2029
|
return true;
|
|
2146
2030
|
}
|
|
2147
|
-
const
|
|
2031
|
+
const existingRemoveInfo = toRemovalInfo(segment);
|
|
2148
2032
|
|
|
2149
2033
|
// The "last-to-obliterate-gets-to-insert" policy described by the doc comment on `obliteratePrecedingInsertion`
|
|
2150
2034
|
// is mostly handled by logic at insertion time, but we need a small bit of handling here.
|
|
2151
2035
|
// Specifically, we want to avoid marking a local-only segment as obliterated when we know one of our own local obliterates
|
|
2152
2036
|
// will win against the obliterate we're processing, hence the early exit.
|
|
2153
2037
|
if (
|
|
2154
|
-
segment.
|
|
2155
|
-
segment.obliteratePrecedingInsertion?.seq === UnassignedSequenceNumber &&
|
|
2156
|
-
|
|
2038
|
+
opstampUtils.isLocal(segment.insert) &&
|
|
2039
|
+
segment.obliteratePrecedingInsertion?.stamp.seq === UnassignedSequenceNumber &&
|
|
2040
|
+
opstampUtils.isAcked(stamp)
|
|
2157
2041
|
) {
|
|
2158
2042
|
// We chose to not obliterate this segment because we are aware of an unacked local obliteration.
|
|
2159
2043
|
// The local obliterate has not been sequenced yet, so it is still the newest obliterate we are aware of.
|
|
@@ -2162,94 +2046,69 @@ export class MergeTree {
|
|
|
2162
2046
|
}
|
|
2163
2047
|
|
|
2164
2048
|
// Partial lengths incrementality is not supported for overlapping obliterate/removes.
|
|
2165
|
-
_overwrite ||=
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2049
|
+
_overwrite ||= existingRemoveInfo !== undefined;
|
|
2050
|
+
|
|
2051
|
+
// - Record the segment as removed
|
|
2052
|
+
// - If this was the first thing to remove the segment from the local view, add it to removedSegments
|
|
2053
|
+
// - Otherwise, if it was the first thing to remove the segment from the acked view, add it to localOverlapWithRefs (so we can slide them)
|
|
2054
|
+
if (existingRemoveInfo === undefined) {
|
|
2055
|
+
const removed = overwriteInfo<IHasRemovalInfo, ISegmentLeaf>(segment, {
|
|
2056
|
+
removes: [stamp],
|
|
2173
2057
|
});
|
|
2174
2058
|
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2059
|
+
removedSegments.push(removed);
|
|
2060
|
+
} else {
|
|
2061
|
+
// The segment has already been removed, so we don't need to add it to removedSegments. However,
|
|
2062
|
+
// if it's only been removed locally, we still need to slide any references that may exist on it.
|
|
2063
|
+
if (
|
|
2064
|
+
!opstampUtils.hasAnyAckedOperation(existingRemoveInfo.removes) &&
|
|
2180
2065
|
segment.localRefs?.empty === false
|
|
2181
2066
|
) {
|
|
2182
|
-
// We removed this locally already so we don't need to event it again, but it might have references
|
|
2183
|
-
// that need sliding now that a move may have been acked.
|
|
2184
2067
|
localOverlapWithRefs.push(segment);
|
|
2185
2068
|
}
|
|
2186
|
-
|
|
2187
|
-
if (existingMoveInfo.movedSeq === UnassignedSequenceNumber) {
|
|
2188
|
-
assert(
|
|
2189
|
-
!wasMovedOnInsert(segment),
|
|
2190
|
-
0xab4 /* Local obliterate cannot have removed a segment as soon as it was inserted */,
|
|
2191
|
-
);
|
|
2192
|
-
assert(
|
|
2193
|
-
seq !== UnassignedSequenceNumber,
|
|
2194
|
-
0xab5 /* Cannot obliterate the same segment locally twice */,
|
|
2195
|
-
);
|
|
2196
|
-
|
|
2197
|
-
// we moved this locally, but someone else moved it first
|
|
2198
|
-
// so put them at the head of the list
|
|
2199
|
-
// The list isn't ordered, but we keep the first move at the head
|
|
2200
|
-
// for partialLengths bookkeeping purposes
|
|
2201
|
-
existingMoveInfo.movedClientIds.unshift(clientId);
|
|
2202
|
-
|
|
2203
|
-
existingMoveInfo.movedSeq = seq;
|
|
2204
|
-
existingMoveInfo.movedSeqs.unshift(seq);
|
|
2205
|
-
if (segment.localRefs?.empty === false) {
|
|
2206
|
-
localOverlapWithRefs.push(segment);
|
|
2207
|
-
}
|
|
2208
|
-
} else {
|
|
2209
|
-
// Do not replace earlier sequence number for move
|
|
2210
|
-
existingMoveInfo.movedClientIds.push(clientId);
|
|
2211
|
-
existingMoveInfo.movedSeqs.push(seq);
|
|
2212
|
-
}
|
|
2069
|
+
opstampUtils.spliceIntoList(existingRemoveInfo.removes, stamp);
|
|
2213
2070
|
}
|
|
2214
|
-
|
|
2215
|
-
// Save segment so can assign
|
|
2071
|
+
assertRemoved(segment);
|
|
2072
|
+
// Save segment so can assign sequence number when acked by server
|
|
2216
2073
|
if (this.collabWindow.collaborating) {
|
|
2217
2074
|
if (
|
|
2218
|
-
segment.
|
|
2219
|
-
clientId === this.collabWindow.clientId
|
|
2075
|
+
opstampUtils.isLocal(segment.removes[0]) &&
|
|
2076
|
+
stamp.clientId === this.collabWindow.clientId
|
|
2220
2077
|
) {
|
|
2221
2078
|
obliterate.segmentGroup = this.addToPendingList(
|
|
2222
2079
|
segment,
|
|
2223
2080
|
obliterate.segmentGroup,
|
|
2224
|
-
localSeq,
|
|
2081
|
+
stamp.localSeq,
|
|
2225
2082
|
);
|
|
2226
2083
|
} else {
|
|
2227
2084
|
if (MergeTree.options.zamboniSegments) {
|
|
2228
|
-
this.addToLRUSet(segment, seq);
|
|
2085
|
+
this.addToLRUSet(segment, stamp.seq);
|
|
2229
2086
|
}
|
|
2230
2087
|
}
|
|
2231
2088
|
}
|
|
2232
2089
|
return true;
|
|
2233
2090
|
};
|
|
2234
2091
|
|
|
2235
|
-
const
|
|
2092
|
+
const afterMarkRemoved = (node: MergeBlock): boolean => {
|
|
2236
2093
|
if (_overwrite) {
|
|
2237
2094
|
this.nodeUpdateLengthNewStructure(node);
|
|
2238
2095
|
} else {
|
|
2239
|
-
this.blockUpdateLength(node,
|
|
2096
|
+
this.blockUpdateLength(node, stamp);
|
|
2240
2097
|
}
|
|
2241
2098
|
return true;
|
|
2242
2099
|
};
|
|
2243
2100
|
|
|
2244
2101
|
this.nodeMap(
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
afterMarkMoved,
|
|
2102
|
+
perspective,
|
|
2103
|
+
markRemoved,
|
|
2104
|
+
afterMarkRemoved,
|
|
2249
2105
|
start.pos,
|
|
2250
2106
|
end.pos + 1, // include the segment containing the end reference
|
|
2251
|
-
|
|
2252
|
-
|
|
2107
|
+
// Use a visibilityPerspective which includes all segments (including local ones) which are in the obliteration range.
|
|
2108
|
+
// This ensures that concurrently inserted segments will also be marked obliterated.
|
|
2109
|
+
opstampUtils.isLocal(stamp)
|
|
2110
|
+
? perspective
|
|
2111
|
+
: new RemoteObliteratePerspective(stamp.clientId),
|
|
2253
2112
|
);
|
|
2254
2113
|
|
|
2255
2114
|
this.slideAckedRemovedSegmentReferences(localOverlapWithRefs);
|
|
@@ -2257,20 +2116,20 @@ export class MergeTree {
|
|
|
2257
2116
|
if (start.pos !== end.pos || start.side !== end.side) {
|
|
2258
2117
|
this.mergeTreeDeltaCallback?.(opArgs, {
|
|
2259
2118
|
operation: MergeTreeDeltaType.OBLITERATE,
|
|
2260
|
-
deltaSegments:
|
|
2119
|
+
deltaSegments: removedSegments.map((segment) => ({ segment })),
|
|
2261
2120
|
});
|
|
2262
2121
|
}
|
|
2263
2122
|
|
|
2264
2123
|
// these events are newly removed
|
|
2265
2124
|
// so we slide after eventing in case the consumer wants to make reference
|
|
2266
2125
|
// changes at remove time, like add a ref to track undo redo.
|
|
2267
|
-
if (!this.collabWindow.collaborating || clientId !== this.collabWindow.clientId) {
|
|
2268
|
-
this.slideAckedRemovedSegmentReferences(
|
|
2126
|
+
if (!this.collabWindow.collaborating || stamp.clientId !== this.collabWindow.clientId) {
|
|
2127
|
+
this.slideAckedRemovedSegmentReferences(removedSegments);
|
|
2269
2128
|
}
|
|
2270
2129
|
|
|
2271
2130
|
if (
|
|
2272
2131
|
this.collabWindow.collaborating &&
|
|
2273
|
-
|
|
2132
|
+
opstampUtils.isAcked(stamp) &&
|
|
2274
2133
|
MergeTree.options.zamboniSegments
|
|
2275
2134
|
) {
|
|
2276
2135
|
zamboniSegments(this);
|
|
@@ -2280,18 +2139,18 @@ export class MergeTree {
|
|
|
2280
2139
|
public obliterateRange(
|
|
2281
2140
|
start: number | InteriorSequencePlace,
|
|
2282
2141
|
end: number | InteriorSequencePlace,
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
seq: number,
|
|
2142
|
+
perspective: Perspective,
|
|
2143
|
+
stampArg: OperationStamp,
|
|
2286
2144
|
opArgs: IMergeTreeDeltaOpArgs,
|
|
2287
2145
|
): void {
|
|
2288
2146
|
errorIfOptionNotTrue(this.options, "mergeTreeEnableObliterate");
|
|
2147
|
+
const stamp: SliceRemoveOperationStamp = { ...stampArg, type: "sliceRemove" };
|
|
2289
2148
|
if (this.options?.mergeTreeEnableSidedObliterate) {
|
|
2290
2149
|
assert(
|
|
2291
2150
|
typeof start === "object" && typeof end === "object",
|
|
2292
2151
|
0xa45 /* Start and end must be of type InteriorSequencePlace if mergeTreeEnableSidedObliterate is enabled. */,
|
|
2293
2152
|
);
|
|
2294
|
-
this.obliterateRangeSided(start, end,
|
|
2153
|
+
this.obliterateRangeSided(start, end, perspective, stamp, opArgs);
|
|
2295
2154
|
} else {
|
|
2296
2155
|
assert(
|
|
2297
2156
|
typeof start === "number" && typeof end === "number",
|
|
@@ -2300,9 +2159,8 @@ export class MergeTree {
|
|
|
2300
2159
|
this.obliterateRangeSided(
|
|
2301
2160
|
{ pos: start, side: Side.Before },
|
|
2302
2161
|
{ pos: end - 1, side: Side.After },
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
seq,
|
|
2162
|
+
perspective,
|
|
2163
|
+
stamp,
|
|
2306
2164
|
opArgs,
|
|
2307
2165
|
);
|
|
2308
2166
|
}
|
|
@@ -2311,20 +2169,19 @@ export class MergeTree {
|
|
|
2311
2169
|
public markRangeRemoved(
|
|
2312
2170
|
start: number,
|
|
2313
2171
|
end: number,
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
seq: number,
|
|
2172
|
+
perspective: Perspective,
|
|
2173
|
+
stampArg: OperationStamp,
|
|
2317
2174
|
opArgs: IMergeTreeDeltaOpArgs,
|
|
2318
2175
|
): void {
|
|
2319
2176
|
let _overwrite = false;
|
|
2320
|
-
|
|
2321
|
-
this.ensureIntervalBoundary(
|
|
2177
|
+
const stamp: SetRemoveOperationStamp = { ...stampArg, type: "setRemove" };
|
|
2178
|
+
this.ensureIntervalBoundary(start, perspective);
|
|
2179
|
+
this.ensureIntervalBoundary(end, perspective);
|
|
2322
2180
|
|
|
2323
2181
|
let segmentGroup: SegmentGroup;
|
|
2324
|
-
const removedSegments: SegmentWithInfo<
|
|
2182
|
+
const removedSegments: SegmentWithInfo<IHasRemovalInfo, ISegmentLeaf>[] = [];
|
|
2325
2183
|
const localOverlapWithRefs: ISegmentLeaf[] = [];
|
|
2326
|
-
|
|
2327
|
-
seq === UnassignedSequenceNumber ? ++this.collabWindow.localSeq : undefined;
|
|
2184
|
+
|
|
2328
2185
|
const markRemoved = (
|
|
2329
2186
|
segment: ISegmentLeaf,
|
|
2330
2187
|
pos: number,
|
|
@@ -2334,53 +2191,34 @@ export class MergeTree {
|
|
|
2334
2191
|
const existingRemovalInfo = toRemovalInfo(segment);
|
|
2335
2192
|
|
|
2336
2193
|
// Partial lengths incrementality is not supported for overlapping obliterate/removes.
|
|
2337
|
-
_overwrite ||= existingRemovalInfo !== undefined
|
|
2194
|
+
_overwrite ||= existingRemovalInfo !== undefined;
|
|
2338
2195
|
if (existingRemovalInfo === undefined) {
|
|
2339
|
-
const removed = overwriteInfo<
|
|
2340
|
-
|
|
2341
|
-
removedSeq: seq,
|
|
2342
|
-
localRemovedSeq: localSeq,
|
|
2196
|
+
const removed = overwriteInfo<IHasRemovalInfo, ISegmentLeaf>(segment, {
|
|
2197
|
+
removes: [stamp],
|
|
2343
2198
|
});
|
|
2344
2199
|
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
existingMoveInfo.movedSeq === UnassignedSequenceNumber &&
|
|
2200
|
+
removedSegments.push(removed);
|
|
2201
|
+
} else {
|
|
2202
|
+
if (
|
|
2203
|
+
!opstampUtils.hasAnyAckedOperation(existingRemovalInfo.removes) &&
|
|
2350
2204
|
segment.localRefs?.empty === false
|
|
2351
2205
|
) {
|
|
2352
|
-
// We moved this locally already so we don't need to event it again, but it might have references
|
|
2353
|
-
// that need sliding now that a remove may have been acked.
|
|
2354
2206
|
localOverlapWithRefs.push(segment);
|
|
2355
2207
|
}
|
|
2356
|
-
|
|
2357
|
-
if (existingRemovalInfo.removedSeq === UnassignedSequenceNumber) {
|
|
2358
|
-
// we removed this locally, but someone else removed it first
|
|
2359
|
-
// so put them at the head of the list
|
|
2360
|
-
// The list isn't ordered, but we keep the first removal at the head
|
|
2361
|
-
// for partialLengths bookkeeping purposes
|
|
2362
|
-
existingRemovalInfo.removedClientIds.unshift(clientId);
|
|
2363
|
-
|
|
2364
|
-
existingRemovalInfo.removedSeq = seq;
|
|
2365
|
-
if (segment.localRefs?.empty === false) {
|
|
2366
|
-
localOverlapWithRefs.push(segment);
|
|
2367
|
-
}
|
|
2368
|
-
} else {
|
|
2369
|
-
// Do not replace earlier sequence number for remove
|
|
2370
|
-
existingRemovalInfo.removedClientIds.push(clientId);
|
|
2371
|
-
}
|
|
2208
|
+
opstampUtils.spliceIntoList(existingRemovalInfo.removes, stamp);
|
|
2372
2209
|
}
|
|
2373
2210
|
assertRemoved(segment);
|
|
2211
|
+
|
|
2374
2212
|
// Save segment so we can assign removed sequence number when acked by server
|
|
2375
2213
|
if (this.collabWindow.collaborating) {
|
|
2376
2214
|
if (
|
|
2377
|
-
segment.
|
|
2378
|
-
clientId === this.collabWindow.clientId
|
|
2215
|
+
opstampUtils.isLocal(segment.removes[0]) &&
|
|
2216
|
+
stamp.clientId === this.collabWindow.clientId
|
|
2379
2217
|
) {
|
|
2380
|
-
segmentGroup = this.addToPendingList(segment, segmentGroup, localSeq);
|
|
2218
|
+
segmentGroup = this.addToPendingList(segment, segmentGroup, stamp.localSeq);
|
|
2381
2219
|
} else {
|
|
2382
2220
|
if (MergeTree.options.zamboniSegments) {
|
|
2383
|
-
this.addToLRUSet(segment, seq);
|
|
2221
|
+
this.addToLRUSet(segment, stamp.seq);
|
|
2384
2222
|
}
|
|
2385
2223
|
}
|
|
2386
2224
|
}
|
|
@@ -2390,11 +2228,11 @@ export class MergeTree {
|
|
|
2390
2228
|
if (_overwrite) {
|
|
2391
2229
|
this.nodeUpdateLengthNewStructure(node);
|
|
2392
2230
|
} else {
|
|
2393
|
-
this.blockUpdateLength(node,
|
|
2231
|
+
this.blockUpdateLength(node, stamp);
|
|
2394
2232
|
}
|
|
2395
2233
|
return true;
|
|
2396
2234
|
};
|
|
2397
|
-
this.nodeMap(
|
|
2235
|
+
this.nodeMap(perspective, markRemoved, afterMarkRemoved, start, end);
|
|
2398
2236
|
// these segments are already viewed as being removed locally and are not event-ed
|
|
2399
2237
|
// so can slide non-StayOnRemove refs immediately
|
|
2400
2238
|
this.slideAckedRemovedSegmentReferences(localOverlapWithRefs);
|
|
@@ -2408,13 +2246,13 @@ export class MergeTree {
|
|
|
2408
2246
|
// these events are newly removed
|
|
2409
2247
|
// so we slide after eventing in case the consumer wants to make reference
|
|
2410
2248
|
// changes at remove time, like add a ref to track undo redo.
|
|
2411
|
-
if (!this.collabWindow.collaborating || clientId !== this.collabWindow.clientId) {
|
|
2249
|
+
if (!this.collabWindow.collaborating || stamp.clientId !== this.collabWindow.clientId) {
|
|
2412
2250
|
this.slideAckedRemovedSegmentReferences(removedSegments);
|
|
2413
2251
|
}
|
|
2414
2252
|
|
|
2415
2253
|
if (
|
|
2416
2254
|
this.collabWindow.collaborating &&
|
|
2417
|
-
|
|
2255
|
+
opstampUtils.isAcked(stamp) &&
|
|
2418
2256
|
MergeTree.options.zamboniSegments
|
|
2419
2257
|
) {
|
|
2420
2258
|
zamboniSegments(this);
|
|
@@ -2424,7 +2262,6 @@ export class MergeTree {
|
|
|
2424
2262
|
/**
|
|
2425
2263
|
* Revert an unacked local op
|
|
2426
2264
|
*/
|
|
2427
|
-
|
|
2428
2265
|
public rollback(op: IMergeTreeDeltaOp, localOpMetadata: SegmentGroup): void {
|
|
2429
2266
|
if (op.type === MergeTreeDeltaType.REMOVE) {
|
|
2430
2267
|
const pendingSegmentGroup = this.pendingSegments.pop()?.data;
|
|
@@ -2441,24 +2278,30 @@ export class MergeTree {
|
|
|
2441
2278
|
);
|
|
2442
2279
|
|
|
2443
2280
|
assert(
|
|
2444
|
-
isRemoved(segment) &&
|
|
2281
|
+
isRemoved(segment) &&
|
|
2282
|
+
segment.removes[0].clientId === this.collabWindow.clientId &&
|
|
2283
|
+
segment.removes[0].type === "setRemove",
|
|
2445
2284
|
0x39d /* Rollback segment removedClientId does not match local client */,
|
|
2446
2285
|
);
|
|
2447
2286
|
let updateNode: MergeBlock | undefined = segment.parent;
|
|
2287
|
+
// This also removes obliterates, but that should be ok as we can only remove a segment once.
|
|
2288
|
+
// If we were able to remove it locally, that also means there are no remote removals (since rollback is synchronous).
|
|
2448
2289
|
removeRemovalInfo(segment);
|
|
2449
2290
|
|
|
2450
2291
|
for (updateNode; updateNode !== undefined; updateNode = updateNode.parent) {
|
|
2451
|
-
this.blockUpdateLength(
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
);
|
|
2292
|
+
this.blockUpdateLength(updateNode, {
|
|
2293
|
+
seq: UnassignedSequenceNumber,
|
|
2294
|
+
clientId: this.collabWindow.clientId,
|
|
2295
|
+
});
|
|
2456
2296
|
}
|
|
2457
2297
|
|
|
2458
2298
|
// Note: optional chaining short-circuits:
|
|
2459
2299
|
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Optional_chaining#short-circuiting
|
|
2460
2300
|
this.mergeTreeDeltaCallback?.(
|
|
2461
|
-
{
|
|
2301
|
+
{
|
|
2302
|
+
op: createInsertSegmentOp(this.findRollbackPosition(segment), segment),
|
|
2303
|
+
rollback: true,
|
|
2304
|
+
},
|
|
2462
2305
|
{
|
|
2463
2306
|
operation: MergeTreeDeltaType.INSERT,
|
|
2464
2307
|
deltaSegments: [{ segment }],
|
|
@@ -2487,29 +2330,38 @@ export class MergeTree {
|
|
|
2487
2330
|
|
|
2488
2331
|
const start = this.findRollbackPosition(segment);
|
|
2489
2332
|
if (op.type === MergeTreeDeltaType.INSERT) {
|
|
2490
|
-
segment.
|
|
2491
|
-
|
|
2333
|
+
segment.insert = {
|
|
2334
|
+
type: "insert",
|
|
2335
|
+
seq: UniversalSequenceNumber,
|
|
2336
|
+
clientId: this.collabWindow.clientId,
|
|
2337
|
+
};
|
|
2492
2338
|
const removeOp = createRemoveRangeOp(start, start + segment.cachedLength);
|
|
2339
|
+
const removeStamp: SetRemoveOperationStamp = {
|
|
2340
|
+
type: "setRemove",
|
|
2341
|
+
seq: UniversalSequenceNumber,
|
|
2342
|
+
clientId: this.collabWindow.clientId,
|
|
2343
|
+
};
|
|
2493
2344
|
this.markRangeRemoved(
|
|
2494
2345
|
start,
|
|
2495
2346
|
start + segment.cachedLength,
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
|
|
2499
|
-
{ op: removeOp },
|
|
2347
|
+
this.localPerspective,
|
|
2348
|
+
removeStamp,
|
|
2349
|
+
{ op: removeOp, rollback: true },
|
|
2500
2350
|
);
|
|
2501
2351
|
} /* op.type === MergeTreeDeltaType.ANNOTATE */ else {
|
|
2502
2352
|
const props = pendingSegmentGroup.previousProps![i];
|
|
2503
2353
|
const annotateOp = createAnnotateRangeOp(start, start + segment.cachedLength, props);
|
|
2354
|
+
const annotateStamp: OperationStamp = {
|
|
2355
|
+
seq: UniversalSequenceNumber,
|
|
2356
|
+
clientId: this.collabWindow.clientId,
|
|
2357
|
+
};
|
|
2504
2358
|
this.annotateRange(
|
|
2505
2359
|
start,
|
|
2506
2360
|
start + segment.cachedLength,
|
|
2507
2361
|
{ props },
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
{ op: annotateOp },
|
|
2512
|
-
PropertiesRollback.Rollback,
|
|
2362
|
+
this.localPerspective,
|
|
2363
|
+
annotateStamp,
|
|
2364
|
+
{ op: annotateOp, rollback: true },
|
|
2513
2365
|
);
|
|
2514
2366
|
i++;
|
|
2515
2367
|
}
|
|
@@ -2570,7 +2422,7 @@ export class MergeTree {
|
|
|
2570
2422
|
if (
|
|
2571
2423
|
_segment !== "start" &&
|
|
2572
2424
|
_segment !== "end" &&
|
|
2573
|
-
|
|
2425
|
+
isRemovedAndAcked(_segment) &&
|
|
2574
2426
|
!refTypeIncludesFlag(
|
|
2575
2427
|
refType,
|
|
2576
2428
|
ReferenceType.SlideOnRemove | ReferenceType.Transient | ReferenceType.StayOnRemove,
|
|
@@ -2637,7 +2489,7 @@ export class MergeTree {
|
|
|
2637
2489
|
affectedSegments.insertAfter(lastLocalSegment, segmentToSlide.data);
|
|
2638
2490
|
} else if (isRemoved(segmentToSlide.data)) {
|
|
2639
2491
|
assert(
|
|
2640
|
-
segmentToSlide.data.
|
|
2492
|
+
segmentToSlide.data.removes[0].seq !== undefined,
|
|
2641
2493
|
0x54d /* Removed segment that hasnt had its removal acked should be locally removed */,
|
|
2642
2494
|
);
|
|
2643
2495
|
// Slide each locally removed item past all segments that have localSeq > lremoveItem.localSeq
|
|
@@ -2647,8 +2499,8 @@ export class MergeTree {
|
|
|
2647
2499
|
while (
|
|
2648
2500
|
scan !== undefined &&
|
|
2649
2501
|
!isRemovedAndAcked(scan.data) &&
|
|
2650
|
-
scan.data.localSeq !== undefined &&
|
|
2651
|
-
scan.data.
|
|
2502
|
+
scan.data.insert.localSeq !== undefined &&
|
|
2503
|
+
opstampUtils.greaterThan(scan.data.insert, segmentToSlide.data.removes[0])
|
|
2652
2504
|
) {
|
|
2653
2505
|
cur = scan;
|
|
2654
2506
|
scan = scan.next;
|
|
@@ -2741,11 +2593,11 @@ export class MergeTree {
|
|
|
2741
2593
|
}
|
|
2742
2594
|
};
|
|
2743
2595
|
walkAllChildSegments(this.root, (seg) => {
|
|
2744
|
-
if (isRemoved(seg) || seg.
|
|
2596
|
+
if (isRemoved(seg) || opstampUtils.isLocal(seg.insert)) {
|
|
2745
2597
|
if (isRemovedAndAcked(seg)) {
|
|
2746
2598
|
rangeContainsRemoteRemovedSegs = true;
|
|
2747
2599
|
}
|
|
2748
|
-
if (seg.
|
|
2600
|
+
if (opstampUtils.isLocal(seg.insert)) {
|
|
2749
2601
|
rangeContainsLocalSegs = true;
|
|
2750
2602
|
}
|
|
2751
2603
|
currentRangeToNormalize.push(seg);
|
|
@@ -2776,7 +2628,7 @@ export class MergeTree {
|
|
|
2776
2628
|
}
|
|
2777
2629
|
if (node.isLeaf()) {
|
|
2778
2630
|
const segment = node;
|
|
2779
|
-
if ((this.
|
|
2631
|
+
if ((this.leafLength(segment) ?? 0) > 0 && Marker.is(segment)) {
|
|
2780
2632
|
const markerId = segment.getId();
|
|
2781
2633
|
// Also in insertMarker but need for reload segs case
|
|
2782
2634
|
// can add option for this only from reload segs
|
|
@@ -2809,8 +2661,7 @@ export class MergeTree {
|
|
|
2809
2661
|
|
|
2810
2662
|
public blockUpdatePathLengths(
|
|
2811
2663
|
startBlock: MergeBlock | undefined,
|
|
2812
|
-
|
|
2813
|
-
clientId: number,
|
|
2664
|
+
stamp: OperationStamp,
|
|
2814
2665
|
newStructure = false,
|
|
2815
2666
|
): void {
|
|
2816
2667
|
let block: MergeBlock | undefined = startBlock;
|
|
@@ -2818,31 +2669,31 @@ export class MergeTree {
|
|
|
2818
2669
|
if (newStructure) {
|
|
2819
2670
|
this.nodeUpdateLengthNewStructure(block);
|
|
2820
2671
|
} else {
|
|
2821
|
-
this.blockUpdateLength(block,
|
|
2672
|
+
this.blockUpdateLength(block, stamp);
|
|
2822
2673
|
}
|
|
2823
2674
|
block = block.parent;
|
|
2824
2675
|
}
|
|
2825
2676
|
}
|
|
2826
2677
|
|
|
2827
|
-
private blockUpdateLength(node: MergeBlock,
|
|
2678
|
+
private blockUpdateLength(node: MergeBlock, stamp: OperationStamp): void {
|
|
2828
2679
|
this.blockUpdate(node);
|
|
2829
2680
|
this.localPartialsComputed = false;
|
|
2830
2681
|
if (
|
|
2831
2682
|
this.collabWindow.collaborating &&
|
|
2832
|
-
|
|
2833
|
-
seq !== TreeMaintenanceSequenceNumber
|
|
2683
|
+
opstampUtils.isAcked(stamp) &&
|
|
2684
|
+
stamp.seq !== TreeMaintenanceSequenceNumber
|
|
2834
2685
|
) {
|
|
2835
2686
|
if (
|
|
2836
2687
|
node.partialLengths !== undefined &&
|
|
2837
2688
|
MergeTree.options.incrementalUpdate &&
|
|
2838
|
-
clientId !== NonCollabClient
|
|
2689
|
+
stamp.clientId !== NonCollabClient
|
|
2839
2690
|
) {
|
|
2840
|
-
node.partialLengths.update(node, seq, clientId, this.collabWindow);
|
|
2691
|
+
node.partialLengths.update(node, stamp.seq, stamp.clientId, this.collabWindow);
|
|
2841
2692
|
} else {
|
|
2842
2693
|
node.partialLengths = PartialSequenceLengths.combine(node, this.collabWindow);
|
|
2843
2694
|
}
|
|
2844
2695
|
|
|
2845
|
-
PartialSequenceLengths.options.verifyExpected?.(this, node, seq, clientId);
|
|
2696
|
+
PartialSequenceLengths.options.verifyExpected?.(this, node, stamp.seq, stamp.clientId);
|
|
2846
2697
|
}
|
|
2847
2698
|
}
|
|
2848
2699
|
|
|
@@ -2855,31 +2706,29 @@ export class MergeTree {
|
|
|
2855
2706
|
*/
|
|
2856
2707
|
public mapRange<TClientData>(
|
|
2857
2708
|
handler: ISegmentAction<TClientData>,
|
|
2858
|
-
|
|
2859
|
-
clientId: number,
|
|
2709
|
+
perspective: Perspective,
|
|
2860
2710
|
accum: TClientData,
|
|
2861
2711
|
start?: number,
|
|
2862
2712
|
end?: number,
|
|
2863
2713
|
splitRange: boolean = false,
|
|
2864
|
-
|
|
2714
|
+
visibilityPerspective: Perspective = perspective,
|
|
2865
2715
|
): void {
|
|
2866
2716
|
if (splitRange) {
|
|
2867
2717
|
if (start) {
|
|
2868
|
-
this.ensureIntervalBoundary(start,
|
|
2718
|
+
this.ensureIntervalBoundary(start, perspective);
|
|
2869
2719
|
}
|
|
2870
2720
|
if (end) {
|
|
2871
|
-
this.ensureIntervalBoundary(end,
|
|
2721
|
+
this.ensureIntervalBoundary(end, perspective);
|
|
2872
2722
|
}
|
|
2873
2723
|
}
|
|
2874
2724
|
this.nodeMap(
|
|
2875
|
-
|
|
2876
|
-
|
|
2877
|
-
|
|
2725
|
+
perspective,
|
|
2726
|
+
(seg, pos, _start, _end) =>
|
|
2727
|
+
handler(seg, pos, perspective.refSeq, perspective.clientId, _start, _end, accum),
|
|
2878
2728
|
undefined,
|
|
2879
2729
|
start,
|
|
2880
2730
|
end,
|
|
2881
|
-
|
|
2882
|
-
visibilitySeq,
|
|
2731
|
+
visibilityPerspective,
|
|
2883
2732
|
);
|
|
2884
2733
|
}
|
|
2885
2734
|
|
|
@@ -2907,16 +2756,14 @@ export class MergeTree {
|
|
|
2907
2756
|
* ignored for the purposes of tracking when traversal should end.
|
|
2908
2757
|
*/
|
|
2909
2758
|
private nodeMap(
|
|
2910
|
-
|
|
2911
|
-
clientId: number,
|
|
2759
|
+
perspective: Perspective,
|
|
2912
2760
|
leaf: (segment: ISegmentLeaf, pos: number, start: number, end: number) => boolean,
|
|
2913
2761
|
post?: (block: MergeBlock) => boolean,
|
|
2914
2762
|
start: number = 0,
|
|
2915
2763
|
end?: number,
|
|
2916
|
-
|
|
2917
|
-
visibilitySeq: number = refSeq,
|
|
2764
|
+
visibilityPerspective: Perspective = perspective,
|
|
2918
2765
|
): void {
|
|
2919
|
-
const endPos = end ?? this.nodeLength(this.root,
|
|
2766
|
+
const endPos = end ?? this.nodeLength(this.root, perspective) ?? 0;
|
|
2920
2767
|
if (endPos === start) {
|
|
2921
2768
|
return;
|
|
2922
2769
|
}
|
|
@@ -2931,16 +2778,18 @@ export class MergeTree {
|
|
|
2931
2778
|
return NodeAction.Exit;
|
|
2932
2779
|
}
|
|
2933
2780
|
|
|
2934
|
-
const len = this.nodeLength(node,
|
|
2781
|
+
const len = this.nodeLength(node, visibilityPerspective);
|
|
2935
2782
|
const lenAtRefSeq =
|
|
2936
|
-
(
|
|
2937
|
-
|
|
2938
|
-
: this.nodeLength(node, refSeq, clientId, localSeq)) ?? 0;
|
|
2783
|
+
(visibilityPerspective === perspective ? len : this.nodeLength(node, perspective)) ??
|
|
2784
|
+
0;
|
|
2939
2785
|
|
|
2786
|
+
// NOTE: This code ensures that obliterates have a chance to mark segments which have been inserted locally
|
|
2787
|
+
// as also having been obliterated on the local client. With the introduction of RemoteObliteratePerspective,
|
|
2788
|
+
// it's feasible we could remove it if the `nodeLength` calculation also respects that perspective for blocks
|
|
2789
|
+
// and not just leaves.
|
|
2940
2790
|
const isUnackedAndInObliterate =
|
|
2941
|
-
|
|
2942
|
-
(!node.isLeaf() || node.
|
|
2943
|
-
|
|
2791
|
+
visibilityPerspective !== perspective &&
|
|
2792
|
+
(!node.isLeaf() || opstampUtils.isLocal(node.insert));
|
|
2944
2793
|
if (
|
|
2945
2794
|
(len === undefined && lenAtRefSeq === 0) ||
|
|
2946
2795
|
(len === 0 && !isUnackedAndInObliterate && lenAtRefSeq === 0)
|