From 3ac167799b9b52d4517a6c999179e58990c30105 Mon Sep 17 00:00:00 2001 From: Tyler Date: Wed, 17 Jun 2026 14:32:17 -0600 Subject: [PATCH] feat(pwa): rewrite service worker (shell cache + data cache + offline fallback) --- public/sw.js | 186 +++++++++++++++++++++++---------------------------- 1 file changed, 82 insertions(+), 104 deletions(-) diff --git a/public/sw.js b/public/sw.js index fb33e9e..b9b49d7 100644 --- a/public/sw.js +++ b/public/sw.js @@ -1,130 +1,108 @@ -// Service Worker for PWA - Caching and offline support +// public/sw.js +// Route Commerce Service Worker +// Two-cache strategy: shell (cache-first) + data (stale-while-revalidate) -const CACHE_NAME = "route-commerce-v1"; -const OFFLINE_URL = "/offline"; +const SHELL_CACHE = "rc-shell-v3"; +const DATA_CACHE = "rc-data-v3"; +const OFFLINE_URL = "/offline.html"; -const STATIC_ASSETS = [ +const SHELL_ASSETS = [ "/", "/manifest.json", "/favicon.svg", - "/og-default.jpg", + "/og-default.svg", + "/offline.html", + "/icons/icon-192x192.png", + "/icons/icon-512x512.png", ]; -// Install event - cache static assets self.addEventListener("install", (event) => { event.waitUntil( - caches.open(CACHE_NAME).then((cache) => { - return cache.addAll(STATIC_ASSETS); - }) + caches.open(SHELL_CACHE).then((cache) => cache.addAll(SHELL_ASSETS)) ); self.skipWaiting(); }); -// Activate event - clean old caches self.addEventListener("activate", (event) => { event.waitUntil( - caches.keys().then((cacheNames) => { - return Promise.all( - cacheNames - .filter((name) => name !== CACHE_NAME) - .map((name) => caches.delete(name)) - ); - }) + Promise.all([ + caches.keys().then((cacheNames) => + Promise.all( + cacheNames + .filter((name) => name !== SHELL_CACHE && name !== DATA_CACHE) + .map((name) => caches.delete(name)) + ) + ), + self.clients.claim(), + ]) ); - self.clients.claim(); }); -// Fetch event - network first, fallback to cache self.addEventListener("fetch", (event) => { - // Skip non-GET requests - if (event.request.method !== "GET") return; + const req = event.request; + if (req.method !== "GET") return; + const url = new URL(req.url); + if (url.origin !== self.location.origin) return; - // Skip API requests - if (event.request.url.includes("/api/")) return; + // Navigations → network-first, fall back to cache, fall back to offline page + if (req.mode === "navigate") { + event.respondWith( + fetch(req) + .then((res) => { + const clone = res.clone(); + caches.open(SHELL_CACHE).then((cache) => cache.put(req, clone)); + return res; + }) + .catch(() => + caches.match(req).then((cached) => cached || caches.match(OFFLINE_URL)) + ) + ); + return; + } - // Skip cross-origin requests - if (!event.request.url.startsWith(self.location.origin)) return; - - event.respondWith( - fetch(event.request) - .then((response) => { - // Clone response for caching - const responseClone = response.clone(); - - // Cache successful responses - if (response.status === 200) { - caches.open(CACHE_NAME).then((cache) => { - cache.put(event.request, responseClone); - }); - } - - return response; - }) - .catch(() => { - // Return cached response or offline page - return caches.match(event.request).then((cachedResponse) => { - if (cachedResponse) { - return cachedResponse; + // Static assets → cache-first + if ( + url.pathname.startsWith("/_next/static/") || + url.pathname.startsWith("/icons/") || + url.pathname.startsWith("/screenshots/") || + url.pathname.endsWith(".svg") || + url.pathname.endsWith(".woff2") + ) { + event.respondWith( + caches.match(req).then((cached) => { + if (cached) return cached; + return fetch(req).then((res) => { + const clone = res.clone(); + if (res.status === 200) { + caches.open(SHELL_CACHE).then((cache) => cache.put(req, clone)); } - // Return offline page for navigation requests - if (event.request.mode === "navigate") { - return caches.match(OFFLINE_URL); - } - return new Response("Network error", { status: 408 }); + return res; }); }) - ); -}); - -// Push notification handling -self.addEventListener("push", (event) => { - if (!event.data) return; - - const data = event.data.json(); - - const options = { - body: data.body, - icon: "/icons/icon-192x192.png", - badge: "/icons/badge-72x72.png", - vibrate: [100, 50, 100], - data: { - url: data.url || "/", - }, - actions: data.actions || [], - }; - - event.waitUntil(self.registration.showNotification(data.title, options)); -}); - -// Notification click handling -self.addEventListener("notificationclick", (event) => { - event.notification.close(); - - const url = event.notification.data?.url || "/"; - - event.waitUntil( - clients.matchAll({ type: "window", includeUncontrolled: true }).then((clientList) => { - // Focus existing window or open new one - for (const client of clientList) { - if (client.url === url && "focus" in client) { - return client.focus(); - } - } - if (clients.openWindow) { - return clients.openWindow(url); - } - }) - ); -}); - -// Background sync for offline actions -self.addEventListener("sync", (event) => { - if (event.tag === "sync-orders") { - event.waitUntil(syncOrders()); + ); + return; } -}); -async function syncOrders() { - // Implement order sync logic - console.log("Syncing orders in background..."); -} \ No newline at end of file + // API GETs (read-only data) → stale-while-revalidate + if (url.pathname.startsWith("/api/")) { + event.respondWith( + caches.open(DATA_CACHE).then((cache) => + cache.match(req).then((cached) => { + const network = fetch(req) + .then((res) => { + if (res.status === 200) cache.put(req, res.clone()); + return res; + }) + .catch(() => cached); + return cached || network; + }) + ) + ); + return; + } + + // Default: try network, fall back to cache + event.respondWith( + fetch(req).catch(() => caches.match(req)) + ); +});