Source: lib/pubsub.js

/*
Copyright (c) 2010,2011,2012,2013 Morgan Roderick http://roderick.dk
License: MIT - http://mrgnrdrck.mit-license.org

Copyright (c) 2014 Visisoft OHG http://visisoft.de
https://github.com/semmel/PubSubJS

see Crockford: JavaScript: The Good Parts, section "Parts"
*/
/*jslint white:true, plusplus:true, stupid:true*/
/*global
	setTimeout,
	module,
	exports,
	define,
	require,
	window
*/

(function(root, factory){
	'use strict';

	// CommonJS
	if (typeof exports === 'object' && module)
	{
		module.exports = factory();

	// AMD
	}
	else if (typeof define === 'function' && define.amd)
	{
		define(factory);

	// Browser
	}
	else
	{
		root.createPublisher = factory();
	}
}( ( typeof window === 'object' && window ) || this
	, function()
{
	function hasKeys(obj){
		var key;

		for (key in obj){
			if ( obj.hasOwnProperty(key) ){
				return true;
			}
		}
		return false;
	}

	/**
	 *	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;
		};
	}

	/**
	 * @file An Open Source PubSub Implementation
	 * @see {@link https://github.com/semmel/PubSubJS|Source Code on GitHub}
	 * @module lib/pubsub
	 * @version 1.4.0
	 * @example
	 * // create a function to receive messages
	 * var mySubscriber = function( msg, data ){
	 * 	console.log( msg, data );
	 * };
	 *
	 * require(['lib/pubsub'], function(createPublisher){
	 * 	// create a publisher using the factory method
	 * 	var publisher = createPublisher();
	 *
	 * 	// add the function to the list of subscribers for a particular message
	 * 	var subscription = publisher.subscribe( 'MY MESSAGE', mySubscriber );
	 *
	 * 	// publish a message asyncronously
	 * 	publisher.publish( 'MY MESSAGE', 'hello world!' );
	 * 	// logs 'MY MESSAGE', 'hello world!' to the console
	 *
	 * 	// unsubscribe from further messages
	 * 	subscription.dispose();
	 * });
	 */

	/**
	 * @class Publisher
	 */

	/**
	 * Factory method to create a new Publisher or for adding PubSub functionality to the given object.
	 * I.e. augments <tt>publish, publishSync, subscribe</tt> and <tt>unsubscribe</tt> methods.
	 * @function module:lib/pubsub.createPublisher
	 * @constructs Publisher
	 * @param {Object=} subject If not given a plain Publisher object is returned
	 * @returns {module:lib/pubsub~Publisher}
	 */
	function createPublisher(subject)
	{
		var
			self = subject || {},
			messages = {},
			lastUid = -1;

		/**
		 * adapt the parameters to the handlers function signature
		 * @memberof module:lib/pubsub~Publisher#
		 * @member {function} handlerInvoker
		 * @param {Function} handler the registered subscriber for the particular topic
		 * @param {String} message the topic
		 * @param {*} data the event payload
		 * @example
		 * // the new handler function receives the event's payload
		 * // in it's single argument
		 * var mySubscriber = function( data ){
	 	 * 	console.log( data );
		 * };
		 * // create a publisher using the factory method
		 * var publisher = createPublisher({
		 * 	handlerInvoker: function(handler, message, payload){
		 * 		handler(payload);
		 * 	}
		 *	});
		 * // register the handler
		 * var subscription = publisher.subscribe( 'topic', mySubscriber );
		 *
		 * publisher.publish( 'topic', 'hello world!' );
		 * // logs 'hello world!' to the console
		 */
		self.handlerInvoker = self.handlerInvoker ||	function handlerInvoker (handler, message, data)
		{
			handler.call(undefined, message, data);
		};

		function callSubscriberWithDelayedExceptions( subscriber, message, data ){
			try {
				self.handlerInvoker(subscriber, message, data );
			} catch( ex ){
				setTimeout( throwException( ex ), 0);
			}
		}

		function callSubscriberWithImmediateExceptions( subscriber, message, data ){
			self.handlerInvoker(subscriber, message, data );
		}

		function deliverMessage( originalMessage, matchedMessage, data, immediateExceptions )
		{
			var
				subscribers = messages[matchedMessage],
				callSubscriber = immediateExceptions ? callSubscriberWithImmediateExceptions
					: callSubscriberWithDelayedExceptions,
				s;

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

			for (s in subscribers){
				if ( subscribers.hasOwnProperty(s)){
					callSubscriber( subscribers[s], originalMessage, data );
				}
			}
		}

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

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

				// 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 = Boolean(messages.hasOwnProperty( topic ) && hasKeys(messages[topic])),
				position = topic.lastIndexOf( '.' );

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

			return found;
		}

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

			if ( !hasSubscribers ){
				return false;
			}

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

		/**
		 *	Publishes the the message, passing the data to it's subscribers
		 *	@memberof module:lib/pubsub~Publisher#
		 *	@method publish
		 *	@param {String} message The message to publish
		 *	@param {*} data The data to pass to subscribers
		 *	@returns {boolean}
		 *
		**/
		self.publish =	function ( message, data ){
				return publish_( message, data, false, self.immediateExceptions );
			};

		/**
		 * Publishes the the message synchronously, passing the data to it's subscribers
		 *	@memberof module:lib/pubsub~Publisher#
		 *	@method publishSync
		 *	@param {String} message The message to publish
		 *	@param {*} data The data to pass to subscribers
		 *	@returns {boolean}
		**/
		self.publishSync = function( message, data ){
			return publish_( message, data, true, self.immediateExceptions );
		};

		/**
		 * Subscribes the passed function to the passed message.
		 * @memberof module:lib/pubsub~Publisher#
		 * @method subscribe
		 *	@param {String} message The message to subscribe to
		 *	@param {module:lib/pubsub~Publisher~eventCallback} callback The function to call when
		 *	a new message is published
		 *	@returns {Object} a subscription object with a single method <tt>dispose</tt>
		 *	you need to call to unsubscribe
		**/
		self.subscribe = function( message, callback ){
			if ( typeof callback !== 'function'){
				return false;
			}

			// 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 = 'uid_' + String(++lastUid);
			messages[message][token] = callback;

			// return token for unsubscribing
			//return token;
			// return a subscription object with a dispose method
			return { dispose: self.unsubscribe.bind(self, token) };
		};

		/**
		 * @callback module:lib/pubsub~Publisher~eventCallback
		 * @param {string} message the topic
		 * @param {*=} data the payload
		 */


		/**
		 *	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
		**/
		self.unsubscribe = function( tokenOrFunction ){
			var isToken = typeof tokenOrFunction === 'string',
				result = false,
				m, message, t, token;

			for ( m in messages ){
				if ( messages.hasOwnProperty( m ) ){
					message = messages[m];

					if ( isToken && message[tokenOrFunction] ){
						delete message[tokenOrFunction];
						result = tokenOrFunction;
						// tokens are unique, so we can just stop here
						break;
					} else if (!isToken){
						for ( t in message ){
							if (message.hasOwnProperty(t) && message[t] === tokenOrFunction){
								delete message[t];
								result = true;
							}
						}
					}
				}
			}

			return result;
		};

		/**
		 * remove all references to the subscribers. Is useful if the publisher is destroyed.
		 * @memberof module:lib/pubsub~Publisher#
		 * @method unsubscribeAll
		 */
		self.unsubscribeAll = function()
		{
			messages = {};
		};

		return self;
	}

	return createPublisher;
}));