diff --git a/packages/crdt/package.json b/packages/crdt/package.json index 41a5bfb8..23332bea 100644 --- a/packages/crdt/package.json +++ b/packages/crdt/package.json @@ -1,39 +1,35 @@ { - "name": "@topology-foundation/crdt", - "version": "0.0.23-5", - "license": "MIT", - "repository": { - "type": "git", - "url": "git+https://github.com/topology-foundation/ts-topology.git" - }, - "type": "module", - "types": "./dist/src/index.d.ts", - "files": [ - "src", - "dist", - "!dist/test", - "!**/*.tsbuildinfo" - ], - "exports": { - ".": { - "types": "./dist/src/index.d.ts", - "import": "./dist/src/index.js" - }, - "./wasm": { - "types": "./dist/src/index.d.ts", - "import": "./src/index.asc.ts" - } - }, - "scripts": { - "asbuild": "yarn asbuild:debug && yarn asbuild:release", - "asbuild:debug": "asc --config asconfig.json --target debug", - "asbuild:release": "asc --config asconfig.json --target release", - "build": "tsc -b", - "clean": "rm -rf dist/ node_modules/", - "prepack": "tsc -b", - "test": "vitest" - }, - "devDependencies": { - "assemblyscript": "^0.27.29" - } + "name": "@topology-foundation/crdt", + "version": "0.0.23-5", + "license": "MIT", + "repository": { + "type": "git", + "url": "git+https://github.com/topology-foundation/ts-topology.git" + }, + "type": "module", + "types": "./dist/src/index.d.ts", + "files": ["src", "dist", "!dist/test", "!**/*.tsbuildinfo"], + "exports": { + ".": { + "types": "./dist/src/index.d.ts", + "import": "./dist/src/index.js" + }, + "./wasm": { + "types": "./dist/src/index.d.ts", + "import": "./src/index.asc.ts" + } + }, + "scripts": { + "asbuild": "yarn asbuild:debug && yarn asbuild:release", + "asbuild:debug": "asc --config asconfig.json --target debug", + "asbuild:release": "asc --config asconfig.json --target release", + "build": "tsc -b", + "clean": "rm -rf dist/ node_modules/", + "prepack": "tsc -b", + "test": "vitest" + }, + "devDependencies": { + "@topology-foundation/object": "0.0.23-5", + "assemblyscript": "^0.27.29" + } } diff --git a/packages/crdt/src/cros/AddWinsSet/index.ts b/packages/crdt/src/cros/AddWinsSet/index.ts new file mode 100644 index 00000000..795d7e88 --- /dev/null +++ b/packages/crdt/src/cros/AddWinsSet/index.ts @@ -0,0 +1,65 @@ +import { ActionType, HashGraph, Operation, OperationType } from "@topology-foundation/object"; + +/// AddWinsSet with support for state and op changes +export class AddWinsSet { + state: Map; + hashGraph: HashGraph; + + constructor(nodeId: string) { + this.state = new Map(); + this.hashGraph = new HashGraph(this.resolveConflicts, nodeId); + } + + resolveConflicts(op1: Operation, op2: Operation): ActionType { + if (op1.type !== op2.type && op1.value === op2.value) { + return op1.type === OperationType.Add ? ActionType.DropRight : ActionType.DropLeft; + } + return ActionType.Nop; + } + + add(value: T): void { + const op = new Operation(OperationType.Add, value); + this.state.set(value, (this.state.get(value) || 0) + 1); + } + + remove(value: T): void { + const op = new Operation(OperationType.Remove, value); + this.add(value); + } + + getValue(value: T): number { + return this.state.get(value) || 0; + } + + isInSet(value: T): boolean { + const count = this.getValue(value); + return count > 0 && count % 2 === 1; + } + + values(): T[] { + return Array.from(this.state.entries()) + .filter(([_, count]) => count % 2 === 1) + .map(([value, _]) => value); + } + + merge(other: AddWinsSet): void { + for (const [value, count] of other.state) { + this.state.set(value, Math.max(this.getValue(value), count)); + } + } + + read(): T[] { + const operations = this.hashGraph.linearizeOps(); + const tempCounter = new AddWinsSet(""); + + for (const op of operations) { + if (op.type === OperationType.Add) { + tempCounter.add(op.value); + } else { + tempCounter.remove(op.value); + } + } + + return tempCounter.values(); + } +} diff --git a/packages/crdt/src/index.ts b/packages/crdt/src/index.ts index 26e12ebd..b74f7c8c 100644 --- a/packages/crdt/src/index.ts +++ b/packages/crdt/src/index.ts @@ -6,3 +6,5 @@ export * from "./crdts/LWWElementSet/index.js"; export * from "./crdts/LWWRegister/index.js"; export * from "./crdts/PNCounter/index.js"; export * from "./crdts/RGA/index.js"; + +export * from "./cros/AddWinsSet/index.js"; diff --git a/packages/crdt/tests/AddWinsSet.test.ts b/packages/crdt/tests/AddWinsSet.test.ts new file mode 100644 index 00000000..cb821d9e --- /dev/null +++ b/packages/crdt/tests/AddWinsSet.test.ts @@ -0,0 +1,339 @@ +import { describe, test, expect, beforeEach } from "vitest"; +import { AddWinsSet } from "../src/cros/AddWinsSet/index.js"; +import { Operation, OperationType } from "@topology-foundation/object"; + +describe("HashGraph for AddWinSet tests", () => { + let cro: AddWinsSet; + let op0: Operation; + let vertexHash0: string + const peerId = "peerId0" + + beforeEach(() => { + cro = new AddWinsSet("peer0"); + op0 = new Operation(OperationType.Nop, 0); + vertexHash0 = cro.hashGraph.rootHash; + }); + + test("Test: Add Two Vertices", () => { + + /* + V1:NOP <- V2:ADD(1) <- V2:REMOVE(1) + */ + let op1: Operation = new Operation(OperationType.Add, 1); + let deps1: string[] = [vertexHash0] + let vertexHash1 = cro.hashGraph.addVertex(op1, deps1, peerId); + let linearOps = cro.hashGraph.linearizeOps(); + expect(linearOps).toEqual([op0, op1]); + + // Add second vertex + let op2: Operation = new Operation(OperationType.Remove, 1); + let deps2: string[] = [vertexHash1]; + let vertexHash2 = cro.hashGraph.addVertex(op2, deps2, peerId); + linearOps = cro.hashGraph.linearizeOps(); + let orderArray = cro.hashGraph.topologicalSort(); + expect(linearOps).toEqual([op0, op1, op2]); + }); + + test("Test: Add Two Concurrent Vertices With Same Value", () => { + /* + _ V2:REMOVE(1) + V1:ADD(1) / + \ _ V3:ADD(1) + */ + + let op1: Operation = new Operation(OperationType.Add, 1);; + let deps1: string[] = [vertexHash0] + let vertexHash1 = cro.hashGraph.addVertex(op1, deps1, peerId); + + let linearOps = cro.hashGraph.linearizeOps(); + expect(linearOps).toEqual([op0, op1]); + + // Add second vertex + let op2: Operation = new Operation(OperationType.Remove, 1); + let deps2: string[] = [vertexHash1]; + let vertexHash2 = cro.hashGraph.addVertex(op2, deps2, peerId); + + linearOps = cro.hashGraph.linearizeOps(); + expect(linearOps).toEqual([op0, op1, op2]); + + // Add the third vertex V3 concurrent with V2 + let op3: Operation = new Operation(OperationType.Add, 1); + let deps3: string[] = [vertexHash1]; + let vertexHash3 = cro.hashGraph.addVertex(op3, deps3, peerId); + + linearOps = cro.hashGraph.linearizeOps(); + expect(linearOps).toEqual([op0, op1, op3]); + }); + + test("Test: Add Two Concurrent Vertices With Different Values", () => { + /* + _ V2:REMOVE(1) + V1:ADD(1) / + \ _ V3:ADD(2) + */ + + let op1: Operation = new Operation(OperationType.Add, 1); + let deps1: string[] = [vertexHash0] + let vertexHash1 = cro.hashGraph.addVertex(op1, deps1, peerId); + let linearOps = cro.hashGraph.linearizeOps(); + expect(linearOps).toEqual([op0, op1]); + + // Add second vertex + let op2: Operation = new Operation(OperationType.Remove, 1); + let deps2: string[] = [vertexHash1]; + let vertexHash2 = cro.hashGraph.addVertex(op2, deps2, peerId); + linearOps = cro.hashGraph.linearizeOps(); + expect(linearOps).toEqual([op0, op1, op2]); + + // Add the third vertex V3 concurrent with V2 + let op3: Operation = new Operation(OperationType.Add, 3); + let deps3: string[] = [vertexHash1]; + let vertexHash3 = cro.hashGraph.addVertex(op3, deps3, peerId); + linearOps = cro.hashGraph.linearizeOps(); + expect([[op0, op1, op2, op3], [op0, op1, op3, op2]]).toContainEqual(linearOps); + }); + + test("Test: Tricky Case", () => { + /* + ___ V2:REMOVE(1) <- V4:ADD(10) + V1:ADD(1) / + \ ___ V3:ADD(1) <- V5:REMOVE(5) + */ + + let op1: Operation = new Operation(OperationType.Add, 1); + let deps1: string[] = [vertexHash0] + let vertexHash1 = cro.hashGraph.addVertex(op1, deps1, peerId); + + // Add second vertex + let op2: Operation = new Operation(OperationType.Remove, 1); + let deps2: string[] = [vertexHash1]; + let vertexHash2 = cro.hashGraph.addVertex(op2, deps2, peerId); + + // Add the third vertex V3 concurrent with V2 + let op3: Operation = new Operation(OperationType.Add, 1); + let deps3: string[] = [vertexHash1]; + let vertexHash3 = cro.hashGraph.addVertex(op3, deps3, peerId); + + // Add the vertex V4 with dependency on V2 + let op4: Operation = new Operation(OperationType.Add, 10); + let deps4: string[] = [vertexHash2]; + let vertexHash4 = cro.hashGraph.addVertex(op4, deps4, peerId); + + // Add the vertex V5 with dependency on V3 + let op5: Operation = new Operation(OperationType.Remove, 5); + let deps5: string[] = [vertexHash3]; + let vertexHash5 = cro.hashGraph.addVertex(op5, deps5, peerId); + const linearOps = cro.hashGraph.linearizeOps(); + expect([[op0, op1, op4, op3, op5], [op0, op1, op3, op5, op4]]).toContainEqual(linearOps); + + }); + + test("Test: Yuta Papa's Case", () => { + /* + ___ V2:REMOVE(1) <- V4:ADD(2) + V1:ADD(1) / + \ ___ V3:REMOVE(2) <- V5:ADD(1) + */ + + let op1: Operation = new Operation(OperationType.Add, 1); + let deps1: string[] = [vertexHash0] + let vertexHash1 = cro.hashGraph.addVertex(op1, deps1, peerId); + + // Add second vertex + let op2: Operation = new Operation(OperationType.Remove, 1); + let deps2: string[] = [vertexHash1]; + let vertexHash2 = cro.hashGraph.addVertex(op2, deps2, peerId); + + + // Add the third vertex V3 concurrent with V2 + let op3: Operation = new Operation(OperationType.Remove, 2); + let deps3: string[] = [vertexHash1]; + let vertexHash3 = cro.hashGraph.addVertex(op3, deps3, peerId); + + // Add the vertex V4 with dependency on V2 + let op4: Operation = new Operation(OperationType.Add, 2); + let deps4: string[] = [vertexHash2]; + let vertexHash4 = cro.hashGraph.addVertex(op4, deps4, peerId); + + // Add the vertex V5 with dependency on V3 + let op5: Operation = new Operation(OperationType.Add, 1); + let deps5: string[] = [vertexHash3]; + let vertexHash5 = cro.hashGraph.addVertex(op5, deps5, peerId); + const linearOps = cro.hashGraph.linearizeOps(); + expect([[op0, op1, op4, op5], [op0, op1, op5, op4]]).toContainEqual(linearOps); + }); + + test("Test: Mega Complex Case", () => { + /* + __ V6:ADD(3) + / + ___ V2:ADD(1) <-- V3:RM(2) <-- V7:RM(1) <-- V8:RM(3) + / ______________/ + V1:ADD(1)/ / + \ / + \ ___ V4:RM(2) <-- V5:ADD(2) <-- V9:RM(1) + + Topological Sorted Array: + [V1, V4, V5, V9, V2, V3, V7, V8, V6] + OR + [V1, V2, V3, V6, V7, V4, V5, V8, V9] + OR + [V1, V2, V3, V6, V7, V4, V5, V9, V8] + */ + + let op1: Operation = new Operation(OperationType.Add, 1); + let deps1: string[] = [vertexHash0] + let vertexHash1 = cro.hashGraph.addVertex(op1, deps1, peerId); + console.log("vertex1: ", vertexHash1); + // Add second vertex + let op2: Operation = new Operation(OperationType.Add, 1); + let deps2: string[] = [vertexHash1]; + let vertexHash2 = cro.hashGraph.addVertex(op2, deps2, peerId); + console.log("vertex2: ", vertexHash2); + // Add the third vertex V3 with dependency on V2 + let op3: Operation = new Operation(OperationType.Remove, 2); + let deps3: string[] = [vertexHash2]; + let vertexHash3 = cro.hashGraph.addVertex(op3, deps3, peerId); + console.log("vertex3: ", vertexHash3); + // Add the vertex V4 -> [V1] + let op4: Operation = new Operation(OperationType.Remove, 2); + let deps4: string[] = [vertexHash1]; + let vertexHash4 = cro.hashGraph.addVertex(op4, deps4, peerId); + console.log("vertex4: ", vertexHash4); + // Add the vertex V5 -> [V4] + let op5: Operation = new Operation(OperationType.Add, 2); + let deps5: string[] = [vertexHash4]; + let vertexHash5 = cro.hashGraph.addVertex(op5, deps5, peerId); + console.log("vertex5: ", vertexHash5); + // Add the vertex V6 ->[V3] + let op6: Operation = new Operation(OperationType.Add, 3); + let deps6: string[] = [vertexHash3]; + let vertexHash6 = cro.hashGraph.addVertex(op6, deps6, peerId); + console.log("vertex6: ", vertexHash6); + // Add the vertex V7 -> [V3] + let op7: Operation = new Operation(OperationType.Remove, 1); + let deps7: string[] = [vertexHash3]; + let vertexHash7 = cro.hashGraph.addVertex(op7, deps7, peerId); + console.log("vertex7: ", vertexHash7); + // Add the vertex V8 -> [V7, V5] + let op8: Operation = new Operation(OperationType.Remove, 3); + let deps8: string[] = [vertexHash7, vertexHash5]; + let vertexHash8 = cro.hashGraph.addVertex(op8, deps8, peerId); + console.log("vertex8: ", vertexHash8); + // Add the vertex V9 -> [V5] + let op9: Operation = new Operation(OperationType.Remove, 1); + let deps9: string[] = [vertexHash5]; + let vertexHash9 = cro.hashGraph.addVertex(op9, deps9, peerId); + console.log("vertex9: ", vertexHash9); + + let sortedOrder = cro.hashGraph.topologicalSort(); + expect([[vertexHash0, vertexHash1, vertexHash4, vertexHash5, vertexHash9, vertexHash2, vertexHash3, vertexHash7, vertexHash8, vertexHash6]]).toContainEqual(sortedOrder); + console.log(sortedOrder) + let linearOps = cro.hashGraph.linearizeOps(); + // expect([[op0, op1, op2, op6, op7, op4, op5], [op0, op1, op4, op5, op9, op2, op3, op]]).toContainEqual(linearOps); + }); + + test("Test: Mega Complex Case 1", () => { + /* + __ V6:ADD(3) + / + ___ V2:ADD(1) <-- V3:RM(2) <-- V7:RM(1) <-- V8:RM(3) + / ^ + V1:ADD(1)/ \ + \ \ + \ ___ V4:RM(2) <-------------------- V5:ADD(2) <-- V9:RM(1) + 6, 7, 8, 3, 2, 9, 5, 4, 1 + Topological Sorted Array: + [V1, V2, V3, V6, V4, V5, V9, V7, V8] + OR + [V1, V4, V2, V3, V7, V8, V6, V5, V9] + OR + [1, 4, 2, 3, 7, 5, 9, 8, 6] + */ + + let op1: Operation = new Operation(OperationType.Add, 1); + let deps1: string[] = [vertexHash0] + let vertexHash1 = cro.hashGraph.addVertex(op1, deps1, peerId); + console.log("vertex1: ", vertexHash1); + // Add second vertex + let op2: Operation = new Operation(OperationType.Add, 1); + let deps2: string[] = [vertexHash1]; + let vertexHash2 = cro.hashGraph.addVertex(op2, deps2, peerId); + console.log("vertex2: ", vertexHash2); + // Add the third vertex V3 with dependency on V2 + let op3: Operation = new Operation(OperationType.Remove, 2); + let deps3: string[] = [vertexHash2]; + let vertexHash3 = cro.hashGraph.addVertex(op3, deps3, peerId); + console.log("vertex3: ", vertexHash3); + // Add the vertex V4 -> [V1] + let op4: Operation = new Operation(OperationType.Remove, 2); + let deps4: string[] = [vertexHash1]; + let vertexHash4 = cro.hashGraph.addVertex(op4, deps4, peerId); + console.log("vertex4: ", vertexHash4); + // Add the vertex V6 ->[V3] + let op6: Operation = new Operation(OperationType.Add, 3); + let deps6: string[] = [vertexHash3]; + let vertexHash6 = cro.hashGraph.addVertex(op6, deps6, peerId); + console.log("vertex6: ", vertexHash6); + // Add the vertex V7 -> [V3] + let op7: Operation = new Operation(OperationType.Remove, 1); + let deps7: string[] = [vertexHash3]; + let vertexHash7 = cro.hashGraph.addVertex(op7, deps7, peerId); + console.log("vertex7: ", vertexHash7); + // Add the vertex V5 -> [V4, V7] + let op5: Operation = new Operation(OperationType.Add, 2); + let deps5: string[] = [vertexHash4, vertexHash7]; + let vertexHash5 = cro.hashGraph.addVertex(op5, deps5, peerId); + console.log("vertex5: ", vertexHash5); + // Add the vertex V8 -> [V7] + let op8: Operation = new Operation(OperationType.Remove, 3); + let deps8: string[] = [vertexHash7, vertexHash5]; + let vertexHash8 = cro.hashGraph.addVertex(op8, deps8, peerId); + console.log("vertex8: ", vertexHash8); + // Add the vertex V9 -> [V5] + let op9: Operation = new Operation(OperationType.Remove, 1); + let deps9: string[] = [vertexHash5]; + let vertexHash9 = cro.hashGraph.addVertex(op9, deps9, peerId); + console.log("vertex9: ", vertexHash9); + + let sortedOrder = cro.hashGraph.topologicalSort(); + console.log(sortedOrder) + // expect([[op0, op1, op2, op3, op6, op4, op5, op9, op7, op8]]).toContainEqual(sortedOrder); + }); + + test("Test: Joao's latest brain teaser", () => { + /* + + __ V2:Add(2) <------------\ + V1:Add(1) / \ - V5:RM(2) + \__ V3:RM(2) <- V4:RM(2) <--/ + + */ + let op1: Operation = new Operation(OperationType.Add, 1); + let deps1: string[] = [vertexHash0] + let vertexHash1 = cro.hashGraph.addVertex(op1, deps1, peerId); + + // Add the second vertex V2 <- [V1] + let op2: Operation = new Operation(OperationType.Add, 2); + let deps2: string[] = [vertexHash1]; + let vertexHash2 = cro.hashGraph.addVertex(op2, deps2, peerId); + + // Add the third vertex V3 <- [V1] + let op3: Operation = new Operation(OperationType.Remove, 2); + let deps3: string[] = [vertexHash1]; + let vertexHash3 = cro.hashGraph.addVertex(op3, deps3, peerId); + + // Add the fourth vertex V4 <- [V3] + let op4: Operation = new Operation(OperationType.Remove, 2); + let deps4: string[] = [vertexHash3]; + let vertexHash4 = cro.hashGraph.addVertex(op4, deps4, peerId); + + // Add the fifth vertex V5 <- [V2, V4] + let op5: Operation = new Operation(OperationType.Remove, 2); + let deps5: string[] = [vertexHash2, vertexHash4]; + let vertexHash5 = cro.hashGraph.addVertex(op5, deps5, peerId); + + const linearOps = cro.hashGraph.linearizeOps(); + expect(linearOps).toEqual([op0, op1, op2, op5]); + }); +}); diff --git a/packages/crdt/tests/RGA.test.ts b/packages/crdt/tests/RGA.test.ts index f4b16b73..c0f7085f 100644 --- a/packages/crdt/tests/RGA.test.ts +++ b/packages/crdt/tests/RGA.test.ts @@ -1,91 +1,92 @@ import { describe, test, expect, beforeEach } from "vitest"; -import { RGA } from "../src/crdts/RGA/index.js"; // Adjust the import path according to your project structure +import { RGA } from "../src/crdts/RGA/index.js"; describe("Replicable Growable Array Tests", () => { - let rga: RGA; - let peerRGA: RGA; - - beforeEach(() => { - rga = new RGA("node1"); - peerRGA = new RGA("node2"); - }); - - test("Test Insert", () => { - rga.insert(0, "A"); - rga.insert(1, "B"); - rga.insert(1, "C"); - rga.insert(0, "D"); - - expect(rga.getArray()).toEqual(["D", "A", "C", "B"]); - }); - - test("Test Read", () => { - rga.insert(0, "A"); - rga.insert(1, "B"); - rga.insert(1, "C"); - rga.delete(1); - - expect(rga.read(0)).toBe("A"); - expect(rga.read(1)).toBe("B"); - }); - - test("Test Insert and Delete", () => { - rga.insert(0, "A"); - rga.insert(1, "B"); - rga.insert(1, "C"); - rga.delete(0); - rga.delete(0); - expect(rga.getArray()).toEqual(["B"]); - - rga.clear(); - - rga.insert(0, "A"); - rga.insert(1, "B"); - rga.delete(0); - - expect(rga.getArray()).toEqual(["B"]); - - rga.insert(0, "C"); - rga.insert(1, "D"); - expect(rga.getArray()).toEqual(["C", "D", "B"]); - - rga.delete(1); - expect(rga.getArray()).toEqual(["C", "B"]); - - rga.delete(1); - expect(rga.getArray()).toEqual(["C"]); - - peerRGA.insert(0, "E"); - peerRGA.insert(0, "F"); - peerRGA.insert(2, "G"); - peerRGA.insert(3, "H"); - peerRGA.delete(1); - peerRGA.delete(1); - peerRGA.delete(1); - expect(peerRGA.getArray()).toEqual(["F"]); - }); - - test("Test Update", () => { - rga.insert(0, "A"); - rga.insert(1, "B"); - rga.update(0, "C"); - rga.update(1, "D"); - - expect(rga.getArray()).toEqual(["C", "D"]); - }); - - test("Test Merge", () => { - rga.insert(0, "A"); - rga.insert(1, "B"); - - peerRGA.insert(0, "C"); - peerRGA.insert(1, "D"); - peerRGA.insert(0, "E"); - - rga.merge(peerRGA); - expect(rga.getArray()).toEqual(["E", "C", "A", "D", "B"]); - - peerRGA.merge(rga); - expect(peerRGA.getArray()).toEqual(rga.getArray()); - }); + let rga: RGA; + let peerRGA: RGA; + + beforeEach(() => { + rga = new RGA("node1"); + peerRGA = new RGA("node2"); + }); + + test("Test Insert", () => { + rga.insert(0, "A"); + rga.insert(1, "B"); + rga.insert(1, "C"); + rga.insert(0, "D"); + + expect(rga.getArray()).toEqual(["D", "A", "C", "B"]); + }); + + test("Test Read", () => { + rga.insert(0, "A"); + rga.insert(1, "B"); + rga.insert(1, "C"); + rga.delete(1); + + expect(rga.read(0)).toBe("A"); + expect(rga.read(1)).toBe("B"); + }); + + test("Test Insert and Delete", () => { + rga.insert(0, "A"); + rga.insert(1, "B"); + rga.insert(1, "C"); + rga.delete(0); + rga.delete(0); + expect(rga.getArray()).toEqual(["B"]); + + rga.clear(); + + rga.insert(0, "A"); + rga.insert(1, "B"); + rga.delete(0); + + expect(rga.getArray()).toEqual(["B"]); + + rga.insert(0, "C"); + rga.insert(1, "D"); + expect(rga.getArray()).toEqual(["C", "D", "B"]); + + rga.delete(1); + expect(rga.getArray()).toEqual(["C", "B"]); + + rga.delete(1); + expect(rga.getArray()).toEqual(["C"]); + + peerRGA.insert(0, "E"); + peerRGA.insert(0, "F"); + peerRGA.insert(2, "G"); + peerRGA.insert(3, "H"); + peerRGA.delete(1); + peerRGA.delete(1); + peerRGA.delete(1); + expect(peerRGA.getArray()).toEqual(["F"]); + }); + + test("Test Update", () => { + rga.insert(0, "A"); + rga.insert(1, "B"); + rga.update(0, "C"); + rga.update(1, "D"); + + expect(rga.getArray()).toEqual(["C", "D"]); + }); + + test("Test Merge", () => { + rga.insert(0, "A"); + rga.insert(1, "B"); + + peerRGA.insert(0, "C"); + peerRGA.insert(1, "D"); + peerRGA.insert(0, "E"); + + rga.merge(peerRGA); + expect(rga.getArray()).toEqual(["E", "C", "A", "D", "B"]); + + peerRGA.merge(rga); + expect(peerRGA.getArray()).toEqual(rga.getArray()); + }); +>>>>>>> main }); diff --git a/packages/crdt/tsconfig.json b/packages/crdt/tsconfig.json index 46493a83..3e8b28b1 100644 --- a/packages/crdt/tsconfig.json +++ b/packages/crdt/tsconfig.json @@ -1,8 +1,13 @@ { - "extends": "../../tsconfig.json", - "compilerOptions": { - "outDir": "dist" - }, - "include": ["src/**/*.ts"], - "exclude": ["src/**/*.asc.ts"] + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "dist" + }, + "references": [ + { + "path": "../object" + } + ], + "include": ["src/**/*.ts"], + "exclude": ["src/**/*.asc.ts"] } diff --git a/packages/object/src/hashgraph.ts b/packages/object/src/hashgraph.ts new file mode 100644 index 00000000..2fec2f97 --- /dev/null +++ b/packages/object/src/hashgraph.ts @@ -0,0 +1,252 @@ +import * as crypto from "crypto"; + +type Hash = string; + +class Vertex { + constructor( + readonly hash: Hash, + readonly operation: Operation, + readonly dependencies: Set + ) { } +} + +export enum ActionType { + DropLeft, + DropRight, + Nop, + Swap +} + +export enum OperationType { + Add, + Remove, + Nop +} + +export class Operation { + constructor( + readonly type: OperationType, + readonly value: T + ) { } +} + +export interface IHashGraph { + addVertex(op: T, deps: Hash[], nodeId: string): Hash; + addToFrontier(op: T): Hash; + topologicalSort(): Hash[]; + areCausallyRelated(vertexHash1: Hash, vertexHash2: Hash): boolean; + getFrontier(): Hash[]; + getDependencies(vertexHash: Hash): Hash[] | undefined; + getVertex(vertexHash: Hash): Vertex | undefined; + getAllVertices(): Vertex[]; +} + +export class HashGraph { + private vertices: Map> = new Map(); + private frontier: Set = new Set(); + private forwardEdges: Map> = new Map(); + rootHash: Hash = ""; + + constructor(private resolveConflicts: (op1: Operation, op2: Operation) => ActionType, private nodeId: string) { + // Create and add the NOP root vertex + const nopOperation = new Operation(OperationType.Nop, 0 as T); + this.rootHash = this.computeHash(nopOperation, [], ""); + const rootVertex = new Vertex(this.rootHash, nopOperation, new Set()); + this.vertices.set(this.rootHash, rootVertex); + this.frontier.add(this.rootHash); + this.forwardEdges.set(this.rootHash, new Set()); + } + + // Time complexity: O(1), Space complexity: O(1) + private computeHash(op: Operation, deps: Hash[], nodeId: String): Hash { + const serialized = JSON.stringify({ op, deps, nodeId }); + const hash = crypto + .createHash("sha256") + .update(serialized) + .digest("hex"); + + return hash; + } + + addToFrontier(operation: Operation): Hash { + const deps = this.getFrontier(); + const hash = this.computeHash(operation, deps, this.nodeId) + const vertex = new Vertex(hash, operation, new Set(deps)); + + this.vertices.set(hash, vertex); + this.frontier.add(hash); + + // Update forward edges + for (const dep of deps) { + if (!this.forwardEdges.has(dep)) { + this.forwardEdges.set(dep, new Set()); + } + this.forwardEdges.get(dep)!.add(hash); + this.frontier.delete(dep); + } + return hash; + } + // Time complexity: O(d), where d is the number of dependencies + // Space complexity: O(d) + addVertex(op: Operation, deps: Hash[], nodeId: string): Hash { + + // Temporary fix: don't add the vertex if the dependencies are not present in the local HG. + if (!deps.every(dep => this.forwardEdges.has(dep) || this.vertices.has(dep))) { + console.log("Invalid dependency detected."); + return "" + } + + const hash = this.computeHash(op, deps, nodeId); + if (this.vertices.has(hash)) { + return hash; // Vertex already exists + } + + const vertex = new Vertex(hash, op, new Set(deps)); + this.vertices.set(hash, vertex); + this.frontier.add(hash); + + // Update forward edges + for (const dep of deps) { + if (!this.forwardEdges.has(dep)) { + this.forwardEdges.set(dep, new Set()); + } + this.forwardEdges.get(dep)!.add(hash); + this.frontier.delete(dep); + } + + return hash; + } + + // Time complexity: O(V + E), Space complexity: O(V) + topologicalSort(): Hash[] { + const result: Hash[] = []; + const visited = new Set(); + + const visit = (hash: Hash) => { + if (visited.has(hash)) return; + + visited.add(hash); + + const children = this.forwardEdges.get(hash) || new Set(); + for (const child of children) { + visit(child); + } + result.push(hash); + }; + // Start with the root vertex + visit(this.rootHash); + + return result.reverse(); + } + + linearizeOps(): Operation[] { + const order = this.topologicalSort(); + const result: Operation[] = []; + let i = 0; + + while (i < order.length) { + const anchor = order[i]; + let j = i + 1; + let shouldIncrementI = true; + + while (j < order.length) { + const moving = order[j]; + + if (!this.areCausallyRelated(anchor, moving)) { + const op1 = this.vertices.get(anchor)!.operation; + const op2 = this.vertices.get(moving)!.operation; + const action = this.resolveConflicts(op1, op2); + + switch (action) { + case ActionType.DropLeft: + order.splice(i, 1); + j = order.length; // Break out of inner loop + shouldIncrementI = false; + continue; // Continue outer loop without incrementing i + case ActionType.DropRight: + order.splice(j, 1); + continue; // Continue with the same j + case ActionType.Swap: + [order[i], order[j]] = [order[j], order[i]]; + j = order.length; // Break out of inner loop + break; + case ActionType.Nop: + j++; + break; + } + } else { + j++; + } + } + + if (shouldIncrementI) { + result.push(this.vertices.get(order[i])!.operation); + i++; + } + } + + return result; + } + + // Time complexity: O(V), Space complexity: O(V) + areCausallyRelated(hash1: Hash, hash2: Hash): boolean { + const visited = new Set(); + const stack = [hash1]; + + while (stack.length > 0) { + const current = stack.pop()!; + if (current === hash2) return true; + visited.add(current); + + const vertex = this.vertices.get(current)!; + for (const dep of vertex.dependencies) { + if (!visited.has(dep)) { + stack.push(dep); + } + } + } + + visited.clear(); + stack.push(hash2); + + while (stack.length > 0) { + const current = stack.pop()!; + if (current === hash1) return true; + visited.add(current); + + const vertex = this.vertices.get(current)!; + for (const dep of vertex.dependencies) { + if (!visited.has(dep)) { + stack.push(dep); + } + } + } + + return false; + } + + // Time complexity: O(1), Space complexity: O(1) + getFrontier(): Hash[] { + return Array.from(this.frontier); + } + + // Time complexity: O(1), Space complexity: O(1) + getRoot(): Hash { + return this.rootHash; + } + + // Time complexity: O(1), Space complexity: O(1) + getDependencies(vertexHash: Hash): Hash[] { + return Array.from(this.vertices.get(vertexHash)?.dependencies || []); + } + + // Time complexity: O(1), Space complexity: O(1) + getVertex(hash: Hash): Vertex | undefined { + return this.vertices.get(hash); + } + + // Time complexity: O(V), Space complexity: O(V) + getAllVertices(): Vertex[] { + return Array.from(this.vertices.values()); + } +} diff --git a/packages/object/src/index.ts b/packages/object/src/index.ts index 8a0fc4f1..a3957e4e 100644 --- a/packages/object/src/index.ts +++ b/packages/object/src/index.ts @@ -3,40 +3,41 @@ import { TopologyObject } from "./proto/object_pb.js"; import { compileWasm } from "./wasm/compiler.js"; export * from "./proto/object_pb.js"; +export * from "./hashgraph.js"; /* Creates a new TopologyObject */ export async function newTopologyObject( - peerId: string, - path?: string, - id?: string, - abi?: string, + peerId: string, + path?: string, + id?: string, + abi?: string, ): Promise { - // const bytecode = await compileWasm(path); - const bytecode = new Uint8Array(); - return { - id: - id ?? - crypto - .createHash("sha256") - .update(abi ?? "") - .update(peerId) - .update(Math.floor(Math.random() * Number.MAX_VALUE).toString()) - .digest("hex"), - abi: abi ?? "", - bytecode: bytecode ?? new Uint8Array(), - operations: [], - }; + // const bytecode = await compileWasm(path); + const bytecode = new Uint8Array(); + return { + id: + id ?? + crypto + .createHash("sha256") + .update(abi ?? "") + .update(peerId) + .update(Math.floor(Math.random() * Number.MAX_VALUE).toString()) + .digest("hex"), + abi: abi ?? "", + bytecode: bytecode ?? new Uint8Array(), + operations: [], + }; } export async function callFn( - obj: TopologyObject, - fn: string, - args: string[], + obj: TopologyObject, + fn: string, + args: string[], ): Promise { - obj.operations.push({ - nonce: Math.floor(Math.random() * Number.MAX_VALUE).toString(), - fn: fn, - args: args, - }); - return obj; + obj.operations.push({ + nonce: Math.floor(Math.random() * Number.MAX_VALUE).toString(), + fn: fn, + args: args, + }); + return obj; }