workbox icon indicating copy to clipboard operation
workbox copied to clipboard

Cache is not updated in StaleWhileRevalidate

Open CapInSpase opened this issue 4 years ago • 3 comments

Library Affected: workbox 6.4.1

Browser & Platform: all browsers

Issue or Feature Request Description: Hi, I ran into a problem that after refreshing the page, the html cache for NavigationRoute is not updated

importScripts('https://storage.googleapis.com/workbox-cdn/releases/6.4.1/workbox-sw.js');


self.addEventListener('install', async (event) => {
    event.waitUntil(
        caches.open(OFFLINE_CACHE_NAME)
            .then((cache) => {
                cache.add(FALLBACK_HTML_URL)
            })
    );
});

workbox.navigationPreload.enable();

const navigationHandler = async (params) => {
    try {
        return await new workbox.strategies.StaleWhileRevalidate({
            cacheName: "navigation-and-route",
            plugins: [
                new workbox.broadcastUpdate.BroadcastUpdatePlugin('workbox-broadcast-update'),
                new workbox.expiration.ExpirationPlugin({
                    maxEntries: 20,
                    maxAgeSeconds: 7 * 24 * 60 * 60, // 1 week
                    purgeOnQuotaError: true,
                }),
            ],
        }).handle(params);
    } catch (error) {
        return caches.match(FALLBACK_HTML_URL, {
            cacheName: OFFLINE_CACHE_NAME,
        });
    }
};

workbox.routing.registerRoute(
    new workbox.routing.NavigationRoute(navigationHandler, {
        denylist: [
            new RegExp('admin'),
        ],
    })
);


workbox.precaching.cleanupOutdatedCaches();

workbox.precaching.precacheAndRoute(self.__WB_MANIFEST);



workbox.core.skipWaiting();
workbox.core.clientsClaim();
`

but at the same time new workbox.broadcastUpdate.BroadcastUpdatePlugin ('workbox-broadcast-update') is triggered and a page reload notification is shown to me

CapInSpase avatar Nov 22 '21 09:11 CapInSpase

And something else. I am using BroadcastUpdatePlugin to show a restart notification. In chrome, this works great, but in Safari, the broadcast continues to send a notification that the cache has been updated. For example

  workbox.routing.registerRoute(
    ({request}) =>
        request.destination === "style",
    new workbox.strategies.StaleWhileRevalidate({
        cacheName: style,
        plugins: [
            new workbox.broadcastUpdate.BroadcastUpdatePlugin('workbox-broadcast-update'),
            new workbox.cacheableResponse.CacheableResponsePlugin({
                statuses: [200]
            }),
            new workbox.expiration.ExpirationPlugin({
                maxEntries: 15,
                maxAgeSeconds: 30 * 24 * 60 * 60, // 30 Days
                purgeOnQuotaError: true,
            }),
        ],
    })
);


navigator.serviceWorker.addEventListener('message', async (event) => {
        // Optional: ensure the message came from workbox-broadcast-update
        console.log(event.data)
        if (event.data.meta === "workbox-broadcast-update") {
           //show notification for reload page
            showUpdateBar();
        }
    });
`

CapInSpase avatar Nov 22 '21 12:11 CapInSpase

Hello @CapInSpase!

Regarding your first comment, the BroadcastUpdatePlugin only runs if a cache update happened: https://github.com/GoogleChrome/workbox/blob/cdfc4cbc5d16f076d6ad51a49f19ad961c0c7482/packages/workbox-broadcast-update/src/BroadcastUpdatePlugin.ts#L58-L60

By the time you see a message broadcast while using the plugin, the cache put() should have already occurred. I am not sure how you are confirming that the cached HTML was not updated, but is it possible that you're mistaken? If not, is there a reproduction available on a live site that I could take a look at?

Regarding your second comment, while there is some specific logic in place for broadcasting update notifications for navigation request, your code snippet implies that you're seeing odd behavior when broadcasting updates about cached CSS. The code for that is fairly straightforward: https://github.com/GoogleChrome/workbox/blob/cdfc4cbc5d16f076d6ad51a49f19ad961c0c7482/packages/workbox-broadcast-update/src/BroadcastCacheUpdate.ts#L200-L203

I'm not aware of anything in Workbox that would cause that postMessage() to execute again independent of an actual cache update. You mentioned that Safari exhibits this behavior, and Chrome doesn't. What about Firefox? And again, is there a live version of the web app (along with a mechanism for triggering an update) that I can reproduce this against?

jeffposnick avatar Dec 01 '21 19:12 jeffposnick

Hi @jeffposnick I'm currently experiencing this issue (alongside another one I think) and I do have a live site you can check out.

Everything seems to be working fine on every other platform except Safari.

I've tested Safari 17.1 on OS X and 16.7.2 on iOS.

The English version of the site can be found here: https://en.buses.uy

The site should display an alert prompting the users to "update" whenever a service worker update or a cache update is detected. (The alert is the same in both cases, although the callback for the "update" button differs for each case).

Code snippets:

service-worker.js

importScripts('https://storage.googleapis.com/workbox-cdn/releases/7.0.0/workbox-sw.js')

const CACHE = 'busesuy-offline-pages'

const offlineFallbackPage = 'sin-conexion'

const ignoredHosts = ['localhost']
const ignoredPaths = ['/admin', '/imgs/bg']

self.addEventListener('message', (event) => {
  if (event.data && event.data.type === 'SKIP_WAITING') {
    self.skipWaiting()
  }
})

self.addEventListener('install', async (event) => {
  event.waitUntil(
    new Promise((resolve, reject) => {
      // Delete all cached items when the SW first installs
      caches.delete(CACHE).then(() => {
        // Cache the offline fallback page
        caches.open(CACHE).then((cache) => cache.add(offlineFallbackPage)).then(resolve).catch(reject)
      }).catch(reject)
    })
  )
})

if (workbox.navigationPreload.isSupported()) {
  workbox.navigationPreload.enable()
}

workbox.routing.registerRoute(
  (event) => {
    return (
      (ignoredHosts.indexOf(event.url.hostname) === -1) &&
      (ignoredPaths.reduce((noMatch, path) => noMatch && !event.url.pathname.includes(path), true))
    )
  },
  new workbox.strategies.StaleWhileRevalidate({
    cacheName: CACHE,
    plugins: [
      new workbox.broadcastUpdate.BroadcastUpdatePlugin(),
    ],
  })
)

self.addEventListener('fetch', (event) => {
  if (event.request.mode === 'navigate') {
    event.respondWith((async () => {
      try {
        const preloadResp = await event.preloadResponse
        if (preloadResp) return preloadResp
        const networkResp = await fetch(event.request)
        return networkResp
      } catch (error) {
        const cache = await caches.open(CACHE)
        const cachedResp = await cache.match(offlineFallbackPage)
        return cachedResp
      }
    })())
  }
})

SW Registration

installServiceWorker() {
    if (typeof navigator.serviceWorker !== 'undefined') {
      // Display an "Update" alert whenever the service worker detects an update
      navigator.serviceWorker.addEventListener('message', async (event) => {
        if (event.data.meta !== 'workbox-broadcast-update') return
        this.updateAlert(() => window.location.reload())
      })
      // Install the service worker
      navigator.serviceWorker.register('/service-worker.js').then((registration) => {
        registration.addEventListener('updatefound', () => {
          const newServiceWorker = registration.installing
          if (newServiceWorker) newServiceWorker.addEventListener('statechange', () => serviceWorkerChangeStateHandler(newServiceWorker))
        })
        // If there's a waiting/installing service worker
        // Listen for state changes to reload the page once the new service worker is active
        const newServiceWorker = registration.waiting
        if (newServiceWorker) newServiceWorker.addEventListener('statechange', () => serviceWorkerChangeStateHandler(newServiceWorker))
        serviceWorkerChangeStateHandler(newServiceWorker)
      })
    }
  }

serviceWorkerChangeStateHandler

function serviceWorkerChangeStateHandler(serviceWorker: ServiceWorker | null) {
  // When the service worker gets activated reload the page
  if (serviceWorker?.state === 'activated') window.location.reload()
  else if (serviceWorker?.state === 'installed') {
    // Display an alert prompting the user to update
    window.Buses.updateAlert(() => serviceWorker?.postMessage({ type: 'SKIP_WAITING' }))
  }
}

window.Buses.updateAlert/this.updateAlert This one is just a function that displays the alert. It accepts a callback function as a parameter that gets called when/if the users clicks on the "Update" button. In these cases the callback is either "window.location.reload()" or a post message to the SW telling it to "SKIP_WAITING"

I've tried commenting out code to see where the issue/s are and the infinite "update prompting" stops if I comment the following sections of code on the "installServiceWorker()" function:

const newServiceWorker = registration.waiting
if (newServiceWorker) newServiceWorker.addEventListener('statechange', () => serviceWorkerChangeStateHandler(newServiceWorker))
serviceWorkerChangeStateHandler(newServiceWorker)

And:

navigator.serviceWorker.addEventListener('message', async (event) => {
  if (event.data.meta !== 'workbox-broadcast-update') return
  this.updateAlert(() => window.location.reload())
})

This last bit might be causing issues given that it looks like new versions of the service worker fail to "skipWaiting()" on Safari because I've noticed I have 2 service workers I can inspect. This last issue might be unrelated to the other one but I mention it just in case.

Thank you in advance for your time and help!

ferares avatar Nov 29 '23 12:11 ferares