@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/src/mergeTree.ts
CHANGED
|
@@ -16,7 +16,6 @@ import {
|
|
|
16
16
|
NonCollabClient,
|
|
17
17
|
TreeMaintenanceSequenceNumber,
|
|
18
18
|
UnassignedSequenceNumber,
|
|
19
|
-
UniversalSequenceNumber,
|
|
20
19
|
} from "./constants.js";
|
|
21
20
|
import { EndOfTreeSegment, StartOfTreeSegment } from "./endOfTreeSegment.js";
|
|
22
21
|
import {
|
|
@@ -24,7 +23,6 @@ import {
|
|
|
24
23
|
LocalReferencePosition,
|
|
25
24
|
SlidingPreference,
|
|
26
25
|
anyLocalReferencePosition,
|
|
27
|
-
createDetachedLocalReferencePosition,
|
|
28
26
|
filterLocalReferencePositions,
|
|
29
27
|
} from "./localReference.js";
|
|
30
28
|
import {
|
|
@@ -64,7 +62,7 @@ import {
|
|
|
64
62
|
type ISegmentPrivate,
|
|
65
63
|
type ObliterateInfo,
|
|
66
64
|
} from "./mergeTreeNodes.js";
|
|
67
|
-
import type
|
|
65
|
+
import { UnorderedTrackingGroup, type TrackingGroup } from "./mergeTreeTracking.js";
|
|
68
66
|
import {
|
|
69
67
|
createAnnotateRangeOp,
|
|
70
68
|
createInsertSegmentOp,
|
|
@@ -83,6 +81,7 @@ import {
|
|
|
83
81
|
type Perspective,
|
|
84
82
|
LocalDefaultPerspective,
|
|
85
83
|
RemoteObliteratePerspective,
|
|
84
|
+
allAckedChangesPerspective,
|
|
86
85
|
} from "./perspective.js";
|
|
87
86
|
import { PropertySet, createMap, extend, extendIfUndefined } from "./properties.js";
|
|
88
87
|
import {
|
|
@@ -95,6 +94,8 @@ import {
|
|
|
95
94
|
import { SegmentGroupCollection } from "./segmentGroupCollection.js";
|
|
96
95
|
import {
|
|
97
96
|
assertRemoved,
|
|
97
|
+
ISegmentInsideObliterateInfo,
|
|
98
|
+
isInsideObliterate,
|
|
98
99
|
isMergeNodeInfo,
|
|
99
100
|
isRemoved,
|
|
100
101
|
overwriteInfo,
|
|
@@ -197,14 +198,6 @@ function ackSegment(
|
|
|
197
198
|
type: op.type === MergeTreeDeltaType.REMOVE ? "setRemove" : "sliceRemove",
|
|
198
199
|
};
|
|
199
200
|
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
201
|
break;
|
|
209
202
|
}
|
|
210
203
|
|
|
@@ -400,10 +393,15 @@ export function findRootMergeBlock(
|
|
|
400
393
|
function getSlideToSegment(
|
|
401
394
|
segment: ISegmentLeaf | undefined,
|
|
402
395
|
slidingPreference: SlidingPreference = SlidingPreference.FORWARD,
|
|
396
|
+
perspective: Perspective,
|
|
403
397
|
cache?: Map<ISegmentLeaf, { seg?: ISegmentLeaf }>,
|
|
404
398
|
useNewSlidingBehavior: boolean = false,
|
|
405
399
|
): [ISegmentLeaf | undefined, "start" | "end" | undefined] {
|
|
406
|
-
if (
|
|
400
|
+
if (
|
|
401
|
+
!segment ||
|
|
402
|
+
perspective.isSegmentPresent(segment) ||
|
|
403
|
+
segment.endpointType !== undefined
|
|
404
|
+
) {
|
|
407
405
|
return [segment, undefined];
|
|
408
406
|
}
|
|
409
407
|
|
|
@@ -414,7 +412,7 @@ function getSlideToSegment(
|
|
|
414
412
|
const result: { seg?: ISegmentLeaf } = {};
|
|
415
413
|
cache?.set(segment, result);
|
|
416
414
|
const goFurtherToFindSlideToSegment = (seg: ISegmentLeaf): boolean => {
|
|
417
|
-
if (
|
|
415
|
+
if (perspective.isSegmentPresent(seg)) {
|
|
418
416
|
result.seg = seg;
|
|
419
417
|
return false;
|
|
420
418
|
}
|
|
@@ -473,6 +471,7 @@ function getSlideToSegment(
|
|
|
473
471
|
export function getSlideToSegoff(
|
|
474
472
|
segoff: { segment: ISegmentInternal | undefined; offset: number | undefined },
|
|
475
473
|
slidingPreference: SlidingPreference = SlidingPreference.FORWARD,
|
|
474
|
+
perspective: Perspective = allAckedChangesPerspective,
|
|
476
475
|
useNewSlidingBehavior: boolean = false,
|
|
477
476
|
): {
|
|
478
477
|
segment: ISegmentInternal | undefined;
|
|
@@ -484,6 +483,7 @@ export function getSlideToSegoff(
|
|
|
484
483
|
const [segment, _] = getSlideToSegment(
|
|
485
484
|
segoff.segment,
|
|
486
485
|
slidingPreference,
|
|
486
|
+
perspective,
|
|
487
487
|
undefined,
|
|
488
488
|
useNewSlidingBehavior,
|
|
489
489
|
);
|
|
@@ -535,6 +535,10 @@ class Obliterates {
|
|
|
535
535
|
}
|
|
536
536
|
}
|
|
537
537
|
|
|
538
|
+
public onNormalize(): void {
|
|
539
|
+
this.startOrdered.onSortOrderChange();
|
|
540
|
+
}
|
|
541
|
+
|
|
538
542
|
public addOrUpdate(obliterateInfo: ObliterateInfo): void {
|
|
539
543
|
const {
|
|
540
544
|
stamp: { seq },
|
|
@@ -567,6 +571,48 @@ class Obliterates {
|
|
|
567
571
|
}
|
|
568
572
|
return overlapping;
|
|
569
573
|
}
|
|
574
|
+
|
|
575
|
+
/**
|
|
576
|
+
* Remove a local obliterate from this data structure.
|
|
577
|
+
* @privateRemarks
|
|
578
|
+
* This data structure could support removing non-local obliterates if we wanted it to, but when adding support for that
|
|
579
|
+
* we should reconsider the indexing structure for seq ordered obliterates (right now it would be an O(# obliterates) operation)
|
|
580
|
+
*/
|
|
581
|
+
public removeLocalObliterate(obliterateInfo: ObliterateInfo): void {
|
|
582
|
+
assert(
|
|
583
|
+
obliterateInfo.stamp.seq === UnassignedSequenceNumber,
|
|
584
|
+
0xb6e /* Expected local obliterate */,
|
|
585
|
+
);
|
|
586
|
+
this.startOrdered.remove(obliterateInfo.start);
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
/**
|
|
590
|
+
* Returns an iterator over the `ObliterateInfo` for all obliterates in the collab window. Obliterates are not guaranteed to be ordered.
|
|
591
|
+
* The iterator is not guaranteed to be valid over edits to the set of obliterates.
|
|
592
|
+
*/
|
|
593
|
+
public [Symbol.iterator](): IterableIterator<ObliterateInfo> {
|
|
594
|
+
let index = 0;
|
|
595
|
+
const { items: starts } = this.startOrdered;
|
|
596
|
+
const iterator: IterableIterator<ObliterateInfo> = {
|
|
597
|
+
next(): IteratorResult<ObliterateInfo> {
|
|
598
|
+
if (index < starts.length) {
|
|
599
|
+
const start = starts[index++];
|
|
600
|
+
const info = start.properties?.obliterate as ObliterateInfo;
|
|
601
|
+
assert(
|
|
602
|
+
info?.start !== undefined && info?.end !== undefined,
|
|
603
|
+
0xb6f /* Expected obliterateInfo endpoint to map to its obliterate */,
|
|
604
|
+
);
|
|
605
|
+
return { value: info, done: false };
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
return { value: undefined, done: true };
|
|
609
|
+
},
|
|
610
|
+
[Symbol.iterator]() {
|
|
611
|
+
return this;
|
|
612
|
+
},
|
|
613
|
+
};
|
|
614
|
+
return iterator;
|
|
615
|
+
}
|
|
570
616
|
}
|
|
571
617
|
|
|
572
618
|
interface InsertResult {
|
|
@@ -631,6 +677,16 @@ export class MergeTree {
|
|
|
631
677
|
this.attributionPolicy = options?.attribution?.policyFactory?.();
|
|
632
678
|
}
|
|
633
679
|
|
|
680
|
+
public rebaseObliterateTo(
|
|
681
|
+
existing: ObliterateInfo,
|
|
682
|
+
newObliterate: ObliterateInfo | undefined,
|
|
683
|
+
): void {
|
|
684
|
+
this.obliterates.removeLocalObliterate(existing);
|
|
685
|
+
if (newObliterate !== undefined) {
|
|
686
|
+
this.obliterates.addOrUpdate(newObliterate);
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
|
|
634
690
|
private _root: IRootMergeBlock;
|
|
635
691
|
public get root(): IRootMergeBlock {
|
|
636
692
|
return this._root;
|
|
@@ -927,6 +983,7 @@ export class MergeTree {
|
|
|
927
983
|
const [slideToSegment, maybeEndpoint] = getSlideToSegment(
|
|
928
984
|
segment,
|
|
929
985
|
slidingPreference,
|
|
986
|
+
allAckedChangesPerspective,
|
|
930
987
|
slidingPreference === SlidingPreference.FORWARD
|
|
931
988
|
? forwardSegmentCache
|
|
932
989
|
: backwardSegmentCache,
|
|
@@ -1248,10 +1305,9 @@ export class MergeTree {
|
|
|
1248
1305
|
}
|
|
1249
1306
|
|
|
1250
1307
|
/**
|
|
1251
|
-
* Assign sequence number to existing
|
|
1252
|
-
* @param seq - sequence number given by server to pending segment
|
|
1308
|
+
* Assign sequence number to existing segments affected by an op; update partial lengths to reflect the change
|
|
1253
1309
|
*/
|
|
1254
|
-
public
|
|
1310
|
+
public ackOp(opArgs: IMergeTreeDeltaOpArgs): void {
|
|
1255
1311
|
const seq = opArgs.sequencedMessage!.sequenceNumber;
|
|
1256
1312
|
const stamp: OperationStamp = {
|
|
1257
1313
|
seq,
|
|
@@ -1261,9 +1317,33 @@ export class MergeTree {
|
|
|
1261
1317
|
const nodesToUpdate: MergeBlock[] = [];
|
|
1262
1318
|
let overwrite = false;
|
|
1263
1319
|
if (pendingSegmentGroup !== undefined) {
|
|
1320
|
+
const { obliterateInfo, segments } = pendingSegmentGroup;
|
|
1321
|
+
const hasObliterateInfo = obliterateInfo !== undefined;
|
|
1322
|
+
const isObliterate =
|
|
1323
|
+
opArgs.op.type === MergeTreeDeltaType.OBLITERATE ||
|
|
1324
|
+
opArgs.op.type === MergeTreeDeltaType.OBLITERATE_SIDED;
|
|
1325
|
+
assert(hasObliterateInfo === isObliterate, 0xa40 /* must have obliterate info */);
|
|
1326
|
+
if (hasObliterateInfo) {
|
|
1327
|
+
obliterateInfo.stamp = { ...stamp, type: "sliceRemove" };
|
|
1328
|
+
this.obliterates.addOrUpdate(obliterateInfo);
|
|
1329
|
+
assert(
|
|
1330
|
+
obliterateInfo.tiebreakTrackingGroup !== undefined,
|
|
1331
|
+
0xb70 /* obliterateInfo should have a tiebreak tracking group on ack */,
|
|
1332
|
+
);
|
|
1333
|
+
for (const segment of obliterateInfo.tiebreakTrackingGroup.tracked) {
|
|
1334
|
+
segment.trackingCollection.unlink(obliterateInfo.tiebreakTrackingGroup);
|
|
1335
|
+
assert(
|
|
1336
|
+
isSegmentLeaf(segment) && isInsideObliterate(segment),
|
|
1337
|
+
0xb71 /* Expected segment leaf inside obliterate */,
|
|
1338
|
+
);
|
|
1339
|
+
segment.insertionRefSeqStamp = undefined;
|
|
1340
|
+
}
|
|
1341
|
+
obliterateInfo.tiebreakTrackingGroup = undefined;
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1264
1344
|
const deltaSegments: IMergeTreeSegmentDelta[] = [];
|
|
1265
1345
|
const overlappingRemoves: boolean[] = [];
|
|
1266
|
-
|
|
1346
|
+
segments.map((pendingSegment: ISegmentLeaf) => {
|
|
1267
1347
|
const overlappingRemove = !ackSegment(
|
|
1268
1348
|
pendingSegment,
|
|
1269
1349
|
pendingSegmentGroup,
|
|
@@ -1285,11 +1365,6 @@ export class MergeTree {
|
|
|
1285
1365
|
});
|
|
1286
1366
|
});
|
|
1287
1367
|
|
|
1288
|
-
if (pendingSegmentGroup.obliterateInfo !== undefined) {
|
|
1289
|
-
pendingSegmentGroup.obliterateInfo.stamp = { type: "sliceRemove", ...stamp };
|
|
1290
|
-
this.obliterates.addOrUpdate(pendingSegmentGroup.obliterateInfo);
|
|
1291
|
-
}
|
|
1292
|
-
|
|
1293
1368
|
// Perform slides after all segments have been acked, so that
|
|
1294
1369
|
// positions after slide are final
|
|
1295
1370
|
if (
|
|
@@ -1297,7 +1372,7 @@ export class MergeTree {
|
|
|
1297
1372
|
opArgs.op.type === MergeTreeDeltaType.OBLITERATE ||
|
|
1298
1373
|
opArgs.op.type === MergeTreeDeltaType.OBLITERATE_SIDED
|
|
1299
1374
|
) {
|
|
1300
|
-
this.slideAckedRemovedSegmentReferences(
|
|
1375
|
+
this.slideAckedRemovedSegmentReferences(segments);
|
|
1301
1376
|
}
|
|
1302
1377
|
|
|
1303
1378
|
this.mergeTreeMaintenanceCallback?.(
|
|
@@ -1326,6 +1401,10 @@ export class MergeTree {
|
|
|
1326
1401
|
): SegmentGroup {
|
|
1327
1402
|
let _segmentGroup = segmentGroup;
|
|
1328
1403
|
if (_segmentGroup === undefined) {
|
|
1404
|
+
assert(
|
|
1405
|
+
localSeq !== undefined,
|
|
1406
|
+
0xb72 /* Local seq should be passed when creating new segment group */,
|
|
1407
|
+
);
|
|
1329
1408
|
_segmentGroup = {
|
|
1330
1409
|
segments: [],
|
|
1331
1410
|
localSeq,
|
|
@@ -1550,16 +1629,17 @@ export class MergeTree {
|
|
|
1550
1629
|
continue;
|
|
1551
1630
|
}
|
|
1552
1631
|
|
|
1632
|
+
const refSeqStamp: OperationStamp = {
|
|
1633
|
+
seq: perspective.refSeq,
|
|
1634
|
+
clientId: stamp.clientId,
|
|
1635
|
+
};
|
|
1636
|
+
|
|
1553
1637
|
const overlappingAckedObliterates: RemoveOperationStamp[] = [];
|
|
1554
1638
|
let oldest: ObliterateInfo | undefined;
|
|
1555
1639
|
let newest: ObliterateInfo | undefined;
|
|
1556
1640
|
let newestAcked: ObliterateInfo | undefined;
|
|
1557
1641
|
let oldestUnacked: ObliterateInfo | undefined;
|
|
1558
|
-
|
|
1559
|
-
seq: perspective.refSeq,
|
|
1560
|
-
clientId: stamp.clientId,
|
|
1561
|
-
localSeq: stamp.localSeq,
|
|
1562
|
-
};
|
|
1642
|
+
|
|
1563
1643
|
for (const ob of this.obliterates.findOverlapping(newSegment)) {
|
|
1564
1644
|
if (opstampUtils.greaterThan(ob.stamp, refSeqStamp)) {
|
|
1565
1645
|
// Any obliterate from the same client that's inserting this segment cannot cause the segment to be marked as
|
|
@@ -1601,7 +1681,19 @@ export class MergeTree {
|
|
|
1601
1681
|
}
|
|
1602
1682
|
}
|
|
1603
1683
|
|
|
1604
|
-
newSegment
|
|
1684
|
+
overwriteInfo<ISegmentInsideObliterateInfo>(newSegment, {
|
|
1685
|
+
obliteratePrecedingInsertion: newest,
|
|
1686
|
+
});
|
|
1687
|
+
if (newest !== undefined && opstampUtils.isLocal(newest.stamp)) {
|
|
1688
|
+
assert(
|
|
1689
|
+
newest?.tiebreakTrackingGroup !== undefined,
|
|
1690
|
+
0xb73 /* Expected local obliterateinfo to have tiebreak group */,
|
|
1691
|
+
);
|
|
1692
|
+
newest.tiebreakTrackingGroup.link(newSegment);
|
|
1693
|
+
overwriteInfo<ISegmentInsideObliterateInfo>(newSegment, {
|
|
1694
|
+
insertionRefSeqStamp: refSeqStamp,
|
|
1695
|
+
});
|
|
1696
|
+
}
|
|
1605
1697
|
// See doc comment on obliteratePrecedingInsertion for more details: if the newest obliterate was performed
|
|
1606
1698
|
// by the same client that's inserting this segment, we let them insert into this range and therefore don't
|
|
1607
1699
|
// mark it obliterated.
|
|
@@ -1642,6 +1734,22 @@ export class MergeTree {
|
|
|
1642
1734
|
}
|
|
1643
1735
|
}
|
|
1644
1736
|
|
|
1737
|
+
private computeObliteratePrecedingInsertion(
|
|
1738
|
+
segment: ISegmentLeaf,
|
|
1739
|
+
refSeqStamp: OperationStamp,
|
|
1740
|
+
): ObliterateInfo | undefined {
|
|
1741
|
+
let newest: ObliterateInfo | undefined;
|
|
1742
|
+
for (const ob of this.obliterates.findOverlapping(segment)) {
|
|
1743
|
+
if (
|
|
1744
|
+
opstampUtils.greaterThan(ob.stamp, refSeqStamp) &&
|
|
1745
|
+
(newest === undefined || opstampUtils.greaterThan(ob.stamp, newest.stamp))
|
|
1746
|
+
) {
|
|
1747
|
+
newest = ob;
|
|
1748
|
+
}
|
|
1749
|
+
}
|
|
1750
|
+
return newest;
|
|
1751
|
+
}
|
|
1752
|
+
|
|
1645
1753
|
private readonly splitLeafSegment = (
|
|
1646
1754
|
segment: ISegmentLeaf | undefined,
|
|
1647
1755
|
pos: number,
|
|
@@ -1658,13 +1766,8 @@ export class MergeTree {
|
|
|
1658
1766
|
segment.segmentGroups.copyTo(next.segmentGroups);
|
|
1659
1767
|
}
|
|
1660
1768
|
|
|
1661
|
-
if (segment.obliteratePrecedingInsertion) {
|
|
1662
|
-
next.obliteratePrecedingInsertion = segment.obliteratePrecedingInsertion;
|
|
1663
|
-
}
|
|
1664
1769
|
copyPropertiesAndManager(segment, next);
|
|
1665
|
-
|
|
1666
|
-
segment.localRefs.split(pos, next);
|
|
1667
|
-
}
|
|
1770
|
+
segment.localRefs?.split(pos, next);
|
|
1668
1771
|
|
|
1669
1772
|
this.mergeTreeMaintenanceCallback?.(
|
|
1670
1773
|
{
|
|
@@ -1712,7 +1815,14 @@ export class MergeTree {
|
|
|
1712
1815
|
stamp: OperationStamp,
|
|
1713
1816
|
context: InsertContext,
|
|
1714
1817
|
): void {
|
|
1715
|
-
const { remainder } = this.insertRecursive(
|
|
1818
|
+
const { remainder } = this.insertRecursive(
|
|
1819
|
+
this.root,
|
|
1820
|
+
pos,
|
|
1821
|
+
perspective,
|
|
1822
|
+
stamp,
|
|
1823
|
+
context,
|
|
1824
|
+
true,
|
|
1825
|
+
);
|
|
1716
1826
|
if (remainder !== undefined) {
|
|
1717
1827
|
this.updateRoot(remainder);
|
|
1718
1828
|
}
|
|
@@ -1724,7 +1834,7 @@ export class MergeTree {
|
|
|
1724
1834
|
perspective: Perspective,
|
|
1725
1835
|
stamp: OperationStamp,
|
|
1726
1836
|
context: InsertContext,
|
|
1727
|
-
|
|
1837
|
+
isLastBlock: boolean,
|
|
1728
1838
|
): InsertResult {
|
|
1729
1839
|
let _pos: number = pos;
|
|
1730
1840
|
|
|
@@ -1736,10 +1846,12 @@ export class MergeTree {
|
|
|
1736
1846
|
let hadChanges = false;
|
|
1737
1847
|
for (childIndex = 0; childIndex < block.childCount; childIndex++) {
|
|
1738
1848
|
child = children[childIndex];
|
|
1739
|
-
//
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
const
|
|
1849
|
+
// removed blocks below the min seq will have an undefined length, and be skipped
|
|
1850
|
+
// however if it is the last block in the layer of the tree we don't want to skip it, so we correctly
|
|
1851
|
+
// walk down the far edge of the tree.
|
|
1852
|
+
const isLastChildOfLastBlock = isLastBlock && childIndex === block.childCount - 1;
|
|
1853
|
+
const len =
|
|
1854
|
+
this.nodeLength(child, perspective) ?? (isLastChildOfLastBlock ? 0 : undefined);
|
|
1743
1855
|
|
|
1744
1856
|
if (len === undefined) {
|
|
1745
1857
|
// if the seg len is undefined, the segment
|
|
@@ -1767,15 +1879,14 @@ export class MergeTree {
|
|
|
1767
1879
|
return { remainder: undefined, hadChanges };
|
|
1768
1880
|
}
|
|
1769
1881
|
} else {
|
|
1770
|
-
const childBlock = child;
|
|
1771
1882
|
// Internal node
|
|
1772
1883
|
const insertResult = this.insertRecursive(
|
|
1773
|
-
|
|
1884
|
+
child,
|
|
1774
1885
|
_pos,
|
|
1775
1886
|
perspective,
|
|
1776
1887
|
stamp,
|
|
1777
1888
|
context,
|
|
1778
|
-
|
|
1889
|
+
isLastChildOfLastBlock,
|
|
1779
1890
|
);
|
|
1780
1891
|
hadChanges ||= insertResult.hadChanges;
|
|
1781
1892
|
if (insertResult.remainder === undefined) {
|
|
@@ -1971,50 +2082,51 @@ export class MergeTree {
|
|
|
1971
2082
|
const localOverlapWithRefs: ISegmentLeaf[] = [];
|
|
1972
2083
|
const removedSegments: SegmentWithInfo<IHasRemovalInfo, ISegmentLeaf>[] = [];
|
|
1973
2084
|
|
|
2085
|
+
const createRefFromSequencePlace = (
|
|
2086
|
+
place: InteriorSequencePlace,
|
|
2087
|
+
): LocalReferencePosition => {
|
|
2088
|
+
const { segment: placeSeg, offset: placeOffset } = this.getContainingSegment(
|
|
2089
|
+
place.pos,
|
|
2090
|
+
perspective,
|
|
2091
|
+
);
|
|
2092
|
+
assert(
|
|
2093
|
+
isSegmentLeaf(placeSeg) && placeOffset !== undefined,
|
|
2094
|
+
0xa3f /* segments cannot be undefined */,
|
|
2095
|
+
);
|
|
2096
|
+
return this.createLocalReferencePosition(
|
|
2097
|
+
placeSeg,
|
|
2098
|
+
placeOffset,
|
|
2099
|
+
ReferenceType.StayOnRemove,
|
|
2100
|
+
undefined,
|
|
2101
|
+
);
|
|
2102
|
+
};
|
|
2103
|
+
|
|
1974
2104
|
const obliterate: ObliterateInfo = {
|
|
1975
|
-
start:
|
|
1976
|
-
|
|
2105
|
+
start: createRefFromSequencePlace(start),
|
|
2106
|
+
startSide: start.side,
|
|
2107
|
+
end: createRefFromSequencePlace(end),
|
|
2108
|
+
endSide: end.side,
|
|
1977
2109
|
refSeq: perspective.refSeq,
|
|
1978
2110
|
stamp,
|
|
1979
2111
|
segmentGroup: undefined,
|
|
2112
|
+
tiebreakTrackingGroup: undefined,
|
|
1980
2113
|
};
|
|
2114
|
+
// Link references back to this obliterate info
|
|
2115
|
+
obliterate.start.addProperties({ obliterate });
|
|
2116
|
+
obliterate.end.addProperties({ obliterate });
|
|
1981
2117
|
|
|
1982
|
-
const { segment: startSeg } = this.getContainingSegment(start.pos, perspective);
|
|
1983
|
-
const { segment: endSeg } = this.getContainingSegment(end.pos, perspective);
|
|
1984
|
-
assert(
|
|
1985
|
-
isSegmentLeaf(startSeg) && isSegmentLeaf(endSeg),
|
|
1986
|
-
0xa3f /* segments cannot be undefined */,
|
|
1987
|
-
);
|
|
1988
|
-
|
|
1989
|
-
obliterate.start = this.createLocalReferencePosition(
|
|
1990
|
-
startSeg,
|
|
1991
|
-
start.side === Side.Before ? 0 : Math.max(startSeg.cachedLength - 1, 0),
|
|
1992
|
-
ReferenceType.StayOnRemove,
|
|
1993
|
-
{
|
|
1994
|
-
obliterate,
|
|
1995
|
-
},
|
|
1996
|
-
);
|
|
1997
|
-
|
|
1998
|
-
obliterate.end = this.createLocalReferencePosition(
|
|
1999
|
-
endSeg,
|
|
2000
|
-
end.side === Side.Before ? 0 : Math.max(endSeg.cachedLength - 1, 0),
|
|
2001
|
-
ReferenceType.StayOnRemove,
|
|
2002
|
-
{
|
|
2003
|
-
obliterate,
|
|
2004
|
-
},
|
|
2005
|
-
);
|
|
2006
|
-
|
|
2007
|
-
// Always create a segment group for obliterate,
|
|
2008
|
-
// even if there are no segments currently in the obliteration range.
|
|
2009
|
-
// Segments may be concurrently inserted into the obliteration range,
|
|
2010
|
-
// at which point they are added to the segment group.
|
|
2011
|
-
obliterate.segmentGroup = {
|
|
2012
|
-
segments: [],
|
|
2013
|
-
localSeq: stamp.localSeq,
|
|
2014
|
-
refSeq: this.collabWindow.currentSeq,
|
|
2015
|
-
obliterateInfo: obliterate,
|
|
2016
|
-
};
|
|
2017
2118
|
if (this.collabWindow.collaborating && stamp.clientId === this.collabWindow.clientId) {
|
|
2119
|
+
// Always create a segment group for local obliterates,
|
|
2120
|
+
// even if there are no segments currently in the obliteration range.
|
|
2121
|
+
// Segments may be concurrently inserted into the obliteration range,
|
|
2122
|
+
// at which point they are added to the segment group.
|
|
2123
|
+
obliterate.segmentGroup = {
|
|
2124
|
+
segments: [],
|
|
2125
|
+
localSeq: stamp.localSeq,
|
|
2126
|
+
refSeq: this.collabWindow.currentSeq,
|
|
2127
|
+
obliterateInfo: obliterate,
|
|
2128
|
+
};
|
|
2129
|
+
obliterate.tiebreakTrackingGroup = new UnorderedTrackingGroup();
|
|
2018
2130
|
this.pendingSegments.push(obliterate.segmentGroup);
|
|
2019
2131
|
}
|
|
2020
2132
|
this.obliterates.addOrUpdate(obliterate);
|
|
@@ -2036,6 +2148,7 @@ export class MergeTree {
|
|
|
2036
2148
|
// will win against the obliterate we're processing, hence the early exit.
|
|
2037
2149
|
if (
|
|
2038
2150
|
opstampUtils.isLocal(segment.insert) &&
|
|
2151
|
+
isInsideObliterate(segment) &&
|
|
2039
2152
|
segment.obliteratePrecedingInsertion?.stamp.seq === UnassignedSequenceNumber &&
|
|
2040
2153
|
opstampUtils.isAcked(stamp)
|
|
2041
2154
|
) {
|
|
@@ -2263,6 +2376,10 @@ export class MergeTree {
|
|
|
2263
2376
|
* Revert an unacked local op
|
|
2264
2377
|
*/
|
|
2265
2378
|
public rollback(op: IMergeTreeDeltaOp, localOpMetadata: SegmentGroup): void {
|
|
2379
|
+
const rollbackStamp: OperationStamp = {
|
|
2380
|
+
seq: TreeMaintenanceSequenceNumber,
|
|
2381
|
+
clientId: NonCollabClient,
|
|
2382
|
+
};
|
|
2266
2383
|
if (op.type === MergeTreeDeltaType.REMOVE) {
|
|
2267
2384
|
const pendingSegmentGroup = this.pendingSegments.pop()?.data;
|
|
2268
2385
|
if (pendingSegmentGroup === undefined || pendingSegmentGroup !== localOpMetadata) {
|
|
@@ -2283,17 +2400,11 @@ export class MergeTree {
|
|
|
2283
2400
|
segment.removes[0].type === "setRemove",
|
|
2284
2401
|
0x39d /* Rollback segment removedClientId does not match local client */,
|
|
2285
2402
|
);
|
|
2286
|
-
let updateNode: MergeBlock | undefined = segment.parent;
|
|
2287
2403
|
// This also removes obliterates, but that should be ok as we can only remove a segment once.
|
|
2288
2404
|
// If we were able to remove it locally, that also means there are no remote removals (since rollback is synchronous).
|
|
2289
2405
|
removeRemovalInfo(segment);
|
|
2290
2406
|
|
|
2291
|
-
|
|
2292
|
-
this.blockUpdateLength(updateNode, {
|
|
2293
|
-
seq: UnassignedSequenceNumber,
|
|
2294
|
-
clientId: this.collabWindow.clientId,
|
|
2295
|
-
});
|
|
2296
|
-
}
|
|
2407
|
+
this.blockUpdatePathLengths(segment.parent, rollbackStamp);
|
|
2297
2408
|
|
|
2298
2409
|
// Note: optional chaining short-circuits:
|
|
2299
2410
|
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Optional_chaining#short-circuiting
|
|
@@ -2332,14 +2443,12 @@ export class MergeTree {
|
|
|
2332
2443
|
if (op.type === MergeTreeDeltaType.INSERT) {
|
|
2333
2444
|
segment.insert = {
|
|
2334
2445
|
type: "insert",
|
|
2335
|
-
|
|
2336
|
-
clientId: this.collabWindow.clientId,
|
|
2446
|
+
...rollbackStamp,
|
|
2337
2447
|
};
|
|
2338
2448
|
const removeOp = createRemoveRangeOp(start, start + segment.cachedLength);
|
|
2339
2449
|
const removeStamp: SetRemoveOperationStamp = {
|
|
2340
2450
|
type: "setRemove",
|
|
2341
|
-
|
|
2342
|
-
clientId: this.collabWindow.clientId,
|
|
2451
|
+
...rollbackStamp,
|
|
2343
2452
|
};
|
|
2344
2453
|
this.markRangeRemoved(
|
|
2345
2454
|
start,
|
|
@@ -2351,16 +2460,13 @@ export class MergeTree {
|
|
|
2351
2460
|
} /* op.type === MergeTreeDeltaType.ANNOTATE */ else {
|
|
2352
2461
|
const props = pendingSegmentGroup.previousProps![i];
|
|
2353
2462
|
const annotateOp = createAnnotateRangeOp(start, start + segment.cachedLength, props);
|
|
2354
|
-
|
|
2355
|
-
seq: UniversalSequenceNumber,
|
|
2356
|
-
clientId: this.collabWindow.clientId,
|
|
2357
|
-
};
|
|
2463
|
+
|
|
2358
2464
|
this.annotateRange(
|
|
2359
2465
|
start,
|
|
2360
2466
|
start + segment.cachedLength,
|
|
2361
2467
|
{ props },
|
|
2362
2468
|
this.localPerspective,
|
|
2363
|
-
|
|
2469
|
+
rollbackStamp,
|
|
2364
2470
|
{ op: annotateOp, rollback: true },
|
|
2365
2471
|
);
|
|
2366
2472
|
i++;
|
|
@@ -2457,7 +2563,17 @@ export class MergeTree {
|
|
|
2457
2563
|
return segRef;
|
|
2458
2564
|
}
|
|
2459
2565
|
|
|
2460
|
-
|
|
2566
|
+
/**
|
|
2567
|
+
* Segments should either be removed remotely, removed locally, or inserted locally
|
|
2568
|
+
*
|
|
2569
|
+
* See description of {@link normalizeSegmentsOnRebase}.
|
|
2570
|
+
*
|
|
2571
|
+
* This normalizes a block of adjacent segments whose positions have collapsed between the time of the original submission and now
|
|
2572
|
+
* such that removed segments come after ones that still exist.
|
|
2573
|
+
*
|
|
2574
|
+
* TODO:AB#34898: It looks like this method has some bugs, search code for this tag for an example test that demonstrates
|
|
2575
|
+
* segment normalization yielding an order that remote clients wouldn't have seen.
|
|
2576
|
+
*/
|
|
2461
2577
|
private normalizeAdjacentSegments(affectedSegments: DoublyLinkedList<ISegmentLeaf>): void {
|
|
2462
2578
|
// Eagerly demand this since we're about to shift elements in the list around
|
|
2463
2579
|
const currentOrder = Array.from(affectedSegments, ({ data: seg }) => ({
|
|
@@ -2612,6 +2728,57 @@ export class MergeTree {
|
|
|
2612
2728
|
});
|
|
2613
2729
|
|
|
2614
2730
|
normalize();
|
|
2731
|
+
this.obliterates.onNormalize();
|
|
2732
|
+
const segmentTiebreakChanges = new Set<{
|
|
2733
|
+
segment: ISegmentLeaf;
|
|
2734
|
+
old: ObliterateInfo;
|
|
2735
|
+
new: ObliterateInfo | undefined;
|
|
2736
|
+
}>();
|
|
2737
|
+
for (const info of this.obliterates) {
|
|
2738
|
+
if (info.tiebreakTrackingGroup !== undefined) {
|
|
2739
|
+
for (const segment of info.tiebreakTrackingGroup.tracked) {
|
|
2740
|
+
// Recompute previous obliterate
|
|
2741
|
+
assert(
|
|
2742
|
+
isSegmentLeaf(segment) &&
|
|
2743
|
+
isInsideObliterate(segment) &&
|
|
2744
|
+
segment.insertionRefSeqStamp !== undefined,
|
|
2745
|
+
0xb74 /* Expected segment leaf inside obliterate with insertionRefSeqStamp */,
|
|
2746
|
+
);
|
|
2747
|
+
// This may have changed as a result of segments shuffling: outstanding local obliterates that previously surrounded a segment may no longer surround it.
|
|
2748
|
+
const newObliteratePrecedingInsertion = this.computeObliteratePrecedingInsertion(
|
|
2749
|
+
segment,
|
|
2750
|
+
segment.insertionRefSeqStamp,
|
|
2751
|
+
);
|
|
2752
|
+
|
|
2753
|
+
if (newObliteratePrecedingInsertion !== info) {
|
|
2754
|
+
segmentTiebreakChanges.add({
|
|
2755
|
+
segment,
|
|
2756
|
+
old: info,
|
|
2757
|
+
new: newObliteratePrecedingInsertion,
|
|
2758
|
+
});
|
|
2759
|
+
}
|
|
2760
|
+
}
|
|
2761
|
+
}
|
|
2762
|
+
}
|
|
2763
|
+
|
|
2764
|
+
for (const { segment, old, new: newInfo } of segmentTiebreakChanges) {
|
|
2765
|
+
// Update tiebreak tracking groups on old/new segment as well as `ISegmentInsideObliterateInfo` state
|
|
2766
|
+
// which we only keep around as long as obliterates are in flight.
|
|
2767
|
+
old.tiebreakTrackingGroup?.unlink(segment);
|
|
2768
|
+
if (newInfo?.tiebreakTrackingGroup === undefined) {
|
|
2769
|
+
// Segment is either no longer inside any obliterate or only inside acked obliterates.
|
|
2770
|
+
overwriteInfo<ISegmentInsideObliterateInfo>(segment, {
|
|
2771
|
+
obliteratePrecedingInsertion: newInfo,
|
|
2772
|
+
insertionRefSeqStamp: undefined,
|
|
2773
|
+
});
|
|
2774
|
+
} else {
|
|
2775
|
+
overwriteInfo<ISegmentInsideObliterateInfo>(segment, {
|
|
2776
|
+
obliteratePrecedingInsertion: newInfo,
|
|
2777
|
+
});
|
|
2778
|
+
|
|
2779
|
+
newInfo.tiebreakTrackingGroup.link(segment);
|
|
2780
|
+
}
|
|
2781
|
+
}
|
|
2615
2782
|
}
|
|
2616
2783
|
private blockUpdate(block: MergeBlock): void {
|
|
2617
2784
|
let len: number | undefined;
|