Sam's Website Propeller Hat Icon

A Very Simple Service Worker

Here’s a recent example of how I got Clinic Helper to work offline in less than fifty lines of JavaScript, no fancy-pants libraries, and no build steps.

let version = "1"
let cacheName = `v${version}_data`
let cachedAssetPaths = [
    // …
]

// install (pre-activation) event
async function install() {
    let cache = await caches.open(cacheName)

    for (let p of cachedAssetPaths) {
        try {
            await cache.add(p)
        } catch ({name, message}) {
            console.error(`SW ${version}: failed to pre-download asset @ ${p}`)
        } finally {
            continue
        }
    }
}
self.addEventListener('install', event => {
    self.skipWaiting()
    event.waitUntil(install())
})

// activation (post-installation) event
async function activate() {
    let allCaches = await caches.keys()
    let badCaches = allCaches.filter((key) => { return key != cacheName })
    for (let c of badCaches) {
        caches.delete(c)
    }
    await self.clients.claim()
}
self.addEventListener('activate', (e) => {
    e.waitUntil(activate())
})

// fetch (every request)
self.addEventListener('fetch', async (e) => {
    // https://stackoverflow.com/a/49719964
    if (e.request.cache === 'only-if-cached' && e.request.mode !== 'same-origin') return

    // match in cache
    let match = await caches.match(e.request)
    if (match) e.respondWith(match)

    // fall back to network
    e.respondWith(await fetch(e.request))
})

There she is in all her glory. Forty nine measly lines.

To get it plumbed in to your site, add this to your index.html:

<script>
if ('serviceWorker' in navigator) {
    const registration = navigator.serviceWorker.register('/sw.js')
}
</script>

Lessons Learned

.waitUntil() is more like .waitForThisPromiseToResolve()

All of the example code I could find online made liberal use of .then() and .catch() synax, which I was keen to translate into modern async/await code. My first few attemps looked a bit like this:

self.addEventListener('install', event => {
    self.skipWaiting()
    event.waitUntil(async () => {
        let cache = await caches.open(cacheName)
        cache.addAll(cachedAssetPaths)
    })
})

I was very frustrated to find that my code wasn’t being called at all.

Turns out, .skipWaiting() takes a Promise, not a function. I solved my problem by factoring install() and activate() into standalone async functions and calling them from .waitUntil() without the await keyword to ensure I got a Promise, not the return type of install().

Beware .addAll()

The .addAll() is concise but there’s a catch: a single 404 nukes the whole caching process. I opted to iterate over the paths manually so I could catch errors without halting the installation altogether.

In Production

You can find the version of this service worker that I used in production over on GitHub.