freeze cache after first install
Hunter
Wednesday, April 29th, 2026 at 11:54 PM
76 return "#080a0c"
77
78
-def _next_cache_name(app_name, manifest_path):
- """Determine the next cache name by incrementing the version in an existing manifest.
-
- If manifest.json doesn't exist or has no versioned cache_name, returns "{app_name}-v1".
- If it already has e.g. "heaven-v1", returns "heaven-v1.1".
- If it already has e.g. "heaven-v1.3", returns "heaven-v1.4".
- """
- if manifest_path.exists():
- try:
- with open(manifest_path, 'r', encoding='utf-8') as f:
- existing = json.load(f)
- old_cache = existing.get("cache_name", "")
- # Match pattern like "appname-v1" or "appname-v1.3"
- match = re.match(r'^(.+)-v(\d+)(?:\.(\d+))?$', old_cache)
- if match:
- prefix = match.group(1)
- major = int(match.group(2))
- minor = int(match.group(3)) if match.group(3) is not None else 0
- # If app was renamed, start fresh with new name
- if prefix != app_name:
- return f"{app_name}-v1"
- return f"{app_name}-v{major}.{minor + 1}"
- except (json.JSONDecodeError, KeyError):
- pass
- return f"{app_name}-v1"
-
-
79 def build_pwa(app_name=None, base_path=None):
80 """Generate manifest.json and service-worker.js based on tracks.json
81
89
90 # Derived values
91 short_name = app_name
- cache_name = _next_cache_name(app_name, SCRIPT_DIR / "manifest.json")
92 + cache_name = app_name
93 app_description = f"{app_name}"
94
95 print("Building PWA files...")
176 // Static files to cache on install
177 const STATIC_FILES = {static_files_js};
178
-// Install event - cache static resources
-// Audio files will be cached by the main app's blob preloading system
179 +// Install event - cache static resources only if not already cached.
180 +// This makes installs immutable: once a file is in the cache, redeploys
181 +// will not overwrite it, so the app stays frozen at its first-installed version.
182 +// Audio files will be cached by the main app's blob preloading system.
183 self.addEventListener('install', (event) => {{
184 console.log('Service Worker installing...', 'Base path:', basePath);
185 event.waitUntil(
188 if (url === './') return new URL(basePath, self.location.href).href;
189 return new URL(url, new URL(basePath, self.location.href)).href;
190 }});
- console.log('Caching', absoluteUrls.length, 'static resources');
191
- // Cache files individually with better error handling
- // Using Promise.allSettled to continue even if some fail
192 return Promise.allSettled(
193 absoluteUrls.map(url =>
- fetch(url, {{ cache: 'no-cache' }})
- .then(response => {{
- if (!response.ok) {{
- throw new Error(`HTTP error! status: ${{response.status}}`);
- }}
- return cache.put(url, response);
- }})
- .then(() => console.log('✓ Cached:', url))
- .catch(err => {{
- console.error('✗ Failed to cache:', url, err);
- throw err;
- }})
194 + cache.match(url).then(existing => {{
195 + if (existing) {{
196 + console.log('• Already cached, skipping:', url);
197 + return;
198 + }}
199 + return fetch(url, {{ cache: 'no-cache' }})
200 + .then(response => {{
201 + if (!response.ok) {{
202 + throw new Error(`HTTP error! status: ${{response.status}}`);
203 + }}
204 + return cache.put(url, response);
205 + }})
206 + .then(() => console.log('✓ Cached:', url))
207 + .catch(err => {{
208 + console.error('✗ Failed to cache:', url, err);
209 + throw err;
210 + }});
211 + }})
212 )
213 ).then(results => {{
214 const failed = results.filter(r => r.status === 'rejected');
- const succeeded = results.filter(r => r.status === 'fulfilled');
- console.log(`Cached ${{succeeded.length}}/${{results.length}} static resources`);
- if (failed.length > 0) {{
- console.warn(`Failed to cache ${{failed.length}} resources`);
- }}
215 + console.log(`Install complete: ${{results.length - failed.length}}/${{results.length}} ok`);
216 }});
- }}).then(() => {{
- console.log('Service Worker installation complete');
- return self.skipWaiting();
217 }}).catch(error => {{
218 console.error('Service Worker installation failed:', error);
219 }})
220 );
221 }});
222
-// Activate event - clean up old caches
-self.addEventListener('activate', (event) => {{
- console.log('Service Worker activating...');
- event.waitUntil(
- caches.keys().then((cacheNames) => {{
- return Promise.all(
- cacheNames.map((cacheName) => {{
- if (cacheName !== CACHE_NAME) {{
- console.log('Deleting old cache:', cacheName);
- return caches.delete(cacheName);
- }}
- }})
- );
- }}).then(() => self.clients.claim())
- );
-}});
-
223 // Fetch event - cache first, network fallback
224 self.addEventListener('fetch', (event) => {{
225 // Ignore non-http(s) requests like blob: URLs, data: URLs, chrome-extension:, etc.