diff --git a/packages/object/src/hashgraph/index.ts b/packages/object/src/hashgraph/index.ts index 97b23657..a3ec3753 100644 --- a/packages/object/src/hashgraph/index.ts +++ b/packages/object/src/hashgraph/index.ts @@ -1,15 +1,15 @@ import * as crypto from "node:crypto"; import { log } from "../index.js"; -import { linearizeMultiple } from "../linearize/multipleSemantics.js"; -import { linearizePair } from "../linearize/pairSemantics.js"; -import { +import { linearizeMultipleSemantics } from "../linearize/multipleSemantics.js"; +import { linearizePairSemantics } from "../linearize/pairSemantics.js"; +import type { Vertex_Operation as Operation, Vertex, } from "../proto/drp/object/v1/object_pb.js"; import { BitSet } from "./bitset.js"; // Reexporting the Vertex and Operation types from the protobuf file -export { Vertex, Operation }; +export type { Vertex, Operation }; export type Hash = string; @@ -42,6 +42,11 @@ export type ResolveConflictsType = { vertices?: Hash[]; }; +export type VertexDistance = { + distance: number; + closestDependency?: Hash; +}; + export class HashGraph { nodeId: string; resolveConflicts: (vertices: Vertex[]) => ResolveConflictsType; @@ -62,6 +67,7 @@ export class HashGraph { private arePredecessorsFresh = false; private reachablePredecessors: Map = new Map(); private topoSortedIndex: Map = new Map(); + private vertexDistances: Map = new Map(); // We start with a bitset of size 1, and double it every time we reach the limit private currentBitsetSize = 1; @@ -86,6 +92,9 @@ export class HashGraph { this.vertices.set(HashGraph.rootHash, rootVertex); this.frontier.push(HashGraph.rootHash); this.forwardEdges.set(HashGraph.rootHash, []); + this.vertexDistances.set(HashGraph.rootHash, { + distance: 0, + }); } addToFrontier(operation: Operation): Vertex { @@ -110,9 +119,24 @@ export class HashGraph { this.forwardEdges.get(dep)?.push(hash); } + // Compute the distance of the vertex + const vertexDistance: VertexDistance = { + distance: Number.MAX_VALUE, + closestDependency: "", + }; + for (const dep of deps) { + const depDistance = this.vertexDistances.get(dep); + if (depDistance && depDistance.distance + 1 < vertexDistance.distance) { + vertexDistance.distance = depDistance.distance + 1; + vertexDistance.closestDependency = dep; + } + } + this.vertexDistances.set(hash, vertexDistance); + const depsSet = new Set(deps); this.frontier = this.frontier.filter((hash) => !depsSet.has(hash)); this.arePredecessorsFresh = false; + return vertex; } @@ -149,22 +173,41 @@ export class HashGraph { this.forwardEdges.get(dep)?.push(hash); } + // Compute the distance of the vertex + const vertexDistance: VertexDistance = { + distance: Number.MAX_VALUE, + closestDependency: "", + }; + for (const dep of deps) { + const depDistance = this.vertexDistances.get(dep); + if (depDistance && depDistance.distance + 1 < vertexDistance.distance) { + vertexDistance.distance = depDistance.distance + 1; + vertexDistance.closestDependency = dep; + } + } + this.vertexDistances.set(hash, vertexDistance); + const depsSet = new Set(deps); this.frontier = this.frontier.filter((hash) => !depsSet.has(hash)); this.arePredecessorsFresh = false; return hash; } - depthFirstSearch(visited: Map = new Map()): Hash[] { + depthFirstSearch( + origin: Hash, + subgraph: Set, + visited: Map = new Map(), + ): Hash[] { const result: Hash[] = []; - for (const vertex of this.getAllVertices()) { - visited.set(vertex.hash, DepthFirstSearchState.UNVISITED); + for (const hash of subgraph) { + visited.set(hash, DepthFirstSearchState.UNVISITED); } const visit = (hash: Hash) => { visited.set(hash, DepthFirstSearchState.VISITING); const children = this.forwardEdges.get(hash) || []; for (const child of children) { + if (!subgraph.has(child)) continue; if (visited.get(child) === DepthFirstSearchState.VISITING) { log.error("::hashgraph::DFS: Cycle detected"); return; @@ -182,16 +225,20 @@ export class HashGraph { visited.set(hash, DepthFirstSearchState.VISITED); }; - visit(HashGraph.rootHash); + visit(origin); return result; } - topologicalSort(updateBitsets = false): Hash[] { - const result = this.depthFirstSearch(); + /* Topologically sort the vertices in the whole hashgraph or the past of a given vertex. */ + topologicalSort( + updateBitsets = false, + origin: Hash = HashGraph.rootHash, + subgraph: Set = new Set(this.vertices.keys()), + ): Hash[] { + const result = this.depthFirstSearch(origin, subgraph); result.reverse(); if (!updateBitsets) return result; - this.reachablePredecessors.clear(); this.topoSortedIndex.clear(); @@ -221,17 +268,109 @@ export class HashGraph { return result; } - linearizeOperations(): Operation[] { + linearizeOperations( + origin: Hash = HashGraph.rootHash, + subgraph: Set = new Set(this.vertices.keys()), + ): Operation[] { switch (this.semanticsType) { case SemanticsType.pair: - return linearizePair(this); + return linearizePairSemantics(this, origin, subgraph); case SemanticsType.multiple: - return linearizeMultiple(this); + return linearizeMultipleSemantics(this, origin, subgraph); default: return []; } } + lowestCommonAncestorMultipleVertices( + hashes: Hash[], + visited: Set, + ): Hash { + if (hashes.length === 0) { + throw new Error("Vertex dependencies are empty"); + } + if (hashes.length === 1) { + return hashes[0]; + } + let lca: Hash | undefined = hashes[0]; + const targetVertices: Hash[] = [...hashes]; + for (let i = 1; i < targetVertices.length; i++) { + if (!lca) { + throw new Error("LCA not found"); + } + if (!visited.has(targetVertices[i])) { + lca = this.lowestCommonAncestorPairVertices( + lca, + targetVertices[i], + visited, + targetVertices, + ); + } + } + if (!lca) { + throw new Error("LCA not found"); + } + return lca; + } + + private lowestCommonAncestorPairVertices( + hash1: Hash, + hash2: Hash, + visited: Set, + targetVertices: Hash[], + ): Hash | undefined { + let currentHash1 = hash1; + let currentHash2 = hash2; + visited.add(currentHash1); + visited.add(currentHash2); + + while (currentHash1 !== currentHash2) { + const distance1 = this.vertexDistances.get(currentHash1); + if (!distance1) { + log.error("::hashgraph::LCA: Vertex not found"); + return; + } + const distance2 = this.vertexDistances.get(currentHash2); + if (!distance2) { + log.error("::hashgraph::LCA: Vertex not found"); + return; + } + + if (distance1.distance > distance2.distance) { + if (!distance1.closestDependency) { + log.error("::hashgraph::LCA: Closest dependency not found"); + return; + } + for (const dep of this.vertices.get(currentHash1)?.dependencies || []) { + if (dep !== distance1.closestDependency && !visited.has(dep)) { + targetVertices.push(dep); + } + } + currentHash1 = distance1.closestDependency; + if (visited.has(currentHash1)) { + return currentHash2; + } + visited.add(currentHash1); + } else { + if (!distance2.closestDependency) { + log.error("::hashgraph::LCA: Closest dependency not found"); + return; + } + for (const dep of this.vertices.get(currentHash2)?.dependencies || []) { + if (dep !== distance2.closestDependency && !visited.has(dep)) { + targetVertices.push(dep); + } + } + currentHash2 = distance2.closestDependency; + if (visited.has(currentHash2)) { + return currentHash1; + } + visited.add(currentHash2); + } + } + return currentHash1; + } + areCausallyRelatedUsingBitsets(hash1: Hash, hash2: Hash): boolean { if (!this.arePredecessorsFresh) { this.topologicalSort(true); @@ -303,7 +442,11 @@ export class HashGraph { } const visited = new Map(); - this.depthFirstSearch(visited); + this.depthFirstSearch( + HashGraph.rootHash, + new Set(this.vertices.keys()), + visited, + ); for (const vertex of this.getAllVertices()) { if (!visited.has(vertex.hash)) { return false; diff --git a/packages/object/src/index.ts b/packages/object/src/index.ts index bcc83e74..b756e800 100644 --- a/packages/object/src/index.ts +++ b/packages/object/src/index.ts @@ -1,6 +1,7 @@ import * as crypto from "node:crypto"; import { Logger, type LoggerOptions } from "@ts-drp/logger"; import { + type Hash, HashGraph, type Operation, type ResolveConflictsType, @@ -17,8 +18,15 @@ export interface DRP { semanticsType: SemanticsType; resolveConflicts: (vertices: Vertex[]) => ResolveConflictsType; mergeCallback: (operations: Operation[]) => void; + // biome-ignore lint: attributes can be anything + [key: string]: any; } +type DRPState = { + // biome-ignore lint: attributes can be anything + state: Map; +}; + export type DRPObjectCallback = ( object: DRPObject, origin: string, @@ -46,6 +54,9 @@ export class DRPObject implements IDRPObject { vertices: ObjectPb.Vertex[]; drp: ProxyHandler | null; hashGraph: HashGraph; + // mapping from vertex hash to the DRP state + states: Map; + originalDRP: DRP; subscriptions: DRPObjectCallback[]; constructor( @@ -75,7 +86,11 @@ export class DRPObject implements IDRPObject { drp?.semanticsType, ); this.subscriptions = []; - + this.states = new Map([[HashGraph.rootHash, { state: new Map() }]]); + this.originalDRP = Object.create( + Object.getPrototypeOf(drp), + Object.getOwnPropertyDescriptors(structuredClone(drp)), + ); this.vertices = this.hashGraph.getAllVertices(); } @@ -104,6 +119,8 @@ export class DRPObject implements IDRPObject { // biome-ignore lint: value can't be unknown because of protobuf callFn(fn: string, args: any) { const vertex = this.hashGraph.addToFrontier({ type: fn, value: args }); + this._setState(vertex); + const serializedVertex = ObjectPb.Vertex.create({ hash: vertex.hash, nodeId: vertex.nodeId, @@ -122,7 +139,7 @@ export class DRPObject implements IDRPObject { const missing = []; for (const vertex of vertices) { // Check to avoid manually crafted `undefined` operations - if (!vertex.operation) { + if (!vertex.operation || this.hashGraph.vertices.has(vertex.hash)) { continue; } @@ -132,6 +149,8 @@ export class DRPObject implements IDRPObject { vertex.dependencies, vertex.nodeId, ); + + this._setState(vertex); } catch (e) { missing.push(vertex.hash); } @@ -155,4 +174,57 @@ export class DRPObject implements IDRPObject { callback(this, origin, vertices); } } + + private _setState(vertex: Vertex) { + const subgraph: Set = new Set(); + const lca = this.hashGraph.lowestCommonAncestorMultipleVertices( + vertex.dependencies, + subgraph, + ); + const linearizedOperations = this.hashGraph.linearizeOperations( + lca, + subgraph, + ); + + const drp = Object.create( + Object.getPrototypeOf(this.originalDRP), + Object.getOwnPropertyDescriptors(structuredClone(this.originalDRP)), + ) as DRP; + + const fetchedState = this.states.get(lca); + if (!fetchedState) { + throw new Error("State is undefined"); + } + + const state = Object.create( + Object.getPrototypeOf(fetchedState), + Object.getOwnPropertyDescriptors(structuredClone(fetchedState)), + ).state; + + for (const [key, value] of state.entries()) { + drp[key] = value; + } + + let applyIdx = 1; + if (lca === HashGraph.rootHash) { + applyIdx = 0; + } + + for (; applyIdx < linearizedOperations.length; applyIdx++) { + const op = linearizedOperations[applyIdx]; + drp[op.type](op.value); + } + if (vertex.operation) { + drp[vertex.operation.type](vertex.operation.value); + } + + const varNames: string[] = Object.keys(drp); + // biome-ignore lint: values can be anything + const newState: Map = new Map(); + for (const varName of varNames) { + newState.set(varName, drp[varName]); + } + + this.states.set(vertex.hash, { state: newState }); + } } diff --git a/packages/object/src/linearize/multipleSemantics.ts b/packages/object/src/linearize/multipleSemantics.ts index 2d74be0b..f1c866b9 100644 --- a/packages/object/src/linearize/multipleSemantics.ts +++ b/packages/object/src/linearize/multipleSemantics.ts @@ -1,4 +1,3 @@ -import { BitSet } from "../hashgraph/bitset.js"; import { ActionType, type Hash, @@ -7,8 +6,12 @@ import { type Vertex, } from "../hashgraph/index.js"; -export function linearizeMultiple(hashGraph: HashGraph): Operation[] { - const order = hashGraph.topologicalSort(true); +export function linearizeMultipleSemantics( + hashGraph: HashGraph, + origin: Hash, + subgraph: Set, +): Operation[] { + const order = hashGraph.topologicalSort(true, origin, subgraph); const dropped = new Array(order.length).fill(false); const indices: Map = new Map(); const result: Operation[] = []; @@ -23,82 +26,60 @@ export function linearizeMultiple(hashGraph: HashGraph): Operation[] { let j = i + 1; while (j < order.length) { - if (dropped[j]) { - j = hashGraph.findNextCausallyUnrelated(anchor, j) ?? order.length; + if ( + hashGraph.areCausallyRelatedUsingBitsets(anchor, order[j]) || + dropped[j] + ) { + j++; continue; } const moving = order[j]; - if (!hashGraph.areCausallyRelatedUsingBitsets(anchor, moving)) { - const concurrentOps: Hash[] = []; - concurrentOps.push(anchor); - indices.set(anchor, i); - concurrentOps.push(moving); - indices.set(moving, j); + const concurrentOps: Hash[] = []; + concurrentOps.push(anchor); + indices.set(anchor, i); + concurrentOps.push(moving); + indices.set(moving, j); - let reachableVertices: BitSet = new BitSet( - hashGraph.getCurrentBitsetSize(), - ); - const anchorReachablePredecessors = - hashGraph.getReachablePredecessors(anchor); - if (anchorReachablePredecessors) { - reachableVertices = reachableVertices.or(anchorReachablePredecessors); + let k = j + 1; + while (k < order.length) { + if (dropped[k]) { + k++; + continue; } - const movingReachablePredecessors = - hashGraph.getReachablePredecessors(moving); - if (movingReachablePredecessors) { - reachableVertices = reachableVertices.or(movingReachablePredecessors); - } - - let k = reachableVertices.findNext(j, 0); - while (k < order.length) { - if (dropped[k]) { - k = reachableVertices.findNext(k, 0); - continue; - } - let add = true; - for (const hash of concurrentOps) { - if (hashGraph.areCausallyRelatedUsingBitsets(hash, order[k])) { - add = false; - break; - } - } - if (add) { - concurrentOps.push(order[k]); - indices.set(order[k], k); - const reachablePredecessors = hashGraph.getReachablePredecessors( - order[k], - ); - if (reachablePredecessors) { - reachableVertices = reachableVertices.or(reachablePredecessors); - } + let add = true; + for (const hash of concurrentOps) { + if (hashGraph.areCausallyRelatedUsingBitsets(hash, order[k])) { + add = false; + break; } - - k = reachableVertices.findNext(k, 0); } - const resolved = hashGraph.resolveConflicts( - concurrentOps.map((hash) => hashGraph.vertices.get(hash) as Vertex), - ); + if (add) { + concurrentOps.push(order[k]); + indices.set(order[k], k); + } + k++; + } + const resolved = hashGraph.resolveConflicts( + concurrentOps.map((hash) => hashGraph.vertices.get(hash) as Vertex), + ); - switch (resolved.action) { - case ActionType.Drop: { - for (const hash of resolved.vertices || []) { - dropped[indices.get(hash) || -1] = true; - } - if (dropped[i]) { - j = order.length; - } - break; + switch (resolved.action) { + case ActionType.Drop: { + for (const hash of resolved.vertices || []) { + dropped[indices.get(hash) || -1] = true; } - case ActionType.Nop: - j = hashGraph.findNextCausallyUnrelated(anchor, j) ?? order.length; - break; - default: - break; + if (dropped[i]) { + j = order.length; + } + break; } - } else { - j = hashGraph.findNextCausallyUnrelated(anchor, j) ?? order.length; + case ActionType.Nop: + j++; + break; + default: + break; } } diff --git a/packages/object/src/linearize/pairSemantics.ts b/packages/object/src/linearize/pairSemantics.ts index 3182631b..bba101fd 100644 --- a/packages/object/src/linearize/pairSemantics.ts +++ b/packages/object/src/linearize/pairSemantics.ts @@ -1,13 +1,18 @@ import { ActionType, + type Hash, type HashGraph, type Operation, } from "../hashgraph/index.js"; -export function linearizePair(hashGraph: HashGraph): Operation[] { - const order = hashGraph.topologicalSort(true); +export function linearizePairSemantics( + hashGraph: HashGraph, + origin: Hash, + subgraph: Set, +): Operation[] { + const order: Hash[] = hashGraph.topologicalSort(true, origin, subgraph); const dropped = new Array(order.length).fill(false); - const result: Operation[] = []; + const result = []; let i = 0; while (i < order.length) { @@ -19,41 +24,40 @@ export function linearizePair(hashGraph: HashGraph): Operation[] { let j = i + 1; while (j < order.length) { - if (dropped[j]) { + if ( + hashGraph.areCausallyRelatedUsingBitsets(anchor, order[j]) || + dropped[j] + ) { j++; continue; } const moving = order[j]; - if (!hashGraph.areCausallyRelatedUsingBitsets(anchor, moving)) { - const v1 = hashGraph.vertices.get(anchor); - const v2 = hashGraph.vertices.get(moving); - let action: ActionType; - if (!v1 || !v2) { - action = ActionType.Nop; - } else { - action = hashGraph.resolveConflicts([v1, v2]).action; - } - - switch (action) { - case ActionType.DropLeft: - dropped[i] = true; - j = order.length; - break; - case ActionType.DropRight: - dropped[j] = true; - j++; - break; - case ActionType.Swap: - [order[i], order[j]] = [order[j], order[i]]; - j = i + 1; - break; - case ActionType.Nop: - j++; - break; - } + const v1 = hashGraph.vertices.get(anchor); + const v2 = hashGraph.vertices.get(moving); + let action: ActionType; + if (!v1 || !v2) { + action = ActionType.Nop; } else { - j++; + action = hashGraph.resolveConflicts([v1, v2]).action; + } + + switch (action) { + case ActionType.DropLeft: + dropped[i] = true; + j = order.length; + break; + case ActionType.DropRight: + dropped[j] = true; + j++; + break; + case ActionType.Swap: + [order[i], order[j]] = [order[j], order[i]]; + j = i + 1; + break; + case ActionType.Nop: + j++; + break; } } diff --git a/packages/object/tests/hashgraph.test.ts b/packages/object/tests/hashgraph.test.ts index 725e0313..07c5435b 100644 --- a/packages/object/tests/hashgraph.test.ts +++ b/packages/object/tests/hashgraph.test.ts @@ -114,10 +114,10 @@ describe("HashGraph for AddWinSet tests", () => { test("Test: Add Two Concurrent Vertices With Same Value", () => { /* - _ V2:REMOVE(1) - V1:ADD(1) / - \ _ V3:ADD(1) - */ + _ V2:REMOVE(1) + V1:ADD(1) / + \ _ V3:ADD(1) + */ const drp1 = obj1.drp as AddWinsSet; const drp2 = obj2.drp as AddWinsSet; @@ -142,10 +142,10 @@ describe("HashGraph for AddWinSet tests", () => { test("Test: Add Two Concurrent Vertices With Different Values", () => { /* - _ V2:REMOVE(1) - V1:ADD(1) / - \ _ V3:ADD(2) - */ + _ V2:REMOVE(1) + V1:ADD(1) / + \ _ V3:ADD(2) + */ const drp1 = obj1.drp as AddWinsSet; const drp2 = obj2.drp as AddWinsSet; @@ -172,10 +172,10 @@ describe("HashGraph for AddWinSet tests", () => { test("Test: Tricky Case", () => { /* - ___ V2:REMOVE(1) <- V4:ADD(10) - V1:ADD(1) / - \ ___ V3:ADD(1) <- V5:REMOVE(5) - */ + ___ V2:REMOVE(1) <- V4:ADD(10) + V1:ADD(1) / + \ ___ V3:ADD(1) <- V5:REMOVE(5) + */ const drp1 = obj1.drp as AddWinsSet; const drp2 = obj2.drp as AddWinsSet; @@ -206,10 +206,10 @@ describe("HashGraph for AddWinSet tests", () => { test("Test: Yuta Papa's Case", () => { /* - ___ V2:REMOVE(1) <- V4:ADD(2) - V1:ADD(1) / - \ ___ V3:REMOVE(2) <- V5:ADD(1) - */ + ___ V2:REMOVE(1) <- V4:ADD(2) + V1:ADD(1) / + \ ___ V3:REMOVE(2) <- V5:ADD(1) + */ const drp1 = obj1.drp as AddWinsSet; const drp2 = obj2.drp as AddWinsSet; @@ -238,14 +238,14 @@ describe("HashGraph for AddWinSet tests", () => { 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) - */ + __ 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) + */ const drp1 = obj1.drp as AddWinsSet; const drp2 = obj2.drp as AddWinsSet; @@ -293,14 +293,14 @@ describe("HashGraph for AddWinSet tests", () => { test("Test: Mega Complex Case 1", () => { /* - __ V5:ADD(3) - / - ___ V2:ADD(1) <-- V3:RM(2) <-- V6:RM(1) <-- V8:RM(3) - / ^ - V1:ADD(1)/ \ - \ \ - \ ___ V4:RM(2) <-------------------- V7:ADD(2) <-- V9:RM(1) - */ + __ V5:ADD(3) + / + ___ V2:ADD(1) <-- V3:RM(2) <-- V6:RM(1) <-- V8:RM(3) + / ^ + V1:ADD(1)/ \ + \ \ + \ ___ V4:RM(2) <-------------------- V7:ADD(2) <-- V9:RM(1) + */ const drp1 = obj1.drp as AddWinsSet; const drp2 = obj2.drp as AddWinsSet; @@ -350,10 +350,10 @@ describe("HashGraph for AddWinSet tests", () => { test("Test: Joao's latest brain teaser", () => { /* - __ V2:Add(2) <------------\ - V1:Add(1) / \ - V5:RM(2) - \__ V3:RM(2) <- V4:RM(2) <--/ - */ + __ V2:Add(2) <------------\ + V1:Add(1) / \ - V5:RM(2) + \__ V3:RM(2) <- V4:RM(2) <--/ + */ const drp1 = obj1.drp as AddWinsSet; const drp2 = obj2.drp as AddWinsSet; @@ -402,10 +402,10 @@ describe("HashGraph for PseudoRandomWinsSet tests", () => { /* --- V1:ADD(1) /---- V2:ADD(2) - V0:Nop -- V3:ADD(3) + V0:Nop -- V3:ADD(3) \---- V4:ADD(4) - ---- V5:ADD(5) - */ + ---- V5:ADD(5) + */ const drp1 = obj1.drp as PseudoRandomWinsSet; const drp2 = obj2.drp as PseudoRandomWinsSet; @@ -468,3 +468,148 @@ describe("HashGraph for undefined operations tests", () => { } as Operation); }); }); + +describe("Vertex state tests", () => { + let obj1: DRPObject; + let obj2: DRPObject; + let obj3: DRPObject; + + beforeEach(async () => { + obj1 = new DRPObject("peer1", new AddWinsSet()); + obj2 = new DRPObject("peer2", new AddWinsSet()); + obj3 = new DRPObject("peer3", new AddWinsSet()); + }); + + test("Test: Vertex states work correctly with single HashGraph", () => { + /* + root---V1:ADD(1)---V2:ADD(2)---V3:ADD(3) + */ + const drp1 = obj1.drp as AddWinsSet; + + drp1.add(1); + drp1.add(2); + drp1.add(3); + + const vertices = obj1.hashGraph.topologicalSort(); + + const drpState1 = obj1.states.get(vertices[1]); + expect(drpState1?.state.get("state").get(1)).toBe(true); + expect(drpState1?.state.get("state").get(2)).toBe(undefined); + expect(drpState1?.state.get("state").get(3)).toBe(undefined); + + const drpState2 = obj1.states.get(vertices[2]); + expect(drpState2?.state.get("state").get(1)).toBe(true); + expect(drpState2?.state.get("state").get(2)).toBe(true); + expect(drpState2?.state.get("state").get(3)).toBe(undefined); + + const drpState3 = obj1.states.get(vertices[3]); + expect(drpState3?.state.get("state").get(1)).toBe(true); + expect(drpState3?.state.get("state").get(2)).toBe(true); + expect(drpState3?.state.get("state").get(3)).toBe(true); + }); + + test("Test: Tricky merging", () => { + /* + A1<----------A4 __ + / / \ + root <-- B2 <-- A6 + \ \ / + C3<--------C5 ----- + */ + + // in above hashgraph, A represents drp1, B represents drp2, C represents drp3 + const drp1 = obj1.drp as AddWinsSet; + const drp2 = obj2.drp as AddWinsSet; + const drp3 = obj3.drp as AddWinsSet; + + drp1.add(1); + drp2.add(2); + drp3.add(3); + + obj1.merge(obj2.hashGraph.getAllVertices()); + obj3.merge(obj2.hashGraph.getAllVertices()); + + drp1.add(4); + drp3.add(5); + + obj1.merge(obj3.hashGraph.getAllVertices()); + obj3.merge(obj1.hashGraph.getAllVertices()); + + drp1.add(6); + + const hashA4 = + "8e6f4369010528ae3668efce452da04d077e0957955d62d671b90f2934c755fe"; + const hashC5 = + "a8d94f7e2b421be2d5cd1124ca9ddb831e38246065db6e9a32ce493ca9604038"; + const hashA6 = + "cd6a955f0734a09df1bff44c5e0458365d3a26ec7f1cae0df2c0f708b9f100a8"; + + const drpState1 = obj1.states.get(hashA4); + expect(drpState1?.state.get("state").get(1)).toBe(true); + expect(drpState1?.state.get("state").get(2)).toBe(true); + expect(drpState1?.state.get("state").get(3)).toBe(undefined); + expect(drpState1?.state.get("state").get(4)).toBe(true); + expect(drpState1?.state.get("state").get(5)).toBe(undefined); + + const drpState2 = obj1.states.get(hashC5); + expect(drpState2?.state.get("state").get(1)).toBe(undefined); + expect(drpState2?.state.get("state").get(2)).toBe(true); + expect(drpState2?.state.get("state").get(3)).toBe(true); + expect(drpState2?.state.get("state").get(4)).toBe(undefined); + expect(drpState2?.state.get("state").get(5)).toBe(true); + + const drpState3 = obj1.states.get(hashA6); + expect(drpState3?.state.get("state").get(1)).toBe(true); + expect(drpState3?.state.get("state").get(2)).toBe(true); + expect(drpState3?.state.get("state").get(3)).toBe(true); + expect(drpState3?.state.get("state").get(4)).toBe(true); + expect(drpState3?.state.get("state").get(5)).toBe(true); + expect(drpState3?.state.get("state").get(6)).toBe(true); + }); + + test("Test: Vertex states with 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) + */ + + const drp1 = obj1.drp as AddWinsSet; + const drp2 = obj2.drp as AddWinsSet; + const drp3 = obj3.drp as AddWinsSet; + + drp1.add(1); + obj2.merge(obj1.hashGraph.getAllVertices()); + + drp1.add(1); + drp1.remove(2); + drp2.remove(2); + drp2.add(2); + + obj3.merge(obj1.hashGraph.getAllVertices()); + drp3.add(3); + drp1.remove(1); + + obj1.merge(obj2.hashGraph.getAllVertices()); + drp1.remove(3); + drp2.remove(1); + + obj1.merge(obj2.hashGraph.getAllVertices()); + obj1.merge(obj3.hashGraph.getAllVertices()); + obj2.merge(obj1.hashGraph.getAllVertices()); + obj2.merge(obj3.hashGraph.getAllVertices()); + obj3.merge(obj1.hashGraph.getAllVertices()); + obj3.merge(obj2.hashGraph.getAllVertices()); + + const hashV8 = + "be97d8fe9169800893c28b3d8aaefda517b98936efb069673e0250317b5e4a0b"; + const drpStateV8 = obj1.states.get(hashV8); + expect(drpStateV8?.state.get("state").get(1)).toBe(false); + expect(drpStateV8?.state.get("state").get(2)).toBe(true); + expect(drpStateV8?.state.get("state").get(3)).toBe(undefined); + }); +});