Stability: cache-control at origin + non-hanging startup
Intermittent blank screens / long "Gathering the good news…" — two fixes: Origin cache headers (Caddyfile, deployed separately): content-hashed /_app/immutable/* → max-age=31536000, immutable; everything else (HTML shell, service worker, version manifest, webmanifest, word lists, icons) → no-cache, so a deploy can't leave a stale shell/SW pinned. (Cloudflare's 4h Browser Cache TTL still overrides this until its dashboard setting is switched to "Respect Existing Headers" — that's the actual root cause.) App startup hardening: * getJSON now has a 10s AbortController timeout — a stuck request can never hang the loading state forever. * Home onMount loads moods+categories in parallel then the view, with loading ALWAYS cleared in finally; lanes/families dropped to non-blocking decoration. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
+10
-4
@@ -1,7 +1,13 @@
|
|||||||
export async function getJSON(url) {
|
export async function getJSON(url, { timeout = 10000 } = {}) {
|
||||||
const r = await fetch(url);
|
const ctrl = new AbortController();
|
||||||
if (!r.ok) throw new Error((await r.text().catch(() => '')) || r.statusText);
|
const t = setTimeout(() => ctrl.abort(), timeout); // never hang on a stuck request
|
||||||
return r.json();
|
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) {
|
export async function postJSON(url, body) {
|
||||||
|
|||||||
@@ -429,19 +429,29 @@
|
|||||||
dismissed = new Set(P.loadJSON(DISMISSED_KEY, []));
|
dismissed = new Set(P.loadJSON(DISMISSED_KEY, []));
|
||||||
refreshAuth();
|
refreshAuth();
|
||||||
trackVisit();
|
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 {
|
try {
|
||||||
moods = await getJSON('/api/moods');
|
const [m, c] = await Promise.all([
|
||||||
topics = (await getJSON('/api/categories')).topics;
|
getJSON('/api/moods').catch(() => []),
|
||||||
try { lanePool = await getJSON('/api/lanes'); } catch { lanePool = null; }
|
getJSON('/api/categories').catch(() => ({ topics: [] })),
|
||||||
try { families = await getJSON('/api/families'); } catch { families = []; }
|
]);
|
||||||
// Load whatever the URL asks for (Highlights, or a deep-linked source/tag/
|
moods = m; topics = c.topics;
|
||||||
// view) — the param stays in the URL so reload and share are honest.
|
|
||||||
await loadView(selected);
|
await loadView(selected);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
error = 'Could not reach Upbeat Bytes.';
|
error = 'Could not reach Upbeat Bytes.';
|
||||||
|
} finally {
|
||||||
|
loading = false;
|
||||||
}
|
}
|
||||||
loading = false;
|
|
||||||
checkSince(); // after the first paint; non-blocking
|
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)),
|
||||||
|
]);
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user