@bernierllc/retry-policy
Version:
Atomic retry policy utilities with exponential backoff and jitter
303 lines (248 loc) • 10.2 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 validation script for @bernierllc/retry-policy core package
*
* Usage: npm run config:validate [--strict]
*/
const fs = require('fs');
const path = require('path');
const { z } = require('zod');
// Package configuration
const PACKAGE_NAME = 'retry-policy';
const CONFIG_FILE = `${PACKAGE_NAME}.config.js`;
// Zod validation schemas
const JitterConfigSchema = z.object({
type: z.enum(['none', 'full', 'equal', 'decorrelated']).optional().default('full'),
factor: z.number().min(0).max(1).optional().default(0.1)
});
const BackoffConfigSchema = z.object({
type: z.enum(['exponential', 'linear', 'constant']).optional().default('exponential'),
baseDelay: z.number().positive().optional().default(1000),
maxDelay: z.number().positive().optional().default(30000),
factor: z.number().positive().optional().default(2),
jitter: JitterConfigSchema.optional()
});
const RetryPolicyConfigSchema = z.object({
maxRetries: z.number().min(0).optional().default(5),
initialDelayMs: z.number().positive().optional().default(1000),
maxDelayMs: z.number().positive().optional().default(30000),
backoffFactor: z.number().positive().optional().default(2),
jitter: z.boolean().optional().default(true),
enabled: z.boolean().optional().default(true),
backoff: BackoffConfigSchema.optional(),
shouldRetry: z.function().optional(),
onRetry: z.function().optional(),
onFailure: z.function().optional()
});
// Environment variable validation
const EnvironmentVariablesSchema = z.object({
RETRY_MAX_RETRIES: z.string().regex(/^\d+$/).optional(),
RETRY_INITIAL_DELAY: z.string().regex(/^\d+$/).optional(),
RETRY_MAX_DELAY: z.string().regex(/^\d+$/).optional(),
RETRY_BACKOFF_FACTOR: z.string().regex(/^\d+(\.\d+)?$/).optional(),
RETRY_JITTER: z.enum(['true', 'false']).optional(),
RETRY_ENABLED: z.enum(['true', 'false']).optional(),
RETRY_BACKOFF_TYPE: z.enum(['exponential', 'linear', 'constant']).optional(),
RETRY_BACKOFF_BASE_DELAY: z.string().regex(/^\d+$/).optional(),
RETRY_BACKOFF_MAX_DELAY: z.string().regex(/^\d+$/).optional(),
RETRY_BACKOFF_MULTIPLIER: z.string().regex(/^\d+(\.\d+)?$/).optional(),
RETRY_JITTER_TYPE: z.enum(['none', 'full', 'equal', 'decorrelated']).optional(),
RETRY_JITTER_FACTOR: z.string().regex(/^\d+(\.\d+)?$/).optional()
});
class RetryPolicyConfigurationValidator {
constructor() {
this.configPath = path.join(process.cwd(), CONFIG_FILE);
this.strict = process.argv.includes('--strict');
this.errors = [];
this.warnings = [];
}
validateConfigurationFile() {
console.log(`🔍 Validating configuration file: ${CONFIG_FILE}`);
if (!fs.existsSync(this.configPath)) {
console.log('⚪ Configuration file not found (optional for core packages)');
return true; // This is OK for core packages
}
try {
// Clear require cache
delete require.cache[require.resolve(this.configPath)];
const config = require(this.configPath);
// Validate structure
const result = RetryPolicyConfigSchema.safeParse(config);
if (!result.success) {
this.errors.push('Configuration validation failed:');
result.error.issues.forEach(issue => {
const path = issue.path.join('.');
this.errors.push(` └── ${path}: ${issue.message}`);
});
return false;
}
// Additional validation for strict mode
if (this.strict) {
this.performStrictValidation(result.data);
}
console.log('✅ Configuration file is valid');
return true;
} catch (error) {
this.errors.push(`Failed to load configuration file: ${error.message}`);
return false;
}
}
validateEnvironmentVariables() {
console.log('🌍 Validating environment variables');
const retryEnvVars = Object.fromEntries(
Object.entries(process.env).filter(([key]) => key.startsWith('RETRY_'))
);
const result = EnvironmentVariablesSchema.safeParse(retryEnvVars);
if (!result.success) {
this.errors.push('Environment variables validation failed:');
result.error.issues.forEach(issue => {
const envVar = issue.path[0];
this.errors.push(` └── ${envVar}: ${issue.message}`);
});
return false;
}
const setVars = Object.keys(retryEnvVars).length;
if (setVars > 0) {
console.log(`✅ Environment variables are valid (${setVars} set)`);
// List set environment variables
Object.keys(retryEnvVars).forEach(key => {
console.log(` └── ${key}=${retryEnvVars[key]}`);
});
} else {
console.log('✅ No retry policy environment variables set (using defaults)');
}
return true;
}
performStrictValidation(config) {
console.log('🔒 Performing strict validation');
// Check for logical inconsistencies
if (config.maxDelayMs && config.initialDelayMs && config.maxDelayMs < config.initialDelayMs) {
this.errors.push('maxDelayMs cannot be less than initialDelayMs');
}
if (config.maxRetries === 0 && config.enabled !== false) {
this.warnings.push('maxRetries is 0 but enabled is not explicitly false');
}
if (config.backoff?.maxDelay && config.backoff.baseDelay &&
config.backoff.maxDelay < config.backoff.baseDelay) {
this.errors.push('backoff.maxDelay cannot be less than backoff.baseDelay');
}
// Check for conflicting configurations
if (config.jitter === false && config.backoff?.jitter?.type !== 'none') {
this.warnings.push('jitter is disabled but backoff.jitter.type is not "none"');
}
// Performance warnings
if (config.maxRetries > 10) {
this.warnings.push('maxRetries > 10 may lead to long operation times');
}
if (config.maxDelayMs > 300000) { // 5 minutes
this.warnings.push('maxDelayMs > 5 minutes may cause timeout issues');
}
}
validateDependencies() {
console.log('📦 Validating dependencies');
try {
// Check if cosmiconfig is available
require('cosmiconfig');
console.log('✅ Configuration dependency cosmiconfig is available');
return true;
} catch (error) {
this.errors.push(`Dependency validation failed: ${error.message}`);
return false;
}
}
validateBackwardCompatibility() {
console.log('🔄 Validating backward compatibility');
try {
// Test that the package still works without configuration
const builtPackage = require('../dist/index.js');
// Test basic factory function
const policy = builtPackage.createRetryPolicy();
if (!policy) {
this.errors.push('createRetryPolicy() factory function failed');
return false;
}
// Test utility functions
const delay = builtPackage.calculateRetryDelay(1);
if (typeof delay !== 'number') {
this.errors.push('calculateRetryDelay() utility function failed');
return false;
}
const shouldRetry = builtPackage.shouldRetry(1, new Error('test'));
if (typeof shouldRetry !== 'boolean') {
this.errors.push('shouldRetry() utility function failed');
return false;
}
console.log('✅ Backward compatibility validated');
return true;
} catch (error) {
this.warnings.push(`Backward compatibility check failed: ${error.message} (may need to build package first)`);
return true; // Don't fail validation for this
}
}
generateReport() {
console.log('');
console.log('📊 Validation Report');
console.log('='.repeat(50));
if (this.errors.length === 0 && this.warnings.length === 0) {
console.log('✅ All validations passed');
console.log(`🔧 Package: ${PACKAGE_NAME} (core - behavioral)`);
console.log('📋 Configuration is ready for use');
console.log('');
console.log('🎯 Core Package Features:');
console.log(' • Optional configuration - works without it');
console.log(' • Full backward compatibility maintained');
console.log(' • Service package integration support');
console.log(' • Environment variable support');
return true;
}
if (this.errors.length > 0) {
console.log('');
console.log('❌ Errors found:');
this.errors.forEach(error => console.log(` ${error}`));
}
if (this.warnings.length > 0) {
console.log('');
console.log('⚠️ Warnings:');
this.warnings.forEach(warning => console.log(` ${warning}`));
}
console.log('');
console.log('💡 Next steps:');
if (this.errors.length > 0) {
console.log(' 1. Fix configuration errors listed above');
console.log(' 2. Run validation again');
}
if (this.warnings.length > 0) {
console.log(' • Review warnings for best practices');
}
console.log(' • Run "npm run config:print" to see resolved configuration');
console.log(' • Run "npm run build" to build the package');
return this.errors.length === 0;
}
validate() {
console.log(`🔧 Validating ${PACKAGE_NAME} configuration`);
if (this.strict) {
console.log('🔒 Strict mode enabled');
}
console.log('');
const fileValid = this.validateConfigurationFile();
const envValid = this.validateEnvironmentVariables();
const depsValid = this.validateDependencies();
const backwardValid = this.validateBackwardCompatibility();
return this.generateReport() && fileValid && envValid && depsValid && backwardValid;
}
}
function main() {
const validator = new RetryPolicyConfigurationValidator();
const isValid = validator.validate();
process.exit(isValid ? 0 : 1);
}
if (require.main === module) {
main();
}
module.exports = { RetryPolicyConfigurationValidator };