@fluidframework/merge-tree 2.41.0 → 2.43.0-343119
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 +9 -6
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +79 -36
- package/dist/client.js.map +1 -1
- package/dist/collections/list.d.ts +95 -5
- package/dist/collections/list.d.ts.map +1 -1
- package/dist/collections/list.js +67 -5
- package/dist/collections/list.js.map +1 -1
- package/dist/constants.d.ts +5 -0
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +6 -1
- package/dist/constants.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -2
- package/dist/index.js.map +1 -1
- package/dist/mergeTree.d.ts +9 -9
- package/dist/mergeTree.d.ts.map +1 -1
- package/dist/mergeTree.js +15 -7
- package/dist/mergeTree.js.map +1 -1
- package/dist/perspective.d.ts +15 -0
- package/dist/perspective.d.ts.map +1 -1
- package/dist/perspective.js +25 -1
- package/dist/perspective.js.map +1 -1
- package/dist/revertibles.js +1 -1
- package/dist/revertibles.js.map +1 -1
- package/dist/stamps.d.ts +1 -0
- package/dist/stamps.d.ts.map +1 -1
- package/dist/stamps.js +5 -1
- package/dist/stamps.js.map +1 -1
- package/dist/test/beastTest.spec.js +1 -1
- package/dist/test/beastTest.spec.js.map +1 -1
- package/dist/test/client.annotateMarker.spec.js +1 -1
- package/dist/test/client.annotateMarker.spec.js.map +1 -1
- package/dist/test/client.applyMsg.spec.js +25 -25
- package/dist/test/client.applyMsg.spec.js.map +1 -1
- package/dist/test/client.applyStashedOpFarm.spec.d.ts.map +1 -1
- package/dist/test/client.applyStashedOpFarm.spec.js +3 -3
- package/dist/test/client.applyStashedOpFarm.spec.js.map +1 -1
- package/dist/test/client.attributionFarm.spec.js +1 -1
- package/dist/test/client.attributionFarm.spec.js.map +1 -1
- package/dist/test/client.getPosition.spec.js +1 -0
- package/dist/test/client.getPosition.spec.js.map +1 -1
- package/dist/test/client.localReference.spec.js +62 -48
- package/dist/test/client.localReference.spec.js.map +1 -1
- package/dist/test/client.localReferenceFarm.spec.js +1 -0
- package/dist/test/client.localReferenceFarm.spec.js.map +1 -1
- package/dist/test/client.reconnectFarm.spec.js +1 -1
- package/dist/test/client.reconnectFarm.spec.js.map +1 -1
- package/dist/test/client.rollback.spec.js +8 -6
- package/dist/test/client.rollback.spec.js.map +1 -1
- package/dist/test/client.searchForMarker.spec.js +2 -2
- package/dist/test/client.searchForMarker.spec.js.map +1 -1
- package/dist/test/clientTestHelper.js +1 -1
- package/dist/test/clientTestHelper.js.map +1 -1
- package/dist/test/mergeTree.annotate.spec.js +25 -25
- package/dist/test/mergeTree.annotate.spec.js.map +1 -1
- package/dist/test/mergeTree.markRangeRemoved.spec.js +1 -1
- package/dist/test/mergeTree.markRangeRemoved.spec.js.map +1 -1
- package/dist/test/obliterate.spec.js +4 -4
- package/dist/test/obliterate.spec.js.map +1 -1
- package/dist/test/resetPendingSegmentsToOp.spec.js +14 -14
- package/dist/test/resetPendingSegmentsToOp.spec.js.map +1 -1
- package/dist/test/snapshot.utils.js +1 -1
- package/dist/test/snapshot.utils.js.map +1 -1
- package/dist/test/sortedSegmentSet.spec.js +3 -3
- package/dist/test/sortedSegmentSet.spec.js.map +1 -1
- package/dist/test/testClient.js +3 -3
- package/dist/test/testClient.js.map +1 -1
- package/dist/test/tracking.spec.js +7 -7
- package/dist/test/tracking.spec.js.map +1 -1
- package/dist/test/wordUnitTests.spec.js.map +1 -1
- package/lib/client.d.ts +9 -6
- package/lib/client.d.ts.map +1 -1
- package/lib/client.js +81 -38
- package/lib/client.js.map +1 -1
- package/lib/collections/list.d.ts +95 -5
- package/lib/collections/list.d.ts.map +1 -1
- package/lib/collections/list.js +67 -5
- package/lib/collections/list.js.map +1 -1
- package/lib/constants.d.ts +5 -0
- package/lib/constants.d.ts.map +1 -1
- package/lib/constants.js +5 -0
- package/lib/constants.js.map +1 -1
- package/lib/index.d.ts +1 -1
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +1 -1
- package/lib/index.js.map +1 -1
- package/lib/mergeTree.d.ts +9 -9
- package/lib/mergeTree.d.ts.map +1 -1
- package/lib/mergeTree.js +15 -7
- package/lib/mergeTree.js.map +1 -1
- package/lib/perspective.d.ts +15 -0
- package/lib/perspective.d.ts.map +1 -1
- package/lib/perspective.js +23 -0
- package/lib/perspective.js.map +1 -1
- package/lib/revertibles.js +1 -1
- package/lib/revertibles.js.map +1 -1
- package/lib/stamps.d.ts +1 -0
- package/lib/stamps.d.ts.map +1 -1
- package/lib/stamps.js +4 -1
- package/lib/stamps.js.map +1 -1
- package/lib/test/beastTest.spec.js +1 -1
- package/lib/test/beastTest.spec.js.map +1 -1
- package/lib/test/client.annotateMarker.spec.js +1 -1
- package/lib/test/client.annotateMarker.spec.js.map +1 -1
- package/lib/test/client.applyMsg.spec.js +25 -25
- package/lib/test/client.applyMsg.spec.js.map +1 -1
- package/lib/test/client.applyStashedOpFarm.spec.d.ts.map +1 -1
- package/lib/test/client.applyStashedOpFarm.spec.js +3 -3
- package/lib/test/client.applyStashedOpFarm.spec.js.map +1 -1
- package/lib/test/client.attributionFarm.spec.js +1 -1
- package/lib/test/client.attributionFarm.spec.js.map +1 -1
- package/lib/test/client.getPosition.spec.js +1 -0
- package/lib/test/client.getPosition.spec.js.map +1 -1
- package/lib/test/client.localReference.spec.js +62 -48
- package/lib/test/client.localReference.spec.js.map +1 -1
- package/lib/test/client.localReferenceFarm.spec.js +1 -0
- package/lib/test/client.localReferenceFarm.spec.js.map +1 -1
- package/lib/test/client.reconnectFarm.spec.js +1 -1
- package/lib/test/client.reconnectFarm.spec.js.map +1 -1
- package/lib/test/client.rollback.spec.js +8 -6
- package/lib/test/client.rollback.spec.js.map +1 -1
- package/lib/test/client.searchForMarker.spec.js +2 -2
- package/lib/test/client.searchForMarker.spec.js.map +1 -1
- package/lib/test/clientTestHelper.js +1 -1
- package/lib/test/clientTestHelper.js.map +1 -1
- package/lib/test/mergeTree.annotate.spec.js +25 -25
- package/lib/test/mergeTree.annotate.spec.js.map +1 -1
- package/lib/test/mergeTree.markRangeRemoved.spec.js +1 -1
- package/lib/test/mergeTree.markRangeRemoved.spec.js.map +1 -1
- package/lib/test/obliterate.spec.js +4 -4
- package/lib/test/obliterate.spec.js.map +1 -1
- package/lib/test/resetPendingSegmentsToOp.spec.js +14 -14
- package/lib/test/resetPendingSegmentsToOp.spec.js.map +1 -1
- package/lib/test/snapshot.utils.js +1 -1
- package/lib/test/snapshot.utils.js.map +1 -1
- package/lib/test/sortedSegmentSet.spec.js +3 -3
- package/lib/test/sortedSegmentSet.spec.js.map +1 -1
- package/lib/test/testClient.js +3 -3
- package/lib/test/testClient.js.map +1 -1
- package/lib/test/tracking.spec.js +7 -7
- package/lib/test/tracking.spec.js.map +1 -1
- package/lib/test/wordUnitTests.spec.js.map +1 -1
- package/package.json +16 -16
- package/src/client.ts +117 -50
- package/src/collections/list.ts +101 -5
- package/src/constants.ts +6 -0
- package/src/index.ts +3 -0
- package/src/mergeTree.ts +30 -24
- package/src/perspective.ts +26 -0
- package/src/revertibles.ts +1 -1
- package/src/stamps.ts +9 -1
package/src/client.ts
CHANGED
|
@@ -27,7 +27,7 @@ import {
|
|
|
27
27
|
|
|
28
28
|
import { MergeTreeTextHelper, type IMergeTreeTextHelper } from "./MergeTreeTextHelper.js";
|
|
29
29
|
import { DoublyLinkedList, RedBlackTree } from "./collections/index.js";
|
|
30
|
-
import { NonCollabClient, UniversalSequenceNumber } from "./constants.js";
|
|
30
|
+
import { NonCollabClient, SquashClient, UniversalSequenceNumber } from "./constants.js";
|
|
31
31
|
import { LocalReferencePosition, SlidingPreference } from "./localReference.js";
|
|
32
32
|
import {
|
|
33
33
|
MergeTree,
|
|
@@ -84,6 +84,7 @@ import {
|
|
|
84
84
|
} from "./ops.js";
|
|
85
85
|
import {
|
|
86
86
|
LocalReconnectingPerspective,
|
|
87
|
+
LocalSquashPerspective,
|
|
87
88
|
PriorPerspective,
|
|
88
89
|
type Perspective,
|
|
89
90
|
} from "./perspective.js";
|
|
@@ -95,6 +96,7 @@ import {
|
|
|
95
96
|
overwriteInfo,
|
|
96
97
|
toRemovalInfo,
|
|
97
98
|
type IHasInsertionInfo,
|
|
99
|
+
type IHasRemovalInfo,
|
|
98
100
|
} from "./segmentInfos.js";
|
|
99
101
|
import { Side, type InteriorSequencePlace } from "./sequencePlace.js";
|
|
100
102
|
import { SnapshotLoader } from "./snapshotLoader.js";
|
|
@@ -363,6 +365,7 @@ export class Client extends TypedEventEmitter<IClientEvents> {
|
|
|
363
365
|
end: number | undefined,
|
|
364
366
|
accum: TClientData,
|
|
365
367
|
splitRange?: boolean,
|
|
368
|
+
perspective?: Pick<ISequencedDocumentMessage, "clientId" | "referenceSequenceNumber">,
|
|
366
369
|
): void;
|
|
367
370
|
public walkSegments(
|
|
368
371
|
handler: ISegmentAction<undefined>,
|
|
@@ -370,6 +373,7 @@ export class Client extends TypedEventEmitter<IClientEvents> {
|
|
|
370
373
|
end?: number,
|
|
371
374
|
accum?: undefined,
|
|
372
375
|
splitRange?: boolean,
|
|
376
|
+
perspective?: Pick<ISequencedDocumentMessage, "clientId" | "referenceSequenceNumber">,
|
|
373
377
|
): void;
|
|
374
378
|
public walkSegments<TClientData>(
|
|
375
379
|
handler: ISegmentAction<TClientData>,
|
|
@@ -377,10 +381,13 @@ export class Client extends TypedEventEmitter<IClientEvents> {
|
|
|
377
381
|
end: number | undefined,
|
|
378
382
|
accum: TClientData,
|
|
379
383
|
splitRange: boolean = false,
|
|
384
|
+
perspective?: Pick<ISequencedDocumentMessage, "clientId" | "referenceSequenceNumber">,
|
|
380
385
|
): void {
|
|
381
386
|
this._mergeTree.mapRange(
|
|
382
387
|
handler,
|
|
383
|
-
|
|
388
|
+
perspective === undefined
|
|
389
|
+
? this.getCollabWindow().localPerspective
|
|
390
|
+
: this.getOperationPerspective(perspective),
|
|
384
391
|
accum,
|
|
385
392
|
start,
|
|
386
393
|
end,
|
|
@@ -556,7 +563,9 @@ export class Client extends TypedEventEmitter<IClientEvents> {
|
|
|
556
563
|
}
|
|
557
564
|
|
|
558
565
|
private getOperationPerspective(
|
|
559
|
-
sequencedMessage:
|
|
566
|
+
sequencedMessage:
|
|
567
|
+
| Pick<ISequencedDocumentMessage, "clientId" | "referenceSequenceNumber">
|
|
568
|
+
| undefined,
|
|
560
569
|
): Perspective {
|
|
561
570
|
if (!sequencedMessage) {
|
|
562
571
|
return this._mergeTree.localPerspective;
|
|
@@ -869,17 +878,20 @@ export class Client extends TypedEventEmitter<IClientEvents> {
|
|
|
869
878
|
const useNewSlidingBehavior = true;
|
|
870
879
|
// Destructuring segment + offset is convenient and segment is reassigned
|
|
871
880
|
// eslint-disable-next-line prefer-const
|
|
872
|
-
|
|
881
|
+
const segOff = getSlideToSegoff(
|
|
873
882
|
{ segment: oldSegment, offset: oldOffset },
|
|
874
883
|
slidePreference,
|
|
875
884
|
reconnectingPerspective,
|
|
876
885
|
useNewSlidingBehavior,
|
|
877
886
|
);
|
|
878
887
|
|
|
879
|
-
newSegment
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
888
|
+
const { segment: newSegment, offset: newOffset } = segOff ?? {
|
|
889
|
+
segment:
|
|
890
|
+
slidePreference === SlidingPreference.FORWARD
|
|
891
|
+
? this._mergeTree.endOfTree
|
|
892
|
+
: this._mergeTree.startOfTree,
|
|
893
|
+
offset: 0,
|
|
894
|
+
};
|
|
883
895
|
|
|
884
896
|
assert(
|
|
885
897
|
isSegmentLeaf(newSegment) && newOffset !== undefined,
|
|
@@ -898,16 +910,17 @@ export class Client extends TypedEventEmitter<IClientEvents> {
|
|
|
898
910
|
return { segment: newSegment, offset: newOffset, side: newSide };
|
|
899
911
|
}
|
|
900
912
|
|
|
901
|
-
private computeNewObliterateEndpoints(
|
|
913
|
+
private computeNewObliterateEndpoints(
|
|
914
|
+
obliterateInfo: ObliterateInfo,
|
|
915
|
+
squash: boolean,
|
|
916
|
+
): {
|
|
902
917
|
start: RebasedObliterateEndpoint;
|
|
903
918
|
end: RebasedObliterateEndpoint;
|
|
904
919
|
} {
|
|
905
920
|
const { currentSeq, clientId } = this.getCollabWindow();
|
|
906
|
-
const reconnectingPerspective = new
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
obliterateInfo.stamp.localSeq! - 1,
|
|
910
|
-
);
|
|
921
|
+
const reconnectingPerspective = new (
|
|
922
|
+
squash ? LocalSquashPerspective : LocalReconnectingPerspective
|
|
923
|
+
)(currentSeq, clientId, obliterateInfo.stamp.localSeq! - 1);
|
|
911
924
|
|
|
912
925
|
const newStart = this.rebaseSidedLocalReference(
|
|
913
926
|
obliterateInfo.start,
|
|
@@ -932,6 +945,7 @@ export class Client extends TypedEventEmitter<IClientEvents> {
|
|
|
932
945
|
resetOp: IMergeTreeDeltaOp,
|
|
933
946
|
|
|
934
947
|
segmentGroup: SegmentGroup,
|
|
948
|
+
squash: boolean,
|
|
935
949
|
): IMergeTreeDeltaOp[] {
|
|
936
950
|
assert(!!segmentGroup, 0x033 /* "Segment group undefined" */);
|
|
937
951
|
const NACKedSegmentGroup = this.pendingRebase?.shift()?.data;
|
|
@@ -991,13 +1005,20 @@ export class Client extends TypedEventEmitter<IClientEvents> {
|
|
|
991
1005
|
);
|
|
992
1006
|
const lastRemove = segment.removes[segment.removes.length - 1];
|
|
993
1007
|
assert(
|
|
994
|
-
lastRemove.type === "sliceRemove" &&
|
|
995
|
-
|
|
1008
|
+
(lastRemove.type === "sliceRemove" &&
|
|
1009
|
+
lastRemove.localSeq === segmentGroup.localSeq) ||
|
|
1010
|
+
opstampUtils.isSquashedOp(lastRemove),
|
|
1011
|
+
0xbad /* Last remove should be the obliterate that is being resubmitted. */,
|
|
996
1012
|
);
|
|
997
|
-
|
|
998
|
-
//
|
|
999
|
-
//
|
|
1000
|
-
|
|
1013
|
+
|
|
1014
|
+
// The original obliterate affected this segment, but it has since been removed.
|
|
1015
|
+
// This can happen when a concurrent obliterate also removed the segment, as well as when the segment was
|
|
1016
|
+
// only locally inserted and its insertion was squashed upon reconnecting.
|
|
1017
|
+
// In the concurrent removal case (where we didn't avoid sending the segment's insertion in the first place due
|
|
1018
|
+
// to squashing), we adjust the metadata on that segment to reflect the fact that this obliterate no longer removes it.
|
|
1019
|
+
if (!opstampUtils.isSquashedOp(lastRemove)) {
|
|
1020
|
+
segment.removes.pop();
|
|
1021
|
+
}
|
|
1001
1022
|
}
|
|
1002
1023
|
|
|
1003
1024
|
this._mergeTree.rebaseObliterateTo(obliterateInfo, undefined);
|
|
@@ -1047,10 +1068,11 @@ export class Client extends TypedEventEmitter<IClientEvents> {
|
|
|
1047
1068
|
0x035 /* "Segment group not in segment pending queue" */,
|
|
1048
1069
|
);
|
|
1049
1070
|
if (
|
|
1050
|
-
(segment
|
|
1071
|
+
!isRemovedAndAcked(segment) &&
|
|
1072
|
+
((segment.ordinal > newStartSegment.ordinal &&
|
|
1051
1073
|
segment.ordinal < newEndSegment.ordinal) ||
|
|
1052
|
-
|
|
1053
|
-
|
|
1074
|
+
(segment === newStartSegment && newStartSide === Side.Before) ||
|
|
1075
|
+
(segment === newEndSegment && newEndSide === Side.After))
|
|
1054
1076
|
) {
|
|
1055
1077
|
segment.segmentGroups.enqueue(newObliterate.segmentGroup);
|
|
1056
1078
|
} else {
|
|
@@ -1060,12 +1082,17 @@ export class Client extends TypedEventEmitter<IClientEvents> {
|
|
|
1060
1082
|
);
|
|
1061
1083
|
const lastRemove = segment.removes[segment.removes.length - 1];
|
|
1062
1084
|
assert(
|
|
1063
|
-
lastRemove.type === "sliceRemove" &&
|
|
1064
|
-
|
|
1085
|
+
(lastRemove.type === "sliceRemove" &&
|
|
1086
|
+
lastRemove.localSeq === segmentGroup.localSeq) ||
|
|
1087
|
+
opstampUtils.isSquashedOp(lastRemove),
|
|
1088
|
+
0xbae /* Last remove should be the obliterate that is being resubmitted. */,
|
|
1065
1089
|
);
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1090
|
+
|
|
1091
|
+
if (!opstampUtils.isSquashedOp(lastRemove)) {
|
|
1092
|
+
// The original obliterate affected this segment, but it has since been removed and it's impossible to apply the
|
|
1093
|
+
// local obliterate so that is so. We adjust the metadata on that segment now.
|
|
1094
|
+
segment.removes.pop();
|
|
1095
|
+
}
|
|
1069
1096
|
}
|
|
1070
1097
|
}
|
|
1071
1098
|
|
|
@@ -1154,13 +1181,25 @@ export class Client extends TypedEventEmitter<IClientEvents> {
|
|
|
1154
1181
|
}
|
|
1155
1182
|
|
|
1156
1183
|
case MergeTreeDeltaType.INSERT: {
|
|
1184
|
+
if (isInserted(segment) && opstampUtils.isSquashedOp(segment.insert)) {
|
|
1185
|
+
break;
|
|
1186
|
+
}
|
|
1157
1187
|
assert(
|
|
1158
1188
|
isInserted(segment) && opstampUtils.isLocal(segment.insert),
|
|
1159
1189
|
0x037 /* "Segment already has assigned sequence number" */,
|
|
1160
1190
|
);
|
|
1161
1191
|
const removeInfo = toRemovalInfo(segment);
|
|
1162
1192
|
|
|
1163
|
-
|
|
1193
|
+
const unusedStamp: OperationStamp = { seq: 0, clientId: 0 };
|
|
1194
|
+
if (removeInfo !== undefined && squash) {
|
|
1195
|
+
assert(
|
|
1196
|
+
removeInfo.removes.length === 1 ||
|
|
1197
|
+
opstampUtils.isAcked(removeInfo.removes[removeInfo.removes.length - 2]),
|
|
1198
|
+
0xbaf /* Expected only one local remove */,
|
|
1199
|
+
);
|
|
1200
|
+
this.squashInsertion(segment);
|
|
1201
|
+
break;
|
|
1202
|
+
} else if (removeInfo !== undefined && opstampUtils.isAcked(removeInfo.removes[0])) {
|
|
1164
1203
|
assert(
|
|
1165
1204
|
removeInfo.removes[0].type === "sliceRemove",
|
|
1166
1205
|
0xb5c /* Remove on insertion must be caused by obliterate. */,
|
|
@@ -1181,6 +1220,7 @@ export class Client extends TypedEventEmitter<IClientEvents> {
|
|
|
1181
1220
|
clientId: NonCollabClient,
|
|
1182
1221
|
},
|
|
1183
1222
|
});
|
|
1223
|
+
this._mergeTree.blockUpdatePathLengths(segment.parent, unusedStamp, true);
|
|
1184
1224
|
break;
|
|
1185
1225
|
}
|
|
1186
1226
|
|
|
@@ -1360,13 +1400,40 @@ export class Client extends TypedEventEmitter<IClientEvents> {
|
|
|
1360
1400
|
{ start: RebasedObliterateEndpoint; end: RebasedObliterateEndpoint }
|
|
1361
1401
|
> = new Map();
|
|
1362
1402
|
|
|
1403
|
+
private squashInsertion(segment: ISegmentLeaf): void {
|
|
1404
|
+
overwriteInfo<IHasInsertionInfo & IHasRemovalInfo>(segment, {
|
|
1405
|
+
insert: {
|
|
1406
|
+
type: "insert",
|
|
1407
|
+
seq: UniversalSequenceNumber,
|
|
1408
|
+
localSeq: undefined,
|
|
1409
|
+
clientId: SquashClient,
|
|
1410
|
+
},
|
|
1411
|
+
removes: [
|
|
1412
|
+
{
|
|
1413
|
+
type: "setRemove",
|
|
1414
|
+
seq: UniversalSequenceNumber,
|
|
1415
|
+
localSeq: undefined,
|
|
1416
|
+
clientId: SquashClient,
|
|
1417
|
+
},
|
|
1418
|
+
],
|
|
1419
|
+
});
|
|
1420
|
+
|
|
1421
|
+
this._mergeTree.blockUpdatePathLengths(segment.parent, { seq: 0, clientId: 0 }, true);
|
|
1422
|
+
}
|
|
1423
|
+
|
|
1363
1424
|
/**
|
|
1364
1425
|
* Given a pending operation and segment group, regenerate the op, so it
|
|
1365
1426
|
* can be resubmitted
|
|
1366
1427
|
* @param resetOp - The op to reset
|
|
1367
1428
|
* @param segmentGroup - The segment group associated with the op
|
|
1429
|
+
* @param squash - whether intermediate states should be squashed. See `IDeltaHandler.reSubmit`'s squash parameter
|
|
1430
|
+
* documentation for more details.
|
|
1368
1431
|
*/
|
|
1369
|
-
public regeneratePendingOp(
|
|
1432
|
+
public regeneratePendingOp(
|
|
1433
|
+
resetOp: IMergeTreeOp,
|
|
1434
|
+
localOpMetadata: unknown,
|
|
1435
|
+
squash: boolean,
|
|
1436
|
+
): IMergeTreeOp {
|
|
1370
1437
|
const segmentGroup = localOpMetadata as SegmentGroup | SegmentGroup[];
|
|
1371
1438
|
if (this.pendingRebase === undefined || this.pendingRebase.empty) {
|
|
1372
1439
|
let firstGroup: SegmentGroup;
|
|
@@ -1396,13 +1463,14 @@ export class Client extends TypedEventEmitter<IClientEvents> {
|
|
|
1396
1463
|
collabWindow.currentSeq !== this.lastNormalization.refSeq ||
|
|
1397
1464
|
collabWindow.localSeq !== this.lastNormalization.localRefSeq
|
|
1398
1465
|
) {
|
|
1466
|
+
const allPendingSegments = [...this._mergeTree.pendingSegments, ...this.pendingRebase];
|
|
1399
1467
|
// Compute obliterate endpoint destinations before segments are normalized.
|
|
1400
1468
|
// Segment normalization can affect what should be the semantically correct segments for the endpoints to be placed on.
|
|
1401
1469
|
this.cachedObliterateRebases.clear();
|
|
1402
|
-
for (const group of
|
|
1470
|
+
for (const group of allPendingSegments) {
|
|
1403
1471
|
const { obliterateInfo } = group.data;
|
|
1404
1472
|
if (obliterateInfo !== undefined) {
|
|
1405
|
-
const { start, end } = this.computeNewObliterateEndpoints(obliterateInfo);
|
|
1473
|
+
const { start, end } = this.computeNewObliterateEndpoints(obliterateInfo, squash);
|
|
1406
1474
|
const { localSeq } = obliterateInfo.stamp;
|
|
1407
1475
|
assert(localSeq !== undefined, 0xb6d /* Local seq must be defined */);
|
|
1408
1476
|
this.cachedObliterateRebases.set(localSeq, { start, end });
|
|
@@ -1426,7 +1494,7 @@ export class Client extends TypedEventEmitter<IClientEvents> {
|
|
|
1426
1494
|
);
|
|
1427
1495
|
|
|
1428
1496
|
for (let i = 0; i < resetOp.ops.length; i++) {
|
|
1429
|
-
opList.push(...this.resetPendingDeltaToOps(resetOp.ops[i], segmentGroup[i]));
|
|
1497
|
+
opList.push(...this.resetPendingDeltaToOps(resetOp.ops[i], segmentGroup[i], squash));
|
|
1430
1498
|
}
|
|
1431
1499
|
} else {
|
|
1432
1500
|
// A group op containing a single op will pass a direct reference to 'segmentGroup'
|
|
@@ -1435,7 +1503,7 @@ export class Client extends TypedEventEmitter<IClientEvents> {
|
|
|
1435
1503
|
resetOp.ops.length === 1,
|
|
1436
1504
|
0x03b /* "Number of ops in 'resetOp' must match the number of segment groups provided." */,
|
|
1437
1505
|
);
|
|
1438
|
-
opList.push(...this.resetPendingDeltaToOps(resetOp.ops[0], segmentGroup));
|
|
1506
|
+
opList.push(...this.resetPendingDeltaToOps(resetOp.ops[0], segmentGroup, squash));
|
|
1439
1507
|
}
|
|
1440
1508
|
} else {
|
|
1441
1509
|
assert(
|
|
@@ -1446,7 +1514,7 @@ export class Client extends TypedEventEmitter<IClientEvents> {
|
|
|
1446
1514
|
!Array.isArray(segmentGroup),
|
|
1447
1515
|
0x03d /* "segmentGroup is array rather than singleton!" */,
|
|
1448
1516
|
);
|
|
1449
|
-
opList.push(...this.resetPendingDeltaToOps(resetOp, segmentGroup));
|
|
1517
|
+
opList.push(...this.resetPendingDeltaToOps(resetOp, segmentGroup, squash));
|
|
1450
1518
|
}
|
|
1451
1519
|
|
|
1452
1520
|
return opList.length === 1 ? opList[0] : createGroupOp(...opList);
|
|
@@ -1547,10 +1615,12 @@ export class Client extends TypedEventEmitter<IClientEvents> {
|
|
|
1547
1615
|
pos: number,
|
|
1548
1616
|
sequenceArgs?: Pick<ISequencedDocumentMessage, "referenceSequenceNumber" | "clientId">,
|
|
1549
1617
|
localSeq?: number,
|
|
1550
|
-
):
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1618
|
+
):
|
|
1619
|
+
| {
|
|
1620
|
+
segment: T;
|
|
1621
|
+
offset: number;
|
|
1622
|
+
}
|
|
1623
|
+
| undefined {
|
|
1554
1624
|
let perspective: Perspective;
|
|
1555
1625
|
const clientId =
|
|
1556
1626
|
sequenceArgs === undefined
|
|
@@ -1565,20 +1635,17 @@ export class Client extends TypedEventEmitter<IClientEvents> {
|
|
|
1565
1635
|
perspective = new PriorPerspective(refSeq, clientId);
|
|
1566
1636
|
}
|
|
1567
1637
|
|
|
1568
|
-
return this._mergeTree.getContainingSegment(pos, perspective) as
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1638
|
+
return this._mergeTree.getContainingSegment(pos, perspective) as
|
|
1639
|
+
| {
|
|
1640
|
+
segment: T;
|
|
1641
|
+
offset: number;
|
|
1642
|
+
}
|
|
1643
|
+
| undefined;
|
|
1572
1644
|
}
|
|
1573
1645
|
|
|
1574
1646
|
getPropertiesAtPosition(pos: number): PropertySet | undefined {
|
|
1575
|
-
let propertiesAtPosition: PropertySet | undefined;
|
|
1576
1647
|
const segoff = this.getContainingSegment(pos);
|
|
1577
|
-
|
|
1578
|
-
if (seg) {
|
|
1579
|
-
propertiesAtPosition = seg.properties;
|
|
1580
|
-
}
|
|
1581
|
-
return propertiesAtPosition;
|
|
1648
|
+
return segoff?.segment?.properties;
|
|
1582
1649
|
}
|
|
1583
1650
|
|
|
1584
1651
|
getRangeExtentsOfPosition(pos: number): {
|
|
@@ -1589,7 +1656,7 @@ export class Client extends TypedEventEmitter<IClientEvents> {
|
|
|
1589
1656
|
let posAfterEnd: number | undefined;
|
|
1590
1657
|
|
|
1591
1658
|
const segoff = this.getContainingSegment(pos);
|
|
1592
|
-
const seg = segoff
|
|
1659
|
+
const seg = segoff?.segment;
|
|
1593
1660
|
if (seg) {
|
|
1594
1661
|
posStart = this.getPosition(seg);
|
|
1595
1662
|
posAfterEnd = posStart + seg.cachedLength;
|
package/src/collections/list.ts
CHANGED
|
@@ -5,15 +5,46 @@
|
|
|
5
5
|
|
|
6
6
|
import { UsageError } from "@fluidframework/telemetry-utils/internal";
|
|
7
7
|
|
|
8
|
+
/**
|
|
9
|
+
* Represents a node in a doubly linked list.
|
|
10
|
+
* @internal
|
|
11
|
+
*/
|
|
8
12
|
export interface ListNode<T> {
|
|
13
|
+
/**
|
|
14
|
+
* The list this node belongs to, or undefined if not attached.
|
|
15
|
+
*/
|
|
9
16
|
readonly list: DoublyLinkedList<T> | undefined;
|
|
17
|
+
/**
|
|
18
|
+
* The data value stored in this node.
|
|
19
|
+
*/
|
|
10
20
|
readonly data: T;
|
|
21
|
+
/**
|
|
22
|
+
* The next node in the list, or undefined if this is the last node.
|
|
23
|
+
*/
|
|
11
24
|
readonly next: ListNode<T> | undefined;
|
|
25
|
+
/**
|
|
26
|
+
* The previous node in the list, or undefined if this is the first node.
|
|
27
|
+
*/
|
|
12
28
|
readonly prev: ListNode<T> | undefined;
|
|
29
|
+
/**
|
|
30
|
+
* Removes this node from its list.
|
|
31
|
+
* @returns The removed node, or undefined if not in a list.
|
|
32
|
+
*/
|
|
33
|
+
remove(): ListNode<T> | undefined;
|
|
13
34
|
}
|
|
14
35
|
|
|
36
|
+
/**
|
|
37
|
+
* Represents a range of nodes in a doubly linked list.
|
|
38
|
+
* @internal
|
|
39
|
+
*/
|
|
15
40
|
export interface ListNodeRange<T> {
|
|
41
|
+
/**
|
|
42
|
+
* The first node in the range.
|
|
43
|
+
*/
|
|
16
44
|
first: ListNode<T>;
|
|
45
|
+
/**
|
|
46
|
+
* The last node in the range.
|
|
47
|
+
*/
|
|
17
48
|
last: ListNode<T>;
|
|
18
49
|
}
|
|
19
50
|
|
|
@@ -50,6 +81,9 @@ class DataNode<T> extends HeadNode<T> implements ListNode<T> {
|
|
|
50
81
|
super(undefined);
|
|
51
82
|
this.headNode = headNode;
|
|
52
83
|
}
|
|
84
|
+
remove(): ListNode<T> | undefined {
|
|
85
|
+
return this.list?.remove(this);
|
|
86
|
+
}
|
|
53
87
|
}
|
|
54
88
|
|
|
55
89
|
function insertAfter<T>(node: DataNode<T> | HeadNode<T>, items: T[]): ListNodeRange<T> {
|
|
@@ -80,6 +114,11 @@ function insertAfter<T>(node: DataNode<T> | HeadNode<T>, items: T[]): ListNodeRa
|
|
|
80
114
|
return newRange;
|
|
81
115
|
}
|
|
82
116
|
|
|
117
|
+
/**
|
|
118
|
+
* A doubly linked list implementation with array-like methods and node access.
|
|
119
|
+
* @typeParam T - The type of data stored in the list nodes.
|
|
120
|
+
* @internal
|
|
121
|
+
*/
|
|
83
122
|
export class DoublyLinkedList<T>
|
|
84
123
|
implements
|
|
85
124
|
Iterable<ListNode<T>>,
|
|
@@ -87,12 +126,21 @@ export class DoublyLinkedList<T>
|
|
|
87
126
|
// try to match array signature and semantics where possible
|
|
88
127
|
Pick<ListNode<T>[], "pop" | "shift" | "length" | "includes">
|
|
89
128
|
{
|
|
129
|
+
/**
|
|
130
|
+
* Creates a new doubly linked list optionally initialized with values.
|
|
131
|
+
* @param values - Optional iterable of values to populate the list.
|
|
132
|
+
*/
|
|
90
133
|
constructor(values?: Iterable<T>) {
|
|
91
134
|
if (values !== undefined) {
|
|
92
135
|
this.push(...values);
|
|
93
136
|
}
|
|
94
137
|
}
|
|
95
138
|
|
|
139
|
+
/**
|
|
140
|
+
* Finds the first node matching the predicate.
|
|
141
|
+
* @param predicate - Function to test each node.
|
|
142
|
+
* @returns The first matching node, or undefined if none found.
|
|
143
|
+
*/
|
|
96
144
|
find(
|
|
97
145
|
predicate: (value: ListNode<T>, obj: DoublyLinkedList<T>) => unknown,
|
|
98
146
|
): ListNode<T> | undefined {
|
|
@@ -106,6 +154,10 @@ export class DoublyLinkedList<T>
|
|
|
106
154
|
return found;
|
|
107
155
|
}
|
|
108
156
|
|
|
157
|
+
/**
|
|
158
|
+
* Returns an iterable that maps each node to a new value.
|
|
159
|
+
* @param callbackfn - Function to produce a new value for each node.
|
|
160
|
+
*/
|
|
109
161
|
map<U>(callbackfn: (value: ListNode<T>) => U): Iterable<U> {
|
|
110
162
|
let node = this.first;
|
|
111
163
|
const iterator: IterableIterator<U> = {
|
|
@@ -124,6 +176,12 @@ export class DoublyLinkedList<T>
|
|
|
124
176
|
return iterator;
|
|
125
177
|
}
|
|
126
178
|
|
|
179
|
+
/**
|
|
180
|
+
* Inserts items after the specified node.
|
|
181
|
+
* @param preceding - The node to insert after.
|
|
182
|
+
* @param items - Items to insert.
|
|
183
|
+
* @returns The range of newly inserted nodes.
|
|
184
|
+
*/
|
|
127
185
|
insertAfter(preceding: ListNode<T>, ...items: T[]): ListNodeRange<T> {
|
|
128
186
|
if (!this._includes(preceding)) {
|
|
129
187
|
throw new Error("preceding not in list");
|
|
@@ -132,10 +190,19 @@ export class DoublyLinkedList<T>
|
|
|
132
190
|
return insertAfter(preceding, items);
|
|
133
191
|
}
|
|
134
192
|
|
|
193
|
+
/**
|
|
194
|
+
* Removes and returns the last node in the list.
|
|
195
|
+
* @returns The removed node, or undefined if the list is empty.
|
|
196
|
+
*/
|
|
135
197
|
pop(): ListNode<T> | undefined {
|
|
136
198
|
return this.remove(this.last);
|
|
137
199
|
}
|
|
138
200
|
|
|
201
|
+
/**
|
|
202
|
+
* Appends items to the end of the list.
|
|
203
|
+
* @param items - Items to append.
|
|
204
|
+
* @returns The range of newly inserted nodes.
|
|
205
|
+
*/
|
|
139
206
|
push(...items: T[]): ListNodeRange<T> {
|
|
140
207
|
this._len += items.length;
|
|
141
208
|
const start = this.headNode._prev;
|
|
@@ -143,14 +210,17 @@ export class DoublyLinkedList<T>
|
|
|
143
210
|
}
|
|
144
211
|
|
|
145
212
|
/**
|
|
146
|
-
*
|
|
213
|
+
* Removes and returns the first node in the list.
|
|
214
|
+
* @returns The removed node, or undefined if the list is empty.
|
|
147
215
|
*/
|
|
148
216
|
shift(): ListNode<T> | undefined {
|
|
149
217
|
return this.remove(this.first);
|
|
150
218
|
}
|
|
151
219
|
|
|
152
220
|
/**
|
|
153
|
-
*
|
|
221
|
+
* Inserts items at the start of the list.
|
|
222
|
+
* @param items - Items to insert.
|
|
223
|
+
* @returns The range of newly inserted nodes.
|
|
154
224
|
*/
|
|
155
225
|
unshift(...items: T[]): ListNodeRange<T> {
|
|
156
226
|
this._len += items.length;
|
|
@@ -158,9 +228,10 @@ export class DoublyLinkedList<T>
|
|
|
158
228
|
}
|
|
159
229
|
|
|
160
230
|
/**
|
|
161
|
-
*
|
|
162
|
-
*
|
|
163
|
-
*
|
|
231
|
+
* Removes nodes starting at `start` until `end` or `count` is reached.
|
|
232
|
+
* @param start - The node to start removing from.
|
|
233
|
+
* @param countOrEnd - The number of nodes to remove or the end node.
|
|
234
|
+
* @returns A new list containing the removed nodes.
|
|
164
235
|
*/
|
|
165
236
|
splice(start: ListNode<T>, countOrEnd?: ListNode<T> | number): DoublyLinkedList<T> {
|
|
166
237
|
const newList = new DoublyLinkedList<T>();
|
|
@@ -187,6 +258,11 @@ export class DoublyLinkedList<T>
|
|
|
187
258
|
return newList;
|
|
188
259
|
}
|
|
189
260
|
|
|
261
|
+
/**
|
|
262
|
+
* Checks if the node is in this list.
|
|
263
|
+
* @param node - The node to check.
|
|
264
|
+
* @returns True if the node is in the list.
|
|
265
|
+
*/
|
|
190
266
|
public includes(node: ListNode<T> | undefined): node is ListNode<T> {
|
|
191
267
|
return this._includes(node);
|
|
192
268
|
}
|
|
@@ -207,6 +283,11 @@ export class DoublyLinkedList<T>
|
|
|
207
283
|
return undefined;
|
|
208
284
|
}
|
|
209
285
|
|
|
286
|
+
/**
|
|
287
|
+
* Removes the specified node from the list.
|
|
288
|
+
* @param node - The node to remove.
|
|
289
|
+
* @returns The removed node, or undefined if not in the list.
|
|
290
|
+
*/
|
|
210
291
|
public remove(node: ListNode<T> | undefined): ListNode<T> | undefined {
|
|
211
292
|
return this._remove(node);
|
|
212
293
|
}
|
|
@@ -231,16 +312,31 @@ export class DoublyLinkedList<T>
|
|
|
231
312
|
|
|
232
313
|
private _len: number = 0;
|
|
233
314
|
private readonly headNode: HeadNode<T> | DataNode<T> = new HeadNode(this);
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* The number of nodes in the list.
|
|
318
|
+
*/
|
|
234
319
|
public get length(): number {
|
|
235
320
|
return this._len;
|
|
236
321
|
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Whether the list is empty.
|
|
325
|
+
*/
|
|
237
326
|
public get empty(): boolean {
|
|
238
327
|
return this._len === 0;
|
|
239
328
|
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* The first node in the list, or undefined if empty.
|
|
332
|
+
*/
|
|
240
333
|
public get first(): ListNode<T> | undefined {
|
|
241
334
|
return this.headNode.next;
|
|
242
335
|
}
|
|
243
336
|
|
|
337
|
+
/**
|
|
338
|
+
* The last node in the list, or undefined if empty.
|
|
339
|
+
*/
|
|
244
340
|
public get last(): ListNode<T> | undefined {
|
|
245
341
|
return this.headNode.prev;
|
|
246
342
|
}
|
package/src/constants.ts
CHANGED
|
@@ -34,3 +34,9 @@ export const LocalClientId = -1;
|
|
|
34
34
|
* @internal
|
|
35
35
|
*/
|
|
36
36
|
export const NonCollabClient = -2;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Used as the client id for operations that were squashed upon resubmission and should therefore
|
|
40
|
+
* never be seen by other clients.
|
|
41
|
+
*/
|
|
42
|
+
export const SquashClient = -3;
|
package/src/index.ts
CHANGED