mpneuried
1/11/2013 - 9:46 AM

Simple namespaced Pub/Sub coffee class to use as extendable module for NodeJS and the browser.

Simple namespaced Pub/Sub coffee class to use as extendable module for NodeJS and the browser.

# # PubSub
# 
# Is a small helper to simply realize a pub/sub pattern to a coffee class.
# 
# A namespaceing of the topics is also included.
# This means you can subscribe to `a` and also get the `a.b`. But if you subscribe to `a.b` you will not get a `a`.
# 
# **required module**: `underscore`
# 
class PubSub
	
	# **_ps_handles** *Object* The handle store. It stores the handles unter each topic as array
	_ps_handles: {}
	# **_ps_count** *Number* Small helper to stop if no subscribers are listening
	_ps_count: 0

	# **ps_delimiter** *String* Namespace delimiter String. You can change this to your prefered delimiter
	ps_delimiter: "."

	###
	## subscribe
	
	`pubsub.subscribe( topic, handle )`
	
	subscribe to a topic
	
	@param { String } topic A Topic you want to subscribe. It can be namespaced. The namesapce delimiter can be defined by the var `ps_delimiter`
	@param { Function } handle The handle function wich will be called by a `publish`

	@api public
	###
	subscribe: ( topic, handle )=>
		# just for a fast exit if no subscribers exists
		@_ps_count++
		# fire change
		@listenerCountChanged( @_ps_count )

		# save the handle to the handle store
		@_ps_handles[ topic ] or= []
		@_ps_handles[ topic ].push( handle )
		return

	###
	## unsubscribe
	
	`pubsub.unsubscribe( handle )`
	
	unsubscribe from a topic
	
	@param { Function } handle The same handle used by `subscribe`
	
	@api public
	###
	unsubscribe: ( handle )=>
		# just for a fast exit if no subscribers exists
		@_ps_count--
		# fire change
		@listenerCountChanged( @_ps_count )

		# try to find the handle and remove it
		for topic, _thandles of @_ps_handles
			if handle in _thandles
				@_ps_handles[ topic ] = _.without( _thandles, handle )

				# cleanup if topic is empty
				if not @_ps_handles[ topic ].length
					@_ps_handles = _.omit( @_ps_handles, topic );
				break

		return

	###
	## publish
	
	`pubsub.publish( topic, data )`
	
	publish from a topic
	
	@param { String } topic The topic you want to publish.
	@param { Any } data Additional data to send the the subscriber.
	
	@api public
	###
	publish: ( topic, data )=>
		# find the handles and call the handles
		for handle in @_ps_findHandles( topic )
			handle( topic, data )
		return
	

	###
	## listenerCount
	
	`pubsub.listenerCount(  )`
	
	Get the count of listeners. Helps you the find out if anyone listens
	
	@return { Number } Number of listeners 
	
	@api public
	###
	listenerCount: =>
		@_ps_count

	###
	## listenerCountChanged
	
	`pubsub.listenerCountChanged( count )`
	
	Overridable method fired if a listener subscribes or unsubscribes. Can be used the activate or deactivate something.
	
	@param { Number } count New number of listener
	
	@api public
	###
	listenerCountChanged: ( count )=>
		return

	###
	## _ps_findHandles
	
	`pubsub._ps_findHandles( topic )`
	
	Find all matching handles of a topic. This also inculdes the use of namespacing
	
	@param { String } topic The topic to find the matiching handles 
	
	@return { Array } An array of handles
	
	@api private
	###
	_ps_findHandles: ( topic )=>
		if @_ps_count
			_keys = _.keys( @_ps_handles )
			_matched = []

			# loop through all cached topics and check if they matching the topic
			for _kn in _keys
				if @_ps_match( _kn.split( @ps_delimiter ), topic.split( @ps_delimiter ) )
					_matched = _.union( _matched, @_ps_handles[ _kn ] )
			_matched
		else
			[]

	###
	## _ps_match
	
	`pubsub._ps_match( inp, test )`
	
	Matching helper to find the handles by namespace. This will be calles recrusive through the depth of the topic namespace
	
	@param { Array } inp Array of namespace cache handles to check against 
	@param { Array } test Array of topic namespace to check against `inp`
	
	@return { Boolean } First `inp` level is matching first `test` level.
	
	@api private
	###
	_ps_match: ( inp = [], test = [] )->
		_tk = test[ 0 ]
		_ik = inp[ 0 ]

		if test.length and _tk is _ik
			# if the first level matches return true or if possible go deeper
			if inp.length > 1
				@_ps_match( inp.splice( 1 ), test.splice( 1 ) )
			else
				true
		else
			# return false on a mismatch
			false