chore: move source to repo root and standardize config
Phase 1 — Structural overhaul: - Move all source from headlamp-sealed-secrets/ subdirectory to repo root - Delete 23 AI-generated docs, 8 pre-built tarballs, release snapshots dir - Remove all working-directory refs from CI/release workflows - Update install-plugin.sh and typedoc.json paths Phase 2 — Config standardization: - Create .eslintrc.js and .prettierrc.js (standard Headlamp configs) - Remove inline eslintConfig/prettier from package.json (drop jsx-a11y, prettier extends) - Rewrite tsconfig.json (package name extend, add compilerOptions.types) - Create vitest.config.mts and vitest.setup.ts (standard from polaris) - Replace headlamp-plugin CLI scripts with direct tool invocation - Rewrite .gitignore with standard baseline Phase 3 — MCP & Claude settings: - Create .mcp.json with github/kubernetes/flux/playwright servers - Create .claude/settings.local.json - Remove 7 specialized agents, keep 3 meta-orchestration agents Phase 4 — Documentation: - Rewrite CLAUDE.md (remove subdirectory refs, standard format) - Add ArtifactHub badge, Architecture section, standardized install methods to README.md - Create CONTRIBUTING.md and SECURITY.md - Fix pre-existing test bugs in validators.test.ts (isValidNamespace returns boolean, not ValidationResult; error message string mismatches) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,188 @@
|
||||
/**
|
||||
* 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')}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Predicate to check if error is a network error (retryable)
|
||||
*
|
||||
* @param error Error to check
|
||||
* @returns true if error is network-related
|
||||
*/
|
||||
export function isNetworkError(error: Error): boolean {
|
||||
const message = error.message.toLowerCase();
|
||||
return (
|
||||
message.includes('network') ||
|
||||
message.includes('timeout') ||
|
||||
message.includes('fetch') ||
|
||||
message.includes('connection') ||
|
||||
message.includes('econnrefused') ||
|
||||
message.includes('enotfound')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Predicate to check if HTTP error is retryable (5xx, 429, 408)
|
||||
*
|
||||
* @param error Error to check
|
||||
* @returns true if HTTP status is retryable
|
||||
*/
|
||||
export function isRetryableHttpError(error: Error): boolean {
|
||||
const message = error.message;
|
||||
|
||||
// Check for 5xx server errors
|
||||
if (/5\d{2}/.test(message)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check for specific retryable status codes
|
||||
return message.includes('429') || // Too Many Requests
|
||||
message.includes('408') || // Request Timeout
|
||||
message.includes('503') || // Service Unavailable
|
||||
message.includes('504'); // Gateway Timeout
|
||||
}
|
||||
|
||||
/**
|
||||
* Combined predicate for network and HTTP errors
|
||||
*
|
||||
* @param error Error to check
|
||||
* @returns true if error is retryable
|
||||
*/
|
||||
export function isRetryableError(error: Error): boolean {
|
||||
return isNetworkError(error) || isRetryableHttpError(error);
|
||||
}
|
||||
Reference in New Issue
Block a user