Files
LibreChat/client/sw/heal.js
Danny Avila 788cc5ac07 🛟 fix: Auto-Recover from Stale Service Worker Assets After Deploys (#13686)
* 🛟 fix: Auto-Recover from Stale Service Worker Assets After Deploys

- 404 missing static assets in the SPA fallback instead of serving index.html
- inline recovery script unregisters stale SWs and reloads once on chunk failure
- route vite:preloadError into the same recovery path for stale lazy chunks

* 🛟 fix: Address Review — SW-Side Recovery, Scoped Unregister, Shared Fallback

- importScripts'd sw-heal.js pings window clients on activation and reloads
  ones that can't pong: stale pages carry no recovery code of their own
- scope SW unregistration to the deployment base for subpath installs
- preventDefault vite:preloadError only when a recovery reload was initiated
- extract createSpaFallback and apply the asset 404 guard to experimental.js
2026-06-11 11:57:06 -04:00

61 lines
1.7 KiB
JavaScript

/* Runs inside the generated service worker via workbox `importScripts`.
* When a new build's worker activates, pages served from a previous build
* can no longer load their hashed chunks (the old precache is purged) and
* carry no recovery code of their own — the worker is the only code path
* stale clients fetch fresh. Ping every window client; reload the ones
* that cannot answer. */
const PING_TYPE = 'LC_SW_PING';
const PONG_TYPE = 'LC_SW_PONG';
const PONG_TIMEOUT_MS = 1500;
const pendingPongs = new Map();
self.addEventListener('message', (event) => {
if (!event.data || event.data.type !== PONG_TYPE || !event.source) {
return;
}
const resolvePong = pendingPongs.get(event.source.id);
if (resolvePong) {
pendingPongs.delete(event.source.id);
resolvePong(true);
}
});
function pingClient(client) {
return new Promise((resolve) => {
pendingPongs.set(client.id, resolve);
setTimeout(() => {
if (pendingPongs.delete(client.id)) {
resolve(false);
}
}, PONG_TIMEOUT_MS);
client.postMessage({ type: PING_TYPE });
});
}
async function reloadUnresponsiveClients() {
await self.clients.claim();
const windowClients = await self.clients.matchAll({
type: 'window',
includeUncontrolled: true,
});
const topLevelClients = windowClients.filter((client) => client.frameType !== 'nested');
await Promise.all(
topLevelClients.map(async (client) => {
const responsive = await pingClient(client);
if (responsive) {
return;
}
try {
await client.navigate(client.url);
} catch {
/* client closed or no longer controllable */
}
}),
);
}
self.addEventListener('activate', (event) => {
event.waitUntil(reloadUnresponsiveClients());
});