tenantpilot/worker/utils/retry.ts

76 lines
1.9 KiB
TypeScript

export interface RetryOptions {
maxAttempts?: number;
initialDelayMs?: number;
maxDelayMs?: number;
backoffMultiplier?: number;
shouldRetry?: (error: Error, attempt: number) => boolean;
}
const DEFAULT_OPTIONS: Required<RetryOptions> = {
maxAttempts: 3,
initialDelayMs: 1000,
maxDelayMs: 30000,
backoffMultiplier: 2,
shouldRetry: () => true,
};
/**
* Execute a function with exponential backoff retry logic
*/
export async function withRetry<T>(
fn: () => Promise<T>,
options: RetryOptions = {}
): Promise<T> {
const opts = { ...DEFAULT_OPTIONS, ...options };
let lastError: Error | undefined;
for (let attempt = 1; attempt <= opts.maxAttempts; attempt++) {
try {
return await fn();
} catch (error) {
lastError = error instanceof Error ? error : new Error(String(error));
if (attempt >= opts.maxAttempts || !opts.shouldRetry(lastError, attempt)) {
throw lastError;
}
const delay = Math.min(
opts.initialDelayMs * Math.pow(opts.backoffMultiplier, attempt - 1),
opts.maxDelayMs
);
await new Promise((resolve) => setTimeout(resolve, delay));
}
}
throw lastError || new Error('Retry failed');
}
/**
* Determine if an error is transient and should be retried
*/
export function isTransientError(error: Error): boolean {
const message = error.message.toLowerCase();
// Network errors
if (message.includes('econnreset') ||
message.includes('enotfound') ||
message.includes('etimedout') ||
message.includes('network')) {
return true;
}
// HTTP status codes that should be retried
if (message.includes('429') || // Too Many Requests
message.includes('500') || // Internal Server Error
message.includes('502') || // Bad Gateway
message.includes('503') || // Service Unavailable
message.includes('504')) { // Gateway Timeout
return true;
}
return false;
}
export default withRetry;