Looks like Service Worker runs in a worker context and has no access to the DOM. However, once Service Worker installed, I want my users to know that the app will now work offline. How can I do that?
When the Service Worker is in activated state, this is the perfect time to display the toast 'Content is cached for offline use'. Try something below code while registering your service worker.
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/service-worker.js').then(function(reg) {
// updatefound is fired if service-worker.js changes.
reg.onupdatefound = function() {
var installingWorker = reg.installing;
installingWorker.onstatechange = function() {
switch (installingWorker.state) {
case 'installed':
if (navigator.serviceWorker.controller) {
// At this point, the old content will have been purged and the fresh content will
// have been added to the cache.
// It's the perfect time to display a "New content is available; please refresh."
// message in the page's interface.
console.log('New or updated content is available.');
} else {
// At this point, everything has been precached.
// It's the perfect time to display a "Content is cached for offline use." message.
console.log('Content is now available offline!');
}
break;
case 'redundant':
console.error('The installing service worker became redundant.');
break;
}
};
};
}).catch(function(e) {
console.error('Error during service worker registration:', e);
});
}
After testing #Prototype Chain's answer above, I wanted to use named functions as opposed to nested anonymous functions as event handlers to make code more pleasant to look at for my taste, and hopefully easier to understand later/for others.
But only after spending some time sorting docs, I managed to listen correct events on correct objects. So sharing my working example here in hopes of saving someone else from tedious process.
// make sure that Service Workers are supported.
if (navigator.serviceWorker) {
navigator.serviceWorker.register('/sw.js')
.then(function (registration) {
console.log("ServiceWorker registered");
// updatefound event is fired if sw.js changed
registration.onupdatefound = swUpdated;
}).catch(function (e) {
console.log("Failed to register ServiceWorker", e);
})
}
function swUpdated(e) {
console.log('swUpdated');
// get the SW which being installed
var sw = e.target.installing;
// listen for installation stage changes
sw.onstatechange = swInstallationStateChanged;
}
function swInstallationStateChanged(e) {
// get the SW which being installed
var sw = e.target;
console.log('swInstallationStateChanged: ' + sw.state);
if (sw.state == 'installed') {
// is any sw already installed? This function will run 'before' 'SW's activate' handler, so we are checking for any previous sw, not this one.
if (navigator.serviceWorker.controller) {
console.log('Content has updated!');
} else {
console.log('Content is now available offline!');
}
}
if (sw.state == 'activated') {
// new|updated SW is now activated.
console.log('SW is activated!');
}
}
Related
service worker file:
registration.onupdatefound = () => {
const installingWorker = registration.installing;
if (installingWorker == null) {
return;
}
installingWorker.onstatechange = () => {
if (installingWorker.state === 'installed') {
if (navigator.serviceWorker.controller) {
// At this point, the updated precached content has been fetched,
// but the previous service worker will still serve the older
// content until all client tabs are closed.
console.log(
'New content is available and will be used when all ' +
'tabs for this page are closed.
);
// Execute callback
registration.postMessage({action: 'skipWaiting'})
if (config && config.onUpdate) {
config.onUpdate(registration);
}
} else {
// At this point, everything has been precached.
// It's the perfect time to display a
// "Content is cached for offline use." message.
console.log('Content is cached for offline use.');
// Execute callback
if (config && config.onSuccess) {
config.onSuccess(registration);
}
}
}
};
};
}`
self.addEventListener('message', function(event) {
if (event.data.action === 'skipWaiting') {
self.skipWaiting();
}
});
//listening in app.js controller change event
// reload once when the new Service Worker starts activating
var refreshing;
navigator.serviceWorker.addEventListener('controllerchange',
function() {
if (refreshing) return;
refreshing = true;
window.location.reload();
}
);
what it actually does:
If it finds any update, sending a postMessage as skipWating
2.Once the message recieved by listener , it calls the skipWaiting()
3.then controllerchange event gets called where we refreshing the page.
Why it gets into infinite loop of reload()
Your first bit of code, everything under registration.onupdatefound, shouldn't be in the service worker file. It should be in your web app's window context.
The bit that should be in your service worker file is:
self.addEventListener('message', function(event) {
if (event.data.action === 'skipWaiting') {
self.skipWaiting();
}
}
There's documentation for another "recipe" that accomplishes this at https://developers.google.com/web/tools/workbox/guides/advanced-recipes#offer_a_page_reload_for_users. It uses the workbox-window library to simplify things a bit.
I am developing a chat application using firebase with ionic 3. I have to change status to 'offline' when disconnected from the internet.
I have referred to the following sites:
1) https://firebase.google.com/docs/database/web/offline-capabilities
2) https://firebase.google.com/docs/reference/js/firebase.database.OnDisconnect
this.disconnectSubscription = this.network
.onDisconnect()
.subscribe(() => {
this.online = false;
var userLastOnlineRef = firebase.database().ref('/accounts/'this.loggedInUserId);
userLastOnlineRef.onDisconnect().set({status:offline});
});
When I turn off the internet connection the status of offline will not get changed.
You can listen for online and offline event like this
window.addEventListener("online", this.doSomethingWhenOnline);
window.addEventListener("offline", this.doSomethingWhenOffline);
doSomethingWhenOnline() {
}
doSomethingWhenOffline() {
}
My intention is for one message to be passed to the worker after it is created, and for that message to be logged by the worker and a reply to be sent back. What happens is that the message sent to the web worker is logged twice, and only one reply is sent. If you refresh the page or pause execution using developer tools, the message is correctly logged once.
I created a miniature extension to demonstrate this. There is a background script listening for a click on the browser action button. When it is clicked, a new tab is opened, and this tab generates 2 web workers. These workers listen for messages and log them, and send a reply stating that the message was recieved. The newly opened tab then sends the message, and logs the reply.
Google drive with all of the files needed to run this as a chrome extension
Image demonstrating the double logging of the message
Some references:
general web worker
use
.postMessage to
worker
chrome browser action
chrome tabs API
I have been working on my extension for a while now and have run into all sorts of fun bugs, particularly with async code. In the end I was always able to either debug them myself or find a reference that explained the problem. Thank you for any help you can give!
background.js
const tabUrl = chrome.extension.getURL("tab.html");
function browserActionCallback(tab) {
function createResultsTab() {
chrome.tabs.create({
"url": tabUrl,
"index": tab.index + 1
}, function() {
console.log("Tab created");
});
}
(function tabHandler() {
chrome.tabs.query({"url":tabUrl}, (tabQueryResults) => {
if (tabQueryResults.length > 0) {
chrome.tabs.remove(tabQueryResults[0].id, () => {
createResultsTab();
});
} else {
createResultsTab();
}
});
})();
}
chrome.browserAction.onClicked.addListener((tab) => {
browserActionCallback(tab);
});
tab.js
function createWorkers(num) {
/*in original extension I intended to let the user change number of workers in options*/
var workers = new Array(num);
for (let i = 0; i < num; i++) {
workers[i] = new Worker("worker.js");
}
return workers;
}
function messageWorkers(workers) {
let len = workers.length
for (let i = 0; i < len; i++) {
let message = "Connection " + i + " started";
workers[i].postMessage(message);
workers[i].onmessage = function(e) {
console.log(e.data);
}
}
}
var numWorkers = 2;
const WORKERS = createWorkers(numWorkers);
console.log("Before");
messageWorkers(WORKERS);
console.log("After");
worker.js
onmessage = function(msg) {
console.log(msg.data);
postMessage("Worker reply- " + msg.data);
}
EDIT 1: changing the tab.js messageWorkers function's for loop such that onmessage is set before postMessage does not change the results. Sending multiple messages also doesn't change results. If I have a console.log statement at the start of worker's onmessage function which logs that the function has begun, it too logs twice. To reiterate, this only happens when this tab is created by the background script and not when the page is refreshed or debugger is used.
EDIT 2: in the devtools console, there is a drop down menu to choose between top and workers. Checking the option 'selected context only' makes the repeated logging disappear, however this view is a little narrower than I would like
I can confirm this issue. Sample code to reproduce it:
index.js
function worker() {
console.log('this logged twice');
}
const workerBlob = new Blob(
[worker.toString().match(/function[^{]+\{([\s\S]*)\}$/)[1]],
{type: 'text/javascript'}
);
const workerBlobUrl = URL.createObjectURL(workerBlob);
new Worker(workerBlobUrl);
Use index.js as a Chrome extension background script and "this logged twice" will be logged twice when loading the extension or re-launching Chrome.
I am connection through Vertx eventbus (SockJS to my Java based backend. Everything work fine, However, I cannot find a way to send an initial message.
Is there a way to send back data when SockJS bridge receives SOCKET_CREATED to the sockjs browser side?
Thank you.
Taken from their documentation:
if (event.type() == SOCKET_CREATED || event.type() == SOCKET_CLOSED)
{
//...
vertx.eventBus().publish("fromServer", jmsg.toJSONString());
}
Your event instantiation may be different, but that would be how you check for the specific event and run code after it has occurred
You can check this code , where I'm using EventBus.
Here is the Reference code
this.eventBus = new EventBus(this.URL);
this.eventBus.onopen = (e) => {
this._opened = true;
console.log("open connection");
this.callHandlers('open', e);
this.eventBus.publish("http://localhost:8082", "USER LOGIN INFO");
this.eventBus.registerHandler("http://localhost:8081/pushNotification", function (error, message) {
console.log(message.body);
//$("<div title='Basic dialog'>Test message</div>").dialog();
});
}
this.eventBus.onclose = (e) => {
this.callHandlers('close', e);
}
}
What I stated in the title only happens in chrome as in firefox "('serviceWorker' in navigator)" is always false and "console.warn('Service workers aren\'t supported in this browser.');" triggers instead.
If you clear the browser or run incognito then it works initially but when you log out for the first time and then log back in the problems start.
This is what it looks like in the network log
Here's the code in which I register the SW:
function activateSW(){
if('serviceWorker' in navigator){
if(window.location.pathname != '/'){
//register with API
if(!navigator.serviceWorker.controller) navigator.serviceWorker.register('/service-worker', { scope: '/' });
//once registration is complete
navigator.serviceWorker.ready.then(function(serviceWorkerRegistration){
//get subscription
serviceWorkerRegistration.pushManager.getSubscription().then(function(subscription){
//enable the user to alter the subscription
$('.js-enable-sub-test').removeAttr("disabled");
//set it to allready subscribed if it is so
if(subscription){
$('.js-enable-sub-test').prop("checked", true);
$('.js-enable-sub-test').parent().addClass('checked');
}
});
});
}
}else{
console.warn('Service workers aren\'t supported in this browser.');
}
}
'/service-worker' is a request that gets sent to index.php (via .htaccess). It eventually end up in this function:
function serviceworkerJS($params){
$hash = API::request('settings', 'getSWHash', '');
if($hash != false){
setcookie('SW_Hash', $hash, time() + (86400 * 365 * 10), "/");
header('Content-type: text/javascript');
echo "'use strict';
var hash = '".$hash."';";
include(ROOT_PATH.'public/js/service-worker.js');
}elseif(isset($_COOKIE['SW_Hash'])){
header('Content-type: text/javascript');
echo "'use strict';
var hash = '".$_COOKIE['SW_Hash']."';";
include(ROOT_PATH.'public/js/service-worker.js');
}else{
header('HTTP/1.1 404 Not Found');
}
}
Service-worker.js as seen in chrome://serviceworker-internals/ looks like this:
(Several references to the adress has been replaced by stars)
'use strict';
var hash = 'bd8e78963deebf350f851fbf8cdc5080';
var *****_API_ENDPOINT = 'https://*********.***/';
//For displaying notifications
function showNotification(title, body, icon, data, id) {
var notificationOptions = {
body: body,
icon: icon,
tag: id,
data: data
};
//possibly unnecessary
if(self.registration.showNotification){
return self.registration.showNotification(title, notificationOptions);
}else{
return new Notification(title, notificationOptions);
}
}
//asks the server for messages and sends them for displaying.
function getMessages(event){
//showNotification('debug', 'initial', '', '', 'debug1');
//build question
var FD = new FormData();
FD.append('hash', hash);
//ask start20 for the notifications
event.waitUntil(
fetch(*****_API_ENDPOINT + 'ajax-get-SW-notification/', {method: 'post', body: FD}).then(function(response){
//something went wrong
if (response.status !== 200){
console.log('Error communicating with ******, code: ' + response.status);
showNotification('debug', 'picnic', '', '', 'debug2');
throw new Error();
}
//decode the response
return response.json().then(function(data){
var len = data.notifications.length;
//showNotification('debug', len, '', '', 'propertyName');
//Display
for(var i = 0; i < len -1; i++){
showNotification(data.notifications[i].title,
data.notifications[i].body,
data.notifications[i].imageurl,
data.notifications[i].linkurl,
data.notifications[i].hash);
}
//the last one needs to be returned to complete the promise
return showNotification(data.notifications[len -1].title,
data.notifications[len -1].body,
data.notifications[len -1].imageurl,
data.notifications[len -1].linkurl,
data.notifications[len -1].hash);
});
})
);
}
//when the user installs a new SW
/*self.addEventListener('activate', function(event){
//getMessages(event);
//event.preventDefault();
event.waitUntil(return self.registration.showNotification('bicnic', { body: '*p' }));
});*/
//when the serviceworker gets a puch from the server
self.addEventListener('push', function(event){
getMessages(event);
event.preventDefault();
});
//get the link associated witht he message when a user clicks on it
self.addEventListener('notificationclick', function(event){
//ask if the notification has any link associated with it
var FD = new FormData();
FD.append('hash', event.notification.tag);
//get the link
event.waitUntil(
fetch(******_API_ENDPOINT + 'ajax-notification-has-link/', {method: 'post', body: FD}).then(function(response){
//something went wrong
if (response.status !== 200){
console.log('Error communicating with ********, code: ' + response.status);
return;
}
//decode the response
return response.json().then(function(data){
//if there's a link associated with the message hash
if(data.link){
console.log(******_API_ENDPOINT + 'notification-link/' + event.notification.tag);
return clients.openWindow(*****_API_ENDPOINT + 'notification-link/' + event.notification.tag);
}
});
})
);
});
//unnecessary?
/*self.addEventListener('install', function(event){
//event.preventDefault();
});
self.addEventListener("fetch", function(event) {
});//*/
Now if you comment away "if(!navigator.serviceWorker.controller) navigator.serviceWorker.register( '/service-worker', { scope: '/' });" then the issue disappears but ofc the serviceworker subscribing and unsubscribing stops working. (The if statement doesn't seem to do much and was only added in an attempt to solve this)
I've tried numerous versions of activateSW() with various conditions for the different expressions to run and haven't managed to make a version that works without breaking the serviceworker. I have also tried to catch errors on various points (registration, posts) but this has been unsuccessful as none of them throws any.
What I suspect might be the problem is that as you register the serviceworker an activate event is triggered. Now if you catch this and complete the event promise then you become unable to subscribe. However I suspect that the serviceworker remains active as you log out and this causes a problem.
If you have questions or want to see more code then just ask.
edit; here's the solution:
self.addEventListener("fetch", function(event) {
event.respondWith(
fetch(event.request)
);
});
What you're seeing is confusing noise in the Network panel of Chrome DevTools, but shouldn't have a practical effect on your application's behavior.
If a service worker controls a page but doesn't include a fetch event handler, current versions of Chrome (M44 stable, and M46 canary) end up logging all network requests in the Network panel with (canceled) status, like you're seeing. The actual request is still made without service worker intervention, which is the second, successful logged entry. Things should work as expected from the perspective of your page, but I completely understand the confusion.
I have heard that there are plans to make Chrome's service worker implementation behave much more like a "no-op" when a network request is made and there's no fetch event handler. I don't know how far along those plans are, but I would expect that the noise you're seeing logged to go away once that happens.