Search code examples
reactjsprogressive-web-appsworkbox

Is there any way to force new service worker cache version update during app runtime?


I am working with Workbox, and my current app's implementation needs the following behavior:

If I make a major feature update that results in many changes in my JavaScript file, I want the app to detect that a new version is available.

The idea is that in many instances, a user can open the app and leave it hanging in the background without closing the tab. This scenario is problematic because the update has no chance to take place, and as a result, the new features won't be available at all. What I want to do is force the update to show. Does Workbox have any listeners for changes in the source?

my current work box sw registration is the following but this needs at least 2 reloads

  1. first for the new sw to be fetch
  2. for the sw to be installed

function registerValidSW(swUrl: string, config?: Config) {
  navigator.serviceWorker
    .register(swUrl)
    .then((registration) => {
      registration.onupdatefound = () => {
        

        const installingWorker = registration.installing;
        if (installingWorker == null) {
          return;
        }

        installingWorker.onstatechange = () => {
          if (installingWorker.state === 'installed') {
            // wait for an new version of the service worker to avaliable by the developer

            if (navigator.serviceWorker.controller) {
          
              console.log('Update available. Please refresh the page.... Reload');
              
              // Execute callback
              if (config && config.onUpdate) {
                config.onUpdate(registration);
              }
            } else {
              // At this point, everything has been precached.
              // It's the perfect time to display a
              // "Content is cached for offline use." message.
              console.log('Content is cached for offline use........');

              // Execute callback
              if (config && config.onSuccess) {
                config.onSuccess(registration);
              }
            }
          }
        };
      };
    })
    .catch((error) => {
      console.error('Error during service worker registration:', error);
    });
}

function checkValidServiceWorker(swUrl: string, config?: Config) {
  // Check if the service worker can be found. If it can't reload the page.
  fetch(swUrl, {
    headers: { 'Service-Worker': 'script' },
  })
    .then((response) => {
      // Ensure service worker exists, and that we really are getting a JS file.
      const contentType = response.headers.get('content-type');
      if (response.status === 404 || (contentType != null && contentType.indexOf('javascript') === -1)) {
        // No service worker found. Probably a different app. Reload the page.
        navigator.serviceWorker.ready.then((registration) => {
          registration.unregister().then(() => {
            window.location.reload();
          });
        });
      } else {
        // Service worker found. Proceed as normal.
        registerValidSW(swUrl, config);
      }
    })
    .catch(() => {
      console.log('No internet connection found. App is running in offline mode.');
    });
}


Solution

  • I'm using Workbox with svelte and this is the component I have that will detect that there is a new version of the app and show a button to install it and restart the app:

    <script lang="ts">
      // @ts-ignore
      import { useRegisterSW } from 'virtual:pwa-register/svelte';
      import type { ReloadPromptParams } from './Interfaces';
      import Button from '#components/Button/Button.svelte';
      import { reloadApp } from '#js/utils/reloadApp';
      import { t } from '#js/utils/t';
    
      export let params: ReloadPromptParams;
    
      const { offlineReady, needRefresh, updateServiceWorker } = useRegisterSW({
        onNeedRefresh() {},
        onOfflineReady() {},
        onRegisteredSW(swUrl, r) {
          r &&
            setInterval(async () => {
              if (!(!r.installing && navigator)) return;
    
              if ('connection' in navigator && !navigator.onLine) return;
    
              const resp = await fetch(swUrl, {
                cache: 'no-store',
                headers: {
                  cache: 'no-store',
                  'cache-control': 'no-cache',
                },
              });
    
              if (resp?.status === 200) await r.update();
            }, 70 * 1000);
        },
        onRegisterError(error) {
          console.log('SW registration error', error);
        },
      });
      $: toast = $offlineReady || $needRefresh;
    
      async function updateNow() {
        await updateServiceWorker();
        reloadApp(10);
      }
    </script>
    
    {#if toast}
      <div class="m-2 px-2">
        <Button
          params={{
            page: params.page,
            text: t('reloadpromp.button'),
            onClick: updateNow,
          }}
        />
      </div>
    {/if}
    

    That works very well, it detect the new version in a few seconds after I push it on the server and show the button inside the app for the user to update when he wants to.