jiken.js

"use strict";

/**
 * Browser EventEmitter
 *
 * Mimics API of node.js [EventEmitter]{@link https://nodejs.org/api/events.html}.
 *
 * And gives you some extra features.
 *
 * ## Usage
 *
 * ### Extend class:
 * ```
   const Jiken = require('jiken').Jiken;
   class MySuperEmitter extends Jiken {
       constructor() {
           super()
           this.on('some-event', () => console.log('trigger some-event'));
       }
   }

   const emitter = new MySuperEmitter();
   emitter.emit('some-event');
 * ```
 *
 * ### Use instance:
 * ```
   const Jiken = require('jiken').Jiken;

   const test = new Jiken();

   test.on('lolka', () => console.log('lol'));
 * ```
 */
class Jiken {
    /**
     * Creates new instance.
     */
    constructor() {
        this._events = {};

        /**
         * Alias to [on]{@link Jiken#on}.
         *
         * @memberof Jiken#
         * @method addListener
         *
         * @param {Any} name Event name.
         * @param {Function} listener Event listener to invoke.
         *
         * @returns {this} Itself for chain.
         */
        this.addListener = this.on;
        this.sync();
    }

    /**
     * Sets synchronous execution for listeners.
     * @returns {this} Itself for chain.
     */
    sync() {
        this._invoke_listener = (listener, args) => listener.apply(this, args);
        return this;
    }

    /**
     * Sets asynchronous execution for listeners.
     *
     * Under hood it uses [setTimeout]{@link https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/setTimeout} method to schedule execution of listeners.
     * While it is likely that order of execution will be preserved, it is not guaranteed.
     * Therefore you SHOULD not rely on your listeners to be executed in order they are set.
     *
     * @param {Integer} timeout Timeout. Optional. Default is 0.
     * @returns {this} Itself for chain.
     */
    not_sync(timeout) {
        this._invoke_listener = (listener, args) => setTimeout(() => listener.apply(this, args), timeout || 0);
        return this;
    }

    /**
     * Validator of listener. Throws if invalid.
     * @private
     * @param {Function} listener Event listener to invoke.
     * @returns {void}
     */
    _throw_on_invalid_listener(listener) {
        if (typeof listener !== "function") throw new TypeError("listener must be a function!");
    }

    /**
     * Initializes event array if required
     * @private
     * @param {Any} name Event name.
     * @returns {this} Itself for chain.
     */
    _init_event_if(name) {
        if (this._events[name] === undefined) this._events[name] = [];

        return this;
    }

    /**
     * Invokes event.
     *
     * @param {Any} name Event name.
     * @param {Any} args Arguments for listener.
     * @returns {Boolean} True if there are any listeners. False otherwise.
     */
    emit(name, ...args) {
        const event = this._events[name];

        //We should delete even completely once all listeners are removed.
        if (!event) return false;

        for (let idx = 0; idx < event.length; idx += 1) {
            let listener = event[idx];
            if (listener.once) {
                listener = listener.inner;
                event.splice(idx, 1);
                idx -= 1;

                this._invoke_listener(listener, args);
                if (event.length === 0) {
                    delete this._events[name];
                    break;
                }
            }
            else this._invoke_listener(listener, args);
        }

        return true;
    }

    /**
     * Invokes event.
     *
     * The same as [emit]{@link Jiken#emit}, but returns self for chain.
     *
     * @param {Any} name Event name.
     * @param {Any} args Arguments for listener.
     * @returns {this} Itself for chain.
     */
    trigger(name, ...args) {
        this.emit(name, ...args);
        return this;
    }

    /**
     * Retrieves array of events for which there are registered listeners.
     *
     * @returns {Array} Array of event names.
     */
    eventNames() {
        return Object.keys(this._events);
    }

    /**
     * Retrieves number of registered listeners for the event.
     *
     * @param {Any} name Event name.
     *
     * @returns {Integer} Number of listeners.
     */
    listenerCount(name) {
        return this._events[name] ? this._events[name].length : 0;
    }

    /**
     * Retrieves array of listeners for the event.
     *
     * @param {Any} name Event name.
     *
     * @returns {Array} Listeners.
     */
    listeners(name) {
        const event = this._events[name];

        if (event) {
            return event.map((listener) => listener.once ? listener.inner : listener);
        }
        else {
            return [];
        }
    }

    /**
     * Registers new event listener.
     *
     * Note that no check are made.
     * Listener is appended regardless if it is present or not.
     *
     * @param {Any} name Event name.
     * @param {Function} listener Event listener to invoke.
     *
     * @returns {this} Itself for chain.
     */
    on(name, listener) {
        this._throw_on_invalid_listener(listener);

        this._init_event_if(name);

        this._events[name].push(listener);

        return this;
    }

    /**
     * Registers new event listener and  adds it before any other.
     *
     * @param {Any} name Event name.
     * @param {Function} listener Event listener to invoke.
     *
     * @returns {this} Itself for chain.
     */
    prependListener(name, listener) {
        this._throw_on_invalid_listener(listener);

        this._init_event_if(name);

        this._events[name].unshift(listener);

        return this;
    }

    /**
     * Registers new event listener to be executed ONCE.
     *
     * @param {Any} name Event name.
     * @param {Function} listener Event listener to invoke.
     *
     * @returns {this} Itself for chain.
     */
    once(name, listener) {
        this._throw_on_invalid_listener(listener);

        this._init_event_if(name);

        this._events[name].push({
            once: true,
            inner: listener
        });

        return this;
    }

    /**
     * Registers new event listener to be executed ONCE and adds it before any other.
     *
     * @param {Any} name Event name.
     * @param {Function} listener Event listener to invoke.
     *
     * @returns {this} Itself for chain.
     */
    prependOnceListener(name, listener) {
        this._throw_on_invalid_listener(listener);

        this._init_event_if(name);

        this._events[name].unshift({
            once: true,
            inner: listener
        });

        return this;
    }

    /**
     * Removes all listeners for all events or particular one..
     *
     * @param {Any} name Event name. Optional.
     *
     * @returns {this} Itself for chain.
     */
    removeAllListeners(name) {
        name === undefined ? this._events = {} : delete this._events[name];

        return this;
    }

    /**
     * Removes particular listener for the event.
     *
     * It removes at most one listener.
     *
     * @param {Any} name Event name.
     * @param {Function} listener Event listener to invoke.
     *
     * @returns {this} Itself for chain.
     */
    removeListener(name, listener) {
        this._throw_on_invalid_listener(listener);

        const event = this._events[name];
        if (event) {
            for (let idx = 0; idx < event.length; idx += 1) {
                const event_listener = event[idx];
                if ((event_listener.once && event_listener.inner === listener) || event_listener === listener) {
                    event.splice(idx, 1);
                    if (event.length === 0) delete this._events[name];
                    return this;
                }
            }
        }

        return this;
    }
}

export {Jiken};