49 lines
1.3 KiB
TypeScript
49 lines
1.3 KiB
TypeScript
import { log } from "./cli.ts";
|
|
|
|
export type RetryOptions = {
|
|
maxAttempts?: number;
|
|
delayMs?: number;
|
|
shouldRetry?: (error: unknown) => boolean;
|
|
label?: string;
|
|
};
|
|
|
|
const defaultShouldRetry = (error: unknown): boolean => {
|
|
if (!(error instanceof Error)) return false;
|
|
// retry on transient network errors
|
|
return (
|
|
error.name === "AbortError" ||
|
|
error.message.includes("fetch failed") ||
|
|
error.message.includes("ECONNRESET") ||
|
|
error.message.includes("ETIMEDOUT")
|
|
);
|
|
};
|
|
|
|
export async function retry<T>(fn: () => Promise<T>, options: RetryOptions = {}): Promise<T> {
|
|
const maxAttempts = options.maxAttempts ?? 3;
|
|
const delayMs = options.delayMs ?? 1000;
|
|
const shouldRetry = options.shouldRetry ?? defaultShouldRetry;
|
|
const label = options.label ?? "operation";
|
|
|
|
let lastError: unknown;
|
|
|
|
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
try {
|
|
return await fn();
|
|
} catch (error) {
|
|
lastError = error;
|
|
|
|
if (attempt === maxAttempts || !shouldRetry(error)) {
|
|
throw error;
|
|
}
|
|
|
|
const delay = delayMs * attempt;
|
|
log.warning(
|
|
`» ${label} failed (attempt ${attempt}/${maxAttempts}), retrying in ${delay}ms...`
|
|
);
|
|
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
}
|
|
}
|
|
|
|
throw lastError;
|
|
}
|