1

I am using service workers to intercept requests for me and provide the responses to the fetch requests by communicating with a Web worker (also created from the same parent page). I have used message channels for direct communication between the worker and service worker. Here is a simple POC I have written:

var otherPort, parentPort;
var dummyObj;

var DummyHandler = function()
{
    this.onmessage = null;
    var selfRef = this;

    this.callHandler = function(arg)
    {
        if (typeof selfRef.onmessage === "function")
        {
            selfRef.onmessage(arg);
        }
        else
        {
            console.error("Message Handler not set");
        }
    };
};

function msgFromW(evt)
{
    console.log(evt.data);
    dummyObj.callHandler(evt);
}

self.addEventListener("message", function(evt) {
    var data = evt.data;
    if(data.msg === "connect")
    {
        otherPort = evt.ports[1];
        otherPort.onmessage = msgFromW;
        parentPort = evt.ports[0];
        parentPort.postMessage({"msg": "connect"});
    }
});

self.addEventListener("fetch", function(event)
{
    var url = event.request.url;
    var urlObj = new URL(url);
    if(!isToBeIntercepted(url))
    {
        return fetch(event.request);
    }
    url = decodeURI(url);

    var key = processURL(url).toLowerCase();
    console.log("Fetch For: " + key);

    event.respondWith(new Promise(function(resolve, reject){
        dummyObj = new DummyHandler();
        dummyObj.onmessage = function(e)
        {
            if(e.data.error)
            {
                reject(e.data.error);
            }
            else
            {
                var content = e.data.data;
                var blob = new Blob([content]);
                resolve(new Response(blob));
            }
        };

        otherPort.postMessage({"msg":  "content", param: key});
    }));
});

Roles of the ports:

otherPort: Communication with worker

parentPort: Communication with parent page

In the worker, I have a database say this:

var dataBase = {
    "file1.txt": "This is File1",
    "file2.txt": "This is File2"
};

The worker just serves the correct data according to the key sent by the service worker. In reality these will be very large files.

The problem I am facing with this is the following:

  1. Since I am using a global dummyObj, the older dummyObj and hence the older onmessage is lost and only the latest resource is responded with the received data.
  2. In fact, file2 gets This is File1, because the latest dummyObj is for file2.txt but the worker first sends data for file1.txt.

I tried by creating an iframe directly and all the requests inside it are intercepted:

<html>
<head></head>
<body><iframe src="tointercept/file1.txt" ></iframe><iframe src="tointercept/file2.txt"></iframe>
</body>
</html>

Here is what I get as output: enter image description here

One approach could be to write all the files that could be fetched into IndexedDB in the worker before creating the iframe. Then in the Service Worker fetch those from indexed DB. But I don't want to save all the resources in IDB. So this approach is not what I want.

Does anybody know a way to accomplish what I am trying to do in some other way? Or is there a fix to what I am doing.

Please Help!

UPDATE

I have got this to work by queuing the dummyObjs in a global queue instead of having a global object. And on receiving the response from the worker in msgFromW I pop an element from the queue and call its callHandler function. But I am not sure if this is a reliable solution. As it assumes that everything will occur in order. Is this assumption correct?

tapananand
  • 4,392
  • 2
  • 19
  • 35

1 Answers1

0

I'd recommend wrapping your message passing between the service worker and the web worker in promises, and then pass a promise that resolves with the data from the web worker to fetchEvent.respondWith().

The promise-worker library can automate this promise-wrapping for you, or you could do it by hand, using this example as a guide.

If you were using promise-worker, your code would look something like:

var promiseWorker = new PromiseWorker(/* your web worker */);

self.addEventListener('fetch', function(fetchEvent) {
  if (/* some optional check to see if you want to handle this event */) {
    fetchEvent.respondWith(promiseWorker.postMessage(/* file name */));
  }
});
Jeff Posnick
  • 53,580
  • 14
  • 141
  • 167
  • You're using promises, but there shouldn't be any need for a global queue that's maintained outside of the message callbacks. You can rely on promises that are scoped to the specific callback. – Jeff Posnick Jul 11 '16 at 15:26
  • I am using promises but I call the onmessage of dummyobj manually on receiving response from the worker. So I need the queue/map to keep track of whose onmessage to call – tapananand Jul 11 '16 at 15:32
  • My point is that if you respond with a promise that wraps the message request/response within its scope, you no longer need to keep track of that and concerns about race conditions go away. – Jeff Posnick Jul 11 '16 at 16:05
  • This exact same thing seems to be difficult to do. Hopefully you can help. – tapananand Jul 11 '16 at 16:07
  • Have you given the [`promise-worker`](https://github.com/nolanlawson/promise-worker) library a try? It looks like a straightforward way of doing it. – Jeff Posnick Jul 13 '16 at 15:01