3

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.

Chong Lip Phang
  • 8,755
  • 5
  • 65
  • 100
  • What exactly are you trying to do? why do you try to send the message port to talk to the worker to the worker? But basicly, to send a port you need to refference it in the transferList `b.postMessage({type:'start', port:a}, [a]);`. – Bellian Oct 03 '20 at 08:06
  • I am trying to let worker a talk to worker b directly. – Chong Lip Phang Oct 03 '20 at 08:09
  • But since this is a sharedWorker, these are the same. – Bellian Oct 03 '20 at 08:14
  • Are you saying that there can only be one SharedWorker? – Chong Lip Phang Oct 03 '20 at 08:14
  • No, i am saying: If two shared worker are loaded by the same script on the same origin, it will only spawn one SharedWorker. You can have multiple Shared worker, but then you need multiple scripts. In your example you are initializing them both with the same script / file / code. so there should be only one instance. – Bellian Oct 03 '20 at 08:17
  • Then what is the use of a SharedWorker? Can we share data between two shared workers loaded with different scripts? – Chong Lip Phang Oct 03 '20 at 08:21
  • No. For example: you have a page that allows you to browse it with alot of tabs or windows, but need to be able pass data between all these tabs. for this scenario you can use SharedWorkers. Or a BrodcastChannel.. – Bellian Oct 03 '20 at 08:24
  • Could you please create a demo that illustrates the concept with different tabs? – Chong Lip Phang Oct 03 '20 at 08:35
  • A demo is hard to doo since in a demo i can not refference a seperate file, and SharedWorker are not allowed to be initialized by scripts created by `createObjectURL`. – Bellian Oct 03 '20 at 08:40

1 Answers1

12

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);
}
Chong Lip Phang
  • 8,755
  • 5
  • 65
  • 100
Bellian
  • 2,009
  • 14
  • 20
  • ok. Now I got a demo working. Is your demo working? Would you mind if I replace your code with mine? I wish to award you the points! – Chong Lip Phang Oct 03 '20 at 08:47
  • I can add it yes. I also made a theoretical demo application ;) – Bellian Oct 03 '20 at 08:48
  • @ChongLipPhang but how to tell that some port was closed and we shouldn't send messages to it anymore? I couldn't find any relaiable solution so far.. – maxpovver Feb 27 '23 at 12:25
  • @maxpovver I am not sure if there are better solutions. but perhaps you can consider echoing a confirmation whenever a message is received. If no confirmation is received, then just stop sending messages to the port. – Chong Lip Phang Feb 28 '23 at 09:05
  • @Bellian It is not entirely clear to me if the the `SharedWorker` constructor script argument is executed each time when it is called or only the first time.. Do you know ? – cassepipe May 16 '23 at 16:40
  • The code will be executed once on the first creation (or better said first message send to the worker). If you initialize multiple SharedWorker with the same script (scriptURL) the worker will not be initialized again, but the `onconnect` event will trigger and pass the newly connected port via `ev.ports[0]`. – Bellian May 17 '23 at 13:36