1// CANONIC service worker â Tier A offline cache. 2// Per SERVICES/APP/CANON.md § OFFLINE_CACHE. 3// 4// Strategy: 5// - App shell (/, /offline, /icon.svg, /manifest.webmanifest): cache-first. 6// - Governed JSON routes (/api/canon/*, /api/galaxy/*): network-first with 7// cached fallback so fresh-on-connect / safe-on-plane. 8// - Everything else: pass-through to network. 9// 10// Version string invalidates cache on deploy â bump when shell changes. 11 12const CACHE_VERSION = 'canonic-v1-2026-04-24' 13const SHELL_CACHE = `${CACHE_VERSION}-shell` 14const API_CACHE = `${CACHE_VERSION}-api` 15 16const SHELL_URLS = [ 17 '/', 18 '/icon.svg', 19 '/manifest.webmanifest', 20] 21 22const API_PREFIXES = ['/api/canon/', '/api/galaxy/'] 23 24self.addEventListener('install', (event) => { 25 event.waitUntil( 26 caches.open(SHELL_CACHE).then((cache) => cache.addAll(SHELL_URLS)) 27 .then(() => self.skipWaiting()) 28 ) 29}) 30 31self.addEventListener('activate', (event) => { 32 event.waitUntil( 33 caches.keys().then((keys) => 34 Promise.all( 35 keys 36 .filter((k) => !k.startsWith(CACHE_VERSION)) 37 .map((k) => caches.delete(k)) 38 ) 39 ).then(() => self.clients.claim()) 40 ) 41}) 42 43function isGovernedApi(url) { 44 return API_PREFIXES.some((p) => url.pathname.startsWith(p)) 45} 46 47function offlineShellResponse() { 48 return new Response( 49 '<!doctype html><html><head><meta charset="utf-8">' + 50 '<title>CANONIC â offline</title>' + 51 '<meta name="viewport" content="width=device-width,initial-scale=1">' + 52 // Self-contained offline page â :root holds the governed palette SOT; every 53 // rule below references var(--â¦). No globals.css here (sw scope). color-allow below. 54 '<style>:root{--bg:#fff;--fg:#111;--muted:#555;--code-bg:#f4f4f4}' + // color-allow:self-contained-offline-page-palette-sot 55 'body{font-family:Georgia,serif;background:var(--bg);color:var(--fg);' + 56 'display:flex;align-items:center;justify-content:center;min-height:100vh;' + 57 'margin:0;text-align:center;padding:2rem}h1{font-size:2rem;margin:0 0 .5rem}' + 58 'p{color:var(--muted);max-width:32rem}code{background:var(--code-bg);padding:.15rem .35rem}</style>' + 59 '</head><body><div><h1>â© CANONIC</h1>' + 60 '<p>You\'re offline. Cached governance is still available â navigate back, ' + 61 'or reconnect to refresh. Service worker: <code>' + CACHE_VERSION + '</code></p>' + 62 '</div></body></html>', 63 { status: 200, headers: { 'content-type': 'text/html; charset=utf-8' } } 64 ) 65} 66 67self.addEventListener('fetch', (event) => { 68 const req = event.request 69 if (req.method !== 'GET') return 70 const url = new URL(req.url) 71 if (url.origin !== self.location.origin) return 72 73 // Governed JSON routes â network-first, fall back to cache, then empty 503. 74 if (isGovernedApi(url)) { 75 event.respondWith( 76 fetch(req) 77 .then((res) => { 78 if (res.ok) { 79 const copy = res.clone() 80 caches.open(API_CACHE).then((cache) => cache.put(req, copy)) 81 } 82 return res 83 }) 84 .catch(() => 85 caches.match(req).then((cached) => 86 cached || 87 new Response( 88 JSON.stringify({ offline: true, cached: false }), 89 { status: 503, headers: { 'content-type': 'application/json' } } 90 ) 91 ) 92 ) 93 ) 94 return 95 } 96 97 // Navigations â cache-first shell, network as refresh, offline fallback. 98 if (req.mode === 'navigate') { 99 event.respondWith( 100 caches.match(req).then((cached) => { 101 const fetchPromise = fetch(req) 102 .then((res) => { 103 if (res.ok) { 104 const copy = res.clone() 105 caches.open(SHELL_CACHE).then((cache) => cache.put(req, copy)) 106 } 107 return res 108 }) 109 .catch(() => 110 caches.match('/').then((root) => root || offlineShellResponse()) 111 ) 112 return cached || fetchPromise 113 }) 114 ) 115 return 116 } 117 118 // Static shell assets â cache-first. 119 if (SHELL_URLS.includes(url.pathname)) { 120 event.respondWith( 121 caches.match(req).then((cached) => cached || fetch(req)) 122 ) 123 } 124})
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.