diff --git a/src/components/__tests__/version-poller.test.ts b/src/components/__tests__/version-poller.test.ts index fd77fae98..14b0a24da 100644 --- a/src/components/__tests__/version-poller.test.ts +++ b/src/components/__tests__/version-poller.test.ts @@ -15,7 +15,7 @@ import { describe, expect, it } from "vitest"; -import { resolveVersionPollDecision } from "../version-poller"; +import { cachesToEvict, resolveVersionPollDecision } from "../version-poller"; describe("resolveVersionPollDecision", () => { it("is up-to-date when live matches the shell", () => { @@ -144,3 +144,31 @@ describe("evictAndReload", () => { ]); }); }); + +describe("cachesToEvict", () => { + const keys = [ + "healthlog-static-v1.41.2", + "healthlog-pages-v1.41.2", + "healthlog-data-v1.41.2", + "healthlog-static-v1.42.0", + "another-app-cache", + ]; + + it("keeps the target release's caches the new worker just filled", () => { + expect(cachesToEvict(keys, "1.42.0")).toEqual([ + "healthlog-static-v1.41.2", + "healthlog-pages-v1.41.2", + "healthlog-data-v1.41.2", + ]); + }); + + it("reads the target with or without its v", () => { + expect(cachesToEvict(keys, "v1.42.0")).not.toContain( + "healthlog-static-v1.42.0", + ); + }); + + it("never touches another app's caches", () => { + expect(cachesToEvict(keys, "1.43.0")).not.toContain("another-app-cache"); + }); +}); diff --git a/src/components/version-poller.tsx b/src/components/version-poller.tsx index 00391c215..6252509fb 100644 --- a/src/components/version-poller.tsx +++ b/src/components/version-poller.tsx @@ -101,6 +101,27 @@ async function fetchLiveVersion(signal: AbortSignal): Promise { // to break the loop within one document lifetime. let inMemoryReloadGuard: string | null = null; +/** + * The worker caches the "new version" reload clears: every HealthLog cache + * except the target release's own. `registration.update()` resolves as soon + * as the new worker starts installing, so its freshly precached logos and + * favicon (`healthlog-static-v`) may already exist by then; deleting + * them left the installed app without them offline until they were fetched + * again. Cache names carry the version with a `v`, the version endpoint may + * answer without one. + */ +export function cachesToEvict( + keys: readonly string[], + targetVersion: string, +): string[] { + const target = targetVersion.replace(/^v/, ""); + return keys.filter((key) => { + if (!HEALTHLOG_CACHE_NAME_RE.test(key)) return false; + const version = key.replace(HEALTHLOG_CACHE_NAME_RE, "").replace(/^v/, ""); + return version !== target; + }); +} + export async function evictAndReload(targetVersion: string): Promise { inMemoryReloadGuard = targetVersion; try { @@ -123,9 +144,7 @@ export async function evictAndReload(targetVersion: string): Promise { try { const keys = await caches.keys(); await Promise.all( - keys - .filter((k) => HEALTHLOG_CACHE_NAME_RE.test(k)) - .map((k) => caches.delete(k)), + cachesToEvict(keys, targetVersion).map((k) => caches.delete(k)), ); } catch { /* best effort */