Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Replace maxCreatedAtMap with version vector for causal-concurrent operations #932

Merged
merged 1 commit into from
Dec 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion packages/sdk/src/document/change/change.ts
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,11 @@ export class Change<P extends Indexable> {
const reverseOps: Array<HistoryOperation<P>> = [];

for (const operation of this.operations) {
const executionResult = operation.execute(root, source);
const executionResult = operation.execute(
root,
source,
this.id.getVersionVector(),
);
// NOTE(hackerwins): If the element was removed while executing undo/redo,
// the operation is not executed and executionResult is undefined.
if (!executionResult) continue;
Expand Down
59 changes: 41 additions & 18 deletions packages/sdk/src/document/crdt/rga_tree_split.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,11 @@ import { SplayNode, SplayTree } from '@yorkie-js-sdk/src/util/splay_tree';
import { LLRBTree } from '@yorkie-js-sdk/src/util/llrb_tree';
import {
InitialTimeTicket,
MaxTimeTicket,
MaxLamport,
TimeTicket,
TimeTicketStruct,
} from '@yorkie-js-sdk/src/document/time/ticket';
import { VersionVector } from '@yorkie-js-sdk/src/document/time/version_vector';
import { GCChild, GCPair, GCParent } from '@yorkie-js-sdk/src/document/crdt/gc';
import { Code, YorkieError } from '@yorkie-js-sdk/src/util/error';

Expand Down Expand Up @@ -439,12 +440,17 @@ export class RGATreeSplitNode<T extends RGATreeSplitValue>
/**
* `canDelete` checks if node is able to delete.
*/
public canDelete(editedAt: TimeTicket, maxCreatedAt: TimeTicket): boolean {
public canDelete(
editedAt: TimeTicket,
maxCreatedAt: TimeTicket | undefined,
clientLamportAtChange: bigint,
): boolean {
const justRemoved = !this.removedAt;
if (
!this.getCreatedAt().after(maxCreatedAt) &&
(!this.removedAt || editedAt.after(this.removedAt))
) {
const nodeExisted = maxCreatedAt
? !this.getCreatedAt().after(maxCreatedAt)
: this.getCreatedAt().getLamport() <= clientLamportAtChange;

if (nodeExisted && (!this.removedAt || editedAt.after(this.removedAt))) {
return justRemoved;
}

Expand All @@ -454,11 +460,16 @@ export class RGATreeSplitNode<T extends RGATreeSplitValue>
/**
* `canStyle` checks if node is able to set style.
*/
public canStyle(editedAt: TimeTicket, maxCreatedAt: TimeTicket): boolean {
return (
!this.getCreatedAt().after(maxCreatedAt) &&
(!this.removedAt || editedAt.after(this.removedAt))
);
public canStyle(
editedAt: TimeTicket,
maxCreatedAt: TimeTicket | undefined,
clientLamportAtChange: bigint,
): boolean {
const nodeExisted = maxCreatedAt
? !this.getCreatedAt().after(maxCreatedAt)
: this.getCreatedAt().getLamport() <= clientLamportAtChange;

return nodeExisted && (!this.removedAt || editedAt.after(this.removedAt));
}

/**
Expand Down Expand Up @@ -552,6 +563,7 @@ export class RGATreeSplit<T extends RGATreeSplitValue> implements GCParent {
editedAt: TimeTicket,
value?: T,
maxCreatedAtMapByActor?: Map<string, TimeTicket>,
versionVector?: VersionVector,
): [
RGATreeSplitPos,
Map<string, TimeTicket>,
Expand All @@ -568,6 +580,7 @@ export class RGATreeSplit<T extends RGATreeSplitValue> implements GCParent {
nodesToDelete,
editedAt,
maxCreatedAtMapByActor,
versionVector,
);

const caretID = toRight ? toRight.getID() : toLeft.getID();
Expand Down Expand Up @@ -878,6 +891,7 @@ export class RGATreeSplit<T extends RGATreeSplitValue> implements GCParent {
candidates: Array<RGATreeSplitNode<T>>,
editedAt: TimeTicket,
maxCreatedAtMapByActor?: Map<string, TimeTicket>,
versionVector?: VersionVector,
): [
Array<ValueChange<T>>,
Map<string, TimeTicket>,
Expand All @@ -894,6 +908,7 @@ export class RGATreeSplit<T extends RGATreeSplitValue> implements GCParent {
candidates,
editedAt,
maxCreatedAtMapByActor,
versionVector,
);

const createdAtMapByActor = new Map();
Expand Down Expand Up @@ -922,8 +937,8 @@ export class RGATreeSplit<T extends RGATreeSplitValue> implements GCParent {
candidates: Array<RGATreeSplitNode<T>>,
editedAt: TimeTicket,
maxCreatedAtMapByActor?: Map<string, TimeTicket>,
versionVector?: VersionVector,
): [Array<RGATreeSplitNode<T>>, Array<RGATreeSplitNode<T> | undefined>] {
const isRemote = !!maxCreatedAtMapByActor;
const nodesToDelete: Array<RGATreeSplitNode<T>> = [];
const nodesToKeep: Array<RGATreeSplitNode<T> | undefined> = [];

Expand All @@ -932,14 +947,22 @@ export class RGATreeSplit<T extends RGATreeSplitValue> implements GCParent {

for (const node of candidates) {
const actorID = node.getCreatedAt().getActorID();

const maxCreatedAt = isRemote
? maxCreatedAtMapByActor!.has(actorID)
let maxCreatedAt: TimeTicket | undefined;
let clientLamportAtChange = 0n;
if (versionVector === undefined && maxCreatedAtMapByActor === undefined) {
// Local edit - use version vector comparison
clientLamportAtChange = MaxLamport;
} else if (versionVector!.size() > 0) {
clientLamportAtChange = versionVector!.get(actorID)
? versionVector!.get(actorID)!
: 0n;
} else {
maxCreatedAt = maxCreatedAtMapByActor!.has(actorID)
? maxCreatedAtMapByActor!.get(actorID)
: InitialTimeTicket
: MaxTimeTicket;
: InitialTimeTicket;
}
chacha912 marked this conversation as resolved.
Show resolved Hide resolved

if (node.canDelete(editedAt, maxCreatedAt!)) {
if (node.canDelete(editedAt, maxCreatedAt, clientLamportAtChange)) {
nodesToDelete.push(node);
} else {
nodesToKeep.push(node);
Expand Down
28 changes: 20 additions & 8 deletions packages/sdk/src/document/crdt/text.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,11 @@
*/

import {
MaxLamport,
InitialTimeTicket,
MaxTimeTicket,
TimeTicket,
} from '@yorkie-js-sdk/src/document/time/ticket';
import { VersionVector } from '@yorkie-js-sdk/src/document/time/version_vector';
import { Indexable } from '@yorkie-js-sdk/src/document/document';
import { RHT, RHTNode } from '@yorkie-js-sdk/src/document/crdt/rht';
import { CRDTElement } from '@yorkie-js-sdk/src/document/crdt/element';
Expand Down Expand Up @@ -224,6 +225,7 @@ export class CRDTText<A extends Indexable = Indexable> extends CRDTElement {
editedAt: TimeTicket,
attributes?: Record<string, string>,
maxCreatedAtMapByActor?: Map<string, TimeTicket>,
versionVector?: VersionVector,
): [
Map<string, TimeTicket>,
Array<TextChange<A>>,
Expand All @@ -243,6 +245,7 @@ export class CRDTText<A extends Indexable = Indexable> extends CRDTElement {
editedAt,
crdtTextValue,
maxCreatedAtMapByActor,
versionVector,
);

const changes: Array<TextChange<A>> = valueChanges.map((change) => ({
Expand Down Expand Up @@ -278,6 +281,7 @@ export class CRDTText<A extends Indexable = Indexable> extends CRDTElement {
attributes: Record<string, string>,
editedAt: TimeTicket,
maxCreatedAtMapByActor?: Map<string, TimeTicket>,
versionVector?: VersionVector,
): [Map<string, TimeTicket>, Array<GCPair>, Array<TextChange<A>>] {
// 01. split nodes with from and to
const [, toRight] = this.rgaTreeSplit.findNodeWithSplit(range[1], editedAt);
Expand All @@ -294,14 +298,22 @@ export class CRDTText<A extends Indexable = Indexable> extends CRDTElement {

for (const node of nodes) {
const actorID = node.getCreatedAt().getActorID();
let maxCreatedAt: TimeTicket | undefined;
let clientLamportAtChange = 0n;
if (versionVector === undefined && maxCreatedAtMapByActor === undefined) {
// Local edit - use version vector comparison
clientLamportAtChange = MaxLamport;
} else if (versionVector!.size() > 0) {
clientLamportAtChange = versionVector!.get(actorID)
? versionVector!.get(actorID)!
: 0n;
} else {
maxCreatedAt = maxCreatedAtMapByActor!.has(actorID)
? maxCreatedAtMapByActor!.get(actorID)
: InitialTimeTicket;
}
chacha912 marked this conversation as resolved.
Show resolved Hide resolved

const maxCreatedAt = maxCreatedAtMapByActor?.size
? maxCreatedAtMapByActor!.has(actorID)
? maxCreatedAtMapByActor!.get(actorID)!
: InitialTimeTicket
: MaxTimeTicket;

if (node.canStyle(editedAt, maxCreatedAt)) {
if (node.canStyle(editedAt, maxCreatedAt, clientLamportAtChange)) {
const maxCreatedAt = createdAtMapByActor.get(actorID);
const createdAt = node.getCreatedAt();
if (!maxCreatedAt || createdAt.after(maxCreatedAt)) {
Expand Down
Loading
Loading