Search code examples
service-workerprogressive-web-appsworkboxbackground-sync

Service Workers: when does the browser sync back again?


I am building a progressive web application using Google Workbox. I have setup a service worker with bgSync (in my build folder) so that my POST requests are pushed to a queue and sent to my endpoint when the user regains connection, but when does the syncing event happen exactly?

For development purposes, I use the "Sync" button included in Chrome as detailed in this answer: How to manually trigger a background sync (for testing)?, But this is manual, I'd like the requests to be sent to the endpoint as soon as the application is back online, but when I get connection my requests are not sent, I have to manually click the "Sync" button for that to happen, and it does work beautifully, but users won't be clicking the Sync button in a real scenario.

Precisely, I'd like to know if there is a way, inside my service-worker, to detect when the application is back online, and force a Sync. Or to know when the sync happens. Here is a snippet for reference (Using Workbox 3.0.0):

const bgSyncPlugin = new workbox.backgroundSync.Plugin('myQueue', {
  callbacks: {
    requestWillEnqueue: (storableRequest) => {}, // I show a push notification indicating user is offline
    requestWillReplay: (storableRequest) => {},
    queueDidReplay: (storableRequestArray) => {} // If I get a response, I show a push notification
      }
   },
);

workbox.routing.registerRoute(
  "https://myapi.com/action",
  workbox.strategies.networkOnly({
    plugins: [bgSyncPlugin]
  }),
  'POST'
);

Solution

  • So, as of today, the background-sync in Chrome tries to push the queued requests 3 times (Never explained in the workbox or background sync documentation):

    • First time: When the request is actually tried for the first time and the browser realizes the user has no connection, so it puts the request in indexedDB.
    • Second time: Exactly 5 minutes after the first try.
    • Third time: Exactly 15 minutes after the first try.

    If the user has no connection after 15 minutes, then the requests just get stuck inside indexedDB until a new queued request tries to push the rest. This is not very helpful in a scenario when we are expecting the user to not have internet connection for hours.

    There are plans (since 2016!) to implement PeriodicSync, which would let the developer choose how many times and how long would it take for the browser to try to sync, but it's never been really implemented, see: https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerRegistration/periodicSync

    I came up with a clunky solution though, might not be the best one but it does what I need. There IS a sync event that we can manipulate to retry the requests that we have stuck in indexedDB. We need to use the Workbox Queue class instead of Plugin though.

    // our service worker file
    // we create a push notification function so the user knows when the requests are being synced
    
    const notificate = (title, message) => {
        self.registration.showNotification(title, {
        body: message,
        icon: '/image.png',
        tag: 'service-worker'
     })
    }
    
    // let's create our queue
    const queue = new workbox.backgroundSync.Queue('myQueue', {
       callbacks: {
           requestWillEnqueue: () => {
           notificate('You are offline! 🛠', 'Your request has been submitted to the Offline 
    queue. The queue will sync with the server once you are back online.')
        }
    });
    
    // sync event handler
    self.addEventListener("sync", (ev) => {
      queue.replayRequests().then((a) => {
        notificate('Syncing Application... 💾', 'Any pending requests will be sent to the 
    server.');
      }).catch(
        notificate('We could not submit your requests. ❌', 'Please hit the \'Sync Pending 
    Requests\' button when you regain internet connection.')
        );
     });
    

    Now inside our HTML/React/Node view file we can do:

      // this will trigger our Sync event
      <button type="button" onClick={navigator.serviceWorker.ready.then(reg =>
          reg.sync.register('myEvent'))}>{'Sync Pending
      Requests'}</button>
    

    Notice I created an html button that forces my service worker to run queue.replayRequests(), so the background sync feature does not happen automatically, I have to manually click a button for it to happen.