From 888f39777668082b24bc691b4c70c2ce1c6353fe Mon Sep 17 00:00:00 2001 From: savinmax Date: Sat, 14 Jun 2025 15:07:58 +0200 Subject: [PATCH] Initial commit --- .gitignore | 2 ++ bun.lockb | Bin 0 -> 1663 bytes example/test.ts | 36 ++++++++++++++++++++++++++++++++++++ index.ts | 41 +++++++++++++++++++++++++++++++++++++++++ package-lock.json | 30 ++++++++++++++++++++++++++++++ package.json | 15 +++++++++++++++ test/server.test.ts | 41 +++++++++++++++++++++++++++++++++++++++++ tsconfig.json | 21 +++++++++++++++++++++ 8 files changed, 186 insertions(+) create mode 100644 .gitignore create mode 100755 bun.lockb create mode 100644 example/test.ts create mode 100644 index.ts create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 test/server.test.ts create mode 100644 tsconfig.json diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..552f221 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +node_modules/ +*.log diff --git a/bun.lockb b/bun.lockb new file mode 100755 index 0000000000000000000000000000000000000000..6f71ef3516aba46503ab514035a8ffaacb8523ce GIT binary patch literal 1663 zcmY#Z)GsYA(of3F(@)JSQ%EY!;{sycoc!eMw9K4T-L(9o+{6;yG6OCq1_p+<=Eco+ zvF{G8-F5hRjig}Lp8O-rvc!~UpZRTke|?~e)H=#nL>6J&)OY*Hm9rKq@V2&dac9=HGy0Tjx0AgLBJs=kf z0WnM*$ixd^>SBSatbqDq?t#&GHA5vqX238Dk_MORngXSa7*wGp5sc=tDNfBTOD)oKttd$?%1g`%E-A{) zOGo3{DHtMLZ2@<60MxMOP#PGpKo1&X^Iv&tQZcaXOfAtZO3g{E1eK*Fl?AEA86_nJ z#a8 { + ws.addEventListener("message", ev => { + console.log(`[Client#${id}] Received message:`, ev.data.toString()); + }); + const int = setInterval(() => { + console.log(`[Client#${id}] Sending message ${messageId}`); + ws.send(`Message#${messageId++} from Client#${id}`); + }, interval); + ws.addEventListener("close", () => { + console.log(`[Client#${id}] Closed`); + clearInterval(int); + }); + }); + return ws; +} + +const user1 = newClient("foo", 900); +const user2 = newClient("bar", 500); +const user3 = newClient("foo", 400); +const user4 = newClient("bar", 800); + +setTimeout(() => { + user1.close(); + user2.close(); + user3.close(); + user4.close(); +}, 4000); diff --git a/index.ts b/index.ts new file mode 100644 index 0000000..78b04af --- /dev/null +++ b/index.ts @@ -0,0 +1,41 @@ +import {serve, ServerWebSocket} from "bun"; + +interface WSData { + room: string; +} + +const PORT = 8080; +const MAIN_CHANNEL = "central"; + +const msg = (data: Record) => JSON.stringify(data); + +const server = serve({ + fetch(req, server) { + const url = new URL(req.url); + const room = url.pathname.split("/")[1] ?? MAIN_CHANNEL; + if (server.upgrade(req, {data: {room}})) { + return; + } + return new Response("Upgrade failed :(", { status: 500 }); + }, + websocket: { + open(ws: ServerWebSocket) { + const room = ws.data.room ?? MAIN_CHANNEL; + console.log(`${ws.remoteAddress} connected to '${room}' room`); + ws.subscribe(room); + }, + message(ws, message) { + const room = ws.data.room ?? MAIN_CHANNEL; + ws.publish(room, message as unknown as BufferSource); + }, + close(ws) { + const room = ws.data.room ?? MAIN_CHANNEL; + console.log(`${ws.remoteAddress} disconnected from '${room}' room`); + ws.unsubscribe(room); + ws.publish(room, msg({message: "someone left"})); + }, + }, + port: PORT, +}); + +console.log(`Listening on ${server.hostname}:${server.port}`); diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..13c0c32 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,30 @@ +{ + "name": "websocket-relay", + "version": "1.0.0", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "websocket-relay", + "version": "1.0.0", + "license": "ISC", + "devDependencies": { + "bun-types": "^1.0.1" + } + }, + "node_modules/bun-types": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/bun-types/-/bun-types-1.0.1.tgz", + "integrity": "sha512-7NrXqhMIaNKmWn2dSWEQ50znMZqrN/5Z0NBMXvQTRu/+Y1CvoXRznFy0pnqLe024CeZgVdXoEpARNO1JZLAPGw==", + "dev": true + } + }, + "dependencies": { + "bun-types": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/bun-types/-/bun-types-1.0.1.tgz", + "integrity": "sha512-7NrXqhMIaNKmWn2dSWEQ50znMZqrN/5Z0NBMXvQTRu/+Y1CvoXRznFy0pnqLe024CeZgVdXoEpARNO1JZLAPGw==", + "dev": true + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..927e48e --- /dev/null +++ b/package.json @@ -0,0 +1,15 @@ +{ + "name": "websocket-relay", + "version": "1.0.0", + "author": "", + "main": "index.js", + "devDependencies": { + "bun-types": "^1.0.1", + "ws": "^8.18.2" + }, + "description": "", + "license": "ISC", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + } +} diff --git a/test/server.test.ts b/test/server.test.ts new file mode 100644 index 0000000..3fdc8fb --- /dev/null +++ b/test/server.test.ts @@ -0,0 +1,41 @@ +import { afterAll, beforeAll, describe, expect, it } from "bun:test"; +import { spawn, ChildProcessWithoutNullStreams } from "child_process"; +import { WebSocket } from "ws"; + +describe("Test connections", () => { + let process: ChildProcessWithoutNullStreams; + + beforeAll(async () => { + process = spawn("bun", ["run", "./index.ts"]); + await new Promise((resolve, reject) => { + process.on("error", (err) => reject(err)) + process.stdout.on("data", chunk => { + if (chunk.toString().includes("Listening")) { + resolve(); + } + }); + }); + }); + + afterAll(() => { + process.kill(); + }); + + it("test", async () => { + const ws1 = new WebSocket("ws://localhost:8080/"); + const ws2 = new WebSocket("ws://localhost:8080/"); + + await new Promise((resolve, reject) => { + ws1.onerror = reject; + ws2.onerror = reject; + + ws2.onmessage = (data) => { + expect(data.data).toBe("TEST"); + resolve(); + }; + ws1.onopen = () => { + ws1.send("TEST"); + }; + }); + }); +}); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..a601153 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "lib": [ + "ESNext" + ], + "module": "nodenext", + "target": "esnext", + "moduleResolution": "nodenext", + "strict": false, + "downlevelIteration": true, + "skipLibCheck": true, + "jsx": "preserve", + "allowSyntheticDefaultImports": true, + "forceConsistentCasingInFileNames": true, + "allowJs": true, + "outDir": "dist/", + "types": [ + "bun-types" // add Bun global + ] + } +}