di/src/Injectable.js

/**
 * @module flitter-di/src/Injectable
 */

/** Base class for classes that support service injection. */
class Injectable {
    /**
     * If true, the injector will defer the class if the class
     * requests any services that the container is missing. These
     * services are filled in later and added to the prototype and
     * any instances. True by default.
     * @type {boolean}
     * @private
     */
    static _di_allow_defer = true

    /**
     * List of services that were deferred and not provided at the time of injection.
     * @type {Array<string>}
     * @private
     */
    static _di_deferred_services = []

    /**
     * Collection of instances of this class that need to have the deferred service
     * instances injected into them when the deferred services are finally provided.
     * @type {Array<module:flitter-di/src/Injectable~Injectable>}
     * @private
     */
    static _di_deferred_instances = []

    /**
     * Get the names of services required by this class.
     * @returns {Array<string>}
     */
    static get services() {
        return []
    }

    /**
     * Checks if the class has any missing deferred services.
     * @returns {boolean} - true if there are deferred services
     * @private
     */
    static get __has_deferred_services() {
        return this._di_deferred_services.length > 0
    }

    /**
     * Inject the class with services from the specified container. These
     * services are injected directly into this class' prototype. If deferral
     * is enabled, services not in the container will be stored and the class
     * will be deferred.
     * @param {module:flitter-di/src/Container~Container} container
     * @private
     */
    static __inject(container) {
        this.services.forEach(name => {
            if ( !this._di_allow_defer ) {
                // If this class' services aren't deferrable, then just fetch their instances
                this.prototype[name] = container.get(name)
            } else {
                // Otherwise, grab them if they exist, or put in deferral requests
                if ( container.has(name) ) {
                    this.prototype[name] = container.get(name)
                } else {
                    this._di_deferred_services.push(name)
                }
            }
        })

        if ( this._di_allow_defer && this.__has_deferred_services ) {
            container.defer(this)
        }
    }

    /**
     * Called when a deferred service is registered with the container.
     * Injects the missing service into the prototype and any instances of this
     * class.
     * @param {string} service_name - the deferred service name
     * @param {module:flitter-di/src/Service~Service} service_instance - the instance of the service
     * @private
     */
    static __deferral_callback(service_name, service_instance) {
        if ( this._di_deferred_services.includes(service_name) ) {
            for ( const instance of this._di_deferred_instances ) {
                instance[service_name] = service_instance
            }

            this.prototype[service_name] = service_instance

            this._di_deferred_services = this._di_deferred_services.filter(x => x !== service_name)
        }
    }

    /**
     * Instantiates the class. If deferral is enabled and the static class
     * has deferred services, register this instance as needing to be injected
     * with those services when they become available.
     */
    constructor() {
        const Class = this.constructor
        if ( Class._di_allow_defer && Class.__has_deferred_services ) {
            Class._di_deferred_instances.push(this)
        }
    }
}

module.exports = exports = Injectable