Mercurial
view mrjunejune/src/sw.js @ 273:e02e2036ef84 default tip
add Layer 2 JRPG component system
Add reusable content and window modals, an isolated component sandbox, shared cyberpunk scroll areas, production-safe cache freshness, and server-rendered JRPG panel state.
Co-authored-by: Copilot <[email protected]>
| author | MrJuneJune <me@mrjunejune.com> |
|---|---|
| date | Sat, 08 Aug 2026 02:08:08 -0700 |
| parents | 056790c4fb0d |
| children |
line wrap: on
line source
// Root-scoped Service Worker for MrJuneJune PWA const CACHE_VERSION = 'v37-network-first'; const CACHE_NAME = `mrjunejune-${CACHE_VERSION}`; const DEVELOPMENT_SANDBOX_PATHS = [ '/public/component-sandbox', '/public/composer-lab', '/public/mjj-composer.', '/public/mjj-modal.', ]; // Files to cache immediately on install const STATIC_CACHE = [ '/', '/offline.html', '/index.js', '/base.css', '/public/design-system/styles/tokens.css', '/public/design-system/styles/reference.css', '/public/design-system/styles/semantic.css', '/public/design-system/styles/density.css', '/public/design-system/styles/themes.css', '/public/design-system/styles/components.css', '/public/design-system/styles/elements.css', '/public/design-system/components/index.js', '/public/design-system/components/alert.js', '/public/design-system/components/button.js', '/public/design-system/components/card.js', '/public/design-system/components/collections.js', '/public/design-system/components/disclosure.js', '/public/design-system/components/field.js', '/public/design-system/components/forms.js', '/public/design-system/components/icon.js', '/public/design-system/components/link.js', '/public/design-system/components/notifications.js', '/public/design-system/components/overlays.js', '/public/design-system/components/primitives.js', '/public/design-system/components/stack.js', '/public/design-system/components/story.js', '/public/epi_all_colors.svg', '/public/fonts/Roboto-Regular.ttf', '/public/fonts/Roboto-Thin.ttf', '/public/fonts/more-sugar.regular.otf', '/public/fonts/more-sugar.thin.otf', ]; // Install event - cache static assets self.addEventListener('install', (event) => { console.log('[SW] Installing service worker...'); event.waitUntil( caches.open(CACHE_NAME).then((cache) => { console.log('[SW] Caching static assets'); return cache.addAll(STATIC_CACHE); }).then(() => { console.log('[SW] Skip waiting'); return self.skipWaiting(); }) ); }); // Activate event - clean up old caches self.addEventListener('activate', (event) => { console.log('[SW] Activating service worker...'); event.waitUntil( caches.keys().then((cacheNames) => { return Promise.all( cacheNames.map((cacheName) => { if (cacheName !== CACHE_NAME) { console.log('[SW] Deleting old cache:', cacheName); return caches.delete(cacheName); } }) ); }).then(() => { console.log('[SW] Claiming clients'); return self.clients.claim(); }) ); }); function isCacheable(response) { return response && response.status === 200 && response.type !== 'error'; } async function updateCache(request, response) { if (!isCacheable(response)) return; const cache = await caches.open(CACHE_NAME); await cache.put(request, response.clone()); } async function networkFirst(request, offlineFallback = false) { try { const response = await fetch(request, { cache: 'no-cache' }); await updateCache(request, response); return response; } catch (error) { const cached = await caches.match(request); if (cached) return cached; if (offlineFallback) { const offline = await caches.match('/offline.html'); if (offline) return offline; } throw error; } } async function staleWhileRevalidate(event, request) { const cached = await caches.match(request); const update = fetch(request, { cache: 'no-cache' }).then(async response => { await updateCache(request, response); return response; }); if (!cached) return update; event.waitUntil(update.catch(() => undefined)); return cached; } // Fetch event - current code first, cached media first, offline fallback. self.addEventListener('fetch', (event) => { const { request } = event; const url = new URL(request.url); // Skip non-GET requests if (request.method !== 'GET') { return; } // Skip chrome-extension and other non-http(s) requests if (!url.protocol.startsWith('http')) { return; } if (url.origin !== self.location.origin) { return; } // Skip API calls and media uploads (always go to network) if (url.pathname.startsWith('/api/')) { return; } // Development servers should always serve current source and assets. if (url.hostname === 'localhost' || url.hostname === '127.0.0.1') { return; } // Component workbenches must reflect every ibazel rebuild on any hostname. if (DEVELOPMENT_SANDBOX_PATHS.some(path => url.pathname.startsWith(path))) { return; } // The HLS tool and media must always reflect the current player version. if (url.pathname.startsWith('/tools/hls_player') || url.pathname.startsWith('/public/hls-sample/')) { return; } const acceptsHtml = request.mode === 'navigate' || request.headers.get('Accept')?.includes('text/html'); const currentCode = /\.(?:css|js|json|wasm)$/i.test(url.pathname); const revalidatedMedia = /\.(?:svg|jpe?g|webp|woff2?|ttf|otf|pdf)$/i.test(url.pathname); if (acceptsHtml || currentCode) { event.respondWith(networkFirst(request, acceptsHtml)); return; } if (revalidatedMedia) { event.respondWith(staleWhileRevalidate(event, request)); } }); // Handle messages from the client self.addEventListener('message', (event) => { if (event.data && event.data.type === 'SKIP_WAITING') { self.skipWaiting(); } if (event.data && event.data.type === 'CLEAR_CACHE') { event.waitUntil( caches.keys().then((cacheNames) => { return Promise.all( cacheNames.map((cacheName) => caches.delete(cacheName)) ); }) ); } });