/**
* Retry with exponential backoff for throttled AWS calls.
*/
const THROTTLE_NAMES = [
"ThrottlingException",
"TooManyRequestsException",
"ProvisionedThroughputExceededException",
"RequestLimitExceeded",
"Rate exceeded",
];
function isThrottleError(err: unknown): boolean {
const msg = (err as Error)?.message ?? String(err);
const name = (err as Error & { name?: string })?.name ?? "";
return (
THROTTLE_NAMES.some((n) => msg.includes(n) || name.includes(n)) ||
(err as { $metadata?: { httpStatusCode?: number } })?.$metadata
?.httpStatusCode === 429
);
}
export async function withRetry<T>(
fn: () => Promise<T>,
options: { maxAttempts?: number; baseDelayMs?: number } = {}
): Promise<T> {
const maxAttempts = options.maxAttempts ?? 3;
const baseDelayMs = options.baseDelayMs ?? 500;
let lastError: unknown;
for (let attempt = 0; attempt < maxAttempts; attempt++) {
try {
return await fn();
} catch (err) {
lastError = err;
if (!isThrottleError(err) || attempt === maxAttempts - 1) throw err;
const delay = baseDelayMs * Math.pow(2, attempt);
await new Promise((r) => setTimeout(r, delay));
}
}
throw lastError;
}