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') {