Register inline service worker in web app - javascript

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.

Related

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).

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

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.

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));
}

Chrome - How To Load Channel Token?

I have an extension where I authenticate the user when they enable to app. The server then returns a channel token, which I use to establish a channel. The code for the authentication occurs in script.js, where-as the channel creation is in background.html. My question is how would I get the channelToken into background.html, when the authentication runs after background.html is loaded?
I want to note that I am running Google App Engine (Python) as my server. I have also copied the javascript code from here and placed it in my manifest as oppose to putting <script type="text/javascript" src="/_ah/channel/jsapi"></script> in background.html.
//background.html
var channel = new goog.appengine.Channel(channelToken);
var socket = channel.open()
socket.onopen = function() {
// Do stuff right after opening a channel
console.log('socket opened');
}
socket.onmessage = function(evt) {
// Do more cool stuff when a channel message comes in
console.log('message recieved');
console.log(evt);
}
You should use messagePassing to inform the background.html if a channelToken is received.
http://code.google.com/chrome/extensions/messaging.html

Is it possible to use workers in a Greasemonkey script?

I would like to use the Web Worker facility introduced in Firefox 3.5 to enhance a Greasemonkey script I'm working on.
Is this even possible?
I've done some experimentation, but I can't get past the issue of loading a worker script from an arbitrary domain.
For example, this does not work:
var myWorker = new Worker("http://dl.getdropbox.com/u/93604/js/worker.js");
This code generates an error message in my Firebug console:
Failed to load script:
http://dl.getdropbox.com/u/93604/js/worker.js
(nsresult = 0x805303f4)
Apparently there's a limitation that does not allow you to start a worker from a URL that's not relative to the base URL of the calling script. You can load a worker script at a relative URL like this just fine:
var myWorker = new Worker("worker.js");
But there's no way for me to get the worker script on the user's filesystem so that it could be at a path relative to the calling script.
Am I screwed here? Should I give up on trying to use workers within my Greasemonkey script?
For years I thought it wasn't possible to use web workers in GM. Of course the first idea was to use data-urls. But the Worker constructor didn't seem to accept them.
Today I tried it again and it worked without any problems at first. Only when I started to use functions of the GM API the Worker constructor stopped working.
Seemingly Firefox has a bug that prevents you from accessing Worker from a sandbox with X-ray vision. Even evaluating typeof Worker throws an exception. So the only way to use workers is to get the unwrapped version from the unwrapped window:
var echoWorker = new unsafeWindow.Worker("data:text/javascript," +
"self.onmessage = function(e) {\n" +
" self.postMessage(e.data);\n" +
"};"
);
Of course you have to be careful about special characters. It's better to encode the script with base64:
var dataURL = 'data:text/javascript;base64,' + btoa(script);
var worker = unsafeWindow.Worker(dataURL);
Alternatively you can also use blob-urls:
var blob = new Blob([script], {type: 'text/javascript'});
var blobURL = URL.createObjectURL(blob);
var worker = new unsafeWindow.Worker(blobURL);
URL.revokeObjectURL(blobURL);
If you really want to use a script hosted on a different domain that's not a problem because same origin policy doesn't apply for GM_xmlhttpRequest:
function createWorkerFromExternalURL(url, callback) {
GM_xmlhttpRequest({
method: 'GET',
url: url,
onload: function(response) {
var script, dataURL, worker = null;
if (response.status === 200) {
script = response.responseText;
dataURL = 'data:text/javascript;base64,' + btoa(script);
worker = new unsafeWindow.Worker(dataURL);
}
callback(worker);
},
onerror: function() {
callback(null);
}
});
}
By now (10 years later), it's possible to use Web Workers with Firefox 77 and Tampermonkey. I've tested sucessfully using inline workers:
var blob = new Blob(["onmessage = function(e){postMessage('whats up?');console.log(e.data)}"], {type: 'text/javascript'})
var url = URL.createObjectURL(blob)
var worker = new Worker(url)
worker.onmessage = function(e){
console.log(e.data)
}
worker.postMessage('hey there!')
With Chrome or other extension like Greasemonkey ou Violentmonkey, i'ts not working because of CSP worker-src (see violation cases at https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/worker-src). This is why is not possible to use HTTP URLs or string as argument of Worker constructor, only works with blob URLs in this very specific case.
Still, there is a catch about the context of Workers. They can't access DOM, window, document or parent objects (see features available to workers at https://www.html5rocks.com/en/tutorials/workers/basics/).
See:
Can I load a web worker script from an absolute URL?

Categories