commit e6a8e4795a01867899886fa55cdfdae0b548f1fc Author: Ludwig Lehnert Date: Thu May 8 23:08:22 2025 +0200 initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a14702c --- /dev/null +++ b/.gitignore @@ -0,0 +1,34 @@ +# dependencies (bun install) +node_modules + +# output +out +dist +*.tgz + +# code coverage +coverage +*.lcov + +# logs +logs +_.log +report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json + +# dotenv environment variable files +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# caches +.eslintcache +.cache +*.tsbuildinfo + +# IntelliJ based IDEs +.idea + +# Finder (MacOS) folder config +.DS_Store diff --git a/README.md b/README.md new file mode 100644 index 0000000..58d8cba --- /dev/null +++ b/README.md @@ -0,0 +1,15 @@ +# arca-signaling + +To install dependencies: + +```bash +bun install +``` + +To run: + +```bash +bun run index.ts +``` + +This project was created using `bun init` in bun v1.2.12. [Bun](https://bun.sh) is a fast all-in-one JavaScript runtime. diff --git a/bun.lock b/bun.lock new file mode 100644 index 0000000..f1238dd --- /dev/null +++ b/bun.lock @@ -0,0 +1,36 @@ +{ + "lockfileVersion": 1, + "workspaces": { + "": { + "name": "arca-signaling", + "dependencies": { + "ulid": "^3.0.0", + "ws": "^8.18.2", + }, + "devDependencies": { + "@types/bun": "latest", + "@types/ws": "^8.18.1", + }, + "peerDependencies": { + "typescript": "^5", + }, + }, + }, + "packages": { + "@types/bun": ["@types/bun@1.2.12", "", { "dependencies": { "bun-types": "1.2.12" } }, "sha512-lY/GQTXDGsolT/TiH72p1tuyUORuRrdV7VwOTOjDOt8uTBJQOJc5zz3ufwwDl0VBaoxotSk4LdP0hhjLJ6ypIQ=="], + + "@types/node": ["@types/node@22.15.17", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-wIX2aSZL5FE+MR0JlvF87BNVrtFWf6AE6rxSE9X7OwnVvoyCQjpzSRJ+M87se/4QCkCiebQAqrJ0y6fwIyi7nw=="], + + "@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="], + + "bun-types": ["bun-types@1.2.12", "", { "dependencies": { "@types/node": "*" } }, "sha512-tvWMx5vPqbRXgE8WUZI94iS1xAYs8bkqESR9cxBB1Wi+urvfTrF1uzuDgBHFAdO0+d2lmsbG3HmeKMvUyj6pWA=="], + + "typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="], + + "ulid": ["ulid@3.0.0", "", { "bin": { "ulid": "dist/cli.js" } }, "sha512-yvZYdXInnJve6LdlPIuYmURdS2NP41ZoF4QW7SXwbUKYt53+0eDAySO+rGSvM2O/ciuB/G+8N7GQrZ1mCJpuqw=="], + + "undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], + + "ws": ["ws@8.18.2", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-DMricUmwGZUVr++AEAe2uiVM7UoO9MAVZMDu05UQOaUII0lp+zOzLLU4Xqh/JvTqklB1T4uELaaPBKyjE1r4fQ=="], + } +} diff --git a/index.ts b/index.ts new file mode 100644 index 0000000..0507f7c --- /dev/null +++ b/index.ts @@ -0,0 +1,95 @@ +import { WebSocket, WebSocketServer } from 'ws'; +import { ulid } from 'ulid'; + +const wss = new WebSocketServer({ port: 8080 }); + +const rooms = new Map>(); +const sockets = new Map(); + +wss.on('connection', (ws) => { + let peerId = ulid(); + + const join = (room: string) => { + if (!rooms.has(room)) { + rooms.set(room, new Set([peerId])); + } else { + rooms.get(room)!.add(peerId); + } + }; + + const unjoinAll = () => { + rooms.entries().forEach(([room, peers]) => { + peers.delete(peerId); + if (!peers.size) rooms.delete(room); + }); + }; + + ws.on('message', (message) => { + try { + const text = message.toString('utf8'); + const data = JSON.parse(text); + + if (data.type === 'self') { + ws.send(JSON.stringify({ + type: 'self', + id: peerId.toString(), + })); + + return; + } + + if (data.type === 'join' && typeof data.room === 'string') { + unjoinAll(); + join(data.room); + + const peers = rooms.get(data.room) ?? []; + peers.forEach((peer) => { + sockets.get(peer)!.send(JSON.stringify({ + type: 'peers', + peers: peers, + })); + }); + + return; + } + + if (data.type === 'offer' && typeof data.peer === 'string' && typeof data.offer === 'object') { + const otherWS = sockets.get(data.peer); + if (!otherWS) return; + + otherWS.send(JSON.stringify({ + type: 'offer', + from: peerId, + offer: data.offer, + })); + + return; + } + + if (data.type === 'accept' && typeof data.peer === 'string' && typeof data.counterOffer === 'object') { + const otherWS = sockets.get(data.peer); + if (!otherWS) return; + + otherWS.send(JSON.stringify({ + type: 'accept', + from: peerId, + counterOffer: data.counterOffer, + })); + + return; + } + + throw 0; + } catch (e) { + ws.send(JSON.stringify({ + type: 'error', + message: 'Bad Request', + })); + } + }); + + ws.on('close', () => { + unjoinAll(); + sockets.delete(peerId); + }); +}); diff --git a/package.json b/package.json new file mode 100644 index 0000000..e1bd247 --- /dev/null +++ b/package.json @@ -0,0 +1,17 @@ +{ + "name": "arca-signaling", + "module": "index.ts", + "type": "module", + "private": true, + "devDependencies": { + "@types/bun": "latest", + "@types/ws": "^8.18.1" + }, + "peerDependencies": { + "typescript": "^5" + }, + "dependencies": { + "ulid": "^3.0.0", + "ws": "^8.18.2" + } +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..9c62f74 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,28 @@ +{ + "compilerOptions": { + // Environment setup & latest features + "lib": ["ESNext"], + "target": "ESNext", + "module": "ESNext", + "moduleDetection": "force", + "jsx": "react-jsx", + "allowJs": true, + + // Bundler mode + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "noEmit": true, + + // Best practices + "strict": true, + "skipLibCheck": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedIndexedAccess": true, + + // Some stricter flags (disabled by default) + "noUnusedLocals": false, + "noUnusedParameters": false, + "noPropertyAccessFromIndexSignature": false + } +}