@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/client.ts
CHANGED
|
@@ -32,6 +32,7 @@ import { LocalReferencePosition, SlidingPreference } from "./localReference.js";
|
|
|
32
32
|
import {
|
|
33
33
|
MergeTree,
|
|
34
34
|
errorIfOptionNotTrue,
|
|
35
|
+
getSlideToSegoff,
|
|
35
36
|
isRemovedAndAcked,
|
|
36
37
|
type IMergeTreeOptionsInternal,
|
|
37
38
|
} from "./mergeTree.js";
|
|
@@ -50,6 +51,8 @@ import {
|
|
|
50
51
|
SegmentGroup,
|
|
51
52
|
compareStrings,
|
|
52
53
|
isSegmentLeaf,
|
|
54
|
+
type ISegmentLeaf,
|
|
55
|
+
type ObliterateInfo,
|
|
53
56
|
} from "./mergeTreeNodes.js";
|
|
54
57
|
import {
|
|
55
58
|
createAdjustRangeOp,
|
|
@@ -103,6 +106,12 @@ import * as opstampUtils from "./stamps.js";
|
|
|
103
106
|
type IMergeTreeDeltaRemoteOpArgs = Omit<IMergeTreeDeltaOpArgs, "sequencedMessage"> &
|
|
104
107
|
Required<Pick<IMergeTreeDeltaOpArgs, "sequencedMessage">>;
|
|
105
108
|
|
|
109
|
+
interface RebasedObliterateEndpoint {
|
|
110
|
+
segment: ISegmentLeaf;
|
|
111
|
+
offset: number;
|
|
112
|
+
side: Side;
|
|
113
|
+
}
|
|
114
|
+
|
|
106
115
|
/**
|
|
107
116
|
* A range [start, end)
|
|
108
117
|
* @internal
|
|
@@ -783,14 +792,14 @@ export class Client extends TypedEventEmitter<IClientEvents> {
|
|
|
783
792
|
private ackPendingSegment(opArgs: IMergeTreeDeltaRemoteOpArgs): void {
|
|
784
793
|
if (opArgs.op.type === MergeTreeDeltaType.GROUP) {
|
|
785
794
|
for (const memberOp of opArgs.op.ops) {
|
|
786
|
-
this._mergeTree.
|
|
795
|
+
this._mergeTree.ackOp({
|
|
787
796
|
groupOp: opArgs.op,
|
|
788
797
|
op: memberOp,
|
|
789
798
|
sequencedMessage: opArgs.sequencedMessage,
|
|
790
799
|
});
|
|
791
800
|
}
|
|
792
801
|
} else {
|
|
793
|
-
this._mergeTree.
|
|
802
|
+
this._mergeTree.ackOp(opArgs);
|
|
794
803
|
}
|
|
795
804
|
}
|
|
796
805
|
|
|
@@ -842,6 +851,83 @@ export class Client extends TypedEventEmitter<IClientEvents> {
|
|
|
842
851
|
return this._mergeTree.getPosition(segment, perspective);
|
|
843
852
|
}
|
|
844
853
|
|
|
854
|
+
/**
|
|
855
|
+
* Rebases a sided local reference to the best fitting position in the current tree.
|
|
856
|
+
*/
|
|
857
|
+
private rebaseSidedLocalReference(
|
|
858
|
+
ref: LocalReferencePosition,
|
|
859
|
+
side: Side,
|
|
860
|
+
reconnectingPerspective: Perspective,
|
|
861
|
+
slidePreference: SlidingPreference,
|
|
862
|
+
): RebasedObliterateEndpoint {
|
|
863
|
+
const oldSegment = ref.getSegment();
|
|
864
|
+
const oldOffset = ref.getOffset();
|
|
865
|
+
assert(
|
|
866
|
+
oldSegment !== undefined && oldOffset !== undefined,
|
|
867
|
+
0xb61 /* Invalid old reference */,
|
|
868
|
+
);
|
|
869
|
+
const useNewSlidingBehavior = true;
|
|
870
|
+
// Destructuring segment + offset is convenient and segment is reassigned
|
|
871
|
+
// eslint-disable-next-line prefer-const
|
|
872
|
+
let { segment: newSegment, offset: newOffset } = getSlideToSegoff(
|
|
873
|
+
{ segment: oldSegment, offset: oldOffset },
|
|
874
|
+
slidePreference,
|
|
875
|
+
reconnectingPerspective,
|
|
876
|
+
useNewSlidingBehavior,
|
|
877
|
+
);
|
|
878
|
+
|
|
879
|
+
newSegment ??=
|
|
880
|
+
slidePreference === SlidingPreference.FORWARD
|
|
881
|
+
? this._mergeTree.endOfTree
|
|
882
|
+
: this._mergeTree.startOfTree;
|
|
883
|
+
|
|
884
|
+
assert(
|
|
885
|
+
isSegmentLeaf(newSegment) && newOffset !== undefined,
|
|
886
|
+
0xb62 /* Invalid new segment on rebase */,
|
|
887
|
+
);
|
|
888
|
+
|
|
889
|
+
const newSide: Side =
|
|
890
|
+
newSegment === oldSegment
|
|
891
|
+
? side
|
|
892
|
+
: // If the reference slid to a new position, the closest fit to the original position will be independent of
|
|
893
|
+
// the original side and "in the direction of where the reference was".
|
|
894
|
+
slidePreference === SlidingPreference.FORWARD
|
|
895
|
+
? Side.Before
|
|
896
|
+
: Side.After;
|
|
897
|
+
|
|
898
|
+
return { segment: newSegment, offset: newOffset, side: newSide };
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
private computeNewObliterateEndpoints(obliterateInfo: ObliterateInfo): {
|
|
902
|
+
start: RebasedObliterateEndpoint;
|
|
903
|
+
end: RebasedObliterateEndpoint;
|
|
904
|
+
} {
|
|
905
|
+
const { currentSeq, clientId } = this.getCollabWindow();
|
|
906
|
+
const reconnectingPerspective = new LocalReconnectingPerspective(
|
|
907
|
+
currentSeq,
|
|
908
|
+
clientId,
|
|
909
|
+
obliterateInfo.stamp.localSeq! - 1,
|
|
910
|
+
);
|
|
911
|
+
|
|
912
|
+
const newStart = this.rebaseSidedLocalReference(
|
|
913
|
+
obliterateInfo.start,
|
|
914
|
+
obliterateInfo.startSide,
|
|
915
|
+
reconnectingPerspective,
|
|
916
|
+
SlidingPreference.FORWARD,
|
|
917
|
+
);
|
|
918
|
+
const newEnd = this.rebaseSidedLocalReference(
|
|
919
|
+
obliterateInfo.end,
|
|
920
|
+
obliterateInfo.endSide,
|
|
921
|
+
reconnectingPerspective,
|
|
922
|
+
SlidingPreference.BACKWARD,
|
|
923
|
+
);
|
|
924
|
+
|
|
925
|
+
return {
|
|
926
|
+
start: newStart,
|
|
927
|
+
end: newEnd,
|
|
928
|
+
};
|
|
929
|
+
}
|
|
930
|
+
|
|
845
931
|
private resetPendingDeltaToOps(
|
|
846
932
|
resetOp: IMergeTreeDeltaOp,
|
|
847
933
|
|
|
@@ -853,20 +939,177 @@ export class Client extends TypedEventEmitter<IClientEvents> {
|
|
|
853
939
|
segmentGroup === NACKedSegmentGroup,
|
|
854
940
|
0x034 /* "Segment group not at head of pending rebase queue" */,
|
|
855
941
|
);
|
|
942
|
+
assert(
|
|
943
|
+
segmentGroup.localSeq !== undefined,
|
|
944
|
+
0x867 /* expected segment group localSeq to be defined */,
|
|
945
|
+
);
|
|
856
946
|
if (this.pendingRebase?.empty) {
|
|
857
947
|
this.pendingRebase = undefined;
|
|
858
948
|
}
|
|
859
949
|
|
|
860
|
-
|
|
950
|
+
if (
|
|
951
|
+
resetOp.type === MergeTreeDeltaType.OBLITERATE ||
|
|
952
|
+
resetOp.type === MergeTreeDeltaType.OBLITERATE_SIDED
|
|
953
|
+
) {
|
|
954
|
+
errorIfOptionNotTrue(this._mergeTree.options, "mergeTreeEnableObliterateReconnect");
|
|
861
955
|
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
956
|
+
// sliceRemove reconnect logic is characteristically different from other ops (which can only apply to segments they originally saw).
|
|
957
|
+
// This is because the ranges that other ops apply to can be broken up by concurrent insertions, so even though setRemoves are originally
|
|
958
|
+
// applied to a contiguous set of segments, at resubmission time they may no longer be.
|
|
959
|
+
// On the other hand, the closest analog to a `sliceRemove` that we can submit is obtained by resolving the "closest" start and end points
|
|
960
|
+
// for that slice, updating the local obliterate metadata to reflect that slice, and submitting a single op.
|
|
961
|
+
|
|
962
|
+
const obliterateInfo: ObliterateInfo | undefined = segmentGroup.obliterateInfo;
|
|
963
|
+
assert(
|
|
964
|
+
obliterateInfo !== undefined,
|
|
965
|
+
0xb63 /* Resubmitting obliterate op without obliterate info in segment group */,
|
|
966
|
+
);
|
|
967
|
+
assert(
|
|
968
|
+
obliterateInfo.stamp.localSeq === segmentGroup.localSeq,
|
|
969
|
+
0xb64 /* Local seq mismatch */,
|
|
970
|
+
);
|
|
971
|
+
|
|
972
|
+
const cachedNewPositions = this.cachedObliterateRebases.get(
|
|
973
|
+
obliterateInfo.stamp.localSeq,
|
|
974
|
+
);
|
|
975
|
+
assert(
|
|
976
|
+
cachedNewPositions !== undefined,
|
|
977
|
+
0xb65 /* didn't compute new positions for obliterate on reconnect early enough */,
|
|
978
|
+
);
|
|
979
|
+
const {
|
|
980
|
+
start: { segment: newStartSegment, offset: newStartOffset, side: newStartSide },
|
|
981
|
+
end: { segment: newEndSegment, offset: newEndOffset, side: newEndSide },
|
|
982
|
+
} = cachedNewPositions;
|
|
983
|
+
|
|
984
|
+
const { currentSeq, clientId } = this.getCollabWindow();
|
|
985
|
+
|
|
986
|
+
if (newEndSegment.ordinal < newStartSegment.ordinal) {
|
|
987
|
+
for (const segment of segmentGroup.segments) {
|
|
988
|
+
assert(
|
|
989
|
+
isRemovedAndAcked(segment),
|
|
990
|
+
0xb66 /* On reconnect, obliterate applied to new segments even though original ones were not removed. */,
|
|
991
|
+
);
|
|
992
|
+
const lastRemove = segment.removes[segment.removes.length - 1];
|
|
993
|
+
assert(
|
|
994
|
+
lastRemove.type === "sliceRemove" && lastRemove.localSeq === segmentGroup.localSeq,
|
|
995
|
+
0xb67 /* Last remove should be the obliterate that is being resubmitted. */,
|
|
996
|
+
);
|
|
997
|
+
// The original obliterate affected this segment, but it has since been removed and overlapping removes
|
|
998
|
+
// are only possible when they are concurrent. We adjust the metadata on that segment now to reflect
|
|
999
|
+
// the fact that the obliterate no longer affects it.
|
|
1000
|
+
segment.removes.pop();
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
this._mergeTree.rebaseObliterateTo(obliterateInfo, undefined);
|
|
1004
|
+
return [];
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
assert(
|
|
1008
|
+
obliterateInfo.tiebreakTrackingGroup !== undefined,
|
|
1009
|
+
0xb68 /* Tiebreak tracking group missing */,
|
|
1010
|
+
);
|
|
1011
|
+
|
|
1012
|
+
const newObliterate: ObliterateInfo = {
|
|
1013
|
+
// Recreate the start position using the perspective that other clients will see.
|
|
1014
|
+
// This may not be at the same position as the original reference, since the segment the original reference was on could have been removed.
|
|
1015
|
+
start: this._mergeTree.createLocalReferencePosition(
|
|
1016
|
+
newStartSegment,
|
|
1017
|
+
newStartOffset,
|
|
1018
|
+
ReferenceType.StayOnRemove,
|
|
1019
|
+
undefined,
|
|
1020
|
+
),
|
|
1021
|
+
startSide: newStartSide,
|
|
1022
|
+
end: this._mergeTree.createLocalReferencePosition(
|
|
1023
|
+
newEndSegment,
|
|
1024
|
+
newEndOffset,
|
|
1025
|
+
ReferenceType.StayOnRemove,
|
|
1026
|
+
undefined,
|
|
1027
|
+
),
|
|
1028
|
+
endSide: newEndSide,
|
|
1029
|
+
refSeq: currentSeq,
|
|
1030
|
+
// We reuse the localSeq from the original obliterate.
|
|
1031
|
+
stamp: obliterateInfo.stamp,
|
|
1032
|
+
segmentGroup: undefined,
|
|
1033
|
+
tiebreakTrackingGroup: obliterateInfo.tiebreakTrackingGroup,
|
|
1034
|
+
};
|
|
1035
|
+
newObliterate.start.addProperties({ obliterate: newObliterate });
|
|
1036
|
+
newObliterate.end.addProperties({ obliterate: newObliterate });
|
|
1037
|
+
newObliterate.segmentGroup = {
|
|
1038
|
+
segments: [],
|
|
1039
|
+
localSeq: segmentGroup.localSeq,
|
|
1040
|
+
refSeq: this.getCollabWindow().currentSeq,
|
|
1041
|
+
obliterateInfo: newObliterate,
|
|
1042
|
+
};
|
|
1043
|
+
|
|
1044
|
+
for (const segment of segmentGroup.segments) {
|
|
1045
|
+
assert(
|
|
1046
|
+
segment.segmentGroups?.remove(segmentGroup) === true,
|
|
1047
|
+
0x035 /* "Segment group not in segment pending queue" */,
|
|
1048
|
+
);
|
|
1049
|
+
if (
|
|
1050
|
+
(segment.ordinal > newStartSegment.ordinal &&
|
|
1051
|
+
segment.ordinal < newEndSegment.ordinal) ||
|
|
1052
|
+
(segment === newStartSegment && newStartSide === Side.Before) ||
|
|
1053
|
+
(segment === newEndSegment && newEndSide === Side.After)
|
|
1054
|
+
) {
|
|
1055
|
+
segment.segmentGroups.enqueue(newObliterate.segmentGroup);
|
|
1056
|
+
} else {
|
|
1057
|
+
assert(
|
|
1058
|
+
isRemovedAndAcked(segment),
|
|
1059
|
+
0xb69 /* On reconnect, obliterate applied to new segments even though original ones were not removed. */,
|
|
1060
|
+
);
|
|
1061
|
+
const lastRemove = segment.removes[segment.removes.length - 1];
|
|
1062
|
+
assert(
|
|
1063
|
+
lastRemove.type === "sliceRemove" && lastRemove.localSeq === segmentGroup.localSeq,
|
|
1064
|
+
0xb6a /* Last remove should be the obliterate that is being resubmitted. */,
|
|
1065
|
+
);
|
|
1066
|
+
// The original obliterate affected this segment, but it has since been removed and it's impossible to apply the
|
|
1067
|
+
// local obliterate so that is so. We adjust the metadata on that segment now.
|
|
1068
|
+
segment.removes.pop();
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
this._mergeTree.rebaseObliterateTo(obliterateInfo, newObliterate);
|
|
1073
|
+
this._mergeTree.pendingSegments.push(newObliterate.segmentGroup);
|
|
1074
|
+
|
|
1075
|
+
const reconnectingPerspective = new LocalReconnectingPerspective(
|
|
1076
|
+
currentSeq,
|
|
1077
|
+
clientId,
|
|
1078
|
+
obliterateInfo.stamp.localSeq - 1,
|
|
1079
|
+
);
|
|
1080
|
+
|
|
1081
|
+
const newStartPos =
|
|
1082
|
+
this._mergeTree.getPosition(newStartSegment, reconnectingPerspective) + newStartOffset;
|
|
1083
|
+
const newEndPos =
|
|
1084
|
+
this._mergeTree.getPosition(newEndSegment, reconnectingPerspective) + newEndOffset;
|
|
1085
|
+
if (resetOp.type === MergeTreeDeltaType.OBLITERATE) {
|
|
1086
|
+
assert(
|
|
1087
|
+
newStartSide === Side.Before && newEndSide === Side.After,
|
|
1088
|
+
0xb6b /* Non-sided obliterate should have start side before and end side after */,
|
|
1089
|
+
);
|
|
1090
|
+
// Use a non-sided obliterate op if the original op was non-sided. Some combinations of feature flags disallow sided obliterate ops
|
|
1091
|
+
// but allow non-sided ones, and if we convert a non-sided op to a sided one on reconnect, we may cause errors.
|
|
1092
|
+
return [
|
|
1093
|
+
createObliterateRangeOp(
|
|
1094
|
+
newStartPos,
|
|
1095
|
+
newEndPos +
|
|
1096
|
+
1 /* to make the end exclusive, see corresponding -1 in `createObliterateRangeOpSided` on converting non-sided to sided. */,
|
|
1097
|
+
),
|
|
1098
|
+
];
|
|
1099
|
+
}
|
|
1100
|
+
return [
|
|
1101
|
+
createObliterateRangeOpSided(
|
|
1102
|
+
{
|
|
1103
|
+
pos: newStartPos,
|
|
1104
|
+
side: newStartSide,
|
|
1105
|
+
},
|
|
1106
|
+
{
|
|
1107
|
+
pos: newEndPos,
|
|
1108
|
+
side: newEndSide,
|
|
1109
|
+
},
|
|
1110
|
+
),
|
|
1111
|
+
];
|
|
1112
|
+
}
|
|
870
1113
|
|
|
871
1114
|
const opList: IMergeTreeDeltaOp[] = [];
|
|
872
1115
|
// We need to sort the segments by ordinal, as the segments are not sorted in the segment group.
|
|
@@ -879,11 +1122,7 @@ export class Client extends TypedEventEmitter<IClientEvents> {
|
|
|
879
1122
|
)) {
|
|
880
1123
|
assert(
|
|
881
1124
|
segment.segmentGroups?.remove(segmentGroup) === true,
|
|
882
|
-
|
|
883
|
-
);
|
|
884
|
-
assert(
|
|
885
|
-
segmentGroup.localSeq !== undefined,
|
|
886
|
-
0x867 /* expected segment group localSeq to be defined */,
|
|
1125
|
+
0xb6c /* Segment group not in segment pending queue */,
|
|
887
1126
|
);
|
|
888
1127
|
const segmentPosition = this.findReconnectionPosition(segment, segmentGroup.localSeq);
|
|
889
1128
|
let newOp: IMergeTreeDeltaOp | undefined;
|
|
@@ -966,44 +1205,18 @@ export class Client extends TypedEventEmitter<IClientEvents> {
|
|
|
966
1205
|
}
|
|
967
1206
|
break;
|
|
968
1207
|
}
|
|
969
|
-
case MergeTreeDeltaType.OBLITERATE: {
|
|
970
|
-
errorIfOptionNotTrue(this._mergeTree.options, "mergeTreeEnableObliterateReconnect");
|
|
971
|
-
// Only bother resubmitting if nobody else has removed it in the meantime.
|
|
972
|
-
// When that happens, the first removal will have been acked.
|
|
973
|
-
if (isRemoved(segment) && opstampUtils.isLocal(segment.removes[0])) {
|
|
974
|
-
newOp = createObliterateRangeOp(
|
|
975
|
-
segmentPosition,
|
|
976
|
-
segmentPosition + segment.cachedLength,
|
|
977
|
-
);
|
|
978
|
-
}
|
|
979
|
-
break;
|
|
980
|
-
}
|
|
981
1208
|
default: {
|
|
982
1209
|
throw new Error(`Invalid op type`);
|
|
983
1210
|
}
|
|
984
1211
|
}
|
|
985
1212
|
|
|
986
|
-
if (newOp
|
|
987
|
-
segment.segmentGroups.enqueue(obliterateSegmentGroup);
|
|
988
|
-
|
|
989
|
-
const first = opList[0];
|
|
990
|
-
|
|
991
|
-
if (
|
|
992
|
-
!!first &&
|
|
993
|
-
first.pos2 !== undefined &&
|
|
994
|
-
first.type !== MergeTreeDeltaType.OBLITERATE_SIDED &&
|
|
995
|
-
newOp.type !== MergeTreeDeltaType.OBLITERATE_SIDED
|
|
996
|
-
) {
|
|
997
|
-
first.pos2 += newOp.pos2! - newOp.pos1!;
|
|
998
|
-
} else {
|
|
999
|
-
opList.push(newOp);
|
|
1000
|
-
}
|
|
1001
|
-
} else if (newOp) {
|
|
1213
|
+
if (newOp) {
|
|
1002
1214
|
const newSegmentGroup: SegmentGroup = {
|
|
1003
1215
|
segments: [],
|
|
1004
1216
|
localSeq: segmentGroup.localSeq,
|
|
1005
1217
|
refSeq: this.getCollabWindow().currentSeq,
|
|
1006
1218
|
};
|
|
1219
|
+
|
|
1007
1220
|
segment.segmentGroups.enqueue(newSegmentGroup);
|
|
1008
1221
|
|
|
1009
1222
|
this._mergeTree.pendingSegments.push(newSegmentGroup);
|
|
@@ -1012,13 +1225,6 @@ export class Client extends TypedEventEmitter<IClientEvents> {
|
|
|
1012
1225
|
}
|
|
1013
1226
|
}
|
|
1014
1227
|
|
|
1015
|
-
if (
|
|
1016
|
-
resetOp.type === MergeTreeDeltaType.OBLITERATE &&
|
|
1017
|
-
obliterateSegmentGroup.segments.length > 0
|
|
1018
|
-
) {
|
|
1019
|
-
this._mergeTree.pendingSegments.push(obliterateSegmentGroup);
|
|
1020
|
-
}
|
|
1021
|
-
|
|
1022
1228
|
return opList;
|
|
1023
1229
|
}
|
|
1024
1230
|
|
|
@@ -1145,10 +1351,15 @@ export class Client extends TypedEventEmitter<IClientEvents> {
|
|
|
1145
1351
|
);
|
|
1146
1352
|
}
|
|
1147
1353
|
|
|
1148
|
-
private
|
|
1354
|
+
private lastNormalization: undefined | { refSeq: number; localRefSeq: number };
|
|
1149
1355
|
|
|
1150
1356
|
private pendingRebase: DoublyLinkedList<SegmentGroup> | undefined;
|
|
1151
1357
|
|
|
1358
|
+
private readonly cachedObliterateRebases: Map<
|
|
1359
|
+
number, // obliterateInfo.stamp.localSeq
|
|
1360
|
+
{ start: RebasedObliterateEndpoint; end: RebasedObliterateEndpoint }
|
|
1361
|
+
> = new Map();
|
|
1362
|
+
|
|
1152
1363
|
/**
|
|
1153
1364
|
* Given a pending operation and segment group, regenerate the op, so it
|
|
1154
1365
|
* can be resubmitted
|
|
@@ -1179,11 +1390,31 @@ export class Client extends TypedEventEmitter<IClientEvents> {
|
|
|
1179
1390
|
this.pendingRebase = this._mergeTree.pendingSegments.splice(firstGroupNode);
|
|
1180
1391
|
}
|
|
1181
1392
|
|
|
1182
|
-
const
|
|
1183
|
-
if (
|
|
1393
|
+
const collabWindow = this.getCollabWindow();
|
|
1394
|
+
if (
|
|
1395
|
+
this.lastNormalization === undefined ||
|
|
1396
|
+
collabWindow.currentSeq !== this.lastNormalization.refSeq ||
|
|
1397
|
+
collabWindow.localSeq !== this.lastNormalization.localRefSeq
|
|
1398
|
+
) {
|
|
1399
|
+
// Compute obliterate endpoint destinations before segments are normalized.
|
|
1400
|
+
// Segment normalization can affect what should be the semantically correct segments for the endpoints to be placed on.
|
|
1401
|
+
this.cachedObliterateRebases.clear();
|
|
1402
|
+
for (const group of [...this._mergeTree.pendingSegments, ...this.pendingRebase]) {
|
|
1403
|
+
const { obliterateInfo } = group.data;
|
|
1404
|
+
if (obliterateInfo !== undefined) {
|
|
1405
|
+
const { start, end } = this.computeNewObliterateEndpoints(obliterateInfo);
|
|
1406
|
+
const { localSeq } = obliterateInfo.stamp;
|
|
1407
|
+
assert(localSeq !== undefined, 0xb6d /* Local seq must be defined */);
|
|
1408
|
+
this.cachedObliterateRebases.set(localSeq, { start, end });
|
|
1409
|
+
}
|
|
1410
|
+
}
|
|
1184
1411
|
this.emit("normalize", this);
|
|
1412
|
+
|
|
1185
1413
|
this._mergeTree.normalizeSegmentsOnRebase();
|
|
1186
|
-
this.
|
|
1414
|
+
this.lastNormalization = {
|
|
1415
|
+
refSeq: collabWindow.currentSeq,
|
|
1416
|
+
localRefSeq: collabWindow.localSeq,
|
|
1417
|
+
};
|
|
1187
1418
|
}
|
|
1188
1419
|
|
|
1189
1420
|
const opList: IMergeTreeDeltaOp[] = [];
|
package/src/index.ts
CHANGED
|
@@ -141,5 +141,5 @@ export {
|
|
|
141
141
|
revertMergeTreeDeltaRevertibles,
|
|
142
142
|
} from "./revertibles.js";
|
|
143
143
|
export type { OperationStamp } from "./stamps.js";
|
|
144
|
-
export type
|
|
144
|
+
export { createLocalReconnectingPerspective, type Perspective } from "./perspective.js";
|
|
145
145
|
export type { IMergeTreeTextHelper } from "./MergeTreeTextHelper.js";
|