Source: EmbeddedEvent.js

(function() {
    /**
     * @typedef {'high'|'normal'} Help4.EmbeddedEvent.Priority
     */

    /**
     * @typedef {Object} Help4.EmbeddedEvent.EventResponse
     * @property {string} type - event response type
     * @property {Object} [scope] - responder
     * @property {*} [response] - result value
     */

    /**
     * @callback Help4.EmbeddedEvent.Listener
     * @param {Object} data
     */

    /**
     * used to embed event functionality everywhere
     * @augments Help4.jscore.Base
     * @property {Array<Help4.EmbeddedEvent.Listener>} _listeners - event listeners
     * @property {Array<Help4.EmbeddedEvent.Priority>} _priorities - listener priorities
     */
    Help4.EmbeddedEvent = class extends Help4.jscore.Base {
        /** @override */
        constructor() {
            super({
                statics: {
                    _listeners:  {init: [], destroy: false},  // incompatible override with base class!
                    _priorities: {init: [], destroy: false}
                }
            });
        }

        /**
         * {@link Help4.EmbeddedEvent.Priority}
         * @memberof Help4.EmbeddedEvent
         * @type {Object}
         */
        static PRIORITY = {high: 'high', normal: 'normal'}

        /** @override */
        destroy() {
            // as we override this._listeners in an incompatible way
            // we need to prevent crashes during destroy in base class destructor
            // just delete this._listeners manually here
            delete this._listeners;

            super.destroy();
        }

        /**
         * returns the number of subscribed listeners
         * @returns {number}
         */
        countListeners() {
            return this._listeners.length;
        }

        /**
         * add a listener to the EmbeddedEvent
         * @override
         * @param {Help4.EmbeddedEvent.Listener} listener - the callback function
         * @param {Help4.EmbeddedEvent.Priority} [priority]
         * @returns {Help4.EmbeddedEvent}
         */
        addListener(listener, priority) {
            const {_listeners, _priorities} = this;

            if (!_listeners) {
                throw new Error('EmbeddedEvent has been destroyed!');
            } else if (typeof listener !== 'function') {
                throw new TypeError('listener has to be a function!');
            } else if (_listeners.indexOf(listener) < 0) {
                const {PRIORITY} = this.constructor;
                _listeners.push(listener);
                _priorities.push(PRIORITY[priority] || PRIORITY.normal);
            }

            return this;
        }

        /**
         * remove a listener from the EmbeddedEvent
         * @override
         * @param {Help4.EmbeddedEvent.Listener} listener - the to be removed callback function
         * @returns {Help4.EmbeddedEvent}
         */
        removeListener(listener) {
            if (typeof listener !== 'function') {
                throw new TypeError('listener has to be a function!');
            }

            const {_listeners, _priorities} = this;
            const index = _listeners?.indexOf(listener) ?? -1;
            if (index >= 0) {
                _listeners.splice(index, 1);
                _priorities.splice(index, 1);
            }

            return this;
        }

        /**
         * send an event to all callbacks
         * @param {Object} data
         * @param {Help4.EmbeddedEvent.Priority} [priority]
         * @returns {Help4.EmbeddedEvent}
         * @throws {Error}
         */
        onEvent(data, priority) {
            if (typeof data !== 'object' || !data) throw new Error('It is good practice to use an event object!');

            // priority: only send items WITH a specific priority
            const {PRIORITY} = this.constructor;
            priority = PRIORITY[priority] || null;

            const {_listeners, _priorities} = this;
            (_listeners || []).forEach((listener, index) => {
                if (!priority || priority === _priorities[index]) setTimeout(() => listener(data), 1);
            });

            return this;
        }

        /**
         * send an event to all callbacks; delivers the return value
         * @param {Object} data
         * @param {Help4.EmbeddedEvent.Priority} [priority]
         * @returns {Promise<Help4.EmbeddedEvent.EventResponse[]>}
         * @throws {Error}
         */
        async onEvent2(data, priority) {
            if (typeof data !== 'object' || !data) throw new Error('It is good practice to use an event object!');

            // priority: only send items WITH a specific priority
            const {PRIORITY} = this.constructor;
            priority = PRIORITY[priority] || null;

            const result = /** @type {Help4.EmbeddedEvent.EventResponse[]} */ [];
            const {_listeners, _priorities} = this;
            const count = _listeners.length;

            (_listeners || []).forEach((listener, index) => {
                if (!priority || priority === _priorities[index]) {
                    setTimeout(async () => result.push(await listener(data)), 1);
                }
            });

            return new Help4.Promise(resolve => {
                const check = () => {
                    result.length === count
                        ? resolve(result)
                        : setTimeout(check, 100);
                }
                check();
            });
        }
    }
})();