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 16 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 @@ -36,5 +36,8 @@
"devDependencies": {
"@topology-foundation/object": "0.1.1",
"assemblyscript": "^0.27.29"
},
"dependencies": {
"@thi.ng/random": "^4.0.3"
}
}
11 changes: 7 additions & 4 deletions packages/crdt/src/cros/AddWinsSet/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,15 @@ import {
ActionType,
type CRO,
type Operation,
type ResolveConflictsType,
SemanticsType,
type Vertex,
} from "@topology-foundation/object";

export class AddWinsSet<T> implements CRO {
operations: string[] = ["add", "remove"];
state: Map<T, boolean>;
semanticsType = SemanticsType.pair;

constructor() {
this.state = new Map<T, boolean>();
Expand Down Expand Up @@ -40,16 +43,16 @@ export class AddWinsSet<T> implements CRO {
}

// in this case is an array of length 2 and there are only two possible operations
resolveConflicts(vertices: Vertex[]): ActionType {
resolveConflicts(vertices: Vertex[]): ResolveConflictsType {
if (
vertices[0].operation.type !== vertices[1].operation.type &&
vertices[0].operation.value === vertices[1].operation.value
) {
return vertices[0].operation.type === "add"
? ActionType.DropRight
: ActionType.DropLeft;
? { action: ActionType.DropRight }
: { action: ActionType.DropLeft };
}
return ActionType.Nop;
return { action: ActionType.Nop };
}

// merged at HG level and called as a callback
Expand Down
90 changes: 90 additions & 0 deletions packages/crdt/src/cros/PseudoRandomWinsSet/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { Smush32 } from "@thi.ng/random";
import {
ActionType,
type CRO,
type Hash,
type Operation,
type ResolveConflictsType,
SemanticsType,
type Vertex,
} from "@topology-foundation/object";

const MOD = 1e9 + 9;

function computeHash(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 multi-vertex semantics that uses the reduce action type.
An arbitrary number of concurrent operations can be reduced to a single operation.
The winning operation is chosen using a pseudo-random number generator.
*/
export class PseudoRandomWinsSet<T> implements CRO {
operations: string[] = ["add", "remove"];
state: Map<T, boolean>;
semanticsType = SemanticsType.multiple;

constructor() {
this.state = new Map<T, boolean>();
}

private _add(value: T): void {
if (!this.state.get(value)) this.state.set(value, true);
}

add(value: T): void {
this._add(value);
}

private _remove(value: T): void {
if (this.state.get(value)) this.state.set(value, false);
}

remove(value: T): void {
this._remove(value);
}

contains(value: T): boolean {
return this.state.get(value) === true;
}

values(): T[] {
return Array.from(this.state.entries())
.filter(([_, exists]) => exists)
.map(([value, _]) => value);
}

resolveConflicts(vertices: Vertex[]): ResolveConflictsType {
vertices.sort((a, b) => (a.hash < b.hash ? -1 : 1));
const seed: string = vertices.map((vertex) => vertex.hash).join("");
guiltygyoza marked this conversation as resolved.
Show resolved Hide resolved
const rnd = new Smush32(computeHash(seed));
const chosen = rnd.int() % vertices.length;
const hashes: Hash[] = vertices.map((vertex) => vertex.hash);
hashes.splice(chosen, 1);
return { action: ActionType.Reduce, vertices: hashes };
JanLewDev marked this conversation as resolved.
Show resolved Hide resolved
}

// merged at HG level and called as a callback
mergeCallback(operations: Operation[]): void {
this.state = new Map<T, boolean>();
for (const op of operations) {
switch (op.type) {
case "add":
if (op.value !== null) this._add(op.value);
break;
case "remove":
if (op.value !== null) this._remove(op.value);
break;
default:
break;
}
}
}
}
35 changes: 35 additions & 0 deletions packages/crdt/tests/PseudoRandomWinsSet.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { beforeEach, describe, expect, test } from "vitest";
import { PseudoRandomWinsSet } from "../src/cros/PseudoRandomWinsSet/index.js";

describe("HashGraph for PseudoRandomWinsSet tests", () => {
let cro: PseudoRandomWinsSet<number>;

beforeEach(() => {
cro = new PseudoRandomWinsSet();
});

test("Test: Add", () => {
cro.add(1);
let set = cro.values();
expect(set).toEqual([1]);

cro.add(2);
set = cro.values();
expect(set).toEqual([1, 2]);
});

test("Test: Add and Remove", () => {
cro.add(1);
let set = cro.values();
expect(set).toEqual([1]);

cro.add(2);
set = cro.values();
expect(set).toEqual([1, 2]);

cro.remove(1);
set = cro.values();
expect(cro.contains(1)).toBe(false);
expect(set).toEqual([2]);
});
});
2 changes: 1 addition & 1 deletion packages/node/src/version.ts
Original file line number Diff line number Diff line change
@@ -1 +1 @@
export const VERSION = "0.1.1-3";
export const VERSION = "0.1.1";
79 changes: 26 additions & 53 deletions packages/object/src/hashgraph/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import * as crypto from "node:crypto";
import { linearizeMultiple } from "../linearize/multiplesemantics.js";
import { linearizePair } from "../linearize/pairsemantics.js";
import { BitSet } from "./bitset.js";

export type Hash = string;
Expand All @@ -14,8 +16,20 @@ export enum ActionType {
DropRight = 1,
Nop = 2,
Swap = 3,
Reduce = 4,
}

export enum SemanticsType {
pair = 0,
multiple = 1,
}

// In the case of multi-vertex semantics, we are returning an array of vertices (their hashes) to be reduced.
export type ResolveConflictsType = {
action: ActionType;
vertices?: Hash[];
};

export interface Vertex {
hash: Hash;
nodeId: string;
Expand All @@ -27,7 +41,8 @@ export interface Vertex {

export class HashGraph {
nodeId: string;
resolveConflicts: (vertices: Vertex[]) => ActionType;
resolveConflicts: (vertices: Vertex[]) => ResolveConflictsType;
semanticsType: SemanticsType;

vertices: Map<Hash, Vertex> = new Map();
frontier: Hash[] = [];
Expand All @@ -45,10 +60,12 @@ export class HashGraph {

constructor(
nodeId: string,
resolveConflicts: (vertices: Vertex[]) => ActionType,
resolveConflicts: (vertices: Vertex[]) => ResolveConflictsType,
semanticsType: SemanticsType,
) {
this.nodeId = nodeId;
this.resolveConflicts = resolveConflicts;
this.semanticsType = semanticsType;

// Create and add the NOP root vertex
const rootVertex: Vertex = {
Expand Down Expand Up @@ -182,58 +199,14 @@ export class HashGraph {
}

linearizeOperations(): Operation[] {
const order = this.topologicalSort(true);
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.areCausallyRelatedUsingBitsets(anchor, moving)) {
const v1 = this.vertices.get(anchor);
const v2 = this.vertices.get(moving);
let action: ActionType;
if (!v1 || !v2) {
action = ActionType.Nop;
} else {
action = this.resolveConflicts([v1, v2]);
}

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 && op.value !== null) result.push(op);
i++;
}
switch (this.semanticsType) {
case SemanticsType.pair:
return linearizePair(this);
case SemanticsType.multiple:
return linearizeMultiple(this);
default:
return [];
}

return result;
}

// Amortised time complexity: O(1), Amortised space complexity: O(1)
Expand Down
11 changes: 9 additions & 2 deletions packages/object/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import {
type ActionType,
HashGraph,
type Operation,
type ResolveConflictsType,
type SemanticsType,
type Vertex,
} from "./hashgraph/index.js";
import * as ObjectPb from "./proto/object_pb.js";
Expand All @@ -11,7 +13,8 @@ export * as ObjectPb from "./proto/object_pb.js";
export * from "./hashgraph/index.js";

export interface CRO {
resolveConflicts: (vertices: Vertex[]) => ActionType;
semanticsType: SemanticsType;
resolveConflicts: (vertices: Vertex[]) => ResolveConflictsType;
mergeCallback: (operations: Operation[]) => void;
}

Expand Down Expand Up @@ -51,7 +54,11 @@ export class TopologyObject implements ITopologyObject {
this.bytecode = new Uint8Array();
this.vertices = [];
this.cro = new Proxy(cro, this.proxyCROHandler());
this.hashGraph = new HashGraph(nodeId, cro.resolveConflicts);
this.hashGraph = new HashGraph(
nodeId,
cro.resolveConflicts,
cro.semanticsType,
);
this.subscriptions = [];
}

Expand Down
Loading