IO-2924-Refactor-Production-board-to-use-Socket-Provider: Finalize
Signed-off-by: Dave Richer <dave@imexsystems.ca>
This commit is contained in:
@@ -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";
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
});
|
||||
|
||||
@@ -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" });
|
||||
};
|
||||
|
||||
17
server/utils/ioHelpers.js
Normal file
17
server/utils/ioHelpers.js
Normal file
@@ -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;
|
||||
@@ -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.`);
|
||||
|
||||
Reference in New Issue
Block a user