@bernierllc/retry-policy
Version:
Atomic retry policy utilities with exponential backoff and jitter
258 lines (226 loc) • 7.62 kB
text/typescript
/*
Copyright (c) 2025 Bernier LLC
This file is licensed to the client under a limited-use license.
The client may use and modify this code *only within the scope of the project it was delivered for*.
Redistribution or use in other products or commercial offerings is not permitted without written consent from Bernier LLC.
*/
import { RetryPolicyOptions, RetryPolicyResult, JitterConfig, BackoffConfig } from './types';
import { loadOptionalRetryPolicyConfig } from './config/loadOptionalConfig';
/**
* Default retry policy options
*/
export const DEFAULT_RETRY_OPTIONS: Required<RetryPolicyOptions> = {
maxRetries: 5,
initialDelayMs: 1000,
maxDelayMs: 30000,
backoffFactor: 2,
jitter: true,
shouldRetry: () => true,
onRetry: () => {},
onFailure: () => {}
};
/**
* Default jitter configuration
*/
export const DEFAULT_JITTER_CONFIG: JitterConfig = {
type: 'full',
factor: 0.1
};
/**
* Default backoff configuration
*/
export const DEFAULT_BACKOFF_CONFIG: BackoffConfig = {
type: 'exponential',
baseDelay: 1000,
maxDelay: 30000,
factor: 2,
jitter: DEFAULT_JITTER_CONFIG
};
/**
* Core retry policy class that provides atomic retry utilities with optional runtime configuration
*/
export class RetryPolicy {
private options: Required<RetryPolicyOptions>;
private backoffConfig: BackoffConfig;
constructor(options?: Partial<RetryPolicyOptions>, backoffConfig?: Partial<BackoffConfig>) {
// Load optional runtime configuration
const runtimeConfig = loadOptionalRetryPolicyConfig();
// Map runtime config to constructor parameters format, filtering out undefined values
const runtimeOptions: Partial<RetryPolicyOptions> = {};
if (runtimeConfig.maxRetries !== undefined) runtimeOptions.maxRetries = runtimeConfig.maxRetries;
if (runtimeConfig.initialDelayMs !== undefined) runtimeOptions.initialDelayMs = runtimeConfig.initialDelayMs;
if (runtimeConfig.maxDelayMs !== undefined) runtimeOptions.maxDelayMs = runtimeConfig.maxDelayMs;
if (runtimeConfig.backoffFactor !== undefined) runtimeOptions.backoffFactor = runtimeConfig.backoffFactor;
if (runtimeConfig.jitter !== undefined) runtimeOptions.jitter = runtimeConfig.jitter;
if (runtimeConfig.shouldRetry !== undefined) runtimeOptions.shouldRetry = runtimeConfig.shouldRetry;
if (runtimeConfig.onRetry !== undefined) runtimeOptions.onRetry = runtimeConfig.onRetry;
if (runtimeConfig.onFailure !== undefined) runtimeOptions.onFailure = runtimeConfig.onFailure;
const runtimeBackoffConfig: Partial<BackoffConfig> = runtimeConfig.backoff || {};
// Merge configuration: defaults < runtime config < constructor params
this.options = {
...DEFAULT_RETRY_OPTIONS,
...runtimeOptions,
...options
};
this.backoffConfig = {
...DEFAULT_BACKOFF_CONFIG,
...runtimeBackoffConfig,
...backoffConfig
};
// Check if retry functionality is globally disabled
if (runtimeConfig.enabled === false) {
// Override maxRetries to 0 to disable retries
this.options.maxRetries = 0;
}
}
/**
* Evaluate whether an operation should be retried based on current attempt and error
*/
evaluateRetry(attempt: number, error: any): RetryPolicyResult {
const shouldRetry = this.shouldRetry(attempt, error);
const delay = shouldRetry ? this.calculateDelay(attempt) : 0;
const isFinalAttempt = attempt >= this.options.maxRetries;
return {
shouldRetry,
delay,
attempt,
isFinalAttempt
};
}
/**
* Calculate the delay for the next retry attempt
*/
calculateDelay(attempt: number): number {
let delay: number;
switch (this.backoffConfig.type) {
case 'exponential':
delay = this.calculateExponentialDelay(attempt);
break;
case 'linear':
delay = this.calculateLinearDelay(attempt);
break;
case 'constant':
delay = this.backoffConfig.baseDelay;
break;
default:
delay = this.calculateExponentialDelay(attempt);
}
// Apply jitter if enabled
if (this.options.jitter && this.backoffConfig.jitter) {
delay = this.applyJitter(delay, this.backoffConfig.jitter);
}
// Ensure delay doesn't exceed maximum
return Math.min(delay, this.backoffConfig.maxDelay);
}
/**
* Calculate exponential backoff delay
*/
private calculateExponentialDelay(attempt: number): number {
const factor = this.backoffConfig.factor || 2;
return this.backoffConfig.baseDelay * Math.pow(factor, attempt);
}
/**
* Calculate linear backoff delay
*/
private calculateLinearDelay(attempt: number): number {
return this.backoffConfig.baseDelay * (attempt + 1);
}
/**
* Apply jitter to a delay value
*/
private applyJitter(delay: number, jitterConfig: JitterConfig): number {
const random = Math.random();
switch (jitterConfig.type) {
case 'full':
// Full jitter: random value between 0 and delay
return delay * random;
case 'equal':
// Equal jitter: random value between delay/2 and delay
return delay * (0.5 + random * 0.5);
case 'decorrelated':
// Decorrelated jitter: random value between delay and delay * 3
return delay * (1 + random * 2);
case 'none':
default:
return delay;
}
}
/**
* Determine if an operation should be retried based on attempt count and error
*/
private shouldRetry(attempt: number, error: any): boolean {
// Don't retry if we've exceeded max attempts
if (attempt >= this.options.maxRetries) {
return false;
}
// Use custom retry condition if provided
return this.options.shouldRetry(error);
}
/**
* Get the current retry policy options
*/
getOptions(): Required<RetryPolicyOptions> {
return { ...this.options };
}
/**
* Get the current backoff configuration
*/
getBackoffConfig(): BackoffConfig {
return { ...this.backoffConfig };
}
/**
* Update retry policy options
*/
updateOptions(options: Partial<RetryPolicyOptions>): void {
this.options = { ...this.options, ...options };
}
/**
* Update backoff configuration
*/
updateBackoffConfig(config: Partial<BackoffConfig>): void {
this.backoffConfig = { ...this.backoffConfig, ...config };
}
/**
* Get configuration sources for transparency
*/
getConfigurationSources(): any[] {
try {
const { loadOptionalRetryPolicyConfigWithSources } = require('./config/loadOptionalConfig');
const { sources } = loadOptionalRetryPolicyConfigWithSources();
return sources;
} catch (error) {
return [];
}
}
}
/**
* Factory function to create a retry policy with default options
*/
export function createRetryPolicy(
options?: Partial<RetryPolicyOptions>,
backoffConfig?: Partial<BackoffConfig>
): RetryPolicy {
return new RetryPolicy(options, backoffConfig);
}
/**
* Utility function to calculate delay for a specific attempt
*/
export function calculateRetryDelay(
attempt: number,
options: Partial<RetryPolicyOptions> = {},
backoffConfig: Partial<BackoffConfig> = {}
): number {
const policy = createRetryPolicy(options, backoffConfig);
return policy.calculateDelay(attempt);
}
/**
* Utility function to determine if an error should trigger a retry
*/
export function shouldRetry(
attempt: number,
error: any,
options: Partial<RetryPolicyOptions> = {}
): boolean {
const policy = createRetryPolicy(options);
return policy.evaluateRetry(attempt, error).shouldRetry;
}