@bernierllc/retry-policy
Version:
Atomic retry policy utilities with exponential backoff and jitter
312 lines (268 loc) • 9.81 kB
JavaScript
/*
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.
*/
/**
* Configuration printing script for @bernierllc/retry-policy core package
*
* Usage:
* npm run config:print - Print configuration
* npm run config:print -- --sources - Show configuration sources
* npm run config:print -- --json - Print in JSON format
*/
const fs = require('fs');
const path = require('path');
// Package configuration
const PACKAGE_NAME = 'retry-policy';
const CONFIG_FILE = `${PACKAGE_NAME}.config.js`;
// Environment variable mappings
const ENV_MAPPINGS = {
'RETRY_MAX_RETRIES': 'maxRetries',
'RETRY_INITIAL_DELAY': 'initialDelayMs',
'RETRY_MAX_DELAY': 'maxDelayMs',
'RETRY_BACKOFF_FACTOR': 'backoffFactor',
'RETRY_JITTER': 'jitter',
'RETRY_ENABLED': 'enabled',
'RETRY_BACKOFF_TYPE': 'backoff.type',
'RETRY_BACKOFF_BASE_DELAY': 'backoff.baseDelay',
'RETRY_BACKOFF_MAX_DELAY': 'backoff.maxDelay',
'RETRY_BACKOFF_MULTIPLIER': 'backoff.factor',
'RETRY_JITTER_TYPE': 'backoff.jitter.type',
'RETRY_JITTER_FACTOR': 'backoff.jitter.factor'
};
class RetryPolicyConfigurationPrinter {
constructor() {
this.sources = {};
}
parseEnvironmentValue(value) {
if (value === 'true') return true;
if (value === 'false') return false;
if (/^\d+$/.test(value)) return parseInt(value);
if (/^\d+\.\d+$/.test(value)) return parseFloat(value);
return value;
}
setNestedValue(obj, path, value) {
const keys = path.split('.');
let current = obj;
for (let i = 0; i < keys.length - 1; i++) {
const key = keys[i];
if (!(key in current)) {
current[key] = {};
}
current = current[key];
}
current[keys[keys.length - 1]] = value;
}
mergeConfiguration(target, source) {
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];
}
});
}
loadConfiguration() {
// Default configuration
const config = {
maxRetries: 5,
initialDelayMs: 1000,
maxDelayMs: 30000,
backoffFactor: 2,
jitter: true,
enabled: true,
backoff: {
type: 'exponential',
baseDelay: 1000,
maxDelay: 30000,
factor: 2,
jitter: {
type: 'full',
factor: 0.1
}
}
};
// Try to load from built package if available
try {
const builtPackage = require('../dist/index.js');
if (builtPackage.loadOptionalRetryPolicyConfigWithSources) {
const result = builtPackage.loadOptionalRetryPolicyConfigWithSources();
if (result.config && Object.keys(result.config).length > 0) {
this.mergeConfiguration(config, result.config);
}
// Track sources
result.sources?.forEach(source => {
this.sources[source.key || source.source] = source;
});
return config;
}
} catch (error) {
console.log('📦 Using fallback configuration loading (package not built)');
}
// Fallback: manual loading
// Load configuration file if it exists
const configPath = path.join(process.cwd(), CONFIG_FILE);
if (fs.existsSync(configPath)) {
try {
delete require.cache[require.resolve(configPath)];
const fileConfig = require(configPath);
this.mergeConfiguration(config, fileConfig);
this.sources['file'] = {
source: 'file',
description: `Configuration file: ${CONFIG_FILE}`
};
} catch (error) {
console.warn(`⚠️ Failed to load ${CONFIG_FILE}:`, error.message);
}
}
// Apply environment variable overrides
Object.entries(ENV_MAPPINGS).forEach(([envVar, configPath]) => {
if (process.env[envVar]) {
const envValue = this.parseEnvironmentValue(process.env[envVar]);
this.setNestedValue(config, configPath, envValue);
this.sources[configPath] = {
source: 'environment',
description: `Environment variable: ${envVar}=${process.env[envVar]}`
};
}
});
return config;
}
printConfiguration(config, options = {}) {
const { showSources = false, format = 'table' } = options;
console.log(`🔧 ${PACKAGE_NAME} Configuration (${showSources ? 'with sources' : 'core package'})`);
console.log('='.repeat(60));
console.log('');
if (format === 'json') {
if (showSources) {
const configWithSources = {};
this.flattenObject(config).forEach(([key, value]) => {
configWithSources[key] = {
value: value,
source: this.sources[key] || { source: 'default', description: 'Package default' }
};
});
console.log(JSON.stringify(configWithSources, null, 2));
} else {
console.log(JSON.stringify(config, null, 2));
}
return;
}
// Print configuration hierarchy for core packages
console.log('📊 Configuration Hierarchy (Core Package):');
console.log(' 1. 📦 Package Defaults');
console.log(' 2. 🌍 Global Configuration (from service packages)');
console.log(' 3. 📄 Configuration File (retry-policy.config.js)');
console.log(' 4. 🌍 Environment Variables (RETRY_*)');
console.log(' 5. 🔄 Constructor Parameters (highest priority)');
console.log('');
// Core settings
this.printSection('Retry Policy Settings', {
maxRetries: config.maxRetries,
initialDelayMs: config.initialDelayMs,
maxDelayMs: config.maxDelayMs,
backoffFactor: config.backoffFactor,
jitter: config.jitter,
enabled: config.enabled
}, showSources);
// Backoff configuration
if (config.backoff) {
this.printSection('Backoff Strategy', {
type: config.backoff.type,
baseDelay: config.backoff.baseDelay,
maxDelay: config.backoff.maxDelay,
factor: config.backoff.factor
}, showSources);
if (config.backoff.jitter) {
this.printSection('Jitter Configuration', {
type: config.backoff.jitter.type,
factor: config.backoff.jitter.factor
}, showSources);
}
}
// Show environment variable overrides
const envOverrides = Object.entries(this.sources)
.filter(([, source]) => source.source === 'environment');
if (envOverrides.length > 0) {
console.log('🌍 Environment Variable Overrides:');
envOverrides.forEach(([key, source]) => {
console.log(` └── ${source.description}`);
});
console.log('');
}
// Show available environment variables
console.log('🔧 Available Environment Variables:');
Object.entries(ENV_MAPPINGS).forEach(([envVar, configPath]) => {
const isSet = process.env[envVar] ? '✅' : '⚪';
console.log(` ${isSet} ${envVar} → ${configPath}`);
});
console.log('');
// Core package specific notes
console.log('📖 Core Package Notes:');
console.log(' • Configuration is optional - package works without it');
console.log(' • Constructor parameters override configuration');
console.log(' • Service packages can inject global configuration');
console.log(' • No breaking changes to existing usage patterns');
console.log('');
// Show integration example
console.log('🔗 Service Package Integration:');
console.log(' import { setGlobalRetryPolicyConfig } from "@bernierllc/retry-policy";');
console.log(' setGlobalRetryPolicyConfig({ maxRetries: 5, enabled: true });');
}
printSection(title, section, showSources) {
console.log(`📋 ${title}:`);
Object.entries(section).forEach(([key, value]) => {
const source = this.sources[key];
const icon = source ? {
'default': '📦',
'file': '📄',
'environment': '🌍',
'global': '🔗'
}[source.source] || '❓' : '📦';
const displayValue = typeof value === 'object' ? '[object]' : String(value);
if (showSources && source) {
console.log(` ${icon} ${key}: ${displayValue} (${source.source})`);
} else {
console.log(` ${icon} ${key}: ${displayValue}`);
}
});
console.log('');
}
flattenObject(obj, prefix = '') {
const flattened = [];
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
const newKey = prefix ? `${prefix}.${key}` : key;
if (typeof obj[key] === 'object' && obj[key] !== null && !Array.isArray(obj[key])) {
flattened.push(...this.flattenObject(obj[key], newKey));
} else {
flattened.push([newKey, obj[key]]);
}
}
}
return flattened;
}
}
function main() {
const args = process.argv.slice(2);
const options = {
showSources: args.includes('--sources') || args.includes('-s'),
format: args.includes('--json') ? 'json' : 'table'
};
const printer = new RetryPolicyConfigurationPrinter();
try {
const config = printer.loadConfiguration();
printer.printConfiguration(config, options);
} catch (error) {
console.error('❌ Failed to print configuration:', error.message);
process.exit(1);
}
}
if (require.main === module) {
main();
}
module.exports = { RetryPolicyConfigurationPrinter };