@oat-sa/tao-core-sdk
Version:
Core libraries of TAO
416 lines (397 loc) • 16.1 kB
JavaScript
define(['lodash', 'core/promise', 'core/uuid', 'core/logger'], function (_, Promise, uuid, loggerFactory) { 'use strict';
_ = _ && Object.prototype.hasOwnProperty.call(_, 'default') ? _['default'] : _;
Promise = Promise && Object.prototype.hasOwnProperty.call(Promise, 'default') ? Promise['default'] : Promise;
uuid = uuid && Object.prototype.hasOwnProperty.call(uuid, 'default') ? uuid['default'] : uuid;
loggerFactory = loggerFactory && Object.prototype.hasOwnProperty.call(loggerFactory, 'default') ? loggerFactory['default'] : loggerFactory;
/*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; under version 2
* of the License (non-upgradable).
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*
* Copyright (c) 2015-2019 (original work) Open Assessment Technologies SA;
*
*/
/**
* All events have a namespace, this one is the default
*/
const defaultNs = '@';
/**
* Namespace that targets all event
*/
const globalNs = '*';
/**
* Create a logger
*/
const eventifierLogger = loggerFactory('core/eventifier');
/**
* Get the list of events from an eventName string (ie, separated by spaces)
* @param {String} eventNames - the event strings
* @returns {String[]} the event list (no empty, no duplicate)
*/
function getEventNames(eventNames) {
if (!_.isString(eventNames) || _.isEmpty(eventNames)) {
return [];
}
return _(eventNames.split(/\s/g)).compact().uniq().value();
}
/**
* Get the name part of an event name: the 'foo' of 'foo.bar'
* @param {String} eventName - the name of the event
* @returns {String} the name part
*/
function getName(eventName) {
if (eventName.indexOf('.') > -1) {
return eventName.substr(0, eventName.indexOf('.'));
}
return eventName;
}
/**
* Get the namespace part of an event name: the 'bar' of 'foo.bar'
* @param {String} eventName - the name of the event
* @returns {String} the namespace, that defaults to defaultNs
*/
function getNamespace(eventName) {
if (eventName.indexOf('.') > -1) {
return eventName.substr(eventName.indexOf('.') + 1);
}
return defaultNs;
}
/**
* Creates a new EventHandler object structure
* @returns {Object} the handler structure
*/
function getHandlerObject() {
return {
before: [],
between: [],
after: []
};
}
/**
* Makes the target an event emitter by delegating calls to the event API.
* @param {Object} [target] - the target object, a new plain object is created when omited.
* @returns {Object} the target for conveniance
*/
function eventifier(target) {
var targetName;
var logger;
var stoppedEvents;
//it stores all the handlers under ns/name/[handlers]
let eventHandlers = {};
/**
* Get the handlers for an event type
* @param {String} eventName - the event name, namespace included
* @param {String} [type='between'] - the type of event in before, between and after
* @returns {Function[]} the handlers
*/
function getHandlers(eventName, type) {
const name = getName(eventName);
const ns = getNamespace(eventName);
type = type || 'between';
eventHandlers[ns] = eventHandlers[ns] || {};
eventHandlers[ns][name] = eventHandlers[ns][name] || getHandlerObject();
return eventHandlers[ns][name][type];
}
/**
* The API itself is just a placeholder, all methods will be delegated to a target.
*/
const eventApi = {
/**
* Attach an handler to an event.
* Calling `on` with the same eventName multiple times add callbacks: they
* will all be executed.
*
* @example target.on('foo', function(bar){ console.log('Cool ' + bar) } );
*
* @this the target
* @param {String} eventNames - the name of the event, or multiple events separated by a space
* @param {Function} handler - the callback to run once the event is triggered
* @returns {Object} the target object
*/
on(eventNames, handler) {
if (_.isFunction(handler)) {
_.forEach(getEventNames(eventNames), eventName => {
getHandlers(eventName).push(handler);
});
}
return this;
},
/**
* Remove ALL handlers for an event.
*
* @example remove ALL
* target.off('foo');
*
* @example remove targeted namespace
* target.off('foo.bar');
*
* @example remove all handlers by namespace
* target.off('.bar');
*
* @example remove all namespaces, keep non namespace
* target.off('.*');
*
* @this the target
* @param {String} eventNames - the name of the event, or multiple events separated by a space
* @returns {Object} the target object
*/
off(eventNames) {
_.forEach(getEventNames(eventNames), function (eventName) {
const name = getName(eventName);
const ns = getNamespace(eventName);
if (ns && !name) {
if (ns === globalNs) {
const offNamespaces = {};
offNamespaces[defaultNs] = eventHandlers[defaultNs];
eventHandlers = offNamespaces;
} else {
//off the complete namespace
eventHandlers[ns] = {};
}
} else {
_.forEach(eventHandlers, function (nsHandlers, namespace) {
if (nsHandlers[name] && (ns === defaultNs || ns === namespace)) {
nsHandlers[name] = getHandlerObject();
}
});
}
});
return this;
},
/**
* Remove ALL registered handlers
*
* @example remove ALL
* target.removeAllListeners();
*
* @this the target
* @returns {Object} the target object
*/
removeAllListeners() {
// full erase
eventHandlers = {};
return this;
},
/**
* Trigger an event.
*
* @example target.trigger('foo', 'Awesome');
*
* @this the target
* @param {String} eventNames - the name of the event to trigger, or multiple events separated by a space
* @param {...*} [args] - parameters that will be passed to the listeners.
* @returns {Object} the target object
*/
trigger(eventNames) {
for (var _len = arguments.length, args = new Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) {
args[_key - 1] = arguments[_key];
}
// @todo: remove self
const self = this;
stoppedEvents = {};
_.forEach(getEventNames(eventNames), function (eventName) {
const ns = getNamespace(eventName);
const name = getName(eventName);
//check which ns needs to be executed and then merge the handlers to be executed
const mergedHandlers = _(eventHandlers).filter(function (nsHandlers, namespace) {
return nsHandlers[name] && (ns === defaultNs || ns === namespace || namespace === globalNs);
}).reduce(function (acc, nsHandlers) {
acc.before = acc.before.concat(nsHandlers[name].before);
acc.between = acc.between.concat(nsHandlers[name].between);
acc.after = acc.after.concat(nsHandlers[name].after);
return acc;
}, getHandlerObject());
logger.trace({
event: eventName,
args: args
}, 'trigger %s', eventName);
if (mergedHandlers) {
triggerAllHandlers(mergedHandlers, name, ns);
}
});
function triggerAllHandlers(allHandlers, name, ns) {
const event = {
name: name,
namespace: ns
};
if (allHandlers.before.length) {
triggerBefore(allHandlers.before, event).then(function () {
triggerBetween(allHandlers, event);
}).catch(function (err) {
logHandlerStop('before', event, err);
});
} else {
triggerBetween(allHandlers, event);
}
}
function triggerBefore(handlers, event) {
// .before() handlers will get a special 'event' object as their first parameter
const beforeArgs = [event, ...args];
const pHandlers = handlers.map(handler => {
// .before() handlers use to return false to cancel the call stack
// to maintain backward compatibility, we treat this case as a rejected Promise
const value = shouldStop(event.name) ? false : handler.apply(self, beforeArgs);
return value === false ? Promise.reject() : value;
});
return Promise.all(pHandlers);
}
function triggerBetween(allHandlers, event) {
if (shouldStop(event.name)) {
logHandlerStop('before', event); // .stopEvent() has been called in an async .before() callback
} else {
// trigger the event handlers
triggerHandlers(allHandlers.between, event).then(function () {
triggerAfter(allHandlers.after, event);
}).catch(function (err) {
logHandlerStop('on', event, err);
});
}
}
function triggerAfter(handlers, event) {
if (shouldStop(event.name)) {
logHandlerStop('on', event); // .stopEvent() has been called in an async .on() callback
} else {
triggerHandlers(handlers, event).then(function () {
if (shouldStop(event.name)) {
logHandlerStop('after', event); // .stopEvent() has been called in an async .after() callback
}
}).catch(function (err) {
logHandlerStop('after', event, err);
});
}
}
function triggerHandlers(handlers, event) {
const pHandlers = handlers.map(handler => {
if (shouldStop(event.name)) {
return Promise.reject();
}
return handler.apply(self, args);
});
return Promise.all(pHandlers);
}
function logHandlerStop(stoppedIn, event, err) {
if (err instanceof Error) {
logger.error(err);
}
logger.trace({
err: err,
event: event.name,
stoppedIn: stoppedIn
}, `${event.name} handlers stopped`);
}
function shouldStop(name) {
return stoppedEvents[name];
}
return this;
},
/**
* Register a callback that is executed before the given event name
* Provides an opportunity to cancel the execution of the event if one of the returned value is false
*
* @this the target
* @param {String} eventNames - the name of the event, or multiple events separated by a space
* @param {Function} handler - the callback to run once the event is triggered
* @returns {Object} the target object
*/
before(eventNames, handler) {
if (_.isFunction(handler)) {
_.forEach(getEventNames(eventNames), function (eventName) {
getHandlers(eventName, 'before').push(handler);
});
}
return this;
},
/**
* Register a callback that is executed after the given event name
* The handlers will all be executed, no matter what
*
* @this the target
* @param {String} eventNames - the name of the event, or multiple events separated by a space
* @param {Function} handler - the callback to run once the event is triggered
* @returns {Object} the target object
*/
after(eventNames, handler) {
if (_.isFunction(handler)) {
_.forEach(getEventNames(eventNames), function (eventName) {
getHandlers(eventName, 'after').push(handler);
});
}
return this;
},
/**
* If triggered into an sync handler, this immediately cancels the execution of all following handlers
* regardless of their category
* If triggered asynchronously, this will only cancel the next category of handlers:
* - .on() and .after() if triggered during a .before() handler
* - .after() if triggered during a .on() handler
* - nothing if triggered during a .after() handler
* In an async context, you can also reject a Promise with the same results
*
* @param {string} name - of the event to stop
*/
stopEvent(name) {
if (_.isString(name) && !_.isEmpty(name.trim())) {
stoppedEvents[name.trim()] = true;
}
},
/**
* Spread events to another eventifier object.
* So when an event is triggered on the current target,
* it get's triggered on the destination too.
*
* Be careful, the forward will be triggered only if the event reach the `on` steps
* (it can be canceled by a before).
*
* @param {eventifier} destination - the destination emitter
* @param {String|String[]} eventNames - the list of events to forward
* @returns {Object} target - chains
*/
spread(destination, eventNames) {
if (destination && _.isFunction(destination.trigger)) {
if (_.isString(eventNames)) {
eventNames = getEventNames(eventNames);
}
_.forEach(eventNames, eventName => {
this.on(eventName, function () {
for (var _len2 = arguments.length, args = new Array(_len2), _key2 = 0; _key2 < _len2; _key2++) {
args[_key2] = arguments[_key2];
}
destination.trigger(eventName, ...args);
});
});
}
return this;
}
};
target = target || {};
//try to get something that looks like a name, an id or generate one only for logging purposes
targetName = target.name || target.id || target.serial || uuid(6);
//create a child logger per eventifier
logger = eventifierLogger.child({
target: targetName
});
_(eventApi).functions().forEach(function (method) {
if (_.isFunction(target[method])) {
eventifierLogger.warn(`The target object has already a method named ${method}`, target);
}
target[method] = function delegate() {
for (var _len3 = arguments.length, args = new Array(_len3), _key3 = 0; _key3 < _len3; _key3++) {
args[_key3] = arguments[_key3];
}
return eventApi[method].apply(target, args);
};
});
return target;
}
return eventifier;
});