The streamed loaders on bookmarks, collections, library, search, and the
collection detail page swallowed only ApiError into their in-band `error`
field and re-threw anything else. A raw network failure (a non-ApiError
TypeError from fetch) therefore escaped the `{#await}` to SvelteKit's
generic error page. On the collection detail page a content-load failure
showed a misleading "this collection is empty", and on search a failed
results query showed a false "no matches" because the streamed `error`
was never rendered.
Fold non-ApiError failures into the same in-band `error` so every
transient blip renders as an inline message, add a `contentError` branch
to the collection detail grid, and render the search results `error` in
both result views.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
63 lines
2.6 KiB
TypeScript
63 lines
2.6 KiB
TypeScript
import { ApiError } from '$lib/api/client';
|
|
import { listMyBookmarks } from '$lib/api/bookmarks';
|
|
import { listMyCollections } from '$lib/api/collections';
|
|
import { listMyReadProgress } from '$lib/api/read_progress';
|
|
import {
|
|
listMyPageTags,
|
|
listMyDistinctPageTags
|
|
} from '$lib/api/page_tags';
|
|
import type { PageLoad } from './$types';
|
|
|
|
export const ssr = false;
|
|
|
|
/**
|
|
* Loads bookmarks + collections + history + page-tags in one shot so
|
|
* the Library segmented control can swap between sub-tabs without
|
|
* firing a second round trip per tap. 401 → unauthenticated path; the
|
|
* page surfaces a sign-in prompt and renders empty lists.
|
|
*/
|
|
export const load: PageLoad = async () => {
|
|
const empty = {
|
|
authenticated: true,
|
|
bookmarks: [] as Awaited<ReturnType<typeof listMyBookmarks>>['items'],
|
|
collections: [] as Awaited<ReturnType<typeof listMyCollections>>['items'],
|
|
history: [] as Awaited<ReturnType<typeof listMyReadProgress>>['items'],
|
|
pageTags: [] as Awaited<ReturnType<typeof listMyPageTags>>['items'],
|
|
distinctPageTags: [] as Awaited<ReturnType<typeof listMyDistinctPageTags>>,
|
|
error: null as string | null
|
|
};
|
|
// Streamed so the active sub-tab shows a skeleton while the one-shot fetch
|
|
// (also the auth gate) is in flight.
|
|
return {
|
|
result: (async () => {
|
|
try {
|
|
const [bookmarks, collections, history, pageTags, distinctPageTags] =
|
|
await Promise.all([
|
|
listMyBookmarks(),
|
|
listMyCollections({ limit: 200 }),
|
|
listMyReadProgress({ limit: 100 }),
|
|
listMyPageTags({ limit: 100 }),
|
|
listMyDistinctPageTags(undefined, 100)
|
|
]);
|
|
return {
|
|
...empty,
|
|
bookmarks: bookmarks.items,
|
|
collections: collections.items,
|
|
history: history.items,
|
|
pageTags: pageTags.items,
|
|
distinctPageTags
|
|
};
|
|
} catch (e) {
|
|
if (e instanceof ApiError && e.status === 401) {
|
|
return { ...empty, authenticated: false };
|
|
}
|
|
// An HTTP error or a raw network failure (a non-ApiError
|
|
// TypeError) renders inline rather than escaping to the
|
|
// framework error page for a transient API blip.
|
|
const message = e instanceof ApiError ? e.message : 'Something went wrong.';
|
|
return { ...empty, error: message };
|
|
}
|
|
})()
|
|
};
|
|
};
|