feature/IO-3303-Socket-IO-Optimization-Auto-Add-Watchers-Gate - SocketIO Optimization / Auto Add Watchers Gate

This commit is contained in:
Dave Richer
2025-07-14 18:42:27 -04:00
parent fbd6766dcd
commit 72091e9eae
5 changed files with 182 additions and 98 deletions

View File

@@ -3,7 +3,6 @@ import axios from "axios";
import { useState } from "react"; import { useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { logImEXEvent } from "../../firebase/firebase.utils"; import { logImEXEvent } from "../../firebase/firebase.utils";
import cleanAxios from "../../utils/CleanAxios";
import formatBytes from "../../utils/formatbytes"; import formatBytes from "../../utils/formatbytes";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { createStructuredSelector } from "reselect"; import { createStructuredSelector } from "reselect";

View File

@@ -16,7 +16,7 @@ import {
import { useMutation } from "@apollo/client"; import { useMutation } from "@apollo/client";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { useSplitTreatments } from "@splitsoftware/splitio-react"; import { useSplitTreatments } from "@splitsoftware/splitio-react";
import { SocketContext, INITIAL_NOTIFICATIONS } from "./useSocket.js"; import { INITIAL_NOTIFICATIONS, SocketContext } from "./useSocket.js";
/** /**
* Socket Provider - Scenario Notifications / Web Socket related items * Socket Provider - Scenario Notifications / Web Socket related items
@@ -31,6 +31,7 @@ const SocketProvider = ({ children, bodyshop, navigate, currentUser }) => {
const socketRef = useRef(null); const socketRef = useRef(null);
const [clientId, setClientId] = useState(null); const [clientId, setClientId] = useState(null);
const [isConnected, setIsConnected] = useState(false); const [isConnected, setIsConnected] = useState(false);
const [socketInitialized, setSocketInitialized] = useState(false);
const notification = useNotification(); const notification = useNotification();
const userAssociationId = bodyshop?.associations?.[0]?.id; const userAssociationId = bodyshop?.associations?.[0]?.id;
const { t } = useTranslation(); const { t } = useTranslation();
@@ -146,6 +147,13 @@ const SocketProvider = ({ children, bodyshop, navigate, currentUser }) => {
onError: (err) => console.error("MARK_ALL_NOTIFICATIONS_READ error:", err) onError: (err) => console.error("MARK_ALL_NOTIFICATIONS_READ error:", err)
}); });
const checkAndReconnect = () => {
if (socketRef.current && !socketRef.current.connected) {
console.log("Attempting manual reconnect due to event trigger");
socketRef.current.connect();
}
};
useEffect(() => { useEffect(() => {
const initializeSocket = async (token) => { const initializeSocket = async (token) => {
if (!bodyshop || !bodyshop.id || socketRef.current) return; if (!bodyshop || !bodyshop.id || socketRef.current) return;
@@ -157,10 +165,14 @@ const SocketProvider = ({ children, bodyshop, navigate, currentUser }) => {
auth: { token, bodyshopId: bodyshop.id }, auth: { token, bodyshopId: bodyshop.id },
reconnectionAttempts: Infinity, reconnectionAttempts: Infinity,
reconnectionDelay: 2000, reconnectionDelay: 2000,
reconnectionDelayMax: 10000 reconnectionDelayMax: 60000,
randomizationFactor: 0.5,
transports: ["websocket", "polling"], // Add this to prefer WebSocket with polling fallback
rememberUpgrade: true
}); });
socketRef.current = socketInstance; socketRef.current = socketInstance;
setSocketInitialized(true);
const handleBodyshopMessage = (message) => { const handleBodyshopMessage = (message) => {
if (!message || !message.type) return; if (!message || !message.type) return;
@@ -469,6 +481,57 @@ const SocketProvider = ({ children, bodyshop, navigate, currentUser }) => {
t t
]); ]);
useEffect(() => {
if (!socketInitialized) return;
const onVisibilityChange = () => {
if (document.visibilityState === "visible") {
checkAndReconnect();
}
};
const onFocus = () => {
checkAndReconnect();
};
const onOnline = () => {
checkAndReconnect();
};
const onPageShow = (event) => {
if (event.persisted) {
checkAndReconnect();
}
};
document.addEventListener("visibilitychange", onVisibilityChange);
window.addEventListener("focus", onFocus);
window.addEventListener("online", onOnline);
window.addEventListener("pageshow", onPageShow);
// Sleep/wake detection using timer
let lastTime = Date.now();
const intervalMs = 1000; // Check every second
const thresholdMs = 2000; // If more than 2 seconds elapsed, assume sleep/wake
const sleepCheckInterval = setInterval(() => {
const currentTime = Date.now();
if (currentTime > lastTime + intervalMs + thresholdMs) {
console.log("Detected potential wake from sleep/hibernate");
checkAndReconnect();
}
lastTime = currentTime;
}, intervalMs);
return () => {
document.removeEventListener("visibilitychange", onVisibilityChange);
window.removeEventListener("focus", onFocus);
window.removeEventListener("online", onOnline);
window.removeEventListener("pageshow", onPageShow);
clearInterval(sleepCheckInterval);
};
}, [socketInitialized]);
return ( return (
<SocketContext.Provider <SocketContext.Provider
value={{ value={{

View File

@@ -19,7 +19,7 @@ async function JobCosting(req, res) {
const client = req.userGraphQLClient; const client = req.userGraphQLClient;
//Uncomment for further testing //Uncomment for further testing
// logger.log("job-costing-start", "DEBUG", req.user.email, jobid, null); logger.log("job-costing-start", "DEBUG", req.user.email, jobid, null);
try { try {
const resp = await client.setHeaders({ Authorization: BearerToken }).request(queries.QUERY_JOB_COSTING_DETAILS, { const resp = await client.setHeaders({ Authorization: BearerToken }).request(queries.QUERY_JOB_COSTING_DETAILS, {
@@ -47,9 +47,9 @@ async function JobCostingMulti(req, res) {
const client = req.userGraphQLClient; const client = req.userGraphQLClient;
//Uncomment for further testing //Uncomment for further testing
// logger.log("job-costing-multi-start", "DEBUG", req?.user?.email, null, { logger.log("job-costing-multi-start", "DEBUG", req?.user?.email, null, {
// jobids jobids
// }); });
try { try {
const resp = await client const resp = await client
@@ -589,7 +589,7 @@ function GenerateCostingData(job) {
amount: Math.round((job.storage_payable || 0) * 100) amount: Math.round((job.storage_payable || 0) * 100)
}); });
} }
//Is it a DMS Setup? //Is it a DMS Setup?
const selectedDmsAllocationConfig = const selectedDmsAllocationConfig =
(job.bodyshop.md_responsibility_centers.dms_defaults && (job.bodyshop.md_responsibility_centers.dms_defaults &&

View File

@@ -8,6 +8,7 @@ const getLifecycleStatusColor = require("../utils/getLifecycleStatusColor");
const jobLifecycle = async (req, res) => { const jobLifecycle = async (req, res) => {
// Grab the jobids and statuses from the request body // Grab the jobids and statuses from the request body
const { jobids, statuses } = req.body; const { jobids, statuses } = req.body;
const { logger } = req;
if (!jobids) { if (!jobids) {
return res.status(400).json({ return res.status(400).json({
@@ -16,102 +17,118 @@ const jobLifecycle = async (req, res) => {
} }
const jobIDs = _.isArray(jobids) ? jobids : [jobids]; const jobIDs = _.isArray(jobids) ? jobids : [jobids];
const client = req.userGraphQLClient;
const resp = await client.request(queries.QUERY_TRANSITIONS_BY_JOBID, { jobids: jobIDs });
const transitions = resp.transitions; logger.log("job-lifecycle-start", "DEBUG", req?.user?.email, null, {
jobids: jobIDs
});
try {
const client = req.userGraphQLClient;
const resp = await client.request(queries.QUERY_TRANSITIONS_BY_JOBID, { jobids: jobIDs });
const transitions = resp.transitions;
if (!transitions) {
return res.status(200).json({
jobIDs,
transitions: []
});
}
const transitionsByJobId = _.groupBy(resp.transitions, "jobid");
const groupedTransitions = {};
const allDurations = [];
for (let jobId in transitionsByJobId) {
let lifecycle = transitionsByJobId[jobId].map((transition) => {
transition.start_readable = transition.start ? moment(transition.start).fromNow() : "N/A";
transition.end_readable = transition.end ? moment(transition.end).fromNow() : "N/A";
if (transition.duration) {
transition.duration_seconds = Math.round(transition.duration / 1000);
transition.duration_minutes = Math.round(transition.duration_seconds / 60);
let duration = moment.duration(transition.duration);
transition.duration_readable = durationToHumanReadable(duration);
} else {
transition.duration_seconds = 0;
transition.duration_minutes = 0;
transition.duration_readable = "N/A";
}
return transition;
});
const durations = calculateStatusDuration(lifecycle, statuses);
groupedTransitions[jobId] = {
lifecycle,
durations
};
if (durations?.summations) {
allDurations.push(durations.summations);
}
}
const finalSummations = [];
const flatGroupedAllDurations = _.groupBy(allDurations.flat(), "status");
const finalStatusCounts = Object.keys(flatGroupedAllDurations).reduce((acc, status) => {
acc[status] = flatGroupedAllDurations[status].length;
return acc;
}, {});
// Calculate total value of all statuses
const finalTotal = Object.values(flatGroupedAllDurations).reduce((total, statusArr) => {
return total + statusArr.reduce((acc, curr) => acc + curr.value, 0);
}, 0);
Object.keys(flatGroupedAllDurations).forEach((status) => {
const value = flatGroupedAllDurations[status].reduce((acc, curr) => acc + curr.value, 0);
const humanReadable = durationToHumanReadable(moment.duration(value));
const percentage = finalTotal > 0 ? (value / finalTotal) * 100 : 0;
const color = getLifecycleStatusColor(status);
const roundedPercentage = `${Math.round(percentage)}%`;
const averageValue = _.size(jobIDs) > 0 ? value / jobIDs.length : 0;
const averageHumanReadable = durationToHumanReadable(moment.duration(averageValue));
finalSummations.push({
status,
value,
humanReadable,
percentage,
color,
roundedPercentage,
averageValue,
averageHumanReadable
});
});
if (!transitions) {
return res.status(200).json({ return res.status(200).json({
jobIDs, jobIDs,
transitions: [] transition: groupedTransitions,
}); durations: {
} jobs: jobIDs.length,
summations: finalSummations,
const transitionsByJobId = _.groupBy(resp.transitions, "jobid"); totalStatuses: finalSummations.length,
total: finalTotal,
const groupedTransitions = {}; statusCounts: finalStatusCounts,
const allDurations = []; humanReadable: durationToHumanReadable(moment.duration(finalTotal)),
averageValue: _.size(jobIDs) > 0 ? finalTotal / jobIDs.length : 0,
for (let jobId in transitionsByJobId) { averageHumanReadable:
let lifecycle = transitionsByJobId[jobId].map((transition) => { _.size(jobIDs) > 0
transition.start_readable = transition.start ? moment(transition.start).fromNow() : "N/A"; ? durationToHumanReadable(moment.duration(finalTotal / jobIDs.length))
transition.end_readable = transition.end ? moment(transition.end).fromNow() : "N/A"; : durationToHumanReadable(moment.duration(0))
if (transition.duration) {
transition.duration_seconds = Math.round(transition.duration / 1000);
transition.duration_minutes = Math.round(transition.duration_seconds / 60);
let duration = moment.duration(transition.duration);
transition.duration_readable = durationToHumanReadable(duration);
} else {
transition.duration_seconds = 0;
transition.duration_minutes = 0;
transition.duration_readable = "N/A";
} }
return transition;
}); });
} catch (error) {
const durations = calculateStatusDuration(lifecycle, statuses); logger.log("job-lifecycle-error", "ERROR", req?.user?.email, null, {
jobids: jobIDs,
groupedTransitions[jobId] = { statuses: statuses ? JSON.stringify(statuses) : "N/A",
lifecycle, error: error.message
durations });
}; return res.status(500).json({
error: "Internal server error"
if (durations?.summations) { });
allDurations.push(durations.summations);
}
} }
const finalSummations = [];
const flatGroupedAllDurations = _.groupBy(allDurations.flat(), "status");
const finalStatusCounts = Object.keys(flatGroupedAllDurations).reduce((acc, status) => {
acc[status] = flatGroupedAllDurations[status].length;
return acc;
}, {});
// Calculate total value of all statuses
const finalTotal = Object.values(flatGroupedAllDurations).reduce((total, statusArr) => {
return total + statusArr.reduce((acc, curr) => acc + curr.value, 0);
}, 0);
Object.keys(flatGroupedAllDurations).forEach((status) => {
const value = flatGroupedAllDurations[status].reduce((acc, curr) => acc + curr.value, 0);
const humanReadable = durationToHumanReadable(moment.duration(value));
const percentage = finalTotal > 0 ? (value / finalTotal) * 100 : 0;
const color = getLifecycleStatusColor(status);
const roundedPercentage = `${Math.round(percentage)}%`;
const averageValue = _.size(jobIDs) > 0 ? value / jobIDs.length : 0;
const averageHumanReadable = durationToHumanReadable(moment.duration(averageValue));
finalSummations.push({
status,
value,
humanReadable,
percentage,
color,
roundedPercentage,
averageValue,
averageHumanReadable
});
});
return res.status(200).json({
jobIDs,
transition: groupedTransitions,
durations: {
jobs: jobIDs.length,
summations: finalSummations,
totalStatuses: finalSummations.length,
total: finalTotal,
statusCounts: finalStatusCounts,
humanReadable: durationToHumanReadable(moment.duration(finalTotal)),
averageValue: _.size(jobIDs) > 0 ? finalTotal / jobIDs.length : 0,
averageHumanReadable:
_.size(jobIDs) > 0
? durationToHumanReadable(moment.duration(finalTotal / jobIDs.length))
: durationToHumanReadable(moment.duration(0))
}
});
}; };
module.exports = jobLifecycle; module.exports = jobLifecycle;

View File

@@ -50,7 +50,12 @@ const autoAddWatchers = async (req) => {
try { try {
// Fetch bodyshop data from Redis // Fetch bodyshop data from Redis
const bodyshopData = await getBodyshopFromRedis(shopId); const bodyshopData = await getBodyshopFromRedis(shopId);
const notificationFollowers = bodyshopData?.notification_followers || []; let notificationFollowers = bodyshopData?.notification_followers;
// Bail if notification_followers is missing or not an array
if (!notificationFollowers || !Array.isArray(notificationFollowers)) {
return;
}
// Execute queries in parallel // Execute queries in parallel
const [notificationData, existingWatchersData] = await Promise.all([ const [notificationData, existingWatchersData] = await Promise.all([