Skip to content

Instantly share code, notes, and snippets.

@mkruisselbrink
Last active August 29, 2015 14:11
Show Gist options
  • Save mkruisselbrink/536632fcd99d45005064 to your computer and use it in GitHub Desktop.
Save mkruisselbrink/536632fcd99d45005064 to your computer and use it in GitHub Desktop.

Stashed MessagePorts

MessagePorts aren't too useful in service workers as is, since to be able to use them the javascript code needs to keep a reference to them. This is a proposal for a mechanism to "stash" a MessagePort in a service worker so the port can outlive the service workers javascript context.

partial interface ServiceWorkerGlobalScope {
    void stashPort(USVString key, MessagePort port);
    Promise<sequence<MessagePort>> getStashedPorts(USVString key);
};

interface StashedMessageEvent : MessageEvent {
    readonly attribute USVString key;
};

Each Service Worker registration has multiple lists of stashed message ports associated with them, each list with its own key. With the stashPort method a service worker can add a new MessagePort to the list of ports with a specific key. Once a port has been "stashed" this way, messages sent to it no longer result in message events on the port, but instead message events (or maybe some new event type) will be sent to the service workers global scope. When a message is from a stashed message port, the source attribute of the MessageEvent is set to the MessagePort, and additionally a key property is present to indicate the key the MessagePort was stashed with.

Why is this helpful?

This makes it possible to change the navigator.connect proposal to return a MessagePort on both sides of the connection. This is both simpler, and more powerful: now the service side connection can also be transferred, and on top of that if the client side of a navigator.connect channel is a service worker, with this stashed ports thing it is now possible for that port to survive the service worker being shut down.

Updated CrossOriginConnectEvent

[Exposed=ServiceWorker]
interface CrossOriginConnectEvent : Event {
    readonly attribute DOMString origin;
    readonly attribute DOMString targetUrl;
    Promise<MessagePort> acceptConnection (Promise<boolean> shouldAccept);
};

Sample code

Client side service worker:

// client-worker.js
navigator.connect('https://example.com/services/push')
  .then(function(port) {
      port.postMessage({register: 'apikey'});
      self.stashPort('pushService', port);
    });

self.addEventListener('message', function(e) {
  if (e.key === 'pushService') {
    // Do something with the message.
  }
});

Service side service worker:

// service-worker.js
self.addEventListener('crossoriginconnect', function(e) {
  // Optionally check e.origin
  e.acceptConnection(e.targetUrl === 'https://example.com/service/push')
    .then(function(port) {
        self.stashPort('pushClients', port);
      });
});

self.addEventListener('message', function(e) {
  if (e.key === 'pushClients') {
    // Do something with the data sent
    e.source.postMessage('registered');
  }
});

self.addEventListener('push', function(e) {
  self.getStashedPorts('pushClients')
    .then(function(ports) {
        for (var i = 0; i < ports.length; ++i) {
          ports[i].postMessage('pushmessage');
        }
      });
});
@jakearchibald
Copy link

The simple bit: The API should probably be on ServiceWorkerRegistration, so that way a ServiceWorker can stash a port for itself:

self.registration.ports.add(port);

And a window/worker or even other ServiceWorker can stash a port for a ServiceWorker:

window.navigator.serviceWorker.getRegistration(url).then(r => r.ports.add(port));

Adding a port effectively transfers it into ServiceWorker internals, so hopefully the transferables spec will help there.

Using ServiceWorkerRegistration makes sense as it's where we store push registrations (and soon, background sync), which means the ports survive ServiceWorker updates.

registration.ports is basically a PortCollection https://html.spec.whatwg.org/multipage/comms.html#broadcasting-to-many-ports, but the methods would need to return promises. AsyncPortCollection I guess.

Here's the hard bit:

Assuming the ServiceWorker's PortCollection contains a port2, we can remove it once port1 is GC'd. However, if both ports are registered with a ServiceWorkerRegistration, they only GC if the ServiceWorkerRegistration is dropped, which only happens on unregister.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment