UNPKG

win-backbone

Version:

The control backbone for WIN, handles message passing between modules in an experiment. Allows for somewhat generic module swapping

806 lines (651 loc) 22.3 kB
//Control all the win module! Need emitter for basic usage. var Emitter = (typeof process != "undefined" ? require('component-emitter') : require('emitter')); var Q = require('q'); // module.exports = winBB; function winBB(homeDirectory) { // var self = this; //we're an emitter! but we also mean extra business, so we override some calls later Emitter(self); //pull the inner versions, we'll overwrite self versions later var innerEmit = self.emit; var innerHasListeners = self.hasListeners; //cache the shift function var shift = [].shift; self.log = function() { throw new Error("Backbone doesn't use log directly anymore. Call backbone.getLogger(moduleObject) instead. "); } self.log.logLevel = function() { throw new Error("Backbone doesn't use log.loglevel anymore. Call backbone.logLevel directly instead. "); } var prependText = function(winFunction) { return !winFunction ? "" : " [" + winFunction + "]: "; } self.silenceBackbone = false; self.logLevel = 1; self.nologging = -1; self.warning = 0; self.normal = 1; self.verbose = 2; self.testing = 3; var muted = {}; var modIDs = 0; var propID = "_backboneID"; var propIDToName = {}; var allLoggers = []; //we assign every module a log identification function nextModID() {return modIDs++;} //backbone handles the most basic logging for now, filtering by logLevel at the time //no stored history -- this will require a separate module //the practice of logging through the backbone should be standard though self.getLogger = function(moduleObject) { var winFunction = moduleObject.winFunction; var prepend = prependText(winFunction); var mid = addLogger(moduleObject); //otherwise ... //already have an mid -- assigned by the loader if(typeof process != "undefined")//&& "".cyan != undefined) { prepend = '\x1B[36m' + prepend + '\x1B[39m'; } var logFunction = function() { var logCategory; if(typeof arguments[0] == "number") { logCategory = [].shift.call(arguments); } else //otherwise, assume it's just a verbose message by default -- why would you log otherwise? logCategory = logFunction.verbose; if(!logCategory) throw new Error("Log category must be defined."); [].splice.call(arguments, 0,0, prepend) //needs to be lower than both our individual level, and our global level -- can't flood the log as any module if(logCategory <= logFunction.logLevel && logCategory <= self.logLevel && !muted[mid]) console.log.apply(console, arguments); } //assign id to our logger! logFunction[propID] = mid; logFunction.log = logFunction; logFunction.logLevel = self.logLevel; logFunction.nologging = self.nologging; logFunction.warning = self.warning; logFunction.normal = self.normal; logFunction.verbose = self.verbose; logFunction.testing = self.testing; return logFunction; } //hold our logger propID var internalLog = self.getLogger({}); //set the backbone logger to this internal object prop ID assigned by logger addNameToMID("backbone", internalLog[propID]); internalLog.logLevel = internalLog.testing; //none modules so far self.moduleCount = 0; //we need to have all calls on record var callerEvents = {}; var requiredEvents = {}; var optionalEvents = {}; var moduleObjects = {}; var mutingAll = false; function addLogger(moduleObject) { //tada var mid = moduleObject[propID]; //if we haven't already gotten an mid assigned to this object if(mid == undefined) { //we need to assign an mid mid = nextModID(); //all we can do is assign it to this winfunction moduleObject[propID] = mid; } //grab the mid -- later we can do other things if necessary allLoggers.push(mid); //please respect the silence if(mutingAll) muted[mid] = true; return mid; } //can mute/unmute self.mute = function(name) { var mid = propIDToName[name]; if(mid != undefined) muted[mid] = true; } self.unmute = function(name) { var mid = propIDToName[name]; delete muted[mid]; } self.muteAll = function() { mutingAll = true; for(var i=0; i < allLoggers.length; i++) muted[allLoggers[i]] = true; } self.unmuteAll = function(){ muted = {}; mutingAll = false; } self.muteLogger = function(logObject) { muted[logObject[propID]] = true; } self.unmuteLogger = function(logObject) { delete muted[logObject[propID]]; } function addNameToMID(name, id) { //don't want duplicates if(propIDToName[name] != undefined) throw new Error("Duplicate prop ID being sent in, likely named another module 'backbone'"); //for silencing by name propIDToName[name] = id; if(mutingAll) muted[id] = true; } //helpful getters for the module objects self.getModules = function(moduleNames){ //empty? jsut send the whole module object back -- pretty dangerous -- ill advised if(!moduleNames) return moduleObjects; //otherwise, we build a map for the name var mReturn = {}; //you can send an array of names, an object indexed by names, or a simple string var nameList = moduleNames; if(typeof moduleNames == "string") moduleNames = [moduleNames]; else if(typeof moduleNames == "object") nameList = Object.keys(moduleNames); else if(!Array.isArray(moduleNames)) throw new Error("Improper module names submitted: must be a string, an array, or a map of the module names"); //loop through, grab the stuff for(var i=0; i < nameList.length; i++) { var name = nameList[i]; mReturn[name] = moduleObjects[name]; } //send it back, simple return mReturn; }; self.getModuleCount = function(){return self.moduleCount;}; self.getModuleNameList = function(){return Object.keys(moduleObjects);}; var parseEventName = function(fullEvent) { var splitEvent = fullEvent.split(':'); //if there is no ":", then this is improperly formatted if(splitEvent.length <= 1) throw new Error("Improper event name format, winFunction:eventName, instead looks like: " + fullEvent); return {winFunction: splitEvent[0], eventName: splitEvent[1]} } self.loadModules = function(inputNameOrObject, allConfiguration, localConfiguration) { var globalConfiguration; if(typeof localConfiguration == "undefined") { //we handle the case where potentially we have a global object and a bunch of local objects allConfiguration = allConfiguration || {}; globalConfiguration = allConfiguration.global || {}; localConfiguration = allConfiguration; } //both are defined -- one assumed to be global, other local else if(allConfiguration && localConfiguration) { globalConfiguration = allConfiguration; localConfiguration = localConfiguration; } else if(localConfiguration) { //allconfiguration is undefined-- this is weird -- maybe they made a mistake //try to pull global from local allConfiguration = localConfiguration; globalConfiguration = localConfiguration.global || {}; } else { //just cover the basics, both undefined allConfiguration = allConfiguration || {}; globalConfiguration = allconfiguration.global || {}; localConfiguration = localConfiguration || {}; } //we have sent in a full object, or just a reference for a text file to load var jsonModules = inputNameOrObject; if(typeof inputNameOrObject == "string") { var fs = require('fs'); var fBuffer = fs.readFileSync(inputNameOrObject); jsonModules = JSON.parse(fBuffer); } //otherwise, json modules is the json module information var mCount = 0; for(var key in jsonModules) { //perhaps there is some relative adjustments that need to be made for this to work? var locationNameOrObject = jsonModules[key]; //if you're a function or object, we just leave you alone (the function will be instantiated at the end) //makes it easier to test things if(typeof locationNameOrObject == "object" || typeof locationNameOrObject == "function") { moduleObjects[key] = locationNameOrObject; } else if(locationNameOrObject.indexOf('/') != -1) { //locations relative to the home directory of the app moduleObjects[key] = require(homeDirectory + locationNameOrObject); } else moduleObjects[key] = require(locationNameOrObject); //if it's a function, we create a new object // if(typeof moduleObjects[key] != "function") // throw new Error("WIN Modules need to be functions for creating objects (that accept win backbone as first argument)") //create the object passing the backbone if(typeof moduleObjects[key] == "function") // then pass on teh configuration, both inputs are guaranteed to exist moduleObjects[key] = new moduleObjects[key](self, globalConfiguration, localConfiguration[key] || {}); //if they were not assign an mid by a logger, then I don't need to worry -- yet var mid = moduleObjects[key][propID]; if(mid == undefined) { mid = nextModID(); moduleObjects[key][propID] = mid; } //go ahead and register this name for muting purposes addNameToMID(key, mid); mCount++; } self.moduleCount = mCount; //now we register our winFunctions for these modules for(var key in moduleObjects) { var wFun = moduleObjects[key].winFunction; if(!wFun || wFun == "" || typeof wFun != "string") { internalLog('Module does not implement winFunction properly-- must be non-empty string unlike: ' + wFun); throw new Error("Improper win function"); } //instead we do this later // if(!callerEvents[wFun]) // { // //duplicate behaviors now allowed in backbone -- multiple objects claiming some events or functionality // callerEvents[wFun] = {}; // requiredEvents[wFun] = {}; // optionalEvents[wFun] = {}; // } } //now we register our callback functions for all the events for(var key in moduleObjects) { var mod = moduleObjects[key]; // if(!mod.eventCallbacks) // { // throw new Error("No callback function inside module: " + mod.winFunction + " full module: " + mod); // } //event callbacks are option -- should cut down on module bloat for simple modules to do stuff if(!mod.eventCallbacks){ internalLog("WARNING, loaded module doesn't provide any callback events inside: ", mod.winFunction, " - with key - ", key); //skip! continue; } //grab the event callbacks var mCallbacks = mod.eventCallbacks(); for(var fullEventName in mCallbacks) { // if(typeof fullEventName != "string") { throw new Error("Event callback keys must be strings: " + fullEventName); } var cb = mCallbacks[fullEventName]; if(!cb || typeof cb != "function") { throw new Error("Event callback must be non-null function: " + cb); } if(self.moduleHasListeners(fullEventName)) { internalLog("Backbone doesn't allow duplicate callbacks for the same event: " + fullEventName); throw new Error("Same event answered more than once: " + fullEventName); } //now we register inside of the backbone //we override what was there before self.off(fullEventName); //sole callback for this event -- always overwriting self.on(fullEventName, cb); //throws error for improper formatting var parsed = parseEventName(fullEventName); var callObject = callerEvents[parsed.winFunction]; if(!callObject){ callObject = {}; callerEvents[parsed.winFunction] = callObject; } callObject[parsed.eventName] = fullEventName; } } //now we grab all the required functionality for the mods for(var key in moduleObjects) { //call the mod for the events var mod = moduleObjects[key]; //guaranteed to exist from callbacks above var fun = mod.winFunction; if(!mod.requiredEvents){ internalLog("WARNING, loaded module doesn't require any events inside: ", fun, " - with key - ", key); //skip! continue; } // if(!mod.requiredEvents) // { // throw new Error("Required events function not written in module: " + fun); // } var reqs = mod.requiredEvents(); if(!reqs) { throw new Error("requiredEvents must return non-null array full of required events."); } //make sure we have all these events for(var i=0; i < reqs.length; i++) { if(!self.moduleHasListeners(reqs[i])) throw new Error("Missing a required listener: " + reqs[i]); var parsed = parseEventName(reqs[i]); //lets keep track of who needs what. var required = requiredEvents[fun]; if(!required) { required = {}; requiredEvents[fun] = required; } //then index into win function if(!required[parsed.winFunction]) { required[parsed.winFunction] = {}; } //and again to pared event name if(!required[parsed.winFunction][parsed.eventName]) { required[parsed.winFunction][parsed.eventName] = reqs[i]; } } //of course any mod can make optional events //these are events that you can optionally call, but aren't necessarily satisfied by any module //you should check the backbone for listeners before making an optional call -- use at your own risk! if(mod.optionalEvents) { var opts = mod.optionalEvents(); for(var i=0; i < opts.length; i++) { var parsed = parseEventName(opts[i]); //lets keep track of who needs what. var optional = optionalEvents[fun]; //if we haven't seen this function requiring stuff before, create our object! if(!optional){ optional = {}; optionalEvents[fun] = optional; } //same for win function, have we seen before? if(!optional[parsed.winFunction]) { optional[parsed.winFunction] = {}; } //then the full on event name if(!optional[parsed.winFunction][parsed.eventName]) { optional[parsed.winFunction][parsed.eventName] = opts[i]; } } } } } //build a custom emitter for our module self.getEmitter = function(module) { if(!module.winFunction) { throw new Error("Can't generate module call function for module that doesn't have a winFunction!"); } //emitter implicitly knows who is calling through closure var moduleFunction = module.winFunction; var emitter = function() { [].splice.call(arguments, 0, 0, moduleFunction); return self.moduleEmit.apply(self, arguments); } //pass the function through emitter.emit = emitter; //pass in the emitter to create a q calling function emitter.qCall = createQCallback(emitter); //use the qcalls to chain multiple calls together using Q.all and Q.allSettled emitter.qConcurrent = qAllCallback(emitter.qCall); //this makes it more convenient to check for listeners //you don't need a backbone object AND an emitter. The emitter tells you both info //-- while being aware of who is making requests emitter.hasListeners = function() { //has listeners is aware, so we can tap in and see who is checking for listeners return self.moduleHasListeners.apply(self, arguments); } return emitter; } //this is for given a module a promise based callback method -- no need to define for every module //requires the Q library -- a worthy addition for cleaning up callback logic function createQCallback(bbEmit) { return function() { //defer -- resolve later var defer = Q.defer(); //first add our own function type var augmentArgs = arguments; //make some assumptions about the returning call var callback = function(err) { if(err) { defer.reject(err); } else { //remove the error object, send the info onwards [].shift.call(arguments); //now we have to do something funky here //if you expect more than one argument, we have to send in the argument object //and you pick out the appropriate arguments //if it's just one, we send the one argument like normal //this is the behavior chosen if(arguments.length > 1) defer.resolve(arguments); else defer.resolve.apply(defer, arguments); } }; //then we add our callback to the end of our function -- which will get resolved here with whatever arguments are passed back [].push.call(augmentArgs, callback); //make the call, we'll catch it inside the callback! bbEmit.apply(bbEmit, augmentArgs); return defer.promise; } } function qAllCallback(qCall) { return function() { var defer = Q.defer(); //send in all the events you want called by win-backbone var eventCalls = [].shift.call(arguments); var options = [].shift.call(arguments) || {}; //these are all the things you want to call var allCalls = []; //either we call the all function (wish fails at the first error) var qfunc = Q.allSettled; //or optionally, we wait till they all fail or succeed if(options.endOnError) qfunc = Q.all; //create a bunch of promises that will be potentially resolved for(var i=0; i < eventCalls.length; i++) allCalls.push(qCall.apply(qCall, eventCalls[i])); //here we go! qfunc.call(qfunc, allCalls) .then(function(results) { //we got back stuff back //it's easy for Q.all //it would have caused an error, and been rejected inside fail if(options.endOnError){ defer.resolve(results); } else { var finalValues = {length:0}; var errors = []; var errored = false; for(var i=0; i < results.length; i++) { var result = results[i]; //we know the outcome if (result.state === "fulfilled") { finalValues[i] = result.value; finalValues.length++; errors.push(undefined); } else { var reason = result.reason; errors.push(reason); errored = true; } } //let the errors be known //we always reject with an array to be consistent if(errored) defer.reject(errors); else //otherwise, all good -- on we go defer.resolve(finalValues); } }) .fail(function(err) { //end on error -- we only have one error to return //we always return arrays defer.reject([err]); }); return defer.promise; } } //backwards compat, but more consistent with getters self.getModuleRequirements = self.moduleRequirements = function() { return JSON.parse(JSON.stringify(requiredEvents)); }; //backwards compat, but more consistent with getters self.getRegisteredEvents = self.registeredEvents = function() { //return a deep copy so it can't be messed with return JSON.parse(JSON.stringify(callerEvents)); } self.initializeModules = function(done) { //call each module for initialization var totalCallbacks = self.moduleCount; var errors; var finishCallback = function(err) { if(err) { //we encountered an error, we should send that back if(!errors) errors = []; errors.push(err); } //no matter what happens, we've finished a callback totalCallbacks--; if(totalCallbacks == 0) { //we've finished all the callbacks, we're done with initialization //send back errors if we have them done(errors); } } var wrapMod = function(mod) { return function() { mod.initialize(function(err) { finishCallback(err); }); } } var hasInit = false; //order of initialization might matter -- perhaps this is part of how objects are arranged in the json file? for(var key in moduleObjects) { var mod = moduleObjects[key]; //make sure not to accidentally forget this if(!mod.initialize) totalCallbacks--; else { hasInit = true; //seems goofy, but we dont want any poorly configured modules returning during this for loop -- awkward race condition! setTimeout(wrapMod(mod), 0) } } //nobody has an initialize function if(!hasInit) { //call done async setTimeout(done, 0); } } self.hasListeners = function() { throw new Error("Backbone doesn't pass listeners through itself any more, it uses the emitter.hasListeners. You must call backbone.getEmitter(moduleObject) to get an emitter."); } self.emit = function() { throw new Error("Backbone doesn't pass messages through emit any more. You must call backbone.getEmitter(moduleObject) -- passing the object."); } self.moduleHasListeners = function() { //pass request through module here! return innerHasListeners.apply(self, arguments); } self.moduleEmit = function() { //there are more than two // internalLog('Emit: ', arguments); if(arguments.length < 2 || typeof arguments[0] != "string" || typeof arguments[1] != "string") { throw new Error("Cannot emit with less than two arguments, each of which must be strings: " + JSON.stringify(arguments)); } //take the first argument from the array -- this is the caller var caller = shift.apply(arguments); //pull out the function and event name arguments to verify the callback var parsed = parseEventName(arguments[0]); var wFunction = parsed.winFunction; var eventName = parsed.eventName; internalLog("[" + caller + "]", "calling", "[" + parsed.winFunction + "]->" + eventName); //now we check if this caller declared intentions if(!self.verifyEmit(caller, wFunction, eventName)) { throw new Error("[" + caller + "] didn't require event [" + parsed.winFunction + "]->" + parsed.eventName); } //otherwise, normal emit will work! We've already peeled off the "caller", so it's just the event + arguments being passed innerEmit.apply(self, arguments); } self.verifyEmit = function(caller, winFunction, eventName) { //did this caller register for this event? if((!requiredEvents[caller] || !requiredEvents[caller][winFunction] || !requiredEvents[caller][winFunction][eventName]) && (!optionalEvents[caller] || !optionalEvents[caller][winFunction] || !optionalEvents[caller][winFunction][eventName])) return false; return true; } return self; }