diff --git a/frontend/src/lib/api.js b/frontend/src/lib/api.js index be63130..ce856cd 100644 --- a/frontend/src/lib/api.js +++ b/frontend/src/lib/api.js @@ -1,7 +1,13 @@ -export async function getJSON(url) { - const r = await fetch(url); - if (!r.ok) throw new Error((await r.text().catch(() => '')) || r.statusText); - return r.json(); +export async function getJSON(url, { timeout = 10000 } = {}) { + const ctrl = new AbortController(); + const t = setTimeout(() => ctrl.abort(), timeout); // never hang on a stuck request + try { + const r = await fetch(url, { signal: ctrl.signal }); + if (!r.ok) throw new Error((await r.text().catch(() => '')) || r.statusText); + return await r.json(); + } finally { + clearTimeout(t); + } } export async function postJSON(url, body) { diff --git a/frontend/src/routes/+page.svelte b/frontend/src/routes/+page.svelte index d1ceab6..3fa1797 100644 --- a/frontend/src/routes/+page.svelte +++ b/frontend/src/routes/+page.svelte @@ -429,19 +429,29 @@ dismissed = new Set(P.loadJSON(DISMISSED_KEY, [])); refreshAuth(); trackVisit(); + // Critical path: moods + categories (needed to resolve a mood/topic view) + // load in PARALLEL, then the requested view. Every getJSON times out, and + // loading ALWAYS clears in finally, so the page can never get stuck on + // "Gathering the good news…". try { - moods = await getJSON('/api/moods'); - topics = (await getJSON('/api/categories')).topics; - try { lanePool = await getJSON('/api/lanes'); } catch { lanePool = null; } - try { families = await getJSON('/api/families'); } catch { families = []; } - // Load whatever the URL asks for (Highlights, or a deep-linked source/tag/ - // view) — the param stays in the URL so reload and share are honest. + const [m, c] = await Promise.all([ + getJSON('/api/moods').catch(() => []), + getJSON('/api/categories').catch(() => ({ topics: [] })), + ]); + moods = m; topics = c.topics; await loadView(selected); } catch (e) { error = 'Could not reach Upbeat Bytes.'; + } finally { + loading = false; } - loading = false; checkSince(); // after the first paint; non-blocking + // Pure decoration (nav-rail customization + tag families): non-blocking — a + // slow or failed one fills in late instead of holding up the content. + Promise.allSettled([ + getJSON('/api/lanes').then((r) => (lanePool = r)), + getJSON('/api/families').then((r) => (families = r)), + ]); });