Skip to content

Commit

Permalink
feat: LWW-Element-Set CRDT & Tests (#71)
Browse files Browse the repository at this point in the history
Co-authored-by: Oak <[email protected]>
  • Loading branch information
joaopereira12 and d-roak authored Jul 29, 2024
1 parent 192a6bf commit e04bb30
Show file tree
Hide file tree
Showing 2 changed files with 165 additions and 0 deletions.
75 changes: 75 additions & 0 deletions packages/crdt/src/builtins/LWWElementSet/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
export enum Bias {
ADD,
REMOVE
}

export class LWWElementSet<T> {
private _adds: Map<T, number>;
private _removes: Map<T, number>;
public _bias: Bias;

constructor(adds: Map<T, number>, removes: Map<T, number>, bias: Bias) {
this._adds = adds;
this._removes = removes;
this._bias = bias;
}

lookup(element: T): boolean {
const addTimestamp = this._adds.get(element);
if(addTimestamp === undefined) {
return false;
}

const removeTimestamp = this._removes.get(element);
if (removeTimestamp === undefined) {
return true;
}
if (addTimestamp > removeTimestamp) {
return true;
}
if (addTimestamp - removeTimestamp === 0 && this._bias === Bias.ADD) {
return true;
}

return false;
}

add(element: T): void {
this._adds.set(element, Date.now());
}

remove(element: T): void {
this._removes.set(element, Date.now());
}

getAdds(): Map<T, number> {
return this._adds;
}

getRemoves(): Map<T, number> {
return this._removes;
}

compare(peerSet: LWWElementSet<T>): boolean {
return (compareSets(this._adds, peerSet._adds) && compareSets(this._removes, peerSet._removes));
}

merge(peerSet: LWWElementSet<T>): void {
for (let [element, timestamp] of peerSet._adds.entries()) {
const thisTimestamp = this._adds.get(element);
if (!thisTimestamp || thisTimestamp < timestamp) {
this._adds.set(element, timestamp);
}
}
for (let [element, timestamp] of peerSet._removes.entries()) {
const thisTimestamp = this._removes.get(element);
if (!thisTimestamp || thisTimestamp < timestamp) {
this._removes.set(element, timestamp);
}
}
}
}

function compareSets<T>(set1: Map<T, number>, set2: Map<T, number>): boolean {
return (set1.size === set2.size && [...set1.keys()].every(key => set2.has(key)));
}
90 changes: 90 additions & 0 deletions packages/crdt/tests/LWWElementSet.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { describe, test, expect, beforeEach } from "vitest";
import { LWWElementSet, Bias } from "../src/builtins/LWWElementSet";

describe("LWW-Element-Set Tests", () => {
const testValues = ["walter", "jesse", "mike"];

let set1: LWWElementSet<string>;
let set2: LWWElementSet<string>;
let set3: LWWElementSet<string>;

beforeEach(() => {
set1 = new LWWElementSet<string>(new Map(), new Map(), Bias.ADD);
set2 = new LWWElementSet<string>(new Map(), new Map(), Bias.ADD);
set3 = new LWWElementSet<string>(new Map(), new Map(), Bias.REMOVE);

testValues.forEach((value) => {
set1.add(value);
set2.add(value);
set3.add(value);
});
});

test("Test Add Elements", () => {
expect(set1.lookup("gustavo")).toBe(false);

set1.add("gustavo");
expect(set1.lookup("gustavo")).toBe(true);
});

test("Test Remove Elements", () => {
expect(set1.lookup("mike")).toBe(true);

set1.getRemoves().set("mike", Date.now() + 1);

expect(set1.lookup("mike")).toBe(false);
});

test("Test Compare Sets", () => {
expect(set1.compare(set2)).toBe(true);
expect(set1.compare(set3)).toBe(true);
expect(set3.compare(set2)).toBe(true);

set1.remove("jesse");

expect(set1.compare(set2)).toBe(false);
expect(set1.compare(set3)).toBe(false);
expect(set3.compare(set2)).toBe(true);
});

describe("Test Merge Elements" , () => {
test("Merge Sets", () => {
// Adding different names to each set
set1.add("gustavo");
set2.add("saul");

expect(set1.compare(set2)).toBe(false);

set1.merge(set2);
set2.merge(set1);

expect(set1.compare(set2)).toBe(true);
});

test("Same Element, different Timestamps", () => {
const timestamp = Date.now();
set1.getAdds().set("gustavo", timestamp);
set2.getAdds().set("gustavo", timestamp + 5);

expect(set1.getAdds().get("gustavo")).toBe(timestamp);

set1.merge(set2);
set2.merge(set1);

expect(set1.getAdds().get("gustavo")).toBe(timestamp + 5);
expect(set2.getAdds().get("gustavo")).toBe(timestamp + 5);
});

test("Merge Removal Timestamps", () => {
const timestamp = Date.now();

set1.getAdds().set("gustavo", timestamp);
set2.getRemoves().set("gustavo", timestamp + 5);

set1.merge(set2);

expect(set1.lookup("gustavo")).toBe(false);
expect(set1.getRemoves().get("gustavo")).toBe(timestamp + 5);
});
});
});

0 comments on commit e04bb30

Please sign in to comment.