From 2d6dd9a791309988574a9a39fc2bd4f2bffa195f Mon Sep 17 00:00:00 2001 From: chen <23054115+cwkang1998@users.noreply.github.com> Date: Thu, 7 Nov 2024 16:05:31 +0700 Subject: [PATCH] feat: add grid collision example Add a new example with grid 2d where collision is implemented. This utilizes the `resolveConflicts` CRO function for conflict resolution on the user position. This also reveals an issue with the `this` context within the `object` package when `resolveConflicts` is callback from `TopologyObject`. --- examples/grid-collision/README.md | 18 ++ examples/grid-collision/index.html | 42 +++ examples/grid-collision/package.json | 35 +++ examples/grid-collision/src/index.ts | 276 ++++++++++++++++++++ examples/grid-collision/src/objects/grid.ts | 165 ++++++++++++ examples/grid-collision/src/util/color.ts | 70 +++++ examples/grid-collision/tsconfig.json | 14 + examples/grid-collision/vite.config.mts | 20 ++ pnpm-lock.yaml | 99 +++++-- 9 files changed, 717 insertions(+), 22 deletions(-) create mode 100644 examples/grid-collision/README.md create mode 100644 examples/grid-collision/index.html create mode 100644 examples/grid-collision/package.json create mode 100644 examples/grid-collision/src/index.ts create mode 100644 examples/grid-collision/src/objects/grid.ts create mode 100644 examples/grid-collision/src/util/color.ts create mode 100644 examples/grid-collision/tsconfig.json create mode 100644 examples/grid-collision/vite.config.mts diff --git a/examples/grid-collision/README.md b/examples/grid-collision/README.md new file mode 100644 index 00000000..6b2d3766 --- /dev/null +++ b/examples/grid-collision/README.md @@ -0,0 +1,18 @@ +# Topology Protocol Collision Example + +This is an example that uses Topology Protocol to implement a 2D grid space where users appear to be circles and can move around the integer grid one grid at a time. We additionally implement collision logic into this example so that no 2 circles can be on one grid at a time. + +## Specifics + +The Grid CRO has a mapping from user id (node id concacenated with a randomly assigned color string) to the user's position on the grid. The CRO leverages the underlying hash graph for conflict-free consistency. The mergeCallback function receives the linearised operations returned from the underlying hash graph, and recomputes the user-position mapping from those operations. + +The `resolveConflict` function is additionally used to implement compensation techniques in order to ensure no 2 node can be on the same circle at a time. + +## How to run locally + +After cloning the repository, run the following commands: + +```bash +cd ts-topology/examples/grid-collision +pnpm dev +``` diff --git a/examples/grid-collision/index.html b/examples/grid-collision/index.html new file mode 100644 index 00000000..c43df353 --- /dev/null +++ b/examples/grid-collision/index.html @@ -0,0 +1,42 @@ + + + + + + Topology - Grid + + +
+

A 2D grid made with CRO

+

Your Peer ID:

+

Peers on dRAM:

+

Discovery Peers:

+ + + | + + +

+ Connected to Grid CRO ID: + + +

+

Peers in CRO:

+
+ +
+ +
+ + + + diff --git a/examples/grid-collision/package.json b/examples/grid-collision/package.json new file mode 100644 index 00000000..7c2c59b9 --- /dev/null +++ b/examples/grid-collision/package.json @@ -0,0 +1,35 @@ +{ + "name": "topology-example-grid-collision", + "version": "0.2.1-0", + "description": "Topology Protocol Grid Example", + "main": "src/index.ts", + "repository": "https://github.com/topology-foundation/ts-topology.git", + "license": "MIT", + "scripts": { + "build": "vite build", + "clean": "rm -rf dist/ node_modules/", + "dev": "vite serve", + "start": "ts-node ./src/index.ts" + }, + "dependencies": { + "@topology-foundation/network": "0.3.0", + "@topology-foundation/node": "0.3.0", + "@topology-foundation/object": "0.3.0", + "assemblyscript": "^0.27.29", + "crypto-browserify": "^3.12.0", + "memfs": "^4.11.1", + "process": "^0.11.10", + "react-spring": "^9.7.4", + "stream-browserify": "^3.0.0", + "ts-node": "^10.9.2", + "uint8arrays": "^5.1.0", + "vm-browserify": "^1.1.2" + }, + "devDependencies": { + "@types/node": "^22.5.4", + "ts-loader": "^9.5.1", + "typescript": "^5.5.4", + "vite": "^5.4.9", + "vite-plugin-node-polyfills": "^0.22.0" + } +} diff --git a/examples/grid-collision/src/index.ts b/examples/grid-collision/src/index.ts new file mode 100644 index 00000000..d03ba078 --- /dev/null +++ b/examples/grid-collision/src/index.ts @@ -0,0 +1,276 @@ +import { TopologyNode } from "@topology-foundation/node"; +import type { TopologyObject } from "@topology-foundation/object"; +import { Grid } from "./objects/grid"; +import { hslToRgb, rgbToHex, rgbToHsl } from "./util/color"; + +const node = new TopologyNode({ + network_config: { + bootstrap_peers: [ + "/dns4/relay.droak.sh/tcp/443/wss/p2p/Qma3GsJmB47xYuyahPZPSadh1avvxfyYQwk8R3UnFrQ6aP", + "/ip4/0.0.0.0/tcp/50000/ws/p2p/12D3KooWC6sm9iwmYbeQJCJipKTRghmABNz1wnpJANvSMabvecwJ", + ], + }, +}); +let topologyObject: TopologyObject; +let gridCRO: Grid; +let peers: string[] = []; +let discoveryPeers: string[] = []; +let objectPeers: string[] = []; + +const formatNodeId = (id: string): string => { + return `${id.slice(0, 4)}...${id.slice(-4)}`; +}; + +const colorMap: Map = new Map(); + +const hashCode = (str: string): number => { + let hash = 0; + for (let i = 0; i < str.length; i++) { + hash = (hash << 5) - hash + str.charCodeAt(i); + hash |= 0; // Convert to 32bit integer + } + return hash; +}; + +const getColorForNodeId = (id: string): string => { + if (!colorMap.has(id)) { + const hash = hashCode(id); + let r = (hash & 0xff0000) >> 16; + let g = (hash & 0x00ff00) >> 8; + let b = hash & 0x0000ff; + + // Convert to HSL and adjust lightness to be below 50% + let [h, s, l] = rgbToHsl(r, g, b); + l = l * 0.5; // Set lightness to below 50% + + // Convert back to RGB + [r, g, b] = hslToRgb(h, s, l); + const color = rgbToHex(r, g, b); // Convert RGB to hex + colorMap.set(id, color); + } + return colorMap.get(id) || "#000000"; +}; + +const render = () => { + if (topologyObject) { + const gridIdElement = document.getElementById("gridId"); + gridIdElement.innerText = topologyObject.id; + const copyGridIdButton = document.getElementById("copyGridId"); + if (copyGridIdButton) { + copyGridIdButton.style.display = "inline"; // Show the button + } + } else { + const copyGridIdButton = document.getElementById("copyGridId"); + if (copyGridIdButton) { + copyGridIdButton.style.display = "none"; // Hide the button + } + } + + const element_peerId = document.getElementById("peerId"); + element_peerId.innerHTML = `${formatNodeId(node.networkNode.peerId)}`; + + const element_peers = document.getElementById("peers"); + element_peers.innerHTML = `[${peers.map((peer) => `${formatNodeId(peer)}`).join(", ")}]`; + + const element_discoveryPeers = ( + document.getElementById("discoveryPeers") + ); + element_discoveryPeers.innerHTML = `[${discoveryPeers.map((peer) => `${formatNodeId(peer)}`).join(", ")}]`; + + const element_objectPeers = ( + document.getElementById("objectPeers") + ); + element_objectPeers.innerHTML = `[${objectPeers.map((peer) => `${formatNodeId(peer)}`).join(", ")}]`; + + if (!gridCRO) return; + const users = gridCRO.getUsers(); + const element_grid = document.getElementById("grid"); + element_grid.innerHTML = ""; + + const gridWidth = element_grid.clientWidth; + const gridHeight = element_grid.clientHeight; + const centerX = Math.floor(gridWidth / 2); + const centerY = Math.floor(gridHeight / 2); + + // Draw grid lines + const numLinesX = Math.floor(gridWidth / 50); + const numLinesY = Math.floor(gridHeight / 50); + + for (let i = -numLinesX; i <= numLinesX; i++) { + const line = document.createElement("div"); + line.style.position = "absolute"; + line.style.left = `${centerX + i * 50}px`; + line.style.top = "0"; + line.style.width = "1px"; + line.style.height = "100%"; + line.style.backgroundColor = "lightgray"; + element_grid.appendChild(line); + } + + for (let i = -numLinesY; i <= numLinesY; i++) { + const line = document.createElement("div"); + line.style.position = "absolute"; + line.style.left = "0"; + line.style.top = `${centerY + i * 50}px`; + line.style.width = "100%"; + line.style.height = "1px"; + line.style.backgroundColor = "lightgray"; + element_grid.appendChild(line); + } + + for (const userColorString of users) { + const [id, color] = userColorString.split(":"); + const position = gridCRO.getUserPosition(userColorString); + + if (position) { + const div = document.createElement("div"); + div.style.position = "absolute"; + div.style.left = `${centerX + position.x * 50 + 5}px`; // Center the circle + div.style.top = `${centerY - position.y * 50 + 5}px`; // Center the circle + if (id === node.networkNode.peerId) { + div.style.width = `${34}px`; + div.style.height = `${34}px`; + } else { + div.style.width = `${34 + 6}px`; + div.style.height = `${34 + 6}px`; + } + div.style.backgroundColor = color; + div.style.borderRadius = "50%"; + div.style.transition = "background-color 1s ease-in-out"; + div.style.animation = `glow-${id} 0.5s infinite alternate`; + + // Add black border for the current user's circle + if (id === node.networkNode.peerId) { + div.style.border = "3px solid black"; + } + + // Create dynamic keyframes for the glow effect + const style = document.createElement("style"); + style.innerHTML = ` + @keyframes glow-${id} { + 0% { + background-color: ${hexToRgba(color, 0.5)}; + } + 100% { + background-color: ${hexToRgba(color, 1)}; + } + }`; + document.head.appendChild(style); + + element_grid.appendChild(div); + } + } +}; + +// Helper function to convert hex color to rgba +function hexToRgba(hex: string, alpha: number) { + const bigint = Number.parseInt(hex.slice(1), 16); + const r = (bigint >> 16) & 255; + const g = (bigint >> 8) & 255; + const b = bigint & 255; + return `rgba(${r}, ${g}, ${b}, ${alpha})`; +} + +async function addUser() { + if (!gridCRO) { + console.error("Grid CRO not initialized"); + alert("Please create or join a grid first"); + return; + } + + gridCRO.addUser( + node.networkNode.peerId, + getColorForNodeId(node.networkNode.peerId), + ); + render(); +} + +async function moveUser(direction: string) { + if (!gridCRO) { + console.error("Grid CRO not initialized"); + alert("Please create or join a grid first"); + return; + } + + gridCRO.moveUser(node.networkNode.peerId, direction); + render(); +} + +async function createConnectHandlers() { + node.addCustomGroupMessageHandler(topologyObject.id, (e) => { + if (topologyObject) + objectPeers = node.networkNode.getGroupPeers(topologyObject.id); + render(); + }); + + node.objectStore.subscribe(topologyObject.id, (_, obj) => { + render(); + }); +} + +async function main() { + await node.start(); + render(); + + node.addCustomGroupMessageHandler("", (e) => { + peers = node.networkNode.getAllPeers(); + discoveryPeers = node.networkNode.getGroupPeers("topology::discovery"); + render(); + }); + + const button_create = ( + document.getElementById("createGrid") + ); + button_create.addEventListener("click", async () => { + topologyObject = await node.createObject(new Grid()); + gridCRO = topologyObject.cro as Grid; + createConnectHandlers(); + await addUser(); + render(); + }); + + const button_connect = document.getElementById("joinGrid"); + button_connect.addEventListener("click", async () => { + const croId = (document.getElementById("gridInput")) + .value; + try { + topologyObject = await node.createObject( + new Grid(), + croId, + undefined, + true, + ); + gridCRO = topologyObject.cro as Grid; + createConnectHandlers(); + await addUser(); + render(); + console.log("Succeeded in connecting with CRO", croId); + } catch (e) { + console.error("Error while connecting with CRO", croId, e); + } + }); + + document.addEventListener("keydown", (event) => { + if (event.key === "w") moveUser("U"); + if (event.key === "a") moveUser("L"); + if (event.key === "s") moveUser("D"); + if (event.key === "d") moveUser("R"); + }); + + const copyButton = document.getElementById("copyGridId"); + copyButton.addEventListener("click", () => { + const gridIdText = (document.getElementById("gridId")) + .innerText; + navigator.clipboard + .writeText(gridIdText) + .then(() => { + // alert("Grid CRO ID copied to clipboard!"); + console.log("Grid CRO ID copied to clipboard"); + }) + .catch((err) => { + console.error("Failed to copy: ", err); + }); + }); +} + +main(); diff --git a/examples/grid-collision/src/objects/grid.ts b/examples/grid-collision/src/objects/grid.ts new file mode 100644 index 00000000..aa9fba56 --- /dev/null +++ b/examples/grid-collision/src/objects/grid.ts @@ -0,0 +1,165 @@ +import { + ActionType, + type CRO, + type Operation, + type ResolveConflictsType, + SemanticsType, + type Vertex, +} from "@topology-foundation/object"; + +export class Grid implements CRO { + operations: string[] = ["addUser", "moveUser"]; + semanticsType: SemanticsType = SemanticsType.pair; + positions: Map; + + constructor() { + this.positions = new Map(); + } + + addUser(userId: string, color: string): void { + this._addUser(userId, color); + } + + private _addUser(userId: string, color: string): void { + const userColorString = `${userId}:${color}`; + this.positions.set(userColorString, { x: 0, y: 0 }); + } + + moveUser(userId: string, direction: string): void { + this._moveUser(userId, direction); + } + + private _computeNewPosition( + pos: { x: number; y: number }, + direction: string, + ): { x: number; y: number } { + let deltaY = 0; + let deltaX = 0; + switch (direction) { + case "U": + deltaY += 1; + break; + case "D": + deltaY -= 1; + break; + case "L": + deltaX -= 1; + break; + case "R": + deltaX += 1; + break; + } + + return { x: pos.x + deltaX, y: pos.y + deltaY }; + } + + private _moveUser(userId: string, direction: string): void { + const userColorString = [...this.positions.keys()].find((u) => + u.startsWith(`${userId}:`), + ); + if (userColorString) { + const position = this.positions.get(userColorString); + if (position) { + const newPos = this._computeNewPosition(position, direction); + this.positions.set(userColorString, newPos); + } + } + } + + getUsers(): string[] { + return [...this.positions.keys()]; + } + + getUserPosition( + userColorString: string, + ): { x: number; y: number } | undefined { + const position = this.positions.get(userColorString); + if (position) { + return position; + } + return undefined; + } + + resolveConflicts(vertices: Vertex[]): ResolveConflictsType { + // Here we implement compensation for the location. + // As we operate based on pairwise comparison, there's always only 2 elements. + // First the vertices must be available, and also not of the same node. + if (vertices.length === 2 && vertices[0].nodeId !== vertices[1].nodeId) { + const leftVertex = vertices[0]; + const rightVertex = vertices[1]; + const leftVertexPosition = leftVertex.operation + ? this.getUserPosition(":".concat(leftVertex.operation.value)) + : undefined; + const rightVertexPosition = rightVertex.operation + ? this.getUserPosition(":".concat(rightVertex.operation.value)) + : undefined; + console.log(vertices); + // Let's first handle adding a new user + if ( + leftVertex.operation?.type === "addUser" && + rightVertex.operation?.type === "addUser" + ) { + // This basically tells the cro to accept only the ones that comes first. + if (leftVertexPosition) { + return { action: ActionType.DropRight }; + } + return { action: ActionType.DropLeft }; + } + + // Now handle moving the user + if ( + leftVertex.operation?.type === "moveUser" && + rightVertex.operation?.type === "moveUser" && + leftVertexPosition && + rightVertexPosition + ) { + const leftVertexNextPosition = this._computeNewPosition( + leftVertexPosition, + leftVertex.operation.value[1], + ); + const rightVertexNextPosition = this._computeNewPosition( + rightVertexPosition, + rightVertex.operation.value[1], + ); + + // If they are going to colide, do nothing so they don't move and thus do not colide. + if ( + leftVertexNextPosition.x === rightVertexNextPosition.x && + leftVertexNextPosition.y === rightVertexNextPosition.y + ) { + return { action: ActionType.Drop }; + } + } + } + + // If none of the operations match our criteria, they are concurrent + // safe, and thus we don't need to do anything. + return { action: ActionType.Nop }; + } + + mergeCallback(operations: Operation[]): void { + // reset this.positions + this.positions = new Map(); + + // apply operations to this.positions + for (const op of operations) { + if (!op.value) continue; + switch (op.type) { + case "addUser": { + const [userId, color] = op.value; + this._addUser(userId, color); + break; + } + case "moveUser": { + const [userId, direction] = op.value; + this._moveUser(userId, direction); + break; + } + } + } + } +} + +export function createGrid(): Grid { + return new Grid(); +} diff --git a/examples/grid-collision/src/util/color.ts b/examples/grid-collision/src/util/color.ts new file mode 100644 index 00000000..a5243339 --- /dev/null +++ b/examples/grid-collision/src/util/color.ts @@ -0,0 +1,70 @@ +export const rgbToHsl = ( + rInt: number, + gInt: number, + bInt: number, +): [number, number, number] => { + const r = rInt / 255; + const g = gInt / 255; + const b = bInt / 255; + const max = Math.max(r, g, b); + const min = Math.min(r, g, b); + let h = 0; + let s: number; + const l = (max + min) / 2; // Initialize h with a default value + + if (max === min) { + h = s = 0; // achromatic + } else { + const chromaticity = max - min; + s = l > 0.5 ? chromaticity / (2 - max - min) : chromaticity / (max + min); + switch (max) { + case r: + h = (g - b) / chromaticity + (g < b ? 6 : 0); + break; + case g: + h = (b - r) / chromaticity + 2; + break; + case b: + h = (r - g) / chromaticity + 4; + break; + } + h /= 6; + } + return [h * 360, s, l]; +}; + +export const hslToRgb = ( + h: number, + s: number, + l: number, +): [number, number, number] => { + let r: number; + let g: number; + let b: number; + + if (s === 0) { + r = g = b = l; // achromatic + } else { + const hue2rgb = (p: number, q: number, t_: number) => { + let t = t_; + if (t < 0) t += 1; + if (t > 1) t -= 1; + if (t < 1 / 6) return p + (q - p) * 6 * t; + if (t < 1 / 2) return q; + if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6; + return p; + }; + + const q = l < 0.5 ? l * (1 + s) : l + s - l * s; + const p = 2 * l - q; + r = hue2rgb(p, q, h / 360 + 1 / 3); + g = hue2rgb(p, q, h / 360); + b = hue2rgb(p, q, h / 360 - 1 / 3); + } + + return [Math.round(r * 255), Math.round(g * 255), Math.round(b * 255)]; +}; + +export const rgbToHex = (r: number, g: number, b: number): string => { + return `#${((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1)}`; +}; diff --git a/examples/grid-collision/tsconfig.json b/examples/grid-collision/tsconfig.json new file mode 100644 index 00000000..23d99aec --- /dev/null +++ b/examples/grid-collision/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "target": "ESNext", + "module": "ESNext", + "rootDir": ".", + "strict": true, + "moduleResolution": "node", + "allowSyntheticDefaultImports": true, + "allowJs": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true + } +} diff --git a/examples/grid-collision/vite.config.mts b/examples/grid-collision/vite.config.mts new file mode 100644 index 00000000..e3518486 --- /dev/null +++ b/examples/grid-collision/vite.config.mts @@ -0,0 +1,20 @@ +import path from "node:path"; +import { defineConfig } from "vite"; +import { nodePolyfills } from "vite-plugin-node-polyfills"; + +export default defineConfig({ + build: { + target: "esnext", + }, + plugins: [nodePolyfills()], + optimizeDeps: { + esbuildOptions: { + target: "esnext", + }, + }, + resolve: { + alias: { + "@topology-foundation": path.resolve(__dirname, "../../packages"), + }, + }, +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 48f78ee1..24280f00 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -45,16 +45,16 @@ importers: examples/canvas: dependencies: '@topology-foundation/blueprints': - specifier: 0.2.1 + specifier: 0.3.0 version: link:../../packages/blueprints '@topology-foundation/network': - specifier: 0.2.1 + specifier: 0.3.0 version: link:../../packages/network '@topology-foundation/node': - specifier: 0.2.1 + specifier: 0.3.0 version: link:../../packages/node '@topology-foundation/object': - specifier: 0.2.1 + specifier: 0.3.0 version: link:../../packages/object crypto-browserify: specifier: ^3.12.0 @@ -91,16 +91,16 @@ importers: examples/chat: dependencies: '@topology-foundation/blueprints': - specifier: 0.2.1 + specifier: 0.3.0 version: link:../../packages/blueprints '@topology-foundation/network': - specifier: 0.2.1 + specifier: 0.3.0 version: link:../../packages/network '@topology-foundation/node': - specifier: 0.2.1 + specifier: 0.3.0 version: link:../../packages/node '@topology-foundation/object': - specifier: 0.2.1 + specifier: 0.3.0 version: link:../../packages/object assemblyscript: specifier: ^0.27.29 @@ -143,13 +143,68 @@ importers: examples/grid: dependencies: '@topology-foundation/network': - specifier: 0.2.1 + specifier: 0.3.0 version: link:../../packages/network '@topology-foundation/node': - specifier: 0.2.1 + specifier: 0.3.0 version: link:../../packages/node '@topology-foundation/object': - specifier: 0.2.1 + specifier: 0.3.0 + version: link:../../packages/object + assemblyscript: + specifier: ^0.27.29 + version: 0.27.30 + crypto-browserify: + specifier: ^3.12.0 + version: 3.12.1 + memfs: + specifier: ^4.11.1 + version: 4.14.0 + process: + specifier: ^0.11.10 + version: 0.11.10 + react-spring: + specifier: ^9.7.4 + version: 9.7.4(@react-three/fiber@8.17.10(react-dom@18.3.1(react@18.3.1))(react-native@0.76.1(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.3.12)(react@18.3.1))(react@18.3.1)(three@0.169.0))(konva@9.3.16)(react-dom@18.3.1(react@18.3.1))(react-konva@18.2.10(konva@9.3.16)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-native@0.76.1(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.3.12)(react@18.3.1))(react-zdog@1.2.2)(react@18.3.1)(three@0.169.0)(zdog@1.1.3) + stream-browserify: + specifier: ^3.0.0 + version: 3.0.0 + ts-node: + specifier: ^10.9.2 + version: 10.9.2(@types/node@22.8.4)(typescript@5.6.3) + uint8arrays: + specifier: ^5.1.0 + version: 5.1.0 + vm-browserify: + specifier: ^1.1.2 + version: 1.1.2 + devDependencies: + '@types/node': + specifier: ^22.5.4 + version: 22.8.4 + ts-loader: + specifier: ^9.5.1 + version: 9.5.1(typescript@5.6.3)(webpack@5.95.0) + typescript: + specifier: ^5.5.4 + version: 5.6.3 + vite: + specifier: ^5.4.9 + version: 5.4.10(@types/node@22.8.4)(terser@5.36.0) + vite-plugin-node-polyfills: + specifier: ^0.22.0 + version: 0.22.0(rollup@4.24.3)(vite@5.4.10(@types/node@22.8.4)(terser@5.36.0)) + + examples/grid-collision: + dependencies: + '@topology-foundation/network': + specifier: 0.3.0 + version: link:../../packages/network + '@topology-foundation/node': + specifier: 0.3.0 + version: link:../../packages/node + '@topology-foundation/object': + specifier: 0.3.0 version: link:../../packages/object assemblyscript: specifier: ^0.27.29 @@ -198,16 +253,16 @@ importers: examples/local-bootstrap: dependencies: '@topology-foundation/blueprints': - specifier: 0.2.1 + specifier: 0.3.0 version: link:../../packages/blueprints '@topology-foundation/network': - specifier: 0.2.1 + specifier: 0.3.0 version: link:../../packages/network '@topology-foundation/node': - specifier: 0.2.1 + specifier: 0.3.0 version: link:../../packages/node '@topology-foundation/object': - specifier: 0.2.1 + specifier: 0.3.0 version: link:../../packages/object assemblyscript: specifier: ^0.27.29 @@ -254,7 +309,7 @@ importers: version: 4.1.2 devDependencies: '@topology-foundation/object': - specifier: 0.2.1 + specifier: 0.3.0 version: link:../object assemblyscript: specifier: ^0.27.29 @@ -326,7 +381,7 @@ importers: specifier: ^12.3.1 version: 12.3.1 '@topology-foundation/logger': - specifier: ^0.2.1 + specifier: ^0.3.0 version: link:../logger it-length-prefixed: specifier: ^9.1.0 @@ -366,16 +421,16 @@ importers: specifier: ^2.1.3 version: 2.2.0 '@topology-foundation/blueprints': - specifier: 0.2.1 + specifier: 0.3.0 version: link:../blueprints '@topology-foundation/logger': - specifier: 0.2.1 + specifier: 0.3.0 version: link:../logger '@topology-foundation/network': - specifier: 0.2.1 + specifier: 0.3.0 version: link:../network '@topology-foundation/object': - specifier: 0.2.1 + specifier: 0.3.0 version: link:../object commander: specifier: ^12.1.0 @@ -409,7 +464,7 @@ importers: specifier: ^2.0.0 version: 2.2.1 '@topology-foundation/logger': - specifier: ^0.2.1 + specifier: ^0.3.0 version: link:../logger ts-proto: specifier: ^2.2.4