/* Service Worker des GTIN-Generators. * * Nach dem ersten Laden liegen HTML, CSS, JavaScript, Manifest und Icons im * Cache. Danach funktioniert die Anwendung ohne WLAN, ohne Mobilfunk und ohne * erreichbaren Server. * * CACHE_VERSION wird beim Build aus dem Inhalt der Anwendungsdateien ersetzt. * Dadurch erkennt der Browser ein neues Release und raeumt den alten Cache auf. */ const sw = /** @type {ServiceWorkerGlobalScope} */ (/** @type {unknown} */ (self)); const CACHE_VERSION = '76462b4916fd'; const CACHE_PREFIX = 'desfoto-gtin-generator-'; const CACHE_NAME = `${CACHE_PREFIX}${CACHE_VERSION}`; // PRECACHE const PRECACHE = [ './', './css/app.css', './js/app.js', './js/gtin.js', './js/barcode.js', './js/liste.js', './manifest.webmanifest', './icons/favicon.svg', './icons/icon-192.png', './icons/icon-512.png', './icons/icon-maskable-512.png', ]; // END PRECACHE /** @param {string} pfad */ function scopeUrl(pfad) { return new URL(pfad, sw.registration.scope).toString(); } const INDEX_URL = scopeUrl('./'); /** Pfad des Geltungsbereichs, z. B. "/gtin/". */ const SCOPE_PFAD = new URL(sw.registration.scope).pathname; /** * Kennung der ausgelieferten Anwendungsquelle. Nur eine Antwort, die diese * Kennung enthaelt, ist die echte index.html dieses Projekts. */ const SHELL_KENNUNG = 'id="artikelnummer"'; /** * Entscheidet, ob eine Antwort die vorgehaltene Anwendungshuelle ersetzen darf. * * Nur eine erfolgreiche HTML-Antwort der eigenen Anwendung ist zulaessig. Eine * Anmeldeseite der Steuerungsebene, eine Magic-Link-Seite oder eine Fehlerseite * (z. B. "App deployment in progress") wuerde die Anwendung sonst offline * unbrauchbar machen, weil sie die echte Seite aus dem Cache verdraengt. * * @param {Response} antwort * @returns {Promise} */ async function istAnwendungshuelle(antwort) { if (!antwort.ok || antwort.type !== 'basic' || antwort.redirected) return false; const inhaltstyp = antwort.headers.get('content-type') ?? ''; if (!inhaltstyp.includes('text/html')) return false; const endgueltig = new URL(antwort.url); if (endgueltig.origin !== sw.location.origin) return false; if (!endgueltig.pathname.startsWith(SCOPE_PFAD)) return false; try { return (await antwort.text()).includes(SHELL_KENNUNG); } catch { return false; } } sw.addEventListener('install', (ereignis) => { ereignis.waitUntil( caches .open(CACHE_NAME) .then((cache) => cache.addAll( PRECACHE.map((pfad) => new Request(scopeUrl(pfad), { cache: 'reload' })), ), ) .then(() => sw.skipWaiting()), ); }); sw.addEventListener('activate', (ereignis) => { ereignis.waitUntil( caches .keys() .then((namen) => Promise.all( namen .filter((name) => name.startsWith(CACHE_PREFIX) && name !== CACHE_NAME) .map((name) => caches.delete(name)), ), ) .then(() => sw.clients.claim()), ); }); /** * Bedient einen Navigationsaufruf. * * Bewusst aus dem Cache: Nur so stammen HTML und Assets immer aus derselben * Cache-Generation. Der neue Service Worker uebernimmt dank * skipWaiting()/clients.claim() sofort; ein neues Release ist damit beim * naechsten Laden konsistent aktiv (kein "neues HTML, altes JavaScript"). * * @param {FetchEvent} ereignis * @param {Request} anfrage * @returns {Promise} */ function bedieneNavigation(ereignis, anfrage) { return caches.match(INDEX_URL).then((treffer) => { if (treffer) return treffer; return fetch(anfrage) .then((antwort) => { const pruefung = antwort.clone(); const kopie = antwort.clone(); // waitUntil haelt die Cache-Schreiboperation am Leben. Ein blosses // "fire and forget" innerhalb von respondWith kann abgebrochen werden. ereignis.waitUntil( istAnwendungshuelle(pruefung) .then((istHuelle) => istHuelle ? caches.open(CACHE_NAME).then((cache) => cache.put(INDEX_URL, kopie)) : undefined, ) .catch(() => undefined), ); return antwort; }) .catch(() => caches.match(INDEX_URL).then((treffer) => treffer ?? Response.error())); }); } sw.addEventListener('fetch', (ereignis) => { const anfrage = ereignis.request; if (anfrage.method !== 'GET') return; const ziel = new URL(anfrage.url); if (ziel.origin !== sw.location.origin) return; if (!ziel.pathname.startsWith(SCOPE_PFAD)) return; if (ziel.pathname.endsWith('/health')) return; if (anfrage.mode === 'navigate') { const appStart = new URL('./', sw.registration.scope).pathname; if (ziel.pathname !== appStart) return; ereignis.respondWith(bedieneNavigation(ereignis, anfrage)); return; } ereignis.respondWith( caches.match(anfrage).then((treffer) => { if (treffer) return treffer; return fetch(anfrage).then((antwort) => { if (antwort.ok && antwort.type === 'basic' && !antwort.redirected) { const kopie = antwort.clone(); ereignis.waitUntil( caches .open(CACHE_NAME) .then((cache) => cache.put(anfrage, kopie)) .catch(() => undefined), ); } return antwort; }); }), ); });