diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 2a73652149..82bd2743a4 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -30,6 +30,7 @@ jobs: USER_SERVICE_URL: ${{ vars.USER_SERVICE_URL }} MATCHING_SERVICE_URL: ${{ vars.MATCHING_SERVICE_URL }} HISTORY_SERVICE_URL: ${{ vars.HISTORY_SERVICE_URL }} + SIGNALLING_SERVICE_URL: ${{ vars.SIGNALLING_SERVICE_URL }} JWT_SECRET: ${{ secrets.JWT_SECRET }} QUESTION_FIREBASE_CREDENTIAL_PATH: ${{ vars.QUESTION_SERVICE_FIREBASE_CREDENTIAL_PATH }} HISTORY_FIREBASE_CREDENTIAL_PATH: ${{ vars.HISTORY_SERVICE_FIREBASE_CREDENTIAL_PATH }} @@ -37,6 +38,7 @@ jobs: USER_SERVICE_PORT: ${{ vars.USER_SERVICE_PORT }} MATCHING_SERVICE_PORT: ${{ vars.MATCHING_SERVICE_PORT }} HISTORY_SERVICE_PORT: ${{ vars.HISTORY_SERVICE_PORT }} + SIGNALLING_SERVICE_PORT: ${{ vars.SIGNALLING_SERVICE_PORT }} MATCHING_SERVICE_TIMEOUT: ${{ vars.MATCHING_SERVICE_TIMEOUT }} REDIS_URL: ${{ vars.REDIS_URL }} QUESTION_SERVICE_GRPC_URL: ${{ vars.QUESTION_SERVICE_GPRC_URL }} @@ -46,6 +48,7 @@ jobs: echo "NEXT_PUBLIC_USER_SERVICE_URL=$USER_SERVICE_URL" >> .env echo "NEXT_PUBLIC_MATCHING_SERVICE_URL=$MATCHING_SERVICE_URL" >> .env echo "NEXT_PUBLIC_HISTORY_SERVICE_URL=$HISTORY_SERVICE_URL" >> .env + echo "NEXT_PUBLIC_SIGNALLING_SERVICE_URL=$SIGNALLING_SERVICE_URL" >> .env cd ../question-service echo "FIREBASE_CREDENTIAL_PATH=$QUESTION_FIREBASE_CREDENTIAL_PATH" >> .env @@ -67,6 +70,9 @@ jobs: echo "FIREBASE_CREDENTIAL_PATH=$HISTORY_FIREBASE_CREDENTIAL_PATH" >> .env echo "PORT=$HISTORY_SERVICE_PORT" >> .env + cd ../signalling-service + echo "PORT=$SIGNALLING_SERVICE_PORT" >> .env + - name: Create Database Credential Files env: QUESTION_FIREBASE_JSON: ${{ secrets.QUESTION_SERVICE_FIREBASE_CREDENTIAL }} @@ -101,6 +107,7 @@ jobs: QUESTION_SERVICE_URL: ${{ vars.QUESTION_SERVICE_URL }} MATCHING_SERVICE_URL: ${{ vars.MATCHING_SERVICE_URL }} HISTORY_SERVICE_URL: ${{ vars.HISTORY_SERVICE_URL }} + SIGNALLING_SERVICE_URL: ${{ vars.SIGNALLING_SERVICE_URL }} run: | echo "Testing Question Service..." curl -sSL -o /dev/null $QUESTION_SERVICE_URL && echo "Question Service is up" @@ -117,5 +124,10 @@ jobs: echo "WebSocket for Matching Service is live" fi # Add in test for matching service in the future - + echo "Testing Signalling Service..." + if ! (echo "Hello" | websocat $SIGNALLING_SERVICE_URL); then + echo "WebSocket for Signalling Service is not live" + else + echo "WebSocket for Signalling Service is live" + fi # We can add more tests here diff --git a/apps/docker-compose.yml b/apps/docker-compose.yml index 0a0ef34fdf..6152c29757 100644 --- a/apps/docker-compose.yml +++ b/apps/docker-compose.yml @@ -11,6 +11,8 @@ services: - ./frontend/.env volumes: - ./frontend:/frontend + depends_on: + - signalling-service user-service: build: @@ -66,6 +68,19 @@ services: - apps_network volumes: - ./history-service:/history-service + + signalling-service: + build: + context: ./signalling-service + dockerfile: Dockerfile + ports: + - 4444:4444 + env_file: + - ./signalling-service/.env + networks: + - apps_network + volumes: + - ./signalling-service:/signalling-service redis: image: redis:latest diff --git a/apps/frontend/src/app/collaboration/[id]/page.tsx b/apps/frontend/src/app/collaboration/[id]/page.tsx index 1e9c5548c8..41c56023f0 100644 --- a/apps/frontend/src/app/collaboration/[id]/page.tsx +++ b/apps/frontend/src/app/collaboration/[id]/page.tsx @@ -5,6 +5,7 @@ import { Col, Input, Layout, + Modal, message, Row, Select, @@ -22,12 +23,15 @@ import { ClockCircleOutlined, CodeOutlined, FileDoneOutlined, + InfoCircleFilled, MessageOutlined, PlayCircleOutlined, SendOutlined, } from "@ant-design/icons"; import { ProgrammingLanguageOptions } from "@/utils/SelectOptions"; -import CollaborativeEditor from "@/components/CollaborativeEditor/CollaborativeEditor"; +import CollaborativeEditor, { + CollaborativeEditorHandle, +} from "@/components/CollaborativeEditor/CollaborativeEditor"; import { CreateOrUpdateHistory } from "@/app/services/history"; import { Language } from "@codemirror/language"; import { WebrtcProvider } from "y-webrtc"; @@ -38,6 +42,8 @@ export default function CollaborationPage(props: CollaborationProps) { const router = useRouter(); const providerRef = useRef(null); + const editorRef = useRef(null); + const [isLoading, setIsLoading] = useState(false); // Code Editor States @@ -48,7 +54,9 @@ export default function CollaborationPage(props: CollaborationProps) { const [questionTitle, setQuestionTitle] = useState( undefined ); - const [questionDocRefId, setQuestionDocRefId] = useState(undefined); + const [questionDocRefId, setQuestionDocRefId] = useState( + undefined + ); const [complexity, setComplexity] = useState(undefined); const [categories, setCategories] = useState([]); // Store the selected filter categories const [description, setDescription] = useState(undefined); @@ -59,8 +67,15 @@ export default function CollaborationPage(props: CollaborationProps) { undefined ); const [currentUser, setCurrentUser] = useState(undefined); - const [matchedUser, setMatchedUser] = useState(undefined); - const [matchedTopics, setMatchedTopics] = useState(undefined); + const [matchedUser, setMatchedUser] = useState("Loading..."); + const [sessionDuration, setSessionDuration] = useState(() => { + const storedTime = localStorage.getItem("session-duration"); + return storedTime ? parseInt(storedTime) : 0; + }); // State for count-up timer (TODO: currently using localstorage to store time, change to db stored time in the future) + const stopwatchRef = useRef(null); + const [matchedTopics, setMatchedTopics] = useState( + undefined + ); // Chat states const [messageToSend, setMessageToSend] = useState( @@ -72,6 +87,50 @@ export default function CollaborationPage(props: CollaborationProps) { undefined ); + // End Button Modal state + const [isModalOpen, setIsModalOpen] = useState(false); + // Session End Modal State + const [isSessionEndModalOpen, setIsSessionEndModalOpen] = + useState(false); + const [countDown, setCountDown] = useState(5); + + // Stops the session duration stopwatch + const stopStopwatch = () => { + if (stopwatchRef.current) { + clearInterval(stopwatchRef.current); + } + }; + + // Starts the session duration stopwatch + const startStopwatch = () => { + if (stopwatchRef.current) { + clearInterval(stopwatchRef.current); + } + + stopwatchRef.current = setInterval(() => { + setSessionDuration((prevTime) => { + const newTime = prevTime + 1; + localStorage.setItem("session-duration", newTime.toString()); + return newTime; + }); + }, 1000); + }; + + // Convert seconds into time of format "hh:mm:ss" + const formatTime = (seconds: number) => { + const hours = Math.floor(seconds / 3600); + const minutes = Math.floor((seconds % 3600) / 60); + const secs = seconds % 60; + + return ( + (hours > 9 ? hours : "0" + hours) + + ":" + + (minutes > 9 ? minutes : "0" + minutes) + + ":" + + (secs > 9 ? secs : "0" + secs) + ); + }; + // Message const [messageApi, contextHolder] = message.useMessage(); @@ -87,34 +146,34 @@ export default function CollaborationPage(props: CollaborationProps) { throw new Error("Provider not initialized"); } providerRef.current.awareness.setLocalStateField("codeSavedStatus", true); - } + }; const handleSubmitCode = async () => { if (!collaborationId) { throw new Error("Collaboration ID not found"); } - const data = await CreateOrUpdateHistory({ - title: questionTitle ?? "", - code: code, - language: selectedLanguage, - user: currentUser ?? "", - matchedUser: matchedUser ?? "", - matchId: collaborationId ?? "", - matchedTopics: matchedTopics ?? [], - questionDocRefId: questionDocRefId ?? "", - questionDifficulty: complexity ?? "", - questionTopics: categories, - }, collaborationId); + const data = await CreateOrUpdateHistory( + { + title: questionTitle ?? "", + code: code, + language: selectedLanguage, + user: currentUser ?? "", + matchedUser: matchedUser ?? "", + matchId: collaborationId ?? "", + matchedTopics: matchedTopics ?? [], + questionDocRefId: questionDocRefId ?? "", + questionDifficulty: complexity ?? "", + questionTopics: categories, + }, + collaborationId + ); successMessage("Code saved successfully!"); sendCodeSavedStatusToMatchedUser(); - } + }; const handleCodeChange = (code: string) => { setCode(code); - } - - // Retrieve the docRefId from query params during page navigation - // const searchParams = useSearchParams(); + }; // Fetch the question on initialisation useEffect(() => { @@ -123,11 +182,13 @@ export default function CollaborationPage(props: CollaborationProps) { } // Retrieve details from localstorage - const questionDocRefId: string = localStorage.getItem("questionDocRefId") ?? ""; + const questionDocRefId: string = + localStorage.getItem("questionDocRefId") ?? ""; const collabId: string = localStorage.getItem("collabId") ?? ""; const matchedUser: string = localStorage.getItem("matchedUser") ?? ""; const currentUser: string = localStorage.getItem("user") ?? ""; - const matchedTopics: string[] = localStorage.getItem("matchedTopics")?.split(",") ?? []; + const matchedTopics: string[] = + localStorage.getItem("matchedTopics")?.split(",") ?? []; // Set states from localstorage setCollaborationId(collabId); @@ -142,8 +203,25 @@ export default function CollaborationPage(props: CollaborationProps) { setCategories(data.categories); setDescription(data.description); }); + + // Start stopwatch + startStopwatch(); }, []); + // useEffect for timer + useEffect(() => { + if (isSessionEndModalOpen && countDown > 0) { + const timer = setInterval(() => { + setCountDown((prevCountDown) => prevCountDown - 1); + }, 1000); + + return () => clearInterval(timer); // Clean up on component unmount or when countdown changes + } else if (countDown === 0) { + router.push("/matching"); // Redirect to matching page + } + }, [isSessionEndModalOpen, countDown]); + + // Tabs component items for testcases const items: TabsProps["items"] = [ { key: "1", @@ -172,16 +250,25 @@ export default function CollaborationPage(props: CollaborationProps) { }, ]; - const handleCloseCollaboration = () => { + // Handles the cleaning of localstorage variables, stopping the timer & signalling collab user on webrtc + // type: "initiator" | "peer" + const handleCloseCollaboration = (type: string) => { + // Stop stopwatch + stopStopwatch(); + if (editorRef.current && type === "initiator") { + editorRef.current.endSession(); // Call the method on the editor + } + + // Trigger modal open showing session end details + setIsSessionEndModalOpen(true); + // Remove localstorage variables for collaboration + localStorage.removeItem("session-duration"); // TODO: Remove this after collaboration backend data stored localStorage.removeItem("user"); localStorage.removeItem("matchedUser"); localStorage.removeItem("collabId"); localStorage.removeItem("questionDocRefId"); localStorage.removeItem("matchedTopics"); - - // Redirect back to matching page - router.push("/matching"); }; return ( @@ -189,6 +276,52 @@ export default function CollaborationPage(props: CollaborationProps) { {contextHolder}
+ +

+ The collaboration session has ended. You will be redirected in{" "} + {countDown} seconds +

+

+ Question:{" "} + {questionTitle} +

+

+ Difficulty:{" "} + + {complexity && + complexity.charAt(0).toUpperCase() + complexity.slice(1)} + +

+

+ Duration:{" "} + + {formatTime(sessionDuration)} + +

+

+ Matched User:{" "} + + {matchedUser} + +

+
@@ -211,7 +344,7 @@ export default function CollaborationPage(props: CollaborationProps) {
- Topics: + Topics: {categories.map((category) => ( {category} ))} @@ -227,7 +360,11 @@ export default function CollaborationPage(props: CollaborationProps) { Test Cases
{/* TODO: Link to execution service for running code against test-cases */} - @@ -246,28 +383,22 @@ export default function CollaborationPage(props: CollaborationProps) { Code {/* TODO: Link to execution service for code submission */} - - {/*
-
Select Language:
-
- Start Time: + Start Time: 01:23:45
- + Session Duration:{" "} - + 01:23:45
- Matched with: + Matched with: John Doe
diff --git a/apps/frontend/src/components/CollaborativeEditor/CollaborativeEditor.tsx b/apps/frontend/src/components/CollaborativeEditor/CollaborativeEditor.tsx index 5b5c01c06f..857e149be1 100644 --- a/apps/frontend/src/components/CollaborativeEditor/CollaborativeEditor.tsx +++ b/apps/frontend/src/components/CollaborativeEditor/CollaborativeEditor.tsx @@ -1,5 +1,16 @@ // Referenced from example in https://www.npmjs.com/package/y-codemirror.next -import React, { MutableRefObject, useEffect, useRef, useState } from "react"; +import React, { + Dispatch, + ForwardedRef, + forwardRef, + RefObject, + SetStateAction, + useEffect, + useImperativeHandle, + useRef, + useState, + MutableRefObject, +} from "react"; import * as Y from "yjs"; import { yCollab } from "y-codemirror.next"; import { WebrtcProvider } from "y-webrtc"; @@ -19,11 +30,17 @@ interface CollaborativeEditorProps { user: string; collaborationId: string; language: string; + setMatchedUser: Dispatch>; + handleCloseCollaboration: (type: string) => void; providerRef: MutableRefObject; - matchedUser: string | undefined; + matchedUser: string; onCodeChange: (code: string) => void; } +export interface CollaborativeEditorHandle { + endSession: () => void; +} + interface AwarenessUpdate { added: number[]; updated: number[]; @@ -54,199 +71,238 @@ export const usercolors = [ export const userColor = usercolors[Math.floor(Math.random() * (usercolors.length - 1))]; -const CollaborativeEditor = (props: CollaborativeEditorProps) => { - const editorRef = useRef(null); - // const viewRef = useRef(null); - const [selectedLanguage, setSelectedLanguage] = useState("JavaScript"); - const [trigger, setTrigger] = useState(false); - - const languageConf = new Compartment(); - - // Referenced: https://codemirror.net/examples/config/#dynamic-configuration - const autoLanguage = EditorState.transactionExtender.of((tr) => { - if (!tr.docChanged) return null; - - const snippet = tr.newDoc.sliceString(0, 100); - - // Handle code change - props.onCodeChange(tr.newDoc.toString()); - - // Test for various language - const docIsPython = /^\s*(def|class)\s/.test(snippet); - const docIsJava = /^\s*(class|public\s+static\s+void\s+main)\s/.test( - snippet - ); // Java has some problems - const docIsCpp = /^\s*(#include|namespace|int\s+main)\s/.test(snippet); // Yet to test c++ - const docIsGo = /^(package|import|func|type|var|const)\s/.test(snippet); - - let newLanguage; - let languageType; - let languageLabel; - - if (docIsPython) { - newLanguage = python(); - languageLabel = "Python"; - languageType = pythonLanguage; - } else if (docIsJava) { - newLanguage = java(); - languageLabel = "Java"; - languageType = javaLanguage; - } else if (docIsGo) { - newLanguage = go(); - languageLabel = "Go"; - languageType = goLanguage; - } else if (docIsCpp) { - newLanguage = cpp(); - languageLabel = "C++"; - languageType = cppLanguage; - } else { - newLanguage = javascript(); // Default to JavaScript - languageLabel = "JavaScript"; - languageType = javascriptLanguage; - } - - const stateLanguage = tr.startState.facet(language); - if (languageType == stateLanguage) return null; - - setSelectedLanguage(languageLabel); - - return { - effects: languageConf.reconfigure(newLanguage), +const CollaborativeEditor = forwardRef( + ( + props: CollaborativeEditorProps, + ref: ForwardedRef + ) => { + const editorRef = useRef(null); + // const providerRef = useRef(null); + const [selectedLanguage, setSelectedLanguage] = useState("JavaScript"); + let sessionEndNotified = false; + + const languageConf = new Compartment(); + + // Referenced: https://codemirror.net/examples/config/#dynamic-configuration + const autoLanguage = EditorState.transactionExtender.of((tr) => { + if (!tr.docChanged) return null; + + const snippet = tr.newDoc.sliceString(0, 100); + + // Handle code change + props.onCodeChange(tr.newDoc.toString()); + + // Test for various language + const docIsPython = /^\s*(def|class)\s/.test(snippet); + const docIsJava = /^\s*(class|public\s+static\s+void\s+main)\s/.test( + snippet + ); // Java has some problems + const docIsCpp = /^\s*(#include|namespace|int\s+main)\s/.test(snippet); // Yet to test c++ + const docIsGo = /^(package|import|func|type|var|const)\s/.test(snippet); + + let newLanguage; + let languageType; + let languageLabel; + + if (docIsPython) { + newLanguage = python(); + languageLabel = "Python"; + languageType = pythonLanguage; + } else if (docIsJava) { + newLanguage = java(); + languageLabel = "Java"; + languageType = javaLanguage; + } else if (docIsGo) { + newLanguage = go(); + languageLabel = "Go"; + languageType = goLanguage; + } else if (docIsCpp) { + newLanguage = cpp(); + languageLabel = "C++"; + languageType = cppLanguage; + } else { + newLanguage = javascript(); // Default to JavaScript + languageLabel = "JavaScript"; + languageType = javascriptLanguage; + } + + const stateLanguage = tr.startState.facet(language); + if (languageType == stateLanguage) return null; + + setSelectedLanguage(languageLabel); + + return { + effects: languageConf.reconfigure(newLanguage), + }; + }); + + const [messageApi, contextHolder] = message.useMessage(); + + const success = (message: string) => { + messageApi.open({ + type: "success", + content: message, + }); }; - }); - const [messageApi, contextHolder] = message.useMessage(); + const info = (message: string) => { + messageApi.open({ + type: "info", + content: message, + }); + }; - const success = (message: string) => { - messageApi.open({ - type: "success", - content: message, - }); - }; + const error = (message: string) => { + messageApi.open({ + type: "error", + content: message, + }); + }; - const error = (message: string) => { - messageApi.open({ - type: "error", - content: message, - }); - }; + const warning = (message: string) => { + messageApi.open({ + type: "warning", + content: message, + }); + }; - const warning = (message: string) => { - messageApi.open({ - type: "warning", - content: message, - }); - }; + useImperativeHandle(ref, () => ({ + endSession: () => { + if (props.providerRef.current) { + // Set awareness state to indicate session ended to notify peer about session ending + props.providerRef.current.awareness.setLocalStateField( + "sessionEnded", + true + ); + success("Session ended. All participants will be notified."); + } + }, + })); + + useEffect(() => { + if (process.env.NEXT_PUBLIC_SIGNALLING_SERVICE_URL === undefined) { + error("Missing Signalling Service Url"); + return; + } + + const ydoc = new Y.Doc(); + const provider = new WebrtcProvider(props.collaborationId, ydoc, { + signaling: [process.env.NEXT_PUBLIC_SIGNALLING_SERVICE_URL], + maxConns: 2, + }); - // const handleLanguageChange = (val: any) => { - // console.log("came in here"); - // console.log(val); - // setSelectedLanguage(val); - - // let languageExtension; - // switch (val) { - // case "python": - // languageExtension = python(); - // break; - // default: - // languageExtension = javascript(); - // } - - // // Update the language configuration - // if (viewRef.current) { - // console.log("insude here"); - // viewRef.current.dispatch({ - // effects: languageConf.reconfigure(languageExtension), - // }); - // } - // }; - - useEffect(() => { - if (process.env.NEXT_PUBLIC_SIGNALLING_SERVICE_URL === undefined) { - error("Missing Signalling Service Url"); - return; - } - - const ydoc = new Y.Doc(); - const provider = new WebrtcProvider(props.collaborationId, ydoc, { - signaling: [process.env.NEXT_PUBLIC_SIGNALLING_SERVICE_URL], - }); - props.providerRef.current = provider; - const ytext = ydoc.getText("codemirror"); - const undoManager = new Y.UndoManager(ytext); - - provider.awareness.setLocalStateField("user", { - name: props.user, - color: userColor.color, - colorLight: userColor.light, - }); + props.providerRef.current = provider; + const ytext = ydoc.getText("codemirror"); + const undoManager = new Y.UndoManager(ytext); - // Listener for awareness updates to receive status changes from peers - provider.awareness.on("update", ({ added, updated } : AwarenessUpdate) => { - added.concat(updated).filter(clientId => clientId !== provider.awareness.clientID).forEach((clientID) => { - const state = provider.awareness.getStates().get(clientID) as Awareness; - if (state && state.codeSavedStatus) { - // Display the received status message - messageApi.open({ - type: "success", - content: `${props.matchedUser ?? "Peer"} saved code successfully!`, - }); + provider.awareness.setLocalStateField("user", { + name: props.user, + color: userColor.color, + colorLight: userColor.light, + }); + + // Check initial awareness states + const states = provider.awareness.getStates(); + for (const [clientID, state] of Array.from(states)) { + if (state.user && state.user.name !== props.user) { + props.setMatchedUser(state.user.name); + break; + } + } + + // Listen for awareness changes + provider.awareness.on("change", () => { + const updatedStates = provider.awareness.getStates(); + for (const [clientID, state] of Array.from(updatedStates)) { + if (state.sessionEnded && state.user.name !== props.user) { + if (!sessionEndNotified) { + info( + `Session has been ended by another participant ${state.user.name}` + ); + + props.handleCloseCollaboration("peer"); + sessionEndNotified = true; + if (props.providerRef.current) { + props.providerRef.current.disconnect(); + } + return; + } + } + + if (state.user && state.user.name !== props.user) { + props.setMatchedUser(state.user.name); + break; + } } }); - }); - const state = EditorState.create({ - doc: ytext.toString(), - extensions: [ - basicSetup, - languageConf.of(javascript()), - autoLanguage, - yCollab(ytext, provider.awareness, { undoManager }), - ], - }); + // Listener for awareness updates to receive status changes from peers + provider.awareness.on("update", ({ added, updated }: AwarenessUpdate) => { + added + .concat(updated) + .filter((clientId) => clientId !== provider.awareness.clientID) + .forEach((clientID) => { + const state = provider.awareness + .getStates() + .get(clientID) as Awareness; + if (state && state.codeSavedStatus) { + // Display the received status message + messageApi.open({ + type: "success", + content: `${ + props.matchedUser ?? "Peer" + } saved code successfully!`, + }); + } + }); + }); - const view = new EditorView({ - state, - parent: editorRef.current || undefined, - }); + const state = EditorState.create({ + doc: ytext.toString(), + extensions: [ + basicSetup, + languageConf.of(javascript()), + autoLanguage, + yCollab(ytext, provider.awareness, { undoManager }), + ], + }); - // viewRef.current = new EditorView({ - // state: state, - // parent: editorRef.current || undefined, - // }); - - return () => { - // Cleanup on component unmount - console.log("unmounting collaboration editor"); // TODO: remove - view.destroy(); - // viewRef.current?.destroy(); - provider.disconnect(); - ydoc.destroy(); - }; - }, []); - - return ( - <> - {contextHolder} -
-
Select Language:
- setSelectedLanguage(val)} + disabled + /> +
+
-
-
-
- Current Language Detected: {selectedLanguage} -
- - ); -}; +
+ Current Language Detected: {selectedLanguage} +
+ + ); + } +); export default CollaborativeEditor; diff --git a/apps/signalling-service/Dockerfile b/apps/signalling-service/Dockerfile new file mode 100644 index 0000000000..6189b2fde6 --- /dev/null +++ b/apps/signalling-service/Dockerfile @@ -0,0 +1,21 @@ +FROM node:18-alpine AS base + +# Install dependencies only when needed +FROM base AS deps +# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed. +RUN apk add --no-cache libc6-compat +WORKDIR /app + +COPY package*.json pnpm-lock.yaml* ./ +RUN \ + if [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm i --frozen-lockfile; \ + else echo "Lockfile not found." && exit 1; \ + fi + +COPY . . + +# Expose port 3001 so it can be mapped by Docker daemon. +EXPOSE 4444 + +# Define the command to run your app using CMD which defines your runtime. +CMD [ "node", "server.js" ] \ No newline at end of file diff --git a/apps/signalling-service/README.md b/apps/signalling-service/README.md index f51e5e76b8..c49ca99356 100644 --- a/apps/signalling-service/README.md +++ b/apps/signalling-service/README.md @@ -25,4 +25,14 @@ First, run the development server: pnpm dev ``` -## Build Dockerfile (TODO) +## Build Dockerfile + +```bash +docker build -t signalling-service -f Dockerfile . +``` + +## Run Docker Container + +```bash +docker run -p 4444:4444 --env-file .env -d signalling-service +```