370d62270b
The post-deploy blank/slow load: new hashed chunks weren't in Cloudflare yet, so the first visitor pulled them cold from the residential origin — AND the service worker simultaneously precached ~30 of those cold assets (a request storm), pushing past the 7s boot timeout. * sync-static.sh now warms the CF edge cache (fetches every immutable asset through the public domain) so the first visitor gets HITs, not cold-origin. * Service worker no longer bulk-precaches on install (the browser already caches immutable assets for a year); it caches the shell + assets lazily as used. No more storm. * Boot-recovery timeout 7s → 10s so a merely-slow load doesn't flash the card. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
73 lines
2.7 KiB
JavaScript
73 lines
2.7 KiB
JavaScript
/// <reference types="@sveltejs/kit" />
|
|
// Calm, lightweight service worker. It does NOT bulk-precache on install — the
|
|
// browser already caches the year-immutable assets on its own, and a cold
|
|
// precache storm right after a deploy hammers the (residential) origin and slows
|
|
// first loads. Instead: cache the shell for an offline fallback, and cache other
|
|
// assets lazily as they're actually used. Live data (API + server-rendered pages)
|
|
// is always fetched fresh.
|
|
import { version } from '$service-worker';
|
|
|
|
const CACHE = `upbeat-${version}`;
|
|
|
|
// Paths the FastAPI server owns — the SW must NOT intercept or cache these.
|
|
function isServerPath(p) {
|
|
if (p.startsWith('/api/') || p.startsWith('/a/') || p.startsWith('/docs')) return true;
|
|
return p === '/openapi.json' || p === '/healthz' || p === '/today' || p === '/sitemap.xml';
|
|
}
|
|
|
|
self.addEventListener('install', (event) => {
|
|
// Best-effort: grab the app shell as an offline fallback. No bulk precache.
|
|
event.waitUntil(
|
|
caches.open(CACHE).then((c) => c.add('/').catch(() => {})).then(() => self.skipWaiting())
|
|
);
|
|
});
|
|
|
|
self.addEventListener('activate', (event) => {
|
|
event.waitUntil(
|
|
caches
|
|
.keys()
|
|
.then((keys) => Promise.all(keys.filter((k) => k !== CACHE).map((k) => caches.delete(k))))
|
|
.then(() => self.clients.claim())
|
|
);
|
|
});
|
|
|
|
self.addEventListener('fetch', (event) => {
|
|
const { request } = event;
|
|
if (request.method !== 'GET') return;
|
|
const url = new URL(request.url);
|
|
if (url.origin !== location.origin) return;
|
|
if (isServerPath(url.pathname)) return; // let the network/server handle these
|
|
|
|
// Navigations: network-first; keep the freshest real HTML shell as the offline
|
|
// fallback; on a failed fetch, serve that cached shell (never blank).
|
|
if (request.mode === 'navigate') {
|
|
event.respondWith(
|
|
fetch(request)
|
|
.then((res) => {
|
|
if (res && res.ok && (res.headers.get('content-type') || '').includes('text/html')) {
|
|
const copy = res.clone();
|
|
caches.open(CACHE).then((c) => c.put('/', copy)).catch(() => {});
|
|
}
|
|
return res;
|
|
})
|
|
.catch(() => caches.match('/'))
|
|
);
|
|
return;
|
|
}
|
|
|
|
// Static assets: serve from cache if present, else fetch and cache for next time
|
|
// (cache-as-you-go — no install storm). Only cache successful same-origin GETs.
|
|
event.respondWith(
|
|
caches.match(request).then((cached) => {
|
|
if (cached) return cached;
|
|
return fetch(request).then((res) => {
|
|
if (res && res.ok && res.type === 'basic') {
|
|
const copy = res.clone();
|
|
caches.open(CACHE).then((c) => c.put(request, copy)).catch(() => {});
|
|
}
|
|
return res;
|
|
});
|
|
})
|
|
);
|
|
});
|