Skip to content

Commit

Permalink
Merge branch 'development' into deployment
Browse files Browse the repository at this point in the history
  • Loading branch information
jolynloh committed Nov 10, 2024
2 parents 1c043de + d1e3dbc commit 086f03b
Show file tree
Hide file tree
Showing 42 changed files with 1,309 additions and 697 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ describe("Code execution routes", () => {
expect(response.body.data).toBeInstanceOf(Array);
expect(response.body.data[0]).toHaveProperty("isMatch", true);
expect(response.body.data[0]["isMatch"]).toBe(true);
expect(response.body.data[1]["isMatch"]).toBe(false);
expect(response.body.data[1]["isMatch"]).toBe(true);
});
});
});
1 change: 1 addition & 0 deletions backend/collab-service/.env.sample
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ ORIGINS=http://localhost:5173,http://127.0.0.1:5173

# Other service APIs
USER_SERVICE_URL=http://user-service:3001/api
QN_HISTORY_SERVICE_URL=http://qn-history-service:3006/api/qnhistories

# Redis configuration
REDIS_URI=redis://collab-service-redis:6379
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,20 +12,31 @@ const qnHistoryService = axios.create({
});

export const createQuestionHistory = (
userIds: string[],
questionId: string,
title: string,
submissionStatus: string,
code: string,
language: string,
...userIds: string[]
authToken: string
) => {
const dateAttempted = new Date();
return qnHistoryService.post("/", {
userIds,
questionId,
title,
submissionStatus,
language,
dateAttempted,
timeTaken: 0,
});
return qnHistoryService.post(
"/",
{
userIds,
questionId,
title,
submissionStatus,
dateAttempted,
timeTaken: 0,
code,
language,
},
{
headers: {
Authorization: authToken,
},
}
);
};
156 changes: 102 additions & 54 deletions backend/collab-service/src/handlers/websocketHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Socket } from "socket.io";
import { io } from "../server";
import redisClient from "../config/redis";
import { Doc, applyUpdateV2, encodeStateAsUpdateV2 } from "yjs";
import { createQuestionHistory } from "../api/questionHistoryService";

enum CollabEvents {
// Receive
Expand All @@ -12,28 +13,30 @@ enum CollabEvents {
UPDATE_REQUEST = "update_request",
UPDATE_CURSOR_REQUEST = "update_cursor_request",
RECONNECT_REQUEST = "reconnect_request",
END_SESSION_REQUEST = "end_session_request",

// Send
ROOM_READY = "room_ready",
DOCUMENT_READY = "document_ready",
DOCUMENT_NOT_FOUND = "document_not_found",
UPDATE = "updateV2",
UPDATE_CURSOR = "update_cursor",
PARTNER_LEFT = "partner_left",
END_SESSION = "end_session",
PARTNER_DISCONNECTED = "partner_disconnected",
}

const EXPIRY_TIME = 3600;
const CONNECTION_DELAY = 3000; // time window to allow for page re-renders / refresh
const CONNECTION_DELAY = 3000; // time window to allow for page re-renders

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

export const handleWebsocketCollabEvents = (socket: Socket) => {
socket.on(CollabEvents.JOIN, async (uid: string, roomId: string) => {
socket.on(CollabEvents.JOIN, (uid: string, roomId: string) => {
const connectionKey = `${uid}:${roomId}`;
if (userConnections.has(connectionKey)) {
clearTimeout(userConnections.get(connectionKey)!);
return;
}
userConnections.set(connectionKey, null);

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

if (
io.sockets.adapter.rooms.get(roomId)?.size === 2 &&
!collabSessions.has(roomId)
) {
createCollabSession(roomId);
if (io.sockets.adapter.rooms.get(roomId)?.size === 2) {
if (!collabSessions.has(roomId)) {
createCollabSession(roomId);
}
io.to(roomId).emit(CollabEvents.ROOM_READY, true);
}
});

socket.on(CollabEvents.INIT_DOCUMENT, (roomId: string, template: string) => {
const doc = getDocument(roomId);
const isPartnerReady = partnerReadiness.get(roomId);

if (isPartnerReady && doc.getText().length === 0) {
doc.transact(() => {
doc.getText().insert(0, template);
});
io.to(roomId).emit(CollabEvents.DOCUMENT_READY);
} else {
partnerReadiness.set(roomId, true);
socket.on(
CollabEvents.INIT_DOCUMENT,
(
roomId: string,
template: string,
uid1: string,
uid2: string,
language: string,
qnId: string,
qnTitle: string
) => {
const doc = getDocument(roomId);
const isPartnerReady = partnerReadiness.get(roomId);

if (isPartnerReady && doc.getText().length === 0) {
const token =
socket.handshake.headers.authorization || socket.handshake.auth.token;
createQuestionHistory(
[uid1, uid2],
qnId,
qnTitle,
"Attempted",
template,
language,
token
)
.then((res) => {
doc.transact(() => {
doc.getText().insert(0, template);
});
io.to(roomId).emit(
CollabEvents.DOCUMENT_READY,
res.data.qnHistory.id
);
})
.catch((err) => {
console.log(err);
});
} else {
partnerReadiness.set(roomId, true);
}
}
});
);

socket.on(
CollabEvents.UPDATE_REQUEST,
Expand All @@ -76,7 +108,8 @@ export const handleWebsocketCollabEvents = (socket: Socket) => {
if (doc) {
applyUpdateV2(doc, new Uint8Array(update));
} else {
// TODO: error handling
io.to(roomId).emit(CollabEvents.DOCUMENT_NOT_FOUND);
io.sockets.adapter.rooms.delete(roomId);
}
}
);
Expand All @@ -93,41 +126,45 @@ export const handleWebsocketCollabEvents = (socket: Socket) => {

socket.on(
CollabEvents.LEAVE,
(uid: string, roomId: string, isImmediate: boolean) => {
(uid: string, roomId: string, isPartnerNotified: boolean) => {
const connectionKey = `${uid}:${roomId}`;
if (isImmediate || !userConnections.has(connectionKey)) {
if (userConnections.has(connectionKey)) {
clearTimeout(userConnections.get(connectionKey)!);
}

if (isPartnerNotified) {
handleUserLeave(uid, roomId, socket);
return;
}

clearTimeout(userConnections.get(connectionKey)!);

const connectionTimeout = setTimeout(() => {
handleUserLeave(uid, roomId, socket);
io.to(roomId).emit(CollabEvents.PARTNER_DISCONNECTED);
}, CONNECTION_DELAY);

userConnections.set(connectionKey, connectionTimeout);
}
);

socket.on(CollabEvents.RECONNECT_REQUEST, async (roomId: string) => {
// TODO: Handle recconnection
socket.join(roomId);

const doc = getDocument(roomId);
const storeData = await redisClient.get(`collaboration:${roomId}`);
socket.on(
CollabEvents.END_SESSION_REQUEST,
(roomId: string, sessionDuration: number) => {
socket.to(roomId).emit(CollabEvents.END_SESSION, sessionDuration);
}
);

if (storeData) {
const tempDoc = new Doc();
const update = Buffer.from(storeData, "base64");
applyUpdateV2(tempDoc, new Uint8Array(update));
const tempText = tempDoc.getText().toString();
socket.on(CollabEvents.RECONNECT_REQUEST, (roomId: string) => {
const room = io.sockets.adapter.rooms.get(roomId);
if (!room || room.size < 2) {
socket.join(roomId);
socket.data.roomId = roomId;
}

const text = doc.getText();
doc.transact(() => {
text.delete(0, text.length);
text.insert(0, tempText);
});
if (
io.sockets.adapter.rooms.get(roomId)?.size === 2 &&
!collabSessions.has(roomId)
) {
restoreDocument(roomId);
}
});
};
Expand All @@ -141,6 +178,7 @@ const removeCollabSession = (roomId: string) => {
collabSessions.get(roomId)?.destroy();
collabSessions.delete(roomId);
partnerReadiness.delete(roomId);
redisClient.del(roomId);
};

const getDocument = (roomId: string) => {
Expand All @@ -157,28 +195,38 @@ const getDocument = (roomId: string) => {
return doc;
};

const saveDocument = async (roomId: string, doc: Doc) => {
const saveDocument = (roomId: string, doc: Doc) => {
const docState = encodeStateAsUpdateV2(doc);
const docAsString = Buffer.from(docState).toString("base64");
await redisClient.set(`collaboration:${roomId}`, docAsString, {
redisClient.set(`collaboration:${roomId}`, docAsString, {
EX: EXPIRY_TIME,
});
};

const restoreDocument = async (roomId: string) => {
const doc = getDocument(roomId);
const storeData = await redisClient.get(`collaboration:${roomId}`);

if (storeData) {
const tempDoc = new Doc();
const update = Buffer.from(storeData, "base64");
applyUpdateV2(tempDoc, new Uint8Array(update));
const tempText = tempDoc.getText().toString();

const text = doc.getText();
doc.transact(() => {
text.delete(0, text.length);
text.insert(0, tempText);
});
}
};

const handleUserLeave = (uid: string, roomId: string, socket: Socket) => {
const connectionKey = `${uid}:${roomId}`;
if (userConnections.has(connectionKey)) {
clearTimeout(userConnections.get(connectionKey)!);
userConnections.delete(connectionKey);
}
userConnections.delete(connectionKey);

socket.leave(roomId);
socket.disconnect();

const room = io.sockets.adapter.rooms.get(roomId);
if (!room || room.size === 0) {
removeCollabSession(roomId);
} else {
io.to(roomId).emit(CollabEvents.PARTNER_LEFT);
}
removeCollabSession(roomId);
};
1 change: 0 additions & 1 deletion backend/matching-service/.env.sample
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ ORIGINS=http://localhost:5173,http://127.0.0.1:5173

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

# RabbitMq configuration
Expand Down
46 changes: 16 additions & 30 deletions backend/matching-service/src/handlers/websocketHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import {
import { io } from "../server";
import { v4 as uuidv4 } from "uuid";
import { getRandomQuestion } from "../api/questionService";
import { createQuestionHistory } from "../api/questionHistoryService";

enum MatchEvents {
// Receive
Expand Down Expand Up @@ -120,37 +119,24 @@ export const handleWebsocketMatchEvents = (socket: Socket) => {
userConnections.delete(uid);
});

socket.on(
MatchEvents.MATCH_ACCEPT_REQUEST,
(matchId: string, userId1: string, userId2: string) => {
const partnerAccepted = handleMatchAccept(matchId);
if (partnerAccepted) {
const match = getMatchById(matchId);
if (!match) {
return;
}

const { complexity, category, language } = match;
getRandomQuestion(complexity, category).then((res) => {
const qnId = res.data.question.id;
createQuestionHistory(
qnId,
res.data.question.title,
"Attempted",
language,
userId1,
userId2
).then((res) => {
io.to(matchId).emit(
MatchEvents.MATCH_SUCCESSFUL,
qnId,
res.data.qnHistory.id
);
});
});
socket.on(MatchEvents.MATCH_ACCEPT_REQUEST, (matchId: string) => {
const partnerAccepted = handleMatchAccept(matchId);
if (partnerAccepted) {
const match = getMatchById(matchId);
if (!match) {
return;
}

const { complexity, category } = match;
getRandomQuestion(complexity, category).then((res) => {
io.to(matchId).emit(
MatchEvents.MATCH_SUCCESSFUL,
res.data.question.id,
res.data.question.title
);
});
}
);
});

socket.on(
MatchEvents.MATCH_DECLINE_REQUEST,
Expand Down
1 change: 0 additions & 1 deletion backend/matching-service/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import app, { allowedOrigins } from "./app.ts";
import { handleWebsocketMatchEvents } from "./handlers/websocketHandler.ts";
import { Server } from "socket.io";
import { connectToRabbitMq } from "./config/rabbitmq.ts";
import { verifyToken } from "./api/userService.ts";
import { verifyUserToken } from "./middlewares/basicAccessControl.ts";

const server = http.createServer(app);
Expand Down
3 changes: 3 additions & 0 deletions backend/qn-history-service/.env.sample
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ SERVICE_PORT=3006
# Origins for cors
ORIGINS=http://localhost:5173,http://127.0.0.1:5173

# Other services
USER_SERVICE_URL=http://user-service:3001/api

# Tests
MONGO_URI_TEST=mongodb://mongo:mongo@test-mongo:27017/

Expand Down
Loading

0 comments on commit 086f03b

Please sign in to comment.