You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

77 lines
2.7 KiB

import { parseRetryAfter } from '$libs/http';
import { HTTP_TIMEOUT_SCOPE_TOTAL } from './consts.ts';
import { HttpAbortError, HttpTimeoutError } from './errors.ts';
import type { HttpMethod, RetryConfig } from './types.ts';
export { parseRetryAfter } from '$libs/http';
/**
* Decide whether a failed attempt should be retried. Hierarchy:
* 1. User signal aborted → never retry.
* 2. Total-timeout fired → never retry.
* 3. Custom `shouldRetry` predicate → its return value wins.
* 4. Attempts exhausted → no retry.
* 5. Method must be in `methods` allow-list.
* 6. Either: error is a network-class failure (no response) OR response
* status is in `statusCodes`.
*/
export function shouldRetryRequest(
method: HttpMethod,
policy: RetryConfig,
ctx: { error: unknown; response?: Response; attempt: number }
): boolean {
// Hard stops independent of policy.
if (ctx.error instanceof HttpAbortError) return false;
if (ctx.error instanceof HttpTimeoutError && ctx.error.scope === HTTP_TIMEOUT_SCOPE_TOTAL)
return false;
if (policy.shouldRetry) return policy.shouldRetry(ctx);
if (ctx.attempt > policy.limit) return false;
if (!policy.methods.includes(method)) return false;
if (ctx.response !== undefined) {
return policy.statusCodes.includes(ctx.response.status);
}
// No response → network or per-attempt timeout. Both are retry-able.
return true;
}
/**
* Compute the delay (ms) before attempt `n+1`. `Retry-After` from the failed
* response trumps the policy; otherwise the policy `delay(attempt)` is used,
* jittered if requested, then capped at `backoffLimit`.
*/
export function computeRetryDelay(
policy: RetryConfig,
attempt: number,
response: Response | undefined,
now: number = Date.now()
): number {
const headerDelay = response !== undefined ? parseRetryAfter(response.headers, now) : undefined;
const baseDelay = headerDelay !== undefined ? headerDelay : policy.delay(attempt);
const jittered = policy.jitter ? baseDelay * Math.random() : baseDelay;
return Math.min(Math.max(0, jittered), policy.backoffLimit);
}
/**
* Wait `ms`, resolving early if `signal` aborts. Resolves either way — the
* caller checks `signal.aborted` after to decide whether to continue.
*/
export function delayWithSignal(ms: number, signal: AbortSignal): Promise<void> {
if (ms <= 0 || signal.aborted) return Promise.resolve();
return new Promise<void>((resolve) => {
const id = setTimeout(() => {
signal.removeEventListener('abort', onAbort);
resolve();
}, ms);
(id as unknown as { unref?: () => void }).unref?.();
function onAbort(): void {
clearTimeout(id);
resolve();
}
signal.addEventListener('abort', onAbort, { once: true });
});
}

Powered by TurnKey Linux.