mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-20 12:51:13 -05:00
126 lines
3.2 KiB
TypeScript
126 lines
3.2 KiB
TypeScript
import { useState, useCallback } from 'react';
|
|
import { toast } from '@/hooks/use-toast';
|
|
|
|
interface RetryOptions {
|
|
maxAttempts?: number;
|
|
delayMs?: number;
|
|
exponentialBackoff?: boolean;
|
|
onProgress?: (attempt: number, maxAttempts: number) => void;
|
|
}
|
|
|
|
export function useRetryProgress() {
|
|
const [isRetrying, setIsRetrying] = useState(false);
|
|
const [currentAttempt, setCurrentAttempt] = useState(0);
|
|
const [abortController, setAbortController] = useState<AbortController | null>(null);
|
|
|
|
const retryWithProgress = useCallback(
|
|
async <T,>(
|
|
operation: () => Promise<T>,
|
|
options: RetryOptions = {}
|
|
): Promise<T> => {
|
|
const {
|
|
maxAttempts = 3,
|
|
delayMs = 1000,
|
|
exponentialBackoff = true,
|
|
onProgress,
|
|
} = options;
|
|
|
|
setIsRetrying(true);
|
|
const controller = new AbortController();
|
|
setAbortController(controller);
|
|
|
|
let lastError: Error | null = null;
|
|
let toastId: string | undefined;
|
|
|
|
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
if (controller.signal.aborted) {
|
|
throw new Error('Operation cancelled');
|
|
}
|
|
|
|
setCurrentAttempt(attempt);
|
|
onProgress?.(attempt, maxAttempts);
|
|
|
|
// Show progress toast
|
|
if (attempt > 1) {
|
|
const delay = exponentialBackoff ? delayMs * Math.pow(2, attempt - 2) : delayMs;
|
|
const countdown = Math.ceil(delay / 1000);
|
|
|
|
toast({
|
|
title: `Retrying (${attempt}/${maxAttempts})`,
|
|
description: `Waiting ${countdown}s before retry...`,
|
|
duration: delay,
|
|
});
|
|
|
|
await new Promise(resolve => setTimeout(resolve, delay));
|
|
}
|
|
|
|
try {
|
|
const result = await operation();
|
|
|
|
setIsRetrying(false);
|
|
setCurrentAttempt(0);
|
|
setAbortController(null);
|
|
|
|
// Show success toast
|
|
toast({
|
|
title: "Success",
|
|
description: attempt > 1
|
|
? `Operation succeeded on attempt ${attempt}`
|
|
: 'Operation completed successfully',
|
|
duration: 3000,
|
|
});
|
|
|
|
return result;
|
|
} catch (error) {
|
|
lastError = error instanceof Error ? error : new Error(String(error));
|
|
|
|
if (attempt < maxAttempts) {
|
|
toast({
|
|
title: `Attempt ${attempt} Failed`,
|
|
description: `${lastError.message}. Retrying...`,
|
|
duration: 2000,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
// All attempts failed
|
|
setIsRetrying(false);
|
|
setCurrentAttempt(0);
|
|
setAbortController(null);
|
|
|
|
toast({
|
|
variant: 'destructive',
|
|
title: "All Retries Failed",
|
|
description: `Failed after ${maxAttempts} attempts: ${lastError?.message}`,
|
|
duration: 5000,
|
|
});
|
|
|
|
throw lastError;
|
|
},
|
|
[]
|
|
);
|
|
|
|
const cancel = useCallback(() => {
|
|
if (abortController) {
|
|
abortController.abort();
|
|
setAbortController(null);
|
|
setIsRetrying(false);
|
|
setCurrentAttempt(0);
|
|
|
|
toast({
|
|
title: 'Cancelled',
|
|
description: 'Retry operation cancelled',
|
|
duration: 2000,
|
|
});
|
|
}
|
|
}, [abortController]);
|
|
|
|
return {
|
|
retryWithProgress,
|
|
isRetrying,
|
|
currentAttempt,
|
|
cancel,
|
|
};
|
|
}
|