@sailboat-computer/event-bus
Version:
Standardized event bus for sailboat computer v3 with resilience features and offline capabilities
154 lines (129 loc) • 3.62 kB
text/typescript
/**
* Retry utilities for the event bus
*/
import { logger } from './logger';
/**
* Retry options
*/
export interface RetryOptions {
/**
* Maximum number of retry attempts
*/
maxRetries: number;
/**
* Base delay in milliseconds
*/
baseDelay: number;
/**
* Maximum delay in milliseconds
*/
maxDelay: number;
/**
* Whether to use exponential backoff
*/
exponential: boolean;
/**
* Jitter factor (0-1) to add randomness to delays
*/
jitter: number;
/**
* Function to determine if an error is retryable
*/
isRetryable?: (error: Error) => boolean;
/**
* Function to call before each retry attempt
*/
onRetry?: (error: Error, attempt: number, delay: number) => void;
}
/**
* Default retry options
*/
export const DEFAULT_RETRY_OPTIONS: RetryOptions = {
maxRetries: 3,
baseDelay: 1000,
maxDelay: 30000,
exponential: true,
jitter: 0.2,
onRetry: (error, attempt, delay) => {
logger.warn(`Retry attempt ${attempt} after ${delay}ms due to error: ${error.message}`);
}
};
/**
* Retry a function with exponential backoff
*
* @param fn - Function to retry
* @param options - Retry options
* @returns Promise that resolves with the function result or rejects with the last error
*/
export async function retry<T>(
fn: () => Promise<T>,
options: Partial<RetryOptions> = {}
): Promise<T> {
const retryOptions: RetryOptions = {
...DEFAULT_RETRY_OPTIONS,
...options
};
let lastError: Error | undefined;
for (let attempt = 0; attempt <= retryOptions.maxRetries; attempt++) {
try {
// First attempt is not a retry
if (attempt === 0) {
return await fn();
}
// Calculate delay with exponential backoff and jitter
const delay = calculateDelay(attempt, retryOptions);
// Call onRetry callback if provided
if (retryOptions.onRetry && lastError) {
retryOptions.onRetry(lastError, attempt, delay);
}
// Wait for the calculated delay
await sleep(delay);
// Try again
return await fn();
} catch (error) {
lastError = error instanceof Error ? error : new Error(String(error));
// Check if we've reached the maximum number of retries
if (attempt >= retryOptions.maxRetries) {
break;
}
// Check if the error is retryable
if (retryOptions.isRetryable && !retryOptions.isRetryable(lastError)) {
break;
}
}
}
// If we get here, all retries failed
if (lastError) {
throw lastError;
}
// This should never happen, but TypeScript needs it
throw new Error('Retry failed for unknown reason');
}
/**
* Calculate delay with exponential backoff and jitter
*
* @param attempt - Retry attempt number (1-based)
* @param options - Retry options
* @returns Delay in milliseconds
*/
function calculateDelay(attempt: number, options: RetryOptions): number {
// Calculate base delay
let delay = options.exponential
? Math.min(options.baseDelay * Math.pow(2, attempt - 1), options.maxDelay)
: options.baseDelay;
// Add jitter
if (options.jitter > 0) {
const jitterAmount = delay * options.jitter;
delay = delay - jitterAmount + Math.random() * jitterAmount * 2;
}
return Math.min(delay, options.maxDelay);
}
/**
* Sleep for a specified duration
*
* @param ms - Duration in milliseconds
* @returns Promise that resolves after the specified duration
*/
function sleep(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}