dc5ecd60f6
Flatten the pnpm monorepo (apps/web + packages/*) into a single web package at the repo root. - Move app/lib/components/scripts/public to root; drop apps/web and packages/* wrappers - Rewrite tsconfig paths (@infiplot/*) to ./lib/*; turbopack.root = __dirname - Update Vercel (no root-directory) and Cloudflare (pnpm build:cf at root) deploy paths - Regenerate pnpm-lock.yaml to drop stale workspace importers - Bump engines.node to >=22 to match wrangler Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
40 lines
1.1 KiB
TypeScript
40 lines
1.1 KiB
TypeScript
type RetryInit = RequestInit & { retries?: number; retryDelayMs?: number };
|
|
|
|
export async function fetchWithRetry(
|
|
url: string,
|
|
init: RetryInit,
|
|
): Promise<Response> {
|
|
const { retries = 2, retryDelayMs = 1500, ...fetchInit } = init;
|
|
|
|
let lastError: unknown;
|
|
for (let attempt = 0; attempt <= retries; attempt++) {
|
|
try {
|
|
const res = await fetch(url, fetchInit);
|
|
if (res.ok) return res;
|
|
// Don't retry 4xx (client errors won't fix themselves)
|
|
if (res.status >= 400 && res.status < 500) return res;
|
|
// 5xx: retry if we have budget left
|
|
if (attempt < retries) {
|
|
await sleep(retryDelayMs * (attempt + 1));
|
|
continue;
|
|
}
|
|
return res;
|
|
} catch (err) {
|
|
lastError = err;
|
|
const isAbort =
|
|
err instanceof DOMException && err.name === "AbortError";
|
|
if (isAbort) throw err;
|
|
if (attempt < retries) {
|
|
await sleep(retryDelayMs * (attempt + 1));
|
|
continue;
|
|
}
|
|
throw err;
|
|
}
|
|
}
|
|
throw lastError;
|
|
}
|
|
|
|
function sleep(ms: number): Promise<void> {
|
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
}
|