Skip to content

Commit

Permalink
Merge pull request #65 from brendantwh/collab
Browse files Browse the repository at this point in the history
Improve session end handling, IDE language sync
  • Loading branch information
brendantwh authored Nov 8, 2024
2 parents 2eced45 + 1ae5e0c commit 9e83fc9
Show file tree
Hide file tree
Showing 9 changed files with 139 additions and 54 deletions.
File renamed without changes.
100 changes: 100 additions & 0 deletions backend/collab-service/index.js
Original file line number Diff line number Diff line change
@@ -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();
File renamed without changes.
File renamed without changes.
37 changes: 0 additions & 37 deletions backend/signaling-service/index.js

This file was deleted.

4 changes: 2 additions & 2 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
3 changes: 2 additions & 1 deletion frontend/app/(authenticated)/questions/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
32 changes: 19 additions & 13 deletions frontend/app/session/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ export default function Session() {
const [controller, setController] = useState<AbortController | null>(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");

Expand Down Expand Up @@ -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(() => {
Expand All @@ -132,7 +134,7 @@ export default function Session() {
};
cleanup();
}
}, [isSessionEnded, isHistoryApiCalled, callUserHistoryAPI, router]);
}, [isSessionEnded, isHistoryApiCalled, callUserHistoryAPI, router, isSessionEndedPeer, isSessionEndedDisconnect]);

useEffect(() => {
setIsClient(true);
Expand Down Expand Up @@ -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);
}
},
});
Expand All @@ -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]);


Expand Down Expand Up @@ -292,7 +292,7 @@ export default function Session() {
setIsSessionEnded(true)
setIsEndDialogOpen(false)
}}
disabled={isSessionEnded}
disabled={isSessionEnded || isSessionEndedPeer || isSessionEndedDisconnect}
>
End session
</Button>
Expand Down Expand Up @@ -361,7 +361,7 @@ export default function Session() {
</ResizablePanelGroup>
</div>

<Dialog open={isSessionEnded}>
<Dialog open={isSessionEnded || isSessionEndedPeer || isSessionEndedDisconnect}>
<DialogContent
className="laptop:max-w-[40vw] bg-white text-black font-sans rounded-2xl [&>button]:hidden"
>
Expand All @@ -372,7 +372,13 @@ export default function Session() {
<DialogDescription className="hidden"></DialogDescription>
</DialogHeader>
<div className="flex flex-col w-full gap-1 py-4 justify-start">
<p>Your session has ended.</p>
{isSessionEnded ? (
<p>Your session has ended.</p>
) : isSessionEndedPeer ? (
<p><span className="font-semibold">{peerUsername}</span> has ended the session.</p>
) : isSessionEndedDisconnect ? (
<p><span className="font-semibold">{peerUsername}</span> disconnected.</p>
) : null}
<p>Redirecting you to the Questions page...</p>
</div>
</DialogContent>
Expand Down
17 changes: 16 additions & 1 deletion frontend/app/session/code-editor/code-editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 <Popover open={langOpen} onOpenChange={setLangOpen}>
<PopoverTrigger asChild>
Expand Down Expand Up @@ -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);
}}
Expand Down

0 comments on commit 9e83fc9

Please sign in to comment.