forked from Timmy_Foundation/the-nexus
feat: PWA manifest + service worker for offline and install support
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
This commit is contained in:
218
sw.js
Normal file
218
sw.js
Normal file
@@ -0,0 +1,218 @@
|
||||
/**
|
||||
* 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');
|
||||
Reference in New Issue
Block a user