How to share data with SharedWorker

7.5k views Asked by At

There has been a lot of discussions and tutorials on SharedWorker on the Internet and StackOverflow, but none has really achieved the most basic goal -- passing data directly between two shared workers.

To me, the strength of SharedWorker over a dedicated web Worker is that the former allows direct communication over different tabs, iframes and threads. I tried the following:

(shared.html:)

<!DOCTYPE html><html><head></head><body>
<button onclick="init('a')">Initiate A</button>
<button onclick="init('b')">Initiate B</button>
<script>

var a,b;
function init(v) {
    if (v=='a'){
        a = (new SharedWorker('./shared.js')).port;
        a.start();
        a.postMessage({type:'start', port:b})
    } else {
        b = (new SharedWorker('./shared.js')).port;
        b.start();
        b.postMessage({type:'start', port:a});    // <== error here
    }
}

</script></body></html>

(shared.js)

let peer = null;

onconnect = function (ev) {
    let port = ev.ports[0];
    port.onmessage = (e) => {
        if (e.data.type=='start' && e.data.port){
            peer=e.data.port;
        } else if (e.data.type=='msg' && peer){
            setInterval(()=>{
                peer.postMessage({type:'msg',msg:'greetings!'});
            },2000);
        }
    }
    port.start();
}

After clicking 'Initiate A' and then 'Initiate B', I got the following error message on the console:

shared.html:15 Uncaught DOMException: Failed to execute 'postMessage' on 'MessagePort': A MessagePort could not be cloned because it was not transferred.

In other words, I couldn't pass the ports around.

So is SharedWorker of any use at all. It seems that the regular dedicated Worker suffices under most circumstances.

1

There are 1 answers

6
Bellian On BEST ANSWER

So basicly you are using SharedWorkers wrong.

From a documentation: https://developer.mozilla.org/de/docs/Web/API/SharedWorker

The SharedWorker interface represents a specific kind of worker that can be accessed from several browsing contexts, such as several windows, iframes or even workers.

That means that you can communicate and compute stuff accross multiple windows / tabs / browsing contexts

|-----------|    |-----------|
|  Window 1 |    |  Window 2 |
|           |    |           |
|           |    |           |
|-----------|    |-----------|
      |                |
      __________________
              |
        |-----------|
        |   Worker  |
        |           |
        |-----------|

Sending starting the worker in a context will open a port on the SharedWorker

//============================================
//== Site Script
//============================================

var worker = new SharedWorker('something.js');
worker.port.start(); // this will trigger the on connect event on the webworker
// this will also start the worker IF this is the first call!

// recieve message from worker
worker.port.addEventListener('message', message => {
  console.log(message);
});

// send a mesasge to the worker
worker.port.postMessage(['I have a nice message for all']);



//============================================
//== Shared Worker
//============================================
const allPorts = [];

onconnect = function(e) {
  // the incoming port
  var port = e.ports[0];
  allPorts.push(port);

  port.addEventListener('message', function(e) {
    // get the message sent to the worker
    var message = e.data[0];
    // send the message to ALL connected worker ports!
    allPorts.forEach(port => {
      port.postMessage(message);
    })
  });

  port.start(); // Required when using addEventListener. Otherwise called implicitly by onmessage setter.
}

You can sen Additional Ports to the worker, but MessagePorts are Transferable objects. These needs to be added to the transfer list when sent.

const startA = document.getElementById('startA');
const startB = document.getElementById('startB');

const workerScript = 'console.log("started")';
const blob = new Blob([workerScript]);
const workerScriptURL = URL.createObjectURL(blob);

const worker = new Worker(workerScriptURL);
const messageChannel = new MessageChannel();

worker.postMessage({port: messageChannel.port2}, [messageChannel.port2]);
//                                                    ^ This is the transfer list!

If you only want to pass data to other contexts though, use a BrodcastChannel:

https://developer.mozilla.org/en-US/docs/Web/API/Broadcast_Channel_API

** EDIT **

Here is a working demo. Try opening shared.html on one tab and shared2.html on another tab. You will see on the second tab, the number won't start from 0.

(shared.html)

<!DOCTYPE html><html><head></head><body>
<button onclick="init()">Initiate</button>
<script>

function init() {
    w = (new SharedWorker('./shared.js')).port;
    w.start();
    w.postMessage(0);
    w.onmessage=e=>{
        console.log(e.data);
        w.postMessage(e.data[0]+1);
    };
}

</script></body></html>

(shared2.html)

<!DOCTYPE html><html><head></head><body>
<button onclick="init()">Initiate</button>
<script>

function init() {
    w = (new SharedWorker('./shared.js')).port;
    w.start();
    w.onmessage=e=>{
        console.log(e.data);
    };
}

</script></body></html>

(shared.js)

const ports = [];

onconnect = function (ev) {
    let port = ev.ports[0];
    port.onmessage = (e) => {
        setTimeout(()=>{
            ports.forEach(p=>p.postMessage([e.data, ev.ports.length]));
        },300);
    }
    port.start();
    ports.push(port);
}