sw.js 3.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899
  1. // PackIt service worker.
  2. //
  3. // A packing list is read where there is no signal, so anything already visited
  4. // has to keep working offline. Navigations are network-first (the list must
  5. // reflect the latest admin changes when we can reach the server) with the
  6. // cached copy as the fallback; static assets are cache-first.
  7. //
  8. // Bump CACHE_VERSION to retire every previously cached response.
  9. const CACHE_VERSION = 'packit-v1';
  10. const OFFLINE_URL = '/offline.html';
  11. // Small and always needed. index.php is added opportunistically because it
  12. // depends on the database and must not be allowed to fail the install.
  13. const PRECACHE = [
  14. OFFLINE_URL,
  15. '/manifest.webmanifest',
  16. '/img/icon-192.png',
  17. '/img/icon-512.png',
  18. ];
  19. self.addEventListener('install', (event) => {
  20. event.waitUntil((async () => {
  21. const cache = await caches.open(CACHE_VERSION);
  22. await cache.addAll(PRECACHE);
  23. await cache.add('/index.php').catch(() => {});
  24. self.skipWaiting();
  25. })());
  26. });
  27. self.addEventListener('activate', (event) => {
  28. event.waitUntil((async () => {
  29. const keys = await caches.keys();
  30. await Promise.all(keys.filter((k) => k !== CACHE_VERSION).map((k) => caches.delete(k)));
  31. await self.clients.claim();
  32. })());
  33. });
  34. async function networkFirst(request) {
  35. const cache = await caches.open(CACHE_VERSION);
  36. try {
  37. const response = await fetch(request);
  38. // A redirected response (list.php -> packing.php) cannot be replayed from
  39. // the cache for a navigation, so only store final ones.
  40. if (response.ok && !response.redirected) {
  41. cache.put(request, response.clone());
  42. }
  43. return response;
  44. } catch (err) {
  45. const cached = await cache.match(request);
  46. return cached || (await cache.match(OFFLINE_URL)) || Response.error();
  47. }
  48. }
  49. async function cacheFirst(request) {
  50. const cache = await caches.open(CACHE_VERSION);
  51. const cached = await cache.match(request);
  52. if (cached) return cached;
  53. const response = await fetch(request);
  54. if (response.ok || response.type === 'opaque') {
  55. cache.put(request, response.clone());
  56. }
  57. return response;
  58. }
  59. async function staleWhileRevalidate(request) {
  60. const cache = await caches.open(CACHE_VERSION);
  61. const cached = await cache.match(request);
  62. const network = fetch(request).then((response) => {
  63. if (response.ok || response.type === 'opaque') cache.put(request, response.clone());
  64. return response;
  65. }).catch(() => cached);
  66. return cached || network;
  67. }
  68. self.addEventListener('fetch', (event) => {
  69. const request = event.request;
  70. if (request.method !== 'GET') return;
  71. const url = new URL(request.url);
  72. // Fonts live on Google's CDN; keep them usable offline.
  73. if (url.origin === 'https://fonts.googleapis.com' || url.origin === 'https://fonts.gstatic.com') {
  74. event.respondWith(staleWhileRevalidate(request));
  75. return;
  76. }
  77. if (url.origin !== self.location.origin) return;
  78. // The admin panel is session-protected and always changing – never cache it,
  79. // and never serve a stale page to someone editing content.
  80. if (url.pathname.startsWith('/admin/')) return;
  81. if (request.mode === 'navigate') {
  82. event.respondWith(networkFirst(request));
  83. return;
  84. }
  85. event.respondWith(cacheFirst(request));
  86. });