UNPKG

@bernierllc/retry-policy

Version:

Atomic retry policy utilities with exponential backoff and jitter

289 lines (252 loc) 8.2 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 { cosmiconfigSync } from 'cosmiconfig'; import { RetryPolicyOptions, BackoffConfig } from '../types'; /** * Runtime configuration interface for retry policy * Extends the base types with optional configuration */ export interface RetryPolicyRuntimeConfig extends Partial<RetryPolicyOptions> { /** Backoff configuration */ backoff?: Partial<BackoffConfig>; /** Enable/disable retry functionality globally */ enabled?: boolean; } /** * Configuration source tracking for transparency */ export interface ConfigurationSource { key: string; value: any; source: 'default' | 'file' | 'environment' | 'global' | 'override'; description: string; } /** * Environment variable mappings for retry policy */ const ENV_MAPPINGS = { maxRetries: 'RETRY_MAX_RETRIES', initialDelayMs: 'RETRY_INITIAL_DELAY', maxDelayMs: 'RETRY_MAX_DELAY', backoffFactor: 'RETRY_BACKOFF_FACTOR', jitter: 'RETRY_JITTER', enabled: 'RETRY_ENABLED', 'backoff.type': 'RETRY_BACKOFF_TYPE', 'backoff.baseDelay': 'RETRY_BACKOFF_BASE_DELAY', 'backoff.maxDelay': 'RETRY_BACKOFF_MAX_DELAY', 'backoff.factor': 'RETRY_BACKOFF_MULTIPLIER', 'backoff.jitter.type': 'RETRY_JITTER_TYPE', 'backoff.jitter.factor': 'RETRY_JITTER_FACTOR' }; /** * Global configuration storage for dependency injection */ let globalRetryPolicyConfig: Partial<RetryPolicyRuntimeConfig> = {}; /** * Set global configuration for dependency injection from service packages */ export function setGlobalRetryPolicyConfig(config: Partial<RetryPolicyRuntimeConfig>): void { globalRetryPolicyConfig = { ...globalRetryPolicyConfig, ...config }; console.log('⚙️ Retry Policy: Global configuration updated'); } /** * Get global configuration */ export function getGlobalRetryPolicyConfig(): Partial<RetryPolicyRuntimeConfig> { return { ...globalRetryPolicyConfig }; } /** * Clear global configuration */ export function clearGlobalRetryPolicyConfig(): void { globalRetryPolicyConfig = {}; } /** * Configuration loader with source tracking */ export class RetryPolicyConfigurationLoader { private sources: ConfigurationSource[] = []; /** * Parse environment variable value to appropriate type */ private parseEnvironmentValue(value: string): any { // Boolean values if (value === 'true') return true; if (value === 'false') return false; // Number values if (/^\d+$/.test(value)) return parseInt(value); if (/^\d+\.\d+$/.test(value)) return parseFloat(value); return value; } /** * Set nested object value using dot notation */ private setNestedValue(obj: any, path: string, value: any): void { const keys = path.split('.'); let current = obj; for (let i = 0; i < keys.length - 1; i++) { const key = keys[i]; if (key && !(key in current)) { current[key] = {}; } if (key) { current = current[key]; } } const lastKey = keys[keys.length - 1]; if (lastKey) { current[lastKey] = value; } } /** * Merge configuration objects recursively */ private mergeConfiguration(target: any, source: any): void { Object.keys(source).forEach(key => { if (source[key] && typeof source[key] === 'object' && !Array.isArray(source[key])) { if (!target[key]) target[key] = {}; this.mergeConfiguration(target[key], source[key]); } else { target[key] = source[key]; } }); } /** * Load optional configuration from all sources */ loadOptionalConfiguration(): RetryPolicyRuntimeConfig { const config: RetryPolicyRuntimeConfig = {}; this.sources = []; // Load global configuration (from service packages) if (Object.keys(globalRetryPolicyConfig).length > 0) { this.mergeConfiguration(config, globalRetryPolicyConfig); this.sources.push({ key: 'global', value: globalRetryPolicyConfig, source: 'global', description: 'Global configuration from service packages' }); } // Load configuration file if it exists const explorer = cosmiconfigSync('retry-policy', { searchPlaces: [ 'retry-policy.config.js', 'retry-policy.config.json', '.retry-policyrc', '.retry-policyrc.js', '.retry-policyrc.json' ] }); try { const result = explorer.search(); if (result && result.config) { this.mergeConfiguration(config, result.config); this.sources.push({ key: 'file', value: result.config, source: 'file', description: `Configuration file: ${result.filepath}` }); } } catch { // Fail silently - configuration is optional } // Apply environment variable overrides const envOverrides: ConfigurationSource[] = []; Object.entries(ENV_MAPPINGS).forEach(([configPath, envVar]) => { const envValue = process.env[envVar]; if (envValue !== undefined) { const parsedValue = this.parseEnvironmentValue(envValue); this.setNestedValue(config, configPath, parsedValue); envOverrides.push({ key: configPath, value: parsedValue, source: 'environment', description: `Environment variable: ${envVar}=${envValue}` }); } }); if (envOverrides.length > 0) { this.sources.push(...envOverrides); } // Log configuration sources for transparency if (this.sources.length > 0) { console.log('⚙️ Retry Policy: Runtime configuration loaded'); this.sources.forEach(source => { console.log(` └── ${source.source}: ${source.description}`); }); } return config; } /** * Get configuration sources for transparency */ getConfigurationSources(): ConfigurationSource[] { return [...this.sources]; } /** * Get environment variable documentation */ getEnvironmentVariableDocumentation(): Record<string, string> { const docs: Record<string, string> = {}; Object.entries(ENV_MAPPINGS).forEach(([configPath, envVar]) => { docs[envVar] = `Controls ${configPath} configuration`; }); return docs; } } /** * Load optional retry policy configuration * This function fails gracefully and returns empty config if loading fails */ export function loadOptionalRetryPolicyConfig(): RetryPolicyRuntimeConfig { try { const loader = new RetryPolicyConfigurationLoader(); return loader.loadOptionalConfiguration(); } catch { // Fail silently - configuration is optional for core packages return {}; } } /** * Load configuration with source tracking */ export function loadOptionalRetryPolicyConfigWithSources(): { config: RetryPolicyRuntimeConfig; sources: ConfigurationSource[]; } { try { const loader = new RetryPolicyConfigurationLoader(); const config = loader.loadOptionalConfiguration(); const sources = loader.getConfigurationSources(); return { config, sources }; } catch { // Fail silently - return empty config and sources return { config: {}, sources: [] }; } } /** * Merge configuration with precedence: defaults < global < file < environment < constructor */ export function mergeConfigurations(...configs: Partial<RetryPolicyRuntimeConfig>[]): RetryPolicyRuntimeConfig { const result: RetryPolicyRuntimeConfig = {}; configs.forEach(config => { if (config) { (Object.keys(config) as Array<keyof RetryPolicyRuntimeConfig>).forEach(key => { const value = config[key]; if (value !== undefined) { if (key === 'backoff' && typeof value === 'object') { result.backoff = { ...result.backoff, ...value }; } else { (result as any)[key] = value; } } }); } }); return result; }