From 72388c6ae8f008a452e82f28de3f3f49c75544ad Mon Sep 17 00:00:00 2001 From: ifandelse Date: Sat, 25 Jan 2014 01:27:33 -0500 Subject: [PATCH] Added strategy/pipeline behavior --- lib/postal.js | 207 +++++++++++++++++++++------- lib/postal.min.js | 2 +- spec/postaljs.spec.js | 85 ++++-------- spec/subscriptionDefinition.spec.js | 38 ++--- spec/utils.spec.js | 3 +- src/Api.js | 3 +- src/SubscriptionDefinition.js | 170 ++++++++++------------- src/postal.js | 4 +- src/strategies.js | 66 +++++++++ src/strategy.js | 68 +++++++++ 10 files changed, 408 insertions(+), 238 deletions(-) create mode 100644 src/strategies.js create mode 100644 src/strategy.js diff --git a/lib/postal.js b/lib/postal.js index 5ed58e0..1b993e3 100644 --- a/lib/postal.js +++ b/lib/postal.js @@ -9,10 +9,7 @@ (function (root, factory) { if (typeof module === "object" && module.exports) { // Node, or CommonJS-Like environments - module.exports = function (_) { - _ = _ || require("underscore"); - return factory(_, this); - }; + module.exports = factory(require("underscore"), this); } else if (typeof define === "function" && define.amd) { // AMD. Register as an anonymous module. define(["underscore"], function (_) { @@ -27,6 +24,138 @@ var postal; var prevPostal = global.postal; + var Strategy = function (options) { + var _target = options.owner[options.prop]; + if (typeof _target !== "function") { + throw new Error("Strategies can only target methods."); + } + var _strategies = []; + var _context = options.context || options.owner; + var strategy = function () { + var idx = 0; + var next = function next() { + var args = Array.prototype.slice.call(arguments, 0); + var thisIdx = idx; + var strategy; + idx += 1; + if (thisIdx < _strategies.length) { + strategy = _strategies[thisIdx]; + strategy.fn.apply(strategy.context || _context, [next].concat(args)); + } else { + _target.apply(_context, args); + } + }; + next.apply(this, arguments); + }; + strategy.target = function () { + return _target; + }; + strategy.context = function (ctx) { + if (arguments.length === 0) { + return _context; + } else { + _context = ctx; + } + }; + strategy.strategies = function () { + return _strategies; + }; + strategy.useStrategy = function (strategy) { + var idx = 0, + exists = false; + while (idx < _strategies.length) { + if (_strategies[idx].name === strategy.name) { + _strategies[idx] = strategy; + exists = true; + break; + } + idx += 1; + } + if (!exists) { + _strategies.push(strategy); + } + }; + strategy.reset = function () { + _strategies = []; + }; + if (options.lazyInit) { + _target.useStrategy = function () { + options.owner[options.prop] = strategy; + strategy.useStrategy.apply(strategy, arguments); + }; + _target.context = function () { + options.owner[options.prop] = strategy; + return strategy.context.apply(strategy, arguments); + }; + return _target; + } else { + return strategy; + } + }; + + var strats = { + setTimeout: function (ms) { + return { + name: "setTimeout", + fn: function (next, data, envelope) { + setTimeout(function () { + next(data, envelope); + }, ms); + } + }; + }, + after: function (maxCalls, callback) { + var dispose = _.after(maxCalls, callback); + return { + name: "after", + fn: function (next, data, envelope) { + dispose(); + next(data, envelope); + } + }; + }, + throttle: function (ms) { + return { + name: "throttle", + fn: _.throttle(function (next, data, envelope) { + next(data, envelope); + }, ms) + }; + }, + debounce: function (ms, immediate) { + return { + name: "debounce", + fn: _.debounce(function (next, data, envelope) { + next(data, envelope); + }, ms, !! immediate) + }; + }, + predicate: function (pred) { + return { + name: "predicate", + fn: function (next, data, envelope) { + if (pred.call(this, data, envelope)) { + next.call(this, data, envelope); + } + } + }; + }, + distinct: function (options) { + options = options || {}; + var accessor = function (args) { + return args[0]; + }; + var check = options.all ? new DistinctPredicate(accessor) : new ConsecutiveDistinctPredicate(accessor); + return { + name: "distinct", + fn: function (next, data, envelope) { + if (check(data)) { + next(data, envelope); + } + } + }; + } + }; var ConsecutiveDistinctPredicate = function () { var previous; @@ -90,9 +219,7 @@ } this.channel = channel; this.topic = topic; - this.callback = callback; - this.constraints = []; - this.context = null; + this.subscribe(callback); }; SubscriptionDefinition.prototype = { @@ -104,13 +231,7 @@ }, defer: function () { - var self = this; - var fn = this.callback; - this.callback = function (data, env) { - setTimeout(function () { - fn.call(self.context, data, env); - }, 0); - }; + this.callback.useStrategy(postal.configuration.strategies.setTimeout(0)); return this; }, @@ -119,25 +240,21 @@ throw "The value provided to disposeAfter (maxCalls) must be a number greater than zero."; } var self = this; - var fn = this.callback; - var dispose = _.after(maxCalls, _.bind(function () { - this.unsubscribe(); - }, this)); - - this.callback = function () { - fn.apply(self.context, arguments); - dispose(); - }; - return this; + self.callback.useStrategy(postal.configuration.strategies.after(maxCalls, function () { + self.unsubscribe.call(self); + })); + return self; }, distinctUntilChanged: function () { - this.withConstraint(new ConsecutiveDistinctPredicate()); + this.callback.useStrategy(postal.configuration.strategies.distinct()); return this; }, distinct: function () { - this.withConstraint(new DistinctPredicate()); + this.callback.useStrategy(postal.configuration.strategies.distinct({ + all: true + })); return this; }, @@ -150,22 +267,12 @@ if (!_.isFunction(predicate)) { throw "Predicate constraint must be a function"; } - this.constraints.push(predicate); + this.callback.useStrategy(postal.configuration.strategies.predicate(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; + this.callback.context(context); return this; }, @@ -182,28 +289,27 @@ if (_.isNaN(milliseconds)) { throw "Milliseconds must be a number"; } - var self = this; - var fn = this.callback; - this.callback = function (data, env) { - setTimeout(function () { - fn.call(self.context, data, env); - }, milliseconds); - }; + this.callback.useStrategy(postal.configuration.strategies.setTimeout(milliseconds)); return this; }, - withThrottle: function (milliseconds, options) { - options = options || {}; + withThrottle: function (milliseconds) { if (_.isNaN(milliseconds)) { throw "Milliseconds must be a number"; } - var fn = this.callback; - this.callback = _.throttle(fn, milliseconds, options); + this.callback.useStrategy(postal.configuration.strategies.throttle(milliseconds)); return this; }, subscribe: function (callback) { this.callback = callback; + this.callback = new Strategy({ + owner: this, + prop: "callback", + context: this, + // TODO: is this the best option? + lazyInit: true + }); return this; } }; @@ -358,7 +464,8 @@ bus: localBus, resolver: bindingsResolver, DEFAULT_CHANNEL: "/", - SYSTEM_CHANNEL: "postal" + SYSTEM_CHANNEL: "postal", + strategies: strats }, ChannelDefinition: ChannelDefinition, diff --git a/lib/postal.min.js b/lib/postal.min.js index 9474c60..7020f00 100644 --- a/lib/postal.min.js +++ b/lib/postal.min.js @@ -5,4 +5,4 @@ * Url: http://github.com/postaljs/postal.js * License(s): MIT, GPL */ -(function(n,t){"object"==typeof module&&module.exports?module.exports=function(n){return n=n||require("underscore"),t(n,this)}:"function"==typeof define&&define.amd?define(["underscore"],function(i){return t(i,n)}):n.postal=t(n._,n)})(this,function(n,t){var i,s=t.postal,e=function(){var t;return function(i){var s=!1;return n.isString(i)?(s=i===t,t=i):(s=n.isEqual(i,t),t=n.clone(i)),!s}},c=function(){var t=[];return function(i){var s=!n.any(t,function(t){return n.isObject(i)||n.isArray(i)?n.isEqual(i,t):i===t});return s&&t.push(i),s}},r=function(n){this.channel=n||i.configuration.DEFAULT_CHANNEL};r.prototype.subscribe=function(){return i.subscribe(1===arguments.length?new o(this.channel,arguments[0].topic,arguments[0].callback):new o(this.channel,arguments[0],arguments[1]))},r.prototype.publish=function(){var n=1===arguments.length?"[object String]"===Object.prototype.toString.call(arguments[0])?{topic:arguments[0]}:arguments[0]:{topic:arguments[0],data:arguments[1]};return n.channel=this.channel,i.configuration.bus.publish(n)};var o=function(n,t,i){if(3!==arguments.length)throw new Error("You must provide a channel, topic and callback when creating a SubscriptionDefinition instance.");if(0===t.length)throw new Error("Topics cannot be empty");this.channel=n,this.topic=t,this.callback=i,this.constraints=[],this.context=null};o.prototype={unsubscribe:function(){this.inactive||(this.inactive=!0,i.unsubscribe(this))},defer:function(){var n=this,t=this.callback;return this.callback=function(i,s){setTimeout(function(){t.call(n.context,i,s)},0)},this},disposeAfter:function(t){if(n.isNaN(t)||0>=t)throw"The value provided to disposeAfter (maxCalls) must be a number greater than zero.";var i=this,s=this.callback,e=n.after(t,n.bind(function(){this.unsubscribe()},this));return this.callback=function(){s.apply(i.context,arguments),e()},this},distinctUntilChanged:function(){return this.withConstraint(new e),this},distinct:function(){return this.withConstraint(new c),this},once:function(){return this.disposeAfter(1),this},withConstraint:function(t){if(!n.isFunction(t))throw"Predicate constraint must be a function";return this.constraints.push(t),this},withConstraints:function(t){var i=this;return n.isArray(t)&&n.each(t,function(n){i.withConstraint(n)}),i},withContext:function(n){return this.context=n,this},withDebounce:function(t,i){if(n.isNaN(t))throw"Milliseconds must be a number";var s=this.callback;return this.callback=n.debounce(s,t,!!i),this},withDelay:function(t){if(n.isNaN(t))throw"Milliseconds must be a number";var i=this,s=this.callback;return this.callback=function(n,e){setTimeout(function(){s.call(i.context,n,e)},t)},this},withThrottle:function(t,i){if(i=i||{},n.isNaN(t))throw"Milliseconds must be a number";var s=this.callback;return this.callback=n.throttle(s,t,i),this},subscribe:function(n){return this.callback=n,this}};var a={cache:{},regex:{},compare:function(t,i){var s,e,c,r=this.cache[i]&&this.cache[i][t];return"undefined"!=typeof r?r:((e=this.regex[t])||(s="^"+n.map(t.split("."),function(n){var t="";return c&&(t="#"!==c?"\\.\\b":"\\b"),t+="#"===n?"[\\s\\S]*":"*"===n?"[^.]+":n,c=n,t}).join("")+"$",e=this.regex[t]=new RegExp(s)),this.cache[i]=this.cache[i]||{},this.cache[i][t]=r=e.test(i),r)},reset:function(){this.cache={},this.regex={}}},u=function(t,s){!t.inactive&&i.configuration.resolver.compare(t.topic,s.topic)&&n.all(t.constraints,function(n){return n.call(t.context,s.data,s)})&&"function"==typeof t.callback&&t.callback.call(t.context,s.data,s)},h=0,l=[],f=function(){for(;l.length;)p.unsubscribe(l.shift())},p={addWireTap:function(n){var t=this;return t.wireTaps.push(n),function(){var i=t.wireTaps.indexOf(n);-1!==i&&t.wireTaps.splice(i,1)}},publish:function(t){return++h,t.timeStamp=new Date,n.each(this.wireTaps,function(n){n(t.data,t)}),this.subscriptions[t.channel]&&n.each(this.subscriptions[t.channel],function(n){for(var i,s=0,e=n.length;e>s;)(i=n[s++])&&u(i,t)}),0===--h&&f(),t},reset:function(){this.subscriptions&&(n.each(this.subscriptions,function(t){n.each(t,function(n){for(;n.length;)n.pop().unsubscribe()})}),this.subscriptions={})},subscribe:function(n){var t,i=this.subscriptions[n.channel];return i||(i=this.subscriptions[n.channel]={}),t=this.subscriptions[n.channel][n.topic],t||(t=this.subscriptions[n.channel][n.topic]=[]),t.push(n),n},subscriptions:{},wireTaps:[],unsubscribe:function(n){if(h)return void l.push(n);if(this.subscriptions[n.channel]&&this.subscriptions[n.channel][n.topic])for(var t=this.subscriptions[n.channel][n.topic].length,i=0;t>i;){if(this.subscriptions[n.channel][n.topic][i]===n){this.subscriptions[n.channel][n.topic].splice(i,1);break}i+=1}}};if(i={configuration:{bus:p,resolver:a,DEFAULT_CHANNEL:"/",SYSTEM_CHANNEL:"postal"},ChannelDefinition:r,SubscriptionDefinition:o,channel:function(n){return new r(n)},subscribe:function(n){var t=new o(n.channel||i.configuration.DEFAULT_CHANNEL,n.topic,n.callback);return i.configuration.bus.publish({channel:i.configuration.SYSTEM_CHANNEL,topic:"subscription.created",data:{event:"subscription.created",channel:t.channel,topic:t.topic}}),i.configuration.bus.subscribe(t)},publish:function(n){return n.channel=n.channel||i.configuration.DEFAULT_CHANNEL,i.configuration.bus.publish(n)},unsubscribe:function(n){i.configuration.bus.unsubscribe(n),i.configuration.bus.publish({channel:i.configuration.SYSTEM_CHANNEL,topic:"subscription.removed",data:{event:"subscription.removed",channel:n.channel,topic:n.topic}})},addWireTap:function(n){return this.configuration.bus.addWireTap(n)},linkChannels:function(t,s){var e=[];return t=n.isArray(t)?t:[t],s=n.isArray(s)?s:[s],n.each(t,function(t){var c=t.topic||"#";n.each(s,function(s){var r=s.channel||i.configuration.DEFAULT_CHANNEL;e.push(i.subscribe({channel:t.channel||i.configuration.DEFAULT_CHANNEL,topic:c,callback:function(t,e){var c=n.clone(e);c.topic=n.isFunction(s.topic)?s.topic(e.topic):s.topic||e.topic,c.channel=r,c.data=t,i.publish(c)}}))})}),e},noConflict:function(){if("undefined"==typeof window)throw new Error("noConflict can only be used in browser clients which aren't using AMD modules");return t.postal=s,this},utils:{getSubscribersFor:function(){var n=arguments[0],t=arguments[1];return 1===arguments.length&&(n=arguments[0].channel||i.configuration.DEFAULT_CHANNEL,t=arguments[0].topic),i.configuration.bus.subscriptions[n]&&Object.prototype.hasOwnProperty.call(i.configuration.bus.subscriptions[n],t)?i.configuration.bus.subscriptions[n][t]:[]},reset:function(){i.configuration.bus.reset(),i.configuration.resolver.reset()}}},p.subscriptions[i.configuration.SYSTEM_CHANNEL]={},t&&Object.prototype.hasOwnProperty.call(t,"__postalReady__")&&n.isArray(t.__postalReady__))for(;t.__postalReady__.length;)t.__postalReady__.shift().onReady(i);return i}); \ No newline at end of file +(function(t,n){"object"==typeof module&&module.exports?module.exports=n(require("underscore"),this):"function"==typeof define&&define.amd?define(["underscore"],function(i){return n(i,t)}):t.postal=n(t._,t)})(this,function(t,n){var i,e=n.postal,r=function(t){var n=t.owner[t.prop];if("function"!=typeof n)throw new Error("Strategies can only target methods.");var i=[],e=t.context||t.owner,r=function(){var t=0,r=function c(){var r,s=Array.prototype.slice.call(arguments,0),o=t;t+=1,o=n)throw"The value provided to disposeAfter (maxCalls) must be a number greater than zero.";var e=this;return e.callback.useStrategy(i.configuration.strategies.after(n,function(){e.unsubscribe.call(e)})),e},distinctUntilChanged:function(){return this.callback.useStrategy(i.configuration.strategies.distinct()),this},distinct:function(){return this.callback.useStrategy(i.configuration.strategies.distinct({all:!0})),this},once:function(){return this.disposeAfter(1),this},withConstraint:function(n){if(!t.isFunction(n))throw"Predicate constraint must be a function";return this.callback.useStrategy(i.configuration.strategies.predicate(n)),this},withContext:function(t){return this.callback.context(t),this},withDebounce:function(n,i){if(t.isNaN(n))throw"Milliseconds must be a number";var e=this.callback;return this.callback=t.debounce(e,n,!!i),this},withDelay:function(n){if(t.isNaN(n))throw"Milliseconds must be a number";return this.callback.useStrategy(i.configuration.strategies.setTimeout(n)),this},withThrottle:function(n){if(t.isNaN(n))throw"Milliseconds must be a number";return this.callback.useStrategy(i.configuration.strategies.throttle(n)),this},subscribe:function(t){return this.callback=t,this.callback=new r({owner:this,prop:"callback",context:this,lazyInit:!0}),this}};var h={cache:{},regex:{},compare:function(n,i){var e,r,c,s=this.cache[i]&&this.cache[i][n];return"undefined"!=typeof s?s:((r=this.regex[n])||(e="^"+t.map(n.split("."),function(t){var n="";return c&&(n="#"!==c?"\\.\\b":"\\b"),n+="#"===t?"[\\s\\S]*":"*"===t?"[^.]+":t,c=t,n}).join("")+"$",r=this.regex[n]=new RegExp(e)),this.cache[i]=this.cache[i]||{},this.cache[i][n]=s=r.test(i),s)},reset:function(){this.cache={},this.regex={}}},l=function(n,e){!n.inactive&&i.configuration.resolver.compare(n.topic,e.topic)&&t.all(n.constraints,function(t){return t.call(n.context,e.data,e)})&&"function"==typeof n.callback&&n.callback.call(n.context,e.data,e)},f=0,p=[],b=function(){for(;p.length;)g.unsubscribe(p.shift())},g={addWireTap:function(t){var n=this;return n.wireTaps.push(t),function(){var i=n.wireTaps.indexOf(t);-1!==i&&n.wireTaps.splice(i,1)}},publish:function(n){return++f,n.timeStamp=new Date,t.each(this.wireTaps,function(t){t(n.data,n)}),this.subscriptions[n.channel]&&t.each(this.subscriptions[n.channel],function(t){for(var i,e=0,r=t.length;r>e;)(i=t[e++])&&l(i,n)}),0===--f&&b(),n},reset:function(){this.subscriptions&&(t.each(this.subscriptions,function(n){t.each(n,function(t){for(;t.length;)t.pop().unsubscribe()})}),this.subscriptions={})},subscribe:function(t){var n,i=this.subscriptions[t.channel];return i||(i=this.subscriptions[t.channel]={}),n=this.subscriptions[t.channel][t.topic],n||(n=this.subscriptions[t.channel][t.topic]=[]),n.push(t),t},subscriptions:{},wireTaps:[],unsubscribe:function(t){if(f)return void p.push(t);if(this.subscriptions[t.channel]&&this.subscriptions[t.channel][t.topic])for(var n=this.subscriptions[t.channel][t.topic].length,i=0;n>i;){if(this.subscriptions[t.channel][t.topic][i]===t){this.subscriptions[t.channel][t.topic].splice(i,1);break}i+=1}}};if(i={configuration:{bus:g,resolver:h,DEFAULT_CHANNEL:"/",SYSTEM_CHANNEL:"postal",strategies:c},ChannelDefinition:a,SubscriptionDefinition:u,channel:function(t){return new a(t)},subscribe:function(t){var n=new u(t.channel||i.configuration.DEFAULT_CHANNEL,t.topic,t.callback);return i.configuration.bus.publish({channel:i.configuration.SYSTEM_CHANNEL,topic:"subscription.created",data:{event:"subscription.created",channel:n.channel,topic:n.topic}}),i.configuration.bus.subscribe(n)},publish:function(t){return t.channel=t.channel||i.configuration.DEFAULT_CHANNEL,i.configuration.bus.publish(t)},unsubscribe:function(t){i.configuration.bus.unsubscribe(t),i.configuration.bus.publish({channel:i.configuration.SYSTEM_CHANNEL,topic:"subscription.removed",data:{event:"subscription.removed",channel:t.channel,topic:t.topic}})},addWireTap:function(t){return this.configuration.bus.addWireTap(t)},linkChannels:function(n,e){var r=[];return n=t.isArray(n)?n:[n],e=t.isArray(e)?e:[e],t.each(n,function(n){var c=n.topic||"#";t.each(e,function(e){var s=e.channel||i.configuration.DEFAULT_CHANNEL;r.push(i.subscribe({channel:n.channel||i.configuration.DEFAULT_CHANNEL,topic:c,callback:function(n,r){var c=t.clone(r);c.topic=t.isFunction(e.topic)?e.topic(r.topic):e.topic||r.topic,c.channel=s,c.data=n,i.publish(c)}}))})}),r},noConflict:function(){if("undefined"==typeof window)throw new Error("noConflict can only be used in browser clients which aren't using AMD modules");return n.postal=e,this},utils:{getSubscribersFor:function(){var t=arguments[0],n=arguments[1];return 1===arguments.length&&(t=arguments[0].channel||i.configuration.DEFAULT_CHANNEL,n=arguments[0].topic),i.configuration.bus.subscriptions[t]&&Object.prototype.hasOwnProperty.call(i.configuration.bus.subscriptions[t],n)?i.configuration.bus.subscriptions[t][n]:[]},reset:function(){i.configuration.bus.reset(),i.configuration.resolver.reset()}}},g.subscriptions[i.configuration.SYSTEM_CHANNEL]={},n&&Object.prototype.hasOwnProperty.call(n,"__postalReady__")&&t.isArray(n.__postalReady__))for(;n.__postalReady__.length;)n.__postalReady__.shift().onReady(i);return i}); \ No newline at end of file diff --git a/spec/postaljs.spec.js b/spec/postaljs.spec.js index d76bf7e..cfa77cc 100644 --- a/spec/postaljs.spec.js +++ b/spec/postaljs.spec.js @@ -63,11 +63,8 @@ it( "should have set subscription topic value", function () { expect( sub.topic ).to.be( "MyTopic" ); } ); - it( "should have defaulted the subscription constraints array", function () { - expect( sub.constraints.length ).to.be( 0 ); - } ); it( "should have defaulted the subscription context value", function () { - expect( sub.context ).to.be( null ); + expect( sub.callback.context() ).to.be( sub ); } ); it( "should have captured subscription creation event", function () { expect( caughtSubscribeEvent ).to.be.ok(); @@ -92,8 +89,8 @@ postal.utils.reset(); subInvokedCnt = 0; } ); - it( "should have a constraint on the subscription", function () { - expect( postal.configuration.bus.subscriptions.MyChannel.MyTopic[0].constraints.length ).to.be( 1 ); + it( "callback should be a strategy", function () { + expect( typeof postal.configuration.bus.subscriptions.MyChannel.MyTopic[0].callback.context ).to.be( "function" ); } ); it( "subscription callback should be invoked once", function () { expect( subInvokedCnt ).to.be( 1 ); @@ -329,57 +326,28 @@ subscription = channel.subscribe( "MyTopic", function ( data ) { recvd = true; } ) - .withConstraints( [function () { + .withConstraint(function () { + return false; + }) + .withConstraint(function () { + return false; + }) + .withConstraint(function () { return true; - }, - function () { - return true; - }, - function () { - return true; - }] ); + }); channel.publish( "MyTopic", "Testing123" ); } ); after( function () { postal.utils.reset(); recvd = false; } ); - it( "should have a constraint on the subscription", function () { - expect( postal.configuration.bus.subscriptions.MyChannel.MyTopic[0].constraints.length ).to.be( 3 ); + it( "should overwrite constraint with last one passed in", function () { + expect( subscription.callback.strategies().length ).to.be( 1 ); } ); it( "should have invoked the callback", function () { expect( recvd ).to.be.ok(); } ); } ); - describe( "When subscribing with multiple constraints and one returning false", function () { - var recvd = false; - before( function () { - channel = postal.channel( "MyChannel" ); - subscription = channel.subscribe( "MyTopic", function ( data ) { - recvd = true; - } ) - .withConstraints( [function () { - return true; - }, - function () { - return false; - }, - function () { - return true; - }] ); - channel.publish( "MyTopic", "Testing123" ); - } ); - after( function () { - postal.utils.reset(); - recvd = false; - } ); - it( "should have a constraint on the subscription", function () { - expect( postal.configuration.bus.subscriptions.MyChannel.MyTopic[0].constraints.length ).to.be( 3 ); - } ); - it( "should not have invoked the callback", function () { - expect( recvd ).to.not.be.ok() - } ); - } ); describe( "When subscribing with one constraint returning false", function () { var recvd = false; before( function () { @@ -397,7 +365,7 @@ recvd = false; } ); it( "should have a constraint on the subscription", function () { - expect( postal.configuration.bus.subscriptions.MyChannel.MyTopic[0].constraints.length ).to.be( 1 ); + expect( subscription.callback.strategies()[0].name ).to.be( "predicate" ); } ); it( "should not have invoked the subscription callback", function () { expect( recvd ).to.not.be.ok(); @@ -420,7 +388,7 @@ recvd = false; } ); it( "should have a constraint on the subscription", function () { - expect( postal.configuration.bus.subscriptions.MyChannel.MyTopic[0].constraints.length ).to.be( 1 ); + expect( subscription.callback.strategies()[0].name ).to.be( "predicate" ); } ); it( "should have invoked the subscription callback", function () { expect( recvd ).to.be.ok(); @@ -506,11 +474,8 @@ it( "should have set subscription topic value", function () { expect( sub.topic ).to.be( "MyTopic" ); } ); - it( "should have defaulted the subscription constraints array", function () { - expect( sub.constraints.length ).to.be( 0 ); - } ); it( "should have defaulted the subscription context value", function () { - expect( sub.context ).to.be( null ); + expect( sub.callback.context() ).to.be( sub ); } ); } ); }); @@ -655,16 +620,16 @@ results.push( "unsubscribed" ); } } ); - subscription1 = channel.subscribe( 'nest.test',function () { - results.push( '1 received message' ); + subscription1 = channel.subscribe( "nest.test",function () { + results.push( "1 received message" ); channel.publish( "nest.test2", "Hai" ); } ).once(); - subscription2 = channel.subscribe( 'nest.test2', function () { - results.push( '2 received message' ); + subscription2 = channel.subscribe( "nest.test2", function () { + results.push( "2 received message" ); } ); - channel.publish( 'nest.test' ); - channel.publish( 'nest.test' ); + channel.publish( "nest.test" ); + channel.publish( "nest.test" ); } ); after( function () { //subscription2.unsubscribe(); @@ -673,9 +638,9 @@ } ); it( "should produce expected messages", function () { expect( results.length ).to.be( 3 ); - expect( results[0] ).to.be( "1 received message" ); - expect( results[1] ).to.be( "2 received message" ); - expect( results[2] ).to.be( "unsubscribed" ); + expect( results[0] ).to.be( "unsubscribed" ); + expect( results[1] ).to.be( "1 received message" ); + expect( results[2] ).to.be( "2 received message" ); } ); } ); }); diff --git a/spec/subscriptionDefinition.spec.js b/spec/subscriptionDefinition.spec.js index 9e73bdf..dc2a175 100644 --- a/spec/subscriptionDefinition.spec.js +++ b/spec/subscriptionDefinition.spec.js @@ -22,42 +22,26 @@ it( "should set the callback", function () { expect( sDef.callback ).to.be( NO_OP ); } ); - it( "should default the constraints", function () { - expect( sDef.constraints.length ).to.be( 0 ); - } ); it( "should default the context", function () { - expect( sDef.context ).to.be( null ); + expect( sDef.context ).to.be( undefined ); } ); } ); describe( "When setting distinctUntilChanged", function () { var sDefa = new SubscriptionDefinition( "TestChannel", "TestTopic", NO_OP ).distinctUntilChanged(); - it( "Should add a DistinctPredicate constraint to the configuration constraints", function () { - expect( sDefa.constraints.length ).to.be( 1 ); - } ); + it( "callback should be a strategy", function () { + expect( typeof sDefa.callback.context ).to.be( "function" ); + }); } ); describe( "When adding a constraint", function () { var sDefb = new SubscriptionDefinition( "TestChannel", "TestTopic", NO_OP ).withConstraint( function () { - } ); + }); - it( "Should add a constraint", function () { - expect( sDefb.constraints.length ).to.be( 1 ); - } ); - } ); - - describe( "When adding multiple constraints", function () { - var sDefc = new SubscriptionDefinition( "TestChannel", "TestTopic", NO_OP ) - .withConstraints( [ - function () {}, - function () {}, - function () {} - ]); - - it( "Should add a constraint", function () { - expect( sDefc.constraints.length ).to.be( 3 ); - } ); + it( "callback should be a strategy", function () { + expect( typeof sDefb.callback.context ).to.be( "function" ); + }); } ); describe( "When setting the context", function () { @@ -69,9 +53,13 @@ name = this.name; return true; } ); + sDefd.callback({ channel : "TestChannel", topic : "TestTopic", data : "Oh, hai"}, "Oh, hai"); it( "Should set context", function () { - expect( sDefd.context ).to.be( obj ); + expect( sDefd.callback.context() ).to.be( obj ); + } ); + it( "Should apply context to predicate/constraint", function () { + expect( name ).to.be( "Rose" ); } ); } ); diff --git a/spec/utils.spec.js b/spec/utils.spec.js index 262ee1b..2056491 100644 --- a/spec/utils.spec.js +++ b/spec/utils.spec.js @@ -48,8 +48,7 @@ it( "should have created a subscription definition", function () { expect( sub.channel ).to.be( "MyChannel" ); expect( sub.topic ).to.be( "MyTopic" ); - expect( sub.constraints.length ).to.be( 0 ); - expect( sub.context ).to.be( null ); + expect( sub.context ).to.be( undefined ); } ); it( "should have created a resolver cache entry", function () { expect( _.isEmpty( resolver ) ).to.not.be.ok() diff --git a/src/Api.js b/src/Api.js index a3be77d..7c1141b 100644 --- a/src/Api.js +++ b/src/Api.js @@ -5,7 +5,8 @@ postal = { bus : localBus, resolver : bindingsResolver, DEFAULT_CHANNEL : "/", - SYSTEM_CHANNEL : "postal" + SYSTEM_CHANNEL : "postal", + strategies : strats }, ChannelDefinition : ChannelDefinition, diff --git a/src/SubscriptionDefinition.js b/src/SubscriptionDefinition.js index f670d1e..fc94225 100644 --- a/src/SubscriptionDefinition.js +++ b/src/SubscriptionDefinition.js @@ -7,11 +7,9 @@ var SubscriptionDefinition = function ( channel, topic, callback ) { if(topic.length === 0) { throw new Error("Topics cannot be empty"); } - this.channel = channel; - this.topic = topic; - this.callback = callback; - this.constraints = []; - this.context = null; + this.channel = channel; + this.topic = topic; + this.subscribe(callback); }; SubscriptionDefinition.prototype = { @@ -22,107 +20,83 @@ SubscriptionDefinition.prototype = { } }, - defer : function () { - var self = this; - var fn = this.callback; - this.callback = function ( data, env ) { - setTimeout( function () { - fn.call( self.context, data, env ); - }, 0 ); - }; - return this; - }, + defer : function () { + this.callback.useStrategy(postal.configuration.strategies.setTimeout(0)); + 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 self = this; - var fn = this.callback; - var dispose = _.after( maxCalls, _.bind( function () { - this.unsubscribe(); - }, this ) ); + disposeAfter : function ( maxCalls ) { + if ( _.isNaN( maxCalls ) || maxCalls <= 0 ) { + throw "The value provided to disposeAfter (maxCalls) must be a number greater than zero."; + } + var self = this; + self.callback.useStrategy(postal.configuration.strategies.after(maxCalls, function() { + self.unsubscribe.call(self); + })); + return self; + }, - this.callback = function () { - fn.apply( self.context, arguments ); - dispose(); - }; - return this; - }, + distinctUntilChanged : function () { + this.callback.useStrategy(postal.configuration.strategies.distinct()); + return this; + }, - distinctUntilChanged : function () { - this.withConstraint( new ConsecutiveDistinctPredicate() ); - return this; - }, + distinct : function () { + this.callback.useStrategy(postal.configuration.strategies.distinct({ all: true })); + return this; + }, - distinct : function () { - this.withConstraint( new DistinctPredicate() ); - return this; - }, + once : function () { + this.disposeAfter( 1 ); + return this; + }, - once : function () { - this.disposeAfter( 1 ); - return this; - }, + withConstraint : function ( predicate ) { + if ( !_.isFunction( predicate ) ) { + throw "Predicate constraint must be a function"; + } + this.callback.useStrategy(postal.configuration.strategies.predicate(predicate)); + return this; + }, - withConstraint : function ( predicate ) { - if ( !_.isFunction( predicate ) ) { - throw "Predicate constraint must be a function"; - } - this.constraints.push( predicate ); - return this; - }, + withContext : function ( context ) { + this.callback.context(context); + return this; + }, - withConstraints : function ( predicates ) { - var self = this; - if ( _.isArray( predicates ) ) { - _.each( predicates, function ( predicate ) { - self.withConstraint( predicate ); - } ); - } - return self; - }, + withDebounce : function ( milliseconds, immediate ) { + if ( _.isNaN( milliseconds ) ) { + throw "Milliseconds must be a number"; + } + var fn = this.callback; + this.callback = _.debounce( fn, milliseconds, !!immediate ); + return this; + }, - withContext : function ( context ) { - this.context = context; - return this; - }, + withDelay : function ( milliseconds ) { + if ( _.isNaN( milliseconds ) ) { + throw "Milliseconds must be a number"; + } + this.callback.useStrategy(postal.configuration.strategies.setTimeout(milliseconds)); + return this; + }, - withDebounce : function ( milliseconds, immediate ) { - if ( _.isNaN( milliseconds ) ) { - throw "Milliseconds must be a number"; - } - var fn = this.callback; - this.callback = _.debounce( fn, milliseconds, !!immediate ); - return this; - }, + withThrottle : function ( milliseconds ) { + if ( _.isNaN( milliseconds ) ) { + throw "Milliseconds must be a number"; + } + this.callback.useStrategy(postal.configuration.strategies.throttle(milliseconds)); + return this; + }, - withDelay : function ( milliseconds ) { - if ( _.isNaN( milliseconds ) ) { - throw "Milliseconds must be a number"; - } - var self = this; - var fn = this.callback; - this.callback = function ( data, env ) { - setTimeout( function () { - fn.call( self.context, data, env ); - }, milliseconds ); - }; - return this; - }, - - withThrottle : function ( milliseconds, options ) { - options = options || { }; - if ( _.isNaN( milliseconds ) ) { - throw "Milliseconds must be a number"; - } - var fn = this.callback; - this.callback = _.throttle( fn, milliseconds, options ); - return this; - }, - - subscribe : function ( callback ) { - this.callback = callback; - return this; - } + subscribe : function ( callback ) { + this.callback = callback; + this.callback = new Strategy({ + owner : this, + prop : "callback", + context : this, // TODO: is this the best option? + lazyInit : true + }); + return this; + } }; \ No newline at end of file diff --git a/src/postal.js b/src/postal.js index e438580..05f32cd 100644 --- a/src/postal.js +++ b/src/postal.js @@ -17,7 +17,9 @@ var postal; var prevPostal = global.postal; - //import("ConsecutiveDistinctPredicate.js"); + //import("strategy.js"); + //import("strategies.js"); + //import("ConsecutiveDistinctPredicate.js"); //import("DistinctPredicate.js"); //import("ChannelDefinition.js"); //import("SubscriptionDefinition.js"); diff --git a/src/strategies.js b/src/strategies.js new file mode 100644 index 0000000..d2cc60f --- /dev/null +++ b/src/strategies.js @@ -0,0 +1,66 @@ +/* global DistinctPredicate,ConsecutiveDistinctPredicate */ +var strats = { + setTimeout: function(ms) { + return { + name: "setTimeout", + fn: function (next, data, envelope) { + setTimeout(function () { + next(data, envelope); + }, ms); + } + }; + }, + after: function(maxCalls, callback) { + var dispose = _.after(maxCalls, callback); + return { + name: "after", + fn: function (next, data, envelope) { + dispose(); + next(data, envelope); + } + }; + }, + throttle : function(ms) { + return { + name: "throttle", + fn: _.throttle(function(next, data, envelope) { + next(data, envelope); + }, ms) + }; + }, + debounce: function(ms, immediate) { + return { + name: "debounce", + fn: _.debounce(function(next, data, envelope) { + next(data, envelope); + }, ms, !!immediate) + }; + }, + predicate: function(pred) { + return { + name: "predicate", + fn: function(next, data, envelope) { + if(pred.call(this, data, envelope)) { + next.call(this, data, envelope); + } + } + }; + }, + distinct : function(options) { + options = options || {}; + var accessor = function(args) { + return args[0]; + }; + var check = options.all ? + new DistinctPredicate(accessor) : + new ConsecutiveDistinctPredicate(accessor); + return { + name : "distinct", + fn : function(next, data, envelope) { + if(check(data)) { + next(data, envelope); + } + } + }; + } +}; \ No newline at end of file diff --git a/src/strategy.js b/src/strategy.js new file mode 100644 index 0000000..b50bd26 --- /dev/null +++ b/src/strategy.js @@ -0,0 +1,68 @@ +var Strategy = function( options ) { + var _target = options.owner[options.prop]; + if ( typeof _target !== "function" ) { + throw new Error( "Strategies can only target methods." ); + } + var _strategies = []; + var _context = options.context || options.owner; + var strategy = function() { + var idx = 0; + var next = function next() { + var args = Array.prototype.slice.call( arguments, 0 ); + var thisIdx = idx; + var strategy; + idx += 1; + if ( thisIdx < _strategies.length ) { + strategy = _strategies[thisIdx]; + strategy.fn.apply( strategy.context || _context, [next].concat( args ) ); + } else { + _target.apply( _context, args ); + } + }; + next.apply( this, arguments ); + }; + strategy.target = function() { + return _target; + }; + strategy.context = function( ctx ) { + if ( arguments.length === 0 ) { + return _context; + } else { + _context = ctx; + } + }; + strategy.strategies = function() { + return _strategies; + }; + strategy.useStrategy = function( strategy ) { + var idx = 0, + exists = false; + while ( idx < _strategies.length ) { + if ( _strategies[idx].name === strategy.name ) { + _strategies[idx] = strategy; + exists = true; + break; + } + idx += 1; + } + if ( !exists ) { + _strategies.push( strategy ); + } + }; + strategy.reset = function() { + _strategies = []; + }; + if ( options.lazyInit ) { + _target.useStrategy = function() { + options.owner[options.prop] = strategy; + strategy.useStrategy.apply( strategy, arguments ); + }; + _target.context = function() { + options.owner[options.prop] = strategy; + return strategy.context.apply( strategy, arguments ); + }; + return _target; + } else { + return strategy; + } +}; \ No newline at end of file