grid-apps-cmms/web/boot/service.js

240 lines
6.8 KiB
JavaScript
Raw Permalink Normal View History

2025-10-12 19:31:14 -04:00
// --- configurable ---
2025-11-12 16:41:42 -05:00
const CACHE_VERSION = 'boot';
2025-10-12 19:31:14 -04:00
const VERSION_KEY = '/__version__';
2025-11-12 09:53:38 -05:00
const cacheOpen = caches.open(CACHE_VERSION);
2025-10-12 19:31:14 -04:00
let loaded = 0;
2025-11-12 09:53:38 -05:00
let debug = false;
2025-10-12 19:31:14 -04:00
let mode = 'cached';
let BUNDLE_URL = '/boot/bundle.bin';
2025-10-12 19:31:14 -04:00
// --- logging ---
const log = (...a) => console.log('[SW]', ...a);
2025-11-12 09:53:38 -05:00
log('cache version', CACHE_VERSION);
2025-10-12 19:31:14 -04:00
// --- lifecycle ---
2025-11-12 16:41:42 -05:00
self.addEventListener('install', _install);
self.addEventListener('activate', _activate);
self.addEventListener('fetch', _fetch);
2025-10-12 19:31:14 -04:00
self.skipWaiting();
// --- mode control ---
self.addEventListener('message', e => {
const data = e.data || {};
2025-11-12 16:41:42 -05:00
const { clear, disable, version } = data;
2025-11-12 09:53:38 -05:00
debug = data.debug ?? debug;
if (clear) {
clearCache();
}
if (disable) {
mode = 'transparent';
broadcast('transparent mode');
unregister();
} else if (version !== undefined) {
mode = 'cached';
BUNDLE_URL = `/boot/bundle-${version}.bin`;
log('version', version);
ensureVersion(version);
2025-11-12 16:41:42 -05:00
} else if (data.mode) {
mode = data.mode;
2025-10-12 19:31:14 -04:00
}
});
2025-11-12 16:41:42 -05:00
async function _install(e) {
log('install');
if (e.addRoutes) {
2026-02-14 19:00:48 -05:00
for (let pre of ["font","mesh","kiri","void","lib","wasm"]) {
2025-11-12 16:41:42 -05:00
e.addRoutes({
condition: { urlPattern: new URLPattern({ pathname: `/${pre}/.*` }) },
source: { cacheName: CACHE_VERSION }
});
}
}
}
async function _activate(event) {
log('activate');
event.waitUntil(clients.claim())
}
// --- unregister current service worker ---
async function unregister() {
await self.registration.unregister();
}
2025-11-12 16:41:42 -05:00
// --- cache helpers ---
async function cacheGetText(key) {
const cache = await cacheOpen;
const entry = await cache.match(key);
if (entry) {
return entry.text();
} else {
return undefined;
}
}
async function cachePutText(key, value) {
const cache = await cacheOpen;
return cache.put( key, new Response(String(value), {
headers: { 'Content-Type': 'text/plain' }
}) );
}
async function clearCache() {
log('clearing cache');
await caches.delete(CACHE_VERSION);
}
2025-10-12 19:31:14 -04:00
// --- version check and preload ---
async function ensureVersion(incomingVersion) {
2025-11-12 09:53:38 -05:00
const cache = await cacheOpen;
2025-10-12 19:31:14 -04:00
let needPreload = false;
2025-11-12 16:41:42 -05:00
const existing = await cacheGetText(VERSION_KEY);
2025-10-12 19:31:14 -04:00
if (!existing) {
log('no version stored → preload');
needPreload = true;
} else {
2025-11-12 16:41:42 -05:00
// const stored = await existing.text();
if (existing !== incomingVersion) {
log(`mismatch ${existing}${incomingVersion} → preload`);
2025-10-12 19:31:14 -04:00
needPreload = true;
}
}
if (needPreload) {
await preloadBundle();
2025-11-12 16:41:42 -05:00
await cachePutText(VERSION_KEY, incomingVersion);
broadcast(`preload added ${loaded} files`);
2025-10-12 19:31:14 -04:00
} else {
broadcast('preload cached');
2025-10-12 19:31:14 -04:00
}
}
// --- fetch with redirect handler ---
async function fetch_safe(req) {
const res = await fetch(req);
if (res.redirected) {
2025-11-12 09:53:38 -05:00
log({ redirect_clone: res.url });
2025-10-12 19:31:14 -04:00
// clone into a fresh non-redirect response
const clone = res.clone();
return new Response(await clone.blob(), {
headers: clone.headers,
status: 200,
statusText: 'OK'
});
}
return res;
}
// --- fetch handler ---
2025-11-12 16:41:42 -05:00
async function _fetch(e) {
2025-10-12 19:31:14 -04:00
const { request } = e;
const url = new URL(request.url);
2025-10-14 02:07:49 -04:00
2025-11-12 16:41:42 -05:00
if (debug) log({ method: request.method, url: request.url });
2025-10-12 19:31:14 -04:00
if (request.method !== 'GET') return;
if (mode == 'transparent') {
2025-11-12 09:53:38 -05:00
e.respondWith(fetch(e.request));
2025-10-12 19:31:14 -04:00
return;
}
2025-10-14 16:00:16 -04:00
if (url.pathname.endsWith("/")) {
return e.respondWith(redirectToUrl(appendURL(url, 'index.html').pathname, request));
} else if (url.pathname.indexOf(".") < 0 || url.pathname.endsWith("/boot")) {
return e.respondWith(redirectToUrl(appendURL(url, '/index.html').pathname, request));
2025-10-14 16:00:16 -04:00
} else {
e.respondWith(fromCacheOrNetwork(request));
}
2025-11-12 16:41:42 -05:00
}
2025-10-12 19:31:14 -04:00
// --- helpers ---
function appendURL(url, append) {
return new URL(url.origin + url.pathname + append + url.search);
}
async function redirectToUrl(path, request) {
if (debug) log('REDIRECT', path);
return Response.redirect(path, 302);
}
2025-10-16 21:39:24 -04:00
async function fromCacheOrNetwork(req) {
2025-11-12 09:53:38 -05:00
const cache = await cacheOpen;
const hit = await cache.match(req, { ignoreSearch: true });
2025-10-12 19:31:14 -04:00
if (hit) {
2025-11-12 16:41:42 -05:00
if (debug) log('CACHE HIT', req.url);
2025-10-12 19:31:14 -04:00
return hit;
}
2025-11-12 16:41:42 -05:00
if (debug) log('CACHE MISS', req.url);
2025-10-12 19:31:14 -04:00
const net = await fetch_safe(req);
cache.put(req, net.clone());
log(`put: ${net.url}`);
2025-10-12 19:31:14 -04:00
return net;
}
async function broadcast(message) {
const clientsList = await clients.matchAll({ type: 'window' });
for (const client of clientsList) {
client.postMessage(message);
}
}
const sec_headers = {
'Cross-Origin-Opener-Policy': 'same-origin',
'Cross-Origin-Embedder-Policy': 'require-corp'
};
// --- preload & unpack bundle ---
async function preloadBundle() {
loaded = 0;
2025-11-12 09:53:38 -05:00
const cache = await cacheOpen;
2025-10-12 19:31:14 -04:00
const res = await fetch(BUNDLE_URL, { cache: 'no-store' });
const buf = await res.arrayBuffer();
const files = await unpackBundle(buf);
2026-02-01 11:09:34 -05:00
const total = Object.keys(files).length;
2025-10-12 19:31:14 -04:00
await Promise.all(
Object.entries(files).map(([path, blob]) => {
2026-02-01 11:09:34 -05:00
broadcast({ progress: loaded/total });
2025-10-12 19:31:14 -04:00
const ext = path.split('.').pop();
const type =
ext === 'html' ? 'text/html' :
ext === 'js' ? 'application/javascript' :
ext === 'css' ? 'text/css' :
ext === 'json' ? 'application/json' :
ext === 'wasm' ? 'application/wasm' :
ext === 'svg' ? 'image/svg+xml' :
'application/octet-stream';
const headers = { 'Content-Type': type, ...sec_headers };
const resp = new Response(blob, { headers });
loaded++;
2025-10-12 19:31:14 -04:00
return cache.put('/' + path, resp);
})
);
}
// --- simple bundle format ---
// [count:uint32][entries...][file data...]
// entry: [nlen:uint16][name][offset:uint32][length:uint32]
async function unpackBundle(buf) {
const view = new DataView(buf);
let pos = 0;
const count = view.getUint32(pos, true); pos += 4;
const decoder = new TextDecoder();
const table = [];
for (let i = 0; i < count; i++) {
const nlen = view.getUint16(pos, true); pos += 2;
const name = decoder.decode(new Uint8Array(buf, pos, nlen)); pos += nlen;
const offset = view.getUint32(pos, true); pos += 4;
const len = view.getUint32(pos, true); pos += 4;
table.push({ name, offset, len });
}
const files = {};
for (const { name, offset, len } of table)
files[name] = buf.slice(offset, offset + len);
return files;
}