Fixed bug where data-less publishes were breaking

This commit is contained in:
Jim Cowart 2012-05-22 00:34:01 -04:00
parent d80554aeba
commit 15a5b11d48
15 changed files with 1718 additions and 5 deletions

View file

@ -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",

481
lib/postal.amd.js Normal file
View file

@ -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;
} );

1
lib/postal.amd.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,89 @@
// This is the amd module version of postal.diagnostics.js
// If you need the standard lib version, go to http://github.com/ifandelse/postal.js
define( [ "postal", "underscore" ], function ( postal, _, undefined ) {
var filters = [],
applyFilter = function ( filter, env ) {
var match = 0, possible = 0;
_.each( filter, function ( item, key ) {
if ( env[key] ) {
possible++;
if ( _.isRegExp( item ) && item.test( env[key] ) ) {
match++;
}
else if ( _.isObject( env[key] ) && !_.isArray( env[key] ) ) {
if ( applyFilter( item, env[key] ) ) {
match++;
}
}
else {
if ( _.isEqual( env[key], item ) ) {
match++;
}
}
}
} );
return match === possible;
};
// this returns a callback that, if invoked, removes the wireTap
var wireTap = postal.addWireTap( function ( data, envelope ) {
if ( !filters.length || _.any( filters, function ( filter ) {
return applyFilter( filter, envelope );
} ) ) {
if ( !JSON ) {
throw "This browser or environment does not provide JSON support";
}
try {
console.log( JSON.stringify( envelope ) );
}
catch ( exception ) {
try {
var env = _.extend( {}, envelope );
delete env.data;
console.log( JSON.stringify( env ) + "\n\t" + "JSON.stringify Error: " + exception.message );
}
catch ( ex ) {
console.log( "Unable to parse data to JSON: " + exception );
}
}
}
} );
postal.diagnostics = postal.diagnostics || {};
postal.diagnostics.console = {
clearFilters : function () {
filters = [];
},
removeFilter : function ( filter ) {
filters = _.filter( filters, function ( item ) {
return !_.isEqual( item, filter );
} );
},
addFilter : function ( constraint ) {
if ( !_.isArray( constraint ) ) {
constraint = [ constraint ];
}
_.each( constraint, function ( item ) {
if ( filters.length === 0 || !_.any( filters, function ( filter ) {
return _.isEqual( filter, item );
} ) ) {
filters.push( item );
}
} );
},
getCurrentFilters : function () {
return filters;
},
removeWireTap : function () {
if ( wireTap ) {
wireTap();
}
}
};
} );

1
lib/postal.diagnostics.amd.min.js vendored Normal file
View file

@ -0,0 +1 @@
define(["postal","underscore"],function(a,b,c){var d=[],e=function(a,c){var d=0,f=0;return b.each(a,function(a,g){c[g]&&(f++,b.isRegExp(a)&&a.test(c[g])?d++:b.isObject(c[g])&&!b.isArray(c[g])?e(a,c[g])&&d++:b.isEqual(c[g],a)&&d++)}),d===f},f=a.addWireTap(function(a,c){if(!d.length||b.any(d,function(a){return e(a,c)})){if(!JSON)throw"This browser or environment does not provide JSON support";try{console.log(JSON.stringify(c))}catch(f){try{var g=b.extend({},c);delete g.data,console.log(JSON.stringify(g)+"\n "+"JSON.stringify Error: "+f.message)}catch(h){console.log("Unable to parse data to JSON: "+f)}}}});a.diagnostics=a.diagnostics||{},a.diagnostics.console={clearFilters:function(){d=[]},removeFilter:function(a){d=b.filter(d,function(c){return!b.isEqual(c,a)})},addFilter:function(a){b.isArray(a)||(a=[a]),b.each(a,function(a){(d.length===0||!b.any(d,function(c){return b.isEqual(c,a)}))&&d.push(a)})},getCurrentFilters:function(){return d},removeWireTap:function(){f&&f()}}})

View file

@ -0,0 +1,85 @@
module.exports = function ( _, postal ) {
var filters = [],
applyFilter = function ( filter, env ) {
var match = 0, possible = 0;
_.each( filter, function ( item, key ) {
if ( env[key] ) {
possible++;
if ( _.isRegExp( item ) && item.test( env[key] ) ) {
match++;
}
else if ( _.isObject( env[key] ) && !_.isArray( env[key] ) ) {
if ( applyFilter( item, env[key] ) ) {
match++;
}
}
else {
if ( _.isEqual( env[key], item ) ) {
match++;
}
}
}
} );
return match === possible;
};
// this returns a callback that, if invoked, removes the wireTap
var wireTap = postal.addWireTap( function ( data, envelope ) {
if ( !filters.length || _.any( filters, function ( filter ) {
return applyFilter( filter, envelope );
} ) ) {
if ( !JSON ) {
throw "This browser or environment does not provide JSON support";
}
try {
console.log( JSON.stringify( envelope ) );
}
catch ( exception ) {
try {
var env = _.extend( {}, envelope );
delete env.data;
console.log( JSON.stringify( env ) + "\n\t" + "JSON.stringify Error: " + exception.message );
}
catch ( ex ) {
console.log( "Unable to parse data to JSON: " + exception );
}
}
}
} );
postal.diagnostics = postal.diagnostics || {};
postal.diagnostics.console = {
clearFilters : function () {
filters = [];
},
removeFilter : function ( filter ) {
filters = _.filter( filters, function ( item ) {
return !_.isEqual( item, filter );
} );
},
addFilter : function ( constraint ) {
if ( !_.isArray( constraint ) ) {
constraint = [ constraint ];
}
_.each( constraint, function ( item ) {
if ( filters.length === 0 || !_.any( filters, function ( filter ) {
return _.isEqual( filter, item );
} ) ) {
filters.push( item );
}
} );
},
getCurrentFilters : function () {
return filters;
},
removeWireTap : function () {
if ( wireTap ) {
wireTap();
}
}
};
};

1
lib/postal.diagnostics.node.min.js vendored Normal file
View file

@ -0,0 +1 @@
module.exports=function(a,b){var c=[],d=function(b,c){var e=0,f=0;return a.each(b,function(b,g){c[g]&&(f++,a.isRegExp(b)&&b.test(c[g])?e++:a.isObject(c[g])&&!a.isArray(c[g])?d(b,c[g])&&e++:a.isEqual(c[g],b)&&e++)}),e===f},e=b.addWireTap(function(b,e){if(!c.length||a.any(c,function(a){return d(a,e)})){if(!JSON)throw"This browser or environment does not provide JSON support";try{console.log(JSON.stringify(e))}catch(f){try{var g=a.extend({},e);delete g.data,console.log(JSON.stringify(g)+"\n "+"JSON.stringify Error: "+f.message)}catch(h){console.log("Unable to parse data to JSON: "+f)}}}});b.diagnostics=b.diagnostics||{},b.diagnostics.console={clearFilters:function(){c=[]},removeFilter:function(b){c=a.filter(c,function(c){return!a.isEqual(c,b)})},addFilter:function(b){a.isArray(b)||(b=[b]),a.each(b,function(b){(c.length===0||!a.any(c,function(c){return a.isEqual(c,b)}))&&c.push(b)})},getCurrentFilters:function(){return c},removeWireTap:function(){e&&e()}}}

View file

@ -0,0 +1,89 @@
// This is the standard lib version of postal.diagnostics.js
// If you need the amd-module style version, go to http://github.com/ifandelse/postal.js
(function ( postal, _, undefined ) {
var filters = [],
applyFilter = function ( filter, env ) {
var match = 0, possible = 0;
_.each( filter, function ( item, key ) {
if ( env[key] ) {
possible++;
if ( _.isRegExp( item ) && item.test( env[key] ) ) {
match++;
}
else if ( _.isObject( env[key] ) && !_.isArray( env[key] ) ) {
if ( applyFilter( item, env[key] ) ) {
match++;
}
}
else {
if ( _.isEqual( env[key], item ) ) {
match++;
}
}
}
} );
return match === possible;
};
// this returns a callback that, if invoked, removes the wireTap
var wireTap = postal.addWireTap( function ( data, envelope ) {
if ( !filters.length || _.any( filters, function ( filter ) {
return applyFilter( filter, envelope );
} ) ) {
if ( !JSON ) {
throw "This browser or environment does not provide JSON support";
}
try {
console.log( JSON.stringify( envelope ) );
}
catch ( exception ) {
try {
var env = _.extend( {}, envelope );
delete env.data;
console.log( JSON.stringify( env ) + "\n\t" + "JSON.stringify Error: " + exception.message );
}
catch ( ex ) {
console.log( "Unable to parse data to JSON: " + exception );
}
}
}
} );
postal.diagnostics = postal.diagnostics || {};
postal.diagnostics.console = {
clearFilters : function () {
filters = [];
},
removeFilter : function ( filter ) {
filters = _.filter( filters, function ( item ) {
return !_.isEqual( item, filter );
} );
},
addFilter : function ( constraint ) {
if ( !_.isArray( constraint ) ) {
constraint = [ constraint ];
}
_.each( constraint, function ( item ) {
if ( filters.length === 0 || !_.any( filters, function ( filter ) {
return _.isEqual( filter, item );
} ) ) {
filters.push( item );
}
} );
},
getCurrentFilters : function () {
return filters;
},
removeWireTap : function () {
if ( wireTap ) {
wireTap();
}
}
};
})( postal, _ );

View file

@ -0,0 +1 @@
(function(a,b,c){var d=[],e=function(a,c){var d=0,f=0;return b.each(a,function(a,g){c[g]&&(f++,b.isRegExp(a)&&a.test(c[g])?d++:b.isObject(c[g])&&!b.isArray(c[g])?e(a,c[g])&&d++:b.isEqual(c[g],a)&&d++)}),d===f},f=a.addWireTap(function(a,c){if(!d.length||b.any(d,function(a){return e(a,c)})){if(!JSON)throw"This browser or environment does not provide JSON support";try{console.log(JSON.stringify(c))}catch(f){try{var g=b.extend({},c);delete g.data,console.log(JSON.stringify(g)+"\n "+"JSON.stringify Error: "+f.message)}catch(h){console.log("Unable to parse data to JSON: "+f)}}}});a.diagnostics=a.diagnostics||{},a.diagnostics.console={clearFilters:function(){d=[]},removeFilter:function(a){d=b.filter(d,function(c){return!b.isEqual(c,a)})},addFilter:function(a){b.isArray(a)||(a=[a]),b.each(a,function(a){(d.length===0||!b.any(d,function(c){return b.isEqual(c,a)}))&&d.push(a)})},getCurrentFilters:function(){return d},removeWireTap:function(){f&&f()}}})(postal,_)

480
lib/postal.node.js Normal file
View file

@ -0,0 +1,480 @@
/*
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 node.js version of postal.js
// If you need the standard or amd client lib version, go to http://github.com/ifandelse/postal.js
var _ = require( 'underscore' );
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();
}
}
};
module.exports = postal;

1
lib/postal.node.min.js vendored Normal file

File diff suppressed because one or more lines are too long

482
lib/postal.standard.js Normal file
View file

@ -0,0 +1,482 @@
/*
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 standard lib version of postal.js
// If you need the amd-module style version, go to http://github.com/ifandelse/postal.js
(function ( _, global, 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();
}
}
};
global.postal = postal;
})( _, window );

1
lib/postal.standard.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View file

@ -15,14 +15,15 @@ ChannelDefinition.prototype = {
},
publish : function ( obj ) {
var _obj = obj || {};
var envelope = {
channel : this.channel,
topic : this._topic,
data : obj || {}
data : _obj
};
// If this is an envelope....
if ( obj.topic && obj.data ) {
envelope = obj;
if ( _obj.topic && _obj.data ) {
envelope = _obj;
envelope.channel = envelope.channel || this.channel;
}
envelope.timeStamp = new Date();

View file

@ -2,5 +2,5 @@
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.0
Version 0.6.1
*/