/* postal.js Author: Jim Cowart License: Dual licensed MIT (http://www.opensource.org/licenses/mit-license) & GPL (http://www.opensource.org/licenses/gpl-license) Version 0.0.1 */ (function(global, undefined) { var isArray = function(value) { var s = typeof value; if (s === 'object') { if (value) { if (typeof value.length === 'number' && !(value.propertyIsEnumerable('length')) && typeof value.splice === 'function') { s = 'array'; } } } return s === 'array'; }, slice = [].slice, DEFAULT_EXCHANGE = "/", SYSTEM_EXCHANGE = "postal", NORMAL_MODE = "Normal", CAPTURE_MODE = "Capture", REPLAY_MODE = "Replay", POSTAL_MSG_STORE_KEY = "postal.captured", _forEachKeyValue = function(object, callback) { for(var x in object) { if(object.hasOwnProperty(x)) { callback(x, object[x]); } } }; var Bus = function() { var _regexify = function(topic) { if(!this.cache[topic]) { this.cache[topic] = topic.replace(".", "\.").replace("*", ".*"); } return this.cache[topic]; }.bind(this), _isTopicMatch = function(topic, comparison) { if(!this.cache[topic + '_' + comparison]) { this.cache[topic + '_' + comparison] = topic === comparison || (comparison.indexOf("*") !== -1 && topic.search(_regexify(comparison)) !== -1) || (topic.indexOf("*") !== -1 && comparison.search(_regexify(topic)) !== -1); } return this.cache[topic + '_' + comparison]; }.bind(this); this.context = undefined; this.cache = {}; this.wireTaps = []; this.subscriptions = {}; this.subscriptions[DEFAULT_EXCHANGE] = {}; this.publish = function(exchange, topic, data) { this.wireTaps.forEach(function(tap) { tap({ exchange: exchange, topic: topic, data: data, timeStamp: new Date() }); }); _forEachKeyValue(this.subscriptions[exchange],function(subTpc, subs) { if(_isTopicMatch(topic, subTpc)) { subs.forEach(function(sub) { if(typeof sub.callback === 'function') { sub.callback.apply(sub.context, [data]); sub.onFired(); } }); } }); }; this.mode = NORMAL_MODE; this[NORMAL_MODE] = { setup: function() { this.mode = NORMAL_MODE; this.context = undefined; }.bind(this), teardown: function() { // no-op }.bind(this) }; this.init = function() { this[NORMAL_MODE](); var systemEx = this.subscriptions[SYSTEM_EXCHANGE] || {}; this.subscriptions = {}; this.subscriptions[DEFAULT_EXCHANGE] = {}; this.subscriptions[SYSTEM_EXCHANGE] = systemEx; this.cache = {}; this.wireTaps = []; }; }; var bus = new Bus(), hashCheck = function() { var regex = /postalmode=(\w+)&*/i, match = regex.exec(window.location.hash), mode; if(match && match.length >= 2) { mode = match[1]; } if(mode) { postal.publish(postal.SYSTEM_EXCHANGE, "mode.set", {mode: mode }); }; }; $(function(){ hashCheck(); window.addEventListener("hashchange", hashCheck) }); var Postal = function() { this.getMode = function() { return bus.mode; }; /* options object has the following optional members: { once: {true || false (true indicates a fire-only-once subscription}, priority: {integer value - lower value == higher priority}, context: {the "this" context for the callback invocation} } */ this.subscribe = function(exchange, topic, callback, options) { var _args = slice.call(arguments, 0), _exchange, _topicList, // we allow multiple topics to be subscribed in one call., _once = false, _subData = { callback: function() { /* placeholder no-op */ }, priority: 50, context: null, onFired: function() { /* placeholder no-op */ } }, _idx, _found; if(_args.length === 2) { // expecting topic and callback _exchange = DEFAULT_EXCHANGE; _topicList = _args[0].split(/\s/); _subData.callback = _args[1]; } else if(_args.length === 3 && typeof _args[2] === 'function') { // expecting exchange, topic, callback _exchange = exchange; _topicList = _args[1].split(/\s/); _subData.callback = _args[2]; } else if(_args.length === 3 && typeof _args[2] === 'object') { // expecting topic, callback and options _exchange = DEFAULT_EXCHANGE; _topicList = _args[0].split(/\s/); _subData.callback = _args[1]; _subData.priority = _args[2].priority ? _args[2].priority : 50; _subData.context = _args[2].context ? _args[2].context : null; _once = _args[2].once ? _args[2].once : false; } else { _exchange = exchange; _topicList = topic.split(/\s/); _subData.callback = callback; _subData.priority = options.priority ? options.priority : 50; _subData.context = options.context ? options.context : null; _once = options.once ? options.once : false; } if(_once) { _subData.onFired = function() { this.unsubscribe.apply(this,[_exchange, _topicList.join(' '), _subData.callback]); }.bind(this); } if(!bus.subscriptions[_exchange]) { bus.subscriptions[_exchange] = {}; } _topicList.forEach(function(tpc) { if(!bus.subscriptions[_exchange][tpc]) { bus.subscriptions[_exchange][tpc] = [_subData]; } else { _idx = bus.subscriptions[_exchange][tpc].length - 1; if(bus.subscriptions[_exchange][tpc].filter(function(sub) { return sub === callback; }).length === 0) { for(; _idx >= 0; _idx--) { if(bus.subscriptions[_exchange][tpc][_idx].priority <= _subData.priority) { bus.subscriptions[_exchange][tpc].splice(_idx + 1, 0, _subData); _found = true; break; } } if(!_found) { bus.subscriptions[_exchange][tpc].unshift(_subData); } } } }, this); // return callback for un-subscribing... return function() { this.unsubscribe(_exchange, _topicList.join(' '), _subData.callback); }.bind(this); }; this.unsubscribe = function(exchange, topic, callback) { var _args = slice.call(arguments,0), _exchange, _topicList, // we allow multiple topics to be unsubscribed in one call. _callback; if(_args.length === 2) { _exchange = DEFAULT_EXCHANGE; _topicList = _args[0].split(/\s/); _callback = _args[1]; } else if(_args.length === 3) { _exchange = exchange; _topicList = topic.split(/\s/); _callback = callback; } _topicList.forEach(function(tpc) { if(bus.subscriptions[_exchange][tpc]) { var _len = bus.subscriptions[_exchange][tpc].length, _idx = 0; for ( ; _idx < _len; _idx++ ) { if (bus.subscriptions[_exchange][tpc][_idx].callback === callback) { bus.subscriptions[_exchange][tpc].splice( _idx, 1 ); break; } } } },this); }; this.publish = function(exchange, topic, data) { var _args = slice.call(arguments,0), _exchange, _topicList, _data; if(_args.length === 1) { _exchange = DEFAULT_EXCHANGE; _topicList = _args[0].split(/\s/); _data = {}; } else if(_args.length === 2 && typeof _args[1] === 'object') { _exchange = DEFAULT_EXCHANGE; _topicList = _args[0].split(/\s/); _data = _args[1] || {}; } else if(_args.length === 2) { _exchange = _args[0]; _topicList = _args[1].split(/\s/); _data = {}; } else { _exchange = exchange; _topicList = topic.split(/\s/); _data = data || {}; } if(bus.mode !== REPLAY_MODE || (bus.mode === REPLAY_MODE && _exchange === SYSTEM_EXCHANGE)) { _topicList.forEach(function(tpc){ bus.publish(_exchange, tpc, _data); }); } }; this.reset = function() { bus.init(); }; this.addBusBehavior = function(behaviorName, setup, teardown) { if(!bus[behaviorName]) { bus[behaviorName] = {}; } bus[behaviorName].setup = function() { bus.mode = behaviorName; setup(bus); }; if(teardown) { bus[behaviorName].teardown = function() { teardown(bus); } } else { bus[behaviorName].teardown = function() { /* no-op */ } } }; this.subscribe(SYSTEM_EXCHANGE, "mode.set", function(data) { if(data.mode && bus[data.mode]) { bus[bus.mode].teardown(); bus[data.mode].setup(data); } }.bind(this)); this.addWireTap = function(callback) { bus.wireTaps.push(callback); return function() { var idx = bus.wireTaps.indexOf(callback); if(idx !== -1) { bus.wireTaps.splice(idx,1); } }; }; }; var postal = new Postal(); global.postal = postal; postal.DEFAULT_EXCHANGE = DEFAULT_EXCHANGE; postal.SYSTEM_EXCHANGE = SYSTEM_EXCHANGE; postal.NORMAL_MODE = NORMAL_MODE; postal.CAPTURE_MODE = CAPTURE_MODE; postal.REPLAY_MODE = REPLAY_MODE; postal.POSTAL_MSG_STORE_KEY = POSTAL_MSG_STORE_KEY; })(window);