]> git.ardis.lu - ardislu.dev/commitdiff
Implement stale-while-revalidate with notification
authorArdis Lu <ardisglu@gmail.com>
Sun, 17 Jul 2022 01:43:24 +0000 (18:43 -0700)
committerArdis Lu <ardisglu@gmail.com>
Sun, 17 Jul 2022 01:43:24 +0000 (18:43 -0700)
index.html
script.js
style.css
sw.js

index 277b39c259050b5959adbfe9efc5a7dfc32b9179..7e8601ea011268dca57a668fe9498dda23dca7c7 100644 (file)
     <main></main>
   </template>
 
+  <template id="toast">
+    <dialog open>
+      <p>A newer version of this page is available.</p>
+      <button>Refresh</button>
+    </dialog>
+  </template>
+
   <script type="module" src="/script.js"></script>
 </body>
 
index d5395b0ee47358559edcb7bd2cddac58c57e3c44..e31878cdd9ad90e6fcb3bd1267831258477d7261 100644 (file)
--- 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 <head> elements for convenience */
 const head = {
index 19619493cc3608aff31b4f205af22305c63138af..ae1c70220888003abd2d35e331988ee3cc28de05 100644 (file)
--- 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 fc6e93d3bfd397df9dfef5084e186d7321896d3c..94bd6146fe3ab0e68ec5f6bb76752f7595ebf67a 100644 (file)
--- 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;
+  }));
+});
This page took 0.058226 seconds and 4 git commands to generate.