UNPKG

claude-flow-novice

Version:

Claude Flow Novice - Advanced orchestration platform for multi-agent AI workflows with CFN Loop architecture Includes Local RuVector Accelerator and all CFN skills for complete functionality.

262 lines (261 loc) 8.14 kB
/** * Retry and Backoff Utilities * * Provides retry logic with exponential backoff for transient failures. * Part of Task 0.5: Implementation Tooling & Utilities (Foundation) * * Usage: * const result = await withRetry( * async () => fetchData(), * { maxAttempts: 3, baseDelayMs: 1000, exponential: true } * ); */ import { createRetryExhaustedError, isRetryableError } from './errors.js'; import { createLogger } from './logging.js'; const logger = createLogger('retry-utility'); /** * Default retry options */ const DEFAULT_RETRY_OPTIONS = { maxAttempts: 3, baseDelayMs: 1000, maxDelayMs: 30000, exponential: true, jitter: true }; /** * Execute a function with retry logic * * @param fn - Async function to execute * @param options - Retry options * @returns Result of the function * @throws RetryExhaustedError if all attempts fail */ export async function withRetry(fn, options = {}) { const opts = { ...DEFAULT_RETRY_OPTIONS, ...options }; const errors = []; const delays = []; const startTime = Date.now(); let attempt = 0; while(attempt < opts.maxAttempts){ attempt++; try { logger.debug('Retry attempt', { attempt, maxAttempts: opts.maxAttempts }); const result = await fn(); // Success! if (attempt > 1) { logger.info('Operation succeeded after retry', { attempt, totalAttempts: attempt, totalTimeMs: Date.now() - startTime }); } return result; } catch (error) { const err = error instanceof Error ? error : new Error(String(error)); errors.push(err); // Check if we should retry const shouldRetry = options.shouldRetry ? options.shouldRetry(err) : isRetryableError(err); if (!shouldRetry) { logger.warn('Error is not retryable', { error: err.message, attempt }); throw err; } // Check if we have attempts remaining if (attempt >= opts.maxAttempts) { logger.error('Retry attempts exhausted', { totalAttempts: attempt, errors: errors.map((e)=>e.message), totalTimeMs: Date.now() - startTime }); throw createRetryExhaustedError(attempt, err); } // Calculate delay for next attempt const delayMs = calculateDelay(attempt, opts); delays.push(delayMs); logger.debug('Retrying after delay', { attempt, delayMs, error: err.message }); // Invoke retry callback if provided if (options.onRetry) { options.onRetry(attempt, err, delayMs); } // Wait before next attempt await sleep(delayMs); } } // This should never be reached due to the throw above, but TypeScript needs it throw createRetryExhaustedError(attempt, errors[errors.length - 1]); } /** * Calculate delay for retry attempt * * @param attempt - Current attempt number (1-based) * @param options - Retry options * @returns Delay in milliseconds */ function calculateDelay(attempt, options) { let delay; if (options.exponential) { // Exponential backoff: baseDelay * 2^(attempt - 1) delay = options.baseDelayMs * Math.pow(2, attempt - 1); } else { // Linear backoff delay = options.baseDelayMs * attempt; } // Cap at max delay delay = Math.min(delay, options.maxDelayMs); // Add jitter to prevent thundering herd if (options.jitter) { const jitterFactor = 0.1; // +/- 10% const jitterRange = delay * jitterFactor; const jitter = (Math.random() * 2 - 1) * jitterRange; delay = Math.max(0, delay + jitter); } return Math.floor(delay); } /** * Sleep for specified duration * * @param ms - Duration in milliseconds * @returns Promise that resolves after duration */ export function sleep(ms) { return new Promise((resolve)=>setTimeout(resolve, ms)); } /** * Execute a function with retry and collect statistics * * @param fn - Async function to execute * @param options - Retry options * @returns Object with result and statistics */ export async function withRetryStats(fn, options = {}) { const opts = { ...DEFAULT_RETRY_OPTIONS, ...options }; const errors = []; const delays = []; const startTime = Date.now(); let attempt = 0; let succeeded = false; let result; const customOnRetry = (attemptNum, error, delayMs)=>{ errors.push(error); delays.push(delayMs); if (options.onRetry) { options.onRetry(attemptNum, error, delayMs); } }; try { result = await withRetry(fn, { ...options, onRetry: customOnRetry }); succeeded = true; attempt = errors.length + 1; } catch (error) { attempt = errors.length + 1; throw error; } finally{ const totalTimeMs = Date.now() - startTime; if (!succeeded) { logger.info('Retry statistics (failed)', { totalAttempts: attempt, succeeded, totalTimeMs, errorCount: errors.length }); } } const stats = { totalAttempts: attempt, succeeded, totalTimeMs: Date.now() - startTime, delays, errors }; return { result: result, stats }; } /** * Retry a function with linear backoff * * @param fn - Async function to execute * @param maxAttempts - Maximum number of attempts * @param delayMs - Delay between attempts in milliseconds * @returns Result of the function */ export async function withLinearRetry(fn, maxAttempts = 3, delayMs = 1000) { return withRetry(fn, { maxAttempts, baseDelayMs: delayMs, exponential: false, jitter: false }); } /** * Retry a function with exponential backoff * * @param fn - Async function to execute * @param maxAttempts - Maximum number of attempts * @param baseDelayMs - Base delay in milliseconds * @returns Result of the function */ export async function withExponentialRetry(fn, maxAttempts = 3, baseDelayMs = 1000) { return withRetry(fn, { maxAttempts, baseDelayMs, exponential: true, jitter: true }); } /** * Create a retryable version of an async function * * @param fn - Async function to make retryable * @param options - Retry options * @returns Retryable function */ export function retryable(fn, options = {}) { return async (...args)=>{ return withRetry(()=>fn(...args), options); }; } /** * Retry an operation until a condition is met * * @param fn - Async function to execute * @param condition - Condition function (returns true to stop retrying) * @param options - Retry options * @returns Result when condition is met * @throws RetryExhaustedError if max attempts reached */ export async function retryUntil(fn, condition, options = {}) { const opts = { ...DEFAULT_RETRY_OPTIONS, ...options }; let attempt = 0; while(attempt < opts.maxAttempts){ attempt++; const result = await fn(); if (condition(result)) { return result; } if (attempt >= opts.maxAttempts) { throw createRetryExhaustedError(attempt); } const delayMs = calculateDelay(attempt, opts); logger.debug('Condition not met, retrying', { attempt, delayMs }); await sleep(delayMs); } throw createRetryExhaustedError(attempt); } //# sourceMappingURL=retry.js.map