Home Reference Source

src/utils/di/injector.js

/* */

import hash from './HashSum.js';
import {
    annotate,
    readAnnotations,
    hasAnnotation,
    Provide as ProvideAnnotation,
    TransientScope as TransientScopeAnnotation
} from './Decorators.js';
import {isFunction, toString} from './Util.js';
import {createProviderFromFnOrClass} from './Providers.js';


function constructResolvingMessage(resolving, token) {
    // If a token is passed in, add it into the resolving array.
    // We need to check arguments.length because it can be null/undefined.
    if (arguments.length > 1) {
        resolving.push(token);
    }

    if (resolving.length > 1) {
        return ` (${resolving.map(toString).join(' -> ')})`;
    }

    return '';
}


// Injector encapsulate a life scope.
// There is exactly one instance for given token in given injector.
//
// All the state is immutable, the only state changes is the cache. There is however no way to produce different instance under given token. In that sense it is immutable.
//
// Injector is responsible for:
// - resolving tokens into
//   - provider
//   - value (cache/calling provider)
// - loading different "providers" and modules
class Injector {

    constructor(modules = [], parentInjector = null, providers = new Map(), scopes = []) {
        this._cache = new Map();
        this._providers = providers;
        this._parent = parentInjector;
        this._scopes = scopes;

        this._tokenCache = new Map();

        this._loadModules(modules);
    }

    _retrieveToken(classConstructor, constructionParams = []) {
        if (!this._tokenCache.has(classConstructor)) {
            this._tokenCache.set(classConstructor, new Map());
        }

        let paramsHash = hash(constructionParams);
        let cachedClass = this._tokenCache.get(classConstructor);
        if (!cachedClass.has(paramsHash)) {
            /* Generate a new token */
            cachedClass.set(paramsHash, `${Date.now()}${Math.random()}`);
        }

        let foundHash = cachedClass.get(paramsHash);
        return classConstructor.name ? `${classConstructor.name}-${foundHash}` : foundHash;
    }


    // Collect all registered providers that has given annotation.
    // Including providers defined in parent injectors.
    _collectProvidersWithAnnotation(annotationClass, collectedProviders) {
        this._providers.forEach((provider, token) => {
            if (!collectedProviders.has(token) && hasAnnotation(provider.provider, annotationClass)) {
                collectedProviders.set(token, provider);
            }
        });

        if (this._parent) {
            this._parent._collectProvidersWithAnnotation(annotationClass, collectedProviders);
        }
    }


    // Load modules/function/classes.
    // This mutates `this._providers`, but it is only called during the constructor.
    _loadModules(modules) {
        for (var module of modules) {
            // A single provider (class or function).
            if (isFunction(module)) {
                this._loadFnOrClass(module);
                continue;
            }

            throw new Error('Invalid module!');
        }
    }


    // Load a function or class.
    // This mutates `this._providers`, but it is only called during the constructor.
    _loadFnOrClass(classConstructor, constructionParams = []) {
        var annotations = readAnnotations(classConstructor);
        var token = this._retrieveToken(annotations.provide.token || classConstructor, constructionParams);
        var provider = createProviderFromFnOrClass(classConstructor, annotations);

        this._providers.set(token, provider);
    }


    // Returns true if there is any provider registered for given token.
    // Including parent injectors.
    _hasProviderFor(token) {
        if (this._providers.has(token)) {
            return true;
        }

        if (this._parent) {
            return this._parent._hasProviderFor(token);
        }

        return false;
    }

    // Find the correct injector where the default provider should be instantiated and cached.
    _instantiateDefaultProvider(provider, token, classConstructor, constructionParams, resolving) {
        // In root injector, instantiate here.
        if (!this._parent) {
            this._providers.set(token, provider);
            return this.get(classConstructor, constructionParams, resolving);
        }

        // Check if this injector forces new instance of this provider.
        for (var ScopeClass of this._scopes) {
            if (hasAnnotation(provider.provider, ScopeClass)) {
                this._providers.set(token, provider);
                return this.get(token, resolving);
            }
        }

        // Otherwise ask parent injector.
        return this._parent._instantiateDefaultProvider(provider, token, resolving);
    }


    // Return an instance for given token.
    get(classConstructor, constructionParams = [], resolving = []) {
        var resolvingMsg = '';
        var provider;
        var instance;
        var token = this._retrieveToken(classConstructor, constructionParams);

        if (token === null || token === undefined) {
            resolvingMsg = constructResolvingMessage(resolving, token);
            throw new Error(`Invalid token "${token}" requested!${resolvingMsg}`);
        }

        // Special case, return itself.
        if (token === Injector) {
            return this;
        }

        // Check if there is a cached instance already.
        if (this._cache.has(token)) {
            instance = this._cache.get(token);
            provider = this._providers.get(token);
            return instance;
        }

        provider = this._providers.get(token);

        // No provider defined (overridden), use the default provider (token).
        if (!provider && isFunction(classConstructor) && !this._hasProviderFor(token)) {
            provider = createProviderFromFnOrClass(classConstructor, readAnnotations(classConstructor));
            return this._instantiateDefaultProvider(provider, token, classConstructor, constructionParams, resolving);
        }

        if (!provider) {
            if (!this._parent) {
                resolvingMsg = constructResolvingMessage(resolving, token);
                throw new Error(`No provider for ${toString(token)}!${resolvingMsg}`);
            }

            return this._parent.get(token, resolving);
        }

        if (resolving.indexOf(token) !== -1) {
            resolvingMsg = constructResolvingMessage(resolving, token);
            throw new Error(`Cannot instantiate cyclic dependency!${resolvingMsg}`);
        }

        resolving.push(token);

        var args = provider.params.map((param) => {
            return this.get(param.token, undefined, resolving);
        });

        /* Add custom construction parameters to construction */
        args = args.concat(constructionParams);

        try {
            instance = provider.create(args);
        } catch (e) {
            resolvingMsg = constructResolvingMessage(resolving);
            var originalMsg = 'ORIGINAL ERROR: ' + e.message;
            e.message = `Error during instantiation of ${toString(token)}!${resolvingMsg}\n${originalMsg}`;
            throw e;
        }

        if (!hasAnnotation(provider.provider, TransientScopeAnnotation)) {
            this._cache.set(token, instance);
        }

        resolving.pop();

        return instance;
    }


    // Create a child injector, which encapsulate shorter life scope.
    // It is possible to add additional providers and also force new instances of existing providers.
    createChild(modules = [], forceNewInstancesOf = []) {
        var forcedProviders = new Map();

        // Always force new instance of TransientScope.
        forceNewInstancesOf.push(TransientScopeAnnotation);

        for (var annotation of forceNewInstancesOf) {
            this._collectProvidersWithAnnotation(annotation, forcedProviders);
        }

        return new Injector(modules, this, forcedProviders, forceNewInstancesOf);
    }
}


export {Injector};