diff --git a/packages/object/src/hashgraph/index.ts b/packages/object/src/hashgraph/index.ts index bde9a13d..12a45cef 100644 --- a/packages/object/src/hashgraph/index.ts +++ b/packages/object/src/hashgraph/index.ts @@ -14,12 +14,6 @@ export type { Vertex, Operation }; export type Hash = string; -export enum DepthFirstSearchState { - UNVISITED = 0, - VISITING = 1, - VISITED = 2, -} - export enum OperationType { NOP = "-1", } @@ -219,39 +213,46 @@ export class HashGraph { return hash; } - depthFirstSearch( - origin: Hash, - subgraph: ObjectSet, - visited: Map = new Map(), - ): Hash[] { + kahnsAlgorithm(origin: Hash, subgraph: ObjectSet): Hash[] { const result: Hash[] = []; + const inDegree = new Map(); + const queue: Hash[] = []; + for (const hash of subgraph.entries()) { - visited.set(hash, DepthFirstSearchState.UNVISITED); + inDegree.set(hash, 0); } - const visit = (hash: Hash) => { - visited.set(hash, DepthFirstSearchState.VISITING); - const children = this.forwardEdges.get(hash) || []; + for (const [vertex, children] of this.forwardEdges) { + if (!inDegree.has(vertex)) continue; for (const child of children) { - if (!subgraph.has(child)) continue; - if (visited.get(child) === DepthFirstSearchState.VISITING) { - log.error("::hashgraph::DFS: Cycle detected"); - return; - } - if (visited.get(child) === undefined) { - log.error("::hashgraph::DFS: Undefined child"); - return; - } - if (visited.get(child) === DepthFirstSearchState.UNVISITED) { - visit(child); - } + if (!inDegree.has(child)) continue; + inDegree.set(child, (inDegree.get(child) || 0) + 1); } + } - result.push(hash); - visited.set(hash, DepthFirstSearchState.VISITED); - }; + let head = 0; + queue.push(origin); + while (queue.length > 0) { + const current = queue[head]; + head++; + if (!current) continue; - visit(origin); + result.push(current); + + for (const child of this.forwardEdges.get(current) || []) { + if (!inDegree.has(child)) continue; + const inDegreeValue = inDegree.get(child) || 0; + inDegree.set(child, inDegreeValue - 1); + if (inDegreeValue - 1 === 0) { + queue.push(child); + } + } + + if (head > queue.length / 2) { + queue.splice(0, head); + head = 0; + } + } return result; } @@ -262,8 +263,7 @@ export class HashGraph { origin: Hash = HashGraph.rootHash, subgraph: ObjectSet = new ObjectSet(this.vertices.keys()), ): Hash[] { - const result = this.depthFirstSearch(origin, subgraph); - result.reverse(); + const result = this.kahnsAlgorithm(origin, subgraph); if (!updateBitsets) return result; this.reachablePredecessors.clear(); this.topoSortedIndex.clear(); @@ -468,18 +468,16 @@ export class HashGraph { } } - const visited = new Map(); - this.depthFirstSearch( + const topoOrder = this.kahnsAlgorithm( HashGraph.rootHash, new ObjectSet(this.vertices.keys()), - visited, ); + for (const vertex of this.getAllVertices()) { - if (!visited.has(vertex.hash)) { + if (!topoOrder.includes(vertex.hash)) { return false; } } - return true; } diff --git a/packages/object/tests/hashgraph.test.ts b/packages/object/tests/hashgraph.test.ts index 19706c74..77989622 100644 --- a/packages/object/tests/hashgraph.test.ts +++ b/packages/object/tests/hashgraph.test.ts @@ -49,8 +49,8 @@ describe("HashGraph construction tests", () => { const linearOps = obj2.hashGraph.linearizeOperations(); expect(linearOps).toEqual([ - { type: "add", value: 1 }, { type: "add", value: 2 }, + { type: "add", value: 1 }, ]); }); @@ -170,8 +170,8 @@ describe("HashGraph for AddWinSet tests", () => { const linearOps = obj1.hashGraph.linearizeOperations(); expect(linearOps).toEqual([ { type: "add", value: 1 }, - { type: "add", value: 2 }, { type: "remove", value: 1 }, + { type: "add", value: 2 }, ]); }); @@ -204,8 +204,8 @@ describe("HashGraph for AddWinSet tests", () => { expect(linearOps).toEqual([ { type: "add", value: 1 }, { type: "add", value: 1 }, - { type: "remove", value: 5 }, { type: "add", value: 10 }, + { type: "remove", value: 5 }, ]); }); @@ -235,9 +235,9 @@ describe("HashGraph for AddWinSet tests", () => { const linearOps = obj1.hashGraph.linearizeOperations(); expect(linearOps).toEqual([ - { type: "add", value: 1 }, { type: "add", value: 1 }, { type: "add", value: 2 }, + { type: "add", value: 1 }, ]); }); @@ -288,12 +288,12 @@ describe("HashGraph for AddWinSet tests", () => { const linearOps = obj1.hashGraph.linearizeOperations(); expect(linearOps).toEqual([ + { type: "add", value: 1 }, { type: "add", value: 1 }, { type: "remove", value: 2 }, { type: "add", value: 2 }, - { type: "add", value: 1 }, - { type: "add", value: 3 }, { type: "remove", value: 1 }, + { type: "add", value: 3 }, ]); }); @@ -345,11 +345,11 @@ describe("HashGraph for AddWinSet tests", () => { const linearOps = obj1.hashGraph.linearizeOperations(); expect(linearOps).toEqual([ { type: "add", value: 1 }, - { type: "remove", value: 2 }, { type: "add", value: 1 }, { type: "remove", value: 2 }, - { type: "add", value: 3 }, + { type: "remove", value: 2 }, { type: "remove", value: 1 }, + { type: "add", value: 3 }, { type: "add", value: 2 }, { type: "remove", value: 1 }, ]);