180 lines
		
	
	
		
			4.7 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			180 lines
		
	
	
		
			4.7 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
/**
 | 
						|
 * 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
 | 
						|
    };
 | 
						|
  }
 | 
						|
}
 |