// TODO: use NodeEventTarget instead of EventEmitter; still experimental in nodejs v14.5
const EventEmitter = require("promise-events");
const {Breakpoint} = require("./Breakpoint");
const {EventFilter} = require("./EventFilter");
const {checkType, checkInstance} = require("./Utility");
/**
* Abstract base class for all the types of events
*/
class EventBase {
/**
* Abstract constructor. Detects errors in derived classes.
*/
constructor() {
checkInstance("EventBase.constructor", "allowedEventTypes", this.allowedEventTypes, Set);
checkInstance("EventBase.constructor", "eventBus", this.eventBus, EventBusBase);
checkType("EventBase.constructor", "sourceType", this.sourceType, "string");
checkType("EventBase.constructor", "sourceName", this.sourceName, "string");
}
/**
* Returns a string representing the name of source of this event
*
* @returns {Set} Set of strings of valid event types
*/
get sourceName() {
throw new Error("sourceName not implemented");
}
/**
* Returns a string that describe the source type of this event
*
* @returns {Set} Set of strings of valid event types
*/
get sourceType() {
throw new Error("sourceType not implemented");
}
/**
* Returns a Set of strings that describe the valid types of sources
*
* @returns {Set} Set of strings of valid event types
*/
// get allowedSourceTypes() {
// throw new Error ("allowedSourceTypes not implemented");
// }
/**
* Returns a Set of strings that describe the valid types of events
*
* @returns {Set} Set of strings of valid event types
*/
get allowedEventTypes() {
throw new Error("allowedEventTypes not implemented");
}
/**
* Returns a EventEmitter that will be used for the global event bus
*
* @returns {EventEmitter} The object that will be used as the global event bus for this kind of event
*/
get eventBus() {
throw new Error("eventBus not implemented");
}
/**
* Emits the event on the specified event bus
*
* @param {string} type The type of the event
* @param {object} data The optional data associated with the event
* @returns {Promise.<boolean>} Returns a Promise resolving to `true` if the event had listeners, `false` otherwise
*/
async emit(type, ... data) {
if (!this.allowedEventTypes.has(type)) {
throw new TypeError(`event type '${type}' not one of the allowedEventTypes`);
}
this.type = type;
this.data = (data.length < 2) ? data[0] : data;
return this.eventBus.emit(type, this, ... data);
}
/**
* Convert an event to a human-readable string
*
* @returns {string} A string describing this event
*/
toString() {
return `${this.sourceName}::${this.sourceType} => ${this.type}`;
}
}
const eventBusMap = new Set();
/**
* Abstract base class for all the event busses
*
* @extends EventEmitter
* @property {Set} allowedEvenets - A `Set` of events that are allowed on this bus
*/
class EventBusBase extends EventEmitter {
/**
* Creates a new event bus that can only send or receive a specific type of events
*
* @param {EventBase} baseEvent - A class that implements EventBase. The event bus will only allow this type of event.
* @param {*} args - Arguments passed through to the {@link https://nodejs.org/api/events.html#events_class_eventemitter|EventEmitter} base class.
*
* @returns {EventBusBase} The EventBusBase object
*/
constructor(baseEvent, ... args) {
super(... args);
checkType("EventBusBase.constructor", "baseEvent", baseEvent, "class");
if (baseEvent === EventBase) {
throw new TypeError("constructor requires a class derived from EventBase but attempted to pass EventBase itself");
}
checkInstance("EventBusBase.constructor", "baseEvent", baseEvent.prototype, EventBase);
this._baseEvent = baseEvent;
eventBusMap.add(this);
}
/**
* Checks if an event is of the correct type for this event bus
*
* @param {EventBase} event - The object to check to see if it is the right type
* @throws TypeError on event that is wrong type
*/
checkEvent(event) {
checkInstance("EventBusBase.checkEvent", "event", event, this._baseEvent);
if (!event.type) {
console.warn("Emitting event without a type:", event);
}
}
// eslint-disable-next-line jsdoc/require-jsdoc
get allowedEvents() {
return this._baseEvent.prototype.allowedEventTypes;
}
/**
* Synchronously calls each of the listeners registered for the event named eventName, in the order they were registered, passing the supplied arguments to each. See also: {link https://nodejs.org/api/events.html#events_emitter_emit_eventname_args|EventEmitter.emit}
*
* @param {string} eventName - The name of the event
* @param {EventBase} event - An event that inherits from EventBase and is type of event described by `eventBase` in {@link EventBusBase.constructor}
* @param {...*} [args] - Any arguments
*
* @returns {Promise.<boolean>} Returns a Promise that resolves to `true` if the event had listeners; `false` otherwise
*/
async emit(eventName, event, ... args) {
this.checkEvent(event);
let ret = await Breakpoint.checkBreak(event, async() => {
return super.emit(eventName, event, ... args);
});
EventListener.listenAllList.forEach((fn) => {
fn(event);
});
return !!ret;
}
/**
* Returns a Map of the event busses that have been created
*
* @returns {Map} A Map of the event busses, where the Map key is the name of the bus and the Map entry is the corresponding EventBusBase object
*/
static get eventBusList() {
return eventBusMap;
}
}
const listenAllList = [];
/**
* Listens for events on the specified {@link EventBusBase}, applying the specified {@link EventFilter}s before calling the
* specified `callback`.
*/
class EventListener {
/**
* Creates an event listener for the specified `bus`, calling the `callback` function for any events that meet pass the `filterList`
*
* @param {EventBusBase} bus - The event bus to listen on
* @param {EventFilter|EventFilter[]|null} filterList - A list of events to filter on. If `null` all events on the
* `bus` will call `callback` or filters may be added later using
* the {@link EventListener#addFilter} method.
* @param {Function} callback - Will be called when an event meeting the `filterList` criteria
* is received. Callback has a single argument of an {@link EventBase}
* event.
*/
constructor(bus, filterList, callback) {
checkInstance("EventListener.constructor", "bus", bus, EventBusBase);
checkType("EventListener.constructor", "callback", callback, "function");
checkType("EventListener.constructor", "filterList", filterList, "object");
if (!Array.isArray(filterList)) {
filterList = [filterList];
}
this._callback = callback;
this.filterList = [];
this.attachedEvents = new Set();
this.bus = bus;
if (filterList[0] !== null) {
filterList.forEach((f) => this.addFilter(f));
}
// TODO: add all listeners
this.update();
}
/**
* Adds a filter to the EventListener. If the filter has a `priority` it will be added in priority order; otherwise,
* it will be added to the end of the list.
*
* @param {EventFilter} filter The new filter to add.
*/
addFilter(filter) {
checkInstance("addFilter", "filter", filter, EventFilter);
this.filterList.push(filter);
this.filterList.sort((e1, e2) => {
return e1.priority - e2.priority;
});
if (filter.allow && filter.criteria.eventType) {
this.attachedEvents.add(filter.criteria.eventType);
}
}
// TODO
// removeFilter() {}
/**
* Calls {@link EventBusBase#addListener} on the bus for all the events that will be detected by the filters.
*/
update() {
// no events specified, listen for all allowable events
if (this.attachedEvents.size === 0) {
this.attachedEvents = new Set([... this.bus.allowedEvents.values()]);
}
this.attachedEvents.forEach((eventType) => this.bus.addListener(eventType, this.applyFilter.bind(this)));
}
// TODO
// stop() {}
/**
* Triggers the `callback` specified in the constructor if the `event` passes the `filterList`.
* Typically called internally when the `bus` emits an event.
*
* @param {EventBase} event An event derived from the {@link EventBase} class
* @returns {undefined} No return value
*/
applyFilter(event) {
let allow = false;
for (const filter of this.filterList) {
if (filter.denyEvent(event)) {
return;
}
if (filter.allowEvent(event)) {
allow = true;
break;
}
}
// default: deny
if (!allow) {
return;
}
this._callback(event);
}
/**
* Registers a callback function to be called on every event
*
* @param {Function} fn The callback function to be called on every event. It will be passed the event that triggered it.
*/
static listenAll(fn) {
checkType("listenAllList", "fn", fn, "function");
listenAllList.push(fn);
}
/**
* Clears all callback functions registered with `listenAll`
*/
static clearListenAll() {
listenAllList.length = 0;
}
/**
* The Array of callback functions registered with `listenAll`
*
* @returns {Array.<Function>} Returns an array of callback functions
*/
static get listenAllList() {
return listenAllList;
}
}
module.exports = {
EventBase,
EventBusBase,
EventListener,
};