@plust/datasleuth
Version:
Build LLM-powered research pipelines and output structured data.
127 lines • 4.85 kB
JavaScript
import { BaseResearchError } from '../types/errors.js';
import { logger } from './logging.js';
/**
* Default retry options
*/
const DEFAULT_RETRY_OPTIONS = {
maxRetries: 3,
retryDelay: 1000,
backoffFactor: 2,
};
/**
* Default function to determine if an error is retryable
*/
const defaultIsRetryable = (error) => error instanceof BaseResearchError && error.retry === true;
/**
* Default function to run before each retry attempt
*/
const defaultOnRetry = (attempt, error, delay) => {
logger.warn(`Retry attempt ${attempt} after error: ${error instanceof Error ? error.message : 'Unknown error'}. ` +
`Retrying in ${delay}ms...`);
};
/**
* Execute a function with automatic retry for transient errors
*
* This utility function wraps an asynchronous operation with retry logic that
* can handle transient failures. It supports exponential backoff, customizable
* retry conditions, and notifications on retry attempts.
*
* @param fn - The async function to execute with retry logic
* @param options - Retry configuration options
* @param options.maxRetries - Maximum number of retry attempts (default: 3)
* @param options.retryDelay - Initial delay between retries in milliseconds (default: 1000)
* @param options.backoffFactor - Factor by which to increase delay on each retry (default: 2)
* @param options.retryableErrors - Function to determine if an error is retryable
* @param options.onRetry - Function to run before each retry attempt
*
* @returns The result of the function execution
* @throws The last error encountered if all retries fail
*
* @example
* ```typescript
* import { executeWithRetry } from '@plust/datasleuth';
*
* const result = await executeWithRetry(
* async () => await fetchDataFromAPI(url),
* {
* maxRetries: 3,
* retryDelay: 1000,
* backoffFactor: 2,
* retryableErrors: (error) => {
* // Retry on network errors or rate limiting
* return error instanceof NetworkError ||
* (error instanceof APIError && error.status === 429);
* }
* }
* );
* ```
*/
export async function executeWithRetry(fn, options = {}) {
const maxRetries = options.maxRetries ?? DEFAULT_RETRY_OPTIONS.maxRetries;
const initialDelay = options.retryDelay ?? DEFAULT_RETRY_OPTIONS.retryDelay;
const backoffFactor = options.backoffFactor ?? DEFAULT_RETRY_OPTIONS.backoffFactor;
const isRetryable = options.retryableErrors ?? defaultIsRetryable;
const onRetry = options.onRetry ?? defaultOnRetry;
let lastError;
let attempt = 0;
// Special handling for test environment to prevent timeouts
const shouldUseTestMode = process.env.NODE_ENV === 'test';
// First attempt (attempt 0)
try {
return await fn();
}
catch (error) {
lastError = error;
// If not retryable or no retries allowed, rethrow immediately
if (maxRetries <= 0 || !isRetryable(error)) {
throw error;
}
}
// Start retry attempts (attempt 1 and up)
while (attempt < maxRetries) {
attempt++;
// Calculate delay with exponential backoff
const delay = initialDelay * Math.pow(backoffFactor, attempt - 1);
// Notify about retry
onRetry(attempt, lastError, delay);
// For test environments, we avoid actual delays and just use Promise.resolve()
// This works better with Jest's fake timers
if (shouldUseTestMode) {
// Just advance execution to the next microtask without actual delay
await Promise.resolve();
}
else {
// Use actual setTimeout for production code
await new Promise((resolve) => setTimeout(resolve, delay));
}
try {
return await fn();
}
catch (error) {
lastError = error;
// If this error is not retryable or we've reached max retries, stop
if (attempt >= maxRetries || !isRetryable(error)) {
logger.debug(`Not retrying after error: ${error instanceof Error ? error.message : 'Unknown error'}. ` +
`Reason: ${attempt >= maxRetries ? 'Max retries reached' : 'Error is not retryable'}`);
throw error;
}
}
}
// This should never be reached due to the throw in the catch block
// but TypeScript requires a return statement
throw lastError;
}
/**
* Decorator function that adds retry behavior to any async function
*
* @param options Retry configuration options
* @returns A function decorator that adds retry behavior
*/
export function withRetry(options = {}) {
return function (target) {
return async function (...args) {
return executeWithRetry(() => target(...args), options);
};
};
}
//# sourceMappingURL=retry.js.map