109 lines
3.5 KiB
TypeScript
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
|
|
}
|
|
|