@rhofkens/mcp-quotes-server-claude-code
Version:
Model Context Protocol (MCP) server for managing and serving quotes
202 lines • 6.47 kB
JavaScript
/**
* Retry Utility with Exponential Backoff
*
* Provides configurable retry logic with circuit breaker integration
*/
import { CircuitState } from './circuitBreaker.js';
import { APIError, ErrorCode } from './errors.js';
import { logger } from './logger.js';
/**
* Default configuration values
*/
const DEFAULT_CONFIG = {
maxAttempts: 3,
initialDelay: 1000,
maxDelay: 30000,
backoffFactor: 2,
jitter: true,
retryableErrors: [
'ECONNRESET',
'ETIMEDOUT',
'ECONNREFUSED',
'ENOTFOUND',
'EHOSTUNREACH',
'EPIPE',
'ECONNABORTED',
408, // Request Timeout
429, // Too Many Requests
500, // Internal Server Error
502, // Bad Gateway
503, // Service Unavailable
504, // Gateway Timeout
],
circuitBreaker: undefined,
onRetry: () => { },
};
/**
* Check if an error is retryable
*/
function isRetryableError(error, retryableErrors) {
if (error instanceof APIError) {
// Check API error codes
if (error.code === ErrorCode.API_TIMEOUT || error.code === ErrorCode.API_RATE_LIMIT) {
return true;
}
// Check status code if available
const status = error.details?.['status'];
if (status && typeof status === 'number' && retryableErrors.includes(status)) {
return true;
}
}
// Check axios error codes
if (error && typeof error === 'object' && 'code' in error) {
const code = error.code;
if (retryableErrors.includes(code)) {
return true;
}
}
// Check HTTP status codes
if (error && typeof error === 'object' && 'response' in error) {
const status = error.response?.status;
if (status && retryableErrors.includes(status)) {
return true;
}
}
return false;
}
/**
* Calculate delay with optional jitter
*/
function calculateDelay(attempt, config) {
const exponentialDelay = Math.min(config.initialDelay * Math.pow(config.backoffFactor, attempt - 1), config.maxDelay);
if (config.jitter) {
// Add random jitter (±25%)
const jitter = exponentialDelay * 0.25;
return exponentialDelay + (Math.random() * 2 - 1) * jitter;
}
return exponentialDelay;
}
/**
* Sleep for specified milliseconds
*/
function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
/**
* Execute a function with retry logic
*/
export async function retry(fn, config) {
const fullConfig = { ...DEFAULT_CONFIG, ...config };
const stats = {
attempts: 0,
totalDelay: 0,
succeeded: false,
};
// If circuit breaker is provided and open, fail fast
if (fullConfig.circuitBreaker && fullConfig.circuitBreaker.getState() === CircuitState.OPEN) {
throw new APIError('Circuit breaker is open, failing fast', ErrorCode.API_ERROR, 'retry');
}
let lastError;
for (let attempt = 1; attempt <= fullConfig.maxAttempts; attempt++) {
stats.attempts = attempt;
try {
logger.debug('Retry attempt', { attempt, maxAttempts: fullConfig.maxAttempts });
// Execute function (with circuit breaker if configured)
const result = fullConfig.circuitBreaker
? await fullConfig.circuitBreaker.execute(fn)
: await fn();
stats.succeeded = true;
if (attempt > 1) {
logger.info('Retry succeeded', {
attempt,
totalDelay: stats.totalDelay,
});
}
return result;
}
catch (error) {
lastError = error;
stats.lastError = error;
// Check if error is retryable
if (!isRetryableError(error, fullConfig.retryableErrors)) {
logger.warn('Non-retryable error encountered', {
error: error instanceof Error ? error.message : String(error),
attempt,
});
throw error;
}
// Check if we've exhausted retries
if (attempt >= fullConfig.maxAttempts) {
logger.error('All retry attempts exhausted', {
attempts: attempt,
totalDelay: stats.totalDelay,
error: error instanceof Error ? error.message : String(error),
});
throw error;
}
// Calculate delay
const delay = calculateDelay(attempt, fullConfig);
stats.totalDelay += delay;
// Call retry callback
fullConfig.onRetry(error, attempt);
logger.warn('Retryable error, waiting before retry', {
attempt,
nextAttempt: attempt + 1,
delay,
error: error instanceof Error ? error.message : String(error),
});
// Wait before retrying
await sleep(delay);
}
}
// This should never be reached, but TypeScript needs it
throw lastError || new Error('Retry failed with unknown error');
}
/**
* Retry decorator for class methods
*/
export function Retryable(config) {
return function (_target, _propertyKey, descriptor) {
const originalMethod = descriptor.value;
descriptor.value = async function (...args) {
return retry(() => originalMethod.apply(this, args), config);
};
return descriptor;
};
}
/**
* Create a retry wrapper with preset configuration
*/
export function createRetryWrapper(config) {
return (fn) => {
return retry(fn, config);
};
}
/**
* Retry with linear backoff (for simpler cases)
*/
export async function retryLinear(fn, maxAttempts = 3, delay = 1000) {
return retry(fn, {
maxAttempts,
initialDelay: delay,
backoffFactor: 1, // Linear backoff
jitter: false,
});
}
/**
* Retry with immediate first attempt then exponential backoff
*/
export async function retryImmediate(fn, config) {
try {
// Try immediately first
return await fn();
}
catch (error) {
// If it fails, use normal retry with exponential backoff
return retry(fn, {
...config,
maxAttempts: (config?.maxAttempts || 3) - 1, // Subtract the immediate attempt
});
}
}
//# sourceMappingURL=retry.js.map