Source: jscore/Base.js

(function() {
    /**
     * @namespace jscore
     * @memberof Help4
     */
    Help4.jscore = {};

    /**
     * @typedef {Object} Help4.jscore.Base.Static
     * @property {*} [init = null]
     * @property {boolean} [destroy = true]
     */

    /**
     * the class parameters
     * @typedef {Object} Help4.jscore.Base.Params
     * @property {?Object} [config] - configuration
     * @property {?Object} [statics] - properties w/o intelligence but w/ automatic creation and cleanup; {@link Help4.jscore.Base.Static}
     * @property {?Object} [derived] - hand-over of derived class params
     */

    /**
     * the class configuration
     * @typedef {Object} Help4.jscore.Base.ClassConfig
     * @property {Object} config - configuration
     * @property {Object} statics - properties w/o intelligence but w/ automatic creation and cleanup; {@link Help4.jscore.Base.Static}
     * @property {?Object} [derived] - hand-over of derived class params
     */

    /**
     * JS base class
     * @abstract
     * @property {Help4.jscore.Base.ClassConfig} ____classConfig - class configuration
     * @property {Object} _listeners - event listeners
     */
    Help4.jscore.Base = class {
        /**
         * @constructor
         * @param {Help4.jscore.Base.Params} [classConfig]
         * @param {boolean} [isDerived = false]
         */
        constructor(classConfig, isDerived = false) {
            classConfig ||= {};

            const {PROP_NAME} = this.constructor;
            this[PROP_NAME] = classConfig;
            classConfig.config ||= {};
            classConfig.statics ||= {};
            classConfig.statics._listeners ||= {init: {}, destroy: false};

            if (!isDerived) {
                this.____resolveDerivedParams(classConfig);
                this.____defineStaticProperties();
            }
        }

        static PROP_NAME = '____classConfig';

        /** destroy instance */
        destroy() {
            const {_listeners} = this;
            for (const [type, {sync, async} = {}] of Object.entries(_listeners || {})) {
                sync?.destroy();
                async?.destroy();
                delete _listeners[type];
            }

            const {statics} = this.____getClassConfig();
            for (const [id, config] of Object.entries(statics || {})) {
                if (config.destroy) this[id]?.destroy?.();
                delete this[id];
            }

            delete this[this.constructor.PROP_NAME];

            this.destroy = () => {};  // avoid multiple destruction
            this.isDestroyed = () => true;
        }

        /**
         * whether instance is destroyed
         * @returns {boolean}
         */
        isDestroyed() {
            return false;
        }

        /**
         * add an event listener
         * @param {string|string[]} eventType - event type to be observed
         * @param {Help4.EmbeddedEvent.Listener} listener - the callback function
         * @returns {Help4.jscore.Base}
         */
        addListener(eventType, listener) {
            if (Help4.isArray(eventType)) {
                eventType.forEach(type => this.addListener(type, listener));
            } else {
                if (typeof listener === 'function') {
                    const {_listeners} = this;
                    _listeners[eventType] ||= {};

                    const listeners = _listeners[eventType];
                    listeners.async ||= new Help4.EmbeddedEvent();
                    listeners.async.addListener(listener);
                }
            }
            return this;
        }

        /**
         * add an event listener
         * @param {string|string[]} eventType - event type to be observed
         * @param {Help4.EmbeddedEvent.Listener} listener - the callback function
         * @returns {Help4.jscore.Base}
         */
        addListenerSync(eventType, listener) {
            if (Help4.isArray(eventType)) {
                eventType.forEach(type => this.addListener(type, listener));
            } else {
                if (typeof listener === 'function') {
                    const {_listeners} = this;
                    _listeners[eventType] ||= {};

                    const listeners = _listeners[eventType];
                    listeners.sync ||= new Help4.EmbeddedEventSync();
                    listeners.sync.addListener(listener);
                }
            }
            return this;
        }

        /**
         * remove an event listener
         * @param {string|string[]} eventType
         * @param {Help4.EmbeddedEvent.Listener} listener
         * @returns {Help4.jscore.Base}
         */
        removeListener(eventType, listener) {
            if (Help4.isArray(eventType)) {
                eventType.forEach(type => this.removeListener(type, listener))
            } else {
                const {_listeners} = this;
                const listeners = _listeners?.[eventType];

                if (listeners) {
                    const listenersS = listeners.sync;
                    if (listenersS) {
                        listenersS.removeListener(listener);
                        if (!listenersS.countListeners()) {
                            listenersS.destroy();
                            delete listeners.sync;
                        }
                    }

                    const listenersA = listeners.async;
                    if (listenersA) {
                        listenersA.removeListener(listener);
                        if (!listenersA.countListeners()) {
                            listenersA.destroy();
                            delete listeners.async;
                        }
                    }

                    if (!Object.keys(listeners).length) {
                        delete _listeners[eventType];
                    }
                }
            }
            return this;
        }

        /**
         * fires an event that can be observed from outside
         * @param {Object} event - the event to be fired
         * @returns {Help4.jscore.Base}
         * @protected
         */
        _fireEvent(event) {
            // events are sometimes async and sometimes with long execution chains
            // sometimes they lead to the destruction of this class instance

            event.target ||= [];
            event.target.unshift(this);

            const {_listeners} = this;
            _listeners?.[event.type]?.async?.onEvent(event);
            _listeners?.['*']?.async?.onEvent(event);

            return this;
        }

        /**
         * fires an event that can be observed from outside
         * @param {Object} event - the event to be fired
         * @returns {Promise<Help4.EmbeddedEvent.EventResponse[]>}
         * @protected
         */
        async _fireEvent2(event) {
            // events are sometimes async and sometimes with long execution chains
            // sometimes they lead to the destruction of this class instance

            event.target ||= [];
            event.target.unshift(this);

            const {_listeners} = this;
            /** @type {Help4.EmbeddedEvent.EventResponse[]} */
            const result1 = await _listeners?.[event.type]?.async?.onEvent2(event) || [];
            /** @type {Help4.EmbeddedEvent.EventResponse[]} */
            const result2 = await _listeners?.['*']?.async?.onEvent2(event) || [];

            return [...result1, ...result2];
        }

        /**
         * fires an event that can be observed from outside
         * @param {Object} event - the event to be fired
         * @returns {Help4.EmbeddedEvent.EventResponse[]}
         * @protected
         */
        _fireEventSync(event) {
            event.target ||= [];
            event.target.unshift(this);

            const {_listeners} = this;
            /** @type {Help4.EmbeddedEvent.EventResponse[]} */
            const result1 = _listeners?.[event.type]?.sync?.onEvent(event) || [];
            /** @type {Help4.EmbeddedEvent.EventResponse[]} */
            const result2 = _listeners?.['*']?.sync?.onEvent(event) || [];

            return [...result1, ...result2];
        }

        /**
         * destroys class properties
         * @param {string} keys
         * @protected
         */
        _destroyControl(...keys) {
            for (const key of keys) {
                this[key]?.destroy?.();
                delete this[key];
            }
        }

        /**
         * @returns {Help4.jscore.Base.Params}
         * @protected
         */
        ____getClassConfig() {
            return this[this.constructor.PROP_NAME];
        }

        /**
         * @param {Help4.jscore.Base.Params} classConfig
         * @protected
         */
        ____resolveDerivedParams(classConfig) {
            let level0 = classConfig;
            let level1 = classConfig.derived;
            while (level1 && level1.derived) {
                level0 = level1;
                level1 = level1.derived;
            }

            if (level1) {
                this.____mergeDerivedParams(level0, level1)
                delete level0.derived;

                if (classConfig.derived) this.____resolveDerivedParams(classConfig);
            }
        }

        /**
         * @param {Help4.jscore.Base.Params} level0
         * @param {Help4.jscore.Base.Params} level1
         * @throws {Error}
         * @protected
         */
        ____mergeDerivedParams(level0, level1) {
            for (const [type, value] of Object.entries(level1)) {
                this.____mergeDerivedParam(level0, type, value);
            }
        }

        /**
         * @param {Help4.jscore.Base.Params} level0
         * @param {string} type
         * @param {*} value
         * @throws {Error}
         * @protected
         */
        ____mergeDerivedParam(level0, type, value) {
            switch (type) {
                case 'statics':
                case 'derived':
                    level0[type] = Help4.extendObject(level0[type] || {}, value);
                    break;
                default:
                    console.error(this);
                    throw new Error(`Unhandled derived parameter "${type}"!`);
            }
        }

        /**
         * auto define all to be cleaned properties
         * @throws {Error}
         * @protected
         */
        ____defineStaticProperties() {
            const {statics} = this.____getClassConfig();
            for (const [id, config] of Object.entries(statics || {})) {
                if (this[id] === undefined) {
                    config.destroy = Help4.clampBoolean(config.destroy, true);
                    this[id] = config.init ?? null;
                } else {
                    console.error(this);
                    throw new Error(`${id} cannot be defined twice!`);
                }
            }
        }
    }
})();