import { asFiniteNumber } from "../shared/number-coercion.js" ;
import { sleep } from "../utils.js" ;
import { generateSecureFraction } from "./secure-random.js" ;
export type RetryConfig = {
attempts?: number;
minDelayMs?: number;
maxDelayMs?: number;
jitter?: number;
};
export type RetryInfo = {
attempt: number;
maxAttempts: number;
delayMs: number;
err: unknown;
label?: string;
};
export type RetryOptions = RetryConfig & {
label?: string;
shouldRetry?: (err: unknown, attempt: number) => boolean ;
retryAfterMs?: (err: unknown) => number | undefined;
onRetry?: (info: RetryInfo) => void ;
};
const DEFAULT_RETRY_CONFIG = {
attempts: 3 ,
minDelayMs: 300 ,
maxDelayMs: 30 _000 ,
jitter: 0 ,
};
const clampNumber = (value: unknown, fallback: number, min?: number, max?: number) => {
const next = asFiniteNumber(value);
if (next === undefined) {
return fallback;
}
const floor = typeof min === "number" ? min : Number.NEGATIVE_INFINITY;
const ceiling = typeof max === "number" ? max : Number.POSITIVE_INFINITY;
return Math.min(Math.max(next, floor), ceiling);
};
export function resolveRetryConfig(
defaults: Required<RetryConfig> = DEFAULT_RETRY_CONFIG,
overrides?: RetryConfig,
): Required<RetryConfig> {
const attempts = Math.max(1 , Math.round(clampNumber(overrides?.attempts, defaults.attempts, 1 )));
const minDelayMs = Math.max(
0 ,
Math.round(clampNumber(overrides?.minDelayMs, defaults.minDelayMs, 0 )),
);
const maxDelayMs = Math.max(
minDelayMs,
Math.round(clampNumber(overrides?.maxDelayMs, defaults.maxDelayMs, 0 )),
);
const jitter = clampNumber(overrides?.jitter, defaults.jitter, 0 , 1 );
return { attempts, minDelayMs, maxDelayMs, jitter };
}
function applyJitter(delayMs: number, jitter: number): number {
if (jitter <= 0 ) {
return delayMs;
}
const offset = (generateSecureFraction() * 2 - 1 ) * jitter;
return Math.max(0 , Math.round(delayMs * (1 + offset)));
}
export async function retryAsync<T>(
fn: () => Promise<T>,
attemptsOrOptions: number | RetryOptions = 3 ,
initialDelayMs = 300 ,
): Promise<T> {
if (typeof attemptsOrOptions === "number" ) {
const attempts = Math.max(1 , Math.round(attemptsOrOptions));
let lastErr: unknown;
for (let i = 0 ; i < attempts; i += 1 ) {
try {
return await fn();
} catch (err) {
lastErr = err;
if (i === attempts - 1 ) {
break ;
}
const delay = initialDelayMs * 2 ** i;
await sleep(delay);
}
}
throw lastErr ?? new Error("Retry failed" );
}
const options = attemptsOrOptions;
const resolved = resolveRetryConfig(DEFAULT_RETRY_CONFIG, options);
const maxAttempts = resolved.attempts;
const minDelayMs = resolved.minDelayMs;
const maxDelayMs =
Number.isFinite(resolved.maxDelayMs) && resolved.maxDelayMs > 0
? resolved.maxDelayMs
: Number.POSITIVE_INFINITY;
const jitter = resolved.jitter;
const shouldRetry = options.shouldRetry ?? (() => true );
let lastErr: unknown;
for (let attempt = 1 ; attempt <= maxAttempts; attempt += 1 ) {
try {
return await fn();
} catch (err) {
lastErr = err;
if (attempt >= maxAttempts || !shouldRetry(err, attempt)) {
break ;
}
const retryAfterMs = options.retryAfterMs?.(err);
const hasRetryAfter = typeof retryAfterMs === "number" && Number.isFinite(retryAfterMs);
const baseDelay = hasRetryAfter
? Math.max(retryAfterMs, minDelayMs)
: minDelayMs * 2 ** (attempt - 1 );
let delay = Math.min(baseDelay, maxDelayMs);
delay = applyJitter(delay, jitter);
delay = Math.min(Math.max(delay, minDelayMs), maxDelayMs);
options.onRetry?.({
attempt,
maxAttempts,
delayMs: delay,
err,
label: options.label,
});
if (delay > 0 ) {
await sleep(delay);
}
}
}
throw lastErr ?? new Error("Retry failed" );
}
Messung V0.5 in Prozent C=99 H=96 G=97
¤ Dauer der Verarbeitung: 0.9 Sekunden
(vorverarbeitet am 2026-06-10)
¤
*© Formatika GbR, Deutschland