Files
bitvid/tests/storage-cache-control.test.mjs
T
thePR0M3TH3ANandClaude Opus 4.8 c20938b4aa [perf] Media caching: immutable upload headers + SW image cache (#55)
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]>
2026-07-02 14:48:18 -04:00

64 lines
2.4 KiB
JavaScript

// Cache-Control policy for uploaded media (TODO #55, part 1). Upload keys are
// content-addressed (buildR2Key namespaces by infohash), so media should get a
// long immutable Cache-Control — browsers then keep thumbnails/videos/torrents
// across visits. Mutable playlists (m3u8/mpd) must stay short-lived.
//
// test_integrity_note:
// change_type: ["new_tests"]
// scenarios:
// - id: SCN-upload-cache-control
// given: "an object key by extension"
// when: "computeCacheControl derives the Cache-Control header"
// then: "media/torrents are immutable-year; playlists revalidate; unknown gets the default"
// observable_outcomes:
// - "video + image (incl webp/avif) + .torrent keys -> public, max-age=31536000, immutable"
// - "m3u8/mpd -> public, max-age=60, must-revalidate"
// - "unknown/empty keys -> the 1h default (never immutable)"
// determinism_controls:
// - "pure function; explicit inputs"
// anti_cheat_rationale:
// prevents: ["hard-coded return value"]
// relaxation:
// did_relax_any_assertion: false
import "./test-helpers/setup-localstorage.mjs";
import test from "node:test";
import { strict as assert } from "node:assert";
import { computeCacheControl } from "../js/storage/s3-multipart.js";
const IMMUTABLE = "public, max-age=31536000, immutable";
test("videos, images (incl. webp/avif), and torrents are immutable for a year", () => {
for (const key of [
"u/npub1x/abc123/video.mp4",
"u/npub1x/abc123/clip.webm",
"u/npub1x/abc123/thumb.jpg",
"u/npub1x/abc123/thumb.webp",
"u/npub1x/abc123/poster.avif",
"u/npub1x/abc123/video.torrent",
"u/npub1x/external/deadbeef.torrent",
"SHOUTY/PATH/THUMB.PNG",
]) {
assert.equal(computeCacheControl(key), IMMUTABLE, key);
}
});
test("mutable playlists stay short-lived (live manifests change in place)", () => {
assert.equal(
computeCacheControl("u/npub1x/live/stream.m3u8"),
"public, max-age=60, must-revalidate",
);
assert.equal(
computeCacheControl("u/npub1x/live/stream.mpd"),
"public, max-age=60, must-revalidate",
);
});
test("unknown or missing keys fall back to the 1h default, never immutable", () => {
for (const key of ["u/npub1x/uploads/file.bin", "no-extension", "", null, undefined]) {
const value = computeCacheControl(key);
assert.equal(value, "public, max-age=3600", String(key));
assert.ok(!value.includes("immutable"));
}
});