/** * Timeout Handler Utility * FE-API-014: Request timeout handling with user feedback * * Provides timeout management with progressive user feedback for slow requests */ // CRITICAL FIX: Utiliser le wrapper lazy pour éviter les collisions de noms de variables import toast from '@/utils/toast'; import { ERROR_MESSAGES } from './errorMessages'; /** * Timeout configuration for different request types */ export const TIMEOUT_CONFIG = { // Default timeout (matches apiClient default) default: 10000, // 10 seconds // Fast operations (should be quick) fast: 5000, // 5 seconds // Normal operations normal: 10000, // 10 seconds // Slow operations (uploads, processing) slow: 30000, // 30 seconds // Very slow operations (large uploads, batch operations) verySlow: 60000, // 60 seconds } as const; /** * Warning thresholds for showing user feedback * These are percentages of the timeout duration */ export const WARNING_THRESHOLDS = { // Show warning at 50% of timeout warning: 0.5, // Show critical warning at 80% of timeout critical: 0.8, } as const; /** * Timeout warning messages */ export const TIMEOUT_MESSAGES = { warning: 'La requête prend plus de temps que prévu...', critical: 'La requête est très lente. Vérifiez votre connexion.', timeout: ERROR_MESSAGES.TIMEOUT, } as const; /** * Options for timeout handling */ export interface TimeoutOptions { /** Timeout duration in milliseconds */ timeout?: number; /** Whether to show warning toasts */ showWarnings?: boolean; /** Custom warning message */ warningMessage?: string; /** Custom critical warning message */ criticalMessage?: string; /** Custom timeout message */ timeoutMessage?: string; /** Callback when warning threshold is reached */ onWarning?: () => void; /** Callback when critical threshold is reached */ onCritical?: () => void; /** Callback when timeout occurs */ onTimeout?: () => void; } /** * Creates a promise that rejects after the specified timeout * @param timeoutMs Timeout duration in milliseconds * @param message Error message for timeout * @returns Promise that rejects with a timeout error */ export function createTimeoutPromise( timeoutMs: number, message: string = TIMEOUT_MESSAGES.timeout, ): Promise { return new Promise((_, reject) => { setTimeout(() => { reject(new Error(message)); }, timeoutMs); }); } /** * Wraps a promise with timeout handling and progressive warnings * @param promise The promise to wrap * @param options Timeout options * @returns Promise that resolves/rejects with the original promise or timeout */ export function withTimeout( promise: Promise, options: TimeoutOptions = {}, ): Promise { const { timeout = TIMEOUT_CONFIG.default, showWarnings = true, warningMessage = TIMEOUT_MESSAGES.warning, criticalMessage = TIMEOUT_MESSAGES.critical, timeoutMessage = TIMEOUT_MESSAGES.timeout, onWarning, onCritical, onTimeout, } = options; let warningShown = false; let criticalShown = false; let warningToastId: string | undefined; let criticalToastId: string | undefined; // Calculate warning times const warningTime = timeout * WARNING_THRESHOLDS.warning; const criticalTime = timeout * WARNING_THRESHOLDS.critical; // Set up warning timers const warningTimer = setTimeout(() => { if (showWarnings && !warningShown) { warningShown = true; warningToastId = toast.loading(warningMessage, { duration: timeout - warningTime, // Show until timeout or completion }); onWarning?.(); } }, warningTime); const criticalTimer = setTimeout(() => { if (showWarnings && !criticalShown) { criticalShown = true; // Dismiss warning toast if shown if (warningToastId) { toast.dismiss(warningToastId); } criticalToastId = toast.loading(criticalMessage, { duration: timeout - criticalTime, // Show until timeout or completion }); onCritical?.(); } }, criticalTime); // Create timeout promise const timeoutPromise = createTimeoutPromise(timeout, timeoutMessage); // Race between the original promise and timeout return Promise.race([promise, timeoutPromise]) .then((result) => { // Clear timers if promise resolves before timeout clearTimeout(warningTimer); clearTimeout(criticalTimer); // Dismiss any active toasts if (warningToastId) { toast.dismiss(warningToastId); } if (criticalToastId) { toast.dismiss(criticalToastId); } return result; }) .catch((error) => { // Clear timers clearTimeout(warningTimer); clearTimeout(criticalTimer); // Dismiss any active toasts if (warningToastId) { toast.dismiss(warningToastId); } if (criticalToastId) { toast.dismiss(criticalToastId); } // Call timeout callback if it's a timeout error if (error.message === timeoutMessage) { onTimeout?.(); } throw error; }); } /** * Gets appropriate timeout for a request type * @param requestType Type of request (fast, normal, slow, verySlow) * @returns Timeout duration in milliseconds */ export function getTimeoutForRequestType( requestType: keyof typeof TIMEOUT_CONFIG = 'normal', ): number { return TIMEOUT_CONFIG[requestType]; } /** * Checks if an error is a timeout error * @param error Error to check * @returns True if error is a timeout error */ export function isTimeoutError(error: unknown): boolean { if (error instanceof Error) { return ( error.message === TIMEOUT_MESSAGES.timeout || error.message.includes('timeout') || error.message.includes('expired') || error.name === 'TimeoutError' ); } // Check for Axios timeout errors if (error && typeof error === 'object' && 'code' in error) { const code = (error as any).code; return code === 'ECONNABORTED' || code === 'ETIMEDOUT'; } return false; } /** * Gets user-friendly timeout message based on request type * @param requestType Type of request * @returns User-friendly timeout message */ export function getTimeoutMessage( requestType: keyof typeof TIMEOUT_CONFIG = 'normal', ): string { const messages: Record = { default: 'La requête a expiré. Veuillez réessayer.', fast: 'La requête a expiré. Vérifiez votre connexion et réessayez.', normal: 'La requête a expiré. Veuillez réessayer.', slow: "L'opération prend plus de temps que prévu. Veuillez patienter ou réessayer plus tard.", verySlow: "L'opération est en cours. Cela peut prendre plusieurs minutes. Veuillez patienter.", }; return messages[requestType] || messages.default; } /** * Wraps an API call with timeout and warning feedback * @param apiCall The API call function * @param requestType Type of request for timeout configuration * @param options Additional timeout options * @returns Promise with timeout handling */ export function withRequestTimeout( apiCall: () => Promise, requestType: keyof typeof TIMEOUT_CONFIG = 'normal', options: Omit = {}, ): Promise { const timeout = getTimeoutForRequestType(requestType); const timeoutMessage = getTimeoutMessage(requestType); return withTimeout(apiCall(), { ...options, timeout, timeoutMessage, }); }