Skip to content

Commit 551bb0d

Browse files
committed
feat: add retry utility with exponential backoff
Adds a generic retry helper for transient failures like API throttling and network errors. Supports configurable max retries, exponential backoff with jitter, and retryable error filtering.
1 parent de2286e commit 551bb0d

1 file changed

Lines changed: 65 additions & 0 deletions

File tree

src/lib/utils/retry.ts

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
/**
2+
* Generic retry utility for transient failures (API throttling, network errors, etc.)
3+
*/
4+
5+
export interface RetryOptions {
6+
maxRetries: number;
7+
baseDelayMs: number;
8+
maxDelayMs: number;
9+
retryableErrors?: string[];
10+
}
11+
12+
const DEFAULT_OPTIONS: RetryOptions = {
13+
maxRetries: 3,
14+
baseDelayMs: 1000,
15+
maxDelayMs: 30000,
16+
};
17+
18+
/**
19+
* Retry an async operation with exponential backoff and jitter.
20+
*/
21+
export async function withRetry<T>(
22+
fn: () => Promise<T>,
23+
options: Partial<RetryOptions> = {},
24+
): Promise<T> {
25+
const opts = { ...DEFAULT_OPTIONS, ...options };
26+
let lastError: Error | undefined;
27+
28+
for (let attempt = 0; attempt < opts.maxRetries; attempt++) {
29+
try {
30+
return await fn();
31+
} catch (err) {
32+
lastError = err as Error;
33+
34+
if (opts.retryableErrors && opts.retryableErrors.length > 0) {
35+
const isRetryable = opts.retryableErrors.some((code) => lastError!.message.includes(code));
36+
if (!isRetryable) {
37+
throw lastError;
38+
}
39+
}
40+
41+
const delay = Math.min(opts.baseDelayMs * Math.pow(2, attempt), opts.maxDelayMs);
42+
await sleep(delay);
43+
}
44+
}
45+
46+
throw lastError;
47+
}
48+
49+
/**
50+
* Parse a retry-after header value to milliseconds.
51+
*/
52+
export function parseRetryAfter(headerValue: string): number {
53+
const seconds = parseInt(headerValue);
54+
if (!isNaN(seconds)) {
55+
return seconds * 1000;
56+
}
57+
58+
// Try parsing as HTTP date
59+
const date = new Date(headerValue);
60+
return date.getTime() - Date.now();
61+
}
62+
63+
function sleep(ms: number): Promise<void> {
64+
return new Promise((resolve) => setTimeout(resolve, ms));
65+
}

0 commit comments

Comments
 (0)