@reldens/utils
Version:
454 lines (422 loc) • 15.3 kB
JavaScript
/**
*
* Reldens - EventsManager
*
*/
const Logger = require('./logger');
const sc = require('./shortcuts');
class EventsManager
{
constructor()
{
this._events = {};
this.eventsByRemoveKeys = {};
this.debug = false;
this._listenersCache = {};
this._validationCache = new Map();
this._debugPatterns = null;
this.maxEventKeyLength = 1000;
this.maxListeners = 10000;
this.maxEventArgs = 50;
this.sensitiveFields = ['password', 'token', 'secret', 'key', 'auth', 'credential'];
this.hasLoggedMaxListeners = false;
this.symbolString = '--[[await-event-emitter]]--';
this.typeKeyName = sc.isFunction(Symbol) ? Symbol.for(this.symbolString) : this.symbolString;
}
assertType(type)
{
if(!sc.isString(type) && !sc.isSymbol(type)){
throw new TypeError('type is not type of string or symbol!');
}
}
assertFn(fn)
{
if(!sc.isFunction(fn)){
throw new TypeError('fn is not type of Function!');
}
}
alwaysListener(fn)
{
return {
[this.typeKeyName]: 'always',
fn
};
}
onceListener(fn)
{
return {
[this.typeKeyName]: 'once',
fn
};
}
validateEventKey(eventKey)
{
if(this._validationCache.has(eventKey)){
return this._validationCache.get(eventKey);
}
if(sc.isString(eventKey) && eventKey.length > this.maxEventKeyLength){
Logger.critical('Event key exceeds maximum length: '+eventKey.length);
this._validationCache.set(eventKey, false);
return false;
}
let isValid = !sc.hasDangerousKeys(null, eventKey);
this._validationCache.set(eventKey, isValid);
return isValid;
}
sanitizeEventArgs(args)
{
if(!sc.isArray(args)){
return [];
}
if(args.length > this.maxEventArgs){
Logger.warning('Event arguments exceed maximum: '+args.length);
return [];
}
return args.map(arg => {
if(sc.isObject(arg)){
return this.filterSensitiveData(arg);
}
return arg;
});
}
filterSensitiveData(obj)
{
if(!sc.isObject(obj)){
return obj;
}
let filtered = {};
for(let key of Object.keys(obj)){
if(sc.hasDangerousKeys(null, key)){
continue;
}
let isKeywordSensitive = this.sensitiveFields.some(sensitive =>
key.toLowerCase().includes(sensitive.toLowerCase())
);
if(isKeywordSensitive){
filtered[key] = '[FILTERED]';
continue;
}
if(sc.isObject(obj[key])){
filtered[key] = this.filterSensitiveData(obj[key]);
continue;
}
filtered[key] = obj[key];
}
return filtered;
}
checkMemoryLeaks()
{
if(this.hasLoggedMaxListeners){
return true;
}
let totalListeners = 0;
for(let eventType of Object.keys(this._events)){
totalListeners += this._events[eventType].length;
}
if(totalListeners > this.maxListeners){
Logger.debug('High listener count detected: '+totalListeners+' total listeners');
this.hasLoggedMaxListeners = true;
}
return true;
}
addListener(type, fn)
{
return this.on(type, fn);
}
on(type, fn)
{
this.assertType(type);
this.assertFn(fn);
if(!this.validateEventKey(type)){
Logger.critical('Invalid event key detected: '+type);
return false;
}
if(false !== this.debug){
this.logDebugEvent(type, 'Listen');
}
this._events[type] = this._events[type] || [];
this._events[type].push(this.alwaysListener(fn));
delete this._listenersCache[type];
this.checkMemoryLeaks();
return this;
}
prependListener(type, fn)
{
return this.prepend(type, fn);
}
prepend(type, fn)
{
this.assertType(type);
this.assertFn(fn);
if(!this.validateEventKey(type)){
Logger.critical('Invalid event key detected: '+type);
return false;
}
this._events[type] = this._events[type] || [];
this._events[type].unshift(this.alwaysListener(fn));
delete this._listenersCache[type];
this.checkMemoryLeaks();
return this;
}
prependOnceListener(type, fn)
{
return this.prependOnce(type, fn);
}
prependOnce(type, fn)
{
this.assertType(type);
this.assertFn(fn);
if(!this.validateEventKey(type)){
Logger.critical('Invalid event key detected: '+type);
return false;
}
this._events[type] = this._events[type] || [];
this._events[type].unshift(this.onceListener(fn));
delete this._listenersCache[type];
this.checkMemoryLeaks();
return this;
}
listeners(type)
{
if(this._listenersCache[type]){
return this._listenersCache[type];
}
let result = (this._events[type] || []).map((x) => x.fn);
this._listenersCache[type] = result;
return result;
}
once(type, fn)
{
this.assertType(type);
this.assertFn(fn);
if(!this.validateEventKey(type)){
Logger.critical('Invalid event key detected: '+type);
return false;
}
this._events[type] = this._events[type] || [];
this._events[type].push(this.onceListener(fn));
delete this._listenersCache[type];
this.checkMemoryLeaks();
return this;
}
removeAllListeners()
{
this._events = {};
this._listenersCache = {};
}
off(type, nullOrFn)
{
return this.removeListener(type, nullOrFn);
}
removeListener(type, nullOrFn)
{
this.assertType(type);
if(!this.validateEventKey(type)){
Logger.critical('Invalid event key detected: '+type);
return false;
}
let listeners = this.listeners(type);
if(sc.isFunction(nullOrFn)){
let index = -1;
let found = false;
while(-1 < (index = listeners.indexOf(nullOrFn))){
listeners.splice(index, 1);
this._events[type].splice(index, 1);
found = true;
}
delete this._listenersCache[type];
return found;
}
delete this._events[type];
delete this._listenersCache[type];
return true;
}
async emit(type, ...args)
{
this.assertType(type);
if(!this.validateEventKey(type)){
Logger.critical('Invalid event key detected: '+type);
return false;
}
let sanitizedArgs = this.sanitizeEventArgs(args);
if(false !== this.debug){
this.logDebugEvent(type, 'Fire', sanitizedArgs);
}
this.checkMemoryLeaks();
let listeners = this.listeners(type);
let onceListeners = [];
if(listeners && listeners.length){
for(let i = 0; i < listeners.length; i++){
let event = listeners[i];
let rlt = event.apply(this, sanitizedArgs);
if(sc.isPromise(rlt)){
await rlt;
}
if(this._events[type] && this._events[type][i] && 'once' === this._events[type][i][this.typeKeyName]){
onceListeners.push(event);
}
}
for(let event of onceListeners){
this.removeListener(type, event);
}
return true;
}
return false;
}
emitSync(type, ...args)
{
this.assertType(type);
if(!this.validateEventKey(type)){
Logger.critical('Invalid event key detected: '+type);
return false;
}
let sanitizedArgs = this.sanitizeEventArgs(args);
let listeners = this.listeners(type);
let onceListeners = [];
if(listeners && listeners.length){
for(let i = 0; i < listeners.length; i++){
let event = listeners[i];
event.apply(this, sanitizedArgs);
if(this._events[type] && this._events[type][i] && 'once' === this._events[type][i][this.typeKeyName]){
onceListeners.push(event);
}
}
for(let event of onceListeners){
this.removeListener(type, event);
}
return true;
}
return false;
}
onWithKey(eventName, callback, uniqueRemoveKey, masterKey)
{
if(!this.validateEventKey(eventName)){
Logger.critical('Invalid event key detected: '+eventName);
return false;
}
if(!this.validateEventKey(uniqueRemoveKey)){
Logger.critical('Invalid remove key detected: '+uniqueRemoveKey);
return false;
}
if(masterKey && !this.validateEventKey(masterKey)){
Logger.critical('Invalid master key detected: '+masterKey);
return false;
}
if(sc.hasOwn(this.eventsByRemoveKeys, uniqueRemoveKey)){
Logger.debug('Event "'+eventName+'" exists with key "'+uniqueRemoveKey+'".');
return false;
}
if(masterKey && sc.hasOwn(this.eventsByRemoveKeys, masterKey) && sc.hasOwn(this.eventsByRemoveKeys[masterKey], uniqueRemoveKey)){
Logger.debug('Event "'+eventName+'" exists with key "'+uniqueRemoveKey+'" and masterKey "'+masterKey+'".');
return false;
}
this.on(eventName, callback);
let dataArr = this.listeners(eventName);
let currentListenerIndex = dataArr.indexOf(callback);
let currentListener = dataArr[currentListenerIndex];
if(!masterKey){
this.eventsByRemoveKeys[uniqueRemoveKey] = {eventName, callback};
return currentListener;
}
if(!sc.hasOwn(this.eventsByRemoveKeys, masterKey)){
this.eventsByRemoveKeys[masterKey] = {};
}
this.eventsByRemoveKeys[masterKey][uniqueRemoveKey] = {eventName, callback};
return currentListener;
}
offWithKey(uniqueRemoveKey, masterKey)
{
if(!this.validateEventKey(uniqueRemoveKey)){
Logger.critical('Invalid remove key detected: '+uniqueRemoveKey);
return false;
}
if(masterKey && !this.validateEventKey(masterKey)){
Logger.critical('Invalid master key detected: '+masterKey);
return false;
}
if(masterKey && !sc.hasOwn(this.eventsByRemoveKeys, masterKey)){
Logger.debug('Event not found by masterKey "'+masterKey+'".');
return false;
}
if(!masterKey && !sc.hasOwn(this.eventsByRemoveKeys, uniqueRemoveKey)){
Logger.debug('Event not found by removeKey "'+uniqueRemoveKey+'".');
return false;
}
let eventToRemove = masterKey
? this.eventsByRemoveKeys[masterKey][uniqueRemoveKey]
: this.eventsByRemoveKeys[uniqueRemoveKey];
if(!this.validateEventKey(eventToRemove.eventName)){
Logger.critical('Invalid event name in stored event: '+eventToRemove.eventName);
return false;
}
let dataArr = this.listeners(eventToRemove.eventName);
let currentListenerIndex = dataArr.indexOf(eventToRemove.callback);
if(-1 === currentListenerIndex){
Logger.debug('Event listener not found in _events array.');
return false;
}
this._events[eventToRemove.eventName].splice(currentListenerIndex, 1);
if(0 === this._events[eventToRemove.eventName].length){
delete this._events[eventToRemove.eventName];
}
delete this._listenersCache[eventToRemove.eventName];
if(masterKey){
delete this.eventsByRemoveKeys[masterKey][uniqueRemoveKey];
Logger.debug('Deleted event by removeKey "'+uniqueRemoveKey+'" and masterKey "'+masterKey+'".');
return true;
}
delete this.eventsByRemoveKeys[uniqueRemoveKey];
Logger.debug('Deleted event by removeKey "'+uniqueRemoveKey+'".');
return true;
}
offByMasterKey(masterKey)
{
if(!this.validateEventKey(masterKey)){
Logger.critical('Invalid master key detected: '+masterKey);
return false;
}
if(!sc.hasOwn(this.eventsByRemoveKeys, masterKey)){
Logger.debug('Events not found by masterKey "'+masterKey+'".');
return false;
}
Logger.debug('Removing events by masterKey: '+masterKey, Object.keys(this.eventsByRemoveKeys[masterKey]));
let eventsToRemove = Object.keys(this.eventsByRemoveKeys[masterKey]);
let affectedEvents = new Set();
for(let uniqueRemoveKey of eventsToRemove){
let eventToRemove = this.eventsByRemoveKeys[masterKey][uniqueRemoveKey];
if(!this.validateEventKey(eventToRemove.eventName)){
continue;
}
affectedEvents.add(eventToRemove.eventName);
let dataArr = this.listeners(eventToRemove.eventName);
let currentListenerIndex = dataArr.indexOf(eventToRemove.callback);
if(-1 === currentListenerIndex){
continue;
}
this._events[eventToRemove.eventName].splice(currentListenerIndex, 1);
if(0 === this._events[eventToRemove.eventName].length){
delete this._events[eventToRemove.eventName];
}
}
for(let eventName of affectedEvents){
delete this._listenersCache[eventName];
}
delete this.eventsByRemoveKeys[masterKey];
}
logDebugEvent(key, type, args = null)
{
if(!this._debugPatterns){
this._debugPatterns = new Set(this.debug.split(','));
}
if(!this._debugPatterns.has('all') && !this._debugPatterns.has(key) && -1 === key.indexOf(this.debug)){
return;
}
let logMessage = type+' Event: '+key;
if(args && 0 < args.length){
let filteredArgs = args.map(arg => this.filterSensitiveData(arg));
logMessage += ' with '+filteredArgs.length+' arguments';
}
Logger.debug(logMessage);
}
}
module.exports = EventsManager;