Source: event-emitter.js

const _ = require('lodash');
const namedEventEmitters = {};

const convertWildcardToRegex = wildcardString => new RegExp(`^${wildcardString.replace(/\*/g, '.*')}$`, 'g');

const wildcardEventMatcher = (leftValue, rightValue) => {
    if (leftValue === '*' || rightValue === '*') {
        return true;
    }
    if (leftValue === rightValue) {
        return true;
    }
    return convertWildcardToRegex(leftValue).test(rightValue) || convertWildcardToRegex(rightValue).test(leftValue);
};

const checkListOptions = (options, value) => {
    return options.some(option => option === value);
};

const getListOptions = string => {
    if (string.startsWith('{') && string.endsWith('}')) {
        return string.substr(1).slice(0, -1).split(',');
    }
    return [];
};

const listOptionEventMatcher = (leftValue, rightValue) => {
    if (leftValue === rightValue) {
        return true;
    }
    return checkListOptions(getListOptions(leftValue), rightValue) || checkListOptions(getListOptions(rightValue), leftValue);
};

const sectionFilter = _.curry((sectionMatchers, lhs, rhs) => {
    const leftSections = lhs.split('.');
    const rightSections = rhs.split('.');
    if (leftSections.length !== rightSections.length) {
        return false;
    }
    return leftSections.every((eventSection, index) => {
        return sectionMatchers.some(sectionMatcher => sectionMatcher(leftSections[index], rightSections[index]));
    });
});

const getHandlers = {
    advanced: _.curry((data, filter, matchEvent) => {
        return Object.entries(data.events)
            .filter(([event]) => filter(matchEvent, event))
            .map(([__event, handlers]) => handlers)
            .reduce((allHandlers, handlers) => allHandlers.concat(handlers), []);
    }),
    exact: _.curry((data, matchEvent) => {
        return data.events[matchEvent] || [];
    })
};

const eventRegistrars = {
    basic: _.curry((data, event, handler) => {
        if (!data.events[event]) {
            data.events[event] = [];
        }
        data.events[event].push(handler);
    })
};

const eventEmitter = ({ getHandlers, registerEventHandler }) => {
    const getEventObject = eventName => {
        const meta = {
            action: 'continue'
        };
        return {
            meta,
            handlers: getHandlers(eventName),
            eventObject: Object.freeze({
                eventName,
                continueWithUndefined: Symbol('continue-with-undefined'),
                returnUndefined: Symbol('return-undefined'),
                preventDefault: () => meta.action = 'return'
            })
        };
    };

    /**
     * Emit an event synchronously. This emit will execute all matching handlers and return an array of their return values.
     *
     * @function
     * @name EventEmitter#emit
     * @param {string} event The name of the event to emit (use full-stop '.' as a namespace delimiter).
     * @param {...*} [args] Any number of arguments to pass to all event handlers.
     * @returns {array} An array of all the values returned by event handlers in the order they were executed.
     */
    const emit = (event, ...args) => {
        const { handlers, eventObject } = getEventObject(event);
        return handlers.map(handler => handler(eventObject, ...args));
    };

    /**
     * Emit an event asynchronously. This emit will execute all matching handlers concurrently (in parallel) and return an
     * array of their return values.
     *
     * @function
     * @name EventEmitter#emitAsync
     * @param {string} event The name of the event to emit (use full-stop '.' as a namespace delimiter).
     * @param {...*} [args] Any number of arguments to pass to all event handlers.
     * @returns {Promise<array>} A Promise that will resolve to an array of all the values returned by the event handlers in the order they were matched.
     */
    const emitAsync = async (event, ...args) => {
        const { handlers, eventObject } = getEventObject(event);
        return await Promise.all(handlers.map(async (handler) => await handler(eventObject, ...args)));
    };

    const updateWaterfallMetaData = ({ meta, eventObject }) => {
        switch (meta.lastResult) {
        case eventObject.continueWithUndefined:
            meta.nextInput = [];
            return;
        case eventObject.returnUndefined:
            meta.action = 'return';
            meta.result = undefined;
            return;
        case undefined:
            return;
        default:
            meta.result = meta.lastResult;
            meta.nextInput = [meta.result];
        }
    };

    /**
     * Emit an event synchronously and in order where the output of each handler in the chain becomes the input to the next.
     * The exception to this rule is that returning undefined from a handler will be interpreted as "leave the data unchanged".
     * If the intention of a handler is to continue execution, but replace the data of previous steps with undefined, return "event.continueWithUndefined" instead
     * of simply returning undefined. If the intention is to halt execution of subsequent steps and return undefined overall, return "event.returnUndefined"
     * where "event" is the first parameter passed to each handler.
     *
     * @function
     * @name EventEmitter#emitWaterfall
     * @param {string} event The name of the event to emit (use full-stop '.' as a namespace delimiter).
     * @param {...*} [args] Any number of arguments to pass to the first event handler.
     * @returns The return value of the last event handler that executed and returned a value.
     */
    const emitWaterfall = (event, ...args) => {
        const { handlers, eventObject, meta } = getEventObject(event);
        meta.nextInput = args;
        for (const handler of handlers) {
            meta.lastResult = handler(eventObject, ...meta.nextInput);
            updateWaterfallMetaData({ meta, eventObject });
            if (meta.action === 'return') {
                break;
            }
        }
        return meta.result;
    };

    /**
     * An asynchronous version of {@link EventEmitter#emitWaterfall}
     *
     * @function
     * @name EventEmitter#emitWaterfallAsync
     * @see {EventEmitter#emitWaterfall}
     * @param {string} event The name of the event to emit (use full-stop '.' as a namespace delimiter).
     * @param {...*} [args] Any number of arguments to pass to the first event handler.
     * @returns The return value of the last event handler that executed and returned a value.
     */
    const emitWaterfallAsync = async (event, ...args) => {
        const { handlers, eventObject, meta } = getEventObject(event);
        meta.nextInput = args;
        for (const handler of handlers) {
            meta.lastResult = await handler(eventObject, ...meta.nextInput);
            updateWaterfallMetaData({ meta, eventObject });
            if (meta.action === 'return') {
                break;
            }
        }
        return meta.result;
    };

    /**
     * Register an event handler. The event handler will be called when any event matching the event string is emitted. The order of execution and parameters
     * passed to the handler can change based on which emit was used. {@link EventEmitter#emit} {@link EventEmitter#emitWaterfall}.
     *
     * @function
     * @name EventEmitter#on
     * @param {string} event The name (or matching string) of the event you want the handler to be registered against.
     * @param {function} handler The event handler to register.
     */

    /**
     * @classdesc The main aggregating event emitter object.
     *
     * @class
     * @hideconstructor
     * @name EventEmitter
     */
    return {
        emit,
        emitAsync,
        emitWaterfall,
        emitWaterfallAsync,
        on: registerEventHandler
    };
};

/**
 * The options that can set on an event emitter when it is created.
 *
 * @typedef options
 * @type {object}
 * @property {boolean} [wildcards=false] Whether or not to enable wildcard matching in event names (e.g., "data.*" to match "data.get").
 * @property {boolean} [listOptions=false] Whether or not to enable list option matching in event names. (e.g., "data.{get,set}" to match both "data.get" and "data.set").
 * @property {boolean|array} [hooks=false] <p>False to disable hooks, or an array to specify lifecycle hooks (in order) to allow handlers to register against.
 *     E.g., if passing in ['first', 'before', 'default' 'after', 'last'] any handler registered as "namespace.event" will be in the "default" lifecycle, which will happen
 *     after those registered as "first:namespace.event" or "before:namespace.event".</p><p>If a "default" is not provided, an error will be raised if any handler is registered
 *     without a lifecycle marker. If the value true is provided instead of an array, the default lifecycles will be used (["early", "before", "default", "after", "late"]).</p><p>NOT YET IMPLEMENTED</p>
 */
const configureEventEmitter = ({ wildcards = false, listOptions = false }) => {
    const data = {
        cache: {},
        events: {}
    };

    const options = {
        getHandlers: getHandlers.exact(data),
        registerEventHandler: eventRegistrars.basic(data)
    };
    const sectionMatchers = [];
    if (wildcards) {
        sectionMatchers.push(wildcardEventMatcher);
    }
    if (listOptions) {
        sectionMatchers.push(listOptionEventMatcher);
    }

    if (sectionMatchers.length > 0) {
        options.getHandlers = getHandlers.advanced(data, sectionFilter(sectionMatchers));
    }
    return eventEmitter(options);
};

/**
 * Find an existing event emitter by name or create a new one. Named event emitters will always return the first instance
 * created when fetched by name unless they've been specifically deleted using the {@link removeNamedEventEmitter} or
 * {@link removeNamedEventEmitters} functions. If no name is provided, an anonymous event emitter that cannot be fetched
 * again will be created.
 *
 * @param {options} [options] The options to set for the event emitter (ignored if an event emitter by that name already exists).
 * @param {string} [options.name] The name of the event emitter to get or create.
 * @returns {EventEmitter} The event emitter associated with the name provided (or an anonymous one if a name is not provided).
 */
const aggregatingEventEmitter = ({ name, ...options } = {}) => {
    const eventEmitter = namedEventEmitters[name] || configureEventEmitter({ ...options });
    if (name) {
        namedEventEmitters[name] = eventEmitter;
    }
    return eventEmitter;
};

/**
 * Remove an existing event emitter by name. This will not stop the event emitter from functioning, only from being
 * returned from {@link aggregatingEventEmitter}.
 *
 * @param {string} name The name of the event emitter to remove the reference to.
 * @returns {boolean} True if the event emitter existed (and was removed), false otherwise.
 */
const removeNamedEventEmitter = (name) => {
    if (namedEventEmitters[name] === undefined) {
        return false;
    }
    delete namedEventEmitters[name];
    return true;
};

/**
 * Remove all existing event emitters. This will not stop the event emitters from functioning, only from being
 * returned from {@link aggregatingEventEmitter}.
 */
const removeNamedEventEmitters = () => {
    Object.keys(namedEventEmitters).forEach(key => delete namedEventEmitters[key]);
};

/**
 * @see {@link aggregatingEventEmitter}
 */
function AggregatingEventEmitter() {
    return aggregatingEventEmitter.apply(null, arguments);
}

module.exports = {
    AggregatingEventEmitter,
    aggregatingEventEmitter,
    removeNamedEventEmitter,
    removeNamedEventEmitters
};