feature/IO-3303-Socket-IO-Optimization-Auto-Add-Watchers-Gate - SocketIO Optimization / Auto Add Watchers Gate
This commit is contained in:
@@ -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";
|
||||||
|
|||||||
@@ -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={{
|
||||||
|
|||||||
@@ -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 &&
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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([
|
||||||
|
|||||||
Reference in New Issue
Block a user