All files retry-policy.ts

91.3% Statements 42/46
55% Branches 11/20
88.23% Functions 15/17
91.3% Lines 42/46

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                          1x           2x               1x               1x                     1x         18x 18x             5x 5x 5x   5x                           17x   9x 9x   3x 3x   5x 5x           17x 4x       17x             9x 9x             3x             4x   4x     4x                                   5x 2x       3x             5x             3x             1x             1x             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';
 
/**
 * 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
 */
export class RetryPolicy {
  private options: Required<RetryPolicyOptions>;
  private backoffConfig: BackoffConfig;
 
  constructor(options?: Partial<RetryPolicyOptions>, backoffConfig?: Partial<BackoffConfig>) {
    this.options = { ...DEFAULT_RETRY_OPTIONS, ...options };
    this.backoffConfig = { ...DEFAULT_BACKOFF_CONFIG, ...backoffConfig };
  }
 
  /**
   * 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 };
  }
}
 
/**
 * 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;
}