Skip to content

Commit 086f03b

Browse files
committed
Merge branch 'development' into deployment
2 parents 1c043de + d1e3dbc commit 086f03b

File tree

42 files changed

+1309
-697
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

42 files changed

+1309
-697
lines changed

backend/code-execution-service/tests/codeExecutionRoutes.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ describe("Code execution routes", () => {
9090
expect(response.body.data).toBeInstanceOf(Array);
9191
expect(response.body.data[0]).toHaveProperty("isMatch", true);
9292
expect(response.body.data[0]["isMatch"]).toBe(true);
93-
expect(response.body.data[1]["isMatch"]).toBe(false);
93+
expect(response.body.data[1]["isMatch"]).toBe(true);
9494
});
9595
});
9696
});

backend/collab-service/.env.sample

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ ORIGINS=http://localhost:5173,http://127.0.0.1:5173
66

77
# Other service APIs
88
USER_SERVICE_URL=http://user-service:3001/api
9+
QN_HISTORY_SERVICE_URL=http://qn-history-service:3006/api/qnhistories
910

1011
# Redis configuration
1112
REDIS_URI=redis://collab-service-redis:6379

backend/matching-service/src/api/questionHistoryService.ts renamed to backend/collab-service/src/api/questionHistoryService.ts

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -12,20 +12,31 @@ const qnHistoryService = axios.create({
1212
});
1313

1414
export const createQuestionHistory = (
15+
userIds: string[],
1516
questionId: string,
1617
title: string,
1718
submissionStatus: string,
19+
code: string,
1820
language: string,
19-
...userIds: string[]
21+
authToken: string
2022
) => {
2123
const dateAttempted = new Date();
22-
return qnHistoryService.post("/", {
23-
userIds,
24-
questionId,
25-
title,
26-
submissionStatus,
27-
language,
28-
dateAttempted,
29-
timeTaken: 0,
30-
});
24+
return qnHistoryService.post(
25+
"/",
26+
{
27+
userIds,
28+
questionId,
29+
title,
30+
submissionStatus,
31+
dateAttempted,
32+
timeTaken: 0,
33+
code,
34+
language,
35+
},
36+
{
37+
headers: {
38+
Authorization: authToken,
39+
},
40+
}
41+
);
3142
};

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

Lines changed: 102 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { Socket } from "socket.io";
22
import { io } from "../server";
33
import redisClient from "../config/redis";
44
import { Doc, applyUpdateV2, encodeStateAsUpdateV2 } from "yjs";
5+
import { createQuestionHistory } from "../api/questionHistoryService";
56

67
enum CollabEvents {
78
// Receive
@@ -12,28 +13,30 @@ enum CollabEvents {
1213
UPDATE_REQUEST = "update_request",
1314
UPDATE_CURSOR_REQUEST = "update_cursor_request",
1415
RECONNECT_REQUEST = "reconnect_request",
16+
END_SESSION_REQUEST = "end_session_request",
1517

1618
// Send
1719
ROOM_READY = "room_ready",
1820
DOCUMENT_READY = "document_ready",
21+
DOCUMENT_NOT_FOUND = "document_not_found",
1922
UPDATE = "updateV2",
2023
UPDATE_CURSOR = "update_cursor",
21-
PARTNER_LEFT = "partner_left",
24+
END_SESSION = "end_session",
25+
PARTNER_DISCONNECTED = "partner_disconnected",
2226
}
2327

2428
const EXPIRY_TIME = 3600;
25-
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
2630

2731
const userConnections = new Map<string, NodeJS.Timeout | null>();
2832
const collabSessions = new Map<string, Doc>();
2933
const partnerReadiness = new Map<string, boolean>();
3034

3135
export const handleWebsocketCollabEvents = (socket: Socket) => {
32-
socket.on(CollabEvents.JOIN, async (uid: string, roomId: string) => {
36+
socket.on(CollabEvents.JOIN, (uid: string, roomId: string) => {
3337
const connectionKey = `${uid}:${roomId}`;
3438
if (userConnections.has(connectionKey)) {
3539
clearTimeout(userConnections.get(connectionKey)!);
36-
return;
3740
}
3841
userConnections.set(connectionKey, null);
3942

@@ -46,28 +49,57 @@ export const handleWebsocketCollabEvents = (socket: Socket) => {
4649
socket.join(roomId);
4750
socket.data.roomId = roomId;
4851

49-
if (
50-
io.sockets.adapter.rooms.get(roomId)?.size === 2 &&
51-
!collabSessions.has(roomId)
52-
) {
53-
createCollabSession(roomId);
52+
if (io.sockets.adapter.rooms.get(roomId)?.size === 2) {
53+
if (!collabSessions.has(roomId)) {
54+
createCollabSession(roomId);
55+
}
5456
io.to(roomId).emit(CollabEvents.ROOM_READY, true);
5557
}
5658
});
5759

58-
socket.on(CollabEvents.INIT_DOCUMENT, (roomId: string, template: string) => {
59-
const doc = getDocument(roomId);
60-
const isPartnerReady = partnerReadiness.get(roomId);
61-
62-
if (isPartnerReady && doc.getText().length === 0) {
63-
doc.transact(() => {
64-
doc.getText().insert(0, template);
65-
});
66-
io.to(roomId).emit(CollabEvents.DOCUMENT_READY);
67-
} else {
68-
partnerReadiness.set(roomId, true);
60+
socket.on(
61+
CollabEvents.INIT_DOCUMENT,
62+
(
63+
roomId: string,
64+
template: string,
65+
uid1: string,
66+
uid2: string,
67+
language: string,
68+
qnId: string,
69+
qnTitle: string
70+
) => {
71+
const doc = getDocument(roomId);
72+
const isPartnerReady = partnerReadiness.get(roomId);
73+
74+
if (isPartnerReady && doc.getText().length === 0) {
75+
const token =
76+
socket.handshake.headers.authorization || socket.handshake.auth.token;
77+
createQuestionHistory(
78+
[uid1, uid2],
79+
qnId,
80+
qnTitle,
81+
"Attempted",
82+
template,
83+
language,
84+
token
85+
)
86+
.then((res) => {
87+
doc.transact(() => {
88+
doc.getText().insert(0, template);
89+
});
90+
io.to(roomId).emit(
91+
CollabEvents.DOCUMENT_READY,
92+
res.data.qnHistory.id
93+
);
94+
})
95+
.catch((err) => {
96+
console.log(err);
97+
});
98+
} else {
99+
partnerReadiness.set(roomId, true);
100+
}
69101
}
70-
});
102+
);
71103

72104
socket.on(
73105
CollabEvents.UPDATE_REQUEST,
@@ -76,7 +108,8 @@ export const handleWebsocketCollabEvents = (socket: Socket) => {
76108
if (doc) {
77109
applyUpdateV2(doc, new Uint8Array(update));
78110
} else {
79-
// TODO: error handling
111+
io.to(roomId).emit(CollabEvents.DOCUMENT_NOT_FOUND);
112+
io.sockets.adapter.rooms.delete(roomId);
80113
}
81114
}
82115
);
@@ -93,41 +126,45 @@ export const handleWebsocketCollabEvents = (socket: Socket) => {
93126

94127
socket.on(
95128
CollabEvents.LEAVE,
96-
(uid: string, roomId: string, isImmediate: boolean) => {
129+
(uid: string, roomId: string, isPartnerNotified: boolean) => {
97130
const connectionKey = `${uid}:${roomId}`;
98-
if (isImmediate || !userConnections.has(connectionKey)) {
131+
if (userConnections.has(connectionKey)) {
132+
clearTimeout(userConnections.get(connectionKey)!);
133+
}
134+
135+
if (isPartnerNotified) {
99136
handleUserLeave(uid, roomId, socket);
100137
return;
101138
}
102139

103-
clearTimeout(userConnections.get(connectionKey)!);
104-
105140
const connectionTimeout = setTimeout(() => {
106141
handleUserLeave(uid, roomId, socket);
142+
io.to(roomId).emit(CollabEvents.PARTNER_DISCONNECTED);
107143
}, CONNECTION_DELAY);
108144

109145
userConnections.set(connectionKey, connectionTimeout);
110146
}
111147
);
112148

113-
socket.on(CollabEvents.RECONNECT_REQUEST, async (roomId: string) => {
114-
// TODO: Handle recconnection
115-
socket.join(roomId);
116-
117-
const doc = getDocument(roomId);
118-
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+
);
119155

120-
if (storeData) {
121-
const tempDoc = new Doc();
122-
const update = Buffer.from(storeData, "base64");
123-
applyUpdateV2(tempDoc, new Uint8Array(update));
124-
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+
}
125162

126-
const text = doc.getText();
127-
doc.transact(() => {
128-
text.delete(0, text.length);
129-
text.insert(0, tempText);
130-
});
163+
if (
164+
io.sockets.adapter.rooms.get(roomId)?.size === 2 &&
165+
!collabSessions.has(roomId)
166+
) {
167+
restoreDocument(roomId);
131168
}
132169
});
133170
};
@@ -141,6 +178,7 @@ const removeCollabSession = (roomId: string) => {
141178
collabSessions.get(roomId)?.destroy();
142179
collabSessions.delete(roomId);
143180
partnerReadiness.delete(roomId);
181+
redisClient.del(roomId);
144182
};
145183

146184
const getDocument = (roomId: string) => {
@@ -157,28 +195,38 @@ const getDocument = (roomId: string) => {
157195
return doc;
158196
};
159197

160-
const saveDocument = async (roomId: string, doc: Doc) => {
198+
const saveDocument = (roomId: string, doc: Doc) => {
161199
const docState = encodeStateAsUpdateV2(doc);
162200
const docAsString = Buffer.from(docState).toString("base64");
163-
await redisClient.set(`collaboration:${roomId}`, docAsString, {
201+
redisClient.set(`collaboration:${roomId}`, docAsString, {
164202
EX: EXPIRY_TIME,
165203
});
166204
};
167205

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+
168224
const handleUserLeave = (uid: string, roomId: string, socket: Socket) => {
169225
const connectionKey = `${uid}:${roomId}`;
170-
if (userConnections.has(connectionKey)) {
171-
clearTimeout(userConnections.get(connectionKey)!);
172-
userConnections.delete(connectionKey);
173-
}
226+
userConnections.delete(connectionKey);
174227

175228
socket.leave(roomId);
176229
socket.disconnect();
177230

178-
const room = io.sockets.adapter.rooms.get(roomId);
179-
if (!room || room.size === 0) {
180-
removeCollabSession(roomId);
181-
} else {
182-
io.to(roomId).emit(CollabEvents.PARTNER_LEFT);
183-
}
231+
removeCollabSession(roomId);
184232
};

backend/matching-service/.env.sample

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ ORIGINS=http://localhost:5173,http://127.0.0.1:5173
66

77
# Other service APIs
88
QUESTION_SERVICE_URL=http://question-service:3000/api/questions
9-
QN_HISTORY_SERVICE_URL=http://qn-history-service:3006/api/qnhistories
109
USER_SERVICE_URL=http://user-service:3001/api
1110

1211
# RabbitMq configuration

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

Lines changed: 16 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ import {
1212
import { io } from "../server";
1313
import { v4 as uuidv4 } from "uuid";
1414
import { getRandomQuestion } from "../api/questionService";
15-
import { createQuestionHistory } from "../api/questionHistoryService";
1615

1716
enum MatchEvents {
1817
// Receive
@@ -120,37 +119,24 @@ export const handleWebsocketMatchEvents = (socket: Socket) => {
120119
userConnections.delete(uid);
121120
});
122121

123-
socket.on(
124-
MatchEvents.MATCH_ACCEPT_REQUEST,
125-
(matchId: string, userId1: string, userId2: string) => {
126-
const partnerAccepted = handleMatchAccept(matchId);
127-
if (partnerAccepted) {
128-
const match = getMatchById(matchId);
129-
if (!match) {
130-
return;
131-
}
132-
133-
const { complexity, category, language } = match;
134-
getRandomQuestion(complexity, category).then((res) => {
135-
const qnId = res.data.question.id;
136-
createQuestionHistory(
137-
qnId,
138-
res.data.question.title,
139-
"Attempted",
140-
language,
141-
userId1,
142-
userId2
143-
).then((res) => {
144-
io.to(matchId).emit(
145-
MatchEvents.MATCH_SUCCESSFUL,
146-
qnId,
147-
res.data.qnHistory.id
148-
);
149-
});
150-
});
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;
151128
}
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+
});
152138
}
153-
);
139+
});
154140

155141
socket.on(
156142
MatchEvents.MATCH_DECLINE_REQUEST,

backend/matching-service/src/server.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import app, { allowedOrigins } from "./app.ts";
33
import { handleWebsocketMatchEvents } from "./handlers/websocketHandler.ts";
44
import { Server } from "socket.io";
55
import { connectToRabbitMq } from "./config/rabbitmq.ts";
6-
import { verifyToken } from "./api/userService.ts";
76
import { verifyUserToken } from "./middlewares/basicAccessControl.ts";
87

98
const server = http.createServer(app);

backend/qn-history-service/.env.sample

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ SERVICE_PORT=3006
44
# Origins for cors
55
ORIGINS=http://localhost:5173,http://127.0.0.1:5173
66

7+
# Other services
8+
USER_SERVICE_URL=http://user-service:3001/api
9+
710
# Tests
811
MONGO_URI_TEST=mongodb://mongo:mongo@test-mongo:27017/
912

0 commit comments

Comments
 (0)