diff --git a/.github/workflows/code-quality.yml b/.github/workflows/code-quality.yml new file mode 100644 index 00000000..fc38e50e --- /dev/null +++ b/.github/workflows/code-quality.yml @@ -0,0 +1,19 @@ +name: Code quality +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + quality: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Setup Biome + uses: biomejs/setup-biome@v2 + with: + version: latest + - name: Run Biome + run: biome ci . diff --git a/biome.json b/biome.json new file mode 100644 index 00000000..0296eec0 --- /dev/null +++ b/biome.json @@ -0,0 +1,15 @@ +{ + "$schema": "https://biomejs.dev/schemas/1.8.3/schema.json", + "files": { + "ignore": ["**/dist/*", "**/docs/*"] + }, + "linter": { + "enabled": true, + "rules": { + "recommended": true + } + }, + "organizeImports": { + "enabled": true + } +} diff --git a/examples/canvas/package.json b/examples/canvas/package.json index 8601c21c..24406d22 100644 --- a/examples/canvas/package.json +++ b/examples/canvas/package.json @@ -1,30 +1,30 @@ { - "name": "ts-topology-examples-canvas", - "version": "0.0.23-5", - "license": "MIT", - "scripts": { - "build": "webpack", - "clean": "rm -rf dist/ node_modules/", - "dev": "webpack serve", - "start": "ts-node ./src/index.ts" - }, - "dependencies": { - "@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", - "ts-node": "^10.9.2", - "vm-browserify": "^1.1.2" - }, - "devDependencies": { - "@types/node": "^22.4.1", - "ts-loader": "^9.3.1", - "typescript": "^5.5.4", - "webpack": "^5.74.0", - "webpack-cli": "^5.1.4", - "webpack-dev-server": "^5.0.4" - } + "name": "ts-topology-examples-canvas", + "version": "0.0.23-5", + "license": "MIT", + "scripts": { + "build": "webpack", + "clean": "rm -rf dist/ node_modules/", + "dev": "webpack serve", + "start": "ts-node ./src/index.ts" + }, + "dependencies": { + "@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", + "ts-node": "^10.9.2", + "vm-browserify": "^1.1.2" + }, + "devDependencies": { + "@types/node": "^22.4.1", + "ts-loader": "^9.3.1", + "typescript": "^5.5.4", + "webpack": "^5.74.0", + "webpack-cli": "^5.1.4", + "webpack-dev-server": "^5.0.4" + } } diff --git a/examples/canvas/src/handlers.ts b/examples/canvas/src/handlers.ts index 52417b6b..f326d1fb 100644 --- a/examples/canvas/src/handlers.ts +++ b/examples/canvas/src/handlers.ts @@ -1,43 +1,43 @@ import { toString as uint8ArrayToString } from "uint8arrays/to-string"; -import { ICanvas } from "./objects/canvas"; +import type { ICanvas } from "./objects/canvas"; // TODO: this should be superseded by wasm and main ts-topology library -export const handleCanvasMessages = (canvas: ICanvas, e: any) => { - if (e.detail.msg.topic === "topology::discovery") return; - const input = uint8ArrayToString(e.detail.msg.data); - const message = JSON.parse(input); - switch (message["type"]) { - case "object_update": { - const fn = uint8ArrayToString(new Uint8Array(message["data"])); - handleObjectUpdate(canvas, fn); - break; - } - default: { - break; - } - } +export const handleCanvasMessages = (canvas: ICanvas, e: CustomEvent) => { + if (e.detail.msg.topic === "topology::discovery") return; + const input = uint8ArrayToString(e.detail.msg.data); + const message = JSON.parse(input); + switch (message.type) { + case "object_update": { + const fn = uint8ArrayToString(new Uint8Array(message.data)); + handleObjectUpdate(canvas, fn); + break; + } + default: { + break; + } + } }; function handleObjectUpdate(canvas: ICanvas, fn: string) { - // In this case we only have paint - // `paint(${node.getPeerId()}, [${[x, y]}], [${painting}])` - let args = fn.replace("paint(", "").replace(")", "").split(", "); - let offset_p = args[1] - .replace("[", "") - .replace("]", "") - .split(",") - .map((s) => parseInt(s, 10)); - const offset: [number, number] = [offset_p[0], offset_p[1]]; - let rgb_p = args[2] - .replace("[", "") - .replace("]", "") - .split(",") - .map((s) => parseInt(s, 10)); - const rgb: [number, number, number] = [rgb_p[0], rgb_p[1], rgb_p[2]]; + // In this case we only have paint + // `paint(${node.getPeerId()}, [${[x, y]}], [${painting}])` + const args = fn.replace("paint(", "").replace(")", "").split(", "); + const offset_p = args[1] + .replace("[", "") + .replace("]", "") + .split(",") + .map((s) => Number.parseInt(s, 10)); + const offset: [number, number] = [offset_p[0], offset_p[1]]; + const rgb_p = args[2] + .replace("[", "") + .replace("]", "") + .split(",") + .map((s) => Number.parseInt(s, 10)); + const rgb: [number, number, number] = [rgb_p[0], rgb_p[1], rgb_p[2]]; - try { - canvas.paint(args[0], offset, rgb); - } catch (e) { - console.error(e); - } + try { + canvas.paint(args[0], offset, rgb); + } catch (e) { + console.error(e); + } } diff --git a/examples/canvas/src/index.ts b/examples/canvas/src/index.ts index 72a5b14b..8d4ad124 100644 --- a/examples/canvas/src/index.ts +++ b/examples/canvas/src/index.ts @@ -1,8 +1,8 @@ -import { TopologyNode } from "@topology-foundation/node"; -import { Canvas, ICanvas } from "./objects/canvas"; -import { Pixel } from "./objects/pixel"; import { GCounter } from "@topology-foundation/crdt"; +import { TopologyNode } from "@topology-foundation/node"; import { handleCanvasMessages } from "./handlers"; +import { Canvas, type ICanvas } from "./objects/canvas"; +import { Pixel } from "./objects/pixel"; const node = new TopologyNode(); let canvasCRO: ICanvas; @@ -11,115 +11,115 @@ let discoveryPeers: string[] = []; let objectPeers: string[] = []; const render = () => { - const peers_element = document.getElementById("peers"); - peers_element.innerHTML = "[" + peers.join(", ") + "]"; - - const discovery_element = ( - document.getElementById("discovery_peers") - ); - discovery_element.innerHTML = "[" + discoveryPeers.join(", ") + "]"; - - const object_element = ( - document.getElementById("object_peers") - ); - object_element.innerHTML = "[" + objectPeers.join(", ") + "]"; - - if (!canvasCRO) return; - const canvas = canvasCRO.canvas; - const canvas_element = document.getElementById("canvas"); - canvas_element.innerHTML = ""; - canvas_element.style.display = "inline-grid"; - - canvas_element.style.gridTemplateColumns = Array(canvas.length) - .fill("1fr") - .join(" "); - - for (let x = 0; x < canvas.length; x++) { - for (let y = 0; y < canvas[x].length; y++) { - let pixel = document.createElement("div"); - pixel.id = `${x}-${y}`; - pixel.style.width = "25px"; - pixel.style.height = "25px"; - pixel.style.backgroundColor = `rgb(${canvas[x][y].color()[0]}, ${canvas[x][y].color()[1]}, ${canvas[x][y].color()[2]})`; - pixel.style.cursor = "pointer"; - pixel.addEventListener("click", () => paint_pixel(pixel)); - canvas_element.appendChild(pixel); - } - } + const peers_element = document.getElementById("peers"); + peers_element.innerHTML = `[${peers.join(", ")}]`; + + const discovery_element = ( + document.getElementById("discovery_peers") + ); + discovery_element.innerHTML = `[${discoveryPeers.join(", ")}]`; + + const object_element = ( + document.getElementById("object_peers") + ); + object_element.innerHTML = `[${objectPeers.join(", ")}]`; + + if (!canvasCRO) return; + const canvas = canvasCRO.canvas; + const canvas_element = document.getElementById("canvas"); + canvas_element.innerHTML = ""; + canvas_element.style.display = "inline-grid"; + + canvas_element.style.gridTemplateColumns = Array(canvas.length) + .fill("1fr") + .join(" "); + + for (let x = 0; x < canvas.length; x++) { + for (let y = 0; y < canvas[x].length; y++) { + const pixel = document.createElement("div"); + pixel.id = `${x}-${y}`; + pixel.style.width = "25px"; + pixel.style.height = "25px"; + pixel.style.backgroundColor = `rgb(${canvas[x][y].color()[0]}, ${canvas[x][y].color()[1]}, ${canvas[x][y].color()[2]})`; + pixel.style.cursor = "pointer"; + pixel.addEventListener("click", () => paint_pixel(pixel)); + canvas_element.appendChild(pixel); + } + } }; const random_int = (max: number) => Math.floor(Math.random() * max); async function paint_pixel(pixel: HTMLDivElement) { - const [x, y] = pixel.id.split("-").map((v) => parseInt(v, 10)); - const painting: [number, number, number] = [ - random_int(256), - random_int(256), - random_int(256), - ]; - canvasCRO.paint(node.networkNode.peerId, [x, y], painting); - const [r, g, b] = canvasCRO.pixel(x, y).color(); - pixel.style.backgroundColor = `rgb(${r}, ${g}, ${b})`; - - node.updateObject( - canvasCRO, - `paint(${node.networkNode.peerId}, [${[x, y]}], [${painting}])`, - ); + const [x, y] = pixel.id.split("-").map((v) => Number.parseInt(v, 10)); + const painting: [number, number, number] = [ + random_int(256), + random_int(256), + random_int(256), + ]; + canvasCRO.paint(node.networkNode.peerId, [x, y], painting); + const [r, g, b] = canvasCRO.pixel(x, y).color(); + pixel.style.backgroundColor = `rgb(${r}, ${g}, ${b})`; + + node.updateObject( + canvasCRO, + `paint(${node.networkNode.peerId}, [${[x, y]}], [${painting}])`, + ); } async function init() { - await node.start(); - - node.addCustomGroupMessageHandler((e) => { - handleCanvasMessages(canvasCRO, e); - peers = node.networkNode.getAllPeers(); - discoveryPeers = node.networkNode.getGroupPeers("topology::discovery"); - if (canvasCRO) { - objectPeers = node.networkNode.getGroupPeers(canvasCRO.getObjectId()); - } - render(); - }); - - let create_button = document.getElementById("create"); - create_button.addEventListener("click", () => { - canvasCRO = new Canvas(node.networkNode.peerId, 5, 10); - node.createObject(canvasCRO); - - (document.getElementById("canvasId")).innerText = - canvasCRO.getObjectId(); - render(); - }); - - let connect_button = document.getElementById("connect"); - connect_button.addEventListener("click", async () => { - let croId = (document.getElementById("canvasIdInput")) - .value; - try { - await node.subscribeObject(croId, true, "", (_, topologyObject) => { - let object: any = topologyObject; - object["canvas"] = object["canvas"].map((x: any) => - x.map((y: any) => { - y["red"] = Object.assign(new GCounter({}), y["red"]); - y["green"] = Object.assign(new GCounter({}), y["green"]); - y["blue"] = Object.assign(new GCounter({}), y["blue"]); - return Object.assign(new Pixel(node.networkNode.peerId), y); - }) - ); - - canvasCRO = Object.assign( - new Canvas(node.networkNode.peerId, 0, 0), - object - ); - - (document.getElementById("canvasId")).innerText = - croId; - render(); - }); - // TODO remove the need to click to time for subscribe and fetch - } catch (e) { - console.error("Error while connecting with CRO", croId, e); - } - }); + await node.start(); + + node.addCustomGroupMessageHandler((e) => { + handleCanvasMessages(canvasCRO, e); + peers = node.networkNode.getAllPeers(); + discoveryPeers = node.networkNode.getGroupPeers("topology::discovery"); + if (canvasCRO) { + objectPeers = node.networkNode.getGroupPeers(canvasCRO.getObjectId()); + } + render(); + }); + + const create_button = document.getElementById("create"); + create_button.addEventListener("click", () => { + canvasCRO = new Canvas(node.networkNode.peerId, 5, 10); + node.createObject(canvasCRO); + + (document.getElementById("canvasId")).innerText = + canvasCRO.getObjectId(); + render(); + }); + + const connect_button = document.getElementById("connect"); + connect_button.addEventListener("click", async () => { + const croId = (document.getElementById("canvasIdInput")) + .value; + try { + await node.subscribeObject(croId, true, "", (_, topologyObject) => { + const object: Canvas = topologyObject as Canvas; + object.canvas = object.canvas.map((x: Pixel[]) => + x.map((y: Pixel) => { + y.red = Object.assign(new GCounter({}), y.red); + y.green = Object.assign(new GCounter({}), y.green); + y.blue = Object.assign(new GCounter({}), y.blue); + return Object.assign(new Pixel(node.networkNode.peerId), y); + }), + ); + + canvasCRO = Object.assign( + new Canvas(node.networkNode.peerId, 0, 0), + object, + ); + + (document.getElementById("canvasId")).innerText = + croId; + render(); + }); + // TODO remove the need to click to time for subscribe and fetch + } catch (e) { + console.error("Error while connecting with CRO", croId, e); + } + }); } init(); diff --git a/examples/canvas/src/objects/abi/canvas.json b/examples/canvas/src/objects/abi/canvas.json index f3e2ae0c..8c259fa6 100644 --- a/examples/canvas/src/objects/abi/canvas.json +++ b/examples/canvas/src/objects/abi/canvas.json @@ -1,11 +1,11 @@ { - "imports": ["./pixel.json"], - "fns": [ - "fn splash(string, [u64][u64], [u64][u64], [u64, u64, u64]): void", - "fn paint(string, [u64][u64], [u64, u64, u64]): void", - "fn canvas(): [u64, u64, u64][][]", - "fn pixel(u64, u64): Pixel" - ], - "vm": "wasm", - "bytecode": [] + "imports": ["./pixel.json"], + "fns": [ + "fn splash(string, [u64][u64], [u64][u64], [u64, u64, u64]): void", + "fn paint(string, [u64][u64], [u64, u64, u64]): void", + "fn canvas(): [u64, u64, u64][][]", + "fn pixel(u64, u64): Pixel" + ], + "vm": "wasm", + "bytecode": [] } diff --git a/examples/canvas/src/objects/abi/pixel.json b/examples/canvas/src/objects/abi/pixel.json index 391024a1..d3ef530c 100644 --- a/examples/canvas/src/objects/abi/pixel.json +++ b/examples/canvas/src/objects/abi/pixel.json @@ -1,10 +1,10 @@ { - "imports": [], - "fns": [ - "fn color(): [u64, u64, u64]", - "fn paint(string, [u64, u64, u64]): void", - "fn counters(): [topology::builtin::GCounter, topology::builtin::GCounter, topology::builtin::GCounter]" - ], - "vm": "wasm", - "bytecode": [] + "imports": [], + "fns": [ + "fn color(): [u64, u64, u64]", + "fn paint(string, [u64, u64, u64]): void", + "fn counters(): [topology::builtin::GCounter, topology::builtin::GCounter, topology::builtin::GCounter]" + ], + "vm": "wasm", + "bytecode": [] } diff --git a/examples/canvas/src/objects/canvas.ts b/examples/canvas/src/objects/canvas.ts index 708ff1a4..ab3bd8f3 100644 --- a/examples/canvas/src/objects/canvas.ts +++ b/examples/canvas/src/objects/canvas.ts @@ -1,73 +1,73 @@ import { TopologyObject } from "@topology-foundation/object"; -import { IPixel, Pixel } from "./pixel"; +import { type IPixel, Pixel } from "./pixel"; export interface ICanvas extends TopologyObject { - width: number; - height: number; - canvas: IPixel[][]; - splash( - node_id: string, - offset: [number, number], - size: [number, number], - rgb: [number, number, number], - ): void; - paint( - nodeId: string, - offset: [number, number], - rgb: [number, number, number], - ): void; - pixel(x: number, y: number): IPixel; - merge(peerCanvas: Canvas): void; + width: number; + height: number; + canvas: IPixel[][]; + splash( + node_id: string, + offset: [number, number], + size: [number, number], + rgb: [number, number, number], + ): void; + paint( + nodeId: string, + offset: [number, number], + rgb: [number, number, number], + ): void; + pixel(x: number, y: number): IPixel; + merge(peerCanvas: Canvas): void; } export class Canvas extends TopologyObject implements ICanvas { - width: number; - height: number; - canvas: IPixel[][]; + width: number; + height: number; + canvas: IPixel[][]; - constructor(peerId: string, width: number, height: number) { - super(peerId); - this.width = width; - this.height = height; - this.canvas = Array.from(new Array(width), () => - Array.from(new Array(height), () => new Pixel(peerId)), - ); - } + constructor(peerId: string, width: number, height: number) { + super(peerId); + this.width = width; + this.height = height; + this.canvas = Array.from(new Array(width), () => + Array.from(new Array(height), () => new Pixel(peerId)), + ); + } - splash( - node_id: string, - offset: [number, number], - size: [number, number], - rgb: [number, number, number], - ): void { - if (offset[0] < 0 || this.width < offset[0]) return; - if (offset[1] < 0 || this.height < offset[1]) return; + splash( + node_id: string, + offset: [number, number], + size: [number, number], + rgb: [number, number, number], + ): void { + if (offset[0] < 0 || this.width < offset[0]) return; + if (offset[1] < 0 || this.height < offset[1]) return; - for (let x = offset[0]; x < this.width || x < offset[0] + size[0]; x++) { - for (let y = offset[1]; y < this.height || y < offset[1] + size[1]; y++) { - this.canvas[x][y].paint(node_id, rgb); - } - } - } + for (let x = offset[0]; x < this.width || x < offset[0] + size[0]; x++) { + for (let y = offset[1]; y < this.height || y < offset[1] + size[1]; y++) { + this.canvas[x][y].paint(node_id, rgb); + } + } + } - paint( - nodeId: string, - offset: [number, number], - rgb: [number, number, number], - ): void { - if (offset[0] < 0 || this.canvas.length < offset[0]) return; - if (offset[1] < 0 || this.canvas[offset[0]].length < offset[1]) return; + paint( + nodeId: string, + offset: [number, number], + rgb: [number, number, number], + ): void { + if (offset[0] < 0 || this.canvas.length < offset[0]) return; + if (offset[1] < 0 || this.canvas[offset[0]].length < offset[1]) return; - this.canvas[offset[0]][offset[1]].paint(nodeId, rgb); - } + this.canvas[offset[0]][offset[1]].paint(nodeId, rgb); + } - pixel(x: number, y: number): IPixel { - return this.canvas[x][y]; - } + pixel(x: number, y: number): IPixel { + return this.canvas[x][y]; + } - merge(peerCanvas: Canvas): void { - this.canvas.forEach((row, x) => - row.forEach((pixel, y) => pixel.merge(peerCanvas.pixel(x, y))), - ); - } + merge(peerCanvas: Canvas): void { + this.canvas.forEach((row, x) => + row.forEach((pixel, y) => pixel.merge(peerCanvas.pixel(x, y))), + ); + } } diff --git a/examples/canvas/src/objects/pixel.ts b/examples/canvas/src/objects/pixel.ts index 8c5eaf40..7d3b433a 100644 --- a/examples/canvas/src/objects/pixel.ts +++ b/examples/canvas/src/objects/pixel.ts @@ -2,49 +2,49 @@ import { GCounter } from "@topology-foundation/crdt"; import { TopologyObject } from "@topology-foundation/object"; export interface IPixel extends TopologyObject { - red: GCounter; - green: GCounter; - blue: GCounter; - color(): [number, number, number]; - paint(nodeId: string, rgb: [number, number, number]): void; - counters(): [GCounter, GCounter, GCounter]; - merge(peerPixel: IPixel): void; + red: GCounter; + green: GCounter; + blue: GCounter; + color(): [number, number, number]; + paint(nodeId: string, rgb: [number, number, number]): void; + counters(): [GCounter, GCounter, GCounter]; + merge(peerPixel: IPixel): void; } export class Pixel extends TopologyObject implements IPixel { - red: GCounter; - green: GCounter; - blue: GCounter; + red: GCounter; + green: GCounter; + blue: GCounter; - constructor(peerId: string) { - super(peerId); - this.red = new GCounter({}); - this.green = new GCounter({}); - this.blue = new GCounter({}); - } + constructor(peerId: string) { + super(peerId); + this.red = new GCounter({}); + this.green = new GCounter({}); + this.blue = new GCounter({}); + } - color(): [number, number, number] { - return [ - this.red.value() % 256, - this.green.value() % 256, - this.blue.value() % 256, - ]; - } + color(): [number, number, number] { + return [ + this.red.value() % 256, + this.green.value() % 256, + this.blue.value() % 256, + ]; + } - paint(nodeId: string, rgb: [number, number, number]): void { - this.red.increment(nodeId, rgb[0]); - this.green.increment(nodeId, rgb[1]); - this.blue.increment(nodeId, rgb[2]); - } + paint(nodeId: string, rgb: [number, number, number]): void { + this.red.increment(nodeId, rgb[0]); + this.green.increment(nodeId, rgb[1]); + this.blue.increment(nodeId, rgb[2]); + } - counters(): [GCounter, GCounter, GCounter] { - return [this.red, this.green, this.blue]; - } + counters(): [GCounter, GCounter, GCounter] { + return [this.red, this.green, this.blue]; + } - merge(peerPixel: Pixel): void { - let peerCounters = peerPixel.counters(); - this.red.merge(peerCounters[0]); - this.green.merge(peerCounters[1]); - this.blue.merge(peerCounters[2]); - } + merge(peerPixel: Pixel): void { + const peerCounters = peerPixel.counters(); + this.red.merge(peerCounters[0]); + this.green.merge(peerCounters[1]); + this.blue.merge(peerCounters[2]); + } } diff --git a/examples/canvas/tsconfig.json b/examples/canvas/tsconfig.json index 10885f11..77115c0f 100644 --- a/examples/canvas/tsconfig.json +++ b/examples/canvas/tsconfig.json @@ -1,14 +1,14 @@ { - "compilerOptions": { - "target": "es6", - "module": "ESNEXT", - "rootDir": ".", - "strict": true, - "moduleResolution": "node", - "allowSyntheticDefaultImports": true, - "allowJs": true, - "esModuleInterop": true, - "skipLibCheck": true, - "forceConsistentCasingInFileNames": true - } + "compilerOptions": { + "target": "es6", + "module": "ESNEXT", + "rootDir": ".", + "strict": true, + "moduleResolution": "node", + "allowSyntheticDefaultImports": true, + "allowJs": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true + } } diff --git a/examples/canvas/webpack.config.js b/examples/canvas/webpack.config.js index 46ac5e36..acdc4d71 100644 --- a/examples/canvas/webpack.config.js +++ b/examples/canvas/webpack.config.js @@ -1,63 +1,66 @@ -const path = require("path"); +const path = require("node:path"); const webpack = require("webpack"); -const fs = require("fs"); +const fs = require("node:fs"); module.exports = { - mode: "production", - entry: path.resolve(__dirname, "./src/index.ts"), - devServer: { - allowedHosts: "all", - client: { - overlay: false, - }, - static: { - directory: path.join(__dirname, "public"), - }, - compress: true, - hot: true, - port: 3000, - }, - module: { - rules: [ - { - test: /\.ts?$/, - use: "ts-loader", - exclude: /node_modules/, - }, - { - test: /\.m?js$/, - resolve: { - fullySpecified: false, - }, - }, - ], - }, - resolve: { - extensions: [".ts", ".js"], - fallback: { - crypto: require.resolve("crypto-browserify"), - dgram: false, - os: false, - net: false, - path: false, - "process/browser": require.resolve("process/browser"), - stream: require.resolve("stream-browserify"), - vm: require.resolve("vm-browserify"), - }, - }, - output: { - filename: "script.js", - path: path.resolve(__dirname, "public", "static", "bundle"), - publicPath: "/static/bundle/", - }, - performance: { - hints: false, - maxEntrypointSize: 512000, - maxAssetSize: 512000, - }, - plugins: [ - new webpack.ProvidePlugin({ - process: "process/browser", - }), - ], + mode: "production", + entry: path.resolve(__dirname, "./src/index.ts"), + externals: { + "node:crypto": "commonjs crypto", + }, + devServer: { + allowedHosts: "all", + client: { + overlay: false, + }, + static: { + directory: path.join(__dirname, "public"), + }, + compress: true, + hot: true, + port: 3000, + }, + module: { + rules: [ + { + test: /\.ts?$/, + use: "ts-loader", + exclude: /node_modules/, + }, + { + test: /\.m?js$/, + resolve: { + fullySpecified: false, + }, + }, + ], + }, + resolve: { + extensions: [".ts", ".js"], + fallback: { + crypto: require.resolve("crypto-browserify"), + dgram: false, + os: false, + net: false, + path: false, + "process/browser": require.resolve("process/browser"), + stream: require.resolve("stream-browserify"), + vm: require.resolve("vm-browserify"), + }, + }, + output: { + filename: "script.js", + path: path.resolve(__dirname, "public", "static", "bundle"), + publicPath: "/static/bundle/", + }, + performance: { + hints: false, + maxEntrypointSize: 512000, + maxAssetSize: 512000, + }, + plugins: [ + new webpack.ProvidePlugin({ + process: "process/browser", + }), + ], }; diff --git a/examples/chat/package.json b/examples/chat/package.json index a1cc3943..bdc5d9f9 100644 --- a/examples/chat/package.json +++ b/examples/chat/package.json @@ -1,33 +1,33 @@ { - "name": "topology-example-chat", - "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": "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", - "ts-node": "^10.9.2", - "vm-browserify": "^1.1.2" - }, - "devDependencies": { - "@types/node": "^22.4.1", - "ts-loader": "^9.3.1", - "typescript": "^5.5.4", - "webpack": "^5.74.0", - "webpack-cli": "^5.1.4", - "webpack-dev-server": "^5.0.4" - }, - "scripts": { - "build": "webpack", - "clean": "rm -rf dist/ node_modules/", - "dev": "webpack serve", - "start": "ts-node ./src/index.ts" - } + "name": "topology-example-chat", + "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": "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", + "ts-node": "^10.9.2", + "vm-browserify": "^1.1.2" + }, + "devDependencies": { + "@types/node": "^22.4.1", + "ts-loader": "^9.3.1", + "typescript": "^5.5.4", + "webpack": "^5.74.0", + "webpack-cli": "^5.1.4", + "webpack-dev-server": "^5.0.4" + }, + "scripts": { + "build": "webpack", + "clean": "rm -rf dist/ node_modules/", + "dev": "webpack serve", + "start": "ts-node ./src/index.ts" + } } diff --git a/examples/chat/src/handlers.ts b/examples/chat/src/handlers.ts index 2c6ccb83..daff3b5b 100644 --- a/examples/chat/src/handlers.ts +++ b/examples/chat/src/handlers.ts @@ -1,31 +1,31 @@ import { toString as uint8ArrayToString } from "uint8arrays/to-string"; -import { IChat } from "./objects/chat"; +import type { IChat } from "./objects/chat"; -export const handleChatMessages = (chat: IChat, e: any) => { - if (e.detail.msg.topic === "topology::discovery") return; - const input = uint8ArrayToString(e.detail.msg.data); - const message = JSON.parse(input); - console.log("Received message!: ", message); - switch (message["type"]) { - case "object_update": { - const fn = uint8ArrayToString(new Uint8Array(message["data"])); - handleObjectUpdate(chat, fn); - break; - } - default: { - break; - } - } +export const handleChatMessages = (chat: IChat, e: CustomEvent) => { + if (e.detail.msg.topic === "topology::discovery") return; + const input = uint8ArrayToString(e.detail.msg.data); + const message = JSON.parse(input); + console.log("Received message!: ", message); + switch (message.type) { + case "object_update": { + const fn = uint8ArrayToString(new Uint8Array(message.data)); + handleObjectUpdate(chat, fn); + break; + } + default: { + break; + } + } }; function handleObjectUpdate(chat: IChat, fn: string) { - // In this case we only have addMessage - // `addMessage(${timestamp}, ${message}, ${node.getPeerId()})` - let args = fn.replace("addMessage(", "").replace(")", "").split(", "); - console.log("Received message: ", args); - try { - chat.addMessage(args[0], args[1], args[2]); - } catch (e) { - console.error(e); - } -} \ No newline at end of file + // In this case we only have addMessage + // `addMessage(${timestamp}, ${message}, ${node.getPeerId()})` + const args = fn.replace("addMessage(", "").replace(")", "").split(", "); + console.log("Received message: ", args); + try { + chat.addMessage(args[0], args[1], args[2]); + } catch (e) { + console.error(e); + } +} diff --git a/examples/chat/src/index.ts b/examples/chat/src/index.ts index 678da38b..b1c771f1 100644 --- a/examples/chat/src/index.ts +++ b/examples/chat/src/index.ts @@ -1,7 +1,8 @@ +import { GSet } from "@topology-foundation/crdt"; import { TopologyNode } from "@topology-foundation/node"; -import { Chat, IChat } from "./objects/chat"; +import { TopologyObject } from "@topology-foundation/object"; import { handleChatMessages } from "./handlers"; -import { GSet } from "@topology-foundation/crdt"; +import { Chat, type IChat } from "./objects/chat"; const node = new TopologyNode(); // CRO = Conflict-free Replicated Object @@ -11,119 +12,144 @@ let discoveryPeers: string[] = []; let objectPeers: string[] = []; const render = () => { - const element_peerId = document.getElementById("peerId"); - element_peerId.innerHTML = node.networkNode.peerId; - - const element_peers = document.getElementById("peers"); - element_peers.innerHTML = "[" + peers.join(", ") + "]"; - - const element_discoveryPeers = document.getElementById("discoveryPeers"); - element_discoveryPeers.innerHTML = "[" + discoveryPeers.join(", ") + "]"; - - const element_objectPeers = document.getElementById("objectPeers"); - element_objectPeers.innerHTML = "[" + objectPeers.join(", ") + "]"; - - if(!chatCRO) return; - const chat = chatCRO.getMessages(); - const element_chat = document.getElementById("chat"); - element_chat.innerHTML = ""; - - if(chat.set().size == 0){ - const div = document.createElement("div"); - div.innerHTML = "No messages yet"; - div.style.padding = "10px"; - element_chat.appendChild(div); - return; - } - Array.from(chat.set()).sort().forEach((message: string) => { - const div = document.createElement("div"); - div.innerHTML = message; - div.style.padding = "10px"; - element_chat.appendChild(div); - }); - -} + const element_peerId = document.getElementById("peerId"); + element_peerId.innerHTML = node.networkNode.peerId; + + const element_peers = document.getElementById("peers"); + element_peers.innerHTML = `[${peers.join(", ")}]`; + + const element_discoveryPeers = ( + document.getElementById("discoveryPeers") + ); + element_discoveryPeers.innerHTML = `[${discoveryPeers.join(", ")}]`; + + const element_objectPeers = ( + document.getElementById("objectPeers") + ); + element_objectPeers.innerHTML = `[${objectPeers.join(", ")}]`; + + if (!chatCRO) return; + const chat = chatCRO.getMessages(); + const element_chat = document.getElementById("chat"); + element_chat.innerHTML = ""; + + if (chat.set.size === 0) { + const div = document.createElement("div"); + div.innerHTML = "No messages yet"; + div.style.padding = "10px"; + element_chat.appendChild(div); + return; + } + + for (const message of [...chat.set].sort()) { + const div = document.createElement("div"); + div.innerHTML = message; + div.style.padding = "10px"; + element_chat.appendChild(div); + } +}; async function sendMessage(message: string) { - let timestamp: string = Date.now().toString(); - if(!chatCRO) { - console.error("Chat CRO not initialized"); - alert("Please create or join a chat room first"); - return; - } - console.log("Sending message: ", `(${timestamp}, ${message}, ${node.networkNode.peerId})`); - chatCRO.addMessage(timestamp, message, node.networkNode.peerId); - - node.updateObject(chatCRO, `addMessage(${timestamp}, ${message}, ${node.networkNode.peerId})`); - render(); + const timestamp: string = Date.now().toString(); + if (!chatCRO) { + console.error("Chat CRO not initialized"); + alert("Please create or join a chat room first"); + return; + } + console.log( + "Sending message: ", + `(${timestamp}, ${message}, ${node.networkNode.peerId})`, + ); + chatCRO.addMessage(timestamp, message, node.networkNode.peerId); + + node.updateObject( + chatCRO, + `addMessage(${timestamp}, ${message}, ${node.networkNode.peerId})`, + ); + render(); } async function main() { - await node.start(); - render(); - - node.addCustomGroupMessageHandler((e) => { - handleChatMessages(chatCRO, e); - peers = node.networkNode.getAllPeers(); - discoveryPeers = node.networkNode.getGroupPeers("topology::discovery"); - if(chatCRO) objectPeers = node.networkNode.getGroupPeers(chatCRO.getObjectId()); - render(); - }); - - let button_create = document.getElementById("createRoom"); - button_create.addEventListener("click", () => { - chatCRO = new Chat(node.networkNode.peerId); - node.createObject(chatCRO); - (document.getElementById("chatId")).innerHTML = chatCRO.getObjectId(); - render(); - }); - - let button_connect = document.getElementById("joinRoom"); - button_connect.addEventListener("click", async () => { - let input: HTMLInputElement = document.getElementById("roomInput"); - let objectId = input.value; - if(!objectId){ - alert("Please enter a room id"); - return; - } - await node.subscribeObject(objectId, true); - }); - - let button_fetch = document.getElementById("fetchMessages"); - button_fetch.addEventListener("click", async () => { - let input: HTMLInputElement = document.getElementById("roomInput"); - let objectId = input.value; - try { - - let object: any = node.getObject(objectId); - console.log("Object received: ", object); - - let arr: string[] = Array.from(object["chat"]["_set"]); - object["chat"]["_set"] = new Set(arr); - object["chat"] = Object.assign(new GSet(new Set()), object["chat"]); - chatCRO = Object.assign(new Chat(node.networkNode.peerId), object); - - (document.getElementById("chatId")).innerHTML = chatCRO.getObjectId(); - render(); - } catch (e) { - console.error("Error while connecting to the CRO ", objectId, e); - } - }); - - let button_send = document.getElementById("sendMessage"); - button_send.addEventListener("click", async () => { - let input: HTMLInputElement = document.getElementById("messageInput"); - let message: string = input.value; - input.value = ""; - if(!message){ - console.error("Tried sending an empty message"); - alert("Please enter a message"); - return; - } - await sendMessage(message); - const element_chat = document.getElementById("chat"); - element_chat.scrollTop = element_chat.scrollHeight; - }); + await node.start(); + render(); + + node.addCustomGroupMessageHandler((e) => { + handleChatMessages(chatCRO, e); + peers = node.networkNode.getAllPeers(); + discoveryPeers = node.networkNode.getGroupPeers("topology::discovery"); + if (chatCRO) + objectPeers = node.networkNode.getGroupPeers(chatCRO.getObjectId()); + render(); + }); + + const button_create = ( + document.getElementById("createRoom") + ); + button_create.addEventListener("click", () => { + chatCRO = new Chat(node.networkNode.peerId); + node.createObject(chatCRO); + (document.getElementById("chatId")).innerHTML = + chatCRO.getObjectId(); + render(); + }); + + const button_connect = document.getElementById("joinRoom"); + button_connect.addEventListener("click", async () => { + const input: HTMLInputElement = ( + document.getElementById("roomInput") + ); + const objectId = input.value; + if (!objectId) { + alert("Please enter a room id"); + return; + } + await node.subscribeObject(objectId, true); + }); + + const button_fetch = ( + document.getElementById("fetchMessages") + ); + button_fetch.addEventListener("click", async () => { + const input: HTMLInputElement = ( + document.getElementById("roomInput") + ); + const objectId = input.value; + try { + const object: Chat = node.getObject(objectId) as Chat; + console.log("Object received: ", object); + + const arr: string[] = Array.from(object.chat.set); + object.chat.set = new Set(arr); + object.chat = Object.assign( + new GSet(new Set()), + object.chat, + ); + chatCRO = Object.assign(new Chat(node.networkNode.peerId), object); + + (document.getElementById("chatId")).innerHTML = + chatCRO.getObjectId(); + render(); + } catch (e) { + console.error("Error while connecting to the CRO ", objectId, e); + } + }); + + const button_send = document.getElementById("sendMessage"); + button_send.addEventListener("click", async () => { + const input: HTMLInputElement = ( + document.getElementById("messageInput") + ); + const message: string = input.value; + input.value = ""; + if (!message) { + console.error("Tried sending an empty message"); + alert("Please enter a message"); + return; + } + await sendMessage(message); + const element_chat = document.getElementById("chat"); + element_chat.scrollTop = element_chat.scrollHeight; + }); } -main(); \ No newline at end of file +main(); diff --git a/examples/chat/src/objects/chat.ts b/examples/chat/src/objects/chat.ts index d8333df7..855890e1 100644 --- a/examples/chat/src/objects/chat.ts +++ b/examples/chat/src/objects/chat.ts @@ -1,32 +1,31 @@ -import { TopologyObject } from "@topology-foundation/object"; import { GSet } from "@topology-foundation/crdt"; +import { TopologyObject } from "@topology-foundation/object"; export interface IChat extends TopologyObject { - chat: GSet; - addMessage(timestamp: string, message: string, node_id: string): void; - getMessages(): GSet; - merge(other: Chat): void; + chat: GSet; + addMessage(timestamp: string, message: string, node_id: string): void; + getMessages(): GSet; + merge(other: Chat): void; } export class Chat extends TopologyObject implements IChat { - // store messages as strings in the format (timestamp, message, peerId) - chat: GSet; - - constructor(peerId: string) { - super(peerId); - this.chat = new GSet(new Set()); - } + // store messages as strings in the format (timestamp, message, peerId) + chat: GSet; - addMessage(timestamp: string, message: string, node_id: string): void { - this.chat.add(`(${timestamp}, ${message}, ${node_id})`); - } + constructor(peerId: string) { + super(peerId); + this.chat = new GSet(new Set()); + } - getMessages(): GSet { - return this.chat; - } + addMessage(timestamp: string, message: string, node_id: string): void { + this.chat.add(`(${timestamp}, ${message}, ${node_id})`); + } - merge(other: Chat): void { - this.chat.merge(other.chat); - } + getMessages(): GSet { + return this.chat; + } + merge(other: Chat): void { + this.chat.merge(other.chat); + } } diff --git a/examples/chat/tsconfig.json b/examples/chat/tsconfig.json index 86c25cb7..77115c0f 100644 --- a/examples/chat/tsconfig.json +++ b/examples/chat/tsconfig.json @@ -1,14 +1,14 @@ { - "compilerOptions": { - "target": "es6", - "module": "ESNEXT", - "rootDir": ".", - "strict": true, - "moduleResolution": "node", - "allowSyntheticDefaultImports": true, - "allowJs": true, - "esModuleInterop": true, - "skipLibCheck": true, - "forceConsistentCasingInFileNames": true - } + "compilerOptions": { + "target": "es6", + "module": "ESNEXT", + "rootDir": ".", + "strict": true, + "moduleResolution": "node", + "allowSyntheticDefaultImports": true, + "allowJs": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true + } } diff --git a/examples/chat/webpack.config.js b/examples/chat/webpack.config.js index 46ac5e36..acdc4d71 100644 --- a/examples/chat/webpack.config.js +++ b/examples/chat/webpack.config.js @@ -1,63 +1,66 @@ -const path = require("path"); +const path = require("node:path"); const webpack = require("webpack"); -const fs = require("fs"); +const fs = require("node:fs"); module.exports = { - mode: "production", - entry: path.resolve(__dirname, "./src/index.ts"), - devServer: { - allowedHosts: "all", - client: { - overlay: false, - }, - static: { - directory: path.join(__dirname, "public"), - }, - compress: true, - hot: true, - port: 3000, - }, - module: { - rules: [ - { - test: /\.ts?$/, - use: "ts-loader", - exclude: /node_modules/, - }, - { - test: /\.m?js$/, - resolve: { - fullySpecified: false, - }, - }, - ], - }, - resolve: { - extensions: [".ts", ".js"], - fallback: { - crypto: require.resolve("crypto-browserify"), - dgram: false, - os: false, - net: false, - path: false, - "process/browser": require.resolve("process/browser"), - stream: require.resolve("stream-browserify"), - vm: require.resolve("vm-browserify"), - }, - }, - output: { - filename: "script.js", - path: path.resolve(__dirname, "public", "static", "bundle"), - publicPath: "/static/bundle/", - }, - performance: { - hints: false, - maxEntrypointSize: 512000, - maxAssetSize: 512000, - }, - plugins: [ - new webpack.ProvidePlugin({ - process: "process/browser", - }), - ], + mode: "production", + entry: path.resolve(__dirname, "./src/index.ts"), + externals: { + "node:crypto": "commonjs crypto", + }, + devServer: { + allowedHosts: "all", + client: { + overlay: false, + }, + static: { + directory: path.join(__dirname, "public"), + }, + compress: true, + hot: true, + port: 3000, + }, + module: { + rules: [ + { + test: /\.ts?$/, + use: "ts-loader", + exclude: /node_modules/, + }, + { + test: /\.m?js$/, + resolve: { + fullySpecified: false, + }, + }, + ], + }, + resolve: { + extensions: [".ts", ".js"], + fallback: { + crypto: require.resolve("crypto-browserify"), + dgram: false, + os: false, + net: false, + path: false, + "process/browser": require.resolve("process/browser"), + stream: require.resolve("stream-browserify"), + vm: require.resolve("vm-browserify"), + }, + }, + output: { + filename: "script.js", + path: path.resolve(__dirname, "public", "static", "bundle"), + publicPath: "/static/bundle/", + }, + performance: { + hints: false, + maxEntrypointSize: 512000, + maxAssetSize: 512000, + }, + plugins: [ + new webpack.ProvidePlugin({ + process: "process/browser", + }), + ], }; diff --git a/package.json b/package.json index f7d89132..2f2ac4a1 100644 --- a/package.json +++ b/package.json @@ -1,46 +1,45 @@ { - "name": "ts-topology", - "description": "The official TypeScript implementation of Topology Protocol", - "version": "0.0.23-5", - "license": "MIT", - "homepage": "https://topology.gg/", - "repository": { - "type": "git", - "url": "git+https://github.com/topology-foundation/ts-topology.git" - }, - "bugs": { - "url": "https://github.com/topology-foundation/ts-topology/issues" - }, - "scripts": { - "docs": "typedoc", - "release": "release-it", - "test": "vitest" - }, - "devDependencies": { - "@release-it-plugins/workspaces": "^4.2.0", - "@types/node": "^22.4.1", - "release-it": "^17.6.0", - "typedoc": "^0.26.4", - "typescript": "^5.5.4", - "vitest": "^2.0.5" - }, - "workspaces": [ - "packages/*", - "examples/*" - ], - "private": true, - "release-it": { - "plugins": { - "@release-it-plugins/workspaces": { - "publish": false - } - }, - "npm": false, - "git": { - "commitMessage": "chore: release v${version}" - }, - "github": { - "release": true - } - } + "name": "ts-topology", + "description": "The official TypeScript implementation of Topology Protocol", + "version": "0.0.23-5", + "license": "MIT", + "homepage": "https://topology.gg/", + "repository": { + "type": "git", + "url": "git+https://github.com/topology-foundation/ts-topology.git" + }, + "bugs": { + "url": "https://github.com/topology-foundation/ts-topology/issues" + }, + "scripts": { + "biome-check": "biome check", + "docs": "typedoc", + "release": "release-it", + "test": "vitest" + }, + "devDependencies": { + "@biomejs/biome": "1.8.3", + "@release-it-plugins/workspaces": "^4.2.0", + "@types/node": "^22.4.1", + "release-it": "^17.6.0", + "typedoc": "^0.26.4", + "typescript": "^5.5.4", + "vitest": "^2.0.5" + }, + "workspaces": ["packages/*", "examples/*"], + "private": true, + "release-it": { + "plugins": { + "@release-it-plugins/workspaces": { + "publish": false + } + }, + "npm": false, + "git": { + "commitMessage": "chore: release v${version}" + }, + "github": { + "release": true + } + } } diff --git a/packages/crdt/package.json b/packages/crdt/package.json index 4a6eab57..040b3232 100644 --- a/packages/crdt/package.json +++ b/packages/crdt/package.json @@ -1,29 +1,24 @@ { - "name": "@topology-foundation/crdt", - "version": "0.0.23-5", - "license": "MIT", - "repository": { - "type": "git", - "url": "git+https://github.com/topology-foundation/ts-topology.git" - }, - "type": "module", - "types": "./dist/src/index.d.ts", - "files": [ - "src", - "dist", - "!dist/test", - "!**/*.tsbuildinfo" - ], - "exports": { - ".": { - "types": "./dist/src/index.d.ts", - "import": "./dist/src/index.js" - } - }, - "scripts": { - "build": "tsc -b", - "clean": "rm -rf dist/ node_modules/", - "prepack": "tsc -b", - "test": "vitest" - } + "name": "@topology-foundation/crdt", + "version": "0.0.23-5", + "license": "MIT", + "repository": { + "type": "git", + "url": "git+https://github.com/topology-foundation/ts-topology.git" + }, + "type": "module", + "types": "./dist/src/index.d.ts", + "files": ["src", "dist", "!dist/test", "!**/*.tsbuildinfo"], + "exports": { + ".": { + "types": "./dist/src/index.d.ts", + "import": "./dist/src/index.js" + } + }, + "scripts": { + "build": "tsc -b", + "clean": "rm -rf dist/ node_modules/", + "prepack": "tsc -b", + "test": "vitest" + } } diff --git a/packages/crdt/src/crdts/2PSet/index.ts b/packages/crdt/src/crdts/2PSet/index.ts index ca566195..fd63dae6 100644 --- a/packages/crdt/src/crdts/2PSet/index.ts +++ b/packages/crdt/src/crdts/2PSet/index.ts @@ -1,44 +1,44 @@ -import { GSet } from "../GSet/index.js"; +import type { GSet } from "../GSet/index.js"; /// 2PSet with support for state and op changes export class TwoPSet { - private _adds: GSet; - private _removes: GSet; - - constructor(adds: GSet, removes: GSet) { - this._adds = adds; - this._removes = removes; - } - - lookup(element: T): boolean { - return this._adds.lookup(element) && !this._removes.lookup(element); - } - - add(element: T): void { - this._adds.add(element); - } - - remove(element: T): void { - this._removes.add(element); - } - - adds(): GSet { - return this._adds; - } - - removes(): GSet { - return this._removes; - } - - compare(peerSet: TwoPSet): boolean { - return ( - this._adds.compare(peerSet.adds()) && - this._removes.compare(peerSet.removes()) - ); - } - - merge(peerSet: TwoPSet): void { - this._adds.merge(peerSet.adds()); - this._removes.merge(peerSet.removes()); - } + private _adds: GSet; + private _removes: GSet; + + constructor(adds: GSet, removes: GSet) { + this._adds = adds; + this._removes = removes; + } + + lookup(element: T): boolean { + return this._adds.lookup(element) && !this._removes.lookup(element); + } + + add(element: T): void { + this._adds.add(element); + } + + remove(element: T): void { + this._removes.add(element); + } + + adds(): GSet { + return this._adds; + } + + removes(): GSet { + return this._removes; + } + + compare(peerSet: TwoPSet): boolean { + return ( + this._adds.compare(peerSet.adds()) && + this._removes.compare(peerSet.removes()) + ); + } + + merge(peerSet: TwoPSet): void { + this._adds.merge(peerSet.adds()); + this._removes.merge(peerSet.removes()); + } } diff --git a/packages/crdt/src/crdts/GCounter/index.ts b/packages/crdt/src/crdts/GCounter/index.ts index e518c359..932a14f6 100644 --- a/packages/crdt/src/crdts/GCounter/index.ts +++ b/packages/crdt/src/crdts/GCounter/index.ts @@ -1,39 +1,47 @@ /// GCounter with support for state and op changes export class GCounter { - globalCounter: number; - // instead of standard incremental id for replicas - // we map the counter with the node id - counts: { [nodeId: string]: number }; + globalCounter: number; + // instead of standard incremental id for replicas + // we map the counter with the node id + counts: { [nodeId: string]: number }; - constructor(counts: { [nodeId: string]: number }) { - this.globalCounter = Object.values(counts).reduce((a, b) => a + b, 0); - this.counts = counts; - } + constructor(counts: { [nodeId: string]: number }) { + this.globalCounter = Object.values(counts).reduce((a, b) => a + b, 0); + this.counts = counts; + } - value(): number { - return this.globalCounter; - } + value(): number { + return this.globalCounter; + } - increment(nodeId: string, amount: number): void { - this.globalCounter += amount; - this.counts[nodeId] += amount; - } + increment(nodeId: string, amount: number): void { + this.globalCounter += amount; + this.counts[nodeId] += amount; + } - compare(peerCounter: GCounter): boolean { - return (this.counts.length === peerCounter.counts.length && Object.keys(this.counts).every(key => this.counts[key] <= peerCounter.counts[key])); - } + compare(peerCounter: GCounter): boolean { + return ( + this.counts.length === peerCounter.counts.length && + Object.keys(this.counts).every( + (key) => this.counts[key] <= peerCounter.counts[key], + ) + ); + } - merge(peerCounter: GCounter): void { - let temp: { [nodeKey: string]: number } = Object.assign( - {}, - this.counts, - peerCounter.counts, - ); + merge(peerCounter: GCounter): void { + const temp: { [nodeKey: string]: number } = Object.assign( + {}, + this.counts, + peerCounter.counts, + ); - Object.keys(temp).forEach((key) => { - this.counts[key] = Math.max(this.counts[key] || 0, peerCounter.counts[key] || 0); - }); + for (const key of Object.keys(temp)) { + 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); - } -} \ No newline at end of file + this.globalCounter = Object.values(this.counts).reduce((a, b) => a + b, 0); + } +} diff --git a/packages/crdt/src/crdts/GSet/index.ts b/packages/crdt/src/crdts/GSet/index.ts index 606e8198..02d251cc 100644 --- a/packages/crdt/src/crdts/GSet/index.ts +++ b/packages/crdt/src/crdts/GSet/index.ts @@ -1,28 +1,27 @@ /// GSet with support for state and op changes export class GSet { - private _set: Set; + set: Set; - constructor(set: Set) { - this._set = set; - } + constructor(set: Set) { + this.set = set; + } - add(element: T): void { - this._set.add(element); - } + add(element: T): void { + this.set.add(element); + } - lookup(element: T): boolean { - return this._set.has(element); - } + lookup(element: T): boolean { + return this.set.has(element); + } - set(): Set { - return this._set; - } + compare(peerSet: GSet): boolean { + return ( + this.set.size === peerSet.set.size && + [...this.set].every((value) => peerSet.set.has(value)) + ); + } - compare(peerSet: GSet): boolean { - return (this._set.size == peerSet.set().size && [...this._set].every(value => peerSet.set().has(value))); - } - - merge(peerSet: GSet): void { - this._set = new Set([...this._set, ...peerSet.set()]); - } -} \ No newline at end of file + merge(peerSet: GSet): void { + this.set = new Set([...this.set, ...peerSet.set]); + } +} diff --git a/packages/crdt/src/crdts/IPSet/index.ts b/packages/crdt/src/crdts/IPSet/index.ts index 4151436e..2148a515 100644 --- a/packages/crdt/src/crdts/IPSet/index.ts +++ b/packages/crdt/src/crdts/IPSet/index.ts @@ -2,76 +2,76 @@ import { GCounter } from "../GCounter/index.js"; /// State-based infinite-phase set (IPSet) export class IPSet { - // Grow-only mapping of elements to GCounters - private _counters: Map; + // Grow-only mapping of elements to GCounters + private _counters: Map; - // 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 + // 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 = new Map()) { - this._counters = counters; - } + constructor(counters: Map = new Map()) { + this._counters = counters; + } - counters(): Map { - return this._counters; - } + counters(): Map { + 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); - } - } + 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() ?? 0) % 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); - } - } + remove(nodeId: string, element: T): void { + if ( + this._counters.has(element) && + (this._counters.get(element)?.value() ?? 0) % 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; - } + contains(element: T): boolean { + if (this._counters.has(element)) { + return (this._counters.get(element)?.value() ?? 0) % 2 === 1; + } + return false; + } - set(): Set { - let result = new Set(); - for (let [element, counter] of this._counters.entries()) { - if (counter.value() % 2 === 1) { - result.add(element); - } - } - return result; - } + set(): Set { + const result = new Set(); + for (const [element, counter] of this._counters.entries()) { + if (counter.value() % 2 === 1) { + result.add(element); + } + } + return result; + } - compare(peerSet: IPSet): 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()! - ); - } + compare(peerSet: IPSet): 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() ?? 0) <= + (peerSet.counters().get(element)?.value() ?? 0), + ); + } - merge(peerSet: IPSet): 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); - } - } - } + merge(peerSet: IPSet): void { + for (const [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); + } + } + } } diff --git a/packages/crdt/src/crdts/LWWElementSet/index.ts b/packages/crdt/src/crdts/LWWElementSet/index.ts index 6e368ad8..a94d48cd 100644 --- a/packages/crdt/src/crdts/LWWElementSet/index.ts +++ b/packages/crdt/src/crdts/LWWElementSet/index.ts @@ -1,75 +1,80 @@ export enum Bias { - ADD, - REMOVE + ADD = 0, + REMOVE = 1, } export class LWWElementSet { - private _adds: Map; - private _removes: Map; - public _bias: Bias; + private _adds: Map; + private _removes: Map; + public _bias: Bias; - constructor(adds: Map, removes: Map, bias: Bias) { - this._adds = adds; - this._removes = removes; - this._bias = bias; - } + constructor(adds: Map, removes: Map, 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; - } + 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; - } + 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; - } + return false; + } - add(element: T): void { - this._adds.set(element, Date.now()); - } + add(element: T): void { + this._adds.set(element, Date.now()); + } - remove(element: T): void { - this._removes.set(element, Date.now()); - } + remove(element: T): void { + this._removes.set(element, Date.now()); + } - getAdds(): Map { - return this._adds; - } + getAdds(): Map { + return this._adds; + } - getRemoves(): Map { - return this._removes; - } + getRemoves(): Map { + return this._removes; + } - compare(peerSet: LWWElementSet): boolean { - return (compareSets(this._adds, peerSet._adds) && compareSets(this._removes, peerSet._removes)); - } + compare(peerSet: LWWElementSet): boolean { + return ( + compareSets(this._adds, peerSet._adds) && + compareSets(this._removes, peerSet._removes) + ); + } - merge(peerSet: LWWElementSet): 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); - } - } - } + merge(peerSet: LWWElementSet): void { + for (const [element, timestamp] of peerSet._adds.entries()) { + const thisTimestamp = this._adds.get(element); + if (!thisTimestamp || thisTimestamp < timestamp) { + this._adds.set(element, timestamp); + } + } + for (const [element, timestamp] of peerSet._removes.entries()) { + const thisTimestamp = this._removes.get(element); + if (!thisTimestamp || thisTimestamp < timestamp) { + this._removes.set(element, timestamp); + } + } + } } function compareSets(set1: Map, set2: Map): boolean { - return (set1.size === set2.size && [...set1.keys()].every(key => set2.has(key))); -} \ No newline at end of file + return ( + set1.size === set2.size && [...set1.keys()].every((key) => set2.has(key)) + ); +} diff --git a/packages/crdt/src/crdts/LWWRegister/index.ts b/packages/crdt/src/crdts/LWWRegister/index.ts index 94fbd5ea..6f1b11f8 100644 --- a/packages/crdt/src/crdts/LWWRegister/index.ts +++ b/packages/crdt/src/crdts/LWWRegister/index.ts @@ -1,47 +1,47 @@ export class LWWRegister { - private _element: T; - private _timestamp: number; - private _nodeId: string; + private _element: T; + private _timestamp: number; + private _nodeId: string; - constructor(element: T, nodeId: string) { - this._element = element; - this._timestamp = Date.now(); - this._nodeId = nodeId; - } + constructor(element: T, nodeId: string) { + this._element = element; + this._timestamp = Date.now(); + this._nodeId = nodeId; + } - assign(element: T, nodeId: string): void { - this._element = element; - this._timestamp = Date.now(); - this._nodeId = nodeId; - } + assign(element: T, nodeId: string): void { + this._element = element; + this._timestamp = Date.now(); + this._nodeId = nodeId; + } - getElement(): T { - return this._element; - } + getElement(): T { + return this._element; + } - getTimestamp(): number { - return this._timestamp; - } + getTimestamp(): number { + return this._timestamp; + } - getNodeId(): string { - return this._nodeId; - } + getNodeId(): string { + return this._nodeId; + } - compare(register: LWWRegister): boolean { - return (this._timestamp <= register.getTimestamp()); - } + compare(register: LWWRegister): boolean { + return this._timestamp <= register.getTimestamp(); + } - merge(register: LWWRegister): void { - const otherTimestamp = register.getTimestamp(); - const otherNodeId = register.getNodeId(); - if (otherTimestamp < this._timestamp) { - return; - } - if (otherTimestamp === this._timestamp && otherNodeId <= this._nodeId) { - return; - } - this._element = register.getElement(); - this._timestamp = otherTimestamp; - this._nodeId = otherNodeId; - } -} \ No newline at end of file + merge(register: LWWRegister): void { + const otherTimestamp = register.getTimestamp(); + const otherNodeId = register.getNodeId(); + if (otherTimestamp < this._timestamp) { + return; + } + if (otherTimestamp === this._timestamp && otherNodeId <= this._nodeId) { + return; + } + this._element = register.getElement(); + this._timestamp = otherTimestamp; + this._nodeId = otherNodeId; + } +} diff --git a/packages/crdt/src/crdts/PNCounter/index.ts b/packages/crdt/src/crdts/PNCounter/index.ts index 7ad3a0d7..f698aed8 100644 --- a/packages/crdt/src/crdts/PNCounter/index.ts +++ b/packages/crdt/src/crdts/PNCounter/index.ts @@ -1,44 +1,44 @@ -import { GCounter } from "../GCounter/index.js"; +import type { GCounter } from "../GCounter/index.js"; /// State-based PNCounter export class PNCounter { - private _increments: GCounter; - private _decrements: GCounter; - - constructor(increments: GCounter, decrements: GCounter) { - this._increments = increments; - this._decrements = decrements; - } - - value(): number { - return this._increments.value() - this._decrements.value(); - } - - increments(): GCounter { - return this._increments; - } - - decrements(): GCounter { - return this._decrements; - } - - increment(nodeId: string, amount: number): void { - this._increments.increment(nodeId, amount); - } - - decrement(nodeId: string, amount: number): void { - this._decrements.increment(nodeId, amount); - } - - compare(peerCounter: PNCounter): boolean { - return ( - this._increments.compare(peerCounter.increments()) && - this._decrements.compare(peerCounter.decrements()) - ); - } - - merge(peerCounter: PNCounter): void { - this._increments.merge(peerCounter.increments()); - this._decrements.merge(peerCounter.decrements()); - } + private _increments: GCounter; + private _decrements: GCounter; + + constructor(increments: GCounter, decrements: GCounter) { + this._increments = increments; + this._decrements = decrements; + } + + value(): number { + return this._increments.value() - this._decrements.value(); + } + + increments(): GCounter { + return this._increments; + } + + decrements(): GCounter { + return this._decrements; + } + + increment(nodeId: string, amount: number): void { + this._increments.increment(nodeId, amount); + } + + decrement(nodeId: string, amount: number): void { + this._decrements.increment(nodeId, amount); + } + + compare(peerCounter: PNCounter): boolean { + return ( + this._increments.compare(peerCounter.increments()) && + this._decrements.compare(peerCounter.decrements()) + ); + } + + merge(peerCounter: PNCounter): void { + this._increments.merge(peerCounter.increments()); + this._decrements.merge(peerCounter.decrements()); + } } diff --git a/packages/crdt/src/crdts/RGA/index.ts b/packages/crdt/src/crdts/RGA/index.ts index 5e812877..dd22726b 100644 --- a/packages/crdt/src/crdts/RGA/index.ts +++ b/packages/crdt/src/crdts/RGA/index.ts @@ -2,163 +2,162 @@ type Identifier = { counter: number; nodeId: string }; class RGAElement { - // Virtual identifier of the element - vid: Identifier; - value: T | null; - parent: Identifier | null; - isDeleted: boolean; - - constructor( - vid: Identifier, - value: T | null, - parent: Identifier | null, - isDeleted: boolean = false - ) { - this.vid = vid; - this.value = value; - this.parent = parent; - this.isDeleted = isDeleted; - } + // Virtual identifier of the element + vid: Identifier; + value: T | null; + parent: Identifier | null; + isDeleted: boolean; + + constructor( + vid: Identifier, + value: T | null, + parent: Identifier | null, + isDeleted = false, + ) { + this.vid = vid; + this.value = value; + this.parent = parent; + this.isDeleted = isDeleted; + } } export class RGA { - /// The sequencer is used to generate unique identifiers for each element - sequencer: Identifier; - /// For now we are using a simple array to store elements - /// This can be optimized using a Btree - elements: RGAElement[]; + /// The sequencer is used to generate unique identifiers for each element + sequencer: Identifier; + /// For now we are using a simple array to store elements + /// This can be optimized using a Btree + elements: RGAElement[]; - /* + /* We are using an empty element as the head of the array to simplify the logic of merging two RGA instances. It acts as an anchor and is the same for all replicas. */ - constructor( - nodeId: string, - sequencer: Identifier = { counter: 0, nodeId: nodeId }, - elements: RGAElement[] = [ - new RGAElement({ counter: 0, nodeId: "" }, null, null, true), - ] - ) { - this.sequencer = sequencer; - this.elements = elements; - } - - getArray(): T[] { - return this.elements - .filter((element) => !element.isDeleted) - .map((element) => element.value! as T); - } - - clear(): void { - this.sequencer = { counter: 0, nodeId: this.sequencer.nodeId }; - this.elements = [ - new RGAElement({ counter: 0, nodeId: "" }, null, null, true), - ]; - } - - // Function to generate the next unique identifier - private nextSeq(sequencer: Identifier): Identifier { - return { counter: sequencer.counter + 1, nodeId: sequencer.nodeId }; - } - - // Check whether a < b, ids are never equal - private compareVIds(a: Identifier, b: Identifier): boolean { - if (a.counter !== b.counter) { - return a.counter < b.counter; - } - return a.nodeId < b.nodeId; - } - - // Function to map a logical index (ignoring tombstones) to a physical index in the elements array - private indexWithTombstones(index: number): number { - let offset = 1; // Start from 1 to skip the head element - while (index > 0) { - if (!this.elements[offset].isDeleted) index--; - offset++; - } - return offset; - } - - // Function to read the value at a given index - read(index: number): T | null { - let i = this.indexWithTombstones(index); - while (this.elements[i].isDeleted) i++; - return this.elements[i].value; - } - - // Function to find the physical index of an element given the virtual id - private indexOfVId(ptr: Identifier): number { - for (let offset = 0; offset < this.elements.length; offset++) { - if ( - ptr.counter === this.elements[offset].vid.counter && - ptr.nodeId === this.elements[offset].vid.nodeId - ) { - return offset; - } - } - return -1; - } + constructor( + nodeId: string, + sequencer: Identifier = { counter: 0, nodeId: nodeId }, + elements: RGAElement[] = [ + new RGAElement({ counter: 0, nodeId: "" }, null, null, true), + ], + ) { + this.sequencer = sequencer; + this.elements = elements; + } + + getArray(): T[] { + return this.elements + .filter((element) => !element.isDeleted) + .map((element) => element.value as T); + } + + clear(): void { + this.sequencer = { counter: 0, nodeId: this.sequencer.nodeId }; + this.elements = [ + new RGAElement({ counter: 0, nodeId: "" }, null, null, true), + ]; + } + + // Function to generate the next unique identifier + private nextSeq(sequencer: Identifier): Identifier { + return { counter: sequencer.counter + 1, nodeId: sequencer.nodeId }; + } + + // Check whether a < b, ids are never equal + private compareVIds(a: Identifier, b: Identifier): boolean { + if (a.counter !== b.counter) { + return a.counter < b.counter; + } + return a.nodeId < b.nodeId; + } + + // Function to map a logical index (ignoring tombstones) to a physical index in the elements array + private indexWithTombstones(index: number): number { + let offset = 1; // Start from 1 to skip the head element + for (let i = 0; i < index; i++) { + while (this.elements[offset].isDeleted) offset++; + offset++; + } + + return offset; + } + + // Function to read the value at a given index + read(index: number): T | null { + let i = this.indexWithTombstones(index); + while (this.elements[i].isDeleted) i++; + return this.elements[i].value; + } + + // Function to find the physical index of an element given the virtual id + private indexOfVId(ptr: Identifier | null): number { + if (!ptr) return -1; + for (let offset = 0; offset < this.elements.length; offset++) { + if ( + ptr.counter === this.elements[offset].vid.counter && + ptr.nodeId === this.elements[offset].vid.nodeId + ) { + return offset; + } + } + return -1; + } // Function to insert a new element after a given index, might not be immidiately after becuase we look at parents - insert(parentIndex: number, value: T): void { - const i = this.indexWithTombstones(parentIndex); - const parent = this.elements[i - 1].vid; - const newVId = this.nextSeq(this.sequencer); - this.insertElement(new RGAElement(newVId, value, parent)); - } - - // Function to insert a new element into the array - private insertElement(element: RGAElement): void { - const parentIdx = this.indexOfVId(element.parent!); - let insertIdx = parentIdx + 1; - for (; insertIdx < this.elements.length; insertIdx++) { - let curr = this.elements[insertIdx]; - let currParentIdx = this.indexOfVId(curr.parent!); - if (currParentIdx > parentIdx) break; - if (currParentIdx === parentIdx) { - if (this.compareVIds(curr.vid, element.vid)) break; - } - } - this.sequencer = { - ...this.sequencer, - counter: Math.max(this.sequencer.counter, element.vid.counter), - }; - // Check if its a duplicate - if ( - this.elements[insertIdx - 1].vid.counter === element.vid.counter && - this.elements[insertIdx - 1].vid.nodeId === element.vid.nodeId - ) { - return; - } - this.elements.splice(insertIdx, 0, element); - } - - // Function to delete an element from the RGA - delete(index: number): void { - let i = this.indexWithTombstones(index); - while (this.elements[i].isDeleted) i++; - this.elements[i].isDeleted = true; - } - - // Function to update the value of an element - update(index: number, value: T): void { - let i = this.indexWithTombstones(index); - while (this.elements[i].isDeleted) i++; - this.elements[i].value = value; - } - - // Merge another RGA instance into this one - merge(peerRGA: RGA): void { - for (let i = 1; i < peerRGA.elements.length; i++) { - this.insertElement(peerRGA.elements[i]); - } - - this.sequencer = { - ...this.sequencer, - counter: Math.max( - this.sequencer.counter, - peerRGA.sequencer.counter - ), - }; - } + insert(parentIndex: number, value: T): void { + const i = this.indexWithTombstones(parentIndex); + const parent = this.elements[i - 1].vid; + const newVId = this.nextSeq(this.sequencer); + this.insertElement(new RGAElement(newVId, value, parent)); + } + + // Function to insert a new element into the array + private insertElement(element: RGAElement): void { + const parentIdx = this.indexOfVId(element.parent); + let insertIdx = parentIdx + 1; + for (; insertIdx < this.elements.length; insertIdx++) { + const curr = this.elements[insertIdx]; + const currParentIdx = this.indexOfVId(curr.parent); + if (currParentIdx > parentIdx) break; + if (currParentIdx === parentIdx) { + if (this.compareVIds(curr.vid, element.vid)) break; + } + } + this.sequencer = { + ...this.sequencer, + counter: Math.max(this.sequencer.counter, element.vid.counter), + }; + // Check if its a duplicate + if ( + this.elements[insertIdx - 1].vid.counter === element.vid.counter && + this.elements[insertIdx - 1].vid.nodeId === element.vid.nodeId + ) { + return; + } + this.elements.splice(insertIdx, 0, element); + } + + // Function to delete an element from the RGA + delete(index: number): void { + let i = this.indexWithTombstones(index); + while (this.elements[i].isDeleted) i++; + this.elements[i].isDeleted = true; + } + + // Function to update the value of an element + update(index: number, value: T): void { + let i = this.indexWithTombstones(index); + while (this.elements[i].isDeleted) i++; + this.elements[i].value = value; + } + + // Merge another RGA instance into this one + merge(peerRGA: RGA): void { + for (let i = 1; i < peerRGA.elements.length; i++) { + this.insertElement(peerRGA.elements[i]); + } + + this.sequencer = { + ...this.sequencer, + counter: Math.max(this.sequencer.counter, peerRGA.sequencer.counter), + }; + } } diff --git a/packages/crdt/src/index.ts b/packages/crdt/src/index.ts index 9bf66ca1..26e12ebd 100644 --- a/packages/crdt/src/index.ts +++ b/packages/crdt/src/index.ts @@ -1,4 +1,4 @@ -export * from "./crdts/2PSet/index.js" +export * from "./crdts/2PSet/index.js"; export * from "./crdts/GCounter/index.js"; export * from "./crdts/GSet/index.js"; export * from "./crdts/IPSet/index.js"; diff --git a/packages/crdt/tests/GCounter.test.ts b/packages/crdt/tests/GCounter.test.ts index 179db510..e77a555e 100644 --- a/packages/crdt/tests/GCounter.test.ts +++ b/packages/crdt/tests/GCounter.test.ts @@ -1,42 +1,42 @@ -import { describe, test, expect, beforeEach } from "vitest"; +import { beforeEach, describe, expect, test } from "vitest"; import { GCounter } from "../src/crdts/GCounter/index.js"; 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}); - }); + 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", () => { + const set2 = new GCounter({ node1: 5, node2: 10 }); + const 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", () => { + const set2 = new GCounter({ node1: 3, node2: 10 }); + const 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 }); + }); }); diff --git a/packages/crdt/tests/GSet.test.ts b/packages/crdt/tests/GSet.test.ts index ba33118f..3b95930f 100644 --- a/packages/crdt/tests/GSet.test.ts +++ b/packages/crdt/tests/GSet.test.ts @@ -1,46 +1,44 @@ -import { describe, test, expect, beforeEach, afterEach } from "vitest"; +import { afterEach, beforeEach, describe, expect, test } from "vitest"; import { GSet } from "../src/crdts/GSet/index.js"; describe("G-Set Tests", () => { + let set1: GSet; + let set2: GSet; - let set1: GSet; - let set2: GSet; + beforeEach(() => { + set1 = new GSet(new Set(["walter", "jesse", "mike"])); + set2 = new GSet(new Set(["walter", "jesse", "mike"])); + }); - beforeEach(() => { - set1 = new GSet(new Set(["walter", "jesse", "mike"])); - set2 = new GSet(new Set(["walter", "jesse", "mike"])); - }); + test("Test Add", () => { + set1.add("gustavo"); + set2.add("gustavo"); - test("Test Add", () => { - set1.add("gustavo"); - set2.add("gustavo"); + expect(set1.lookup("gustavo")).toBe(true); + expect(set2.lookup("gustavo")).toBe(true); + }); - expect(set1.lookup("gustavo")).toBe(true); - expect(set2.lookup("gustavo")).toBe(true); - }); + test("Test Compare", () => { + expect(set1.compare(set2)).toBe(true); - test("Test Compare", () => { - expect(set1.compare(set2)).toBe(true); + set1.add("gustavo"); - set1.add("gustavo"); + expect(set1.compare(set2)).toBe(false); - expect(set1.compare(set2)).toBe(false); + set2.add("gustavo"); - set2.add("gustavo"); + expect(set1.compare(set2)).toBe(true); + }); - expect(set1.compare(set2)).toBe(true); - }); + test("Test Merge", () => { + set1.add("gustavo"); + set2.add("lalo"); - test("Test Merge", () => { - set1.add("gustavo"); - set2.add("lalo"); + expect(set1.compare(set2)).toBe(false); - expect(set1.compare(set2)).toBe(false); + set1.merge(set2); + set2.merge(set1); - set1.merge(set2); - set2.merge(set1); - - expect(set1.compare(set2)).toBe(true); - - }); + expect(set1.compare(set2)).toBe(true); + }); }); diff --git a/packages/crdt/tests/IPSet.test.ts b/packages/crdt/tests/IPSet.test.ts index 216f8917..31f7b324 100644 --- a/packages/crdt/tests/IPSet.test.ts +++ b/packages/crdt/tests/IPSet.test.ts @@ -1,137 +1,145 @@ -import { describe, test, expect, beforeEach } from "vitest"; -import { IPSet } from "../src/crdts/IPSet/index.js"; +import { beforeEach, describe, expect, test } from "vitest"; import { GCounter } from "../src/crdts/GCounter/index.js"; +import { IPSet } from "../src/crdts/IPSet/index.js"; describe("Infinite-phase set Tests", () => { - const peerId = "node"; - - let set1: IPSet; - let set2: IPSet; - let set3: IPSet; - - beforeEach(() => { - set1 = new IPSet(); - set2 = new IPSet(); - set3 = new IPSet(); - }); - - test("Test Add Elements", () => { - expect(set1.contains("alice")).toBe(false); - - set1.add(peerId, "alice"); - expect(set1.contains("alice")).toBe(true); - expect(set1.contains("bob")).toBe(false); - - set1.add(peerId, "alice"); - expect(set1.contains("alice")).toBe(true); - }); - - test("Test Add and Remove Elements", () => { - expect(set1.contains("alice")).toBe(false); - - set1.add(peerId, "alice"); - set1.remove(peerId, "alice"); - expect(set1.contains("alice")).toBe(false); - - set1.add(peerId, "alice"); - set1.add(peerId, "alice"); - set1.remove(peerId, "alice"); - expect(set1.contains("alice")).toBe(false); - - set1.add(peerId, "alice"); - set1.add(peerId, "alice"); - set1.remove(peerId, "alice"); - set1.remove(peerId, "alice"); - set1.add(peerId, "alice"); - set1.add(peerId, "alice"); - set1.add(peerId, "alice"); - expect(set1.contains("alice")).toBe(true); - - set1.add(peerId, "alice"); - set1.remove(peerId, "alice"); - set1.add(peerId, "alice"); - set1.remove(peerId, "alice"); - set1.add(peerId, "alice"); - expect(set1.contains("alice")).toBe(true); - }); - - describe("Test Merge Elements", () => { - test("Merge Sets Overlapping", () => { - set2.add(peerId, 1); - set2.add(peerId, 1); - set2.remove(peerId, 3); - set2.add(peerId, 3); - set2.remove(peerId, 3); - set2.add(peerId, 5); - set2.remove(peerId, 5); - set2.add(peerId, 5); - - set3.add(peerId, 1); - set3.remove(peerId, 1); - set3.add(peerId, 3); - - set2.merge(set3); - expect(set2).toStrictEqual( - new IPSet( - new Map([ - [1, new GCounter({ [peerId]: 2 })], - [3, new GCounter({ [peerId]: 2 })], - [5, new GCounter({ [peerId]: 3 })], - ]) - ) - ); - }); - test("Merge Sets Non-Overlapping", () => { - set2.add(peerId, 1); - set2.add(peerId, 3); - - set3.add(peerId, 5); - set3.add(peerId, 7); - - set3.merge(set2); - expect(set3).toStrictEqual( - new IPSet( - new Map([ - [1, new GCounter({ [peerId]: 1 })], - [3, new GCounter({ [peerId]: 1 })], - [5, new GCounter({ [peerId]: 1 })], - [7, new GCounter({ [peerId]: 1 })], - ]) - ) - ); - }); - }); - - test("Test Compare Sets", () => { - expect(set2.compare(set2)).toBe(true); - expect(set2.compare(set3)).toBe(true); - expect(set3.compare(set2)).toBe(true); - - set2.add(peerId, 1); - - expect(set2.compare(set3)).toBe(false); - expect(set3.compare(set2)).toBe(true); - - set3.add(peerId, 1); - expect(set2.compare(set3)).toBe(true); - expect(set3.compare(set2)).toBe(true); - - set2.remove(peerId, 1); - expect(set2.compare(set3)).toBe(false); - expect(set3.compare(set2)).toBe(true); - }); - - test("Test set() function", () => { - [1, 2, 3, 4, 5].forEach((i) => set2.add(peerId, i)); - expect(set2.set()).toStrictEqual(new Set([1, 2, 3, 4, 5])); - - [1, 3, 4].forEach((i) => set2.remove(peerId, i)); - expect(set2.set()).toStrictEqual(new Set([2, 5])); - - [1, 2, 3].forEach((i) => set2.remove(peerId, i)); - expect(set2.set()).toStrictEqual(new Set([5])); - - [1, 2, 3].forEach((i) => set2.add(peerId, i)); - expect(set2.set()).toStrictEqual(new Set([1, 2, 3, 5])); - }); + const peerId = "node"; + + let set1: IPSet; + let set2: IPSet; + let set3: IPSet; + + beforeEach(() => { + set1 = new IPSet(); + set2 = new IPSet(); + set3 = new IPSet(); + }); + + test("Test Add Elements", () => { + expect(set1.contains("alice")).toBe(false); + + set1.add(peerId, "alice"); + expect(set1.contains("alice")).toBe(true); + expect(set1.contains("bob")).toBe(false); + + set1.add(peerId, "alice"); + expect(set1.contains("alice")).toBe(true); + }); + + test("Test Add and Remove Elements", () => { + expect(set1.contains("alice")).toBe(false); + + set1.add(peerId, "alice"); + set1.remove(peerId, "alice"); + expect(set1.contains("alice")).toBe(false); + + set1.add(peerId, "alice"); + set1.add(peerId, "alice"); + set1.remove(peerId, "alice"); + expect(set1.contains("alice")).toBe(false); + + set1.add(peerId, "alice"); + set1.add(peerId, "alice"); + set1.remove(peerId, "alice"); + set1.remove(peerId, "alice"); + set1.add(peerId, "alice"); + set1.add(peerId, "alice"); + set1.add(peerId, "alice"); + expect(set1.contains("alice")).toBe(true); + + set1.add(peerId, "alice"); + set1.remove(peerId, "alice"); + set1.add(peerId, "alice"); + set1.remove(peerId, "alice"); + set1.add(peerId, "alice"); + expect(set1.contains("alice")).toBe(true); + }); + + describe("Test Merge Elements", () => { + test("Merge Sets Overlapping", () => { + set2.add(peerId, 1); + set2.add(peerId, 1); + set2.remove(peerId, 3); + set2.add(peerId, 3); + set2.remove(peerId, 3); + set2.add(peerId, 5); + set2.remove(peerId, 5); + set2.add(peerId, 5); + + set3.add(peerId, 1); + set3.remove(peerId, 1); + set3.add(peerId, 3); + + set2.merge(set3); + expect(set2).toStrictEqual( + new IPSet( + new Map([ + [1, new GCounter({ [peerId]: 2 })], + [3, new GCounter({ [peerId]: 2 })], + [5, new GCounter({ [peerId]: 3 })], + ]), + ), + ); + }); + test("Merge Sets Non-Overlapping", () => { + set2.add(peerId, 1); + set2.add(peerId, 3); + + set3.add(peerId, 5); + set3.add(peerId, 7); + + set3.merge(set2); + expect(set3).toStrictEqual( + new IPSet( + new Map([ + [1, new GCounter({ [peerId]: 1 })], + [3, new GCounter({ [peerId]: 1 })], + [5, new GCounter({ [peerId]: 1 })], + [7, new GCounter({ [peerId]: 1 })], + ]), + ), + ); + }); + }); + + test("Test Compare Sets", () => { + expect(set2.compare(set2)).toBe(true); + expect(set2.compare(set3)).toBe(true); + expect(set3.compare(set2)).toBe(true); + + set2.add(peerId, 1); + + expect(set2.compare(set3)).toBe(false); + expect(set3.compare(set2)).toBe(true); + + set3.add(peerId, 1); + expect(set2.compare(set3)).toBe(true); + expect(set3.compare(set2)).toBe(true); + + set2.remove(peerId, 1); + expect(set2.compare(set3)).toBe(false); + expect(set3.compare(set2)).toBe(true); + }); + + test("Test set() function", () => { + for (const i of [1, 2, 3, 4, 5]) { + set2.add(peerId, i); + } + expect(set2.set()).toStrictEqual(new Set([1, 2, 3, 4, 5])); + + for (const i of [1, 3, 4]) { + set2.remove(peerId, i); + } + expect(set2.set()).toStrictEqual(new Set([2, 5])); + + for (const i of [1, 2, 3]) { + set2.remove(peerId, i); + } + expect(set2.set()).toStrictEqual(new Set([5])); + + for (const i of [1, 2, 3]) { + set2.add(peerId, i); + } + expect(set2.set()).toStrictEqual(new Set([1, 2, 3, 5])); + }); }); diff --git a/packages/crdt/tests/LWWElementSet.test.ts b/packages/crdt/tests/LWWElementSet.test.ts index 0a763bed..0e96be74 100644 --- a/packages/crdt/tests/LWWElementSet.test.ts +++ b/packages/crdt/tests/LWWElementSet.test.ts @@ -1,90 +1,90 @@ -import { describe, test, expect, beforeEach } from "vitest"; -import { LWWElementSet, Bias } from "../src/crdts/LWWElementSet/index.js"; +import { beforeEach, describe, expect, test } from "vitest"; +import { Bias, LWWElementSet } from "../src/crdts/LWWElementSet/index.js"; describe("LWW-Element-Set Tests", () => { - const testValues = ["walter", "jesse", "mike"]; + const testValues = ["walter", "jesse", "mike"]; - let set1: LWWElementSet; - let set2: LWWElementSet; - let set3: LWWElementSet; + let set1: LWWElementSet; + let set2: LWWElementSet; + let set3: LWWElementSet; - beforeEach(() => { - set1 = new LWWElementSet(new Map(), new Map(), Bias.ADD); - set2 = new LWWElementSet(new Map(), new Map(), Bias.ADD); - set3 = new LWWElementSet(new Map(), new Map(), Bias.REMOVE); + beforeEach(() => { + set1 = new LWWElementSet(new Map(), new Map(), Bias.ADD); + set2 = new LWWElementSet(new Map(), new Map(), Bias.ADD); + set3 = new LWWElementSet(new Map(), new Map(), Bias.REMOVE); - testValues.forEach((value) => { - set1.add(value); - set2.add(value); - set3.add(value); - }); - }); + for (const value of testValues) { + set1.add(value); + set2.add(value); + set3.add(value); + } + }); - test("Test Add Elements", () => { - expect(set1.lookup("gustavo")).toBe(false); + test("Test Add Elements", () => { + expect(set1.lookup("gustavo")).toBe(false); - set1.add("gustavo"); - expect(set1.lookup("gustavo")).toBe(true); - }); + set1.add("gustavo"); + expect(set1.lookup("gustavo")).toBe(true); + }); - test("Test Remove Elements", () => { - expect(set1.lookup("mike")).toBe(true); + test("Test Remove Elements", () => { + expect(set1.lookup("mike")).toBe(true); - set1.getRemoves().set("mike", Date.now() + 1); + set1.getRemoves().set("mike", Date.now() + 1); - expect(set1.lookup("mike")).toBe(false); - }); + expect(set1.lookup("mike")).toBe(false); + }); - test("Test Compare Sets", () => { - expect(set1.compare(set2)).toBe(true); - expect(set1.compare(set3)).toBe(true); - expect(set3.compare(set2)).toBe(true); + test("Test Compare Sets", () => { + expect(set1.compare(set2)).toBe(true); + expect(set1.compare(set3)).toBe(true); + expect(set3.compare(set2)).toBe(true); - set1.remove("jesse"); + set1.remove("jesse"); - expect(set1.compare(set2)).toBe(false); - expect(set1.compare(set3)).toBe(false); - expect(set3.compare(set2)).toBe(true); - }); + expect(set1.compare(set2)).toBe(false); + expect(set1.compare(set3)).toBe(false); + expect(set3.compare(set2)).toBe(true); + }); - describe("Test Merge Elements" , () => { - test("Merge Sets", () => { - // Adding different names to each set - set1.add("gustavo"); - set2.add("saul"); + describe("Test Merge Elements", () => { + test("Merge Sets", () => { + // Adding different names to each set + set1.add("gustavo"); + set2.add("saul"); - expect(set1.compare(set2)).toBe(false); + expect(set1.compare(set2)).toBe(false); - set1.merge(set2); - set2.merge(set1); + set1.merge(set2); + set2.merge(set1); - expect(set1.compare(set2)).toBe(true); - }); + expect(set1.compare(set2)).toBe(true); + }); - test("Same Element, different Timestamps", () => { - const timestamp = Date.now(); - set1.getAdds().set("gustavo", timestamp); - set2.getAdds().set("gustavo", timestamp + 5); + test("Same Element, different Timestamps", () => { + const timestamp = Date.now(); + set1.getAdds().set("gustavo", timestamp); + set2.getAdds().set("gustavo", timestamp + 5); - expect(set1.getAdds().get("gustavo")).toBe(timestamp); + expect(set1.getAdds().get("gustavo")).toBe(timestamp); - set1.merge(set2); - set2.merge(set1); + set1.merge(set2); + set2.merge(set1); - expect(set1.getAdds().get("gustavo")).toBe(timestamp + 5); - expect(set2.getAdds().get("gustavo")).toBe(timestamp + 5); - }); + expect(set1.getAdds().get("gustavo")).toBe(timestamp + 5); + expect(set2.getAdds().get("gustavo")).toBe(timestamp + 5); + }); - test("Merge Removal Timestamps", () => { - const timestamp = Date.now(); + test("Merge Removal Timestamps", () => { + const timestamp = Date.now(); - set1.getAdds().set("gustavo", timestamp); - set2.getRemoves().set("gustavo", timestamp + 5); + set1.getAdds().set("gustavo", timestamp); + set2.getRemoves().set("gustavo", timestamp + 5); - set1.merge(set2); + set1.merge(set2); - expect(set1.lookup("gustavo")).toBe(false); - expect(set1.getRemoves().get("gustavo")).toBe(timestamp + 5); - }); - }); + expect(set1.lookup("gustavo")).toBe(false); + expect(set1.getRemoves().get("gustavo")).toBe(timestamp + 5); + }); + }); }); diff --git a/packages/crdt/tests/LWWRegister.test.ts b/packages/crdt/tests/LWWRegister.test.ts index 8bc86eef..cd08a1d8 100644 --- a/packages/crdt/tests/LWWRegister.test.ts +++ b/packages/crdt/tests/LWWRegister.test.ts @@ -1,64 +1,64 @@ -import { describe, test, expect, beforeEach, vi } from "vitest"; +import { beforeEach, describe, expect, test, vi } from "vitest"; import { LWWRegister } from "../src/crdts/LWWRegister/index.js"; -describe('LWW-Register Tests', () => { - test('Test Assign', () => { - let register1 = new LWWRegister("alice", "node1"); +describe("LWW-Register Tests", () => { + test("Test Assign", () => { + const register1 = new LWWRegister("alice", "node1"); - expect(register1.getElement()).toBe("alice"); - expect(register1.getNodeId()).toEqual("node1"); - register1.assign("bob", "node2"); - expect(register1.getElement()).toBe("bob"); - expect(register1.getNodeId()).toEqual("node2"); - }); + expect(register1.getElement()).toBe("alice"); + expect(register1.getNodeId()).toEqual("node1"); + register1.assign("bob", "node2"); + expect(register1.getElement()).toBe("bob"); + expect(register1.getNodeId()).toEqual("node2"); + }); - test('Test Compare', () => { - vi.useFakeTimers(); - const date = new Date(2000,1,1,13); - vi.setSystemTime(date); + test("Test Compare", () => { + vi.useFakeTimers(); + const date = new Date(2000, 1, 1, 13); + vi.setSystemTime(date); - let register1 = new LWWRegister("alice", "node1"); + const register1 = new LWWRegister("alice", "node1"); - vi.useRealTimers(); + vi.useRealTimers(); - let register2 = new LWWRegister("alice", "node2"); + const register2 = new LWWRegister("alice", "node2"); - expect(register1.compare(register2)).toEqual(true); - expect(register2.compare(register1)).toEqual(false); - }); + expect(register1.compare(register2)).toEqual(true); + expect(register2.compare(register1)).toEqual(false); + }); - test('Test Merge', () => { - let register1 = new LWWRegister("alice", "node1"); - let register2 = new LWWRegister("bob", "node2"); + test("Test Merge", () => { + const register1 = new LWWRegister("alice", "node1"); + const register2 = new LWWRegister("bob", "node2"); - register1.merge(register2); - expect(register1.getElement()).toEqual("bob"); - expect(register2.getNodeId()).toEqual("node2"); + register1.merge(register2); + expect(register1.getElement()).toEqual("bob"); + expect(register2.getNodeId()).toEqual("node2"); - register2.merge(register1); - expect(register1.getElement()).toEqual("bob"); - expect(register2.getNodeId()).toEqual("node2"); - }); + register2.merge(register1); + expect(register1.getElement()).toEqual("bob"); + expect(register2.getNodeId()).toEqual("node2"); + }); - test('Test Merge w/same timestamp', () => { - vi.useFakeTimers(); - const date = new Date(2000,1,1,13); - vi.setSystemTime(date); + test("Test Merge w/same timestamp", () => { + vi.useFakeTimers(); + const date = new Date(2000, 1, 1, 13); + vi.setSystemTime(date); - let register1 = new LWWRegister("alice", "node1"); - let register2 = new LWWRegister("bob", "node2"); + const register1 = new LWWRegister("alice", "node1"); + const register2 = new LWWRegister("bob", "node2"); - expect(register1.getElement()).toBe("alice"); - expect(register1.getNodeId()).toEqual("node1"); + expect(register1.getElement()).toBe("alice"); + expect(register1.getNodeId()).toEqual("node1"); - register1.merge(register2); - expect(register1.getElement()).toBe("bob"); - expect(register1.getNodeId()).toEqual("node2"); + register1.merge(register2); + expect(register1.getElement()).toBe("bob"); + expect(register1.getNodeId()).toEqual("node2"); - register2.merge(register1); - expect(register1.getElement()).toBe("bob"); - expect(register1.getNodeId()).toEqual("node2"); + register2.merge(register1); + expect(register1.getElement()).toBe("bob"); + expect(register1.getNodeId()).toEqual("node2"); - vi.useRealTimers(); - }); + vi.useRealTimers(); + }); }); diff --git a/packages/crdt/tests/PNCounter.test.ts b/packages/crdt/tests/PNCounter.test.ts index c676bf37..e069f4bd 100644 --- a/packages/crdt/tests/PNCounter.test.ts +++ b/packages/crdt/tests/PNCounter.test.ts @@ -1,51 +1,57 @@ -import { describe, test, expect, beforeEach } from "vitest"; -import { PNCounter } from "../src/crdts/PNCounter/index.js"; +import { beforeEach, describe, expect, test } from "vitest"; import { GCounter } from "../src/crdts/GCounter/index.js"; +import { PNCounter } from "../src/crdts/PNCounter/index.js"; describe("PN-Counter Tests", () => { - let set1: PNCounter; - let set2: PNCounter; + let set1: PNCounter; + let set2: PNCounter; - beforeEach(() => { - set1 = new PNCounter(new GCounter({ "node1": 5, "node2": 10, "node3": 15 }), new GCounter({ "node1": 3, "node2": 4, "node3": 3 })); - set2 = new PNCounter(new GCounter({ "node1": 5, "node2": 10, "node3": 15 }), new GCounter({ "node1": 3, "node2": 4, "node3": 3 })); - }); + beforeEach(() => { + set1 = new PNCounter( + new GCounter({ node1: 5, node2: 10, node3: 15 }), + new GCounter({ node1: 3, node2: 4, node3: 3 }), + ); + set2 = new PNCounter( + new GCounter({ node1: 5, node2: 10, node3: 15 }), + new GCounter({ node1: 3, node2: 4, node3: 3 }), + ); + }); - test("Test Initial Value", () => { - expect(set1.value()).toBe(20); - expect(set2.value()).toBe(20); - }); + test("Test Initial Value", () => { + expect(set1.value()).toBe(20); + expect(set2.value()).toBe(20); + }); - test("Test Increment", () => { - set1.increment("node1",10); - set2.increment("node1",20); - expect(set1.value()).toBe(30); - expect(set2.value()).toBe(40); - }); + test("Test Increment", () => { + set1.increment("node1", 10); + set2.increment("node1", 20); + expect(set1.value()).toBe(30); + expect(set2.value()).toBe(40); + }); - test("Test Decrement", () => { - set1.decrement("node1",10); - set2.decrement("node1",20); - expect(set1.value()).toBe(10); - expect(set2.value()).toBe(0); - }); + test("Test Decrement", () => { + set1.decrement("node1", 10); + set2.decrement("node1", 20); + expect(set1.value()).toBe(10); + expect(set2.value()).toBe(0); + }); - test("Test Compare", () => { - expect(set1.compare(set2)).toBe(true); - set1.decrement("node1",10); - expect(set1.compare(set2)).toBe(false); - set2.decrement("node1",10); - expect(set1.compare(set2)).toBe(true); - }); + test("Test Compare", () => { + expect(set1.compare(set2)).toBe(true); + set1.decrement("node1", 10); + expect(set1.compare(set2)).toBe(false); + set2.decrement("node1", 10); + expect(set1.compare(set2)).toBe(true); + }); - test("Test Merge", () => { - set1.increment("node1",10); - set2.decrement("node2",5); - expect(set1.compare(set2)).toBe(false); - expect(set2.compare(set1)).toBe(false); - set1.merge(set2); - set2.merge(set1); - expect(set1.compare(set2)).toBe(true); - expect(set2.compare(set1)).toBe(true); - }); + test("Test Merge", () => { + set1.increment("node1", 10); + set2.decrement("node2", 5); + expect(set1.compare(set2)).toBe(false); + expect(set2.compare(set1)).toBe(false); + set1.merge(set2); + set2.merge(set1); + expect(set1.compare(set2)).toBe(true); + expect(set2.compare(set1)).toBe(true); + }); }); diff --git a/packages/crdt/tests/RGA.test.ts b/packages/crdt/tests/RGA.test.ts index 5b2c70a8..ed894e0a 100644 --- a/packages/crdt/tests/RGA.test.ts +++ b/packages/crdt/tests/RGA.test.ts @@ -1,91 +1,91 @@ -import { describe, test, expect, beforeEach } from "vitest"; -import { RGA } from "../src/builtins/RGA"; // Adjust the import path according to your project structure +import { beforeEach, describe, expect, test } from "vitest"; +import { RGA } from "../src/crdts/RGA/index.js"; describe("Replicable Growable Array Tests", () => { - let rga: RGA; - let peerRGA: RGA; - - beforeEach(() => { - rga = new RGA("node1"); - peerRGA = new RGA("node2"); - }); - - test("Test Insert", () => { - rga.insert(0, "A"); - rga.insert(1, "B"); - rga.insert(1, "C"); - rga.insert(0, "D"); - - expect(rga.getArray()).toEqual(["D", "A", "C", "B"]); - }); - - test("Test Read", () => { - rga.insert(0, "A"); - rga.insert(1, "B"); - rga.insert(1, "C"); - rga.delete(1); - - expect(rga.read(0)).toBe("A"); - expect(rga.read(1)).toBe("B"); - }); - - test("Test Insert and Delete", () => { - rga.insert(0, "A"); - rga.insert(1, "B"); - rga.insert(1, "C"); - rga.delete(0); - rga.delete(0); - expect(rga.getArray()).toEqual(["B"]); - - rga.clear(); - - rga.insert(0, "A"); - rga.insert(1, "B"); - rga.delete(0); - - expect(rga.getArray()).toEqual(["B"]); - - rga.insert(0, "C"); - rga.insert(1, "D"); - expect(rga.getArray()).toEqual(["C", "D", "B"]); - - rga.delete(1); - expect(rga.getArray()).toEqual(["C", "B"]); - - rga.delete(1); - expect(rga.getArray()).toEqual(["C"]); - - peerRGA.insert(0, "E"); - peerRGA.insert(0, "F"); - peerRGA.insert(2, "G"); - peerRGA.insert(3, "H"); - peerRGA.delete(1); - peerRGA.delete(1); - peerRGA.delete(1); - expect(peerRGA.getArray()).toEqual(["F"]); - }); - - test("Test Update", () => { - rga.insert(0, "A"); - rga.insert(1, "B"); - rga.update(0, "C"); - rga.update(1, "D"); - - expect(rga.getArray()).toEqual(["C", "D"]); - }); - - test("Test Merge", () => { - rga.insert(0, "A"); - rga.insert(1, "B"); - - peerRGA.insert(0, "C"); - peerRGA.insert(1, "D"); - peerRGA.insert(0, "E"); - - rga.merge(peerRGA); - expect(rga.getArray()).toEqual(["E", "C", "A", "D", "B"]); - - peerRGA.merge(rga); - expect(peerRGA.getArray()).toEqual(rga.getArray()); - }); + let rga: RGA; + let peerRGA: RGA; + + beforeEach(() => { + rga = new RGA("node1"); + peerRGA = new RGA("node2"); + }); + + test("Test Insert", () => { + rga.insert(0, "A"); + rga.insert(1, "B"); + rga.insert(1, "C"); + rga.insert(0, "D"); + + expect(rga.getArray()).toEqual(["D", "A", "C", "B"]); + }); + + test("Test Read", () => { + rga.insert(0, "A"); + rga.insert(1, "B"); + rga.insert(1, "C"); + rga.delete(1); + + expect(rga.read(0)).toBe("A"); + expect(rga.read(1)).toBe("B"); + }); + + test("Test Insert and Delete", () => { + rga.insert(0, "A"); + rga.insert(1, "B"); + rga.insert(1, "C"); + rga.delete(0); + rga.delete(0); + expect(rga.getArray()).toEqual(["B"]); + + rga.clear(); + + rga.insert(0, "A"); + rga.insert(1, "B"); + rga.delete(0); + + expect(rga.getArray()).toEqual(["B"]); + + rga.insert(0, "C"); + rga.insert(1, "D"); + expect(rga.getArray()).toEqual(["C", "D", "B"]); + + rga.delete(1); + expect(rga.getArray()).toEqual(["C", "B"]); + + rga.delete(1); + expect(rga.getArray()).toEqual(["C"]); + + peerRGA.insert(0, "E"); + peerRGA.insert(0, "F"); + peerRGA.insert(2, "G"); + peerRGA.insert(3, "H"); + peerRGA.delete(1); + peerRGA.delete(1); + peerRGA.delete(1); + expect(peerRGA.getArray()).toEqual(["F"]); + }); + + test("Test Update", () => { + rga.insert(0, "A"); + rga.insert(1, "B"); + rga.update(0, "C"); + rga.update(1, "D"); + + expect(rga.getArray()).toEqual(["C", "D"]); + }); + + test("Test Merge", () => { + rga.insert(0, "A"); + rga.insert(1, "B"); + + peerRGA.insert(0, "C"); + peerRGA.insert(1, "D"); + peerRGA.insert(0, "E"); + + rga.merge(peerRGA); + expect(rga.getArray()).toEqual(["E", "C", "A", "D", "B"]); + + peerRGA.merge(rga); + expect(peerRGA.getArray()).toEqual(rga.getArray()); + }); }); diff --git a/packages/crdt/tests/TwoPSet.test.ts b/packages/crdt/tests/TwoPSet.test.ts index ab58fa2d..dfc80e57 100644 --- a/packages/crdt/tests/TwoPSet.test.ts +++ b/packages/crdt/tests/TwoPSet.test.ts @@ -1,55 +1,59 @@ -import { describe, test, expect, beforeEach } from "vitest"; +import { beforeEach, describe, expect, test } from "vitest"; import { TwoPSet } from "../src/crdts/2PSet/index.js"; import { GSet } from "../src/crdts/GSet/index.js"; describe("2P-Set Tests", () => { + let set1: TwoPSet; + let set2: TwoPSet; - let set1: TwoPSet; - let set2: TwoPSet; + beforeEach(() => { + set1 = new TwoPSet( + new GSet(new Set(["walter", "jesse", "mike"])), + new GSet(new Set()), + ); + set2 = new TwoPSet( + new GSet(new Set(["walter", "jesse", "mike"])), + new GSet(new Set()), + ); + }); - beforeEach(() => { - set1 = new TwoPSet(new GSet(new Set(["walter", "jesse", "mike"])), new GSet(new Set())); - set2 = new TwoPSet(new GSet(new Set(["walter", "jesse", "mike"])), new GSet(new Set())); - }); + test("Test Add Element", () => { + expect(set1.lookup("gustavo")).toBe(false); - test("Test Add Element", () => { - expect(set1.lookup("gustavo")).toBe(false); + set1.add("gustavo"); - set1.add("gustavo"); + expect(set1.lookup("gustavo")).toBe(true); + }); - expect(set1.lookup("gustavo")).toBe(true); - }); + test("Test Remove Element", () => { + expect(set1.lookup("mike")).toBe(true); - test("Test Remove Element", () => { - expect(set1.lookup("mike")).toBe(true); + set1.remove("mike"); - set1.remove("mike"); + expect(set1.lookup("mike")).toBe(false); + }); - expect(set1.lookup("mike")).toBe(false); - }); + test("Test Compare Elements", () => { + expect(set1.compare(set2)).toBe(true); - test("Test Compare Elements", () => { - expect(set1.compare(set2)).toBe(true); + set1.remove("mike"); - set1.remove("mike"); + expect(set1.compare(set2)).toBe(false); - expect(set1.compare(set2)).toBe(false); + set2.remove("mike"); - set2.remove("mike"); + expect(set1.compare(set2)).toBe(true); + }); - expect(set1.compare(set2)).toBe(true); - }); + test("Test Merge Elements", () => { + set1.remove("mike"); + set2.add("gustavo"); - test("Test Merge Elements", () => { - set1.remove("mike"); - set2.add("gustavo"); + expect(set1.compare(set2)).toBe(false); - expect(set1.compare(set2)).toBe(false); - - set1.merge(set2); - set2.merge(set1); - - expect(set1.compare(set2)).toBe(true); - }); + set1.merge(set2); + set2.merge(set1); + expect(set1.compare(set2)).toBe(true); + }); }); diff --git a/packages/crdt/tsconfig.json b/packages/crdt/tsconfig.json index 280f82e7..9cc8b37c 100644 --- a/packages/crdt/tsconfig.json +++ b/packages/crdt/tsconfig.json @@ -1,7 +1,7 @@ { - "extends": "../../tsconfig.json", - "compilerOptions": { - "outDir": "dist" - }, - "include": ["src/**/*.ts"] + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "dist" + }, + "include": ["src/**/*.ts"] } diff --git a/packages/crdt/typedoc.json b/packages/crdt/typedoc.json index ae73b7dc..c9c65c9c 100644 --- a/packages/crdt/typedoc.json +++ b/packages/crdt/typedoc.json @@ -1,6 +1,6 @@ { - "$schema": "https://typedoc.org/schema.json", - "includeVersion": true, - "entryPoints": ["src/index.ts"], - "readme": "README.md" + "$schema": "https://typedoc.org/schema.json", + "includeVersion": true, + "entryPoints": ["src/index.ts"], + "readme": "README.md" } diff --git a/packages/network/package.json b/packages/network/package.json index ee9e7b2f..21f0ed64 100644 --- a/packages/network/package.json +++ b/packages/network/package.json @@ -1,52 +1,47 @@ { - "name": "@topology-foundation/network", - "version": "0.0.23-5", - "license": "MIT", - "repository": { - "type": "git", - "url": "git+https://github.com/topology-foundation/ts-topology.git" - }, - "type": "module", - "types": "./dist/src/index.d.ts", - "files": [ - "src", - "dist", - "!dist/test", - "!**/*.tsbuildinfo" - ], - "exports": { - ".": { - "types": "./dist/src/index.d.ts", - "import": "./dist/src/index.js" - } - }, - "scripts": { - "build": "tsc -b", - "clean": "rm -rf dist/ node_modules/", - "prepack": "tsc -b", - "test": "vitest" - }, - "devDependencies": { - "@libp2p/interface": "^1.7.0", - "react-native-webrtc": "^124.0.3" - }, - "dependencies": { - "@chainsafe/libp2p-gossipsub": "^13.1.0", - "@chainsafe/libp2p-noise": "^15.1.1", - "@chainsafe/libp2p-yamux": "^6.0.2", - "@libp2p/autonat": "^1.1.4", - "@libp2p/bootstrap": "^10.1.4", - "@libp2p/circuit-relay-v2": "^1.1.4", - "@libp2p/dcutr": "^1.1.4", - "@libp2p/identify": "^2.1.4", - "@libp2p/interface-pubsub": "^4.0.1", - "@libp2p/mdns": "^10.1.3", - "@libp2p/pubsub-peer-discovery": "^10.0.2", - "@libp2p/webrtc": "^4.1.7", - "@libp2p/websockets": "^8.1.2", - "@libp2p/webtransport": "^4.1.2", - "@multiformats/multiaddr": "^12.3.0", - "it-pipe": "^3.0.1", - "libp2p": "^1.8.3" - } + "name": "@topology-foundation/network", + "version": "0.0.23-5", + "license": "MIT", + "repository": { + "type": "git", + "url": "git+https://github.com/topology-foundation/ts-topology.git" + }, + "type": "module", + "types": "./dist/src/index.d.ts", + "files": ["src", "dist", "!dist/test", "!**/*.tsbuildinfo"], + "exports": { + ".": { + "types": "./dist/src/index.d.ts", + "import": "./dist/src/index.js" + } + }, + "scripts": { + "build": "tsc -b", + "clean": "rm -rf dist/ node_modules/", + "prepack": "tsc -b", + "test": "vitest" + }, + "devDependencies": { + "@libp2p/interface": "^1.7.0", + "react-native-webrtc": "^124.0.3" + }, + "dependencies": { + "@chainsafe/libp2p-gossipsub": "^13.1.0", + "@chainsafe/libp2p-noise": "^15.1.1", + "@chainsafe/libp2p-yamux": "^6.0.2", + "@libp2p/autonat": "^1.1.4", + "@libp2p/bootstrap": "^10.1.4", + "@libp2p/circuit-relay-v2": "^1.1.4", + "@libp2p/dcutr": "^1.1.4", + "@libp2p/identify": "^2.1.4", + "@libp2p/interface-pubsub": "^4.0.1", + "@libp2p/mdns": "^10.1.3", + "@libp2p/pubsub-peer-discovery": "^10.0.2", + "@libp2p/webrtc": "^4.1.7", + "@libp2p/websockets": "^8.1.2", + "@libp2p/webtransport": "^4.1.2", + "@multiformats/multiaddr": "^12.3.0", + "it-pipe": "^3.0.1", + "libp2p": "^1.8.3" + } } diff --git a/packages/network/src/node.ts b/packages/network/src/node.ts index ac2413c9..f0653388 100644 --- a/packages/network/src/node.ts +++ b/packages/network/src/node.ts @@ -1,244 +1,246 @@ import { - GossipsubEvents, - GossipsubMessage, - gossipsub, + type GossipsubEvents, + type GossipsubMessage, + gossipsub, } from "@chainsafe/libp2p-gossipsub"; import { noise } from "@chainsafe/libp2p-noise"; import { yamux } from "@chainsafe/libp2p-yamux"; +import { autoNAT } from "@libp2p/autonat"; +import { bootstrap } from "@libp2p/bootstrap"; import { - circuitRelayServer, - circuitRelayTransport, + circuitRelayServer, + circuitRelayTransport, } from "@libp2p/circuit-relay-v2"; import { generateKeyPairFromSeed } from "@libp2p/crypto/keys"; import { dcutr } from "@libp2p/dcutr"; import { identify } from "@libp2p/identify"; -import { EventHandler, PubSub, Stream, StreamHandler } from "@libp2p/interface"; +import type { + EventHandler, + PrivateKey, + PubSub, + Stream, + StreamHandler, +} from "@libp2p/interface"; import { createFromPrivKey } from "@libp2p/peer-id-factory"; import { pubsubPeerDiscovery } from "@libp2p/pubsub-peer-discovery"; import { webRTC, webRTCDirect } from "@libp2p/webrtc"; import { webSockets } from "@libp2p/websockets"; -import { multiaddr } from "@multiformats/multiaddr"; -import { Libp2p, createLibp2p } from "libp2p"; -import { stringToStream } from "./stream.js"; -import { bootstrap } from "@libp2p/bootstrap"; import { webTransport } from "@libp2p/webtransport"; -import { autoNAT } from "@libp2p/autonat"; +import { multiaddr } from "@multiformats/multiaddr"; +import { type Libp2p, createLibp2p } from "libp2p"; import { fromString as uint8ArrayFromString } from "uint8arrays/from-string"; +import { stringToStream } from "./stream.js"; // snake_casing to match the JSON config export interface TopologyNetworkNodeConfig { - addresses?: string[]; - bootstrap?: boolean; - bootstrap_peers?: string[]; - private_key_seed?: string; + addresses?: string[]; + bootstrap?: boolean; + bootstrap_peers?: string[]; + private_key_seed?: string; } export class TopologyNetworkNode { - private _config?: TopologyNetworkNodeConfig; - private _node?: Libp2p; - private _pubsub?: PubSub; - - peerId: string = ""; - - constructor(config?: TopologyNetworkNodeConfig) { - this._config = config; - } - - async start() { - let privateKey; - if (this._config?.private_key_seed) { - let tmp = this._config.private_key_seed.padEnd(32, "0"); - privateKey = await generateKeyPairFromSeed( - "Ed25519", - uint8ArrayFromString(tmp), - ); - } - - this._node = await createLibp2p({ - peerId: privateKey ? await createFromPrivKey(privateKey) : undefined, - addresses: { - listen: - this._config && this._config.addresses - ? this._config.addresses - : ["/webrtc"], - }, - connectionEncryption: [noise()], - connectionGater: { - denyDialMultiaddr: () => { - return false; - }, - }, - peerDiscovery: [ - pubsubPeerDiscovery({ - interval: 10_000, - topics: ["topology::discovery"], - }), - bootstrap({ - list: - this._config && this._config.bootstrap_peers - ? this._config.bootstrap_peers - : [ - "/dns4/relay.droak.sh/tcp/443/wss/p2p/Qma3GsJmB47xYuyahPZPSadh1avvxfyYQwk8R3UnFrQ6aP", - ], - }), - ], - services: { - autonat: autoNAT(), - dcutr: dcutr(), - identify: identify(), - pubsub: gossipsub({ - allowPublishToZeroTopicPeers: true, - }), - }, - streamMuxers: [yamux()], - transports: [ - circuitRelayTransport({ - discoverRelays: 2, - reservationConcurrency: 1, - }), - webRTC(), - webRTCDirect(), - webSockets(), - webTransport(), - ], - }); - - if (this._config?.bootstrap) - this._node.services.relay = circuitRelayServer(); - - if (!this._config?.bootstrap) { - for (const addr of this._config?.bootstrap_peers || []) { - this._node.dial(multiaddr(addr)); - } - } - - this._pubsub = this._node.services.pubsub as PubSub; - this.peerId = this._node.peerId.toString(); - - console.log( - "topology::network::start: Successfuly started topology network w/ peer_id", - this.peerId, - ); - - // TODO remove this or add better logger - // we need to keep it now for debugging - this._node.addEventListener("peer:connect", (e) => - console.log("peer:connect", e.detail), - ); - this._node.addEventListener("peer:discovery", (e) => - console.log("peer:discovery", e.detail), - ); - this._node.addEventListener("peer:identify", (e) => - console.log("peer:identify", e.detail), - ); - } - - subscribe(topic: string) { - if (!this._node) { - console.error( - "topology::network::subscribe: Node not initialized, please run .start()", - ); - return; - } - - try { - this._pubsub?.subscribe(topic); - this._pubsub?.getPeers(); - console.log( - "topology::network::subscribe: Successfuly subscribed the topic", - topic, - ); - } catch (e) { - console.error("topology::network::subscribe:", e); - } - } - - unsubscribe(topic: string) { - if (!this._node) { - console.error( - "topology::network::unsubscribe: Node not initialized, please run .start()", - ); - return; - } - - try { - this._pubsub?.unsubscribe(topic); - console.log( - "topology::network::unsubscribe: Successfuly unsubscribed the topic", - topic, - ); - } catch (e) { - console.error("topology::network::unsubscribe:", e); - } - } - - getAllPeers() { - const peers = this._node?.getPeers(); - if (!peers) return []; - return peers.map((peer) => peer.toString()); - } - - getGroupPeers(group: string) { - const peers = this._pubsub?.getSubscribers(group); - if (!peers) return []; - return peers.map((peer) => peer.toString()); - } - - async broadcastMessage(topic: string, message: Uint8Array) { - try { - if (this._pubsub?.getSubscribers(topic)?.length === 0) return; - await this._pubsub?.publish(topic, message); - - console.log( - "topology::network::broadcastMessage: Successfuly broadcasted message to topic", - topic, - ); - } catch (e) { - console.error("topology::network::broadcastMessage:", e); - } - } - - async sendMessage(peerId: string, protocols: string[], message: string) { - try { - const connection = await this._node?.dial([multiaddr(`/p2p/${peerId}`)]); - const stream = await connection?.newStream(protocols); - stringToStream(stream, message); - - console.log( - `topology::network::sendMessage: Successfuly sent message to peer: ${peerId} with message: ${message}`, - ); - } catch (e) { - console.error("topology::network::sendMessage:", e); - } - } - - async sendGroupMessageRandomPeer( - group: string, - protocols: string[], - message: string, - ) { - try { - const peers = this._pubsub?.getSubscribers(group); - if (!peers || peers.length === 0) throw Error("Topic wo/ peers"); - const peerId = peers[Math.floor(Math.random() * peers.length)]; - - const connection = await this._node?.dial(peerId); - const stream: Stream = (await connection?.newStream(protocols)) as Stream; - stringToStream(stream, message); - - console.log( - `topology::network::sendMessageRandomTopicPeer: Successfuly sent message to peer: ${peerId} with message: ${message}`, - ); - } catch (e) { - console.error("topology::network::sendMessageRandomTopicPeer:", e); - } - } - - addGroupMessageHandler(handler: EventHandler>) { - this._pubsub?.addEventListener("gossipsub:message", handler); - } - - addMessageHandler(protocol: string | string[], handler: StreamHandler) { - this._node?.handle(protocol, handler); - } + private _config?: TopologyNetworkNodeConfig; + private _node?: Libp2p; + private _pubsub?: PubSub; + + peerId = ""; + + constructor(config?: TopologyNetworkNodeConfig) { + this._config = config; + } + + async start() { + let privateKey: PrivateKey | undefined = undefined; + if (this._config?.private_key_seed) { + const tmp = this._config.private_key_seed.padEnd(32, "0"); + privateKey = await generateKeyPairFromSeed( + "Ed25519", + uint8ArrayFromString(tmp), + ); + } + + this._node = await createLibp2p({ + peerId: privateKey ? await createFromPrivKey(privateKey) : undefined, + addresses: { + listen: this._config?.addresses ? this._config.addresses : ["/webrtc"], + }, + connectionEncryption: [noise()], + connectionGater: { + denyDialMultiaddr: () => { + return false; + }, + }, + peerDiscovery: [ + pubsubPeerDiscovery({ + interval: 10_000, + topics: ["topology::discovery"], + }), + bootstrap({ + list: this._config?.bootstrap_peers + ? this._config.bootstrap_peers + : [ + "/dns4/relay.droak.sh/tcp/443/wss/p2p/Qma3GsJmB47xYuyahPZPSadh1avvxfyYQwk8R3UnFrQ6aP", + ], + }), + ], + services: { + autonat: autoNAT(), + dcutr: dcutr(), + identify: identify(), + pubsub: gossipsub({ + allowPublishToZeroTopicPeers: true, + }), + }, + streamMuxers: [yamux()], + transports: [ + circuitRelayTransport({ + discoverRelays: 2, + reservationConcurrency: 1, + }), + webRTC(), + webRTCDirect(), + webSockets(), + webTransport(), + ], + }); + + if (this._config?.bootstrap) + this._node.services.relay = circuitRelayServer(); + + if (!this._config?.bootstrap) { + for (const addr of this._config?.bootstrap_peers || []) { + this._node.dial(multiaddr(addr)); + } + } + + this._pubsub = this._node.services.pubsub as PubSub; + this.peerId = this._node.peerId.toString(); + + console.log( + "topology::network::start: Successfuly started topology network w/ peer_id", + this.peerId, + ); + + // TODO remove this or add better logger + // we need to keep it now for debugging + this._node.addEventListener("peer:connect", (e) => + console.log("peer:connect", e.detail), + ); + this._node.addEventListener("peer:discovery", (e) => + console.log("peer:discovery", e.detail), + ); + this._node.addEventListener("peer:identify", (e) => + console.log("peer:identify", e.detail), + ); + } + + subscribe(topic: string) { + if (!this._node) { + console.error( + "topology::network::subscribe: Node not initialized, please run .start()", + ); + return; + } + + try { + this._pubsub?.subscribe(topic); + this._pubsub?.getPeers(); + console.log( + "topology::network::subscribe: Successfuly subscribed the topic", + topic, + ); + } catch (e) { + console.error("topology::network::subscribe:", e); + } + } + + unsubscribe(topic: string) { + if (!this._node) { + console.error( + "topology::network::unsubscribe: Node not initialized, please run .start()", + ); + return; + } + + try { + this._pubsub?.unsubscribe(topic); + console.log( + "topology::network::unsubscribe: Successfuly unsubscribed the topic", + topic, + ); + } catch (e) { + console.error("topology::network::unsubscribe:", e); + } + } + + getAllPeers() { + const peers = this._node?.getPeers(); + if (!peers) return []; + return peers.map((peer) => peer.toString()); + } + + getGroupPeers(group: string) { + const peers = this._pubsub?.getSubscribers(group); + if (!peers) return []; + return peers.map((peer) => peer.toString()); + } + + async broadcastMessage(topic: string, message: Uint8Array) { + try { + if (this._pubsub?.getSubscribers(topic)?.length === 0) return; + await this._pubsub?.publish(topic, message); + + console.log( + "topology::network::broadcastMessage: Successfuly broadcasted message to topic", + topic, + ); + } catch (e) { + console.error("topology::network::broadcastMessage:", e); + } + } + + async sendMessage(peerId: string, protocols: string[], message: string) { + try { + const connection = await this._node?.dial([multiaddr(`/p2p/${peerId}`)]); + const stream = await connection?.newStream(protocols); + stringToStream(stream, message); + + console.log( + `topology::network::sendMessage: Successfuly sent message to peer: ${peerId} with message: ${message}`, + ); + } catch (e) { + console.error("topology::network::sendMessage:", e); + } + } + + async sendGroupMessageRandomPeer( + group: string, + protocols: string[], + message: string, + ) { + try { + const peers = this._pubsub?.getSubscribers(group); + if (!peers || peers.length === 0) throw Error("Topic wo/ peers"); + const peerId = peers[Math.floor(Math.random() * peers.length)]; + + const connection = await this._node?.dial(peerId); + const stream: Stream = (await connection?.newStream(protocols)) as Stream; + stringToStream(stream, message); + + console.log( + `topology::network::sendMessageRandomTopicPeer: Successfuly sent message to peer: ${peerId} with message: ${message}`, + ); + } catch (e) { + console.error("topology::network::sendMessageRandomTopicPeer:", e); + } + } + + addGroupMessageHandler(handler: EventHandler>) { + this._pubsub?.addEventListener("gossipsub:message", handler); + } + + addMessageHandler(protocol: string | string[], handler: StreamHandler) { + this._node?.handle(protocol, handler); + } } diff --git a/packages/network/src/stream.ts b/packages/network/src/stream.ts index e2f2e2d8..1503ac60 100644 --- a/packages/network/src/stream.ts +++ b/packages/network/src/stream.ts @@ -20,7 +20,7 @@ /* eslint-disable no-console */ -import { Stream } from "@libp2p/interface"; +import type { Stream } from "@libp2p/interface"; import * as lp from "it-length-prefixed"; import map from "it-map"; import { pipe } from "it-pipe"; @@ -28,25 +28,25 @@ import { fromString as uint8ArrayFromString } from "uint8arrays/from-string"; import { toString as uint8ArrayToString } from "uint8arrays/to-string"; export async function stringToStream(stream: Stream, input: string) { - await pipe( - input, - (source) => map(source, (string) => uint8ArrayFromString(string)), - (source) => lp.encode(source), - stream.sink, - ); + await pipe( + input, + (source) => map(source, (string) => uint8ArrayFromString(string)), + (source) => lp.encode(source), + stream.sink, + ); } export async function streamToString(stream: Stream) { - return await pipe( - stream.source, - (source) => lp.decode(source), - (source) => map(source, (buf) => uint8ArrayToString(buf.subarray())), - async function (source) { - let output: string[] = []; - for await (const msg of source) { - output.push(msg.toString().replace("\n", "")); - } - return output.join("").trim(); - }, - ); + return await pipe( + stream.source, + (source) => lp.decode(source), + (source) => map(source, (buf) => uint8ArrayToString(buf.subarray())), + async (source) => { + const output: string[] = []; + for await (const msg of source) { + output.push(msg.toString().replace("\n", "")); + } + return output.join("").trim(); + }, + ); } diff --git a/packages/network/tsconfig.json b/packages/network/tsconfig.json index 280f82e7..9cc8b37c 100644 --- a/packages/network/tsconfig.json +++ b/packages/network/tsconfig.json @@ -1,7 +1,7 @@ { - "extends": "../../tsconfig.json", - "compilerOptions": { - "outDir": "dist" - }, - "include": ["src/**/*.ts"] + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "dist" + }, + "include": ["src/**/*.ts"] } diff --git a/packages/network/typedoc.json b/packages/network/typedoc.json index ae73b7dc..c9c65c9c 100644 --- a/packages/network/typedoc.json +++ b/packages/network/typedoc.json @@ -1,6 +1,6 @@ { - "$schema": "https://typedoc.org/schema.json", - "includeVersion": true, - "entryPoints": ["src/index.ts"], - "readme": "README.md" + "$schema": "https://typedoc.org/schema.json", + "includeVersion": true, + "entryPoints": ["src/index.ts"], + "readme": "README.md" } diff --git a/packages/node/configs/bootstrap.json b/packages/node/configs/bootstrap.json index 549106bb..526102ff 100644 --- a/packages/node/configs/bootstrap.json +++ b/packages/node/configs/bootstrap.json @@ -1,10 +1,10 @@ { - "network_config": { - "addresses": ["/ip4/0.0.0.0/tcp/50000/ws", "/ip4/0.0.0.0/tcp/50001"], - "bootstrap": true, - "bootstrap_peers": [ - "/dns4/relay.droak.sh/tcp/443/wss/p2p/Qma3GsJmB47xYuyahPZPSadh1avvxfyYQwk8R3UnFrQ6aP" - ], - "private_key_seed": "bootstrap" - } + "network_config": { + "addresses": ["/ip4/0.0.0.0/tcp/50000/ws", "/ip4/0.0.0.0/tcp/50001"], + "bootstrap": true, + "bootstrap_peers": [ + "/dns4/relay.droak.sh/tcp/443/wss/p2p/Qma3GsJmB47xYuyahPZPSadh1avvxfyYQwk8R3UnFrQ6aP" + ], + "private_key_seed": "bootstrap" + } } diff --git a/packages/node/configs/node.json b/packages/node/configs/node.json index d660844a..00d359ba 100644 --- a/packages/node/configs/node.json +++ b/packages/node/configs/node.json @@ -1,9 +1,9 @@ { - "network_config": { - "addresses": ["/webrtc"], - "bootstrap_peers": [ - "/ip4/127.0.0.1/tcp/50000/ws/p2p/12D3KooWC6sm9iwmYbeQJCJipKTRghmABNz1wnpJANvSMabvecwJ" - ], - "private_key_seed": "node" - } + "network_config": { + "addresses": ["/webrtc"], + "bootstrap_peers": [ + "/ip4/127.0.0.1/tcp/50000/ws/p2p/12D3KooWC6sm9iwmYbeQJCJipKTRghmABNz1wnpJANvSMabvecwJ" + ], + "private_key_seed": "node" + } } diff --git a/packages/node/package.json b/packages/node/package.json index f9951cbd..c99ad132 100644 --- a/packages/node/package.json +++ b/packages/node/package.json @@ -1,43 +1,38 @@ { - "name": "@topology-foundation/node", - "version": "0.0.23-5", - "license": "MIT", - "repository": { - "type": "git", - "url": "git+https://github.com/topology-foundation/ts-topology.git" - }, - "type": "module", - "types": "./dist/src/index.d.ts", - "files": [ - "src", - "dist", - "!dist/test", - "!**/*.tsbuildinfo" - ], - "exports": { - ".": { - "types": "./dist/src/index.d.ts", - "import": "./dist/src/index.js" - } - }, - "scripts": { - "build": "tsc -b", - "clean": "rm -rf dist/ node_modules/", - "cli": "tsx ./src/run.ts", - "prebuild": "node -p \"'export const VERSION = ' + JSON.stringify(require('./package.json').version) + ';'\" > src/version.ts", - "prepack": "tsc -b", - "test": "vitest" - }, - "devDependencies": { - "@types/node": "^22.4.1", - "tsx": "4.16.5", - "typescript": "^5.5.4", - "vitest": "^2.0.5" - }, - "dependencies": { - "@topology-foundation/crdt": "0.0.23-5", - "@topology-foundation/network": "0.0.23-5", - "@topology-foundation/object": "0.0.23-5", - "commander": "^12.1.0" - } + "name": "@topology-foundation/node", + "version": "0.0.23-5", + "license": "MIT", + "repository": { + "type": "git", + "url": "git+https://github.com/topology-foundation/ts-topology.git" + }, + "type": "module", + "types": "./dist/src/index.d.ts", + "files": ["src", "dist", "!dist/test", "!**/*.tsbuildinfo"], + "exports": { + ".": { + "types": "./dist/src/index.d.ts", + "import": "./dist/src/index.js" + } + }, + "scripts": { + "build": "tsc -b", + "clean": "rm -rf dist/ node_modules/", + "cli": "tsx ./src/run.ts", + "prebuild": "node -p \"'export const VERSION = ' + JSON.stringify(require('./package.json').version) + ';'\" > src/version.ts", + "prepack": "tsc -b", + "test": "vitest" + }, + "devDependencies": { + "@types/node": "^22.4.1", + "tsx": "4.16.5", + "typescript": "^5.5.4", + "vitest": "^2.0.5" + }, + "dependencies": { + "@topology-foundation/crdt": "0.0.23-5", + "@topology-foundation/network": "0.0.23-5", + "@topology-foundation/object": "0.0.23-5", + "commander": "^12.1.0" + } } diff --git a/packages/node/src/cli/index.ts b/packages/node/src/cli/index.ts index d1f45a54..822ac3d3 100644 --- a/packages/node/src/cli/index.ts +++ b/packages/node/src/cli/index.ts @@ -6,7 +6,7 @@ program.version(VERSION); program.addOption(new Option("-c, --config ", "config file")); program.addOption( - new Option("-m, --mode ", "mode to run in") - .default("node") - .choices(["node", "relay"]), + new Option("-m, --mode ", "mode to run in") + .default("node") + .choices(["node", "relay"]), ); diff --git a/packages/node/src/index.ts b/packages/node/src/index.ts index f304f5f5..ff91eb70 100644 --- a/packages/node/src/index.ts +++ b/packages/node/src/index.ts @@ -1,195 +1,196 @@ -import { GossipsubMessage } from "@chainsafe/libp2p-gossipsub"; -import { EventHandler, StreamHandler } from "@libp2p/interface"; +import type { GossipsubMessage } from "@chainsafe/libp2p-gossipsub"; +import type { EventHandler, StreamHandler } from "@libp2p/interface"; import { - TopologyNetworkNode, - TopologyNetworkNodeConfig, - streamToString, + TopologyNetworkNode, + type TopologyNetworkNodeConfig, + streamToString, } from "@topology-foundation/network"; -import { TopologyObject } from "@topology-foundation/object"; -import { TopologyObjectStore, TopologyObjectStoreCallback } from "./store"; +import type { TopologyObject } from "@topology-foundation/object"; import { fromString as uint8ArrayFromString } from "uint8arrays/from-string"; import { toString as uint8ArrayToString } from "uint8arrays/to-string"; import { OPERATIONS } from "./operations.js"; +import { TopologyObjectStore, type TopologyObjectStoreCallback } from "./store"; export * from "./operations.js"; // snake_casing to match the JSON config export interface TopologyNodeConfig { - network_config?: TopologyNetworkNodeConfig; + network_config?: TopologyNetworkNodeConfig; } export class TopologyNode { - private _config?: TopologyNodeConfig; - private _objectStore: TopologyObjectStore; - - networkNode: TopologyNetworkNode; - - constructor(config?: TopologyNodeConfig) { - this._config = config; - this.networkNode = new TopologyNetworkNode(config?.network_config); - this._objectStore = new TopologyObjectStore(); - } - - async start(): Promise { - await this.networkNode.start(); - - this.networkNode.addMessageHandler( - ["/topology/message/0.0.1"], - async ({ stream }) => { - let input = await streamToString(stream); - if (!input) return; - - const message = JSON.parse(input); - switch (message["type"]) { - case "object_fetch": { - const objectId = uint8ArrayToString( - new Uint8Array(message["data"]), - ); - const object = this.getObject(objectId); - const object_message = `{ + private _config?: TopologyNodeConfig; + private _objectStore: TopologyObjectStore; + + networkNode: TopologyNetworkNode; + + constructor(config?: TopologyNodeConfig) { + this._config = config; + this.networkNode = new TopologyNetworkNode(config?.network_config); + this._objectStore = new TopologyObjectStore(); + } + + async start(): Promise { + await this.networkNode.start(); + + this.networkNode.addMessageHandler( + ["/topology/message/0.0.1"], + async ({ stream }) => { + const input = await streamToString(stream); + if (!input) return; + + const message = JSON.parse(input); + switch (message.type) { + case "object_fetch": { + const objectId = uint8ArrayToString(new Uint8Array(message.data)); + const object = this.getObject(objectId); + const object_message = `{ "type": "object", "data": [${uint8ArrayFromString(JSON.stringify(object, (_key, value) => (value instanceof Set ? [...value] : value)))}] }`; - await this.networkNode.sendMessage( - message["sender"], - [stream.protocol], - object_message, - ); - // await stringToStream(stream, object_message); - break; - } - case "object": { - const object = JSON.parse( - uint8ArrayToString(new Uint8Array(message["data"])), - ); - this._objectStore.put(object["id"], object); - break; - } - case "object_sync": { - const objectId = uint8ArrayToString( - new Uint8Array(message["data"]), - ); - const object = this.getObject(objectId); - const object_message = `{ + await this.networkNode.sendMessage( + message.sender, + [stream.protocol], + object_message, + ); + // await stringToStream(stream, object_message); + break; + } + case "object": { + const object = JSON.parse( + uint8ArrayToString(new Uint8Array(message.data)), + ); + this._objectStore.put(object.id, object); + break; + } + case "object_sync": { + const objectId = uint8ArrayToString(new Uint8Array(message.data)); + const object = this.getObject(objectId); + const object_message = `{ "type": "object_merge", "data": [${uint8ArrayFromString(JSON.stringify(object))}] }`; - await this.networkNode.sendMessage( - message["sender"], - [stream.protocol], - object_message, - ); - break; - } - case "object_merge": { - const object = JSON.parse( - uint8ArrayToString(new Uint8Array(message["data"])), - ); - const local = this._objectStore.get(object["id"]); - if (local) { - local.merge(object); - this._objectStore.put(object["id"], local); - } - break; - } - default: { - return; - } - } - }, - ); - } - - createObject(object: TopologyObject) { - const objectId = object.getObjectId(); - this.networkNode.subscribe(objectId); - this._objectStore.put(objectId, object); - } - - /// Subscribe to the object's PubSub group - /// and fetch it from a peer - async subscribeObject(objectId: string, fetch = false, peerId = "", subscribionCallback?: TopologyObjectStoreCallback) { - this.networkNode.subscribe(objectId); - if (subscribionCallback) { - this._objectStore.subscribe(objectId, subscribionCallback); - } - if (!fetch) return; - const message = `{ + await this.networkNode.sendMessage( + message.sender, + [stream.protocol], + object_message, + ); + break; + } + case "object_merge": { + const object = JSON.parse( + uint8ArrayToString(new Uint8Array(message.data)), + ); + const local = this._objectStore.get(object.id); + if (local) { + local.merge(object); + this._objectStore.put(object.id, local); + } + break; + } + default: { + return; + } + } + }, + ); + } + + createObject(object: TopologyObject) { + const objectId = object.getObjectId(); + this.networkNode.subscribe(objectId); + this._objectStore.put(objectId, object); + } + + /// Subscribe to the object's PubSub group + /// and fetch it from a peer + async subscribeObject( + objectId: string, + fetch = false, + peerId = "", + subscribionCallback?: TopologyObjectStoreCallback, + ) { + this.networkNode.subscribe(objectId); + if (subscribionCallback) { + this._objectStore.subscribe(objectId, subscribionCallback); + } + if (!fetch) return; + const message = `{ "type": "object_fetch", "sender": "${this.networkNode.peerId}", "data": [${uint8ArrayFromString(objectId)}] }`; - if (!peerId) { - await this.networkNode.sendGroupMessageRandomPeer( - objectId, - ["/topology/message/0.0.1"], - message, - ); - } else { - await this.networkNode.sendMessage( - peerId, - ["/topology/message/0.0.1"], - message, - ); - } - } - - async syncObject(objectId: string, peerId = "") { - const message = `{ + if (!peerId) { + await this.networkNode.sendGroupMessageRandomPeer( + objectId, + ["/topology/message/0.0.1"], + message, + ); + } else { + await this.networkNode.sendMessage( + peerId, + ["/topology/message/0.0.1"], + message, + ); + } + } + + async syncObject(objectId: string, peerId = "") { + const message = `{ "type": "object_sync", "sender": "${this.networkNode.peerId}", "data": [${uint8ArrayFromString(objectId)}] }`; - if (!peerId) { - await this.networkNode.sendGroupMessageRandomPeer( - objectId, - ["/topology/message/0.0.1"], - message, - ); - } else { - await this.networkNode.sendMessage( - peerId, - ["/topology/message/0.0.1"], - message, - ); - } - } - - /// Get the object from the local Object Store - getObject(objectId: string) { - return this._objectStore.get(objectId); - } - - updateObject(object: TopologyObject, update_data: string) { - this._objectStore.put(object.getObjectId(), object); - // not dialed, emitted through pubsub - const message = `{ + if (!peerId) { + await this.networkNode.sendGroupMessageRandomPeer( + objectId, + ["/topology/message/0.0.1"], + message, + ); + } else { + await this.networkNode.sendMessage( + peerId, + ["/topology/message/0.0.1"], + message, + ); + } + } + + /// Get the object from the local Object Store + getObject(objectId: string) { + return this._objectStore.get(objectId); + } + + updateObject(object: TopologyObject, update_data: string) { + this._objectStore.put(object.getObjectId(), object); + // not dialed, emitted through pubsub + const message = `{ "type": "object_update", "data": [${uint8ArrayFromString(update_data)}] }`; - this.networkNode.broadcastMessage( - object.getObjectId(), - uint8ArrayFromString(message), - ); - } - - addCustomGroup(group: string) { - this.networkNode.subscribe(group); - } - - sendGroupMessage(group: string, message: Uint8Array) { - this.networkNode.broadcastMessage(group, message); - } - - addCustomGroupMessageHandler( - handler: EventHandler>, - ) { - this.networkNode.addGroupMessageHandler(handler); - } - - addCustomMessageHandler(protocol: string | string[], handler: StreamHandler) { - this.networkNode.addMessageHandler(protocol, handler); - } + this.networkNode.broadcastMessage( + object.getObjectId(), + uint8ArrayFromString(message), + ); + } + + addCustomGroup(group: string) { + this.networkNode.subscribe(group); + } + + sendGroupMessage(group: string, message: Uint8Array) { + this.networkNode.broadcastMessage(group, message); + } + + addCustomGroupMessageHandler( + handler: EventHandler>, + ) { + this.networkNode.addGroupMessageHandler(handler); + } + + addCustomMessageHandler(protocol: string | string[], handler: StreamHandler) { + this.networkNode.addMessageHandler(protocol, handler); + } } diff --git a/packages/node/src/operations.ts b/packages/node/src/operations.ts index 4b831b73..44a03b8f 100644 --- a/packages/node/src/operations.ts +++ b/packages/node/src/operations.ts @@ -1,24 +1,24 @@ /* Object and P2P Network operations */ export enum OPERATIONS { - // TODO: Confirm if this needs a network message - // who to send to? - /* Create a new CRO */ - CREATE, - /* Update operation on a CRO */ - UPDATE, + // TODO: Confirm if this needs a network message + // who to send to? + /* Create a new CRO */ + CREATE = 0, + /* Update operation on a CRO */ + UPDATE = 1, - // These two are not network messages - /* Subscribe to a PubSub group (either CRO or custom) */ - SUBSCRIBE, - /* Unsubscribe from a PubSub group */ - UNSUBSCRIBE, + // These two are not network messages + /* Subscribe to a PubSub group (either CRO or custom) */ + SUBSCRIBE = 2, + /* Unsubscribe from a PubSub group */ + UNSUBSCRIBE = 3, - /* Actively send the CRO RIBLT to a random peer */ - SYNC, - /* Accept the sync request and send the RIBLT + /* Actively send the CRO RIBLT to a random peer */ + SYNC = 4, + /* Accept the sync request and send the RIBLT after processing the received RIBLT */ - SYNC_ACCEPT, - /* Reject the sync request */ - SYNC_REJECT, + SYNC_ACCEPT = 5, + /* Reject the sync request */ + SYNC_REJECT = 6, } diff --git a/packages/node/src/run.ts b/packages/node/src/run.ts index 64049b39..3a78a8df 100644 --- a/packages/node/src/run.ts +++ b/packages/node/src/run.ts @@ -1,22 +1,22 @@ +import fs from "node:fs"; +import { TopologyNode, type TopologyNodeConfig } from "."; import { program } from "./cli"; -import { TopologyNode, TopologyNodeConfig } from "."; -import fs from "fs"; async function startNode(config?: TopologyNodeConfig) { - const node = new TopologyNode(config); - node.start(); + const node = new TopologyNode(config); + node.start(); } const run = () => { - program.parse(process.argv); - const opts = program.opts(); + program.parse(process.argv); + const opts = program.opts(); - let config: TopologyNodeConfig | undefined; - if (opts.config) { - config = JSON.parse(fs.readFileSync(opts.config, "utf8")); - } + let config: TopologyNodeConfig | undefined; + if (opts.config) { + config = JSON.parse(fs.readFileSync(opts.config, "utf8")); + } - startNode(config); + startNode(config); }; run(); diff --git a/packages/node/src/store/index.ts b/packages/node/src/store/index.ts index f3e5c8fb..744a6e56 100644 --- a/packages/node/src/store/index.ts +++ b/packages/node/src/store/index.ts @@ -1,39 +1,42 @@ -import { TopologyObject } from "@topology-foundation/object"; +import type { TopologyObject } from "@topology-foundation/object"; -export type TopologyObjectStoreCallback = (objectId: string, object: TopologyObject) => void; +export type TopologyObjectStoreCallback = ( + objectId: string, + object: TopologyObject, +) => void; export class TopologyObjectStore { - // TODO: should be abstracted in handling multiple types of storage - private _store: Map; - private _subscriptions: Map; + // TODO: should be abstracted in handling multiple types of storage + private _store: Map; + private _subscriptions: Map; - constructor() { - this._store = new Map(); - this._subscriptions = new Map(); - } + constructor() { + this._store = new Map(); + this._subscriptions = new Map(); + } - get(objectId: string): TopologyObject | undefined { - return this._store.get(objectId); - } + get(objectId: string): TopologyObject | undefined { + return this._store.get(objectId); + } - put(objectId: string, object: TopologyObject) { - this._store.set(objectId, object); - this._notifySubscribers(objectId, object); - } + put(objectId: string, object: TopologyObject) { + this._store.set(objectId, object); + this._notifySubscribers(objectId, object); + } - subscribe(objectId: string, callback: TopologyObjectStoreCallback): void { - if (!this._subscriptions.has(objectId)) { - this._subscriptions.set(objectId, []); - } - this._subscriptions.get(objectId)?.push(callback); - } + subscribe(objectId: string, callback: TopologyObjectStoreCallback): void { + if (!this._subscriptions.has(objectId)) { + this._subscriptions.set(objectId, []); + } + this._subscriptions.get(objectId)?.push(callback); + } - private _notifySubscribers(objectId: string, object: TopologyObject): void { - const callbacks = this._subscriptions.get(objectId); - if (callbacks) { - for (const callback of callbacks) { - callback(objectId, object); - } - } - } + private _notifySubscribers(objectId: string, object: TopologyObject): void { + const callbacks = this._subscriptions.get(objectId); + if (callbacks) { + for (const callback of callbacks) { + callback(objectId, object); + } + } + } } diff --git a/packages/node/src/version.ts b/packages/node/src/version.ts index 85ceb686..2d05f320 100644 --- a/packages/node/src/version.ts +++ b/packages/node/src/version.ts @@ -1 +1 @@ -export const VERSION = "0.0.22"; +export const VERSION = "0.0.23-5"; diff --git a/packages/node/tsconfig.json b/packages/node/tsconfig.json index 79fe75de..af1c9e05 100644 --- a/packages/node/tsconfig.json +++ b/packages/node/tsconfig.json @@ -1,18 +1,18 @@ { - "extends": "../../tsconfig.json", - "compilerOptions": { - "outDir": "dist" - }, - "references": [ - { - "path": "../crdt" - }, - { - "path": "../network" - }, - { - "path": "../object" - } - ], - "include": ["src/**/*.ts"] + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "dist" + }, + "references": [ + { + "path": "../crdt" + }, + { + "path": "../network" + }, + { + "path": "../object" + } + ], + "include": ["src/**/*.ts"] } diff --git a/packages/node/typedoc.json b/packages/node/typedoc.json index 5dbfb93f..8c14864f 100644 --- a/packages/node/typedoc.json +++ b/packages/node/typedoc.json @@ -1,8 +1,8 @@ { - "$schema": "https://typedoc.org/schema.json", - "includeVersion": true, - "entryPoints": ["src/index.ts"], - "readme": "README.md", - "commentStyle": "block", - "sort": ["kind", "instance-first", "source-order", "alphabetical"] + "$schema": "https://typedoc.org/schema.json", + "includeVersion": true, + "entryPoints": ["src/index.ts"], + "readme": "README.md", + "commentStyle": "block", + "sort": ["kind", "instance-first", "source-order", "alphabetical"] } diff --git a/packages/object/package.json b/packages/object/package.json index 5a9c0316..a08edf45 100644 --- a/packages/object/package.json +++ b/packages/object/package.json @@ -1,29 +1,24 @@ { - "name": "@topology-foundation/object", - "version": "0.0.23-5", - "license": "MIT", - "repository": { - "type": "git", - "url": "git+https://github.com/topology-foundation/ts-topology.git" - }, - "type": "module", - "types": "./dist/src/index.d.ts", - "files": [ - "src", - "dist", - "!dist/test", - "!**/*.tsbuildinfo" - ], - "exports": { - ".": { - "types": "./dist/src/index.d.ts", - "import": "./dist/src/index.js" - } - }, - "scripts": { - "build": "tsc -b", - "clean": "rm -rf dist/ node_modules/", - "prepack": "tsc -b", - "test": "vitest" - } + "name": "@topology-foundation/object", + "version": "0.0.23-5", + "license": "MIT", + "repository": { + "type": "git", + "url": "git+https://github.com/topology-foundation/ts-topology.git" + }, + "type": "module", + "types": "./dist/src/index.d.ts", + "files": ["src", "dist", "!dist/test", "!**/*.tsbuildinfo"], + "exports": { + ".": { + "types": "./dist/src/index.d.ts", + "import": "./dist/src/index.js" + } + }, + "scripts": { + "build": "tsc -b", + "clean": "rm -rf dist/ node_modules/", + "prepack": "tsc -b", + "test": "vitest" + } } diff --git a/packages/object/src/index.ts b/packages/object/src/index.ts index 380bc870..0d21c9c5 100644 --- a/packages/object/src/index.ts +++ b/packages/object/src/index.ts @@ -1,29 +1,29 @@ -import * as crypto from "crypto"; +import * as crypto from "node:crypto"; export abstract class TopologyObject { - // TODO generate functions from the abi - private abi?: string; - private id?: string; + // TODO generate functions from the abi + private abi?: string; + private id?: string; - constructor(peerId: string) { - this.abi = ""; + constructor(peerId: string) { + this.abi = ""; - // id = sha256(abi, peer_id, random_nonce) - this.id = crypto - .createHash("sha256") - .update(this.abi) - .update(peerId) - .update(Math.floor(Math.random() * Number.MAX_VALUE).toString()) - .digest("hex"); - } + // id = sha256(abi, peer_id, random_nonce) + this.id = crypto + .createHash("sha256") + .update(this.abi) + .update(peerId) + .update(Math.floor(Math.random() * Number.MAX_VALUE).toString()) + .digest("hex"); + } - getObjectAbi(): string { - return this.abi ?? ""; - } + getObjectAbi(): string { + return this.abi ?? ""; + } - getObjectId(): string { - return this.id ?? ""; - } + getObjectId(): string { + return this.id ?? ""; + } - abstract merge(other: TopologyObject): void; + abstract merge(other: TopologyObject): void; } diff --git a/packages/object/tsconfig.json b/packages/object/tsconfig.json index 280f82e7..9cc8b37c 100644 --- a/packages/object/tsconfig.json +++ b/packages/object/tsconfig.json @@ -1,7 +1,7 @@ { - "extends": "../../tsconfig.json", - "compilerOptions": { - "outDir": "dist" - }, - "include": ["src/**/*.ts"] + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "dist" + }, + "include": ["src/**/*.ts"] } diff --git a/packages/object/typedoc.json b/packages/object/typedoc.json index ae73b7dc..c9c65c9c 100644 --- a/packages/object/typedoc.json +++ b/packages/object/typedoc.json @@ -1,6 +1,6 @@ { - "$schema": "https://typedoc.org/schema.json", - "includeVersion": true, - "entryPoints": ["src/index.ts"], - "readme": "README.md" + "$schema": "https://typedoc.org/schema.json", + "includeVersion": true, + "entryPoints": ["src/index.ts"], + "readme": "README.md" } diff --git a/tsconfig.json b/tsconfig.json index 0d668bc6..a44f70eb 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,19 +1,19 @@ { - "compilerOptions": { - "strict": true, - "allowJs": true, - "target": "ES2022", - "module": "ES2022", + "compilerOptions": { + "strict": true, + "allowJs": true, + "target": "ES2022", + "module": "ES2022", - "esModuleInterop": true, - "moduleResolution": "node", + "esModuleInterop": true, + "moduleResolution": "node", - "declaration": true, - "declarationMap": true, - "composite": true, + "declaration": true, + "declarationMap": true, + "composite": true, - "skipLibCheck": true, - "stripInternal": true, - "resolveJsonModule": true - } + "skipLibCheck": true, + "stripInternal": true, + "resolveJsonModule": true + } } diff --git a/typedoc.json b/typedoc.json index 2644f844..16dc7402 100644 --- a/typedoc.json +++ b/typedoc.json @@ -1,7 +1,7 @@ { - "entryPoints": ["packages/*"], - "entryPointStrategy": "packages", - "includeVersion": true, - "name": "Topology Protocol", - "readme": "README.md" + "entryPoints": ["packages/*"], + "entryPointStrategy": "packages", + "includeVersion": true, + "name": "Topology Protocol", + "readme": "README.md" } diff --git a/yarn.lock b/yarn.lock index d5d10e55..5f2c19ee 100644 --- a/yarn.lock +++ b/yarn.lock @@ -33,6 +33,60 @@ js-tokens "^4.0.0" picocolors "^1.0.0" +"@biomejs/biome@1.8.3": + version "1.8.3" + resolved "https://registry.yarnpkg.com/@biomejs/biome/-/biome-1.8.3.tgz#3b5eecea90d973f71618aae3e6e8be4d2ca23e42" + integrity sha512-/uUV3MV+vyAczO+vKrPdOW0Iaet7UnJMU4bNMinggGJTAnBPjCoLEYcyYtYHNnUNYlv4xZMH6hVIQCAozq8d5w== + optionalDependencies: + "@biomejs/cli-darwin-arm64" "1.8.3" + "@biomejs/cli-darwin-x64" "1.8.3" + "@biomejs/cli-linux-arm64" "1.8.3" + "@biomejs/cli-linux-arm64-musl" "1.8.3" + "@biomejs/cli-linux-x64" "1.8.3" + "@biomejs/cli-linux-x64-musl" "1.8.3" + "@biomejs/cli-win32-arm64" "1.8.3" + "@biomejs/cli-win32-x64" "1.8.3" + +"@biomejs/cli-darwin-arm64@1.8.3": + version "1.8.3" + resolved "https://registry.yarnpkg.com/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-1.8.3.tgz#be2bfdd445cd2d3cb0ff41a96a72ec761753997c" + integrity sha512-9DYOjclFpKrH/m1Oz75SSExR8VKvNSSsLnVIqdnKexj6NwmiMlKk94Wa1kZEdv6MCOHGHgyyoV57Cw8WzL5n3A== + +"@biomejs/cli-darwin-x64@1.8.3": + version "1.8.3" + resolved "https://registry.yarnpkg.com/@biomejs/cli-darwin-x64/-/cli-darwin-x64-1.8.3.tgz#47d408edd9f5c04069fbcf8610bacf1db8c6c0d9" + integrity sha512-UeW44L/AtbmOF7KXLCoM+9PSgPo0IDcyEUfIoOXYeANaNXXf9mLUwV1GeF2OWjyic5zj6CnAJ9uzk2LT3v/wAw== + +"@biomejs/cli-linux-arm64-musl@1.8.3": + version "1.8.3" + resolved "https://registry.yarnpkg.com/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-1.8.3.tgz#44df284383d57cf4f28daeedd080dad7be05df78" + integrity sha512-9yjUfOFN7wrYsXt/T/gEWfvVxKlnh3yBpnScw98IF+oOeCYb5/b/+K7YNqKROV2i1DlMjg9g/EcN9wvj+NkMuQ== + +"@biomejs/cli-linux-arm64@1.8.3": + version "1.8.3" + resolved "https://registry.yarnpkg.com/@biomejs/cli-linux-arm64/-/cli-linux-arm64-1.8.3.tgz#6a6b1da1dfce0294a028cbb5d6c40d73691dd713" + integrity sha512-fed2ji8s+I/m8upWpTJGanqiJ0rnlHOK3DdxsyVLZQ8ClY6qLuPc9uehCREBifRJLl/iJyQpHIRufLDeotsPtw== + +"@biomejs/cli-linux-x64-musl@1.8.3": + version "1.8.3" + resolved "https://registry.yarnpkg.com/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-1.8.3.tgz#ceef30a8ee1a00d4ad31e32dd31ba2a661f2719d" + integrity sha512-UHrGJX7PrKMKzPGoEsooKC9jXJMa28TUSMjcIlbDnIO4EAavCoVmNQaIuUSH0Ls2mpGMwUIf+aZJv657zfWWjA== + +"@biomejs/cli-linux-x64@1.8.3": + version "1.8.3" + resolved "https://registry.yarnpkg.com/@biomejs/cli-linux-x64/-/cli-linux-x64-1.8.3.tgz#665df74d19fb8f83001a9d80824d3a1723e2123f" + integrity sha512-I8G2QmuE1teISyT8ie1HXsjFRz9L1m5n83U1O6m30Kw+kPMPSKjag6QGUn+sXT8V+XWIZxFFBoTDEDZW2KPDDw== + +"@biomejs/cli-win32-arm64@1.8.3": + version "1.8.3" + resolved "https://registry.yarnpkg.com/@biomejs/cli-win32-arm64/-/cli-win32-arm64-1.8.3.tgz#0fb6f58990f4de0331a6ed22c47c66f5a89133cc" + integrity sha512-J+Hu9WvrBevfy06eU1Na0lpc7uR9tibm9maHynLIoAjLZpQU3IW+OKHUtyL8p6/3pT2Ju5t5emReeIS2SAxhkQ== + +"@biomejs/cli-win32-x64@1.8.3": + version "1.8.3" + resolved "https://registry.yarnpkg.com/@biomejs/cli-win32-x64/-/cli-win32-x64-1.8.3.tgz#6a9dc5a4e13357277da43c015cd5cdc374035448" + integrity sha512-/PJ59vA1pnQeKahemaQf4Nyj7IKUvGQSc3Ze1uIGi+Wvr1xF7rGobSrAAG01T/gUDG21vkDsZYM03NAmPiVkqg== + "@chainsafe/as-chacha20poly1305@^0.1.0": version "0.1.0" resolved "https://registry.yarnpkg.com/@chainsafe/as-chacha20poly1305/-/as-chacha20poly1305-0.1.0.tgz#7da6f8796f9b42dac6e830a086d964f1f9189e09"