UNPKG

bubpubsub

Version:

a pubsub system with bubbling, replies and persistence

817 lines (782 loc) 29.9 kB
/* ---------------------------------------------------------------------------- * ---------------------------------------------------------------------------- * TITLE: BubPubSub - the bubbling PubSub system with a twist! * * DESCRIPTION: * - this code is part of the VisualWeb Project by * - LinkCloud ( http://www.mylinkcloud.com ) * - ViSERiON UG (haftungsbeschraenkt) ( http://www.viserion.com ) * - K!Lab Gmbh ( http://www.klab-berlin.com ) * - free for use, reuse, commercial use, forking, editing, whatever... * * PubSub systems are great and widely used today. However, we wanted something that emulates the event-bubbling architecture of DOM events. * Next to publications being able to bubble, subscribers can also choose whether they would like to receive bubbled publications. * Furthermore, the system has the ability to LOG publications and retrieve them later. This comes in handy when using asynchronously loaded code * or instantiation that needs to refer to old publications. * * We created this little thing for use in our framework project. We use it on the serverside with node.js and on the client as well . * We will release and update both versions for community use and happy forking. The code is certainly not brilliant or crazy fast, but we tried to * have it well-annotated and easy to understand (and extend). * We're very happy to learn about ideas, improvements, bugs (and fixes!) .. to get in contact with us, please use twitter ( @itsatony ) * * * BASED ON: * various sources .. the pubsub idea ... * * FILE (in VisualWeb): * /public/lib/bubpubsub.js * * AUTHORS: * Toni Wagner @itsatony * Frederik Rudeck @polym0rph * Paul Nordmann * * DEPENDENCIES: * - dependencies were removed ... this is totally independent now . * - it should integrate into NodeJS on the server-side or any browser-side with javascript enabled. * * TESTS: * (start code) * bubpubsub.subscribe('/aaa', function(data) { alert(data.test); }, true); * bubpubsub.publish('aaa/bbb', {test:1}, {scope: {local:true}, persist:true,bubble:true}, publisher); * bubpubsub.publish('aaa', {test:2}, {scope: {local:true}, persist:false,bubble:true}, publisher); * bubpubsub.publish('aaa/bbb', {test:3}, {scope: document, persist:true,bubble:false}, publisher); * (end) * * VERSION HISTORY: * - v0.10.0 2014-09-04 * - removed chain-subscription-calling * - reorganized code * - moved debugging to boolean * - removed announce * - v0.9.0 2013-04-09 * - added forward function * - v0.8.7 22.11.12 * - introduced silent option as a per-publication flag to avoice console logging * - v0.8.6 22.11.12 * - bugFix * - v0.8.5 19.11.12 * - bugFixes .. * - v0.8.4 15.11.12 * - bugFixes .. * - old parameters for subscribe will be deprecated announcement * - v0.8.3 15.11.12 * - introduced autoUnsubscribe to subscribe options ! * - optional try-catch around subscribers * - scope setting for subscribers * - introduced limits for error and publication logs to 10000 items in order to limit memory usage. * - v0.82 28.07.2012 * - bugfix in chain default and if check. * - v0.81 27.07.12 * - unsubscribe updated to do a better job of namespace (& memory) cleanup * - v0.80 22.06.12 * - added the option to do chaining. this needs quite some field-testing ;) * - added chainDelay config setting for chained publications to allow breathing-time for the cpu * - v0.71 15.06.12 * - made the code a bit more beautiful * - introduced forceUniqueSubscriber to allow replacing subscriber methods by Id * - replaced isFunction method by typeof test * - removed randomString method * - switched from bad style == to good style === operators... * - changed licence * - v0.70 21.03.12 * - renamed * - fused client (jQuery) and server (nodeJS) versions * - series of bugfixes * - v0.60 29.10.11 * - removed dependencies from jQuery. * - enabled using the same file for client- and nodeJS server-side implementation * - added the reply function explicitely * * URLs: * - blogpost/homepage: <http://coffeelog.itsatony.com/bubPubSub> * - github: <https://github.com/itsatony/bubPubSub> * * ---------------------------------------------------------------------------- */ /* -------------------------------------------------------------------------- * -------------------------------------------------------------------------- * class: aBubPubSub * you should not create your own instances of this... * - (client-sided) upon loading of the js file bubPubSub will be available to you globally * - (server-side) require it with nodeJS * * returns: * this - {object} itself. * * example: * > aBubPubSub.publish('/politics/europe', { content: 'this is wonderfool' }, { bubble:true, persist:true }, 'myTestPublisher'); * * --------------------------------------------------------------------------- */ aBubPubSub = function(id) { var myScope = this; this.version = '0.10.0'; this.id = (typeof id === 'string') ? id :'bubPubSub_' + Date.now(); this.defaults = { bubble: true, persist: false, getBubbles: true, getPersists: false, scope: myScope, debugging: false, forceUniqueSubscriber: true, skipOverFailedSubscribers: true, throwErrors: false, catchSubscriberErrors: false }; this.errors = []; this.publicationChannels = {}; this.publicationLog = []; this.unslasher = new RegExp('^[/]+|[/]+$|(/)+/', 'g'); return this; }; /* -------------------------------------------------------------------------- * -------------------------------------------------------------------------- * method: aBubPubSub.prototype.publish * Publish some data on a named topic. * * parameters: * topic - {string} * The channel to publish on. if no / are given, 'root' will be assumed. * data - {array} * The data to publish. Each array item is converted into an ordered * argument on the subscribed functions. * a single attribute is added to the given data: originalTopic (it stores the complete topic, giving access to the full Topic for Subscribers of lower (bubbled) branches) * settings - {object} has four attributes: scope, persist, bubble, silent : * * (start code) * - scope {object} defaults to *this*; * if you want a different scope in the callbacks, just supply it here * - persist {boolean} defaults to *false*; * if true the publication will be saved in the this.publicationLog object (with a timestamp attached) * - bubble {object} defaults to *false*; * if *true*, a topic will bubble up it's branch (*ATTENTION!* while being executed in sequence the bubbling does NOT WAIT for one level of execution before the next is triggered!): * '/politics/europe/internet/myOpinion' would activate callbacks for * [1] '/politics/europe/internet/myOpinion', * [2] then '/politics/europe/internet', * [3] then '/politics/europe', * [4] then '/politics', * [5] and '/' * (end) * * publisher - {object||string} * the identifier or object you want to expose to all subscribers. * onReply - {function} this will be called if a listener uses sendReply(answer). defaults to { return false; } * * returns: * result - {object} with 5 attributes: topic, data, settings, timestamp, publisher : * (start code) * { * 'topic' : {string} topic, * 'data' : {object} data, * 'settings' : {object} settings, * 'timestamp': {date} Date.now(), * 'publisher' : {string||object} publisher * } * (end) * * example: * (start code) aBubPubSub.publish( '/politics/europe', { content: 'this is wonderfool', }, { bubble:true, persist:true }, 'myTestPublisher' ); * (end) * * --------------------------------------------------------------------------- */ aBubPubSub.prototype.publish = function(topic, data, settings, publisher, onReply) { var self = this; var timestamp = Date.now(); var config = { scope: this.defaults.scope, persist: this.defaults.persist, bubble: this.defaults.bubble, silent: false }; if (typeof settings === 'object') { if (typeof settings.scope === 'object') { config.scope = settings.scope; } if (typeof settings.persist === 'boolean') { config.persist = settings.persist; } if (typeof settings.bubble === 'boolean') { config.bubble = settings.bubble; } if (typeof settings.silent === 'boolean') { config.silent = settings.silent; } }; if (typeof onReply !== 'function') { onReply = false; //function(answer) { return false; }; } topic = this.__stripSlashes(topic); data.originalTopic = topic; // let's hope this is unique ;) should be unique-enough at least... data.uniquePublicationId = 'pub_' + timestamp + '_' + Math.floor(Math.random()*100000); // debugging help this.log(config, '[' + self.id + '] publication: #' + data.uniquePublicationId + ' (' + topic + ') | bubbling=' + config.bubble + ' | persistent=' + config.persist + ' | publisher=' + publisher); // -- prepare all publication branches var topicTree = [ topic ]; // support bubbling if (config.bubble === true) { var BranchPoint = topic.lastIndexOf('/'); while (BranchPoint > -1) { var shortenedTopic = topic.substr(0,BranchPoint); if (shortenedTopic !== '') { topicTree.push(shortenedTopic); } BranchPoint = shortenedTopic.lastIndexOf('/'); } } // allow subscriptions to root topicTree.push('/'); // -- fire all subscribers var subscribersFired = []; var topicCount = topicTree.length; for (var branch = 0; branch < topicCount; branch+=1) { for (var subId in this.publicationChannels[topicTree[branch]]) { var subscriber = this.publicationChannels[topicTree[branch]][subId]; if (branch !== 0 && subscriber.getBubbles !== true) { continue; } else { var subscriptionIdObject = { 'topic': topicTree[branch], 'id': subId }; this.log(config, '[' + self.id + '] subscription got #' + data.uniquePublicationId + '. (' + topicTree[branch] + ') from (' + data.originalTopic + ') | getBubbles=' + subscriber.getBubbles + ' | subscriber=' + subId); // specific scope given by subscriber beats our default scope and the publishers scope. var subScope = (typeof subscriber.scope === 'object') ? subscriber.scope : config.scope; if (self.defaults.catchSubscriberErrors === true) { // catching and reporting errors ... useful for debugging try { subscriber.callback.apply(subScope, [data, topicTree[branch], publisher, onReply, subscriptionIdObject, settings ]); } catch(error) { this.onError(error); } if (self.defaults.throwErrors === true) { throw error; } } else { subscriber.callback.apply(subScope, [data, topicTree[branch], publisher, onReply, subscriptionIdObject, settings ]); } subscriber.fired++; subscribersFired.push(subscriber.id); if ( typeof subscriber.autoUnsubscribe === 'number' && subscriber.autoUnsubscribe <= subscriber.fired ) { self.unsubscribe({id: subscriber.id, topic: subscriber.topic}); } } } } // create publication object, save if needed and return var publication = { 'topic': topic, 'data': data, 'settings': settings, 'timestamp': timestamp, 'publisher': publisher, 'subscribersFired': subscribersFired }; this.persist(config, publication); return publication; }; /* -------------------------------------------------------------------------- * -------------------------------------------------------------------------- * method: aBubPubSub.prototype.forward * allows forwarding one topic to another. * * parameters: * topicIn - {string} * subscriberId - {string} * the name/id of the subscriber (potentially for later reference, ... ) . if not passed, this will be a randomString * topicOut - {object} * the topic to be published/forwarded to * outOptions - {object||function} * OPTIONAL. defaults to the original publication settings. an option to overwrite the publish options (like silent, persist, ... ) * publisher - {object||string} * OPTIONAL. defaults to the original publisher. the publisher for the forward. * returns: * forwardSubscriberObject - {object} with 2 attributes: topic and id to allow unsubscribe of the forward * * * example: * (start code) * var myForward = aBubPubSub.forward( * '/politics/europe', * 'superForwardSubscriber', * '/unitednations/', * { bubble: false }, * 'superForwardPublisher' * ); * (end) * * --------------------------------------------------------------------------- */ aBubPubSub.prototype.forward = function(topicIn, subscriberId, topicOut, outOptions, outId) { var self = this; var forwardObject = this.subscribe( topicIn, function(pubData, receivedTopic, publisher, onReply, subObjectId, settings) { var tOut = (typeof topicOut === 'function') ? topiOut(receivedTopic, topicIn) : topicOut; self.publish( tOut, (typeof outOptions === 'object') ? outOptions : settings, (typeof outId !== 'undefined') ? outId : publisher ); }, { getBubbles: false }, subscriberId ); return forwardObject; }; /* -------------------------------------------------------------------------- * -------------------------------------------------------------------------- * method: aBubPubSub.prototype.subscribe * Subscribe to publication on certain topics/channels. * * parameters: * topic - {string} * The channel to listen to. if no / are given, 'root' will be assumed. * callback - {function} * a definition of what to do with the data passed on by the publication * options - {object} * getBubbles - {boolean} should the callback also be executed for all publications that were sent via subbranches of the given topic? * getPersists - {boolean} **NOT WORKING YET** should the callback also be executed for all already published, but persitent publications (getBubbles setting will apply here too)? * subscriberId - {string} * the name/id of the subscriber (potentially for later reference, ... ) . if not passed, this will be a randomString * * returns: * result - {object} with 2 attributes: topic and id : * * (start code) * - 'topic' : {string} topic, * - ''id' : {string} subscriberId * (end) * * example: * (start code) * var mySubscription = aBubPubSub.subscribe( * '/politics/europe', * function(currentBranch, publisher, reply) { * console.log(data.originalTopic); * reply('this is a stupid example'); * }, * { getBubbles: true, getPersists: false } * 'myLittleListener' * ); * (end) * * --------------------------------------------------------------------------- */ aBubPubSub.prototype.subscribe = function(topic, callback, options, subscriberId) { var self = this; var myOptions = { getBubbles: this.defaults.getBubbles, getPersists: this.defaults.getPersists, forceUniqueSubscriber: this.defaults.forceUniqueSubscriber, autoUnsubscribe: false }; var timestamp = Date.now(); if (typeof callback !== 'function') { callback = function() { return true; }; } // LEGACY SUPPORT! before options, we had only getBubbles ... if (typeof options === 'boolean') { console.log('!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! BAD / OLD SUBSCRIPTION PARAMETERS!! UPDATE THE FOLLOWING SUBSCRIPTION:'); console.log('----- legacy parameter support will be deprecated in 0.9 ---------------'); console.log(arguments); throw new Error('PUBSUB ERROR'); var getBubbles = options; options = { getBubbles: getBubbles }; } if (typeof options === 'string') { console.log('!!!---!!!!!!!---!!!!!!!!---!!!!!!---!!!!!! BAD SUBSCRIPTION PARAMETERS!! UPDATE THE FOLLOWING SUBSCRIPTION:'); console.log('----- legacy parameter support will be deprecated in 0.9 ---------------'); console.log(arguments); subscriberId = options; options = {}; } if (typeof options === 'undefined') options = {}; if (typeof options.forceUniqueSubscriber === 'boolean') { myOptions.forceUniqueSubscriber = options.forceUniqueSubscriber; } if (typeof options.getBubbles === 'boolean') { myOptions.getBubbles = options.getBubbles; } if (typeof options.autoUnsubscribe === 'boolean' || typeof options.autoUnsubscribe === 'number') { myOptions.autoUnsubscribe = options.autoUnsubscribe; } if (typeof topic !== 'string') { topic = '/'; } topic = this.__stripSlashes(topic); if (topic === '') { topic = '/'; } // console.log('~~~~~ TOPIC : ' + topic); if (typeof this.publicationChannels[topic] === 'undefined') { this.publicationChannels[topic] = new Array(); } if (typeof subscriberId !== 'string') { subscriberId = 'sub_' + timestamp + '_' + Math.floor(Math.random()*100000); } if (myOptions.forceUniqueSubscriber === true) { while (typeof this.publicationChannels[topic][subscriberId] !== 'undefined') { subscriberId += '_' + Math.floor(Math.random()*10000); } } this.publicationChannels[topic][subscriberId] = { 'callback': callback, 'getBubbles': myOptions.getBubbles, 'getPersists': myOptions.getPersists, 'autoUnsubscribe': myOptions.autoUnsubscribe, 'id': subscriberId, 'topic': topic, 'fired': 0, 'scope': (typeof options.scope === 'object') ? options.scope : self.defaults.scope }; if (typeof options === 'object' && typeof options.scope === 'object') { this.publicationChannels[topic][subscriberId].scope = options.scope; } return { 'topic': topic, 'id': subscriberId }; }; /* -------------------------------------------------------------------------- * -------------------------------------------------------------------------- * method: aBubPubSub.prototype.unsubscribe * Remove a subscription. * * parameters: * topic - {string} * The channel to unsubscribe from. * subscriberId - {string} * the id of the subscriber that should be removed. * * returns: * result - {boolean} * was the subscription found and successfully removed? * * example: * (start code) * var mySubscription = aBubPubSub.subscribe( * '/politics/europe', * function(data) { * console.log(data.originalTopic); * }, * true, * 'myLittleListener' * ); * aBubPubSub.unsubscribe('/politics/europe', mySubscription.id); * (end) * * --------------------------------------------------------------------------- */ aBubPubSub.prototype.unsubscribe = function(subscriptionObject) { if (typeof subscriptionObject === 'undefined') return false; if (typeof subscriptionObject.topic === 'undefined') return false; if (typeof subscriptionObject.id === 'undefined') return false; if ( typeof this.publicationChannels[subscriptionObject.topic] !== 'undefined' && typeof this.publicationChannels[subscriptionObject.topic][subscriptionObject.id] !== 'undefined' ) { delete this.publicationChannels[subscriptionObject.topic][subscriptionObject.id]; // to cleanup the namespace for real here, we need to check whether the channel is empty, and if it is, we remove it. var length = Object.keys(this.publicationChannels[subscriptionObject.topic]).length; if (length === 0) { delete this.publicationChannels[subscriptionObject.topic]; } return true; } return false; }; /* -------------------------------------------------------------------------- * -------------------------------------------------------------------------- * method: aBubPubSub.prototype.announce * deprecated * * --------------------------------------------------------------------------- */ aBubPubSub.prototype.announce = function(topic, data, settings, publisher) { return false; }; /* -------------------------------------------------------------------------- * -------------------------------------------------------------------------- * method: aBubPubSub.prototype.wasPublishedOnTopic * sometimes one might want to know whether publications have been sent out in the past (asynchronously loaded script parts, newly instanced stuff, etc.) * if publications were sent with the 'persist:true' setting, they are saved in this.publicationLog . * wasPublishedOnTopic goes through this log and returns an array of publications matching the given topic. * * parameters: * myTopic - {string} * The topic to look for in the publicationLog. * getBubbles - {boolean} * should also publications on subtopics be returned? * * returns: * result - {array} of publication objects with 5 attributes: * - 'topic' : {string} topic, * - 'data' : {object||string||boolean||anything} data, * - 'settings' : {object} settings, * - 'timestamp' : {date} Date.now(), * - 'publisher' : {string||object} publisher * * example: * > aBubPubSub.wasPublishedOnTopic('/politics', true); * * --------------------------------------------------------------------------- */ aBubPubSub.prototype.wasPublishedOnTopic = function(myTopic, getBubbles) { var matchingPublications = []; getBubbles = (typeof getBubbles === 'boolean') ? getBubbles : false; for (var i in this.publicationLog) { var hit = false; if (getBubbles === true) { hit = (this.publicationLog[i].topic.indexOf(myTopic) === 0); } else { hit = (this.publicationLog[i].topic.indexOf(myTopic) === myTopic); } if (hit === true) { matchingPublications.push(this.publicationLog[i]); } } return matchingPublications; }; /* -------------------------------------------------------------------------- * -------------------------------------------------------------------------- * method: aBubPubSub.prototype.wasPublishedOnTopic * sometimes one might want to know whether publications have been sent out in the past (asynchronously loaded script parts, newly instanced stuff, etc.) * if publications were sent with the 'persist:true' setting, they are saved in this.publicationLog . * wasPublishedOnTopic goes through this log and returns an array of publications matching the given topic. * * parameters: * newDefaults - {object} * - scope {object} defaults to *this* * if you want a different scope in the callbacks, just supply it here * - persist {boolean} defaults to *false* * if true the publication will be saved in the this.publicationLog object (with a timestamp attached) * - bubble {object} defaults to *false* * if true a topic will bubble up it's branch: * /politics/europe/internet/myOpinion would activate callbacks for * first '/politics/europe/internet/myOpinion', * then '/politics/europe/internet', * then '/politics/europe', * then '/politics', * and '/' * * returns: * this.defaults - {object} with three attributes: scope, persist, bubble: * (start code) * - 'scope' : {object} which scope should be applied to callbacks by default? * - 'persist' : {boolean} should publications be logged by default? * - 'bubble' : {object} should publications be bubbling by default? * (end) * * example: | aBubPubSub.setDefaults({ scope: this, persist: true, bubble: false }); * * --------------------------------------------------------------------------- */ aBubPubSub.prototype.setDefaults = function(newDefaults) { if (typeof newDefaults !== 'undefined') { if (typeof newDefaults.scope === 'object') { this.defaults.scope = newDefaults.scope; } if (typeof newDefaults.persist === 'boolean') { this.defaults.persist = newDefaults.persist; } if (typeof newDefaults.bubble === 'boolean') { this.defaults.bubble = newDefaults.bubble; } if (typeof newDefaults.forceUniqueSubscriber === 'boolean') { this.defaults.forceUniqueSubscriber = newDefaults.forceUniqueSubscriber; } if ( typeof newDefaults.debugging === 'number' || typeof newDefaults.debugging === 'boolean' ) { this.defaults.debugging = (newDefaults.debugging == true); } }; return this.defaults; }; /* -------------------------------------------------------------------------- * -------------------------------------------------------------------------- * method: aBubPubSub.prototype.__stripSlashes * used internally. a simply utility function to strip slashes from both ends of the topic string.... * * parameters: * topic - {string} the topic to strip of starting and trailing slashes * * returns: * stripped - {string} the slash-stripped topic. * * example: * > var stripped = aBubPubSub.__stripSlashes('/news/cnn/'); > // stripped == 'news/cnn'; * * --------------------------------------------------------------------------- */ aBubPubSub.prototype.__stripSlashes = function(topic) { if (typeof topic !== 'string') { return ''; } return topic.replace(this.unslasher, '$1'); }; /* -------------------------------------------------------------------------- * -------------------------------------------------------------------------- * method: aBubPubSub.prototype.converse * an efficient means to send a publication and handle the answer. * * parameters: * publication - {object} give the * - topic - {string} the publication topic * - data - {object} the dataObject to be sent out * - options - {object} the publication options * - sender - {string||object} who is sending * subscription - {object} * - autoUnsubscribe - {boolean||number} optional. defaults to 1. after how many calls - if at all - should be unsubscribed? * - callback - {function} the subscription handler function * - scope - {object} optional. defaults to the bubPubSub object. in which scope should the callback be executed? * * returns: * stripped - {string} the slash-stripped topic. * * example: * > var stripped = aBubPubSub.__stripSlashes('/news/cnn/'); > // stripped == 'news/cnn'; * * --------------------------------------------------------------------------- */ aBubPubSub.prototype.converse = function(publication, subscription) { var self = this; var thisConversation = {}; var timestamp = Date.now(); thisConversation.id = 'bpsCon' + timestamp + '_' + Math.round(Math.random()*100000); // check and fill up parameters for subscribe if (typeof subscription.autoUnsubscribe !== 'boolean') { subscription.autoUnsubscribe = 1; } if (typeof subscription.callback !== 'function') { subscription.autoUnsubscribe = 1; } if (typeof subscription.scope !== 'object') { if (typeof arguments.callee !== 'undefined') { subscription.scope = arguments.callee; } if (typeof window === 'object') { subscription.scope = window; } else if (typeof process === 'object') { subscription.scope = process; } else { subscription.scope = thisConversation; } } // check and fill up parameters for publish if (typeof publication.topic !== 'string') { publication.topic = '/'; } if (typeof publication.data !== 'object') { publication.data = {}; } if (typeof publication.options !== 'object') { publication.options = {}; } if (typeof publication.sender === 'undefined') { publication.sender = thisConversation.id; } // create safe branches and topics thisConversation.pubId = 'pub' + timestamp + '_' + Math.round(Math.random()*100000); if (typeof subscription.topic !== 'string') { subscription.topic = publication.topic + '/done'; } // subscribe thisConversation.subscription = self.subscribe( subscription.topic, function(data, currentTopic, publisher, onReply, subscriptionIdObject) { thisConversation.state = 'received'; subscription.callback.apply( subscription.scope, [ data, currentTopic, publisher, onReply, subscriptionIdObject ] ); thisConversation.state = 'returned'; return; }, { scope: subscription.scope, autoUnsubscribe: subscription.autoUnsubscribe }, thisConversation.id ); thisConversation.state = 'subscribed'; //publish thisConversation.publication = self.publish( publication.topic, publication.data, publication.options, publication.sender ); thisConversation.state = 'published'; return thisConversation; }; aBubPubSub.prototype.log = function(config, content) { var doLog = (this.defaults.debugging == true && config.silent === false); if (doLog === true) { console.log(content); } return doLog; }; aBubPubSub.prototype.persist = function(config, publication) { if (config.persist === true) { this.publicationLog.push(publication); if (this.publicationLog.length > 1000) { this.publicationLog.pop(); } } return publication; }; aBubPubSub.prototype.onError = function(error) { this.log(config, '[' + self.id + '] ERROR: callback error after subscription got #' + data.uniquePublicationId + ' @ (' + topicTree[branch] + ') from (' + data.originalTopic + ') | getBubbles=' + subscriber.getBubbles + ' | subscriber=' + subId); this.log(config, error); if (typeof error.stack !== 'undefined') { console.log(error.stack); } self.errors.push(error); if (self.errors.length > 100) { self.errors.pop(); } }; /* ---------------------------------------------------------------------------- * -------------- register aBubPubSub with the system ----------------------- * --------------------------------------------------------------------------- */ function wrapped_bubPubSub() { var thisBubPubSub = new aBubPubSub(); return thisBubPubSub; }; ;( function() { // if we are in nodeJS ... if (typeof exports !== 'undefined') { if (typeof module !== 'undefined' && module.exports) { module.exports = aBubPubSub; } } else { // Exported as a string, for Closure Compiler "advanced" mode. // Establish the root object, 'window'`' in the browser, or 'global' on the server. var root = this; var bubPubSub = new aBubPubSub(); root['bubPubSub'] = bubPubSub; } } ) (); ;