@bernierllc/retry-policy
Version:
Atomic retry policy utilities with exponential backoff and jitter
321 lines (246 loc) • 10.4 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 {
RetryPolicyConfigurationLoader,
loadOptionalRetryPolicyConfig,
loadOptionalRetryPolicyConfigWithSources,
mergeConfigurations,
setGlobalRetryPolicyConfig,
getGlobalRetryPolicyConfig,
clearGlobalRetryPolicyConfig
} from '../src/config/loadOptionalConfig';
describe('Retry Policy Configuration', () => {
beforeEach(() => {
// Clear global config before each test
clearGlobalRetryPolicyConfig();
// Clear environment variables
delete process.env.RETRY_MAX_RETRIES;
delete process.env.RETRY_INITIAL_DELAY;
delete process.env.RETRY_MAX_DELAY;
delete process.env.RETRY_BACKOFF_FACTOR;
delete process.env.RETRY_JITTER;
delete process.env.RETRY_ENABLED;
delete process.env.RETRY_BACKOFF_TYPE;
delete process.env.RETRY_BACKOFF_BASE_DELAY;
delete process.env.RETRY_BACKOFF_MAX_DELAY;
delete process.env.RETRY_BACKOFF_MULTIPLIER;
delete process.env.RETRY_JITTER_TYPE;
delete process.env.RETRY_JITTER_FACTOR;
});
describe('Global Configuration', () => {
it('should set and get global configuration', () => {
const config = { maxRetries: 10, initialDelayMs: 2000 };
setGlobalRetryPolicyConfig(config);
const retrieved = getGlobalRetryPolicyConfig();
expect(retrieved).toEqual(config);
});
it('should merge global configurations', () => {
setGlobalRetryPolicyConfig({ maxRetries: 10 });
setGlobalRetryPolicyConfig({ initialDelayMs: 2000 });
const retrieved = getGlobalRetryPolicyConfig();
expect(retrieved).toEqual({ maxRetries: 10, initialDelayMs: 2000 });
});
it('should clear global configuration', () => {
setGlobalRetryPolicyConfig({ maxRetries: 10 });
clearGlobalRetryPolicyConfig();
const retrieved = getGlobalRetryPolicyConfig();
expect(retrieved).toEqual({});
});
});
describe('Environment Variable Parsing', () => {
it('should load boolean environment variables', () => {
process.env.RETRY_ENABLED = 'true';
process.env.RETRY_JITTER = 'false';
const config = loadOptionalRetryPolicyConfig();
expect(config.enabled).toBe(true);
expect(config.jitter).toBe(false);
});
it('should load integer environment variables', () => {
process.env.RETRY_MAX_RETRIES = '10';
process.env.RETRY_INITIAL_DELAY = '2000';
const config = loadOptionalRetryPolicyConfig();
expect(config.maxRetries).toBe(10);
expect(config.initialDelayMs).toBe(2000);
});
it('should load float environment variables', () => {
process.env.RETRY_BACKOFF_FACTOR = '1.5';
process.env.RETRY_JITTER_FACTOR = '0.25';
const config = loadOptionalRetryPolicyConfig();
expect(config.backoffFactor).toBe(1.5);
expect(config.backoff?.jitter?.factor).toBe(0.25);
});
it('should load string environment variables', () => {
process.env.RETRY_BACKOFF_TYPE = 'linear';
process.env.RETRY_JITTER_TYPE = 'full';
const config = loadOptionalRetryPolicyConfig();
expect(config.backoff?.type).toBe('linear');
expect(config.backoff?.jitter?.type).toBe('full');
});
it('should load nested configuration from environment variables', () => {
process.env.RETRY_BACKOFF_BASE_DELAY = '100';
process.env.RETRY_BACKOFF_MAX_DELAY = '5000';
process.env.RETRY_BACKOFF_MULTIPLIER = '2';
const config = loadOptionalRetryPolicyConfig();
expect(config.backoff?.baseDelay).toBe(100);
expect(config.backoff?.maxDelay).toBe(5000);
expect(config.backoff?.factor).toBe(2);
});
});
describe('Configuration Merging', () => {
it('should merge multiple configurations', () => {
const config1 = { maxRetries: 5 };
const config2 = { initialDelayMs: 1000 };
const config3 = { maxDelayMs: 30000 };
const merged = mergeConfigurations(config1, config2, config3);
expect(merged).toEqual({
maxRetries: 5,
initialDelayMs: 1000,
maxDelayMs: 30000
});
});
it('should merge backoff configurations correctly', () => {
const config1 = { backoff: { type: 'exponential' as const, baseDelay: 100 } };
const config2 = { backoff: { maxDelay: 5000 } };
const merged = mergeConfigurations(config1, config2);
expect(merged.backoff).toEqual({
type: 'exponential',
baseDelay: 100,
maxDelay: 5000
});
});
it('should override with later configurations', () => {
const config1 = { maxRetries: 5 };
const config2 = { maxRetries: 10 };
const merged = mergeConfigurations(config1, config2);
expect(merged.maxRetries).toBe(10);
});
it('should handle undefined configurations', () => {
const config1 = { maxRetries: 5 };
const merged = mergeConfigurations(config1, undefined as any, { initialDelayMs: 1000 });
expect(merged).toEqual({
maxRetries: 5,
initialDelayMs: 1000
});
});
});
describe('Configuration Loader', () => {
it('should create loader instance', () => {
const loader = new RetryPolicyConfigurationLoader();
expect(loader).toBeInstanceOf(RetryPolicyConfigurationLoader);
});
it('should load configuration with sources', () => {
process.env.RETRY_MAX_RETRIES = '10';
const loader = new RetryPolicyConfigurationLoader();
const config = loader.loadOptionalConfiguration();
const sources = loader.getConfigurationSources();
expect(config.maxRetries).toBe(10);
expect(sources.length).toBeGreaterThan(0);
});
it('should track environment variable sources', () => {
process.env.RETRY_MAX_RETRIES = '10';
process.env.RETRY_INITIAL_DELAY = '2000';
const loader = new RetryPolicyConfigurationLoader();
loader.loadOptionalConfiguration();
const sources = loader.getConfigurationSources();
const envSources = sources.filter(s => s.source === 'environment');
expect(envSources.length).toBe(2);
});
it('should track global configuration source', () => {
setGlobalRetryPolicyConfig({ maxRetries: 10 });
const loader = new RetryPolicyConfigurationLoader();
loader.loadOptionalConfiguration();
const sources = loader.getConfigurationSources();
const globalSource = sources.find(s => s.source === 'global');
expect(globalSource).toBeDefined();
expect(globalSource?.description).toContain('service packages');
});
it('should provide environment variable documentation', () => {
const loader = new RetryPolicyConfigurationLoader();
const docs = loader.getEnvironmentVariableDocumentation();
expect(docs.RETRY_MAX_RETRIES).toContain('maxRetries');
expect(docs.RETRY_INITIAL_DELAY).toContain('initialDelayMs');
expect(docs.RETRY_BACKOFF_TYPE).toContain('backoff.type');
});
});
describe('loadOptionalRetryPolicyConfig', () => {
it('should return empty config when no configuration provided', () => {
const config = loadOptionalRetryPolicyConfig();
expect(config).toEqual({});
});
it('should load environment configuration', () => {
process.env.RETRY_MAX_RETRIES = '10';
process.env.RETRY_INITIAL_DELAY = '2000';
const config = loadOptionalRetryPolicyConfig();
expect(config.maxRetries).toBe(10);
expect(config.initialDelayMs).toBe(2000);
});
it('should merge global and environment configuration', () => {
setGlobalRetryPolicyConfig({ maxRetries: 5 });
process.env.RETRY_INITIAL_DELAY = '2000';
const config = loadOptionalRetryPolicyConfig();
expect(config.maxRetries).toBe(5);
expect(config.initialDelayMs).toBe(2000);
});
it('should handle errors gracefully', () => {
// Force an error by making setNestedValue fail
const originalProcessEnv = process.env;
Object.defineProperty(process, 'env', {
get: () => { throw new Error('Test error'); },
configurable: true
});
const config = loadOptionalRetryPolicyConfig();
// Should return empty config on error
expect(config).toEqual({});
// Restore process.env
Object.defineProperty(process, 'env', {
value: originalProcessEnv,
configurable: true
});
});
});
describe('loadOptionalRetryPolicyConfigWithSources', () => {
it('should return config and sources', () => {
process.env.RETRY_MAX_RETRIES = '10';
const result = loadOptionalRetryPolicyConfigWithSources();
expect(result.config.maxRetries).toBe(10);
expect(result.sources.length).toBeGreaterThan(0);
});
it('should handle errors gracefully', () => {
// Force an error
const originalProcessEnv = process.env;
Object.defineProperty(process, 'env', {
get: () => { throw new Error('Test error'); },
configurable: true
});
const result = loadOptionalRetryPolicyConfigWithSources();
expect(result.config).toEqual({});
expect(result.sources).toEqual([]);
// Restore process.env
Object.defineProperty(process, 'env', {
value: originalProcessEnv,
configurable: true
});
});
});
describe('Configuration Precedence', () => {
it('should follow precedence: global < environment', () => {
setGlobalRetryPolicyConfig({ maxRetries: 5 });
process.env.RETRY_MAX_RETRIES = '10';
const config = loadOptionalRetryPolicyConfig();
// Environment should override global
expect(config.maxRetries).toBe(10);
});
it('should allow constructor options to override all', () => {
setGlobalRetryPolicyConfig({ maxRetries: 5 });
process.env.RETRY_MAX_RETRIES = '10';
const runtimeConfig = loadOptionalRetryPolicyConfig();
const constructorConfig = { maxRetries: 15 };
const final = mergeConfigurations(runtimeConfig, constructorConfig);
expect(final.maxRetries).toBe(15);
});
});
});