2025-12-25 12:22:15 +00:00
|
|
|
/**
|
|
|
|
|
* Timeout Handler Utility
|
|
|
|
|
* FE-API-014: Request timeout handling with user feedback
|
2026-01-13 18:47:57 +00:00
|
|
|
*
|
2025-12-25 12:22:15 +00:00
|
|
|
* Provides timeout management with progressive user feedback for slow requests
|
|
|
|
|
*/
|
|
|
|
|
|
2026-01-15 18:26:53 +00:00
|
|
|
// CRITICAL FIX: Utiliser le wrapper lazy pour éviter les collisions de noms de variables
|
|
|
|
|
import toast from '@/utils/toast';
|
2025-12-25 12:22:15 +00:00
|
|
|
import { ERROR_MESSAGES } from './errorMessages';
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Timeout configuration for different request types
|
|
|
|
|
*/
|
|
|
|
|
export const TIMEOUT_CONFIG = {
|
|
|
|
|
// Default timeout (matches apiClient default)
|
|
|
|
|
default: 10000, // 10 seconds
|
2026-01-13 18:47:57 +00:00
|
|
|
|
2025-12-25 12:22:15 +00:00
|
|
|
// Fast operations (should be quick)
|
|
|
|
|
fast: 5000, // 5 seconds
|
2026-01-13 18:47:57 +00:00
|
|
|
|
2025-12-25 12:22:15 +00:00
|
|
|
// Normal operations
|
|
|
|
|
normal: 10000, // 10 seconds
|
2026-01-13 18:47:57 +00:00
|
|
|
|
2025-12-25 12:22:15 +00:00
|
|
|
// Slow operations (uploads, processing)
|
|
|
|
|
slow: 30000, // 30 seconds
|
2026-01-13 18:47:57 +00:00
|
|
|
|
2025-12-25 12:22:15 +00:00
|
|
|
// 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<never> {
|
|
|
|
|
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<T>(
|
|
|
|
|
promise: Promise<T>,
|
|
|
|
|
options: TimeoutOptions = {},
|
|
|
|
|
): Promise<T> {
|
|
|
|
|
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);
|
2026-01-13 18:47:57 +00:00
|
|
|
|
2025-12-25 12:22:15 +00:00
|
|
|
// Dismiss any active toasts
|
|
|
|
|
if (warningToastId) {
|
|
|
|
|
toast.dismiss(warningToastId);
|
|
|
|
|
}
|
|
|
|
|
if (criticalToastId) {
|
|
|
|
|
toast.dismiss(criticalToastId);
|
|
|
|
|
}
|
2026-01-13 18:47:57 +00:00
|
|
|
|
2025-12-25 12:22:15 +00:00
|
|
|
return result;
|
|
|
|
|
})
|
|
|
|
|
.catch((error) => {
|
|
|
|
|
// Clear timers
|
|
|
|
|
clearTimeout(warningTimer);
|
|
|
|
|
clearTimeout(criticalTimer);
|
2026-01-13 18:47:57 +00:00
|
|
|
|
2025-12-25 12:22:15 +00:00
|
|
|
// Dismiss any active toasts
|
|
|
|
|
if (warningToastId) {
|
|
|
|
|
toast.dismiss(warningToastId);
|
|
|
|
|
}
|
|
|
|
|
if (criticalToastId) {
|
|
|
|
|
toast.dismiss(criticalToastId);
|
|
|
|
|
}
|
2026-01-13 18:47:57 +00:00
|
|
|
|
2025-12-25 12:22:15 +00:00
|
|
|
// Call timeout callback if it's a timeout error
|
|
|
|
|
if (error.message === timeoutMessage) {
|
|
|
|
|
onTimeout?.();
|
|
|
|
|
}
|
2026-01-13 18:47:57 +00:00
|
|
|
|
2025-12-25 12:22:15 +00:00
|
|
|
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'
|
|
|
|
|
);
|
|
|
|
|
}
|
2026-01-13 18:47:57 +00:00
|
|
|
|
2025-12-25 12:22:15 +00:00
|
|
|
// Check for Axios timeout errors
|
|
|
|
|
if (error && typeof error === 'object' && 'code' in error) {
|
|
|
|
|
const code = (error as any).code;
|
|
|
|
|
return code === 'ECONNABORTED' || code === 'ETIMEDOUT';
|
|
|
|
|
}
|
2026-01-13 18:47:57 +00:00
|
|
|
|
2025-12-25 12:22:15 +00:00
|
|
|
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<keyof typeof TIMEOUT_CONFIG, string> = {
|
|
|
|
|
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.',
|
2026-01-13 18:47:57 +00:00
|
|
|
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.",
|
2025-12-25 12:22:15 +00:00
|
|
|
};
|
2026-01-13 18:47:57 +00:00
|
|
|
|
2025-12-25 12:22:15 +00:00
|
|
|
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<T>(
|
|
|
|
|
apiCall: () => Promise<T>,
|
|
|
|
|
requestType: keyof typeof TIMEOUT_CONFIG = 'normal',
|
|
|
|
|
options: Omit<TimeoutOptions, 'timeout'> = {},
|
|
|
|
|
): Promise<T> {
|
|
|
|
|
const timeout = getTimeoutForRequestType(requestType);
|
|
|
|
|
const timeoutMessage = getTimeoutMessage(requestType);
|
2026-01-13 18:47:57 +00:00
|
|
|
|
2025-12-25 12:22:15 +00:00
|
|
|
return withTimeout(apiCall(), {
|
|
|
|
|
...options,
|
|
|
|
|
timeout,
|
|
|
|
|
timeoutMessage,
|
|
|
|
|
});
|
|
|
|
|
}
|