import { MutationCache, QueryClient } from "@tanstack/react-query"; import type { InvalidatesMeta, UpdatesMeta } from "@/utils/endpoint"; /** * Single app-wide React Query client (do not nest additional providers). * * Declarative invalidation: any mutation built via `api.*.mutationOptions()` * (see `services/api.ts` + `utils/endpoint.ts`) carries an `invalidates` * function in its `meta`. The shared `MutationCache` below runs it on success * and invalidates the returned query keys — so invalidation is declared once in * the endpoint definition rather than re-wired in every component. */ export const queryClient = new QueryClient({ mutationCache: new MutationCache({ onSuccess: (data, variables, _context, mutation) => { // Seed first: endpoints that return the entity they just changed write it // straight into its cache key, so the screen updates from the response // instead of round-tripping for data it already holds. const updates = mutation.meta?.updates as UpdatesMeta | undefined; const seeded: readonly unknown[][] = []; if (typeof updates === "function") { for (const [queryKey, value] of updates(variables, data)) { queryClient.setQueryData(queryKey, value); seeded.push(queryKey as unknown[]); } } const invalidates = mutation.meta?.invalidates as | InvalidatesMeta | undefined; if (typeof invalidates !== "function") return; for (const queryKey of invalidates(variables, data)) { void queryClient.invalidateQueries({ queryKey, // A seeded key already holds the authoritative value from this very // response — invalidating it would refetch it right back. predicate: seeded.length ? (query) => !seeded.some( (key) => key.length === query.queryKey.length && key.every((part, i) => Object.is(part, query.queryKey[i])), ) : undefined, }); } }, // Mutation failures are surfaced globally by the axios interceptor in // auth/http.ts (server-message toast on every non-401 failure), so no // onError toast here — it would double up. }), defaultOptions: { queries: { retry: 1, // staleTime: 30_000, staleTime:0, // Data freshness is driven by mutation invalidation (MutationCache above), // socket pushes, and explicit polling — not by tab focus. Focus refetch // just re-fires every mounted query each time the window is refocused. refetchOnWindowFocus: false, }, }, });