61 lines
1.6 KiB
TypeScript
61 lines
1.6 KiB
TypeScript
const DEFAULT_MAX_RETRIES = 3;
|
|
const MAX_BACKOFF_MS = 8000;
|
|
|
|
function sleep(ms: number) {
|
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
}
|
|
|
|
function parseRetryAfterMs(value: string | null): number | null {
|
|
if (!value) return null;
|
|
const seconds = Number(value);
|
|
if (Number.isFinite(seconds) && seconds > 0) {
|
|
return Math.round(seconds * 1000);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function computeBackoffMs(attempt: number, retryAfterMs: number | null) {
|
|
if (retryAfterMs && retryAfterMs > 0) {
|
|
return Math.min(retryAfterMs, MAX_BACKOFF_MS);
|
|
}
|
|
const exp = Math.min(MAX_BACKOFF_MS, 500 * 2 ** attempt);
|
|
const jitter = Math.floor(Math.random() * 300);
|
|
return exp + jitter;
|
|
}
|
|
|
|
export async function fetchUploadWithRetry(
|
|
formData: FormData,
|
|
options?: {
|
|
maxRetries?: number;
|
|
signal?: AbortSignal;
|
|
}
|
|
) {
|
|
const maxRetries = options?.maxRetries ?? DEFAULT_MAX_RETRIES;
|
|
|
|
for (let attempt = 0; ; attempt++) {
|
|
const response = await fetch("/api/files/upload", {
|
|
method: "POST",
|
|
body: formData,
|
|
signal: options?.signal,
|
|
});
|
|
|
|
if (response.status !== 429 || attempt >= maxRetries) {
|
|
return response;
|
|
}
|
|
|
|
let retryAfterMs = parseRetryAfterMs(response.headers.get("Retry-After"));
|
|
if (!retryAfterMs) {
|
|
try {
|
|
const payload = await response.clone().json();
|
|
const retryAfterSec = Number(payload?.retryAfter || 0);
|
|
if (Number.isFinite(retryAfterSec) && retryAfterSec > 0) {
|
|
retryAfterMs = Math.round(retryAfterSec * 1000);
|
|
}
|
|
} catch {}
|
|
}
|
|
|
|
const delayMs = computeBackoffMs(attempt, retryAfterMs);
|
|
await sleep(delayMs);
|
|
}
|
|
}
|