Skip to content

Commit

Permalink
merge main
Browse files Browse the repository at this point in the history
  • Loading branch information
d-roak committed Jul 29, 2024
2 parents ba66917 + 7fed7c6 commit a06c2c8
Show file tree
Hide file tree
Showing 14 changed files with 483 additions and 48 deletions.
14 changes: 14 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
8 changes: 4 additions & 4 deletions examples/canvas/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
4 changes: 4 additions & 0 deletions examples/chat/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
dist/
docs/
node_modules/
public/static
27 changes: 27 additions & 0 deletions examples/chat/README.md
Original file line number Diff line number Diff line change
@@ -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\<string> 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",
...
}
```
33 changes: 33 additions & 0 deletions examples/chat/package.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
51 changes: 51 additions & 0 deletions examples/chat/public/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Topology - Chat</title>
</head>
<body>
<div>
<h1>Topology Protocol - Chat</h1>
<p>Current peer ID <span id="peerId"></span></p>
<p>Connected to <span id="chatId"></span></p>
<p>peers: <span id="peers"></span></p>
<p>discovery_peers: <span id="discoveryPeers"></span></p>
<p>object_peers: <span id="objectPeers"></span></p>

<input id="roomInput" type="text" placeholder="Room ID" />
<button id="joinRoom">Connect</button>
<button id="fetchMessages">Fetch Messages</button>
<button id="createRoom">Create Room</button>
</div>

<div
id="chat"
style="
overflow-y: scroll;
min-height: 200px;
max-height: 60vh;
width: 100%;
"
>
<!-- Messages will appear here -->
</div>

<div style="margin-bottom: 10px">
<input id="messageInput" type="text" placeholder="Message" />
<button id="sendMessage">Send</button>
</div>

<script>
var input = document.getElementById("messageInput");
input.addEventListener("keypress", function (event) {
if (event.key === "Enter") {
document.getElementById("sendMessage").click();
}
});
</script>

<script src="./static/bundle/script.js"></script>
</body>
</html>
31 changes: 31 additions & 0 deletions examples/chat/src/handlers.ts
Original file line number Diff line number Diff line change
@@ -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);
}
}
129 changes: 129 additions & 0 deletions examples/chat/src/index.ts
Original file line number Diff line number Diff line change
@@ -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 = <HTMLDivElement>document.getElementById("peerId");
element_peerId.innerHTML = node.networkNode.peerId;

const element_peers = <HTMLDivElement>document.getElementById("peers");
element_peers.innerHTML = "[" + peers.join(", ") + "]";

const element_discoveryPeers = <HTMLDivElement>document.getElementById("discoveryPeers");
element_discoveryPeers.innerHTML = "[" + discoveryPeers.join(", ") + "]";

const element_objectPeers = <HTMLDivElement>document.getElementById("objectPeers");
element_objectPeers.innerHTML = "[" + objectPeers.join(", ") + "]";

if(!chatCRO) return;
const chat = chatCRO.getMessages();
const element_chat = <HTMLDivElement>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 = <HTMLButtonElement>document.getElementById("createRoom");
button_create.addEventListener("click", () => {
chatCRO = new Chat(node.networkNode.peerId);
node.createObject(chatCRO);
(<HTMLButtonElement>document.getElementById("chatId")).innerHTML = chatCRO.getObjectId();
render();
});

let button_connect = <HTMLButtonElement>document.getElementById("joinRoom");
button_connect.addEventListener("click", async () => {
let input: HTMLInputElement = <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 = <HTMLButtonElement>document.getElementById("fetchMessages");
button_fetch.addEventListener("click", async () => {
let input: HTMLInputElement = <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<string>(arr);
object["chat"] = Object.assign(new GSet<string>(new Set<string>()), object["chat"]);
chatCRO = Object.assign(new Chat(node.networkNode.peerId), object);

(<HTMLButtonElement>document.getElementById("chatId")).innerHTML = objectId;
render();
} catch (e) {
console.error("Error while connecting to the CRO ", objectId, e);
}
});

let button_send = <HTMLButtonElement>document.getElementById("sendMessage");
button_send.addEventListener("click", async () => {
let input: HTMLInputElement = <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 = <HTMLDivElement>document.getElementById("chat");
element_chat.scrollTop = element_chat.scrollHeight;
});
}

main();
32 changes: 32 additions & 0 deletions examples/chat/src/objects/chat.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { TopologyObject } from "@topology-foundation/object";
import { GSet } from "@topology-foundation/crdt";

export interface IChat extends TopologyObject {
chat: GSet<string>;
addMessage(timestamp: string, message: string, node_id: string): void;
getMessages(): GSet<string>;
merge(other: Chat): void;
}

export class Chat extends TopologyObject implements IChat {
// store messages as strings in the format (timestamp, message, peerId)
chat: GSet<string>;

constructor(peerId: string) {
super(peerId);
this.chat = new GSet<string>(new Set<string>());
}

addMessage(timestamp: string, message: string, node_id: string): void {
this.chat.add(`(${timestamp}, ${message}, ${node_id})`);
}

getMessages(): GSet<string> {
return this.chat;
}

merge(other: Chat): void {
this.chat.merge(other.chat);
}

}
14 changes: 14 additions & 0 deletions examples/chat/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"compilerOptions": {
"target": "es6",
"module": "ESNEXT",
"rootDir": ".",
"strict": true,
"moduleResolution": "node",
"allowSyntheticDefaultImports": true,
"allowJs": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
}
}
Loading

0 comments on commit a06c2c8

Please sign in to comment.