From 67f969c2765c4ac48cff65b751cfbe11916c6d02 Mon Sep 17 00:00:00 2001 From: chunweii <47494777+chunweii@users.noreply.github.com> Date: Sun, 8 Oct 2023 17:20:51 +0800 Subject: [PATCH] Code refactoring --- services/matching-service/src/app.ts | 246 +------------- .../src/controllers/matchingController.ts | 310 +++++++++++++++++- .../matching-service/src/routes/index.html | 4 +- 3 files changed, 325 insertions(+), 235 deletions(-) diff --git a/services/matching-service/src/app.ts b/services/matching-service/src/app.ts index 9843d030..c8bb5b8a 100644 --- a/services/matching-service/src/app.ts +++ b/services/matching-service/src/app.ts @@ -1,11 +1,11 @@ import express from "express"; import logger from "morgan"; import { Server } from "socket.io"; -import { io as ioClient } from "socket.io-client"; import matchingRoutes from "./routes/matchingRoutes"; -import prisma from "./prismaClient"; -import { Match, Prisma } from "@prisma/client"; -import EventEmitter from "events"; +import { handleConnection, handleDisconnect, handleLooking } from "./controllers/matchingController"; +import { handleCancelLooking } from "./controllers/matchingController"; +import { handleLeaveMatch } from "./controllers/matchingController"; +import { handleSendMessage } from "./controllers/matchingController"; const app = express(); const port = process.env.PORT || 5002; @@ -15,249 +15,31 @@ app.use(logger("dev")); app.use("/api/matching-service", matchingRoutes); const httpServer = require("http").createServer(app); -const io = new Server(httpServer); +export const io = new Server(httpServer); app.set("io", io); -const MAX_WAITING_TIME = 60 * 1000; // 60 seconds - -type UserMatchReq = { - userId: string; - difficulties: string[]; - programmingLang: string; -}; - -const userQueuesByProgrammingLanguage: { [language: string]: UserMatchReq[] } = { - "python": [], - "java": [], - "cpp": [] -}; - -const waitingUsers: Map = new Map(); // key: user id, val: Event - io.on("connection", (socket) => { - let userId = socket.handshake.query.username as string || ""; - console.log(`User connected: ${socket.id} and username ${userId}`); - - let userMatchReq: UserMatchReq = { - userId: userId, - difficulties: [], - programmingLang: "python" - }; - - prisma.match.findFirst({ - where: { - OR: [ - { userId1: userId }, - { userId2: userId } - ] - } - }).then(existingMatch => { - if (existingMatch) { - console.log(`User ${userId} is already matched with user ${existingMatch.userId1 === userId ? existingMatch.userId2 : existingMatch.userId1}`); - socket.emit("error", "You are already matched with someone."); - socket.join(existingMatch.roomId); - socket.emit("matchFound", existingMatch); - } - }); - let timer = setTimeout(() => { }, 0); - - socket.on("disconnect", () => { - console.log(`User disconnected: ${socket.id}`); - // Remove user from queue if they disconnect - clearTimeout(timer); - if (waitingUsers.has(userMatchReq.userId)) { - console.log(`User ${userMatchReq.userId} disconnected while waiting for a match`); - userQueuesByProgrammingLanguage[userMatchReq.programmingLang] = userQueuesByProgrammingLanguage[userMatchReq.programmingLang].filter(user => user.userId !== userMatchReq.userId); - waitingUsers.get(userMatchReq.userId)?.removeAllListeners(); - waitingUsers.delete(userMatchReq.userId); - } - // Match should not be cancelled since the user might reconnect - }); - - socket.on("lookingForMatch", async (userId: string, difficulties: string[], programmingLang: string) => { - if (!userId || !difficulties || !programmingLang) { - console.log(`Invalid request from user ${userId}`); - socket.emit("error", "Invalid request"); - return; - } - if (waitingUsers.has(userMatchReq.userId)) { - console.log(`User ${userId} is already in the queue`); - return; - } - if (userMatchReq.userId && userMatchReq.userId !== userId) { - console.log(`Different username. Please log in again.`); - return; - } else if (!userMatchReq.userId) { - userMatchReq.userId = userId; - } - - const existingMatch = await prisma.match.findFirst({ - where: { - OR: [ - { userId1: userId }, - { userId2: userId } - ] - } - }); - - if (existingMatch) { - console.log(`User ${userId} is already matched with user ${existingMatch.userId1 === userId ? existingMatch.userId2 : existingMatch.userId1}`); - socket.emit("error", "You are already matched with someone."); - socket.join(existingMatch.roomId); - socket.emit("matchFound", existingMatch); - return; - } + let { userId, userMatchReq, timer } = handleConnection(socket); - userMatchReq.difficulties = difficulties; - userMatchReq.programmingLang = programmingLang; + socket.on("disconnect", handleDisconnect(socket, timer, userId, userMatchReq)); - console.log(`User ${userId} is looking for a match with difficulties ${difficulties} and programming language ${programmingLang}`); + socket.on("lookingForMatch", handleLooking(socket, userId, userMatchReq, timer)); - // Attempt to find a match for the user - const matchedUser = userQueuesByProgrammingLanguage[programmingLang] - .find((userMatchReq) => userMatchReq.userId !== userId && - userMatchReq.difficulties.filter(v => difficulties.includes(v))); - const matchId = matchedUser?.userId; - const difficulty = matchedUser?.difficulties.find(v => difficulties.includes(v)); + socket.on("cancelLooking", handleCancelLooking(userId, timer, userMatchReq)) - if (matchId) { - console.log(`Match found for user ${userId} with user ${matchId} and difficulty ${difficulty}`); + socket.on("leaveMatch", handleLeaveMatch(userId, socket)); - // Inform both users of the match - const newMatch = await prisma.match.create({ - data: { - userId1: userId, - userId2: matchId, - chosenDifficulty: difficulty || "easy", - chosenProgrammingLanguage: programmingLang - } - }); - waitingUsers.get(matchId)?.emit("matchFound", newMatch); - socket.emit("matchFound", newMatch); - socket.join(newMatch.roomId); - - // Update the database with the matched users (pseudo-code) - await prisma.user.update({ - where: { id: userId }, - data: { matchedUserId: matchId }, - }); - - await prisma.user.update({ - where: { id: matchId }, - data: { matchedUserId: userId }, - }); - - // Remove both users from the queue - userQueuesByProgrammingLanguage[programmingLang] = userQueuesByProgrammingLanguage[programmingLang].filter(user => user.userId !== matchId && user.userId !== userId); - waitingUsers.delete(matchId); - waitingUsers.delete(userId); - - } else { - // Add user to the queue - userQueuesByProgrammingLanguage[programmingLang].push({ userId: userId, difficulties, programmingLang }); - let event = new EventEmitter(); - waitingUsers.set(userId, event); - event.on("matchFound", (match: Match) => { - console.log(`Match found for user ${userId} with user ${match.userId1 === userId ? match.userId2 : match.userId1} and difficulty ${match.chosenDifficulty}`); - socket.join(match.roomId); - socket.emit("matchFound", match); - clearTimeout(timer); - }); - setTimeout(() => { - if (waitingUsers.has(userId)) { - console.log(`No match found for user ${userId} yet.`); - userQueuesByProgrammingLanguage[programmingLang] = userQueuesByProgrammingLanguage[programmingLang].filter(user => user.userId !== userId); - waitingUsers.delete(userId); - socket.emit("matchNotFound"); - } - }, MAX_WAITING_TIME); - console.log(`Queueing user ${userId}.`); - } - }); - - socket.on("cancelLooking", async () => { - console.log(`User ${userMatchReq.userId} is no longer looking for a match`); - clearTimeout(timer); - userQueuesByProgrammingLanguage[userMatchReq.programmingLang] = userQueuesByProgrammingLanguage[userMatchReq.programmingLang].filter(user => user.userId !== userMatchReq.userId); - waitingUsers.delete(userMatchReq.userId); - }) - - socket.on("leaveMatch", async () => { - console.log(`User ${userMatchReq.userId} has left the match`); - - const match = await prisma.match.findFirst({ - where: { - OR: [ - { userId1: userMatchReq.userId }, - { userId2: userMatchReq.userId } - ] - } - }); - - if (match) { - // Notify the matched user - await prisma.match.delete( - { - where: { - roomId: match?.roomId - } - } - ); - const matchingUserId = match?.userId1 === userMatchReq.userId ? match?.userId2 : match?.userId1; - console.log(`Notifying user ${matchingUserId} that user ${userMatchReq.userId} has left the match`); - io.to(match.roomId).emit("matchLeft", match); - - // Update database to remove matchedUserId for both users - await prisma.user.update({ - where: { id: userMatchReq.userId }, - data: { matchedUserId: null }, - }); - await prisma.user.update({ - where: { id: matchingUserId }, - data: { matchedUserId: null }, - }); - } - }); - - socket.on("sendMessage", async (userId: string, message: string) => { - if (!userId || !message) { - console.log(`Invalid request from user ${userId}`); - socket.emit("error", "Invalid request"); - return; - } - console.log(`User ${userId} sent a message: ${message}`); - - const match = await prisma.match.findFirst({ - where: { - OR: [ - { userId1: userId }, - { userId2: userId } - ] - } - }); - - const matchedUser = match?.userId1 === userId ? match?.userId2 : match?.userId1; - - if (matchedUser) { - // Forward the message to the matched user - socket.to(match?.roomId || "").emit( - "receiveMessage", - userId, - message - ); - } else { - // Error handling if the user tries to send a message without a match - console.log(`User ${userId} is not currently matched with anyone.`) - socket.emit("error", "You are not currently matched with anyone."); - } - }); + socket.on("sendMessage", handleSendMessage(userId, socket)); socket.on("matchFound", async (userId: string, matchedUserId: string) => { // todo - in the FE handle this }); + + }); httpServer.listen(port, () => { console.log(`Matching service is running at http://localhost:${port}`); }); + diff --git a/services/matching-service/src/controllers/matchingController.ts b/services/matching-service/src/controllers/matchingController.ts index 037eefb1..f2571b13 100644 --- a/services/matching-service/src/controllers/matchingController.ts +++ b/services/matching-service/src/controllers/matchingController.ts @@ -1,6 +1,312 @@ import { Request, Response } from "express"; -import { Server } from "socket.io"; +import { Server, Socket } from "socket.io"; +import { io } from "../app"; import prisma from "../prismaClient"; +import EventEmitter from "events"; +import { Match } from "@prisma/client"; + +export const MAX_WAITING_TIME = 60 * 1000; // 60 seconds + +export type UserMatchReq = { + userId: string; + difficulties: string[]; + programmingLang: string; +}; + +export const userQueuesByProgrammingLanguage: { [language: string]: UserMatchReq[]; } = { + "python": [], + "java": [], + "cpp": [] +}; + +export const waitingUsers: Map = new Map(); // key: user id, val: Event + +export function handleConnection(socket: Socket) { + let userId = socket.handshake.query.username as string || ""; + console.log(`User connected: ${socket.id} and username ${userId}`); + + let userMatchReq: UserMatchReq = { + userId: userId, + difficulties: [], + programmingLang: "python" + }; + + if (waitingUsers.has(userId)) { + console.log(`User ${userId} is waiting in the queue in another session`); + socket.emit("error", "You are already waiting in the queue in another session.") + socket.disconnect(); + } else { + prisma.match.findFirst({ + where: { + OR: [ + { userId1: userId }, + { userId2: userId } + ] + } + }).then(existingMatch => { + if (existingMatch) { + console.log(`User ${userId} is already matched with user ${existingMatch.userId1 === userId ? existingMatch.userId2 : existingMatch.userId1}`); + socket.emit("error", "You are already matched with someone."); + socket.join(existingMatch.roomId); + socket.emit("matchFound", existingMatch); + } + }).catch(err => { + console.log(err); + socket.emit("error", "An error occurred."); + }); + } + + + let timer = setTimeout(() => { }, 0); + return { userId, userMatchReq, timer }; +} + +export function handleDisconnect(socket: Socket, timer: NodeJS.Timeout, userId: string, userMatchReq: UserMatchReq) { + return () => { + console.log(`User disconnected: ${socket.id}`); + // Remove user from queue if they disconnect + clearTimeout(timer); + if (waitingUsers.has(userId)) { + console.log(`User ${userId} disconnected while waiting for a match`); + userQueuesByProgrammingLanguage[userMatchReq.programmingLang] = userQueuesByProgrammingLanguage[userMatchReq.programmingLang]?.filter(user => user.userId !== userId); + waitingUsers.get(userId)?.removeAllListeners(); + waitingUsers.delete(userId); + } + // Match should not be cancelled since the user might reconnect but we can notify the other user + prisma.match.findFirst({ + where: { + OR: [ + { userId1: userId }, + { userId2: userId } + ] + } + }).then(match => { + if (match) { + const matchingUserId = match?.userId1 === userId ? match?.userId2 : match?.userId1; + console.log(`Notifying user ${matchingUserId} that user ${userId} has disconnected`); + io.to(match?.roomId || "").emit("receiveMessage", "Server", "Your partner has disconnected"); + } + }).catch(err => { + console.log(err); + socket.emit("error", "An error occurred."); + }); + + }; +} + +export function handleLooking(socket: Socket, userId: string, userMatchReq: UserMatchReq, timer: NodeJS.Timeout): (...args: any[]) => void { + return async (difficulties: string[], programmingLang: string) => { + if (!difficulties || !programmingLang) { + console.log(`Invalid request from user ${userId}`); + socket.emit("error", "Invalid request"); + return; + } + if (waitingUsers.has(userId)) { + console.log(`User ${userId} is already in the queue`); + socket.emit("error", "You are already in the queue."); + return; + } + + let hasError = false; + const existingMatch = await prisma.match.findFirst({ + where: { + OR: [ + { userId1: userId }, + { userId2: userId } + ] + } + }).catch(err => { + console.log(err); + socket.emit("error", "An error occurred in lookingForMatch."); + hasError = true; + }); + + if (hasError) { + return; + } + + if (existingMatch) { + console.log(`User ${userId} is already matched with user ${existingMatch.userId1 === userId ? existingMatch.userId2 : existingMatch.userId1}`); + socket.emit("error", "You are already matched with someone."); + socket.join(existingMatch.roomId); + socket.emit("matchFound", existingMatch); + return; + } + + userMatchReq.difficulties = difficulties; + userMatchReq.programmingLang = programmingLang; + + console.log(`User ${userId} is looking for a match with difficulties ${difficulties} and programming language ${programmingLang}`); + + // Attempt to find a match for the user + const matchedUser = userQueuesByProgrammingLanguage[programmingLang] + ?.find((userMatchReq) => userId !== userMatchReq.userId && + userMatchReq.difficulties.find(v => difficulties.includes(v))); + const matchId = matchedUser?.userId; + const difficulty = matchedUser?.difficulties.find(v => difficulties.includes(v)); + + if (matchId) { + console.log(`Match found for user ${userId} with user ${matchId} and difficulty ${difficulty}`); + + // Inform both users of the match + const newMatch = await prisma.$transaction([ + prisma.match.create({ + data: { + userId1: userId, + userId2: matchId, + chosenDifficulty: difficulty || "easy", + chosenProgrammingLanguage: programmingLang + } + }), + prisma.user.update({ + where: { id: userId }, + data: { matchedUserId: matchId }, + }), + prisma.user.update({ + where: { id: matchId }, + data: { matchedUserId: userId }, + }) + ]).catch(err => { + console.log(err); + socket.emit("error", "An error occurred in lookingForMatch."); + hasError = true; + }).then(res => { + return res && res[0]; + }); + if (hasError || !newMatch) { + return; + } + waitingUsers.get(matchId)?.emit("matchFound", newMatch); + socket.emit("matchFound", newMatch); + socket.join(newMatch.roomId); + // Remove both users from the queue + userQueuesByProgrammingLanguage[programmingLang] = userQueuesByProgrammingLanguage[programmingLang].filter(user => user.userId !== matchId && user.userId !== userId); + waitingUsers.delete(matchId); + waitingUsers.delete(userId); + + } else { + // Add user to the queue + userQueuesByProgrammingLanguage[programmingLang] = userQueuesByProgrammingLanguage[programmingLang] || []; + userQueuesByProgrammingLanguage[programmingLang].push({ userId: userId, difficulties, programmingLang }); + let event = new EventEmitter(); + waitingUsers.set(userId, event); + event.on("matchFound", (match: Match) => { + console.log(`Match found for user ${userId} with user ${match.userId1 === userId ? match.userId2 : match.userId1} and difficulty ${match.chosenDifficulty}`); + socket.join(match.roomId); + socket.emit("matchFound", match); + clearTimeout(timer); + }); + timer = setTimeout(() => { + if (waitingUsers.has(userId)) { + console.log(`No match found for user ${userId} yet.`); + userQueuesByProgrammingLanguage[programmingLang] = userQueuesByProgrammingLanguage[programmingLang].filter(user => user.userId !== userId); + waitingUsers.delete(userId); + socket.emit("matchNotFound"); + } + }, MAX_WAITING_TIME); + console.log(`Queueing user ${userId}.`); + } + }; +} +export function handleCancelLooking(userId: string, timer: NodeJS.Timeout, userMatchReq: UserMatchReq): (...args: any[]) => void { + return async () => { + console.log(`User ${userId} is no longer looking for a match`); + clearTimeout(timer); + userQueuesByProgrammingLanguage[userMatchReq.programmingLang] = userQueuesByProgrammingLanguage[userMatchReq.programmingLang].filter(user => user.userId !== userId); + waitingUsers.delete(userId); + }; +} + +export function handleLeaveMatch(userId: string, socket: Socket): (...args: any[]) => void { + return async () => { + console.log(`User ${userId} has left the match`); + + const match = await prisma.match.findFirst({ + where: { + OR: [ + { userId1: userId }, + { userId2: userId } + ] + } + }).catch(err => { + console.log(err); + socket.emit("error", "An error occurred in leaveMatch."); + }); + + if (match) { + // Notify the matched user + const matchingUserId = match?.userId1 === userId ? match?.userId2 : match?.userId1; + console.log(`Notifying user ${matchingUserId} that user ${userId} has left the match`); + io.to(match.roomId).emit("matchLeft", match); + + await prisma.$transaction([ + prisma.user.update({ + where: { id: userId }, + data: { matchedUserId: null }, + }), + prisma.user.update({ + where: { id: match.userId1 === userId ? match.userId2 : match.userId1 }, + data: { matchedUserId: null }, + }), + prisma.match.delete( + { + where: { + roomId: match?.roomId + } + } + ) + ]).catch(err => { + console.log(err); + socket.emit("error", "An error occurred in leaveMatch."); + }); + } + }; +} + +export function handleSendMessage(userId: string, socket: Socket): (...args: any[]) => void { + return async (message: string) => { + if (!userId || !message) { + console.log(`Invalid request from user ${userId}`); + socket.emit("error", "Invalid request"); + return; + } + console.log(`User ${userId} sent a message: ${message}`); + + let hasError = false; + const match = await prisma.match.findFirst({ + where: { + OR: [ + { userId1: userId }, + { userId2: userId } + ] + } + }).catch(err => { + console.log(err); + socket.emit("error", "An error occurred in sendMessage."); + hasError = true; + }); + + if (hasError) { + return; + } + + const matchedUser = match?.userId1 === userId ? match?.userId2 : match?.userId1; + + if (matchedUser) { + // Forward the message to the matched user + socket.to(match?.roomId || "").emit( + "receiveMessage", + userId, + message + ); + } else { + // Error handling if the user tries to send a message without a match + console.log(`User ${userId} is not currently matched with anyone.`); + socket.emit("error", "You are not currently matched with anyone."); + } + }; +} + export const findMatch = async (req: Request, res: Response) => { const io: Server = req.app.get("io"); @@ -144,4 +450,6 @@ export const leaveMatch = async (req: Request, res: Response) => { }); res.status(200).json({ message: "Successfully left the match" }); + }; + diff --git a/services/matching-service/src/routes/index.html b/services/matching-service/src/routes/index.html index fb0bf42c..5246debf 100644 --- a/services/matching-service/src/routes/index.html +++ b/services/matching-service/src/routes/index.html @@ -61,7 +61,7 @@

// const username = document.getElementById("username"); const messages = document.getElementById("messages"); look_button.addEventListener("click", () => { - socket.emit("lookingForMatch", username, getSelectValues(difficulties), lang.value); + socket.emit("lookingForMatch",getSelectValues(difficulties), lang.value); look_button.setAttribute("disabled", true); cancel_button.removeAttribute("disabled"); }); @@ -77,7 +77,7 @@

cancel_button.setAttribute("disabled", true); }); send_button.addEventListener("click", () => { - socket.emit("sendMessage", username, messages.value); + socket.emit("sendMessage", messages.value); }); socket.on("matchFound", (data) => {