From 90a4f3d98565105542534ffa2d80e9fc730029a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Pereira?= <77340776+joaopereira12@users.noreply.github.com> Date: Mon, 26 Aug 2024 14:53:22 +0900 Subject: [PATCH] feat: OOR-Set CRDT & Tests (#72) Co-authored-by: droak --- packages/crdt/src/crdts/OORSet/index.ts | 76 +++++++++++++++++++++++++ packages/crdt/src/index.ts | 3 +- packages/crdt/tests/OORSet.test.ts | 44 ++++++++++++++ packages/crdt/tests/RGA.test.ts | 2 +- 4 files changed, 123 insertions(+), 2 deletions(-) create mode 100644 packages/crdt/src/crdts/OORSet/index.ts create mode 100644 packages/crdt/tests/OORSet.test.ts diff --git a/packages/crdt/src/crdts/OORSet/index.ts b/packages/crdt/src/crdts/OORSet/index.ts new file mode 100644 index 00000000..0b057693 --- /dev/null +++ b/packages/crdt/src/crdts/OORSet/index.ts @@ -0,0 +1,76 @@ +export interface ElementTuple { + 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 { + elements: Set> = new Set>(); + summary: Map = new Map(); + nodeId: string= ""; + + constructor(nodeId?: string, elements?: Set>) { + if(nodeId !== undefined) { + this.nodeId = nodeId; + this.summary = new Map([[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): boolean { + return (this.elements.size == peerSet.elements.size && + [...this.elements].every((value) => peerSet.elements.has(value))); + } + + merge(peerSet: OORSet): 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>([...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])!)); + } + } +} \ No newline at end of file diff --git a/packages/crdt/src/index.ts b/packages/crdt/src/index.ts index b74f7c8c..a3131c2a 100644 --- a/packages/crdt/src/index.ts +++ b/packages/crdt/src/index.ts @@ -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"; \ No newline at end of file diff --git a/packages/crdt/tests/OORSet.test.ts b/packages/crdt/tests/OORSet.test.ts new file mode 100644 index 00000000..c6b0324e --- /dev/null +++ b/packages/crdt/tests/OORSet.test.ts @@ -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; + let set2: OORSet; + + const testValues = ["walter", "jesse", "mike"]; + + beforeEach(() => { + set1 = new OORSet("set1", new Set>()); + set2 = new OORSet("set2", new Set>()); + + 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); + }); +}); \ No newline at end of file diff --git a/packages/crdt/tests/RGA.test.ts b/packages/crdt/tests/RGA.test.ts index 9c2dae8b..fab68526 100644 --- a/packages/crdt/tests/RGA.test.ts +++ b/packages/crdt/tests/RGA.test.ts @@ -88,4 +88,4 @@ describe("Replicable Growable Array Tests", () => { peerRGA.merge(rga); expect(peerRGA.getArray()).toEqual(rga.getArray()); }); -}); +}); \ No newline at end of file