The service worker now caches the app shell: page loads are network first with the last index.html as fallback, hashed bundles come from the cache, icons and manifest are refreshed in the background; API calls are never intercepted. GET answers are kept per user in IndexedDB and served when the server cannot be reached, so the app starts logged in with the last known thoughts, tasks and lists. Captured thoughts and checked tasks or list items go to an outbox that is sent in order when the connection returns (also at start and every 30 s). A banner shows offline state and waiting changes. Logging out clears cache and outbox. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
97 lines
3.3 KiB
JavaScript
97 lines
3.3 KiB
JavaScript
// MindSift service worker: keeps the app shell so the PWA also starts without network.
|
|
// - Page loads (navigation): network first, so a new release is picked up right away; offline the
|
|
// last loaded index.html is used.
|
|
// - Built bundles (/assets/*, file names contain a content hash): from the cache, loaded once.
|
|
// - Icons and manifest: from the cache, refreshed in the background.
|
|
// - API requests are never touched; offline data comes from the app's own cache (lib/offline.ts).
|
|
// Raise VERSION when this file changes in a way that needs fresh caches.
|
|
const VERSION = 'v2';
|
|
const SHELL_CACHE = `mindsift-shell-${VERSION}`;
|
|
const ASSET_CACHE = `mindsift-assets-${VERSION}`;
|
|
const MAX_ASSETS = 80;
|
|
const SHELL_URLS = [
|
|
'/',
|
|
'/index.html',
|
|
'/manifest.json',
|
|
'/favicon.ico',
|
|
'/icon.svg',
|
|
'/icon-192x192.png',
|
|
'/icon-512x512.png',
|
|
'/apple-touch-icon.png',
|
|
];
|
|
|
|
self.addEventListener('install', (event) => {
|
|
event.waitUntil(
|
|
caches
|
|
.open(SHELL_CACHE)
|
|
.then((cache) => Promise.all(SHELL_URLS.map((url) => cache.add(new Request(url, { cache: 'reload' })).catch(() => {}))))
|
|
.then(() => self.skipWaiting())
|
|
);
|
|
});
|
|
|
|
self.addEventListener('activate', (event) => {
|
|
event.waitUntil(
|
|
caches
|
|
.keys()
|
|
.then((keys) => Promise.all(keys.filter((k) => k !== SHELL_CACHE && k !== ASSET_CACHE).map((k) => caches.delete(k))))
|
|
.then(() => self.clients.claim())
|
|
);
|
|
});
|
|
|
|
// Old bundles pile up with every release; keep only the newest entries
|
|
async function trimAssets() {
|
|
const cache = await caches.open(ASSET_CACHE);
|
|
const keys = await cache.keys();
|
|
await Promise.all(keys.slice(0, Math.max(0, keys.length - MAX_ASSETS)).map((k) => cache.delete(k)));
|
|
}
|
|
|
|
async function networkFirstPage(request) {
|
|
const cache = await caches.open(SHELL_CACHE);
|
|
try {
|
|
const response = await fetch(request, { cache: 'no-store' });
|
|
if (response.ok && response.type === 'basic') cache.put('/index.html', response.clone());
|
|
return response;
|
|
} catch {
|
|
return (await cache.match('/index.html')) || (await cache.match('/')) || Response.error();
|
|
}
|
|
}
|
|
|
|
async function cacheFirstAsset(request) {
|
|
const cache = await caches.open(ASSET_CACHE);
|
|
const hit = await cache.match(request);
|
|
if (hit) return hit;
|
|
const response = await fetch(request);
|
|
if (response.ok && response.type === 'basic') {
|
|
await cache.put(request, response.clone());
|
|
trimAssets();
|
|
}
|
|
return response;
|
|
}
|
|
|
|
async function staleWhileRevalidate(request) {
|
|
const cache = await caches.open(SHELL_CACHE);
|
|
const hit = await cache.match(request);
|
|
const refresh = fetch(request)
|
|
.then((response) => {
|
|
if (response.ok && response.type === 'basic') cache.put(request, response.clone());
|
|
return response;
|
|
})
|
|
.catch(() => hit || Response.error());
|
|
return hit || refresh;
|
|
}
|
|
|
|
self.addEventListener('fetch', (event) => {
|
|
const { request } = event;
|
|
if (request.method !== 'GET') return;
|
|
const url = new URL(request.url);
|
|
if (url.origin !== self.location.origin || url.pathname.startsWith('/api/')) return;
|
|
|
|
if (request.mode === 'navigate') {
|
|
event.respondWith(networkFirstPage(request));
|
|
} else if (url.pathname.startsWith('/assets/')) {
|
|
event.respondWith(cacheFirstAsset(request));
|
|
} else if (SHELL_URLS.includes(url.pathname)) {
|
|
event.respondWith(staleWhileRevalidate(request));
|
|
}
|
|
});
|