/**
* Exponential Backoff Utility
* Provides retry logic with exponential backoff for network operations
*/
export class ExponentialBackoff {
constructor(options = {}) {
this.initialDelay = options.initialDelay || 1000; // 1 second
this.maxDelay = options.maxDelay || 30000; // 30 seconds
this.multiplier = options.multiplier || 2;
this.maxAttempts = options.maxAttempts || 10;
this.jitter = options.jitter !== false; // Add randomization by default
this.reset();
}
reset() {
this.attempt = 0;
this.currentDelay = this.initialDelay;
}
get canRetry() {
return this.attempt < this.maxAttempts;
}
get attemptsRemaining() {
return Math.max(0, this.maxAttempts - this.attempt);
}
get nextDelay() {
if (!this.canRetry) {
return null;
}
this.attempt++;
// Calculate exponential delay
let delay = Math.min(
this.initialDelay * Math.pow(this.multiplier, this.attempt - 1),
this.maxDelay
);
// Add jitter to prevent thundering herd
if (this.jitter) {
delay = delay * (0.5 + Math.random() * 0.5);
}
this.currentDelay = Math.floor(delay);
return this.currentDelay;
}
async wait() {
const delay = this.nextDelay;
if (delay === null) {
throw new Error('Maximum retry attempts exceeded');
}
return new Promise(resolve => setTimeout(resolve, delay));
}
async execute(fn, options = {}) {
const { onRetry, shouldRetry } = options;
while (this.canRetry) {
try {
const result = await fn(this.attempt);
this.reset(); // Success, reset the backoff
return result;
} catch (error) {
// Check if we should retry this error
if (shouldRetry && !shouldRetry(error, this.attempt)) {
throw error;
}
if (!this.canRetry) {
throw new Error(`Failed after ${this.attempt} attempts: ${error.message}`);
}
// Notify about retry
if (onRetry) {
onRetry(error, this.attempt, this.nextDelay);
}
await this.wait();
}
}
throw new Error('Maximum retry attempts exceeded');
}
}
/**
* Create a simple retry function with exponential backoff
*/
export function createRetryWithBackoff(options = {}) {
const backoff = new ExponentialBackoff(options);
return async function retry(fn) {
return backoff.execute(fn, {
onRetry: options.onRetry,
shouldRetry: options.shouldRetry
});
};
}