Files
llink/js/mobile/src/lib/query-client.ts
T
Arjun PatelandGitHub a8a0b7db1b infra: add linting and formatting for js projects (#230)
* wip

* wip

* wip

* format

* wip(mobile): lint and format

* cleanup

* nits

* nits

* idiomatic react

* nit

* format all root files
2026-06-02 07:44:24 -07:00

66 lines
2.1 KiB
TypeScript

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));
},
}),
});
}