/** * Package-app service worker shipped via kody.app.assets (`public/sw.js`). * Precache the fingerprinted client from GET /__version.json — no hashes in source. * Brand revision: kody-racer list/PWA mark (v4) — image/* cache guard + handoff-aware precache. */ 'use strict' var HTML_PATHS = ['/', '/about', '/notes'] function appBase() { try { return new URL(self.registration.scope).pathname.replace(/\/$/, '') } catch (e) { return self.location.pathname .replace(/\/_assets\/sw\.js$/, '') .replace(/\/sw\.js$/, '') } } function assetBase(base) { return base + '/_assets' } function sameOrigin(url) { return url.origin === self.location.origin } function isApi(url, base) { return url.pathname.indexOf(base + '/api/') === 0 } function isManifest(url) { return /manifest\.webmanifest$/.test(url.pathname) } function isStatic(url) { return !isManifest(url) && /\.(png|svg|ico|webp|jpg|jpeg)$/.test(url.pathname) } function isClientAsset(url) { return /\/_assets\/client\.[^/]+\.js$/.test(url.pathname) } function isImagePath(url) { return /\.(png|svg|ico|webp|jpg|jpeg)$/.test(url.pathname) } function contentType(response) { return String(response && response.headers && response.headers.get('content-type') || '') .toLowerCase() .split(';')[0] .trim() } function isImageContentType(response) { return contentType(response).indexOf('image/') === 0 } function isHandoffRequired(response) { if (!response) return false if (response.status === 403) return true try { return String(response.headers.get('x-kody-handoff') || '').toLowerCase() === 'required' } catch (e) { return false } } function relativePath(url, base) { var path = url.pathname if (path === base || path === base + '/') return '/' if (path.indexOf(base + '/') === 0) { var rel = path.slice(base.length) if (rel.length > 1 && rel.charAt(rel.length - 1) === '/') rel = rel.slice(0, -1) return rel || '/' } return path } function isHtmlPage(url, base) { return HTML_PATHS.indexOf(relativePath(url, base)) !== -1 } function cacheableResponse(response) { return Boolean(response && response.ok && response.type !== 'opaque' && response.type !== 'error') } /** Never cache 403 handoff HTML (or non-image bodies) as static image assets. */ function cacheableStaticResponse(url, response) { if (!cacheableResponse(response) || isHandoffRequired(response)) return false if (isImagePath(url)) return isImageContentType(response) return true } function offlineHtml() { return new Response( 'Offline

Offline

Re-open this app from Kody when you are back online.

', { status: 503, headers: { 'content-type': 'text/html; charset=utf-8' } }, ) } function matchShell(cache, request) { return cache.match(request).then(function (cached) { if (cached) return cached var url = new URL(request.url) var alt = url.pathname.endsWith('/') ? url.origin + url.pathname.replace(/\/$/, '') + url.search : url.origin + url.pathname + '/' + url.search return cache.match(alt) }) } function activeCacheName(base) { return fetch(assetBase(base) + '/__version.json', { cache: 'no-store', credentials: 'same-origin', }) .then(function (res) { if (!res.ok) throw new Error('version ' + res.status) return res.json() }) .then(function (version) { return { name: version.publishedCommit ? 'pak-' + version.publishedCommit : 'pak-fallback', version: version, } }) .catch(function () { return { name: 'pak-fallback', version: null } }) } function openActiveCache(base) { return activeCacheName(base).then(function (info) { return caches.open(info.name).then(function (cache) { return { cache: cache, info: info } }) }) } /** Precache brand/icons only when the network returns a real image/* body. */ function precacheImage(cache, url) { return fetch(url, { credentials: 'same-origin', cache: 'no-store' }).then(function (response) { if (!cacheableStaticResponse(new URL(url, self.location.origin), response)) { console.warn('[pak-sw] skip precache (not image/* or handoff):', url, response && response.status) return false } return cache.put(url, response).then(function () { return true }) }) } function precacheUrl(cache, url) { if (isImagePath(new URL(url, self.location.origin))) { return precacheImage(cache, url) } return cache.add(url).then(function () { return true }).catch(function () { return false }) } self.addEventListener('install', function (event) { event.waitUntil( openActiveCache(appBase()).then(function (opened) { var base = appBase() var version = opened.info.version var urls = [ base + '/', base + '/about', base + '/notes', base + '/manifest.webmanifest', base + '/icons/icon-192.png?v=4', base + '/icons/icon-512.png?v=4', assetBase(base) + '/sw.js', assetBase(base) + '/styles.css', ] if (version && version.clientModuleUrl) urls.push(version.clientModuleUrl) return Promise.all( urls.map(function (url) { return precacheUrl(opened.cache, url) }), ).then(function () { return self.skipWaiting() }) }), ) }) self.addEventListener('message', function (event) { if (event.data === 'SKIP_WAITING') self.skipWaiting() }) self.addEventListener('activate', function (event) { event.waitUntil( activeCacheName(appBase()) .then(function (info) { return caches.keys().then(function (keys) { return Promise.all( keys.map(function (key) { if (key === info.name) return null if (key.indexOf('pak-') === 0) return caches.delete(key) if (key.indexOf('package-app-kit-demo-') === 0) return caches.delete(key) return null }), ) }) }) .then(function () { return self.clients.claim() }), ) }) self.addEventListener('fetch', function (event) { var request = event.request if (request.method !== 'GET') return var url = new URL(request.url) var base = appBase() if (!sameOrigin(url) || isApi(url, base)) return if (isManifest(url)) { event.respondWith( fetch(request, { credentials: 'same-origin', cache: 'no-store' }) .then(function (response) { if (cacheableResponse(response) && !isHandoffRequired(response)) { var copy = response.clone() openActiveCache(base).then(function (opened) { opened.cache.put(request, copy) }) } return response }) .catch(function () { return caches.match(request).then(function (cached) { return cached && cached.ok ? cached : Response.error() }) }), ) return } if (request.mode === 'navigate' || isHtmlPage(url, base)) { event.respondWith( openActiveCache(base).then(function (opened) { return matchShell(opened.cache, request).then(function (cached) { var network = fetch(request, { credentials: 'same-origin' }) .then(function (response) { if (cacheableResponse(response) && !isHandoffRequired(response)) { opened.cache.put(request, response.clone()) var sibling = url.pathname.endsWith('/') ? url.origin + url.pathname.replace(/\/$/, '') + url.search : url.origin + url.pathname + '/' + url.search opened.cache.put(sibling, response.clone()) } return response }) .catch(function () { return cached || offlineHtml() }) return cached || network }) }), ) return } if (isStatic(url) || isClientAsset(url)) { event.respondWith( fetch(request, { credentials: 'same-origin' }) .then(function (response) { if (cacheableStaticResponse(url, response)) { var copy = response.clone() openActiveCache(base).then(function (opened) { opened.cache.put(request, copy) }) } return response }) .catch(function () { return caches.match(request).then(function (cached) { if (!cached || !cached.ok) return Response.error() if (isImagePath(url) && !isImageContentType(cached)) return Response.error() return cached }) }), ) return } event.respondWith( fetch(request, { credentials: 'same-origin' }).catch(function () { return caches.match(request).then(function (cached) { return cached || Response.error() }) }), ) })