Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: reduce action type for the hashgraph #132

Merged
merged 23 commits into from
Sep 13, 2024
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions packages/crdt/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,5 +31,8 @@
"devDependencies": {
"@topology-foundation/object": "0.0.23-5",
"assemblyscript": "^0.27.29"
},
"dependencies": {
"@thi.ng/random": "^4.0.3"
}
}
19 changes: 2 additions & 17 deletions packages/crdt/src/cros/AddWinsSet/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,13 @@ import {
} from "@topology-foundation/object";

/// AddWinsSet with support for state and op changes
export class AddWinsSet<T extends number> {
export class AddWinsSet<T> {
state: Map<T, number>;
hashGraph: HashGraph<T>;

constructor(nodeId: string) {
this.state = new Map<T, number>();
this.hashGraph = new HashGraph<T>(this.resolveConflicts, nodeId);
this.hashGraph = new HashGraph<T>(nodeId);
}

resolveConflicts(op1: Operation<T>, op2: Operation<T>): ActionType {
Expand Down Expand Up @@ -54,19 +54,4 @@ export class AddWinsSet<T extends number> {
this.state.set(value, Math.max(this.getValue(value), count));
}
}

read(): T[] {
const operations = this.hashGraph.linearizeOps();
const tempCounter = new AddWinsSet<T>("");

for (const op of operations) {
if (op.type === OperationType.Add) {
tempCounter.add(op.value);
} else {
tempCounter.remove(op.value);
}
}

return tempCounter.values();
}
}
121 changes: 121 additions & 0 deletions packages/crdt/src/cros/ReduceActionType/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import { Smush32 } from "@thi.ng/random";
import {
type Hash,
HashGraph,
type Operation,
} from "@topology-foundation/object";

type ReductionType<T> = { hash: Hash; op: Operation<T>; index: number };
type ResolvedConflict = { action: ActionType; indices: number[] };

export enum ActionType {
Reduce = 0,
Nop = 1,
}

const MOD = 1e9 + 9;

function compute_hash(s: string): number {
let hash = 0;
for (let i = 0; i < s.length; i++) {
// Same as hash = hash * 31 + s.charCodeAt(i);
hash = (hash << 5) - hash + s.charCodeAt(i);
hash %= MOD;
}
return hash;
}

/// Example implementation of the Reduce action type
/// An arbitrary number of concurrent operations can be reduced to a single operation
export class ReduceActionType<T> {
hashGraph: HashGraph<T>;

constructor(nodeId: string) {
this.hashGraph = new HashGraph<T>(nodeId);
}

resolveConflicts(ops: ReductionType<T>[]): ResolvedConflict {
ops.sort((a, b) => (a.hash < b.hash ? -1 : 1));
const seed: string = ops.map((op) => op.hash).join("");
const rnd = new Smush32(compute_hash(seed));
const chosen = rnd.int() % ops.length;
const indices = ops.map((op) => op.index);
indices.splice(chosen, 1);
return { action: ActionType.Reduce, indices: indices };
}

linearizeOps(): Operation<T>[] {
const order = this.hashGraph.topologicalSort();
const result: Operation<T>[] = [];
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.hashGraph.areCausallyRelated(anchor, moving)) {
const concurrentOps: ReductionType<T>[] = [];
concurrentOps.push({
hash: anchor,
op: this.hashGraph.vertices.get(anchor)?.operation as Operation<T>,
index: i,
});
concurrentOps.push({
hash: moving,
op: this.hashGraph.vertices.get(moving)?.operation as Operation<T>,
index: j,
});
let k = j + 1;
for (; k < order.length; k++) {
let add = true;
for (const op of concurrentOps) {
if (this.hashGraph.areCausallyRelated(op.hash, order[k])) {
add = false;
break;
}
}
if (add) {
concurrentOps.push({
hash: order[k],
op: this.hashGraph.vertices.get(order[k])
?.operation as Operation<T>,
index: k,
});
}
}
const resolved = this.resolveConflicts(concurrentOps);

switch (resolved.action) {
case ActionType.Reduce:
// Sort the indices in descending order, so that splice does not mess up the order
resolved.indices.sort((a, b) => (a < b ? 1 : -1));
for (const idx of resolved.indices) {
if (idx === i) shouldIncrementI = false;
order.splice(idx, 1);
}
if (!shouldIncrementI) j = order.length; // Break out of inner loop
break;
case ActionType.Nop:
j++;
break;
}
} else {
j++;
}
}

if (shouldIncrementI) {
result.push(
this.hashGraph.vertices.get(order[i])?.operation as Operation<T>,
);
i++;
}
}

return result;
}
}
120 changes: 120 additions & 0 deletions packages/crdt/tests/ReduceActionType.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import { Operation, OperationType } from "@topology-foundation/object/";
import { beforeEach, describe, expect, test } from "vitest";
import { ReduceActionType } from "../src/cros/ReduceActionType/index.js";

describe("Reduce Action Type tests", () => {
let cro: ReduceActionType<number>;
let op0: Operation<number>;
let vertexHash0: string;
const peerId = "peerId0";

beforeEach(() => {
cro = new ReduceActionType("peer0");
op0 = new Operation(OperationType.Nop, 0);
vertexHash0 = cro.hashGraph.rootHash;
});

test("Test: Giga Chad 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]
*/

const op1: Operation<number> = new Operation(OperationType.Add, 1);
const deps1: string[] = [vertexHash0];
const vertexHash1 = cro.hashGraph.addVertex(op1, deps1, peerId);
// Add second vertex
const op2: Operation<number> = new Operation(OperationType.Add, 1);
const deps2: string[] = [vertexHash1];
const vertexHash2 = cro.hashGraph.addVertex(op2, deps2, peerId);
// Add the third vertex V3 with dependency on V2
const op3: Operation<number> = new Operation(OperationType.Remove, 2);
const deps3: string[] = [vertexHash2];
const vertexHash3 = cro.hashGraph.addVertex(op3, deps3, peerId);
// Add the vertex V4 -> [V1]
const op4: Operation<number> = new Operation(OperationType.Remove, 2);
const deps4: string[] = [vertexHash1];
const vertexHash4 = cro.hashGraph.addVertex(op4, deps4, peerId);
// Add the vertex V5 -> [V4]
const op5: Operation<number> = new Operation(OperationType.Add, 2);
const deps5: string[] = [vertexHash4];
const vertexHash5 = cro.hashGraph.addVertex(op5, deps5, peerId);
// Add the vertex V6 ->[V3]
const op6: Operation<number> = new Operation(OperationType.Add, 3);
const deps6: string[] = [vertexHash3];
const vertexHash6 = cro.hashGraph.addVertex(op6, deps6, peerId);
// Add the vertex V7 -> [V3]
const op7: Operation<number> = new Operation(OperationType.Remove, 1);
const deps7: string[] = [vertexHash3];
const vertexHash7 = cro.hashGraph.addVertex(op7, deps7, peerId);
// Add the vertex V8 -> [V7, V5]
const op8: Operation<number> = new Operation(OperationType.Remove, 3);
const deps8: string[] = [vertexHash7, vertexHash5];
const vertexHash8 = cro.hashGraph.addVertex(op8, deps8, peerId);
// Add the vertex V9 -> [V5]
const op9: Operation<number> = new Operation(OperationType.Remove, 1);
const deps9: string[] = [vertexHash5];
const vertexHash9 = cro.hashGraph.addVertex(op9, deps9, peerId);

const sortedOrder = cro.hashGraph.topologicalSort();
expect([
[
vertexHash1,
vertexHash4,
vertexHash5,
vertexHash9,
vertexHash2,
vertexHash3,
vertexHash7,
vertexHash8,
vertexHash6,
],
]).toContainEqual(sortedOrder);
const linearOps = cro.linearizeOps();
expect([[op1, op5, op8]]).toContainEqual(linearOps);
});

test("Test: Many concurrent operations", () => {
/*
--- V1:ADD(1)
/---- V2:ADD(2)
V0:Nop -- V3:ADD(3)
\---- V4:ADD(4)
\--- V5:ADD(5)
*/
const op1: Operation<number> = new Operation(OperationType.Add, 1);
const deps1: string[] = [vertexHash0];
const vertexHash1 = cro.hashGraph.addVertex(op1, deps1, peerId);

const op2: Operation<number> = new Operation(OperationType.Add, 2);
const deps2: string[] = [vertexHash0];
const vertexHash2 = cro.hashGraph.addVertex(op2, deps2, peerId);

const op3: Operation<number> = new Operation(OperationType.Add, 3);
const deps3: string[] = [vertexHash0];
const vertexHash3 = cro.hashGraph.addVertex(op3, deps3, peerId);

const op4: Operation<number> = new Operation(OperationType.Add, 4);
const deps4: string[] = [vertexHash0];
const vertexHash4 = cro.hashGraph.addVertex(op4, deps4, peerId);

const op5: Operation<number> = new Operation(OperationType.Add, 5);
const deps5: string[] = [vertexHash0];
const vertexHash5 = cro.hashGraph.addVertex(op5, deps5, peerId);

const linearOps = cro.linearizeOps();
expect(linearOps).toEqual([op1]);
});
});
72 changes: 6 additions & 66 deletions packages/object/src/hashgraph.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import * as crypto from "node:crypto";

type Hash = string;
export type Hash = string;

class Vertex<T> {
export class Vertex<T> {
constructor(
readonly hash: Hash,
readonly operation: Operation<T>,
Expand Down Expand Up @@ -41,19 +41,13 @@ export interface IHashGraph<T> {
getAllVertices(): Vertex<T>[];
}

export class HashGraph<T extends number> {
private vertices: Map<Hash, Vertex<T>> = new Map();
export class HashGraph<T> {
vertices: Map<Hash, Vertex<T>> = new Map();
private frontier: Set<Hash> = new Set();
private forwardEdges: Map<Hash, Set<Hash>> = new Map();
rootHash: Hash = "";

constructor(
private resolveConflicts: (
op1: Operation<T>,
op2: Operation<T>,
) => ActionType,
private nodeId: string,
) {
constructor(private nodeId: string) {
// Create and add the NOP root vertex
const nopOperation = new Operation(OperationType.Nop, 0 as T);
this.rootHash = this.computeHash(nopOperation, [], "");
Expand Down Expand Up @@ -140,61 +134,7 @@ export class HashGraph<T extends number> {
// Start with the root vertex
visit(this.rootHash);

return result.reverse();
}

linearizeOps(): Operation<T>[] {
const order = this.topologicalSort();
const result: Operation<T>[] = [];
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;
let action: ActionType;
if (!op1 || !op2) {
action = ActionType.Nop;
} else {
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) {
const op = this.vertices.get(order[i])?.operation;
if (op) result.push();
i++;
}
}

result.reverse().splice(0, 1); // Remove the Nop
return result;
}

Expand Down
Loading
Loading