From bef60d0657142724a3e61551ff5165bde984d271 Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Wed, 23 Jul 2025 14:39:07 +0400 Subject: [PATCH] feat: add caching of assets using service workers --- public/sw.js | 132 ++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 109 insertions(+), 23 deletions(-) diff --git a/public/sw.js b/public/sw.js index 2ad6cafb0..c529b0e59 100644 --- a/public/sw.js +++ b/public/sw.js @@ -1,37 +1,123 @@ /* eslint-disable no-restricted-globals, no-console */ /* globals clients */ -self.addEventListener('push', event => { - let notification = event.data && event.data.json(); +const CACHE_NAME = 'vite-assets-cache-v1'; // Bump this to invalidate old caches +const CACHE_TTL = 7 * 24 * 60 * 60 * 1000; // 7 days in milliseconds +const CACHE_HEADER_NAME = 'x-woot-sw-cache-time'; +async function addCacheTime(response) { + const headers = new Headers(response.headers); + headers.set(CACHE_HEADER_NAME, Date.now().toString()); + const body = await response.blob(); + return new Response(body, { + status: response.status, + statusText: response.statusText, + headers, + }); +} + +function isFresh(response) { + const cachedTime = response.headers.get(CACHE_HEADER_NAME); + if (!cachedTime) return false; + return Date.now() - Number(cachedTime) < CACHE_TTL; +} + +async function handleAssetRequest(request) { + const cache = await caches.open(CACHE_NAME); + const cached = await cache.match(request); + + if (cached && isFresh(cached)) { + return cached; + } + + try { + const response = await fetch(request); + if (response.ok) { + const responseToCache = await addCacheTime(response); + // We naively cache everything - the browser will purge when needed + cache.put(request, responseToCache.clone()); + return responseToCache; + } + return cached || response; + } catch { + return cached || new Response('', { status: 504, statusText: 'Gateway Timeout' }); + } +} + +self.addEventListener('fetch', event => { + const url = new URL(event.request.url); + + if (url.pathname.startsWith('/vite/assets/')) { + event.respondWith(handleAssetRequest(event.request)); + } +}); + +// Clean up old caches on activate +// To trigger a cleanup we can simply change the `CACHE_NAME` +self.addEventListener('activate', event => { event.waitUntil( - self.registration.showNotification(notification.title, { - tag: notification.tag, - data: { - url: notification.url, - }, - }) + caches.keys().then(keys => + Promise.all( + keys + .filter(key => key !== CACHE_NAME) + .map(key => caches.delete(key)) + ) + ) ); + self.clients.claim(); +}); + +self.addEventListener('push', event => { + if (!event.data) { + console.warn('Service Worker: Push event received without data'); + return; + } + + try { + const notification = event.data.json(); + + if (!notification.title) { + console.warn('Service Worker: Push notification missing title'); + return; + } + + event.waitUntil( + self.registration.showNotification(notification.title, { + tag: notification.tag || 'default', + body: notification.body || '', + icon: notification.icon || '/favicon.ico', + data: { + url: notification.url || '/', + }, + }) + ); + } catch (err) { + console.error('Service Worker: Error parsing push notification', err); + } }); self.addEventListener('notificationclick', event => { - let notification = event.notification; + const notification = event.notification; + notification.close(); + + const targetUrl = notification.data?.url || '/'; event.waitUntil( - clients.matchAll({ type: 'window' }).then(windowClients => { - let matchingWindowClients = windowClients.filter( - client => client.url === notification.data.url - ); + clients.matchAll({ type: 'window', includeUncontrolled: true }) + .then(windowClients => { + const matchingClient = windowClients.find( + client => client.url === targetUrl + ); - if (matchingWindowClients.length) { - let firstWindow = matchingWindowClients[0]; - if (firstWindow && 'focus' in firstWindow) { - firstWindow.focus(); - return; + if (matchingClient) { + return matchingClient.focus(); } - } - if (clients.openWindow) { - clients.openWindow(notification.data.url); - } - }) + + if (clients.openWindow) { + return clients.openWindow(targetUrl); + } + }) + .catch(err => { + console.error('Service Worker: Error handling notification click', err); + }) ); });