diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ab102e4a..b17f4bdd 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -57,4 +57,18 @@ jobs: yarn build cd ../../examples/canvas yarn install --frozen-lockfile + yarn build + + build-example-chat: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - shell: bash + run: | + # needs to build dist beforehand + cd packages/node + yarn install --frozen-lockfile + yarn build + cd ../../examples/chat + yarn install --frozen-lockfile yarn build \ No newline at end of file diff --git a/examples/canvas/package.json b/examples/canvas/package.json index acfe6bc5..f5f132ab 100644 --- a/examples/canvas/package.json +++ b/examples/canvas/package.json @@ -9,10 +9,10 @@ "start": "ts-node ./src/index.ts" }, "dependencies": { - "@topology-foundation/crdt": "0.0.22", - "@topology-foundation/network": "0.0.22", - "@topology-foundation/node": "0.0.22", - "@topology-foundation/object": "0.0.22", + "@topology-foundation/crdt": "file:../../packages/crdt", + "@topology-foundation/network": "file:../../packages/network", + "@topology-foundation/node": "file:../../packages/node", + "@topology-foundation/object": "file:../../packages/object", "crypto-browserify": "^3.12.0", "process": "^0.11.10", "stream-browserify": "^3.0.0", diff --git a/examples/chat/.gitignore b/examples/chat/.gitignore new file mode 100644 index 00000000..abca009d --- /dev/null +++ b/examples/chat/.gitignore @@ -0,0 +1,4 @@ +dist/ +docs/ +node_modules/ +public/static \ No newline at end of file diff --git a/examples/chat/README.md b/examples/chat/README.md new file mode 100644 index 00000000..d6623dcc --- /dev/null +++ b/examples/chat/README.md @@ -0,0 +1,27 @@ +# Topology Protocol Example + +This is an example of Topology Protocol usage in a chat system where a user can create or connect to a chat room, send and read the messages sent in the group chat. + +## Specifics + +Messages are represented as strings in the format (timestamp, content, senderId). Chat is a class which extends TopologyObject and has Gset\ as an attribute to store the list of messages. + +## How to run locally + +After cloning the repository, run the following commands: + +```bash +cd ts-topology/examples/chat +yarn +yarn build +yarn dev +``` + +Debugging is made easier by setting the mode in `webpack.config.js` to "development": + +```js +module.exports = { + mode: "development", + ... +} +``` diff --git a/examples/chat/package.json b/examples/chat/package.json new file mode 100644 index 00000000..dc23e559 --- /dev/null +++ b/examples/chat/package.json @@ -0,0 +1,33 @@ +{ + "name": "topology-example-chat", + "version": "1.0.0", + "description": "Topology Protocol Chat Exmaple", + "main": "src/index.ts", + "repository": "https://github.com/topology-foundation/ts-topology.git", + "license": "MIT", + "dependencies": { + "@topology-foundation/crdt": "file:../../packages/crdt", + "@topology-foundation/network": "file:../../packages/network", + "@topology-foundation/node": "file:../../packages/node", + "@topology-foundation/object": "file:../../packages/object", + "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": "^20.11.16", + "ts-loader": "^9.3.1", + "typescript": "^4.7.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/public/index.html b/examples/chat/public/index.html new file mode 100644 index 00000000..928d948b --- /dev/null +++ b/examples/chat/public/index.html @@ -0,0 +1,51 @@ + + + + + + Topology - Chat + + +
+

Topology Protocol - Chat

+

Current peer ID

+

Connected to

+

peers:

+

discovery_peers:

+

object_peers:

+ + + + + +
+ +
+ +
+ +
+ + +
+ + + + + + diff --git a/examples/chat/src/handlers.ts b/examples/chat/src/handlers.ts new file mode 100644 index 00000000..2c6ccb83 --- /dev/null +++ b/examples/chat/src/handlers.ts @@ -0,0 +1,31 @@ +import { toString as uint8ArrayToString } from "uint8arrays/to-string"; +import { 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; + } + } +}; + +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 diff --git a/examples/chat/src/index.ts b/examples/chat/src/index.ts new file mode 100644 index 00000000..d2d2888a --- /dev/null +++ b/examples/chat/src/index.ts @@ -0,0 +1,129 @@ +import { TopologyNode } from "@topology-foundation/node"; +import { Chat, IChat } from "./objects/chat"; +import { handleChatMessages } from "./handlers"; +import { GSet } from "@topology-foundation/crdt"; + +const node = new TopologyNode(); +// CRO = Conflict-free Replicated Object +let chatCRO: IChat; +let peers: string[] = []; +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); + }); + +} + +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(); +} + +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 = objectId; + 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; + }); +} + +main(); \ No newline at end of file diff --git a/examples/chat/src/objects/chat.ts b/examples/chat/src/objects/chat.ts new file mode 100644 index 00000000..d8333df7 --- /dev/null +++ b/examples/chat/src/objects/chat.ts @@ -0,0 +1,32 @@ +import { TopologyObject } from "@topology-foundation/object"; +import { GSet } from "@topology-foundation/crdt"; + +export interface IChat extends TopologyObject { + 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()); + } + + addMessage(timestamp: string, message: string, node_id: string): void { + this.chat.add(`(${timestamp}, ${message}, ${node_id})`); + } + + 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 new file mode 100644 index 00000000..86c25cb7 --- /dev/null +++ b/examples/chat/tsconfig.json @@ -0,0 +1,14 @@ +{ + "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 new file mode 100644 index 00000000..46ac5e36 --- /dev/null +++ b/examples/chat/webpack.config.js @@ -0,0 +1,63 @@ +const path = require("path"); +const webpack = require("webpack"); +const fs = require("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", + }), + ], +}; diff --git a/packages/network/package.json b/packages/network/package.json index 199d8c43..a7c2f9d6 100644 --- a/packages/network/package.json +++ b/packages/network/package.json @@ -35,16 +35,16 @@ "@chainsafe/libp2p-noise": "^15.1.0", "@chainsafe/libp2p-yamux": "^6.0.2", "@libp2p/autonat": "^1.0.0", - "@libp2p/bootstrap": "^10.1.0", + "@libp2p/bootstrap": "^10.1.2", "@libp2p/circuit-relay-v2": "^1.1.2", "@libp2p/dcutr": "^1.1.0", - "@libp2p/identify": "^2.0.2", + "@libp2p/identify": "^2.1.2", "@libp2p/interface-pubsub": "^4.0.1", - "@libp2p/mdns": "^10.1.1", + "@libp2p/mdns": "^10.1.2", "@libp2p/pubsub-peer-discovery": "^10.0.2", "@libp2p/webrtc": "^4.1.2", - "@libp2p/websockets": "^8.1.1", - "@libp2p/webtransport": "^4.1.1", + "@libp2p/websockets": "^8.1.2", + "@libp2p/webtransport": "^4.1.2", "@multiformats/multiaddr": "^12.3.0", "it-pipe": "^3.0.1", "libp2p": "^1.8.1" diff --git a/packages/node/src/index.ts b/packages/node/src/index.ts index d8b20b83..75efe54f 100644 --- a/packages/node/src/index.ts +++ b/packages/node/src/index.ts @@ -48,7 +48,7 @@ export class TopologyNode { const object = this.getObject(objectId); const object_message = `{ "type": "object", - "data": [${uint8ArrayFromString(JSON.stringify(object))}] + "data": [${uint8ArrayFromString(JSON.stringify(object, (_key, value) => (value instanceof Set ? [...value] : value)))}] }`; await this.networkNode.sendMessage( message["sender"], @@ -63,6 +63,7 @@ export class TopologyNode { uint8ArrayToString(new Uint8Array(message["data"])), ); this.objectStore.put(object["id"], object); + break; } case "object_sync": { const objectId = uint8ArrayToString( @@ -90,6 +91,7 @@ export class TopologyNode { // local.merge(object); this.objectStore.put(object["id"], local); } + break; } default: { return; @@ -111,7 +113,7 @@ export class TopologyNode { "data": [${uint8ArrayFromString(objectId)}] }`; - if (peerId === "") { + if (!peerId) { await this.networkNode.sendGroupMessageRandomPeer( objectId, ["/topology/message/0.0.1"], @@ -133,7 +135,7 @@ export class TopologyNode { "data": [${uint8ArrayFromString(objectId)}] }`; - if (peerId === "") { + if (!peerId) { await this.networkNode.sendGroupMessageRandomPeer( objectId, ["/topology/message/0.0.1"], diff --git a/yarn.lock b/yarn.lock index 22f3cdb9..fb197ff7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -351,13 +351,13 @@ protons-runtime "^5.4.0" uint8arraylist "^2.4.8" -"@libp2p/bootstrap@^10.1.0": - version "10.1.1" - resolved "https://registry.yarnpkg.com/@libp2p/bootstrap/-/bootstrap-10.1.1.tgz#cc8d9ed18ce1fc7af7f4b6d222390c83b39834b6" - integrity sha512-pm7LyIQnKjGjmLq89IvwRWJIkB90bUspVl3WMTq7boPeZ3Keo+RlNFSJHL5YtDZa/Jvu/iQR+SVxSmjrKdzn8w== +"@libp2p/bootstrap@^10.1.2": + version "10.1.2" + resolved "https://registry.yarnpkg.com/@libp2p/bootstrap/-/bootstrap-10.1.2.tgz#dae2a2061af16755a14879b58a3f5930466e1a42" + integrity sha512-FTM4hZTSiIBOtB9CtCQfpFfwWJva6+l3phR9eRa5gvsEdJdiUCFKe6VFHqwnsnRN1hobcLUWYi2AN6+0OdPdXw== dependencies: - "@libp2p/interface" "^1.6.0" - "@libp2p/peer-id" "^4.2.0" + "@libp2p/interface" "^1.6.1" + "@libp2p/peer-id" "^4.2.1" "@multiformats/mafmt" "^12.1.6" "@multiformats/multiaddr" "^12.2.3" @@ -414,15 +414,15 @@ protons-runtime "^5.4.0" uint8arraylist "^2.4.8" -"@libp2p/identify@^2.0.2": - version "2.1.1" - resolved "https://registry.yarnpkg.com/@libp2p/identify/-/identify-2.1.1.tgz#f01ea4ff5faa3545358354766509a08717356637" - integrity sha512-vNaz2sKJh7wzNdDnkDn9RBPL7WhotGd78ogkDmr3N5J8V50wSXjA556tHa8eO5dqtyTTHZ0TTa6C0BPW/vxGng== +"@libp2p/identify@^2.1.2": + version "2.1.2" + resolved "https://registry.yarnpkg.com/@libp2p/identify/-/identify-2.1.2.tgz#85a69eb341d893a767d81b3b6138790278109fb9" + integrity sha512-neLNz6WhYAxDHgsb/LGyZF9i+nE4E+dk30/5gFxc/876oVwgc1IqSs2eEEEx9t0l2ToeHADScBM+bHrFsLIk3g== dependencies: - "@libp2p/interface" "^1.6.0" - "@libp2p/interface-internal" "^1.3.0" - "@libp2p/peer-id" "^4.2.0" - "@libp2p/peer-record" "^7.0.21" + "@libp2p/interface" "^1.6.1" + "@libp2p/interface-internal" "^1.3.1" + "@libp2p/peer-id" "^4.2.1" + "@libp2p/peer-record" "^7.0.22" "@multiformats/multiaddr" "^12.2.3" "@multiformats/multiaddr-matcher" "^1.2.1" it-drain "^3.0.7" @@ -501,15 +501,15 @@ interface-datastore "^8.2.11" multiformats "^13.1.0" -"@libp2p/mdns@^10.1.1": - version "10.1.1" - resolved "https://registry.yarnpkg.com/@libp2p/mdns/-/mdns-10.1.1.tgz#92a606144ad968e4fc16489711db308c1484138e" - integrity sha512-XtOv8EWx/nSFrMjFEAUjxt6OO74hny6miEovyTK6XoX7f0iWss307Umro362RyumsC6UNt7J5IXsMQ/MOAxLog== +"@libp2p/mdns@^10.1.2": + version "10.1.2" + resolved "https://registry.yarnpkg.com/@libp2p/mdns/-/mdns-10.1.2.tgz#39629cbebf98c0eba7e9f48620e29dbf2a24ca20" + integrity sha512-OmKa6RnFt2SDVoBK6d/gUUF5AC5+IeL5poBfvDh90ihqZihEukKWWixaiPcmLRt375sMEkGqeY0Jk5vuy+XUsA== dependencies: - "@libp2p/interface" "^1.6.0" - "@libp2p/interface-internal" "^1.3.0" - "@libp2p/peer-id" "^4.2.0" - "@libp2p/utils" "^5.4.5" + "@libp2p/interface" "^1.6.1" + "@libp2p/interface-internal" "^1.3.1" + "@libp2p/peer-id" "^4.2.1" + "@libp2p/utils" "^5.4.6" "@multiformats/multiaddr" "^12.2.3" "@types/multicast-dns" "^7.2.4" dns-packet "^5.6.1" @@ -560,7 +560,7 @@ multiformats "^13.1.0" uint8arrays "^5.1.0" -"@libp2p/peer-record@^7.0.21", "@libp2p/peer-record@^7.0.22": +"@libp2p/peer-record@^7.0.22": version "7.0.22" resolved "https://registry.yarnpkg.com/@libp2p/peer-record/-/peer-record-7.0.22.tgz#d5af9e5208646b5dca2d232618dbd13823dd03cb" integrity sha512-7QnpzENWtuU75E1L9xANmNAoiOMElMR5DZUZdXouvs7Yw0hGq1xI2MzqSH8TYISqjsDvE5SwKod6YQX0vCfoXw== @@ -682,13 +682,13 @@ uint8arraylist "^2.4.8" uint8arrays "^5.1.0" -"@libp2p/websockets@^8.1.1": - version "8.1.1" - resolved "https://registry.yarnpkg.com/@libp2p/websockets/-/websockets-8.1.1.tgz#09d8e7dbdbee41ad0c341d290688c50ca6e3faf7" - integrity sha512-Iwh+bZnDlft5rVEfJCddtUFRYRT2OARUm60UuD10bYndwnJM3f/kMWXPo0EoE3nULpejOS5VCcR6/Ipm3PIUiw== +"@libp2p/websockets@^8.1.2": + version "8.1.2" + resolved "https://registry.yarnpkg.com/@libp2p/websockets/-/websockets-8.1.2.tgz#1cbabca6a9f0a70758f11800e617f3307afa3c67" + integrity sha512-URq0udV+QDRzB31uNKfH/PAte4gdP5pYiZSJZvJ99YZudERTjjjU0+ElqEy3qwm6MX6w1bgf/muld6X0cGWfBg== dependencies: - "@libp2p/interface" "^1.6.0" - "@libp2p/utils" "^5.4.5" + "@libp2p/interface" "^1.6.1" + "@libp2p/utils" "^5.4.6" "@multiformats/mafmt" "^12.1.6" "@multiformats/multiaddr" "^12.2.3" "@multiformats/multiaddr-to-uri" "^10.0.1" @@ -700,15 +700,15 @@ wherearewe "^2.0.1" ws "^8.17.0" -"@libp2p/webtransport@^4.1.1": - version "4.1.1" - resolved "https://registry.yarnpkg.com/@libp2p/webtransport/-/webtransport-4.1.1.tgz#791da4d720f6387e99d2b65c587a9cca132b03b0" - integrity sha512-bissN51kOnSDOazoPfvOTPRr7tOGp8VeqzYbnbynGHbbB+LcgTUGkHzjAdYcbcF/vgz0ITrLYPUQaPaoPHrwag== +"@libp2p/webtransport@^4.1.2": + version "4.1.2" + resolved "https://registry.yarnpkg.com/@libp2p/webtransport/-/webtransport-4.1.2.tgz#c0c4ca0107f19f4fba3afec79cce3132769b771e" + integrity sha512-TAKb+dKxVxA8mNFsqio/gJSkK2jvZbKZECSZnHWMehpzQ8x8FFfGSK5ogy99Y1LhuC206e4YaDY/FocrFgNcfA== dependencies: "@chainsafe/libp2p-noise" "^15.0.0" - "@libp2p/interface" "^1.6.0" - "@libp2p/peer-id" "^4.2.0" - "@libp2p/utils" "^5.4.5" + "@libp2p/interface" "^1.6.1" + "@libp2p/peer-id" "^4.2.1" + "@libp2p/utils" "^5.4.6" "@multiformats/multiaddr" "^12.2.3" "@multiformats/multiaddr-matcher" "^1.2.1" it-stream-types "^2.0.1" @@ -1106,6 +1106,41 @@ resolved "https://registry.yarnpkg.com/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz#db4ecfd499a9765ab24002c3b696d02e6d32a12c" integrity sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA== +"@topology-foundation/crdt@file:packages/crdt": + version "0.0.22" + +"@topology-foundation/network@file:packages/network": + version "0.0.22" + dependencies: + "@chainsafe/libp2p-gossipsub" "^13.1.0" + "@chainsafe/libp2p-noise" "^15.1.0" + "@chainsafe/libp2p-yamux" "^6.0.2" + "@libp2p/autonat" "^1.0.0" + "@libp2p/bootstrap" "^10.1.0" + "@libp2p/circuit-relay-v2" "^1.1.2" + "@libp2p/dcutr" "^1.1.0" + "@libp2p/identify" "^2.0.2" + "@libp2p/interface-pubsub" "^4.0.1" + "@libp2p/mdns" "^10.1.1" + "@libp2p/pubsub-peer-discovery" "^10.0.2" + "@libp2p/webrtc" "^4.1.2" + "@libp2p/websockets" "^8.1.1" + "@libp2p/webtransport" "^4.1.1" + "@multiformats/multiaddr" "^12.3.0" + it-pipe "^3.0.1" + libp2p "^1.8.1" + +"@topology-foundation/node@file:packages/node": + version "0.0.22" + dependencies: + "@topology-foundation/crdt" "0.0.22" + "@topology-foundation/network" "0.0.22" + "@topology-foundation/object" "0.0.22" + commander "^12.1.0" + +"@topology-foundation/object@file:packages/object": + version "0.0.22" + "@tsconfig/node10@^1.0.7": version "1.0.11" resolved "https://registry.yarnpkg.com/@tsconfig/node10/-/node10-1.0.11.tgz#6ee46400685f130e278128c7b38b7e031ff5b2f2"