Skip to content

Commit

Permalink
Replace MaxCreatedAtMapByActor with VersionVector (#932)
Browse files Browse the repository at this point in the history
Refactored causal and concurrent relationship handling in text and tree
operations to use version vectors instead of MaxCreatedAtMapByActor. This
change enables more precise tracking of client Lamport times during
operations like deletion, improving concurrency management and providing
a more robust method for determining node existence.
  • Loading branch information
chacha912 authored Dec 9, 2024
1 parent 98fe2a3 commit db02eb9
Show file tree
Hide file tree
Showing 9 changed files with 187 additions and 62 deletions.
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;
}

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;
}

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

0 comments on commit db02eb9

Please sign in to comment.