Files
giles 8f88e52b27
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 6m57s
Add DOM primitives (dom-set-prop, dom-call-method, dom-post-message), bump SW cache v2, remove video demo
New platform_js primitives for direct DOM property/method access and
cross-origin iframe communication. Service worker static cache bumped
to v2 to flush stale assets. Removed experimental video embed from
header island, routes, and home page.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 21:51:05 +00:00

220 lines
7.0 KiB
JavaScript

// ==========================================================================
// sx-sw.js — SX Service Worker
//
// Provides offline-capable caching for the SX platform:
// /sx/data/* — Network-first, IndexedDB fallback (page data)
// /sx/io/* — Network-first, IndexedDB fallback (IO proxy)
// /static/* — Stale-while-revalidate (CSS, JS, images)
// Other — Network-only (pass through)
//
// IndexedDB stores response text + content-type with timestamps.
// Stale IDB entries are served when offline but refreshed when online.
// ==========================================================================
var IDB_NAME = "sx-offline";
var IDB_VERSION = 1;
var IDB_STORE = "responses";
var STATIC_CACHE = "sx-static-v2";
// ---------------------------------------------------------------------------
// IndexedDB helpers
// ---------------------------------------------------------------------------
function openDB() {
return new Promise(function(resolve, reject) {
var req = indexedDB.open(IDB_NAME, IDB_VERSION);
req.onupgradeneeded = function() {
if (!req.result.objectStoreNames.contains(IDB_STORE)) {
req.result.createObjectStore(IDB_STORE);
}
};
req.onsuccess = function() { resolve(req.result); };
req.onerror = function() { reject(req.error); };
});
}
function idbGet(key) {
return openDB().then(function(db) {
return new Promise(function(resolve, reject) {
var tx = db.transaction(IDB_STORE, "readonly");
var req = tx.objectStore(IDB_STORE).get(key);
req.onsuccess = function() { resolve(req.result || null); };
req.onerror = function() { reject(req.error); };
});
});
}
function idbSet(key, value) {
return openDB().then(function(db) {
return new Promise(function(resolve, reject) {
var tx = db.transaction(IDB_STORE, "readwrite");
tx.objectStore(IDB_STORE).put(value, key);
tx.oncomplete = function() { resolve(); };
tx.onerror = function() { reject(tx.error); };
});
});
}
function idbDelete(key) {
return openDB().then(function(db) {
return new Promise(function(resolve, reject) {
var tx = db.transaction(IDB_STORE, "readwrite");
tx.objectStore(IDB_STORE).delete(key);
tx.oncomplete = function() { resolve(); };
tx.onerror = function() { reject(tx.error); };
});
});
}
function idbClear() {
return openDB().then(function(db) {
return new Promise(function(resolve, reject) {
var tx = db.transaction(IDB_STORE, "readwrite");
tx.objectStore(IDB_STORE).clear();
tx.oncomplete = function() { resolve(); };
tx.onerror = function() { reject(tx.error); };
});
});
}
// ---------------------------------------------------------------------------
// Fetch strategies
// ---------------------------------------------------------------------------
// Network-first with IndexedDB fallback.
// Used for /sx/data/ and /sx/io/ — dynamic data that should be fresh
// when online but available from cache when offline.
function networkFirstIDB(request) {
var key = request.url;
return fetch(request.clone()).then(function(resp) {
if (resp.ok) {
var ct = resp.headers.get("Content-Type") || "text/sx";
resp.clone().text().then(function(text) {
idbSet(key, { text: text, contentType: ct, ts: Date.now() });
});
}
return resp;
}).catch(function() {
// Network failed — try IndexedDB
return idbGet(key).then(function(cached) {
if (cached) {
return new Response(cached.text, {
status: 200,
headers: {
"Content-Type": cached.contentType || "text/sx",
"X-SX-Offline": "1",
"X-SX-Cached-At": String(cached.ts)
}
});
}
return new Response("", { status: 503, statusText: "Offline" });
});
});
}
// Stale-while-revalidate for static assets.
// Serve from Cache API immediately, update in background.
function staleWhileRevalidate(request) {
return caches.open(STATIC_CACHE).then(function(cache) {
return cache.match(request).then(function(cached) {
var fetchPromise = fetch(request.clone()).then(function(resp) {
if (resp.ok) {
cache.put(request, resp.clone());
}
return resp;
}).catch(function() {
return cached || new Response("", { status: 503 });
});
return cached || fetchPromise;
});
});
}
// ---------------------------------------------------------------------------
// Service Worker lifecycle
// ---------------------------------------------------------------------------
self.addEventListener("install", function(event) {
// Activate immediately — don't wait for old SW to stop
self.skipWaiting();
});
self.addEventListener("activate", function(event) {
// Claim all clients immediately
event.waitUntil(
Promise.all([
self.clients.claim(),
// Clean old static caches
caches.keys().then(function(names) {
return Promise.all(
names.filter(function(n) { return n !== STATIC_CACHE; })
.map(function(n) { return caches.delete(n); })
);
})
])
);
});
// ---------------------------------------------------------------------------
// Fetch routing
// ---------------------------------------------------------------------------
self.addEventListener("fetch", function(event) {
var url = new URL(event.request.url);
// Only handle same-origin GET requests
if (event.request.method !== "GET") return;
if (url.origin !== self.location.origin) return;
// SX data endpoints — network-first with IDB fallback
if (url.pathname.indexOf("/sx/data/") === 0 ||
url.pathname.indexOf("/sx/io/") === 0) {
event.respondWith(networkFirstIDB(event.request));
return;
}
// Static assets — stale-while-revalidate
if (url.pathname.indexOf("/static/") === 0) {
event.respondWith(staleWhileRevalidate(event.request));
return;
}
// Everything else: pass through to network (default behavior)
});
// ---------------------------------------------------------------------------
// Message handling — cache control from main thread
// ---------------------------------------------------------------------------
self.addEventListener("message", function(event) {
var msg = event.data;
if (!msg || !msg.type) return;
if (msg.type === "invalidate") {
// Invalidate specific page data from IDB
if (msg.page === "*") {
idbClear();
} else if (msg.page) {
// Remove all IDB entries matching this page
openDB().then(function(db) {
var tx = db.transaction(IDB_STORE, "readwrite");
var store = tx.objectStore(IDB_STORE);
var req = store.openCursor();
req.onsuccess = function() {
var cursor = req.result;
if (cursor) {
if (cursor.key.indexOf("/sx/data/" + msg.page) >= 0) {
cursor.delete();
}
cursor.continue();
}
};
});
}
}
if (msg.type === "clear-static") {
caches.delete(STATIC_CACHE);
}
});