diff --git a/.github/workflows/production.yml b/.github/workflows/production.yml index 15df37ad..2ba05463 100644 --- a/.github/workflows/production.yml +++ b/.github/workflows/production.yml @@ -14,16 +14,19 @@ env: ARTIFACT_REPOSITORY_NAME: codeparty-prod-images GKE_CLUSTER: codeparty-g11-prod # Add your cluster name here. GKE_REGION: asia-southeast1 # Add your cluster zone here. - FIREBASE_SERVICE_ACCOUNT_PROD: ${{ secrets.FIREBASE_SERVICE_ACCOUNT_PROD }} - PRISMA_DATABASE_URL_PROD: ${{ secrets.PRISMA_DATABASE_URL_PROD }} - MONGO_ATLAS_URL_PROD: ${{ secrets.MONGO_ATLAS_URL_PROD }} - FRONTEND_FIREBASE_CONFIG_PROD: ${{ secrets.FRONTEND_FIREBASE_CONFIG_PROD }} + FIREBASE_SERVICE_ACCOUNT: ${{ secrets.FIREBASE_SERVICE_ACCOUNT_PROD }} + PRISMA_DATABASE_URL: ${{ secrets.PRISMA_DATABASE_URL_PROD }} + MONGO_ATLAS_URL: ${{ secrets.MONGO_ATLAS_URL_PROD }} + FRONTEND_FIREBASE_CONFIG: ${{ secrets.FRONTEND_FIREBASE_CONFIG_PROD }} jobs: setup-build-publish-deploy: name: Setup, Build, Publish, and Deploy runs-on: ubuntu-latest environment: production + permissions: + contents: 'read' + id-token: 'write' steps: - name: Checkout @@ -52,6 +55,10 @@ jobs: cluster_name: ${{ env.GKE_CLUSTER }} location: ${{ env.GKE_REGION }} + # Install the dependencies such as prisma + - name: Install dependencies with immutable lockfile + run: yarn install --frozen-lockfile + # Apply prisma migrations to production prisma database - name: Apply prisma database migrations run: |- @@ -60,24 +67,26 @@ jobs: # Build the Docker images and push to Google Artifact Repository - name: Build and push Docker images run: |- - chmod u+x ./deployment/build-prod-images.sh - ./deployment/build-prod-images.sh + chmod u+x ./build-prod-images.sh + ./build-prod-images.sh + working-directory: ./deployment # Set the secrets that are used as env variables in the manifest files - name: Set kubectl secrets run: |- kubectl create secret generic firebase-service-account \ - --from-literal=firebase-service-account=$FIREBASE_SERVICE_ACCOUNT_PROD + --from-literal=firebase-service-account=$FIREBASE_SERVICE_ACCOUNT kubectl create secret generic prisma-database-url \ - --from-literal=prisma-database-url=$PRISMA_DATABASE_URL_PROD + --from-literal=prisma-database-url=$PRISMA_DATABASE_URL kubectl create secret generic mongo-atlas-url \ - --from-literal=mongo-atlas-url=$MONGO_ATLAS_URL_PROD + --from-literal=mongo-atlas-url=$MONGO_ATLAS_URL kubectl create secret generic frontend-firebase-config \ - --from-literal=frontend-firebase-config=$FRONTEND_FIREBASE_CONFIG_PROD + --from-literal=frontend-firebase-config=$FRONTEND_FIREBASE_CONFIG # Deploy the Docker images to the GKE cluster - name: Deploy production application run: |- - kubectl apply -f ./deployment/gke-prod-manifests + kubectl apply -f ./gke-prod-manifests kubectl rollout status deployment kubectl get services -o wide + working-directory: ./deployment diff --git a/deployment/build-prod-images.sh b/deployment/build-prod-images.sh index d0f88300..73f557da 100644 --- a/deployment/build-prod-images.sh +++ b/deployment/build-prod-images.sh @@ -1,13 +1,13 @@ -# Build root docker image -docker build -t peerprep-base -f ../Dockerfile . +# Build root docker image with context set to be parent directory +docker build -t peerprep-base -f ../Dockerfile .. # Create array of services service_array=("admin-service" "collaboration-service" "gateway" "matching-service" "question-service" "user-service" "frontend") -# Build and publish prod images +# Build and publish prod images with context set to be parent directory for s in ${service_array[@]}; do docker build \ --tag asia-southeast1-docker.pkg.dev/$PROJECT_ID/$ARTIFACT_REPOSITORY_NAME/${service_array[s]}:latest \ - --file prod-dockerfiles/Dockerfile.${service_array[s]}-prod . + --file prod-dockerfiles/Dockerfile.${service_array[s]}-prod .. docker push asia-southeast1-docker.pkg.dev/$PROJECT_ID/$ARTIFACT_REPOSITORY_NAME/${service_array[s]}:latest done diff --git a/frontend/providers/MatchmakingProvider.tsx b/frontend/providers/MatchmakingProvider.tsx new file mode 100644 index 00000000..cae79f38 --- /dev/null +++ b/frontend/providers/MatchmakingProvider.tsx @@ -0,0 +1,148 @@ +import React, { + createContext, + useContext, + useState, + useEffect, + useCallback, +} from "react"; +import { io, Socket } from "socket.io-client"; +import { Match } from "@prisma/client"; +import { AuthContext } from "@/contexts/AuthContext"; + +const SERVER_URL = "http://localhost:5002"; + +interface MatchmakingContextValue { + socket: Socket | null; + match: Match | null; + message: string; + error: string; + joinQueue: (difficulties: string[], programmingLang: string) => void; + sendMessage: (message: string) => void; + leaveMatch: () => void; + cancelLooking: () => void; +} + +export const MatchmakingContext = createContext< + MatchmakingContextValue | undefined +>(undefined); + +interface MatchmakingProviderProps { + children: React.ReactNode; +} + +export const MatchmakingProvider: React.FC = ({ + children, +}) => { + const [socket, setSocket] = useState(null); + const [match, setMatch] = useState(null); + const [message, setMessage] = useState(""); + const [error, setError] = useState(""); + + const { user: currentUser, authIsReady } = useContext(AuthContext); + + const generateRandomNumber = () => { + // Return a random number either 0 or 1 as a string + return Math.floor(Math.random() * 2).toString(); + }; + + // Initialize socket connection + useEffect(() => { + if (currentUser) { + const newSocket = io(SERVER_URL, { + autoConnect: false, + // query: { username: currentUser?.email }, + query: { username: generateRandomNumber() }, + }); + setSocket(newSocket); + newSocket.connect(); + + console.log("Socket connected"); + + return () => { + newSocket.close(); + }; + } + }, [currentUser]); + + useEffect(() => { + if (!socket) return; + + socket.on("connect", () => { + console.log("Connected to server"); + }); + + socket.on("matchFound", (match: Match) => { + console.log("Match found:", match); + setMatch(match); + }); + + socket.on("receiveMessage", (message: string) => { + console.log("Message received:", message); + setMessage(message); + }); + + socket.on("error", (error: string) => { + console.error("An error occurred:", error); + setError(error); + }); + + socket.on("disconnect", () => { + console.log("Disconnected from server"); + }); + + return () => { + socket.off("connect"); + socket.off("matchFound"); + socket.off("receiveMessage"); + socket.off("error"); + socket.off("disconnect"); + }; + }, [socket]); + + const joinQueue = useCallback( + (difficulties: string[], programmingLang: string) => { + if (!socket) return; + + socket.emit("lookingForMatch", difficulties, programmingLang); + }, + [socket] + ); + + const sendMessage = useCallback( + (message: string) => { + if (!socket) return; + + socket.emit("sendMessage", message); + }, + [socket] + ); + + const leaveMatch = useCallback(() => { + if (!socket) return; + + socket.emit("leaveMatch"); + }, [socket]); + + const cancelLooking = useCallback(() => { + if (!socket) return; + + socket.emit("cancelLooking"); + }, [socket]); + + const value = { + socket, + match, + message, + error, + joinQueue, + sendMessage, + leaveMatch, + cancelLooking, + }; + + return ( + + {children} + + ); +}; diff --git a/frontend/src/components/interviews/InterviewsLayout.tsx b/frontend/src/components/interviews/InterviewsLayout.tsx new file mode 100644 index 00000000..f55ba545 --- /dev/null +++ b/frontend/src/components/interviews/InterviewsLayout.tsx @@ -0,0 +1,12 @@ +import React from "react"; +import { MatchmakingProvider } from "../../../providers/MatchmakingProvider"; + +interface InterviewsLayoutProps { + children: React.ReactNode; +} + +const InterviewsLayout: React.FC = ({ children }) => { + return {children}; +}; + +export default InterviewsLayout; diff --git a/frontend/src/hooks/useMatchmaking.tsx b/frontend/src/hooks/useMatchmaking.tsx new file mode 100644 index 00000000..b5f252dc --- /dev/null +++ b/frontend/src/hooks/useMatchmaking.tsx @@ -0,0 +1,10 @@ +import { useContext } from "react"; +import { MatchmakingContext } from "../../providers/MatchmakingProvider"; + +export function useMatchmaking() { + const context = useContext(MatchmakingContext); + if (!context) { + throw new Error("useMatchmaking must be used within a MatchmakingProvider"); + } + return context; +} diff --git a/frontend/src/pages/_app.tsx b/frontend/src/pages/_app.tsx index 734350a9..84133eb8 100644 --- a/frontend/src/pages/_app.tsx +++ b/frontend/src/pages/_app.tsx @@ -1,14 +1,15 @@ -import '@/styles/globals.scss' -import type { AppProps } from 'next/app' -import Layout from '../components/common/layout' -import { Noto_Sans } from 'next/font/google' +import "@/styles/globals.scss"; +import type { AppProps } from "next/app"; +import Layout from "../components/common/layout"; +import { Noto_Sans } from "next/font/google"; import AuthContextProvider from "@/contexts/AuthContext"; -import AuthChecker from '@/components/common/auth-checker'; +import { MatchmakingProvider } from "../../providers/MatchmakingProvider"; +import AuthChecker from "@/components/common/auth-checker"; const notoSans = Noto_Sans({ - weight: ['400', '500', '600', '700', '800', '900'], - preload: false -}) + weight: ["400", "500", "600", "700", "800", "900"], + preload: false, +}); export default function App({ Component, pageProps }: AppProps) { return ( @@ -21,12 +22,14 @@ export default function App({ Component, pageProps }: AppProps) {
- - - + + + + +
- ) + ); } diff --git a/frontend/src/pages/interviews/[id]/find-match.tsx b/frontend/src/pages/interviews/[id]/find-match.tsx deleted file mode 100644 index 38aa2200..00000000 --- a/frontend/src/pages/interviews/[id]/find-match.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import Loader from "@/components/interviews/loader"; -import { Button } from "@/components/ui/button"; -import { TypographyBody, TypographyH2 } from "@/components/ui/typography"; -import Link from "next/link"; -import { useRouter } from "next/router"; -import { useEffect } from "react"; - -export default function FindMatch() { - const router = useRouter(); - - useEffect(() => { - setTimeout(() => { - router.push("/interviews/1/match-found"); - }, 5000) - - }, []); - - return ( -
-
- - Finding a match for your interview prep... - - - - Estimated time: 25 secs - -
- - - - - - -
- ) -} diff --git a/frontend/src/pages/interviews/[id]/match-found.tsx b/frontend/src/pages/interviews/[id]/match-found.tsx deleted file mode 100644 index 45212569..00000000 --- a/frontend/src/pages/interviews/[id]/match-found.tsx +++ /dev/null @@ -1,52 +0,0 @@ -import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; -import { Button } from "@/components/ui/button"; -import { Card } from "@/components/ui/card"; -import { TypographyCode, TypographyH2, TypographyH3 } from "@/components/ui/typography"; -import Link from "next/link"; - -type UserInfo = { - name: string - username: string - avatar: string -} - -const defaultUser: UserInfo = { - name: "John Doe", - username: "johndoe", - avatar: "https://github.com/shadcn.png" -} - -export default function MatchFound() { - return ( -
- - Match Found! - - - -
- - - {defaultUser.name.charAt(0).toUpperCase()} - -
- {defaultUser?.name} - @{defaultUser?.username} -
-
-
- -
- - - - - - - - - -
-
- ) -} diff --git a/frontend/src/pages/interviews/find-match.tsx b/frontend/src/pages/interviews/find-match.tsx new file mode 100644 index 00000000..e35303d5 --- /dev/null +++ b/frontend/src/pages/interviews/find-match.tsx @@ -0,0 +1,40 @@ +import Loader from "@/components/interviews/loader"; +import { Button } from "@/components/ui/button"; +import { TypographyBody, TypographyH2 } from "@/components/ui/typography"; +import Link from "next/link"; +import { useRouter } from "next/router"; +import { useEffect } from "react"; +import InterviewsLayout from "@/components/interviews/InterviewsLayout"; +import { useMatchmaking } from "@/hooks/useMatchmaking"; + +export default function FindMatch() { + const router = useRouter(); + const { match, cancelLooking } = useMatchmaking(); + + const onClickCancel = () => { + cancelLooking(); + router.push("/interviews"); + }; + + useEffect(() => { + if (match) { + router.push("/interviews/match-found"); + } + }, [match, router]); + + return ( +
+
+ Finding a match for your interview prep... + + Estimated time: 25 secs +
+ + + + +
+ ); +} diff --git a/frontend/src/pages/interviews/index.tsx b/frontend/src/pages/interviews/index.tsx index ba102560..7e12f6bd 100644 --- a/frontend/src/pages/interviews/index.tsx +++ b/frontend/src/pages/interviews/index.tsx @@ -1,6 +1,5 @@ -import { TypographyBodyHeavy, TypographyH1, TypographyH2, TypographySmall } from '@/components/ui/typography' -import Link from 'next/link' -import { Button } from '@/components/ui/button' +import DifficultySelector from "@/components/common/difficulty-selector"; +import { Button } from "@/components/ui/button"; import { Command, CommandEmpty, @@ -13,15 +12,19 @@ import { PopoverContent, PopoverTrigger, } from "@/components/ui/popover"; -import { cn } from "@/lib/utils"; import { - Check, - ChevronsUpDown, -} from "lucide-react"; -import { useState } from 'react'; -import DifficultySelector from '@/components/common/difficulty-selector'; + TypographyBodyHeavy, + TypographyH1, + TypographyH2, + TypographySmall, +} from "@/components/ui/typography"; +import { useMatchmaking } from "@/hooks/useMatchmaking"; +import { cn } from "@/lib/utils"; +import { Check, ChevronsUpDown } from "lucide-react"; +import { useRouter } from "next/router"; +import { useState } from "react"; -type Difficulty = 'easy' | 'medium' | 'hard' | 'any'; +type Difficulty = "easy" | "medium" | "hard" | "any"; const frameworks = [ { @@ -51,9 +54,21 @@ export default function Interviews() { const [value, setValue] = useState(""); const [difficulty, setDifficulty] = useState("medium"); - return ( -
+ const router = useRouter(); + const { joinQueue } = useMatchmaking(); + const onClickSearch = () => { + try { + joinQueue([difficulty], "python"); + console.log("Joined queue"); + router.push("/interviews/find-match"); + } catch (error) { + console.error(error); + } + }; + + return ( +
Interviews @@ -62,13 +77,15 @@ export default function Interviews() { Try out mock interviews with your peers! -
- - Quick Match - +
+ Quick Match
Choose question difficulty - setDifficulty(value)} showAny={true} defaultValue={difficulty} /> + setDifficulty(value)} + showAny={true} + defaultValue={difficulty} + />
@@ -84,7 +101,7 @@ export default function Interviews() { > {value ? frameworks.find((framework) => framework.value === value) - ?.label + ?.label : "Select framework..."} @@ -121,7 +138,9 @@ export default function Interviews() {
- +
- ) + ); } diff --git a/frontend/src/pages/interviews/match-found.tsx b/frontend/src/pages/interviews/match-found.tsx new file mode 100644 index 00000000..0f1b7614 --- /dev/null +++ b/frontend/src/pages/interviews/match-found.tsx @@ -0,0 +1,76 @@ +import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; +import { Button } from "@/components/ui/button"; +import { Card } from "@/components/ui/card"; +import { + TypographyCode, + TypographyH2, + TypographyH3, +} from "@/components/ui/typography"; +import { useMatchmaking } from "@/hooks/useMatchmaking"; +import Link from "next/link"; +import { useRouter } from "next/router"; + +type UserInfo = { + name: string; + username: string; + avatar: string; +}; + +const defaultUser: UserInfo = { + name: "John Doe", + username: "johndoe", + avatar: "https://github.com/shadcn.png", +}; + +export default function MatchFound() { + const router = useRouter(); + const { match, leaveMatch, joinQueue, cancelLooking } = useMatchmaking(); + + const onClickCancel = () => { + leaveMatch(); + router.push("/interviews"); + }; + + const onClickRetry = () => { + cancelLooking(); + joinQueue(["easy", "medium", "hard"], "python"); + router.push("/interviews/find-match"); + }; + + const onClickAccept = () => { + router.push(`/room/${match?.roomId}`); + }; + + return ( +
+ Match Found! + + +
+ + + + {defaultUser.name.charAt(0).toUpperCase()} + + +
+ {defaultUser?.name} + @{defaultUser?.username} +
+
+
+ +
+ + + +
+
+ ); +} diff --git a/frontend/src/pages/interviews/[id]/match-not-found.tsx b/frontend/src/pages/interviews/match-not-found.tsx similarity index 100% rename from frontend/src/pages/interviews/[id]/match-not-found.tsx rename to frontend/src/pages/interviews/match-not-found.tsx diff --git a/package.json b/package.json index 86c2f133..2f61bfb1 100644 --- a/package.json +++ b/package.json @@ -23,14 +23,14 @@ "eslint": "^8.49.0", "firebase-tools": "^12.6.1", "nodemon": "^3.0.1", - "prisma": "^5.3.1", + "prisma": "^5.4.2", "shx": "^0.3.4", "swagger-autogen": "^2.23.6", "swagger-express-ts": "^1.1.0", "swagger-ui-express": "^5.0.0" }, "dependencies": { - "@prisma/client": "^5.3.1", + "@prisma/client": "^5.4.2", "dotenv-cli": "^7.3.0" } } diff --git a/services/matching-service/src/app.ts b/services/matching-service/src/app.ts index 995179bc..b1650048 100644 --- a/services/matching-service/src/app.ts +++ b/services/matching-service/src/app.ts @@ -10,6 +10,7 @@ import { import { handleCancelLooking } from "./controllers/matchingController"; import { handleLeaveMatch } from "./controllers/matchingController"; import { handleSendMessage } from "./controllers/matchingController"; +import cors from "cors"; import swaggerUi from "swagger-ui-express"; import swaggerFile from "../swagger-output.json"; @@ -17,12 +18,20 @@ const app = express(); const port = process.env.PORT || 5002; app.use(express.json()); +app.use(cors()); app.use(logger("dev")); app.use("/api/matching-service", matchingRoutes); app.use("/docs", swaggerUi.serve, swaggerUi.setup(swaggerFile)); +const socketIoOptions: any = { + cors: { + origin: "http://localhost:3000", + methods: ["GET", "POST"], + }, +}; + const httpServer = require("http").createServer(app); -export const io = new Server(httpServer); +export const io = new Server(httpServer, socketIoOptions); app.set("io", io); diff --git a/services/matching-service/src/controllers/matchingController.ts b/services/matching-service/src/controllers/matchingController.ts index f2571b13..4d88c4f1 100644 --- a/services/matching-service/src/controllers/matchingController.ts +++ b/services/matching-service/src/controllers/matchingController.ts @@ -13,88 +13,117 @@ export type UserMatchReq = { programmingLang: string; }; -export const userQueuesByProgrammingLanguage: { [language: string]: UserMatchReq[]; } = { - "python": [], - "java": [], - "cpp": [] +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 || ""; + 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" + 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.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."); - }); + 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); + let timer = setTimeout(() => {}, 0); return { userId, userMatchReq, timer }; } -export function handleDisconnect(socket: Socket, timer: NodeJS.Timeout, userId: string, userMatchReq: UserMatchReq) { +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); + 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."); - }); - + 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 { +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}`); @@ -108,25 +137,30 @@ export function handleLooking(socket: Socket, userId: string, userMatchReq: User } 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; - }); + 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}`); + 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); @@ -136,43 +170,54 @@ export function handleLooking(socket: Socket, userId: string, userMatchReq: User userMatchReq.difficulties = difficulties; userMatchReq.programmingLang = programmingLang; - console.log(`User ${userId} is looking for a match with difficulties ${difficulties} and programming language ${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 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)); + const difficulty = matchedUser?.difficulties.find((v) => + difficulties.includes(v) + ); if (matchId) { - console.log(`Match found for user ${userId} with user ${matchId} and difficulty ${difficulty}`); + 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 }, + 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; }) - ]).catch(err => { - console.log(err); - socket.emit("error", "An error occurred in lookingForMatch."); - hasError = true; - }).then(res => { - return res && res[0]; - }); + .then((res) => { + return res && res[0]; + }); if (hasError || !newMatch) { return; } @@ -180,18 +225,29 @@ export function handleLooking(socket: Socket, userId: string, userMatchReq: User 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); + 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 }); + 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}`); + 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); @@ -199,7 +255,10 @@ export function handleLooking(socket: Socket, userId: string, userMatchReq: User timer = setTimeout(() => { if (waitingUsers.has(userId)) { console.log(`No match found for user ${userId} yet.`); - userQueuesByProgrammingLanguage[programmingLang] = userQueuesByProgrammingLanguage[programmingLang].filter(user => user.userId !== userId); + userQueuesByProgrammingLanguage[programmingLang] = + userQueuesByProgrammingLanguage[programmingLang].filter( + (user) => user.userId !== userId + ); waitingUsers.delete(userId); socket.emit("matchNotFound"); } @@ -208,62 +267,79 @@ export function handleLooking(socket: Socket, userId: string, userMatchReq: User } }; } -export function handleCancelLooking(userId: string, timer: NodeJS.Timeout, userMatchReq: UserMatchReq): (...args: any[]) => void { +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); + userQueuesByProgrammingLanguage[userMatchReq.programmingLang] = + userQueuesByProgrammingLanguage[userMatchReq.programmingLang].filter( + (user) => user.userId !== userId + ); waitingUsers.delete(userId); }; } -export function handleLeaveMatch(userId: string, socket: Socket): (...args: any[]) => void { +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."); - }); + 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`); + 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( - { + await prisma + .$transaction([ + prisma.user.update({ + where: { id: userId }, + data: { matchedUserId: null }, + }), + prisma.user.update({ where: { - roomId: match?.roomId - } - } - ) - ]).catch(err => { - console.log(err); - socket.emit("error", "An error occurred in leaveMatch."); - }); + 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 { +export function handleSendMessage( + userId: string, + socket: Socket +): (...args: any[]) => void { return async (message: string) => { if (!userId || !message) { console.log(`Invalid request from user ${userId}`); @@ -273,32 +349,28 @@ export function handleSendMessage(userId: string, socket: Socket): (...args: any 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; - }); + 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; + 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 - ); + 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.`); @@ -307,14 +379,13 @@ export function handleSendMessage(userId: string, socket: Socket): (...args: any }; } - export const findMatch = async (req: Request, res: Response) => { const io: Server = req.app.get("io"); const userId = req.params.userId; const difficulties = req.body.difficulties || ["easy", "medium", "hard"]; const programming_language = req.body.programming_language || "python"; - + const user = await prisma.user.findUnique({ where: { id: userId }, }); @@ -450,6 +521,4 @@ export const leaveMatch = async (req: Request, res: Response) => { }); res.status(200).json({ message: "Successfully left the match" }); - }; - diff --git a/yarn.lock b/yarn.lock index 4d7161b3..0312b84a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2089,22 +2089,22 @@ "@pnpm/network.ca-file" "^1.0.1" config-chain "^1.1.11" -"@prisma/client@^5.3.1": - version "5.4.1" - resolved "https://registry.yarnpkg.com/@prisma/client/-/client-5.4.1.tgz#f891d1bc2739d303afaf4406b673f57bf84b226c" - integrity sha512-xyD0DJ3gRNfLbPsC+YfMBBuLJtZKQfy1OD2qU/PZg+HKrr7SO+09174LMeTlWP0YF2wca9LxtVd4HnAiB5ketQ== +"@prisma/client@^5.4.2": + version "5.4.2" + resolved "https://registry.yarnpkg.com/@prisma/client/-/client-5.4.2.tgz#786f9c1d8f06d955933004ac638d14da4bf14025" + integrity sha512-2xsPaz4EaMKj1WS9iW6MlPhmbqtBsXAOeVttSePp8vTFTtvzh2hZbDgswwBdSCgPzmmwF+tLB259QzggvCmJqA== dependencies: - "@prisma/engines-version" "5.4.1-1.2f302df92bd8945e20ad4595a73def5b96afa54f" + "@prisma/engines-version" "5.4.1-2.ac9d7041ed77bcc8a8dbd2ab6616b39013829574" -"@prisma/engines-version@5.4.1-1.2f302df92bd8945e20ad4595a73def5b96afa54f": - version "5.4.1-1.2f302df92bd8945e20ad4595a73def5b96afa54f" - resolved "https://registry.yarnpkg.com/@prisma/engines-version/-/engines-version-5.4.1-1.2f302df92bd8945e20ad4595a73def5b96afa54f.tgz#347534906f2a9d6fcf02aeeb911103c5ebfe4384" - integrity sha512-+nUQM/y8C+1GG5Ioeqcu6itFslCfxvQSAUVSMC9XM2G2Fcq0F4Afnp6m0pXF6X6iUBWen7jZBPmM9Qlq4Nr3/A== +"@prisma/engines-version@5.4.1-2.ac9d7041ed77bcc8a8dbd2ab6616b39013829574": + version "5.4.1-2.ac9d7041ed77bcc8a8dbd2ab6616b39013829574" + resolved "https://registry.yarnpkg.com/@prisma/engines-version/-/engines-version-5.4.1-2.ac9d7041ed77bcc8a8dbd2ab6616b39013829574.tgz#ff14f2926890edee47e8f1d08df7b4f392ee34bf" + integrity sha512-wvupDL4AA1vf4TQNANg7kR7y98ITqPsk6aacfBxZKtrJKRIsWjURHkZCGcQliHdqCiW/hGreO6d6ZuSv9MhdAA== -"@prisma/engines@5.4.1": - version "5.4.1" - resolved "https://registry.yarnpkg.com/@prisma/engines/-/engines-5.4.1.tgz#80b854c87ba30d02ce8df1578cc955ac1c97216a" - integrity sha512-vJTdY4la/5V3N7SFvWRmSMUh4mIQnyb/MNoDjzVbh9iLmEC+uEykj/1GPviVsorvfz7DbYSQC4RiwmlEpTEvGA== +"@prisma/engines@5.4.2": + version "5.4.2" + resolved "https://registry.yarnpkg.com/@prisma/engines/-/engines-5.4.2.tgz#ba2b7faeb227c76e423e88f962afe6a031319f3f" + integrity sha512-fqeucJ3LH0e1eyFdT0zRx+oETLancu5+n4lhiYECyEz6H2RDskPJHJYHkVc0LhkU4Uv7fuEnppKU3nVKNzMh8g== "@protobufjs/aspromise@^1.1.1", "@protobufjs/aspromise@^1.1.2": version "1.1.2" @@ -9637,12 +9637,12 @@ pretty-ms@^7.0.1: dependencies: parse-ms "^2.1.0" -prisma@^5.3.1: - version "5.4.1" - resolved "https://registry.yarnpkg.com/prisma/-/prisma-5.4.1.tgz#2abbd2ae446519654330d67acd81e7bafbac67a2" - integrity sha512-op9PmU8Bcw5dNAas82wBYTG0yHnpq9/O3bhxbDBrNzwZTwBqsVCxxYRLf6wHNh9HVaDGhgjjHlu1+BcW8qdnBg== +prisma@^5.4.2: + version "5.4.2" + resolved "https://registry.yarnpkg.com/prisma/-/prisma-5.4.2.tgz#7eac9276439ec7073ec697c6c0dfa259d96e955e" + integrity sha512-GDMZwZy7mysB2oXU+angQqJ90iaPFdD0rHaZNkn+dio5NRkGLmMqmXs31//tg/qXT3iB0cTQwnGGQNuirhSTZg== dependencies: - "@prisma/engines" "5.4.1" + "@prisma/engines" "5.4.2" private@~0.1.5: version "0.1.8"