174 lines
5.3 KiB
JavaScript
174 lines
5.3 KiB
JavaScript
/* 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 = '15d23ab0263e';
|
|
const CACHE_PREFIX = 'desfoto-gtin-generator-';
|
|
const CACHE_NAME = `${CACHE_PREFIX}${CACHE_VERSION}`;
|
|
|
|
// PRECACHE
|
|
const PRECACHE = [
|
|
'./',
|
|
'./index.html',
|
|
'./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('./index.html');
|
|
|
|
/** 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<boolean>}
|
|
*/
|
|
async function istAnwendungshuelle(antwort) {
|
|
if (!antwort.ok || antwort.type !== 'basic') 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<Response>}
|
|
*/
|
|
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;
|
|
const appIndex = new URL('./index.html', sw.registration.scope).pathname;
|
|
if (ziel.pathname !== appStart && ziel.pathname !== appIndex) 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') {
|
|
const kopie = antwort.clone();
|
|
ereignis.waitUntil(
|
|
caches
|
|
.open(CACHE_NAME)
|
|
.then((cache) => cache.put(anfrage, kopie))
|
|
.catch(() => undefined),
|
|
);
|
|
}
|
|
return antwort;
|
|
});
|
|
}),
|
|
);
|
|
});
|