@fluidframework/merge-tree 2.4.0-294316 → 2.4.0-297027
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/api-report/merge-tree.legacy.alpha.api.md +26 -5
- package/dist/attributionPolicy.d.ts.map +1 -1
- package/dist/attributionPolicy.js +10 -3
- package/dist/attributionPolicy.js.map +1 -1
- package/dist/client.d.ts +14 -4
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +97 -9
- 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.map +1 -1
- package/dist/legacy.d.ts +1 -0
- package/dist/mergeTree.d.ts +15 -1
- package/dist/mergeTree.d.ts.map +1 -1
- package/dist/mergeTree.js +65 -19
- package/dist/mergeTree.js.map +1 -1
- package/dist/mergeTreeNodes.d.ts +6 -0
- package/dist/mergeTreeNodes.d.ts.map +1 -1
- package/dist/mergeTreeNodes.js +2 -1
- package/dist/mergeTreeNodes.js.map +1 -1
- package/dist/opBuilder.d.ts +15 -1
- package/dist/opBuilder.d.ts.map +1 -1
- package/dist/opBuilder.js +28 -1
- package/dist/opBuilder.js.map +1 -1
- package/dist/ops.d.ts +27 -1
- package/dist/ops.d.ts.map +1 -1
- package/dist/ops.js +1 -0
- package/dist/ops.js.map +1 -1
- package/dist/sequencePlace.d.ts +4 -0
- package/dist/sequencePlace.d.ts.map +1 -1
- package/dist/sequencePlace.js +17 -1
- package/dist/sequencePlace.js.map +1 -1
- package/dist/test/obliterate.concurrent.spec.js +18 -0
- package/dist/test/obliterate.concurrent.spec.js.map +1 -1
- package/dist/test/obliterate.rangeExpansion.spec.js +109 -53
- package/dist/test/obliterate.rangeExpansion.spec.js.map +1 -1
- package/dist/test/obliterate.spec.js +2 -2
- package/dist/test/obliterate.spec.js.map +1 -1
- package/dist/test/reconnectHelper.d.ts +8 -6
- package/dist/test/reconnectHelper.d.ts.map +1 -1
- package/dist/test/reconnectHelper.js +14 -13
- package/dist/test/reconnectHelper.js.map +1 -1
- package/dist/test/testClientLogger.d.ts.map +1 -1
- package/dist/test/testClientLogger.js +19 -8
- package/dist/test/testClientLogger.js.map +1 -1
- package/lib/attributionPolicy.d.ts.map +1 -1
- package/lib/attributionPolicy.js +10 -3
- package/lib/attributionPolicy.js.map +1 -1
- package/lib/client.d.ts +14 -4
- package/lib/client.d.ts.map +1 -1
- package/lib/client.js +98 -10
- 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.map +1 -1
- package/lib/legacy.d.ts +1 -0
- package/lib/mergeTree.d.ts +15 -1
- package/lib/mergeTree.d.ts.map +1 -1
- package/lib/mergeTree.js +65 -19
- package/lib/mergeTree.js.map +1 -1
- package/lib/mergeTreeNodes.d.ts +6 -0
- package/lib/mergeTreeNodes.d.ts.map +1 -1
- package/lib/mergeTreeNodes.js +2 -1
- package/lib/mergeTreeNodes.js.map +1 -1
- package/lib/opBuilder.d.ts +15 -1
- package/lib/opBuilder.d.ts.map +1 -1
- package/lib/opBuilder.js +26 -0
- package/lib/opBuilder.js.map +1 -1
- package/lib/ops.d.ts +27 -1
- package/lib/ops.d.ts.map +1 -1
- package/lib/ops.js +1 -0
- package/lib/ops.js.map +1 -1
- package/lib/sequencePlace.d.ts +4 -0
- package/lib/sequencePlace.d.ts.map +1 -1
- package/lib/sequencePlace.js +15 -0
- package/lib/sequencePlace.js.map +1 -1
- package/lib/test/obliterate.concurrent.spec.js +18 -0
- package/lib/test/obliterate.concurrent.spec.js.map +1 -1
- package/lib/test/obliterate.rangeExpansion.spec.js +109 -53
- package/lib/test/obliterate.rangeExpansion.spec.js.map +1 -1
- package/lib/test/obliterate.spec.js +2 -2
- package/lib/test/obliterate.spec.js.map +1 -1
- package/lib/test/reconnectHelper.d.ts +8 -6
- package/lib/test/reconnectHelper.d.ts.map +1 -1
- package/lib/test/reconnectHelper.js +15 -14
- package/lib/test/reconnectHelper.js.map +1 -1
- package/lib/test/testClientLogger.d.ts.map +1 -1
- package/lib/test/testClientLogger.js +19 -8
- package/lib/test/testClientLogger.js.map +1 -1
- package/package.json +29 -16
- package/src/attributionPolicy.ts +5 -0
- package/src/client.ts +136 -21
- package/src/index.ts +1 -0
- package/src/mergeTree.ts +108 -22
- package/src/mergeTreeNodes.ts +9 -1
- package/src/opBuilder.ts +32 -0
- package/src/ops.ts +23 -1
- package/src/sequencePlace.ts +16 -0
package/src/client.ts
CHANGED
|
@@ -56,6 +56,7 @@ import {
|
|
|
56
56
|
createGroupOp,
|
|
57
57
|
createInsertSegmentOp,
|
|
58
58
|
createObliterateRangeOp,
|
|
59
|
+
createObliterateRangeOpSided,
|
|
59
60
|
createRemoveRangeOp,
|
|
60
61
|
} from "./opBuilder.js";
|
|
61
62
|
import {
|
|
@@ -72,9 +73,11 @@ import {
|
|
|
72
73
|
IRelativePosition,
|
|
73
74
|
MergeTreeDeltaType,
|
|
74
75
|
ReferenceType,
|
|
76
|
+
type IMergeTreeObliterateSidedMsg,
|
|
75
77
|
} from "./ops.js";
|
|
76
78
|
import { PropertySet } from "./properties.js";
|
|
77
79
|
import { DetachedReferencePosition, ReferencePosition } from "./referencePositions.js";
|
|
80
|
+
import { Side, type InteriorSequencePlace } from "./sequencePlace.js";
|
|
78
81
|
import { SnapshotLoader } from "./snapshotLoader.js";
|
|
79
82
|
import { SnapshotV1 } from "./snapshotV1.js";
|
|
80
83
|
import { SnapshotLegacy } from "./snapshotlegacy.js";
|
|
@@ -99,6 +102,7 @@ export interface IIntegerRange {
|
|
|
99
102
|
* they need for rebasing their ops on reconnection.
|
|
100
103
|
* @legacy
|
|
101
104
|
* @alpha
|
|
105
|
+
* @deprecated This functionality was not meant to be exported and will be removed in a future release
|
|
102
106
|
*/
|
|
103
107
|
export interface IClientEvents {
|
|
104
108
|
(event: "normalize", listener: (target: IEventThisPlaceHolder) => void): void;
|
|
@@ -252,15 +256,26 @@ export class Client extends TypedEventEmitter<IClientEvents> {
|
|
|
252
256
|
* Obliterates the range. This is similar to removing the range, but also
|
|
253
257
|
* includes any concurrently inserted content.
|
|
254
258
|
*
|
|
255
|
-
* @param start - The
|
|
256
|
-
* @param end - The
|
|
259
|
+
* @param start - The start of the range to obliterate. Inclusive is side is Before (default).
|
|
260
|
+
* @param end - The end of the range to obliterate. Exclusive is side is After
|
|
261
|
+
* (default is to be after the last included character, but number index is exclusive).
|
|
257
262
|
*/
|
|
258
263
|
public obliterateRangeLocal(
|
|
259
|
-
start: number,
|
|
260
|
-
end: number,
|
|
264
|
+
start: number | InteriorSequencePlace,
|
|
265
|
+
end: number | InteriorSequencePlace,
|
|
266
|
+
// eslint-disable-next-line import/no-deprecated
|
|
267
|
+
): IMergeTreeObliterateMsg | IMergeTreeObliterateSidedMsg {
|
|
261
268
|
// eslint-disable-next-line import/no-deprecated
|
|
262
|
-
|
|
263
|
-
|
|
269
|
+
let obliterateOp: IMergeTreeObliterateMsg | IMergeTreeObliterateSidedMsg;
|
|
270
|
+
if (this._mergeTree.options?.mergeTreeEnableSidedObliterate) {
|
|
271
|
+
obliterateOp = createObliterateRangeOpSided(start, end);
|
|
272
|
+
} else {
|
|
273
|
+
assert(
|
|
274
|
+
typeof start === "number" && typeof end === "number",
|
|
275
|
+
"Start and end must be numbers if mergeTreeEnableSidedObliterate is not enabled.",
|
|
276
|
+
);
|
|
277
|
+
obliterateOp = createObliterateRangeOp(start, end);
|
|
278
|
+
}
|
|
264
279
|
this.applyObliterateRangeOp({ op: obliterateOp });
|
|
265
280
|
return obliterateOp;
|
|
266
281
|
}
|
|
@@ -472,22 +487,39 @@ export class Client extends TypedEventEmitter<IClientEvents> {
|
|
|
472
487
|
|
|
473
488
|
private applyObliterateRangeOp(opArgs: IMergeTreeDeltaOpArgs): void {
|
|
474
489
|
assert(
|
|
475
|
-
opArgs.op.type === MergeTreeDeltaType.OBLITERATE
|
|
490
|
+
opArgs.op.type === MergeTreeDeltaType.OBLITERATE ||
|
|
491
|
+
opArgs.op.type === MergeTreeDeltaType.OBLITERATE_SIDED,
|
|
476
492
|
0x866 /* Unexpected op type on range obliterate! */,
|
|
477
493
|
);
|
|
478
494
|
const op = opArgs.op;
|
|
479
495
|
const clientArgs = this.getClientSequenceArgs(opArgs);
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
496
|
+
if (this._mergeTree.options?.mergeTreeEnableSidedObliterate) {
|
|
497
|
+
const { start, end } = this.getValidSidedRange(op, clientArgs);
|
|
498
|
+
this._mergeTree.obliterateRange(
|
|
499
|
+
start,
|
|
500
|
+
end,
|
|
501
|
+
clientArgs.referenceSequenceNumber,
|
|
502
|
+
clientArgs.clientId,
|
|
503
|
+
clientArgs.sequenceNumber,
|
|
504
|
+
false,
|
|
505
|
+
opArgs,
|
|
506
|
+
);
|
|
507
|
+
} else {
|
|
508
|
+
assert(
|
|
509
|
+
op.type === MergeTreeDeltaType.OBLITERATE,
|
|
510
|
+
"Unexpected sided obliterate while mergeTreeEnableSidedObliterate is disabled",
|
|
511
|
+
);
|
|
512
|
+
const range = this.getValidOpRange(op, clientArgs);
|
|
513
|
+
this._mergeTree.obliterateRange(
|
|
514
|
+
range.start,
|
|
515
|
+
range.end,
|
|
516
|
+
clientArgs.referenceSequenceNumber,
|
|
517
|
+
clientArgs.clientId,
|
|
518
|
+
clientArgs.sequenceNumber,
|
|
519
|
+
false,
|
|
520
|
+
opArgs,
|
|
521
|
+
);
|
|
522
|
+
}
|
|
491
523
|
}
|
|
492
524
|
|
|
493
525
|
/**
|
|
@@ -565,6 +597,82 @@ export class Client extends TypedEventEmitter<IClientEvents> {
|
|
|
565
597
|
);
|
|
566
598
|
}
|
|
567
599
|
|
|
600
|
+
/**
|
|
601
|
+
* Returns a valid range for the op, or throws if the range is invalid
|
|
602
|
+
* @param op - The op to generate the range for
|
|
603
|
+
* @param clientArgs - The client args for the op
|
|
604
|
+
* @throws LoggingError if the range is invalid
|
|
605
|
+
*/
|
|
606
|
+
private getValidSidedRange(
|
|
607
|
+
// eslint-disable-next-line import/no-deprecated
|
|
608
|
+
op: IMergeTreeObliterateSidedMsg | IMergeTreeObliterateMsg,
|
|
609
|
+
clientArgs: IMergeTreeClientSequenceArgs,
|
|
610
|
+
): {
|
|
611
|
+
start: InteriorSequencePlace;
|
|
612
|
+
end: InteriorSequencePlace;
|
|
613
|
+
} {
|
|
614
|
+
const invalidPositions: string[] = [];
|
|
615
|
+
let start: InteriorSequencePlace | undefined;
|
|
616
|
+
let end: InteriorSequencePlace | undefined;
|
|
617
|
+
if (op.pos1 === undefined) {
|
|
618
|
+
invalidPositions.push("start");
|
|
619
|
+
} else {
|
|
620
|
+
start =
|
|
621
|
+
typeof op.pos1 === "object"
|
|
622
|
+
? { pos: op.pos1.pos, side: op.pos1.before ? Side.Before : Side.After }
|
|
623
|
+
: { pos: op.pos1, side: Side.Before };
|
|
624
|
+
}
|
|
625
|
+
if (op.pos2 === undefined) {
|
|
626
|
+
invalidPositions.push("end");
|
|
627
|
+
} else {
|
|
628
|
+
end =
|
|
629
|
+
typeof op.pos2 === "object"
|
|
630
|
+
? { pos: op.pos2.pos, side: op.pos2.before ? Side.Before : Side.After }
|
|
631
|
+
: { pos: op.pos2 - 1, side: Side.After };
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
// Validate if local op
|
|
635
|
+
if (clientArgs.clientId === this.getClientId()) {
|
|
636
|
+
const length = this._mergeTree.getLength(
|
|
637
|
+
this.getCollabWindow().currentSeq,
|
|
638
|
+
this.getClientId(),
|
|
639
|
+
);
|
|
640
|
+
if (start !== undefined && (start.pos >= length || start.pos < 0)) {
|
|
641
|
+
// start out of bounds
|
|
642
|
+
invalidPositions.push("start");
|
|
643
|
+
}
|
|
644
|
+
if (end !== undefined && (end.pos >= length || end.pos < 0)) {
|
|
645
|
+
invalidPositions.push("end");
|
|
646
|
+
}
|
|
647
|
+
if (
|
|
648
|
+
start !== undefined &&
|
|
649
|
+
end !== undefined &&
|
|
650
|
+
(start.pos > end.pos ||
|
|
651
|
+
(start.pos === end.pos && start.side !== end.side && start.side === Side.After))
|
|
652
|
+
) {
|
|
653
|
+
// end is before start
|
|
654
|
+
invalidPositions.push("inverted");
|
|
655
|
+
}
|
|
656
|
+
if (invalidPositions.length > 0) {
|
|
657
|
+
throw new LoggingError("InvalidRange", {
|
|
658
|
+
usageError: true,
|
|
659
|
+
invalidPositions: invalidPositions.toString(),
|
|
660
|
+
length,
|
|
661
|
+
opType: op.type,
|
|
662
|
+
opPos1Relative: op.relativePos1 !== undefined,
|
|
663
|
+
opPos2Relative: op.relativePos2 !== undefined,
|
|
664
|
+
opPos1: JSON.stringify(op.pos1),
|
|
665
|
+
opPos2: JSON.stringify(op.pos2),
|
|
666
|
+
start: JSON.stringify(start),
|
|
667
|
+
end: JSON.stringify(end),
|
|
668
|
+
});
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
assert(start !== undefined && end !== undefined, "Missing start or end of range");
|
|
673
|
+
return { start, end };
|
|
674
|
+
}
|
|
675
|
+
|
|
568
676
|
/**
|
|
569
677
|
* Returns a valid range for the op, or undefined
|
|
570
678
|
* @param op - The op to generate the range for
|
|
@@ -614,7 +722,6 @@ export class Client extends TypedEventEmitter<IClientEvents> {
|
|
|
614
722
|
invalidPositions.push("start");
|
|
615
723
|
}
|
|
616
724
|
// Validate end if not insert, or insert has end
|
|
617
|
-
//
|
|
618
725
|
if (
|
|
619
726
|
(op.type !== MergeTreeDeltaType.INSERT || end !== undefined) &&
|
|
620
727
|
(end === undefined || end <= start!)
|
|
@@ -892,7 +999,12 @@ export class Client extends TypedEventEmitter<IClientEvents> {
|
|
|
892
999
|
|
|
893
1000
|
const first = opList[0];
|
|
894
1001
|
|
|
895
|
-
if (
|
|
1002
|
+
if (
|
|
1003
|
+
!!first &&
|
|
1004
|
+
first.pos2 !== undefined &&
|
|
1005
|
+
first.type !== MergeTreeDeltaType.OBLITERATE_SIDED &&
|
|
1006
|
+
newOp.type !== MergeTreeDeltaType.OBLITERATE_SIDED
|
|
1007
|
+
) {
|
|
896
1008
|
first.pos2 += newOp.pos2! - newOp.pos1!;
|
|
897
1009
|
} else {
|
|
898
1010
|
opList.push(newOp);
|
|
@@ -939,7 +1051,8 @@ export class Client extends TypedEventEmitter<IClientEvents> {
|
|
|
939
1051
|
this.applyAnnotateRangeOp(opArgs);
|
|
940
1052
|
break;
|
|
941
1053
|
}
|
|
942
|
-
case MergeTreeDeltaType.OBLITERATE:
|
|
1054
|
+
case MergeTreeDeltaType.OBLITERATE:
|
|
1055
|
+
case MergeTreeDeltaType.OBLITERATE_SIDED: {
|
|
943
1056
|
this.applyObliterateRangeOp(opArgs);
|
|
944
1057
|
break;
|
|
945
1058
|
}
|
|
@@ -973,6 +1086,7 @@ export class Client extends TypedEventEmitter<IClientEvents> {
|
|
|
973
1086
|
this.applyAnnotateRangeOp({ op });
|
|
974
1087
|
break;
|
|
975
1088
|
}
|
|
1089
|
+
case MergeTreeDeltaType.OBLITERATE_SIDED:
|
|
976
1090
|
case MergeTreeDeltaType.OBLITERATE: {
|
|
977
1091
|
this.applyObliterateRangeOp({ op });
|
|
978
1092
|
break;
|
|
@@ -1205,6 +1319,7 @@ export class Client extends TypedEventEmitter<IClientEvents> {
|
|
|
1205
1319
|
this.applyRemoveRangeOp(opArgs);
|
|
1206
1320
|
break;
|
|
1207
1321
|
}
|
|
1322
|
+
case MergeTreeDeltaType.OBLITERATE_SIDED:
|
|
1208
1323
|
case MergeTreeDeltaType.OBLITERATE: {
|
|
1209
1324
|
this.applyObliterateRangeOp(opArgs);
|
|
1210
1325
|
break;
|
package/src/index.ts
CHANGED
package/src/mergeTree.ts
CHANGED
|
@@ -90,6 +90,7 @@ import {
|
|
|
90
90
|
} from "./referencePositions.js";
|
|
91
91
|
// eslint-disable-next-line import/no-deprecated
|
|
92
92
|
import { PropertiesRollback } from "./segmentPropertiesManager.js";
|
|
93
|
+
import { Side, type InteriorSequencePlace } from "./sequencePlace.js";
|
|
93
94
|
import { SortedSegmentSet } from "./sortedSegmentSet.js";
|
|
94
95
|
import { zamboniSegments } from "./zamboni.js";
|
|
95
96
|
|
|
@@ -197,6 +198,17 @@ export interface IMergeTreeOptions {
|
|
|
197
198
|
* @defaultValue `false`
|
|
198
199
|
*/
|
|
199
200
|
mergeTreeEnableObliterateReconnect?: boolean;
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Enables support for obliterate endpoint expansion.
|
|
204
|
+
* When enabled, obliterate operations can have sidedness specified for their endpoints.
|
|
205
|
+
* If an endpoint is externally anchored
|
|
206
|
+
* (aka the start is after a given position, or the end is before a given position),
|
|
207
|
+
* then concurrent inserts adjacent to the exclusive endpoint of an obliterated range will be included in the obliteration
|
|
208
|
+
*
|
|
209
|
+
* @defaultValue `false`
|
|
210
|
+
*/
|
|
211
|
+
mergeTreeEnableSidedObliterate?: boolean;
|
|
200
212
|
}
|
|
201
213
|
export function errorIfOptionNotTrue(
|
|
202
214
|
options: IMergeTreeOptions | undefined,
|
|
@@ -210,6 +222,7 @@ export function errorIfOptionNotTrue(
|
|
|
210
222
|
/**
|
|
211
223
|
* @legacy
|
|
212
224
|
* @alpha
|
|
225
|
+
* @deprecated This functionality was not meant to be exported and will be removed in a future release
|
|
213
226
|
*/
|
|
214
227
|
export interface IMergeTreeAttributionOptions {
|
|
215
228
|
/**
|
|
@@ -237,6 +250,7 @@ export interface IMergeTreeAttributionOptions {
|
|
|
237
250
|
* @sealed
|
|
238
251
|
* @legacy
|
|
239
252
|
* @alpha
|
|
253
|
+
* @deprecated This functionality was not meant to be exported and will be removed in a future release
|
|
240
254
|
*/
|
|
241
255
|
export interface AttributionPolicy {
|
|
242
256
|
/**
|
|
@@ -1234,7 +1248,10 @@ export class MergeTree {
|
|
|
1234
1248
|
});
|
|
1235
1249
|
});
|
|
1236
1250
|
|
|
1237
|
-
if (
|
|
1251
|
+
if (
|
|
1252
|
+
opArgs.op.type === MergeTreeDeltaType.OBLITERATE ||
|
|
1253
|
+
opArgs.op.type === MergeTreeDeltaType.OBLITERATE_SIDED
|
|
1254
|
+
) {
|
|
1238
1255
|
this.obliterates.addOrUpdate(pendingSegmentGroup.obliterateInfo!);
|
|
1239
1256
|
}
|
|
1240
1257
|
|
|
@@ -1242,7 +1259,8 @@ export class MergeTree {
|
|
|
1242
1259
|
// positions after slide are final
|
|
1243
1260
|
if (
|
|
1244
1261
|
opArgs.op.type === MergeTreeDeltaType.REMOVE ||
|
|
1245
|
-
opArgs.op.type === MergeTreeDeltaType.OBLITERATE
|
|
1262
|
+
opArgs.op.type === MergeTreeDeltaType.OBLITERATE ||
|
|
1263
|
+
opArgs.op.type === MergeTreeDeltaType.OBLITERATE_SIDED
|
|
1246
1264
|
) {
|
|
1247
1265
|
this.slideAckedRemovedSegmentReferences(pendingSegmentGroup.segments);
|
|
1248
1266
|
}
|
|
@@ -1510,11 +1528,11 @@ export class MergeTree {
|
|
|
1510
1528
|
}
|
|
1511
1529
|
|
|
1512
1530
|
this.updateRoot(splitNode);
|
|
1513
|
-
saveIfLocal(newSegment);
|
|
1514
1531
|
|
|
1515
1532
|
insertPos += newSegment.cachedLength;
|
|
1516
1533
|
|
|
1517
1534
|
if (!this.options?.mergeTreeEnableObliterate || this.obliterates.empty()) {
|
|
1535
|
+
saveIfLocal(newSegment);
|
|
1518
1536
|
continue;
|
|
1519
1537
|
}
|
|
1520
1538
|
|
|
@@ -1542,13 +1560,13 @@ export class MergeTree {
|
|
|
1542
1560
|
movedClientIds.unshift(ob.clientId);
|
|
1543
1561
|
movedSeqs.unshift(ob.seq);
|
|
1544
1562
|
} else {
|
|
1545
|
-
if (newest === undefined || normalizedNewestSeq < normalizedObSeq) {
|
|
1546
|
-
normalizedNewestSeq = normalizedObSeq;
|
|
1547
|
-
newest = ob;
|
|
1548
|
-
}
|
|
1549
1563
|
movedClientIds.push(ob.clientId);
|
|
1550
1564
|
movedSeqs.push(ob.seq);
|
|
1551
1565
|
}
|
|
1566
|
+
if (newest === undefined || normalizedNewestSeq < normalizedObSeq) {
|
|
1567
|
+
normalizedNewestSeq = normalizedObSeq;
|
|
1568
|
+
newest = ob;
|
|
1569
|
+
}
|
|
1552
1570
|
}
|
|
1553
1571
|
}
|
|
1554
1572
|
|
|
@@ -1575,7 +1593,11 @@ export class MergeTree {
|
|
|
1575
1593
|
if (newSegment.parent) {
|
|
1576
1594
|
this.blockUpdatePathLengths(newSegment.parent, seq, clientId);
|
|
1577
1595
|
}
|
|
1596
|
+
} else if (oldest && newest?.clientId === clientId) {
|
|
1597
|
+
newSegment.prevObliterateByInserter = newest;
|
|
1578
1598
|
}
|
|
1599
|
+
|
|
1600
|
+
saveIfLocal(newSegment);
|
|
1579
1601
|
}
|
|
1580
1602
|
}
|
|
1581
1603
|
}
|
|
@@ -1866,19 +1888,20 @@ export class MergeTree {
|
|
|
1866
1888
|
}
|
|
1867
1889
|
}
|
|
1868
1890
|
|
|
1869
|
-
|
|
1870
|
-
start:
|
|
1871
|
-
end:
|
|
1891
|
+
private obliterateRangeSided(
|
|
1892
|
+
start: InteriorSequencePlace,
|
|
1893
|
+
end: InteriorSequencePlace,
|
|
1872
1894
|
refSeq: number,
|
|
1873
1895
|
clientId: number,
|
|
1874
1896
|
seq: number,
|
|
1875
1897
|
overwrite: boolean = false,
|
|
1876
1898
|
opArgs: IMergeTreeDeltaOpArgs,
|
|
1877
1899
|
): void {
|
|
1878
|
-
|
|
1900
|
+
const startPos = start.side === Side.Before ? start.pos : start.pos + 1;
|
|
1901
|
+
const endPos = end.side === Side.Before ? end.pos : end.pos + 1;
|
|
1879
1902
|
|
|
1880
|
-
this.ensureIntervalBoundary(
|
|
1881
|
-
this.ensureIntervalBoundary(
|
|
1903
|
+
this.ensureIntervalBoundary(startPos, refSeq, clientId);
|
|
1904
|
+
this.ensureIntervalBoundary(endPos, refSeq, clientId);
|
|
1882
1905
|
|
|
1883
1906
|
let _overwrite = overwrite;
|
|
1884
1907
|
const localOverlapWithRefs: ISegment[] = [];
|
|
@@ -1896,8 +1919,8 @@ export class MergeTree {
|
|
|
1896
1919
|
segmentGroup: undefined,
|
|
1897
1920
|
};
|
|
1898
1921
|
|
|
1899
|
-
const { segment: startSeg } = this.getContainingSegment(start, refSeq, clientId);
|
|
1900
|
-
const { segment: endSeg } = this.getContainingSegment(end
|
|
1922
|
+
const { segment: startSeg } = this.getContainingSegment(start.pos, refSeq, clientId);
|
|
1923
|
+
const { segment: endSeg } = this.getContainingSegment(end.pos, refSeq, clientId);
|
|
1901
1924
|
assert(
|
|
1902
1925
|
startSeg !== undefined && endSeg !== undefined,
|
|
1903
1926
|
0xa3f /* segments cannot be undefined */,
|
|
@@ -1905,7 +1928,7 @@ export class MergeTree {
|
|
|
1905
1928
|
|
|
1906
1929
|
obliterate.start = this.createLocalReferencePosition(
|
|
1907
1930
|
startSeg,
|
|
1908
|
-
0,
|
|
1931
|
+
start.side === Side.Before ? 0 : Math.max(startSeg.cachedLength - 1, 0),
|
|
1909
1932
|
ReferenceType.StayOnRemove,
|
|
1910
1933
|
{
|
|
1911
1934
|
obliterate,
|
|
@@ -1914,20 +1937,53 @@ export class MergeTree {
|
|
|
1914
1937
|
|
|
1915
1938
|
obliterate.end = this.createLocalReferencePosition(
|
|
1916
1939
|
endSeg,
|
|
1917
|
-
endSeg.cachedLength - 1,
|
|
1940
|
+
end.side === Side.Before ? 0 : Math.max(endSeg.cachedLength - 1, 0),
|
|
1918
1941
|
ReferenceType.StayOnRemove,
|
|
1919
1942
|
{
|
|
1920
1943
|
obliterate,
|
|
1921
1944
|
},
|
|
1922
1945
|
);
|
|
1923
1946
|
|
|
1947
|
+
// Always create a segment group for obliterate,
|
|
1948
|
+
// even if there are no segments currently in the obliteration range.
|
|
1949
|
+
// Segments may be concurrently inserted into the obliteration range,
|
|
1950
|
+
// at which point they are added to the segment group.
|
|
1951
|
+
obliterate.segmentGroup = {
|
|
1952
|
+
segments: [],
|
|
1953
|
+
localSeq,
|
|
1954
|
+
refSeq: this.collabWindow.currentSeq,
|
|
1955
|
+
obliterateInfo: obliterate,
|
|
1956
|
+
};
|
|
1957
|
+
if (this.collabWindow.collaborating && clientId === this.collabWindow.clientId) {
|
|
1958
|
+
this.pendingSegments.push(obliterate.segmentGroup);
|
|
1959
|
+
}
|
|
1960
|
+
this.obliterates.addOrUpdate(obliterate);
|
|
1961
|
+
|
|
1924
1962
|
const markMoved = (
|
|
1925
1963
|
segment: ISegment,
|
|
1926
1964
|
pos: number,
|
|
1927
1965
|
_start: number,
|
|
1928
1966
|
_end: number,
|
|
1929
1967
|
): boolean => {
|
|
1968
|
+
if (
|
|
1969
|
+
(start.side === Side.After && startPos === pos + segment.cachedLength) || // exclusive start segment
|
|
1970
|
+
(end.side === Side.Before &&
|
|
1971
|
+
endPos === pos &&
|
|
1972
|
+
isSegmentPresent(segment, { refSeq, localSeq })) // exclusive end segment
|
|
1973
|
+
) {
|
|
1974
|
+
// We walk these segments because we want to also walk any concurrently inserted segments between here and the obliterated segments.
|
|
1975
|
+
// These segments are outside of the obliteration range though, so return true to keep walking.
|
|
1976
|
+
return true;
|
|
1977
|
+
}
|
|
1930
1978
|
const existingMoveInfo = toMoveInfo(segment);
|
|
1979
|
+
|
|
1980
|
+
if (segment.prevObliterateByInserter?.seq === UnassignedSequenceNumber) {
|
|
1981
|
+
// We chose to not obliterate this segment because we are aware of an unacked local obliteration.
|
|
1982
|
+
// The local obliterate has not been sequenced yet, so it is still the newest obliterate we are aware of.
|
|
1983
|
+
// Other clients will also choose not to obliterate this segment because the most recent obliteration has the same clientId
|
|
1984
|
+
return true;
|
|
1985
|
+
}
|
|
1986
|
+
|
|
1931
1987
|
if (
|
|
1932
1988
|
clientId !== segment.clientId &&
|
|
1933
1989
|
segment.seq !== undefined &&
|
|
@@ -1978,7 +2034,6 @@ export class MergeTree {
|
|
|
1978
2034
|
obliterate.segmentGroup,
|
|
1979
2035
|
localSeq,
|
|
1980
2036
|
);
|
|
1981
|
-
obliterate.segmentGroup.obliterateInfo ??= obliterate;
|
|
1982
2037
|
} else {
|
|
1983
2038
|
if (MergeTree.options.zamboniSegments) {
|
|
1984
2039
|
this.addToLRUSet(segment, seq);
|
|
@@ -2008,14 +2063,12 @@ export class MergeTree {
|
|
|
2008
2063
|
markMoved,
|
|
2009
2064
|
undefined,
|
|
2010
2065
|
afterMarkMoved,
|
|
2011
|
-
start,
|
|
2012
|
-
end,
|
|
2066
|
+
start.pos,
|
|
2067
|
+
end.pos + 1, // include the segment containing the end reference
|
|
2013
2068
|
undefined,
|
|
2014
2069
|
seq === UnassignedSequenceNumber ? undefined : seq,
|
|
2015
2070
|
);
|
|
2016
2071
|
|
|
2017
|
-
this.obliterates.addOrUpdate(obliterate);
|
|
2018
|
-
|
|
2019
2072
|
this.slideAckedRemovedSegmentReferences(localOverlapWithRefs);
|
|
2020
2073
|
// opArgs == undefined => test code
|
|
2021
2074
|
if (movedSegments.length > 0) {
|
|
@@ -2041,6 +2094,39 @@ export class MergeTree {
|
|
|
2041
2094
|
}
|
|
2042
2095
|
}
|
|
2043
2096
|
|
|
2097
|
+
public obliterateRange(
|
|
2098
|
+
start: number | InteriorSequencePlace,
|
|
2099
|
+
end: number | InteriorSequencePlace,
|
|
2100
|
+
refSeq: number,
|
|
2101
|
+
clientId: number,
|
|
2102
|
+
seq: number,
|
|
2103
|
+
overwrite: boolean = false,
|
|
2104
|
+
opArgs: IMergeTreeDeltaOpArgs,
|
|
2105
|
+
): void {
|
|
2106
|
+
errorIfOptionNotTrue(this.options, "mergeTreeEnableObliterate");
|
|
2107
|
+
if (this.options?.mergeTreeEnableSidedObliterate) {
|
|
2108
|
+
assert(
|
|
2109
|
+
typeof start === "object" && typeof end === "object",
|
|
2110
|
+
"Start and end must be of type InteriorSequencePlace if mergeTreeEnableSidedObliterate is enabled.",
|
|
2111
|
+
);
|
|
2112
|
+
this.obliterateRangeSided(start, end, refSeq, clientId, seq, overwrite, opArgs);
|
|
2113
|
+
} else {
|
|
2114
|
+
assert(
|
|
2115
|
+
typeof start === "number" && typeof end === "number",
|
|
2116
|
+
"Start and end must be numbers if mergeTreeEnableSidedObliterate is not enabled.",
|
|
2117
|
+
);
|
|
2118
|
+
this.obliterateRangeSided(
|
|
2119
|
+
{ pos: start, side: Side.Before },
|
|
2120
|
+
{ pos: end - 1, side: Side.After },
|
|
2121
|
+
refSeq,
|
|
2122
|
+
clientId,
|
|
2123
|
+
seq,
|
|
2124
|
+
overwrite,
|
|
2125
|
+
opArgs,
|
|
2126
|
+
);
|
|
2127
|
+
}
|
|
2128
|
+
}
|
|
2129
|
+
|
|
2044
2130
|
public markRangeRemoved(
|
|
2045
2131
|
start: number,
|
|
2046
2132
|
end: number,
|
package/src/mergeTreeNodes.ts
CHANGED
|
@@ -160,6 +160,13 @@ export interface IMoveInfo {
|
|
|
160
160
|
* calculations
|
|
161
161
|
*/
|
|
162
162
|
wasMovedOnInsert: boolean;
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* If a segment is inserted into an obliterated range,
|
|
166
|
+
* but the newest obliteration of that range was by the inserting client,
|
|
167
|
+
* then the segment is not obliterated because it is aware of the latest obliteration.
|
|
168
|
+
*/
|
|
169
|
+
prevObliterateByInserter?: ObliterateInfo;
|
|
163
170
|
}
|
|
164
171
|
|
|
165
172
|
export function toMoveInfo(maybe: Partial<IMoveInfo> | undefined): IMoveInfo | undefined {
|
|
@@ -661,7 +668,8 @@ export abstract class BaseSegment implements ISegment {
|
|
|
661
668
|
return false;
|
|
662
669
|
}
|
|
663
670
|
|
|
664
|
-
case MergeTreeDeltaType.OBLITERATE:
|
|
671
|
+
case MergeTreeDeltaType.OBLITERATE:
|
|
672
|
+
case MergeTreeDeltaType.OBLITERATE_SIDED: {
|
|
665
673
|
const moveInfo: IMoveInfo | undefined = toMoveInfo(this);
|
|
666
674
|
assert(moveInfo !== undefined, 0x86e /* On obliterate ack, missing move info! */);
|
|
667
675
|
const obliterateInfo = segmentGroup.obliterateInfo;
|
package/src/opBuilder.ts
CHANGED
|
@@ -14,8 +14,10 @@ import {
|
|
|
14
14
|
IMergeTreeObliterateMsg,
|
|
15
15
|
IMergeTreeRemoveMsg,
|
|
16
16
|
MergeTreeDeltaType,
|
|
17
|
+
type IMergeTreeObliterateSidedMsg,
|
|
17
18
|
} from "./ops.js";
|
|
18
19
|
import { PropertySet } from "./properties.js";
|
|
20
|
+
import { normalizePlace, Side, type SequencePlace } from "./sequencePlace.js";
|
|
19
21
|
|
|
20
22
|
/**
|
|
21
23
|
* Creates the op for annotating the markers with the provided properties
|
|
@@ -97,6 +99,36 @@ export function createObliterateRangeOp(start: number, end: number): IMergeTreeO
|
|
|
97
99
|
};
|
|
98
100
|
}
|
|
99
101
|
|
|
102
|
+
/**
|
|
103
|
+
* Creates the op to obliterate a range
|
|
104
|
+
*
|
|
105
|
+
* @param start - The start of the range to obliterate.
|
|
106
|
+
* If a number is provided, the range will start before that index.
|
|
107
|
+
* @param end - The end of the range to obliterate.
|
|
108
|
+
* If a number is provided, the range will end after that index -1.
|
|
109
|
+
* This preserves the previous behavior of not expanding obliteration ranges at the endpoints
|
|
110
|
+
* for uses which predate the availability of endpoint expansion.
|
|
111
|
+
*
|
|
112
|
+
* @internal
|
|
113
|
+
*/
|
|
114
|
+
export function createObliterateRangeOpSided(
|
|
115
|
+
start: SequencePlace,
|
|
116
|
+
end: SequencePlace,
|
|
117
|
+
): IMergeTreeObliterateSidedMsg {
|
|
118
|
+
const startPlace = normalizePlace(start);
|
|
119
|
+
// If a number is provided, default to after the previous index.
|
|
120
|
+
// This preserves the behavior of obliterate prior to the introduction of endpoint expansion.
|
|
121
|
+
const endPlace =
|
|
122
|
+
typeof end === "number"
|
|
123
|
+
? { pos: end - 1, side: Side.After } // default to inclusive bounds
|
|
124
|
+
: normalizePlace(end);
|
|
125
|
+
return {
|
|
126
|
+
type: MergeTreeDeltaType.OBLITERATE_SIDED,
|
|
127
|
+
pos1: { pos: startPlace.pos, before: startPlace.side === Side.Before },
|
|
128
|
+
pos2: { pos: endPlace.pos, before: endPlace.side === Side.Before },
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
100
132
|
/**
|
|
101
133
|
* Creates an op for inserting a segment at the specified position.
|
|
102
134
|
*
|
package/src/ops.ts
CHANGED
|
@@ -70,6 +70,7 @@ export const MergeTreeDeltaType = {
|
|
|
70
70
|
*/
|
|
71
71
|
GROUP: 3,
|
|
72
72
|
OBLITERATE: 4,
|
|
73
|
+
OBLITERATE_SIDED: 5,
|
|
73
74
|
} as const;
|
|
74
75
|
|
|
75
76
|
/**
|
|
@@ -162,6 +163,26 @@ export interface IMergeTreeObliterateMsg extends IMergeTreeDelta {
|
|
|
162
163
|
relativePos2?: never;
|
|
163
164
|
}
|
|
164
165
|
|
|
166
|
+
/**
|
|
167
|
+
* @legacy
|
|
168
|
+
* @alpha
|
|
169
|
+
*/
|
|
170
|
+
export interface IMergeTreeObliterateSidedMsg extends IMergeTreeDelta {
|
|
171
|
+
type: typeof MergeTreeDeltaType.OBLITERATE_SIDED;
|
|
172
|
+
pos1: { pos: number; before: boolean };
|
|
173
|
+
/**
|
|
174
|
+
* This field is currently unused, but we keep it around to make the union
|
|
175
|
+
* type of all merge-tree messages have the same fields
|
|
176
|
+
*/
|
|
177
|
+
relativePos1?: never;
|
|
178
|
+
pos2: { pos: number; before: boolean };
|
|
179
|
+
/**
|
|
180
|
+
* This field is currently unused, but we keep it around to make the union
|
|
181
|
+
* type of all merge-tree messages have the same fields
|
|
182
|
+
*/
|
|
183
|
+
relativePos2?: never;
|
|
184
|
+
}
|
|
185
|
+
|
|
165
186
|
/**
|
|
166
187
|
* @legacy
|
|
167
188
|
* @alpha
|
|
@@ -206,7 +227,8 @@ export type IMergeTreeDeltaOp =
|
|
|
206
227
|
| IMergeTreeInsertMsg
|
|
207
228
|
| IMergeTreeRemoveMsg
|
|
208
229
|
| IMergeTreeAnnotateMsg
|
|
209
|
-
| IMergeTreeObliterateMsg
|
|
230
|
+
| IMergeTreeObliterateMsg
|
|
231
|
+
| IMergeTreeObliterateSidedMsg;
|
|
210
232
|
|
|
211
233
|
/**
|
|
212
234
|
* @legacy
|
package/src/sequencePlace.ts
CHANGED
|
@@ -87,3 +87,19 @@ export function endpointPosAndSide(
|
|
|
87
87
|
endPos,
|
|
88
88
|
};
|
|
89
89
|
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Returns the given place in InteriorSequencePlace form.
|
|
93
|
+
*/
|
|
94
|
+
export function normalizePlace(place: SequencePlace): InteriorSequencePlace {
|
|
95
|
+
if (typeof place === "number") {
|
|
96
|
+
return { pos: place, side: Side.Before };
|
|
97
|
+
}
|
|
98
|
+
if (place === "start") {
|
|
99
|
+
return { pos: -1, side: Side.After };
|
|
100
|
+
}
|
|
101
|
+
if (place === "end") {
|
|
102
|
+
return { pos: -1, side: Side.Before };
|
|
103
|
+
}
|
|
104
|
+
return place;
|
|
105
|
+
}
|