From ad620bcf804430b02b7f60818f633cbdf41e2349 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Thu, 8 Oct 2026 21:38:59 +0200 Subject: [PATCH] fix(pwa): keep the new worker's precache when loading a new version The reload cleared every HealthLog cache after asking the worker to update, but the update resolves as soon as the new worker starts installing. Its fresh precache of logos and favicon was deleted with the old ones, so the installed app lacked them offline. Leave the target release's caches in place. --- .../__tests__/version-poller.test.ts | 30 ++++++++++++++++++- src/components/version-poller.tsx | 25 ++++++++++++++-- 2 files changed, 51 insertions(+), 4 deletions(-) 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 */