Pitfalls of Service Workers

    In this short essay, I will describe those things about service workers that I would like to read about a year or at least six months ago and thereby avoid a very long and painful period of debugging the application.

    If you got here on a request like "what the hell is my service worker not working on production?", Welcome to cat.

    For those who do not know what it is about, it’s very briefly - a service worker is a script that is executed by the browser in the background, separate from the web page and capable of performing functions that do not require interaction with the page or user.

    Full documentation is not difficult to find, but here is the link .

    This material was also very useful to meand I am very sorry that I did not carefully read it when I first started getting acquainted with the ServiceWorker API.

    In practice, the Service Worker API allows you to do such a magical thing as caching files of an online web application to a user's local device and then working completely offline if necessary.

    In the future, it is planned to add such cool things as cache synchronization in the background, that is, even if the user is not currently on your site, the service worker can still start and download updates, for example. And also access to PushApi from the background again (that is, when you receive an update, send you a push notification).

    How it works.

    • Web page registers service worker
    • Service worker is installed, activated and starts to do something in the background. For example, “listen” to the events of 'fetch' and, if necessary, change or cancel them completely

    Using the Channel Messaging API, a web page can send and receive service worker messages (and vice versa).
    Service Worker can NOT access the DOM and web page data except through messages.

    What I did not know until recently, is that even if the user is now on your site, this does not guarantee that the service worker will work all the time. And in no case can you rely on the global scope of the worker.

    That is, such a working scheme led me to very disastrous consequences and a long process of debugging the application ( attention, bad code, DO NOT use it in any case ):

    registering / installing service worker
    Index.html

    var regSW = require("./register-worker.js");
    var sharedData = {filesDir: localDir};
    regSW.registerServiceWorker(sharedData);
    

    register-worker.js

    var registerServiceWorker = function(sharedData){
      navigator.serviceWorker.register('service-worker.js', { scope: './' })
        .then(navigator.serviceWorker.ready)
        .then(function () {
          console.log('service worker registered');
          sendMessageToServiceWorker(sharedData).then(function(data) {
            console.log('service worker respond with message:', data);
          })
          .catch(function(error) {
            console.error('send message fails with error:', error);
          });
        })
        .catch(function (error) {
          console.error('error when registering service worker', error, arguments)
        });
    };
    var sendMessageToServiceWorker = function(data){
      return new Promise(function(resolve, reject) {
        var messageChannel = new MessageChannel();
        messageChannel.port1.onmessage = function(event) {
          if (event.data.error) {
            reject(event.data.error);
          } else {
            resolve(event.data);
          }
        };
        navigator.serviceWorker.controller.postMessage(data,
          [messageChannel.port2]);
      });
    };
    

    Worker code, listening to fetch and spoofing
    service-worker.js response

    self.addEventListener('message', function(event) {
      self.filesDir = event.data.filesDir;
      event.ports[0].postMessage({'received': event.data});
    });
    self.addEventListener('fetch', function fetcher(event) {
      let url = event.request.url;
      if (url.indexOf("s3") > -1) {
        //redirect to local stored file
        url = "file://" + self.filesDir + self.getPath(url);
        let responseInit = {
          status: 302,
          statusText: 'Found',
          headers: {
            Location: url
          }
        };
        let redirectResponse = new Response('', responseInit);
        event.respondWith(redirectResponse);
      }
    });
    

    What happened here:

    • We registered a worker and sent him a message on which local path to look for previously cached files.
    • In the service worker, we received a message and saved the path in the global scope of the worker to the variable self.filesDir.
    • The worker listens for the fetch event and responds to a local file with a redirect to everything that contains “s3” in the path.

    I will make a reservation that the code is greatly simplified (of course I do not replace everything that contains s3 in the way, I'm not so lazy), but it shows the main thing.

    And everything would be fine if it weren’t for the fact that after a random amount of time (3-10 minutes) the application was running, the service worker started redirecting requests to nowhere, or rather to something like “file: // undefined / images /image1.png »
    That is, after some time, the self.filesDir variable is simply deleted and we get a ton of 404 file not found instead of pictures.

    Naturally, no self-respecting programmer will test the application for as long as 5 minutes. Therefore, a tester detects a bug at best. And usually even a client. Because you know these testers yourself ... And in general, no one paid for the testing, say thank you for not crashing at the start.

    In general, in order not to drag out for a long time, the problem is that if the service worker is not used [for some time], the browser nails it (sorry, I didn’t come up with a more appropriate translation for the terminate word) and then starts again the next time it is accessed. Accordingly, the new copy of the worker does not know what his dead predecessor was talking to the web page about and there is no information about where to get the files from.

    Therefore, if you need to save something, do it in a permanent storage, namely in IndexedDB.

    Another note - they say the worker cannot use synchronous APIs, so the localStorage API cannot be used. But I personally have not tried.

    By the way, the debugging in my case was delayed also because even when I tested for a long, long time (about 7 minutes) in the hope of reproducing the bug, I couldn’t succeed, because when the Developer Tools window is open, the insidious chrome does not kill the worker. Although it reports this in a concise message in the logs “Service Worker termination by a timeout was canceled because DevTools is attached”

    Actually, it dawned on me why my repeated attempts to find out why ServiceWorker works differently for me than the client did on production ...

    image

    In general, after I removed the installation of the path in a variable and transferred it to indexedDB, my misfortunes ended and I started to like the ServiceWorker API again.

    But the actual working example of the code that can be taken and used unlike the previous one:

    registration / installation of service worker
    index.html

    var regSW = require("./register-worker.js");
    idxDB.setObject('filesDir', filesDir);
    regSW.registerServiceWorker();
    

    register-worker.js

    var registerServiceWorker = function(){
      navigator.serviceWorker.register('service-worker.js', { scope: './' })
        .then(navigator.serviceWorker.ready)
        .then(function () {
          console.log('service worker registered');
        })
        .catch(function (error) {
          console.error('error when registering service worker', error, arguments)
        });
    };
    

    Worker code, listening to fetch and spoofing
    service-worker.js response

    self.getLocalDir = function() {
      let DB_NAME = 'localCache';
      let DB_VERSION = 1;
      let STORE = 'cache';
      let KEY_NAME = 'filesDir';
      return new Promise(function(resolve, reject) {
        var open = indexedDB.open(DB_NAME, DB_VERSION);
        open.onerror = function(event) {
          reject('error while opening indexdb cache');
        };
        open.onsuccess = function(event) {
          let db = event.target.result, result;
          result = db.transaction([STORE])
            .objectStore(STORE)
            .get(KEY_NAME);
          result.onsuccess = function(event) {
            if (!event.target.result) {
              reject('filesDir not set');
            } else {
              resolve(JSON.parse(event.target.result.value));
            }
          };
          result.onerror = function(event) {
            reject('error while getting playthroughDir');
          };
        }
      });
    };
    self.addEventListener('fetch', function fetcher(event) {
      let url = event.request.url;
      if (url.indexOf("s3") > -1) {
        //redirect to local stored file
        event.respondWith(getLocalDir().then(function(filesDir){
            url = "file://" + filesDir + self.getPath(url);
            var responseInit = {
              status: 302,
              statusText: 'Found',
              headers: {
                Location: url
              }
            };
            return new Response('', responseInit);
        }));
    });
    

    PS The author does not pretend to be original, but believes that if this article is found, read and will help at least one unfortunate person - it's worth it.

    Also popular now: