Home Manual Reference Source Repository

docs/di/Injector.js

import { 
  Type,
  Provider, 
  ProviderArg,
  INJECT_PARAM_KEY, 
  InjectionMetadata,
  InjectableConfig,
  INJECTABLE_META_KEY,
  ClassProvider,
  FactoryProvider,
  ValueProvider,
  ExistingProvider
} from './common';

/**
 * An instance of a forward ref function.
 * @export
 * @class ForwardRef
 * @example
 * const test = new ForwardRef(() => 'test');
 * 
 * test.ref; // => 'test'
 */
export class ForwardRef {
  constructor(private fn: Function) {}

  /**
   * The reference invoked reference function result.
   * @readonly
   * @type {*}
   */
  get ref(): any {
    return this.fn();
  }

  /**
   * Resolves a potential forward reference.
   * @static
   * @param {*} val 
   * @returns {*} 
   */
  static resolve(val: any): any {
    if (val instanceof ForwardRef) {
      return val.ref;
    }

    return val;
  }
}

/**
 * A factory that creates a ForwardRef.
 * @export
 * @param {Function} fn 
 * @returns {ForwardRef}
 * @example
 * const test = forwardRef(() => 'test');
 * 
 * test.ref; // => 'test'
 */
export function forwardRef(fn: Function): ForwardRef {
  return new ForwardRef(fn);
}

/**
 * A dependency injector for resolving dependencies. Injectors are hierarchicle.
 * @export
 * @class Injector
 * @example
 * const injector = new Injector([
 *   { provide: 'test', useValue: 'blorg' },
 *   { provide: 'factory', useFactory: () => 'BOOM' },
 *   { provide: 'myClass', useClass: MyClass },
 *   MyClass
 * ]);
 * 
 * injector.get('test'); // => 'blorg';
 * injector.get('factory'); // => 'BOOM';
 * injector.get('myClass'); // => instanceof MyClass;
 * injector.get(MyClass); // => instanceof MyClass;
 */
export class Injector {
  private _providers: Map<any, Provider> = new Map();
  private _cache: Map<any, any> = new Map();
  
  /**
   * Creates an instance of Injector.
   * @param {ProviderArg[]} [providers=[]] List of providers for this injector.
   * @param {(Injector|null)} [_parent=null] A parent injector.
   */
  constructor(
    providers: ProviderArg[] = [],
    private _parent: Injector|null = null
  ) {
    this.registerProvider({ provide: Injector, useValue: this });

    providers.forEach(p => this.registerProvider(p));
  }

  /**
   * The parent injector if it is set.
   * @readonly
   * @type {(Injector|null)}
   */
  get parent(): Injector|null {
    return this._parent;
  }

  /**
   * Registers a provider with the injector.
   * @param {ProviderArg} provider 
   */
  registerProvider(provider: ProviderArg): void {
    const _provider = this._normalizeProvider(provider);

    this._providers.set(_provider.provide, _provider);
  }

  /**
   * Gets a dependecy from the provided token.
   * @param {*} token 
   * @param {*} [defaultValue] 
   * @param {InjectionMetadata} [metadata={}] 
   * @returns {*} 
   */
  get(token: any, defaultValue?: any, metadata: InjectionMetadata = {}): any {
    let resource;
    let { optional = false, lazy = false } = metadata;

    if (lazy) {
      return () => this.get(token, defaultValue, { ...metadata, lazy: false });
    }
    
    if (this._cache.has(token)) {
      return this._cache.get(token);
    }

    if (this._providers.has(token)) {
      const provider = this._providers.get(token);
      resource = this._resolve(provider as Provider, metadata);

      this._cache.set(token, resource);
    }

    if (resource === undefined && !metadata.self && this._parent) {
      resource = this._parent.get(token, defaultValue, metadata);
    }
    
    if (resource === undefined) {
      if (defaultValue !== undefined) {
        resource = defaultValue;
      } else if (optional) {
        resource = null;
      } else {
        throw new Error(`Injector -> no token exists for ${token}`);
      }
    }

    return resource;
  }
  
  /**
   * Creates a new injector with the given providers and sets
   * this injector as it's parent.
   * @param {ProviderArg[]} [providers=[]] 
   * @returns {Injector} 
   */
  resolveAndCreateChild(providers: ProviderArg[] = []): Injector {
    return new Injector(providers, this);    
  }

  /**
   * Programmatically set the parent injector.
   * @param {Injector} parent 
   */
  setParent(parent: Injector): void {
    this._parent = parent;
  }

  /**
   * Resolves the given provider with this injector.
   * @template T The return type.
   * @param {*} provider 
   * @returns {T} 
   */
  resolveAndInstantiate<T>(provider: any): T {
    return this._resolve(provider);
  }

  invoke(fn: Function, providers: ProviderArg[]): any {
    return fn(...providers.map(p => this._resolve(p)));
  }

  private _resolve(_provider: ProviderArg, metadata: InjectionMetadata = {}): any {
    const provider = this._normalizeProvider(_provider);
    
    if (this._isClassProvider(provider)) {
      const injections = Reflect.getMetadata(INJECT_PARAM_KEY, provider.useClass, (<any>undefined)) || [];
      const resolved = this.getDependencies(injections);
      const ref = ForwardRef.resolve(provider.useClass);

      return this.instantiate(ref, ...resolved);
    } else if (this._isFactoryProvider(provider)) {
      const resolved = this.getDependencies((provider.deps || []).map(token => ({ token })));
      const ref = ForwardRef.resolve(provider.useFactory);

      return ref(...resolved);
    } else if (this._isValueProvider(provider)) {
      return ForwardRef.resolve(provider.useValue);
    } else if (this._isExistingProvider(provider)) {
      return this.get(ForwardRef.resolve(provider.useExisting));
    }

    throw new Error(`Injector -> could not resolve provider ${(<Provider>provider).provide}`);
  }
      
  private getDependencies(metadata: any[]): any[] {
    return metadata.map(meta => this.get(meta.token, undefined, meta));
  }

  private instantiate(Ref: any, ...d: any[]): any {
    switch (d.length) {
      case 0: return new Ref();
      case 1: return new Ref(d[0]);
      case 2: return new Ref(d[0], d[1]);
      case 3: return new Ref(d[0], d[1], d[2]);
      case 4: return new Ref(d[0], d[1], d[2], d[3]);
      case 5: return new Ref(d[0], d[1], d[2], d[3], d[4]);
      case 6: return new Ref(d[0], d[1], d[2], d[3], d[4], d[5]);
      case 7: return new Ref(d[0], d[1], d[2], d[3], d[4], d[5], d[6]);
      case 8: return new Ref(d[0], d[1], d[2], d[3], d[4], d[5], d[6], d[7]);
      case 9: return new Ref(d[0], d[1], d[2], d[3], d[4], d[5], d[6], d[7], d[8]);
      case 10: return new Ref(d[0], d[1], d[2], d[3], d[4], d[5], d[6], d[7], d[8], d[9]);
      case 11: return new Ref(d[0], d[1], d[2], d[3], d[4], d[5], d[6], d[7], d[8], d[9], d[10]);
      case 12: return new Ref(d[0], d[1], d[2], d[3], d[4], d[5], d[6], d[7], d[8], d[9], d[10], d[11]);
      default: 
        return new Ref(...d);
    }
  }

  private _isClassProvider(provider: Provider): provider is ClassProvider {
    return provider.hasOwnProperty('useClass');
  }

  private _isFactoryProvider(provider: Provider): provider is FactoryProvider {
    return provider.hasOwnProperty('useFactory');
  }

  private _isValueProvider(provider: Provider): provider is ValueProvider {
    return provider.hasOwnProperty('useValue');
  }

  private _isExistingProvider(provider: Provider): provider is ExistingProvider {
    return provider.hasOwnProperty('useExisting');
  }

  private _normalizeProvider(_provider: ProviderArg): Provider {
    if (typeof _provider === 'function') {
      return { provide: _provider, useClass: _provider };
    } 

    return _provider;
  }

  /**
   * Creates a new injector from an annotated injectable Class. 
   * See {@link Injectable} for more details.
   * @static
   * @param {Type<any>} injectable 
   * @param {ProviderArg[]} [providers=[]] 
   * @param {Injector} [parent] 
   * @returns {Injector} 
   */
  static fromInjectable(injectable: Type<any>, providers: ProviderArg[] = [], parent?: Injector): Injector {
    return new Injector([ ...Injector.resolveInjectables(injectable), ...providers ], parent);
  }

  static resolveInjectables(injectable: Type<any>): ProviderArg[] {
    const metadata = Reflect.getOwnMetadata(INJECTABLE_META_KEY, injectable);

    if (metadata) {
      return (<InjectableConfig>metadata).providers;
    }
    
    return [];
  }
}