const waterfall = require('async/waterfall');
const RootEmitter = require('./root_emitter');
const augmentSender = require('../plugins/augment/sender');
/**
An ChatEngine generic emitter that supports plugins and duplicates
events on the root emitter.
@class Emitter
@extends RootEmitter
*/
class Emitter extends RootEmitter {
constructor(chatEngine) {
super();
this.chatEngine = chatEngine;
this.name = 'Emitter';
/**
Stores a list of plugins bound to this object
@private
*/
this.plugins = [];
/**
Stores in memory keys and values
@private
*/
this._dataset = {};
this.plugin(augmentSender(chatEngine));
/**
Emit events locally.
@private
@param {String} event The event payload object
*/
this._emit = (event, data = {}) => {
// all events are forwarded to ChatEngine object
// so you can globally bind to events with ChatEngine.on()
this.chatEngine._emit(event, data, this);
// emit the event from the object that created it
this.emitter.emit(event, data);
return this;
};
/**
* Listen for a specific event and fire a callback when it's emitted. Supports wildcard matching.
* @method
* @param {String} event The event name
* @param {Function} cb The function to run when the event is emitted
* @example
*
* // Get notified whenever someone joins the room
* object.on('event', (payload) => {
* console.log('event was fired').
* })
*
* // Get notified of event.a and event.b
* object.on('event.*', (payload) => {
* console.log('event.a or event.b was fired').;
* })
*/
this.on = (event, cb) => {
// call the private _on property
this._on(event, cb);
return this;
};
}
// add an object as a subobject under a namespace
/**
* @private
*/
addChild(childName, childOb) {
// assign the new child object as a property of parent under the
// given namespace
this[childName] = childOb;
// assign a data set for the namespace if it doesn't exist
if (!this._dataset[childName]) {
this._dataset[childName] = {};
}
// the new object can use ```this.parent``` to access
// the root class
childOb.parent = this;
// bind get() and set() to the data set
childOb.get = this.get.bind(this._dataset[childName]);
childOb.set = this.set.bind(this._dataset[childName]);
}
get(key) {
return this[key];
}
set(key, value) {
if (this[key] && !value) {
delete this[key];
} else {
this[key] = value;
}
}
/**
Binds a plugin to this object
@param {Object} module The plugin module
@tutorial using
*/
plugin(module) {
// add this plugin to a list of plugins for this object
this.plugins.push(module);
// see if there are plugins to attach to this class
if (module.extends && module.extends[this.name]) {
// attach the plugins to this class
// under their namespace
this.addChild(module.namespace, new module.extends[this.name]());
this[module.namespace].ChatEngine = this.chatEngine;
// if the plugin has a special construct function
// run it
if (this[module.namespace].construct) {
this[module.namespace].construct();
}
}
return this;
}
/**
* @private
*/
bindProtoPlugins() {
if (this.chatEngine.protoPlugins[this.name]) {
this.chatEngine.protoPlugins[this.name].forEach((module) => {
this.plugin(module);
});
}
}
/**
Broadcasts an event locally to all listeners.
@private
@param {String} event The event name
@param {Object} payload The event payload object
*/
trigger(event, payload = {}, done = () => {}) {
// let plugins modify the event
this.runPluginQueue('on', event, (next) => {
next(null, payload);
}, (reject, pluginResponse) => {
if (reject) {
done(reject);
} else {
// emit this event to any listener
this._emit(event, pluginResponse);
done(null, event, pluginResponse);
}
});
}
/**
Load plugins and attach a queue of functions to execute before and
after events are trigger or received.
@private
@param {String} location Where in the middleeware the event should run (emit, trigger)
@param {String} event The event name
@param {String} first The first function to run before the plugins have run
@param {String} last The last function to run after the plugins have run
*/
runPluginQueue(location, event, first, last) {
// this assembles a queue of functions to run as middleware
// event is a triggered event key
let pluginQueue = [];
// the first function is always required
pluginQueue.push(first);
// look through the configured plugins
this.plugins.forEach((pluginItem) => {
// if they have defined a function to run specifically
// for this event
if (pluginItem.middleware && pluginItem.middleware[location]) {
if (pluginItem.middleware[location][event]) {
// add the function to the queue
pluginQueue.push(pluginItem.middleware[location][event]);
}
if (pluginItem.middleware[location]['*']) {
// add the function to the queue
pluginQueue.push(pluginItem.middleware[location]['*']);
}
}
});
// waterfall runs the functions in assigned order
// waiting for one to complete before moving to the next
// when it's done, the ```last``` parameter is called
waterfall(pluginQueue, last);
}
/**
* @private
*/
onConstructed() {
this.bindProtoPlugins();
this.trigger(['$', 'created', this.name.toLowerCase()].join('.'));
}
}
module.exports = Emitter;