Version-stamped static URLs cached forever, thumbnail cache 25000 with throttled trimming

This commit is contained in:
Claude
2026-10-02 02:11:38 +00:00
parent c69cc3a4bc
commit 1b10a994dd
2 changed files with 18 additions and 6 deletions

View File

@@ -46,7 +46,7 @@ const THUMB_HOSTS = new Set([
]); ]);
// Thumbnails accumulate with every card ever scrolled past — keep it bounded. // Thumbnails accumulate with every card ever scrolled past — keep it bounded.
const THUMB_CACHE_MAX = 2500; const THUMB_CACHE_MAX = 25000;
function isVersionedShellCache(key) { function isVersionedShellCache(key) {
return key.startsWith(SHELL_CACHE_PREFIX) && !UTILITY_CACHES.has(key); return key.startsWith(SHELL_CACHE_PREFIX) && !UTILITY_CACHES.has(key);
} }
@@ -222,7 +222,10 @@ self.addEventListener('message', (e) => {
// Cache-first: serve from cache; if missing, fetch, cache, return. // Cache-first: serve from cache; if missing, fetch, cache, return.
async function cacheFirst(request) { async function cacheFirst(request) {
const cache = await caches.open(CACHE); const cache = await caches.open(CACHE);
const cached = await cache.match(request); // Shell files are precached under their plain URL; index.html asks for them as
// `file?v=<build>` (so the HTTP cache can keep them forever) — match ignoring it.
const versioned = /[?&]v=/.test(request.url);
const cached = await cache.match(request, versioned ? { ignoreSearch: true } : undefined);
if (cached) return cached; if (cached) return cached;
try { try {
const response = await fetch(request); const response = await fetch(request);
@@ -262,7 +265,7 @@ async function thumbnail(request) {
const cors = await fetch(request.url, { mode: 'cors', credentials: 'omit' }); const cors = await fetch(request.url, { mode: 'cors', credentials: 'omit' });
if (cors && cors.ok) { if (cors && cors.ok) {
await cache.put(request, cors.clone()).catch(() => {}); await cache.put(request, cors.clone()).catch(() => {});
trimThumbCache(); if (++_thumbPuts % 100 === 0) trimThumbCache();
return cors; return cors;
} }
} catch { /* CORS refused or offline — fall through to the raw request */ } } catch { /* CORS refused or offline — fall through to the raw request */ }
@@ -271,7 +274,8 @@ async function thumbnail(request) {
const raw = await fetch(request); const raw = await fetch(request);
if (raw && (raw.status === 200 || raw.status === 0)) { if (raw && (raw.status === 200 || raw.status === 0)) {
await cache.put(request, raw.clone()).catch(() => {}); await cache.put(request, raw.clone()).catch(() => {});
trimThumbCache(); // keys() on a 25k-entry cache is not free — trim every 100th put, not each one.
if (++_thumbPuts % 100 === 0) trimThumbCache();
} }
return raw; return raw;
} catch { } catch {
@@ -282,6 +286,7 @@ async function thumbnail(request) {
// Cache.keys() is insertion-ordered, so the front of the list is the oldest. // Cache.keys() is insertion-ordered, so the front of the list is the oldest.
let _trimmingThumbs = false; let _trimmingThumbs = false;
let _thumbPuts = 0;
async function trimThumbCache() { async function trimThumbCache() {
if (_trimmingThumbs) return; if (_trimmingThumbs) return;
_trimmingThumbs = true; _trimmingThumbs = true;

View File

@@ -2187,7 +2187,10 @@ function indexHtml(c) {
try { _indexSource = readFileSync('./public/index.html', 'utf8'); } try { _indexSource = readFileSync('./public/index.html', 'utf8'); }
catch { return c.text('index.html not found', 404); } catch { return c.text('index.html not found', 404); }
} }
const html = _indexSource.replace('__BUILD_TAG__', BUILD_TAG); // Local css/js get `?v=<build>`: that URL's bytes never change, so the static
// handler below lets browsers keep it forever; a new build is a new URL.
const html = _indexSource.replace('__BUILD_TAG__', BUILD_TAG)
.replace(/((?:href|src)=")((?![a-z]+:|\/)[^"?]+\.(?:css|js))(")/g, `$1$2?v=${BUILD_TAG}$3`);
return sendCompressed(c, compressedEntry('index.html', Buffer.from(html), MIME.html), 'no-cache'); return sendCompressed(c, compressedEntry('index.html', Buffer.from(html), MIME.html), 'no-cache');
} }
app.get('/', indexHtml); app.get('/', indexHtml);
@@ -2203,7 +2206,11 @@ app.get('/*', async (c, next) => {
if (!(await file.exists())) return next(); if (!(await file.exists())) return next();
const raw = Buffer.from(await file.arrayBuffer()); const raw = Buffer.from(await file.arrayBuffer());
const ext = p.slice(p.lastIndexOf('.') + 1).toLowerCase(); const ext = p.slice(p.lastIndexOf('.') + 1).toLowerCase();
return sendCompressed(c, compressedEntry(p, raw, MIME[ext] || 'application/octet-stream'), 'no-cache'); // `?v=` equal to the running build → forever (a new build changes the URL).
// Any other / missing v keeps revalidating.
const forever = c.req.query('v') === BUILD_TAG;
return sendCompressed(c, compressedEntry(p, raw, MIME[ext] || 'application/octet-stream'),
forever ? 'public, max-age=31536000, immutable' : 'no-cache');
}); });
// Self-hosted fonts and icons rarely change: let the browser keep them for 30 days // Self-hosted fonts and icons rarely change: let the browser keep them for 30 days
// (the service worker precaches the shell anyway); everything else revalidates. // (the service worker precaches the shell anyway); everything else revalidates.