← Public packages
@kentcdodds/package-app-kit
Design tokens, PWA install/update, About/version, cache helpers, and optional realtime notes sync for Kody package apps.
starter-fetch/public/sw.js
238 lines · 6.2 KB · JavaScript/**
* Package-app service worker shipped via kody.app.assets (`public/sw.js`).
* Precache the fingerprinted client from GET <assetBase>/__version.json — no hashes in source.
*/
'use strict'
var HTML_PATHS = ['/', '/about']
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 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')
}
function offlineHtml() {
return new Response(
'<!doctype html><meta name="viewport" content="width=device-width, initial-scale=1"><title>Offline</title><body style="font-family:system-ui;padding:24px"><h1>Offline</h1><p>Re-open this app from Kody when you are back online.</p></body>',
{ 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 }
})
})
}
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 + '/manifest.webmanifest',
base + '/icons/icon-192.png',
base + '/icons/icon-512.png',
assetBase(base) + '/sw.js',
]
if (version && version.clientModuleUrl) urls.push(version.clientModuleUrl)
return Promise.all(
urls.map(function (url) {
return opened.cache.add(url).catch(function () {
return null
})
}),
).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)) {
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)
.then(function (response) {
if (cacheableResponse(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(
caches.match(request).then(function (cached) {
if (cached && cached.ok) return cached
return fetch(request).then(function (response) {
if (cacheableResponse(response)) {
var copy = response.clone()
openActiveCache(base).then(function (opened) {
opened.cache.put(request, copy)
})
}
return response
})
}),
)
return
}
event.respondWith(
fetch(request).catch(function () {
return caches.match(request).then(function (cached) {
return cached || Response.error()
})
}),
)
})