From a9479299ce2c41fde1481ce18ff0bf399681e36b Mon Sep 17 00:00:00 2001 From: Kavinda L Jayarathna <128674785+kavindalj@users.noreply.github.com> Date: Fri, 24 Oct 2025 08:42:34 +0530 Subject: [PATCH 1/7] feat(socket): broadcast new_issue to /new-issue namespace --- docker-compose.yml | 2 +- src/controllers/issueController.js | 4 ++++ src/socket/socket.js | 34 +++++++++++++++++++++++------- 3 files changed, 31 insertions(+), 9 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 0a22368..1b8ea21 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -10,7 +10,7 @@ services: ports: - "${POSTGRES_PORT}:5432" volumes: - - postgres_data:/var/lib/postgresql/data + - postgres_data:/var/lib/postgresql # MinIO Object Storage minio: diff --git a/src/controllers/issueController.js b/src/controllers/issueController.js index 8305d30..3d1b0d4 100644 --- a/src/controllers/issueController.js +++ b/src/controllers/issueController.js @@ -1,4 +1,5 @@ import issueService from '../services/issueService.js'; +import { notifyNewIssue } from '../socket/socket.js'; class IssueController { // POST /api/issues - Create new issue @@ -47,6 +48,9 @@ class IssueController { const result = await issueService.createIssue(issueData); if (result.success) { + // Notify all Maintenance Executives about the new issue(real-time) + try { notifyNewIssue(result); } catch (e) { console.error(e);} + return res.status(201).json(result); } else { return res.status(400).json(result); diff --git a/src/socket/socket.js b/src/socket/socket.js index 38de0fd..fd30356 100644 --- a/src/socket/socket.js +++ b/src/socket/socket.js @@ -2,10 +2,18 @@ import 'dotenv/config'; import { Server } from "socket.io"; import jwt from "jsonwebtoken"; +let ioInstance = null; +let newIssue = null; +let assign = null; + export const setupSocket = (server) => { const io = new Server(server, { cors: { origin: "*" } }); + ioInstance = io; + + newIssue = io.of("/new-issue"); // the "new-issue" namespace for creating new issues + assign = io.of("/assign"); // the "assign" namespace for assigning issues // Middleware to authenticate user // io.use((socket, next) => { @@ -21,16 +29,16 @@ export const setupSocket = (server) => { // } // }); - io.on("connection", async (socket) => { - console.log(`User connected with socket ${socket.id}`); + newIssue.on("connection", async (socket) => { + console.log(`User connected(new-issue) with socket ${socket.id}`); - //Recieve message - socket.on("send_message", async (message) => { - console.log(`Message received from ${socket.id}:`, message); + socket.on("disconnect", () => { + console.log(`User disconnected: ${socket.id}`); + }); + }); - // Broadcast to everyone EXCEPT the sender - socket.broadcast.emit("receive_message", message); - }) + assign.on("connection", async (socket) => { + console.log(`User connected with socket ${socket.id}`); socket.on("disconnect", () => { console.log(`User disconnected: ${socket.id}`); @@ -38,4 +46,14 @@ export const setupSocket = (server) => { }); return io; +} + +//Emit a new_issue event to all clients(Maintenance Executives) connected to the /new-issue namespace. +export function notifyNewIssue(issue) { + if (!newIssue) return; + try { + newIssue.emit('new_issue', issue); + } catch (err) { + console.error('notifyNewIssue error:', err); + } } \ No newline at end of file From 1e3165321cef783fcbefc62f29125f732c693e47 Mon Sep 17 00:00:00 2001 From: Kavinda L Jayarathna <128674785+kavindalj@users.noreply.github.com> Date: Fri, 24 Oct 2025 12:35:47 +0530 Subject: [PATCH 2/7] feat(socket): notify assigned technician via /assign namespace --- src/controllers/issueController.js | 9 ++++++- src/socket/socket.js | 41 +++++++++++++++++++++++++++++- 2 files changed, 48 insertions(+), 2 deletions(-) diff --git a/src/controllers/issueController.js b/src/controllers/issueController.js index 3d1b0d4..ee14867 100644 --- a/src/controllers/issueController.js +++ b/src/controllers/issueController.js @@ -1,5 +1,5 @@ import issueService from '../services/issueService.js'; -import { notifyNewIssue } from '../socket/socket.js'; +import { notifyNewIssue, notifyAssign } from '../socket/socket.js'; class IssueController { // POST /api/issues - Create new issue @@ -224,6 +224,13 @@ class IssueController { const result = await issueService.assignTechnician(parseInt(id), parseInt(technician_id)); if (result.success) { + // broadcast assignment to the assigned technician (real-time) + try { + notifyAssign(parseInt(technician_id), result); + } catch (emitErr) { + console.error('notifyAssign failed:', emitErr); + } + return res.status(200).json(result); } else { return res.status(400).json(result); diff --git a/src/socket/socket.js b/src/socket/socket.js index fd30356..1d4c2bd 100644 --- a/src/socket/socket.js +++ b/src/socket/socket.js @@ -28,17 +28,46 @@ export const setupSocket = (server) => { // next(new Error("Invalid token")); // } // }); + let userID = 1; // temp for testing + let role = "technician"; // temp for testing + newIssue.on("connection", async (socket) => { + // testing without auth + role = "maintenance_executive"; + + if (role !== "maintenance_executive") { + console.log(`Non-ME tried to connect to new-issue namespace: ${socket.id}`); + socket.disconnect(); + return; + } console.log(`User connected(new-issue) with socket ${socket.id}`); + socket.on("send_message", (message) => { + console.log(message); + }); + socket.on("disconnect", () => { console.log(`User disconnected: ${socket.id}`); }); }); assign.on("connection", async (socket) => { - console.log(`User connected with socket ${socket.id}`); + // testing without auth + role = "technician"; + + if (role !== "technician") { + console.log(`Non-technician tried to connect to assign namespace: ${socket.id}`); + socket.disconnect(); + return; + } + const room = `technician:${userID}`; + socket.join(room); + console.log(`Technician ${userID} joined room: ${room} with socket ${socket.id}`); + + socket.on("send_message", (message) => { + console.log(message); + }); socket.on("disconnect", () => { console.log(`User disconnected: ${socket.id}`); @@ -56,4 +85,14 @@ export function notifyNewIssue(issue) { } catch (err) { console.error('notifyNewIssue error:', err); } +} + +export function notifyAssign(id,issue) { + if (!assign) return; + try { + const room = `technician:${id}`; + assign.to(room).emit('assigned_issue', issue); + } catch (err) { + console.error('notifyAssign error:', err); + } } \ No newline at end of file From 7c25d6107c797e6e764d4ba7f4e42585a547d715 Mon Sep 17 00:00:00 2001 From: Kavinda L Jayarathna <128674785+kavindalj@users.noreply.github.com> Date: Fri, 24 Oct 2025 18:49:15 +0530 Subject: [PATCH 3/7] feat(socket): add dynamic /issue- namespace with user & issue rooms --- scripts/issue-chat.html | 148 ++++++++++++++++++++++++++++ scripts/issue-client1.js | 21 ++++ scripts/socket-client-ME.js | 18 ++++ scripts/socket-client-Technician.js | 16 +++ src/controllers/issueController.js | 9 +- src/socket/socket.js | 48 ++++++++- 6 files changed, 256 insertions(+), 4 deletions(-) create mode 100644 scripts/issue-chat.html create mode 100644 scripts/issue-client1.js create mode 100644 scripts/socket-client-ME.js create mode 100644 scripts/socket-client-Technician.js diff --git a/scripts/issue-chat.html b/scripts/issue-chat.html new file mode 100644 index 0000000..1026496 --- /dev/null +++ b/scripts/issue-chat.html @@ -0,0 +1,148 @@ + + + + + + Issue Namespace Chat (test) + + + +

Issue Namespace Chat — Test Page

+
+ + + + + + +
+ +
+
Not connected
+ +
+
+
+ +
+ + + + +
+ + + + + diff --git a/scripts/issue-client1.js b/scripts/issue-client1.js new file mode 100644 index 0000000..f273f35 --- /dev/null +++ b/scripts/issue-client1.js @@ -0,0 +1,21 @@ +import { io } from "socket.io-client"; + +const socket = io("http://localhost:5050/issue-30", { + auth: { userId: 1, role: "building_manager" } +}); + +socket.on("connect", () => { + console.log("connected", socket.id); + + // broadcast to everyone in the issue + socket.emit("send_message_to_all", { text: "hello everyone from user 1" }); + + // private message to user with id 2 + socket.emit("send_message_to_user", { to: 2, text: "private hello to user 2 from user 1" }); +}); + +socket.on("receive_message", (msg) => { + console.log("receive_message:", msg); +}); + +socket.on("disconnect", () => console.log("disconnected")); \ No newline at end of file diff --git a/scripts/socket-client-ME.js b/scripts/socket-client-ME.js new file mode 100644 index 0000000..dcafc55 --- /dev/null +++ b/scripts/socket-client-ME.js @@ -0,0 +1,18 @@ +import { io } from "socket.io-client"; + +const socket = io("http://localhost:5050/new-issue", { + // auth: { token: "YOUR_JWT" }, // uncomment if server uses token +}); + +socket.on("connect", () => { + console.log("connected", socket.id); + socket.emit("send_message", { text: "hello from ME" }); +}); + +socket.on("new_issue", (issue) => { + console.log("new_issue received:", issue); +}); + +socket.on("disconnect", () => { + console.log("disconnected"); +}); \ No newline at end of file diff --git a/scripts/socket-client-Technician.js b/scripts/socket-client-Technician.js new file mode 100644 index 0000000..0ab84ad --- /dev/null +++ b/scripts/socket-client-Technician.js @@ -0,0 +1,16 @@ +import { io } from "socket.io-client"; + +const socket = io("http://localhost:5050/assign", { + //auth: { userId: "TECHNICIAN_ID_HERE", role: "technician" } // replace with real id or use query +}); + +socket.on("connect", () => { + console.log("connected", socket.id); + socket.emit("send_message", { text: "hello from Technician" }); +}); + +socket.on("assigned_issue", (issue) => { + console.log("assigned_issue received:", issue); +}); + +socket.on("disconnect", () => console.log("disconnected")); \ No newline at end of file diff --git a/src/controllers/issueController.js b/src/controllers/issueController.js index ee14867..49f1722 100644 --- a/src/controllers/issueController.js +++ b/src/controllers/issueController.js @@ -1,5 +1,5 @@ import issueService from '../services/issueService.js'; -import { notifyNewIssue, notifyAssign } from '../socket/socket.js'; +import { notifyNewIssue, notifyAssign, makeDynamicNamespace } from '../socket/socket.js'; class IssueController { // POST /api/issues - Create new issue @@ -51,6 +51,13 @@ class IssueController { // Notify all Maintenance Executives about the new issue(real-time) try { notifyNewIssue(result); } catch (e) { console.error(e);} + try { + // Create dynamic namespaces for real-time communication + makeDynamicNamespace(result.data.id); + } catch (nsErr) { + console.error('makeDynamicNamespace failed:', nsErr); + } + return res.status(201).json(result); } else { return res.status(400).json(result); diff --git a/src/socket/socket.js b/src/socket/socket.js index 1d4c2bd..ec50997 100644 --- a/src/socket/socket.js +++ b/src/socket/socket.js @@ -28,7 +28,7 @@ export const setupSocket = (server) => { // next(new Error("Invalid token")); // } // }); - let userID = 1; // temp for testing + let roleSpecificID = 1; // temp for testing let role = "technician"; // temp for testing @@ -61,9 +61,9 @@ export const setupSocket = (server) => { socket.disconnect(); return; } - const room = `technician:${userID}`; + const room = `technician:${roleSpecificID}`; socket.join(room); - console.log(`Technician ${userID} joined room: ${room} with socket ${socket.id}`); + console.log(`Technician ${roleSpecificID} joined room: ${room} with socket ${socket.id}`); socket.on("send_message", (message) => { console.log(message); @@ -87,6 +87,7 @@ export function notifyNewIssue(issue) { } } +//Emit an assigned_issue event to a specific technician in the /assign namespace. export function notifyAssign(id,issue) { if (!assign) return; try { @@ -95,4 +96,45 @@ export function notifyAssign(id,issue) { } catch (err) { console.error('notifyAssign error:', err); } +} + +export function makeDynamicNamespace(issueId) { + console.log(`makeDynamicNamespace for issueId: ${issueId}`); + if (!ioInstance) return null; + + const dynamicNamespace = ioInstance.of(`/issue-${issueId}`); + + dynamicNamespace.on("connection", (socket) => { + + // For testing without auth + const userId = socket.handshake?.auth?.userId || socket.handshake?.query?.userId || socket.data?.userId || socket.id; + + console.log(`User connected to /issue-${issueId} with socket ${socket.id}`); + + // room for all users in this issue + const issueRoom = `issue:${issueId}`; + socket.join(issueRoom); + console.log(`User ${userId} joined room: ${issueRoom}`); + + // personal room for user + const userRoom = `user:${userId}`; + socket.join(userRoom); + console.log(`User ${userId} joined room: ${userRoom}`); + + socket.on("send_message_to_all", (message) => { + console.log(message); + socket.to(issueRoom).emit("receive_message", message); + }); + + socket.on("send_message_to_user", (message, receiverId) => { + console.log(message); + socket.to(`user:${receiverId}`).emit("receive_message", message); + }); + + socket.on("disconnect", () => { + console.log(`User disconnected: ${socket.id}`); + }); + }); + + return dynamicNamespace; } \ No newline at end of file From 2ec5bf9c524986bdcae79666de36f57db3203736 Mon Sep 17 00:00:00 2001 From: Kavinda L Jayarathna <128674785+kavindalj@users.noreply.github.com> Date: Fri, 24 Oct 2025 21:54:39 +0530 Subject: [PATCH 4/7] feat(socket): save messages to database --- src/socket/socket.js | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/socket/socket.js b/src/socket/socket.js index ec50997..b59093c 100644 --- a/src/socket/socket.js +++ b/src/socket/socket.js @@ -1,5 +1,6 @@ import 'dotenv/config'; import { Server } from "socket.io"; +import messageService from '../services/messageService.js'; import jwt from "jsonwebtoken"; let ioInstance = null; @@ -126,9 +127,19 @@ export function makeDynamicNamespace(issueId) { socket.to(issueRoom).emit("receive_message", message); }); - socket.on("send_message_to_user", (message, receiverId) => { + socket.on("send_message_to_user", async (message, receiverId) => { console.log(message); socket.to(`user:${receiverId}`).emit("receive_message", message); + try { + await messageService.createMessage({ + body: message.text, + sender_id: userId, + receiver_id: receiverId, + issue_id: issueId, + }); + } catch (err) { + console.error('createMessage error:', err); + } }); socket.on("disconnect", () => { From 5fa84792e012795b4d7ad8d711a14522801a3455 Mon Sep 17 00:00:00 2001 From: Kavinda L Jayarathna <128674785+kavindalj@users.noreply.github.com> Date: Sat, 25 Oct 2025 18:34:08 +0530 Subject: [PATCH 5/7] feat(socket): automatically broadcast issue related event to dynamic issue namespace --- scripts/issue-chat.html | 5 ++++ src/controllers/cashRequestController.js | 29 +++++++++++++++++++ src/controllers/issueController.js | 36 +++++++++++++++++++++++- src/socket/socket.js | 14 ++++++++- 4 files changed, 82 insertions(+), 2 deletions(-) diff --git a/scripts/issue-chat.html b/scripts/issue-chat.html index 1026496..00fb33f 100644 --- a/scripts/issue-chat.html +++ b/scripts/issue-chat.html @@ -93,6 +93,11 @@

Issue Namespace Chat — Test Page

appendMessage(JSON.stringify(msg), 'received'); }); + // Listen for issue realtime updates (emitted by server via issueRealtimeUpdate) + socket.on('issue_update', (update) => { + appendMessage(JSON.stringify(update), 'issue_update'); + }); + socket.on('disconnect', (reason) => { appendMessage('Disconnected: ' + reason); statusEl.textContent = 'Disconnected'; diff --git a/src/controllers/cashRequestController.js b/src/controllers/cashRequestController.js index 4ae362d..c9d2dc0 100644 --- a/src/controllers/cashRequestController.js +++ b/src/controllers/cashRequestController.js @@ -1,3 +1,4 @@ +import { issueRealtimeUpdate } from '../socket/socket.js'; /** * Cash Request Controller * Handles HTTP requests for petty cash requests @@ -110,6 +111,13 @@ export const createCashRequest = async (req, res) => { description }); + try { + // Notify via realtime update that a new cash request has been created for the issue + issueRealtimeUpdate(issue_id, newCashRequest); + } catch (err) { + console.error('issueRealtimeUpdate error after creating cash request:', err); + } + res.status(201).json({ success: true, message: 'Petty cash request created successfully', @@ -161,6 +169,13 @@ export const updateCashRequest = async (req, res) => { const updatedCashRequest = await CashRequestService.updateCashRequest(id, updateData); + try { + // Notify via realtime update that the cash request has been updated + issueRealtimeUpdate(updatedCashRequest.issue_id, updatedCashRequest); + } catch (err) { + console.error('issueRealtimeUpdate error after updating cash request:', err); + } + if (!updatedCashRequest) { return res.status(404).json({ success: false, @@ -226,6 +241,13 @@ export const approveCashRequest = async (req, res) => { const approvedRequest = await CashRequestService.approveCashRequest(id); + try { + // Notify via realtime update that the cash request has been approved + issueRealtimeUpdate(approvedRequest.issue_id, approvedRequest); + } catch (err) { + console.error('issueRealtimeUpdate error after approving cash request:', err); + } + if (!approvedRequest) { return res.status(404).json({ success: false, @@ -266,6 +288,13 @@ export const rejectCashRequest = async (req, res) => { }); } + try { + // Notify via realtime update that the cash request has been rejected + issueRealtimeUpdate(rejectedRequest.issue_id, rejectedRequest); + } catch (err) { + console.error('issueRealtimeUpdate error after rejecting cash request:', err); + } + res.status(200).json({ success: true, message: 'Cash request rejected successfully', diff --git a/src/controllers/issueController.js b/src/controllers/issueController.js index 49f1722..fdb212a 100644 --- a/src/controllers/issueController.js +++ b/src/controllers/issueController.js @@ -1,5 +1,5 @@ import issueService from '../services/issueService.js'; -import { notifyNewIssue, notifyAssign, makeDynamicNamespace } from '../socket/socket.js'; +import { notifyNewIssue, notifyAssign, makeDynamicNamespace, issueRealtimeUpdate } from '../socket/socket.js'; class IssueController { // POST /api/issues - Create new issue @@ -166,6 +166,13 @@ class IssueController { const result = await issueService.updateIssue(parseInt(id), updateData); + try { + // Notify via realtime update that the issue has been updated + issueRealtimeUpdate(result.data.id, result); + } catch (err) { + console.error('issueRealtimeUpdate error after updating issue:', err); + } + if (result.success) { return res.status(200).json(result); } else { @@ -231,6 +238,13 @@ class IssueController { const result = await issueService.assignTechnician(parseInt(id), parseInt(technician_id)); if (result.success) { + // Notify via realtime update that the technician has been assigned + try { + issueRealtimeUpdate(parseInt(id), result); + } catch (emitErr) { + console.error('issueRealtimeUpdate failed:', emitErr); + } + // broadcast assignment to the assigned technician (real-time) try { notifyAssign(parseInt(technician_id), result); @@ -274,6 +288,13 @@ class IssueController { const result = await issueService.assignMaintenanceExecutive(parseInt(id), parseInt(maintenance_executive_id)); if (result.success) { + // Notify via realtime update that the maintenance executive has been assigned + try { + issueRealtimeUpdate(parseInt(id), result); + } catch (emitErr) { + console.error('issueRealtimeUpdate failed:', emitErr); + } + return res.status(200).json(result); } else { return res.status(400).json(result); @@ -310,6 +331,13 @@ class IssueController { const result = await issueService.assignThirdParty(parseInt(id), parseInt(third_party_id)); if (result.success) { + // Notify via realtime update that the third party has been assigned + try { + issueRealtimeUpdate(parseInt(id), result); + } catch (emitErr) { + console.error('issueRealtimeUpdate failed:', emitErr); + } + return res.status(200).json(result); } else { return res.status(400).json(result); @@ -354,6 +382,12 @@ class IssueController { const result = await issueService.updateStatus(parseInt(id), status); if (result.success) { + // Notify via realtime update that the issue status has been updated + try { + issueRealtimeUpdate(parseInt(id), result); + } catch (emitErr) { + console.error('issueRealtimeUpdate failed:', emitErr); + } return res.status(200).json(result); } else { return res.status(400).json(result); diff --git a/src/socket/socket.js b/src/socket/socket.js index b59093c..6a0caef 100644 --- a/src/socket/socket.js +++ b/src/socket/socket.js @@ -99,10 +99,11 @@ export function notifyAssign(id,issue) { } } +// Create a dynamic namespace for a specific issue ID export function makeDynamicNamespace(issueId) { - console.log(`makeDynamicNamespace for issueId: ${issueId}`); if (!ioInstance) return null; + console.log(`makeDynamicNamespace for issueId: ${issueId}`); const dynamicNamespace = ioInstance.of(`/issue-${issueId}`); dynamicNamespace.on("connection", (socket) => { @@ -148,4 +149,15 @@ export function makeDynamicNamespace(issueId) { }); return dynamicNamespace; +} + +// Emit an issue_update event to all clients connected to the /issue- namespace. +export function issueRealtimeUpdate(issueId, issueData) { + if (!ioInstance) return; + try { + const namespace = ioInstance.of(`/issue-${issueId}`); + namespace.emit('issue_update', issueData); + } catch (err) { + console.error('issueRealtimeUpdate error:', err); + } } \ No newline at end of file From 04f7fcb3760643ee1578a660385de9fa4889e7f2 Mon Sep 17 00:00:00 2001 From: Kavinda L Jayarathna Date: Wed, 29 Oct 2025 12:22:42 +0530 Subject: [PATCH 6/7] feat(socket): add dynamic namespace cleanup functionality --- src/controllers/issueController.js | 17 +++++++++++++++- src/socket/socket.js | 32 ++++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 1 deletion(-) diff --git a/src/controllers/issueController.js b/src/controllers/issueController.js index fdb212a..746b068 100644 --- a/src/controllers/issueController.js +++ b/src/controllers/issueController.js @@ -1,5 +1,5 @@ import issueService from '../services/issueService.js'; -import { notifyNewIssue, notifyAssign, makeDynamicNamespace, issueRealtimeUpdate } from '../socket/socket.js'; +import { notifyNewIssue, notifyAssign, makeDynamicNamespace, issueRealtimeUpdate, removeDynamicNamespace } from '../socket/socket.js'; class IssueController { // POST /api/issues - Create new issue @@ -173,6 +173,7 @@ class IssueController { console.error('issueRealtimeUpdate error after updating issue:', err); } + if (result.success) { return res.status(200).json(result); } else { @@ -202,6 +203,8 @@ class IssueController { const result = await issueService.deleteIssue(parseInt(id)); if (result.success) { + // Remove all connections and delete the namespace for the issue + removeDynamicNamespace(parseInt(id)); return res.status(200).json(result); } else { return res.status(404).json(result); @@ -295,6 +298,9 @@ class IssueController { console.error('issueRealtimeUpdate failed:', emitErr); } + // Notify all Maintenance Executives about the new issue(real-time) update with assigned ME + try { notifyNewIssue(result); } catch (e) { console.error(e);} + return res.status(200).json(result); } else { return res.status(400).json(result); @@ -388,6 +394,15 @@ class IssueController { } catch (emitErr) { console.error('issueRealtimeUpdate failed:', emitErr); } + + // Notify all users about the issue status update(real-time) + try { notifyNewIssue(result); } catch (e) { console.error(e);} + + // Remove all connections and delete the namespace for the issue + if (result.data.status === 'Closed' || result.data.status === 'Done') { + removeDynamicNamespace(result.data.id); + } + return res.status(200).json(result); } else { return res.status(400).json(result); diff --git a/src/socket/socket.js b/src/socket/socket.js index 6a0caef..9d316e9 100644 --- a/src/socket/socket.js +++ b/src/socket/socket.js @@ -151,6 +151,38 @@ export function makeDynamicNamespace(issueId) { return dynamicNamespace; } +export function removeDynamicNamespace(issueId) { + if (!ioInstance) return; + + try { + const namespaceName = `/issue-${issueId}`; + const namespace = ioInstance.of(namespaceName); + + console.log(`Removing dynamic namespace: ${namespaceName}`); + + // Get all sockets in the namespace and disconnect them + namespace.fetchSockets().then((sockets) => { + sockets.forEach((socket) => { + console.log(`Disconnecting socket ${socket.id} from namespace ${namespaceName}`); + socket.disconnect(true); + }); + + // Remove all listeners from the namespace + namespace.removeAllListeners(); + + // Delete the namespace from the server + ioInstance._nsps.delete(namespaceName); + + console.log(`Dynamic namespace ${namespaceName} removed successfully`); + }).catch((err) => { + console.error(`Error removing namespace ${namespaceName}:`, err); + }); + + } catch (err) { + console.error('removeDynamicNamespace error:', err); + } +} + // Emit an issue_update event to all clients connected to the /issue- namespace. export function issueRealtimeUpdate(issueId, issueData) { if (!ioInstance) return; From 8afde3bc297b9dffa4bbc63bb102a44ca7c7f9b9 Mon Sep 17 00:00:00 2001 From: Kavinda L Jayarathna Date: Wed, 29 Oct 2025 19:51:24 +0530 Subject: [PATCH 7/7] feat: add tests for socket --- src/__tests__/socket.test.js | 107 +++++++++++++++++++++++------ src/controllers/issueController.js | 7 +- src/socket/socket.js | 2 +- 3 files changed, 93 insertions(+), 23 deletions(-) diff --git a/src/__tests__/socket.test.js b/src/__tests__/socket.test.js index 0a38121..d6c7f83 100644 --- a/src/__tests__/socket.test.js +++ b/src/__tests__/socket.test.js @@ -1,15 +1,19 @@ import { createServer } from "http"; import { io as Client } from "socket.io-client"; -import { setupSocket } from "../socket/socket.js"; +import { setupSocket, makeDynamicNamespace } from "../socket/socket.js"; import { jest } from '@jest/globals'; describe("Socket.IO Basic Communication", () => { let httpServer, socketServer, port; - let client1, client2; + let issueClient1, issueClient2; + + // Set a timeout for the entire test suite + jest.setTimeout(10000); beforeAll(() => { // Suppress console logs during tests jest.spyOn(console, "log").mockImplementation(() => { }); + jest.spyOn(console, "error").mockImplementation(() => { }); }); beforeAll((done) => { @@ -19,8 +23,23 @@ describe("Socket.IO Basic Communication", () => { httpServer.listen(() => { port = httpServer.address().port; - client1 = new Client(`http://localhost:${port}`); - client2 = new Client(`http://localhost:${port}`); + // Create a dynamic namespace first + const testIssueId = 123; + makeDynamicNamespace(testIssueId); + + // Connect clients to the dynamic namespace + issueClient1 = new Client(`http://localhost:${port}/issue-${testIssueId}`, { + forceNew: true, + timeout: 5000, + transports: ['websocket'], + query: { userId: 'user1' } + }); + issueClient2 = new Client(`http://localhost:${port}/issue-${testIssueId}`, { + forceNew: true, + timeout: 5000, + transports: ['websocket'], + query: { userId: 'user2' } + }); let connectedCount = 0; const checkDone = () => { @@ -28,43 +47,91 @@ describe("Socket.IO Basic Communication", () => { if (connectedCount === 2) done(); }; - client1.on("connect", checkDone); - client2.on("connect", checkDone); + const handleError = (error) => { + done(error); + }; + + issueClient1.on("connect", checkDone); + issueClient2.on("connect", checkDone); + issueClient1.on("connect_error", handleError); + issueClient2.on("connect_error", handleError); }); }); - afterAll(async () => { - if (client1.connected) client1.disconnect(); - if (client2.connected) client2.disconnect(); + afterEach(() => { + // Clean up event listeners after each test + if (issueClient1) { + issueClient1.removeAllListeners("receive_message"); + } + if (issueClient2) { + issueClient2.removeAllListeners("receive_message"); + } + }); - await new Promise(resolve => socketServer.close(resolve)); - await new Promise(resolve => httpServer.close(resolve)); + afterAll(async () => { + // Close clients first + if (issueClient1?.connected) { + issueClient1.disconnect(); + } + if (issueClient2?.connected) { + issueClient2.disconnect(); + } + + // Wait a bit for disconnections to process + await new Promise(resolve => setTimeout(resolve, 100)); + + // Close socket server + if (socketServer) { + await new Promise(resolve => { + socketServer.close(() => { + resolve(); + }); + }); + } + + // Close HTTP server + if (httpServer && httpServer.listening) { + await new Promise(resolve => { + httpServer.close(() => { + resolve(); + }); + }); + } }); - test("should broadcast message to other clients", (done) => { + test("should broadcast message to all clients in issue room", (done) => { const testMessage = { text: "Hello world" }; - client2.once("receive_message", (msg) => { + issueClient2.once("receive_message", (msg) => { expect(msg).toEqual(testMessage); done(); }); - client1.emit("send_message", testMessage); + issueClient1.emit("send_message_to_all", testMessage); }); test("sender should not receive its own message", (done) => { const testMessage = { text: "Sender check" }; + let messageReceived = false; // If sender receives a message, fail - client2.once("receive_message", () => { + const messageHandler = () => { + messageReceived = true; done(new Error("Sender received its own message!")); - }); + }; - // Emit from client2 - client2.emit("send_message", testMessage); + issueClient2.once("receive_message", messageHandler); - // Wait a bit to ensure no message arrives - setTimeout(done, 500); + // Emit from client2 + issueClient2.emit("send_message_to_all", testMessage); + + // Wait a bit to ensure no message arrives, then clean up + setTimeout(() => { + issueClient2.removeListener("receive_message", messageHandler); + if (!messageReceived) { + done(); + } + }, 300); }); }); diff --git a/src/controllers/issueController.js b/src/controllers/issueController.js index 746b068..a64a57e 100644 --- a/src/controllers/issueController.js +++ b/src/controllers/issueController.js @@ -388,16 +388,19 @@ class IssueController { const result = await issueService.updateStatus(parseInt(id), status); if (result.success) { - // Notify via realtime update that the issue status has been updated + // Notify via realtime update that the issue status has been updated (in chat interface) try { issueRealtimeUpdate(parseInt(id), result); } catch (emitErr) { console.error('issueRealtimeUpdate failed:', emitErr); } - // Notify all users about the issue status update(real-time) + // Notify Maintenance Executives about the issue status update(real-time in home dashboard) try { notifyNewIssue(result); } catch (e) { console.error(e);} + // Notify assigned technician about the issue status update(real-time in home dashboard) + try { notifyAssign(result.data.technician_id, result); } catch (e) { console.error(e);} + // Remove all connections and delete the namespace for the issue if (result.data.status === 'Closed' || result.data.status === 'Done') { removeDynamicNamespace(result.data.id); diff --git a/src/socket/socket.js b/src/socket/socket.js index 9d316e9..15dd122 100644 --- a/src/socket/socket.js +++ b/src/socket/socket.js @@ -29,7 +29,7 @@ export const setupSocket = (server) => { // next(new Error("Invalid token")); // } // }); - let roleSpecificID = 1; // temp for testing + let roleSpecificID = 3; // temp for testing let role = "technician"; // temp for testing