|
| 1 | +/** |
| 2 | + * Centralized React Query configuration for NiffyInsur. |
| 3 | + * |
| 4 | + * Rationale |
| 5 | + * --------- |
| 6 | + * Default React Query settings (staleTime: 0, retry: 3 with fixed backoff) |
| 7 | + * are poorly suited to a blockchain-backed app where: |
| 8 | + * - On-chain data changes infrequently (new ledger every ~5 s, indexer lags ~15 s). |
| 9 | + * - RPC/indexer errors are transient — aggressive retries waste bandwidth. |
| 10 | + * - 4xx errors (bad request, unauthorized) are never transient and must not retry. |
| 11 | + * - Background refetch on a hidden tab drains mobile battery. |
| 12 | + * |
| 13 | + * Stale times (per query type) |
| 14 | + * ---------------------------- |
| 15 | + * policies 30 s — policy state changes only on user action (renew/terminate) |
| 16 | + * claims 10 s — claims can be filed by any holder; moderate freshness needed |
| 17 | + * votes 5 s — active voting windows are time-sensitive |
| 18 | + * ledger 5 s — latest ledger advances every ~5 s |
| 19 | + * default 15 s — catch-all for uncategorized queries |
| 20 | + * |
| 21 | + * Retry policy |
| 22 | + * ------------ |
| 23 | + * - Max 3 attempts for transient errors (network, 5xx, 429). |
| 24 | + * - No retry for 4xx client errors (except 429 Retry-After). |
| 25 | + * - Exponential backoff: 1 s → 2 s → 4 s (capped at 30 s). |
| 26 | + * |
| 27 | + * Background refetch |
| 28 | + * ------------------ |
| 29 | + * - refetchOnWindowFocus: false globally; enabled per-query only for votes. |
| 30 | + * - refetchOnReconnect: true — always resync after coming back online. |
| 31 | + * - refetchIntervalInBackground: false — respects Page Visibility API. |
| 32 | + */ |
| 33 | + |
| 34 | +import { QueryClient } from '@tanstack/react-query'; |
| 35 | + |
| 36 | +// --------------------------------------------------------------------------- |
| 37 | +// Stale time constants — import these in useQuery calls for consistency. |
| 38 | +// --------------------------------------------------------------------------- |
| 39 | + |
| 40 | +export const STALE_TIMES = { |
| 41 | + /** Policy list / detail — changes only on user-initiated transactions. */ |
| 42 | + policies: 30_000, |
| 43 | + /** Claims list — any holder can file; moderate freshness. */ |
| 44 | + claims: 10_000, |
| 45 | + /** Vote tallies — time-sensitive during open voting windows. */ |
| 46 | + votes: 5_000, |
| 47 | + /** Latest ledger sequence — advances every ~5 s. */ |
| 48 | + ledger: 5_000, |
| 49 | + /** Default catch-all. */ |
| 50 | + default: 15_000, |
| 51 | +} as const; |
| 52 | + |
| 53 | +// --------------------------------------------------------------------------- |
| 54 | +// Retry predicate — never retry 4xx (except 429). |
| 55 | +// --------------------------------------------------------------------------- |
| 56 | + |
| 57 | +interface MaybeHttpError { |
| 58 | + status?: number; |
| 59 | +} |
| 60 | + |
| 61 | +function isNonRetryable(error: unknown): boolean { |
| 62 | + const status = (error as MaybeHttpError)?.status; |
| 63 | + if (typeof status !== 'number') return false; |
| 64 | + // 4xx except 429 (rate limit) are client errors — retrying won't help. |
| 65 | + return status >= 400 && status < 500 && status !== 429; |
| 66 | +} |
| 67 | + |
| 68 | +function retryDelay(attempt: number): number { |
| 69 | + // Exponential backoff: 1s, 2s, 4s — capped at 30s. |
| 70 | + return Math.min(1_000 * Math.pow(2, attempt), 30_000); |
| 71 | +} |
| 72 | + |
| 73 | +// --------------------------------------------------------------------------- |
| 74 | +// QueryClient factory — call once at app root. |
| 75 | +// --------------------------------------------------------------------------- |
| 76 | + |
| 77 | +export function createQueryClient(): QueryClient { |
| 78 | + return new QueryClient({ |
| 79 | + defaultOptions: { |
| 80 | + queries: { |
| 81 | + staleTime: STALE_TIMES.default, |
| 82 | + // 3 retries for transient errors; skip for 4xx. |
| 83 | + retry: (failureCount: number, error: unknown) => { |
| 84 | + if (isNonRetryable(error)) return false; |
| 85 | + return failureCount < 3; |
| 86 | + }, |
| 87 | + retryDelay: (attempt: number) => retryDelay(attempt), |
| 88 | + // Disable window-focus refetch globally; enable per-query for votes. |
| 89 | + refetchOnWindowFocus: false, |
| 90 | + // Always resync after reconnect. |
| 91 | + refetchOnReconnect: true, |
| 92 | + // Never refetch in a background tab — respects Page Visibility API. |
| 93 | + refetchIntervalInBackground: false, |
| 94 | + }, |
| 95 | + mutations: { |
| 96 | + retry: (failureCount: number, error: unknown) => { |
| 97 | + if (isNonRetryable(error)) return false; |
| 98 | + return failureCount < 2; |
| 99 | + }, |
| 100 | + retryDelay: (attempt: number) => retryDelay(attempt), |
| 101 | + }, |
| 102 | + }, |
| 103 | + }); |
| 104 | +} |
0 commit comments