forked from Timmy_Foundation/the-nexus
Add full Progressive Web App support to The Nexus: - manifest.json: Web app manifest with Nexus branding, icons, and PWA config - sw.js: Service worker with cache-first strategy for local assets and stale-while-revalidate for CDN resources (Three.js, fonts) - offline.html: Styled offline fallback page with auto-reconnect - icons/: SVG icons (standard + maskable) with crystalline Nexus design - index.html: Add manifest link, theme colors, Apple PWA meta tags, and service worker registration script Features: - Works offline after first visit - Installable to home screen on mobile and desktop - Graceful degradation when offline - Auto-refresh when connection restored Fixes #14
219 lines
5.4 KiB
JavaScript
219 lines
5.4 KiB
JavaScript
/**
|
|
* The Nexus Service Worker
|
|
* Provides offline capability and home screen install support
|
|
* Strategy: Cache-first for local assets, stale-while-revalidate for CDN
|
|
*/
|
|
|
|
const CACHE_VERSION = 'nexus-v1';
|
|
const STATIC_CACHE = `${CACHE_VERSION}-static`;
|
|
const CDN_CACHE = `${CACHE_VERSION}-cdn`;
|
|
const OFFLINE_PAGE = '/offline.html';
|
|
|
|
// Core local assets that must be cached
|
|
const CORE_ASSETS = [
|
|
'/',
|
|
'/index.html',
|
|
'/style.css',
|
|
'/app.js',
|
|
'/manifest.json',
|
|
'/icons/nexus-icon.svg',
|
|
'/icons/nexus-maskable.svg',
|
|
OFFLINE_PAGE
|
|
];
|
|
|
|
// CDN resources that benefit from caching but can be stale
|
|
const CDN_PATTERNS = [
|
|
/^https:\/\/unpkg\.com/,
|
|
/^https:\/\/cdn\.jsdelivr\.net/,
|
|
/^https:\/\/fonts\.googleapis\.com/,
|
|
/^https:\/\/fonts\.gstatic\.com/,
|
|
/^https:\/\/cdn\.threejs\.org/
|
|
];
|
|
|
|
/**
|
|
* Check if a URL matches any CDN pattern
|
|
*/
|
|
function isCdnResource(url) {
|
|
return CDN_PATTERNS.some(pattern => pattern.test(url));
|
|
}
|
|
|
|
/**
|
|
* Install event - cache core assets
|
|
*/
|
|
self.addEventListener('install', (event) => {
|
|
console.log('[Nexus SW] Installing...');
|
|
|
|
event.waitUntil(
|
|
caches.open(STATIC_CACHE)
|
|
.then(cache => {
|
|
console.log('[Nexus SW] Caching core assets');
|
|
return cache.addAll(CORE_ASSETS);
|
|
})
|
|
.then(() => {
|
|
console.log('[Nexus SW] Core assets cached');
|
|
return self.skipWaiting();
|
|
})
|
|
.catch(err => {
|
|
console.error('[Nexus SW] Cache failed:', err);
|
|
})
|
|
);
|
|
});
|
|
|
|
/**
|
|
* Activate event - clean up old caches
|
|
*/
|
|
self.addEventListener('activate', (event) => {
|
|
console.log('[Nexus SW] Activating...');
|
|
|
|
event.waitUntil(
|
|
caches.keys()
|
|
.then(cacheNames => {
|
|
return Promise.all(
|
|
cacheNames
|
|
.filter(name => name.startsWith('nexus-') && name !== STATIC_CACHE && name !== CDN_CACHE)
|
|
.map(name => {
|
|
console.log('[Nexus SW] Deleting old cache:', name);
|
|
return caches.delete(name);
|
|
})
|
|
);
|
|
})
|
|
.then(() => {
|
|
console.log('[Nexus SW] Activated');
|
|
return self.clients.claim();
|
|
})
|
|
);
|
|
});
|
|
|
|
/**
|
|
* Fetch event - handle requests with appropriate strategy
|
|
*/
|
|
self.addEventListener('fetch', (event) => {
|
|
const { request } = event;
|
|
const url = new URL(request.url);
|
|
|
|
// Skip non-GET requests
|
|
if (request.method !== 'GET') {
|
|
return;
|
|
}
|
|
|
|
// Skip cross-origin requests that aren't CDN resources
|
|
if (url.origin !== self.location.origin && !isCdnResource(url.href)) {
|
|
return;
|
|
}
|
|
|
|
// Handle CDN resources with stale-while-revalidate
|
|
if (isCdnResource(url.href)) {
|
|
event.respondWith(handleCdnRequest(request));
|
|
return;
|
|
}
|
|
|
|
// Handle local assets with cache-first strategy
|
|
event.respondWith(handleLocalRequest(request));
|
|
});
|
|
|
|
/**
|
|
* Cache-first strategy for local assets
|
|
* Fastest response, updates cache in background
|
|
*/
|
|
async function handleLocalRequest(request) {
|
|
try {
|
|
const cache = await caches.open(STATIC_CACHE);
|
|
const cached = await cache.match(request);
|
|
|
|
// Return cached version immediately if available
|
|
if (cached) {
|
|
// Revalidate in background for next time
|
|
fetch(request)
|
|
.then(response => {
|
|
if (response.ok) {
|
|
cache.put(request, response.clone());
|
|
}
|
|
})
|
|
.catch(() => {
|
|
// Network failed, cached version is already being used
|
|
});
|
|
|
|
return cached;
|
|
}
|
|
|
|
// Not in cache - fetch from network
|
|
const response = await fetch(request);
|
|
|
|
if (response.ok) {
|
|
cache.put(request, response.clone());
|
|
}
|
|
|
|
return response;
|
|
} catch (error) {
|
|
console.error('[Nexus SW] Local request failed:', error);
|
|
|
|
// Return offline page for navigation requests
|
|
if (request.mode === 'navigate') {
|
|
const cache = await caches.open(STATIC_CACHE);
|
|
const offlinePage = await cache.match(OFFLINE_PAGE);
|
|
if (offlinePage) {
|
|
return offlinePage;
|
|
}
|
|
}
|
|
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Stale-while-revalidate strategy for CDN resources
|
|
* Serves stale content while updating in background
|
|
*/
|
|
async function handleCdnRequest(request) {
|
|
const cache = await caches.open(CDN_CACHE);
|
|
const cached = await cache.match(request);
|
|
|
|
// Always try to fetch fresh version
|
|
const fetchPromise = fetch(request)
|
|
.then(response => {
|
|
if (response.ok) {
|
|
cache.put(request, response.clone());
|
|
}
|
|
return response;
|
|
})
|
|
.catch(error => {
|
|
console.error('[Nexus SW] CDN fetch failed:', error);
|
|
throw error;
|
|
});
|
|
|
|
// Return cached version immediately if available, otherwise wait for network
|
|
if (cached) {
|
|
// Return stale but revalidate for next time
|
|
fetchPromise.catch(() => {}); // Swallow errors, we have cached version
|
|
return cached;
|
|
}
|
|
|
|
// No cache - must wait for network
|
|
return fetchPromise;
|
|
}
|
|
|
|
/**
|
|
* Message handler for runtime cache updates
|
|
*/
|
|
self.addEventListener('message', (event) => {
|
|
if (event.data && event.data.type === 'SKIP_WAITING') {
|
|
self.skipWaiting();
|
|
}
|
|
|
|
if (event.data && event.data.type === 'GET_VERSION') {
|
|
event.ports[0].postMessage({ version: CACHE_VERSION });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Background sync for deferred actions (future enhancement)
|
|
*/
|
|
self.addEventListener('sync', (event) => {
|
|
if (event.tag === 'nexus-sync') {
|
|
console.log('[Nexus SW] Background sync triggered');
|
|
// Future: sync chat messages, state updates, etc.
|
|
}
|
|
});
|
|
|
|
console.log('[Nexus SW] Service worker loaded');
|