mirror of
https://github.com/badlogic/pi-mono.git
synced 2026-11-05 23:37:25 +00:00
68 lines
2.3 KiB
TypeScript
68 lines
2.3 KiB
TypeScript
type FetchInput = Parameters<typeof fetch>[0];
|
|
|
|
const RETRYABLE_STATUS_CODES = new Set([408, 425, 429, 500, 502, 503, 504]);
|
|
|
|
export interface FetchRetryOptions {
|
|
/** Number of additional attempts after the initial request. Defaults to two. */
|
|
maxRetries?: number;
|
|
/** Retry transient HTTP responses as well as transport failures. Defaults to true. */
|
|
retryOnStatus?: boolean;
|
|
/** Per-attempt timeout. A new timeout is created for every attempt. */
|
|
timeoutMs?: number;
|
|
}
|
|
|
|
/**
|
|
* Fetch a management HTTP resource with a bounded immediate retry.
|
|
*
|
|
* This is intentionally a transport-level helper for idempotent management
|
|
* requests (version checks, catalogs, and downloads). It must not be used for
|
|
* agent/model operations: those can fail after the HTTP request starts and are
|
|
* retried by their semantic caller instead.
|
|
*
|
|
* Caller cancellation is terminal. When timeoutMs is supplied, it is the
|
|
* overall time budget shared by all attempts.
|
|
*/
|
|
export async function fetchWithRetry(
|
|
input: FetchInput,
|
|
init: RequestInit | undefined = undefined,
|
|
options: FetchRetryOptions = {},
|
|
): Promise<Response> {
|
|
const maxRetries =
|
|
options.maxRetries === undefined || !Number.isFinite(options.maxRetries)
|
|
? 2
|
|
: Math.max(0, Math.floor(options.maxRetries));
|
|
const retryOnStatus = options.retryOnStatus ?? true;
|
|
const parentSignal = init?.signal;
|
|
const timeoutSignal =
|
|
options.timeoutMs !== undefined && options.timeoutMs > 0 ? AbortSignal.timeout(options.timeoutMs) : undefined;
|
|
const signal = timeoutSignal
|
|
? parentSignal
|
|
? AbortSignal.any([parentSignal, timeoutSignal])
|
|
: timeoutSignal
|
|
: parentSignal;
|
|
|
|
for (let attempt = 0; ; attempt++) {
|
|
signal?.throwIfAborted();
|
|
|
|
try {
|
|
const response = await fetch(input, signal ? { ...init, signal } : init);
|
|
const shouldRetry = retryOnStatus && RETRYABLE_STATUS_CODES.has(response.status) && attempt < maxRetries;
|
|
if (!shouldRetry) return response;
|
|
try {
|
|
await response.body?.cancel();
|
|
} catch {
|
|
// The response is being discarded before a retry. There is nothing useful to
|
|
// do if cancelling its body also fails.
|
|
}
|
|
} catch (error) {
|
|
if (
|
|
parentSignal?.aborted ||
|
|
timeoutSignal?.aborted ||
|
|
(error instanceof Error && error.name === "AbortError" && timeoutSignal === undefined) ||
|
|
attempt >= maxRetries
|
|
) {
|
|
throw error;
|
|
}
|
|
}
|
|
}
|
|
}
|