nyuchimzizi
Mzizi — an open-architecture project of the Bundu Foundation, operated and developed by Nyuchi. Built on the Five African Minerals palette.
Built by Nyuchi Africav4.0.39
Retry with exponential backoff, jitter, and configurable retry predicates.
View the full component source code below.
/**
* Retry with exponential backoff for the Nyuchi ecosystem.
*
* Wraps async operations with configurable retry logic, exponential delays,
* and jitter to prevent thundering herd problems.
*
* Install via: npx shadcn@latest add https://design.nyuchi.com/api/v1/ui/retry
*/
import { createLogger } from "@/lib/observability"
const logger = createLogger("retry")
export interface RetryConfig {
/** Maximum number of attempts including the first try (default: 3) */
maxAttempts: number
/** Base delay in ms before first retry (default: 1000) */
baseDelayMs: number
/** Maximum delay in ms — caps exponential growth (default: 30000) */
maxDelayMs: number
/** Add random jitter to prevent thundering herd (default: true) */
jitter: boolean
/** Custom predicate — return false to skip retry for certain errors */
retryIf?: (error: unknown, attempt: number) => boolean
/** Called before each retry — useful for logging or metrics */
onRetry?: (error: unknown, attempt: number, delayMs: number) => void
}
const DEFAULT_CONFIG: RetryConfig = {
maxAttempts: 3,
baseDelayMs: 1000,
maxDelayMs: 30_000,
jitter: true,
}
/**
* Error thrown when all retry attempts are exhausted.
*/
export class RetriesExhaustedError extends Error {
readonly attempts: number
readonly lastError: unknown
constructor(attempts: number, lastError: unknown) {
const msg = lastError instanceof Error ? lastError.message : String(lastError)
super(`All ${attempts} retry attempts exhausted. Last error: ${msg}`)
this.name = "RetriesExhaustedError"
this.attempts = attempts
this.lastError = lastError
}
}
function computeDelay(attempt: number, config: RetryConfig): number {
// Exponential: baseDelay * 2^attempt
const exponential = config.baseDelayMs * Math.pow(2, attempt)
const capped = Math.min(exponential, config.maxDelayMs)
if (config.jitter) {
// Add 0–50% random jitter
return capped + Math.random() * capped * 0.5
}
return capped
}
function sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms))
}
/**
* Retry an async operation with exponential backoff.
*
* @example
* ```ts
* import { withRetry } from "@/lib/retry"
*
* // Basic usage — 3 attempts with 1s base delay
* const data = await withRetry(() => fetch("/api/weather").then(r => r.json()))
*
* // Custom configuration
* const result = await withRetry(
* () => fetchFromProvider(),
* {
* maxAttempts: 5,
* baseDelayMs: 500,
* retryIf: (error) => {
* // Don't retry 404s or 400s
* if (error instanceof Response && error.status < 500) return false
* return true
* },
* }
* )
*
* // With git push retry pattern (from CLAUDE.md)
* await withRetry(() => gitPush(), {
* maxAttempts: 4,
* baseDelayMs: 2000, // 2s, 4s, 8s, 16s
* jitter: false,
* })
* ```
*/
export async function withRetry<T>(
fn: () => Promise<T>,
config?: Partial<RetryConfig>
): Promise<T> {
const cfg = { ...DEFAULT_CONFIG, ...config }
let lastError: unknown
for (let attempt = 0; attempt < cfg.maxAttempts; attempt++) {
try {
return await fn()
} catch (error) {
lastError = error
const isLastAttempt = attempt === cfg.maxAttempts - 1
// Check if we should retry this error
if (!isLastAttempt && cfg.retryIf && !cfg.retryIf(error, attempt)) {
logger.info("Retry skipped by predicate", {
data: { attempt: attempt + 1, maxAttempts: cfg.maxAttempts },
})
throw error
}
if (!isLastAttempt) {
const delay = computeDelay(attempt, cfg)
logger.warn(
`Attempt ${attempt + 1}/${cfg.maxAttempts} failed, retrying in ${Math.round(delay)}ms`,
{
data: {
attempt: attempt + 1,
maxAttempts: cfg.maxAttempts,
delayMs: Math.round(delay),
},
}
)
cfg.onRetry?.(error, attempt + 1, delay)
await sleep(delay)
}
}
}
logger.error(`All ${cfg.maxAttempts} attempts exhausted`, {
data: { maxAttempts: cfg.maxAttempts },
error: lastError instanceof Error ? lastError : new Error(String(lastError)),
})
throw new RetriesExhaustedError(cfg.maxAttempts, lastError)
}
npx shadcn@latest add https://mzizi.dev/api/v1/ui/retryFetch this component's metadata and source code from the registry API.
/api/v1/ui/retrylib/retry.ts