commit 888f39777668082b24bc691b4c70c2ce1c6353fe Author: savinmax Date: Sat Jun 14 15:07:58 2025 +0200 Initial commit 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 0000000..6f71ef3 Binary files /dev/null and b/bun.lockb differ diff --git a/example/test.ts b/example/test.ts new file mode 100644 index 0000000..d3b13cd --- /dev/null +++ b/example/test.ts @@ -0,0 +1,36 @@ +import { WebSocket } from "ws"; + +let cliId = 1; + +function newClient(room, interval) { + const id = cliId++; + let messageId = 0; + console.log(`Client #${id} room is`, room) + const ws = new WebSocket(`ws://localhost:8080/${room}`); + ws.addEventListener("open", () => { + 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 + ] + } +}