Files
igny8/frontend/src/components/common/LoadingStateMonitor.tsx
2025-11-09 10:27:02 +00:00

109 lines
3.5 KiB
TypeScript

import { useEffect, useState, useRef } from 'react';
import { useErrorHandler } from '../../hooks/useErrorHandler';
interface LoadingState {
source: string;
startTime: number;
duration: number;
}
const loadingStates = new Map<string, LoadingState>();
const listeners = new Set<(states: LoadingState[]) => void>();
export function trackLoading(source: string, isLoading: boolean) {
if (isLoading) {
loadingStates.set(source, {
source,
startTime: Date.now(),
duration: 0,
});
} else {
loadingStates.delete(source);
}
listeners.forEach(listener => {
const states = Array.from(loadingStates.values()).map(s => ({
...s,
duration: Date.now() - s.startTime,
}));
listener(states);
});
}
export default function LoadingStateMonitor() {
const [localLoadingStates, setLocalLoadingStates] = useState<LoadingState[]>([]);
const { addError } = useErrorHandler('LoadingStateMonitor');
const reportedStuckStates = useRef<Set<string>>(new Set());
const addErrorRef = useRef(addError);
// Keep addError ref updated
useEffect(() => {
addErrorRef.current = addError;
}, [addError]);
useEffect(() => {
const updateStates = (statesFromListener: LoadingState[]) => {
// Use states from listener (always provided by trackLoading)
const states = statesFromListener.filter(s => s.duration < 60000); // Only show states less than 60 seconds old
setLocalLoadingStates(states);
// Detect stuck loading states (more than 5 seconds) - only report once per state
const stuck = states.filter(s => s.duration > 5000 && !reportedStuckStates.current.has(s.source));
if (stuck.length > 0) {
stuck.forEach(state => {
reportedStuckStates.current.add(state.source);
// Use ref to avoid dependency issues
addErrorRef.current(
new Error(`Loading state stuck: ${state.source} (${(state.duration / 1000).toFixed(1)}s)`),
'LoadingStateMonitor'
);
});
}
// Clean up reported states that are no longer stuck
const noLongerStuck = Array.from(reportedStuckStates.current).filter(
source => !states.find(s => s.source === source && s.duration > 5000)
);
noLongerStuck.forEach(source => reportedStuckStates.current.delete(source));
};
// Initial update from global Map
const initialStates = Array.from(loadingStates.values()).map(s => ({
...s,
duration: Date.now() - s.startTime,
}));
updateStates(initialStates);
listeners.add(updateStates);
// Periodic check (in case listener doesn't fire)
const interval = setInterval(() => {
const currentStates = Array.from(loadingStates.values()).map(s => ({
...s,
duration: Date.now() - s.startTime,
}));
updateStates(currentStates);
}, 1000);
return () => {
listeners.delete(updateStates);
clearInterval(interval);
};
}, []); // Empty deps - updateStates reads from global Map via listeners
// Auto-reset stuck loading states after 10 seconds
useEffect(() => {
const stuck = localLoadingStates.filter(s => s.duration > 10000);
if (stuck.length > 0) {
stuck.forEach(state => {
console.warn(`Auto-resetting stuck loading state: ${state.source}`);
trackLoading(state.source, false);
reportedStuckStates.current.delete(state.source);
});
}
}, [localLoadingStates]);
return null; // This component doesn't render anything visible
}