Skip to content
Draft
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 20 additions & 6 deletions public/sw.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
const CACHE_VERSION = "v3";
const CACHE_VERSION = "v4";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Commit message could be more descriptive

The message fix: avoid caching personalized pages follows conventional-commit form but is thin — it doesn't mention that /auth exact-match was missing from the no-cache set, that Cache-Control: private/no-store/no-cache is now inspected before every cache write, or that the version bump was needed to evict stale entries. A reader scanning git log won't understand the root cause or the scope of the fix without opening the PR. Something like fix(sw): add /auth+/ exact-no-cache paths, honour Cache-Control private/no-store, bump cache to v4 captures all three changes in one line.

Context Used: Encourage people to write better commit messages (source)

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Fix in Codex Fix in Claude Code Fix in Cursor

const STATIC_CACHE = `examcooker-static-${CACHE_VERSION}`;
const PAGE_CACHE = `examcooker-pages-${CACHE_VERSION}`;
const RUNTIME_CACHE = `examcooker-runtime-${CACHE_VERSION}`;
Expand All @@ -17,6 +17,7 @@ const KNOWN_CACHES = new Set([STATIC_CACHE, PAGE_CACHE, RUNTIME_CACHE]);
const STATIC_PATH_PREFIXES = ["/_next/static/", "/icons/", "/assets/", "/vendor/"];
const STATIC_PATH_EXACT = new Set(["/manifest.webmanifest", "/offline.html", "/sw.js"]);
const FONT_HOSTS = new Set(["fonts.googleapis.com", "fonts.gstatic.com"]);
const NO_CACHE_PATHS = new Set(["/", "/auth"]);
const NO_CACHE_PATH_PREFIXES = [
"/api/",
"/auth/",
Expand All @@ -42,7 +43,17 @@ function isStaticAsset(url) {
}

function isUncacheable(url) {
return NO_CACHE_PATH_PREFIXES.some((prefix) => url.pathname.startsWith(prefix));
return (
NO_CACHE_PATHS.has(url.pathname) ||
NO_CACHE_PATH_PREFIXES.some((prefix) => url.pathname.startsWith(prefix))
);
}

function isCacheableResponse(response) {
if (!response || !response.ok || response.type === "opaque") return false;

const cacheControl = response.headers.get("cache-control") || "";
return !/(^|,\s*)(no-store|no-cache|private)(\s|,|=|$)/i.test(cacheControl);
}
Comment on lines +52 to 57

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 no-cache treated as uncacheable — more conservative than HTTP requires

Per RFC 9111, Cache-Control: no-cache means "validate before serving from cache," not "never cache." Treating it as uncacheable here prevents the SW from ever storing or serving such responses, which is safe but more aggressive than necessary. It means any response annotated no-cache (e.g. a public asset served with short-lived revalidation) will always hit the network, bypassing the stale-while-revalidate benefit entirely. Consider whether this is intentional before this becomes a performance concern for non-personalized routes.

Fix in Codex Fix in Claude Code Fix in Cursor


function isHtmlAccept(request) {
Expand Down Expand Up @@ -103,8 +114,11 @@ self.addEventListener("message", (event) => {
for (const route of event.data.routes) {
if (typeof route !== "string" || !route.startsWith("/")) continue;
try {
const url = new URL(route, self.location.origin);
if (isUncacheable(url)) continue;

const response = await fetch(route, { credentials: "same-origin" });
if (response && response.ok) {
if (isCacheableResponse(response)) {
await cache.put(route, response.clone());
}
} catch {
Expand All @@ -125,7 +139,7 @@ async function staleWhileRevalidate(event, cacheName) {

const networkFetch = (preloadResponse ? Promise.resolve(preloadResponse) : fetch(event.request))
.then((response) => {
if (response && response.ok && response.type !== "opaque") {
if (isCacheableResponse(response)) {
cache.put(event.request, response.clone()).catch(() => undefined);
}
return response;
Expand Down Expand Up @@ -163,7 +177,7 @@ async function cacheFirst(event) {
event.waitUntil(
fetch(event.request)
.then((response) => {
if (response && response.ok && response.type !== "opaque") {
if (isCacheableResponse(response)) {
return cache.put(event.request, response.clone());
}
return undefined;
Expand All @@ -174,7 +188,7 @@ async function cacheFirst(event) {
}
try {
const response = await fetch(event.request);
if (response && response.ok && response.type !== "opaque") {
if (isCacheableResponse(response)) {
cache.put(event.request, response.clone()).catch(() => undefined);
}
return response;
Expand Down