159 lines
4.7 KiB
JavaScript
159 lines
4.7 KiB
JavaScript
/**
|
|
* Retry with Exponential Backoff
|
|
*
|
|
* Production-ready retry logic with jitter, max retries, and error filtering.
|
|
*
|
|
* @module v3/shared/resilience/retry
|
|
*/
|
|
/**
|
|
* Retry error with attempt history
|
|
*/
|
|
export class RetryError extends Error {
|
|
attempts;
|
|
errors;
|
|
totalTime;
|
|
constructor(message, attempts, errors, totalTime) {
|
|
super(message);
|
|
this.attempts = attempts;
|
|
this.errors = errors;
|
|
this.totalTime = totalTime;
|
|
this.name = 'RetryError';
|
|
}
|
|
}
|
|
/**
|
|
* Default retry options
|
|
*/
|
|
const DEFAULT_OPTIONS = {
|
|
maxAttempts: 3,
|
|
initialDelay: 100,
|
|
maxDelay: 10000,
|
|
backoffMultiplier: 2,
|
|
jitter: 0.1,
|
|
timeout: 30000,
|
|
};
|
|
/**
|
|
* Retry a function with exponential backoff
|
|
*
|
|
* @param fn Function to retry
|
|
* @param options Retry configuration
|
|
* @returns Result with success/failure and metadata
|
|
*
|
|
* @example
|
|
* const result = await retry(
|
|
* () => fetchData(),
|
|
* { maxAttempts: 5, initialDelay: 200 }
|
|
* );
|
|
*
|
|
* if (result.success) {
|
|
* console.log('Data:', result.result);
|
|
* } else {
|
|
* console.log('Failed after', result.attempts, 'attempts');
|
|
* }
|
|
*/
|
|
export async function retry(fn, options = {}) {
|
|
const opts = { ...DEFAULT_OPTIONS, ...options };
|
|
const errors = [];
|
|
const startTime = Date.now();
|
|
for (let attempt = 1; attempt <= opts.maxAttempts; attempt++) {
|
|
try {
|
|
// Execute with timeout
|
|
const result = await withTimeout(fn(), opts.timeout, attempt);
|
|
return {
|
|
success: true,
|
|
result,
|
|
attempts: attempt,
|
|
totalTime: Date.now() - startTime,
|
|
errors,
|
|
};
|
|
}
|
|
catch (error) {
|
|
const err = error instanceof Error ? error : new Error(String(error));
|
|
errors.push(err);
|
|
// Check if error is retryable
|
|
if (opts.retryableErrors && !opts.retryableErrors(err)) {
|
|
return {
|
|
success: false,
|
|
attempts: attempt,
|
|
totalTime: Date.now() - startTime,
|
|
errors,
|
|
};
|
|
}
|
|
// If this was the last attempt, don't delay
|
|
if (attempt >= opts.maxAttempts) {
|
|
break;
|
|
}
|
|
// Calculate delay with exponential backoff and jitter
|
|
const baseDelay = opts.initialDelay * Math.pow(opts.backoffMultiplier, attempt - 1);
|
|
const jitter = baseDelay * opts.jitter * (Math.random() * 2 - 1);
|
|
const delay = Math.min(baseDelay + jitter, opts.maxDelay);
|
|
// Callback before retry
|
|
if (opts.onRetry) {
|
|
opts.onRetry(err, attempt, delay);
|
|
}
|
|
// Wait before next attempt
|
|
await sleep(delay);
|
|
}
|
|
}
|
|
return {
|
|
success: false,
|
|
attempts: opts.maxAttempts,
|
|
totalTime: Date.now() - startTime,
|
|
errors,
|
|
};
|
|
}
|
|
/**
|
|
* Wrap a function with retry behavior
|
|
*
|
|
* @param fn Function to wrap
|
|
* @param options Retry configuration
|
|
* @returns Wrapped function that retries on failure
|
|
*/
|
|
export function withRetry(fn, options = {}) {
|
|
return async (...args) => {
|
|
return retry(() => fn(...args), options);
|
|
};
|
|
}
|
|
/**
|
|
* Execute with timeout
|
|
*/
|
|
async function withTimeout(promise, timeout, attempt) {
|
|
const timeoutPromise = new Promise((_, reject) => {
|
|
setTimeout(() => {
|
|
reject(new Error(`Attempt ${attempt} timed out after ${timeout}ms`));
|
|
}, timeout);
|
|
});
|
|
return Promise.race([promise, timeoutPromise]);
|
|
}
|
|
/**
|
|
* Sleep for specified milliseconds
|
|
*/
|
|
function sleep(ms) {
|
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
}
|
|
/**
|
|
* Common retryable error predicates
|
|
*/
|
|
export const RetryableErrors = {
|
|
/** Network errors (ECONNRESET, ETIMEDOUT, etc.) */
|
|
network: (error) => {
|
|
const networkCodes = ['ECONNRESET', 'ETIMEDOUT', 'ECONNREFUSED', 'ENOTFOUND', 'EAI_AGAIN'];
|
|
return networkCodes.some((code) => error.message.includes(code));
|
|
},
|
|
/** Rate limit errors (429) */
|
|
rateLimit: (error) => {
|
|
return error.message.includes('429') || error.message.toLowerCase().includes('rate limit');
|
|
},
|
|
/** Server errors (5xx) */
|
|
serverError: (error) => {
|
|
return /5\d\d/.test(error.message) || error.message.includes('Internal Server Error');
|
|
},
|
|
/** Transient errors (network + rate limit + 5xx) */
|
|
transient: (error) => {
|
|
return (RetryableErrors.network(error) ||
|
|
RetryableErrors.rateLimit(error) ||
|
|
RetryableErrors.serverError(error));
|
|
},
|
|
/** All errors are retryable */
|
|
all: () => true,
|
|
};
|
|
//# sourceMappingURL=retry.js.map
|