Skip to content

Commit

Permalink
Merge pull request #146 from CS3219-AY2324S1/bryan/convert-matchmakin…
Browse files Browse the repository at this point in the history
…g-to-socketio

Convert matchmaking to use socket.io from ws
  • Loading branch information
bryanlohxz authored Oct 27, 2023
2 parents 124a206 + a457ab5 commit e08f951
Show file tree
Hide file tree
Showing 7 changed files with 228 additions and 132 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/deploy-production.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ jobs:
run: |
export REACT_APP_QUESTIONS_SERVICE_HOST=http://peerprep.bryanlohxz.com/api/questions-service
export REACT_APP_USERS_SERVICE_HOST=http://peerprep.bryanlohxz.com/api/users-service
export REACT_APP_MATCHMAKING_SERVICE_HOST=ws://peerprep.bryanlohxz.com/api/matchmaking-service
export REACT_APP_MATCHMAKING_SERVICE_HOST=http://peerprep.bryanlohxz.com
export REACT_APP_COLLABORATION_SERVICE_HOST=http://peerprep.bryanlohxz.com
export CI=false
yarn build
Expand Down
2 changes: 1 addition & 1 deletion compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ services:
environment:
REACT_APP_QUESTIONS_SERVICE_HOST: http://localhost:3001
REACT_APP_USERS_SERVICE_HOST: http://localhost:3002
REACT_APP_MATCHMAKING_SERVICE_HOST: ws://localhost:3003
REACT_APP_MATCHMAKING_SERVICE_HOST: http://localhost:3003
REACT_APP_COLLABORATION_SERVICE_HOST: http://localhost:3004
volumes:
- ./frontend-service:/app
Expand Down
66 changes: 42 additions & 24 deletions frontend-service/src/pages/MatchmakingFind.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import axios from "axios";
import { useEffect, useState } from "react";
import { useNavigate, useSearchParams } from "react-router-dom";
import { io } from "socket.io-client";

import { useUser } from "../contexts/UserContext";

Expand Down Expand Up @@ -44,6 +45,7 @@ const MatchmakingFind = () => {
const [isMatchError, setIsMatchError] = useState(false);
const [isMatchFinding, setIsMatchFinding] = useState(true);
const [matchedUser, setMatchedUser] = useState(null);
const [roomId, setRoomId] = useState(null);
const isMatchFailed = !isMatchFinding && matchedUser == null;
const isMatchPassed = !isMatchFinding && matchedUser;

Expand All @@ -53,10 +55,10 @@ const MatchmakingFind = () => {
return navigate("/matchmaking");

const token = window.localStorage.getItem("token");
const ws = new WebSocket(
`${process.env.REACT_APP_MATCHMAKING_SERVICE_HOST}?difficulty=${difficulty}`,
token
);
const socket = io(`${process.env.REACT_APP_MATCHMAKING_SERVICE_HOST}`, {
query: { difficulty, token },
path: "/api/matchmaking-service",
});

const clock = setInterval(() => {
setSearchTimeElapsed((prevState) => {
Expand All @@ -67,28 +69,26 @@ const MatchmakingFind = () => {
});
}, 1000);

const closeEventHandler = async (event) => {
let isUserMatched = false;
socket.on("user-matched", async ({ matchedUser, roomId }) => {
isUserMatched = true;
clearInterval(clock);
if (event.code !== 1000) {
console.error(event.reason);
return setIsMatchError(true);
}
const result = JSON.parse(event.reason);
const matchedUserId = result.matchedUser;
const getProfielUrl = `${process.env.REACT_APP_USERS_SERVICE_HOST}/profile/${matchedUserId}`;
const getProfielUrl = `${process.env.REACT_APP_USERS_SERVICE_HOST}/profile/${matchedUser}`;
const response = await axios.get(getProfielUrl, {
headers: { Authorization: token },
});
setMatchedUser(response.data);
setRoomId(roomId);
setIsMatchFinding(false);
};
});

ws.addEventListener("close", closeEventHandler);
socket.on("disconnect", () => {
if (!isUserMatched) setIsMatchError(true);
});

return async () => {
clearInterval(clock);
ws.removeEventListener("close", closeEventHandler);
ws.close(1000, "Client has left the page");
socket.disconnect();
};
}, [navigate, searchParams]);

Expand Down Expand Up @@ -137,15 +137,21 @@ const MatchmakingFind = () => {
alignItems="center"
>
<Chip
label={searchParams.get("difficulty").charAt(0).toUpperCase() +
searchParams.get("difficulty").substring(1)}
color={getDifficultyChipColor(searchParams.get("difficulty").substring(0))}
sx={{ fontSize: '15px', color: "white"}}
/>
label={
searchParams.get("difficulty").charAt(0).toUpperCase() +
searchParams.get("difficulty").substring(1)
}
color={getDifficultyChipColor(
searchParams.get("difficulty").substring(0)
)}
sx={{ fontSize: "15px", color: "white" }}
/>
{!isMatchError && isMatchFinding && (
<>
<CircularProgress variant="indeterminate" />
<Typography marginTop={3}>{searchTimeElapsed + "s"}</Typography>
<Typography marginTop={3}>
{searchTimeElapsed + "s"}
</Typography>
</>
)}
{isMatchFinding && (
Expand All @@ -163,7 +169,18 @@ const MatchmakingFind = () => {
</Button>
)}
{isMatchPassed && (
<Button variant="contained">Go To Question</Button>
<Button
variant="contained"
onClick={() =>
navigate(
`/collaboration?roomId=${roomId}&difficulty=${searchParams.get(
"difficulty"
)}`
)
}
>
Go To Question
</Button>
)}
</Stack>
<Card>
Expand Down Expand Up @@ -267,7 +284,8 @@ const MatchmakingFind = () => {
</Card>
</Stack>
<Typography textAlign="center">
Please hold on for a moment while we are searching for your partner...
Please hold on for a moment while we are searching for your
partner...
</Typography>
</Stack>
</Box>
Expand Down
4 changes: 2 additions & 2 deletions matchmaking-service/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@
},
"dependencies": {
"bull": "^4.11.4",
"morgan": "^1.10.0",
"node-cache": "^5.1.2",
"redis": "^4.6.10",
"ws": "^8.14.2"
"socket.io": "^4.7.2",
"uuid": "^9.0.1"
}
}
3 changes: 2 additions & 1 deletion matchmaking-service/src/cache.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import NodeCache from "node-cache";
import { v4 as uuidv4 } from "uuid";

const cache = new NodeCache();

Expand All @@ -10,7 +11,7 @@ export const findMatchRequestHandler = (userId, difficulty) => {
const matchedUserId = cache.get(difficulty);
if (matchedUserId === userId) return null;
cache.del(difficulty);
return { users: [userId, matchedUserId], difficulty };
return { users: [userId, matchedUserId], difficulty, roomId: uuidv4() };
};

export const cancelFindMatchRequest = (userId, difficulty) => {
Expand Down
86 changes: 42 additions & 44 deletions matchmaking-service/src/index.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { WebSocketServer } from "ws";
import { createServer } from "http";
import { Server } from "socket.io";

import { getUserFromToken } from "./authorization.js";
import {
enqueueCancelFindMatch,
Expand All @@ -7,72 +9,68 @@ import {
unsubscribeMatchResponse,
} from "./redis.js";

const wss = new WebSocketServer({ port: process.env.PORT });
const httpServer = createServer();
const io = new Server(httpServer, {
cors: { origin: "*" },
path: "/api/matchmaking-service",
});

const difficulties = ["easy", "medium", "hard"];

wss.on("connection", async (ws, request) => {
const requestId = Math.floor(Math.random() * 100 + 1);
console.log(`[${requestId}]`, "Connection received.");
try {
console.log(`[${requestId}]`, "Retrieving authorization token.");
const token = request.headers["sec-websocket-protocol"];
if (!token) {
console.error("Token cannot be found in authorization header.");
return ws.close(1008, "Unauthorized");
}
io.on("connection", async (socket) => {
const { id, handshake } = socket;
const { difficulty, token } = handshake.query;
if (!difficulty || !difficulties.includes(difficulty) || !token) {
if (!difficulty) console.error(`[${id}] Difficulty cannot be found.`);
if (!difficulties.includes(difficulty))
console.error(`[${id}] Difficulty found is not recgonized.`);
if (!token) console.error(`[${id}] Token cannot be found.`);
return socket.disconnect();
}

console.log(`[${requestId}]`, "Verifying authorization token.");
console.log(`[${id}]`, "Connection received.");
try {
console.log(`[${id}]`, "Verifying authorization token.");
let user = await getUserFromToken(token);
if (!user) {
console.error("Invalid token found in authorization header.");
return ws.close(1008, "Unauthorized");
}

console.log(`[${requestId}]`, "Retrieving matchmaking difficulty.");
const urlSearchParams = new URLSearchParams(request.url.split("?")[1]);
const difficulty = urlSearchParams.get("difficulty");
console.log(`[${requestId}]`, "Validating matchmaking difficulty.");
if (!difficulty || !difficulties.includes(difficulty.toLowerCase())) {
const errorMsg =
"Difficulty is missing from search params or is incorrect.";
console.error(errorMsg);
return ws.close(1008, errorMsg);
console.error(`[${id}] Invalid token found in authorization header.`);
return socket.disconnect();
}

let isFindMatchSuccess = false;
const matchResponseHandler = async (response) => {
console.log(`[${requestId}]`, "Matchmaking response received.");
const { users, difficulty } = response;
console.log(`[${id}]`, "Matchmaking response received.");
const { users, difficulty, roomId } = response;
const matchedUser = users.filter((userId) => userId !== user.id)[0];
console.log(`[${requestId}]`, "Sending matchmaking response to caller.");
ws.send(matchedUser);
console.log(`[${requestId}]`, "Closing websocket.");
ws.close(1000, JSON.stringify({ matchedUser, difficulty }));
console.log(`[${id}]`, "Sending matchmaking response to caller.");
socket.emit("user-matched", { matchedUser, difficulty, roomId });
console.log(`[${id}]`, "Closing websocket.");
isFindMatchSuccess = true;
socket.disconnect();
};

console.log(`[${requestId}]`, "Subscribing to matchmaking responses.");
console.log(`[${id}]`, "Subscribing to matchmaking responses.");
const subscriber = await subscribeMatchResponse(
user.id,
matchResponseHandler
);
console.log(`[${requestId}]`, "Enqueing matchmaking request.");
console.log(`[${id}]`, "Enqueing matchmaking request.");
await enqueueFindMatch(user.id, difficulty.toLowerCase());
console.log(`[${requestId}]`, "Waiting for matchmaking response.");
console.log(`[${id}]`, "Waiting for matchmaking response.");

ws.on("close", async () => {
socket.on("disconnect", async () => {
if (isFindMatchSuccess) return;
console.log(`[${requestId}]`, "Cancelling matchmaking request.");
console.log(`[${id}]`, "Cancelling matchmaking request.");
await enqueueCancelFindMatch(user.id, difficulty.toLowerCase());
console.log(
`[${requestId}]`,
"Unsubscribing from matchmaking responses."
);
console.log(`[${id}]`, "Unsubscribing from matchmaking responses.");
await unsubscribeMatchResponse(subscriber);
console.log(`[${requestId}]`, "Closing websocket.");
console.log(`[${id}]`, "Closing websocket.");
});
} catch (error) {
console.error(error);
return ws.close(1011, "Internal Server Error");
console.error(`[${id}]`, error);
return socket.disconnect();
}
});

httpServer.listen(process.env.PORT);
console.log(`Server is listening on port ${process.env.PORT}`);
Loading

0 comments on commit e08f951

Please sign in to comment.