Prevent rebuilt dashboard tabs from reusing stale cached assets after a deploy. - revalidate service-worker asset requests against the network before falling back to cache and bump the cache version - suppress repeated version-change reloads for the same remote build and cover the new behavior with dashboard tests - update engine heartbeat test expectations for summarized task creation metadata and add the published package changeset Files changed: .changeset/fn-blank-page-service-worker-assets.md | 3 ++ packages/dashboard/app/__tests__/pwa.test.ts | 11 ++++++ packages/dashboard/app/__tests__/versionCheck.test.ts | 28 ++++++++++++++ packages/dashboard/app/public/sw.js | 37 +++++++++++++++++- packages/dashboard/app/versionCheck.ts | 45 ++++++++++++++++++++++ packages/engine/src/__tests__/heartbeat-executor.test.ts | 3 +- packages/engine/src/__tests__/heartbeat-session-prompt.test.ts | 7 +++- 7 files changed, 131 insertions(+), 3 deletions(-) Fusion-Task-Id: FN-6180 Fusion-Task-Lineage: 4f07e5f0-56c7-4241-8719-f576dfd8613e
176 lines
5.2 KiB
JavaScript
176 lines
5.2 KiB
JavaScript
const CACHE_NAME = "fusion-cache-v3";
|
|
const APP_SHELL_URLS = [
|
|
"/",
|
|
"/index.html",
|
|
"/manifest.json",
|
|
"/logo.svg",
|
|
"/icons/icon-192.png",
|
|
"/icons/icon-512.png",
|
|
];
|
|
|
|
self.addEventListener("message", (event) => {
|
|
if (event.data && event.data.type === "SKIP_WAITING") {
|
|
self.skipWaiting();
|
|
}
|
|
});
|
|
|
|
self.addEventListener("install", (event) => {
|
|
event.waitUntil((async () => {
|
|
try {
|
|
const cache = await caches.open(CACHE_NAME);
|
|
await cache.addAll(APP_SHELL_URLS);
|
|
await self.skipWaiting();
|
|
} catch (error) {
|
|
console.warn("[sw] install cache warmup failed", error);
|
|
}
|
|
})());
|
|
});
|
|
|
|
self.addEventListener("activate", (event) => {
|
|
event.waitUntil((async () => {
|
|
try {
|
|
const keys = await caches.keys();
|
|
await Promise.all(
|
|
keys
|
|
.filter((key) => key !== CACHE_NAME)
|
|
.map((key) => caches.delete(key)),
|
|
);
|
|
await self.clients.claim();
|
|
} catch (error) {
|
|
console.warn("[sw] activate cleanup failed", error);
|
|
}
|
|
})());
|
|
});
|
|
|
|
self.addEventListener("fetch", (event) => {
|
|
const request = event.request;
|
|
|
|
if (request.method !== "GET") {
|
|
return;
|
|
}
|
|
|
|
const url = new URL(request.url);
|
|
const accept = request.headers.get("accept") ?? "";
|
|
const isApiRequest = url.pathname.startsWith("/api/");
|
|
const isEventStreamRequest =
|
|
accept.includes("text/event-stream") ||
|
|
url.pathname === "/api/events" ||
|
|
url.pathname.startsWith("/api/events/");
|
|
const isNavigationRequest =
|
|
request.mode === "navigate" ||
|
|
request.destination === "document" ||
|
|
url.pathname === "/" ||
|
|
url.pathname === "/index.html";
|
|
const isBuiltAssetRequest =
|
|
url.pathname.startsWith("/assets/") ||
|
|
request.destination === "script" ||
|
|
request.destination === "style" ||
|
|
request.destination === "font";
|
|
|
|
// EventSource requests stay open indefinitely. Waiting on cache.put() for an
|
|
// infinite response body prevents the browser from ever receiving the stream
|
|
// and leaks the underlying connection across reloads. Let SSE bypass the
|
|
// service worker entirely so the browser talks to the network directly.
|
|
if (isEventStreamRequest) {
|
|
return;
|
|
}
|
|
|
|
// Always revalidate the HTML shell so navigation picks up the latest hashed
|
|
// asset names instead of getting stuck on a cached index.html that points at
|
|
// a stale bundle.
|
|
if (isNavigationRequest) {
|
|
event.respondWith((async () => {
|
|
try {
|
|
const networkResponse = await fetch(request);
|
|
try {
|
|
const cache = await caches.open(CACHE_NAME);
|
|
await cache.put(request, networkResponse.clone());
|
|
} catch (cacheError) {
|
|
console.warn("[sw] navigation cache put failed", cacheError);
|
|
}
|
|
return networkResponse;
|
|
} catch (networkError) {
|
|
const fallback = await caches.match(request);
|
|
if (fallback) {
|
|
return fallback;
|
|
}
|
|
throw networkError;
|
|
}
|
|
})());
|
|
return;
|
|
}
|
|
|
|
if (isApiRequest) {
|
|
event.respondWith((async () => {
|
|
try {
|
|
const networkResponse = await fetch(request);
|
|
try {
|
|
const cache = await caches.open(CACHE_NAME);
|
|
await cache.put(request, networkResponse.clone());
|
|
} catch (cacheError) {
|
|
console.warn("[sw] api cache put failed", cacheError);
|
|
}
|
|
return networkResponse;
|
|
} catch (networkError) {
|
|
try {
|
|
const cachedResponse = await caches.match(request);
|
|
if (cachedResponse) {
|
|
return cachedResponse;
|
|
}
|
|
} catch (cacheError) {
|
|
console.warn("[sw] api cache lookup failed", cacheError);
|
|
}
|
|
throw networkError;
|
|
}
|
|
})());
|
|
return;
|
|
}
|
|
|
|
// Built assets are content-hashed, but an already-controlled browser can
|
|
// keep old entries in this named cache across local rebuilds. Prefer the
|
|
// server response so tabs cannot stay on stale JS/CSS and render a blank
|
|
// shell after an update. The cache remains an offline fallback.
|
|
if (isBuiltAssetRequest) {
|
|
event.respondWith((async () => {
|
|
try {
|
|
const networkResponse = await fetch(request);
|
|
try {
|
|
const cache = await caches.open(CACHE_NAME);
|
|
await cache.put(request, networkResponse.clone());
|
|
} catch (cacheError) {
|
|
console.warn("[sw] asset cache put failed", cacheError);
|
|
}
|
|
return networkResponse;
|
|
} catch (networkError) {
|
|
try {
|
|
const cachedResponse = await caches.match(request);
|
|
if (cachedResponse) {
|
|
return cachedResponse;
|
|
}
|
|
} catch (cacheError) {
|
|
console.warn("[sw] asset cache lookup failed", cacheError);
|
|
}
|
|
throw networkError;
|
|
}
|
|
})());
|
|
return;
|
|
}
|
|
|
|
event.respondWith((async () => {
|
|
try {
|
|
const cache = await caches.open(CACHE_NAME);
|
|
const cachedResponse = await cache.match(request);
|
|
if (cachedResponse) {
|
|
return cachedResponse;
|
|
}
|
|
|
|
const networkResponse = await fetch(request);
|
|
await cache.put(request, networkResponse.clone());
|
|
return networkResponse;
|
|
} catch (error) {
|
|
console.warn("[sw] static cache flow failed", error);
|
|
return fetch(request);
|
|
}
|
|
})());
|
|
});
|