@bernierllc/retry-policy
Version:
Atomic retry policy utilities with exponential backoff and jitter
289 lines (252 loc) • 8.2 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 { 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;
}