@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/dist/mergeTree.js
CHANGED
|
@@ -39,6 +39,7 @@ const localReference_js_1 = require("./localReference.js");
|
|
|
39
39
|
const mergeTreeDeltaCallback_js_1 = require("./mergeTreeDeltaCallback.js");
|
|
40
40
|
const mergeTreeNodeWalk_js_1 = require("./mergeTreeNodeWalk.js");
|
|
41
41
|
const mergeTreeNodes_js_1 = require("./mergeTreeNodes.js");
|
|
42
|
+
const mergeTreeTracking_js_1 = require("./mergeTreeTracking.js");
|
|
42
43
|
const opBuilder_js_1 = require("./opBuilder.js");
|
|
43
44
|
const ops_js_1 = require("./ops.js");
|
|
44
45
|
const partialLengths_js_1 = require("./partialLengths.js");
|
|
@@ -101,13 +102,6 @@ function ackSegment(segment, segmentGroup, opArgs, stamp) {
|
|
|
101
102
|
type: op.type === ops_js_1.MergeTreeDeltaType.REMOVE ? "setRemove" : "sliceRemove",
|
|
102
103
|
};
|
|
103
104
|
segment.removes[segment.removes.length - 1] = removeStamp;
|
|
104
|
-
const { obliterateInfo } = segmentGroup;
|
|
105
|
-
const hasObliterateInfo = obliterateInfo !== undefined;
|
|
106
|
-
const isObliterate = op.type !== ops_js_1.MergeTreeDeltaType.REMOVE;
|
|
107
|
-
(0, internal_1.assert)(hasObliterateInfo === isObliterate, 0xa40 /* must have obliterate info */);
|
|
108
|
-
if (hasObliterateInfo) {
|
|
109
|
-
obliterateInfo.stamp = removeStamp;
|
|
110
|
-
}
|
|
111
105
|
break;
|
|
112
106
|
}
|
|
113
107
|
default: {
|
|
@@ -146,8 +140,10 @@ exports.findRootMergeBlock = findRootMergeBlock;
|
|
|
146
140
|
* This can reduce the number of times the tree needs to be scanned if a range containing many
|
|
147
141
|
* SlideOnRemove references is removed.
|
|
148
142
|
*/
|
|
149
|
-
function getSlideToSegment(segment, slidingPreference = localReference_js_1.SlidingPreference.FORWARD, cache, useNewSlidingBehavior = false) {
|
|
150
|
-
if (!segment ||
|
|
143
|
+
function getSlideToSegment(segment, slidingPreference = localReference_js_1.SlidingPreference.FORWARD, perspective, cache, useNewSlidingBehavior = false) {
|
|
144
|
+
if (!segment ||
|
|
145
|
+
perspective.isSegmentPresent(segment) ||
|
|
146
|
+
segment.endpointType !== undefined) {
|
|
151
147
|
return [segment, undefined];
|
|
152
148
|
}
|
|
153
149
|
const cachedSegment = cache?.get(segment);
|
|
@@ -157,7 +153,7 @@ function getSlideToSegment(segment, slidingPreference = localReference_js_1.Slid
|
|
|
157
153
|
const result = {};
|
|
158
154
|
cache?.set(segment, result);
|
|
159
155
|
const goFurtherToFindSlideToSegment = (seg) => {
|
|
160
|
-
if (
|
|
156
|
+
if (perspective.isSegmentPresent(seg)) {
|
|
161
157
|
result.seg = seg;
|
|
162
158
|
return false;
|
|
163
159
|
}
|
|
@@ -208,11 +204,11 @@ function getSlideToSegment(segment, slidingPreference = localReference_js_1.Slid
|
|
|
208
204
|
* @returns segment and offset to slide the reference to
|
|
209
205
|
* @internal
|
|
210
206
|
*/
|
|
211
|
-
function getSlideToSegoff(segoff, slidingPreference = localReference_js_1.SlidingPreference.FORWARD, useNewSlidingBehavior = false) {
|
|
207
|
+
function getSlideToSegoff(segoff, slidingPreference = localReference_js_1.SlidingPreference.FORWARD, perspective = perspective_js_1.allAckedChangesPerspective, useNewSlidingBehavior = false) {
|
|
212
208
|
if (!(0, mergeTreeNodes_js_1.isSegmentLeaf)(segoff.segment)) {
|
|
213
209
|
return segoff;
|
|
214
210
|
}
|
|
215
|
-
const [segment, _] = getSlideToSegment(segoff.segment, slidingPreference, undefined, useNewSlidingBehavior);
|
|
211
|
+
const [segment, _] = getSlideToSegment(segoff.segment, slidingPreference, perspective, undefined, useNewSlidingBehavior);
|
|
216
212
|
if (segment === segoff.segment) {
|
|
217
213
|
return segoff;
|
|
218
214
|
}
|
|
@@ -254,6 +250,9 @@ class Obliterates {
|
|
|
254
250
|
this.mergeTree.removeLocalReferencePosition(ob.data.end);
|
|
255
251
|
}
|
|
256
252
|
}
|
|
253
|
+
onNormalize() {
|
|
254
|
+
this.startOrdered.onSortOrderChange();
|
|
255
|
+
}
|
|
257
256
|
addOrUpdate(obliterateInfo) {
|
|
258
257
|
const { stamp: { seq }, start, } = obliterateInfo;
|
|
259
258
|
if (seq !== constants_js_1.UnassignedSequenceNumber) {
|
|
@@ -282,6 +281,39 @@ class Obliterates {
|
|
|
282
281
|
}
|
|
283
282
|
return overlapping;
|
|
284
283
|
}
|
|
284
|
+
/**
|
|
285
|
+
* Remove a local obliterate from this data structure.
|
|
286
|
+
* @privateRemarks
|
|
287
|
+
* This data structure could support removing non-local obliterates if we wanted it to, but when adding support for that
|
|
288
|
+
* we should reconsider the indexing structure for seq ordered obliterates (right now it would be an O(# obliterates) operation)
|
|
289
|
+
*/
|
|
290
|
+
removeLocalObliterate(obliterateInfo) {
|
|
291
|
+
(0, internal_1.assert)(obliterateInfo.stamp.seq === constants_js_1.UnassignedSequenceNumber, 0xb6e /* Expected local obliterate */);
|
|
292
|
+
this.startOrdered.remove(obliterateInfo.start);
|
|
293
|
+
}
|
|
294
|
+
/**
|
|
295
|
+
* Returns an iterator over the `ObliterateInfo` for all obliterates in the collab window. Obliterates are not guaranteed to be ordered.
|
|
296
|
+
* The iterator is not guaranteed to be valid over edits to the set of obliterates.
|
|
297
|
+
*/
|
|
298
|
+
[Symbol.iterator]() {
|
|
299
|
+
let index = 0;
|
|
300
|
+
const { items: starts } = this.startOrdered;
|
|
301
|
+
const iterator = {
|
|
302
|
+
next() {
|
|
303
|
+
if (index < starts.length) {
|
|
304
|
+
const start = starts[index++];
|
|
305
|
+
const info = start.properties?.obliterate;
|
|
306
|
+
(0, internal_1.assert)(info?.start !== undefined && info?.end !== undefined, 0xb6f /* Expected obliterateInfo endpoint to map to its obliterate */);
|
|
307
|
+
return { value: info, done: false };
|
|
308
|
+
}
|
|
309
|
+
return { value: undefined, done: true };
|
|
310
|
+
},
|
|
311
|
+
[Symbol.iterator]() {
|
|
312
|
+
return this;
|
|
313
|
+
},
|
|
314
|
+
};
|
|
315
|
+
return iterator;
|
|
316
|
+
}
|
|
285
317
|
}
|
|
286
318
|
/**
|
|
287
319
|
* @internal
|
|
@@ -317,13 +349,8 @@ class MergeTree {
|
|
|
317
349
|
next.segmentGroups ??= new segmentGroupCollection_js_1.SegmentGroupCollection(next);
|
|
318
350
|
segment.segmentGroups.copyTo(next.segmentGroups);
|
|
319
351
|
}
|
|
320
|
-
if (segment.obliteratePrecedingInsertion) {
|
|
321
|
-
next.obliteratePrecedingInsertion = segment.obliteratePrecedingInsertion;
|
|
322
|
-
}
|
|
323
352
|
(0, segmentPropertiesManager_js_1.copyPropertiesAndManager)(segment, next);
|
|
324
|
-
|
|
325
|
-
segment.localRefs.split(pos, next);
|
|
326
|
-
}
|
|
353
|
+
segment.localRefs?.split(pos, next);
|
|
327
354
|
this.mergeTreeMaintenanceCallback?.({
|
|
328
355
|
operation: mergeTreeDeltaCallback_js_1.MergeTreeMaintenanceType.SPLIT,
|
|
329
356
|
deltaSegments: [{ segment }, { segment: next }],
|
|
@@ -336,6 +363,12 @@ class MergeTree {
|
|
|
336
363
|
this._root.mergeTree = this;
|
|
337
364
|
this.attributionPolicy = options?.attribution?.policyFactory?.();
|
|
338
365
|
}
|
|
366
|
+
rebaseObliterateTo(existing, newObliterate) {
|
|
367
|
+
this.obliterates.removeLocalObliterate(existing);
|
|
368
|
+
if (newObliterate !== undefined) {
|
|
369
|
+
this.obliterates.addOrUpdate(newObliterate);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
339
372
|
get root() {
|
|
340
373
|
return this._root;
|
|
341
374
|
}
|
|
@@ -549,7 +582,7 @@ class MergeTree {
|
|
|
549
582
|
if (!segment.localRefs || !(0, localReference_js_1.anyLocalReferencePosition)(segment.localRefs, pred)) {
|
|
550
583
|
return;
|
|
551
584
|
}
|
|
552
|
-
const [slideToSegment, maybeEndpoint] = getSlideToSegment(segment, slidingPreference, slidingPreference === localReference_js_1.SlidingPreference.FORWARD
|
|
585
|
+
const [slideToSegment, maybeEndpoint] = getSlideToSegment(segment, slidingPreference, perspective_js_1.allAckedChangesPerspective, slidingPreference === localReference_js_1.SlidingPreference.FORWARD
|
|
553
586
|
? forwardSegmentCache
|
|
554
587
|
: backwardSegmentCache, this.options?.mergeTreeReferencesCanSlideToEndpoint);
|
|
555
588
|
const slideIsForward = slideToSegment === undefined ? false : slideToSegment.ordinal > segment.ordinal;
|
|
@@ -750,10 +783,9 @@ class MergeTree {
|
|
|
750
783
|
this.nodeUpdateLengthNewStructure(this.root);
|
|
751
784
|
}
|
|
752
785
|
/**
|
|
753
|
-
* Assign sequence number to existing
|
|
754
|
-
* @param seq - sequence number given by server to pending segment
|
|
786
|
+
* Assign sequence number to existing segments affected by an op; update partial lengths to reflect the change
|
|
755
787
|
*/
|
|
756
|
-
|
|
788
|
+
ackOp(opArgs) {
|
|
757
789
|
const seq = opArgs.sequencedMessage.sequenceNumber;
|
|
758
790
|
const stamp = {
|
|
759
791
|
seq,
|
|
@@ -763,9 +795,25 @@ class MergeTree {
|
|
|
763
795
|
const nodesToUpdate = [];
|
|
764
796
|
let overwrite = false;
|
|
765
797
|
if (pendingSegmentGroup !== undefined) {
|
|
798
|
+
const { obliterateInfo, segments } = pendingSegmentGroup;
|
|
799
|
+
const hasObliterateInfo = obliterateInfo !== undefined;
|
|
800
|
+
const isObliterate = opArgs.op.type === ops_js_1.MergeTreeDeltaType.OBLITERATE ||
|
|
801
|
+
opArgs.op.type === ops_js_1.MergeTreeDeltaType.OBLITERATE_SIDED;
|
|
802
|
+
(0, internal_1.assert)(hasObliterateInfo === isObliterate, 0xa40 /* must have obliterate info */);
|
|
803
|
+
if (hasObliterateInfo) {
|
|
804
|
+
obliterateInfo.stamp = { ...stamp, type: "sliceRemove" };
|
|
805
|
+
this.obliterates.addOrUpdate(obliterateInfo);
|
|
806
|
+
(0, internal_1.assert)(obliterateInfo.tiebreakTrackingGroup !== undefined, 0xb70 /* obliterateInfo should have a tiebreak tracking group on ack */);
|
|
807
|
+
for (const segment of obliterateInfo.tiebreakTrackingGroup.tracked) {
|
|
808
|
+
segment.trackingCollection.unlink(obliterateInfo.tiebreakTrackingGroup);
|
|
809
|
+
(0, internal_1.assert)((0, mergeTreeNodes_js_1.isSegmentLeaf)(segment) && (0, segmentInfos_js_1.isInsideObliterate)(segment), 0xb71 /* Expected segment leaf inside obliterate */);
|
|
810
|
+
segment.insertionRefSeqStamp = undefined;
|
|
811
|
+
}
|
|
812
|
+
obliterateInfo.tiebreakTrackingGroup = undefined;
|
|
813
|
+
}
|
|
766
814
|
const deltaSegments = [];
|
|
767
815
|
const overlappingRemoves = [];
|
|
768
|
-
|
|
816
|
+
segments.map((pendingSegment) => {
|
|
769
817
|
const overlappingRemove = !ackSegment(pendingSegment, pendingSegmentGroup, opArgs, stamp);
|
|
770
818
|
overwrite ||= overlappingRemove;
|
|
771
819
|
overlappingRemoves.push(overlappingRemove);
|
|
@@ -779,16 +827,12 @@ class MergeTree {
|
|
|
779
827
|
segment: pendingSegment,
|
|
780
828
|
});
|
|
781
829
|
});
|
|
782
|
-
if (pendingSegmentGroup.obliterateInfo !== undefined) {
|
|
783
|
-
pendingSegmentGroup.obliterateInfo.stamp = { type: "sliceRemove", ...stamp };
|
|
784
|
-
this.obliterates.addOrUpdate(pendingSegmentGroup.obliterateInfo);
|
|
785
|
-
}
|
|
786
830
|
// Perform slides after all segments have been acked, so that
|
|
787
831
|
// positions after slide are final
|
|
788
832
|
if (opArgs.op.type === ops_js_1.MergeTreeDeltaType.REMOVE ||
|
|
789
833
|
opArgs.op.type === ops_js_1.MergeTreeDeltaType.OBLITERATE ||
|
|
790
834
|
opArgs.op.type === ops_js_1.MergeTreeDeltaType.OBLITERATE_SIDED) {
|
|
791
|
-
this.slideAckedRemovedSegmentReferences(
|
|
835
|
+
this.slideAckedRemovedSegmentReferences(segments);
|
|
792
836
|
}
|
|
793
837
|
this.mergeTreeMaintenanceCallback?.({
|
|
794
838
|
deltaSegments,
|
|
@@ -805,6 +849,7 @@ class MergeTree {
|
|
|
805
849
|
addToPendingList(segment, segmentGroup, localSeq, previousProps) {
|
|
806
850
|
let _segmentGroup = segmentGroup;
|
|
807
851
|
if (_segmentGroup === undefined) {
|
|
852
|
+
(0, internal_1.assert)(localSeq !== undefined, 0xb72 /* Local seq should be passed when creating new segment group */);
|
|
808
853
|
_segmentGroup = {
|
|
809
854
|
segments: [],
|
|
810
855
|
localSeq,
|
|
@@ -982,16 +1027,15 @@ class MergeTree {
|
|
|
982
1027
|
saveIfLocal(newSegment);
|
|
983
1028
|
continue;
|
|
984
1029
|
}
|
|
1030
|
+
const refSeqStamp = {
|
|
1031
|
+
seq: perspective.refSeq,
|
|
1032
|
+
clientId: stamp.clientId,
|
|
1033
|
+
};
|
|
985
1034
|
const overlappingAckedObliterates = [];
|
|
986
1035
|
let oldest;
|
|
987
1036
|
let newest;
|
|
988
1037
|
let newestAcked;
|
|
989
1038
|
let oldestUnacked;
|
|
990
|
-
const refSeqStamp = {
|
|
991
|
-
seq: perspective.refSeq,
|
|
992
|
-
clientId: stamp.clientId,
|
|
993
|
-
localSeq: stamp.localSeq,
|
|
994
|
-
};
|
|
995
1039
|
for (const ob of this.obliterates.findOverlapping(newSegment)) {
|
|
996
1040
|
if (opstampUtils.greaterThan(ob.stamp, refSeqStamp)) {
|
|
997
1041
|
// Any obliterate from the same client that's inserting this segment cannot cause the segment to be marked as
|
|
@@ -1024,7 +1068,16 @@ class MergeTree {
|
|
|
1024
1068
|
}
|
|
1025
1069
|
}
|
|
1026
1070
|
}
|
|
1027
|
-
newSegment
|
|
1071
|
+
(0, segmentInfos_js_1.overwriteInfo)(newSegment, {
|
|
1072
|
+
obliteratePrecedingInsertion: newest,
|
|
1073
|
+
});
|
|
1074
|
+
if (newest !== undefined && opstampUtils.isLocal(newest.stamp)) {
|
|
1075
|
+
(0, internal_1.assert)(newest?.tiebreakTrackingGroup !== undefined, 0xb73 /* Expected local obliterateinfo to have tiebreak group */);
|
|
1076
|
+
newest.tiebreakTrackingGroup.link(newSegment);
|
|
1077
|
+
(0, segmentInfos_js_1.overwriteInfo)(newSegment, {
|
|
1078
|
+
insertionRefSeqStamp: refSeqStamp,
|
|
1079
|
+
});
|
|
1080
|
+
}
|
|
1028
1081
|
// See doc comment on obliteratePrecedingInsertion for more details: if the newest obliterate was performed
|
|
1029
1082
|
// by the same client that's inserting this segment, we let them insert into this range and therefore don't
|
|
1030
1083
|
// mark it obliterated.
|
|
@@ -1055,6 +1108,16 @@ class MergeTree {
|
|
|
1055
1108
|
saveIfLocal(newSegment);
|
|
1056
1109
|
}
|
|
1057
1110
|
}
|
|
1111
|
+
computeObliteratePrecedingInsertion(segment, refSeqStamp) {
|
|
1112
|
+
let newest;
|
|
1113
|
+
for (const ob of this.obliterates.findOverlapping(segment)) {
|
|
1114
|
+
if (opstampUtils.greaterThan(ob.stamp, refSeqStamp) &&
|
|
1115
|
+
(newest === undefined || opstampUtils.greaterThan(ob.stamp, newest.stamp))) {
|
|
1116
|
+
newest = ob;
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
return newest;
|
|
1120
|
+
}
|
|
1058
1121
|
ensureIntervalBoundary(pos, perspective) {
|
|
1059
1122
|
this.insertingWalk(pos, perspective, {
|
|
1060
1123
|
seq: constants_js_1.TreeMaintenanceSequenceNumber,
|
|
@@ -1077,12 +1140,12 @@ class MergeTree {
|
|
|
1077
1140
|
}
|
|
1078
1141
|
}
|
|
1079
1142
|
insertingWalk(pos, perspective, stamp, context) {
|
|
1080
|
-
const { remainder } = this.insertRecursive(this.root, pos, perspective, stamp, context);
|
|
1143
|
+
const { remainder } = this.insertRecursive(this.root, pos, perspective, stamp, context, true);
|
|
1081
1144
|
if (remainder !== undefined) {
|
|
1082
1145
|
this.updateRoot(remainder);
|
|
1083
1146
|
}
|
|
1084
1147
|
}
|
|
1085
|
-
insertRecursive(block, pos, perspective, stamp, context,
|
|
1148
|
+
insertRecursive(block, pos, perspective, stamp, context, isLastBlock) {
|
|
1086
1149
|
let _pos = pos;
|
|
1087
1150
|
const children = block.children;
|
|
1088
1151
|
let childIndex;
|
|
@@ -1092,9 +1155,11 @@ class MergeTree {
|
|
|
1092
1155
|
let hadChanges = false;
|
|
1093
1156
|
for (childIndex = 0; childIndex < block.childCount; childIndex++) {
|
|
1094
1157
|
child = children[childIndex];
|
|
1095
|
-
//
|
|
1096
|
-
|
|
1097
|
-
|
|
1158
|
+
// removed blocks below the min seq will have an undefined length, and be skipped
|
|
1159
|
+
// however if it is the last block in the layer of the tree we don't want to skip it, so we correctly
|
|
1160
|
+
// walk down the far edge of the tree.
|
|
1161
|
+
const isLastChildOfLastBlock = isLastBlock && childIndex === block.childCount - 1;
|
|
1162
|
+
const len = this.nodeLength(child, perspective) ?? (isLastChildOfLastBlock ? 0 : undefined);
|
|
1098
1163
|
if (len === undefined) {
|
|
1099
1164
|
// if the seg len is undefined, the segment
|
|
1100
1165
|
// will be removed, so should just be skipped for now
|
|
@@ -1121,9 +1186,8 @@ class MergeTree {
|
|
|
1121
1186
|
}
|
|
1122
1187
|
}
|
|
1123
1188
|
else {
|
|
1124
|
-
const childBlock = child;
|
|
1125
1189
|
// Internal node
|
|
1126
|
-
const insertResult = this.insertRecursive(
|
|
1190
|
+
const insertResult = this.insertRecursive(child, _pos, perspective, stamp, context, isLastChildOfLastBlock);
|
|
1127
1191
|
hadChanges ||= insertResult.hadChanges;
|
|
1128
1192
|
if (insertResult.remainder === undefined) {
|
|
1129
1193
|
if (insertResult.hadChanges) {
|
|
@@ -1270,33 +1334,36 @@ class MergeTree {
|
|
|
1270
1334
|
let _overwrite = false;
|
|
1271
1335
|
const localOverlapWithRefs = [];
|
|
1272
1336
|
const removedSegments = [];
|
|
1337
|
+
const createRefFromSequencePlace = (place) => {
|
|
1338
|
+
const { segment: placeSeg, offset: placeOffset } = this.getContainingSegment(place.pos, perspective);
|
|
1339
|
+
(0, internal_1.assert)((0, mergeTreeNodes_js_1.isSegmentLeaf)(placeSeg) && placeOffset !== undefined, 0xa3f /* segments cannot be undefined */);
|
|
1340
|
+
return this.createLocalReferencePosition(placeSeg, placeOffset, ops_js_1.ReferenceType.StayOnRemove, undefined);
|
|
1341
|
+
};
|
|
1273
1342
|
const obliterate = {
|
|
1274
|
-
start: (
|
|
1275
|
-
|
|
1343
|
+
start: createRefFromSequencePlace(start),
|
|
1344
|
+
startSide: start.side,
|
|
1345
|
+
end: createRefFromSequencePlace(end),
|
|
1346
|
+
endSide: end.side,
|
|
1276
1347
|
refSeq: perspective.refSeq,
|
|
1277
1348
|
stamp,
|
|
1278
1349
|
segmentGroup: undefined,
|
|
1350
|
+
tiebreakTrackingGroup: undefined,
|
|
1279
1351
|
};
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
obliterate.start = this.createLocalReferencePosition(startSeg, start.side === sequencePlace_js_1.Side.Before ? 0 : Math.max(startSeg.cachedLength - 1, 0), ops_js_1.ReferenceType.StayOnRemove, {
|
|
1284
|
-
obliterate,
|
|
1285
|
-
});
|
|
1286
|
-
obliterate.end = this.createLocalReferencePosition(endSeg, end.side === sequencePlace_js_1.Side.Before ? 0 : Math.max(endSeg.cachedLength - 1, 0), ops_js_1.ReferenceType.StayOnRemove, {
|
|
1287
|
-
obliterate,
|
|
1288
|
-
});
|
|
1289
|
-
// Always create a segment group for obliterate,
|
|
1290
|
-
// even if there are no segments currently in the obliteration range.
|
|
1291
|
-
// Segments may be concurrently inserted into the obliteration range,
|
|
1292
|
-
// at which point they are added to the segment group.
|
|
1293
|
-
obliterate.segmentGroup = {
|
|
1294
|
-
segments: [],
|
|
1295
|
-
localSeq: stamp.localSeq,
|
|
1296
|
-
refSeq: this.collabWindow.currentSeq,
|
|
1297
|
-
obliterateInfo: obliterate,
|
|
1298
|
-
};
|
|
1352
|
+
// Link references back to this obliterate info
|
|
1353
|
+
obliterate.start.addProperties({ obliterate });
|
|
1354
|
+
obliterate.end.addProperties({ obliterate });
|
|
1299
1355
|
if (this.collabWindow.collaborating && stamp.clientId === this.collabWindow.clientId) {
|
|
1356
|
+
// Always create a segment group for local obliterates,
|
|
1357
|
+
// even if there are no segments currently in the obliteration range.
|
|
1358
|
+
// Segments may be concurrently inserted into the obliteration range,
|
|
1359
|
+
// at which point they are added to the segment group.
|
|
1360
|
+
obliterate.segmentGroup = {
|
|
1361
|
+
segments: [],
|
|
1362
|
+
localSeq: stamp.localSeq,
|
|
1363
|
+
refSeq: this.collabWindow.currentSeq,
|
|
1364
|
+
obliterateInfo: obliterate,
|
|
1365
|
+
};
|
|
1366
|
+
obliterate.tiebreakTrackingGroup = new mergeTreeTracking_js_1.UnorderedTrackingGroup();
|
|
1300
1367
|
this.pendingSegments.push(obliterate.segmentGroup);
|
|
1301
1368
|
}
|
|
1302
1369
|
this.obliterates.addOrUpdate(obliterate);
|
|
@@ -1314,6 +1381,7 @@ class MergeTree {
|
|
|
1314
1381
|
// Specifically, we want to avoid marking a local-only segment as obliterated when we know one of our own local obliterates
|
|
1315
1382
|
// will win against the obliterate we're processing, hence the early exit.
|
|
1316
1383
|
if (opstampUtils.isLocal(segment.insert) &&
|
|
1384
|
+
(0, segmentInfos_js_1.isInsideObliterate)(segment) &&
|
|
1317
1385
|
segment.obliteratePrecedingInsertion?.stamp.seq === constants_js_1.UnassignedSequenceNumber &&
|
|
1318
1386
|
opstampUtils.isAcked(stamp)) {
|
|
1319
1387
|
// We chose to not obliterate this segment because we are aware of an unacked local obliteration.
|
|
@@ -1479,6 +1547,10 @@ class MergeTree {
|
|
|
1479
1547
|
* Revert an unacked local op
|
|
1480
1548
|
*/
|
|
1481
1549
|
rollback(op, localOpMetadata) {
|
|
1550
|
+
const rollbackStamp = {
|
|
1551
|
+
seq: constants_js_1.TreeMaintenanceSequenceNumber,
|
|
1552
|
+
clientId: constants_js_1.NonCollabClient,
|
|
1553
|
+
};
|
|
1482
1554
|
if (op.type === ops_js_1.MergeTreeDeltaType.REMOVE) {
|
|
1483
1555
|
const pendingSegmentGroup = this.pendingSegments.pop()?.data;
|
|
1484
1556
|
if (pendingSegmentGroup === undefined || pendingSegmentGroup !== localOpMetadata) {
|
|
@@ -1492,16 +1564,10 @@ class MergeTree {
|
|
|
1492
1564
|
(0, internal_1.assert)((0, segmentInfos_js_1.isRemoved)(segment) &&
|
|
1493
1565
|
segment.removes[0].clientId === this.collabWindow.clientId &&
|
|
1494
1566
|
segment.removes[0].type === "setRemove", 0x39d /* Rollback segment removedClientId does not match local client */);
|
|
1495
|
-
let updateNode = segment.parent;
|
|
1496
1567
|
// This also removes obliterates, but that should be ok as we can only remove a segment once.
|
|
1497
1568
|
// If we were able to remove it locally, that also means there are no remote removals (since rollback is synchronous).
|
|
1498
1569
|
(0, segmentInfos_js_1.removeRemovalInfo)(segment);
|
|
1499
|
-
|
|
1500
|
-
this.blockUpdateLength(updateNode, {
|
|
1501
|
-
seq: constants_js_1.UnassignedSequenceNumber,
|
|
1502
|
-
clientId: this.collabWindow.clientId,
|
|
1503
|
-
});
|
|
1504
|
-
}
|
|
1570
|
+
this.blockUpdatePathLengths(segment.parent, rollbackStamp);
|
|
1505
1571
|
// Note: optional chaining short-circuits:
|
|
1506
1572
|
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Optional_chaining#short-circuiting
|
|
1507
1573
|
this.mergeTreeDeltaCallback?.({
|
|
@@ -1529,25 +1595,19 @@ class MergeTree {
|
|
|
1529
1595
|
if (op.type === ops_js_1.MergeTreeDeltaType.INSERT) {
|
|
1530
1596
|
segment.insert = {
|
|
1531
1597
|
type: "insert",
|
|
1532
|
-
|
|
1533
|
-
clientId: this.collabWindow.clientId,
|
|
1598
|
+
...rollbackStamp,
|
|
1534
1599
|
};
|
|
1535
1600
|
const removeOp = (0, opBuilder_js_1.createRemoveRangeOp)(start, start + segment.cachedLength);
|
|
1536
1601
|
const removeStamp = {
|
|
1537
1602
|
type: "setRemove",
|
|
1538
|
-
|
|
1539
|
-
clientId: this.collabWindow.clientId,
|
|
1603
|
+
...rollbackStamp,
|
|
1540
1604
|
};
|
|
1541
1605
|
this.markRangeRemoved(start, start + segment.cachedLength, this.localPerspective, removeStamp, { op: removeOp, rollback: true });
|
|
1542
1606
|
} /* op.type === MergeTreeDeltaType.ANNOTATE */
|
|
1543
1607
|
else {
|
|
1544
1608
|
const props = pendingSegmentGroup.previousProps[i];
|
|
1545
1609
|
const annotateOp = (0, opBuilder_js_1.createAnnotateRangeOp)(start, start + segment.cachedLength, props);
|
|
1546
|
-
|
|
1547
|
-
seq: constants_js_1.UniversalSequenceNumber,
|
|
1548
|
-
clientId: this.collabWindow.clientId,
|
|
1549
|
-
};
|
|
1550
|
-
this.annotateRange(start, start + segment.cachedLength, { props }, this.localPerspective, annotateStamp, { op: annotateOp, rollback: true });
|
|
1610
|
+
this.annotateRange(start, start + segment.cachedLength, { props }, this.localPerspective, rollbackStamp, { op: annotateOp, rollback: true });
|
|
1551
1611
|
i++;
|
|
1552
1612
|
}
|
|
1553
1613
|
}
|
|
@@ -1608,7 +1668,17 @@ class MergeTree {
|
|
|
1608
1668
|
const segRef = localRefs.createLocalRef(offset, refType, properties, slidingPreference, canSlideToEndpoint);
|
|
1609
1669
|
return segRef;
|
|
1610
1670
|
}
|
|
1611
|
-
|
|
1671
|
+
/**
|
|
1672
|
+
* Segments should either be removed remotely, removed locally, or inserted locally
|
|
1673
|
+
*
|
|
1674
|
+
* See description of {@link normalizeSegmentsOnRebase}.
|
|
1675
|
+
*
|
|
1676
|
+
* This normalizes a block of adjacent segments whose positions have collapsed between the time of the original submission and now
|
|
1677
|
+
* such that removed segments come after ones that still exist.
|
|
1678
|
+
*
|
|
1679
|
+
* TODO:AB#34898: It looks like this method has some bugs, search code for this tag for an example test that demonstrates
|
|
1680
|
+
* segment normalization yielding an order that remote clients wouldn't have seen.
|
|
1681
|
+
*/
|
|
1612
1682
|
normalizeAdjacentSegments(affectedSegments) {
|
|
1613
1683
|
// Eagerly demand this since we're about to shift elements in the list around
|
|
1614
1684
|
const currentOrder = Array.from(affectedSegments, ({ data: seg }) => ({
|
|
@@ -1743,6 +1813,45 @@ class MergeTree {
|
|
|
1743
1813
|
return true;
|
|
1744
1814
|
});
|
|
1745
1815
|
normalize();
|
|
1816
|
+
this.obliterates.onNormalize();
|
|
1817
|
+
const segmentTiebreakChanges = new Set();
|
|
1818
|
+
for (const info of this.obliterates) {
|
|
1819
|
+
if (info.tiebreakTrackingGroup !== undefined) {
|
|
1820
|
+
for (const segment of info.tiebreakTrackingGroup.tracked) {
|
|
1821
|
+
// Recompute previous obliterate
|
|
1822
|
+
(0, internal_1.assert)((0, mergeTreeNodes_js_1.isSegmentLeaf)(segment) &&
|
|
1823
|
+
(0, segmentInfos_js_1.isInsideObliterate)(segment) &&
|
|
1824
|
+
segment.insertionRefSeqStamp !== undefined, 0xb74 /* Expected segment leaf inside obliterate with insertionRefSeqStamp */);
|
|
1825
|
+
// This may have changed as a result of segments shuffling: outstanding local obliterates that previously surrounded a segment may no longer surround it.
|
|
1826
|
+
const newObliteratePrecedingInsertion = this.computeObliteratePrecedingInsertion(segment, segment.insertionRefSeqStamp);
|
|
1827
|
+
if (newObliteratePrecedingInsertion !== info) {
|
|
1828
|
+
segmentTiebreakChanges.add({
|
|
1829
|
+
segment,
|
|
1830
|
+
old: info,
|
|
1831
|
+
new: newObliteratePrecedingInsertion,
|
|
1832
|
+
});
|
|
1833
|
+
}
|
|
1834
|
+
}
|
|
1835
|
+
}
|
|
1836
|
+
}
|
|
1837
|
+
for (const { segment, old, new: newInfo } of segmentTiebreakChanges) {
|
|
1838
|
+
// Update tiebreak tracking groups on old/new segment as well as `ISegmentInsideObliterateInfo` state
|
|
1839
|
+
// which we only keep around as long as obliterates are in flight.
|
|
1840
|
+
old.tiebreakTrackingGroup?.unlink(segment);
|
|
1841
|
+
if (newInfo?.tiebreakTrackingGroup === undefined) {
|
|
1842
|
+
// Segment is either no longer inside any obliterate or only inside acked obliterates.
|
|
1843
|
+
(0, segmentInfos_js_1.overwriteInfo)(segment, {
|
|
1844
|
+
obliteratePrecedingInsertion: newInfo,
|
|
1845
|
+
insertionRefSeqStamp: undefined,
|
|
1846
|
+
});
|
|
1847
|
+
}
|
|
1848
|
+
else {
|
|
1849
|
+
(0, segmentInfos_js_1.overwriteInfo)(segment, {
|
|
1850
|
+
obliteratePrecedingInsertion: newInfo,
|
|
1851
|
+
});
|
|
1852
|
+
newInfo.tiebreakTrackingGroup.link(segment);
|
|
1853
|
+
}
|
|
1854
|
+
}
|
|
1746
1855
|
}
|
|
1747
1856
|
blockUpdate(block) {
|
|
1748
1857
|
let len;
|