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
 | |
|     };
 | |
|   }
 | |
| }
 |