Skip to content

Commit

Permalink
Merge branch 'main' into orset
Browse files Browse the repository at this point in the history
  • Loading branch information
d-roak committed Aug 4, 2024
2 parents e6076bf + b5e8951 commit 0e54168
Show file tree
Hide file tree
Showing 22 changed files with 756 additions and 152 deletions.
3 changes: 1 addition & 2 deletions .github/workflows/docker.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
name: Build Docker Image
on:
release:
types: [published]
workflow_call:
permissions:
packages: write
env:
Expand Down
3 changes: 1 addition & 2 deletions .github/workflows/npm-publish.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
name: Publish Package to npmjs
on:
release:
types: [published]
workflow_call:
jobs:
publish-npm:
runs-on: ubuntu-latest
Expand Down
20 changes: 20 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
name: Workflow - release
on:
release:
types: [published]
jobs:
npm_publish:
name: Publish packages to npmjs
secrets: inherit
permissions:
id-token: write
packages: write
uses: ./.github/workflows/npm-publish.yml

build_docker_images:
name: Build Docker Images
permissions:
packages: write
needs:
- npm_publish
uses: ./.github/workflows/docker.yml
10 changes: 5 additions & 5 deletions examples/canvas/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "ts-topology-examples-canvas",
"version": "0.0.22",
"version": "0.0.23-5",
"license": "MIT",
"scripts": {
"build": "webpack",
Expand All @@ -9,10 +9,10 @@
"start": "ts-node ./src/index.ts"
},
"dependencies": {
"@topology-foundation/crdt": "file:../../packages/crdt",
"@topology-foundation/network": "file:../../packages/network",
"@topology-foundation/node": "file:../../packages/node",
"@topology-foundation/object": "file:../../packages/object",
"@topology-foundation/crdt": "0.0.23-5",
"@topology-foundation/network": "0.0.23-5",
"@topology-foundation/node": "0.0.23-5",
"@topology-foundation/object": "0.0.23-5",
"crypto-browserify": "^3.12.0",
"process": "^0.11.10",
"stream-browserify": "^3.0.0",
Expand Down
10 changes: 5 additions & 5 deletions examples/chat/package.json
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
{
"name": "topology-example-chat",
"version": "1.0.0",
"version": "0.0.23-5",
"description": "Topology Protocol Chat Exmaple",
"main": "src/index.ts",
"repository": "https://github.com/topology-foundation/ts-topology.git",
"license": "MIT",
"dependencies": {
"@topology-foundation/crdt": "file:../../packages/crdt",
"@topology-foundation/network": "file:../../packages/network",
"@topology-foundation/node": "file:../../packages/node",
"@topology-foundation/object": "file:../../packages/object",
"@topology-foundation/crdt": "0.0.23-5",
"@topology-foundation/network": "0.0.23-5",
"@topology-foundation/node": "0.0.23-5",
"@topology-foundation/object": "0.0.23-5",
"crypto-browserify": "^3.12.0",
"process": "^0.11.10",
"stream-browserify": "^3.0.0",
Expand Down
2 changes: 1 addition & 1 deletion examples/chat/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ async function main() {
object["chat"] = Object.assign(new GSet<string>(new Set<string>()), object["chat"]);
chatCRO = Object.assign(new Chat(node.networkNode.peerId), object);

(<HTMLButtonElement>document.getElementById("chatId")).innerHTML = objectId;
(<HTMLButtonElement>document.getElementById("chatId")).innerHTML = chatCRO.getObjectId();
render();
} catch (e) {
console.error("Error while connecting to the CRO ", objectId, e);
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "ts-topology",
"description": "The official TypeScript implementation of Topology Protocol",
"version": "0.0.22",
"version": "0.0.23-5",
"license": "MIT",
"homepage": "https://topology.gg/",
"repository": {
Expand Down
2 changes: 1 addition & 1 deletion packages/crdt/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@topology-foundation/crdt",
"version": "0.0.22",
"version": "0.0.23-5",
"license": "MIT",
"repository": {
"type": "git",
Expand Down
14 changes: 6 additions & 8 deletions packages/crdt/src/builtins/GCounter/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,7 @@ export class GCounter {
}

compare(peerCounter: GCounter): boolean {
for (let key in Object.keys(this.counts)) {
if (this.counts[key] > peerCounter.counts[key]) {
return false;
}
}
return true;
return (this.counts.length === peerCounter.counts.length && Object.keys(this.counts).every(key => this.counts[key] <= peerCounter.counts[key]));
}

merge(peerCounter: GCounter): void {
Expand All @@ -34,8 +29,11 @@ export class GCounter {
this.counts,
peerCounter.counts,
);

Object.keys(temp).forEach((key) => {
this.counts[key] = Math.max(this.counts[key], peerCounter.counts[key]);
this.counts[key] = Math.max(this.counts[key] || 0, peerCounter.counts[key] || 0);
});

this.globalCounter = Object.values(this.counts).reduce((a, b) => a + b, 0);
}
}
}
4 changes: 2 additions & 2 deletions packages/crdt/src/builtins/GSet/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,10 @@ export class GSet<T> {
}

compare(peerSet: GSet<T>): boolean {
return this._set === peerSet.set();
return (this._set.size == peerSet.set().size && [...this._set].every(value => peerSet.set().has(value)));
}

merge(peerSet: GSet<T>): void {
this._set = new Set<T>([...this._set, ...peerSet.set()]);
}
}
}
77 changes: 77 additions & 0 deletions packages/crdt/src/builtins/IPSet/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { GCounter } from "../GCounter/index.js";

/// State-based infinite-phase set (IPSet)
export class IPSet<T> {
// Grow-only mapping of elements to GCounters
private _counters: Map<T, GCounter>;

// State:
// - an element exists in the IPSet if _counter[element] exists and is an odd number
// - otherwise the element doesn't exist in the IPSet

constructor(counters: Map<T, GCounter> = new Map()) {
this._counters = counters;
}

counters(): Map<T, GCounter> {
return this._counters;
}

add(nodeId: string, element: T): void {
if (!this._counters.has(element)) {
this._counters.set(element, new GCounter({ [nodeId]: 1 }));
} else if (this._counters.get(element)?.value()! % 2 === 0) {
this._counters.get(element)?.increment(nodeId, 1);
}
}

remove(nodeId: string, element: T): void {
if (
this._counters.has(element) &&
this._counters.get(element)?.value()! % 2 === 1
) {
this._counters.get(element)?.increment(nodeId, 1);
}
}

contains(element: T): boolean {
if (this._counters.has(element)) {
return this._counters.get(element)?.value()! % 2 === 1;
}
return false;
}

set(): Set<T> {
let result = new Set<T>();
for (let [element, counter] of this._counters.entries()) {
if (counter.value() % 2 === 1) {
result.add(element);
}
}
return result;
}

compare(peerSet: IPSet<T>): boolean {
// Returns true if peerSet includes all operations that were performed on the given IPSet and possibly more.
// this._counters has to be a subset of peerSet._counters
// and for each element, the value of the counter in this._counters has to be less than or equal to the value of the counter in peerSet._counters
return [...this._counters.keys()].every(
(element) =>
peerSet.counters().has(element) &&
this._counters.get(element)?.value()! <=
peerSet.counters().get(element)?.value()!
);
}

merge(peerSet: IPSet<T>): void {
for (let [element, counter] of peerSet._counters.entries()) {
// if element is not in local replica, set local counter for element to counter
// otherwise, merge the counters
if (!this._counters.has(element)) {
this._counters.set(element, counter);
} else {
this._counters.get(element)?.merge(counter);
}
}
}
}
75 changes: 75 additions & 0 deletions packages/crdt/src/builtins/LWWElementSet/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
export enum Bias {
ADD,
REMOVE
}

export class LWWElementSet<T> {
private _adds: Map<T, number>;
private _removes: Map<T, number>;
public _bias: Bias;

constructor(adds: Map<T, number>, removes: Map<T, number>, bias: Bias) {
this._adds = adds;
this._removes = removes;
this._bias = bias;
}

lookup(element: T): boolean {
const addTimestamp = this._adds.get(element);
if(addTimestamp === undefined) {
return false;
}

const removeTimestamp = this._removes.get(element);
if (removeTimestamp === undefined) {
return true;
}
if (addTimestamp > removeTimestamp) {
return true;
}
if (addTimestamp - removeTimestamp === 0 && this._bias === Bias.ADD) {
return true;
}

return false;
}

add(element: T): void {
this._adds.set(element, Date.now());
}

remove(element: T): void {
this._removes.set(element, Date.now());
}

getAdds(): Map<T, number> {
return this._adds;
}

getRemoves(): Map<T, number> {
return this._removes;
}

compare(peerSet: LWWElementSet<T>): boolean {
return (compareSets(this._adds, peerSet._adds) && compareSets(this._removes, peerSet._removes));
}

merge(peerSet: LWWElementSet<T>): void {
for (let [element, timestamp] of peerSet._adds.entries()) {
const thisTimestamp = this._adds.get(element);
if (!thisTimestamp || thisTimestamp < timestamp) {
this._adds.set(element, timestamp);
}
}
for (let [element, timestamp] of peerSet._removes.entries()) {
const thisTimestamp = this._removes.get(element);
if (!thisTimestamp || thisTimestamp < timestamp) {
this._removes.set(element, timestamp);
}
}
}
}

function compareSets<T>(set1: Map<T, number>, set2: Map<T, number>): boolean {
return (set1.size === set2.size && [...set1.keys()].every(key => set2.has(key)));
}
42 changes: 42 additions & 0 deletions packages/crdt/tests/GCounter.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { describe, test, expect, beforeEach } from "vitest";
import { GCounter } from "../src/builtins/GCounter";

describe("G-Counter Tests", () => {
let set1: GCounter;

beforeEach(() => {
set1 = new GCounter({ "node1": 5, "node2": 10});
});

test("Test Initial Values", () => {
expect(set1.value()).toBe(15);
});

test("Test Increment", () => {
set1.increment("node1", 10);
set1.increment("node2", 5);

expect(set1.value()).toBe(30);
});

test("Test Compare", () => {
let set2 = new GCounter({ "node1": 5, "node2": 10});
let set3 = new GCounter({ "node1": 5, "node2": 10, "node3": 15 });

expect(set1.compare(set2)).toBe(true);
set1.increment("node1", 5);
expect(set1.compare(set2)).toBe(false);
expect(set1.compare(set3)).toBe(false);
});

test("Test Merge", () => {
let set2 = new GCounter({ "node1": 3, "node2": 10});
let set3 = new GCounter({ "node1": 5, "node3": 15});

expect(set1.counts).toEqual({"node1": 5, "node2": 10});
set2.merge(set1);
expect(set2.counts).toEqual({"node1": 5, "node2": 10});
set1.merge(set3);
expect(set1.counts).toEqual({"node1": 5, "node2": 10, "node3": 15});
});
});
46 changes: 46 additions & 0 deletions packages/crdt/tests/GSet.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { describe, test, expect, beforeEach, afterEach } from "vitest";
import { GSet } from "../src/builtins/GSet";

describe("G-Set Tests", () => {

let set1: GSet<string>;
let set2: GSet<string>;

beforeEach(() => {
set1 = new GSet<string>(new Set<string>(["walter", "jesse", "mike"]));
set2 = new GSet<string>(new Set<string>(["walter", "jesse", "mike"]));
});

test("Test Add", () => {
set1.add("gustavo");
set2.add("gustavo");

expect(set1.lookup("gustavo")).toBe(true);
expect(set2.lookup("gustavo")).toBe(true);
});

test("Test Compare", () => {
expect(set1.compare(set2)).toBe(true);

set1.add("gustavo");

expect(set1.compare(set2)).toBe(false);

set2.add("gustavo");

expect(set1.compare(set2)).toBe(true);
});

test("Test Merge", () => {
set1.add("gustavo");
set2.add("lalo");

expect(set1.compare(set2)).toBe(false);

set1.merge(set2);
set2.merge(set1);

expect(set1.compare(set2)).toBe(true);

});
});
Loading

0 comments on commit 0e54168

Please sign in to comment.