From: Ardis Lu Date: Sun, 17 Jul 2022 01:43:24 +0000 (-0700) Subject: Implement stale-while-revalidate with notification X-Git-Url: https://git.ardis.lu/?a=commitdiff_plain;h=d86e8532576c5fd0fbf972bd9532758aee2d3b79;p=ardislu.dev Implement stale-while-revalidate with notification --- diff --git a/index.html b/index.html index 277b39c..7e8601e 100644 --- a/index.html +++ b/index.html @@ -76,6 +76,13 @@
+ + diff --git a/script.js b/script.js index d5395b0..e31878c 100644 --- a/script.js +++ b/script.js @@ -3,7 +3,15 @@ import hljs from 'https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.5.1/bui import powershell from 'https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.5.1/build/es/languages/powershell.min.js'; hljs.registerLanguage('powershell', powershell); +/* Set up logic for stale-while-revalidate cache strategy */ navigator.serviceWorker.register('/sw.js'); +const cacheChannel = new BroadcastChannel('cache'); +cacheChannel.addEventListener('message', e => { + // Ignore static assets like logo.svg, style.css, script.js, etc. + if (new URL(e.data.resource).pathname === '/api' && e.data.isUpdated) { + document.body.append(toast); + } +}); /* Global data store for posts */ const posts = new Map(); @@ -21,6 +29,8 @@ const homeLoader = components.get('home-loader'); const homeMain = components.get('home-main'); const postLoader = components.get('post-loader'); const postMain = components.get('post-main'); +const toast = components.get('toast'); +toast.querySelector('button').addEventListener('click', _ => location.reload()); /* Fetch important elements for convenience */ const head = { diff --git a/style.css b/style.css index 1961949..ae1c702 100644 --- a/style.css +++ b/style.css @@ -109,3 +109,37 @@ kbd { .post p { line-height: 1.5; } + +dialog { + position: fixed; + inset-block-end: 1rem; + background: hsl(50deg 72% 80%); + padding: 1rem; + border: solid hsl(50deg 72% 55% / 0.1); + border-radius: 1rem; + box-shadow: + 0px 1px 1.1px hsl(50deg 72% 69% / 0.17), + 0px 2.3px 2.6px -0.6px hsl(50deg 72% 69% / 0.17), + 0px 4.3px 4.8px -1.2px hsl(50deg 72% 69% / 0.17), + 0px 8.4px 9.5px -1.9px hsl(50deg 72% 69% / 0.17), + 0.1px 16px 18px -2.5px hsl(50deg 72% 69% / 0.17); +} + +dialog * { + display: inline; +} + +dialog > button { + cursor: pointer; + text-transform: uppercase; + font-weight: bold; + background: hsl(216deg 100% 80%); + padding: 0.25rem; + border: 1px hsl(216deg 100% 70%) solid; + border-radius: 0.5rem; + transition: 0.15s; +} + +dialog > button:hover, dialog > button:focus { + background-color: hsl(216deg 100% 70%); +} diff --git a/sw.js b/sw.js index fc6e93d..94bd614 100644 --- a/sw.js +++ b/sw.js @@ -1,29 +1,35 @@ -const cacheName = 'v3'; +const cacheName = 'v4'; +const cacheChannel = new BroadcastChannel('cache'); -self.addEventListener('install', event => event.waitUntil( - caches.open(cacheName).then(cache => cache.addAll( - [ - 'index.html', - 'favicon.svg', - 'apple-touch-icon.png', - 'style.css', - 'print.css', - 'script.js' - ] - )) -)); +async function revalidate(request) { + const cachePromise = caches.match(request); + const originPromise = fetch(request); + const [cacheResponse, originResponse] = await Promise.all([cachePromise, originPromise]); -self.addEventListener('fetch', event => event.respondWith( - caches.match(event.request, { ignoreVary: true }).then(response => { - if (response !== undefined) { - return response; - } - else { - return fetch(event.request).then(response => { - const responseClone = response.clone(); - caches.open(cacheName).then(cache => cache.put(event.request, responseClone)); - return response; - }) - } - }) -)); + // Must make clones before calling .text() + const originClone = originResponse.clone(); // For updating the cache + const originClone2 = originResponse.clone(); // For the function to return + + const [cacheText, responseText] = await Promise.all([cacheResponse?.text(), originResponse?.text()]); + + if (originResponse.ok && cacheText !== responseText) { + caches.open(cacheName) + .then(cache => cache.put(request, originClone)) + .then(() => cacheChannel.postMessage({ + resource: request.url, + isCreated: cacheText === undefined, + isUpdated: cacheText !== undefined + })); + } + + return originClone2; +} + +// Implements stale-while-revalidate strategy: if there's anything in the cache, return that immediately. +// Wait for the origin server's response in parallel and notify cacheChannel if the cache changed. +globalThis.addEventListener('fetch', event => { + event.respondWith(caches.match(event.request).then(cacheResponse => { + const originResponse = revalidate(event.request); + return cacheResponse === undefined ? originResponse : cacheResponse; + })); +});