From b1e259f9f0a80f6ee0c77e52332fd6a256b5aa81 Mon Sep 17 00:00:00 2001 From: ifandelse Date: Mon, 27 Jan 2014 01:52:22 -0500 Subject: [PATCH] Working towards a basic and full build option. Added separate strategies-only build, etc. --- gulpfile.js | 24 +- lib/basic/postal.basic.js | 417 ++++++++++++++++++ lib/basic/postal.basic.min.js | 8 + lib/postal.js | 291 ++++++------ lib/postal.min.js | 2 +- lib/strategies-add-on/postal.strategies.js | 195 ++++++++ .../postal.strategies.min.js | 8 + spec/basic.html | 34 ++ spec/index.html | 2 + spec/postaljs.spec.js | 273 ------------ spec/postaljs.strategies.spec.js | 288 ++++++++++++ spec/subDef.strategies.spec.js | 197 +++++++++ spec/subscriptionDefinition.spec.js | 188 -------- src/Api.js | 3 +- src/ConsecutiveDistinctPredicate.js | 16 - src/DistinctPredicate.js | 17 - src/SubscriptionDefinition.js | 77 +--- src/postal.basic.js | 36 ++ src/postal.js | 4 +- src/postal.strategies.js | 24 + src/strategies.js | 119 ++++- 21 files changed, 1500 insertions(+), 723 deletions(-) create mode 100644 lib/basic/postal.basic.js create mode 100644 lib/basic/postal.basic.min.js create mode 100644 lib/strategies-add-on/postal.strategies.js create mode 100644 lib/strategies-add-on/postal.strategies.min.js create mode 100644 spec/basic.html create mode 100644 spec/postaljs.strategies.spec.js create mode 100644 spec/subDef.strategies.spec.js delete mode 100644 src/ConsecutiveDistinctPredicate.js delete mode 100644 src/DistinctPredicate.js create mode 100644 src/postal.basic.js create mode 100644 src/postal.strategies.js diff --git a/gulpfile.js b/gulpfile.js index f1fe1bc..f1676e0 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -35,6 +35,28 @@ gulp.task("combine", function() { .pipe(header(banner, { pkg : pkg })) .pipe(rename("postal.min.js")) .pipe(gulp.dest("./lib/")); + + gulp.src(["./src/postal.basic.js"]) + .pipe(header(banner, { pkg : pkg })) + .pipe(fileImports()) + .pipe(hintNot()) + .pipe(beautify({indentSize: 4})) + .pipe(gulp.dest("./lib/basic/")) + .pipe(uglify({ compress: { negate_iife: false }})) + .pipe(header(banner, { pkg : pkg })) + .pipe(rename("postal.basic.min.js")) + .pipe(gulp.dest("./lib/basic/")); + + gulp.src(["./src/postal.strategies.js"]) + .pipe(header(banner, { pkg : pkg })) + .pipe(fileImports()) + .pipe(hintNot()) + .pipe(beautify({indentSize: 4})) + .pipe(gulp.dest("./lib/strategies-add-on/")) + .pipe(uglify({ compress: { negate_iife: false }})) + .pipe(header(banner, { pkg : pkg })) + .pipe(rename("postal.strategies.min.js")) + .pipe(gulp.dest("./lib/strategies-add-on/")); }); gulp.task("default", function() { @@ -62,7 +84,7 @@ var createServer = function(port) { var servers; gulp.task("server", function(){ - gulp.run("report"); + gulp.run("combine", "report"); if(!servers) { servers = createServer(port); } diff --git a/lib/basic/postal.basic.js b/lib/basic/postal.basic.js new file mode 100644 index 0000000..bece19d --- /dev/null +++ b/lib/basic/postal.basic.js @@ -0,0 +1,417 @@ +/** + * postal - Pub/Sub library providing wildcard subscriptions, complex message handling, etc. Works server and client-side. + * Author: Jim Cowart (http://freshbrewedcode.com/jimcowart) + * Version: v0.8.11 + * Url: http://github.com/postaljs/postal.js + * License(s): MIT, GPL + */ + +(function (root, factory) { + if (typeof module === "object" && module.exports) { + // Node, or CommonJS-Like environments + module.exports = factory(require("underscore"), this); + } else if (typeof define === "function" && define.amd) { + // AMD. Register as an anonymous module. + define(["underscore"], function (_) { + return factory(_, root); + }); + } else { + // Browser globals + root.postal = factory(root._, root); + } +}(this, function (_, global, undefined) { + + 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 ChannelDefinition = function (channelName) { + this.channel = channelName || postal.configuration.DEFAULT_CHANNEL; + }; + + ChannelDefinition.prototype.subscribe = function () { + return postal.subscribe(arguments.length === 1 ? new SubscriptionDefinition(this.channel, arguments[0].topic, arguments[0].callback) : new SubscriptionDefinition(this.channel, arguments[0], arguments[1])); + }; + + ChannelDefinition.prototype.publish = function () { + var envelope = arguments.length === 1 ? (Object.prototype.toString.call(arguments[0]) === "[object String]" ? { + topic: arguments[0] + } : arguments[0]) : { + topic: arguments[0], + data: arguments[1] + }; + envelope.channel = this.channel; + return postal.configuration.bus.publish(envelope); + }; + + + var SubscriptionDefinition = function (channel, topic, callback) { + if (arguments.length !== 3) { + throw new Error("You must provide a channel, topic and callback when creating a SubscriptionDefinition instance."); + } + if (topic.length === 0) { + throw new Error("Topics cannot be empty"); + } + this.channel = channel; + this.topic = topic; + this.subscribe(callback); + }; + + SubscriptionDefinition.prototype = { + unsubscribe: function () { + if (!this.inactive) { + this.inactive = true; + postal.unsubscribe(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; + }, + + withContext: function (context) { + this.callback.context(context); + return this; + } + }; + + + + + var bindingsResolver = { + cache: {}, + regex: {}, + + compare: function (binding, topic) { + var pattern, rgx, prevSegment, result = (this.cache[topic] && this.cache[topic][binding]); + if (typeof result !== "undefined") { + return result; + } + if (!(rgx = this.regex[binding])) { + pattern = "^" + _.map(binding.split("."), function (segment) { + var res = ""; + if ( !! prevSegment) { + res = prevSegment !== "#" ? "\\.\\b" : "\\b"; + } + if (segment === "#") { + res += "[\\s\\S]*"; + } else if (segment === "*") { + res += "[^.]+"; + } else { + res += segment; + } + prevSegment = segment; + return res; + }).join("") + "$"; + rgx = this.regex[binding] = new RegExp(pattern); + } + this.cache[topic] = this.cache[topic] || {}; + this.cache[topic][binding] = result = rgx.test(topic); + return result; + }, + + reset: function () { + this.cache = {}; + this.regex = {}; + } + }; + + var fireSub = function (subDef, envelope) { + if (!subDef.inactive && postal.configuration.resolver.compare(subDef.topic, envelope.topic)) { + if (_.all(subDef.constraints, function (constraint) { + return constraint.call(subDef.context, envelope.data, envelope); + })) { + if (typeof subDef.callback === "function") { + subDef.callback.call(subDef.context, envelope.data, envelope); + } + } + } + }; + + var pubInProgress = 0; + var unSubQueue = []; + var clearUnSubQueue = function () { + while (unSubQueue.length) { + localBus.unsubscribe(unSubQueue.shift()); + } + }; + + 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) { + ++pubInProgress; + envelope.timeStamp = new Date(); + _.each(this.wireTaps, function (tap) { + tap(envelope.data, envelope); + }); + if (this.subscriptions[envelope.channel]) { + _.each(this.subscriptions[envelope.channel], function (subscribers) { + var idx = 0, + len = subscribers.length, + subDef; + while (idx < len) { + if (subDef = subscribers[idx++]) { + fireSub(subDef, envelope); + } + } + }); + } + if (--pubInProgress === 0) { + clearUnSubQueue(); + } + return envelope; + }, + + 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 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] = []; + } + subs.push(subDef); + return subDef; + }, + + subscriptions: {}, + + wireTaps: [], + + unsubscribe: function (config) { + if (pubInProgress) { + unSubQueue.push(config); + return; + } + if (this.subscriptions[config.channel] && this.subscriptions[config.channel][config.topic]) { + var len = this.subscriptions[config.channel][config.topic].length, + idx = 0; + while (idx < len) { + if (this.subscriptions[config.channel][config.topic][idx] === config) { + this.subscriptions[config.channel][config.topic].splice(idx, 1); + break; + } + idx += 1; + } + } + } + }; + + + postal = { + configuration: { + bus: localBus, + resolver: bindingsResolver, + DEFAULT_CHANNEL: "/", + SYSTEM_CHANNEL: "postal" + }, + + ChannelDefinition: ChannelDefinition, + SubscriptionDefinition: SubscriptionDefinition, + + channel: function (channelName) { + return new ChannelDefinition(channelName); + }, + + subscribe: function (options) { + var subDef = new SubscriptionDefinition(options.channel || postal.configuration.DEFAULT_CHANNEL, options.topic, options.callback); + postal.configuration.bus.publish({ + channel: postal.configuration.SYSTEM_CHANNEL, + topic: "subscription.created", + data: { + event: "subscription.created", + channel: subDef.channel, + topic: subDef.topic + } + }); + return postal.configuration.bus.subscribe(subDef); + }, + + publish: function (envelope) { + envelope.channel = envelope.channel || postal.configuration.DEFAULT_CHANNEL; + return postal.configuration.bus.publish(envelope); + }, + + unsubscribe: function (subdef) { + postal.configuration.bus.unsubscribe(subdef); + postal.configuration.bus.publish({ + channel: postal.configuration.SYSTEM_CHANNEL, + topic: "subscription.removed", + data: { + event: "subscription.removed", + channel: subdef.channel, + topic: subdef.topic + } + }); + }, + + addWireTap: function (callback) { + return this.configuration.bus.addWireTap(callback); + }, + + linkChannels: function (sources, destinations) { + var result = []; + sources = !_.isArray(sources) ? [sources] : sources; + destinations = !_.isArray(destinations) ? [destinations] : destinations; + _.each(sources, function (source) { + var sourceTopic = source.topic || "#"; + _.each(destinations, function (destination) { + var destChannel = destination.channel || postal.configuration.DEFAULT_CHANNEL; + result.push( + postal.subscribe({ + channel: source.channel || postal.configuration.DEFAULT_CHANNEL, + topic: sourceTopic, + callback: function (data, env) { + var newEnv = _.clone(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; + }, + + noConflict: function () { + if (typeof window === "undefined") { + throw new Error("noConflict can only be used in browser clients which aren't using AMD modules"); + } + global.postal = prevPostal; + return this; + }, + + utils: { + getSubscribersFor: function () { + var channel = arguments[0], + tpc = arguments[1]; + if (arguments.length === 1) { + channel = arguments[0].channel || postal.configuration.DEFAULT_CHANNEL; + tpc = arguments[0].topic; + } + if (postal.configuration.bus.subscriptions[channel] && Object.prototype.hasOwnProperty.call(postal.configuration.bus.subscriptions[channel], tpc)) { + return postal.configuration.bus.subscriptions[channel][tpc]; + } + return []; + }, + + reset: function () { + postal.configuration.bus.reset(); + postal.configuration.resolver.reset(); + } + } + }; + localBus.subscriptions[postal.configuration.SYSTEM_CHANNEL] = {}; + + + if (global && Object.prototype.hasOwnProperty.call(global, "__postalReady__") && _.isArray(global.__postalReady__)) { + while (global.__postalReady__.length) { + global.__postalReady__.shift().onReady(postal); + } + } + + + return postal; +})); \ No newline at end of file diff --git a/lib/basic/postal.basic.min.js b/lib/basic/postal.basic.min.js new file mode 100644 index 0000000..5d6eb05 --- /dev/null +++ b/lib/basic/postal.basic.min.js @@ -0,0 +1,8 @@ +/** + * postal - Pub/Sub library providing wildcard subscriptions, complex message handling, etc. Works server and client-side. + * Author: Jim Cowart (http://freshbrewedcode.com/jimcowart) + * Version: v0.8.11 + * Url: http://github.com/postaljs/postal.js + * License(s): MIT, GPL + */ +(function(n,t){"object"==typeof module&&module.exports?module.exports=t(require("underscore"),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,e=t.postal,r=function(n){var t=n.owner[n.prop];if("function"!=typeof t)throw new Error("Strategies can only target methods.");var i=[],e=n.context||n.owner,r=function(){var n=0,r=function c(){var r,o=Array.prototype.slice.call(arguments,0),s=n;n+=1,se;)(i=n[e++])&&a(i,t)}),0===--u&&h(),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(u)return void p.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:l,resolver:s,DEFAULT_CHANNEL:"/",SYSTEM_CHANNEL:"postal"},ChannelDefinition:c,SubscriptionDefinition:o,channel:function(n){return new c(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,e){var r=[];return t=n.isArray(t)?t:[t],e=n.isArray(e)?e:[e],n.each(t,function(t){var c=t.topic||"#";n.each(e,function(e){var o=e.channel||i.configuration.DEFAULT_CHANNEL;r.push(i.subscribe({channel:t.channel||i.configuration.DEFAULT_CHANNEL,topic:c,callback:function(t,r){var c=n.clone(r);c.topic=n.isFunction(e.topic)?e.topic(r.topic):e.topic||r.topic,c.channel=o,c.data=t,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 t.postal=e,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()}}},l.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 diff --git a/lib/postal.js b/lib/postal.js index d934421..b45b0b7 100644 --- a/lib/postal.js +++ b/lib/postal.js @@ -24,6 +24,48 @@ var postal; var prevPostal = global.postal; + + + var SubscriptionDefinition = function (channel, topic, callback) { + if (arguments.length !== 3) { + throw new Error("You must provide a channel, topic and callback when creating a SubscriptionDefinition instance."); + } + if (topic.length === 0) { + throw new Error("Topics cannot be empty"); + } + this.channel = channel; + this.topic = topic; + this.subscribe(callback); + }; + + SubscriptionDefinition.prototype = { + unsubscribe: function () { + if (!this.inactive) { + this.inactive = true; + postal.unsubscribe(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; + }, + + withContext: function (context) { + this.callback.context(context); + return this; + } + }; + + + var Strategy = function (options) { var _target = options.owner[options.prop]; if (typeof _target !== "function") { @@ -93,10 +135,46 @@ } }; + + var ConsecutiveDistinctPredicate = 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 DistinctPredicate = function () { + var previous = []; + return function (data) { + var isDistinct = !_.any(previous, function (p) { + if (_.isObject(data) || _.isArray(data)) { + return _.isEqual(data, p); + } + return data === p; + }); + if (isDistinct) { + previous.push(data); + } + return isDistinct; + }; + }; + var strats = { - setTimeout: function (ms) { + withDelay: function (ms) { + if (_.isNaN(ms)) { + throw "Milliseconds must be a number"; + } return { - name: "setTimeout", + name: "withDelay", fn: function (next, data, envelope) { setTimeout(function () { next(data, envelope); @@ -104,25 +182,37 @@ } }; }, - after: function (maxCalls, callback) { + defer: function () { + return this.withDelay(0); + }, + stopAfter: function (maxCalls, callback) { + if (_.isNaN(maxCalls) || maxCalls <= 0) { + throw "The value provided to disposeAfter (maxCalls) must be a number greater than zero."; + } var dispose = _.after(maxCalls, callback); return { - name: "after", + name: "stopAfter", fn: function (next, data, envelope) { dispose(); next(data, envelope); } }; }, - throttle: function (ms) { + withThrottle: function (ms) { + if (_.isNaN(ms)) { + throw "Milliseconds must be a number"; + } return { - name: "throttle", + name: "withThrottle", fn: _.throttle(function (next, data, envelope) { next(data, envelope); }, ms) }; }, - debounce: function (ms, immediate) { + withDebounce: function (ms, immediate) { + if (_.isNaN(ms)) { + throw "Milliseconds must be a number"; + } return { name: "debounce", fn: _.debounce(function (next, data, envelope) { @@ -130,9 +220,12 @@ }, ms, !! immediate) }; }, - predicate: function (pred) { + withConstraint: function (pred) { + if (!_.isFunction(pred)) { + throw "Predicate constraint must be a function"; + } return { - name: "predicate", + name: "withConstraint", fn: function (next, data, envelope) { if (pred.call(this, data, envelope)) { next.call(this, data, envelope); @@ -157,37 +250,54 @@ } }; - var ConsecutiveDistinctPredicate = 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; - }; + SubscriptionDefinition.prototype.defer = function () { + this.callback.useStrategy(strats.defer()); + return this; }; - var DistinctPredicate = function () { - var previous = []; + SubscriptionDefinition.prototype.disposeAfter = function (maxCalls) { + var self = this; + self.callback.useStrategy(strats.stopAfter(maxCalls, function () { + self.unsubscribe.call(self); + })); + return self; + }; - return function (data) { - var isDistinct = !_.any(previous, function (p) { - if (_.isObject(data) || _.isArray(data)) { - return _.isEqual(data, p); - } - return data === p; - }); - if (isDistinct) { - previous.push(data); - } - return isDistinct; - }; + SubscriptionDefinition.prototype.distinctUntilChanged = function () { + this.callback.useStrategy(strats.distinct()); + return this; + }; + + SubscriptionDefinition.prototype.distinct = function () { + this.callback.useStrategy(strats.distinct({ + all: true + })); + return this; + }; + + SubscriptionDefinition.prototype.once = function () { + this.disposeAfter(1); + return this; + }; + + SubscriptionDefinition.prototype.withConstraint = function (predicate) { + this.callback.useStrategy(strats.withConstraint(predicate)); + return this; + }; + + SubscriptionDefinition.prototype.withDebounce = function (milliseconds, immediate) { + this.callback.useStrategy(strats.withDebounce(milliseconds, immediate)); + return this; + }; + + SubscriptionDefinition.prototype.withDelay = function (milliseconds) { + this.callback.useStrategy(strats.withDelay(milliseconds)); + return this; + }; + + SubscriptionDefinition.prototype.withThrottle = function (milliseconds) { + this.callback.useStrategy(strats.withThrottle(milliseconds)); + return this; }; var ChannelDefinition = function (channelName) { @@ -209,110 +319,6 @@ return postal.configuration.bus.publish(envelope); }; - - var SubscriptionDefinition = function (channel, topic, callback) { - if (arguments.length !== 3) { - throw new Error("You must provide a channel, topic and callback when creating a SubscriptionDefinition instance."); - } - if (topic.length === 0) { - throw new Error("Topics cannot be empty"); - } - this.channel = channel; - this.topic = topic; - this.subscribe(callback); - }; - - SubscriptionDefinition.prototype = { - unsubscribe: function () { - if (!this.inactive) { - this.inactive = true; - postal.unsubscribe(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; - self.callback.useStrategy(postal.configuration.strategies.after(maxCalls, function () { - self.unsubscribe.call(self); - })); - return self; - }, - - distinctUntilChanged: function () { - this.callback.useStrategy(postal.configuration.strategies.distinct()); - return this; - }, - - distinct: function () { - this.callback.useStrategy(postal.configuration.strategies.distinct({ - all: true - })); - 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; - }, - - withContext: function (context) { - this.callback.context(context); - return this; - }, - - withDebounce: function (milliseconds, immediate) { - if (_.isNaN(milliseconds)) { - throw "Milliseconds must be a number"; - } - this.callback.useStrategy(postal.configuration.strategies.debounce(milliseconds, immediate)); - return this; - }, - - withDelay: function (milliseconds) { - if (_.isNaN(milliseconds)) { - throw "Milliseconds must be a number"; - } - this.callback.useStrategy(postal.configuration.strategies.setTimeout(milliseconds)); - return this; - }, - - withThrottle: function (milliseconds) { - if (_.isNaN(milliseconds)) { - throw "Milliseconds must be a number"; - } - 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; - } - }; - var bindingsResolver = { cache: {}, regex: {}, @@ -463,8 +469,7 @@ bus: localBus, resolver: bindingsResolver, DEFAULT_CHANNEL: "/", - SYSTEM_CHANNEL: "postal", - strategies: strats + SYSTEM_CHANNEL: "postal" }, ChannelDefinition: ChannelDefinition, diff --git a/lib/postal.min.js b/lib/postal.min.js index 329ed10..6bf2d6b 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(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,e){if(t.isNaN(n))throw"Milliseconds must be a number";return this.callback.useStrategy(i.configuration.strategies.debounce(n,e)),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 +(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,n,i){if(3!==arguments.length)throw new Error("You must provide a channel, topic and callback when creating a SubscriptionDefinition instance.");if(0===n.length)throw new Error("Topics cannot be empty");this.channel=t,this.topic=n,this.subscribe(i)};r.prototype={unsubscribe:function(){this.inactive||(this.inactive=!0,i.unsubscribe(this))},subscribe:function(t){return this.callback=t,this.callback=new o({owner:this,prop:"callback",context:this,lazyInit:!0}),this},withContext:function(t){return this.callback.context(t),this}};var o=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 o(){var r,c=Array.prototype.slice.call(arguments,0),s=t;t+=1,s=n)throw"The value provided to disposeAfter (maxCalls) must be a number greater than zero.";var e=t.after(n,i);return{name:"stopAfter",fn:function(t,n,i){e(),t(n,i)}}},withThrottle:function(n){if(t.isNaN(n))throw"Milliseconds must be a number";return{name:"withThrottle",fn:t.throttle(function(t,n,i){t(n,i)},n)}},withDebounce:function(n,i){if(t.isNaN(n))throw"Milliseconds must be a number";return{name:"debounce",fn:t.debounce(function(t,n,i){t(n,i)},n,!!i)}},withConstraint:function(n){if(!t.isFunction(n))throw"Predicate constraint must be a function";return{name:"withConstraint",fn:function(t,i,e){n.call(this,i,e)&&t.call(this,i,e)}}},distinct:function(t){t=t||{};var n=function(t){return t[0]},i=t.all?new s(n):new c(n);return{name:"distinct",fn:function(t,n,e){i(n)&&t(n,e)}}}};r.prototype.defer=function(){return this.callback.useStrategy(a.defer()),this},r.prototype.disposeAfter=function(t){var n=this;return n.callback.useStrategy(a.stopAfter(t,function(){n.unsubscribe.call(n)})),n},r.prototype.distinctUntilChanged=function(){return this.callback.useStrategy(a.distinct()),this},r.prototype.distinct=function(){return this.callback.useStrategy(a.distinct({all:!0})),this},r.prototype.once=function(){return this.disposeAfter(1),this},r.prototype.withConstraint=function(t){return this.callback.useStrategy(a.withConstraint(t)),this},r.prototype.withDebounce=function(t,n){return this.callback.useStrategy(a.withDebounce(t,n)),this},r.prototype.withDelay=function(t){return this.callback.useStrategy(a.withDelay(t)),this},r.prototype.withThrottle=function(t){return this.callback.useStrategy(a.withThrottle(t)),this};var u=function(t){this.channel=t||i.configuration.DEFAULT_CHANNEL};u.prototype.subscribe=function(){return i.subscribe(1===arguments.length?new r(this.channel,arguments[0].topic,arguments[0].callback):new r(this.channel,arguments[0],arguments[1]))},u.prototype.publish=function(){var t=1===arguments.length?"[object String]"===Object.prototype.toString.call(arguments[0])?{topic:arguments[0]}:arguments[0]:{topic:arguments[0],data:arguments[1]};return t.channel=this.channel,i.configuration.bus.publish(t)};var h={cache:{},regex:{},compare:function(n,i){var e,r,o,c=this.cache[i]&&this.cache[i][n];return"undefined"!=typeof c?c:((r=this.regex[n])||(e="^"+t.map(n.split("."),function(t){var n="";return o&&(n="#"!==o?"\\.\\b":"\\b"),n+="#"===t?"[\\s\\S]*":"*"===t?"[^.]+":t,o=t,n}).join("")+"$",r=this.regex[n]=new RegExp(e)),this.cache[i]=this.cache[i]||{},this.cache[i][n]=c=r.test(i),c)},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)},p=0,f=[],b=function(){for(;f.length;)g.unsubscribe(f.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++p,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===--p&&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(p)return void f.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"},ChannelDefinition:u,SubscriptionDefinition:r,channel:function(t){return new u(t)},subscribe:function(t){var n=new r(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 o=n.topic||"#";t.each(e,function(e){var c=e.channel||i.configuration.DEFAULT_CHANNEL;r.push(i.subscribe({channel:n.channel||i.configuration.DEFAULT_CHANNEL,topic:o,callback:function(n,r){var o=t.clone(r);o.topic=t.isFunction(e.topic)?e.topic(r.topic):e.topic||r.topic,o.channel=c,o.data=n,i.publish(o)}}))})}),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/lib/strategies-add-on/postal.strategies.js b/lib/strategies-add-on/postal.strategies.js new file mode 100644 index 0000000..02aa022 --- /dev/null +++ b/lib/strategies-add-on/postal.strategies.js @@ -0,0 +1,195 @@ +/** + * postal - Pub/Sub library providing wildcard subscriptions, complex message handling, etc. Works server and client-side. + * Author: Jim Cowart (http://freshbrewedcode.com/jimcowart) + * Version: v0.8.11 + * Url: http://github.com/postaljs/postal.js + * License(s): MIT, GPL + */ + +(function (root, factory) { + if (typeof module === "object" && module.exports) { + // Node, or CommonJS-Like environments + module.exports = function (postal) { + return factory(postal, this); + }; + } else if (typeof define === "function" && define.amd) { + // AMD. Register as an anonymous module. + define(["postal"], function (postal) { + return factory(postal, root); + }); + } else { + // Browser globals + root.postal = factory(root.postal, root); + } +}(this, function (postal, global, undefined) { + + (function (SubscriptionDefinition) { + + + var ConsecutiveDistinctPredicate = 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 DistinctPredicate = function () { + var previous = []; + return function (data) { + var isDistinct = !_.any(previous, function (p) { + if (_.isObject(data) || _.isArray(data)) { + return _.isEqual(data, p); + } + return data === p; + }); + if (isDistinct) { + previous.push(data); + } + return isDistinct; + }; + }; + + var strats = { + withDelay: function (ms) { + if (_.isNaN(ms)) { + throw "Milliseconds must be a number"; + } + return { + name: "withDelay", + fn: function (next, data, envelope) { + setTimeout(function () { + next(data, envelope); + }, ms); + } + }; + }, + defer: function () { + return this.withDelay(0); + }, + stopAfter: function (maxCalls, callback) { + if (_.isNaN(maxCalls) || maxCalls <= 0) { + throw "The value provided to disposeAfter (maxCalls) must be a number greater than zero."; + } + var dispose = _.after(maxCalls, callback); + return { + name: "stopAfter", + fn: function (next, data, envelope) { + dispose(); + next(data, envelope); + } + }; + }, + withThrottle: function (ms) { + if (_.isNaN(ms)) { + throw "Milliseconds must be a number"; + } + return { + name: "withThrottle", + fn: _.throttle(function (next, data, envelope) { + next(data, envelope); + }, ms) + }; + }, + withDebounce: function (ms, immediate) { + if (_.isNaN(ms)) { + throw "Milliseconds must be a number"; + } + return { + name: "debounce", + fn: _.debounce(function (next, data, envelope) { + next(data, envelope); + }, ms, !! immediate) + }; + }, + withConstraint: function (pred) { + if (!_.isFunction(pred)) { + throw "Predicate constraint must be a function"; + } + return { + name: "withConstraint", + 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); + } + } + }; + } + }; + + SubscriptionDefinition.prototype.defer = function () { + this.callback.useStrategy(strats.defer()); + return this; + }; + + SubscriptionDefinition.prototype.disposeAfter = function (maxCalls) { + var self = this; + self.callback.useStrategy(strats.stopAfter(maxCalls, function () { + self.unsubscribe.call(self); + })); + return self; + }; + + SubscriptionDefinition.prototype.distinctUntilChanged = function () { + this.callback.useStrategy(strats.distinct()); + return this; + }; + + SubscriptionDefinition.prototype.distinct = function () { + this.callback.useStrategy(strats.distinct({ + all: true + })); + return this; + }; + + SubscriptionDefinition.prototype.once = function () { + this.disposeAfter(1); + return this; + }; + + SubscriptionDefinition.prototype.withConstraint = function (predicate) { + this.callback.useStrategy(strats.withConstraint(predicate)); + return this; + }; + + SubscriptionDefinition.prototype.withDebounce = function (milliseconds, immediate) { + this.callback.useStrategy(strats.withDebounce(milliseconds, immediate)); + return this; + }; + + SubscriptionDefinition.prototype.withDelay = function (milliseconds) { + this.callback.useStrategy(strats.withDelay(milliseconds)); + return this; + }; + + SubscriptionDefinition.prototype.withThrottle = function (milliseconds) { + this.callback.useStrategy(strats.withThrottle(milliseconds)); + return this; + }; + }(postal.SubscriptionDefinition)); + + return postal; +})); \ No newline at end of file diff --git a/lib/strategies-add-on/postal.strategies.min.js b/lib/strategies-add-on/postal.strategies.min.js new file mode 100644 index 0000000..2b76575 --- /dev/null +++ b/lib/strategies-add-on/postal.strategies.min.js @@ -0,0 +1,8 @@ +/** + * postal - Pub/Sub library providing wildcard subscriptions, complex message handling, etc. Works server and client-side. + * Author: Jim Cowart (http://freshbrewedcode.com/jimcowart) + * Version: v0.8.11 + * Url: http://github.com/postaljs/postal.js + * License(s): MIT, GPL + */ +(function(t,n){"object"==typeof module&&module.exports?module.exports=function(t){return n(t,this)}:"function"==typeof define&&define.amd?define(["postal"],function(e){return n(e,t)}):t.postal=n(t.postal,t)})(this,function(t){return function(t){var n=function(){var t;return function(n){var e=!1;return _.isString(n)?(e=n===t,t=n):(e=_.isEqual(n,t),t=_.clone(n)),!e}},e=function(){var t=[];return function(n){var e=!_.any(t,function(t){return _.isObject(n)||_.isArray(n)?_.isEqual(n,t):n===t});return e&&t.push(n),e}},i={withDelay:function(t){if(_.isNaN(t))throw"Milliseconds must be a number";return{name:"withDelay",fn:function(n,e,i){setTimeout(function(){n(e,i)},t)}}},defer:function(){return this.withDelay(0)},stopAfter:function(t,n){if(_.isNaN(t)||0>=t)throw"The value provided to disposeAfter (maxCalls) must be a number greater than zero.";var e=_.after(t,n);return{name:"stopAfter",fn:function(t,n,i){e(),t(n,i)}}},withThrottle:function(t){if(_.isNaN(t))throw"Milliseconds must be a number";return{name:"withThrottle",fn:_.throttle(function(t,n,e){t(n,e)},t)}},withDebounce:function(t,n){if(_.isNaN(t))throw"Milliseconds must be a number";return{name:"debounce",fn:_.debounce(function(t,n,e){t(n,e)},t,!!n)}},withConstraint:function(t){if(!_.isFunction(t))throw"Predicate constraint must be a function";return{name:"withConstraint",fn:function(n,e,i){t.call(this,e,i)&&n.call(this,e,i)}}},distinct:function(t){t=t||{};var i=function(t){return t[0]},r=t.all?new e(i):new n(i);return{name:"distinct",fn:function(t,n,e){r(n)&&t(n,e)}}}};t.prototype.defer=function(){return this.callback.useStrategy(i.defer()),this},t.prototype.disposeAfter=function(t){var n=this;return n.callback.useStrategy(i.stopAfter(t,function(){n.unsubscribe.call(n)})),n},t.prototype.distinctUntilChanged=function(){return this.callback.useStrategy(i.distinct()),this},t.prototype.distinct=function(){return this.callback.useStrategy(i.distinct({all:!0})),this},t.prototype.once=function(){return this.disposeAfter(1),this},t.prototype.withConstraint=function(t){return this.callback.useStrategy(i.withConstraint(t)),this},t.prototype.withDebounce=function(t,n){return this.callback.useStrategy(i.withDebounce(t,n)),this},t.prototype.withDelay=function(t){return this.callback.useStrategy(i.withDelay(t)),this},t.prototype.withThrottle=function(t){return this.callback.useStrategy(i.withThrottle(t)),this}}(t.SubscriptionDefinition),t}); \ No newline at end of file diff --git a/spec/basic.html b/spec/basic.html new file mode 100644 index 0000000..00d6e69 --- /dev/null +++ b/spec/basic.html @@ -0,0 +1,34 @@ + + + + + + + +
+ + + + + + + + + + + + + + + + + + + diff --git a/spec/index.html b/spec/index.html index a258525..1528187 100644 --- a/spec/index.html +++ b/spec/index.html @@ -24,6 +24,8 @@ + + diff --git a/spec/postaljs.spec.js b/spec/postaljs.spec.js index 5fbd3aa..6c81762 100644 --- a/spec/postaljs.spec.js +++ b/spec/postaljs.spec.js @@ -70,76 +70,6 @@ expect( caughtSubscribeEvent ).to.be.ok(); } ); } ); - describe( "When subscribing and ignoring duplicates", function () { - var subInvokedCnt = 0; - before( function () { - channel = postal.channel( "MyChannel" ); - subscription = channel.subscribe( "MyTopic", function ( data ) { - subInvokedCnt++; - } ) - .distinctUntilChanged(); - channel.publish( "MyTopic", "Testing123" ); - channel.publish( "MyTopic", "Testing123" ); - channel.publish( "MyTopic", "Testing123" ); - channel.publish( "MyTopic", "Testing123" ); - channel.publish( "MyTopic", "Testing123" ); - channel.publish( "MyTopic", "Testing123" ); - } ); - after( function () { - postal.utils.reset(); - subInvokedCnt = 0; - } ); - 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 ); - } ); - } ); - describe( "When subscribing with a disposeAfter of 5", function () { - var msgReceivedCnt = 0, subExistsAfter, systemSubscription; - before( function () { - caughtUnsubscribeEvent = false; - channel = postal.channel( "MyChannel" ); - subscription = channel.subscribe( "MyTopic", function () { - msgReceivedCnt++; - }).disposeAfter( 5 ); - systemSubscription = postal.subscribe( { - channel : "postal", - topic : "subscription.*", - callback : function ( data, env ) { - if ( data.event && - data.event === "subscription.removed" && - data.channel === "MyChannel" && - data.topic === "MyTopic" ) { - caughtUnsubscribeEvent = true; - } - } - } ); - channel.publish( "MyTopic", "Testing123" ); - channel.publish( "MyTopic", "Testing123" ); - channel.publish( "MyTopic", "Testing123" ); - channel.publish( "MyTopic", "Testing123" ); - channel.publish( "MyTopic", "Testing123" ); - channel.publish( "MyTopic", "Testing123" ); - subExistsAfter = postal.configuration.bus.subscriptions.MyChannel.MyTopic.length !== 0; - } ); - after( function () { - postal.utils.reset(); - } ); - it( "subscription callback should be invoked 5 times", function () { - expect( msgReceivedCnt ).to.be( 5 ); - } ); - it( "subscription should not exist after unsubscribe", function () { - expect( subExistsAfter ).to.not.be.ok(); - } ); - it( "should have captured unsubscription creation event", function () { - expect( caughtUnsubscribeEvent ).to.be.ok(); - } ); - it( "postal.getSubscribersFor('MyChannel', 'MyTopic') should not return any subscriptions", function () { - expect( postal.utils.getSubscribersFor("MyChannel", "MyTopic").length ).to.be(0); - } ); - } ); describe( "When subscribing with a hierarchical binding, no wildcards", function () { var count = 0, channelB, channelC; before( function () { @@ -221,179 +151,6 @@ expect( count ).to.be( 3 ); } ); } ); - describe( "When subscribing with debounce", function () { - var results = [], debouncedChannel; - before( function () { - debouncedChannel = postal.channel( "DebounceChannel" ); - subscription = debouncedChannel.subscribe( "MyTopic", - function ( data ) { - results.push( data ); - } ).withDebounce( 800 ); - } ); - after( function () { - postal.utils.reset(); - } ); - it( "should have only invoked debounced callback once", function ( done ) { - debouncedChannel.publish( "MyTopic", 1 ); // starts the two second clock on debounce - setTimeout( function () { - debouncedChannel.publish( "MyTopic", 2 ); - }, 20 ); // should not invoke callback - setTimeout( function () { - debouncedChannel.publish( "MyTopic", 3 ); - }, 80 ); // should not invoke callback - setTimeout( function () { - debouncedChannel.publish( "MyTopic", 4 ); - }, 250 ); // should not invoke callback - setTimeout( function () { - debouncedChannel.publish( "MyTopic", 5 ); - }, 500 ); // should not invoke callback - setTimeout( function () { - debouncedChannel.publish( "MyTopic", 6 ); - }, 1000 ); // should invoke callback - setTimeout( function () { - expect( results[0] ).to.be( 6 ); - expect( results.length ).to.be( 1 ); - done(); - }, 1900 ); - } ); - } ); - describe( "When subscribing with defer", function () { - var results = []; - before( function () { - channel = postal.channel( "DeferChannel" ); - - } ); - after( function () { - postal.utils.reset(); - } ); - it( "should have met expected results", function ( done ) { - subscription = channel.subscribe( "MyTopic", - function ( data ) { - results.push( "second" ); - expect( results[0] ).to.be( "first" ); - expect( results[1] ).to.be( "second" ); - done(); - } ).defer(); - channel.publish( "MyTopic", "Testing123" ); - results.push( "first" ); - } ); - } ); - describe( "When subscribing with delay", function () { - var results = []; - before( function () { - channel = postal.channel( "DelayChannel" ); - } ); - after( function () { - postal.utils.reset(); - } ); - it( "should have met expected results", function ( done ) { - subscription = channel.subscribe( "MyTopic", - function ( data ) { - results.push( "second" ); - expect( results[0] ).to.be( "first" ); - expect( results[1] ).to.be( "second" ); - done(); - } ).withDelay( 500 ); - channel.publish( "MyTopic", "Testing123" ); - results.push( "first" ); - } ); - } ); - describe( "When subscribing with once()", function () { - var msgReceivedCnt = 0; - before( function () { - channel = postal.channel( "MyChannel" ); - subscription = channel.subscribe( "MyTopic",function ( data ) { - msgReceivedCnt++; - } ).once(); - channel.publish( "MyTopic", "Testing123" ); - channel.publish( "MyTopic", "Testing123" ); - channel.publish( "MyTopic", "Testing123" ); - channel.publish( "MyTopic", "Testing123" ); - channel.publish( "MyTopic", "Testing123" ); - channel.publish( "MyTopic", "Testing123" ); - } ); - after( function () { - postal.utils.reset(); - } ); - it( "subscription callback should be invoked 1 time", function () { - expect( msgReceivedCnt ).to.be( 1 ); - } ); - } ); - describe( "When subscribing with multiple constraints returning true", function () { - var recvd = false; - before( function () { - channel = postal.channel( "MyChannel" ); - subscription = channel.subscribe( "MyTopic", function ( data ) { - recvd = true; - } ) - .withConstraint(function () { - return false; - }) - .withConstraint(function () { - return false; - }) - .withConstraint(function () { - return true; - }); - channel.publish( "MyTopic", "Testing123" ); - } ); - after( function () { - postal.utils.reset(); - recvd = false; - } ); - 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 one constraint returning false", function () { - var recvd = false; - before( function () { - channel = postal.channel( "MyChannel" ); - subscription = channel.subscribe( "MyTopic", function ( data ) { - recvd = true; - } ) - .withConstraint( function () { - return false; - } ); - channel.publish( "MyTopic", "Testing123" ); - } ); - after( function () { - postal.utils.reset(); - recvd = false; - } ); - it( "should have a constraint on the subscription", function () { - expect( subscription.callback.strategies()[0].name ).to.be( "predicate" ); - } ); - it( "should not have invoked the subscription callback", function () { - expect( recvd ).to.not.be.ok(); - } ); - } ); - describe( "When subscribing with one constraint returning true", function () { - var recvd = false; - before( function () { - channel = postal.channel( "MyChannel" ); - subscription = channel.subscribe( "MyTopic", function ( data ) { - recvd = true; - } ) - .withConstraint( function () { - return true; - } ); - channel.publish( "MyTopic", "Testing123" ); - } ); - after( function () { - postal.utils.reset(); - recvd = false; - } ); - it( "should have a constraint on the subscription", function () { - expect( subscription.callback.strategies()[0].name ).to.be( "predicate" ); - } ); - it( "should have invoked the subscription callback", function () { - expect( recvd ).to.be.ok(); - } ); - } ); describe( "When subscribing with the context being set", function () { var count = 0, obj = { @@ -416,36 +173,6 @@ expect( count ).to.be( 1 ); } ); } ); - describe( "When subscribing with throttle", function () { - var results = [], throttledChannel; - before( function () { - throttledChannel = postal.channel( "ThrottleChannel" ); - subscription = throttledChannel.subscribe( "MyTopic", - function ( data ) { - results.push( data ); - } ).withThrottle( 500 ); - } ); - after( function () { - postal.utils.reset(); - } ); - it( "should have only invoked throttled callback twice", function ( done ) { - throttledChannel.publish( "MyTopic", 1 ); // starts the two second clock on debounce - setTimeout( function () { - throttledChannel.publish( "MyTopic", 800 ); - }, 800 ); // should invoke callback - for ( var i = 0; i < 20; i++ ) { - (function ( x ) { - throttledChannel.publish( "MyTopic", x ); - })( i ); - } - setTimeout( function () { - expect( results[0] ).to.be( 1 ); - expect( results[2] ).to.be( 800 ); - expect( results.length ).to.be( 3 ); - done(); - }, 1500 ); - } ); - } ); describe( "When using global subscribe api", function () { before( function () { subscription = postal.subscribe( { diff --git a/spec/postaljs.strategies.spec.js b/spec/postaljs.strategies.spec.js new file mode 100644 index 0000000..d41b39d --- /dev/null +++ b/spec/postaljs.strategies.spec.js @@ -0,0 +1,288 @@ +/* global describe, postal, it, after, before, expect */ +(function(global) { + var postal = typeof window === "undefined" ? require("../lib/postal.js") : window.postal; + var expect = typeof window === "undefined" ? require("expect.js") : window.expect; + var _ = typeof window === "undefined" ? require("underscore") : window._; + var subscription; + var sub; + var channel; + var caughtSubscribeEvent = false; + var caughtUnsubscribeEvent = false; + + describe("Subscription Creation - Strategies", function(){ + describe( "When subscribing and ignoring duplicates", function () { + var subInvokedCnt = 0; + before( function () { + channel = postal.channel( "MyChannel" ); + subscription = channel.subscribe( "MyTopic", function ( data ) { + subInvokedCnt++; + } ) + .distinctUntilChanged(); + channel.publish( "MyTopic", "Testing123" ); + channel.publish( "MyTopic", "Testing123" ); + channel.publish( "MyTopic", "Testing123" ); + channel.publish( "MyTopic", "Testing123" ); + channel.publish( "MyTopic", "Testing123" ); + channel.publish( "MyTopic", "Testing123" ); + } ); + after( function () { + postal.utils.reset(); + subInvokedCnt = 0; + } ); + 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 ); + } ); + } ); + describe( "When subscribing with a disposeAfter of 5", function () { + var msgReceivedCnt = 0, subExistsAfter, systemSubscription; + before( function () { + caughtUnsubscribeEvent = false; + channel = postal.channel( "MyChannel" ); + subscription = channel.subscribe( "MyTopic", function () { + msgReceivedCnt++; + }).disposeAfter( 5 ); + systemSubscription = postal.subscribe( { + channel : "postal", + topic : "subscription.*", + callback : function ( data, env ) { + if ( data.event && + data.event === "subscription.removed" && + data.channel === "MyChannel" && + data.topic === "MyTopic" ) { + caughtUnsubscribeEvent = true; + } + } + } ); + channel.publish( "MyTopic", "Testing123" ); + channel.publish( "MyTopic", "Testing123" ); + channel.publish( "MyTopic", "Testing123" ); + channel.publish( "MyTopic", "Testing123" ); + channel.publish( "MyTopic", "Testing123" ); + channel.publish( "MyTopic", "Testing123" ); + subExistsAfter = postal.configuration.bus.subscriptions.MyChannel.MyTopic.length !== 0; + } ); + after( function () { + postal.utils.reset(); + } ); + it( "subscription callback should be invoked 5 times", function () { + expect( msgReceivedCnt ).to.be( 5 ); + } ); + it( "subscription should not exist after unsubscribe", function () { + expect( subExistsAfter ).to.not.be.ok(); + } ); + it( "should have captured unsubscription creation event", function () { + expect( caughtUnsubscribeEvent ).to.be.ok(); + } ); + it( "postal.getSubscribersFor('MyChannel', 'MyTopic') should not return any subscriptions", function () { + expect( postal.utils.getSubscribersFor("MyChannel", "MyTopic").length ).to.be(0); + } ); + } ); + describe( "When subscribing with debounce", function () { + var results = [], debouncedChannel; + before( function () { + debouncedChannel = postal.channel( "DebounceChannel" ); + subscription = debouncedChannel.subscribe( "MyTopic", + function ( data ) { + results.push( data ); + } ).withDebounce( 800 ); + } ); + after( function () { + postal.utils.reset(); + } ); + it( "should have only invoked debounced callback once", function ( done ) { + debouncedChannel.publish( "MyTopic", 1 ); // starts the two second clock on debounce + setTimeout( function () { + debouncedChannel.publish( "MyTopic", 2 ); + }, 20 ); // should not invoke callback + setTimeout( function () { + debouncedChannel.publish( "MyTopic", 3 ); + }, 80 ); // should not invoke callback + setTimeout( function () { + debouncedChannel.publish( "MyTopic", 4 ); + }, 250 ); // should not invoke callback + setTimeout( function () { + debouncedChannel.publish( "MyTopic", 5 ); + }, 500 ); // should not invoke callback + setTimeout( function () { + debouncedChannel.publish( "MyTopic", 6 ); + }, 1000 ); // should invoke callback + setTimeout( function () { + expect( results[0] ).to.be( 6 ); + expect( results.length ).to.be( 1 ); + done(); + }, 1900 ); + } ); + } ); + describe( "When subscribing with defer", function () { + var results = []; + before( function () { + channel = postal.channel( "DeferChannel" ); + + } ); + after( function () { + postal.utils.reset(); + } ); + it( "should have met expected results", function ( done ) { + subscription = channel.subscribe( "MyTopic", + function ( data ) { + results.push( "second" ); + expect( results[0] ).to.be( "first" ); + expect( results[1] ).to.be( "second" ); + done(); + } ).defer(); + channel.publish( "MyTopic", "Testing123" ); + results.push( "first" ); + } ); + } ); + describe( "When subscribing with delay", function () { + var results = []; + before( function () { + channel = postal.channel( "DelayChannel" ); + } ); + after( function () { + postal.utils.reset(); + } ); + it( "should have met expected results", function ( done ) { + subscription = channel.subscribe( "MyTopic", + function ( data ) { + results.push( "second" ); + expect( results[0] ).to.be( "first" ); + expect( results[1] ).to.be( "second" ); + done(); + } ).withDelay( 500 ); + channel.publish( "MyTopic", "Testing123" ); + results.push( "first" ); + } ); + } ); + describe( "When subscribing with once()", function () { + var msgReceivedCnt = 0; + before( function () { + channel = postal.channel( "MyChannel" ); + subscription = channel.subscribe( "MyTopic",function ( data ) { + msgReceivedCnt++; + } ).once(); + channel.publish( "MyTopic", "Testing123" ); + channel.publish( "MyTopic", "Testing123" ); + channel.publish( "MyTopic", "Testing123" ); + channel.publish( "MyTopic", "Testing123" ); + channel.publish( "MyTopic", "Testing123" ); + channel.publish( "MyTopic", "Testing123" ); + } ); + after( function () { + postal.utils.reset(); + } ); + it( "subscription callback should be invoked 1 time", function () { + expect( msgReceivedCnt ).to.be( 1 ); + } ); + } ); + describe( "When subscribing with multiple constraints returning true", function () { + var recvd = false; + before( function () { + channel = postal.channel( "MyChannel" ); + subscription = channel.subscribe( "MyTopic", function ( data ) { + recvd = true; + } ) + .withConstraint(function () { + return false; + }) + .withConstraint(function () { + return false; + }) + .withConstraint(function () { + return true; + }); + channel.publish( "MyTopic", "Testing123" ); + } ); + after( function () { + postal.utils.reset(); + recvd = false; + } ); + 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 one constraint returning false", function () { + var recvd = false; + before( function () { + channel = postal.channel( "MyChannel" ); + subscription = channel.subscribe( "MyTopic", function ( data ) { + recvd = true; + } ) + .withConstraint( function () { + return false; + } ); + channel.publish( "MyTopic", "Testing123" ); + } ); + after( function () { + postal.utils.reset(); + recvd = false; + } ); + it( "should have a constraint on the subscription", function () { + expect( subscription.callback.strategies()[0].name ).to.be( "withConstraint" ); + } ); + it( "should not have invoked the subscription callback", function () { + expect( recvd ).to.not.be.ok(); + } ); + } ); + describe( "When subscribing with one constraint returning true", function () { + var recvd = false; + before( function () { + channel = postal.channel( "MyChannel" ); + subscription = channel.subscribe( "MyTopic", function ( data ) { + recvd = true; + } ) + .withConstraint( function () { + return true; + } ); + channel.publish( "MyTopic", "Testing123" ); + } ); + after( function () { + postal.utils.reset(); + recvd = false; + } ); + it( "should have a constraint on the subscription", function () { + expect( subscription.callback.strategies()[0].name ).to.be( "withConstraint" ); + } ); + it( "should have invoked the subscription callback", function () { + expect( recvd ).to.be.ok(); + } ); + } ); + describe( "When subscribing with throttle", function () { + var results = [], throttledChannel; + before( function () { + throttledChannel = postal.channel( "ThrottleChannel" ); + subscription = throttledChannel.subscribe( "MyTopic", + function ( data ) { + results.push( data ); + } ).withThrottle( 500 ); + } ); + after( function () { + postal.utils.reset(); + } ); + it( "should have only invoked throttled callback twice", function ( done ) { + throttledChannel.publish( "MyTopic", 1 ); // starts the two second clock on debounce + setTimeout( function () { + throttledChannel.publish( "MyTopic", 800 ); + }, 800 ); // should invoke callback + for ( var i = 0; i < 20; i++ ) { + (function ( x ) { + throttledChannel.publish( "MyTopic", x ); + })( i ); + } + setTimeout( function () { + expect( results[0] ).to.be( 1 ); + expect( results[2] ).to.be( 800 ); + expect( results.length ).to.be( 3 ); + done(); + }, 1500 ); + } ); + } ); + }); + +}(this)); \ No newline at end of file diff --git a/spec/subDef.strategies.spec.js b/spec/subDef.strategies.spec.js new file mode 100644 index 0000000..0e6851f --- /dev/null +++ b/spec/subDef.strategies.spec.js @@ -0,0 +1,197 @@ +/* global describe, postal, it, after, before, expect */ +(function() { + var postal = typeof window === "undefined" ? require("../lib/postal.js") : window.postal; + var expect = typeof window === "undefined" ? require("expect.js") : window.expect; + var NO_OP = function () {}; + var SubscriptionDefinition = postal.SubscriptionDefinition; + describe( "SubscriptionDefinition - Strategies", function () { + + describe( "When setting distinctUntilChanged", function () { + var sDefa = new SubscriptionDefinition( "TestChannel", "TestTopic", NO_OP ).distinctUntilChanged(); + + 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( "callback should be a strategy", function () { + expect( typeof sDefb.callback.context ).to.be( "function" ); + }); + } ); + + describe( "When deferring the callback", function () { + var results = [], sDefe; + + it( "Should defer the callback", function ( done ) { + sDefe = new SubscriptionDefinition( "TestChannel", "TestTopic", function ( data, env ) { + results.push( data ); + expect( results[0] ).to.be( "first" ); + expect( results[1] ).to.be( "second" ); + expect( env.topic ).to.be( "TestTopic" ); + done(); + } ).defer(); + + sDefe.callback( "second", { topic : "TestTopic" } ); + results.push( "first" ); + } ); + + it( "Should keep the context intact", function ( done ) { + var context = { + key : 1234 + }; + sDefe = new SubscriptionDefinition( "TestChannel", "TestTopic", function ( data, env ) { + expect( this ).to.be( context ); + done(); + } ).withContext(context).defer(); + sDefe.callback.call( sDefe.context, "stuff", { topic : "TestTopic" } ); + } ); + + it( "Should keep the context intact when modified later", function ( done ) { + var context = { + key : 1234 + }; + sDefe = new SubscriptionDefinition( "TestChannel", "TestTopic", function ( data, env ) { + expect( this ).to.be( context ); + done(); + } ).defer().withContext(context); + sDefe.callback.call( sDefe.context, "stuff", { topic : "TestTopic" } ); + } ); + } ); + + describe( "When throttling the callback", function () { + + it( "should have only invoked throttled callback twice", function ( done ) { + var results = [], timeout1, timeout2; + var sDefe = new SubscriptionDefinition( "ThrottleTest", "TestTopic", function ( data ) { + results.push( data ); + } ).withThrottle( 1200, { leading: true } ); + sDefe.callback( 1 ); // starts the clock on throttle + timeout1 = setTimeout( function () { + sDefe.callback( 700 ); + }, 700 ); // should invoke callback + for ( var i = 0; i < 20; i++ ) { + (function ( x ) { + sDefe.callback( x ); + })( i ); + } + timeout2 = setTimeout( function () { + expect( results[0] ).to.be( 1 ); + expect( results.length ).to.be( 2 ); + done(); + }, 1500 ); + } ); + + it( "Should keep the context intact", function( done ) { + var results = [], timeout1, timeout2; + var sDefe = new SubscriptionDefinition( "ThrottleTest", "TestTopic", function ( data ) { + results.push( data ); + } ).withThrottle( 1000, { leading: true } ); + var context = { + key : 'abcd' + }; + sDefe = new SubscriptionDefinition( "ThrottleTest", "TestTopic", function( data, env ) { + expect( this ).to.be( context ); + done(); + } ).withContext( context ).withThrottle( 500 ); + + sDefe.callback.call( sDefe.context, 1 ); + } ); + } ); + + describe( "When delaying the callback", function () { + var results = [], sDefe; + + it( "Should delay the callback", function ( done ) { + sDefe = new SubscriptionDefinition( "DelayTest", "TestTopic", function ( data, env ) { + results.push( data ); + expect( results[0] ).to.be( "first" ); + expect( results[1] ).to.be( "second" ); + expect( env.topic ).to.be( "TestTopic" ); + done(); + } ).withDelay( 200 ); + sDefe.callback( "second", { topic : "TestTopic" } ); + results.push( "first" ); + } ); + + it( "Should keep the context intact", function ( done ) { + var context = { + key : 1234 + }; + sDefe = new SubscriptionDefinition( "DelayTest", "TestTopic", function ( data, env ) { + expect( this ).to.be( context ); + done(); + } ).withContext(context).withDelay( 200 ); + sDefe.callback.call( sDefe.context, "stuff", { topic : "TestTopic" } ); + } ); + } ); + + describe( "When debouncing the callback", function () { + var results = [], + sDefe = new SubscriptionDefinition( "DebounceTest", "TestTopic", function ( data ) { + results.push( data ); + } ).withDebounce( 800 ); + + it( "should have only invoked debounced callback once", function ( done ) { + sDefe.callback( 1 ); // starts the two second clock on debounce + setTimeout( function () { + sDefe.callback( 2 ); + }, 20 ); // should not invoke callback + setTimeout( function () { + sDefe.callback( 3 ); + }, 80 ); // should not invoke callback + setTimeout( function () { + sDefe.callback( 6 ); + }, 800 ); // should invoke callback + setTimeout( function () { + expect( results[0] ).to.be( 6 ); + expect( results.length ).to.be( 1 ); + done(); + }, 1800 ); + } ); + + it( "Should keep the context intact", function ( done ) { + var context = { + key : 5678 + }; + var timeout; + sDefe = new SubscriptionDefinition( "DebounceTest", "TestTopic", function ( data, env ) { + expect( this ).to.be( context ); + clearTimeout(timeout); + done(); + } ).withContext(context).withDebounce( 100 ); + + sDefe.callback.call( sDefe.context, 1 ); + timeout = setTimeout( function () { + sDefe.callback.call( sDefe.context, 2 ); + }, 200 ); // should invoke callback + }); + } ); + + describe( "When self disposing", function () { + var context = { + key : 'abcd' + }; + + it( "Should be inactive", function () { + var sDefe = new SubscriptionDefinition( "DisposeTest", "TestTopic", function ( data, env ) { + } ).withContext(context).disposeAfter( 1 ); + + sDefe.callback.call( sDefe.context, "stuff", { topic : "TestTopic" } ); + + expect( sDefe.inactive ).to.be( true ); + } ); + + it( "Should keep the context intact", function ( done ) { + var sDefe = new SubscriptionDefinition( "DisposeTest", "TestTopic", function ( data, env ) { + expect( this ).to.be( context ); + done(); + } ).withContext(context).disposeAfter( 200 ); + sDefe.callback.call( sDefe.context, "stuff", { topic : "TestTopic" } ); + } ); + } ); + } ); +}()); \ No newline at end of file diff --git a/spec/subscriptionDefinition.spec.js b/spec/subscriptionDefinition.spec.js index 3c72698..12cd4ef 100644 --- a/spec/subscriptionDefinition.spec.js +++ b/spec/subscriptionDefinition.spec.js @@ -27,23 +27,6 @@ } ); } ); - describe( "When setting distinctUntilChanged", function () { - var sDefa = new SubscriptionDefinition( "TestChannel", "TestTopic", NO_OP ).distinctUntilChanged(); - - 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( "callback should be a strategy", function () { - expect( typeof sDefb.callback.context ).to.be( "function" ); - }); - } ); - describe( "When setting the context", function () { var obj = { name : "Rose" }, name, @@ -72,176 +55,5 @@ expect( sDefe.callback ).to.be( fn ); } ); } ); - - describe( "When deferring the callback", function () { - var results = [], sDefe; - - it( "Should defer the callback", function ( done ) { - sDefe = new SubscriptionDefinition( "TestChannel", "TestTopic", function ( data, env ) { - results.push( data ); - expect( results[0] ).to.be( "first" ); - expect( results[1] ).to.be( "second" ); - expect( env.topic ).to.be( "TestTopic" ); - done(); - } ).defer(); - - sDefe.callback( "second", { topic : "TestTopic" } ); - results.push( "first" ); - } ); - - it( "Should keep the context intact", function ( done ) { - var context = { - key : 1234 - }; - sDefe = new SubscriptionDefinition( "TestChannel", "TestTopic", function ( data, env ) { - expect( this ).to.be( context ); - done(); - } ).withContext(context).defer(); - sDefe.callback.call( sDefe.context, "stuff", { topic : "TestTopic" } ); - } ); - - it( "Should keep the context intact when modified later", function ( done ) { - var context = { - key : 1234 - }; - sDefe = new SubscriptionDefinition( "TestChannel", "TestTopic", function ( data, env ) { - expect( this ).to.be( context ); - done(); - } ).defer().withContext(context); - sDefe.callback.call( sDefe.context, "stuff", { topic : "TestTopic" } ); - } ); - } ); - - describe( "When throttling the callback", function () { - - it( "should have only invoked throttled callback twice", function ( done ) { - var results = [], timeout1, timeout2; - var sDefe = new SubscriptionDefinition( "ThrottleTest", "TestTopic", function ( data ) { - results.push( data ); - } ).withThrottle( 1200, { leading: true } ); - sDefe.callback( 1 ); // starts the clock on throttle - timeout1 = setTimeout( function () { - sDefe.callback( 700 ); - }, 700 ); // should invoke callback - for ( var i = 0; i < 20; i++ ) { - (function ( x ) { - sDefe.callback( x ); - })( i ); - } - timeout2 = setTimeout( function () { - expect( results[0] ).to.be( 1 ); - expect( results.length ).to.be( 2 ); - done(); - }, 1500 ); - } ); - - it( "Should keep the context intact", function( done ) { - var results = [], timeout1, timeout2; - var sDefe = new SubscriptionDefinition( "ThrottleTest", "TestTopic", function ( data ) { - results.push( data ); - } ).withThrottle( 1000, { leading: true } ); - var context = { - key : 'abcd' - }; - sDefe = new SubscriptionDefinition( "ThrottleTest", "TestTopic", function( data, env ) { - expect( this ).to.be( context ); - done(); - } ).withContext( context ).withThrottle( 500 ); - - sDefe.callback.call( sDefe.context, 1 ); - } ); - } ); - - describe( "When delaying the callback", function () { - var results = [], sDefe; - - it( "Should delay the callback", function ( done ) { - sDefe = new SubscriptionDefinition( "DelayTest", "TestTopic", function ( data, env ) { - results.push( data ); - expect( results[0] ).to.be( "first" ); - expect( results[1] ).to.be( "second" ); - expect( env.topic ).to.be( "TestTopic" ); - done(); - } ).withDelay( 200 ); - sDefe.callback( "second", { topic : "TestTopic" } ); - results.push( "first" ); - } ); - - it( "Should keep the context intact", function ( done ) { - var context = { - key : 1234 - }; - sDefe = new SubscriptionDefinition( "DelayTest", "TestTopic", function ( data, env ) { - expect( this ).to.be( context ); - done(); - } ).withContext(context).withDelay( 200 ); - sDefe.callback.call( sDefe.context, "stuff", { topic : "TestTopic" } ); - } ); - } ); - - describe( "When debouncing the callback", function () { - var results = [], - sDefe = new SubscriptionDefinition( "DebounceTest", "TestTopic", function ( data ) { - results.push( data ); - } ).withDebounce( 800 ); - - it( "should have only invoked debounced callback once", function ( done ) { - sDefe.callback( 1 ); // starts the two second clock on debounce - setTimeout( function () { - sDefe.callback( 2 ); - }, 20 ); // should not invoke callback - setTimeout( function () { - sDefe.callback( 3 ); - }, 80 ); // should not invoke callback - setTimeout( function () { - sDefe.callback( 6 ); - }, 800 ); // should invoke callback - setTimeout( function () { - expect( results[0] ).to.be( 6 ); - expect( results.length ).to.be( 1 ); - done(); - }, 1800 ); - } ); - - it( "Should keep the context intact", function ( done ) { - var context = { - key : 5678 - }; - var timeout; - sDefe = new SubscriptionDefinition( "DebounceTest", "TestTopic", function ( data, env ) { - expect( this ).to.be( context ); - clearTimeout(timeout); - done(); - } ).withContext(context).withDebounce( 100 ); - - sDefe.callback.call( sDefe.context, 1 ); - timeout = setTimeout( function () { - sDefe.callback.call( sDefe.context, 2 ); - }, 200 ); // should invoke callback - }); - } ); - - describe( "When self disposing", function () { - var context = { - key : 'abcd' - }; - - it( "Should be inactive", function () { - var sDefe = new SubscriptionDefinition( "DisposeTest", "TestTopic", function ( data, env ) { - } ).withContext(context).disposeAfter( 1 ); - - sDefe.callback.call( sDefe.context, "stuff", { topic : "TestTopic" } ); - - expect( sDefe.inactive ).to.be( true ); - } ); - - it( "Should keep the context intact", function ( done ) { - var sDefe = new SubscriptionDefinition( "DisposeTest", "TestTopic", function ( data, env ) { - expect( this ).to.be( context ); - done(); - } ).withContext(context).disposeAfter( 200 ); - sDefe.callback.call( sDefe.context, "stuff", { topic : "TestTopic" } ); - } ); - } ); } ); }()); \ No newline at end of file diff --git a/src/Api.js b/src/Api.js index 7c1141b..a3be77d 100644 --- a/src/Api.js +++ b/src/Api.js @@ -5,8 +5,7 @@ postal = { bus : localBus, resolver : bindingsResolver, DEFAULT_CHANNEL : "/", - SYSTEM_CHANNEL : "postal", - strategies : strats + SYSTEM_CHANNEL : "postal" }, ChannelDefinition : ChannelDefinition, diff --git a/src/ConsecutiveDistinctPredicate.js b/src/ConsecutiveDistinctPredicate.js deleted file mode 100644 index e94ed48..0000000 --- a/src/ConsecutiveDistinctPredicate.js +++ /dev/null @@ -1,16 +0,0 @@ -/*jshint -W098 */ -var ConsecutiveDistinctPredicate = 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; - }; -}; \ No newline at end of file diff --git a/src/DistinctPredicate.js b/src/DistinctPredicate.js deleted file mode 100644 index 16ad115..0000000 --- a/src/DistinctPredicate.js +++ /dev/null @@ -1,17 +0,0 @@ -/*jshint -W098 */ -var DistinctPredicate = function () { - var previous = []; - - return function ( data ) { - var isDistinct = !_.any( previous, function ( p ) { - if ( _.isObject( data ) || _.isArray( data ) ) { - return _.isEqual( data, p ); - } - return data === p; - } ); - if ( isDistinct ) { - previous.push( data ); - } - return isDistinct; - }; -}; \ No newline at end of file diff --git a/src/SubscriptionDefinition.js b/src/SubscriptionDefinition.js index ed8ae4b..3f5e07e 100644 --- a/src/SubscriptionDefinition.js +++ b/src/SubscriptionDefinition.js @@ -20,74 +20,6 @@ SubscriptionDefinition.prototype = { } }, - 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; - self.callback.useStrategy(postal.configuration.strategies.after(maxCalls, function() { - self.unsubscribe.call(self); - })); - return self; - }, - - distinctUntilChanged : function () { - this.callback.useStrategy(postal.configuration.strategies.distinct()); - return this; - }, - - distinct : function () { - this.callback.useStrategy(postal.configuration.strategies.distinct({ all: true })); - 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; - }, - - withContext : function ( context ) { - this.callback.context(context); - return this; - }, - - withDebounce : function ( milliseconds, immediate ) { - if ( _.isNaN( milliseconds ) ) { - throw "Milliseconds must be a number"; - } - this.callback.useStrategy(postal.configuration.strategies.debounce(milliseconds, immediate)); - return this; - }, - - withDelay : function ( milliseconds ) { - if ( _.isNaN( milliseconds ) ) { - throw "Milliseconds must be a number"; - } - this.callback.useStrategy(postal.configuration.strategies.setTimeout(milliseconds)); - return this; - }, - - withThrottle : function ( milliseconds ) { - if ( _.isNaN( milliseconds ) ) { - throw "Milliseconds must be a number"; - } - this.callback.useStrategy(postal.configuration.strategies.throttle(milliseconds)); - return this; - }, - subscribe : function ( callback ) { this.callback = callback; this.callback = new Strategy({ @@ -97,5 +29,12 @@ SubscriptionDefinition.prototype = { lazyInit : true }); return this; + }, + + withContext : function ( context ) { + this.callback.context(context); + return this; } -}; \ No newline at end of file +}; + + diff --git a/src/postal.basic.js b/src/postal.basic.js new file mode 100644 index 0000000..7872407 --- /dev/null +++ b/src/postal.basic.js @@ -0,0 +1,36 @@ +/*jshint -W098 */ +(function ( root, factory ) { + if ( typeof module === "object" && module.exports ) { + // Node, or CommonJS-Like environments + module.exports = factory( require( "underscore" ), this ); + } else if ( typeof define === "function" && define.amd ) { + // AMD. Register as an anonymous module. + define( ["underscore"], function ( _ ) { + return factory( _, root ); + } ); + } else { + // Browser globals + root.postal = factory( root._, root ); + } +}( this, function ( _, global, undefined ) { + + var postal; + var prevPostal = global.postal; + + //import("strategy.js"); + //import("ChannelDefinition.js"); + //import("SubscriptionDefinition.js"); + //import("AmqpBindingsResolver.js"); + //import("LocalBus.js"); + //import("Api.js"); + + /*jshint -W106 */ + if ( global && Object.prototype.hasOwnProperty.call( global, "__postalReady__" ) && _.isArray( global.__postalReady__ ) ) { + while(global.__postalReady__.length) { + global.__postalReady__.shift().onReady(postal); + } + } + /*jshint +W106 */ + + return postal; +} )); \ No newline at end of file diff --git a/src/postal.js b/src/postal.js index 05f32cd..df816d7 100644 --- a/src/postal.js +++ b/src/postal.js @@ -17,12 +17,10 @@ var postal; var prevPostal = global.postal; + //import("SubscriptionDefinition.js"); //import("strategy.js"); //import("strategies.js"); - //import("ConsecutiveDistinctPredicate.js"); - //import("DistinctPredicate.js"); //import("ChannelDefinition.js"); - //import("SubscriptionDefinition.js"); //import("AmqpBindingsResolver.js"); //import("LocalBus.js"); //import("Api.js"); diff --git a/src/postal.strategies.js b/src/postal.strategies.js new file mode 100644 index 0000000..490ee83 --- /dev/null +++ b/src/postal.strategies.js @@ -0,0 +1,24 @@ +/*jshint -W098 */ +(function ( root, factory ) { + if ( typeof module === "object" && module.exports ) { + // Node, or CommonJS-Like environments + module.exports = function(postal) { + return factory( postal, this ); + }; + } else if ( typeof define === "function" && define.amd ) { + // AMD. Register as an anonymous module. + define( ["postal"], function ( postal ) { + return factory( postal, root ); + } ); + } else { + // Browser globals + root.postal = factory( root.postal, root ); + } +}( this, function ( postal, global, undefined ) { + + (function(SubscriptionDefinition) { + //import("strategies.js"); + }(postal.SubscriptionDefinition)); + + return postal; +} )); \ No newline at end of file diff --git a/src/strategies.js b/src/strategies.js index d2cc60f..da192a6 100644 --- a/src/strategies.js +++ b/src/strategies.js @@ -1,8 +1,44 @@ -/* global DistinctPredicate,ConsecutiveDistinctPredicate */ +/* global DistinctPredicate,ConsecutiveDistinctPredicate,SubscriptionDefinition,postal */ +/*jshint -W098 */ +var ConsecutiveDistinctPredicate = 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 DistinctPredicate = function () { + var previous = []; + return function ( data ) { + var isDistinct = !_.any( previous, function ( p ) { + if ( _.isObject( data ) || _.isArray( data ) ) { + return _.isEqual( data, p ); + } + return data === p; + } ); + if ( isDistinct ) { + previous.push( data ); + } + return isDistinct; + }; +}; + var strats = { - setTimeout: function(ms) { + withDelay: function(ms) { + if ( _.isNaN( ms ) ) { + throw "Milliseconds must be a number"; + } return { - name: "setTimeout", + name: "withDelay", fn: function (next, data, envelope) { setTimeout(function () { next(data, envelope); @@ -10,25 +46,37 @@ var strats = { } }; }, - after: function(maxCalls, callback) { + defer: function() { + return this.withDelay(0); + }, + stopAfter: function(maxCalls, callback) { + if ( _.isNaN( maxCalls ) || maxCalls <= 0 ) { + throw "The value provided to disposeAfter (maxCalls) must be a number greater than zero."; + } var dispose = _.after(maxCalls, callback); return { - name: "after", + name: "stopAfter", fn: function (next, data, envelope) { dispose(); next(data, envelope); } }; }, - throttle : function(ms) { + withThrottle : function(ms) { + if ( _.isNaN( ms ) ) { + throw "Milliseconds must be a number"; + } return { - name: "throttle", + name: "withThrottle", fn: _.throttle(function(next, data, envelope) { next(data, envelope); }, ms) }; }, - debounce: function(ms, immediate) { + withDebounce: function(ms, immediate) { + if ( _.isNaN( ms ) ) { + throw "Milliseconds must be a number"; + } return { name: "debounce", fn: _.debounce(function(next, data, envelope) { @@ -36,9 +84,12 @@ var strats = { }, ms, !!immediate) }; }, - predicate: function(pred) { + withConstraint: function(pred) { + if ( !_.isFunction( pred ) ) { + throw "Predicate constraint must be a function"; + } return { - name: "predicate", + name: "withConstraint", fn: function(next, data, envelope) { if(pred.call(this, data, envelope)) { next.call(this, data, envelope); @@ -63,4 +114,52 @@ var strats = { } }; } +}; + +SubscriptionDefinition.prototype.defer = function () { + this.callback.useStrategy(strats.defer()); + return this; +}; + +SubscriptionDefinition.prototype.disposeAfter = function ( maxCalls ) { + var self = this; + self.callback.useStrategy(strats.stopAfter(maxCalls, function() { + self.unsubscribe.call(self); + })); + return self; +}; + +SubscriptionDefinition.prototype.distinctUntilChanged = function () { + this.callback.useStrategy(strats.distinct()); + return this; +}; + +SubscriptionDefinition.prototype.distinct = function () { + this.callback.useStrategy(strats.distinct({ all : true })); + return this; +}; + +SubscriptionDefinition.prototype.once = function () { + this.disposeAfter(1); + return this; +}; + +SubscriptionDefinition.prototype.withConstraint = function ( predicate ) { + this.callback.useStrategy(strats.withConstraint(predicate)); + return this; +}; + +SubscriptionDefinition.prototype.withDebounce = function ( milliseconds, immediate ) { + this.callback.useStrategy(strats.withDebounce(milliseconds, immediate)); + return this; +}; + +SubscriptionDefinition.prototype.withDelay = function ( milliseconds ) { + this.callback.useStrategy(strats.withDelay(milliseconds)); + return this; +}; + +SubscriptionDefinition.prototype.withThrottle = function ( milliseconds ) { + this.callback.useStrategy(strats.withThrottle(milliseconds)); + return this; }; \ No newline at end of file