Skip to content

Commit 7a55edd

Browse files
authored
Merge pull request #113 from ruiqi7/feature/collab-service
Add pop up on end session and fix end session bugs
2 parents 17de192 + 77e231a commit 7a55edd

File tree

10 files changed

+608
-347
lines changed

10 files changed

+608
-347
lines changed

backend/collab-service/src/handlers/websocketHandler.ts

Lines changed: 59 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -13,28 +13,30 @@ enum CollabEvents {
1313
UPDATE_REQUEST = "update_request",
1414
UPDATE_CURSOR_REQUEST = "update_cursor_request",
1515
RECONNECT_REQUEST = "reconnect_request",
16+
END_SESSION_REQUEST = "end_session_request",
1617

1718
// Send
1819
ROOM_READY = "room_ready",
1920
DOCUMENT_READY = "document_ready",
21+
DOCUMENT_NOT_FOUND = "document_not_found",
2022
UPDATE = "updateV2",
2123
UPDATE_CURSOR = "update_cursor",
22-
PARTNER_LEFT = "partner_left",
24+
END_SESSION = "end_session",
25+
PARTNER_DISCONNECTED = "partner_disconnected",
2326
}
2427

2528
const EXPIRY_TIME = 3600;
26-
const CONNECTION_DELAY = 3000; // time window to allow for page re-renders / refresh
29+
const CONNECTION_DELAY = 3000; // time window to allow for page re-renders
2730

2831
const userConnections = new Map<string, NodeJS.Timeout | null>();
2932
const collabSessions = new Map<string, Doc>();
3033
const partnerReadiness = new Map<string, boolean>();
3134

3235
export const handleWebsocketCollabEvents = (socket: Socket) => {
33-
socket.on(CollabEvents.JOIN, async (uid: string, roomId: string) => {
36+
socket.on(CollabEvents.JOIN, (uid: string, roomId: string) => {
3437
const connectionKey = `${uid}:${roomId}`;
3538
if (userConnections.has(connectionKey)) {
3639
clearTimeout(userConnections.get(connectionKey)!);
37-
return;
3840
}
3941
userConnections.set(connectionKey, null);
4042

@@ -47,11 +49,10 @@ export const handleWebsocketCollabEvents = (socket: Socket) => {
4749
socket.join(roomId);
4850
socket.data.roomId = roomId;
4951

50-
if (
51-
io.sockets.adapter.rooms.get(roomId)?.size === 2 &&
52-
!collabSessions.has(roomId)
53-
) {
54-
createCollabSession(roomId);
52+
if (io.sockets.adapter.rooms.get(roomId)?.size === 2) {
53+
if (!collabSessions.has(roomId)) {
54+
createCollabSession(roomId);
55+
}
5556
io.to(roomId).emit(CollabEvents.ROOM_READY, true);
5657
}
5758
});
@@ -107,7 +108,8 @@ export const handleWebsocketCollabEvents = (socket: Socket) => {
107108
if (doc) {
108109
applyUpdateV2(doc, new Uint8Array(update));
109110
} else {
110-
// TODO: error handling
111+
io.to(roomId).emit(CollabEvents.DOCUMENT_NOT_FOUND);
112+
io.sockets.adapter.rooms.delete(roomId);
111113
}
112114
}
113115
);
@@ -124,41 +126,45 @@ export const handleWebsocketCollabEvents = (socket: Socket) => {
124126

125127
socket.on(
126128
CollabEvents.LEAVE,
127-
(uid: string, roomId: string, isImmediate: boolean) => {
129+
(uid: string, roomId: string, isPartnerNotified: boolean) => {
128130
const connectionKey = `${uid}:${roomId}`;
129-
if (isImmediate || !userConnections.has(connectionKey)) {
131+
if (userConnections.has(connectionKey)) {
132+
clearTimeout(userConnections.get(connectionKey)!);
133+
}
134+
135+
if (isPartnerNotified) {
130136
handleUserLeave(uid, roomId, socket);
131137
return;
132138
}
133139

134-
clearTimeout(userConnections.get(connectionKey)!);
135-
136140
const connectionTimeout = setTimeout(() => {
137141
handleUserLeave(uid, roomId, socket);
142+
io.to(roomId).emit(CollabEvents.PARTNER_DISCONNECTED);
138143
}, CONNECTION_DELAY);
139144

140145
userConnections.set(connectionKey, connectionTimeout);
141146
}
142147
);
143148

144-
socket.on(CollabEvents.RECONNECT_REQUEST, async (roomId: string) => {
145-
// TODO: Handle recconnection
146-
socket.join(roomId);
147-
148-
const doc = getDocument(roomId);
149-
const storeData = await redisClient.get(`collaboration:${roomId}`);
149+
socket.on(
150+
CollabEvents.END_SESSION_REQUEST,
151+
(roomId: string, sessionDuration: number) => {
152+
socket.to(roomId).emit(CollabEvents.END_SESSION, sessionDuration);
153+
}
154+
);
150155

151-
if (storeData) {
152-
const tempDoc = new Doc();
153-
const update = Buffer.from(storeData, "base64");
154-
applyUpdateV2(tempDoc, new Uint8Array(update));
155-
const tempText = tempDoc.getText().toString();
156+
socket.on(CollabEvents.RECONNECT_REQUEST, (roomId: string) => {
157+
const room = io.sockets.adapter.rooms.get(roomId);
158+
if (!room || room.size < 2) {
159+
socket.join(roomId);
160+
socket.data.roomId = roomId;
161+
}
156162

157-
const text = doc.getText();
158-
doc.transact(() => {
159-
text.delete(0, text.length);
160-
text.insert(0, tempText);
161-
});
163+
if (
164+
io.sockets.adapter.rooms.get(roomId)?.size === 2 &&
165+
!collabSessions.has(roomId)
166+
) {
167+
restoreDocument(roomId);
162168
}
163169
});
164170
};
@@ -172,6 +178,7 @@ const removeCollabSession = (roomId: string) => {
172178
collabSessions.get(roomId)?.destroy();
173179
collabSessions.delete(roomId);
174180
partnerReadiness.delete(roomId);
181+
redisClient.del(roomId);
175182
};
176183

177184
const getDocument = (roomId: string) => {
@@ -188,28 +195,38 @@ const getDocument = (roomId: string) => {
188195
return doc;
189196
};
190197

191-
const saveDocument = async (roomId: string, doc: Doc) => {
198+
const saveDocument = (roomId: string, doc: Doc) => {
192199
const docState = encodeStateAsUpdateV2(doc);
193200
const docAsString = Buffer.from(docState).toString("base64");
194-
await redisClient.set(`collaboration:${roomId}`, docAsString, {
201+
redisClient.set(`collaboration:${roomId}`, docAsString, {
195202
EX: EXPIRY_TIME,
196203
});
197204
};
198205

206+
const restoreDocument = async (roomId: string) => {
207+
const doc = getDocument(roomId);
208+
const storeData = await redisClient.get(`collaboration:${roomId}`);
209+
210+
if (storeData) {
211+
const tempDoc = new Doc();
212+
const update = Buffer.from(storeData, "base64");
213+
applyUpdateV2(tempDoc, new Uint8Array(update));
214+
const tempText = tempDoc.getText().toString();
215+
216+
const text = doc.getText();
217+
doc.transact(() => {
218+
text.delete(0, text.length);
219+
text.insert(0, tempText);
220+
});
221+
}
222+
};
223+
199224
const handleUserLeave = (uid: string, roomId: string, socket: Socket) => {
200225
const connectionKey = `${uid}:${roomId}`;
201-
if (userConnections.has(connectionKey)) {
202-
clearTimeout(userConnections.get(connectionKey)!);
203-
userConnections.delete(connectionKey);
204-
}
226+
userConnections.delete(connectionKey);
205227

206228
socket.leave(roomId);
207229
socket.disconnect();
208230

209-
const room = io.sockets.adapter.rooms.get(roomId);
210-
if (!room || room.size === 0) {
211-
removeCollabSession(roomId);
212-
} else {
213-
io.to(roomId).emit(CollabEvents.PARTNER_LEFT);
214-
}
231+
removeCollabSession(roomId);
215232
};

backend/matching-service/src/handlers/websocketHandler.ts

Lines changed: 16 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -119,27 +119,24 @@ export const handleWebsocketMatchEvents = (socket: Socket) => {
119119
userConnections.delete(uid);
120120
});
121121

122-
socket.on(
123-
MatchEvents.MATCH_ACCEPT_REQUEST,
124-
(matchId: string, userId1: string, userId2: string) => {
125-
const partnerAccepted = handleMatchAccept(matchId);
126-
if (partnerAccepted) {
127-
const match = getMatchById(matchId);
128-
if (!match) {
129-
return;
130-
}
131-
132-
const { complexity, category } = match;
133-
getRandomQuestion(complexity, category).then((res) => {
134-
io.to(matchId).emit(
135-
MatchEvents.MATCH_SUCCESSFUL,
136-
res.data.question.id,
137-
res.data.question.title
138-
);
139-
});
122+
socket.on(MatchEvents.MATCH_ACCEPT_REQUEST, (matchId: string) => {
123+
const partnerAccepted = handleMatchAccept(matchId);
124+
if (partnerAccepted) {
125+
const match = getMatchById(matchId);
126+
if (!match) {
127+
return;
140128
}
129+
130+
const { complexity, category } = match;
131+
getRandomQuestion(complexity, category).then((res) => {
132+
io.to(matchId).emit(
133+
MatchEvents.MATCH_SUCCESSFUL,
134+
res.data.question.id,
135+
res.data.question.title
136+
);
137+
});
141138
}
142-
);
139+
});
143140

144141
socket.on(
145142
MatchEvents.MATCH_DECLINE_REQUEST,

frontend/src/components/CodeEditor/index.tsx

Lines changed: 30 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,17 @@ import { useEffect, useState } from "react";
88
import { initDocument } from "../../utils/collabSocket";
99
import { cursorExtension } from "../../utils/collabCursor";
1010
import { yCollab } from "y-codemirror.next";
11-
import { Text } from "yjs";
11+
import { Doc, Text } from "yjs";
1212
import { Awareness } from "y-protocols/awareness";
1313
import { useCollab } from "../../contexts/CollabContext";
14-
import { USE_COLLAB_ERROR_MESSAGE, USE_MATCH_ERROR_MESSAGE } from "../../utils/constants";
14+
import {
15+
USE_COLLAB_ERROR_MESSAGE,
16+
USE_MATCH_ERROR_MESSAGE,
17+
} from "../../utils/constants";
1518
import { useMatch } from "../../contexts/MatchContext";
1619

1720
interface CodeEditorProps {
18-
editorState?: { text: Text; awareness: Awareness };
21+
editorState?: { doc: Doc; text: Text; awareness: Awareness };
1922
uid?: string;
2023
username?: string;
2124
language: string;
@@ -46,14 +49,15 @@ const CodeEditor: React.FC<CodeEditorProps> = (props) => {
4649
throw new Error(USE_MATCH_ERROR_MESSAGE);
4750
}
4851

49-
const { matchCriteria, matchUser, partner, questionId, questionTitle } = match;
52+
const { matchCriteria, matchUser, partner, questionId, questionTitle } =
53+
match;
5054

5155
const collab = useCollab();
5256
if (!collab) {
5357
throw new Error(USE_COLLAB_ERROR_MESSAGE);
5458
}
5559

56-
const { setCode, checkDocReady } = collab;
60+
const { checkDocReady } = collab;
5761

5862
const [isEditorReady, setIsEditorReady] = useState<boolean>(false);
5963
const [isDocumentLoaded, setIsDocumentLoaded] = useState<boolean>(false);
@@ -64,25 +68,36 @@ const CodeEditor: React.FC<CodeEditorProps> = (props) => {
6468
}
6569
};
6670

67-
const handleChange = (value: string) => {
68-
setCode(value);
69-
};
70-
7171
useEffect(() => {
72-
if (isReadOnly || !isEditorReady) {
72+
if (isReadOnly || !isEditorReady || !editorState) {
7373
return;
7474
}
7575

7676
const loadTemplate = async () => {
77-
if (matchUser && partner && matchCriteria && questionId && questionTitle) {
78-
checkDocReady();
79-
await initDocument(uid, roomId, template, matchUser.id, partner.id, matchCriteria.language, questionId, questionTitle);
77+
if (
78+
matchUser &&
79+
partner &&
80+
matchCriteria &&
81+
questionId &&
82+
questionTitle
83+
) {
84+
checkDocReady(roomId, editorState.doc, setIsDocumentLoaded);
85+
await initDocument(
86+
uid,
87+
roomId,
88+
template,
89+
matchUser.id,
90+
partner.id,
91+
matchCriteria.language,
92+
questionId,
93+
questionTitle
94+
);
8095
setIsDocumentLoaded(true);
8196
}
8297
};
8398
loadTemplate();
8499
// eslint-disable-next-line react-hooks/exhaustive-deps
85-
}, [isReadOnly, isEditorReady]);
100+
}, [isReadOnly, isEditorReady, editorState]);
86101

87102
return (
88103
<CodeMirror
@@ -92,7 +107,6 @@ const CodeEditor: React.FC<CodeEditorProps> = (props) => {
92107
width="100%"
93108
basicSetup={false}
94109
id="codeEditor"
95-
onChange={handleChange}
96110
extensions={[
97111
indentUnit.of("\t"),
98112
basicSetup(),
@@ -109,9 +123,7 @@ const CodeEditor: React.FC<CodeEditorProps> = (props) => {
109123
]}
110124
value={isReadOnly ? template : undefined}
111125
placeholder={
112-
!isReadOnly && !isDocumentLoaded
113-
? "Loading code template..."
114-
: undefined
126+
!isReadOnly && !isDocumentLoaded ? "Loading the code..." : undefined
115127
}
116128
/>
117129
);

0 commit comments

Comments
 (0)