Skip to content

Commit

Permalink
feat: reduce action type for the hashgraph (#132)
Browse files Browse the repository at this point in the history
  • Loading branch information
JanLewDev authored Sep 13, 2024
1 parent 3d0a645 commit 6198600
Show file tree
Hide file tree
Showing 13 changed files with 727 additions and 409 deletions.
7 changes: 5 additions & 2 deletions examples/canvas/src/objects/canvas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,14 @@ import {
ActionType,
type CRO,
type Operation,
type ResolveConflictsType,
SemanticsType,
} from "@topology-foundation/object";
import { Pixel } from "./pixel";

export class Canvas implements CRO {
operations: string[] = ["splash", "paint"];
semanticsType: SemanticsType = SemanticsType.pair;

width: number;
height: number;
Expand Down Expand Up @@ -74,8 +77,8 @@ export class Canvas implements CRO {
);
}

resolveConflicts(_): ActionType {
return ActionType.Nop;
resolveConflicts(_): ResolveConflictsType {
return { action: ActionType.Nop };
}

mergeCallback(operations: Operation[]): void {
Expand Down
7 changes: 5 additions & 2 deletions examples/chat/src/objects/chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,14 @@ import {
ActionType,
type CRO,
type Operation,
type ResolveConflictsType,
SemanticsType,
type Vertex,
} from "@topology-foundation/object";

export class Chat implements CRO {
operations: string[] = ["addMessage"];
semanticsType: SemanticsType = SemanticsType.pair;
// store messages as strings in the format (timestamp, message, nodeId)
messages: GSet<string>;
constructor() {
Expand All @@ -40,8 +43,8 @@ export class Chat implements CRO {
this.messages.merge(other.messages);
}

resolveConflicts(vertices: Vertex[]): ActionType {
return ActionType.Nop;
resolveConflicts(vertices: Vertex[]): ResolveConflictsType {
return { action: ActionType.Nop };
}

mergeCallback(operations: Operation[]): void {
Expand Down
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("");
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.Drop, vertices: hashes };
}

// 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,
Drop = 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
12 changes: 10 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,9 @@ export * as ObjectPb from "./proto/object_pb.js";
export * from "./hashgraph/index.js";

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

Expand Down Expand Up @@ -51,7 +55,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

0 comments on commit 6198600

Please sign in to comment.