"Recently Added" grids loaded visibly slower after the image cache shipped. The
feed's data path was untouched — the regression was the thumbnails: the SW handler
had three compounding costs on a cold cache:
1. The network fetch waited behind caches.open + cache.match, delaying every
thumbnail's download start.
2. caches.open ran per request instead of once.
3. trimImageCache ran a full keys() enumeration after EVERY put — a grid burst of
dozens of thumbnails did dozens of O(n) enumerations, all contending with the
cache.match calls of later images.
Fix: start the network fetch immediately (cold path is now as fast as no SW; a
warm cache still serves the cached copy for first paint), memoize the cache open,
and amortize trims (every 20 puts). SW_VERSION bumped so testers pick it up.
Tests: sw-image-cache (7 — adds a regression test that gates cache.match behind a
promise and asserts the download starts anyway; all prior behaviors re-verified
against the real sw.min.js). Lint + build green.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Verified first that event caching needs nothing: allEvents (incl. ingested NIP-71)
already persists to IndexedDB and every grid renders cache-first + merge-new. The
real gap was MEDIA: thumbnails/avatars/banners re-downloaded across visits.
Part 1 — Cache-Control on uploads (js/storage/s3-multipart.js):
computeCacheControl already stamped most media immutable but missed webp/avif/ico
and .torrent files (which fell to the 1h default). Added them — safe because
buildR2Key namespaces keys by infohash (content-addressed, a key's bytes never
change). Playlists (m3u8/mpd) stay short-lived (live manifests mutate in place).
Exported for tests.
Part 2 — service-worker image cache (sw.min.js):
Cross-origin images (the hosts bitvid does NOT control headers for) are now served
stale-while-revalidate from a versioned Cache-API cache (bitvid-images-v1):
- instant on revisit; background refresh self-heals changed images (and where
part 1's immutable headers apply, that refresh is served by the HTTP cache — free)
- opaque (no-cors) responses cached — exactly the external-thumbnail case
- LRU-capped (300); quota failures drop the cache and fail open; every handler
error falls back to plain fetch so a caching bug can never break images
- the activate handler's wipe-all now PRESERVES the current image cache (old
versions + other caches still wiped); same-origin assets untouched (they ride
the host HTTP cache + dist cache-purge pipeline)
- app._initServiceWorker registers the SW at boot when none exists, same
path/scope/options as webtorrent.js's setupServiceWorker so playback's later
register() resolves to the same registration (playback path untouched)
- SW_VERSION bumped
Tests: storage-cache-control (3, exported policy) + sw-image-cache (6 — evaluates
the REAL sw.min.js in a VM sandbox and drives its handlers: cold/warm/offline/
opaque flows, same-origin + non-image non-interception, activate preserves the
image cache). Lint + build green.
Co-Authored-By: Claude Opus 4.8 <[email protected]>