The code cited its plan instead of saying why: `E28 review J4`, `(R6 S3b)`, `S1 justified…`,
`deep-review round 5`. That plan lives in the private ops repo, so for every reader of this one the
citation was noise, and a sentence whose subject was the plan step ("round 2 showed…") hid its
lesson behind an id. Each one is gone and the "why" kept: a bare citation is deleted, a sentence
carrying a lesson is reworded around it ("a later attempt showed…").
Measured (`scripts/audit_metrics.py`, product code): 652 lines in 270 files carried an epic code by
the definition the next commit holds (507 / 241 by the old, two-digit-only one) -> 0. Across all
code, tests, scripts and configs: ~1100 epic-code lines plus ~320 sprint/round ids.
Behaviour unchanged, checked mechanically: every changed .py file has the same AST once docstrings
are blanked; css/sh/toml/yml/json files are identical once comments are stripped; the only
non-comment changes in TS/JS are test titles, one description string in a contract table, and five
ESLint messages, whose text is all the gates snapshot diff holds. Not touched: Markdown (docs and
the changelog keep their history), Alembic migrations, TV episode notation (`S2 · E5`), DESIGN.md
ids (`C1`, `B4`, `S3h`), tracker ids (`SEC-16`, `DX-17`).
One stale claim fixed on the way: test_hls_reaper's docstring said the player checkpoints "every
10 s whether or not it is playing", which plex/stream.py calls wrong; it now says what the code does.
68 lines
3.0 KiB
TypeScript
68 lines
3.0 KiB
TypeScript
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<string, unknown> {
|
||
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 } },
|
||
});
|
||
}
|