ServiceWorkerContainer.ready for a specific scriptURL? - javascript

ServiceWorkerContainer.ready resolves to an active ServiceWorkerRegistration, but if there are multiple service workers in play (e.g. service workers from previous page loads, multiple registrations within the same page) there are multiple service workers it could resolve to. How can I make sure that a particular service worker is handling network events when a chunk of code is executed?
That is, how can I write a function registerReady(scriptURL, options) that can be used as follows:
registerReady("foo.js").then(function (r) {
// "foo.js" is active and r.active === navigator.serviceWorker.controller
fetch("/"); // uses foo.js's "fetch" handler
});
registerReady("bar.js").then(function (r) {
// "bar.js" is active and r.active === navigator.serviceWorker.controller
fetch("/"); // use bar.js's "fetch" handler
});

First, notice there can be only one active service worker per scope and your client page can be controlled by at most one and only one service worker. So, when ready resolves, you know for sure the service worker controlling your page is active.
To know if an arbitrary sw is active you can register it and check the queue of service workers in the registration and listen for changes in their state. For instance:
function registerReady(swScript, options) {
return navigator.serviceWorker.register(swScript, options)
.then(reg => {
// If there is an active worker and nothing incoming, we are done.
var incomingSw = reg.installing || reg.waiting;
if (reg.active && !incomingSw) {
return Promise.resolve();
}
// If not, wait for the newest service worker to become activated.
return new Promise(fulfill => {
incomingSw.onstatechange = evt => {
if (evt.target.state === 'activated') {
incomingSw.onstatechange = null;
return fulfill();
}
};
});
})
}
Hope it makes sense to you.

Related

service worker doesn't skip waiting state

I'm still doing experiments in order to master service workers, and I'm facing a problem, probably because of my lack of expertise in JavaScript and service workers.
The problem happens when I want the new service worker to skipWaiting() using postMessage(). If I show a popup with a button and I bind a call to postMessage() there, everything works. If I call postMessage() directly, it doesn't work. It's a race condition because SOMETIMES it works, but I can't identify the race condition.
BTW, the postMessage() call WORKS, the service worker is logging what it should when getting the message:
// Listen to messages from clients.
self.addEventListener('message', event => {
switch(event.data) {
case 'skipWaiting': self.skipWaiting(); console.log('I skipped waiting... EXTRA');
break;
}
});
Here is the code. The important bit is on the if (registration.waiting) conditional. The uncommented code works, the commented one doesn't:
// Register service worker.
if ('serviceWorker' in navigator) {
// Helpers to show and hide the update toast.
let hideUpdateToast = () => {
document.getElementById('update_available').style.visibility = 'hidden';
};
let showUpdateToast = (serviceworker) => {
document.getElementById('update_available').style.visibility = 'visible';
document.getElementById('force_install').onclick = () => {
serviceworker.postMessage('skipWaiting');
hideUpdateToast();
};
document.getElementById('close').onclick = () => hideUpdateToast();
};
window.addEventListener('load', () => {
let refreshing = false;
navigator.serviceWorker.addEventListener('controllerchange', () => {
if (refreshing) return;
refreshing = true;
window.location.reload();
});
navigator.serviceWorker.register('/sw.js').then(registration => {
// A new service worker has been fetched, watch for state changes.
//
// This event is fired EVERY TIME a service worker is fetched and
// succesfully parsed and goes into 'installing' state. This
// happens, too, the very first time the page is visited, the very
// first time a service worker is fetched for this page, when the
// page doesn't have a controller, but in that case there's no new
// version available and the notification must not appear.
//
// So, if the page doesn't have a controller, no notification shown.
registration.addEventListener('updatefound', () => {
// return; // FIXME
registration.installing.onstatechange = function () { // No arrow function because 'this' is needed.
if (this.state == 'installed') {
if (!navigator.serviceWorker.controller) {
console.log('First install for this service worker.');
} else {
console.log('New service worker is ready to activate.');
showUpdateToast(this);
}
}
};
});
// If a service worker is in 'waiting' state, then maybe the user
// dismissed the notification when the service worker was in the
// 'installing' state or maybe the 'updatefound' event was fired
// before it could be listened, or something like that. Anyway, in
// that case the notification has to be shown again.
//
if (registration.waiting) {
console.log('New service worker is waiting.');
// showUpdateToast(registration.waiting);
// The above works, but this DOESN'T WORK.
registration.waiting.postMessage('skipWaiting');
}
}).catch(error => {
console.log('Service worker registration failed!');
console.log(error);
});
});
}
Why does the indirect call using a button onclick event works, but calling postMessage() doesn't?
I'm absolutely at a loss and I bet the answer is simple and I'm just too blind to see it.
Thanks a lot in advance.
Looks like a bug in Chromium or WebKit, because this code works all the time in Firefox but fails in Chrome and Edge most of the time.
I've reported that bug to Chromium, let's see if it is a bug or my code is weird. I've managed to build the smallest code possible that still reproduces the issue, it's quite small and it can be found on the bug report.
The report can be found here.
Sorry for the noise, I'll keep investigating the issue but no matter how I tried, the code still fails from time to time, I can't spot the race condition on my code.

Service Worker "fetch" event for requesting the root path never fires

I am trying to set up my Service Worker so that it intercepts the request for the home page (ie the home page, like www.mywebsite.com/), which would eventually allow me to return a cached template instead. My code looks like this so far:
main.js:
navigator.serviceWorker.register('/sw.js')
sw.js:
self.addEventListener('fetch', function(event) {
console.log(event.request.url)
/**
* only ever detects requests for resources but never the route itself..
*
* logged:
* https://www.mywebsite.com/main.js
* https://www.mywebsite.com/myimage.png
*
* not logged:
* https://www.mywebsite.com/
*/
})
I'm pretty sure the service worker is getting set up correctly, as I am indeed detecting events being fired for requests for resources (like /main.js or /myimage.png). However, the problem is that only the resources' events ever get fired, even though I'd like that event for requesting the route itself (/) to be fired. Am I missing anything, or should I be listening for a different event?
Thanks
I discovered that the request for the root path was in fact getting intercepted. The reason I never noticed this is because the request happens before the page is loaded (ie before there's even a console to log to). If I turn on the Preserve log feature in Chrome DevTools, I will notice the logs for the root path requests as I should.
You can chain .then() to. .register() call then check location.href to determine the page at which the ServiceWorker has been registered
navigator.serviceWorker.register("sw.js")
.then(function(reg) {
if (location.href === "https://www.mywebsite.com/") {
// do stuff
console.log(location.href, reg.scope);
}
})
.catch(function(err) {
console.error(err);
});
I just figured this out, with help from https://livebook.manning.com/book/progressive-web-apps/chapter-4/27.
Here's a short version of my service worker:
const cacheName = "Cache_v1";
const urlsToCache = [
"Offline.html"
// styles, images and scripts
];
self.addEventListener('install', (e) =>
{
e.waitUntil(
caches.open(cacheName).then((cache) =>
{
return cache.addAll(urlsToCache);
})
);
});
self.addEventListener('fetch', (e) =>
{
if (e.request.url == "https://<DOMAIN_NAME>/")
{
console.log("root");
e.respondWith(
fetch("offline.html")
);
}
}

Why does postMessage called upon terminated worker not throw some error?

After a web worker is terminated, why does postMessage not throw an error if I call it?
Is it possible to restart a worker, with an existing terminated instance, without the constructor new Worker("same-worker.js")?
const myWorker = new Worker("my-worker.js");
myWorker.addEventListener("message", function(event) {
const message = event.data;
console.log("from worker", message);
myWorker.terminate();
myWorker.postMessage("");
// what happens here, is only silence
// Why not throw error?
});
myWorker.postMessage(""); //start it, maybe receive a response
Edit: I am asking about the rationale for this particular design. XHR, WebSocket and WebRTC immediately throw when trying to do stuff on a terminated instance.
tl;dr jsFiddle
I cannot answer why, because I am not the one who wrote javascript standard. But there is no indication whatsoever whether the worker has terminated or not.
Detecting termination
If you need to detect this state, you must create your own API. I propose this simple function based draft as a starting point:
//Override worker methods to detect termination
function makeWorkerSmart(workerURL) {
// make normal worker
var worker = new Worker(workerURL);
// assume that it's running from the start
worker.terminated = false;
// allows the worker to terminate itself
worker.addEventListener("message", function(e) {
if(e.data == "SECRET_WORKER_TERMINATE_MESSAGE") {
this.terminated = true;
console.info("Worker killed itself.");
}
});
// Throws error if terminated is true
worker.postMessage = function() {
if(this.terminated)
throw new Error("Tried to use postMessage on worker that is terminated.");
// normal post message
return Worker.prototype.postMessage.apply(this, arguments);
}
// sets terminate to true
// throws error if called multiple times
worker.terminate = function() {
if(this.terminated)
throw new Error("Tried to terminate terminated worker.");
this.terminated = true;
// normal terminate
return Worker.prototype.terminate.apply(this, arguments);
}
// creates NEW WORKER with the same URL as itself
worker.restart = function() {
return makeWorkerSmart(workerURL);
}
return worker;
}
To also detect termination from the side of the worker, you will need to run this code inside every worker:
function makeInsideWorkerSmart(workerScope) {
var oldClose = workerScope.close;
workerScope.close = function() {
postMessage("SECRET_WORKER_TERMINATE_MESSAGE");
oldClose();
}
}
makeInsideWorkerSmart(self);
This will send message to the main window when worker terminates itself with close.
You can use it like this:
var worker = makeWorkerSmart(url);
worker.terminate();
worker.postMessage("test"); // throws error!
Restarting the worker
As of restarting the worker: it is not technically possible to start from some previous state without saving it somewhere yourself. I propose this solution that I have implemented above:
worker.terminate();
worker = worker.restart();
You can also clone the worker this way as it doesn't stop the original worker.
According to the docs, the terminate method of the worker
immediately terminates the Worker. This does not offer the worker an opportunity to finish its operations; it is simply stopped at once.
I think that since the instance it's still there even if it's in this dead state, doing a postMessage will not throw, but since your worker is not processing any operation, it will simply do nothing.
There is no way to my knowledge from resuming a worker, but you could potentially set a Boolean pausing your processing via a message too, if you want to be able to resume it at will.

To check if ServiceWorker is in waiting state

I am trying to understand the Service Worker API, and I know the bits and parts about registering a Service Worker.
As stated in the API doc, if a service worker update is found, the service worker is registered and added to the queue. This SW takes over a page if and only if, the page is closed and opened again.That is, A window is closed and reopened again.
Now, this has a few downfalls:
The user might be seeing a previous version that might have a very serious grammatical mistake, or whatever.
The user needs to be somehow notified that the content has changed and that a referesh would do it.
I know how to tell the SW.js to skipWaiting() and take over. I also know how to send a message to the SW.js telling it that the user wants a automatic refresh.
However, what I do not know is how to know whether a new SW is actually in a waiting state.
I have used this:
navigator.serviceWorker.ready.then((a) => {
console.log("Response, ", a);
if (a.waiting !== null && a.waiting.state === "installed") {
console.log("okay");
}
});
However, it usually returns the waiting state as null.(Possibly because the SW is still installing when the request is fired.)
How can I know on the client page that a waiting service worker is available?
Here's some code that will detect and allow you to handle various states whenever there's a new or updated service worker registration.
Please note that the log message assumes that skipWaiting() is not being called during the service worker's installation; if it is being called, then instead of having to close all tabs to get the new service worker to activate, it will just activate automatically.
if ('serviceWorker' in navigator) {
window.addEventListener('load', async function() {
const registration = await navigator.serviceWorker.register('/service-worker.js');
if (registration.waiting && registration.active) {
// The page has been loaded when there's already a waiting and active SW.
// This would happen if skipWaiting() isn't being called, and there are
// still old tabs open.
console.log('Please close all tabs to get updates.');
} else {
// updatefound is also fired for the very first install. ¯\_(ツ)_/¯
registration.addEventListener('updatefound', () => {
registration.installing.addEventListener('statechange', () => {
if (event.target.state === 'installed') {
if (registration.active) {
// If there's already an active SW, and skipWaiting() is not
// called in the SW, then the user needs to close all their
// tabs before they'll get updates.
console.log('Please close all tabs to get updates.');
} else {
// Otherwise, this newly installed SW will soon become the
// active SW. Rather than explicitly wait for that to happen,
// just show the initial "content is cached" message.
console.log('Content is cached for the first time!');
}
}
});
});
}
});
}

NowJS server event notifications

I'm trying to implement a system where an external server (SuperFeedr) sends a request to my server (running Node) and my server processes, then sends that data straight to the client in realtime using NowJS.
Problem is, I cannot access the everyonce namespace in my server functions since it has to be initialized after the listen() function is called which has to happen after the functions are declared. So basically:
Needs:
NowJS->Listen->Server functions->everyone variable->NowJS
Seems I have a dependency loop and I have no idea how to resolve it.
Start all of them independently. When one of them is up, put a reference to it into a shared parent scope. When e.g. the server receives a notification, just drop it if nowjs isn't ready yet. Simplified example:
var a, b;
initializeA(function(a_) {
a = a_
a.on('request', function(request, response) {
if (!b) {
// B isn't ready yet, drop the request
return response.end()
}
// ...
})
})
initializeB(function(b_) {
b = b_
b.on('request', function(request, response) {
if (!a) {
// A isn't ready yet, drop the request
return response.end()
}
// ...
})
})

Categories