UNPKG

@bernierllc/retry-policy

Version:

Atomic retry policy utilities with exponential backoff and jitter

258 lines (226 loc) 7.62 kB
/* 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; }