diff --git a/backend/signaling-service/Dockerfile.dev b/backend/collab-service/Dockerfile.dev similarity index 100% rename from backend/signaling-service/Dockerfile.dev rename to backend/collab-service/Dockerfile.dev diff --git a/backend/collab-service/index.js b/backend/collab-service/index.js new file mode 100644 index 0000000000..d458088725 --- /dev/null +++ b/backend/collab-service/index.js @@ -0,0 +1,100 @@ +import { Hocuspocus } from "@hocuspocus/server"; + +const disconnectTimers = new Map(); +const disconnectedClients = new Map(); + +const destroyedSessions = new Set(); +setInterval(() => { + destroyedSessions.clear(); +}, 24 * 60 * 60 * 1000); + +const server = new Hocuspocus({ + port: 3003, + + async onAuthenticate(data) { + const { token } = data; + + if (token !== "abc") { + throw new Error("Not authorized!"); + } + + if (destroyedSessions.has(data.documentName)) { + throw new Error("Session has ended"); + } + + return + }, + + onConfigure: data => { + console.log('Connection being configured:', data.documentName); + }, + onConnect: data => { + console.log('Client connected: ', data.documentName); + + if (destroyedSessions.has(data.documentName)) { + data.document.broadcastStateless("sessionEnded"); + data.document.destroy(); + return; + } + + const currentCount = disconnectedClients.get(data.documentName) || 0; + if (currentCount > 0) { + disconnectedClients.set(data.documentName, currentCount - 1); + } + + if (disconnectTimers.has(data.documentName) && + disconnectedClients.get(data.documentName) === 0) { + clearTimeout(disconnectTimers.get(data.documentName)); + disconnectTimers.delete(data.documentName); + console.log('Reconnected within time window'); + } + }, + + onDisconnect(data) { + // network disconnects or last person leaves + console.log('Client disconnected from:', data.documentName); + + if (destroyedSessions.has(data.documentName)) { + return; + } + + const currentCount = disconnectedClients.get(data.documentName) || 0; + disconnectedClients.set(data.documentName, currentCount + 1); + + if (!disconnectTimers.has(data.documentName)) { + const timeoutId = setTimeout(() => { + // only destroy if clients are still disconnected after 1 min + if (disconnectedClients.get(data.documentName) > 0) { + console.log('Reconnection window expired'); + data.document.broadcastStateless("sessionEndedNetwork"); + data.document.destroy(); + disconnectTimers.delete(data.documentName); + disconnectedClients.delete(data.documentName); + destroyedSessions.add(data.documentName); + } + }, 60000); + + disconnectTimers.set(data.documentName, timeoutId); + console.log('Started reconnection window'); + } + }, + + onStateless: ({ payload, document, connection }) => { + // explicit session end + if (payload === "endSession") { + console.log('Session explicitly ended'); + if (disconnectTimers.has(document.name)) { + clearTimeout(disconnectTimers.get(document.name)); + disconnectTimers.delete(document.name); + } + disconnectedClients.delete(document.name); + destroyedSessions.add(document.name); + document.broadcastStateless("sessionEnded"); + setTimeout(() => { + document.destroy(); + }, 5000); + } + } +}); + +server.listen(); \ No newline at end of file diff --git a/backend/signaling-service/package-lock.json b/backend/collab-service/package-lock.json similarity index 100% rename from backend/signaling-service/package-lock.json rename to backend/collab-service/package-lock.json diff --git a/backend/signaling-service/package.json b/backend/collab-service/package.json similarity index 100% rename from backend/signaling-service/package.json rename to backend/collab-service/package.json diff --git a/backend/signaling-service/index.js b/backend/signaling-service/index.js deleted file mode 100644 index 0685e37751..0000000000 --- a/backend/signaling-service/index.js +++ /dev/null @@ -1,37 +0,0 @@ -import { Hocuspocus } from "@hocuspocus/server"; - -const rooms = new Map(); - -const server = new Hocuspocus({ - port: 3003, - - async onAuthenticate(data) { - const { token } = data; - - if (token !== "abc") { - throw new Error("Not authorized!"); - } - - return - }, - - onConfigure: data => { - console.log('Connection being configured:', data.documentName); - }, - onConnect: data => { - console.log('Client connected: ', data.documentName); - }, - - onDisconnect(data) { - console.log(data) - - if (data.clientsCount == 1) { - console.log('User disconnected'); - - data.document.broadcastStateless("sessionEnded"); - data.document.destroy(); - } - }, -}); - -server.listen(); \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 92cdb1a85b..400139ae1f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -86,9 +86,9 @@ services: kafka: condition: service_healthy - signaling-service: + collab-service: build: - context: ./backend/signaling-service + context: ./backend/collab-service dockerfile: Dockerfile.dev ports: - "${COLLAB_API_PORT}:3003" diff --git a/frontend/app/(authenticated)/questions/page.tsx b/frontend/app/(authenticated)/questions/page.tsx index 9f50a818ec..965265ce00 100644 --- a/frontend/app/(authenticated)/questions/page.tsx +++ b/frontend/app/(authenticated)/questions/page.tsx @@ -353,7 +353,8 @@ export default function Questions() { useEffect(() => { if (isMatchFoundDialogOpen) { - setRedirectTime(3); + router.prefetch('/session/[id]'); + setRedirectTime(2); const redirectTimer = setInterval(() => { setRedirectTime((prevTime) => { if (prevTime <= 1) { diff --git a/frontend/app/session/[id]/page.tsx b/frontend/app/session/[id]/page.tsx index 1c3be975a8..09be7a84cf 100644 --- a/frontend/app/session/[id]/page.tsx +++ b/frontend/app/session/[id]/page.tsx @@ -45,6 +45,8 @@ export default function Session() { const [controller, setController] = useState(null); const [timeElapsed, setTimeElapsed] = useState(0); const [isSessionEnded, setIsSessionEnded] = useState(false); + const [isSessionEndedPeer, setIsSessionEndedPeer] = useState(false); + const [isSessionEndedDisconnect, setIsSessionEndedDisconnect] = useState(false); const [isEndDialogOpen, setIsEndDialogOpen] = useState(false); const [language, setLanguage] = useState("javascript"); @@ -123,7 +125,7 @@ export default function Session() { }, [isHistoryApiCalled, language, questionId, timeElapsed]); useEffect(() => { - if (isSessionEnded && !isHistoryApiCalled) { + if ((isSessionEnded || isSessionEndedPeer || isSessionEndedDisconnect) && !isHistoryApiCalled) { const cleanup = async () => { await callUserHistoryAPI(); setTimeout(() => { @@ -132,7 +134,7 @@ export default function Session() { }; cleanup(); } - }, [isSessionEnded, isHistoryApiCalled, callUserHistoryAPI, router]); + }, [isSessionEnded, isHistoryApiCalled, callUserHistoryAPI, router, isSessionEndedPeer, isSessionEndedDisconnect]); useEffect(() => { setIsClient(true); @@ -179,8 +181,11 @@ export default function Session() { onStateless: ({ payload }) => { console.log('Received message:', payload); if (payload === 'sessionEnded') { - console.log("Session ended"); - setIsSessionEnded(true); + console.log("Session explicitly ended"); + setIsSessionEndedPeer(true); + } else if (payload === 'sessionEndedNetwork') { + console.log("Session ended due to network disconnect by peer"); + setIsSessionEndedDisconnect(true); } }, }); @@ -199,13 +204,8 @@ export default function Session() { notesProviderRef.current = notesProvider; if (isSessionEnded) { - socket.disconnect(); + codeProvider.sendStateless("endSession"); } - - return () => { - codeProvider.destroy(); - socket.disconnect(); - }; }, [isSessionEnded, params.id, questionId, router]); @@ -292,7 +292,7 @@ export default function Session() { setIsSessionEnded(true) setIsEndDialogOpen(false) }} - disabled={isSessionEnded} + disabled={isSessionEnded || isSessionEndedPeer || isSessionEndedDisconnect} > End session @@ -361,7 +361,7 @@ export default function Session() { - + @@ -372,7 +372,13 @@ export default function Session() {
-

Your session has ended.

+ {isSessionEnded ? ( +

Your session has ended.

+ ) : isSessionEndedPeer ? ( +

{peerUsername} has ended the session.

+ ) : isSessionEndedDisconnect ? ( +

{peerUsername} disconnected.

+ ) : null}

Redirecting you to the Questions page...

diff --git a/frontend/app/session/code-editor/code-editor.tsx b/frontend/app/session/code-editor/code-editor.tsx index 00bd963e60..3d6ecea327 100644 --- a/frontend/app/session/code-editor/code-editor.tsx +++ b/frontend/app/session/code-editor/code-editor.tsx @@ -136,6 +136,21 @@ export default function CodeEditor({ sessionId, provider, setLanguage }: CodeEdi const [langOpen, setLangOpen] = useState(false) const [lang, setLang] = useState("javascript") + + const ymap = bindingRef.current?.doc.getMap('editorSettings'); + ymap?.observe((event) => { + if (event.keysChanged.has('lang')) { + const newLang = ymap.get('lang'); + monaco?.editor.setModelLanguage(editorRef.current!.getModel()!, newLang as string); + setLang(newLang as string); + } + }); + + function setLangPropagate(newLang: string) { + setLang(newLang); + ymap?.set('lang', newLang); + } + function langCombobox() { return @@ -164,7 +179,7 @@ export default function CodeEditor({ sessionId, provider, setLanguage }: CodeEdi value={l.value} onSelect={(currentValue) => { monaco?.editor.setModelLanguage(editorRef.current!.getModel()!, currentValue); - setLang(currentValue); + setLangPropagate(currentValue); setLanguage(currentValue); setLangOpen(false); }}