/* eslint-disable */ /** * Der Dienstarbeiter der Lern-Bar. * * Er hat genau eine Aufgabe: Der Azubi soll im Serverraum, im Keller oder im * Zug weiterlernen können, wo kein Netz ist. Alles andere wäre Ballast. * * ─── Was offline liegen darf ────────────────────────────────────────────── * * Nur das Lernmaterial: Handouts, Merkkarten, Glossar und die eigene Woche. * * **Nicht** zwischengespeichert werden Lernzielkontrollen, Berichtsheft, * Bewertungen, Feedbackgespräche, Anhänge und jedes PDF. Der Grund ist keine * Sparsamkeit, sondern eine Zusage aus dem Pflichtenheft: Musterlösungen und * Bewertungsdaten stehen hinter einer eigenen Prüfung. Was im Browsercache * liegt, ist an dieser Prüfung vorbei – also liegt es dort nicht. * * ─── Wie zwischengespeichert wird ───────────────────────────────────────── * * Seiten: erst das Netz, dann der Speicher. So sieht man nach einer * Aktualisierung sofort die neue Fassung und nur bei fehlendem Netz die alte. * * Dateien unter /_next/static/: erst der Speicher. Die Namen tragen eine * Prüfsumme, ihr Inhalt ändert sich nie. * * Alles andere – Serveraktionen, Anmeldung, POST – geht immer ans Netz. */ const FASSUNG = 'v1'; const SCHALE = `lernbar-schale-${FASSUNG}`; const SEITEN = `lernbar-seiten-${FASSUNG}`; const DATEIEN = `lernbar-dateien-${FASSUNG}`; /** Was schon beim Einrichten vorliegen muss. */ const GRUNDVORRAT = ['/offline', '/symbol-192.png', '/lernbar.svg', '/manifest.webmanifest']; /** Höchstens so viele Seiten liegen im Speicher – ein Handy ist kein Archiv. */ const HOECHSTZAHL_SEITEN = 60; /** Nur diese Pfade dürfen offline vorliegen. Alles andere niemals. */ function darfOfflineLiegen(pfad) { if (pfad === '/' || pfad === '/offline' || pfad === '/glossar') return true; if (pfad === '/handouts' || pfad.startsWith('/handouts/')) return true; if (pfad === '/karten' || pfad.startsWith('/karten/')) return true; if (/^\/woche\/\d+$/.test(pfad)) return true; return false; } self.addEventListener('install', (ereignis) => { ereignis.waitUntil( caches .open(SCHALE) // Einzeln, damit ein fehlendes Stück nicht die ganze Einrichtung kippt. .then((speicher) => Promise.all(GRUNDVORRAT.map((pfad) => speicher.add(pfad).catch(() => undefined)))) .then(() => self.skipWaiting()), ); }); self.addEventListener('activate', (ereignis) => { ereignis.waitUntil( caches .keys() .then((namen) => Promise.all( namen .filter((name) => name.startsWith('lernbar-') && ![SCHALE, SEITEN, DATEIEN].includes(name)) .map((name) => caches.delete(name)), ), ) .then(() => self.clients.claim()), ); }); /** * Beim Abmelden wird alles Persönliche weggeräumt. * * Ein geteiltes Werkstattgerät soll nach dem Abmelden nicht noch die Woche des * vorigen Azubis im Speicher haben. */ self.addEventListener('message', (ereignis) => { if (ereignis.data && ereignis.data.typ === 'aufraeumen') { ereignis.waitUntil(Promise.all([caches.delete(SEITEN), caches.delete(DATEIEN)])); } }); async function speicherKuerzen(name, hoechstzahl) { const speicher = await caches.open(name); const schluessel = await speicher.keys(); // Die ältesten zuerst – `keys()` liefert sie in der Reihenfolge des Ablegens. for (const alt of schluessel.slice(0, Math.max(0, schluessel.length - hoechstzahl))) { await speicher.delete(alt); } } /** Seiten: erst das Netz, bei Ausfall der Speicher, sonst die Offlineseite. */ async function seiteHolen(anfrage) { const pfad = new URL(anfrage.url).pathname; try { const antwort = await fetch(anfrage); if (antwort.ok && darfOfflineLiegen(pfad)) { const speicher = await caches.open(SEITEN); await speicher.put(anfrage, antwort.clone()); await speicherKuerzen(SEITEN, HOECHSTZAHL_SEITEN); } return antwort; } catch (fehler) { const gespeichert = await caches.match(anfrage, { ignoreSearch: false }); if (gespeichert) return gespeichert; const offline = await caches.match('/offline'); if (offline) return offline; return new Response('Kein Netz und nichts im Speicher.', { status: 503, headers: { 'Content-Type': 'text/plain; charset=utf-8' }, }); } } /** Unveränderliche Dateien: erst der Speicher. */ async function dateiHolen(anfrage) { const gespeichert = await caches.match(anfrage); if (gespeichert) return gespeichert; const antwort = await fetch(anfrage); if (antwort.ok) { const speicher = await caches.open(DATEIEN); await speicher.put(anfrage, antwort.clone()); } return antwort; } self.addEventListener('fetch', (ereignis) => { const anfrage = ereignis.request; // Nur eigene Adressen, nur Lesezugriffe. if (anfrage.method !== 'GET') return; const adresse = new URL(anfrage.url); if (adresse.origin !== self.location.origin) return; // Serveraktionen und Datenschnittstellen gehen immer ans Netz – eine // zwischengespeicherte Antwort wäre hier schlicht falsch. if (adresse.pathname.startsWith('/api/') || anfrage.headers.has('Next-Action')) return; // Auch die Teilnachladungen von Next (RSC) nicht: Sie gehören zu einer // Sitzung und sollen nicht als Seite verwechselt werden. if (adresse.searchParams.has('_rsc')) return; if (anfrage.mode === 'navigate') { ereignis.respondWith(seiteHolen(anfrage)); return; } if (adresse.pathname.startsWith('/_next/static/') || adresse.pathname.endsWith('.svg') || adresse.pathname.endsWith('.png')) { ereignis.respondWith(dateiHolen(anfrage)); } });