diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 11c92b7035..635b95557e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -5,6 +5,7 @@ on: branches: - main - staging + - frontend-websocket-test pull_request: branches: - main @@ -27,6 +28,7 @@ jobs: env: QUESTION_SERVICE_URL: ${{ vars.QUESTION_SERVICE_URL }} USER_SERVICE_URL: ${{ vars.USER_SERVICE_URL }} + MATCHING_SERVICE_URL: ${{ vars.MATCHING_SERVICE_URL }} JWT_SECRET: ${{ secrets.JWT_SECRET }} FIREBASE_CREDENTIAL_PATH: ${{ vars.QUESTION_SERVICE_FIREBASE_CREDENTIAL_PATH }} DB_CLOUD_URI: ${{ secrets.USER_SERVICE_DB_CLOUD_URI }} @@ -35,6 +37,7 @@ jobs: cd ./apps/frontend echo "NEXT_PUBLIC_QUESTION_SERVICE_URL=$QUESTION_SERVICE_URL" >> .env echo "NEXT_PUBLIC_USER_SERVICE_URL=$USER_SERVICE_URL" >> .env + echo "NEXT_PUBLIC_MATCHING_SERVICE_URL=$MATCHING_SERVICE_URL" >> .env cd ../question-service echo "FIREBASE_CREDENTIAL_PATH=$FIREBASE_CREDENTIAL_PATH" >> .env diff --git a/apps/frontend/.env.example b/apps/frontend/.env.example index a6c274c271..547a68c4c0 100644 --- a/apps/frontend/.env.example +++ b/apps/frontend/.env.example @@ -1,3 +1,4 @@ # URL endpoints of the services NEXT_PUBLIC_QUESTION_SERVICE_URL="http://localhost:8080/" -NEXT_PUBLIC_USER_SERVICE_URL="http://localhost:3001/" \ No newline at end of file +NEXT_PUBLIC_USER_SERVICE_URL="http://localhost:3001/" +NEXT_PUBLIC_MATCHING_SERVICE_URL="ws://localhost:8081/match" \ No newline at end of file diff --git a/apps/frontend/README.md b/apps/frontend/README.md index 9012775719..280e388d75 100644 --- a/apps/frontend/README.md +++ b/apps/frontend/README.md @@ -25,6 +25,7 @@ Then, follow the `.env.example` file and create a `.env` file in the current dir ```bash NEXT_PUBLIC_QUESTION_SERVICE_URL="http://localhost:8080" NEXT_PUBLIC_USER_SERVICE_URL="http://localhost:3001/" +NEXT_PUBLIC_MATCHING_SERVICE_URL="wss://localhost:8081" ``` First, run the development server: diff --git a/apps/frontend/package.json b/apps/frontend/package.json index 14c855521f..97d014fb96 100644 --- a/apps/frontend/package.json +++ b/apps/frontend/package.json @@ -16,6 +16,8 @@ "next": "14.2.13", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-timer-hook": "^3.0.7", + "react-use-websocket": "^4.9.0", "sass": "^1.79.2", "typeface-montserrat": "^1.1.13" }, diff --git a/apps/frontend/pnpm-lock.yaml b/apps/frontend/pnpm-lock.yaml index 31ded77ff4..7e165fe896 100644 --- a/apps/frontend/pnpm-lock.yaml +++ b/apps/frontend/pnpm-lock.yaml @@ -29,6 +29,12 @@ importers: react-dom: specifier: ^18.2.0 version: 18.2.0(react@18.2.0) + react-timer-hook: + specifier: ^3.0.7 + version: 3.0.7(react@18.2.0) + react-use-websocket: + specifier: ^4.9.0 + version: 4.9.0 sass: specifier: ^1.79.2 version: 1.79.2 @@ -1462,6 +1468,14 @@ packages: react-is@18.3.1: resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==} + react-timer-hook@3.0.7: + resolution: {integrity: sha512-ATpNcU+PQRxxfNBPVqce2+REtjGAlwmfoNQfcEBMZFxPj0r3GYdKhyPHdStvqrejejEi0QvqaJZjy2lBlFvAsA==} + peerDependencies: + react: '>=16.8.0' + + react-use-websocket@4.9.0: + resolution: {integrity: sha512-/6OaCMggQCTnryCAsw/N+/wfH7bBfIXk5WXTMPdyf0x9HWJXLGUVttAT5hqAimRytD1dkHEJCUrFHAGzOAg1eg==} + react@18.2.0: resolution: {integrity: sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==} engines: {node: '>=0.10.0'} @@ -3536,6 +3550,12 @@ snapshots: react-is@18.3.1: {} + react-timer-hook@3.0.7(react@18.2.0): + dependencies: + react: 18.2.0 + + react-use-websocket@4.9.0: {} + react@18.2.0: dependencies: loose-envify: 1.4.0 diff --git a/apps/frontend/src/app/layout.tsx b/apps/frontend/src/app/layout.tsx index 0f28b2dbb9..61070cf842 100644 --- a/apps/frontend/src/app/layout.tsx +++ b/apps/frontend/src/app/layout.tsx @@ -1,6 +1,7 @@ import React from "react"; import { AntdRegistry } from "@ant-design/nextjs-registry"; import { ConfigProvider } from "antd"; +import WebSocketProvider from "@/components/WebSocketProvider/websocketprovider"; const RootLayout = ({ children, @@ -23,7 +24,11 @@ const RootLayout = ({ }, }} > - {children} + + + {children} + + diff --git a/apps/frontend/src/app/matching/MatchingModal.tsx b/apps/frontend/src/app/matching/MatchingModal.tsx index 684fb2f548..27cc098818 100644 --- a/apps/frontend/src/app/matching/MatchingModal.tsx +++ b/apps/frontend/src/app/matching/MatchingModal.tsx @@ -6,50 +6,82 @@ import 'typeface-montserrat'; import './styles.scss'; import FindMatchContent from './modalContent/FindMatchContent'; import MatchingInProgressContent from './modalContent/MatchingInProgressContent'; -import MatchFound from './modalContent/MatchFoundContent'; +import MatchFoundContent from './modalContent/MatchFoundContent'; import JoinedMatchContent from './modalContent/JoinedMatchContent'; import MatchNotFoundContent from './modalContent/MatchNotFoundContent'; import MatchCancelledContent from './modalContent/MatchCancelledContent'; +import useMatching from '../services/use-matching'; interface MatchingModalProps { isOpen: boolean; - onClose: () => void; + close: () => void; } -const MatchingModal: React.FC = ({ isOpen, onClose }) => { - // TODO: placehoder for now, to be replaced my useContext - const [matchingState, setMatchingState] = useState('finding'); +const MatchingModal: React.FC = ({ isOpen, close: _close }) => { + const matchingState = useMatching(); + const [closedType, setClosedType] = useState<"finding" | "cancelled" | "joined">("finding"); + const [timeoutAfter, setTimeoutAfter] = useState(9999); + const isClosable = ["timeout", "closed"].includes(matchingState.state); - // TODO: remove this after testing - useEffect(() => { - // Uncomment the following lines to test the different matching states - // setMatchingState('finding'); - // setMatchingState('matching'); - // setMatchingState('found'); - // setMatchingState('joined'); - // setMatchingState('notFound'); - // setMatchingState('cancelled'); - }, []); - - // TODO: modify by using matchingState via useContext - const isClosableMatchingState = () => { - return matchingState === 'finding' || matchingState === 'notFound' || matchingState === 'cancelled'; - }; + function close() { + // clean up matching and closedType State + if (matchingState.state === "timeout") { + matchingState.ok(); + } + setClosedType("finding"); + _close(); + } const renderModalContent = () => { - switch (matchingState) { - case 'finding': - return ; + switch (matchingState.state) { + case 'closed': + switch (closedType) { + case "finding": + return ; + case "cancelled": + return { + setClosedType("finding"); + }} + retry={() => {}} + canceledIn={timeoutAfter} + />; + case "joined": + return { + setClosedType("cancelled"); + }}/>; + } case 'matching': - return ; + return { + setClosedType("cancelled"); + setTimeoutAfter(timeoutAfter); + matchingState.cancel(); + }} + timeout={(timeoutAfter: number) => { + matchingState.timeout() + setTimeoutAfter(timeoutAfter); + }} + />; + case 'cancelling': + return {}} timeout={() => {}}/>; + case 'starting': + return {}}/> case 'found': - return ; - case 'joined': - return ; - case 'notFound': - return ; - case 'cancelled': - return ; + return { + matchingState.ok(); + setClosedType("cancelled"); + }} + join={() => { + matchingState.ok(); + setClosedType("joined"); + }} + name1={matchingState.info.myName} + name2={matchingState.info.partnerName} + /> + case 'timeout': + return {}} timedOutIn={10}/>; default: throw new Error('Invalid matching state.'); } @@ -57,15 +89,15 @@ const MatchingModal: React.FC = ({ isOpen, onClose }) => { return ( {renderModalContent()} - {isClosableMatchingState() && ( - + {isClosable && ( + )} ) diff --git a/apps/frontend/src/app/matching/modalContent/FindMatchContent.tsx b/apps/frontend/src/app/matching/modalContent/FindMatchContent.tsx index ed5e082c58..ac65cbe033 100644 --- a/apps/frontend/src/app/matching/modalContent/FindMatchContent.tsx +++ b/apps/frontend/src/app/matching/modalContent/FindMatchContent.tsx @@ -11,7 +11,8 @@ import { import type { SelectProps } from 'antd'; import 'typeface-montserrat'; import './styles.scss'; -import { handleFindMatch } from '../handlers'; +import { ValidateUser } from "@/app/services/user" +import { type MatchRequestParams } from '@/app/services/use-matching'; interface DifficultySelectorProps { className?: string; @@ -25,9 +26,14 @@ interface TopicSelectorProps { onChange: (topics: string[]) => void; } -const FindMatchContent: React.FC = () => { +interface Props { + beginMatch(request: MatchRequestParams): void +} + +const FindMatchContent: React.FC = ({ beginMatch }) => { const [selectedDifficulties, setSelectedDifficulties] = useState([]); const [selectedTopics, setSelectedTopics] = useState([]); + const [isLoading, setIsLoading] = useState(false); const handleDifficultyChange = (difficulties: string[]) => { setSelectedDifficulties(difficulties); @@ -55,7 +61,18 @@ const FindMatchContent: React.FC = () => { /> @@ -77,8 +94,8 @@ const DifficultySelector: React.FC = ({ selectedDifficu handleChange(difficultyOption.value)} + checked={selectedDifficulties.includes(difficultyOption.label)} + onChange={() => handleChange(difficultyOption.label)} > {difficultyOption.label} @@ -90,11 +107,8 @@ const DifficultySelector: React.FC = ({ selectedDifficu const TopicSelector: React.FC = ({ selectedTopics, onChange}) => { const topicOptions: SelectProps[] = CategoriesOption; - const handleChange = (topic: string) => { - const newSelectedTopics = selectedTopics.includes(topic) - ? selectedTopics.filter(selectedTopic => selectedTopic !== topic) - : [...selectedTopics, topic]; - onChange(newSelectedTopics); + const handleChange = (topics: string[]) => { + onChange(topics); } return ( diff --git a/apps/frontend/src/app/matching/modalContent/JoinedMatchContent.tsx b/apps/frontend/src/app/matching/modalContent/JoinedMatchContent.tsx index bdafae215d..8f5ed99723 100644 --- a/apps/frontend/src/app/matching/modalContent/JoinedMatchContent.tsx +++ b/apps/frontend/src/app/matching/modalContent/JoinedMatchContent.tsx @@ -10,7 +10,12 @@ import './styles.scss'; import { handleCancelMatch } from '../handlers'; import { formatTime } from '@/utils/DateTime'; -const JoinedMatchContent: React.FC = () => { + +interface Props { + cancel(): void +} + +const JoinedMatchContent: React.FC = ({cancel}) => { const matchAlreadyJoined = () => { throw new Error('Match already joined.'); } @@ -36,12 +41,11 @@ const JoinedMatchContent: React.FC = () => { diff --git a/apps/frontend/src/app/matching/modalContent/MatchCancelledContent.tsx b/apps/frontend/src/app/matching/modalContent/MatchCancelledContent.tsx index 309fa4824d..ca7cff5273 100644 --- a/apps/frontend/src/app/matching/modalContent/MatchCancelledContent.tsx +++ b/apps/frontend/src/app/matching/modalContent/MatchCancelledContent.tsx @@ -4,7 +4,13 @@ import './styles.scss'; import { handleReselectMatchOptions, handleRetryMatch } from '../handlers'; import { formatTime } from '@/utils/DateTime'; -const MatchCancelledContent: React.FC = () => { +interface Props { + retry(): void, + reselect(): void, + canceledIn: number, +} + +const MatchCancelledContent: React.FC = ({retry, reselect, canceledIn}) => { return (
@@ -20,15 +26,15 @@ const MatchCancelledContent: React.FC = () => {
Match Cancelled!
- Your match request has been cancelled after waiting {formatTime(83)} + Your match request has been cancelled after waiting {formatTime(canceledIn)}
- + */} diff --git a/apps/frontend/src/app/matching/modalContent/MatchFoundContent.tsx b/apps/frontend/src/app/matching/modalContent/MatchFoundContent.tsx index 5bb0b87f9d..229b481ffe 100644 --- a/apps/frontend/src/app/matching/modalContent/MatchFoundContent.tsx +++ b/apps/frontend/src/app/matching/modalContent/MatchFoundContent.tsx @@ -5,12 +5,30 @@ import 'typeface-montserrat'; import './styles.scss'; import { handleCancelMatch, handleJoinMatch } from '../handlers'; import { formatTime } from '@/utils/DateTime'; +import { useTimer } from "react-timer-hook" -const MatchFoundContent: React.FC = () => { +interface Props { + join(): void, + cancel(): void, + name1: string, // user's username + name2: string, // matched user's username +} + +const TIMEOUT = 10; + +const MatchFoundContent: React.FC = ({join, cancel, name1: me, name2: you}) => { + const { totalSeconds } = useTimer({ + expiryTimestamp: new Date(Date.now() + 10 * 1000), + onExpire: join + }); + return (
- } /> +
+ } /> +
{me}
+
{ > - } /> +
+ } /> +
{you}
+
Match Found!
- Joining in... {formatTime(83)} + Joining in... {formatTime(totalSeconds)}
diff --git a/apps/frontend/src/app/matching/modalContent/MatchNotFoundContent.tsx b/apps/frontend/src/app/matching/modalContent/MatchNotFoundContent.tsx index a0c8b08383..e0a817dae5 100644 --- a/apps/frontend/src/app/matching/modalContent/MatchNotFoundContent.tsx +++ b/apps/frontend/src/app/matching/modalContent/MatchNotFoundContent.tsx @@ -4,7 +4,13 @@ import './styles.scss'; import { handleReselectMatchOptions, handleRetryMatch } from '../handlers'; import { formatTime } from '@/utils/DateTime'; -const MatchNotFoundContent: React.FC = () => { +const MatchNotFoundContent: React.FC<{ + retry(): void, + reselect(): void, + timedOutIn: number, +}> = ({ + retry, reselect, timedOutIn +}) => { return (
@@ -20,15 +26,15 @@ const MatchNotFoundContent: React.FC = () => {
Match Not Found!
- Sorry, we could not find a match after {formatTime(83)} + Sorry, we could not find a match after {formatTime(timedOutIn)}
- + */} diff --git a/apps/frontend/src/app/matching/modalContent/MatchingInProgressContent.tsx b/apps/frontend/src/app/matching/modalContent/MatchingInProgressContent.tsx index 4986624c8e..fb0e8b2e00 100644 --- a/apps/frontend/src/app/matching/modalContent/MatchingInProgressContent.tsx +++ b/apps/frontend/src/app/matching/modalContent/MatchingInProgressContent.tsx @@ -1,21 +1,36 @@ -import React from 'react'; +import React, { useState } from 'react'; import { LoadingOutlined } from '@ant-design/icons'; import 'typeface-montserrat'; import './styles.scss'; import { handleCancelMatch } from '../handlers'; +import { useTimer } from "react-timer-hook" import {formatTime} from '@/utils/DateTime'; -const MatchingInProgressContent: React.FC = () => { +const TIMEOUT = 10; + +interface Props { + cancelMatch(cancelledIn: number): void + timeout(timeoutIn: number): void +} + +const MatchingInProgressContent: React.FC = ({cancelMatch: cancel, timeout}) => { + const { totalSeconds } = useTimer({ + expiryTimestamp: new Date(Date.now() + 10 * 1000), + onExpire: () => timeout(TIMEOUT - totalSeconds), + }); + return (
Matching in Progress
Please be patient as we match you with other online users

- {formatTime(83)} + {formatTime(TIMEOUT - totalSeconds)}
diff --git a/apps/frontend/src/app/matching/modalContent/styles.scss b/apps/frontend/src/app/matching/modalContent/styles.scss index 64bfc4e94c..67d5c63204 100644 --- a/apps/frontend/src/app/matching/modalContent/styles.scss +++ b/apps/frontend/src/app/matching/modalContent/styles.scss @@ -228,3 +228,12 @@ button:disabled, .joined-match-deactivated-button { height: 60px; fill: #463F3A; } + +.user-caption { + display: flex; + justify-content: center; +} + +.avatar-caption-container { + width: 64px; +} diff --git a/apps/frontend/src/app/page.tsx b/apps/frontend/src/app/page.tsx index d2b8d45dfc..da2ded7c96 100644 --- a/apps/frontend/src/app/page.tsx +++ b/apps/frontend/src/app/page.tsx @@ -20,7 +20,7 @@ const HomePage = () => {
- +
diff --git a/apps/frontend/src/app/services/use-matching.ts b/apps/frontend/src/app/services/use-matching.ts new file mode 100644 index 0000000000..61d07654e0 --- /dev/null +++ b/apps/frontend/src/app/services/use-matching.ts @@ -0,0 +1,146 @@ +import { MatchInfo, MatchState } from "@/contexts/websocketcontext"; +import { useEffect, useState } from "react"; +import useWebSocket, { Options, ReadyState } from "react-use-websocket"; + +const MATCHING_SERVICE_URL = process.env.NEXT_PUBLIC_MATCHING_SERVICE_URL; + +if (MATCHING_SERVICE_URL == undefined) { + throw "NEXT_PUBLIC_MATCHING_SERVICE_URL was not defined in .env"; +} + +export type MatchRequestParams = { + type: "match_request", + username: string, + email: string, + topics: string[], + difficulties: string[], +} + +export type MatchFoundResponse = { + type: "match_found", + matchId: number, + partnerId: number, + partnerName: string, +} | { + type: "match_found", + matchId: string, + user: string, + matchedUser: string, + topic: string | string[], + difficulty: string +} + +export type MatchTimeoutResponse = { + type: "timeout", + message: string, +} + +export type MatchRejectedResponse = { + type: "match_rejected", + message: string, +} + +type MatchResponse = MatchFoundResponse | MatchTimeoutResponse | MatchRejectedResponse; + +export default function useMatching(): MatchState { + const [isSocket, setIsSocket] = useState(false); + const [ste, setSte] = useState({ + state: "closed", + start, + }); + + const options: Options = { + onClose() { + setIsSocket(false); + }, + onMessage({data: response}) { + const responseJson: MatchResponse = JSON.parse(response); + if (responseJson.type == "timeout") { + timeout(); + return; + } + + if (responseJson.type == "match_found") { + setIsSocket(false); + + const info: MatchInfo = parseInfoFromResponse(responseJson); + setSte({ + state: "found", + info: info, + ok: cancel + }) + return; + } + + if (responseJson.type == "match_rejected") { + console.log("match rejected: " + responseJson.message); + cancel(); + return; + } + } + } + + const { + readyState: socketState, + sendJsonMessage, + } = useWebSocket(MATCHING_SERVICE_URL as string, options, isSocket); + + function timeout() { + setIsSocket(false); + setSte({ + state: "timeout", + ok: cancel, + }); + } + + function cancel() { + setIsSocket(false) + setSte({ + state: "closed", + start, + }) + } + + function start(request: MatchRequestParams) { + setIsSocket(true) + sendJsonMessage(request); + } + + let matchState: MatchState; + switch (socketState) { + case ReadyState.CLOSED: + case ReadyState.UNINSTANTIATED: + matchState = {state: "closed", start} + break; + case ReadyState.OPEN: + matchState = {state: "matching", cancel, timeout} + break; + case ReadyState.CONNECTING: + matchState = {state: "starting"} + break; + case ReadyState.CLOSING: + matchState = {state: "cancelling"} + break; + } + + return isSocket ? matchState : ste; +} + +function parseInfoFromResponse(responseJson: MatchFoundResponse): MatchInfo { + // test whether old or new + if ("partnerId" in responseJson) { + return { + matchId: responseJson.matchId?.toString() ?? "unknown", + partnerId: responseJson.partnerId?.toString() ?? "unknown", + partnerName: responseJson.partnerName ?? "unknown", + myName: "unknown", + }; + } else { + return { + matchId: responseJson.matchId?.toString() ?? "unknown", + partnerId: "unknown", + partnerName: responseJson.matchedUser ?? "unknown", + myName: responseJson.user ?? "unknown", + }; + } +} diff --git a/apps/frontend/src/components/WebSocketProvider/websocketprovider.tsx b/apps/frontend/src/components/WebSocketProvider/websocketprovider.tsx new file mode 100644 index 0000000000..8dd197da03 --- /dev/null +++ b/apps/frontend/src/components/WebSocketProvider/websocketprovider.tsx @@ -0,0 +1,18 @@ +"use client" + +import { ReactNode } from "react" +import { WebSocketContext } from "@/contexts/websocketcontext"; +import useMatching from "@/app/services/use-matching"; +const MATCHING_SERVICE_URL = process.env.NEXT_PUBLIC_MATCHING_SERVICE_URL; + +if (MATCHING_SERVICE_URL == undefined) { + throw "NEXT_PUBLIC_MATCHING_SERVICE_URL was not defined in .env"; +} + +export default function WebSocketProvider({children}: {children: ReactNode}) { + const matchState = useMatching(); + + return + {children} + +} \ No newline at end of file diff --git a/apps/frontend/src/contexts/websocketcontext.tsx b/apps/frontend/src/contexts/websocketcontext.tsx new file mode 100644 index 0000000000..5b7a041c18 --- /dev/null +++ b/apps/frontend/src/contexts/websocketcontext.tsx @@ -0,0 +1,31 @@ +import { MatchRequestParams } from "@/app/services/use-matching"; +import { createContext } from "react"; + + +export type SocketState = { + state: "cancelling" | "starting" +} | { + state: "closed"; + start(req: MatchRequestParams): void; +} | { + state: "matching"; + cancel(): void; + timeout(): void; +}; +export type MatchInfo = { + matchId: string; + partnerId: string; + myName: string; + partnerName: string; +} +export type MatchState = SocketState | { + state: "found"; + info: MatchInfo; + ok(): void; +} | { + state: "timeout"; + ok(): void; +}; + +export const WebSocketContext = createContext(null); + diff --git a/apps/matching-service/main.go b/apps/matching-service/main.go index a19a417922..678ec5312c 100644 --- a/apps/matching-service/main.go +++ b/apps/matching-service/main.go @@ -17,15 +17,15 @@ func main() { if err != nil { log.Fatalf("err loading: %v", err) } - + // Setup redis client processes.SetupRedisClient() // Run a goroutine that matches users - + // Routes http.HandleFunc("/match", handlers.HandleWebSocketConnections) - + // Start the server port := os.Getenv("PORT") log.Println(fmt.Sprintf("Server starting on :%s", port)) diff --git a/apps/matching-service/processes/match.go b/apps/matching-service/processes/match.go index fa94a2e79c..7864a75253 100644 --- a/apps/matching-service/processes/match.go +++ b/apps/matching-service/processes/match.go @@ -57,13 +57,13 @@ func PerformMatching(matchRequest models.MatchRequest, ctx context.Context, matc // Log down which users got matched log.Printf("Users %s and %s matched on the topic: %s with difficulty: %s", username, matchedUsername, matchedTopic, matchedDifficulty) - // Log queue after matchmaking - PrintMatchingQueue(redisClient, "After Matchmaking", context.Background()) - // Clean up redis for this match cleanUp(redisClient, username, ctx) cleanUp(redisClient, matchedUsername, ctx) + // Log queue after matchmaking + PrintMatchingQueue(redisClient, "After Matchmaking", context.Background()) + // Generate a random match ID matchId, err := utils.GenerateMatchID() if err != nil {