From 05c1e2c3229081d86fbcabad92933ecf8e70ad05 Mon Sep 17 00:00:00 2001 From: Chenyme <118253778+chenyme@users.noreply.github.com> Date: Tue, 14 Jul 2026 20:32:46 +0800 Subject: [PATCH] refactor: remove legacy PWA service worker and update related assets --- Dockerfile | 1 - frontend/app/layout.tsx | 4 +- frontend/public/sw.js | 133 +----------------- frontend/scripts/sync-pwa-assets.mjs | 16 --- .../pwa-service-worker-register.tsx | 37 ----- frontend/shared/generated/pwa-assets.ts | 1 - .../legacy-service-worker-migration.tsx | 42 ++++++ scripts/sync-version.mjs | 11 -- 8 files changed, 50 insertions(+), 195 deletions(-) delete mode 100644 frontend/shared/components/pwa-service-worker-register.tsx create mode 100644 frontend/shared/pwa/migrations/legacy-service-worker-migration.tsx diff --git a/Dockerfile b/Dockerfile index feb58ad3..4d1f035c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -35,7 +35,6 @@ COPY scripts /src/scripts COPY frontend/package.json frontend/pnpm-lock.yaml ./ COPY frontend/scripts ./scripts COPY frontend/public/pwa ./public/pwa -COPY frontend/public/sw.js ./public/sw.js RUN corepack enable diff --git a/frontend/app/layout.tsx b/frontend/app/layout.tsx index 0d78fcb9..bffdbcf4 100644 --- a/frontend/app/layout.tsx +++ b/frontend/app/layout.tsx @@ -6,8 +6,8 @@ import { AppearancePreferencesProvider } from "@/features/settings"; import { AppI18nProvider } from "@/i18n/app-i18n-provider"; import { DevtoolsBrandBanner } from "@/shared/components/devtools-brand-banner"; import { ThemeProvider } from "@/shared/components/theme-provider"; -import { PWAServiceWorkerRegister } from "@/shared/components/pwa-service-worker-register"; import { brandAssets, brandText } from "@/shared/lib/branding"; +import { LegacyPWAServiceWorkerMigration } from "@/shared/pwa/migrations/legacy-service-worker-migration"; import { Toaster } from "@/components/ui/sonner"; import "./globals.css"; @@ -81,7 +81,7 @@ export default function RootLayout({ {children} - + diff --git a/frontend/public/sw.js b/frontend/public/sw.js index 2853aa6b..2340f485 100644 --- a/frontend/public/sw.js +++ b/frontend/public/sw.js @@ -1,45 +1,8 @@ -const PWA_ASSET_VERSION = "0.3.2"; -const PWA_ASSET_CACHE_KEY = "632cb83037d9"; -const PWA_ASSET_MANIFEST = { - "/pwa/icon.svg": "/pwa/generated/icon.fe1d64d9758c.svg", - "/pwa/icon-192.png": "/pwa/generated/icon-192.d7a0049daaa7.png", - "/pwa/icon-512.png": "/pwa/generated/icon-512.bd37e5f66cc5.png", - "/pwa/icon-maskable-512.png": "/pwa/generated/icon-maskable-512.647d8497d850.png", - "/pwa/apple-touch-icon.png": "/pwa/generated/apple-touch-icon.0c62df73d41b.png" -}; -const STATIC_CACHE = `deeix-chat-static-${PWA_ASSET_VERSION}-${PWA_ASSET_CACHE_KEY}`; -const PAGE_CACHE = `deeix-chat-pages-${PWA_ASSET_VERSION}`; -const STATIC_CACHE_MAX_ENTRIES = 160; -const PAGE_CACHE_MAX_ENTRIES = 24; - -const APP_SHELL_URLS = [ - "/", - "/chat", - "/logo.svg", - "/logo-color.svg", - pwaAsset("/pwa/icon.svg"), - pwaAsset("/pwa/icon-192.png"), - pwaAsset("/pwa/icon-512.png"), -]; - -const BACKEND_PATH_PREFIXES = [ - "/api/", - "/swagger", - "/healthz", - "/readyz", -]; - -function pwaAsset(path) { - return PWA_ASSET_MANIFEST[path] ?? path; -} +// Temporary tombstone for the retired PWA service worker. Keep this URL stable during migration. +const LEGACY_CACHE_PREFIX = "deeix-chat-"; self.addEventListener("install", (event) => { - event.waitUntil( - caches.open(STATIC_CACHE) - .then((cache) => cache.addAll(APP_SHELL_URLS)) - .catch(() => undefined) - .then(() => self.skipWaiting()), - ); + event.waitUntil(self.skipWaiting()); }); self.addEventListener("activate", (event) => { @@ -47,94 +10,10 @@ self.addEventListener("activate", (event) => { caches.keys() .then((keys) => Promise.all( keys - .filter((key) => key !== STATIC_CACHE && key !== PAGE_CACHE) + .filter((key) => key.startsWith(LEGACY_CACHE_PREFIX)) .map((key) => caches.delete(key)), )) - .then(() => self.clients.claim()), + .then(() => self.clients.claim()) + .then(() => self.registration.unregister()), ); }); - -self.addEventListener("fetch", (event) => { - const request = event.request; - if (request.method !== "GET") { - return; - } - - const url = new URL(request.url); - if (url.origin !== self.location.origin || shouldBypassCache(url)) { - return; - } - - if (request.mode === "navigate") { - event.respondWith(networkFirst(request, PAGE_CACHE, PAGE_CACHE_MAX_ENTRIES)); - return; - } - - if (isStaticAsset(url)) { - event.respondWith(staleWhileRevalidate(request, STATIC_CACHE, STATIC_CACHE_MAX_ENTRIES)); - } -}); - -function shouldBypassCache(url) { - if (BACKEND_PATH_PREFIXES.some((prefix) => url.pathname === prefix || url.pathname.startsWith(prefix))) { - return true; - } - if (url.pathname.includes("/content") || url.pathname.includes("/download")) { - return true; - } - return false; -} - -function isStaticAsset(url) { - return url.pathname.startsWith("/_next/static/") || - url.pathname.startsWith("/pwa/") || - url.pathname.startsWith("/vendor/") || - /\.(?:css|js|mjs|png|jpg|jpeg|gif|webp|svg|ico|woff2?|ttf|otf|wasm)$/i.test(url.pathname); -} - -async function networkFirst(request, cacheName, maxEntries) { - const cache = await caches.open(cacheName); - try { - const response = await fetch(request); - if (isCacheable(response)) { - await cache.put(request, response.clone()); - await trimCache(cacheName, maxEntries); - } - return response; - } catch { - const cached = await cache.match(request); - if (cached) { - return cached; - } - return cache.match("/") || Response.error(); - } -} - -async function staleWhileRevalidate(request, cacheName, maxEntries) { - const cache = await caches.open(cacheName); - const cached = await cache.match(request); - const fresh = fetch(request) - .then(async (response) => { - if (isCacheable(response)) { - await cache.put(request, response.clone()); - await trimCache(cacheName, maxEntries); - } - return response; - }) - .catch(() => undefined); - - return cached || fresh || Response.error(); -} - -function isCacheable(response) { - return response && response.ok && response.type === "basic"; -} - -async function trimCache(cacheName, maxEntries) { - const cache = await caches.open(cacheName); - const keys = await cache.keys(); - if (keys.length <= maxEntries) { - return; - } - await Promise.all(keys.slice(0, keys.length - maxEntries).map((key) => cache.delete(key))); -} diff --git a/frontend/scripts/sync-pwa-assets.mjs b/frontend/scripts/sync-pwa-assets.mjs index ccd2f167..b4b9ca83 100644 --- a/frontend/scripts/sync-pwa-assets.mjs +++ b/frontend/scripts/sync-pwa-assets.mjs @@ -38,28 +38,12 @@ for (const asset of assets) { pwaAssetManifest[asset.path] = targetPath; } -const pwaAssetCacheKey = contentHash(Buffer.from(JSON.stringify(pwaAssetManifest))); - mkdirSync(dirname(manifestFile), { recursive: true }); writeFileSync( manifestFile, [ "// This file is generated by scripts/sync-pwa-assets.mjs.", - `export const pwaAssetCacheKey = ${JSON.stringify(pwaAssetCacheKey)};`, `export const pwaAssetManifest = ${JSON.stringify(pwaAssetManifest, null, 2)} as const;`, "", ].join("\n"), ); - -const serviceWorkerFile = join(publicDir, "sw.js"); -const serviceWorker = readFileSync(serviceWorkerFile, "utf8") - .replace( - /const PWA_ASSET_CACHE_KEY = "[^"]+";/u, - `const PWA_ASSET_CACHE_KEY = ${JSON.stringify(pwaAssetCacheKey)};`, - ) - .replace( - /const PWA_ASSET_MANIFEST = \{[\s\S]*?\};/u, - `const PWA_ASSET_MANIFEST = ${JSON.stringify(pwaAssetManifest, null, 2)};`, - ); - -writeFileSync(serviceWorkerFile, serviceWorker); diff --git a/frontend/shared/components/pwa-service-worker-register.tsx b/frontend/shared/components/pwa-service-worker-register.tsx deleted file mode 100644 index d148712f..00000000 --- a/frontend/shared/components/pwa-service-worker-register.tsx +++ /dev/null @@ -1,37 +0,0 @@ -"use client"; - -import * as React from "react"; - -export function PWAServiceWorkerRegister() { - React.useEffect(() => { - if (process.env.NODE_ENV !== "production") { - return undefined; - } - if (!("serviceWorker" in navigator)) { - return undefined; - } - - let cancelled = false; - const register = () => { - if (cancelled) { - return; - } - void navigator.serviceWorker.register("/sw.js", { scope: "/" }).catch(() => { - // PWA enhancement only; the app must keep working without a service worker. - }); - }; - - if (document.readyState === "complete") { - register(); - } else { - window.addEventListener("load", register, { once: true }); - } - - return () => { - cancelled = true; - window.removeEventListener("load", register); - }; - }, []); - - return null; -} diff --git a/frontend/shared/generated/pwa-assets.ts b/frontend/shared/generated/pwa-assets.ts index 98ede030..30f9571d 100644 --- a/frontend/shared/generated/pwa-assets.ts +++ b/frontend/shared/generated/pwa-assets.ts @@ -1,5 +1,4 @@ // This file is generated by scripts/sync-pwa-assets.mjs. -export const pwaAssetCacheKey = "632cb83037d9"; export const pwaAssetManifest = { "/pwa/icon.svg": "/pwa/generated/icon.fe1d64d9758c.svg", "/pwa/icon-192.png": "/pwa/generated/icon-192.d7a0049daaa7.png", diff --git a/frontend/shared/pwa/migrations/legacy-service-worker-migration.tsx b/frontend/shared/pwa/migrations/legacy-service-worker-migration.tsx new file mode 100644 index 00000000..c4be2efa --- /dev/null +++ b/frontend/shared/pwa/migrations/legacy-service-worker-migration.tsx @@ -0,0 +1,42 @@ +"use client"; + +import * as React from "react"; + +const LEGACY_CACHE_PREFIX = "deeix-chat-"; + +export function LegacyPWAServiceWorkerMigration() { + React.useEffect(() => { + if (process.env.NODE_ENV !== "production" || !("serviceWorker" in navigator)) { + return; + } + + const migrate = async () => { + const cacheNames = "caches" in window ? await window.caches.keys() : []; + const legacyCacheNames = cacheNames.filter((name) => + name.startsWith(LEGACY_CACHE_PREFIX), + ); + const registration = await navigator.serviceWorker.getRegistration("/"); + const worker = registration?.active ?? registration?.waiting ?? registration?.installing; + const isLegacyRegistration = worker + ? new URL(worker.scriptURL).pathname === "/sw.js" + : false; + + await Promise.all(legacyCacheNames.map((name) => window.caches.delete(name))); + + if (!isLegacyRegistration) { + return; + } + + await navigator.serviceWorker.register("/sw.js", { + scope: "/", + updateViaCache: "none", + }); + }; + + void migrate().catch(() => { + // Migration is best-effort and must not block the application. + }); + }, []); + + return null; +} diff --git a/scripts/sync-version.mjs b/scripts/sync-version.mjs index 46e55b29..054fb399 100644 --- a/scripts/sync-version.mjs +++ b/scripts/sync-version.mjs @@ -40,20 +40,9 @@ function replaceOrThrow(content, pattern, replacement, label) { function syncFrontend() { const packageFile = join(repoRoot, "frontend", "package.json"); - const serviceWorkerFile = join(repoRoot, "frontend", "public", "sw.js"); const packageJson = JSON.parse(readFileSync(packageFile, "utf8")); packageJson.version = version; writeIfChanged(packageFile, `${JSON.stringify(packageJson, null, 2)}\n`); - - writeIfChanged( - serviceWorkerFile, - replaceOrThrow( - readFileSync(serviceWorkerFile, "utf8"), - /const PWA_ASSET_VERSION = "[^"]+";/u, - `const PWA_ASSET_VERSION = ${JSON.stringify(version)};`, - "frontend service worker PWA asset version", - ), - ); } function syncBackend() {