import { MutationCache, QueryCache, QueryClient } from "@tanstack/react-query"; import i18n from "../i18n"; import { notifyActionError } from "./actionErrors"; import { log } from "./log"; /** A failed query or mutation, for the client log. Only the key's HEAD (the resource, `"videos"`): * the rest of a key is filter state, which the base level does not keep. The status is what * `req` threw; the request line itself is already logged, under its request id. */ function failureAttrs(key: readonly unknown[] | undefined, err: unknown): Record { const status = (err as { status?: unknown } | null)?.status; return { key: typeof key?.[0] === "string" ? key[0] : null, ...(typeof status === "number" ? { status } : {}), message: err instanceof Error ? err.message : String(err), }; } declare module "@tanstack/react-query" { interface Register { mutationMeta: { /** i18n key of the toast title that names what failed ("Couldn't hide the channel"). Without * one the failure is still reported, under the generic "Couldn't complete that". */ failTitle?: string; /** The component renders this mutation's failure on the page itself — no toast. */ errorShownInline?: boolean; }; } } type FailMeta = { failTitle?: string; errorShownInline?: boolean } | undefined; /** * The ONE reporter for a failed mutation. `req` shows its blocking dialog for 400/409/422/5xx only; * a 403/404/429 was left to the caller and a 502–504 or network failure got just the connection * notice — so every `useMutation` without a reporting `onError` failed in silence: a channel hide * that snapped back, a share link that never revoked, a scheduler run that never started (an audit * counted 34 such mutations). Reporting centrally means a new mutation cannot be silent. * * react-query awaits this BEFORE the mutation's own `onError`, so it waits a task: a site that * reported the failure itself (through `notifyActionError`, which marks it) has had its turn, and * `notifyActionError` skips what is already on screen. */ function reportMutationFailure(err: unknown, meta: FailMeta): void { if (meta?.errorShownInline) return; setTimeout(() => notifyActionError(err, i18n.t(meta?.failTitle ?? "errors.title")), 0); } export function createQueryClient(): QueryClient { return new QueryClient({ // Queries report their failure where they render it (QueryState); this only records it. queryCache: new QueryCache({ onError: (err, query) => log.warn("query.failed", "a query failed", failureAttrs(query.queryKey, err)), }), mutationCache: new MutationCache({ onError: (err, _vars, _onMutateResult, mutation) => { log.warn( "mutation.failed", "a mutation failed", failureAttrs(mutation.options.mutationKey, err), ); reportMutationFailure(err, mutation.meta); }, }), defaultOptions: { queries: { retry: false, staleTime: 30_000, refetchOnWindowFocus: false } }, }); }