diff --git a/matching-service/package-lock.json b/matching-service/package-lock.json index d30cb99818..bb5229a66c 100644 --- a/matching-service/package-lock.json +++ b/matching-service/package-lock.json @@ -10,7 +10,8 @@ "license": "ISC", "dependencies": { "dotenv": "^16.4.5", - "express": "^4.19.2" + "express": "^4.19.2", + "socket.io": "^4.7.5" }, "devDependencies": { "@types/express": "^4.17.21", @@ -61,6 +62,12 @@ "@jridgewell/sourcemap-codec": "^1.4.10" } }, + "node_modules/@socket.io/component-emitter": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", + "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", + "license": "MIT" + }, "node_modules/@tsconfig/node10": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", @@ -110,6 +117,21 @@ "@types/node": "*" } }, + "node_modules/@types/cookie": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.4.1.tgz", + "integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==", + "license": "MIT" + }, + "node_modules/@types/cors": { + "version": "2.8.17", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz", + "integrity": "sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/express": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz", @@ -154,7 +176,6 @@ "version": "20.16.5", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.5.tgz", "integrity": "sha512-VwYCweNo3ERajwy0IUlqqcyZ8/A7Zwa9ZP3MnENWcB11AejO+tLy3pu850goUW2FC/IJMdZUfKpX/yxL1gymCA==", - "dev": true, "license": "MIT", "dependencies": { "undici-types": "~6.19.2" @@ -270,6 +291,15 @@ "dev": true, "license": "MIT" }, + "node_modules/base64id": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", + "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==", + "license": "MIT", + "engines": { + "node": "^4.5.0 || >= 5.9" + } + }, "node_modules/binary-extensions": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", @@ -427,6 +457,19 @@ "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", "license": "MIT" }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/create-require": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", @@ -516,6 +559,68 @@ "node": ">= 0.8" } }, + "node_modules/engine.io": { + "version": "6.5.5", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.5.5.tgz", + "integrity": "sha512-C5Pn8Wk+1vKBoHghJODM63yk8MvrO9EWZUfkAt5HAqIgPE4/8FF0PEGHXtEd40l223+cE5ABWuPzm38PHFXfMA==", + "license": "MIT", + "dependencies": { + "@types/cookie": "^0.4.1", + "@types/cors": "^2.8.12", + "@types/node": ">=10.0.0", + "accepts": "~1.3.4", + "base64id": "2.0.0", + "cookie": "~0.4.1", + "cors": "~2.8.5", + "debug": "~4.3.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.17.1" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/engine.io-parser": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", + "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/engine.io/node_modules/cookie": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz", + "integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/engine.io/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/engine.io/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, "node_modules/es-define-property": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", @@ -1006,6 +1111,15 @@ "node": ">=0.10.0" } }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/object-inspect": { "version": "1.13.2", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz", @@ -1268,6 +1382,116 @@ "node": ">=10" } }, + "node_modules/socket.io": { + "version": "4.7.5", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.7.5.tgz", + "integrity": "sha512-DmeAkF6cwM9jSfmp6Dr/5/mfMwb5Z5qRrSXLpo3Fq5SqyU8CMF15jIN4ZhfSwu35ksM1qmHZDQ/DK5XTccSTvA==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.4", + "base64id": "~2.0.0", + "cors": "~2.8.5", + "debug": "~4.3.2", + "engine.io": "~6.5.2", + "socket.io-adapter": "~2.5.2", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/socket.io-adapter": { + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.5.tgz", + "integrity": "sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg==", + "license": "MIT", + "dependencies": { + "debug": "~4.3.4", + "ws": "~8.17.1" + } + }, + "node_modules/socket.io-adapter/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io-adapter/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/socket.io-parser": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", + "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-parser/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io-parser/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/socket.io/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, "node_modules/statuses": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", @@ -1404,7 +1628,6 @@ "version": "6.19.8", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", - "dev": true, "license": "MIT" }, "node_modules/unpipe": { @@ -1441,6 +1664,27 @@ "node": ">= 0.8" } }, + "node_modules/ws": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/yn": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", diff --git a/matching-service/package.json b/matching-service/package.json index d87e703f14..b962c78a6f 100644 --- a/matching-service/package.json +++ b/matching-service/package.json @@ -4,23 +4,24 @@ "description": "", "main": "dist/server.js", "scripts": { - "build": "npx tsc", - "start": "node dist/server.js", - "dev": "nodemon src/server.ts", - "test": "echo \"Error: no test specified\" && exit 1" + "build": "npx tsc", + "start": "node dist/server.js", + "dev": "nodemon src/server.ts", + "test": "echo \"Error: no test specified\" && exit 1" }, "keywords": [], "author": "", "license": "ISC", "dependencies": { - "dotenv": "^16.4.5", - "express": "^4.19.2" + "dotenv": "^16.4.5", + "express": "^4.19.2", + "socket.io": "^4.7.5" }, "devDependencies": { - "@types/express": "^4.17.21", - "@types/node": "^20.12.7", - "nodemon": "^3.1.0", - "ts-node": "^10.9.2", - "typescript": "^5.4.5" + "@types/express": "^4.17.21", + "@types/node": "^20.12.7", + "nodemon": "^3.1.0", + "ts-node": "^10.9.2", + "typescript": "^5.4.5" } - } +} diff --git a/matching-service/src/controller/matching-controller.ts b/matching-service/src/controller/matching-controller.ts new file mode 100644 index 0000000000..d76ff2dd6d --- /dev/null +++ b/matching-service/src/controller/matching-controller.ts @@ -0,0 +1,86 @@ +import { Request, Response } from 'express'; +import { + addUserToSearchPool, + getTimeSpentMatching, + getCurrentMatchingUsersCount, + removeUserFromSearchPool, + findUserInPool, + matchUsers +} from '../model/matching-model'; + +export async function registerForMatching(req: Request, res: Response) { + try { + const { userId, criteria } = req.body; + if (userId && criteria) { + addUserToSearchPool(userId, criteria); + return res.status(200).json({ message: 'User registered for matching successfully' }); + } else { + return res.status(400).json({ message: 'User ID or criteria are missing' }); + } + } catch (err) { + console.error(err); + return res.status(500).json({ message: 'Unknown error when registering user for matching' }); + } +} + +export async function getMatchingTime(req: Request, res: Response) { + try { + const { userId } = req.params; + if (!userId) { + return res.status(400).json({ message: 'User ID is missing' }); + } + + const timeSpent = getTimeSpentMatching(userId); + if (timeSpent !== null) { + return res.status(200).json({ timeSpent }); + } else { + return res.status(404).json({ message: `User ${userId} not found` }); + } + } catch (err) { + console.error(err); + return res.status(500).json({ message: 'Unknown error when retrieving matching time' }); + } +} + +export async function getMatchingUsersCount(req: Request, res: Response) { + try { + const count = getCurrentMatchingUsersCount(); + return res.status(200).json({ count }); + } catch (err) { + console.error(err); + return res.status(500).json({ message: 'Unknown error when retrieving matching users count' }); + } +} + +export async function cancelMatching(req: Request, res: Response) { + try { + const { userId } = req.params; + if (!userId) { + return res.status(400).json({ message: 'User ID is missing' }); + } + + const user = removeUserFromSearchPool(userId); + if (user) { + return res.status(200).json({ message: `Cancelled matching for user ${userId}` }); + } else { + return res.status(404).json({ message: `User ${userId} not found` }); + } + } catch (err) { + console.error(err); + return res.status(500).json({ message: 'Unknown error when cancelling matching' }); + } +} + +export async function findMatches(req: Request, res: Response) { + try { + const matches = matchUsers(); + if (matches) { + return res.status(200).json({ matches }); + } else { + return res.status(404).json({ message: 'No matches found' }); + } + } catch (err) { + console.error(err); + return res.status(500).json({ message: 'Unknown error when finding matches' }); + } +} diff --git a/matching-service/src/controller/template-controller.ts b/matching-service/src/controller/template-controller.ts deleted file mode 100644 index 96d0665a75..0000000000 --- a/matching-service/src/controller/template-controller.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { Request, Response } from 'express'; - -export const templateController = { - home: (req: Request, res: Response) => { - res.send('Welcome to Matching Service!'); - }, -}; diff --git a/matching-service/src/index.ts b/matching-service/src/index.ts deleted file mode 100644 index a8683f6c79..0000000000 --- a/matching-service/src/index.ts +++ /dev/null @@ -1,14 +0,0 @@ -import express, { Application } from 'express'; -import router from './routes/template-routes'; -import authMiddleware from './middleware/template-middleware'; - -const app: Application = express(); - -// Middleware -app.use(express.json()); -app.use(authMiddleware); - -// Routes -app.use('/api', router); - -export default app; diff --git a/matching-service/src/middleware/template-middleware.ts b/matching-service/src/middleware/template-middleware.ts deleted file mode 100644 index 465f984c89..0000000000 --- a/matching-service/src/middleware/template-middleware.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { Request, Response, NextFunction } from 'express'; - -const authMiddleware = (req: Request, res: Response, next: NextFunction) => { - console.log('Auth middleware'); - // Implement authentication logic - next(); -}; - -export default authMiddleware; diff --git a/matching-service/src/model/matching-model.ts b/matching-service/src/model/matching-model.ts new file mode 100644 index 0000000000..1fd3fa0a1e --- /dev/null +++ b/matching-service/src/model/matching-model.ts @@ -0,0 +1,68 @@ +interface SearchCriteria { + difficulty: string; + topic: string; +} + +interface UserSearch { + userId: string; + criteria: SearchCriteria; + startTime: Date; +} + +const searchPool: UserSearch[] = []; + +// Add user to the search pool +export function addUserToSearchPool(userId: string, criteria: SearchCriteria) { + const startTime = new Date(); + searchPool.push({ userId, criteria, startTime }); +} + +// Get time spent matching for a specific user +export function getTimeSpentMatching(userId: string): number | null { + const user = searchPool.find(u => u.userId === userId); + if (!user) return null; + + const now = new Date(); + const timeSpent = now.getTime() - user.startTime.getTime(); + return Math.floor(timeSpent / 1000); // Time in seconds +} + +// Get the count of users currently matching +export function getCurrentMatchingUsersCount(): number { + return searchPool.length; +} + +// Find user in the search pool +export function findUserInPool(userId: string): UserSearch | undefined { + return searchPool.find(u => u.userId === userId); +} + +// Remove a user from the search pool +export function removeUserFromSearchPool(userId: string): UserSearch | null { + const index = searchPool.findIndex(u => u.userId === userId); + if (index !== -1) { + return searchPool.splice(index, 1)[0]; + } + return null; +} + +// Perform the matching logic +export function matchUsers() { + for (let i = 0; i < searchPool.length - 1; i++) { + for (let j = i + 1; j < searchPool.length; j++) { + if (isCriteriaMatching(searchPool[i], searchPool[j])) { + const matchedUsers = [searchPool[i], searchPool[j]]; + removeUserFromSearchPool(searchPool[i].userId); + removeUserFromSearchPool(searchPool[j].userId); + return { matchedUsers }; + } + } + } + return null; +} + +// Check if two users have matching criteria +function isCriteriaMatching(user1: UserSearch, user2: UserSearch): boolean { + return user1.criteria.difficulty === user2.criteria.difficulty && + user1.criteria.topic === user2.criteria.topic; +} diff --git a/matching-service/src/model/template-model.ts b/matching-service/src/model/template-model.ts deleted file mode 100644 index 377cecf2c6..0000000000 --- a/matching-service/src/model/template-model.ts +++ /dev/null @@ -1,7 +0,0 @@ -interface User { - id: string; - name: string; - email: string; - } - - export const users: User[] = []; diff --git a/matching-service/src/routes/matching-routes.ts b/matching-service/src/routes/matching-routes.ts new file mode 100644 index 0000000000..ac0d67caef --- /dev/null +++ b/matching-service/src/routes/matching-routes.ts @@ -0,0 +1,11 @@ +import { Router } from 'express'; +import { registerForMatching, getMatchingTime, getMatchingUsersCount } from '../controller/matching-controller'; + +const router = Router(); + +router.get('/', (req, res) => {res.send('Hello from matching service!')}); // Test route + +router.post('/match/register', registerForMatching); // Register for matching +router.get('/match/count', getMatchingUsersCount); // Retrieve the count of users matching + +export const matchingRoutes = router; diff --git a/matching-service/src/routes/template-routes.ts b/matching-service/src/routes/template-routes.ts deleted file mode 100644 index eba37db534..0000000000 --- a/matching-service/src/routes/template-routes.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { Router } from 'express'; -import { templateController } from '../controller/template-controller'; - -const router = Router(); - -router.get('/', templateController.home); - -export default router; diff --git a/matching-service/src/server.ts b/matching-service/src/server.ts index 0bca47a7a1..b47af90f53 100644 --- a/matching-service/src/server.ts +++ b/matching-service/src/server.ts @@ -1,7 +1,35 @@ -import app from './index'; +import express from 'express'; +import http from 'http'; +import { Server as SocketIOServer } from 'socket.io'; +import { matchingRoutes } from './routes/matching-routes'; -const PORT = process.env.PORT || 8002; +const app = express(); +const server = http.createServer(app); +const io = new SocketIOServer(server); -app.listen(PORT, () => { - console.log(`Server is running on port ${PORT}`); +const PORT = process.env.PORT || 3000; + +// Middleware for authenticating JWT +// app.use(authMiddleware); + +// Middleware for JSON parsing +app.use(express.json()); + +// Apply routes +app.use('/api', matchingRoutes); + +// Handle socket.io connections +io.on('connection', (socket) => { + console.log('A user connected:', socket.id); + + socket.on('disconnect', () => { + console.log('A user disconnected:', socket.id); + // Handle removing user from search pool on disconnect + }); + + // You can add more event listeners here for matching +}); + +server.listen(PORT, () => { + console.log(`Server running on port ${PORT}`); });