@fluidframework/merge-tree 0.59.4001 → 1.1.0-75972
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/.eslintrc.js +1 -1
- package/README.md +1 -1
- package/REFERENCEPOSITIONS.md +199 -0
- package/dist/client.d.ts +30 -4
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +89 -47
- package/dist/client.js.map +1 -1
- package/dist/collections.d.ts +5 -4
- package/dist/collections.d.ts.map +1 -1
- package/dist/collections.js +17 -18
- package/dist/collections.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -1
- package/dist/index.js.map +1 -1
- package/dist/localReference.d.ts +11 -3
- package/dist/localReference.d.ts.map +1 -1
- package/dist/localReference.js +25 -8
- package/dist/localReference.js.map +1 -1
- package/dist/mergeTree.d.ts +23 -3
- package/dist/mergeTree.d.ts.map +1 -1
- package/dist/mergeTree.js +136 -48
- package/dist/mergeTree.js.map +1 -1
- package/dist/mergeTreeDeltaCallback.d.ts +8 -10
- package/dist/mergeTreeDeltaCallback.d.ts.map +1 -1
- package/dist/mergeTreeDeltaCallback.js +6 -10
- package/dist/mergeTreeDeltaCallback.js.map +1 -1
- package/dist/opBuilder.js +6 -5
- package/dist/opBuilder.js.map +1 -1
- package/dist/ops.d.ts +12 -10
- package/dist/ops.d.ts.map +1 -1
- package/dist/ops.js +7 -7
- package/dist/ops.js.map +1 -1
- package/dist/referencePositions.d.ts +1 -1
- package/dist/referencePositions.d.ts.map +1 -1
- package/dist/referencePositions.js +3 -2
- package/dist/referencePositions.js.map +1 -1
- package/lib/client.d.ts +30 -4
- package/lib/client.d.ts.map +1 -1
- package/lib/client.js +89 -47
- package/lib/client.js.map +1 -1
- package/lib/collections.d.ts +5 -4
- package/lib/collections.d.ts.map +1 -1
- package/lib/collections.js +17 -18
- package/lib/collections.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/localReference.d.ts +11 -3
- package/lib/localReference.d.ts.map +1 -1
- package/lib/localReference.js +23 -7
- package/lib/localReference.js.map +1 -1
- package/lib/mergeTree.d.ts +23 -3
- package/lib/mergeTree.d.ts.map +1 -1
- package/lib/mergeTree.js +137 -49
- package/lib/mergeTree.js.map +1 -1
- package/lib/mergeTreeDeltaCallback.d.ts +8 -10
- package/lib/mergeTreeDeltaCallback.d.ts.map +1 -1
- package/lib/mergeTreeDeltaCallback.js +6 -10
- package/lib/mergeTreeDeltaCallback.js.map +1 -1
- package/lib/opBuilder.js +6 -5
- package/lib/opBuilder.js.map +1 -1
- package/lib/ops.d.ts +12 -10
- package/lib/ops.d.ts.map +1 -1
- package/lib/ops.js +7 -7
- package/lib/ops.js.map +1 -1
- package/lib/referencePositions.d.ts +1 -1
- package/lib/referencePositions.d.ts.map +1 -1
- package/lib/referencePositions.js +3 -2
- package/lib/referencePositions.js.map +1 -1
- package/package.json +17 -94
- package/src/client.ts +87 -27
- package/src/collections.ts +5 -4
- package/src/index.ts +1 -1
- package/src/localReference.ts +33 -8
- package/src/mergeTree.ts +136 -43
- package/src/mergeTreeDeltaCallback.ts +8 -10
- package/src/ops.ts +13 -10
- package/src/referencePositions.ts +3 -2
package/src/client.ts
CHANGED
|
@@ -16,7 +16,7 @@ import { LoggingError } from "@fluidframework/telemetry-utils";
|
|
|
16
16
|
import { IIntegerRange } from "./base";
|
|
17
17
|
import { RedBlackTree } from "./collections";
|
|
18
18
|
import { UnassignedSequenceNumber, UniversalSequenceNumber } from "./constants";
|
|
19
|
-
import { LocalReference } from "./localReference";
|
|
19
|
+
import { LocalReference, LocalReferencePosition } from "./localReference";
|
|
20
20
|
import {
|
|
21
21
|
CollaborationWindow,
|
|
22
22
|
compareStrings,
|
|
@@ -53,7 +53,7 @@ import { SnapshotLegacy } from "./snapshotlegacy";
|
|
|
53
53
|
import { SnapshotLoader } from "./snapshotLoader";
|
|
54
54
|
import { MergeTreeTextHelper } from "./textSegment";
|
|
55
55
|
import { SnapshotV1 } from "./snapshotV1";
|
|
56
|
-
import { ReferencePosition, RangeStackMap
|
|
56
|
+
import { ReferencePosition, RangeStackMap } from "./referencePositions";
|
|
57
57
|
import {
|
|
58
58
|
IMergeTreeClientSequenceArgs,
|
|
59
59
|
IMergeTreeDeltaOpArgs,
|
|
@@ -333,21 +333,17 @@ export class Client {
|
|
|
333
333
|
}
|
|
334
334
|
|
|
335
335
|
public createLocalReferencePosition(
|
|
336
|
-
segment: ISegment, offset: number, refType: ReferenceType, properties: PropertySet | undefined,
|
|
337
|
-
):
|
|
336
|
+
segment: ISegment, offset: number | undefined, refType: ReferenceType, properties: PropertySet | undefined,
|
|
337
|
+
): LocalReferencePosition {
|
|
338
338
|
return this.mergeTree.createLocalReferencePosition(segment, offset, refType, properties, this);
|
|
339
339
|
}
|
|
340
340
|
|
|
341
|
-
public removeLocalReferencePosition(lref:
|
|
341
|
+
public removeLocalReferencePosition(lref: LocalReferencePosition) {
|
|
342
342
|
return this.mergeTree.removeLocalReferencePosition(lref);
|
|
343
343
|
}
|
|
344
344
|
|
|
345
345
|
public localReferencePositionToPosition(lref: ReferencePosition) {
|
|
346
|
-
|
|
347
|
-
if (segment === undefined) {
|
|
348
|
-
return DetachedReferencePosition;
|
|
349
|
-
}
|
|
350
|
-
return this.getPosition(segment) + lref.getOffset();
|
|
346
|
+
return this.mergeTree.referencePositionToLocalPosition(lref);
|
|
351
347
|
}
|
|
352
348
|
|
|
353
349
|
/**
|
|
@@ -575,13 +571,14 @@ export class Client {
|
|
|
575
571
|
|
|
576
572
|
/**
|
|
577
573
|
* Gets the client args from the op if remote, otherwise uses the local clients info
|
|
578
|
-
* @param
|
|
574
|
+
* @param sequencedMessage - The sequencedMessage to get the client sequence args for
|
|
579
575
|
*/
|
|
580
|
-
|
|
576
|
+
private getClientSequenceArgsForMessage(sequencedMessage: ISequencedDocumentMessage | undefined):
|
|
577
|
+
IMergeTreeClientSequenceArgs {
|
|
581
578
|
// If there this no sequenced message, then the op is local
|
|
582
579
|
// and unacked, so use this clients sequenced args
|
|
583
580
|
//
|
|
584
|
-
if (!
|
|
581
|
+
if (!sequencedMessage) {
|
|
585
582
|
const segWindow = this.getCollabWindow();
|
|
586
583
|
return {
|
|
587
584
|
clientId: segWindow.clientId,
|
|
@@ -590,13 +587,21 @@ export class Client {
|
|
|
590
587
|
};
|
|
591
588
|
} else {
|
|
592
589
|
return {
|
|
593
|
-
clientId: this.
|
|
594
|
-
referenceSequenceNumber:
|
|
595
|
-
sequenceNumber:
|
|
590
|
+
clientId: this.getOrAddShortClientId(sequencedMessage.clientId),
|
|
591
|
+
referenceSequenceNumber: sequencedMessage.referenceSequenceNumber,
|
|
592
|
+
sequenceNumber: sequencedMessage.sequenceNumber,
|
|
596
593
|
};
|
|
597
594
|
}
|
|
598
595
|
}
|
|
599
596
|
|
|
597
|
+
/**
|
|
598
|
+
* Gets the client args from the op if remote, otherwise uses the local clients info
|
|
599
|
+
* @param opArgs - The op arg to get the client sequence args for
|
|
600
|
+
*/
|
|
601
|
+
private getClientSequenceArgs(opArgs: IMergeTreeDeltaOpArgs): IMergeTreeClientSequenceArgs {
|
|
602
|
+
return this.getClientSequenceArgsForMessage(opArgs.sequencedMessage);
|
|
603
|
+
}
|
|
604
|
+
|
|
600
605
|
private ackPendingSegment(opArgs: IMergeTreeDeltaOpArgs) {
|
|
601
606
|
const ackOp = (deltaOpArgs: IMergeTreeDeltaOpArgs) => {
|
|
602
607
|
let trace: Trace | undefined;
|
|
@@ -704,6 +709,61 @@ export class Client {
|
|
|
704
709
|
return segmentPosition;
|
|
705
710
|
}
|
|
706
711
|
|
|
712
|
+
/**
|
|
713
|
+
* Rebases a (local) position from the perspective `{ seq: seqNumberFrom, localSeq }` to the perspective
|
|
714
|
+
* of the current sequence number. This is desirable when rebasing operations for reconnection.
|
|
715
|
+
*
|
|
716
|
+
* If the position refers to a segment/offset that was removed by some operation between `seqNumberFrom` and
|
|
717
|
+
* the current sequence number, the returned position will align with the position of a reference given
|
|
718
|
+
* `SlideOnRemove` semantics.
|
|
719
|
+
*/
|
|
720
|
+
public rebasePosition(
|
|
721
|
+
pos: number,
|
|
722
|
+
seqNumberFrom: number,
|
|
723
|
+
localSeq: number,
|
|
724
|
+
): number {
|
|
725
|
+
assert(localSeq <= this.mergeTree.collabWindow.localSeq, 0x300 /* localSeq greater than collab window */);
|
|
726
|
+
let segment: ISegment | undefined;
|
|
727
|
+
let posAccumulated = 0;
|
|
728
|
+
let offset = pos;
|
|
729
|
+
const isInsertedInView = (seg: ISegment) =>
|
|
730
|
+
(seg.seq !== undefined && seg.seq !== UnassignedSequenceNumber && seg.seq <= seqNumberFrom)
|
|
731
|
+
|| (seg.localSeq !== undefined && seg.localSeq <= localSeq);
|
|
732
|
+
|
|
733
|
+
const isRemovedFromView = ({ removedSeq, localRemovedSeq }: ISegment) =>
|
|
734
|
+
(removedSeq !== undefined && removedSeq !== UnassignedSequenceNumber && removedSeq <= seqNumberFrom)
|
|
735
|
+
|| (localRemovedSeq !== undefined && localRemovedSeq <= localSeq);
|
|
736
|
+
|
|
737
|
+
this.mergeTree.walkAllSegments(this.mergeTree.root, (seg) => {
|
|
738
|
+
assert(seg.seq !== undefined || seg.localSeq !== undefined,
|
|
739
|
+
0x301 /* Either seq or localSeq should be defined */);
|
|
740
|
+
segment = seg;
|
|
741
|
+
|
|
742
|
+
if (isInsertedInView(seg) && !isRemovedFromView(seg)) {
|
|
743
|
+
posAccumulated += seg.cachedLength;
|
|
744
|
+
if (offset >= seg.cachedLength) {
|
|
745
|
+
offset -= seg.cachedLength;
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
// Keep going while we've yet to reach the segment at the desired position
|
|
750
|
+
return posAccumulated <= pos;
|
|
751
|
+
});
|
|
752
|
+
|
|
753
|
+
assert(segment !== undefined, 0x302 /* No segment found */);
|
|
754
|
+
const seqNumberTo = this.getCollabWindow().currentSeq;
|
|
755
|
+
if ((segment.removedSeq !== undefined &&
|
|
756
|
+
segment.removedSeq !== UnassignedSequenceNumber &&
|
|
757
|
+
segment.removedSeq <= seqNumberTo)
|
|
758
|
+
|| (segment.localRemovedSeq !== undefined && segment.localRemovedSeq <= localSeq)) {
|
|
759
|
+
// Segment that the position was in has been removed: null out offset.
|
|
760
|
+
offset = 0;
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
assert(0 <= offset && offset < segment.cachedLength, 0x303 /* Invalid offset */);
|
|
764
|
+
return this.findReconnectionPosition(segment, localSeq) + offset;
|
|
765
|
+
}
|
|
766
|
+
|
|
707
767
|
private resetPendingDeltaToOps(
|
|
708
768
|
resetOp: IMergeTreeDeltaOp,
|
|
709
769
|
segmentGroup: SegmentGroup): IMergeTreeDeltaOp[] {
|
|
@@ -1021,17 +1081,17 @@ export class Client {
|
|
|
1021
1081
|
}
|
|
1022
1082
|
|
|
1023
1083
|
getContainingSegment<T extends ISegment>(pos: number, op?: ISequencedDocumentMessage) {
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
return this.mergeTree.
|
|
1084
|
+
const args = this.getClientSequenceArgsForMessage(op);
|
|
1085
|
+
return this.mergeTree.getContainingSegment<T>(pos, args.referenceSequenceNumber, args.clientId);
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
/**
|
|
1089
|
+
* Returns the position to slide a reference to if a slide is required.
|
|
1090
|
+
* @param segoff - The segment and offset to slide from
|
|
1091
|
+
* @returns - segment and offset to slide the reference to
|
|
1092
|
+
*/
|
|
1093
|
+
getSlideToSegment(segoff: { segment: ISegment | undefined; offset: number | undefined; }) {
|
|
1094
|
+
return this.mergeTree._getSlideToSegment(segoff);
|
|
1035
1095
|
}
|
|
1036
1096
|
|
|
1037
1097
|
getPropertiesAtPosition(pos: number) {
|
package/src/collections.ts
CHANGED
|
@@ -245,10 +245,11 @@ export class Heap<T> {
|
|
|
245
245
|
/* eslint-enable no-bitwise */
|
|
246
246
|
}
|
|
247
247
|
|
|
248
|
-
export const
|
|
249
|
-
RED,
|
|
250
|
-
BLACK,
|
|
251
|
-
}
|
|
248
|
+
export const RBColor = {
|
|
249
|
+
RED: 0,
|
|
250
|
+
BLACK: 1,
|
|
251
|
+
} as const;
|
|
252
|
+
export type RBColor = typeof RBColor[keyof typeof RBColor];
|
|
252
253
|
|
|
253
254
|
export interface RBNode<TKey, TData> {
|
|
254
255
|
key: TKey;
|
package/src/index.ts
CHANGED
|
@@ -7,7 +7,7 @@ export * from "./base";
|
|
|
7
7
|
export * from "./client";
|
|
8
8
|
export * from "./collections";
|
|
9
9
|
export * from "./constants";
|
|
10
|
-
export
|
|
10
|
+
export { LocalReference, LocalReferencePosition, LocalReferenceCollection } from "./localReference";
|
|
11
11
|
export * from "./mergeTree";
|
|
12
12
|
export * from "./mergeTreeDeltaCallback";
|
|
13
13
|
export * from "./mergeTreeTracking";
|
package/src/localReference.ts
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { assert } from "@fluidframework/common-utils";
|
|
7
|
+
import { UsageError } from "@fluidframework/container-utils";
|
|
7
8
|
import { Client } from "./client";
|
|
8
9
|
import { List, ListMakeHead, ListRemoveEntry } from "./collections";
|
|
9
10
|
import {
|
|
@@ -26,9 +27,33 @@ import {
|
|
|
26
27
|
} from "./referencePositions";
|
|
27
28
|
|
|
28
29
|
/**
|
|
29
|
-
* @
|
|
30
|
+
* @internal
|
|
30
31
|
*/
|
|
31
|
-
export
|
|
32
|
+
export function _validateReferenceType(refType: ReferenceType) {
|
|
33
|
+
let exclusiveCount = 0;
|
|
34
|
+
if (refTypeIncludesFlag(refType, ReferenceType.Transient)) {
|
|
35
|
+
++exclusiveCount;
|
|
36
|
+
}
|
|
37
|
+
if (refTypeIncludesFlag(refType, ReferenceType.SlideOnRemove)) {
|
|
38
|
+
++exclusiveCount;
|
|
39
|
+
}
|
|
40
|
+
if (refTypeIncludesFlag(refType, ReferenceType.StayOnRemove)) {
|
|
41
|
+
++exclusiveCount;
|
|
42
|
+
}
|
|
43
|
+
if (exclusiveCount > 1) {
|
|
44
|
+
throw new UsageError(
|
|
45
|
+
"Reference types can only be one of Transient, SlideOnRemove, and StayOnRemove");
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface LocalReferencePosition extends ReferencePosition {
|
|
50
|
+
callbacks?: Partial<Record<"beforeSlide" | "afterSlide", () => void>>;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* @deprecated - Use LocalReferencePosition
|
|
55
|
+
*/
|
|
56
|
+
export class LocalReference implements LocalReferencePosition {
|
|
32
57
|
/**
|
|
33
58
|
* @deprecated - use DetachedReferencePosition
|
|
34
59
|
*/
|
|
@@ -44,6 +69,8 @@ export class LocalReference implements ReferencePosition {
|
|
|
44
69
|
*/
|
|
45
70
|
public segment: ISegment | undefined;
|
|
46
71
|
|
|
72
|
+
public callbacks?: Partial<Record<"beforeSlide" | "afterSlide", () => void>> | undefined;
|
|
73
|
+
|
|
47
74
|
/**
|
|
48
75
|
* @deprecated - use createReferencePosition
|
|
49
76
|
*/
|
|
@@ -57,6 +84,7 @@ export class LocalReference implements ReferencePosition {
|
|
|
57
84
|
public refType = ReferenceType.Simple,
|
|
58
85
|
properties?: PropertySet,
|
|
59
86
|
) {
|
|
87
|
+
_validateReferenceType(refType);
|
|
60
88
|
this.segment = initSegment;
|
|
61
89
|
this.properties = properties;
|
|
62
90
|
}
|
|
@@ -144,9 +172,6 @@ export class LocalReference implements ReferencePosition {
|
|
|
144
172
|
}
|
|
145
173
|
|
|
146
174
|
public getOffset() {
|
|
147
|
-
if (this.segment?.removedSeq) {
|
|
148
|
-
return 0;
|
|
149
|
-
}
|
|
150
175
|
return this.offset;
|
|
151
176
|
}
|
|
152
177
|
|
|
@@ -286,7 +311,7 @@ export class LocalReferenceCollection {
|
|
|
286
311
|
* @internal - this method should only be called by mergeTree
|
|
287
312
|
*/
|
|
288
313
|
public createLocalRef(
|
|
289
|
-
offset: number,
|
|
314
|
+
offset: number | undefined,
|
|
290
315
|
refType: ReferenceType,
|
|
291
316
|
properties: PropertySet | undefined,
|
|
292
317
|
client: Client): ReferencePosition {
|
|
@@ -312,8 +337,8 @@ export class LocalReferenceCollection {
|
|
|
312
337
|
!refTypeIncludesFlag(lref, ReferenceType.Transient),
|
|
313
338
|
0x2df /* "transient references cannot be bound to segments" */);
|
|
314
339
|
assertLocalReferences(lref);
|
|
315
|
-
const refsAtOffset = this.refsByOffset[lref.
|
|
316
|
-
this.refsByOffset[lref.
|
|
340
|
+
const refsAtOffset = this.refsByOffset[lref.offset] =
|
|
341
|
+
this.refsByOffset[lref.offset]
|
|
317
342
|
?? { at: ListMakeHead() };
|
|
318
343
|
const atRefs = refsAtOffset.at =
|
|
319
344
|
refsAtOffset.at
|
package/src/mergeTree.ts
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
/* eslint-disable @typescript-eslint/prefer-optional-chain, no-bitwise */
|
|
10
10
|
|
|
11
11
|
import { assert } from "@fluidframework/common-utils";
|
|
12
|
+
import { UsageError } from "@fluidframework/container-utils";
|
|
12
13
|
import {
|
|
13
14
|
Comparer,
|
|
14
15
|
Heap,
|
|
@@ -23,7 +24,7 @@ import {
|
|
|
23
24
|
UnassignedSequenceNumber,
|
|
24
25
|
UniversalSequenceNumber,
|
|
25
26
|
} from "./constants";
|
|
26
|
-
import { LocalReference, LocalReferenceCollection } from "./localReference";
|
|
27
|
+
import { LocalReference, LocalReferenceCollection, LocalReferencePosition } from "./localReference";
|
|
27
28
|
import {
|
|
28
29
|
IMergeTreeDeltaOpArgs,
|
|
29
30
|
IMergeTreeSegmentDelta,
|
|
@@ -109,6 +110,15 @@ export function toRemovalInfo(maybe: Partial<IRemovalInfo> | undefined): IRemova
|
|
|
109
110
|
0x2bf /* "both removedClientIds and removedSeq should be set or not set" */);
|
|
110
111
|
}
|
|
111
112
|
|
|
113
|
+
function isRemoved(segment: ISegment): boolean {
|
|
114
|
+
return toRemovalInfo(segment) !== undefined;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function isRemovedAndAcked(segment: ISegment): boolean {
|
|
118
|
+
const removalInfo = toRemovalInfo(segment);
|
|
119
|
+
return removalInfo !== undefined && removalInfo.removedSeq !== UnassignedSequenceNumber;
|
|
120
|
+
}
|
|
121
|
+
|
|
112
122
|
/**
|
|
113
123
|
* A segment representing a portion of the merge tree.
|
|
114
124
|
*/
|
|
@@ -554,7 +564,6 @@ export abstract class BaseSegment extends MergeNode implements ISegment {
|
|
|
554
564
|
return true;
|
|
555
565
|
|
|
556
566
|
case MergeTreeDeltaType.REMOVE:
|
|
557
|
-
|
|
558
567
|
const removalInfo: IRemovalInfo | undefined = toRemovalInfo(this);
|
|
559
568
|
assert(removalInfo !== undefined, 0x046 /* "On remove ack, missing removal info!" */);
|
|
560
569
|
this.localRemovedSeq = undefined;
|
|
@@ -562,7 +571,6 @@ export abstract class BaseSegment extends MergeNode implements ISegment {
|
|
|
562
571
|
removalInfo.removedSeq = opArgs.sequencedMessage!.sequenceNumber;
|
|
563
572
|
return true;
|
|
564
573
|
}
|
|
565
|
-
|
|
566
574
|
return false;
|
|
567
575
|
|
|
568
576
|
default:
|
|
@@ -1414,6 +1422,110 @@ export class MergeTree {
|
|
|
1414
1422
|
return { segment, offset };
|
|
1415
1423
|
}
|
|
1416
1424
|
|
|
1425
|
+
/**
|
|
1426
|
+
* @internal must only be used by client
|
|
1427
|
+
* @param segoff - The segment and offset to slide from
|
|
1428
|
+
* @returns The segment and offset to slide to
|
|
1429
|
+
*/
|
|
1430
|
+
public _getSlideToSegment(segoff: { segment: ISegment | undefined; offset: number | undefined; }) {
|
|
1431
|
+
if (!segoff.segment || !isRemovedAndAcked(segoff.segment)) {
|
|
1432
|
+
return segoff;
|
|
1433
|
+
}
|
|
1434
|
+
let slideToSegment: ISegment | undefined;
|
|
1435
|
+
const goFurtherToFindSlideToSegment = (seg) => {
|
|
1436
|
+
if (seg.seq !== UnassignedSequenceNumber && !isRemovedAndAcked(seg)) {
|
|
1437
|
+
slideToSegment = seg;
|
|
1438
|
+
return false;
|
|
1439
|
+
}
|
|
1440
|
+
return true;
|
|
1441
|
+
};
|
|
1442
|
+
// Slide to the next farthest valid segment in the tree.
|
|
1443
|
+
this.rightExcursion(segoff.segment, goFurtherToFindSlideToSegment);
|
|
1444
|
+
if (slideToSegment) {
|
|
1445
|
+
return { segment: slideToSegment, offset: 0 };
|
|
1446
|
+
}
|
|
1447
|
+
// If no such segment is found, slide to the last valid segment.
|
|
1448
|
+
this.leftExcursion(segoff.segment, goFurtherToFindSlideToSegment);
|
|
1449
|
+
|
|
1450
|
+
// Workaround TypeScript issue (https://github.com/microsoft/TypeScript/issues/9998)
|
|
1451
|
+
slideToSegment = slideToSegment as ISegment | undefined;
|
|
1452
|
+
|
|
1453
|
+
if (slideToSegment) {
|
|
1454
|
+
// If slid nearer then offset should be at the end of the segment
|
|
1455
|
+
return { segment: slideToSegment, offset: slideToSegment.cachedLength - 1 };
|
|
1456
|
+
}
|
|
1457
|
+
|
|
1458
|
+
return { segment: undefined, offset: 0 };
|
|
1459
|
+
}
|
|
1460
|
+
|
|
1461
|
+
/**
|
|
1462
|
+
* This method should only be called when the current client sequence number is
|
|
1463
|
+
* max(remove segment sequence number, add reference sequence number).
|
|
1464
|
+
* Otherwise eventual consistency is not guaranteed.
|
|
1465
|
+
* See `packages\dds\merge-tree\REFERENCEPOSITIONS.md`
|
|
1466
|
+
*/
|
|
1467
|
+
private slideReferences(segment: ISegment, refsToSlide: LocalReference[]) {
|
|
1468
|
+
assert(
|
|
1469
|
+
isRemovedAndAcked(segment),
|
|
1470
|
+
0x2f1 /* slideReferences from a segment which has not been removed and acked */);
|
|
1471
|
+
assert(!!segment.localRefs, 0x2f2 /* Ref not in the segment localRefs */);
|
|
1472
|
+
const newSegoff = this._getSlideToSegment({ segment, offset: 0 });
|
|
1473
|
+
const newSegment = newSegoff.segment;
|
|
1474
|
+
if (newSegment && !newSegment.localRefs) {
|
|
1475
|
+
newSegment.localRefs = new LocalReferenceCollection(newSegment);
|
|
1476
|
+
}
|
|
1477
|
+
for (const ref of refsToSlide) {
|
|
1478
|
+
ref.callbacks?.beforeSlide?.();
|
|
1479
|
+
const removedRef = segment.localRefs.removeLocalRef(ref);
|
|
1480
|
+
assert(ref === removedRef, 0x2f3 /* Ref not in the segment localRefs */);
|
|
1481
|
+
if (!newSegment) {
|
|
1482
|
+
// No valid segments (all nodes removed or not yet created)
|
|
1483
|
+
ref.segment = undefined;
|
|
1484
|
+
ref.offset = 0;
|
|
1485
|
+
} else {
|
|
1486
|
+
ref.segment = newSegment;
|
|
1487
|
+
ref.offset = newSegoff.offset ?? 0;
|
|
1488
|
+
assert(!!newSegment.localRefs, 0x2f4 /* localRefs must be allocated */);
|
|
1489
|
+
newSegment.localRefs.addLocalRef(ref);
|
|
1490
|
+
}
|
|
1491
|
+
ref.callbacks?.afterSlide?.();
|
|
1492
|
+
}
|
|
1493
|
+
// TODO is it required to update the path lengths?
|
|
1494
|
+
if (newSegment) {
|
|
1495
|
+
this.blockUpdatePathLengths(newSegment.parent, TreeMaintenanceSequenceNumber,
|
|
1496
|
+
LocalClientId);
|
|
1497
|
+
}
|
|
1498
|
+
}
|
|
1499
|
+
|
|
1500
|
+
private updateSegmentRefsAfterMarkRemoved(segment: ISegment, pending: boolean) {
|
|
1501
|
+
if (!segment.localRefs || segment.localRefs.empty) {
|
|
1502
|
+
return;
|
|
1503
|
+
}
|
|
1504
|
+
const refsToSlide: LocalReference[] = [];
|
|
1505
|
+
const refsToStay: LocalReference[] = [];
|
|
1506
|
+
for (const lref of segment.localRefs) {
|
|
1507
|
+
if (refTypeIncludesFlag(lref, ReferenceType.StayOnRemove)) {
|
|
1508
|
+
refsToStay.push(lref);
|
|
1509
|
+
} else if (refTypeIncludesFlag(lref, ReferenceType.SlideOnRemove)) {
|
|
1510
|
+
if (pending) {
|
|
1511
|
+
refsToStay.push(lref);
|
|
1512
|
+
} else {
|
|
1513
|
+
refsToSlide.push(lref);
|
|
1514
|
+
}
|
|
1515
|
+
}
|
|
1516
|
+
}
|
|
1517
|
+
// Rethink implementation of keeping and sliding refs once other reference
|
|
1518
|
+
// changes are complete. This works but is fragile and possibly slow.
|
|
1519
|
+
if (!pending) {
|
|
1520
|
+
this.slideReferences(segment, refsToSlide);
|
|
1521
|
+
}
|
|
1522
|
+
segment.localRefs.clear();
|
|
1523
|
+
for (const lref of refsToStay) {
|
|
1524
|
+
lref.segment = segment;
|
|
1525
|
+
segment.localRefs.addLocalRef(lref);
|
|
1526
|
+
}
|
|
1527
|
+
}
|
|
1528
|
+
|
|
1417
1529
|
private blockLength(node: IMergeBlock, refSeq: number, clientId: number) {
|
|
1418
1530
|
if ((this.collabWindow.collaborating) && (clientId !== this.collabWindow.clientId)) {
|
|
1419
1531
|
return node.partialLengths!.getPartialLength(refSeq, clientId);
|
|
@@ -1676,7 +1788,12 @@ export class MergeTree {
|
|
|
1676
1788
|
if (pendingSegmentGroup !== undefined) {
|
|
1677
1789
|
const deltaSegments: IMergeTreeSegmentDelta[] = [];
|
|
1678
1790
|
pendingSegmentGroup.segments.map((pendingSegment) => {
|
|
1679
|
-
|
|
1791
|
+
const overlappingRemove = !pendingSegment.ack(pendingSegmentGroup, opArgs, this);
|
|
1792
|
+
overwrite = overlappingRemove || overwrite;
|
|
1793
|
+
|
|
1794
|
+
if (!overlappingRemove && opArgs.op.type === MergeTreeDeltaType.REMOVE) {
|
|
1795
|
+
this.updateSegmentRefsAfterMarkRemoved(pendingSegment, false);
|
|
1796
|
+
}
|
|
1680
1797
|
if (MergeTree.options.zamboniSegments) {
|
|
1681
1798
|
this.addToLRUSet(pendingSegment, seq);
|
|
1682
1799
|
}
|
|
@@ -2338,7 +2455,7 @@ export class MergeTree {
|
|
|
2338
2455
|
this.ensureIntervalBoundary(end, refSeq, clientId);
|
|
2339
2456
|
let segmentGroup: SegmentGroup;
|
|
2340
2457
|
const removedSegments: IMergeTreeSegmentDelta[] = [];
|
|
2341
|
-
const
|
|
2458
|
+
const segmentsWithRefs: ISegment[] = [];
|
|
2342
2459
|
const localSeq = seq === UnassignedSequenceNumber ? ++this.collabWindow.localSeq : undefined;
|
|
2343
2460
|
const markRemoved = (segment: ISegment, pos: number, _start: number, _end: number) => {
|
|
2344
2461
|
const existingRemovalInfo = toRemovalInfo(segment);
|
|
@@ -2362,10 +2479,9 @@ export class MergeTree {
|
|
|
2362
2479
|
segment.localRemovedSeq = localSeq;
|
|
2363
2480
|
|
|
2364
2481
|
removedSegments.push({ segment });
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
segment.localRefs = undefined;
|
|
2482
|
+
}
|
|
2483
|
+
if (segment.localRefs && !segment.localRefs.empty) {
|
|
2484
|
+
segmentsWithRefs.push(segment);
|
|
2369
2485
|
}
|
|
2370
2486
|
|
|
2371
2487
|
// Save segment so can assign removed sequence number when acked by server
|
|
@@ -2389,37 +2505,9 @@ export class MergeTree {
|
|
|
2389
2505
|
return true;
|
|
2390
2506
|
};
|
|
2391
2507
|
this.mapRange({ leaf: markRemoved, post: afterMarkRemoved }, refSeq, clientId, undefined, start, end);
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
if (start < length) {
|
|
2396
|
-
const afterSegOff = this.getContainingSegment(start, refSeq, clientId);
|
|
2397
|
-
refSegment = afterSegOff.segment;
|
|
2398
|
-
assert(!!refSegment, 0x052 /* "Missing reference segment!" */);
|
|
2399
|
-
if (!refSegment.localRefs) {
|
|
2400
|
-
refSegment.localRefs = new LocalReferenceCollection(refSegment);
|
|
2401
|
-
}
|
|
2402
|
-
refSegment.localRefs.addBeforeTombstones(...savedLocalRefs);
|
|
2403
|
-
} else if (length > 0) {
|
|
2404
|
-
const beforeSegOff = this.getContainingSegment(length - 1, refSeq, clientId);
|
|
2405
|
-
refSegment = beforeSegOff.segment;
|
|
2406
|
-
assert(!!refSegment, 0x053 /* "Missing reference segment!" */);
|
|
2407
|
-
if (!refSegment.localRefs) {
|
|
2408
|
-
refSegment.localRefs = new LocalReferenceCollection(refSegment);
|
|
2409
|
-
}
|
|
2410
|
-
refSegment.localRefs.addAfterTombstones(...savedLocalRefs);
|
|
2411
|
-
} else {
|
|
2412
|
-
// TODO: The tree is empty, so there isn't anywhere to put these
|
|
2413
|
-
// they should be preserved somehow
|
|
2414
|
-
for (const refsCollection of savedLocalRefs) {
|
|
2415
|
-
refsCollection.clear();
|
|
2416
|
-
}
|
|
2417
|
-
}
|
|
2418
|
-
|
|
2419
|
-
if (refSegment) {
|
|
2420
|
-
this.blockUpdatePathLengths(refSegment.parent, TreeMaintenanceSequenceNumber,
|
|
2421
|
-
LocalClientId);
|
|
2422
|
-
}
|
|
2508
|
+
const pending = this.collabWindow.collaborating && clientId === this.collabWindow.clientId;
|
|
2509
|
+
for (const segment of segmentsWithRefs) {
|
|
2510
|
+
this.updateSegmentRefsAfterMarkRemoved(segment, pending);
|
|
2423
2511
|
}
|
|
2424
2512
|
|
|
2425
2513
|
// opArgs == undefined => test code
|
|
@@ -2445,7 +2533,7 @@ export class MergeTree {
|
|
|
2445
2533
|
}
|
|
2446
2534
|
}
|
|
2447
2535
|
|
|
2448
|
-
public removeLocalReferencePosition(lref:
|
|
2536
|
+
public removeLocalReferencePosition(lref: LocalReferencePosition): LocalReferencePosition | undefined {
|
|
2449
2537
|
const segment = lref.getSegment();
|
|
2450
2538
|
if (segment) {
|
|
2451
2539
|
const removedRefs = segment?.localRefs?.removeLocalRef(lref);
|
|
@@ -2457,9 +2545,14 @@ export class MergeTree {
|
|
|
2457
2545
|
}
|
|
2458
2546
|
}
|
|
2459
2547
|
public createLocalReferencePosition(
|
|
2460
|
-
segment: ISegment, offset: number, refType: ReferenceType, properties: PropertySet | undefined,
|
|
2548
|
+
segment: ISegment, offset: number | undefined, refType: ReferenceType, properties: PropertySet | undefined,
|
|
2461
2549
|
client: Client,
|
|
2462
|
-
):
|
|
2550
|
+
): LocalReferencePosition {
|
|
2551
|
+
if (isRemoved(segment)) {
|
|
2552
|
+
if (!refTypeIncludesFlag(refType, ReferenceType.SlideOnRemove)) {
|
|
2553
|
+
throw new UsageError("Can only create SlideOnRemove local reference position on a removed segment");
|
|
2554
|
+
}
|
|
2555
|
+
}
|
|
2463
2556
|
const localRefs = segment.localRefs ?? new LocalReferenceCollection(segment);
|
|
2464
2557
|
segment.localRefs = localRefs;
|
|
2465
2558
|
|
|
@@ -13,28 +13,26 @@ import { PropertySet } from "./properties";
|
|
|
13
13
|
import { ISegment } from "./mergeTree";
|
|
14
14
|
|
|
15
15
|
export type MergeTreeDeltaOperationType =
|
|
16
|
-
MergeTreeDeltaType.ANNOTATE | MergeTreeDeltaType.INSERT | MergeTreeDeltaType.REMOVE;
|
|
16
|
+
typeof MergeTreeDeltaType.ANNOTATE | typeof MergeTreeDeltaType.INSERT | typeof MergeTreeDeltaType.REMOVE;
|
|
17
17
|
|
|
18
18
|
// Note: Assigned negative integers to avoid clashing with MergeTreeDeltaType
|
|
19
|
-
export const
|
|
20
|
-
APPEND
|
|
21
|
-
SPLIT
|
|
19
|
+
export const MergeTreeMaintenanceType = {
|
|
20
|
+
APPEND: -1,
|
|
21
|
+
SPLIT: -2,
|
|
22
22
|
/**
|
|
23
23
|
* Notification that a segment has been unlinked from the MergeTree. This occurs during
|
|
24
24
|
* Zamboni when:
|
|
25
25
|
*
|
|
26
|
-
* a) The minSeq has moved past the segment's removeSeq, in which case the segment
|
|
27
|
-
* can no longer be referenced by incoming remote ops, and...
|
|
28
|
-
*
|
|
29
26
|
* b) The segment's tracking collection is empty (e.g., not being tracked for undo/redo).
|
|
30
27
|
*/
|
|
31
|
-
UNLINK
|
|
28
|
+
UNLINK: -3,
|
|
32
29
|
/**
|
|
33
30
|
* Notification that a local change has been acknowledged by the server.
|
|
34
31
|
* This means that it has made the round trip to the server and has had a sequence number assigned.
|
|
35
32
|
*/
|
|
36
|
-
ACKNOWLEDGED
|
|
37
|
-
}
|
|
33
|
+
ACKNOWLEDGED: -4,
|
|
34
|
+
} as const;
|
|
35
|
+
export type MergeTreeMaintenanceType = typeof MergeTreeMaintenanceType[keyof typeof MergeTreeMaintenanceType];
|
|
38
36
|
|
|
39
37
|
export type MergeTreeDeltaOperationTypes = MergeTreeDeltaOperationType | MergeTreeMaintenanceType;
|
|
40
38
|
|
package/src/ops.ts
CHANGED
|
@@ -11,6 +11,7 @@ export enum ReferenceType {
|
|
|
11
11
|
RangeBegin = 0x10,
|
|
12
12
|
RangeEnd = 0x20,
|
|
13
13
|
SlideOnRemove = 0x40,
|
|
14
|
+
StayOnRemove = 0x80,
|
|
14
15
|
Transient = 0x100,
|
|
15
16
|
}
|
|
16
17
|
|
|
@@ -19,12 +20,14 @@ export interface IMarkerDef {
|
|
|
19
20
|
}
|
|
20
21
|
|
|
21
22
|
// Note: Assigned positive integers to avoid clashing with MergeTreeMaintenanceType
|
|
22
|
-
export const
|
|
23
|
-
INSERT
|
|
24
|
-
REMOVE
|
|
25
|
-
ANNOTATE
|
|
26
|
-
GROUP
|
|
27
|
-
}
|
|
23
|
+
export const MergeTreeDeltaType = {
|
|
24
|
+
INSERT: 0,
|
|
25
|
+
REMOVE: 1,
|
|
26
|
+
ANNOTATE: 2,
|
|
27
|
+
GROUP: 3,
|
|
28
|
+
} as const;
|
|
29
|
+
|
|
30
|
+
export type MergeTreeDeltaType = typeof MergeTreeDeltaType[keyof typeof MergeTreeDeltaType];
|
|
28
31
|
|
|
29
32
|
export interface IMergeTreeDelta {
|
|
30
33
|
/**
|
|
@@ -54,7 +57,7 @@ export interface IRelativePosition {
|
|
|
54
57
|
}
|
|
55
58
|
|
|
56
59
|
export interface IMergeTreeInsertMsg extends IMergeTreeDelta {
|
|
57
|
-
type: MergeTreeDeltaType.INSERT;
|
|
60
|
+
type: typeof MergeTreeDeltaType.INSERT;
|
|
58
61
|
pos1?: number;
|
|
59
62
|
relativePos1?: IRelativePosition;
|
|
60
63
|
pos2?: number;
|
|
@@ -63,7 +66,7 @@ export interface IMergeTreeInsertMsg extends IMergeTreeDelta {
|
|
|
63
66
|
}
|
|
64
67
|
|
|
65
68
|
export interface IMergeTreeRemoveMsg extends IMergeTreeDelta {
|
|
66
|
-
type: MergeTreeDeltaType.REMOVE;
|
|
69
|
+
type: typeof MergeTreeDeltaType.REMOVE;
|
|
67
70
|
pos1?: number;
|
|
68
71
|
relativePos1?: IRelativePosition;
|
|
69
72
|
pos2?: number;
|
|
@@ -78,7 +81,7 @@ export interface ICombiningOp {
|
|
|
78
81
|
}
|
|
79
82
|
|
|
80
83
|
export interface IMergeTreeAnnotateMsg extends IMergeTreeDelta {
|
|
81
|
-
type: MergeTreeDeltaType.ANNOTATE;
|
|
84
|
+
type: typeof MergeTreeDeltaType.ANNOTATE;
|
|
82
85
|
pos1?: number;
|
|
83
86
|
relativePos1?: IRelativePosition;
|
|
84
87
|
pos2?: number;
|
|
@@ -88,7 +91,7 @@ export interface IMergeTreeAnnotateMsg extends IMergeTreeDelta {
|
|
|
88
91
|
}
|
|
89
92
|
|
|
90
93
|
export interface IMergeTreeGroupMsg extends IMergeTreeDelta {
|
|
91
|
-
type: MergeTreeDeltaType.GROUP;
|
|
94
|
+
type: typeof MergeTreeDeltaType.GROUP;
|
|
92
95
|
ops: IMergeTreeDeltaOp[];
|
|
93
96
|
}
|
|
94
97
|
|
|
@@ -11,9 +11,10 @@ import { PropertySet, MapLike } from "./properties";
|
|
|
11
11
|
export const reservedTileLabelsKey = "referenceTileLabels";
|
|
12
12
|
export const reservedRangeLabelsKey = "referenceRangeLabels";
|
|
13
13
|
|
|
14
|
-
export function refTypeIncludesFlag(
|
|
14
|
+
export function refTypeIncludesFlag(refPosOrType: ReferencePosition | ReferenceType, flags: ReferenceType): boolean {
|
|
15
|
+
const refType = typeof refPosOrType === "number" ? refPosOrType : refPosOrType.refType;
|
|
15
16
|
// eslint-disable-next-line no-bitwise
|
|
16
|
-
return (
|
|
17
|
+
return (refType & flags) !== 0;
|
|
17
18
|
}
|
|
18
19
|
|
|
19
20
|
export const refGetTileLabels = (refPos: ReferencePosition): string[] | undefined =>
|