@fluidframework/merge-tree 2.31.0 → 2.32.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +4 -0
- package/dist/client.d.ts +7 -1
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +153 -44
- package/dist/client.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/mergeTree.d.ts +17 -5
- package/dist/mergeTree.d.ts.map +1 -1
- package/dist/mergeTree.js +188 -79
- package/dist/mergeTree.js.map +1 -1
- package/dist/mergeTreeNodes.d.ts +16 -18
- package/dist/mergeTreeNodes.d.ts.map +1 -1
- package/dist/mergeTreeNodes.js +6 -0
- package/dist/mergeTreeNodes.js.map +1 -1
- package/dist/perspective.d.ts +9 -0
- package/dist/perspective.d.ts.map +1 -1
- package/dist/perspective.js +14 -1
- package/dist/perspective.js.map +1 -1
- package/dist/segmentInfos.d.ts +32 -4
- package/dist/segmentInfos.d.ts.map +1 -1
- package/dist/segmentInfos.js +3 -1
- package/dist/segmentInfos.js.map +1 -1
- package/dist/sortedSegmentSet.d.ts +1 -0
- package/dist/sortedSegmentSet.d.ts.map +1 -1
- package/dist/sortedSegmentSet.js +3 -0
- package/dist/sortedSegmentSet.js.map +1 -1
- package/dist/test/beastTest.spec.js +5 -5
- package/dist/test/beastTest.spec.js.map +1 -1
- package/dist/test/client.localReference.spec.js +3 -3
- package/dist/test/client.localReference.spec.js.map +1 -1
- package/dist/test/client.rollback.spec.js +17 -0
- package/dist/test/client.rollback.spec.js.map +1 -1
- package/dist/test/clientTestHelper.d.ts +100 -0
- package/dist/test/clientTestHelper.d.ts.map +1 -0
- package/dist/test/clientTestHelper.js +196 -0
- package/dist/test/clientTestHelper.js.map +1 -0
- package/dist/test/mergeTree.annotate.spec.js +12 -12
- package/dist/test/mergeTree.annotate.spec.js.map +1 -1
- package/dist/test/mergeTree.markRangeRemoved.deltaCallback.spec.js +1 -1
- package/dist/test/mergeTree.markRangeRemoved.deltaCallback.spec.js.map +1 -1
- package/dist/test/obliterate.concurrent.spec.js +93 -90
- package/dist/test/obliterate.concurrent.spec.js.map +1 -1
- package/dist/test/obliterate.deltaCallback.spec.js +121 -116
- package/dist/test/obliterate.deltaCallback.spec.js.map +1 -1
- package/dist/test/obliterate.rangeExpansion.spec.js +29 -79
- package/dist/test/obliterate.rangeExpansion.spec.js.map +1 -1
- package/dist/test/obliterate.reconnect.spec.js +235 -58
- package/dist/test/obliterate.reconnect.spec.js.map +1 -1
- package/dist/test/testClient.js +1 -1
- package/dist/test/testClient.js.map +1 -1
- package/dist/test/testUtils.d.ts +13 -0
- package/dist/test/testUtils.d.ts.map +1 -1
- package/dist/test/testUtils.js +22 -1
- package/dist/test/testUtils.js.map +1 -1
- package/lib/client.d.ts +7 -1
- package/lib/client.d.ts.map +1 -1
- package/lib/client.js +155 -46
- package/lib/client.js.map +1 -1
- package/lib/index.d.ts +1 -1
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +1 -0
- package/lib/index.js.map +1 -1
- package/lib/mergeTree.d.ts +17 -5
- package/lib/mergeTree.d.ts.map +1 -1
- package/lib/mergeTree.js +192 -83
- package/lib/mergeTree.js.map +1 -1
- package/lib/mergeTreeNodes.d.ts +16 -18
- package/lib/mergeTreeNodes.d.ts.map +1 -1
- package/lib/mergeTreeNodes.js +7 -1
- package/lib/mergeTreeNodes.js.map +1 -1
- package/lib/perspective.d.ts +9 -0
- package/lib/perspective.d.ts.map +1 -1
- package/lib/perspective.js +12 -0
- package/lib/perspective.js.map +1 -1
- package/lib/segmentInfos.d.ts +32 -4
- package/lib/segmentInfos.d.ts.map +1 -1
- package/lib/segmentInfos.js +2 -1
- package/lib/segmentInfos.js.map +1 -1
- package/lib/sortedSegmentSet.d.ts +1 -0
- package/lib/sortedSegmentSet.d.ts.map +1 -1
- package/lib/sortedSegmentSet.js +3 -0
- package/lib/sortedSegmentSet.js.map +1 -1
- package/lib/test/beastTest.spec.js +5 -5
- package/lib/test/beastTest.spec.js.map +1 -1
- package/lib/test/client.localReference.spec.js +3 -3
- package/lib/test/client.localReference.spec.js.map +1 -1
- package/lib/test/client.rollback.spec.js +18 -1
- package/lib/test/client.rollback.spec.js.map +1 -1
- package/lib/test/clientTestHelper.d.ts +100 -0
- package/lib/test/clientTestHelper.d.ts.map +1 -0
- package/lib/test/clientTestHelper.js +192 -0
- package/lib/test/clientTestHelper.js.map +1 -0
- package/lib/test/mergeTree.annotate.spec.js +12 -12
- package/lib/test/mergeTree.annotate.spec.js.map +1 -1
- package/lib/test/mergeTree.markRangeRemoved.deltaCallback.spec.js +1 -1
- package/lib/test/mergeTree.markRangeRemoved.deltaCallback.spec.js.map +1 -1
- package/lib/test/obliterate.concurrent.spec.js +93 -90
- package/lib/test/obliterate.concurrent.spec.js.map +1 -1
- package/lib/test/obliterate.deltaCallback.spec.js +121 -116
- package/lib/test/obliterate.deltaCallback.spec.js.map +1 -1
- package/lib/test/obliterate.rangeExpansion.spec.js +1 -51
- package/lib/test/obliterate.rangeExpansion.spec.js.map +1 -1
- package/lib/test/obliterate.reconnect.spec.js +236 -59
- package/lib/test/obliterate.reconnect.spec.js.map +1 -1
- package/lib/test/testClient.js +1 -1
- package/lib/test/testClient.js.map +1 -1
- package/lib/test/testUtils.d.ts +13 -0
- package/lib/test/testUtils.d.ts.map +1 -1
- package/lib/test/testUtils.js +20 -0
- package/lib/test/testUtils.js.map +1 -1
- package/package.json +19 -18
- package/src/client.ts +286 -55
- package/src/index.ts +1 -1
- package/src/mergeTree.ts +265 -98
- package/src/mergeTreeNodes.ts +24 -18
- package/src/perspective.ts +21 -0
- package/src/segmentInfos.ts +48 -6
- package/src/sortedSegmentSet.ts +4 -0
- package/dist/test/partialSyncHelper.d.ts +0 -42
- package/dist/test/partialSyncHelper.d.ts.map +0 -1
- package/dist/test/partialSyncHelper.js +0 -96
- package/dist/test/partialSyncHelper.js.map +0 -1
- package/dist/test/reconnectHelper.d.ts +0 -50
- package/dist/test/reconnectHelper.d.ts.map +0 -1
- package/dist/test/reconnectHelper.js +0 -106
- package/dist/test/reconnectHelper.js.map +0 -1
- package/lib/test/partialSyncHelper.d.ts +0 -42
- package/lib/test/partialSyncHelper.d.ts.map +0 -1
- package/lib/test/partialSyncHelper.js +0 -92
- package/lib/test/partialSyncHelper.js.map +0 -1
- package/lib/test/reconnectHelper.d.ts +0 -50
- package/lib/test/reconnectHelper.d.ts.map +0 -1
- package/lib/test/reconnectHelper.js +0 -102
- package/lib/test/reconnectHelper.js.map +0 -1
package/lib/mergeTree.js
CHANGED
|
@@ -7,20 +7,21 @@
|
|
|
7
7
|
import { assert, Heap } from "@fluidframework/core-utils/internal";
|
|
8
8
|
import { DataProcessingError, UsageError } from "@fluidframework/telemetry-utils/internal";
|
|
9
9
|
import { DoublyLinkedList } from "./collections/index.js";
|
|
10
|
-
import { NonCollabClient, TreeMaintenanceSequenceNumber, UnassignedSequenceNumber,
|
|
10
|
+
import { NonCollabClient, TreeMaintenanceSequenceNumber, UnassignedSequenceNumber, } from "./constants.js";
|
|
11
11
|
import { EndOfTreeSegment, StartOfTreeSegment } from "./endOfTreeSegment.js";
|
|
12
|
-
import { LocalReferenceCollection, SlidingPreference, anyLocalReferencePosition,
|
|
12
|
+
import { LocalReferenceCollection, SlidingPreference, anyLocalReferencePosition, filterLocalReferencePositions, } from "./localReference.js";
|
|
13
13
|
import { MergeTreeMaintenanceType, } from "./mergeTreeDeltaCallback.js";
|
|
14
14
|
import { LeafAction, NodeAction, backwardExcursion, depthFirstNodeWalk, forwardExcursion, walkAllChildSegments, } from "./mergeTreeNodeWalk.js";
|
|
15
15
|
import { CollaborationWindow, Marker, MaxNodesInBlock, MergeBlock, assertSegmentLeaf, assignChild, getMinSeqPerspective, getMinSeqStamp, isSegmentLeaf, reservedMarkerIdKey, } from "./mergeTreeNodes.js";
|
|
16
|
+
import { UnorderedTrackingGroup } from "./mergeTreeTracking.js";
|
|
16
17
|
import { createAnnotateRangeOp, createInsertSegmentOp, createRemoveRangeOp, } from "./opBuilder.js";
|
|
17
18
|
import { MergeTreeDeltaType, ReferenceType, } from "./ops.js";
|
|
18
19
|
import { PartialSequenceLengths } from "./partialLengths.js";
|
|
19
|
-
import { PriorPerspective, LocalReconnectingPerspective, LocalDefaultPerspective, RemoteObliteratePerspective, } from "./perspective.js";
|
|
20
|
+
import { PriorPerspective, LocalReconnectingPerspective, LocalDefaultPerspective, RemoteObliteratePerspective, allAckedChangesPerspective, } from "./perspective.js";
|
|
20
21
|
import { createMap, extend, extendIfUndefined } from "./properties.js";
|
|
21
22
|
import { DetachedReferencePosition, refGetTileLabels, refHasTileLabel, refTypeIncludesFlag, } from "./referencePositions.js";
|
|
22
23
|
import { SegmentGroupCollection } from "./segmentGroupCollection.js";
|
|
23
|
-
import { assertRemoved, isMergeNodeInfo, isRemoved, overwriteInfo, removeRemovalInfo, toRemovalInfo, } from "./segmentInfos.js";
|
|
24
|
+
import { assertRemoved, isInsideObliterate, isMergeNodeInfo, isRemoved, overwriteInfo, removeRemovalInfo, toRemovalInfo, } from "./segmentInfos.js";
|
|
24
25
|
import { copyPropertiesAndManager, PropertiesManager, } from "./segmentPropertiesManager.js";
|
|
25
26
|
import { Side } from "./sequencePlace.js";
|
|
26
27
|
import { SortedSegmentSet } from "./sortedSegmentSet.js";
|
|
@@ -74,13 +75,6 @@ function ackSegment(segment, segmentGroup, opArgs, stamp) {
|
|
|
74
75
|
type: op.type === MergeTreeDeltaType.REMOVE ? "setRemove" : "sliceRemove",
|
|
75
76
|
};
|
|
76
77
|
segment.removes[segment.removes.length - 1] = removeStamp;
|
|
77
|
-
const { obliterateInfo } = segmentGroup;
|
|
78
|
-
const hasObliterateInfo = obliterateInfo !== undefined;
|
|
79
|
-
const isObliterate = op.type !== MergeTreeDeltaType.REMOVE;
|
|
80
|
-
assert(hasObliterateInfo === isObliterate, 0xa40 /* must have obliterate info */);
|
|
81
|
-
if (hasObliterateInfo) {
|
|
82
|
-
obliterateInfo.stamp = removeStamp;
|
|
83
|
-
}
|
|
84
78
|
break;
|
|
85
79
|
}
|
|
86
80
|
default: {
|
|
@@ -117,8 +111,10 @@ export function findRootMergeBlock(segmentOrNode) {
|
|
|
117
111
|
* This can reduce the number of times the tree needs to be scanned if a range containing many
|
|
118
112
|
* SlideOnRemove references is removed.
|
|
119
113
|
*/
|
|
120
|
-
function getSlideToSegment(segment, slidingPreference = SlidingPreference.FORWARD, cache, useNewSlidingBehavior = false) {
|
|
121
|
-
if (!segment ||
|
|
114
|
+
function getSlideToSegment(segment, slidingPreference = SlidingPreference.FORWARD, perspective, cache, useNewSlidingBehavior = false) {
|
|
115
|
+
if (!segment ||
|
|
116
|
+
perspective.isSegmentPresent(segment) ||
|
|
117
|
+
segment.endpointType !== undefined) {
|
|
122
118
|
return [segment, undefined];
|
|
123
119
|
}
|
|
124
120
|
const cachedSegment = cache?.get(segment);
|
|
@@ -128,7 +124,7 @@ function getSlideToSegment(segment, slidingPreference = SlidingPreference.FORWAR
|
|
|
128
124
|
const result = {};
|
|
129
125
|
cache?.set(segment, result);
|
|
130
126
|
const goFurtherToFindSlideToSegment = (seg) => {
|
|
131
|
-
if (
|
|
127
|
+
if (perspective.isSegmentPresent(seg)) {
|
|
132
128
|
result.seg = seg;
|
|
133
129
|
return false;
|
|
134
130
|
}
|
|
@@ -179,11 +175,11 @@ function getSlideToSegment(segment, slidingPreference = SlidingPreference.FORWAR
|
|
|
179
175
|
* @returns segment and offset to slide the reference to
|
|
180
176
|
* @internal
|
|
181
177
|
*/
|
|
182
|
-
export function getSlideToSegoff(segoff, slidingPreference = SlidingPreference.FORWARD, useNewSlidingBehavior = false) {
|
|
178
|
+
export function getSlideToSegoff(segoff, slidingPreference = SlidingPreference.FORWARD, perspective = allAckedChangesPerspective, useNewSlidingBehavior = false) {
|
|
183
179
|
if (!isSegmentLeaf(segoff.segment)) {
|
|
184
180
|
return segoff;
|
|
185
181
|
}
|
|
186
|
-
const [segment, _] = getSlideToSegment(segoff.segment, slidingPreference, undefined, useNewSlidingBehavior);
|
|
182
|
+
const [segment, _] = getSlideToSegment(segoff.segment, slidingPreference, perspective, undefined, useNewSlidingBehavior);
|
|
187
183
|
if (segment === segoff.segment) {
|
|
188
184
|
return segoff;
|
|
189
185
|
}
|
|
@@ -224,6 +220,9 @@ class Obliterates {
|
|
|
224
220
|
this.mergeTree.removeLocalReferencePosition(ob.data.end);
|
|
225
221
|
}
|
|
226
222
|
}
|
|
223
|
+
onNormalize() {
|
|
224
|
+
this.startOrdered.onSortOrderChange();
|
|
225
|
+
}
|
|
227
226
|
addOrUpdate(obliterateInfo) {
|
|
228
227
|
const { stamp: { seq }, start, } = obliterateInfo;
|
|
229
228
|
if (seq !== UnassignedSequenceNumber) {
|
|
@@ -252,6 +251,39 @@ class Obliterates {
|
|
|
252
251
|
}
|
|
253
252
|
return overlapping;
|
|
254
253
|
}
|
|
254
|
+
/**
|
|
255
|
+
* Remove a local obliterate from this data structure.
|
|
256
|
+
* @privateRemarks
|
|
257
|
+
* This data structure could support removing non-local obliterates if we wanted it to, but when adding support for that
|
|
258
|
+
* we should reconsider the indexing structure for seq ordered obliterates (right now it would be an O(# obliterates) operation)
|
|
259
|
+
*/
|
|
260
|
+
removeLocalObliterate(obliterateInfo) {
|
|
261
|
+
assert(obliterateInfo.stamp.seq === UnassignedSequenceNumber, 0xb6e /* Expected local obliterate */);
|
|
262
|
+
this.startOrdered.remove(obliterateInfo.start);
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* Returns an iterator over the `ObliterateInfo` for all obliterates in the collab window. Obliterates are not guaranteed to be ordered.
|
|
266
|
+
* The iterator is not guaranteed to be valid over edits to the set of obliterates.
|
|
267
|
+
*/
|
|
268
|
+
[Symbol.iterator]() {
|
|
269
|
+
let index = 0;
|
|
270
|
+
const { items: starts } = this.startOrdered;
|
|
271
|
+
const iterator = {
|
|
272
|
+
next() {
|
|
273
|
+
if (index < starts.length) {
|
|
274
|
+
const start = starts[index++];
|
|
275
|
+
const info = start.properties?.obliterate;
|
|
276
|
+
assert(info?.start !== undefined && info?.end !== undefined, 0xb6f /* Expected obliterateInfo endpoint to map to its obliterate */);
|
|
277
|
+
return { value: info, done: false };
|
|
278
|
+
}
|
|
279
|
+
return { value: undefined, done: true };
|
|
280
|
+
},
|
|
281
|
+
[Symbol.iterator]() {
|
|
282
|
+
return this;
|
|
283
|
+
},
|
|
284
|
+
};
|
|
285
|
+
return iterator;
|
|
286
|
+
}
|
|
255
287
|
}
|
|
256
288
|
/**
|
|
257
289
|
* @internal
|
|
@@ -287,13 +319,8 @@ export class MergeTree {
|
|
|
287
319
|
next.segmentGroups ??= new SegmentGroupCollection(next);
|
|
288
320
|
segment.segmentGroups.copyTo(next.segmentGroups);
|
|
289
321
|
}
|
|
290
|
-
if (segment.obliteratePrecedingInsertion) {
|
|
291
|
-
next.obliteratePrecedingInsertion = segment.obliteratePrecedingInsertion;
|
|
292
|
-
}
|
|
293
322
|
copyPropertiesAndManager(segment, next);
|
|
294
|
-
|
|
295
|
-
segment.localRefs.split(pos, next);
|
|
296
|
-
}
|
|
323
|
+
segment.localRefs?.split(pos, next);
|
|
297
324
|
this.mergeTreeMaintenanceCallback?.({
|
|
298
325
|
operation: MergeTreeMaintenanceType.SPLIT,
|
|
299
326
|
deltaSegments: [{ segment }, { segment: next }],
|
|
@@ -306,6 +333,12 @@ export class MergeTree {
|
|
|
306
333
|
this._root.mergeTree = this;
|
|
307
334
|
this.attributionPolicy = options?.attribution?.policyFactory?.();
|
|
308
335
|
}
|
|
336
|
+
rebaseObliterateTo(existing, newObliterate) {
|
|
337
|
+
this.obliterates.removeLocalObliterate(existing);
|
|
338
|
+
if (newObliterate !== undefined) {
|
|
339
|
+
this.obliterates.addOrUpdate(newObliterate);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
309
342
|
get root() {
|
|
310
343
|
return this._root;
|
|
311
344
|
}
|
|
@@ -519,7 +552,7 @@ export class MergeTree {
|
|
|
519
552
|
if (!segment.localRefs || !anyLocalReferencePosition(segment.localRefs, pred)) {
|
|
520
553
|
return;
|
|
521
554
|
}
|
|
522
|
-
const [slideToSegment, maybeEndpoint] = getSlideToSegment(segment, slidingPreference, slidingPreference === SlidingPreference.FORWARD
|
|
555
|
+
const [slideToSegment, maybeEndpoint] = getSlideToSegment(segment, slidingPreference, allAckedChangesPerspective, slidingPreference === SlidingPreference.FORWARD
|
|
523
556
|
? forwardSegmentCache
|
|
524
557
|
: backwardSegmentCache, this.options?.mergeTreeReferencesCanSlideToEndpoint);
|
|
525
558
|
const slideIsForward = slideToSegment === undefined ? false : slideToSegment.ordinal > segment.ordinal;
|
|
@@ -720,10 +753,9 @@ export class MergeTree {
|
|
|
720
753
|
this.nodeUpdateLengthNewStructure(this.root);
|
|
721
754
|
}
|
|
722
755
|
/**
|
|
723
|
-
* Assign sequence number to existing
|
|
724
|
-
* @param seq - sequence number given by server to pending segment
|
|
756
|
+
* Assign sequence number to existing segments affected by an op; update partial lengths to reflect the change
|
|
725
757
|
*/
|
|
726
|
-
|
|
758
|
+
ackOp(opArgs) {
|
|
727
759
|
const seq = opArgs.sequencedMessage.sequenceNumber;
|
|
728
760
|
const stamp = {
|
|
729
761
|
seq,
|
|
@@ -733,9 +765,25 @@ export class MergeTree {
|
|
|
733
765
|
const nodesToUpdate = [];
|
|
734
766
|
let overwrite = false;
|
|
735
767
|
if (pendingSegmentGroup !== undefined) {
|
|
768
|
+
const { obliterateInfo, segments } = pendingSegmentGroup;
|
|
769
|
+
const hasObliterateInfo = obliterateInfo !== undefined;
|
|
770
|
+
const isObliterate = opArgs.op.type === MergeTreeDeltaType.OBLITERATE ||
|
|
771
|
+
opArgs.op.type === MergeTreeDeltaType.OBLITERATE_SIDED;
|
|
772
|
+
assert(hasObliterateInfo === isObliterate, 0xa40 /* must have obliterate info */);
|
|
773
|
+
if (hasObliterateInfo) {
|
|
774
|
+
obliterateInfo.stamp = { ...stamp, type: "sliceRemove" };
|
|
775
|
+
this.obliterates.addOrUpdate(obliterateInfo);
|
|
776
|
+
assert(obliterateInfo.tiebreakTrackingGroup !== undefined, 0xb70 /* obliterateInfo should have a tiebreak tracking group on ack */);
|
|
777
|
+
for (const segment of obliterateInfo.tiebreakTrackingGroup.tracked) {
|
|
778
|
+
segment.trackingCollection.unlink(obliterateInfo.tiebreakTrackingGroup);
|
|
779
|
+
assert(isSegmentLeaf(segment) && isInsideObliterate(segment), 0xb71 /* Expected segment leaf inside obliterate */);
|
|
780
|
+
segment.insertionRefSeqStamp = undefined;
|
|
781
|
+
}
|
|
782
|
+
obliterateInfo.tiebreakTrackingGroup = undefined;
|
|
783
|
+
}
|
|
736
784
|
const deltaSegments = [];
|
|
737
785
|
const overlappingRemoves = [];
|
|
738
|
-
|
|
786
|
+
segments.map((pendingSegment) => {
|
|
739
787
|
const overlappingRemove = !ackSegment(pendingSegment, pendingSegmentGroup, opArgs, stamp);
|
|
740
788
|
overwrite ||= overlappingRemove;
|
|
741
789
|
overlappingRemoves.push(overlappingRemove);
|
|
@@ -749,16 +797,12 @@ export class MergeTree {
|
|
|
749
797
|
segment: pendingSegment,
|
|
750
798
|
});
|
|
751
799
|
});
|
|
752
|
-
if (pendingSegmentGroup.obliterateInfo !== undefined) {
|
|
753
|
-
pendingSegmentGroup.obliterateInfo.stamp = { type: "sliceRemove", ...stamp };
|
|
754
|
-
this.obliterates.addOrUpdate(pendingSegmentGroup.obliterateInfo);
|
|
755
|
-
}
|
|
756
800
|
// Perform slides after all segments have been acked, so that
|
|
757
801
|
// positions after slide are final
|
|
758
802
|
if (opArgs.op.type === MergeTreeDeltaType.REMOVE ||
|
|
759
803
|
opArgs.op.type === MergeTreeDeltaType.OBLITERATE ||
|
|
760
804
|
opArgs.op.type === MergeTreeDeltaType.OBLITERATE_SIDED) {
|
|
761
|
-
this.slideAckedRemovedSegmentReferences(
|
|
805
|
+
this.slideAckedRemovedSegmentReferences(segments);
|
|
762
806
|
}
|
|
763
807
|
this.mergeTreeMaintenanceCallback?.({
|
|
764
808
|
deltaSegments,
|
|
@@ -775,6 +819,7 @@ export class MergeTree {
|
|
|
775
819
|
addToPendingList(segment, segmentGroup, localSeq, previousProps) {
|
|
776
820
|
let _segmentGroup = segmentGroup;
|
|
777
821
|
if (_segmentGroup === undefined) {
|
|
822
|
+
assert(localSeq !== undefined, 0xb72 /* Local seq should be passed when creating new segment group */);
|
|
778
823
|
_segmentGroup = {
|
|
779
824
|
segments: [],
|
|
780
825
|
localSeq,
|
|
@@ -952,16 +997,15 @@ export class MergeTree {
|
|
|
952
997
|
saveIfLocal(newSegment);
|
|
953
998
|
continue;
|
|
954
999
|
}
|
|
1000
|
+
const refSeqStamp = {
|
|
1001
|
+
seq: perspective.refSeq,
|
|
1002
|
+
clientId: stamp.clientId,
|
|
1003
|
+
};
|
|
955
1004
|
const overlappingAckedObliterates = [];
|
|
956
1005
|
let oldest;
|
|
957
1006
|
let newest;
|
|
958
1007
|
let newestAcked;
|
|
959
1008
|
let oldestUnacked;
|
|
960
|
-
const refSeqStamp = {
|
|
961
|
-
seq: perspective.refSeq,
|
|
962
|
-
clientId: stamp.clientId,
|
|
963
|
-
localSeq: stamp.localSeq,
|
|
964
|
-
};
|
|
965
1009
|
for (const ob of this.obliterates.findOverlapping(newSegment)) {
|
|
966
1010
|
if (opstampUtils.greaterThan(ob.stamp, refSeqStamp)) {
|
|
967
1011
|
// Any obliterate from the same client that's inserting this segment cannot cause the segment to be marked as
|
|
@@ -994,7 +1038,16 @@ export class MergeTree {
|
|
|
994
1038
|
}
|
|
995
1039
|
}
|
|
996
1040
|
}
|
|
997
|
-
newSegment
|
|
1041
|
+
overwriteInfo(newSegment, {
|
|
1042
|
+
obliteratePrecedingInsertion: newest,
|
|
1043
|
+
});
|
|
1044
|
+
if (newest !== undefined && opstampUtils.isLocal(newest.stamp)) {
|
|
1045
|
+
assert(newest?.tiebreakTrackingGroup !== undefined, 0xb73 /* Expected local obliterateinfo to have tiebreak group */);
|
|
1046
|
+
newest.tiebreakTrackingGroup.link(newSegment);
|
|
1047
|
+
overwriteInfo(newSegment, {
|
|
1048
|
+
insertionRefSeqStamp: refSeqStamp,
|
|
1049
|
+
});
|
|
1050
|
+
}
|
|
998
1051
|
// See doc comment on obliteratePrecedingInsertion for more details: if the newest obliterate was performed
|
|
999
1052
|
// by the same client that's inserting this segment, we let them insert into this range and therefore don't
|
|
1000
1053
|
// mark it obliterated.
|
|
@@ -1025,6 +1078,16 @@ export class MergeTree {
|
|
|
1025
1078
|
saveIfLocal(newSegment);
|
|
1026
1079
|
}
|
|
1027
1080
|
}
|
|
1081
|
+
computeObliteratePrecedingInsertion(segment, refSeqStamp) {
|
|
1082
|
+
let newest;
|
|
1083
|
+
for (const ob of this.obliterates.findOverlapping(segment)) {
|
|
1084
|
+
if (opstampUtils.greaterThan(ob.stamp, refSeqStamp) &&
|
|
1085
|
+
(newest === undefined || opstampUtils.greaterThan(ob.stamp, newest.stamp))) {
|
|
1086
|
+
newest = ob;
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
return newest;
|
|
1090
|
+
}
|
|
1028
1091
|
ensureIntervalBoundary(pos, perspective) {
|
|
1029
1092
|
this.insertingWalk(pos, perspective, {
|
|
1030
1093
|
seq: TreeMaintenanceSequenceNumber,
|
|
@@ -1047,12 +1110,12 @@ export class MergeTree {
|
|
|
1047
1110
|
}
|
|
1048
1111
|
}
|
|
1049
1112
|
insertingWalk(pos, perspective, stamp, context) {
|
|
1050
|
-
const { remainder } = this.insertRecursive(this.root, pos, perspective, stamp, context);
|
|
1113
|
+
const { remainder } = this.insertRecursive(this.root, pos, perspective, stamp, context, true);
|
|
1051
1114
|
if (remainder !== undefined) {
|
|
1052
1115
|
this.updateRoot(remainder);
|
|
1053
1116
|
}
|
|
1054
1117
|
}
|
|
1055
|
-
insertRecursive(block, pos, perspective, stamp, context,
|
|
1118
|
+
insertRecursive(block, pos, perspective, stamp, context, isLastBlock) {
|
|
1056
1119
|
let _pos = pos;
|
|
1057
1120
|
const children = block.children;
|
|
1058
1121
|
let childIndex;
|
|
@@ -1062,9 +1125,11 @@ export class MergeTree {
|
|
|
1062
1125
|
let hadChanges = false;
|
|
1063
1126
|
for (childIndex = 0; childIndex < block.childCount; childIndex++) {
|
|
1064
1127
|
child = children[childIndex];
|
|
1065
|
-
//
|
|
1066
|
-
|
|
1067
|
-
|
|
1128
|
+
// removed blocks below the min seq will have an undefined length, and be skipped
|
|
1129
|
+
// however if it is the last block in the layer of the tree we don't want to skip it, so we correctly
|
|
1130
|
+
// walk down the far edge of the tree.
|
|
1131
|
+
const isLastChildOfLastBlock = isLastBlock && childIndex === block.childCount - 1;
|
|
1132
|
+
const len = this.nodeLength(child, perspective) ?? (isLastChildOfLastBlock ? 0 : undefined);
|
|
1068
1133
|
if (len === undefined) {
|
|
1069
1134
|
// if the seg len is undefined, the segment
|
|
1070
1135
|
// will be removed, so should just be skipped for now
|
|
@@ -1091,9 +1156,8 @@ export class MergeTree {
|
|
|
1091
1156
|
}
|
|
1092
1157
|
}
|
|
1093
1158
|
else {
|
|
1094
|
-
const childBlock = child;
|
|
1095
1159
|
// Internal node
|
|
1096
|
-
const insertResult = this.insertRecursive(
|
|
1160
|
+
const insertResult = this.insertRecursive(child, _pos, perspective, stamp, context, isLastChildOfLastBlock);
|
|
1097
1161
|
hadChanges ||= insertResult.hadChanges;
|
|
1098
1162
|
if (insertResult.remainder === undefined) {
|
|
1099
1163
|
if (insertResult.hadChanges) {
|
|
@@ -1240,33 +1304,36 @@ export class MergeTree {
|
|
|
1240
1304
|
let _overwrite = false;
|
|
1241
1305
|
const localOverlapWithRefs = [];
|
|
1242
1306
|
const removedSegments = [];
|
|
1307
|
+
const createRefFromSequencePlace = (place) => {
|
|
1308
|
+
const { segment: placeSeg, offset: placeOffset } = this.getContainingSegment(place.pos, perspective);
|
|
1309
|
+
assert(isSegmentLeaf(placeSeg) && placeOffset !== undefined, 0xa3f /* segments cannot be undefined */);
|
|
1310
|
+
return this.createLocalReferencePosition(placeSeg, placeOffset, ReferenceType.StayOnRemove, undefined);
|
|
1311
|
+
};
|
|
1243
1312
|
const obliterate = {
|
|
1244
|
-
start:
|
|
1245
|
-
|
|
1313
|
+
start: createRefFromSequencePlace(start),
|
|
1314
|
+
startSide: start.side,
|
|
1315
|
+
end: createRefFromSequencePlace(end),
|
|
1316
|
+
endSide: end.side,
|
|
1246
1317
|
refSeq: perspective.refSeq,
|
|
1247
1318
|
stamp,
|
|
1248
1319
|
segmentGroup: undefined,
|
|
1320
|
+
tiebreakTrackingGroup: undefined,
|
|
1249
1321
|
};
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
obliterate.start = this.createLocalReferencePosition(startSeg, start.side === Side.Before ? 0 : Math.max(startSeg.cachedLength - 1, 0), ReferenceType.StayOnRemove, {
|
|
1254
|
-
obliterate,
|
|
1255
|
-
});
|
|
1256
|
-
obliterate.end = this.createLocalReferencePosition(endSeg, end.side === Side.Before ? 0 : Math.max(endSeg.cachedLength - 1, 0), ReferenceType.StayOnRemove, {
|
|
1257
|
-
obliterate,
|
|
1258
|
-
});
|
|
1259
|
-
// Always create a segment group for obliterate,
|
|
1260
|
-
// even if there are no segments currently in the obliteration range.
|
|
1261
|
-
// Segments may be concurrently inserted into the obliteration range,
|
|
1262
|
-
// at which point they are added to the segment group.
|
|
1263
|
-
obliterate.segmentGroup = {
|
|
1264
|
-
segments: [],
|
|
1265
|
-
localSeq: stamp.localSeq,
|
|
1266
|
-
refSeq: this.collabWindow.currentSeq,
|
|
1267
|
-
obliterateInfo: obliterate,
|
|
1268
|
-
};
|
|
1322
|
+
// Link references back to this obliterate info
|
|
1323
|
+
obliterate.start.addProperties({ obliterate });
|
|
1324
|
+
obliterate.end.addProperties({ obliterate });
|
|
1269
1325
|
if (this.collabWindow.collaborating && stamp.clientId === this.collabWindow.clientId) {
|
|
1326
|
+
// Always create a segment group for local obliterates,
|
|
1327
|
+
// even if there are no segments currently in the obliteration range.
|
|
1328
|
+
// Segments may be concurrently inserted into the obliteration range,
|
|
1329
|
+
// at which point they are added to the segment group.
|
|
1330
|
+
obliterate.segmentGroup = {
|
|
1331
|
+
segments: [],
|
|
1332
|
+
localSeq: stamp.localSeq,
|
|
1333
|
+
refSeq: this.collabWindow.currentSeq,
|
|
1334
|
+
obliterateInfo: obliterate,
|
|
1335
|
+
};
|
|
1336
|
+
obliterate.tiebreakTrackingGroup = new UnorderedTrackingGroup();
|
|
1270
1337
|
this.pendingSegments.push(obliterate.segmentGroup);
|
|
1271
1338
|
}
|
|
1272
1339
|
this.obliterates.addOrUpdate(obliterate);
|
|
@@ -1284,6 +1351,7 @@ export class MergeTree {
|
|
|
1284
1351
|
// Specifically, we want to avoid marking a local-only segment as obliterated when we know one of our own local obliterates
|
|
1285
1352
|
// will win against the obliterate we're processing, hence the early exit.
|
|
1286
1353
|
if (opstampUtils.isLocal(segment.insert) &&
|
|
1354
|
+
isInsideObliterate(segment) &&
|
|
1287
1355
|
segment.obliteratePrecedingInsertion?.stamp.seq === UnassignedSequenceNumber &&
|
|
1288
1356
|
opstampUtils.isAcked(stamp)) {
|
|
1289
1357
|
// We chose to not obliterate this segment because we are aware of an unacked local obliteration.
|
|
@@ -1449,6 +1517,10 @@ export class MergeTree {
|
|
|
1449
1517
|
* Revert an unacked local op
|
|
1450
1518
|
*/
|
|
1451
1519
|
rollback(op, localOpMetadata) {
|
|
1520
|
+
const rollbackStamp = {
|
|
1521
|
+
seq: TreeMaintenanceSequenceNumber,
|
|
1522
|
+
clientId: NonCollabClient,
|
|
1523
|
+
};
|
|
1452
1524
|
if (op.type === MergeTreeDeltaType.REMOVE) {
|
|
1453
1525
|
const pendingSegmentGroup = this.pendingSegments.pop()?.data;
|
|
1454
1526
|
if (pendingSegmentGroup === undefined || pendingSegmentGroup !== localOpMetadata) {
|
|
@@ -1462,16 +1534,10 @@ export class MergeTree {
|
|
|
1462
1534
|
assert(isRemoved(segment) &&
|
|
1463
1535
|
segment.removes[0].clientId === this.collabWindow.clientId &&
|
|
1464
1536
|
segment.removes[0].type === "setRemove", 0x39d /* Rollback segment removedClientId does not match local client */);
|
|
1465
|
-
let updateNode = segment.parent;
|
|
1466
1537
|
// This also removes obliterates, but that should be ok as we can only remove a segment once.
|
|
1467
1538
|
// If we were able to remove it locally, that also means there are no remote removals (since rollback is synchronous).
|
|
1468
1539
|
removeRemovalInfo(segment);
|
|
1469
|
-
|
|
1470
|
-
this.blockUpdateLength(updateNode, {
|
|
1471
|
-
seq: UnassignedSequenceNumber,
|
|
1472
|
-
clientId: this.collabWindow.clientId,
|
|
1473
|
-
});
|
|
1474
|
-
}
|
|
1540
|
+
this.blockUpdatePathLengths(segment.parent, rollbackStamp);
|
|
1475
1541
|
// Note: optional chaining short-circuits:
|
|
1476
1542
|
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Optional_chaining#short-circuiting
|
|
1477
1543
|
this.mergeTreeDeltaCallback?.({
|
|
@@ -1499,25 +1565,19 @@ export class MergeTree {
|
|
|
1499
1565
|
if (op.type === MergeTreeDeltaType.INSERT) {
|
|
1500
1566
|
segment.insert = {
|
|
1501
1567
|
type: "insert",
|
|
1502
|
-
|
|
1503
|
-
clientId: this.collabWindow.clientId,
|
|
1568
|
+
...rollbackStamp,
|
|
1504
1569
|
};
|
|
1505
1570
|
const removeOp = createRemoveRangeOp(start, start + segment.cachedLength);
|
|
1506
1571
|
const removeStamp = {
|
|
1507
1572
|
type: "setRemove",
|
|
1508
|
-
|
|
1509
|
-
clientId: this.collabWindow.clientId,
|
|
1573
|
+
...rollbackStamp,
|
|
1510
1574
|
};
|
|
1511
1575
|
this.markRangeRemoved(start, start + segment.cachedLength, this.localPerspective, removeStamp, { op: removeOp, rollback: true });
|
|
1512
1576
|
} /* op.type === MergeTreeDeltaType.ANNOTATE */
|
|
1513
1577
|
else {
|
|
1514
1578
|
const props = pendingSegmentGroup.previousProps[i];
|
|
1515
1579
|
const annotateOp = createAnnotateRangeOp(start, start + segment.cachedLength, props);
|
|
1516
|
-
|
|
1517
|
-
seq: UniversalSequenceNumber,
|
|
1518
|
-
clientId: this.collabWindow.clientId,
|
|
1519
|
-
};
|
|
1520
|
-
this.annotateRange(start, start + segment.cachedLength, { props }, this.localPerspective, annotateStamp, { op: annotateOp, rollback: true });
|
|
1580
|
+
this.annotateRange(start, start + segment.cachedLength, { props }, this.localPerspective, rollbackStamp, { op: annotateOp, rollback: true });
|
|
1521
1581
|
i++;
|
|
1522
1582
|
}
|
|
1523
1583
|
}
|
|
@@ -1578,7 +1638,17 @@ export class MergeTree {
|
|
|
1578
1638
|
const segRef = localRefs.createLocalRef(offset, refType, properties, slidingPreference, canSlideToEndpoint);
|
|
1579
1639
|
return segRef;
|
|
1580
1640
|
}
|
|
1581
|
-
|
|
1641
|
+
/**
|
|
1642
|
+
* Segments should either be removed remotely, removed locally, or inserted locally
|
|
1643
|
+
*
|
|
1644
|
+
* See description of {@link normalizeSegmentsOnRebase}.
|
|
1645
|
+
*
|
|
1646
|
+
* This normalizes a block of adjacent segments whose positions have collapsed between the time of the original submission and now
|
|
1647
|
+
* such that removed segments come after ones that still exist.
|
|
1648
|
+
*
|
|
1649
|
+
* TODO:AB#34898: It looks like this method has some bugs, search code for this tag for an example test that demonstrates
|
|
1650
|
+
* segment normalization yielding an order that remote clients wouldn't have seen.
|
|
1651
|
+
*/
|
|
1582
1652
|
normalizeAdjacentSegments(affectedSegments) {
|
|
1583
1653
|
// Eagerly demand this since we're about to shift elements in the list around
|
|
1584
1654
|
const currentOrder = Array.from(affectedSegments, ({ data: seg }) => ({
|
|
@@ -1713,6 +1783,45 @@ export class MergeTree {
|
|
|
1713
1783
|
return true;
|
|
1714
1784
|
});
|
|
1715
1785
|
normalize();
|
|
1786
|
+
this.obliterates.onNormalize();
|
|
1787
|
+
const segmentTiebreakChanges = new Set();
|
|
1788
|
+
for (const info of this.obliterates) {
|
|
1789
|
+
if (info.tiebreakTrackingGroup !== undefined) {
|
|
1790
|
+
for (const segment of info.tiebreakTrackingGroup.tracked) {
|
|
1791
|
+
// Recompute previous obliterate
|
|
1792
|
+
assert(isSegmentLeaf(segment) &&
|
|
1793
|
+
isInsideObliterate(segment) &&
|
|
1794
|
+
segment.insertionRefSeqStamp !== undefined, 0xb74 /* Expected segment leaf inside obliterate with insertionRefSeqStamp */);
|
|
1795
|
+
// This may have changed as a result of segments shuffling: outstanding local obliterates that previously surrounded a segment may no longer surround it.
|
|
1796
|
+
const newObliteratePrecedingInsertion = this.computeObliteratePrecedingInsertion(segment, segment.insertionRefSeqStamp);
|
|
1797
|
+
if (newObliteratePrecedingInsertion !== info) {
|
|
1798
|
+
segmentTiebreakChanges.add({
|
|
1799
|
+
segment,
|
|
1800
|
+
old: info,
|
|
1801
|
+
new: newObliteratePrecedingInsertion,
|
|
1802
|
+
});
|
|
1803
|
+
}
|
|
1804
|
+
}
|
|
1805
|
+
}
|
|
1806
|
+
}
|
|
1807
|
+
for (const { segment, old, new: newInfo } of segmentTiebreakChanges) {
|
|
1808
|
+
// Update tiebreak tracking groups on old/new segment as well as `ISegmentInsideObliterateInfo` state
|
|
1809
|
+
// which we only keep around as long as obliterates are in flight.
|
|
1810
|
+
old.tiebreakTrackingGroup?.unlink(segment);
|
|
1811
|
+
if (newInfo?.tiebreakTrackingGroup === undefined) {
|
|
1812
|
+
// Segment is either no longer inside any obliterate or only inside acked obliterates.
|
|
1813
|
+
overwriteInfo(segment, {
|
|
1814
|
+
obliteratePrecedingInsertion: newInfo,
|
|
1815
|
+
insertionRefSeqStamp: undefined,
|
|
1816
|
+
});
|
|
1817
|
+
}
|
|
1818
|
+
else {
|
|
1819
|
+
overwriteInfo(segment, {
|
|
1820
|
+
obliteratePrecedingInsertion: newInfo,
|
|
1821
|
+
});
|
|
1822
|
+
newInfo.tiebreakTrackingGroup.link(segment);
|
|
1823
|
+
}
|
|
1824
|
+
}
|
|
1716
1825
|
}
|
|
1717
1826
|
blockUpdate(block) {
|
|
1718
1827
|
let len;
|