gateway: prevent stale web UI assets on localhost
This commit is contained in:
@@ -295,7 +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');
|
||||
expect(res.headers.get('cache-control')).toBe('no-store');
|
||||
const body = await res.text();
|
||||
expect(body).toContain('Flynn');
|
||||
});
|
||||
@@ -307,6 +307,7 @@ describe('GatewayServer integration', () => {
|
||||
const res = await fetch(`http://127.0.0.1:${TEST_PORT}/style.css`);
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.headers.get('content-type')).toBe('text/css');
|
||||
expect(res.headers.get('cache-control')).toBe('no-store');
|
||||
});
|
||||
|
||||
it('serves sw.js with no-store cache policy', async () => {
|
||||
|
||||
+22
-4
@@ -16,6 +16,25 @@ const CONTENT_TYPES: Record<string, string> = {
|
||||
'.woff2': 'font/woff2',
|
||||
};
|
||||
|
||||
function resolveCacheControl(pathname: string, ext: string): string | null {
|
||||
if (pathname === '/sw.js') {
|
||||
// Always fetch the latest worker script so clients can update immediately.
|
||||
return 'no-store';
|
||||
}
|
||||
|
||||
if (pathname === '/index.html' || pathname === '/chat.html') {
|
||||
// Prevent stale shell HTML from pinning old asset references.
|
||||
return 'no-store';
|
||||
}
|
||||
|
||||
if (ext === '.js' || ext === '.mjs' || ext === '.css') {
|
||||
// Dashboard/frontend code changes frequently in local deployments.
|
||||
return 'no-store';
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Serve static files from a directory.
|
||||
*
|
||||
@@ -72,10 +91,9 @@ export async function serveStatic(
|
||||
try {
|
||||
const content = await readFile(filePath);
|
||||
const headers: Record<string, string> = { 'Content-Type': contentType };
|
||||
if (pathname === '/sw.js') {
|
||||
headers['Cache-Control'] = 'no-store';
|
||||
} else if (pathname === '/index.html') {
|
||||
headers['Cache-Control'] = 'no-cache';
|
||||
const cacheControl = resolveCacheControl(pathname, ext);
|
||||
if (cacheControl) {
|
||||
headers['Cache-Control'] = cacheControl;
|
||||
}
|
||||
res.writeHead(200, headers);
|
||||
res.end(content);
|
||||
|
||||
@@ -13,6 +13,27 @@ function withToken(path) {
|
||||
return `${path}${separator}token=${encodeURIComponent(token)}`;
|
||||
}
|
||||
|
||||
function isLocalhostHost(hostname) {
|
||||
return hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '::1';
|
||||
}
|
||||
|
||||
async function unregisterAllServiceWorkers() {
|
||||
if (!('serviceWorker' in navigator)) {
|
||||
return;
|
||||
}
|
||||
const registrations = await navigator.serviceWorker.getRegistrations();
|
||||
await Promise.all(registrations.map((registration) => registration.unregister()));
|
||||
}
|
||||
|
||||
async function clearFlynnUiCaches() {
|
||||
if (!('caches' in globalThis)) {
|
||||
return;
|
||||
}
|
||||
const cacheKeys = await caches.keys();
|
||||
const targets = cacheKeys.filter((key) => key.startsWith('flynn-webchat-'));
|
||||
await Promise.all(targets.map((key) => caches.delete(key)));
|
||||
}
|
||||
|
||||
async function requestJson(path, options) {
|
||||
const response = await fetch(withToken(path), {
|
||||
...(options ?? {}),
|
||||
@@ -60,6 +81,12 @@ export async function registerPwaServiceWorker() {
|
||||
if (!('serviceWorker' in navigator)) {
|
||||
return null;
|
||||
}
|
||||
if (isLocalhostHost(window.location.hostname)) {
|
||||
// Local dev/runtime should always reflect latest dashboard assets.
|
||||
await unregisterAllServiceWorkers();
|
||||
await clearFlynnUiCaches();
|
||||
return null;
|
||||
}
|
||||
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);
|
||||
|
||||
Reference in New Issue
Block a user