A Comparison of JS Publish/Subscribe Approaches (v93)

Revision 93 of this benchmark created on


Description

Change: Added the EventEmitter2 test.

Just Added my recently released PubSub Implementation as well, it is part of the GitHub project called (BrianMc21/ControllerJS)[https://github.com/BrianMc21/ControllerJS-PubSub-StateControl]

A Comparison JS Publish/Subscribe Approaches

In this comparison I'm trying to focus on event inheritance feature present in PubSubJS and my implementation Peter Higgins´ Port from Dojo.

In most PubSub implementations Subscribers and Publishers have a one to one relationship. This gives them a huge performance benefit, but can be a drawback when building complex decoupled applications as you have to wire every single event. In this comparison I'm looking at a pretty standard GUI like the one on google.com. We have a HEADER region, a tool region to the LEFT, a CONTENT region and a FOOTER. Each one of these regions are dependent on each other using PubSub to communicate. A MANAGER in each region is responsible for Loading/Unloading modules. A typical PubSub message for this application would look something like this:

"/APP/REGION/MODULE/EVENT"

Using a PubSub implementation that allows for inheritance allow us to create a subscriber for "/APP/REGION" that would listen to ALL events that occur within this region. Using the simpler implementations we would have to publish two events, one for the module and one for the region.

In this example each PubSub implementation has 4 subscribers. One for APP, REGION, MODULE and EVENT each. PubSubJS and JQuery Subscriber may invoke all four subscriber callbacks by a single publication to app/region/module/event where as the rest each has to publish 4 events.

More info

Compared:

Preparation HTML

<script src="http://ajax.googleapis.com/ajax/libs/jquery/1/jquery.min.js" type="text/javascript">
</script>
;!function(exports, undefined) {

  var isArray = Array.isArray ? Array.isArray : function _isArray(obj) {
    return Object.prototype.toString.call(obj) === "[object Array]";
  };
  var defaultMaxListeners = 10;

  function init() {
    this._events = {};
    if (this._conf) {
      configure.call(this, this._conf);
    }
  }

  function configure(conf) {
    if (conf) {
      
      this._conf = conf;
      
      conf.delimiter && (this.delimiter = conf.delimiter);
      conf.maxListeners && (this._events.maxListeners = conf.maxListeners);
      conf.wildcard && (this.wildcard = conf.wildcard);
      conf.newListener && (this.newListener = conf.newListener);

      if (this.wildcard) {
        this.listenerTree = {};
      }
    }
  }

  function EventEmitter(conf) {
    this._events = {};
    this.newListener = false;
    configure.call(this, conf);
  }

  //
  // Attention, function return type now is array, always !
  // It has zero elements if no any matches found and one or more
  // elements (leafs) if there are matches
  //
  function searchListenerTree(handlers, type, tree, i) {
    if (!tree) {
      return [];
    }
    var listeners=[], leaf, len, branch, xTree, xxTree, isolatedBranch, endReached,
        typeLength = type.length, currentType = type[i], nextType = type[i+1];
    if (i === typeLength && tree._listeners) {
      //
      // If at the end of the event(s) list and the tree has listeners
      // invoke those listeners.
      //
      if (typeof tree._listeners === 'function') {
        handlers && handlers.push(tree._listeners);
        return [tree];
      } else {
        for (leaf = 0, len = tree._listeners.length; leaf < len; leaf++) {
          handlers && handlers.push(tree._listeners[leaf]);
        }
        return [tree];
      }
    }

    if ((currentType === '*' || currentType === '**') || tree[currentType]) {
      //
      // If the event emitted is '*' at this part
      // or there is a concrete match at this patch
      //
      if (currentType === '*') {
        for (branch in tree) {
          if (branch !== '_listeners' && tree.hasOwnProperty(branch)) {
            listeners = listeners.concat(searchListenerTree(handlers, type, tree[branch], i+1));
          }
        }
        return listeners;
      } else if(currentType === '**') {
        endReached = (i+1 === typeLength || (i+2 === typeLength && nextType === '*'));
        if(endReached && tree._listeners) {
          // The next element has a _listeners, add it to the handlers.
          listeners = listeners.concat(searchListenerTree(handlers, type, tree, typeLength));
        }

        for (branch in tree) {
          if (branch !== '_listeners' && tree.hasOwnProperty(branch)) {
            if(branch === '*' || branch === '**') {
              if(tree[branch]._listeners && !endReached) {
                listeners = listeners.concat(searchListenerTree(handlers, type, tree[branch], typeLength));
              }
              listeners = listeners.concat(searchListenerTree(handlers, type, tree[branch], i));
            } else if(branch === nextType) {
              listeners = listeners.concat(searchListenerTree(handlers, type, tree[branch], i+2));
            } else {
              // No match on this one, shift into the tree but not in the type array.
              listeners = listeners.concat(searchListenerTree(handlers, type, tree[branch], i));
            }
          }
        }
        return listeners;
      }

      listeners = listeners.concat(searchListenerTree(handlers, type, tree[currentType], i+1));
    }

    xTree = tree['*'];
    if (xTree) {
      //
      // If the listener tree will allow any match for this part,
      // then recursively explore all branches of the tree
      //
      searchListenerTree(handlers, type, xTree, i+1);
    }
    
    xxTree = tree['**'];
    if(xxTree) {
      if(i < typeLength) {
        if(xxTree._listeners) {
          // If we have a listener on a '**', it will catch all, so add its handler.
          searchListenerTree(handlers, type, xxTree, typeLength);
        }
        
        // Build arrays of matching next branches and others.
        for(branch in xxTree) {
          if(branch !== '_listeners' && xxTree.hasOwnProperty(branch)) {
            if(branch === nextType) {
              // We know the next element will match, so jump twice.
              searchListenerTree(handlers, type, xxTree[branch], i+2);
            } else if(branch === currentType) {
              // Current node matches, move into the tree.
              searchListenerTree(handlers, type, xxTree[branch], i+1);
            } else {
              isolatedBranch = {};
              isolatedBranch[branch] = xxTree[branch];
              searchListenerTree(handlers, type, { '**': isolatedBranch }, i+1);
            }
          }
        }
      } else if(xxTree._listeners) {
        // We have reached the end and still on a '**'
        searchListenerTree(handlers, type, xxTree, typeLength);
      } else if(xxTree['*'] && xxTree['*']._listeners) {
        searchListenerTree(handlers, type, xxTree['*'], typeLength);
      }
    }

    return listeners;
  }

  function growListenerTree(type, listener) {

    type = typeof type === 'string' ? type.split(this.delimiter) : type.slice();
    
    //
    // Looks for two consecutive '**', if so, don't add the event at all.
    //
    for(var i = 0, len = type.length; i+1 < len; i++) {
      if(type[i] === '**' && type[i+1] === '**') {
        return;
      }
    }

    var tree = this.listenerTree;
    var name = type.shift();

    while (name) {

      if (!tree[name]) {
        tree[name] = {};
      }

      tree = tree[name];

      if (type.length === 0) {

        if (!tree._listeners) {
          tree._listeners = listener;
        }
        else if(typeof tree._listeners === 'function') {
          tree._listeners = [tree._listeners, listener];
        }
        else if (isArray(tree._listeners)) {

          tree._listeners.push(listener);

          if (!tree._listeners.warned) {

            var m = defaultMaxListeners;
            
            if (typeof this._events.maxListeners !== 'undefined') {
              m = this._events.maxListeners;
            }

            if (m > 0 && tree._listeners.length > m) {

              tree._listeners.warned = true;
              console.error('(node) warning: possible EventEmitter memory ' +
                            'leak detected. %d listeners added. ' +
                            'Use emitter.setMaxListeners() to increase limit.',
                            tree._listeners.length);
              console.trace();
            }
          }
        }
        return true;
      }
      name = type.shift();
    }
    return true;
  };

  // By default EventEmitters will print a warning if more than
  // 10 listeners are added to it. This is a useful default which
  // helps finding memory leaks.
  //
  // Obviously not all Emitters should be limited to 10. This function allows
  // that to be increased. Set to zero for unlimited.

  EventEmitter.prototype.delimiter = '.';

  EventEmitter.prototype.setMaxListeners = function(n) {
    this._events || init.call(this);
    this._events.maxListeners = n;
    if (!this._conf) this._conf = {};
    this._conf.maxListeners = n;
  };

  EventEmitter.prototype.event = '';

  EventEmitter.prototype.once = function(event, fn) {
    this.many(event, 1, fn);
    return this;
  };

  EventEmitter.prototype.many = function(event, ttl, fn) {
    var self = this;

    if (typeof fn !== 'function') {
      throw new Error('many only accepts instances of Function');
    }

    function listener() {
      if (--ttl === 0) {
        self.off(event, listener);
      }
      fn.apply(this, arguments);
    };

    listener._origin = fn;

    this.on(event, listener);

    return self;
  };

  EventEmitter.prototype.emit = function() {
    
    this._events || init.call(this);

    var type = arguments[0];

    if (type === 'newListener' && !this.newListener) {
      if (!this._events.newListener) { return false; }
    }

    // Loop through the *_all* functions and invoke them.
    if (this._all) {
      var l = arguments.length;
      var args = new Array(l - 1);
      for (var i = 1; i < l; i++) args[i - 1] = arguments[i];
      for (i = 0, l = this._all.length; i < l; i++) {
        this.event = type;
        this._all[i].apply(this, args);
      }
    }

    // If there is no 'error' event listener then throw.
    if (type === 'error') {
      
      if (!this._all && 
        !this._events.error && 
        !(this.wildcard && this.listenerTree.error)) {

        if (arguments[1] instanceof Error) {
          throw arguments[1]; // Unhandled 'error' event
        } else {
          throw new Error("Uncaught, unspecified 'error' event.");
        }
        return false;
      }
    }

    var handler;

    if(this.wildcard) {
      handler = [];
      var ns = typeof type === 'string' ? type.split(this.delimiter) : type.slice();
      searchListenerTree.call(this, handler, ns, this.listenerTree, 0);
    }
    else {
      handler = this._events[type];
    }

    if (typeof handler === 'function') {
      this.event = type;
      if (arguments.length === 1) {
        handler.call(this);
      }
      else if (arguments.length > 1)
        switch (arguments.length) {
          case 2:
            handler.call(this, arguments[1]);
            break;
          case 3:
            handler.call(this, arguments[1], arguments[2]);
            break;
          // slower
          default:
            var l = arguments.length;
            var args = new Array(l - 1);
            for (var i = 1; i < l; i++) args[i - 1] = arguments[i];
            handler.apply(this, args);
        }
      return true;
    }
    else if (handler) {
      var l = arguments.length;
      var args = new Array(l - 1);
      for (var i = 1; i < l; i++) args[i - 1] = arguments[i];

      var listeners = handler.slice();
      for (var i = 0, l = listeners.length; i < l; i++) {
        this.event = type;
        listeners[i].apply(this, args);
      }
      return (listeners.length > 0) || this._all;
    }
    else {
      return this._all;
    }

  };

  EventEmitter.prototype.on = function(type, listener) {
    
    if (typeof type === 'function') {
      this.onAny(type);
      return this;
    }

    if (typeof listener !== 'function') {
      throw new Error('on only accepts instances of Function');
    }
    this._events || init.call(this);

    // To avoid recursion in the case that type == "newListeners"! Before
    // adding it to the listeners, first emit "newListeners".
    this.emit('newListener', type, listener);

    if(this.wildcard) {
      growListenerTree.call(this, type, listener);
      return this;
    }

    if (!this._events[type]) {
      // Optimize the case of one listener. Don't need the extra array object.
      this._events[type] = listener;
    }
    else if(typeof this._events[type] === 'function') {
      // Adding the second element, need to change to array.
      this._events[type] = [this._events[type], listener];
    }
    else if (isArray(this._events[type])) {
      // If we've already got an array, just append.
      this._events[type].push(listener);

      // Check for listener leak
      if (!this._events[type].warned) {

        var m = defaultMaxListeners;
        
        if (typeof this._events.maxListeners !== 'undefined') {
          m = this._events.maxListeners;
        }

        if (m > 0 && this._events[type].length > m) {

          this._events[type].warned = true;
          console.error('(node) warning: possible EventEmitter memory ' +
                        'leak detected. %d listeners added. ' +
                        'Use emitter.setMaxListeners() to increase limit.',
                        this._events[type].length);
          console.trace();
        }
      }
    }
    return this;
  };

  EventEmitter.prototype.onAny = function(fn) {

    if(!this._all) {
      this._all = [];
    }

    if (typeof fn !== 'function') {
      throw new Error('onAny only accepts instances of Function');
    }

    // Add the function to the event listener collection.
    this._all.push(fn);
    return this;
  };

  EventEmitter.prototype.addListener = EventEmitter.prototype.on;

  EventEmitter.prototype.off = function(type, listener) {
    if (typeof listener !== 'function') {
      throw new Error('removeListener only takes instances of Function');
    }

    var handlers,leafs=[];

    if(this.wildcard) {
      var ns = typeof type === 'string' ? type.split(this.delimiter) : type.slice();
      leafs = searchListenerTree.call(this, null, ns, this.listenerTree, 0);
    }
    else {
      // does not use listeners(), so no side effect of creating _events[type]
      if (!this._events[type]) return this;
      handlers = this._events[type];
      leafs.push({_listeners:handlers});
    }

    for (var iLeaf=0; iLeaf<leafs.length; iLeaf++) {
      var leaf = leafs[iLeaf];
      handlers = leaf._listeners;
      if (isArray(handlers)) {

        var position = -1;

        for (var i = 0, length = handlers.length; i < length; i++) {
          if (handlers[i] === listener ||
            (handlers[i].listener && handlers[i].listener === listener) ||
            (handlers[i]._origin && handlers[i]._origin === listener)) {
            position = i;
            break;
          }
        }

        if (position < 0) {
          return this;
        }

        if(this.wildcard) {
          leaf._listeners.splice(position, 1)
        }
        else {
          this._events[type].splice(position, 1);
        }

        if (handlers.length === 0) {
          if(this.wildcard) {
            delete leaf._listeners;
          }
          else {
            delete this._events[type];
          }
        }
      }
      else if (handlers === listener ||
        (handlers.listener && handlers.listener === listener) ||
        (handlers._origin && handlers._origin === listener)) {
        if(this.wildcard) {
          delete leaf._listeners;
        }
        else {
          delete this._events[type];
        }
      }
    }

    return this;
  };

  EventEmitter.prototype.offAny = function(fn) {
    var i = 0, l = 0, fns;
    if (fn && this._all && this._all.length > 0) {
      fns = this._all;
      for(i = 0, l = fns.length; i < l; i++) {
        if(fn === fns[i]) {
          fns.splice(i, 1);
          return this;
        }
      }
    } else {
      this._all = [];
    }
    return this;
  };

  EventEmitter.prototype.removeListener = EventEmitter.prototype.off;

  EventEmitter.prototype.removeAllListeners = function(type) {
    if (arguments.length === 0) {
      !this._events || init.call(this);
      return this;
    }

    if(this.wildcard) {
      var ns = typeof type === 'string' ? type.split(this.delimiter) : type.slice();
      var leafs = searchListenerTree.call(this, null, ns, this.listenerTree, 0);

      for (var iLeaf=0; iLeaf<leafs.length; iLeaf++) {
        var leaf = leafs[iLeaf];
        leaf._listeners = null;
      }
    }
    else {
      if (!this._events[type]) return this;
      this._events[type] = null;
    }
    return this;
  };

  EventEmitter.prototype.listeners = function(type) {
    if(this.wildcard) {
      var handlers = [];
      var ns = typeof type === 'string' ? type.split(this.delimiter) : type.slice();
      searchListenerTree.call(this, handlers, ns, this.listenerTree, 0);
      return handlers;
    }

    this._events || init.call(this);

    if (!this._events[type]) this._events[type] = [];
    if (!isArray(this._events[type])) {
      this._events[type] = [this._events[type]];
    }
    return this._events[type];
  };

  EventEmitter.prototype.listenersAny = function() {

    if(this._all) {
      return this._all;
    }
    else {
      return [];
    }

  };

  if (typeof define === 'function' && define.amd) {
    define(function() {
      return EventEmitter;
    });
  } else {
    exports.EventEmitter2 = EventEmitter; 
  }

}(typeof process !== 'undefined' && typeof process.title !== 'undefined' && typeof exports !== 'undefined' ? exports : window);

<script type="text/javascript">
var subtopic=(function(){var b={},c=function(f,e,h){var k=f.split("/");while(k[0]){if(b[f]){var j=b[f],g=j.length-1;for(g;g>=0;g-=1){j[g].apply(h||this,e||[])}}k.pop();f=k.join("/")
}},a=function(e,f){if(!b[e]){b[e]=[]}b[e].push(f);return[e,f]},d=function(g,h){var f=g[0],e=b[f].length-1;if(b[f]){for(e;e>=0;e-=1){if(b[f][e]===g[1]){b[f].splice(b[f][e],1);if(h){delete b[f]
}}}}};return{publish:c,subscribe:a,unsubscribe:d}}());
</script>
<script type="text/javascript">
/**
         *      Events. Pub/Sub system for Loosely Coupled logic.
         *      Based on Peter Higgins' port from Dojo to jQuery
         *      https://github.com/phiggins42/bloody-jquery-plugins/blob/master/pubsub.js
         *
         *      Re-adapted to vanilla Javascript
         *
         *      @class Events
         */
        var Events = (function (){
                var cache = {},
                        /**
                         *      Events.publish
                         *      e.g.: Events.publish("/Article/added", [article], this);
                         *
                         *      @class Events
                         *      @method publish
                         *      @param topic {String}
                         *      @param args     {Array}
                         *      @param scope {Object} Optional
                         */
                        publish = function (topic, args, scope) {
                                if (cache[topic]) {
                                        var thisTopic = cache[topic],
                                                i = thisTopic.length - 1;

                                        for (i; i >= 0; i -= 1) {
                                                thisTopic[i].apply( scope || this, args || []);
                                        }
                                }
                        },
                        /**
                         *      Events.subscribe
                         *      e.g.: Events.subscribe("/Article/added", Articles.validate)
                         *
                         *      @class Events
                         *      @method subscribe
                         *      @param topic {String}
                         *      @param callback {Function}
                         *      @return Event handler {Array}
                         */
                        subscribe = function (topic, callback) {
                                if (!cache[topic]) {
                                        cache[topic] = [];
                                }
                                cache[topic].push(callback);
                                return [topic, callback];
                        },
                        /**
                         *      Events.unsubscribe
                         *      e.g.: var handle = Events.subscribe("/Article/added", Articles.validate);
                         *              Events.unsubscribe(handle);
                         *
                         *      @class Events
                         *      @method unsubscribe
                         *      @param handle {Array}
                         *      @param completly {Boolean}
                         *      @return {type description }
                         */
                        unsubscribe = function (handle, completly) {
                                var t = handle[0],
                                        i = cache[t].length - 1;

                                if (cache[t]) {
                                        for (i; i >= 0; i -= 1) {
                                                if (cache[t][i] === handle[1]) {
                                                        cache[t].splice(cache[t][i], 1);
                                                        if(completly){ delete cache[t]; }
                                                }
                                        }
                                }
                        };

                return {
                        publish: publish,
                        subscribe: subscribe,
                        unsubscribe: unsubscribe
                };
}());
</script>
<script type="text/javascript">
/*!
 * Amplify Core @VERSION
 *
 * Copyright 2011 appendTo LLC. (http://appendto.com/team)
 * Dual licensed under the MIT or GPL licenses.
 * http://appendto.com/open-source-licenses
 *
 * http://amplifyjs.com
 */
(function( global, undefined ) {

var slice = [].slice,
        subscriptions = {};

var amplify = global.amplify = {
        publish: function( topic ) {
                if ( typeof topic !== "string" ) {
                        throw new Error( "You must provide a valid topic to publish." );
                }

                var args = slice.call( arguments, 1 ),
                        topicSubscriptions,
                        subscription,
                        length,
                        i = 0,
                        ret;

                if ( !subscriptions[ topic ] ) {
                        return true;
                }

                topicSubscriptions = subscriptions[ topic ].slice();
                for ( length = topicSubscriptions.length; i < length; i++ ) {
                        subscription = topicSubscriptions[ i ];
                        ret = subscription.callback.apply( subscription.context, args );
                        if ( ret === false ) {
                                break;
                        }
                }
                return ret !== false;
        },

        subscribe: function( topic, context, callback, priority ) {
                if ( typeof topic !== "string" ) {
                        throw new Error( "You must provide a valid topic to create a subscription." );
                }

                if ( arguments.length === 3 && typeof callback === "number" ) {
                        priority = callback;
                        callback = context;
                        context = null;
                }
                if ( arguments.length === 2 ) {
                        callback = context;
                        context = null;
                }
                priority = priority || 10;

                var topicIndex = 0,
                        topics = topic.split( /\s/ ),
                        topicLength = topics.length,
                        added;
                for ( ; topicIndex < topicLength; topicIndex++ ) {
                        topic = topics[ topicIndex ];
                        added = false;
                        if ( !subscriptions[ topic ] ) {
                                subscriptions[ topic ] = [];
                        }

                        var i = subscriptions[ topic ].length - 1,
                                subscriptionInfo = {
                                        callback: callback,
                                        context: context,
                                        priority: priority
                                };

                        for ( ; i >= 0; i-- ) {
                                if ( subscriptions[ topic ][ i ].priority <= priority ) {
                                        subscriptions[ topic ].splice( i + 1, 0, subscriptionInfo );
                                        added = true;
                                        break;
                                }
                        }

                        if ( !added ) {
                                subscriptions[ topic ].unshift( subscriptionInfo );
                        }
                }

                return callback;
        },

        unsubscribe: function( topic, callback ) {
                if ( typeof topic !== "string" ) {
                        throw new Error( "You must provide a valid topic to remove a subscription." );
                }

                if ( !subscriptions[ topic ] ) {
                        return;
                }

                var length = subscriptions[ topic ].length,
                        i = 0;

                for ( ; i < length; i++ ) {
                        if ( subscriptions[ topic ][ i ].callback === callback ) {
                                subscriptions[ topic ].splice( i, 1 );
                                break;
                        }
                }
        }
};

}( this ) );
</script>
<script type="text/javascript">
// Generated by CoffeeScript 1.3.3
(function() {
  var $, Controller, Events, Log, Model, Module, Spine, createObject, isArray, isBlank, makeArray, moduleKeywords,
    __slice = [].slice,
    __indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; },
    __hasProp = {}.hasOwnProperty,
    __extends = function(child, parent) { for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; },
    __bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };

  Events = {
    bind: function(ev, callback) {
      var calls, evs, name, _i, _len;
      evs = ev.split(' ');
      calls = this.hasOwnProperty('_callbacks') && this._callbacks || (this._callbacks = {});
      for (_i = 0, _len = evs.length; _i < _len; _i++) {
        name = evs[_i];
        calls[name] || (calls[name] = []);
        calls[name].push(callback);
      }
      return this;
    },
    one: function(ev, callback) {
      return this.bind(ev, function() {
        this.unbind(ev, arguments.callee);
        return callback.apply(this, arguments);
      });
    },
    trigger: function() {
      var args, callback, ev, list, _i, _len, _ref;
      args = 1 <= arguments.length ? __slice.call(arguments, 0) : [];
      ev = args.shift();
      list = this.hasOwnProperty('_callbacks') && ((_ref = this._callbacks) != null ? _ref[ev] : void 0);
      if (!list) {
        return;
      }
      for (_i = 0, _len = list.length; _i < _len; _i++) {
        callback = list[_i];
        if (callback.apply(this, args) === false) {
          break;
        }
      }
      return true;
    },
    unbind: function(ev, callback) {
      var cb, i, list, _i, _len, _ref;
      if (!ev) {
        this._callbacks = {};
        return this;
      }
      list = (_ref = this._callbacks) != null ? _ref[ev] : void 0;
      if (!list) {
        return this;
      }
      if (!callback) {
        delete this._callbacks[ev];
        return this;
      }
      for (i = _i = 0, _len = list.length; _i < _len; i = ++_i) {
        cb = list[i];
        if (!(cb === callback)) {
          continue;
        }
        list = list.slice();
        list.splice(i, 1);
        this._callbacks[ev] = list;
        break;
      }
      return this;
    }
  };

  Log = {
    trace: true,
    logPrefix: '(App)',
    log: function() {
      var args;
      args = 1 <= arguments.length ? __slice.call(arguments, 0) : [];
      if (!this.trace) {
        return;
      }
      if (this.logPrefix) {
        args.unshift(this.logPrefix);
      }
      if (typeof console !== "undefined" && console !== null) {
        if (typeof console.log === "function") {
          console.log.apply(console, args);
        }
      }
      return this;
    }
  };

  moduleKeywords = ['included', 'extended'];

  Module = (function() {

    Module.include = function(obj) {
      var key, value, _ref;
      if (!obj) {
        throw new Error('include(obj) requires obj');
      }
      for (key in obj) {
        value = obj[key];
        if (__indexOf.call(moduleKeywords, key) < 0) {
          this.prototype[key] = value;
        }
      }
      if ((_ref = obj.included) != null) {
        _ref.apply(this);
      }
      return this;
    };

    Module.extend = function(obj) {
      var key, value, _ref;
      if (!obj) {
        throw new Error('extend(obj) requires obj');
      }
      for (key in obj) {
        value = obj[key];
        if (__indexOf.call(moduleKeywords, key) < 0) {
          this[key] = value;
        }
      }
      if ((_ref = obj.extended) != null) {
        _ref.apply(this);
      }
      return this;
    };

    Module.proxy = function(func) {
      var _this = this;
      return function() {
        return func.apply(_this, arguments);
      };
    };

    Module.prototype.proxy = function(func) {
      var _this = this;
      return function() {
        return func.apply(_this, arguments);
      };
    };

    function Module() {
      if (typeof this.init === "function") {
        this.init.apply(this, arguments);
      }
    }

    return Module;

  })();

  Model = (function(_super) {

    __extends(Model, _super);

    Model.extend(Events);

    Model.records = {};

    Model.crecords = {};

    Model.attributes = [];

    Model.configure = function() {
      var attributes, name;
      name = arguments[0], attributes = 2 <= arguments.length ? __slice.call(arguments, 1) : [];
      this.className = name;
      this.records = {};
      this.crecords = {};
      if (attributes.length) {
        this.attributes = attributes;
      }
      this.attributes && (this.attributes = makeArray(this.attributes));
      this.attributes || (this.attributes = []);
      this.unbind();
      return this;
    };

    Model.toString = function() {
      return "" + this.className + "(" + (this.attributes.join(", ")) + ")";
    };

    Model.find = function(id) {
      var record;
      record = this.records[id];
      if (!record && ("" + id).match(/c-\d+/)) {
        return this.findCID(id);
      }
      if (!record) {
        throw new Error('Unknown record');
      }
      return record.clone();
    };

    Model.findCID = function(cid) {
      var record;
      record = this.crecords[cid];
      if (!record) {
        throw new Error('Unknown record');
      }
      return record.clone();
    };

    Model.exists = function(id) {
      try {
        return this.find(id);
      } catch (e) {
        return false;
      }
    };

    Model.refresh = function(values, options) {
      var record, records, _i, _len;
      if (options == null) {
        options = {};
      }
      if (options.clear) {
        this.records = {};
        this.crecords = {};
      }
      records = this.fromJSON(values);
      if (!isArray(records)) {
        records = [records];
      }
      for (_i = 0, _len = records.length; _i < _len; _i++) {
        record = records[_i];
        record.id || (record.id = record.cid);
        this.records[record.id] = record;
        this.crecords[record.cid] = record;
      }
      this.trigger('refresh', this.cloneArray(records));
      return this;
    };

    Model.select = function(callback) {
      var id, record, result;
      result = (function() {
        var _ref, _results;
        _ref = this.records;
        _results = [];
        for (id in _ref) {
          record = _ref[id];
          if (callback(record)) {
            _results.push(record);
          }
        }
        return _results;
      }).call(this);
      return this.cloneArray(result);
    };

    Model.findByAttribute = function(name, value) {
      var id, record, _ref;
      _ref = this.records;
      for (id in _ref) {
        record = _ref[id];
        if (record[name] === value) {
          return record.clone();
        }
      }
      return null;
    };

    Model.findAllByAttribute = function(name, value) {
      return this.select(function(item) {
        return item[name] === value;
      });
    };

    Model.each = function(callback) {
      var key, value, _ref, _results;
      _ref = this.records;
      _results = [];
      for (key in _ref) {
        value = _ref[key];
        _results.push(callback(value.clone()));
      }
      return _results;
    };

    Model.all = function() {
      return this.cloneArray(this.recordsValues());
    };

    Model.first = function() {
      var record;
      record = this.recordsValues()[0];
      return record != null ? record.clone() : void 0;
    };

    Model.last = function() {
      var record, values;
      values = this.recordsValues();
      record = values[values.length - 1];
      return record != null ? record.clone() : void 0;
    };

    Model.count = function() {
      return this.recordsValues().length;
    };

    Model.deleteAll = function() {
      var key, value, _ref, _results;
      _ref = this.records;
      _results = [];
      for (key in _ref) {
        value = _ref[key];
        _results.push(delete this.records[key]);
      }
      return _results;
    };

    Model.destroyAll = function() {
      var key, value, _ref, _results;
      _ref = this.records;
      _results = [];
      for (key in _ref) {
        value = _ref[key];
        _results.push(this.records[key].destroy());
      }
      return _results;
    };

    Model.update = function(id, atts, options) {
      return this.find(id).updateAttributes(atts, options);
    };

    Model.create = function(atts, options) {
      var record;
      record = new this(atts);
      return record.save(options);
    };

    Model.destroy = function(id, options) {
      return this.find(id).destroy(options);
    };

    Model.change = function(callbackOrParams) {
      if (typeof callbackOrParams === 'function') {
        return this.bind('change', callbackOrParams);
      } else {
        return this.trigger('change', callbackOrParams);
      }
    };

    Model.fetch = function(callbackOrParams) {
      if (typeof callbackOrParams === 'function') {
        return this.bind('fetch', callbackOrParams);
      } else {
        return this.trigger('fetch', callbackOrParams);
      }
    };

    Model.toJSON = function() {
      return this.recordsValues();
    };

    Model.fromJSON = function(objects) {
      var value, _i, _len, _results;
      if (!objects) {
        return;
      }
      if (typeof objects === 'string') {
        objects = JSON.parse(objects);
      }
      if (isArray(objects)) {
        _results = [];
        for (_i = 0, _len = objects.length; _i < _len; _i++) {
          value = objects[_i];
          _results.push(new this(value));
        }
        return _results;
      } else {
        return new this(objects);
      }
    };

    Model.fromForm = function() {
      var _ref;
      return (_ref = new this).fromForm.apply(_ref, arguments);
    };

    Model.recordsValues = function() {
      var key, result, value, _ref;
      result = [];
      _ref = this.records;
      for (key in _ref) {
        value = _ref[key];
        result.push(value);
      }
      return result;
    };

    Model.cloneArray = function(array) {
      var value, _i, _len, _results;
      _results = [];
      for (_i = 0, _len = array.length; _i < _len; _i++) {
        value = array[_i];
        _results.push(value.clone());
      }
      return _results;
    };

    Model.idCounter = 0;

    Model.uid = function(prefix) {
      var uid;
      if (prefix == null) {
        prefix = '';
      }
      uid = prefix + this.idCounter++;
      if (this.exists(uid)) {
        uid = this.uid(prefix);
      }
      return uid;
    };

    function Model(atts) {
      Model.__super__.constructor.apply(this, arguments);
      if (atts) {
        this.load(atts);
      }
      this.cid = this.constructor.uid('c-');
    }

    Model.prototype.isNew = function() {
      return !this.exists();
    };

    Model.prototype.isValid = function() {
      return !this.validate();
    };

    Model.prototype.validate = function() {};

    Model.prototype.load = function(atts) {
      var key, value;
      for (key in atts) {
        value = atts[key];
        if (typeof this[key] === 'function') {
          this[key](value);
        } else {
          this[key] = value;
        }
      }
      return this;
    };

    Model.prototype.attributes = function() {
      var key, result, _i, _len, _ref;
      result = {};
      _ref = this.constructor.attributes;
      for (_i = 0, _len = _ref.length; _i < _len; _i++) {
        key = _ref[_i];
        if (key in this) {
          if (typeof this[key] === 'function') {
            result[key] = this[key]();
          } else {
            result[key] = this[key];
          }
        }
      }
      if (this.id) {
        result.id = this.id;
      }
      return result;
    };

    Model.prototype.eql = function(rec) {
      return !!(rec && rec.constructor === this.constructor && (rec.cid === this.cid) || (rec.id && rec.id === this.id));
    };

    Model.prototype.save = function(options) {
      var error, record;
      if (options == null) {
        options = {};
      }
      if (options.validate !== false) {
        error = this.validate();
        if (error) {
          this.trigger('error', error);
          return false;
        }
      }
      this.trigger('beforeSave', options);
      record = this.isNew() ? this.create(options) : this.update(options);
      this.trigger('save', options);
      return record;
    };

    Model.prototype.updateAttribute = function(name, value, options) {
      this[name] = value;
      return this.save(options);
    };

    Model.prototype.updateAttributes = function(atts, options) {
      this.load(atts);
      return this.save(options);
    };

    Model.prototype.changeID = function(id) {
      var records;
      records = this.constructor.records;
      records[id] = records[this.id];
      delete records[this.id];
      this.id = id;
      return this.save();
    };

    Model.prototype.destroy = function(options) {
      if (options == null) {
        options = {};
      }
      this.trigger('beforeDestroy', options);
      delete this.constructor.records[this.id];
      delete this.constructor.crecords[this.cid];
      this.destroyed = true;
      this.trigger('destroy', options);
      this.trigger('change', 'destroy', options);
      this.unbind();
      return this;
    };

    Model.prototype.dup = function(newRecord) {
      var result;
      result = new this.constructor(this.attributes());
      if (newRecord === false) {
        result.cid = this.cid;
      } else {
        delete result.id;
      }
      return result;
    };

    Model.prototype.clone = function() {
      return createObject(this);
    };

    Model.prototype.reload = function() {
      var original;
      if (this.isNew()) {
        return this;
      }
      original = this.constructor.find(this.id);
      this.load(original.attributes());
      return original;
    };

    Model.prototype.toJSON = function() {
      return this.attributes();
    };

    Model.prototype.toString = function() {
      return "<" + this.constructor.className + " (" + (JSON.stringify(this)) + ")>";
    };

    Model.prototype.fromForm = function(form) {
      var key, result, _i, _len, _ref;
      result = {};
      _ref = $(form).serializeArray();
      for (_i = 0, _len = _ref.length; _i < _len; _i++) {
        key = _ref[_i];
        result[key.name] = key.value;
      }
      return this.load(result);
    };

    Model.prototype.exists = function() {
      return this.id && this.id in this.constructor.records;
    };

    Model.prototype.update = function(options) {
      var clone, records;
      this.trigger('beforeUpdate', options);
      records = this.constructor.records;
      records[this.id].load(this.attributes());
      clone = records[this.id].clone();
      clone.trigger('update', options);
      clone.trigger('change', 'update', options);
      return clone;
    };

    Model.prototype.create = function(options) {
      var clone, record;
      this.trigger('beforeCreate', options);
      if (!this.id) {
        this.id = this.cid;
      }
      record = this.dup(false);
      this.constructor.records[this.id] = record;
      this.constructor.crecords[this.cid] = record;
      clone = record.clone();
      clone.trigger('create', options);
      clone.trigger('change', 'create', options);
      return clone;
    };

    Model.prototype.bind = function(events, callback) {
      var binder, unbinder,
        _this = this;
      this.constructor.bind(events, binder = function(record) {
        if (record && _this.eql(record)) {
          return callback.apply(_this, arguments);
        }
      });
      this.constructor.bind('unbind', unbinder = function(record) {
        if (record && _this.eql(record)) {
          _this.constructor.unbind(events, binder);
          return _this.constructor.unbind('unbind', unbinder);
        }
      });
      return binder;
    };

    Model.prototype.one = function(events, callback) {
      var binder,
        _this = this;
      return binder = this.bind(events, function() {
        _this.constructor.unbind(events, binder);
        return callback.apply(_this, arguments);
      });
    };

    Model.prototype.trigger = function() {
      var args, _ref;
      args = 1 <= arguments.length ? __slice.call(arguments, 0) : [];
      args.splice(1, 0, this);
      return (_ref = this.constructor).trigger.apply(_ref, args);
    };

    Model.prototype.unbind = function() {
      return this.trigger('unbind');
    };

    return Model;

  })(Module);

  Controller = (function(_super) {

    __extends(Controller, _super);

    Controller.include(Events);

    Controller.include(Log);

    Controller.prototype.eventSplitter = /^(\S+)\s*(.*)$/;

    Controller.prototype.tag = 'div';

    function Controller(options) {
      this.release = __bind(this.release, this);

      var key, value, _ref;
      this.options = options;
      _ref = this.options;
      for (key in _ref) {
        value = _ref[key];
        this[key] = value;
      }
      if (!this.el) {
        this.el = document.createElement(this.tag);
      }
      this.el = $(this.el);
      this.$el = this.el;
      if (this.className) {
        this.el.addClass(this.className);
      }
      if (this.attributes) {
        this.el.attr(this.attributes);
      }
      if (!this.events) {
        this.events = this.constructor.events;
      }
      if (!this.elements) {
        this.elements = this.constructor.elements;
      }
      if (this.events) {
        this.delegateEvents(this.events);
      }
      if (this.elements) {
        this.refreshElements();
      }
      Controller.__super__.constructor.apply(this, arguments);
    }

    Controller.prototype.release = function() {
      this.trigger('release');
      this.el.remove();
      return this.unbind();
    };

    Controller.prototype.$ = function(selector) {
      return $(selector, this.el);
    };

    Controller.prototype.delegateEvents = function(events) {
      var eventName, key, match, method, selector, _results,
        _this = this;
      _results = [];
      for (key in events) {
        method = events[key];
        if (typeof method === 'function') {
          method = (function(method) {
            return function() {
              method.apply(_this, arguments);
              return true;
            };
          })(method);
        } else {
          if (!this[method]) {
            throw new Error("" + method + " doesn't exist");
          }
          method = (function(method) {
            return function() {
              _this[method].apply(_this, arguments);
              return true;
            };
          })(method);
        }
        match = key.match(this.eventSplitter);
        eventName = match[1];
        selector = match[2];
        if (selector === '') {
          _results.push(this.el.bind(eventName, method));
        } else {
          _results.push(this.el.delegate(selector, eventName, method));
        }
      }
      return _results;
    };

    Controller.prototype.refreshElements = function() {
      var key, value, _ref, _results;
      _ref = this.elements;
      _results = [];
      for (key in _ref) {
        value = _ref[key];
        _results.push(this[value] = this.$(key));
      }
      return _results;
    };

    Controller.prototype.delay = function(func, timeout) {
      return setTimeout(this.proxy(func), timeout || 0);
    };

    Controller.prototype.html = function(element) {
      this.el.html(element.el || element);
      this.refreshElements();
      return this.el;
    };

    Controller.prototype.append = function() {
      var e, elements, _ref;
      elements = 1 <= arguments.length ? __slice.call(arguments, 0) : [];
      elements = (function() {
        var _i, _len, _results;
        _results = [];
        for (_i = 0, _len = elements.length; _i < _len; _i++) {
          e = elements[_i];
          _results.push(e.el || e);
        }
        return _results;
      })();
      (_ref = this.el).append.apply(_ref, elements);
      this.refreshElements();
      return this.el;
    };

    Controller.prototype.appendTo = function(element) {
      this.el.appendTo(element.el || element);
      this.refreshElements();
      return this.el;
    };

    Controller.prototype.prepend = function() {
      var e, elements, _ref;
      elements = 1 <= arguments.length ? __slice.call(arguments, 0) : [];
      elements = (function() {
        var _i, _len, _results;
        _results = [];
        for (_i = 0, _len = elements.length; _i < _len; _i++) {
          e = elements[_i];
          _results.push(e.el || e);
        }
        return _results;
      })();
      (_ref = this.el).prepend.apply(_ref, elements);
      this.refreshElements();
      return this.el;
    };

    Controller.prototype.replace = function(element) {
      var previous, _ref;
      _ref = [this.el, $(element.el || element)], previous = _ref[0], this.el = _ref[1];
      previous.replaceWith(this.el);
      this.delegateEvents(this.events);
      this.refreshElements();
      return this.el;
    };

    return Controller;

  })(Module);

  $ = (typeof window !== "undefined" && window !== null ? window.jQuery : void 0) || (typeof window !== "undefined" && window !== null ? window.Zepto : void 0) || function(element) {
    return element;
  };

  createObject = Object.create || function(o) {
    var Func;
    Func = function() {};
    Func.prototype = o;
    return new Func();
  };

  isArray = function(value) {
    return Object.prototype.toString.call(value) === '[object Array]';
  };

  isBlank = function(value) {
    var key;
    if (!value) {
      return true;
    }
    for (key in value) {
      return false;
    }
    return true;
  };

  makeArray = function(args) {
    return Array.prototype.slice.call(args, 0);
  };

  Spine = this.Spine = {};

  if (typeof module !== "undefined" && module !== null) {
    module.exports = Spine;
  }

  Spine.version = '1.0.8';

  Spine.isArray = isArray;

  Spine.isBlank = isBlank;

  Spine.$ = $;

  Spine.Events = Events;

  Spine.Log = Log;

  Spine.Module = Module;

  Spine.Controller = Controller;

  Spine.Model = Model;

  Module.extend.call(Spine, Events);

  Module.create = Module.sub = Controller.create = Controller.sub = Model.sub = function(instances, statics) {
    var result;
    result = (function(_super) {

      __extends(result, _super);

      function result() {
        return result.__super__.constructor.apply(this, arguments);
      }

      return result;

    })(this);
    if (instances) {
      result.include(instances);
    }
    if (statics) {
      result.extend(statics);
    }
    if (typeof result.unbind === "function") {
      result.unbind();
    }
    return result;
  };

  Model.setup = function(name, attributes) {
    var Instance;
    if (attributes == null) {
      attributes = [];
    }
    Instance = (function(_super) {

      __extends(Instance, _super);

      function Instance() {
        return Instance.__super__.constructor.apply(this, arguments);
      }

      return Instance;

    })(this);
    Instance.configure.apply(Instance, [name].concat(__slice.call(attributes)));
    return Instance;
  };

  Spine.Class = Module;

}).call(this);
</script>
<script type="text/javascript">
/*global Ply, jQuery */
/*jshint eqeqeq: true, curly: true, white: true */

// **Ply** is a lightweight JavaScript framework for creating
// reusable UI components and managing application logic. It
// aims to eliminate boilerplate code, create a consistent interface,
// and provide common patterns for code organization and re-use.

// Ply is not an MVC framework and doesn't aim to compete
// with [Backbone.js](http://documentcloud.github.com/backbone/) or [SproutCore](http://www.sproutcore.com/). Instead, it is intended for users
// who need to maintain logic on the server and are looking for a better
// option for code re-use and organization.

// This file comprises the Core module.

// Declare global namespace and assign version number.

window.Ply = {
    VERSION: '0.3.4'
};

// Define `core` module.

Ply.core = (function ($) {

    // ## Private Variables
    // Create private variables. `listeners` is an associative array holding arrays of
    // notification listeners keyed on the notification name.
    var listeners = {},
        // id used to uniquely identify listeners for use in ignore.
        id        = 0,
        debug     = false;

    // ## Public Methods/Properties
    return {

        // ### Notify
        // Notifies listeners of an event. Notifiers should send themselves
        // and optional data as arguments.
        notify: function (note, sender, data) {

            // Cache listeners array or create a new array, assign, and cache it.
            var list = listeners[note] || (listeners[note] = []),
                // Create loop variables.
                i    = 0,
                len  = list.length;

            // Loop over listeners and notify each.
            for (; i < len; i++) {
                list[i].handler.call(list[i].listener, note, sender, data);
            }

        },

        // ### Listen
        // Listens for a particular notification or set of notifications.
        // Clients should pass in a handler function and themselves as arguments.
        // When the handler function is called, it will be applied to the `listener`'s
        // scope, ensuring that `this` refers to what the client expects.
        listen: function (notification, handler, listener) {

            // Cache the notification's listeners if it exists or create and cache
            // a new array otherwise.
            var list  = listeners[notification] || (listeners[notification] = []),
                // Split the notification on whitespace. Clients can listen to
                // multiple notifications by passing in a string with the notification
                // names split by whitespace.
                notes = notification.split(/\s/),
                // Create loop variables.
                len   = notes.length,
                i     = 0;

            // If the notification name contains whitespace,
            // listen on each particular notification (segment).
            if (len > 1) {
                for (; i < len; i++) {
                    this.listen(notes[i], handler, listener);
                }

                return;
            }

            // Add the listener and handler function to the notifications array.
            list.push({
                id: id += 1,
                handler: handler,
                listener: listener
            });
            
            // return handle used to ignore.
            return [notification, id];
        },

        // ### Ignore
        // Removes a particular notification from listeners object.
        // Clients should pass in the returned handle from `Ply.core.listen`.
        ignore: function (handle) {
            
            var note = handle[0];

            listeners[note] && $.each(listeners[note], function(i) {
                if(this.id == handle[1]){
                    listeners[note].splice(i, 1);
                }
            });

            return;
        },

        // ### Log
        // Lightweight logging wrapper around `console`. Useful less so
        // for debugging than for posting notices of interest.
        log: function (msg, type) {

            // Do nothing if debug mode is disabled.
            if (!debug) {
                return;
            }

            // Use the correct logging mechanism based
            // on the parameter type.
            if (window.console) {

                switch (type) {
                case 'warn':
                    console.warn(msg);
                    break;

                case 'info':
                    console.info(msg);
                    break;

                default:
                    console.log(msg);
                    break;
                }
            }

        },

        // ### Error
        // Method to catch JavaScript errors. All Ply methods are run in a `try/catch` block,
        // with exceptions being passed to this method.
        error: function (ex, sev) {

            // If an `onError` callback function is defined in `Ply.config.core`, call it.
            // Note that implementing this callback is highly recommended. See the sample implementation
            // in `Ply.config`.
            if (Ply.config.core.onError && typeof Ply.config.core.onError === 'function') {
                // Pass in the exception and the severity to the handler.
                Ply.config.core.onError(ex, sev);

                return;
            }

            // If no error handler is defined, simply throw the error.
            throw ex;

        },

        // ### Debug
        // Enabled debugging when called with no argues or with any truthy value. To disable debug mode
        // send `false` or another falsy value to this function, e.g.: `Ply.core.debug(0)`.
        debug: function (val) {

            // Default to `true` if no arguments.
            debug = typeof val === 'undefined' ? true : val;

            if (debug) {
                // Print debug notice.
                this.log('debugging...', 'info');

                // Manually set `this.debugOn` in case debug is set after Ply has been initialized.
                // `debugOn` will still be cached to the old value.
                this.debugOn = true;
            }
            else {
                // Do the opposite.
                this.debugOn = false;
            }
        },

        // ### Debug on
        // Cache the value of debug. This is used by clients to determine
        // if debug mode is currently enabled.
        debugOn: function () {
            // Coerce the value to a boolean.
            return !!debug;
        }()

    };

// Alias `jQuery` to `$` in module scope.
})(jQuery);

// &#8618; [Ajax](ajax.html)
</script>
<script type="text/javascript">
/*
Copyright (c) 2010,2011,2012 Morgan Roderick http://roderick.dk
License: MIT - http://mrgnrdrck.mit-license.org

https://github.com/mroderick/PubSubJS
*/
/*global
        setTimeout,
        module,
        exports,
        define,
        window
*/
(function (name, global, definition){
        "use strict";
        if (typeof module !== 'undefined'){
                module.exports = definition(name, global);
        } else if (typeof define === 'function' && typeof define.amd  === 'object'){
                define(definition);     
        } else {
                global[name] = definition(name, global);        
        } 
}('PubSub', ( typeof window !== 'undefined' && window ) || this, function definition(name, global){

        "use strict";
        
        var PubSub = {
                        name: 'PubSubJS',
                        version: '1.3.1-dev'
                },
                messages = {},
                lastUid = -1;

        /**
         *      Returns a function that throws the passed exception, for use as argument for setTimeout
         *      @param { Object } ex An Error object
         */
        function throwException( ex ){
                return function reThrowException(){
                        throw ex;
                };
        }

        function callSubscriber( subscriber, message, data ){
                try {
                        subscriber( message, data );
                } catch( ex ){
                        setTimeout( throwException( ex ), 0);
                }
        }

        function deliverMessage( originalMessage, matchedMessage, data ){
                var subscribers = messages[matchedMessage],
                        i, j; 

                if ( !messages.hasOwnProperty( matchedMessage ) ) {
                        return;
                }

                for ( i = 0, j = subscribers.length; i < j; i++ ){
                        callSubscriber( subscribers[i].func, originalMessage, data );
                }
        }

        function createDeliveryFunction( message, data ){
                return function deliverNamespaced(){
                        var topic = String( message ),
                                position = topic.lastIndexOf( '.' );

                        // deliver the message as it is now
                        deliverMessage(message, message, data);

                        // trim the hierarchy and deliver message to each level
                        while( position !== -1 ){
                                topic = topic.substr( 0, position );
                                position = topic.lastIndexOf('.');
                                deliverMessage( message, topic, data );
                        }
                };
        }

        function messageHasSubscribers( message ){
                var topic = String( message ),
                        found = messages.hasOwnProperty( topic ),
                        position = topic.lastIndexOf( '.' );

                while ( !found && position !== -1 ){
                        topic = topic.substr( 0, position );
                        position = topic.lastIndexOf('.');
                        found = messages.hasOwnProperty( topic );
                }

                return found;
        }

        function publish( message, data, sync ){
                var deliver = createDeliveryFunction( message, data ),
                        hasSubscribers = messageHasSubscribers( message );

                if ( !hasSubscribers ){
                        return false;
                }

                if ( sync === true ){
                        deliver();
                } else {
                        setTimeout( deliver, 0 );
                }
                return true;
        }

        /**
         *      PubSub.publish( message[, data] ) -> Boolean
         *      - message (String): The message to publish
         *      - data: The data to pass to subscribers
         *      Publishes the the message, passing the data to it's subscribers
        **/
        PubSub.publish = function( message, data ){
                return publish( message, data, false );
        };

        /**
         *      PubSub.publishSync( message[, data] ) -> Boolean
         *      - message (String): The message to publish
         *      - data: The data to pass to subscribers
         *      Publishes the the message synchronously, passing the data to it's subscribers
        **/
        PubSub.publishSync = function( message, data ){
                return publish( message, data, true );
        };

        /**
         *      PubSub.subscribe( message, func ) -> String
         *      - message (String): The message to subscribe to
         *      - func (Function): The function to call when a new message is published
         *      Subscribes the passed function to the passed message. Every returned token is unique and should be stored if 
         *      you need to unsubscribe
        **/
        PubSub.subscribe = function( message, func ){
                // message is not registered yet
                if ( !messages.hasOwnProperty( message ) ){
                        messages[message] = [];
                }

                // forcing token as String, to allow for future expansions without breaking usage
                // and allow for easy use as key names for the 'messages' object
                var token = String(++lastUid);
                messages[message].push( { token : token, func : func } );

                // return token for unsubscribing
                return token;
        };

        /**
         *      PubSub.unsubscribe( tokenOrFunction ) -> String | Boolean
         *  - tokenOrFunction (String|Function): The token of the function to unsubscribe or func passed in on subscribe
         *  Unsubscribes a specific subscriber from a specific message using the unique token 
         *  or if using Function as argument, it will remove all subscriptions with that function       
        **/
        PubSub.unsubscribe = function( tokenOrFunction ){
                var isToken = typeof tokenOrFunction === 'string',
                        key = isToken ? 'token' : 'func',
                        succesfulReturnValue = isToken ? tokenOrFunction : true,

                        result = false,
                        m, i, j;
                
                for ( m in messages ){
                        if ( messages.hasOwnProperty( m ) ){
                                for ( i = messages[m].length-1 ; i >= 0; i-- ){
                                        if ( messages[m][i][key] === tokenOrFunction ){
                                                messages[m].splice( i, 1 );
                                                result = succesfulReturnValue;

                                                // tokens are unique, so we can just return here
                                                if ( isToken ){
                                                        return result;
                                                }
                                        }
                                }
                        }
                }

                return result;
        };
        
        return PubSub;
}));
</script>
<script type="text/javascript">
  window.iter = 0
  window.callback = function() {
    iter++
  };
  window.payload = {
    somekey: 'some value'
  };
 var emitter=new EventEmitter2({wildcard:false,maxListeners:0});
var emitterWildcard=new EventEmitter2({wildcard:true,maxListeners:0});




  jQuery(function() {
    jQuery(window).on('app', callback);

    PubSub.subscribe('app', callback);
  
    subtopic.subscribe('app', callback);

    Events.subscribe('app', callback);

    amplify.subscribe('app', callback);

    Spine.bind('app', callback);

    Ply.core.listen('app', callback);
emitter.on('app',callback);
emitterWildcard.on('app',callback);

});
</script>
<script>
(function(context,OBJECT,NUMBER,LENGTH,toString,version,undefined,oldClass,jsface){function isMap(obj){return obj&&typeof obj===OBJECT&&!(typeof obj.length===NUMBER&&!obj.propertyIsEnumerable(LENGTH));}function isArray(obj){return obj&&typeof obj===OBJECT&&typeof obj.length===NUMBER&&!obj.propertyIsEnumerable(LENGTH);}function isFunction(obj){return obj&&typeof obj==="function";}function isFunction(obj){return obj&&typeof obj==="function";}function isString(obj){return toString.apply(obj)==="[object String]";}function isClass(obj){return isFunction(obj)&&obj.prototype&&obj===obj.prototype.constructor;}function copier(key,value,ignoredKeys,object,iClass,oPrototype){if(!ignoredKeys||!ignoredKeys.hasOwnProperty(key)){object[key]=value;if(iClass){oPrototype[key]=value;}}}function extend(object,subject,ignoredKeys){if(isArray(subject)){for(var len=subject.length;--len>=0;){extend(object,subject[len],ignoredKeys);}}else{ignoredKeys=ignoredKeys||{constructor:1,$super:1,prototype:1,$superb:1};var iClass=isClass(object),isSubClass=isClass(subject),oPrototype=object.prototype,supez,key,proto;if(isMap(subject)){for(key in subject){copier(key,subject[key],ignoredKeys,object,iClass,oPrototype);}}if(isSubClass){proto=subject.prototype;for(key in proto){copier(key,proto[key],ignoredKeys,object,iClass,oPrototype);}}if(iClass&&isSubClass){extend(oPrototype,subject.prototype,ignoredKeys);}}}function Class(parent,api){if(!api){parent=(api=parent,0);}var clazz,constructor,singleton,statics,key,bindTo,len,i=0,p,ignoredKeys={constructor:1,$singleton:1,$statics:1,prototype:1,$super:1,$superp:1,main:1},overload=Class.overload,plugins=Class.plugins;api=(typeof api==="function"?api():api)||{};constructor=api.hasOwnProperty("constructor")?api.constructor:0;singleton=api.$singleton;statics=api.$statics;for(key in plugins){ignoredKeys[key]=1;}clazz=singleton?{}:constructor?overload?overload("constructor",constructor):constructor:function(){};bindTo=singleton?clazz:clazz.prototype;parent=!parent||isArray(parent)?parent:[parent];len=parent&&parent.length;while(i<len){p=parent[i++];for(key in p){if(!ignoredKeys[key]){bindTo[key]=p[key];if(!singleton){clazz[key]=p[key];}}}for(key in p.prototype){if(!ignoredKeys[key]){bindTo[key]=p.prototype[key];}}}for(key in api){if(!ignoredKeys[key]){bindTo[key]=api[key];}}for(key in statics){clazz[key]=bindTo[key]=statics[key];}if(!singleton){p=parent&&parent[0]||parent;clazz.$super=p;clazz.$superp=p&&p.prototype?p.prototype:p;}for(key in plugins){plugins[key](clazz,parent,api);}if(isFunction(api.main)){api.main.call(clazz,clazz);}return clazz;}Class.plugins={};jsface={version:version,Class:Class,extend:extend,isMap:isMap,isArray:isArray,isFunction:isFunction,isString:isString,isClass:isClass};if(typeof module!=="undefined"&&module.exports){module.exports=jsface;}else{oldClass=context.Class;context.Class=Class;context.jsface=jsface;jsface.noConflict=function(){context.Class=oldClass;};}})(this,"object","number","length",Object.prototype.toString,"2.1.1");(function(context){var jsface=context.jsface,Class=jsface.Class,isFunction=jsface.isFunction,readyFns=[],readyCount=0;Class.plugins.$ready=function(clazz,parent,api){var r=api.$ready,len=parent?parent.length:0,count=len,pa,i,entry;while(len--){for(i=0;i<readyCount;i++){entry=readyFns[i];pa=parent[len];if(pa===entry[0]){entry[1].call(pa,clazz,parent,api);count--;}if(!count){break;}}}if(isFunction(r)){r.call(clazz,clazz,parent,api);readyFns.push([clazz,r]);readyCount++;}};})(this);var Utils=Class({$singleton:true,noop:function(){},asteriskEnd:function(string){return(string.slice(string.length-1)==="*");},removeAsteriskEnd:function(string){if(this.asteriskEnd(string)){return string.slice(0,string.length-1);}else{return false;}}});var PubSub2=Class(function(){var parseChannel=function(string){if(string.length){if(Utils.asteriskEnd(string)){return Utils.removeAsteriskEnd(string);}}};return{main:function(){this.modules=[];this.channels={};this.channelsList=[];this.subscriptions={};},$singleton:true,$statics:{async:function(fn){setTimeout(function(){fn();},0);},hasSubscribers:function(channel){return((this.channels[channel].subscribers).length>0);}},createChannel:function(channel){if(this.channels[channel]){return this;}else{this.channels[channel]=new Channel(channel,true);this.channelsList.push(channel);var len=this.channels[channel].subChannels.length,i=0;for(i=0;i<len;i++){if(!this.channels[this.channels[channel].subChannels[i]]){this.channels[this.channels[channel].subChannels[i]]=new Channel(this.channels[channel].subChannels[i],false);this.channelsList.push(this.channels[channel].subChannels[i]);}}return this;}},deliver:function(channel,data){var len=this.channels[channel].subscribers.length,i=0;for(i=0;i<len;i++){if(this.channels[channel].subscribers[i]){this.channels[channel].subscribers[i].callback(data);}}},publish:function(channel,data){var theChannel=this.channels[channel];if(theChannel.subscribers.length){var len=theChannel.subscribers.length,i;for(i=0;i<len;i++){if(theChannel.subscribers[i]){theChannel.subscribers[i].callback(data);}}if(theChannel.subChannels.length){len=theChannel.subChannels.length;for(i=0;i<len;i++){this.deliver(theChannel.subChannels[i],data);}}}},subscribe:function(channel,cb){if(Utils.asteriskEnd(channel)){channel=parseChannel(channel);}if(!this.channels[channel]){return null;}if(this.channels[channel].subscribers){this.channels[channel].subscribers.push({callback:cb});return this.channels[channel].subscribers.length-1;}else{return null;}},unsubscribe:function(channel,id){if(this.channels[channel].subscribers[id]){this.channels[channel].subscribers[id]=0;return this;}else{return this;}}};});var Channel=Class({constructor:function(channel,original){this.channel=channel;this.original=original;this.subscribers=[];this.splitter=":";if(this.original){this.subChannels=[];this.parseTopics();}},deleteChannel:function(){this.channel=null;this.original=null;this.subscribers=null;this.splitter=null;this.subChannels=null;this.parseTopics=null;this.clearSubs=null;this.changeChannel=null;return true;},clearSubs:function(){this.subscribers=[];},changeChannel:function(channel){PubSub2.publish("pubsub:channels:changeChannel",{prev:this.channel,next:channel},true);this.channel=channel;},parseTopics:function(){var colonIdx=this.channel.indexOf(":"),dotIdx=this.channel.indexOf(".");if(colonIdx>-1){this.splitter=":";}else{if(dotIdx>-1){this.splitter=".";}}var channelArr=this.channel.split(this.splitter);var len=channelArr.length,str="",i=0,x=0;for(i=0;i<(len-1);i++){str="";for(x=0;x<=i;x++){if(x===0){str=channelArr[x];}else{str=str+this.splitter+channelArr[x];}}this.subChannels.push(str);}}});
var x = 0;
var id = 0;
var id2 = 0;
var noop=function(){void(0);};
PubSub2.createChannel('topic2');
id = PubSub2.subscribe('topic2',noop);
PubSub2.createChannel('topic5');
PubSub2.createChannel('app');
PubSub2.subscribe('app',callback);
</script>

Test runner

Ready to run.

Testing in
TestOps/sec
jQuery Events
jQuery(window).trigger('app', payload);
ready
PubSubJS - async
PubSub.publish('app', payload);
ready
PubSubJS - sync
PubSub.publishSync('app', payload);
ready
Pure JS PubSub
Events.publish('app', [payload]);
ready
Amplify Pub/Sub
amplify.publish('app', payload);
ready
Spine Events
Spine.trigger('app', payload);
ready
Ply Notify/Listen
Ply.core.notify('app', window, payload);
ready
Subtopic
subtopic.publish('app', [payload]);
ready
My-PubSub part of ControllerJS on github
PubSub2.publish('app',payload);
ready
EventEmitter2
emitter.emit('app',payload)
ready
EventEmitter2 with wildcard
emitterWildcard.emit('app',payload)
ready

Revisions

You can edit these tests or add more tests to this page by appending /edit to the URL.