From 40f425f33ad06d3214a6051006334a8ab6478f5c Mon Sep 17 00:00:00 2001 From: Upayan Date: Tue, 21 Oct 2025 15:09:56 +0530 Subject: [PATCH 1/5] Improve service worker caching logic and error handling for fetch requests --- app/public/service-worker.js | 78 ++++++++++++++++++++++++------------ 1 file changed, 52 insertions(+), 26 deletions(-) diff --git a/app/public/service-worker.js b/app/public/service-worker.js index d5580cd..cfed13b 100644 --- a/app/public/service-worker.js +++ b/app/public/service-worker.js @@ -43,7 +43,7 @@ self.addEventListener('install', (event) => { event.waitUntil( caches .open(PRECACHE) - .then((cache) => cache.addAll(CORE_ASSETS)) + .then((cache) => Promise.allSettled(CORE_ASSETS.map((asset) => cache.add(asset)))) .then(() => self.skipWaiting()) ); }); @@ -62,25 +62,32 @@ self.addEventListener('activate', (event) => { ); }); -// Helper: prune runtime cache by age and max entries +// Runtime cache pruning async function pruneRuntimeCache() { const cache = await caches.open(RUNTIME); const requests = await cache.keys(); const now = Date.now(); - const entries = []; - for (const req of requests) { - const res = await cache.match(req); - const fetchedTime = res && res.headers.get('SW-Fetched-Time'); - entries.push({ req, time: fetchedTime ? parseInt(fetchedTime) : 0 }); - } + let entries = await Promise.all( + requests.map(async (req) => { + const res = await cache.match(req); + const fetchedTime = res?.headers.get('SW-Fetched-Time'); + return { req, time: fetchedTime ? parseInt(fetchedTime) : 0 }; + }) + ); // Remove old entries by age - for (const entry of entries) { - if (now - entry.time > RUNTIME_MAX_AGE) { - await cache.delete(entry.req); - } - } + await Promise.all( + entries.map(async (entry) => { + if (now - entry.time > RUNTIME_MAX_AGE) { + await cache.delete(entry.req); + entry.deleted = true; + } + }) + ); + + // Filter out deleted entries + entries = entries.filter((entry) => !entry.deleted); // Remove excess entries beyond max entries entries.sort((a, b) => a.time - b.time); @@ -90,35 +97,51 @@ async function pruneRuntimeCache() { } } -// Fetch +// Fetch handler self.addEventListener('fetch', (event) => { const request = event.request; const url = new URL(request.url); if (request.method !== 'GET' || url.origin !== self.location.origin) return; + // Core assets: cache-first if (CORE_ASSETS.includes(url.pathname)) { event.respondWith( caches.match(request).then((cached) => { const networkFetch = fetch(request) .then((response) => { - caches.open(PRECACHE).then((cache) => cache.put(request, response.clone())); + if (response && response.status === 200) { + caches.open(PRECACHE).then((cache) => cache.put(request, response.clone())); + } return response; }) - .catch(() => cached); + .catch((err) => { + console.error('Fetch failed for core asset:', request.url, err); + return cached || caches.match('/offline') || new Response('Service unavailable', { status: 503 }); + }); return cached || networkFetch; }) ); - } else if (request.mode === 'navigate') { - event.respondWith(fetch(request).catch(() => caches.match('/offline'))); - } else { + } + // Navigation requests: network-first, fallback offline + else if (request.mode === 'navigate') { event.respondWith( - caches.match(request).then((cached) => { + fetch(request).catch((err) => { + console.error('Navigation fetch failed:', request.url, err); + return caches.match('/offline') || new Response('Offline', { status: 503 }); + }) + ); + } + // Other runtime requests: network-first, runtime cache + else { + event.respondWith( + caches.match(request).then(async (cached) => { if (cached) return cached; - return fetch(request) - .then(async (response) => { + try { + const response = await fetch(request); + if (response && response.status === 200) { const cloned = response.clone(); const headers = new Headers(cloned.headers); headers.set('SW-Fetched-Time', Date.now().toString()); @@ -126,9 +149,12 @@ self.addEventListener('fetch', (event) => { const cache = await caches.open(RUNTIME); await cache.put(request, modifiedResponse); await pruneRuntimeCache(); - return response; - }) - .catch(() => {}); + } + return response; + } catch (err) { + console.error('Runtime fetch failed:', request.url, err); + return caches.match('/offline') || new Response('Service unavailable', { status: 503 }); + } }) ); } @@ -136,5 +162,5 @@ self.addEventListener('fetch', (event) => { // Skip waiting via client message self.addEventListener('message', (event) => { - if (event.data && event.data.type === 'SKIP_WAITING') self.skipWaiting(); + if (event.data?.type === 'SKIP_WAITING') self.skipWaiting(); }); From 04173616940979867fc9fe468bad37f7205c1449 Mon Sep 17 00:00:00 2001 From: Upayan Date: Tue, 21 Oct 2025 15:12:07 +0530 Subject: [PATCH 2/5] Update app/public/service-worker.js Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- app/public/service-worker.js | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/app/public/service-worker.js b/app/public/service-worker.js index cfed13b..da46099 100644 --- a/app/public/service-worker.js +++ b/app/public/service-worker.js @@ -43,7 +43,17 @@ self.addEventListener('install', (event) => { event.waitUntil( caches .open(PRECACHE) - .then((cache) => Promise.allSettled(CORE_ASSETS.map((asset) => cache.add(asset)))) + .then((cache) => { + // Map assets to their cache.add promises + const assetPromises = CORE_ASSETS.map((asset) => cache.add(asset)); + return Promise.allSettled(assetPromises).then((results) => { + results.forEach((result, i) => { + if (result.status === 'rejected') { + console.error(`Failed to cache asset: ${CORE_ASSETS[i]}`, result.reason); + } + }); + }); + }) .then(() => self.skipWaiting()) ); }); From 4e9ad04c74e388d0910a534f944aa4f24f3ed396 Mon Sep 17 00:00:00 2001 From: Upayan Date: Tue, 21 Oct 2025 15:13:51 +0530 Subject: [PATCH 3/5] Refactor service worker for improved caching logic and error handling --- app/public/service-worker.js | 199 +++++++++++++++-------------------- 1 file changed, 83 insertions(+), 116 deletions(-) diff --git a/app/public/service-worker.js b/app/public/service-worker.js index da46099..8f62ce1 100644 --- a/app/public/service-worker.js +++ b/app/public/service-worker.js @@ -2,175 +2,142 @@ const VERSION = 'v1.1.1'; const PRECACHE = `precache-${VERSION}`; const RUNTIME = `runtime-${VERSION}`; const RUNTIME_MAX_ENTRIES = 100; -const RUNTIME_MAX_AGE = 7 * 24 * 60 * 60 * 1000; // 7 days in ms +const RUNTIME_MAX_AGE = 7 * 24 * 60 * 60 * 1000; // 7 days const CORE_ASSETS = [ - '/', - '/offline', - '/manifest.webmanifest', + '/', '/offline', '/manifest.webmanifest', '/fonts/AirbnbCereal_W_Bd.otf', '/fonts/Inter-VariableFont_opsz,wght.ttf', '/fonts/Montserrat-Regular.ttf', - '/icons/icon-16x16.webp', - '/icons/icon-32x32.webp', - '/icons/icon-48x48.webp', - '/icons/icon-64x64.webp', - '/icons/icon-72x72.webp', - '/icons/icon-76x76.webp', - '/icons/icon-96x96.webp', - '/icons/icon-114x114.webp', - '/icons/icon-120x120.webp', - '/icons/icon-128x128.webp', - '/icons/icon-144x144.webp', - '/icons/icon-152x152.webp', - '/icons/icon-180x180.webp', - '/icons/icon-192x192.webp', - '/icons/icon-196x196.webp', - '/icons/icon-228x228.webp', - '/icons/icon-256x256.webp', - '/icons/icon-384x384.webp', - '/icons/icon-512x512.webp', - '/apple-touch-icon.webp', - '/icon.avif', - '/icon.png', - '/icon.svg', - '/icon.webp', - '/favicon.ico', + '/icons/icon-16x16.webp', '/icons/icon-32x32.webp', + '/icons/icon-48x48.webp', '/icons/icon-64x64.webp', + '/icons/icon-72x72.webp', '/icons/icon-76x76.webp', + '/icons/icon-96x96.webp', '/icons/icon-114x114.webp', + '/icons/icon-120x120.webp', '/icons/icon-128x128.webp', + '/icons/icon-144x144.webp', '/icons/icon-152x152.webp', + '/icons/icon-180x180.webp', '/icons/icon-192x192.webp', + '/icons/icon-196x196.webp', '/icons/icon-228x228.webp', + '/icons/icon-256x256.webp', '/icons/icon-384x384.webp', + '/icons/icon-512x512.webp', '/apple-touch-icon.webp', + '/icon.avif', '/icon.png', '/icon.svg', '/icon.webp', + '/favicon.ico' ]; -// Install +// -------------------- INSTALL -------------------- self.addEventListener('install', (event) => { event.waitUntil( - caches - .open(PRECACHE) - .then((cache) => { - // Map assets to their cache.add promises - const assetPromises = CORE_ASSETS.map((asset) => cache.add(asset)); - return Promise.allSettled(assetPromises).then((results) => { - results.forEach((result, i) => { - if (result.status === 'rejected') { - console.error(`Failed to cache asset: ${CORE_ASSETS[i]}`, result.reason); - } - }); - }); + caches.open(PRECACHE) + .then(cache => { + const promises = CORE_ASSETS.map(asset => + cache.add(asset).catch(err => console.error(`Failed to cache ${asset}:`, err)) + ); + return Promise.all(promises); }) .then(() => self.skipWaiting()) ); }); -// Activate +// -------------------- ACTIVATE -------------------- self.addEventListener('activate', (event) => { event.waitUntil( - caches - .keys() - .then((keys) => - Promise.all( - keys.filter((key) => key !== PRECACHE && key !== RUNTIME).map((key) => caches.delete(key)) - ) - ) + caches.keys() + .then(keys => Promise.all( + keys.filter(key => key !== PRECACHE && key !== RUNTIME) + .map(key => caches.delete(key)) + )) .then(() => self.clients.claim()) ); }); -// Runtime cache pruning +// -------------------- RUNTIME CACHE PRUNING -------------------- async function pruneRuntimeCache() { const cache = await caches.open(RUNTIME); const requests = await cache.keys(); const now = Date.now(); let entries = await Promise.all( - requests.map(async (req) => { + requests.map(async req => { const res = await cache.match(req); const fetchedTime = res?.headers.get('SW-Fetched-Time'); return { req, time: fetchedTime ? parseInt(fetchedTime) : 0 }; }) ); - // Remove old entries by age - await Promise.all( - entries.map(async (entry) => { - if (now - entry.time > RUNTIME_MAX_AGE) { - await cache.delete(entry.req); - entry.deleted = true; - } - }) - ); - - // Filter out deleted entries - entries = entries.filter((entry) => !entry.deleted); + // Remove old entries + const toDelete = entries.filter(entry => now - entry.time > RUNTIME_MAX_AGE).map(e => e.req); + await Promise.all(toDelete.map(req => cache.delete(req))); - // Remove excess entries beyond max entries + // Remove excess entries + entries = entries.filter(entry => now - entry.time <= RUNTIME_MAX_AGE); entries.sort((a, b) => a.time - b.time); + while (entries.length > RUNTIME_MAX_ENTRIES) { const entry = entries.shift(); if (entry) await cache.delete(entry.req); } } -// Fetch handler +// -------------------- FETCH HANDLER -------------------- self.addEventListener('fetch', (event) => { const request = event.request; const url = new URL(request.url); if (request.method !== 'GET' || url.origin !== self.location.origin) return; - // Core assets: cache-first + // --- CORE ASSETS (cache-first) --- if (CORE_ASSETS.includes(url.pathname)) { - event.respondWith( - caches.match(request).then((cached) => { - const networkFetch = fetch(request) - .then((response) => { - if (response && response.status === 200) { - caches.open(PRECACHE).then((cache) => cache.put(request, response.clone())); - } - return response; - }) - .catch((err) => { - console.error('Fetch failed for core asset:', request.url, err); - return cached || caches.match('/offline') || new Response('Service unavailable', { status: 503 }); - }); - - return cached || networkFetch; - }) - ); + event.respondWith((async () => { + const cached = await caches.match(request); + try { + const response = await fetch(request); + if (response && response.status === 200) { + const cache = await caches.open(PRECACHE); + cache.put(request, response.clone()); + } + return cached || response; + } catch { + return cached || await caches.match('/offline') || new Response('Service unavailable', { status: 503 }); + } + })()); + return; } - // Navigation requests: network-first, fallback offline - else if (request.mode === 'navigate') { - event.respondWith( - fetch(request).catch((err) => { + + // --- NAVIGATION (network-first with offline fallback) --- + if (request.mode === 'navigate') { + event.respondWith((async () => { + try { + return await fetch(request); + } catch (err) { console.error('Navigation fetch failed:', request.url, err); - return caches.match('/offline') || new Response('Offline', { status: 503 }); - }) - ); + return await caches.match('/offline') || new Response('Offline', { status: 503 }); + } + })()); + return; } - // Other runtime requests: network-first, runtime cache - else { - event.respondWith( - caches.match(request).then(async (cached) => { - if (cached) return cached; - try { - const response = await fetch(request); - if (response && response.status === 200) { - const cloned = response.clone(); - const headers = new Headers(cloned.headers); - headers.set('SW-Fetched-Time', Date.now().toString()); - const modifiedResponse = new Response(await cloned.blob(), { headers }); - const cache = await caches.open(RUNTIME); - await cache.put(request, modifiedResponse); - await pruneRuntimeCache(); - } - return response; - } catch (err) { - console.error('Runtime fetch failed:', request.url, err); - return caches.match('/offline') || new Response('Service unavailable', { status: 503 }); - } - }) - ); - } + // --- OTHER RUNTIME REQUESTS (network-first + runtime cache) --- + event.respondWith((async () => { + try { + const response = await fetch(request); + if (response && response.status === 200) { + const cloned = response.clone(); + const headers = new Headers(cloned.headers); + headers.set('SW-Fetched-Time', Date.now().toString()); + const modifiedResponse = new Response(await cloned.blob(), { headers }); + const cache = await caches.open(RUNTIME); + await cache.put(request, modifiedResponse); + await pruneRuntimeCache(); + } + return response; + } catch (err) { + console.error('Runtime fetch failed:', request.url, err); + const cached = await caches.match(request); + return cached || await caches.match('/offline') || new Response('Service unavailable', { status: 503 }); + } + })()); }); -// Skip waiting via client message +// -------------------- SKIP WAITING -------------------- self.addEventListener('message', (event) => { if (event.data?.type === 'SKIP_WAITING') self.skipWaiting(); }); From 144418fb23ee626bf972f705b84b438845e15091 Mon Sep 17 00:00:00 2001 From: Upayan Date: Tue, 21 Oct 2025 15:16:26 +0530 Subject: [PATCH 4/5] Update app/public/service-worker.js Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- app/public/service-worker.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/app/public/service-worker.js b/app/public/service-worker.js index 8f62ce1..6937500 100644 --- a/app/public/service-worker.js +++ b/app/public/service-worker.js @@ -88,15 +88,18 @@ self.addEventListener('fetch', (event) => { if (CORE_ASSETS.includes(url.pathname)) { event.respondWith((async () => { const cached = await caches.match(request); + if (cached) { + return cached; + } try { const response = await fetch(request); if (response && response.status === 200) { const cache = await caches.open(PRECACHE); cache.put(request, response.clone()); } - return cached || response; + return response; } catch { - return cached || await caches.match('/offline') || new Response('Service unavailable', { status: 503 }); + return await caches.match('/offline') || new Response('Service unavailable', { status: 503 }); } })()); return; From 5067720c07d2c34411c51566465113fb7f20bce4 Mon Sep 17 00:00:00 2001 From: Upayan Date: Tue, 21 Oct 2025 15:17:50 +0530 Subject: [PATCH 5/5] Refactor service worker for improved readability and error handling in caching logic --- app/public/service-worker.js | 164 +++++++++++++++++++++-------------- 1 file changed, 97 insertions(+), 67 deletions(-) diff --git a/app/public/service-worker.js b/app/public/service-worker.js index 6937500..bf6ab42 100644 --- a/app/public/service-worker.js +++ b/app/public/service-worker.js @@ -5,31 +5,47 @@ const RUNTIME_MAX_ENTRIES = 100; const RUNTIME_MAX_AGE = 7 * 24 * 60 * 60 * 1000; // 7 days const CORE_ASSETS = [ - '/', '/offline', '/manifest.webmanifest', + '/', + '/offline', + '/manifest.webmanifest', '/fonts/AirbnbCereal_W_Bd.otf', '/fonts/Inter-VariableFont_opsz,wght.ttf', '/fonts/Montserrat-Regular.ttf', - '/icons/icon-16x16.webp', '/icons/icon-32x32.webp', - '/icons/icon-48x48.webp', '/icons/icon-64x64.webp', - '/icons/icon-72x72.webp', '/icons/icon-76x76.webp', - '/icons/icon-96x96.webp', '/icons/icon-114x114.webp', - '/icons/icon-120x120.webp', '/icons/icon-128x128.webp', - '/icons/icon-144x144.webp', '/icons/icon-152x152.webp', - '/icons/icon-180x180.webp', '/icons/icon-192x192.webp', - '/icons/icon-196x196.webp', '/icons/icon-228x228.webp', - '/icons/icon-256x256.webp', '/icons/icon-384x384.webp', - '/icons/icon-512x512.webp', '/apple-touch-icon.webp', - '/icon.avif', '/icon.png', '/icon.svg', '/icon.webp', - '/favicon.ico' + '/icons/icon-16x16.webp', + '/icons/icon-32x32.webp', + '/icons/icon-48x48.webp', + '/icons/icon-64x64.webp', + '/icons/icon-72x72.webp', + '/icons/icon-76x76.webp', + '/icons/icon-96x96.webp', + '/icons/icon-114x114.webp', + '/icons/icon-120x120.webp', + '/icons/icon-128x128.webp', + '/icons/icon-144x144.webp', + '/icons/icon-152x152.webp', + '/icons/icon-180x180.webp', + '/icons/icon-192x192.webp', + '/icons/icon-196x196.webp', + '/icons/icon-228x228.webp', + '/icons/icon-256x256.webp', + '/icons/icon-384x384.webp', + '/icons/icon-512x512.webp', + '/apple-touch-icon.webp', + '/icon.avif', + '/icon.png', + '/icon.svg', + '/icon.webp', + '/favicon.ico', ]; // -------------------- INSTALL -------------------- self.addEventListener('install', (event) => { event.waitUntil( - caches.open(PRECACHE) - .then(cache => { - const promises = CORE_ASSETS.map(asset => - cache.add(asset).catch(err => console.error(`Failed to cache ${asset}:`, err)) + caches + .open(PRECACHE) + .then((cache) => { + const promises = CORE_ASSETS.map((asset) => + cache.add(asset).catch((err) => console.error(`Failed to cache ${asset}:`, err)) ); return Promise.all(promises); }) @@ -40,11 +56,13 @@ self.addEventListener('install', (event) => { // -------------------- ACTIVATE -------------------- self.addEventListener('activate', (event) => { event.waitUntil( - caches.keys() - .then(keys => Promise.all( - keys.filter(key => key !== PRECACHE && key !== RUNTIME) - .map(key => caches.delete(key)) - )) + caches + .keys() + .then((keys) => + Promise.all( + keys.filter((key) => key !== PRECACHE && key !== RUNTIME).map((key) => caches.delete(key)) + ) + ) .then(() => self.clients.claim()) ); }); @@ -56,7 +74,7 @@ async function pruneRuntimeCache() { const now = Date.now(); let entries = await Promise.all( - requests.map(async req => { + requests.map(async (req) => { const res = await cache.match(req); const fetchedTime = res?.headers.get('SW-Fetched-Time'); return { req, time: fetchedTime ? parseInt(fetchedTime) : 0 }; @@ -64,11 +82,11 @@ async function pruneRuntimeCache() { ); // Remove old entries - const toDelete = entries.filter(entry => now - entry.time > RUNTIME_MAX_AGE).map(e => e.req); - await Promise.all(toDelete.map(req => cache.delete(req))); + const toDelete = entries.filter((entry) => now - entry.time > RUNTIME_MAX_AGE).map((e) => e.req); + await Promise.all(toDelete.map((req) => cache.delete(req))); // Remove excess entries - entries = entries.filter(entry => now - entry.time <= RUNTIME_MAX_AGE); + entries = entries.filter((entry) => now - entry.time <= RUNTIME_MAX_AGE); entries.sort((a, b) => a.time - b.time); while (entries.length > RUNTIME_MAX_ENTRIES) { @@ -86,58 +104,70 @@ self.addEventListener('fetch', (event) => { // --- CORE ASSETS (cache-first) --- if (CORE_ASSETS.includes(url.pathname)) { - event.respondWith((async () => { - const cached = await caches.match(request); - if (cached) { - return cached; - } - try { - const response = await fetch(request); - if (response && response.status === 200) { - const cache = await caches.open(PRECACHE); - cache.put(request, response.clone()); + event.respondWith( + (async () => { + const cached = await caches.match(request); + if (cached) { + return cached; } - return response; - } catch { - return await caches.match('/offline') || new Response('Service unavailable', { status: 503 }); - } - })()); + try { + const response = await fetch(request); + if (response && response.status === 200) { + const cache = await caches.open(PRECACHE); + cache.put(request, response.clone()); + } + return response; + } catch { + return ( + (await caches.match('/offline')) || new Response('Service unavailable', { status: 503 }) + ); + } + })() + ); return; } // --- NAVIGATION (network-first with offline fallback) --- if (request.mode === 'navigate') { - event.respondWith((async () => { - try { - return await fetch(request); - } catch (err) { - console.error('Navigation fetch failed:', request.url, err); - return await caches.match('/offline') || new Response('Offline', { status: 503 }); - } - })()); + event.respondWith( + (async () => { + try { + return await fetch(request); + } catch (err) { + console.error('Navigation fetch failed:', request.url, err); + return (await caches.match('/offline')) || new Response('Offline', { status: 503 }); + } + })() + ); return; } // --- OTHER RUNTIME REQUESTS (network-first + runtime cache) --- - event.respondWith((async () => { - try { - const response = await fetch(request); - if (response && response.status === 200) { - const cloned = response.clone(); - const headers = new Headers(cloned.headers); - headers.set('SW-Fetched-Time', Date.now().toString()); - const modifiedResponse = new Response(await cloned.blob(), { headers }); - const cache = await caches.open(RUNTIME); - await cache.put(request, modifiedResponse); - await pruneRuntimeCache(); + event.respondWith( + (async () => { + try { + const response = await fetch(request); + if (response && response.status === 200) { + const cloned = response.clone(); + const headers = new Headers(cloned.headers); + headers.set('SW-Fetched-Time', Date.now().toString()); + const modifiedResponse = new Response(await cloned.blob(), { headers }); + const cache = await caches.open(RUNTIME); + await cache.put(request, modifiedResponse); + await pruneRuntimeCache(); + } + return response; + } catch (err) { + console.error('Runtime fetch failed:', request.url, err); + const cached = await caches.match(request); + return ( + cached || + (await caches.match('/offline')) || + new Response('Service unavailable', { status: 503 }) + ); } - return response; - } catch (err) { - console.error('Runtime fetch failed:', request.url, err); - const cached = await caches.match(request); - return cached || await caches.match('/offline') || new Response('Service unavailable', { status: 503 }); - } - })()); + })() + ); }); // -------------------- SKIP WAITING --------------------