From 932f572fb50ba94d0a5bcd86a62657c21f7bb594 Mon Sep 17 00:00:00 2001 From: Dave Richer Date: Thu, 26 Sep 2024 10:56:48 -0400 Subject: [PATCH] IO-2924-Refactor-Production-board-to-use-Socket-Provider: Finalize Signed-off-by: Dave Richer --- .../production-board-kanban.container.jsx | 8 +- client/src/contexts/SocketIO/useSocket.js | 128 +++++++++--------- server.js | 4 +- server/ioevent/ioevent.js | 8 +- server/job/job-updated.js | 5 +- server/utils/ioHelpers.js | 17 +++ server/web-sockets/redisSocketEvents.js | 46 +++++-- 7 files changed, 127 insertions(+), 89 deletions(-) create mode 100644 server/utils/ioHelpers.js diff --git a/client/src/components/production-board-kanban/production-board-kanban.container.jsx b/client/src/components/production-board-kanban/production-board-kanban.container.jsx index 31220a9bd..6182b3ffb 100644 --- a/client/src/components/production-board-kanban/production-board-kanban.container.jsx +++ b/client/src/components/production-board-kanban/production-board-kanban.container.jsx @@ -1,12 +1,12 @@ -import React, { useEffect, useMemo, useRef, useContext } from "react"; -import { useQuery, useSubscription, useApolloClient, gql } from "@apollo/client"; +import React, { useContext, useEffect, useMemo, useRef } from "react"; +import { useApolloClient, useQuery, useSubscription } from "@apollo/client"; import { connect } from "react-redux"; import { createStructuredSelector } from "reselect"; import { + GET_JOB_BY_PK, QUERY_JOBS_IN_PRODUCTION, SUBSCRIPTION_JOBS_IN_PRODUCTION, - SUBSCRIPTION_JOBS_IN_PRODUCTION_VIEW, - GET_JOB_BY_PK + SUBSCRIPTION_JOBS_IN_PRODUCTION_VIEW } from "../../graphql/jobs.queries"; import { QUERY_KANBAN_SETTINGS } from "../../graphql/user.queries"; import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors"; diff --git a/client/src/contexts/SocketIO/useSocket.js b/client/src/contexts/SocketIO/useSocket.js index c5688cf3c..42385bfb7 100644 --- a/client/src/contexts/SocketIO/useSocket.js +++ b/client/src/contexts/SocketIO/useSocket.js @@ -1,83 +1,83 @@ -import { useEffect, useState } from "react"; +import { useEffect, useState, useRef } from "react"; import SocketIO from "socket.io-client"; import { auth } from "../../firebase/firebase.utils"; const useSocket = (bodyshop) => { - const [socket, setSocket] = useState(null); + const socketRef = useRef(null); const [clientId, setClientId] = useState(null); - const [token, setToken] = useState(null); useEffect(() => { - // Listener for token changes const unsubscribe = auth.onIdTokenChanged(async (user) => { if (user) { const newToken = await user.getIdToken(); - setToken(newToken); + + if (socketRef.current) { + // Send new token to server + socketRef.current.emit("update-token", newToken); + } else if (bodyshop && bodyshop.id) { + // Initialize the socket + const endpoint = import.meta.env.PROD ? import.meta.env.VITE_APP_AXIOS_BASE_API_URL : ""; + + const socketInstance = SocketIO(endpoint, { + path: "/wss", + withCredentials: true, + auth: { token: newToken }, + reconnectionAttempts: Infinity, + reconnectionDelay: 2000, + reconnectionDelayMax: 10000 + }); + + socketRef.current = socketInstance; + + const handleBodyshopMessage = (message) => { + if (!import.meta.env.DEV) return; + console.log(`Received message for bodyshop ${bodyshop.id}:`, message); + }; + + const handleConnect = () => { + console.log("Socket connected:", socketInstance.id); + socketInstance.emit("join-bodyshop-room", bodyshop.id); + setClientId(socketInstance.id); + }; + + const handleReconnect = (attempt) => { + console.log(`Socket reconnected after ${attempt} attempts`); + }; + + const handleConnectionError = (err) => { + console.error("Socket connection error:", err); + }; + + const handleDisconnect = () => { + console.log("Socket disconnected"); + }; + + socketInstance.on("connect", handleConnect); + socketInstance.on("reconnect", handleReconnect); + socketInstance.on("connect_error", handleConnectionError); + socketInstance.on("disconnect", handleDisconnect); + socketInstance.on("bodyshop-message", handleBodyshopMessage); + } } else { - setToken(null); + // User is not authenticated + if (socketRef.current) { + socketRef.current.disconnect(); + socketRef.current = null; + } } }); // Clean up the listener on unmount - return () => unsubscribe(); - }, []); + return () => { + unsubscribe(); + if (socketRef.current) { + socketRef.current.disconnect(); + socketRef.current = null; + } + }; + }, [bodyshop.id]); - useEffect(() => { - if (bodyshop && bodyshop.id && token) { - const endpoint = import.meta.env.PROD ? import.meta.env.VITE_APP_AXIOS_BASE_API_URL : ""; - - const socketInstance = SocketIO(endpoint, { - path: "/wss", - withCredentials: true, - auth: { token }, // Use the current token - reconnectionAttempts: Infinity, - reconnectionDelay: 2000, - reconnectionDelayMax: 10000 - }); - - setSocket(socketInstance); - - const handleBodyshopMessage = (message) => { - console.log(`Received message for bodyshop ${bodyshop.id}:`, message); - }; - - const handleConnect = () => { - console.log("Socket connected:", socketInstance.id); - socketInstance.emit("join-bodyshop-room", bodyshop.id); - setClientId(socketInstance.id); - }; - - const handleReconnect = (attempt) => { - console.log(`Socket reconnected after ${attempt} attempts`); - }; - - const handleConnectionError = (err) => { - console.error("Socket connection error:", err); - }; - - const handleDisconnect = () => { - console.log("Socket disconnected"); - }; - - socketInstance.on("connect", handleConnect); - socketInstance.on("reconnect", handleReconnect); - socketInstance.on("connect_error", handleConnectionError); - socketInstance.on("disconnect", handleDisconnect); - socketInstance.on("bodyshop-message", handleBodyshopMessage); - - return () => { - socketInstance.emit("leave-bodyshop-room", bodyshop.id); - socketInstance.off("connect", handleConnect); - socketInstance.off("reconnect", handleReconnect); - socketInstance.off("connect_error", handleConnectionError); - socketInstance.off("disconnect", handleDisconnect); - socketInstance.off("bodyshop-message", handleBodyshopMessage); - socketInstance.disconnect(); - }; - } - }, [bodyshop, token]); // Include 'token' in dependencies to re-run effect when it changes - - return { socket, clientId }; + return { socket: socketRef.current, clientId }; }; export default useSocket; diff --git a/server.js b/server.js index e11fa31a7..bbf843ab2 100644 --- a/server.js +++ b/server.js @@ -14,6 +14,7 @@ const { instrument, RedisStore } = require("@socket.io/admin-ui"); const { isString, isEmpty } = require("lodash"); const applyRedisHelpers = require("./server/utils/redisHelpers"); +const applyIOHelpers = require("./server/utils/ioHelpers"); // Load environment variables require("dotenv").config({ @@ -192,13 +193,14 @@ const main = async () => { const { pubClient, ioRedis } = await applySocketIO(server, app); const api = applyRedisHelpers(pubClient, app); + const ioHelpers = applyIOHelpers(app, api, ioRedis, logger); // Legacy Socket Events require("./server/web-sockets/web-socket"); applyMiddleware(app); applyRoutes(app); - redisSocketEvents(ioRedis, api); + redisSocketEvents(ioRedis, api, ioHelpers); try { await server.listen(port); diff --git a/server/ioevent/ioevent.js b/server/ioevent/ioevent.js index b53159918..36950b84e 100644 --- a/server/ioevent/ioevent.js +++ b/server/ioevent/ioevent.js @@ -11,7 +11,10 @@ require("dotenv").config({ exports.default = async (req, res) => { const { useremail, bodyshopid, operationName, variables, env, time, dbevent, user } = req.body; - const { ioRedis } = req; + const { + ioRedis, + ioHelpers: { getBodyshopRoom } + } = req; try { await client.request(queries.INSERT_IOEVENT, { event: { @@ -24,8 +27,7 @@ exports.default = async (req, res) => { useremail } }); - - ioRedis.to(bodyshopid).emit("bodyshop-message", { + ioRedis.to(getBodyshopRoom(bodyshopid)).emit("bodyshop-message", { operationName, useremail }); diff --git a/server/job/job-updated.js b/server/job/job-updated.js index a6b527608..bec619837 100644 --- a/server/job/job-updated.js +++ b/server/job/job-updated.js @@ -1,7 +1,7 @@ const { isObject } = require("lodash"); const jobUpdated = async (req, res) => { - const { ioRedis, logger } = req; + const { ioRedis, logger, ioHelpers } = req; if (!req?.body?.event?.data?.new || !isObject(req?.body?.event?.data?.new)) { logger.log("job-update-error", "ERROR", req.user?.email, null, { @@ -24,8 +24,7 @@ const jobUpdated = async (req, res) => { const bodyshopID = updatedJob.shopid; // Emit the job-updated event only to the room corresponding to the bodyshop - - ioRedis.to(bodyshopID).emit("job-updated", updatedJob); + ioRedis.to(ioHelpers.getBodyshopRoom(bodyshopID)).emit("job-updated", updatedJob); return res.json({ message: "Job updated and event emitted" }); }; diff --git a/server/utils/ioHelpers.js b/server/utils/ioHelpers.js new file mode 100644 index 000000000..7ef9098d9 --- /dev/null +++ b/server/utils/ioHelpers.js @@ -0,0 +1,17 @@ +const applyIOHelpers = (app, api, io, logger) => { + const getBodyshopRoom = (bodyshopID) => `broadcast-room-${bodyshopID}`; + + const ioHelpersAPI = { + getBodyshopRoom + }; + + // Helper middleware + app.use((req, res, next) => { + req.ioHelpers = ioHelpersAPI; + next(); + }); + + return ioHelpersAPI; +}; + +module.exports = applyIOHelpers; diff --git a/server/web-sockets/redisSocketEvents.js b/server/web-sockets/redisSocketEvents.js index 516be1fe4..798debd35 100644 --- a/server/web-sockets/redisSocketEvents.js +++ b/server/web-sockets/redisSocketEvents.js @@ -11,32 +11,50 @@ function createLogEvent(socket, level, message) { logger.log("ioredis-log-event", level, socket.user.email, null, { wsmessage: message }); } -const redisSocketEvents = (io, { addUserToRoom, getUsersInRoom, removeUserFromRoom }) => { +const registerUpdateEvents = (socket) => { + socket.on("update-token", async (newToken) => { + try { + socket.user = await admin.auth().verifyIdToken(newToken); + createLogEvent(socket, "INFO", "Token updated successfully"); + socket.emit("token-updated", { success: true }); + } catch (error) { + createLogEvent(socket, "ERROR", `Token update failed: ${error.message}`); + socket.emit("token-updated", { success: false, error: error.message }); + // Optionally disconnect the socket if token verification fails + socket.disconnect(); + } + }); +}; + +const redisSocketEvents = (io, { addUserToRoom, getUsersInRoom, removeUserFromRoom }, { getBodyshopRoom }) => { // Room management and broadcasting events function registerRoomAndBroadcastEvents(socket) { socket.on("join-bodyshop-room", async (bodyshopUUID) => { - socket.join(bodyshopUUID); - await addUserToRoom(bodyshopUUID, { uid: socket.user.uid, email: socket.user.email }); + const room = getBodyshopRoom(bodyshopUUID); + socket.join(room); + await addUserToRoom(room, { uid: socket.user.uid, email: socket.user.email }); createLogEvent(socket, "DEBUG", `Client joined bodyshop room: ${bodyshopUUID}`); // Notify all users in the room about the updated user list const usersInRoom = await getUsersInRoom(bodyshopUUID); - io.to(bodyshopUUID).emit("room-users-updated", usersInRoom); + io.to(room).emit("room-users-updated", usersInRoom); }); socket.on("leave-bodyshop-room", async (bodyshopUUID) => { - socket.leave(bodyshopUUID); - createLogEvent(socket, "DEBUG", `Client left bodyshop room: ${bodyshopUUID}`); + const room = getBodyshopRoom(bodyshopUUID); + socket.leave(room); + createLogEvent(socket, "DEBUG", `Client left bodyshop room: ${room}`); }); socket.on("get-room-users", async (bodyshopUUID, callback) => { - const usersInRoom = await getUsersInRoom(bodyshopUUID); + const usersInRoom = await getUsersInRoom(getBodyshopRoom(bodyshopUUID)); callback(usersInRoom); }); socket.on("broadcast-to-bodyshop", async (bodyshopUUID, message) => { - io.to(bodyshopUUID).emit("bodyshop-message", message); - createLogEvent(socket, "INFO", `Broadcast message to bodyshop ${bodyshopUUID}`); + const room = getBodyshopRoom(bodyshopUUID); + io.to(room).emit("bodyshop-message", message); + createLogEvent(socket, "INFO", `Broadcast message to bodyshop ${room}`); }); socket.on("disconnect", async () => { @@ -45,12 +63,12 @@ const redisSocketEvents = (io, { addUserToRoom, getUsersInRoom, removeUserFromRo // Get all rooms the socket is part of const rooms = Array.from(socket.rooms).filter((room) => room !== socket.id); - for (const bodyshopUUID of rooms) { - await removeUserFromRoom(bodyshopUUID, { uid: socket.user.uid, email: socket.user.email }); + for (const bodyshopRoom of rooms) { + await removeUserFromRoom(bodyshopRoom, { uid: socket.user.uid, email: socket.user.email }); // Notify all users in the room about the updated user list - const usersInRoom = await getUsersInRoom(bodyshopUUID); - io.to(bodyshopUUID).emit("room-users-updated", usersInRoom); + const usersInRoom = await getUsersInRoom(bodyshopRoom); + io.to(bodyshopRoom).emit("room-users-updated", usersInRoom); } }); } @@ -61,7 +79,7 @@ const redisSocketEvents = (io, { addUserToRoom, getUsersInRoom, removeUserFromRo // Register room and broadcasting events registerRoomAndBroadcastEvents(socket); - + registerUpdateEvents(socket); // Handle socket disconnection socket.on("disconnect", async () => { createLogEvent(socket, "DEBUG", `User disconnected.`);