/** * ErrorContext - Global Error State Management * * Provides centralized error handling with: * - Error queue for multiple errors * - Toast notifications for transient errors * - Persistent errors that require user action * - Error dismissal and retry callbacks */ import React, { createContext, useContext, useCallback, useReducer, useMemo, useRef, ReactNode } from 'react'; import { AppError, ErrorSeverity, isAuthError, isCriticalError } from '@/types/errors'; import { logError } from '@/services/errorHandler'; // Maximum number of errors to keep in history const MAX_ERROR_HISTORY = 50; // Auto-dismiss timeout for non-critical errors (ms) const AUTO_DISMISS_TIMEOUT = 5000; // Error with display metadata interface DisplayableError extends AppError { isVisible: boolean; autoDismiss: boolean; onRetry?: () => void; onDismiss?: () => void; } // State shape interface ErrorContextState { // Current visible error (top of queue) currentError: DisplayableError | null; // All pending errors errorQueue: DisplayableError[]; // Error history (for debugging) errorHistory: AppError[]; // Global loading state for error recovery isRecovering: boolean; } // Actions type ErrorAction = | { type: 'ADD_ERROR'; payload: DisplayableError } | { type: 'DISMISS_CURRENT' } | { type: 'DISMISS_ALL' } | { type: 'DISMISS_BY_ID'; payload: string } | { type: 'SET_RECOVERING'; payload: boolean } | { type: 'CLEAR_HISTORY' }; // Initial state const initialState: ErrorContextState = { currentError: null, errorQueue: [], errorHistory: [], isRecovering: false, }; // Reducer function errorReducer(state: ErrorContextState, action: ErrorAction): ErrorContextState { switch (action.type) { case 'ADD_ERROR': { const newError = action.payload; // Add to history const newHistory = [newError, ...state.errorHistory].slice(0, MAX_ERROR_HISTORY); // If no current error, set this as current if (!state.currentError) { return { ...state, currentError: { ...newError, isVisible: true }, errorHistory: newHistory, }; } // Add to queue return { ...state, errorQueue: [...state.errorQueue, newError], errorHistory: newHistory, }; } case 'DISMISS_CURRENT': { // Get next error from queue const [nextError, ...remainingQueue] = state.errorQueue; return { ...state, currentError: nextError ? { ...nextError, isVisible: true } : null, errorQueue: remainingQueue, }; } case 'DISMISS_ALL': { return { ...state, currentError: null, errorQueue: [], }; } case 'DISMISS_BY_ID': { const errorId = action.payload; // If current error matches, dismiss it if (state.currentError?.errorId === errorId) { const [nextError, ...remainingQueue] = state.errorQueue; return { ...state, currentError: nextError ? { ...nextError, isVisible: true } : null, errorQueue: remainingQueue, }; } // Remove from queue return { ...state, errorQueue: state.errorQueue.filter((e) => e.errorId !== errorId), }; } case 'SET_RECOVERING': { return { ...state, isRecovering: action.payload, }; } case 'CLEAR_HISTORY': { return { ...state, errorHistory: [], }; } default: return state; } } // Context value shape interface ErrorContextValue { // State currentError: DisplayableError | null; errorQueue: DisplayableError[]; errorHistory: AppError[]; isRecovering: boolean; hasErrors: boolean; errorCount: number; // Actions showError: ( error: AppError, options?: { autoDismiss?: boolean; onRetry?: () => void; onDismiss?: () => void; } ) => void; dismissCurrent: () => void; dismissAll: () => void; dismissById: (errorId: string) => void; clearHistory: () => void; setRecovering: (recovering: boolean) => void; // Utilities getErrorsByCategory: (category: string) => AppError[]; getErrorsBySeverity: (severity: ErrorSeverity) => AppError[]; } // Create context const ErrorContext = createContext(undefined); // Provider component interface ErrorProviderProps { children: ReactNode; onAuthError?: (error: AppError) => void; onCriticalError?: (error: AppError) => void; } export function ErrorProvider({ children, onAuthError, onCriticalError, }: ErrorProviderProps) { const [state, dispatch] = useReducer(errorReducer, initialState); // Timer refs for auto-dismiss const dismissTimerRef = useRef | null>(null); // Clear existing timer const clearDismissTimer = useCallback(() => { if (dismissTimerRef.current) { clearTimeout(dismissTimerRef.current); dismissTimerRef.current = null; } }, []); // Schedule auto-dismiss const scheduleAutoDismiss = useCallback((timeout: number = AUTO_DISMISS_TIMEOUT) => { clearDismissTimer(); dismissTimerRef.current = setTimeout(() => { dispatch({ type: 'DISMISS_CURRENT' }); }, timeout); }, [clearDismissTimer]); // Show error const showError = useCallback( ( error: AppError, options?: { autoDismiss?: boolean; onRetry?: () => void; onDismiss?: () => void; } ) => { // Log error logError(error); // Handle auth errors specially if (isAuthError(error) && onAuthError) { onAuthError(error); return; } // Handle critical errors specially if (isCriticalError(error) && onCriticalError) { onCriticalError(error); } // Determine auto-dismiss behavior const shouldAutoDismiss = options?.autoDismiss ?? (error.severity !== 'critical' && error.severity !== 'error'); const displayableError: DisplayableError = { ...error, isVisible: true, autoDismiss: shouldAutoDismiss, onRetry: options?.onRetry, onDismiss: options?.onDismiss, }; dispatch({ type: 'ADD_ERROR', payload: displayableError }); // Schedule auto-dismiss if applicable if (shouldAutoDismiss && !state.currentError) { scheduleAutoDismiss(); } }, [state.currentError, onAuthError, onCriticalError, scheduleAutoDismiss] ); // Dismiss current error const dismissCurrent = useCallback(() => { clearDismissTimer(); // Call onDismiss callback if provided state.currentError?.onDismiss?.(); dispatch({ type: 'DISMISS_CURRENT' }); // Schedule auto-dismiss for next error if applicable const nextError = state.errorQueue[0]; if (nextError?.autoDismiss) { scheduleAutoDismiss(); } }, [state.currentError, state.errorQueue, clearDismissTimer, scheduleAutoDismiss]); // Dismiss all errors const dismissAll = useCallback(() => { clearDismissTimer(); dispatch({ type: 'DISMISS_ALL' }); }, [clearDismissTimer]); // Dismiss by ID const dismissById = useCallback( (errorId: string) => { if (state.currentError?.errorId === errorId) { dismissCurrent(); } else { dispatch({ type: 'DISMISS_BY_ID', payload: errorId }); } }, [state.currentError, dismissCurrent] ); // Clear history const clearHistory = useCallback(() => { dispatch({ type: 'CLEAR_HISTORY' }); }, []); // Set recovering state const setRecovering = useCallback((recovering: boolean) => { dispatch({ type: 'SET_RECOVERING', payload: recovering }); }, []); // Get errors by category const getErrorsByCategory = useCallback( (category: string) => { return state.errorHistory.filter((e) => e.category === category); }, [state.errorHistory] ); // Get errors by severity const getErrorsBySeverity = useCallback( (severity: ErrorSeverity) => { return state.errorHistory.filter((e) => e.severity === severity); }, [state.errorHistory] ); // Context value const value = useMemo( () => ({ currentError: state.currentError, errorQueue: state.errorQueue, errorHistory: state.errorHistory, isRecovering: state.isRecovering, hasErrors: state.currentError !== null || state.errorQueue.length > 0, errorCount: (state.currentError ? 1 : 0) + state.errorQueue.length, showError, dismissCurrent, dismissAll, dismissById, clearHistory, setRecovering, getErrorsByCategory, getErrorsBySeverity, }), [ state.currentError, state.errorQueue, state.errorHistory, state.isRecovering, showError, dismissCurrent, dismissAll, dismissById, clearHistory, setRecovering, getErrorsByCategory, getErrorsBySeverity, ] ); return ( {children} ); } // Hook to use error context export function useErrorContext(): ErrorContextValue { const context = useContext(ErrorContext); if (!context) { throw new Error('useErrorContext must be used within an ErrorProvider'); } return context; } // Convenience hook for showing errors export function useShowError() { const { showError } = useErrorContext(); return showError; } // Safe hook that returns undefined if not in ErrorProvider (for optional use) export function useShowErrorSafe() { const context = useContext(ErrorContext); return context?.showError; } // Convenience hook for current error export function useCurrentError() { const { currentError, dismissCurrent } = useErrorContext(); return { error: currentError, dismiss: dismissCurrent }; } export default ErrorContext;