1// Service Worker for AI Nail Art Studio 2// Provides offline support and caching for better performance 3 4const CACHE_NAME = 'nail-art-studio-v4'; 5const STATIC_CACHE = 'nail-art-static-v4'; 6const API_CACHE = 'nail-art-api-v4'; 7const IMAGE_CACHE = 'nail-art-images-v4'; 8 9// Assets to cache immediately 10const STATIC_ASSETS = [ 11 '/', 12 '/manifest.json', 13 '/favicon.ico', 14 '/apple-touch-icon.png', 15]; 16 17// API endpoints to cache 18const API_ENDPOINTS = [ 19 '/api/gallery', 20 '/api/categories', 21]; 22 23// Cacheable image origins 24const CACHEABLE_ORIGINS = [ 25 'cdn.nailartai.app', 26 'pub-05b5ee1a83754aa6b4fcd974016ecde8.r2.dev', 27 'pub-f94b6dc4538f33bcd1553dcdda15b36d.r2.dev', 28 'pub-fc15073de2e24f7bacc00c238f8ada7d.r2.dev' 29]; 30 31// Install event - cache static assets 32self.addEventListener('install', (event) => { 33 console.log('Service Worker: Installing...'); 34 35 event.waitUntil( 36 caches.open(STATIC_CACHE).then((cache) => { 37 console.log('Service Worker: Caching static assets'); 38 return cache.addAll(STATIC_ASSETS); 39 }) 40 ); 41 42 // Skip waiting to activate immediately 43 self.skipWaiting(); 44}); 45 46// Activate event - clean up old caches 47self.addEventListener('activate', (event) => { 48 console.log('Service Worker: Activating...'); 49 50 event.waitUntil( 51 caches.keys().then((cacheNames) => { 52 return Promise.all( 53 cacheNames.map((cacheName) => { 54 if (cacheName !== STATIC_CACHE && cacheName !== API_CACHE && cacheName !== IMAGE_CACHE) { 55 console.log('Service Worker: Deleting old cache:', cacheName); 56 return caches.delete(cacheName); 57 } 58 }) 59 ); 60 }) 61 ); 62 63 // Take control of all clients immediately 64 self.clients.claim(); 65}); 66 67// Fetch event - handle requests with caching strategies 68self.addEventListener('fetch', (event) => { 69 const { request } = event; 70 const url = new URL(request.url); 71 72 // Handle API requests 73 if (url.pathname.startsWith('/api/')) { 74 event.respondWith(handleApiRequest(request)); 75 } 76 // Handle image requests from R2/CDN 77 else if (CACHEABLE_ORIGINS.some(origin => url.hostname.includes(origin))) { 78 event.respondWith(handleImageRequest(request)); 79 } 80 // Handle static assets and pages 81 else { 82 event.respondWith(handleStaticRequest(request)); 83 } 84}); 85 86// API request strategy: Network first, cache fallback 87async function handleApiRequest(request) { 88 try { 89 // Try network first 90 const networkResponse = await fetch(request); 91 92 // If successful, cache the response (only GET requests) 93 if (networkResponse.ok && request.method === 'GET') { 94 const cache = await caches.open(API_CACHE); 95 cache.put(request, networkResponse.clone()); 96 } 97 98 return networkResponse; 99 } catch (error) { 100 // Network failed, try cache (only for GET requests) 101 if (request.method === 'GET') { 102 const cachedResponse = await caches.match(request); 103 if (cachedResponse) { 104 return cachedResponse; 105 } 106 } 107 108 // No cache available, return error 109 return new Response(JSON.stringify({ error: 'Offline' }), { 110 status: 503, 111 headers: { 'Content-Type': 'application/json' } 112 }); 113 } 114} 115 116// Static request strategy: Cache first, network fallback 117async function handleStaticRequest(request) { 118 // App routes must always prefer the network. Caching an HTML/RSC response 119 // here can preserve an old React bundle after a deployment, which means a 120 // signed-in user can see UI from one release with behavior from another. 121 if (request.mode === 'navigate') { 122 try { 123 return await fetch(request); 124 } catch (error) { 125 const fallback = await caches.match(request); 126 if (fallback) return fallback; 127 const homeFallback = await caches.match('/'); 128 if (homeFallback) return homeFallback; 129 return new Response('Offline', { status: 503 }); 130 } 131 } 132 133 // Try cache first for same-origin requests 134 if (request.url.startsWith(self.location.origin)) { 135 const cachedResponse = await caches.match(request); 136 if (cachedResponse) { 137 return cachedResponse; 138 } 139 } 140 141 try { 142 // Network request 143 const networkResponse = await fetch(request); 144 145 // Cache successful responses (only GET requests) 146 if (networkResponse.ok && 147 request.url.startsWith(self.location.origin) && 148 request.method === 'GET' && 149 request.destination !== 'document') { 150 const cache = await caches.open(STATIC_CACHE); 151 cache.put(request, networkResponse.clone()); 152 } 153 154 return networkResponse; 155 } catch (error) { 156 // If it's a navigation request and we have the main page cached, return it 157 if (request.mode === 'navigate') { 158 const fallback = await caches.match('/'); 159 if (fallback) { 160 return fallback; 161 } 162 } 163 164 return new Response('Offline', { status: 503 }); 165 } 166} 167
168// Background sync for form submissions when offline 169self.addEventListener('sync', (event) => { 170 if (event.tag === 'background-sync') { 171 event.waitUntil(syncOfflineData()); 172 } 173}); 174 175// Image request strategy: Cache first with 7-day TTL 176async function handleImageRequest(request) { 177 // Only cache GET requests 178 if (request.method !== 'GET') { 179 return fetch(request); 180 } 181 182 const cache = await caches.open(IMAGE_CACHE); 183 const cachedResponse = await caches.match(request); 184 185 if (cachedResponse) { 186 return cachedResponse; 187 } 188 189 try { 190 const networkResponse = await fetch(request); 191 if (networkResponse.ok) { 192 cache.put(request, networkResponse.clone()); 193 } 194 return networkResponse; 195 } catch (error) { 196 return new Response('Image unavailable', { status: 503 }); 197 } 198} 199 200async function syncOfflineData() { 201 // Handle any offline form submissions or data sync 202 console.log('Service Worker: Background sync triggered'); 203}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.