* Fetch with retry and exponential backoff. * Keeps retrying on network errors and non-OK responses so the app * degrades gracefully when the server is temporarily unreachable.
(
url: string,
options: RequestInit = {},
retryConfig: { retries?: number; delay?: number; backoff?: number } = {},
)
| 78 | * degrades gracefully when the server is temporarily unreachable. |
| 79 | */ |
| 80 | async function fetchWithRetry( |
| 81 | url: string, |
| 82 | options: RequestInit = {}, |
| 83 | retryConfig: { retries?: number; delay?: number; backoff?: number } = {}, |
| 84 | ): Promise<Response> { |
| 85 | const { retries = 6, delay = 1000, backoff = 2 } = retryConfig |
| 86 | |
| 87 | for (let i = 0; i <= retries; i++) { |
| 88 | try { |
| 89 | const response = await fetch(url, options) |
| 90 | if (response.ok) return response |
| 91 | |
| 92 | console.warn( |
| 93 | `Fetch attempt ${i + 1} failed with status: ${response.status}. Retrying...`, |
| 94 | ) |
| 95 | } catch (error) { |
| 96 | console.error( |
| 97 | `Fetch attempt ${i + 1} failed due to a network error:`, |
| 98 | error, |
| 99 | ) |
| 100 | if (i >= retries) throw error |
| 101 | } |
| 102 | |
| 103 | if (i < retries) { |
| 104 | const currentDelay = delay * Math.pow(backoff, i) |
| 105 | await new Promise((resolve) => setTimeout(resolve, currentDelay)) |
| 106 | } |
| 107 | } |
| 108 | |
| 109 | throw new Error(`Failed to fetch ${url} after ${retries} retries.`) |
| 110 | } |
| 111 | |
| 112 | // Schema — use ISO strings for dates (SQLite-friendly) |
| 113 | const todoSchema = z.object({ |