UNPKG

@ai-growth/nextjs

Version:

Seamlessly integrate Sanity CMS with Next.js applications for automated blog routing and rendering

268 lines (267 loc) 8.61 kB
/** * @fileoverview Retry logic utilities for Sanity CMS operations * * This module provides configurable retry mechanisms with exponential backoff, * jitter, and support for different retry strategies. It integrates with the * error handling system to determine which errors are retryable. * * @example * ```typescript * import { withRetry, RetryConfig } from '@ai-growth/nextjs/utils'; * * const config: RetryConfig = { * maxAttempts: 3, * baseDelay: 1000, * maxDelay: 30000, * backoffFactor: 2, * jitter: true * }; * * const result = await withRetry( * () => sanityClient.fetch(query), * config * ); * ``` */ import { isRetryableError, SanityError, createSanityError, SANITY_ERROR_CODES, } from './error-handling'; /** * Default retry configuration */ export const DEFAULT_RETRY_CONFIG = { maxAttempts: 3, baseDelay: 1000, maxDelay: 30000, backoffFactor: 2, strategy: 'exponential', jitter: true, }; /** * Calculate delay for a retry attempt based on strategy */ export function calculateDelay(attempt, config) { let delay; switch (config.strategy) { case 'fixed': delay = config.baseDelay; break; case 'linear': delay = config.baseDelay * attempt; break; case 'exponential': default: delay = config.baseDelay * Math.pow(config.backoffFactor, attempt - 1); break; } // Apply maximum delay limit delay = Math.min(delay, config.maxDelay); // Add jitter if enabled if (config.jitter) { // Add random jitter of ±25% const jitterRange = delay * 0.25; const jitter = (Math.random() - 0.5) * 2 * jitterRange; delay = Math.max(0, delay + jitter); } return Math.round(delay); } /** * Sleep for a specified number of milliseconds */ export function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } /** * Retry an async operation with configurable retry logic */ export async function withRetry(operation, config = {}) { const finalConfig = { ...DEFAULT_RETRY_CONFIG, ...config }; const isRetryableFn = finalConfig.isRetryable || isRetryableError; let lastError; let attempt = 0; while (attempt < finalConfig.maxAttempts) { attempt++; try { const result = await operation(); return result; } catch (error) { lastError = error instanceof Error ? error : new Error(String(error)); // Check if this is the last attempt or if error is not retryable const isLastAttempt = attempt >= finalConfig.maxAttempts; const shouldRetry = isRetryableFn(lastError); if (isLastAttempt || !shouldRetry) { // Call max attempts callback if this was the last attempt if (isLastAttempt && finalConfig.onMaxAttemptsReached) { finalConfig.onMaxAttemptsReached(lastError, attempt); } // Enhance error with retry information if (lastError instanceof SanityError) { throw new SanityError(lastError.message, lastError.code, { ...lastError.context, retryAttempt: attempt, maxAttempts: finalConfig.maxAttempts }, lastError.retryable, lastError.originalError); } else { throw createSanityError(lastError, SANITY_ERROR_CODES.UNKNOWN_ERROR, { retryAttempt: attempt, maxAttempts: finalConfig.maxAttempts, }); } } // Calculate delay for next attempt const delay = calculateDelay(attempt, finalConfig); // Call retry callback if provided if (finalConfig.onRetry) { finalConfig.onRetry(lastError, attempt, delay); } // Wait before next attempt await sleep(delay); } } // This should never be reached, but TypeScript requires it throw lastError; } /** * Create a retry wrapper for a function */ export function createRetryWrapper(fn, config = {}) { return async (...args) => { return withRetry(() => fn(...args), config); }; } /** * Advanced retry with conditional logic */ export async function withConditionalRetry(operation, config = {}) { const conditionalConfig = config; // Create custom retry function that checks conditions const customIsRetryable = (error) => { // First check the base retryable logic if (!isRetryableError(error)) { return false; } // Check SanityError specific conditions if (error instanceof SanityError) { // Check error codes if (conditionalConfig.retryOnCodes && !conditionalConfig.retryOnCodes.includes(error.code)) { return false; } if (conditionalConfig.skipOnCodes && conditionalConfig.skipOnCodes.includes(error.code)) { return false; } // Check status codes if (error.context.statusCode) { if (conditionalConfig.retryOnStatus && !conditionalConfig.retryOnStatus.includes(error.context.statusCode)) { return false; } if (conditionalConfig.skipOnStatus && conditionalConfig.skipOnStatus.includes(error.context.statusCode)) { return false; } } } return true; }; return withRetry(operation, { ...config, isRetryable: customIsRetryable, }); } /** * Retry with circuit breaker pattern */ export class CircuitBreaker { constructor(failureThreshold = 5, recoveryTimeout = 60000 // 1 minute ) { this.failureThreshold = failureThreshold; this.recoveryTimeout = recoveryTimeout; this.failures = 0; this.lastFailureTime = 0; this.state = 'closed'; } async execute(operation) { if (this.state === 'open') { if (Date.now() - this.lastFailureTime > this.recoveryTimeout) { this.state = 'half-open'; } else { throw createSanityError('Circuit breaker is open', SANITY_ERROR_CODES.SERVICE_UNAVAILABLE, { circuitBreakerState: this.state, failures: this.failures, lastFailureTime: this.lastFailureTime, }); } } try { const result = await operation(); // Reset on success if (this.state === 'half-open') { this.state = 'closed'; this.failures = 0; } return result; } catch (error) { this.failures++; this.lastFailureTime = Date.now(); if (this.failures >= this.failureThreshold) { this.state = 'open'; } throw error; } } getState() { return { state: this.state, failures: this.failures, lastFailureTime: this.lastFailureTime, }; } reset() { this.state = 'closed'; this.failures = 0; this.lastFailureTime = 0; } } /** * Create a circuit breaker for Sanity operations */ export function createSanityCircuitBreaker(failureThreshold = 5, recoveryTimeout = 60000) { return new CircuitBreaker(failureThreshold, recoveryTimeout); } /** * Utility to create retry configurations for common scenarios */ export const RETRY_PRESETS = { /** Quick retries for fast operations */ fast: { maxAttempts: 2, baseDelay: 500, maxDelay: 2000, strategy: 'exponential', jitter: true, }, /** Standard retries for most operations */ standard: DEFAULT_RETRY_CONFIG, /** Patient retries for slow operations */ patient: { maxAttempts: 5, baseDelay: 2000, maxDelay: 60000, strategy: 'exponential', jitter: true, }, /** Aggressive retries for critical operations */ aggressive: { maxAttempts: 10, baseDelay: 100, maxDelay: 10000, strategy: 'exponential', jitter: true, }, /** No jitter for predictable timing */ predictable: { maxAttempts: 3, baseDelay: 1000, maxDelay: 30000, strategy: 'exponential', jitter: false, }, };