Files
headlamp-sealed-secrets-plugin/src/lib/retry.ts
T
DevContainer User 9d9bc5f22f fix: remove any types, dead code, unused exports; add comprehensive tests
- Fix handleRotate bug ignoring Result from rotateSealedSecret()
- Fix dead code branch in useControllerHealth
- Replace all `any` types with `unknown` + type guards
- Delete unused functions/exports (452 lines removed)
- Add 18 new test files covering all hooks, libs, and components
- 233 tests passing, zero tsc errors, zero lint issues

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 17:13:00 +00:00

136 lines
4.1 KiB
TypeScript

/**
* Retry logic with exponential backoff
*
* Provides utilities for retrying failed operations with configurable
* backoff strategies and error handling.
*/
import { AsyncResult, Err } from '../types';
/**
* Retry configuration options
*/
export interface RetryOptions {
/** Maximum number of retry attempts (default: 3) */
maxAttempts?: number;
/** Initial delay in milliseconds (default: 1000) */
initialDelayMs?: number;
/** Maximum delay in milliseconds (default: 10000) */
maxDelayMs?: number;
/** Backoff multiplier (default: 2 for exponential) */
backoffMultiplier?: number;
/** Whether to add jitter to delays (default: true) */
useJitter?: boolean;
/** Predicate to determine if error is retryable (default: all errors retryable) */
isRetryable?: (error: Error) => boolean;
}
/**
* Default retry options
*/
const DEFAULT_RETRY_OPTIONS: Required<RetryOptions> = {
maxAttempts: 3,
initialDelayMs: 1000,
maxDelayMs: 10000,
backoffMultiplier: 2,
useJitter: true,
isRetryable: () => true, // All errors retryable by default
};
/**
* Sleep for specified milliseconds
*/
function sleep(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
/**
* Calculate delay with exponential backoff and optional jitter
*
* @param attempt Current attempt number (0-indexed)
* @param options Retry options
* @returns Delay in milliseconds
*/
function calculateDelay(attempt: number, options: Required<RetryOptions>): number {
const { initialDelayMs, maxDelayMs, backoffMultiplier, useJitter } = options;
// Exponential backoff: initialDelay * (multiplier ^ attempt)
let delay = initialDelayMs * Math.pow(backoffMultiplier, attempt);
// Cap at max delay
delay = Math.min(delay, maxDelayMs);
// Add jitter (±25% random variation)
if (useJitter) {
const jitterRange = delay * 0.25;
const jitter = Math.random() * jitterRange * 2 - jitterRange;
delay = Math.max(0, delay + jitter);
}
return Math.floor(delay);
}
/**
* Retry an async operation with exponential backoff
*
* @param operation Async operation to retry (should return AsyncResult)
* @param options Retry configuration
* @returns Result of the operation or final error after all retries
*
* @example
* const result = await retryWithBackoff(
* async () => fetchPublicCertificate(config),
* { maxAttempts: 3, initialDelayMs: 1000 }
* );
*/
export async function retryWithBackoff<T, E>(
operation: () => AsyncResult<T, E>,
options: RetryOptions = {}
): AsyncResult<T, string> {
const opts = { ...DEFAULT_RETRY_OPTIONS, ...options };
const errors: string[] = [];
for (let attempt = 0; attempt < opts.maxAttempts; attempt++) {
try {
const result = await operation();
// If operation succeeded, return immediately
if (result.ok) {
return result;
}
// Operation returned an error - use explicit check for type narrowing
if (result.ok === false) {
const errorMessage = typeof result.error === 'string' ? result.error : String(result.error);
errors.push(`Attempt ${attempt + 1}: ${errorMessage}`);
}
// Check if we should retry
const isLastAttempt = attempt === opts.maxAttempts - 1;
if (isLastAttempt) {
// No more retries, return final error
return Err(`Operation failed after ${opts.maxAttempts} attempts:\n${errors.join('\n')}`);
}
// Wait before retrying
const delay = calculateDelay(attempt, opts);
await sleep(delay);
} catch (error) {
// Unexpected exception (shouldn't happen with AsyncResult, but handle it)
const errorMessage = error instanceof Error ? error.message : String(error);
errors.push(`Attempt ${attempt + 1}: ${errorMessage}`);
const isLastAttempt = attempt === opts.maxAttempts - 1;
if (isLastAttempt) {
return Err(`Operation failed after ${opts.maxAttempts} attempts:\n${errors.join('\n')}`);
}
const delay = calculateDelay(attempt, opts);
await sleep(delay);
}
}
// Should never reach here, but TypeScript needs it
return Err(`Operation failed after ${opts.maxAttempts} attempts:\n${errors.join('\n')}`);
}