diff --git a/lib/node/package.json b/lib/node/package.json index 677d4b1..2c203e7 100644 --- a/lib/node/package.json +++ b/lib/node/package.json @@ -1,7 +1,7 @@ { "name" : "postal", "description" : "Pub/Sub library providing wildcard subscriptions, complex message handling, etc. Works server and client-side.", - "version" : "0.6.0", + "version" : "0.6.1", "homepage" : "http://github.com/ifandelse/postal.js", "repository" : { "type" : "git", diff --git a/lib/postal.amd.js b/lib/postal.amd.js new file mode 100644 index 0000000..13e8427 --- /dev/null +++ b/lib/postal.amd.js @@ -0,0 +1,481 @@ +/* + postal.js + Author: Jim Cowart + License: Dual licensed MIT (http://www.opensource.org/licenses/mit-license) & GPL (http://www.opensource.org/licenses/gpl-license) + Version 0.6.1 + */ + +// This is the amd-module version of postal.js +// If you need the standard lib style version, go to http://github.com/ifandelse/postal.js +define( ["underscore"], function ( _, undefined ) { + +var DEFAULT_CHANNEL = "/", + DEFAULT_PRIORITY = 50, + DEFAULT_DISPOSEAFTER = 0, + SYSTEM_CHANNEL = "postal", + NO_OP = function () { + }; + +var DistinctPredicate = function () { + var previous; + return function ( data ) { + var eq = false; + if ( _.isString( data ) ) { + eq = data === previous; + previous = data; + } + else { + eq = _.isEqual( data, previous ); + previous = _.clone( data ); + } + return !eq; + }; +}; + +var ChannelDefinition = function ( channelName, defaultTopic ) { + this.channel = channelName || DEFAULT_CHANNEL; + this._topic = defaultTopic || ""; +}; + +ChannelDefinition.prototype = { + subscribe : function () { + var len = arguments.length; + if ( len === 1 ) { + return new SubscriptionDefinition( this.channel, this._topic, arguments[0] ); + } + else if ( len === 2 ) { + return new SubscriptionDefinition( this.channel, arguments[0], arguments[1] ); + } + }, + + publish : function ( obj ) { + var _obj = obj || {}; + var envelope = { + channel : this.channel, + topic : this._topic, + data : _obj + }; + // If this is an envelope.... + if ( _obj.topic && _obj.data ) { + envelope = _obj; + envelope.channel = envelope.channel || this.channel; + } + envelope.timeStamp = new Date(); + postal.configuration.bus.publish( envelope ); + return envelope; + }, + + topic : function ( topic ) { + if ( topic === this._topic ) { + return this; + } + return new ChannelDefinition( this.channel, topic ); + } +}; + +var SubscriptionDefinition = function ( channel, topic, callback ) { + this.channel = channel; + this.topic = topic; + this.callback = callback; + this.priority = DEFAULT_PRIORITY; + this.constraints = new Array( 0 ); + this.maxCalls = DEFAULT_DISPOSEAFTER; + this.onHandled = NO_OP; + this.context = null; + postal.configuration.bus.publish( { + channel : SYSTEM_CHANNEL, + topic : "subscription.created", + timeStamp : new Date(), + data : { + event : "subscription.created", + channel : channel, + topic : topic + } + } ); + + postal.configuration.bus.subscribe( this ); + +}; + +SubscriptionDefinition.prototype = { + unsubscribe : function () { + postal.configuration.bus.unsubscribe( this ); + postal.configuration.bus.publish( { + channel : SYSTEM_CHANNEL, + topic : "subscription.removed", + timeStamp : new Date(), + data : { + event : "subscription.removed", + channel : this.channel, + topic : this.topic + } + } ); + }, + + defer : function () { + var fn = this.callback; + this.callback = function ( data ) { + setTimeout( fn, 0, data ); + }; + return this; + }, + + disposeAfter : function ( maxCalls ) { + if ( _.isNaN( maxCalls ) || maxCalls <= 0 ) { + throw "The value provided to disposeAfter (maxCalls) must be a number greater than zero."; + } + + var fn = this.onHandled; + var dispose = _.after( maxCalls, _.bind( function () { + this.unsubscribe( this ); + }, this ) ); + + this.onHandled = function () { + fn.apply( this.context, arguments ); + dispose(); + }; + return this; + }, + + ignoreDuplicates : function () { + this.withConstraint( new DistinctPredicate() ); + return this; + }, + + withConstraint : function ( predicate ) { + if ( !_.isFunction( predicate ) ) { + throw "Predicate constraint must be a function"; + } + this.constraints.push( predicate ); + return this; + }, + + withConstraints : function ( predicates ) { + var self = this; + if ( _.isArray( predicates ) ) { + _.each( predicates, function ( predicate ) { + self.withConstraint( predicate ); + } ); + } + return self; + }, + + withContext : function ( context ) { + this.context = context; + return this; + }, + + withDebounce : function ( milliseconds ) { + if ( _.isNaN( milliseconds ) ) { + throw "Milliseconds must be a number"; + } + var fn = this.callback; + this.callback = _.debounce( fn, milliseconds ); + return this; + }, + + withDelay : function ( milliseconds ) { + if ( _.isNaN( milliseconds ) ) { + throw "Milliseconds must be a number"; + } + var fn = this.callback; + this.callback = function ( data ) { + setTimeout( function () { + fn( data ); + }, milliseconds ); + }; + return this; + }, + + withPriority : function ( priority ) { + if ( _.isNaN( priority ) ) { + throw "Priority must be a number"; + } + this.priority = priority; + return this; + }, + + withThrottle : function ( milliseconds ) { + if ( _.isNaN( milliseconds ) ) { + throw "Milliseconds must be a number"; + } + var fn = this.callback; + this.callback = _.throttle( fn, milliseconds ); + return this; + }, + + subscribe : function ( callback ) { + this.callback = callback; + return this; + } +}; + +var bindingsResolver = { + cache : { }, + + compare : function ( binding, topic ) { + if ( this.cache[topic] && this.cache[topic][binding] ) { + return true; + } + // binding.replace(/\./g,"\\.") // escape actual periods + // .replace(/\*/g, ".*") // asterisks match any value + // .replace(/#/g, "[A-Z,a-z,0-9]*"); // hash matches any alpha-numeric 'word' + var rgx = new RegExp( "^" + binding.replace( /\./g, "\\." ).replace( /\*/g, ".*" ).replace( /#/g, "[A-Z,a-z,0-9]*" ) + "$" ), + result = rgx.test( topic ); + if ( result ) { + if ( !this.cache[topic] ) { + this.cache[topic] = {}; + } + this.cache[topic][binding] = true; + } + return result; + }, + + reset : function () { + this.cache = {}; + } +}; + +var localBus = { + + addWireTap : function ( callback ) { + var self = this; + self.wireTaps.push( callback ); + return function () { + var idx = self.wireTaps.indexOf( callback ); + if ( idx !== -1 ) { + self.wireTaps.splice( idx, 1 ); + } + }; + }, + + publish : function ( envelope ) { + _.each( this.wireTaps, function ( tap ) { + tap( envelope.data, envelope ); + } ); + + _.each( this.subscriptions[envelope.channel], function ( topic ) { + _.each( topic, function ( subDef ) { + if ( postal.configuration.resolver.compare( subDef.topic, envelope.topic ) ) { + if ( _.all( subDef.constraints, function ( constraint ) { + return constraint( envelope.data, envelope ); + } ) ) { + if ( typeof subDef.callback === 'function' ) { + subDef.callback.apply( subDef.context, [envelope.data, envelope] ); + subDef.onHandled(); + } + } + } + } ); + } ); + }, + + reset : function () { + if ( this.subscriptions ) { + _.each( this.subscriptions, function ( channel ) { + _.each( channel, function ( topic ) { + while ( topic.length ) { + topic.pop().unsubscribe(); + } + } ); + } ); + this.subscriptions = {}; + } + }, + + subscribe : function ( subDef ) { + var idx, found, fn, channel = this.subscriptions[subDef.channel], subs; + + if ( !channel ) { + channel = this.subscriptions[subDef.channel] = {}; + } + subs = this.subscriptions[subDef.channel][subDef.topic]; + if ( !subs ) { + subs = this.subscriptions[subDef.channel][subDef.topic] = new Array( 0 ); + } + + idx = subs.length - 1; + for ( ; idx >= 0; idx-- ) { + if ( subs[idx].priority <= subDef.priority ) { + subs.splice( idx + 1, 0, subDef ); + found = true; + break; + } + } + if ( !found ) { + subs.unshift( subDef ); + } + return subDef; + }, + + subscriptions : {}, + + wireTaps : new Array( 0 ), + + unsubscribe : function ( config ) { + if ( this.subscriptions[config.channel][config.topic] ) { + var len = this.subscriptions[config.channel][config.topic].length, + idx = 0; + for ( ; idx < len; idx++ ) { + if ( this.subscriptions[config.channel][config.topic][idx] === config ) { + this.subscriptions[config.channel][config.topic].splice( idx, 1 ); + break; + } + } + } + } +}; + +var publishPicker = { + "1" : function ( envelope ) { + if ( !envelope ) { + throw new Error( "publishing from the 'global' postal.publish call requires a valid envelope." ); + } + envelope.channel = envelope.channel || DEFAULT_CHANNEL; + envelope.timeStamp = new Date(); + postal.configuration.bus.publish( envelope ); + return envelope; + }, + "2" : function ( topic, data ) { + var envelope = { channel : DEFAULT_CHANNEL, topic : topic, timeStamp : new Date(), data : data }; + postal.configuration.bus.publish( envelope ); + return envelope; + }, + "3" : function ( channel, topic, data ) { + var envelope = { channel : channel, topic : topic, timeStamp : new Date(), data : data }; + postal.configuration.bus.publish( envelope ); + return envelope; + } + }, + channelPicker = { + "1" : function ( chn ) { + var channel = chn, topic, options = {}; + if ( Object.prototype.toString.call( channel ) === "[object String]" ) { + channel = DEFAULT_CHANNEL; + topic = chn; + } + else { + channel = chn.channel || DEFAULT_CHANNEL; + topic = chn.topic; + options = chn.options || options; + } + return new postal.channelTypes[ options.type || "local" ]( channel, topic ); + }, + "2" : function ( chn, tpc ) { + var channel = chn, topic = tpc, options = {}; + if ( Object.prototype.toString.call( tpc ) === "[object Object]" ) { + channel = DEFAULT_CHANNEL; + topic = chn; + options = tpc; + } + return new postal.channelTypes[ options.type || "local" ]( channel, topic ); + }, + "3" : function ( channel, topic, options ) { + return new postal.channelTypes[ options.type || "local" ]( channel, topic ); + } + }, + sessionInfo = {}; + +// save some setup time, albeit tiny +localBus.subscriptions[SYSTEM_CHANNEL] = {}; + +var postal = { + configuration : { + bus : localBus, + resolver : bindingsResolver, + DEFAULT_CHANNEL : DEFAULT_CHANNEL, + DEFAULT_PRIORITY : DEFAULT_PRIORITY, + DEFAULT_DISPOSEAFTER : DEFAULT_DISPOSEAFTER, + SYSTEM_CHANNEL : SYSTEM_CHANNEL + }, + + channelTypes : { + local : ChannelDefinition + }, + + channel : function () { + var len = arguments.length; + if ( channelPicker[len] ) { + return channelPicker[len].apply( this, arguments ); + } + }, + + subscribe : function ( options ) { + var callback = options.callback, + topic = options.topic, + channel = options.channel || DEFAULT_CHANNEL; + return new SubscriptionDefinition( channel, topic, callback ); + }, + + publish : function () { + var len = arguments.length; + if ( publishPicker[len] ) { + return publishPicker[len].apply( this, arguments ); + } + }, + + addWireTap : function ( callback ) { + return this.configuration.bus.addWireTap( callback ); + }, + + linkChannels : function ( sources, destinations ) { + var result = []; + if ( !_.isArray( sources ) ) { + sources = [sources]; + } + if ( !_.isArray( destinations ) ) { + destinations = [destinations]; + } + _.each( sources, function ( source ) { + var sourceTopic = source.topic || "*"; + _.each( destinations, function ( destination ) { + var destChannel = destination.channel || DEFAULT_CHANNEL; + result.push( + postal.subscribe( { + channel : source.channel || DEFAULT_CHANNEL, + topic : source.topic || "*", + callback : function ( data, env ) { + var newEnv = env; + newEnv.topic = _.isFunction( destination.topic ) ? destination.topic( env.topic ) : destination.topic || env.topic; + newEnv.channel = destChannel; + newEnv.data = data; + postal.publish( newEnv ); + } + } ) + ); + } ); + } ); + return result; + }, + + utils : { + getSubscribersFor : function () { + var channel = arguments[ 0 ], + tpc = arguments[ 1 ], + result = []; + if ( arguments.length === 1 ) { + if ( Object.prototype.toString.call( channel ) === "[object String]" ) { + channel = postal.configuration.DEFAULT_CHANNEL; + tpc = arguments[ 0 ]; + } + else { + channel = arguments[ 0 ].channel || postal.configuration.DEFAULT_CHANNEL; + tpc = arguments[ 0 ].topic; + } + } + if ( postal.configuration.bus.subscriptions[ channel ] && + postal.configuration.bus.subscriptions[ channel ].hasOwnProperty( tpc ) ) { + result = postal.configuration.bus.subscriptions[ channel ][ tpc ]; + } + return result; + }, + + reset : function () { + postal.configuration.bus.reset(); + postal.configuration.resolver.reset(); + } + } +}; + + return postal; +} ); \ No newline at end of file diff --git a/lib/postal.amd.min.js b/lib/postal.amd.min.js new file mode 100644 index 0000000..a58a803 --- /dev/null +++ b/lib/postal.amd.min.js @@ -0,0 +1 @@ +define(["underscore"],function(a,b){var c="/",d=50,e=0,f="postal",g=function(){},h=function(){var b;return function(c){var d=!1;return a.isString(c)?(d=c===b,b=c):(d=a.isEqual(c,b),b=a.clone(c)),!d}},i=function(a,b){this.channel=a||c,this._topic=b||""};i.prototype={subscribe:function(){var a=arguments.length;if(a===1)return new j(this.channel,this._topic,arguments[0]);if(a===2)return new j(this.channel,arguments[0],arguments[1])},publish:function(a){var b=a||{},c={channel:this.channel,topic:this._topic,data:b};return b.topic&&b.data&&(c=b,c.channel=c.channel||this.channel),c.timeStamp=new Date,p.configuration.bus.publish(c),c},topic:function(a){return a===this._topic?this:new i(this.channel,a)}};var j=function(a,b,c){this.channel=a,this.topic=b,this.callback=c,this.priority=d,this.constraints=new Array(0),this.maxCalls=e,this.onHandled=g,this.context=null,p.configuration.bus.publish({channel:f,topic:"subscription.created",timeStamp:new Date,data:{event:"subscription.created",channel:a,topic:b}}),p.configuration.bus.subscribe(this)};j.prototype={unsubscribe:function(){p.configuration.bus.unsubscribe(this),p.configuration.bus.publish({channel:f,topic:"subscription.removed",timeStamp:new Date,data:{event:"subscription.removed",channel:this.channel,topic:this.topic}})},defer:function(){var a=this.callback;return this.callback=function(b){setTimeout(a,0,b)},this},disposeAfter:function(b){if(a.isNaN(b)||b<=0)throw"The value provided to disposeAfter (maxCalls) must be a number greater than zero.";var c=this.onHandled,d=a.after(b,a.bind(function(){this.unsubscribe(this)},this));return this.onHandled=function(){c.apply(this.context,arguments),d()},this},ignoreDuplicates:function(){return this.withConstraint(new h),this},withConstraint:function(b){if(!a.isFunction(b))throw"Predicate constraint must be a function";return this.constraints.push(b),this},withConstraints:function(b){var c=this;return a.isArray(b)&&a.each(b,function(a){c.withConstraint(a)}),c},withContext:function(a){return this.context=a,this},withDebounce:function(b){if(a.isNaN(b))throw"Milliseconds must be a number";var c=this.callback;return this.callback=a.debounce(c,b),this},withDelay:function(b){if(a.isNaN(b))throw"Milliseconds must be a number";var c=this.callback;return this.callback=function(a){setTimeout(function(){c(a)},b)},this},withPriority:function(b){if(a.isNaN(b))throw"Priority must be a number";return this.priority=b,this},withThrottle:function(b){if(a.isNaN(b))throw"Milliseconds must be a number";var c=this.callback;return this.callback=a.throttle(c,b),this},subscribe:function(a){return this.callback=a,this}};var k={cache:{},compare:function(a,b){if(this.cache[b]&&this.cache[b][a])return!0;var c=new RegExp("^"+a.replace(/\./g,"\\.").replace(/\*/g,".*").replace(/#/g,"[A-Z,a-z,0-9]*")+"$"),d=c.test(b);return d&&(this.cache[b]||(this.cache[b]={}),this.cache[b][a]=!0),d},reset:function(){this.cache={}}},l={addWireTap:function(a){var b=this;return b.wireTaps.push(a),function(){var c=b.wireTaps.indexOf(a);c!==-1&&b.wireTaps.splice(c,1)}},publish:function(b){a.each(this.wireTaps,function(a){a(b.data,b)}),a.each(this.subscriptions[b.channel],function(c){a.each(c,function(c){p.configuration.resolver.compare(c.topic,b.topic)&&a.all(c.constraints,function(a){return a(b.data,b)})&&typeof c.callback=="function"&&(c.callback.apply(c.context,[b.data,b]),c.onHandled())})})},reset:function(){this.subscriptions&&(a.each(this.subscriptions,function(b){a.each(b,function(a){while(a.length)a.pop().unsubscribe()})}),this.subscriptions={})},subscribe:function(a){var b,c,d,e=this.subscriptions[a.channel],f;e||(e=this.subscriptions[a.channel]={}),f=this.subscriptions[a.channel][a.topic],f||(f=this.subscriptions[a.channel][a.topic]=new Array(0)),b=f.length-1;for(;b>=0;b--)if(f[b].priority<=a.priority){f.splice(b+1,0,a),c=!0;break}return c||f.unshift(a),a},subscriptions:{},wireTaps:new Array(0),unsubscribe:function(a){if(this.subscriptions[a.channel][a.topic]){var b=this.subscriptions[a.channel][a.topic].length,c=0;for(;c= 0; idx-- ) { + if ( subs[idx].priority <= subDef.priority ) { + subs.splice( idx + 1, 0, subDef ); + found = true; + break; + } + } + if ( !found ) { + subs.unshift( subDef ); + } + return subDef; + }, + + subscriptions : {}, + + wireTaps : new Array( 0 ), + + unsubscribe : function ( config ) { + if ( this.subscriptions[config.channel][config.topic] ) { + var len = this.subscriptions[config.channel][config.topic].length, + idx = 0; + for ( ; idx < len; idx++ ) { + if ( this.subscriptions[config.channel][config.topic][idx] === config ) { + this.subscriptions[config.channel][config.topic].splice( idx, 1 ); + break; + } + } + } + } +}; + +var publishPicker = { + "1" : function ( envelope ) { + if ( !envelope ) { + throw new Error( "publishing from the 'global' postal.publish call requires a valid envelope." ); + } + envelope.channel = envelope.channel || DEFAULT_CHANNEL; + envelope.timeStamp = new Date(); + postal.configuration.bus.publish( envelope ); + return envelope; + }, + "2" : function ( topic, data ) { + var envelope = { channel : DEFAULT_CHANNEL, topic : topic, timeStamp : new Date(), data : data }; + postal.configuration.bus.publish( envelope ); + return envelope; + }, + "3" : function ( channel, topic, data ) { + var envelope = { channel : channel, topic : topic, timeStamp : new Date(), data : data }; + postal.configuration.bus.publish( envelope ); + return envelope; + } + }, + channelPicker = { + "1" : function ( chn ) { + var channel = chn, topic, options = {}; + if ( Object.prototype.toString.call( channel ) === "[object String]" ) { + channel = DEFAULT_CHANNEL; + topic = chn; + } + else { + channel = chn.channel || DEFAULT_CHANNEL; + topic = chn.topic; + options = chn.options || options; + } + return new postal.channelTypes[ options.type || "local" ]( channel, topic ); + }, + "2" : function ( chn, tpc ) { + var channel = chn, topic = tpc, options = {}; + if ( Object.prototype.toString.call( tpc ) === "[object Object]" ) { + channel = DEFAULT_CHANNEL; + topic = chn; + options = tpc; + } + return new postal.channelTypes[ options.type || "local" ]( channel, topic ); + }, + "3" : function ( channel, topic, options ) { + return new postal.channelTypes[ options.type || "local" ]( channel, topic ); + } + }, + sessionInfo = {}; + +// save some setup time, albeit tiny +localBus.subscriptions[SYSTEM_CHANNEL] = {}; + +var postal = { + configuration : { + bus : localBus, + resolver : bindingsResolver, + DEFAULT_CHANNEL : DEFAULT_CHANNEL, + DEFAULT_PRIORITY : DEFAULT_PRIORITY, + DEFAULT_DISPOSEAFTER : DEFAULT_DISPOSEAFTER, + SYSTEM_CHANNEL : SYSTEM_CHANNEL + }, + + channelTypes : { + local : ChannelDefinition + }, + + channel : function () { + var len = arguments.length; + if ( channelPicker[len] ) { + return channelPicker[len].apply( this, arguments ); + } + }, + + subscribe : function ( options ) { + var callback = options.callback, + topic = options.topic, + channel = options.channel || DEFAULT_CHANNEL; + return new SubscriptionDefinition( channel, topic, callback ); + }, + + publish : function () { + var len = arguments.length; + if ( publishPicker[len] ) { + return publishPicker[len].apply( this, arguments ); + } + }, + + addWireTap : function ( callback ) { + return this.configuration.bus.addWireTap( callback ); + }, + + linkChannels : function ( sources, destinations ) { + var result = []; + if ( !_.isArray( sources ) ) { + sources = [sources]; + } + if ( !_.isArray( destinations ) ) { + destinations = [destinations]; + } + _.each( sources, function ( source ) { + var sourceTopic = source.topic || "*"; + _.each( destinations, function ( destination ) { + var destChannel = destination.channel || DEFAULT_CHANNEL; + result.push( + postal.subscribe( { + channel : source.channel || DEFAULT_CHANNEL, + topic : source.topic || "*", + callback : function ( data, env ) { + var newEnv = env; + newEnv.topic = _.isFunction( destination.topic ) ? destination.topic( env.topic ) : destination.topic || env.topic; + newEnv.channel = destChannel; + newEnv.data = data; + postal.publish( newEnv ); + } + } ) + ); + } ); + } ); + return result; + }, + + utils : { + getSubscribersFor : function () { + var channel = arguments[ 0 ], + tpc = arguments[ 1 ], + result = []; + if ( arguments.length === 1 ) { + if ( Object.prototype.toString.call( channel ) === "[object String]" ) { + channel = postal.configuration.DEFAULT_CHANNEL; + tpc = arguments[ 0 ]; + } + else { + channel = arguments[ 0 ].channel || postal.configuration.DEFAULT_CHANNEL; + tpc = arguments[ 0 ].topic; + } + } + if ( postal.configuration.bus.subscriptions[ channel ] && + postal.configuration.bus.subscriptions[ channel ].hasOwnProperty( tpc ) ) { + result = postal.configuration.bus.subscriptions[ channel ][ tpc ]; + } + return result; + }, + + reset : function () { + postal.configuration.bus.reset(); + postal.configuration.resolver.reset(); + } + } +}; + +module.exports = postal; \ No newline at end of file diff --git a/lib/postal.node.min.js b/lib/postal.node.min.js new file mode 100644 index 0000000..6e25ed3 --- /dev/null +++ b/lib/postal.node.min.js @@ -0,0 +1 @@ +var _=require("underscore"),DEFAULT_CHANNEL="/",DEFAULT_PRIORITY=50,DEFAULT_DISPOSEAFTER=0,SYSTEM_CHANNEL="postal",NO_OP=function(){},DistinctPredicate=function(){var a;return function(b){var c=!1;return _.isString(b)?(c=b===a,a=b):(c=_.isEqual(b,a),a=_.clone(b)),!c}},ChannelDefinition=function(a,b){this.channel=a||DEFAULT_CHANNEL,this._topic=b||""};ChannelDefinition.prototype={subscribe:function(){var a=arguments.length;if(a===1)return new SubscriptionDefinition(this.channel,this._topic,arguments[0]);if(a===2)return new SubscriptionDefinition(this.channel,arguments[0],arguments[1])},publish:function(a){var b=a||{},c={channel:this.channel,topic:this._topic,data:b};return b.topic&&b.data&&(c=b,c.channel=c.channel||this.channel),c.timeStamp=new Date,postal.configuration.bus.publish(c),c},topic:function(a){return a===this._topic?this:new ChannelDefinition(this.channel,a)}};var SubscriptionDefinition=function(a,b,c){this.channel=a,this.topic=b,this.callback=c,this.priority=DEFAULT_PRIORITY,this.constraints=new Array(0),this.maxCalls=DEFAULT_DISPOSEAFTER,this.onHandled=NO_OP,this.context=null,postal.configuration.bus.publish({channel:SYSTEM_CHANNEL,topic:"subscription.created",timeStamp:new Date,data:{event:"subscription.created",channel:a,topic:b}}),postal.configuration.bus.subscribe(this)};SubscriptionDefinition.prototype={unsubscribe:function(){postal.configuration.bus.unsubscribe(this),postal.configuration.bus.publish({channel:SYSTEM_CHANNEL,topic:"subscription.removed",timeStamp:new Date,data:{event:"subscription.removed",channel:this.channel,topic:this.topic}})},defer:function(){var a=this.callback;return this.callback=function(b){setTimeout(a,0,b)},this},disposeAfter:function(a){if(_.isNaN(a)||a<=0)throw"The value provided to disposeAfter (maxCalls) must be a number greater than zero.";var b=this.onHandled,c=_.after(a,_.bind(function(){this.unsubscribe(this)},this));return this.onHandled=function(){b.apply(this.context,arguments),c()},this},ignoreDuplicates:function(){return this.withConstraint(new DistinctPredicate),this},withConstraint:function(a){if(!_.isFunction(a))throw"Predicate constraint must be a function";return this.constraints.push(a),this},withConstraints:function(a){var b=this;return _.isArray(a)&&_.each(a,function(a){b.withConstraint(a)}),b},withContext:function(a){return this.context=a,this},withDebounce:function(a){if(_.isNaN(a))throw"Milliseconds must be a number";var b=this.callback;return this.callback=_.debounce(b,a),this},withDelay:function(a){if(_.isNaN(a))throw"Milliseconds must be a number";var b=this.callback;return this.callback=function(c){setTimeout(function(){b(c)},a)},this},withPriority:function(a){if(_.isNaN(a))throw"Priority must be a number";return this.priority=a,this},withThrottle:function(a){if(_.isNaN(a))throw"Milliseconds must be a number";var b=this.callback;return this.callback=_.throttle(b,a),this},subscribe:function(a){return this.callback=a,this}};var bindingsResolver={cache:{},compare:function(a,b){if(this.cache[b]&&this.cache[b][a])return!0;var c=new RegExp("^"+a.replace(/\./g,"\\.").replace(/\*/g,".*").replace(/#/g,"[A-Z,a-z,0-9]*")+"$"),d=c.test(b);return d&&(this.cache[b]||(this.cache[b]={}),this.cache[b][a]=!0),d},reset:function(){this.cache={}}},localBus={addWireTap:function(a){var b=this;return b.wireTaps.push(a),function(){var c=b.wireTaps.indexOf(a);c!==-1&&b.wireTaps.splice(c,1)}},publish:function(a){_.each(this.wireTaps,function(b){b(a.data,a)}),_.each(this.subscriptions[a.channel],function(b){_.each(b,function(b){postal.configuration.resolver.compare(b.topic,a.topic)&&_.all(b.constraints,function(b){return b(a.data,a)})&&typeof b.callback=="function"&&(b.callback.apply(b.context,[a.data,a]),b.onHandled())})})},reset:function(){this.subscriptions&&(_.each(this.subscriptions,function(a){_.each(a,function(a){while(a.length)a.pop().unsubscribe()})}),this.subscriptions={})},subscribe:function(a){var b,c,d,e=this.subscriptions[a.channel],f;e||(e=this.subscriptions[a.channel]={}),f=this.subscriptions[a.channel][a.topic],f||(f=this.subscriptions[a.channel][a.topic]=new Array(0)),b=f.length-1;for(;b>=0;b--)if(f[b].priority<=a.priority){f.splice(b+1,0,a),c=!0;break}return c||f.unshift(a),a},subscriptions:{},wireTaps:new Array(0),unsubscribe:function(a){if(this.subscriptions[a.channel][a.topic]){var b=this.subscriptions[a.channel][a.topic].length,c=0;for(;c= 0; idx-- ) { + if ( subs[idx].priority <= subDef.priority ) { + subs.splice( idx + 1, 0, subDef ); + found = true; + break; + } + } + if ( !found ) { + subs.unshift( subDef ); + } + return subDef; + }, + + subscriptions : {}, + + wireTaps : new Array( 0 ), + + unsubscribe : function ( config ) { + if ( this.subscriptions[config.channel][config.topic] ) { + var len = this.subscriptions[config.channel][config.topic].length, + idx = 0; + for ( ; idx < len; idx++ ) { + if ( this.subscriptions[config.channel][config.topic][idx] === config ) { + this.subscriptions[config.channel][config.topic].splice( idx, 1 ); + break; + } + } + } + } +}; + +var publishPicker = { + "1" : function ( envelope ) { + if ( !envelope ) { + throw new Error( "publishing from the 'global' postal.publish call requires a valid envelope." ); + } + envelope.channel = envelope.channel || DEFAULT_CHANNEL; + envelope.timeStamp = new Date(); + postal.configuration.bus.publish( envelope ); + return envelope; + }, + "2" : function ( topic, data ) { + var envelope = { channel : DEFAULT_CHANNEL, topic : topic, timeStamp : new Date(), data : data }; + postal.configuration.bus.publish( envelope ); + return envelope; + }, + "3" : function ( channel, topic, data ) { + var envelope = { channel : channel, topic : topic, timeStamp : new Date(), data : data }; + postal.configuration.bus.publish( envelope ); + return envelope; + } + }, + channelPicker = { + "1" : function ( chn ) { + var channel = chn, topic, options = {}; + if ( Object.prototype.toString.call( channel ) === "[object String]" ) { + channel = DEFAULT_CHANNEL; + topic = chn; + } + else { + channel = chn.channel || DEFAULT_CHANNEL; + topic = chn.topic; + options = chn.options || options; + } + return new postal.channelTypes[ options.type || "local" ]( channel, topic ); + }, + "2" : function ( chn, tpc ) { + var channel = chn, topic = tpc, options = {}; + if ( Object.prototype.toString.call( tpc ) === "[object Object]" ) { + channel = DEFAULT_CHANNEL; + topic = chn; + options = tpc; + } + return new postal.channelTypes[ options.type || "local" ]( channel, topic ); + }, + "3" : function ( channel, topic, options ) { + return new postal.channelTypes[ options.type || "local" ]( channel, topic ); + } + }, + sessionInfo = {}; + +// save some setup time, albeit tiny +localBus.subscriptions[SYSTEM_CHANNEL] = {}; + +var postal = { + configuration : { + bus : localBus, + resolver : bindingsResolver, + DEFAULT_CHANNEL : DEFAULT_CHANNEL, + DEFAULT_PRIORITY : DEFAULT_PRIORITY, + DEFAULT_DISPOSEAFTER : DEFAULT_DISPOSEAFTER, + SYSTEM_CHANNEL : SYSTEM_CHANNEL + }, + + channelTypes : { + local : ChannelDefinition + }, + + channel : function () { + var len = arguments.length; + if ( channelPicker[len] ) { + return channelPicker[len].apply( this, arguments ); + } + }, + + subscribe : function ( options ) { + var callback = options.callback, + topic = options.topic, + channel = options.channel || DEFAULT_CHANNEL; + return new SubscriptionDefinition( channel, topic, callback ); + }, + + publish : function () { + var len = arguments.length; + if ( publishPicker[len] ) { + return publishPicker[len].apply( this, arguments ); + } + }, + + addWireTap : function ( callback ) { + return this.configuration.bus.addWireTap( callback ); + }, + + linkChannels : function ( sources, destinations ) { + var result = []; + if ( !_.isArray( sources ) ) { + sources = [sources]; + } + if ( !_.isArray( destinations ) ) { + destinations = [destinations]; + } + _.each( sources, function ( source ) { + var sourceTopic = source.topic || "*"; + _.each( destinations, function ( destination ) { + var destChannel = destination.channel || DEFAULT_CHANNEL; + result.push( + postal.subscribe( { + channel : source.channel || DEFAULT_CHANNEL, + topic : source.topic || "*", + callback : function ( data, env ) { + var newEnv = env; + newEnv.topic = _.isFunction( destination.topic ) ? destination.topic( env.topic ) : destination.topic || env.topic; + newEnv.channel = destChannel; + newEnv.data = data; + postal.publish( newEnv ); + } + } ) + ); + } ); + } ); + return result; + }, + + utils : { + getSubscribersFor : function () { + var channel = arguments[ 0 ], + tpc = arguments[ 1 ], + result = []; + if ( arguments.length === 1 ) { + if ( Object.prototype.toString.call( channel ) === "[object String]" ) { + channel = postal.configuration.DEFAULT_CHANNEL; + tpc = arguments[ 0 ]; + } + else { + channel = arguments[ 0 ].channel || postal.configuration.DEFAULT_CHANNEL; + tpc = arguments[ 0 ].topic; + } + } + if ( postal.configuration.bus.subscriptions[ channel ] && + postal.configuration.bus.subscriptions[ channel ].hasOwnProperty( tpc ) ) { + result = postal.configuration.bus.subscriptions[ channel ][ tpc ]; + } + return result; + }, + + reset : function () { + postal.configuration.bus.reset(); + postal.configuration.resolver.reset(); + } + } +}; + + global.postal = postal; + +})( _, window ); \ No newline at end of file diff --git a/lib/postal.standard.min.js b/lib/postal.standard.min.js new file mode 100644 index 0000000..a03bee4 --- /dev/null +++ b/lib/postal.standard.min.js @@ -0,0 +1 @@ +(function(a,b,c){var d="/",e=50,f=0,g="postal",h=function(){},i=function(){var b;return function(c){var d=!1;return a.isString(c)?(d=c===b,b=c):(d=a.isEqual(c,b),b=a.clone(c)),!d}},j=function(a,b){this.channel=a||d,this._topic=b||""};j.prototype={subscribe:function(){var a=arguments.length;if(a===1)return new k(this.channel,this._topic,arguments[0]);if(a===2)return new k(this.channel,arguments[0],arguments[1])},publish:function(a){var b=a||{},c={channel:this.channel,topic:this._topic,data:b};return b.topic&&b.data&&(c=b,c.channel=c.channel||this.channel),c.timeStamp=new Date,q.configuration.bus.publish(c),c},topic:function(a){return a===this._topic?this:new j(this.channel,a)}};var k=function(a,b,c){this.channel=a,this.topic=b,this.callback=c,this.priority=e,this.constraints=new Array(0),this.maxCalls=f,this.onHandled=h,this.context=null,q.configuration.bus.publish({channel:g,topic:"subscription.created",timeStamp:new Date,data:{event:"subscription.created",channel:a,topic:b}}),q.configuration.bus.subscribe(this)};k.prototype={unsubscribe:function(){q.configuration.bus.unsubscribe(this),q.configuration.bus.publish({channel:g,topic:"subscription.removed",timeStamp:new Date,data:{event:"subscription.removed",channel:this.channel,topic:this.topic}})},defer:function(){var a=this.callback;return this.callback=function(b){setTimeout(a,0,b)},this},disposeAfter:function(b){if(a.isNaN(b)||b<=0)throw"The value provided to disposeAfter (maxCalls) must be a number greater than zero.";var c=this.onHandled,d=a.after(b,a.bind(function(){this.unsubscribe(this)},this));return this.onHandled=function(){c.apply(this.context,arguments),d()},this},ignoreDuplicates:function(){return this.withConstraint(new i),this},withConstraint:function(b){if(!a.isFunction(b))throw"Predicate constraint must be a function";return this.constraints.push(b),this},withConstraints:function(b){var c=this;return a.isArray(b)&&a.each(b,function(a){c.withConstraint(a)}),c},withContext:function(a){return this.context=a,this},withDebounce:function(b){if(a.isNaN(b))throw"Milliseconds must be a number";var c=this.callback;return this.callback=a.debounce(c,b),this},withDelay:function(b){if(a.isNaN(b))throw"Milliseconds must be a number";var c=this.callback;return this.callback=function(a){setTimeout(function(){c(a)},b)},this},withPriority:function(b){if(a.isNaN(b))throw"Priority must be a number";return this.priority=b,this},withThrottle:function(b){if(a.isNaN(b))throw"Milliseconds must be a number";var c=this.callback;return this.callback=a.throttle(c,b),this},subscribe:function(a){return this.callback=a,this}};var l={cache:{},compare:function(a,b){if(this.cache[b]&&this.cache[b][a])return!0;var c=new RegExp("^"+a.replace(/\./g,"\\.").replace(/\*/g,".*").replace(/#/g,"[A-Z,a-z,0-9]*")+"$"),d=c.test(b);return d&&(this.cache[b]||(this.cache[b]={}),this.cache[b][a]=!0),d},reset:function(){this.cache={}}},m={addWireTap:function(a){var b=this;return b.wireTaps.push(a),function(){var c=b.wireTaps.indexOf(a);c!==-1&&b.wireTaps.splice(c,1)}},publish:function(b){a.each(this.wireTaps,function(a){a(b.data,b)}),a.each(this.subscriptions[b.channel],function(c){a.each(c,function(c){q.configuration.resolver.compare(c.topic,b.topic)&&a.all(c.constraints,function(a){return a(b.data,b)})&&typeof c.callback=="function"&&(c.callback.apply(c.context,[b.data,b]),c.onHandled())})})},reset:function(){this.subscriptions&&(a.each(this.subscriptions,function(b){a.each(b,function(a){while(a.length)a.pop().unsubscribe()})}),this.subscriptions={})},subscribe:function(a){var b,c,d,e=this.subscriptions[a.channel],f;e||(e=this.subscriptions[a.channel]={}),f=this.subscriptions[a.channel][a.topic],f||(f=this.subscriptions[a.channel][a.topic]=new Array(0)),b=f.length-1;for(;b>=0;b--)if(f[b].priority<=a.priority){f.splice(b+1,0,a),c=!0;break}return c||f.unshift(a),a},subscriptions:{},wireTaps:new Array(0),unsubscribe:function(a){if(this.subscriptions[a.channel][a.topic]){var b=this.subscriptions[a.channel][a.topic].length,c=0;for(;c