service-worker/service-worker.jsjavascript
/* eslint no-console: 0 */
import "./matomo-offline";
import { clientsClaim } from "workbox-core";
import { registerRoute } from "workbox-routing";
import { StaleWhileRevalidate } from "workbox-strategies";
import { precacheAndRoute } from "workbox-precaching";
// update immediately when a new version is available
self.skipWaiting();
clientsClaim();
precacheAndRoute(self.__WB_MANIFEST || []);
const CACHE_NAME = "dynamic-assets";
// preload assets to ensure they are already cached on first load
// not necessary, but helps ensure especially large videos are cached and available immediately
// make sure to add a "." before the path to ensure it works with different base paths
const PRELOAD_ASSETS = [];
// add matomo offline tracking
self.matomoAnalytics.initialize({ queueLimit: 100 });
self.addEventListener("install", (event) => {
event.waitUntil(
caches.open(CACHE_NAME).then(async (cache) => {
console.log("[SW] Preloading assets:", PRELOAD_ASSETS);
try {
await Promise.all(
PRELOAD_ASSETS.map(async (url) => {
try {
const response = await fetch(url);
if (response.status === 200) {
await cache.put(url, response);
}
if (response.status === 206) {
console.error(
`[SW] Preloaded ${url} with partial content (206). Asset will not be cached. Forgot to use CacheableVideo component?`
);
}
} catch (err) {
console.error(`[SW] Failed to preload asset ${url}:`, err);
console.error(
"[SW] Make sure the asset is available and the URL is correct. Caching is necessary to ensure your exhibition app works offline"
);
}
})
);
} catch (err) {
console.error("[SW] Error preloading assets:", err);
}
})
);
});
// Runtime caching for assets that should be changeable without a build step
// (fonts included as a fallback for anything the precache globs missed)
registerRoute(
({ url }) => url.pathname.match(/\.(json|jpg|jpeg|png|webp|svg|mp4|webm|woff2?)$/),
new StaleWhileRevalidate({
cacheName: CACHE_NAME,
cacheableResponse: { statuses: [200] },
plugins: [
{
requestWillFetch: async ({ request }) => {
const newHeaders = new Headers(request.headers);
// If the request is already cached, add If-None-Match or If-Modified-Since headers
// to avoid unnecessary full fetches and to allow the server to respond with 304 Not Modified
// This is important for videos to ensure we don't always fetch the full video file
// and instead rely on the cache when possible
const cache = await caches.open(CACHE_NAME);
const cachedResponse = await cache.match(request);
if (cachedResponse) {
// let server know we have a cached version
// so it can respond with 304 Not Modified if the asset hasn't changed
const etag = cachedResponse.headers.get("ETag");
const lastModified = cachedResponse.headers.get("Last-Modified");
if (etag) {
newHeaders.set("If-None-Match", etag);
} else if (lastModified) {
newHeaders.set("If-Modified-Since", lastModified);
}
}
return new Request(request.url, {
headers: newHeaders,
method: request.method,
mode: request.mode,
credentials: request.credentials,
cache: request.cache,
redirect: request.redirect,
referrer: request.referrer,
integrity: request.integrity,
});
},
cacheWillUpdate: async ({ request, response }) => {
if (!response || !response.ok) {
console.warn(
`[SW] Asset fetch failed for ${request.url}, keeping old cached version if there is any.`
);
return null;
}
if (response.status === 206) {
console.error(
`[SW] Partial response (206) detected for ${request.url}, skipping cache update. Forgot to use CacheableVideo component?`
);
return null;
}
if (response.status === 304) {
return null; // No update needed, keep old cache
}
return response;
},
},
],
})
);