Bi-directional communication between a client page and a service worker - javascript

Having a preact app generated by preact-cli (uses workbox), my objective is to register a 'message' event handler on the service worker, post a message from the app and finally receive a response back.
Something like:
/* app.js */
navigator.serviceWorker.postMessage('marco');
const response = ? // get the response somehow
/* sw.js */
addEventListener('message', function (e) { return 'polo' });
I don't have much experience with service workers and there are a lot of moving parts that confuse me, like workbox doing magic on service worker, preact hiding away the code that registers the sercice worker and service workers being tricky to debug in general.
So far I've placed a sw.js file in the src/ directory as instructed by the preact-cli docs here: https://preactjs.com/cli/service-worker/
I know I am supposed to attach an event listener but I can't find documentation on which object to do so.

(Neither Workbox nor Preact have much to do with this question. Workbox allows you to use any additional code in your service worker that you'd like, and Preact should as well for your client app.)
This example page demonstrates sending a message from a client page to a service worker and then responding, using MessageChannel. The relevant helper code used on the client page looks like:
function sendMessage(message) {
return new Promise(function(resolve, reject) {
const messageChannel = new MessageChannel();
messageChannel.port1.onmessage = function(event) {
// The response from the service worker is in event.data
if (event.data.error) {
reject(event.data.error);
} else {
resolve(event.data);
}
};
navigator.serviceWorker.controller.postMessage(message,
[messageChannel.port2]);
});
}
And then in your service worker, you used the MessageChannel's port to respond:
self.addEventListener('message', function(event) {
// Your code here.
event.ports[0].postMessage({
error: // Set error if you want to indicate a failure.
message: // This will show up as event.data.message.
});
});
You could also do the same thing using the Comlink library to simplify the logic.

Related

Can a service-worker communicate with clients during 'install' event handler?

I'm building a PWA that is primary for offline use so I'm caching everything during the install process:
self.addEventListener("install", event => {
self.skipWaiting();
event.waitUntil(
caches.open(CACHE_NAME).then(function(cache) {
return cache.addAll(everything);
})
);
});
Is there any way during this step to communicate the progress of the caching to the client?
It doesn't look like the event in the "install" listener has a clientId to postMessages to.
Do new BroadcastChannel()s work in service workers?
I don't need a precise bytes progress/loaded, maybe just a files completed. Could I replace client.addAll with something like this?
for(let file of everything) {
await cache.add(file);
/* ...message clients another file is cached */
}

How to replace appcache lifecycle event with ServiceWorker lifecycle

I am trying to change appcache to serviceworker in my application for offline work. I have removed manifest tag from html page and created serviceworker.js as of lifecycle events. But, appcache events are being used like this below:
function framework(){
this.appCache = window.applicationCache;
//more other codes
}
var fw = new framework();
Now, lifecycle events are checked like this:
if (fw.appCache) {
/* Checking for an update. Always the first event fired in the sequence.*/ fw.appCache.addEventListener('checking', handleCacheEvent, false);
/* An update was found. The browser is fetching resources.*/ fw.appCache.addEventListener('downloading', handleCacheEvent, false);
/* Fired if the manifest file returns a 404 or 410.
This results in the application cache being deleted. */ fw.appCache.addEventListener('obsolete', handleCacheEvent, false);
/* The manifest returns 404 or 410, the download failed,
or the manifest changed while the download was in progress.*/ fw.appCache.addEventListener('error', handleCacheError, false);
/* Fired for each resource listed in the manifest as it is being fetched.*/fw.appCache.addEventListener('progress', handleProgressEvent, false);
/*Fired after the first cache of the manifest. */ fw.appCache.addEventListener('cached', function(event){handleCacheEvent(event);removeProcessing();$("#processingTextId").html("");}, false);
/* Fired after the first download of the manifest.*/ fw.appCache.addEventListener('noupdate', function(event){handleCacheEvent(event);removeProcessing(); $("#processingTextId").html("");}, false);
/* Fired when the manifest resources have been newly redownloaded. */ fw.appCache.addEventListener('updateready', function(e) {if (fw.appCache.status == fw.appCache.UPDATEREADY) {alert('Successful'); try{fw.appCache.swapCache();window.location.reload();} catch(err){}}}, false);
}
function handleCacheEvent(event) {$("#processingTextId").html(event.type);}
function handleProgressEvent(event) {$("#processingTextId").html("(" + event.loaded + " of "+ event.total +")");}
function handleCacheError(event) {$("#processingTextId").html("Cache failed to update!")
So, my question is how to replace this events with service worker events. My serviceworker is registered and caching the assets properly. Now, i am doing like this in index.html
Registartion
<script type="text/javascript">
if('serviceWorker' in navigator){
navigator.serviceWorker.register('serviceworker.js')
.then(registration =>{
console.log('registration successful:',registration.scope);
})
.catch(err=>{
console.log('registration failed:',err);
});
}
</script>
I have created the seperate serviceworker.js.
How to replace those appcache events with serviceworker?
You won't end up with any of those events automatically when using a service worker. Also, the model for when a service worker populates and updates a cache is much more "open ended" than with AppCache, so translating service worker caching into equivalent AppCache events is not always possible.
In general, though, here are two things that can help:
Read up on the Service Worker Lifecycle. Some events that you might care about could be approximated by listening for equivalent changes to the service worker lifecycle. For instance, if you precache some URLs during service worker installation, then a newly registered service worker leaving the installing state would be roughly equivalent to when an AppCache manifest finishes caching. Similarly, if you detect when there's an update to a previously registered service worker (and that update is due to a change in the list of URLs to precache), then that would roughly correspond to when an AppCache manifest is updated.
If your service worker uses "runtime" caching, where URLs are added to the cache inside of a fetch handler, that you could use the following technique to tell your client page(s) when new items have been cached, using postMessage() to communicate.
Part of your service worker's JavaScript:
const cacheAddAndNotify = async (cacheName, url) => {
const cache = await caches.open(cacheName);
await cache.add(url);
const clients = await self.clients.matchAll();
for (const client of clients) {
client.postMessage({cacheName, url});
}
};
self.addEventListener('fetch', (event) => {
// Use cacheAddAndNotify() to add to your cache and notify all clients.
});
Part of your web app's JavaScript:
if ('serviceWorker' in navigator) {
navigator.serviceWorker.addEventListener('message', (event) => {
const {cacheName, url} = event.data;
// cacheName and url represent the entry that was just cached.
});
}
Again, the specifics of what you listen for and how you react to it are really going to depend on exactly what logic you have in your service worker. Don't expect there to be a 1:1 mapping between all events. Instead, use this migration as an opportunity to think about what cache-related changes you actually care about, and focus on listening for those (via service worker lifecycle events, or postMessage() communication).

What exactly triggers this service worker code to run?

Create-react-app comes with a registerServiceWorker.js file that contains code to register a service worker. I'm just a bit confused as to how it works. The code in question is:
function registerValidSW(swUrl) {
navigator.serviceWorker
.register(swUrl)
.then(registration => {
registration.onupdatefound = () => {
const installingWorker = registration.installing;
installingWorker.onstatechange = () => {
if (installingWorker.state === '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 your web app.
console.log('New content is available; please refresh.');
} 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.');
}
}
};
};
})
.catch(error => {
console.error('Error during service worker registration:', error);
});
}
What needs to happen for that first console log, the one that displays "New content is available; please refresh," to display?
More specifically, how can I trigger this code to run when index.html changes (in the event that a script filename changes).
Let's break it down step by step.
navigator.serviceWorker.register Promise is resolved when the valid Service Worker existence has been established
registration.onupdatefound registers a listener for an event that is fired when the HTTP request for Service Worker has been resolved to some other file than previously (or when the SW has been found for the first time)
registration.installing.onstatechange registers a listener for the new Service Worker's lifecycle changes (from installing to installed, from installed to activating etc.)
if (installingWorker.state === 'installed') filters out all the states other than installed - so its positive branch will be executed after each new SW has been installed
if (navigator.serviceWorker.controller) checks if the page is currently controlled by any (previous) Service Worker. If true then we're handling the aforementioned update scenario here.
So summing up - this console.log will execute after the updated (not the first one) Service Worker has been correctly installed.
It will not be triggered after index.html change. It's only Service Worker code (pointed to by serviceWorker.register method) that is checked against. Note also that normally browsers (or Chrome at least?) do not check for the new SW version for 24h after the current one was downloaded. Note also that plain old HTTP cache set for the Service Worker file might mess up here if it was send with too aggresive cache header.

Register inline service worker in web app

I'm toying with adding web push notifications to a web app hosted with apps script. To do so, I need to register a service worker and I ran into an error with a cross-domain request. Raw content is hosted from *.googleusercontent.com while the script URL is script.google.com/*.
I was able to successfully create an inline worker using this post on creating inline URLs created from a blob with the script. Now I'm stuck at the point of registering the worker in the browser.
The following model works:
HTML
<!-- Display the worker status -->
<div id="log"></log>
Service Worker
<script id="worker" type="javascript/worker">
self.onmessage = function(e) {
self.postMessage('msg from worker');
};
console.log('[Service Worker] Process started');
})
</script>
Inline install
<script>
function log(msg) {
var fragment = document.createDocumentFragment();
fragment.appendChild(document.createTextNode(msg));
fragment.appendChild(document.createElement('br'));
document.querySelector("#log").appendChild(fragment);
}
// Create the object with the script
var blob = new Blob([ document.querySelector("#worker").textContent ]);
// assign a absolute URL to the obejct for hosting
var worker = new Worker(window.URL.createObjectURL(blob));
worker.onmessage = function(e) {
log("Received: " + e.data);
}
worker.postMessage('start');
// navigator.serviceWorker.register(worker) this line fails
</script>
navigator.serviceWorker.register(worker); returns a 400: Bad HTTP response code error.
Can inline workers be installed? Or do all installed workers have to come from external scripts?
It's worth noting that Web Workers and Service Workers, which are used with push notifications, are different (see this SO response to read about the difference).
According to MDN Service Workers require a script URL. In Google Apps Script you could serve a service worker file with something like:
function doGet(e) {
var file = e.parameter.file;
if (file == 'service-worker.js'){
return ContentService.createTextOutput(HtmlService.createHtmlOutputFromFile('service-worker.js').getContent())
.setMimeType(ContentService.MimeType.JAVASCRIPT);
} else {
return HtmlService.createHtmlOutputFromFile('index');
}
}
But as in this example (source code here) because of the way web apps are served you'll encounter the error:
SecurityError: Failed to register a ServiceWorker: The script resource is behind a redirect, which is disallowed.
There was a proposal for Foreign Fetch for Service Workers but it's still in draft.

FileReaderSync undefined inside Web Worker in Firefox extension using Add-on SDK

I managed to get a Web Worker (not a content/worker) in my Firefox add-on using the Add-on SDK. I followed Wladimir's advice here to get the Worker class working: Concurrency with Firefox add-on script and content script
Now, I can launch a worker in my code and can talk to it by sending/receiving messages.
This is my main.js file:
// spawn our log reader worker
var worker = new Worker(data.url('log-reader.js'));
// send and respond to some dummy messages
worker.postMessage('halo');
worker.onmessage = function(event) {
console.log('received msg from worker: ' + event.data);
};
This is my log-reader.js file:
// this function gets called when main.js sends a msg to this worker
// using the postMessage call
onmessage = function(event) {
var info = event.data;
// reply back
postMessage('hey addon, i got your message: ' + info);
if (!!FileReaderSync) {
postMessage('ERROR: FileReaderSync is not supported');
} else {
postMessage('FileReaderSync is supported');
}
// var reader = new FileReaderSync();
// postMessage('File contents: ' + reader.readAsText('/tmp/hello.txt'));
};
My problem is that the FileReaderSync class is not defined inside the log-reader.js file, and as a result I get the error message back. If I uncomment the last lines where FileReaderSync is actually used, I will never get the message back in my addon.
I tried using the same trick I used for Worker, by creating a dummy.jsm file and importing in main.js, but FileReaderSync will only be available in main.js and not in log-reader.js:
// In dummy.jsm
var EXPORTED_SYMBOLS=["Worker"];
var EXPORTED_SYMBOLS=["FileReaderSync"];
// In main.js
var { Worker, FileReaderSync } = Cu.import(data.url('workers.jsm'));
Cu.unload(data.url("workers.jsm"));
I figure there has to be a solution since the documentation here seems to indicate that the FileReaderSync class should be available to a Web Worker in Firefox:
This interface is only available in workers as it enables synchronous I/O that could potentially block.
So, is there a way to make FileReaderSync available and usable in the my Web Worker code?
Actually, your worker sends "ERROR" if FileReaderSync is defined since you negated it twice. Change !!FileReaderSync to !FileReaderSync and it will work correctly.
I guess that you tried to find the issue with the code you commented out. The problem is, reader.readAsText('/tmp/hello.txt') won't work - this method expects a blob (or file). The worker itself cannot construct a file but you can create it in your extension and send to the worker with a message:
worker.postMessage(new File("/tmp/hello.txt"));
Note: I'm not sure whether the Add-on SDK defines the File constructor, you likely have to use the same trick as for the Worker constructor.
The worker can then read the data from this file:
onmessage = function(event)
{
var reader = new FileReaderSync();
postMessage("File contents: " + reader.readAsText(event.data));
}

Categories