diff 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 diff
--- a/mrjunejune/src/sw.js	Fri Aug 07 16:05:29 2026 -0700
+++ b/mrjunejune/src/sw.js	Sat Aug 08 02:08:08 2026 -0700
@@ -1,6 +1,12 @@
 // Root-scoped Service Worker for MrJuneJune PWA
-const CACHE_VERSION = 'v35-role-greeting';
+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 = [
@@ -73,7 +79,46 @@
   );
 });
 
-// Fetch event - serve from cache, fallback to network
+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);
@@ -88,6 +133,10 @@
     return;
   }
 
+  if (url.origin !== self.location.origin) {
+    return;
+  }
+
   // Skip API calls and media uploads (always go to network)
   if (url.pathname.startsWith('/api/')) {
     return;
@@ -98,60 +147,32 @@
     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;
   }
 
-  event.respondWith(
-    caches.match(request).then((cachedResponse) => {
-      if (cachedResponse) {
-        console.log('[SW] Serving from cache:', url.pathname);
-        return cachedResponse;
-      }
-
-      // Not in cache, fetch from network
-      return fetch(request).then((networkResponse) => {
-        // Only cache successful responses
-        if (!networkResponse || networkResponse.status !== 200 || networkResponse.type === 'error') {
-          return networkResponse;
-        }
+  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);
 
-        // Cache specific file types
-        const shouldCache =
-          url.pathname.endsWith('.css') ||
-          url.pathname.endsWith('.js') ||
-          url.pathname.endsWith('.svg') ||
-          url.pathname.endsWith('.jpg') ||
-          url.pathname.endsWith('.webp') ||
-          url.pathname.endsWith('.woff') ||
-          url.pathname.endsWith('.woff2') ||
-          url.pathname.endsWith('.ttf') ||
-          url.pathname.endsWith('.otf') ||
-          url.pathname.startsWith('/blog/') ||
-          url.pathname.startsWith('/notes/') ||
-          url.pathname === '/';
+  if (acceptsHtml || currentCode) {
+    event.respondWith(networkFirst(request, acceptsHtml));
+    return;
+  }
 
-        if (shouldCache) {
-          const responseToCache = networkResponse.clone();
-          caches.open(CACHE_NAME).then((cache) => {
-            console.log('[SW] Caching new resource:', url.pathname);
-            cache.put(request, responseToCache);
-          });
-        }
-
-        return networkResponse;
-      }).catch((error) => {
-        console.log('[SW] Fetch failed:', error);
-
-        // Return offline page for HTML requests
-        if (request.headers.get('Accept').includes('text/html')) {
-          return caches.match('/offline.html');
-        }
-      });
-    })
-  );
+  if (revalidatedMedia) {
+    event.respondWith(staleWhileRevalidate(event, request));
+  }
 });
 
 // Handle messages from the client