UNPKG

hermes-bus

Version:

Powerful event-emitter module for responsive bus architecture applications

347 lines (313 loc) 13.6 kB
/** * hermes-bus <https://github.com/jahnestacado/hermes-bus> * Copyright (c) 2014 Ioannis Tzanellis * Licensed under the MIT License (MIT). */ var Q = require("q"); var path = require("path"); var _ = require("underscore"); var DEFAULT_BUSLINE = "$main_busline$"; var NON_OVERRIDABLE_PROPERTIES = [DEFAULT_BUSLINE]; var SYNC_EVENT_TAG = "__"; var eventHandlers = {}; var observerBusLines = { DEFAULT_BUSLINE: { observers: [], beforeHooks: [], afterHooks: [] } }; var PREFIX_KEYWORDS = ["before", "on", "after"]; var MATCHERS = PREFIX_KEYWORDS.reduce(function(matchers, keyword){ matchers[keyword] = { eventMatcher : new RegExp("^"+ keyword + ".*"), syncEventMatcher : new RegExp("^" + SYNC_EVENT_TAG + keyword + ".*") }; return matchers; }, {}); var HermesBus = function(){ var self = this; self.disable = _utilityBelt.buildStatusSwitch(DEFAULT_BUSLINE, false); self.enable = _utilityBelt.buildStatusSwitch(DEFAULT_BUSLINE, true); self.trigger = _utilityBelt.buildTriggerFunction(DEFAULT_BUSLINE); self.hasEvent = _utilityBelt.buildHasEventFunction(DEFAULT_BUSLINE); var API = _.union(Object.keys(Object.getPrototypeOf(self)), Object.keys(self)); NON_OVERRIDABLE_PROPERTIES = _.union(NON_OVERRIDABLE_PROPERTIES, API); }; HermesBus.prototype.subscribe = function subscribe() { var self = this; var subscribedObjectInfo = _utilityBelt.getSubscribedObjectInfo(arguments); var buslineName = subscribedObjectInfo.buslineName; var subscribedObject = subscribedObjectInfo.subscribedObject; if(subscribedObject){ _utilityBelt.initBusline(buslineName); var events = _utilityBelt.createEventSpecs(subscribedObject); events.forEach(function(eventSpecs){ var observer = _utilityBelt.createObserver(eventSpecs); if(eventSpecs.isBeforeHook){ observerBusLines[buslineName].beforeHooks.push(observer); } else if(eventSpecs.isAfterHook){ observerBusLines[buslineName].afterHooks.push(observer); } else{ observerBusLines[buslineName].observers.push(observer); } _utilityBelt.buildNamespace(self, buslineName, eventSpecs.eventName); }); } }; HermesBus.prototype.unsubscribe = function unsubscribe() { var subscribedObjectInfo = _utilityBelt.getSubscribedObjectInfo(arguments); var buslineName = subscribedObjectInfo.buslineName; var subscribedObject = subscribedObjectInfo.subscribedObject; if(observerBusLines[buslineName] && subscribedObject){ var busline = observerBusLines[buslineName]; var events = _utilityBelt.createEventSpecs(subscribedObject); events.forEach(function(eventSpecs){ if(eventSpecs.isBeforeHook){ busline.beforeHooks = busline.beforeHooks.filter(function(beforeHookObserver){ return beforeHookObserver.cb !== eventSpecs.cb; }); } else if(eventSpecs.isAfterHook){ busline.afterHooks = busline.afterHooks.filter(function(afterHookObserver){ return afterHookObserver.cb !== eventSpecs.cb; }); } else{ busline.observers = busline.observers.filter(function(observer){ return observer.cb !== eventSpecs.cb; }); } }); } }; HermesBus.prototype.reset = function reset() { var self = this; _utilityBelt.initBusline(DEFAULT_BUSLINE); Object.keys(eventHandlers[DEFAULT_BUSLINE]).forEach(function(eventName) { delete eventHandlers[DEFAULT_BUSLINE][eventName]; var registeredFunctionName = _utilityBelt.eventToFunctionName(eventName); delete self[registeredFunctionName]; }); }; HermesBus.prototype.hardReset = function hardReset() { var self = this; var buslines = Object.keys(observerBusLines); self.reset(); buslines.forEach(function(busline) { _utilityBelt.destroyBusLine(self, busline); }); }; HermesBus.prototype.require = function hermesBusRequire(basePath, paths) { if(arguments.length === 1){ paths = basePath; basePath = process.cwd(); } var modulePaths = _.isArray(paths) ? paths : [paths]; modulePaths.forEach(function(modulePath) { require(path.join(basePath, modulePath)); }); }; var _utilityBelt = { buildNamespace: function buildNamespace(context, busline, eventName) { var self = this; var exportedNamespace = context; if (busline !== DEFAULT_BUSLINE) { if (!exportedNamespace[busline]) { exportedNamespace[busline] = _utilityBelt.buildBuslineUtils(context, busline); } exportedNamespace = exportedNamespace[busline]; } self.registerEventHandler(exportedNamespace, busline, eventName); }, buildBuslineUtils: function buildBuslineUtils(context, busline) { var utils = { trigger: _utilityBelt.buildTriggerFunction(busline), enable: _utilityBelt.buildStatusSwitch(busline, true), disable: _utilityBelt.buildStatusSwitch(busline, false), destroy: _utilityBelt.buildDestroyBusline(context, busline), hasEvent : _utilityBelt.buildHasEventFunction(busline) }; return utils; }, buildDestroyBusline: function buildDestroyBusline(context, busline) { return function() { _utilityBelt.destroyBusLine(context, busline); }; }, buildTriggerFunction: function buildTriggerFunction(busline) { return function() { var args = Array.prototype.slice.call(arguments); var promise = { then: function(){} }; // Assign first argument to eventName variable and remove it from the args array var eventName = args.shift(); if(eventHandlers[busline][eventName]){ promise = eventHandlers[busline][eventName].cb.apply(this, args); } else{ console.warn("[WARN][Hermes-bus] Event " + eventName + " does not exist or it's not registered yet on the bus."); } return promise; }; }, registerEventHandler: function createTrigger(context, busline, eventName){ if(!eventHandlers[busline][eventName]){ var triggerFunctionName = _utilityBelt.eventToFunctionName(eventName); var executeBeforeHooks = _utilityBelt.getEventHandler(busline, eventName, "before"); var executeEvent = _utilityBelt.getEventHandler(busline, eventName); var executeAfterHooks = _utilityBelt.getEventHandler(busline, eventName, "after"); var triggerEventFunction = function(){ var defer = Q.defer(); var promise = defer.promise; if(eventHandlers[busline][eventName].isActive){ //on-event callback arguments to array var args = Array.prototype.slice.call(arguments); executeBeforeHooks(args) .then(function(){return executeEvent(args)}) .then(function(){return executeAfterHooks(args)}) .then(function(){defer.resolve()}); } else{ defer.resolve(); } return promise; }; eventHandlers[busline][eventName] = { cb: triggerEventFunction, isActive : true }; context[triggerFunctionName] = triggerEventFunction; } }, eventToFunctionName: function eventToFunctionName(eventName){ return "trigger" + _utilityBelt.firstLetterToUpperCase(eventName); }, getEventHandler: function getEventHandler(busline, eventName, hookType) { return function (args) { var promises = []; var executionCycleDeffered = Q.defer(); promises.push(executionCycleDeffered); var registeredEvents; if(hookType){ registeredEvents = observerBusLines[busline][hookType + "Hooks"]; } else{ registeredEvents = observerBusLines[busline].observers; } var handleExecutionOrder = function (observers) { var currentObserver = observers[0]; var remainingObservers = _.without(observers, currentObserver); if (observers.length === 0) { executionCycleDeffered.resolve(); }else if (currentObserver.eventName === eventName) { if (currentObserver.isSync) { promises.push(_utilityBelt.createPromiseEvent(currentObserver, args)); } else { try{ currentObserver.cb.apply(currentObserver, args); } catch(error){ var name = _utilityBelt.firstLetterToUpperCase(currentObserver.eventName); console.error("[ERROR][Hermes-bus] Exception thrown from within a listener" + " (e.g before" + name +", on" + name +" or after"+ name +") ->", error); } } handleExecutionOrder(remainingObservers); } else{ handleExecutionOrder(remainingObservers); } }; handleExecutionOrder(registeredEvents); return Q.all(promises); }; }, createPromiseEvent: function createPromiseEvent(element, args) { var deferred = Q.defer(); var appliedArgs = args.slice(0); appliedArgs.push(deferred.resolve); element.cb.apply(element, appliedArgs); return deferred.promise; }, createEventSpecs: function (subscribedObject) { var keys = Object.keys(subscribedObject); var eventListenerSpecs = keys.reduce(function (listeners, key) { if( _.isFunction(subscribedObject[key])){ var event = null; PREFIX_KEYWORDS.some(function(keyword){ var foundMatch = false; if (MATCHERS[keyword].eventMatcher.test(key) || MATCHERS[keyword].syncEventMatcher.test(key)) { event = { eventName: _utilityBelt.firstLetterToLowerCase(key.replace(new RegExp("^("+ keyword+"|" + SYNC_EVENT_TAG + keyword + ")"), "")), cb: subscribedObject[key], isSync: MATCHERS[keyword].syncEventMatcher.test(key), isBeforeHook: keyword === "before", isAfterHook: keyword === "after" }; foundMatch = true; } return foundMatch; }); if(event){ listeners.push(event); } } return listeners; }, []); return eventListenerSpecs; }, createObserver: function createObserver(eventSpecs) { return { eventName: eventSpecs.eventName, cb: eventSpecs.cb, isSync: eventSpecs.isSync }; }, getSubscribedObjectInfo: function getSubscribedObjectInfo(args){ var objectInfo = { buslineName: DEFAULT_BUSLINE, subscribedObject: null }; if(args.length === 2 && _.isObject(args[1])){ objectInfo.buslineName = args[0]; objectInfo.subscribedObject = args[1]; if(_.contains(NON_OVERRIDABLE_PROPERTIES, objectInfo.buslineName)){ throw new Error("Not permitted busline name: " + objectInfo.buslineName +". Cannot override API functions: [ "+ NON_OVERRIDABLE_PROPERTIES +"]."); } } else if(args.length === 1 && _.isObject(args[0])){ objectInfo.subscribedObject = args[0]; } else{ throw new Error("hermes-bus.subscribe(): Invalid arguments: " + args); } return objectInfo; }, buildStatusSwitch: function buildStatusSwitch(busline, isActive) { return function(eventName) { if(eventHandlers[busline] && eventHandlers[busline][eventName]){ eventHandlers[busline][eventName].isActive = isActive; } }; }, buildHasEventFunction: function(buslineName){ return function(eventName){ return (eventHandlers[buslineName] && eventHandlers[buslineName][eventName]) ? true : false; }; }, destroyBusLine: function destroyBusLine(context, busline) { delete observerBusLines[busline]; delete eventHandlers[busline]; delete context[busline]; }, initBusline: function initBusline(buslineName){ if(!observerBusLines[buslineName]){ observerBusLines[buslineName] = { observers: [], beforeHooks: [], afterHooks: [] }; eventHandlers[buslineName] = {}; } }, firstLetterToUpperCase: function firstLetterToUpperCase(eventName) { return eventName.charAt(0).toUpperCase() + eventName.slice(1); }, firstLetterToLowerCase: function firstLetterToLowerCase(eventName) { return eventName.charAt(0).toLowerCase() + eventName.slice(1); } }; module.exports = new HermesBus();