diff --git a/server/.gitignore b/server/.gitignore new file mode 100644 index 0000000..4c43fe6 --- /dev/null +++ b/server/.gitignore @@ -0,0 +1 @@ +*.js \ No newline at end of file diff --git a/server/package.json b/server/package.json index 7ed8ede..ef43380 100644 --- a/server/package.json +++ b/server/package.json @@ -2,15 +2,26 @@ "name": "websocket-server", "version": "1.0.0", "description": "", - "main": "websocket.js", + "main": "websocket.ts", "scripts": { + "ts": "tsc -p .", + "start": "npm run ts && npm run start:server", + "start:server": "node websocket.js", + "dev": "tsc-watch -p tsconfig.json --onSuccess \"npm run start:server\"", "test": "echo \"Error: no test specified\" && exit 1" }, + "type": "module", "author": "", "license": "ISC", "dependencies": { - "@netlify/functions": "^1.6.0", - "uuid": "^9.0.0", - "ws": "^8.13.0" - } + "@netlify/functions": "^1.6.0", + "@types/body-parser": "^1.19.2", + "@types/express": "^4.17.13", + "@types/ws": "^8.5.4", + "body-parser": "^1.20.0", + "express": "^4.17.3", + "tsc-watch": "^5.0.3", + "typescript": "^4.6.3", + "ws": "^8.12.1" + } } diff --git a/server/tsconfig.json b/server/tsconfig.json new file mode 100644 index 0000000..b7bab42 --- /dev/null +++ b/server/tsconfig.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ES2022", + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "strict": true, + "skipLibCheck": true + } + } \ No newline at end of file diff --git a/server/websocket.ts b/server/websocket.ts new file mode 100644 index 0000000..8be65c0 --- /dev/null +++ b/server/websocket.ts @@ -0,0 +1,105 @@ +import express from 'express'; +import { WebSocketServer, WebSocket } from 'ws'; + +const app = express(); +const port = process.env.PORT || 3000; + +const clients = new Map(); +const estimates = new Map(); + +function onSocketPreError(e: Error) { + console.log(e); +} + +function onSocketPostError(e: Error) { + console.log(e); +} + +console.log(`Attempting to run server on port ${port}`); + +const s = app.listen(port, () => { + console.log(`Listening on port ${port}`); +}); + +const wss = new WebSocketServer({ noServer: true }); + +s.on('upgrade', (req, socket, head) => { + socket.on('error', onSocketPreError); + + // perform auth + if (!!req.headers['BadAuth']) { + socket.write('HTTP/1.1 401 Unauthorized\r\n\r\n'); + socket.destroy(); + return; + } + + wss.handleUpgrade(req, socket, head, (ws) => { + socket.removeListener('error', onSocketPreError); + wss.emit('connection', ws, req); + }); +}); + +wss.on('connection', (ws, req) => { + ws.on('error', onSocketPostError); + + let roomId: string; + + ws.on('message', (message) => { + const data = JSON.parse(message.toString()); + + if (data.type === 'join-room') { + roomId = data.roomId; + clients.set(ws, roomId); + broadcast(roomId, { type: 'user-joined', message: 'A new user has joined the room' }); + } else if (data.type === 'select-estimate') { + const { user, estimate } = data; + + if (!estimates.has(roomId)) { + estimates.set(roomId, new Map()); + } + estimates.get(roomId).set(user, estimate); + + broadcast(roomId, { type: 'estimate-selected', user, estimate }); + + // Check if all users have selected an estimate + if (estimates.get(roomId).size === clientsInRoom(roomId).length) { + broadcast(roomId, { type: 'estimation-closed', message: 'Estimation closed' }); + } + } else if (data.type === 'change-estimate') { + const { user, estimate } = data; + + if (estimates.has(roomId)) { + estimates.get(roomId).set(user, estimate); + broadcast(roomId, { type: 'estimate-changed', user, estimate }); + } + } + }); + + ws.on('close', () => { + const user = clients.get(ws); + clients.delete(ws); + + if (estimates.has(roomId)) { + estimates.get(roomId).delete(user); + + if (estimates.get(roomId).size === 0) { + estimates.delete(roomId); + broadcast(roomId, { type: 'estimation-started', message: 'Estimation started again' }); + } + } + + broadcast(roomId, { type: 'user-left', message: `${user} has left the room` }); + }); +}); + +function broadcast(roomId: string, message: any) { + wss.clients.forEach((client) => { + if (clients.get(client) === roomId && client.readyState === WebSocket.OPEN) { + client.send(JSON.stringify(message)); + } + }); +} + +function clientsInRoom(roomId: string) { + return [...clients.values()].filter((id) => id === roomId); +}