UNPKG

@bernierllc/retry-policy

Version:

Atomic retry policy utilities with exponential backoff and jitter

303 lines (248 loc) 10.2 kB
#!/usr/bin/env node /* 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 };