Files
the-nexus-fork/sw.js
kimi e76b3eee89 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
2026-03-23 23:52:07 -04:00

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');