Skip to content

Instantly share code, notes, and snippets.

@sechel
Created September 13, 2018 15:47
Show Gist options
  • Save sechel/e6aff22d9e56df02c5bd09c4afc516e6 to your computer and use it in GitHub Desktop.
Save sechel/e6aff22d9e56df02c5bd09c4afc516e6 to your computer and use it in GitHub Desktop.
BroadcastChannel Polyfill

BroadcastChannel Polyfill

BroadcastChannel is a new communication API proposed in the HTML Standard but not yet widely implemented. It allows messages to be sent to all other BroadcastChannel instances sharing the same channel name within the same browsing context and origin.

var bc = new BroadcastChannel('name');

bc.postMessage(payload);

bc.onmessage = function(e) {
  console.log('Received: ' + e.data);
};

Included in this gist is a polyfill for the API.

Compatibility

There is native BroadcastChannel support is in:

  • Firefox 38+

The polyfill requires Message Channel support, so should work in:

  • Chrome 4+
  • Safari 5+
  • Opera 11.5+

Does not work in:

  • Firefox 37- (neither BroadcastChannel nor MessageChannel)
  • IE (??)

Caveats

  • The real API should let you transmit anything which can copied by the structured cloning algorithm. This polyfill only copies things using JSON.stringify()/JSON.parse() so it is much more limited.
  • This polyfill uses DOM Storage (localStorage) and storage events. DOM Storage is a synchronous API and so may cause performance issues in pages. In addition, it is not exposed to Workers. Therefore, the polyfill will not function in Workers.
  • Unique storage keys are used for each message, and are cleaned up a few hundred milliseconds after transmission. This is a total hack and may result in the messages failing to be received (if the write and delete are coalesced) or persisting (if the cleanup is prevented by page close).

Example

Here's a sample inter-tab chat app. Note that a BroadcastChannel should broadcast to other BroadcastChannel instances within the same page but not to itself, so there is no local echo of the messages.

<!DOCTYPE html>
<script src="broadcastchannel.js"></script>
<textarea id="out" readonly rows=30 col=80></textarea><br>
<form><input id="in"><input type="submit" id="go" value="Send"></form>

<script>
var $ = document.querySelector.bind(document);

var bc = new BroadcastChannel('chat');
bc.addEventListener('message', function(e) {
  $('#out').value += e.data.message + '\r\n';
});

$('#go').addEventListener('click', function(e) {
  e.preventDefault();
  bc.postMessage({message: $('#in').value});
  $('#in').value = '';
});
</script>
(function(global) {
var channels = [];
function BroadcastChannel(channel) {
var $this = this;
channel = String(channel);
var id = '$BroadcastChannel$' + channel + '$';
channels[id] = channels[id] || [];
channels[id].push(this);
this._name = channel;
this._id = id;
this._closed = false;
this._mc = new MessageChannel();
this._mc.port1.start();
this._mc.port2.start();
global.addEventListener('storage', function(e) {
if (e.storageArea !== global.localStorage) return;
if (e.newValue === null) return;
if (e.key.substring(0, id.length) !== id) return;
var data = JSON.parse(e.newValue);
$this._mc.port2.postMessage(data);
});
}
BroadcastChannel.prototype = {
// BroadcastChannel API
get name() { return this._name; },
postMessage: function(message) {
var $this = this;
if (this._closed) {
var e = new Error();
e.name = 'InvalidStateError';
throw e;
}
var value = JSON.stringify(message);
// Broadcast to other contexts via storage events...
var key = this._id + String(Date.now()) + '$' + String(Math.random());
global.localStorage.setItem(key, value);
setTimeout(function() { global.localStorage.removeItem(key); }, 500);
// Broadcast to current context via ports
channels[this._id].forEach(function(bc) {
if (bc === $this) return;
bc._mc.port2.postMessage(JSON.parse(value));
});
},
close: function() {
if (this._closed) return;
this._closed = true;
this._mc.port1.close();
this._mc.port2.close();
var index = channels[this._id].indexOf(this);
channels[this._id].splice(index, 1);
},
// EventTarget API
get onmessage() { return this._mc.port1.onmessage; },
set onmessage(value) { this._mc.port1.onmessage = value; },
addEventListener: function(type, listener /*, useCapture*/) {
return this._mc.port1.addEventListener.apply(this._mc.port1, arguments);
},
removeEventListener: function(type, listener /*, useCapture*/) {
return this._mc.port1.removeEventListener.apply(this._mc.port1, arguments);
},
dispatchEvent: function(event) {
return this._mc.port1.dispatchEvent.apply(this._mc.port1, arguments);
}
};
global.BroadcastChannel = global.BroadcastChannel || BroadcastChannel;
}(self));
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment