Skip to content

Commit

Permalink
feat: OOR-Set CRDT & Tests (#72)
Browse files Browse the repository at this point in the history
Co-authored-by: droak <[email protected]>
  • Loading branch information
joaopereira12 and d-roak authored Aug 26, 2024
1 parent b5998ad commit 90a4f3d
Show file tree
Hide file tree
Showing 4 changed files with 123 additions and 2 deletions.
76 changes: 76 additions & 0 deletions packages/crdt/src/crdts/OORSet/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
export interface ElementTuple<T> {
element: T,
tag: number,
nodeId: string
}

/* Implementation of the Optimized Observed-Remove Set CRDT
Based on the paper: https://pages.lip6.fr/Marek.Zawirski/papers/RR-8083.pdf
*/
export class OORSet<T> {
elements: Set<ElementTuple<T>> = new Set<ElementTuple<T>>();
summary: Map<string, number> = new Map<string, number>();
nodeId: string= "";

constructor(nodeId?: string, elements?: Set<ElementTuple<T>>) {
if(nodeId !== undefined) {
this.nodeId = nodeId;
this.summary = new Map<string, number>([[this.nodeId,this.elements.size]]);
}

if(elements !== undefined) {
this.elements = elements;
}
}

lookup(element: T): boolean {
return [...this.elements].some(elem => elem.element === element);
}

add(nodeId: string, element: T): void {
let tag: number = this.summary.get(this.nodeId)! + 1;
this.summary.set(this.nodeId, tag);
this.elements.add({ element, tag, nodeId: nodeId });
}

remove(element: T): void {
for (let tuple of this.elements.values()) {
if (tuple.element === element) {
this.elements.delete(tuple); //removes element from the elements
}
}
}

// When comparing both element sets, it just needs to compare them one way because
// the "tag" and "nodeId" are going to be unique for a given element so there are
// not equal elements in the set before they're merged
compare(peerSet: OORSet<T>): boolean {
return (this.elements.size == peerSet.elements.size &&
[...this.elements].every((value) => peerSet.elements.has(value)));
}

merge(peerSet: OORSet<T>): void {
// place: [local, remote, both]
// sets: [elements, removed]
// existence: [in, notIn]
let bothInElements = [...this.elements].filter((element) => peerSet.elements.has(element));
let localInElementsRemoteNotInRemoved = [...this.elements].filter((element) =>
!peerSet.elements.has(element) && element.tag > peerSet.summary.get(element.nodeId)!
);
let localNotInRemovedRemoteInElements = [...peerSet.elements].filter((element) =>
!this.elements.has(element) && element.tag > this.summary.get(element.nodeId)!
);

this.elements = new Set<ElementTuple<T>>([...bothInElements, ...localInElementsRemoteNotInRemoved, ...localNotInRemovedRemoteInElements]);
this.elements = new Set(
[...this.elements].filter((e) =>
[...this.elements].every((e2) => e.tag > e2.tag)
)
);

// update summary
for (let e of peerSet.summary.entries()) {
this.summary.set(e[0], Math.max(e[1], this.summary.get(e[0])!));
}
}
}
3 changes: 2 additions & 1 deletion packages/crdt/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ export * from "./crdts/GSet/index.js";
export * from "./crdts/IPSet/index.js";
export * from "./crdts/LWWElementSet/index.js";
export * from "./crdts/LWWRegister/index.js";
export * from "./crdts/OORSet/index.js";
export * from "./crdts/PNCounter/index.js";
export * from "./crdts/RGA/index.js";

export * from "./cros/AddWinsSet/index.js";
export * from "./cros/AddWinsSet/index.js";
44 changes: 44 additions & 0 deletions packages/crdt/tests/OORSet.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { describe, test, expect, beforeEach } from "vitest";
import { OORSet, ElementTuple } from "../src/crdts/OORSet";

describe("OR-Set Tests", () => {
let set1: OORSet<string>;
let set2: OORSet<string>;

const testValues = ["walter", "jesse", "mike"];

beforeEach(() => {
set1 = new OORSet<string>("set1", new Set<ElementTuple<string>>());
set2 = new OORSet<string>("set2", new Set<ElementTuple<string>>());

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

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

set1.add("set1","gustavo");

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

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

set1.remove("mike");

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

test("Test Merge Elements", () => {
expect(set1.compare(set2)).toBe(false);

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

expect(set1.compare(set2)).toBe(true);
});
});
2 changes: 1 addition & 1 deletion packages/crdt/tests/RGA.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,4 +88,4 @@ describe("Replicable Growable Array Tests", () => {
peerRGA.merge(rga);
expect(peerRGA.getArray()).toEqual(rga.getArray());
});
});
});

0 comments on commit 90a4f3d

Please sign in to comment.