Skip to content
Merged
Show file tree
Hide file tree
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
1 change: 0 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
4 changes: 2 additions & 2 deletions frontend/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -81,7 +81,7 @@ export default function RootLayout({
<AppearancePreferencesProvider>
{children}
<AppVersionGuard />
<PWAServiceWorkerRegister />
<LegacyPWAServiceWorkerMigration />
<Toaster />
<DevtoolsBrandBanner />
</AppearancePreferencesProvider>
Expand Down
133 changes: 6 additions & 127 deletions frontend/public/sw.js
Original file line number Diff line number Diff line change
@@ -1,140 +1,19 @@
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) => {
event.waitUntil(
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)));
}
16 changes: 0 additions & 16 deletions frontend/scripts/sync-pwa-assets.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
37 changes: 0 additions & 37 deletions frontend/shared/components/pwa-service-worker-register.tsx

This file was deleted.

1 change: 0 additions & 1 deletion frontend/shared/generated/pwa-assets.ts
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
42 changes: 42 additions & 0 deletions frontend/shared/pwa/migrations/legacy-service-worker-migration.tsx
Original file line number Diff line number Diff line change
@@ -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;
}
11 changes: 0 additions & 11 deletions scripts/sync-version.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
Loading