Skip to content

Commit

Permalink
Merge branch 'next' into MainToNext
Browse files Browse the repository at this point in the history
  • Loading branch information
heliocliu authored Mar 11, 2022
2 parents f8566fc + 85b2ff6 commit fb61046
Show file tree
Hide file tree
Showing 12 changed files with 180 additions and 87 deletions.
15 changes: 7 additions & 8 deletions api-report/merge-tree.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,9 +56,7 @@ export abstract class BaseSegment extends MergeNode implements ISegment {
// (undocumented)
propertyManager?: PropertiesManager;
// (undocumented)
removedClientId?: number;
// (undocumented)
removedClientOverlap?: number[];
removedClientIds?: number[];
// (undocumented)
removedSeq?: number;
// (undocumented)
Expand Down Expand Up @@ -679,15 +677,13 @@ export interface IRelativePosition {
// @public (undocumented)
export interface IRemovalInfo {
// (undocumented)
removedClientId?: number;
// (undocumented)
removedClientOverlap?: number[];
removedClientIds: number[];
// (undocumented)
removedSeq?: number;
removedSeq: number;
}

// @public
export interface ISegment extends IMergeNodeCommon, IRemovalInfo {
export interface ISegment extends IMergeNodeCommon, Partial<IRemovalInfo> {
ack(segmentGroup: SegmentGroup, opArgs: IMergeTreeDeltaOpArgs, mergeTree: MergeTree): boolean;
// (undocumented)
addProperties(newProps: PropertySet, op?: ICombiningOp, seq?: number, collabWindow?: CollaborationWindow): PropertySet | undefined;
Expand Down Expand Up @@ -1486,6 +1482,9 @@ export class TextSegment extends BaseSegment {
readonly type = "TextSegment";
}

// @public (undocumented)
export function toRemovalInfo(maybe: Partial<IRemovalInfo> | undefined): IRemovalInfo | undefined;

// @public (undocumented)
export class TrackingGroup {
constructor();
Expand Down
4 changes: 4 additions & 0 deletions api-report/sequence.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,8 @@ export interface ISharedIntervalCollection<TInterval extends ISerializableInterv

// @public
export interface ISharedSegmentSequenceEvents extends ISharedObjectEvents {
// (undocumented)
(event: "createIntervalCollection", listener: (label: string, local: boolean, target: IEventThisPlaceHolder) => void): any;
// (undocumented)
(event: "sequenceDelta", listener: (event: SequenceDeltaEvent, target: IEventThisPlaceHolder) => void): any;
// (undocumented)
Expand Down Expand Up @@ -537,6 +539,8 @@ export abstract class SharedSegmentSequence<T extends ISegment> extends SharedOb
getCurrentSeq(): number;
// (undocumented)
getIntervalCollection(label: string): IntervalCollection<SequenceInterval>;
// (undocumented)
getIntervalCollectionLabels(): IterableIterator<string>;
getLength(): number;
getPosition(segment: ISegment): number;
// (undocumented)
Expand Down
81 changes: 36 additions & 45 deletions packages/dds/merge-tree/src/mergeTree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,15 +104,21 @@ export interface IHierBlock extends IMergeBlock {
}

export interface IRemovalInfo {
removedSeq?: number;
removedClientId?: number;
removedClientOverlap?: number[];
removedSeq: number;
removedClientIds: number[];
}
export function toRemovalInfo(maybe: Partial<IRemovalInfo> | undefined): IRemovalInfo | undefined {
if (maybe?.removedClientIds !== undefined && maybe?.removedSeq !== undefined) {
return maybe as IRemovalInfo;
}
assert(maybe?.removedClientIds === undefined && maybe?.removedSeq === undefined,
"both removedClientIds and removedSeq should be set or not set");
}

/**
* A segment representing a portion of the merge tree.
*/
export interface ISegment extends IMergeNodeCommon, IRemovalInfo {
export interface ISegment extends IMergeNodeCommon, Partial<IRemovalInfo> {
readonly type: string;
readonly segmentGroups: SegmentGroupCollection;
readonly trackingCollection: TrackingGroupCollection;
Expand Down Expand Up @@ -482,8 +488,7 @@ export abstract class BaseSegment extends MergeNode implements ISegment {
public clientId: number = LocalClientId;
public seq: number = UniversalSequenceNumber;
public removedSeq?: number;
public removedClientId?: number;
public removedClientOverlap?: number[];
public removedClientIds?: number[];
public readonly segmentGroups: SegmentGroupCollection = new SegmentGroupCollection(this);
public readonly trackingCollection: TrackingGroupCollection = new TrackingGroupCollection(this);
public propertyManager?: PropertiesManager;
Expand Down Expand Up @@ -521,7 +526,7 @@ export abstract class BaseSegment extends MergeNode implements ISegment {
b.clientId = this.clientId;
// TODO: deep clone properties
b.properties = clone(this.properties);
b.removedClientId = this.removedClientId;
b.removedClientIds = this.removedClientIds?.slice();
// TODO: copy removed client overlap and branch removal info
b.removedSeq = this.removedSeq;
b.seq = this.seq;
Expand Down Expand Up @@ -555,10 +560,9 @@ export abstract class BaseSegment extends MergeNode implements ISegment {
return true;

case MergeTreeDeltaType.REMOVE:
// eslint-disable-next-line @typescript-eslint/no-this-alias
const removalInfo: IRemovalInfo = this;
assert(!!removalInfo, 0x046 /* "On remove ack, missing removal info!" */);
assert(!!removalInfo.removedSeq, 0x047 /* "On remove ack, missing removed sequence number!" */);

const removalInfo: IRemovalInfo | undefined = toRemovalInfo(this);
assert(removalInfo !== undefined, 0x046 /* "On remove ack, missing removal info!" */);
this.localRemovedSeq = undefined;
if (removalInfo.removedSeq === UnassignedSequenceNumber) {
removalInfo.removedSeq = opArgs.sequencedMessage!.sequenceNumber;
Expand All @@ -584,15 +588,12 @@ export abstract class BaseSegment extends MergeNode implements ISegment {
// but this ordinal meets all the necessary invariants for now.
leafSegment.ordinal = this.ordinal + String.fromCharCode(0);

leafSegment.removedClientId = this.removedClientId;
leafSegment.removedClientIds = this.removedClientIds?.slice();
leafSegment.removedSeq = this.removedSeq;
leafSegment.localRemovedSeq = this.localRemovedSeq;
leafSegment.seq = this.seq;
leafSegment.localSeq = this.localSeq;
leafSegment.clientId = this.clientId;
if (this.removedClientOverlap) {
leafSegment.removedClientOverlap = [...this.removedClientOverlap];
}
this.segmentGroups.copyTo(leafSegment);
this.trackingCollection.copyTo(leafSegment);
if (this.localRefs) {
Expand Down Expand Up @@ -1100,8 +1101,8 @@ export class MergeTree {
}

public localNetLength(segment: ISegment) {
const removalInfo: IRemovalInfo = segment;
if (removalInfo.removedSeq !== undefined) {
const removalInfo = toRemovalInfo(segment);
if (removalInfo !== undefined) {
return 0;
} else {
return segment.cachedLength;
Expand Down Expand Up @@ -1467,9 +1468,9 @@ export class MergeTree {
return node.partialLengths!.getPartialLength(refSeq, clientId);
} else {
const segment = node;
const removalInfo: IRemovalInfo = segment;
const removalInfo = toRemovalInfo(segment);

if(removalInfo.removedSeq !== undefined
if(removalInfo !== undefined
&& removalInfo.removedSeq !== UnassignedSequenceNumber
&& removalInfo.removedSeq <= refSeq) {
// this segment is a tombstone eligible for zamboni
Expand All @@ -1480,12 +1481,8 @@ export class MergeTree {
if (((segment.clientId === clientId) ||
((segment.seq !== UnassignedSequenceNumber) && (segment.seq! <= refSeq)))) {
// Segment happened by reference sequence number or segment from requesting client
if (removalInfo.removedSeq !== undefined) {
if (
removalInfo.removedClientId === clientId
|| (removalInfo.removedClientOverlap
&& removalInfo.removedClientOverlap.includes(clientId))
) {
if (removalInfo !== undefined) {
if (removalInfo.removedClientIds.includes(clientId)) {
return 0;
} else {
return segment.cachedLength;
Expand All @@ -1497,7 +1494,7 @@ export class MergeTree {
// the segment was inserted and removed before the
// this context, so it will never exist for this
// context
if(removalInfo.removedSeq !== undefined
if(removalInfo !== undefined
&& removalInfo.removedSeq !== UnassignedSequenceNumber) {
return undefined;
}
Expand Down Expand Up @@ -2305,13 +2302,6 @@ export class MergeTree {
}
}

private addOverlappingClient(removalInfo: IRemovalInfo, clientId: number) {
if (!removalInfo.removedClientOverlap) {
removalInfo.removedClientOverlap = <number[]>[];
}
removalInfo.removedClientOverlap.push(clientId);
}

/**
* Annotate a range with properties
* @param start - The inclusive start position of the range to annotate
Expand Down Expand Up @@ -2382,21 +2372,24 @@ export class MergeTree {
const savedLocalRefs: LocalReferenceCollection[] = [];
const localSeq = seq === UnassignedSequenceNumber ? ++this.collabWindow.localSeq : undefined;
const markRemoved = (segment: ISegment, pos: number, _start: number, _end: number) => {
const removalInfo: IRemovalInfo = segment;
if (removalInfo.removedSeq !== undefined) {
const existingRemovalInfo = toRemovalInfo(segment);
if (existingRemovalInfo !== undefined) {
_overwrite = true;
if (removalInfo.removedSeq === UnassignedSequenceNumber) {
// replace because comes later
removalInfo.removedClientId = clientId;
removalInfo.removedSeq = seq;
if (existingRemovalInfo.removedSeq === UnassignedSequenceNumber) {
// we removed this locally, but someone else removed it first
// so put them at the head of the list
// the list isn't ordered, but we
// keep first removal at the head.
existingRemovalInfo.removedClientIds.unshift(clientId);
existingRemovalInfo.removedSeq = seq;
segment.localRemovedSeq = undefined;
} else {
// Do not replace earlier sequence number for remove
this.addOverlappingClient(removalInfo, clientId);
existingRemovalInfo.removedClientIds.push(clientId);
}
} else {
removalInfo.removedClientId = clientId;
removalInfo.removedSeq = seq;
segment.removedClientIds = [clientId];
segment.removedSeq = seq;
segment.localRemovedSeq = localSeq;

removedSegments.push({ segment });
Expand All @@ -2408,9 +2401,7 @@ export class MergeTree {

// Save segment so can assign removed sequence number when acked by server
if (this.collabWindow.collaborating) {
// Use removal information
const _removalInfo: IRemovalInfo = segment;
if (_removalInfo.removedSeq === UnassignedSequenceNumber && clientId === this.collabWindow.clientId) {
if (segment.removedSeq === UnassignedSequenceNumber && clientId === this.collabWindow.clientId) {
segmentGroup = this.addToPendingList(segment, segmentGroup, localSeq);
} else {
if (MergeTree.options.zamboniSegments) {
Expand Down
42 changes: 21 additions & 21 deletions packages/dds/merge-tree/src/partialLengths.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
IRemovalInfo,
ISegment,
MergeTree,
toRemovalInfo,
} from "./mergeTree";

interface IOverlapClient {
Expand Down Expand Up @@ -81,7 +82,7 @@ export class PartialSequenceLengths {
collabWindow: CollaborationWindow,
recur = false) {
let combinedPartialLengths = new PartialSequenceLengths(collabWindow.minSeq);
PartialSequenceLengths.fromLeaves(mergeTree, combinedPartialLengths, block, collabWindow);
PartialSequenceLengths.fromLeaves(combinedPartialLengths, block, collabWindow);
let prevPartial: PartialSequenceLength | undefined;

function cloneOverlapRemoveClients(oldTree: RedBlackTree<number, IOverlapClient> | undefined) {
Expand Down Expand Up @@ -209,35 +210,33 @@ export class PartialSequenceLengths {
}

private static fromLeaves(
mergeTree: MergeTree, combinedPartialLengths: PartialSequenceLengths,
combinedPartialLengths: PartialSequenceLengths,
block: IMergeBlock, collabWindow: CollaborationWindow) {
combinedPartialLengths.minLength = 0;
combinedPartialLengths.segmentCount = block.childCount;

function seqLTE(seq: number, minSeq: number) {
return (seq !== UnassignedSequenceNumber) && (seq <= minSeq);
function seqLTE(seq: number | undefined, minSeq: number) {
return seq !== undefined && seq !== UnassignedSequenceNumber && seq <= minSeq;
}

for (let i = 0; i < block.childCount; i++) {
const child = block.children[i];
if (child.isLeaf()) {
// Leaf segment
const segment = child;
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
if (seqLTE(segment.seq!, collabWindow.minSeq)) {
if (seqLTE(segment.seq, collabWindow.minSeq)) {
combinedPartialLengths.minLength += segment.cachedLength;
} else {
if (segment.seq !== UnassignedSequenceNumber) {
PartialSequenceLengths.insertSegment(combinedPartialLengths, segment);
}
}
const removalInfo: IRemovalInfo = segment;
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
if (seqLTE(removalInfo.removedSeq!, collabWindow.minSeq)) {
const removalInfo = toRemovalInfo(segment);
if (seqLTE(removalInfo?.removedSeq, collabWindow.minSeq)) {
combinedPartialLengths.minLength -= segment.cachedLength;
} else {
if ((removalInfo.removedSeq !== undefined) &&
(removalInfo.removedSeq !== UnassignedSequenceNumber)) {
if (removalInfo !== undefined
&& removalInfo.removedSeq !== UnassignedSequenceNumber) {
PartialSequenceLengths.insertSegment(
combinedPartialLengths,
segment,
Expand Down Expand Up @@ -300,14 +299,15 @@ export class PartialSequenceLengths {
let removeClientOverlap: number[] | undefined;

if (removalInfo) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
seq = removalInfo.removedSeq!;
seq = removalInfo.removedSeq;
segmentLen = -segmentLen;
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
clientId = removalInfo.removedClientId!;
if (removalInfo.removedClientOverlap) {
removeClientOverlap = removalInfo.removedClientOverlap;
}
// this code still assume removed client id and
// overlap clients are separate. so we need to pull
// then apart first.
clientId = removalInfo.removedClientIds[0];
removeClientOverlap = removalInfo.removedClientIds.length > 1
? removalInfo.removedClientIds.slice(1)
: undefined;
}

const seqPartials = combinedPartialLengths.partialLengths;
Expand Down Expand Up @@ -419,14 +419,14 @@ export class PartialSequenceLengths {
segCount += branchPartialLengths.segmentCount;
} else {
const segment = child;
const removalInfo: IRemovalInfo = segment;
const removalInfo = toRemovalInfo(segment);

if (segment.seq === seq) {
if (removalInfo.removedSeq !== seq) {
if (removalInfo?.removedSeq !== seq) {
seqSeglen += segment.cachedLength;
}
} else {
if (removalInfo.removedSeq === seq) {
if (removalInfo?.removedSeq === seq) {
seqSeglen -= segment.cachedLength;
}
}
Expand Down
4 changes: 4 additions & 0 deletions packages/dds/merge-tree/src/snapshotChunks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,11 @@ export interface IJSONSegmentWithMergeInfo {
json: IJSONSegment;
client?: string;
seq?: number;
/**
* @deprecated - use removedClientIds instead. this only exists for back-compat
*/
removedClient?: string;
removedClientIds?: string[];
removedSeq?: number;
}

Expand Down
10 changes: 9 additions & 1 deletion packages/dds/merge-tree/src/snapshotLoader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,8 +102,16 @@ export class SnapshotLoader {
if (spec.removedSeq !== undefined) {
seg.removedSeq = spec.removedSeq;
}
// this format had a bug where it didn't store all the overlap clients
// this is for back compat, so we change the singular id to an array
// this will only cause problems if there is an overlapping delete
// spanning the snapshot, which should be rare
if (spec.removedClient !== undefined) {
seg.removedClientId = this.client.getOrAddShortClientId(spec.removedClient);
seg.removedClientIds = [this.client.getOrAddShortClientId(spec.removedClient)];
}
if (spec.removedClientIds !== undefined) {
seg.removedClientIds = spec.removedClientIds?.map(
(sid)=> this.client.getOrAddShortClientId(sid));
}
} else {
seg = this.client.specToSegment(spec);
Expand Down
10 changes: 8 additions & 2 deletions packages/dds/merge-tree/src/snapshotV1.ts
Original file line number Diff line number Diff line change
Expand Up @@ -218,8 +218,14 @@ export class SnapshotV1 {
assert(segment.removedSeq !== UnassignedSequenceNumber && segment.removedSeq > minSeq,
0x065 /* "On removal info preservation, segment has invalid removed sequence number!" */);
raw.removedSeq = segment.removedSeq;
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
raw.removedClient = this.getLongClientId(segment.removedClientId!);

// back compat for when we split overlap and removed client
raw.removedClient =
segment.removedClientIds !== undefined
? this.getLongClientId(segment.removedClientIds[0])
: undefined;

raw.removedClientIds = segment.removedClientIds?.map((id)=>this.getLongClientId(id));
}

// Sanity check that we are preserving either the seq < minSeq or a removed segment's info.
Expand Down
Loading

0 comments on commit fb61046

Please sign in to comment.