📜 ⬆️ ⬇️

Pitfalls Service Workers

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

If you got here at the request of the type “what the hell is my service worker not working on production?”, Welcome under cat.

For those who are not at all aware of what they are talking about, then very briefly - the service worker is a script that is executed by the browser in the background, separate from the web page and able to perform functions for which interaction with the page or the user is not required.

Full documentation is not difficult to find, but here is the link .
')
I also found this material very useful, and I am very sorry that I inadvertently familiarized myself with it when I first began to get acquainted with the ServiceWorker API.

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

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 will still be able to start and download updates, for example. And also access to PushApi from the background again (that is, when you receive the update, send you a push notification).

How it works.


Using the Channel Messaging API, a web page can send service worker messages and receive responses from it (and vice versa).
The Service Worker may NOT have access to the DOM and webpage data, except through messages.

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

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

registration / installation 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 with fetch tapping and response spoofing
service-worker.js

 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:


I will make a reservation that the code is very simplified (of course, I don’t replace everything that contains s3 on the way, I’m not so lazy), but it shows the main thing.

And everything would be fine if it were not for the fact that after a random amount of time (3-10 minutes) of the application had expired, the service worker began to redirect 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 not test the application as much as 5 minutes. Therefore, a bug is detected at best by a tester. And usually even a client. Because you know, these testers ... In general, nobody paid for the testing, say thank you for not crashing at the start.

In general, in order not to delay for a long time, the problem is that if the service worker is not used [for a while], then the browser nails it (sorry, I didn’t think of a more appropriate translation for the word terminate) and then starts again at the next call. Accordingly, the new copy of the worker does not know what his dead predecessor was talking about there with the web page and he does not have any information about where to get the files.

Therefore, if you need to save something, do it in 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 did not try.

By the way, debugging in my case was delayed also because even when I tested for a long, long time (minutes 7) hoping to reproduce the bug, I did not succeed, because with the Developer Tools window open, the treacherous chrome does not kill the worker. Although it reports this with a concise message in the logs “Service Worker Termination by a timeout was canceled because DevTools is attached”

Actually here it came to me why my repeated attempts to find out why ServiceWorker works for me differently than the production client failed ...

image

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

And here is the actual working example of code that can be taken and used, unlike the previous one:

registration / installation 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 with fetch tapping and response spoofing
service-worker.js

 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 claim originality, but believes that if this article is found, read and will help at least one unfortunate - it's worth it.

Source: https://habr.com/ru/post/351194/


All Articles