Some checks failed
CI / validate (pull_request) Has been cancelled
Adds full Progressive Web App support to The Nexus: - 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/nexus-icon.svg: Nexus crystal sigil icon (SVG) - icons/nexus-maskable.svg: Maskable icon for adaptive shapes - manifest.json: Complete PWA manifest with theme color #4af0c0, standalone display mode, shortcuts, and icon definitions - index.html: Service worker registration, Apple PWA meta tags, theme colors, and MS application config The Nexus now works offline after first visit and can be installed to home screen on mobile and desktop devices. 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');
|