All files / src retry-policy.ts

89.04% Statements 65/73
64.51% Branches 20/31
88.88% Functions 16/18
98.46% Lines 64/65

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258                  1x         1x           2x               1x               1x                     1x           26x     26x 26x 26x 26x 26x 26x 26x 26x 26x   26x     26x           26x             26x   1x               5x 5x 5x   5x                           23x   9x 9x   3x 3x   9x 9x   2x       23x 8x       23x             11x 11x             3x             8x   8x     4x     1x     1x     2x                 5x 2x       3x             6x             3x             1x             1x             2x 2x 2x 2x                   1x       5x           1x         1x 1x           1x         2x 2x  
/*
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;
    Iif (runtimeConfig.initialDelayMs !== undefined) runtimeOptions.initialDelayMs = runtimeConfig.initialDelayMs;
    Iif (runtimeConfig.maxDelayMs !== undefined) runtimeOptions.maxDelayMs = runtimeConfig.maxDelayMs;
    Iif (runtimeConfig.backoffFactor !== undefined) runtimeOptions.backoffFactor = runtimeConfig.backoffFactor;
    Iif (runtimeConfig.jitter !== undefined) runtimeOptions.jitter = runtimeConfig.jitter;
    Iif (runtimeConfig.shouldRetry !== undefined) runtimeOptions.shouldRetry = runtimeConfig.shouldRetry;
    Iif (runtimeConfig.onRetry !== undefined) runtimeOptions.onRetry = runtimeConfig.onRetry;
    Iif (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;
}