Files
the-nexus/sw.js
kimi 5d2d8a12bc
Some checks failed
CI / validate (pull_request) Has been cancelled
feat: PWA manifest + service worker for offline and install support
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
2026-03-23 23:58:37 -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');