I'm having trouble to wrap my head around the Clients.claim API of the ServiceWorker. From what I understand (here and here) I can call claim()
on the service worker activate event to prevent having to refresh the page to initialize the ServiceWorker. I can't get it to work though and always end up having to refresh. Here's my code:
Inside the service worker:
self.addEventListener('install', function (event) {
self.skipWaiting();
event.waitUntil(caches.open(CURRENT_CACHE_DICT.prefetch)
.then(function(cache) {
var cachePromises = PREFETCH_URL_LIST.map(function(prefetch_url) {
var url = new URL(prefetch_url, location.href),
request = new Request(url, {mode: 'no-cors'});
return fetch(request).then(function(response) {
if (response.status >= 400) {
throw new Error('request for ' + prefetch_url +
' failed with status ' + response.statusText);
}
return cache.put(prefetch_url, response);
}).catch(function(error) {
console.error('Not caching ' + prefetch_url + ' due to ' + error);
});
});
return Promise.all(cachePromises).then(function() {
console.log('Pre-fetching plete.');
});
}).catch(function(error) {
console.error('Pre-fetching failed:', error);
})
);
});
self.addEventListener('activate', function (event) {
// claim the scope immediately
// XXX does not work?
//self.clients.claim();
event.waitUntil(self.clients.claim()
.then(caches.keys)
.then(function(cache_name_list) {
return Promise.all(
cache_name_list.map(function() {...}
);
})
);
});
The above runs but I'm ending up having to refresh and found anIllegal invocation
error in the Chrome ServiceWorker internals. If I remove the clients.claim
from the waitUntil
handler and unment the previous one, I get no errors, but I still have to refresh. The debugger shows:
Console: {"lineNumber":128,"message":"Pre-fetching plete.","message_level":1,"sourceIdentifier":3,"sourceURL":""}
Console: {"lineNumber":0,"message":"Uncaught (in promise) TypeError: Illegal invocation","message_level":3,"sourceIdentifier":1,"sourceURL":""}
The refresh is triggered like this:
function waitForInstallation(registration) {
return new RSVP.Promise(function(resolve, reject) {
if (registration.installing) {
registration.installing.addEventListener('statechange', function(e) {
if (e.target.state == 'installed') {
resolve();
} else if (e.target.state == 'redundant') {
reject(e);
}
});
} else {
resolve();
}
});
}
// refreshing should not be necessary if scope is claimed on activate
function claimScope(installation) {
return new RSVP.Promise(function (resolve, reject) {
if (navigator.serviceWorker.controller) {
resolve();
} else {
reject(new Error("Please refresh to initialize serviceworker."));
}
});
}
rJS(window)
.declareMethod('render', function (my_option_dict) {
var gadget = this;
if ('serviceWorker' in navigator) {
return new RSVP.Queue()
.push(function () {
return navigator.serviceWorker.register(
my_option_dict.serviceworker_url,
{scope: my_option_dict.scope}
);
})
.push(function (registration) {
return waitForInstallation(registration);
})
.push(function (installation) {
return claimScope(installation);
})
.push(null, function (my_error) {
console.log(my_error);
throw my_error;
});
} else {
throw new Error("Browser does not support serviceworker.");
}
});
Question:
How do I correctly prevent the page from having to be refreshed to activate the ServiceWorker using claim
? None of the links I found mentioned having to explicitly check for controller
but I assume if a ServiceWorker is active it would have a controller accessible.
Thanks for shedding some info.
EDIT:
Figured it out with help from below. This made it work for me:
// runs while an existing worker runs or nothing controls the page (update here)
self.addEventListener('install', function (event) {
event.waitUntil(caches.open(CURRENT_CACHE_DICT.dictionary)
.then(function(cache) {
var cache_promise_list = DICTIONARY_URL_LIST.map(function(prefetch_url) {...});
return Promise.all(cache_promise_list).then(function() {
console.log('Pre-fetching plete.');
});
})
.then(function () {
// force waiting worker to bee active worker (claim)
self.skipWaiting();
}).catch(function(error) {
console.error('Pre-fetching failed:', error);
})
);
});
// runs active page, changes here (like deleting old cache) breaks page
self.addEventListener('activate', function (event) {
event.waitUntil(caches.keys()
.then(function(cache_name_list) {
return Promise.all(
cache_name_list.map(function(cache_name) { ... })
);
})
.then(function () {
return self.clients.claim();
})
);
});
Triggering script:
var SW = navigator.serviceWorker;
function installServiceWorker(my_option_dict) {
return new RSVP.Queue()
.push(function () {
return SW.getRegistration();
})
.push(function (is_registered_worker) {
// XXX What if this isn't mine?
if (!is_registered_worker) {
return SW.register(
my_option_dict.serviceworker_url, {
"scope": my_option_dict.scope
}
);
}
return is_registered_worker;
});
}
function waitForInstallation(registration) {
return new RSVP.Promise(function(resolve, reject) {
if (registration.installing) {
// If the current registration represents the "installing" service
// worker, then wait until the installation step pletes (during
// which any defined resources are pre-fetched) to continue.
registration.installing.addEventListener('statechange', function(e) {
if (e.target.state == 'installed') {
resolve(registration);
} else if (e.target.state == 'redundant') {
reject(e);
}
});
} else {
// Otherwise, if this isn't the "installing" service worker, then
// installation must have beenpleted during a previous visit to this
// page, and the any resources will already have benn pre-fetched So
// we can proceed right away.
resolve(registration);
}
});
}
// refreshing should not be necessary if scope is claimed on activate
function claimScope(registration) {
return new RSVP.Promise(function (resolve, reject) {
if (registration.active.state === 'activated') {
resolve();
} else {
reject(new Error("Please refresh to initialize serviceworker."));
}
});
}
rJS(window)
.ready(function (my_gadget) {
my_gadget.property_dict = {};
})
.declareMethod('render', function (my_option_dict) {
var gadget = this;
if (!SW) {
throw new Error("Browser does not support serviceworker.");
}
return new RSVP.Queue()
.push(function () {
return installServiceWorker(my_option_dict),
})
.push(function (my_promise) {
return waitForInstallation(my_promise);
})
.push(function (my_installation) {
return claimScope(my_installation);
})
.push(function () {
return gadget;
})
.push(null, function (my_error) {
console.log(my_error);
throw my_error;
});
});
I'm having trouble to wrap my head around the Clients.claim API of the ServiceWorker. From what I understand (here and here) I can call claim()
on the service worker activate event to prevent having to refresh the page to initialize the ServiceWorker. I can't get it to work though and always end up having to refresh. Here's my code:
Inside the service worker:
self.addEventListener('install', function (event) {
self.skipWaiting();
event.waitUntil(caches.open(CURRENT_CACHE_DICT.prefetch)
.then(function(cache) {
var cachePromises = PREFETCH_URL_LIST.map(function(prefetch_url) {
var url = new URL(prefetch_url, location.href),
request = new Request(url, {mode: 'no-cors'});
return fetch(request).then(function(response) {
if (response.status >= 400) {
throw new Error('request for ' + prefetch_url +
' failed with status ' + response.statusText);
}
return cache.put(prefetch_url, response);
}).catch(function(error) {
console.error('Not caching ' + prefetch_url + ' due to ' + error);
});
});
return Promise.all(cachePromises).then(function() {
console.log('Pre-fetching plete.');
});
}).catch(function(error) {
console.error('Pre-fetching failed:', error);
})
);
});
self.addEventListener('activate', function (event) {
// claim the scope immediately
// XXX does not work?
//self.clients.claim();
event.waitUntil(self.clients.claim()
.then(caches.keys)
.then(function(cache_name_list) {
return Promise.all(
cache_name_list.map(function() {...}
);
})
);
});
The above runs but I'm ending up having to refresh and found anIllegal invocation
error in the Chrome ServiceWorker internals. If I remove the clients.claim
from the waitUntil
handler and unment the previous one, I get no errors, but I still have to refresh. The debugger shows:
Console: {"lineNumber":128,"message":"Pre-fetching plete.","message_level":1,"sourceIdentifier":3,"sourceURL":""}
Console: {"lineNumber":0,"message":"Uncaught (in promise) TypeError: Illegal invocation","message_level":3,"sourceIdentifier":1,"sourceURL":""}
The refresh is triggered like this:
function waitForInstallation(registration) {
return new RSVP.Promise(function(resolve, reject) {
if (registration.installing) {
registration.installing.addEventListener('statechange', function(e) {
if (e.target.state == 'installed') {
resolve();
} else if (e.target.state == 'redundant') {
reject(e);
}
});
} else {
resolve();
}
});
}
// refreshing should not be necessary if scope is claimed on activate
function claimScope(installation) {
return new RSVP.Promise(function (resolve, reject) {
if (navigator.serviceWorker.controller) {
resolve();
} else {
reject(new Error("Please refresh to initialize serviceworker."));
}
});
}
rJS(window)
.declareMethod('render', function (my_option_dict) {
var gadget = this;
if ('serviceWorker' in navigator) {
return new RSVP.Queue()
.push(function () {
return navigator.serviceWorker.register(
my_option_dict.serviceworker_url,
{scope: my_option_dict.scope}
);
})
.push(function (registration) {
return waitForInstallation(registration);
})
.push(function (installation) {
return claimScope(installation);
})
.push(null, function (my_error) {
console.log(my_error);
throw my_error;
});
} else {
throw new Error("Browser does not support serviceworker.");
}
});
Question:
How do I correctly prevent the page from having to be refreshed to activate the ServiceWorker using claim
? None of the links I found mentioned having to explicitly check for controller
but I assume if a ServiceWorker is active it would have a controller accessible.
Thanks for shedding some info.
EDIT:
Figured it out with help from below. This made it work for me:
// runs while an existing worker runs or nothing controls the page (update here)
self.addEventListener('install', function (event) {
event.waitUntil(caches.open(CURRENT_CACHE_DICT.dictionary)
.then(function(cache) {
var cache_promise_list = DICTIONARY_URL_LIST.map(function(prefetch_url) {...});
return Promise.all(cache_promise_list).then(function() {
console.log('Pre-fetching plete.');
});
})
.then(function () {
// force waiting worker to bee active worker (claim)
self.skipWaiting();
}).catch(function(error) {
console.error('Pre-fetching failed:', error);
})
);
});
// runs active page, changes here (like deleting old cache) breaks page
self.addEventListener('activate', function (event) {
event.waitUntil(caches.keys()
.then(function(cache_name_list) {
return Promise.all(
cache_name_list.map(function(cache_name) { ... })
);
})
.then(function () {
return self.clients.claim();
})
);
});
Triggering script:
var SW = navigator.serviceWorker;
function installServiceWorker(my_option_dict) {
return new RSVP.Queue()
.push(function () {
return SW.getRegistration();
})
.push(function (is_registered_worker) {
// XXX What if this isn't mine?
if (!is_registered_worker) {
return SW.register(
my_option_dict.serviceworker_url, {
"scope": my_option_dict.scope
}
);
}
return is_registered_worker;
});
}
function waitForInstallation(registration) {
return new RSVP.Promise(function(resolve, reject) {
if (registration.installing) {
// If the current registration represents the "installing" service
// worker, then wait until the installation step pletes (during
// which any defined resources are pre-fetched) to continue.
registration.installing.addEventListener('statechange', function(e) {
if (e.target.state == 'installed') {
resolve(registration);
} else if (e.target.state == 'redundant') {
reject(e);
}
});
} else {
// Otherwise, if this isn't the "installing" service worker, then
// installation must have beenpleted during a previous visit to this
// page, and the any resources will already have benn pre-fetched So
// we can proceed right away.
resolve(registration);
}
});
}
// refreshing should not be necessary if scope is claimed on activate
function claimScope(registration) {
return new RSVP.Promise(function (resolve, reject) {
if (registration.active.state === 'activated') {
resolve();
} else {
reject(new Error("Please refresh to initialize serviceworker."));
}
});
}
rJS(window)
.ready(function (my_gadget) {
my_gadget.property_dict = {};
})
.declareMethod('render', function (my_option_dict) {
var gadget = this;
if (!SW) {
throw new Error("Browser does not support serviceworker.");
}
return new RSVP.Queue()
.push(function () {
return installServiceWorker(my_option_dict),
})
.push(function (my_promise) {
return waitForInstallation(my_promise);
})
.push(function (my_installation) {
return claimScope(my_installation);
})
.push(function () {
return gadget;
})
.push(null, function (my_error) {
console.log(my_error);
throw my_error;
});
});
Share
Improve this question
edited Jan 30, 2017 at 13:02
frequent
asked Jan 28, 2017 at 16:35
frequentfrequent
28.6k61 gold badges187 silver badges336 bronze badges
1 Answer
Reset to default 7Firstly, you are seem to be getting the error because of a typo in your code. See notes about it at the bottom.
Besides, skipWaiting()
and Clients.claim()
does both install and activate new SW with a single request. But quite naturally you will only get static assets like css after you reload.
So, even when equipped with skipWaiting()
and Clients.claim()
, you need two page reloads to see updated static
content like new html or styles;
Page load #1
- Request to
sw.js
is made, and since SW contents is changedinstall
event is fired on it. - Also
activate
event is fired, since you haveself.skipWaiting()
in yourinstall
handler. - Consequently, your
activate
handler run and there is yourself.clients.claim()
call. Which will order the SW to take over the control of all the clients which under control of it's predecessor. - At this point, assets in cache are updated and your pages are all controlled by new service worker. Any Ajax request in range of service worker will return newly cached responses, for example.
Page load #2
Your app loads, and your SW responds from cache by hijacking the requests as usual. But now caches are up-to-date, and user gets to use app pletely with new assets.
The error you are getting
Uncaught (in promise) TypeError: Illegal invocation
error must be due to a missing a parenthesis in your activate
handler;
event.waitUntil(self.clients.claim()
.then(caches.keys)
.then(function(cache_name_list) {
return Promise.all(
cache_name_list.map(function() {...}
); <-- Here is our very lonely single parenthesis.
})
);
That error should go away if you fix it.