/** * Retry utilities with exponential backoff * * Features: * - Simple retry with fixed delay * - Exponential backoff with jitter * - Callback hooks for logging/monitoring */ const sleep = (ms) => new Promise(res => setTimeout(res, ms)); /** * Simple retry with fixed delay * * @param {Function} fn - Async function to retry * @param {Object} options * @param {number} options.retries - Number of retries (default: 3) * @param {number} options.delayMs - Fixed delay between retries (default: 100) * @param {Function} options.onRetry - Callback on retry attempt * @returns {Promise} Result of fn */ export async function retry(fn, { retries = 3, delayMs = 100, onRetry } = {}) { let lastErr; for (let i = 0; i <= retries; i++) { try { return await fn(); } catch (err) { lastErr = err; if (i === retries) { break; } if (onRetry) { onRetry({ attempt: i + 1, maxAttempts: retries + 1, err, delay: delayMs }); } await sleep(delayMs); } } throw lastErr; } /** * Retry with exponential backoff and jitter * * @param {Function} fn - Async function to retry * @param {Object} options * @param {number} options.retries - Number of retries (default: 3) * @param {number} options.baseDelayMs - Base delay for exponential backoff (default: 100) * @param {number} options.maxDelayMs - Maximum delay cap (default: 2000) * @param {boolean} options.jitter - Add random jitter (default: true) * @param {Function} options.onRetry - Callback on retry attempt * @returns {Promise} Result of fn */ export async function retryWithBackoff(fn, { retries = 3, baseDelayMs = 100, maxDelayMs = 2000, jitter = true, onRetry } = {}) { let lastErr; for (let i = 0; i <= retries; i++) { try { return await fn(); } catch (err) { lastErr = err; if (i === retries) { break; } // Exponential backoff: baseDelay * 2^attempt const exponential = Math.min(maxDelayMs, baseDelayMs * Math.pow(2, i)); // Add jitter: random value between 50% and 100% of exponential const delay = jitter ? Math.floor(exponential * (0.5 + Math.random() * 0.5)) : exponential; if (onRetry) { onRetry({ attempt: i + 1, maxAttempts: retries + 1, delay, err, exponential }); } await sleep(delay); } } throw lastErr; } /** * Retry with circuit breaker pattern * Fails fast after consecutive errors threshold * * @param {Function} fn - Async function to retry * @param {Object} options * @param {number} options.retries - Retries per attempt (default: 2) * @param {number} options.failureThreshold - Failures before circuit opens (default: 5) * @param {number} options.resetTimeoutMs - Time before circuit half-open (default: 30000) * @param {Function} options.onRetry - Callback on retry * @param {Function} options.onCircuitOpen - Callback when circuit opens * @returns {Promise} Result of fn */ export class CircuitBreaker { constructor({ retries = 2, failureThreshold = 5, resetTimeoutMs = 30_000 } = {}) { this.retries = retries; this.failureThreshold = failureThreshold; this.resetTimeoutMs = resetTimeoutMs; this.failureCount = 0; this.state = 'closed'; // 'closed' | 'open' | 'half-open' this.lastFailureTime = null; } async execute(fn, { onRetry, onCircuitOpen } = {}) { // Check if circuit should reset if (this.state === 'open') { const timeSinceFailure = Date.now() - this.lastFailureTime; if (timeSinceFailure >= this.resetTimeoutMs) { this.state = 'half-open'; this.failureCount = 0; } else { throw new Error(`Circuit breaker is open (reset in ${this.resetTimeoutMs - timeSinceFailure}ms)`); } } try { const result = await retryWithBackoff(fn, { retries: this.retries, onRetry }); // Success: reset failure count if (this.state === 'half-open') { this.state = 'closed'; } this.failureCount = 0; return result; } catch (err) { this.failureCount++; this.lastFailureTime = Date.now(); if (this.failureCount >= this.failureThreshold) { this.state = 'open'; if (onCircuitOpen) { onCircuitOpen({ failureCount: this.failureCount }); } } throw err; } } reset() { this.state = 'closed'; this.failureCount = 0; this.lastFailureTime = null; } getState() { return { state: this.state, failureCount: this.failureCount, failureThreshold: this.failureThreshold }; } }