diff --git a/packages/object/src/hashgraph/index.ts b/packages/object/src/hashgraph/index.ts index 7173551f..c968fb3c 100644 --- a/packages/object/src/hashgraph/index.ts +++ b/packages/object/src/hashgraph/index.ts @@ -175,6 +175,7 @@ export class HashGraph { if (visited.has(node)) { stack.pop(); result.push(node); + processing.delete(node); continue; } @@ -190,7 +191,6 @@ export class HashGraph { } } } - processing.delete(node); } return result.reverse(); diff --git a/packages/object/tests/hashgraph.test.ts b/packages/object/tests/hashgraph.test.ts index a771e78a..ecf51761 100644 --- a/packages/object/tests/hashgraph.test.ts +++ b/packages/object/tests/hashgraph.test.ts @@ -3,7 +3,7 @@ import { SetDRP } from "@ts-drp/blueprints/src/Set/index.js"; import { beforeAll, beforeEach, describe, expect, test, vi } from "vitest"; import { ObjectACL } from "../src/acl/index.js"; -import { Vertex } from "../src/hashgraph/index.js"; +import { ActionType, SemanticsType, Vertex } from "../src/hashgraph/index.js"; import { ACLGroup, DRP, @@ -116,6 +116,59 @@ describe("HashGraph construction tests", () => { ] as Operation[]); }); + test("Test: Should detect cycle in topological sort", () => { + const hashgraph = new HashGraph( + "", + (_vertices: Vertex[]) => { + return { + action: ActionType.Nop, + }; + }, + (_vertices: Vertex[]) => { + return { + action: ActionType.Nop, + }; + }, + SemanticsType.pair + ); + const frontier = hashgraph.getFrontier(); + const v1 = newVertex( + "", + { + opType: "test", + value: [1], + drpType: DrpType.DRP, + }, + frontier, + Date.now(), + new Uint8Array() + ); + hashgraph.addVertex(v1); + + const v2 = newVertex( + "", + { + opType: "test", + value: [2], + drpType: DrpType.DRP, + }, + [v1.hash], + Date.now(), + new Uint8Array() + ); + hashgraph.addVertex(v2); + + // create a cycle + hashgraph.forwardEdges.set(v2.hash, [HashGraph.rootHash]); + + expect(() => { + hashgraph.dfsTopologicalSortIterative( + HashGraph.rootHash, + new ObjectSet(hashgraph.vertices.keys()) + ); + }).toThrowError("Graph contains a cycle!"); + }); + test("Test: HashGraph with 2 root vertices", () => { /* ROOT -- V1:ADD(1) diff --git a/packages/object/tests/linearize.test.ts b/packages/object/tests/linearize.test.ts index d063d3dc..f4ed9ccd 100644 --- a/packages/object/tests/linearize.test.ts +++ b/packages/object/tests/linearize.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, test, vi } from "vitest"; +import { describe, expect, test, vi, beforeEach, afterEach } from "vitest"; import { ActionType } from "../dist/src/hashgraph/index.js"; import { SemanticsType } from "../dist/src/hashgraph/index.js"; @@ -134,3 +134,245 @@ describe("Linearize correctly", () => { } }); }); + +describe("linearizeMultipleSemantics", () => { + beforeEach(() => { + vi.useFakeTimers(); + vi.setSystemTime(new Date(Date.UTC(1998, 11, 19))); + }); + + afterEach(() => { + vi.clearAllMocks(); + }); + + test("should linearize operations in a simple sequence", () => { + const hashGraph = new HashGraph( + "", + (_vertices: Vertex[]) => ({ + action: ActionType.Nop, + }), + () => ({ + action: ActionType.Nop, + }), + SemanticsType.multiple + ); + const origin = HashGraph.rootHash; + + // Add vertices to the graph + hashGraph.addVertex( + newVertex( + "", + { + opType: "set", + value: [1], + drpType: DrpType.DRP, + }, + hashGraph.getFrontier(), + Date.now(), + new Uint8Array() + ) + ); + + hashGraph.addVertex( + newVertex( + "", + { + opType: "set", + value: [2], + drpType: DrpType.DRP, + }, + hashGraph.getFrontier(), + Date.now(), + new Uint8Array() + ) + ); + + const subgraph = new ObjectSet(); + hashGraph.getAllVertices().forEach((vertex) => subgraph.add(vertex.hash)); + + const result = linearizeMultipleSemantics(hashGraph, origin, subgraph); + expect(result.map((op) => op.value)).toEqual([[1], [2]]); + }); + + test("should handle concurrent operations with conflict resolution", () => { + const hashGraph = new HashGraph( + "", + (_vertices: Vertex[]) => ({ + action: ActionType.Drop, + vertices: _vertices.filter((_, index) => index !== 0).map((vertex) => vertex.hash), + }), + (_vertices: Vertex[]) => ({ + action: ActionType.Drop, + vertices: _vertices.filter((_, index) => index !== 0).map((vertex) => vertex.hash), + }), + SemanticsType.multiple + ); + const origin = HashGraph.rootHash; + let frontier = hashGraph.getFrontier(); + + // Add concurrent vertices + hashGraph.addVertex( + newVertex( + "", + { + opType: "set", + value: [1], + drpType: DrpType.DRP, + }, + frontier, + Date.now(), + new Uint8Array() + ) + ); + + hashGraph.addVertex( + newVertex( + "", + { + opType: "set", + value: [2], + drpType: DrpType.DRP, + }, + frontier, + Date.now(), + new Uint8Array() + ) + ); + + hashGraph.addVertex( + newVertex( + "", + { + opType: "set", + value: [3], + drpType: DrpType.DRP, + }, + frontier, + Date.now(), + new Uint8Array() + ) + ); + + // Get the frontier + frontier = hashGraph.getFrontier(); + + hashGraph.addVertex( + newVertex( + "", + { + opType: "set", + value: [4], + drpType: DrpType.DRP, + }, + frontier, + Date.now(), + new Uint8Array() + ) + ); + console.log(`frontier: ${frontier}`); + hashGraph.addVertex( + newVertex( + "", + { + opType: "set", + value: [5], + drpType: DrpType.DRP, + }, + frontier.filter((_, idx) => idx !== 0), + Date.now(), + new Uint8Array() + ) + ); + + frontier = hashGraph.getFrontier(); + + hashGraph.addVertex( + newVertex( + "", + { + opType: "set", + value: [6], + drpType: DrpType.DRP, + }, + frontier, + Date.now(), + new Uint8Array() + ) + ); + + const subgraph = new ObjectSet(); + hashGraph.getAllVertices().forEach((vertex) => subgraph.add(vertex.hash)); + + const result = linearizeMultipleSemantics(hashGraph, origin, subgraph); + expect(result.map((op) => op.value)).toEqual([[3], [5], [6]]); + }); + + test("should handle operations with null values", () => { + const hashGraph = new HashGraph( + "", + () => ({ + action: ActionType.Nop, + }), + () => ({ + action: ActionType.Nop, + }), + SemanticsType.multiple + ); + const origin = HashGraph.rootHash; + + // Add vertices to the graph + hashGraph.addVertex( + newVertex( + "", + { + opType: "set", + value: null, + drpType: DrpType.DRP, + }, + hashGraph.getFrontier(), + Date.now(), + new Uint8Array() + ) + ); + + hashGraph.addVertex( + newVertex( + "", + { + opType: "set", + value: [2], + drpType: DrpType.DRP, + }, + hashGraph.getFrontier(), + Date.now(), + new Uint8Array() + ) + ); + + const subgraph = new ObjectSet(); + hashGraph.getAllVertices().forEach((vertex) => subgraph.add(vertex.hash)); + + const result = linearizeMultipleSemantics(hashGraph, origin, subgraph); + expect(result.map((op) => op.value)).toEqual([[2]]); + }); + + test("should handle empty subgraph", () => { + const hashGraph = new HashGraph( + "", + () => ({ + action: ActionType.Nop, + }), + () => ({ + action: ActionType.Nop, + }), + SemanticsType.multiple + ); + const origin = HashGraph.rootHash; + + const subgraph = new ObjectSet(); + subgraph.add(origin); + + const result = linearizeMultipleSemantics(hashGraph, origin, subgraph); + expect(result).toEqual([]); + }); +}); diff --git a/packages/object/tests/linearize/multi.test.ts b/packages/object/tests/linearize/multi.test.ts deleted file mode 100644 index c81bc562..00000000 --- a/packages/object/tests/linearize/multi.test.ts +++ /dev/null @@ -1,240 +0,0 @@ -import { newVertex } from "@ts-drp/object/src/index.js"; -import { describe, expect, test } from "vitest"; - -import { ActionType, HashGraph, type Vertex, SemanticsType } from "../../src/hashgraph/index.js"; -import { DrpType } from "../../src/index.js"; -import { linearizeMultipleSemantics } from "../../src/linearize/multipleSemantics.js"; -import { ObjectSet } from "../../src/utils/objectSet.js"; - -describe("linearizeMultipleSemantics", () => { - test("should linearize operations in a simple sequence", () => { - const hashGraph = new HashGraph( - "", - (_vertices: Vertex[]) => ({ - action: ActionType.Nop, - }), - () => ({ - action: ActionType.Nop, - }), - SemanticsType.multiple - ); - const origin = HashGraph.rootHash; - - // Add vertices to the graph - hashGraph.addVertex( - newVertex( - "", - { - opType: "set", - value: [1], - drpType: DrpType.DRP, - }, - hashGraph.getFrontier(), - Date.now(), - new Uint8Array() - ) - ); - - hashGraph.addVertex( - newVertex( - "", - { - opType: "set", - value: [2], - drpType: DrpType.DRP, - }, - hashGraph.getFrontier(), - Date.now(), - new Uint8Array() - ) - ); - - const subgraph = new ObjectSet(); - hashGraph.getAllVertices().forEach((vertex) => subgraph.add(vertex.hash)); - - const result = linearizeMultipleSemantics(hashGraph, origin, subgraph); - expect(result.map((op) => op.value)).toEqual([[1], [2]]); - }); - - test("should handle concurrent operations with conflict resolution", () => { - const hashGraph = new HashGraph( - "", - (_vertices: Vertex[]) => ({ - action: ActionType.Drop, - vertices: _vertices.filter((_, index) => index !== 0).map((vertex) => vertex.hash), - }), - (_vertices: Vertex[]) => ({ - action: ActionType.Drop, - vertices: _vertices.filter((_, index) => index !== 0).map((vertex) => vertex.hash), - }), - SemanticsType.multiple - ); - const origin = HashGraph.rootHash; - let frontier = hashGraph.getFrontier(); - - // Add concurrent vertices - hashGraph.addVertex( - newVertex( - "", - { - opType: "set", - value: [1], - drpType: DrpType.DRP, - }, - frontier, - Date.now(), - new Uint8Array() - ) - ); - - hashGraph.addVertex( - newVertex( - "", - { - opType: "set", - value: [2], - drpType: DrpType.DRP, - }, - frontier, - Date.now(), - new Uint8Array() - ) - ); - - hashGraph.addVertex( - newVertex( - "", - { - opType: "set", - value: [3], - drpType: DrpType.DRP, - }, - frontier, - Date.now(), - new Uint8Array() - ) - ); - - // Get the frontier - frontier = hashGraph.getFrontier(); - - hashGraph.addVertex( - newVertex( - "", - { - opType: "set", - value: [4], - drpType: DrpType.DRP, - }, - frontier, - Date.now(), - new Uint8Array() - ) - ); - console.log(`frontier: ${frontier}`); - hashGraph.addVertex( - newVertex( - "", - { - opType: "set", - value: [5], - drpType: DrpType.DRP, - }, - frontier.filter((_, idx) => idx !== 0), - Date.now(), - new Uint8Array() - ) - ); - - frontier = hashGraph.getFrontier(); - - hashGraph.addVertex( - newVertex( - "", - { - opType: "set", - value: [6], - drpType: DrpType.DRP, - }, - frontier, - Date.now(), - new Uint8Array() - ) - ); - - const subgraph = new ObjectSet(); - hashGraph.getAllVertices().forEach((vertex) => subgraph.add(vertex.hash)); - - const result = linearizeMultipleSemantics(hashGraph, origin, subgraph); - expect(result.map((op) => op.value)).toEqual([[1], [4], [6]]); - }); - - test("should handle operations with null values", () => { - const hashGraph = new HashGraph( - "", - () => ({ - action: ActionType.Nop, - }), - () => ({ - action: ActionType.Nop, - }), - SemanticsType.multiple - ); - const origin = HashGraph.rootHash; - - // Add vertices to the graph - hashGraph.addVertex( - newVertex( - "", - { - opType: "set", - value: null, - drpType: DrpType.DRP, - }, - hashGraph.getFrontier(), - Date.now(), - new Uint8Array() - ) - ); - - hashGraph.addVertex( - newVertex( - "", - { - opType: "set", - value: [2], - drpType: DrpType.DRP, - }, - hashGraph.getFrontier(), - Date.now(), - new Uint8Array() - ) - ); - - const subgraph = new ObjectSet(); - hashGraph.getAllVertices().forEach((vertex) => subgraph.add(vertex.hash)); - - const result = linearizeMultipleSemantics(hashGraph, origin, subgraph); - expect(result.map((op) => op.value)).toEqual([[2]]); - }); - - test("should handle empty subgraph", () => { - const hashGraph = new HashGraph( - "", - () => ({ - action: ActionType.Nop, - }), - () => ({ - action: ActionType.Nop, - }), - SemanticsType.multiple - ); - const origin = HashGraph.rootHash; - - const subgraph = new ObjectSet(); - subgraph.add(origin); - - const result = linearizeMultipleSemantics(hashGraph, origin, subgraph); - expect(result).toEqual([]); - }); -});