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;
        },
      },
    ],
  })
);