From 805bbaead993beaf4670586d98fac8aec6939171 Mon Sep 17 00:00:00 2001 From: William Valentin Date: Wed, 18 Feb 2026 12:22:01 -0800 Subject: [PATCH] fix(gateway-ui): refresh service worker and static cache headers --- src/gateway/server.test.ts | 11 +++++++++++ src/gateway/static.ts | 8 +++++++- src/gateway/ui/lib/pwa.js | 5 ++++- src/gateway/ui/sw.js | 39 +++++++++++++++++++++++++++++--------- 4 files changed, 52 insertions(+), 11 deletions(-) diff --git a/src/gateway/server.test.ts b/src/gateway/server.test.ts index fd00408..d37a885 100644 --- a/src/gateway/server.test.ts +++ b/src/gateway/server.test.ts @@ -295,6 +295,7 @@ describe('GatewayServer integration', () => { const res = await fetch(`http://127.0.0.1:${TEST_PORT}/`); expect(res.status).toBe(200); expect(res.headers.get('content-type')).toBe('text/html'); + expect(res.headers.get('cache-control')).toBe('no-cache'); const body = await res.text(); expect(body).toContain('Flynn'); }); @@ -308,6 +309,16 @@ describe('GatewayServer integration', () => { expect(res.headers.get('content-type')).toBe('text/css'); }); + it('serves sw.js with no-store cache policy', async () => { + if (!LISTEN_ALLOWED) { + return; + } + const res = await fetch(`http://127.0.0.1:${TEST_PORT}/sw.js`); + expect(res.status).toBe(200); + expect(res.headers.get('content-type')).toBe('application/javascript'); + expect(res.headers.get('cache-control')).toBe('no-store'); + }); + it('returns 404 for unknown HTTP path', async () => { if (!LISTEN_ALLOWED) { return; diff --git a/src/gateway/static.ts b/src/gateway/static.ts index 4c91058..2bd0b4d 100644 --- a/src/gateway/static.ts +++ b/src/gateway/static.ts @@ -71,7 +71,13 @@ export async function serveStatic( // Try to read and serve the file try { const content = await readFile(filePath); - res.writeHead(200, { 'Content-Type': contentType }); + const headers: Record = { 'Content-Type': contentType }; + if (pathname === '/sw.js') { + headers['Cache-Control'] = 'no-store'; + } else if (pathname === '/index.html') { + headers['Cache-Control'] = 'no-cache'; + } + res.writeHead(200, headers); res.end(content); return true; } catch { diff --git a/src/gateway/ui/lib/pwa.js b/src/gateway/ui/lib/pwa.js index c3309a1..9f4197d 100644 --- a/src/gateway/ui/lib/pwa.js +++ b/src/gateway/ui/lib/pwa.js @@ -60,7 +60,10 @@ export async function registerPwaServiceWorker() { if (!('serviceWorker' in navigator)) { return null; } - return navigator.serviceWorker.register(withToken('/sw.js')); + const registration = await navigator.serviceWorker.register(withToken('/sw.js')); + // Ask the browser to check for a newer worker on app load. + void registration.update().catch(() => undefined); + return registration; } export async function getPushStatus() { diff --git a/src/gateway/ui/sw.js b/src/gateway/ui/sw.js index 542807e..e9c9dfe 100644 --- a/src/gateway/ui/sw.js +++ b/src/gateway/ui/sw.js @@ -1,4 +1,4 @@ -const CACHE_NAME = 'flynn-webchat-v1'; +const CACHE_NAME = 'flynn-webchat-v2'; const token = new URL(self.location.href).searchParams.get('token'); const withToken = (path) => { if (!token) { @@ -34,17 +34,38 @@ self.addEventListener('fetch', (event) => { return; } - event.respondWith((async () => { - const cached = await caches.match(event.request, { ignoreSearch: true }); - if (cached) { - return cached; - } + const requestUrl = new URL(event.request.url); + const isNavigation = event.request.mode === 'navigate'; + const isStaticAsset = requestUrl.origin === self.location.origin + && (requestUrl.pathname.startsWith('/lib/') + || requestUrl.pathname.startsWith('/pages/') + || requestUrl.pathname === '/app.js' + || requestUrl.pathname === '/style.css' + || requestUrl.pathname === '/index.html' + || requestUrl.pathname === '/manifest.webmanifest'); + event.respondWith((async () => { try { - return await fetch(event.request); + const networkResponse = await fetch(event.request); + if (isNavigation || isStaticAsset) { + const cache = await caches.open(CACHE_NAME); + await cache.put(event.request, networkResponse.clone()); + } + return networkResponse; } catch { - const fallback = await caches.match('/index.html', { ignoreSearch: true }); - return fallback || new Response('Offline', { status: 503 }); + const cached = await caches.match(event.request, { ignoreSearch: true }); + if (cached) { + return cached; + } + + if (isNavigation) { + const fallback = await caches.match('/index.html', { ignoreSearch: true }); + if (fallback) { + return fallback; + } + } + + return new Response('Offline', { status: 503 }); } })()); });