An in-browser wisp.place site explorer
1/**
2 * Retry utility for network operations with exponential backoff
3 */
4
5export interface RetryOptions {
6 maxAttempts?: number;
7 initialDelay?: number;
8 maxDelay?: number;
9 backoffFactor?: number;
10 shouldRetry?: (error: unknown) => boolean;
11}
12
13const defaultRetryOptions: Required<RetryOptions> = {
14 maxAttempts: 3,
15 initialDelay: 1000,
16 maxDelay: 10000,
17 backoffFactor: 2,
18 shouldRetry: (error: unknown) => {
19 // Retry on network errors and 5xx errors
20 if (error instanceof TypeError) {
21 // Network errors (fetch failed)
22 return true;
23 }
24 if (
25 error instanceof Error &&
26 'status' in error &&
27 typeof (error as { status: number }).status === 'number'
28 ) {
29 const status = (error as { status: number }).status;
30 return status >= 500 || status === 429; // 5xx or 429 (rate limit)
31 }
32 return false;
33 },
34};
35
36/**
37 * Sleep for a specified duration
38 */
39function sleep(ms: number): Promise<void> {
40 return new Promise((resolve) => setTimeout(resolve, ms));
41}
42
43/**
44 * Calculate delay with exponential backoff
45 */
46function calculateDelay(
47 attempt: number,
48 options: Required<RetryOptions>
49): number {
50 const delay = options.initialDelay * Math.pow(options.backoffFactor, attempt);
51 return Math.min(delay, options.maxDelay);
52}
53
54/**
55 * Execute a function with retry logic
56 */
57export async function withRetry<T>(
58 fn: () => Promise<T>,
59 options: RetryOptions = {}
60): Promise<T> {
61 const opts = { ...defaultRetryOptions, ...options };
62
63 let lastError: unknown;
64
65 for (let attempt = 0; attempt < opts.maxAttempts; attempt++) {
66 try {
67 return await fn();
68 } catch (error) {
69 lastError = error;
70
71 const isLastAttempt = attempt === opts.maxAttempts - 1;
72 const shouldRetry = !isLastAttempt && opts.shouldRetry(error);
73
74 if (!shouldRetry) {
75 throw error;
76 }
77
78 const delay = calculateDelay(attempt, opts);
79 console.warn(
80 `Retry attempt ${attempt + 1}/${opts.maxAttempts} after ${delay}ms`,
81 { error }
82 );
83
84 await sleep(delay);
85 }
86 }
87
88 throw lastError;
89}
90
91/**
92 * Create a retryable version of a function
93 */
94export function retryable<T extends Array<unknown>, U>(
95 fn: (...args: T) => Promise<U>,
96 options: RetryOptions = {}
97): (...args: T) => Promise<U> {
98 return (...args: T) => withRetry(() => fn(...args), options);
99}