Mercurial
comparison 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 |
comparison
equal
deleted
inserted
replaced
| 272:41a49c29a28f | 273:e02e2036ef84 |
|---|---|
| 1 // Root-scoped Service Worker for MrJuneJune PWA | 1 // Root-scoped Service Worker for MrJuneJune PWA |
| 2 const CACHE_VERSION = 'v35-role-greeting'; | 2 const CACHE_VERSION = 'v37-network-first'; |
| 3 const CACHE_NAME = `mrjunejune-${CACHE_VERSION}`; | 3 const CACHE_NAME = `mrjunejune-${CACHE_VERSION}`; |
| 4 const DEVELOPMENT_SANDBOX_PATHS = [ | |
| 5 '/public/component-sandbox', | |
| 6 '/public/composer-lab', | |
| 7 '/public/mjj-composer.', | |
| 8 '/public/mjj-modal.', | |
| 9 ]; | |
| 4 | 10 |
| 5 // Files to cache immediately on install | 11 // Files to cache immediately on install |
| 6 const STATIC_CACHE = [ | 12 const STATIC_CACHE = [ |
| 7 '/', | 13 '/', |
| 8 '/offline.html', | 14 '/offline.html', |
| 71 return self.clients.claim(); | 77 return self.clients.claim(); |
| 72 }) | 78 }) |
| 73 ); | 79 ); |
| 74 }); | 80 }); |
| 75 | 81 |
| 76 // Fetch event - serve from cache, fallback to network | 82 function isCacheable(response) { |
| 83 return response && | |
| 84 response.status === 200 && | |
| 85 response.type !== 'error'; | |
| 86 } | |
| 87 | |
| 88 async function updateCache(request, response) { | |
| 89 if (!isCacheable(response)) return; | |
| 90 const cache = await caches.open(CACHE_NAME); | |
| 91 await cache.put(request, response.clone()); | |
| 92 } | |
| 93 | |
| 94 async function networkFirst(request, offlineFallback = false) { | |
| 95 try { | |
| 96 const response = await fetch(request, { cache: 'no-cache' }); | |
| 97 await updateCache(request, response); | |
| 98 return response; | |
| 99 } catch (error) { | |
| 100 const cached = await caches.match(request); | |
| 101 if (cached) return cached; | |
| 102 if (offlineFallback) { | |
| 103 const offline = await caches.match('/offline.html'); | |
| 104 if (offline) return offline; | |
| 105 } | |
| 106 throw error; | |
| 107 } | |
| 108 } | |
| 109 | |
| 110 async function staleWhileRevalidate(event, request) { | |
| 111 const cached = await caches.match(request); | |
| 112 const update = fetch(request, { cache: 'no-cache' }).then(async response => { | |
| 113 await updateCache(request, response); | |
| 114 return response; | |
| 115 }); | |
| 116 if (!cached) return update; | |
| 117 event.waitUntil(update.catch(() => undefined)); | |
| 118 return cached; | |
| 119 } | |
| 120 | |
| 121 // Fetch event - current code first, cached media first, offline fallback. | |
| 77 self.addEventListener('fetch', (event) => { | 122 self.addEventListener('fetch', (event) => { |
| 78 const { request } = event; | 123 const { request } = event; |
| 79 const url = new URL(request.url); | 124 const url = new URL(request.url); |
| 80 | 125 |
| 81 // Skip non-GET requests | 126 // Skip non-GET requests |
| 86 // Skip chrome-extension and other non-http(s) requests | 131 // Skip chrome-extension and other non-http(s) requests |
| 87 if (!url.protocol.startsWith('http')) { | 132 if (!url.protocol.startsWith('http')) { |
| 88 return; | 133 return; |
| 89 } | 134 } |
| 90 | 135 |
| 136 if (url.origin !== self.location.origin) { | |
| 137 return; | |
| 138 } | |
| 139 | |
| 91 // Skip API calls and media uploads (always go to network) | 140 // Skip API calls and media uploads (always go to network) |
| 92 if (url.pathname.startsWith('/api/')) { | 141 if (url.pathname.startsWith('/api/')) { |
| 93 return; | 142 return; |
| 94 } | 143 } |
| 95 | 144 |
| 96 // Development servers should always serve current source and assets. | 145 // Development servers should always serve current source and assets. |
| 97 if (url.hostname === 'localhost' || url.hostname === '127.0.0.1') { | 146 if (url.hostname === 'localhost' || url.hostname === '127.0.0.1') { |
| 98 return; | 147 return; |
| 99 } | 148 } |
| 100 | 149 |
| 150 // Component workbenches must reflect every ibazel rebuild on any hostname. | |
| 151 if (DEVELOPMENT_SANDBOX_PATHS.some(path => | |
| 152 url.pathname.startsWith(path))) { | |
| 153 return; | |
| 154 } | |
| 155 | |
| 101 // The HLS tool and media must always reflect the current player version. | 156 // The HLS tool and media must always reflect the current player version. |
| 102 if (url.pathname.startsWith('/tools/hls_player') || | 157 if (url.pathname.startsWith('/tools/hls_player') || |
| 103 url.pathname.startsWith('/public/hls-sample/')) { | 158 url.pathname.startsWith('/public/hls-sample/')) { |
| 104 return; | 159 return; |
| 105 } | 160 } |
| 106 | 161 |
| 107 event.respondWith( | 162 const acceptsHtml = request.mode === 'navigate' || |
| 108 caches.match(request).then((cachedResponse) => { | 163 request.headers.get('Accept')?.includes('text/html'); |
| 109 if (cachedResponse) { | 164 const currentCode = /\.(?:css|js|json|wasm)$/i.test(url.pathname); |
| 110 console.log('[SW] Serving from cache:', url.pathname); | 165 const revalidatedMedia = |
| 111 return cachedResponse; | 166 /\.(?:svg|jpe?g|webp|woff2?|ttf|otf|pdf)$/i.test(url.pathname); |
| 112 } | |
| 113 | 167 |
| 114 // Not in cache, fetch from network | 168 if (acceptsHtml || currentCode) { |
| 115 return fetch(request).then((networkResponse) => { | 169 event.respondWith(networkFirst(request, acceptsHtml)); |
| 116 // Only cache successful responses | 170 return; |
| 117 if (!networkResponse || networkResponse.status !== 200 || networkResponse.type === 'error') { | 171 } |
| 118 return networkResponse; | |
| 119 } | |
| 120 | 172 |
| 121 // Cache specific file types | 173 if (revalidatedMedia) { |
| 122 const shouldCache = | 174 event.respondWith(staleWhileRevalidate(event, request)); |
| 123 url.pathname.endsWith('.css') || | 175 } |
| 124 url.pathname.endsWith('.js') || | |
| 125 url.pathname.endsWith('.svg') || | |
| 126 url.pathname.endsWith('.jpg') || | |
| 127 url.pathname.endsWith('.webp') || | |
| 128 url.pathname.endsWith('.woff') || | |
| 129 url.pathname.endsWith('.woff2') || | |
| 130 url.pathname.endsWith('.ttf') || | |
| 131 url.pathname.endsWith('.otf') || | |
| 132 url.pathname.startsWith('/blog/') || | |
| 133 url.pathname.startsWith('/notes/') || | |
| 134 url.pathname === '/'; | |
| 135 | |
| 136 if (shouldCache) { | |
| 137 const responseToCache = networkResponse.clone(); | |
| 138 caches.open(CACHE_NAME).then((cache) => { | |
| 139 console.log('[SW] Caching new resource:', url.pathname); | |
| 140 cache.put(request, responseToCache); | |
| 141 }); | |
| 142 } | |
| 143 | |
| 144 return networkResponse; | |
| 145 }).catch((error) => { | |
| 146 console.log('[SW] Fetch failed:', error); | |
| 147 | |
| 148 // Return offline page for HTML requests | |
| 149 if (request.headers.get('Accept').includes('text/html')) { | |
| 150 return caches.match('/offline.html'); | |
| 151 } | |
| 152 }); | |
| 153 }) | |
| 154 ); | |
| 155 }); | 176 }); |
| 156 | 177 |
| 157 // Handle messages from the client | 178 // Handle messages from the client |
| 158 self.addEventListener('message', (event) => { | 179 self.addEventListener('message', (event) => { |
| 159 if (event.data && event.data.type === 'SKIP_WAITING') { | 180 if (event.data && event.data.type === 'SKIP_WAITING') { |