fix(library): resuming the app no longer blanks the page on screen

Coming back to the app after a few minutes in the background replaced the
Frasier series page with "Failed to load item". Android cuts a backgrounded
app's network, the app declares the server offline, and on resume the
reconnect and offline-filter reloads refresh the page. Every backend call in
that refresh answered from the cache, yet something in it threw, and:

- any throw replaced the whole page with an error, although it was a
  refresh of content already on screen;
- no later successful reload of the same item cleared that error, so it
  stayed until the viewer navigated away;
- the catch logged nothing and turned every non-Error value (backend
  errors arrive as plain strings) into the generic text, so neither logcat
  nor the screen said what failed.

A failed refresh now keeps the page and logs the value actually thrown;
every successful load clears the error; failing to open an item still shows
one, with the backend's own message. The decision lives in
detailLoadError.ts so it is unit-tested.

The throw itself is not yet identified - the next occurrence names itself
in the log.

DR-297, UT-267.
This commit is contained in:
2026-09-24 07:40:14 -04:00
parent 73fd8a1dfe
commit d7136aef48
4 changed files with 88 additions and 3 deletions
@@ -0,0 +1,35 @@
// What the library detail page's error slot shows after a load.
//
// Extracted from `/library/[id]/+page.svelte` so it can be unit-tested.
export type LoadOutcome = { ok: true } | { ok: false; error: unknown };
/**
* The text of whatever a load threw. Backend commands reject with a plain
* string, not an `Error`, so reading only `Error.message` replaced every
* backend error with the generic fallback and hid what actually failed.
*/
export function loadErrorMessage(error: unknown): string {
if (error instanceof Error && error.message) return error.message;
if (typeof error === "string" && error) return error;
return "Failed to load item";
}
/**
* The error to show once a load has ended.
*
* A success always clears it — a same-item reload used to leave a stale error
* up for good. A failed *refresh* of a page already on screen shows none: the
* page is still showing what the cache answered, and a background refresh
* (reconnect, resume, filter change) must not blank it. Only failing to open an
* item leaves the viewer with nothing to see.
*
* TRACES: UR-062 | DR-297 | UT-267
*/
export function errorAfterLoad(
outcome: LoadOutcome,
options: { refreshing: boolean },
): string | null {
if (outcome.ok || options.refreshing) return null;
return loadErrorMessage(outcome.error);
}