Jan Amoyo

on software development and possibly other things

Using LocalStorage to Publish Messages Across Browser Windows

No comments
Below is a simple JavaScript utility for publishing messages across browser windows of the same domain. This implementation uses the browser's localStorage and the storage event to simulate the behavior of an inter-window topic.
(function (global, window) {
  function Publisher() {
    var PUBLISH_PREFIX = 'publish_';
    this.publish = function (topic, message) {
      message.source    = window.name;
      message.timestamp = Date.now();
      window.localStorage.setItem(PUBLISH_PREFIX + topic,
        JSON.stringify(message));
    };
    this.subscribe = function (topic, callback, alias) {
      var subscriber = function (event) {
        if (event.key === PUBLISH_PREFIX + topic
            && event.newValue !== null) {
          callback(JSON.parse(event.newValue));
        }
      };
      window.addEventListener('storage', subscriber);
      return function () {
        window.removeEventListener('storage', subscriber);
      };
    };
  }
  global.jramoyo = { Publisher: new Publisher() };
})(this, window);
Lines 5 and 6 adds a source and timestamp property to the message to ensure uniqueness
Lines 7 and 8 converts the message to JSON and saves it to the localStorage
Lines 12 and 13 uses the event.key to filter which message should be processed by the callback
Line 14 converts the JSON value to a message objects and passes it as an argument to the callback
Lines 18-20 returns a function that when called, removes the subscriber from the topic

Below is a sample code from a publishing window:
jramoyo.Publisher.publish('greeting_topic', {
    name: 'Kyle Katarn'
});
And here is a sample code from a subscribing window:
jramoyo.Publisher.subscribe('greeting_topic',
    function (message) {
        alert('Greetings, ' + message.name);
    });

This works because every time an item is stored in the localStorage, all browser windows sharing the same localStorage will receive a storage event detailing what has changed (except for the window that wrote to the localStorage).

However, the problem with the above implementation is that it doesn't scale if the number of subscribers increases. Every time a storage event is fired, the JavaScript engine will have to iterate through each listener regardless whether the listener is interested in the event or not.

To address this problem, we can use a map to index the callbacks against the event.key. Below is the updated version of the above code:
(function (global, window) {
  function Publisher() {
    var PUBLISH_PREFIX = 'publish_';
    var listeners = []; 
    window.addEventListener('storage',
      function storageListener(event) {
        var array = listeners[event.key];
        if (array && array.length > 0) {
          var message = JSON.parse(event.newValue);
          array.forEach(function (listener) {
            listener(message);
          });
        }
      }, false);
    this.publish = function (topic, message) {
      message.source    = window.name;
      message.timestamp = Date.now();
      window.localStorage.setItem(PUBLISH_PREFIX + topic,
        JSON.stringify(message));
    }; 
    this.subscribe = function (topic, callback, alias) {
      var key = PUBLISH_PREFIX + topic,
        array = listeners[key];
      if (!array) {
        array = []; listeners[key] = array;
      }
      array.push(callback);
      return function () {
        array.splice(array.indexOf(callback), 1);
      };
    };
  }
  global.jramoyo = { Publisher: new Publisher() };
})(this, window);
Lines 5-14 registers a single storage event listener that uses a map to look-up callbacks identified by the event.key
Lines 22-27 saves the callback into the listeners map, identified by the derived key

No comments :

Post a Comment