@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
JavaScript
/**
* @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,
},
};