import { MutationCache, QueryCache, QueryClient } from '@tanstack/react-query'; import { toast } from 'sonner-native'; import { ApiError, logError, reportError, toUserMessage } from '@/lib/errors'; import { useAuthStore } from '@/stores/auth-store'; declare module '@tanstack/react-query' { interface Register { queryMeta: { toastOnError?: boolean }; mutationMeta: { suppressToast?: boolean }; } } function shouldRetryQuery(failureCount: number, err: unknown): boolean { if (err instanceof ApiError) { // Retry only on transient status codes; 4xx generally won't succeed on retry. if (err.status === 408 || err.status === 429) return failureCount < 2; if (err.status >= 400 && err.status < 500) return false; } return failureCount < 2; } // A 401 surfaced through react-query means the server rejected our bearer // token. This is the *only* place that turns that into an auth state change — // the apiClient is a dumb transport. Direct apiClient callers (signIn, // restoreSession, signInToFirebase) handle their own 401s explicitly. function handleUnauthorized(err: unknown): void { if (err instanceof ApiError && err.status === 401) { void useAuthStore.getState().invalidateSession(); } } export function createQueryClient(): QueryClient { return new QueryClient({ defaultOptions: { queries: { retry: shouldRetryQuery, refetchOnWindowFocus: false, }, mutations: { // Mutations have side effects — never auto-retry. retry: 0, }, }, queryCache: new QueryCache({ onError: (err, query) => { handleUnauthorized(err); logError(err, { scope: 'query', queryKey: query.queryKey }); if (query.meta?.toastOnError) { toast.error(toUserMessage(err)); } }, }), mutationCache: new MutationCache({ onError: (err, _variables, _context, mutation) => { handleUnauthorized(err); reportError(err, { scope: 'mutation', mutationKey: mutation.options.mutationKey, }); if (mutation.meta?.suppressToast) return; toast.error(toUserMessage(err)); }, }), }); }