One Calendar is a privacy-first calendar web app built with Next.js. It has modern security features, including e2ee, password-protected sharing, and self-destructing share links ๐Ÿ“… calendar.xyehr.cn
5
fork

Configure Feed

Select the types of activity you want to include in your feed.

Merge pull request #245 from EvanTechDev/feature/improve-smart-caching-for-project

fix: disable runtime caching and force service worker updates

authored by

Evan Huang and committed by
GitHub
ddf9722b 57f54e5d

+66 -69
+49 -3
components/providers/pwa-provider.tsx
··· 6 6 useEffect(() => { 7 7 if (!('serviceWorker' in navigator)) return 8 8 9 + const clearCaches = async () => { 10 + if (!('caches' in window)) return 11 + const keys = await caches.keys() 12 + await Promise.all(keys.map((key) => caches.delete(key))) 13 + } 14 + 9 15 const registerServiceWorker = async () => { 10 16 try { 11 - await navigator.serviceWorker.register('/sw.js', { 17 + const registration = await navigator.serviceWorker.register('/sw.js', { 12 18 scope: '/', 13 19 updateViaCache: 'none', 14 20 }) 21 + 22 + await clearCaches() 23 + 24 + if (registration.waiting) { 25 + registration.waiting.postMessage({ type: 'SKIP_WAITING' }) 26 + } 27 + 28 + registration.addEventListener('updatefound', () => { 29 + const worker = registration.installing 30 + if (!worker) return 31 + 32 + worker.addEventListener('statechange', () => { 33 + if ( 34 + worker.state === 'installed' && 35 + navigator.serviceWorker.controller 36 + ) { 37 + worker.postMessage({ type: 'SKIP_WAITING' }) 38 + } 39 + }) 40 + }) 41 + 42 + navigator.serviceWorker.addEventListener('controllerchange', () => { 43 + window.location.reload() 44 + }) 45 + 46 + const updateTimer = window.setInterval(() => { 47 + registration.update().catch(() => undefined) 48 + }, 60 * 1000) 49 + 50 + return () => { 51 + window.clearInterval(updateTimer) 52 + } 15 53 } catch { 16 - // Silent fail to avoid breaking the app on unsupported browsers. 54 + return undefined 17 55 } 18 56 } 19 57 20 - registerServiceWorker() 58 + let cleanup: (() => void) | undefined 59 + 60 + registerServiceWorker().then((fn) => { 61 + cleanup = fn 62 + }) 63 + 64 + return () => { 65 + cleanup?.() 66 + } 21 67 }, []) 22 68 23 69 return null
+17 -66
public/sw.js
··· 1 - const CACHE_NAME = 'one-calendar-shell-v3' 2 - const OFFLINE_URLS = ['/app', '/icon.svg'] 3 - const STATIC_PATH_PREFIXES = ['/_next/static/', '/_next/image/', '/icons/'] 4 - const STATIC_FILE_PATTERN = 5 - /\.(?:js|css|png|jpg|jpeg|gif|svg|webp|ico|woff|woff2|ttf|otf|eot|json|txt|xml|webmanifest)$/i 1 + const SW_VERSION = self.registration?.scope 6 2 7 - function shouldCacheRequest(requestUrl) { 8 - if (requestUrl.origin !== self.location.origin) return false 9 - if (OFFLINE_URLS.includes(requestUrl.pathname)) return true 10 - if ( 11 - STATIC_PATH_PREFIXES.some((prefix) => 12 - requestUrl.pathname.startsWith(prefix), 13 - ) 14 - ) { 15 - return true 16 - } 17 - return STATIC_FILE_PATTERN.test(requestUrl.pathname) 3 + const clearAllCaches = async () => { 4 + const keys = await caches.keys() 5 + await Promise.all(keys.map((key) => caches.delete(key))) 18 6 } 19 7 20 8 self.addEventListener('install', (event) => { 21 - event.waitUntil( 22 - caches 23 - .open(CACHE_NAME) 24 - .then((cache) => cache.addAll(OFFLINE_URLS)) 25 - .then(() => self.skipWaiting()), 26 - ) 9 + event.waitUntil(self.skipWaiting()) 27 10 }) 28 11 29 12 self.addEventListener('activate', (event) => { 30 13 event.waitUntil( 31 - caches 32 - .keys() 33 - .then((keys) => 34 - Promise.all( 35 - keys 36 - .filter((key) => key !== CACHE_NAME) 37 - .map((key) => caches.delete(key)), 14 + clearAllCaches().then(async () => { 15 + await self.clients.claim() 16 + const clients = await self.clients.matchAll({ 17 + type: 'window', 18 + includeUncontrolled: true, 19 + }) 20 + await Promise.all( 21 + clients.map((client) => 22 + client.postMessage({ type: 'SW_ACTIVATED', version: SW_VERSION }), 38 23 ), 39 - ), 24 + ) 25 + }), 40 26 ) 41 - 42 - self.clients.claim() 43 27 }) 44 28 45 29 self.addEventListener('message', (event) => { ··· 55 39 if (requestUrl.protocol !== 'http:' && requestUrl.protocol !== 'https:') { 56 40 return 57 41 } 58 - if (requestUrl.pathname.startsWith('/api/')) { 59 - event.respondWith(fetch(event.request)) 60 - return 61 - } 62 - if (!shouldCacheRequest(requestUrl)) { 63 - event.respondWith(fetch(event.request)) 64 - return 65 - } 66 42 67 - event.respondWith( 68 - caches.match(event.request).then((cachedResponse) => { 69 - if (cachedResponse) { 70 - return cachedResponse 71 - } 72 - 73 - return fetch(event.request) 74 - .then((networkResponse) => { 75 - if (!networkResponse || networkResponse.status !== 200) { 76 - return networkResponse 77 - } 78 - 79 - if (networkResponse.type !== 'basic') { 80 - return networkResponse 81 - } 82 - 83 - const responseToCache = networkResponse.clone() 84 - caches.open(CACHE_NAME).then((cache) => { 85 - cache.put(event.request, responseToCache).catch(() => undefined) 86 - }) 87 - 88 - return networkResponse 89 - }) 90 - .catch(() => caches.match('/app')) 91 - }), 92 - ) 43 + event.respondWith(fetch(event.request)) 93 44 }) 94 45 95 46 self.addEventListener('notificationclick', (event) => {