Home Identifier Source Repository

lib/Kernel.js

/**
 * Created by mgoria on 5/26/15.
 */

'use strict';

import Core from 'deep-core';
import DI from 'deep-di';
import {Exception} from './Exception/Exception';
import {Instance as Microservice} from './Microservice/Instance';
import {MissingMicroserviceException} from './Exception/MissingMicroserviceException';
import {Injectable as MicroserviceInjectable} from './Microservice/Injectable';
import {ContainerAware} from './ContainerAware';
import FileSystem from 'fs';
import WaitUntil from 'wait-until';

/**
 * Deep application kernel
 */
export class Kernel {
  /**
   * @param {Array} deepServices
   * @param {String} context
   */
  constructor(deepServices, context) {
    if (Kernel.ALL_CONTEXTS.indexOf(context) === -1) {
      throw new Exception(`Undefined context "${context}"`);
    }

    this._config = {};
    this._services = deepServices;
    this._context = context;
    this._env = null;
    this._container = new DI();
    this._isLoaded = false;
  }

  /**
   * @returns {Boolean}
   */
  get isLoaded() {
    return this._isLoaded;
  }

  /**
   * @param {String} identifier
   * @returns {Microservice}
   */
  microservice(identifier) {
    if (typeof identifier === 'undefined') {
      identifier = this._config.microserviceIdentifier;
    }

    for (let microserviceKey in this.microservices) {
      if (!this.microservices.hasOwnProperty(microserviceKey)) {
        continue;
      }

      let microservice = this.microservices[microserviceKey];

      if (microservice.identifier === identifier) {
        return microservice;
      }
    }

    throw new MissingMicroserviceException(identifier);
  }

  /**
   * @param {String} jsonFile
   * @param {Function} callback
   * @returns {Kernel}
   */
  loadFromFile(jsonFile, callback) {
    // @todo: remove AWS changes the way the things run
    // This is used because of AWS Lambda
    // context sharing after a cold start
    if (this._isLoaded) {
      callback(this);
      return this;
    }

    if (this.isBackend) {
      FileSystem.readFile(jsonFile, 'utf8', function(error, data) {
        if (error) {
          throw new Exception(`Failed to load kernel config from ${jsonFile} (${error})`);
        }

        this.load(JSON.parse(data), callback);
      }.bind(this));
    } else { // @todo: get rid of native code...
      var client = new XMLHttpRequest();

      client.open('GET', jsonFile);
      client.onreadystatechange = function(event) {
        if (client.readyState === 4) {
          if (client.status !== 200) {
            throw new Exception(`Failed to load kernel config from ${jsonFile}`);
          }

          this.load(JSON.parse(client.responseText), callback);
        }
      }.bind(this);

      client.send();
    }

    return this;
  }

  /**
   * Loads all Kernel dependencies
   *
   * @param {Object} globalConfig
   * @param {Function} callback
   */
  load(globalConfig, callback) {
    // @todo: remove AWS changes the way the things run
    // This is used because of AWS Lambda
    // context sharing after a cold start
    if (this._isLoaded) {
      callback(this);
      return this;
    }

    let originalCallback = callback;

    callback = function(kernel) {
      this._isLoaded = true;

      originalCallback(kernel);
    }.bind(this);

    this._config = globalConfig;

    this._buildContainer(callback);

    return this;
  }

  /**
   * @param {Array} args
   * @returns {*}
   */
  get(...args) {
    return this._container.get(...args);
  }

  /**
   * @returns {Array}
   */
  get services() {
    return this._services;
  }

  /**
   * @returns {DI}
   */
  get container() {
    return this._container;
  }

  /**
   * @returns {Boolean}
   */
  get isFrontend() {
    return this._context === Kernel.FRONTEND_CONTEXT;
  }

  /**
   * @returns {Boolean}
   */
  get isLocalhost() {
    return this.isFrontend
      && [
        'localhost', '127.0.0.1',
        '0.0.0.0', '::1',
      ].indexOf(window.location.hostname) !== -1;
  }

  /**
   * @returns {Boolean}
   */
  get isBackend() {
    return this._context === Kernel.BACKEND_CONTEXT;
  }

  /**
   * @returns {String}
   */
  get buildId() {
    return this._config.deployId || '';
  }

  /**
   * @returns {String}
   */
  get context() {
    return this._context;
  }

  /**
   * @returns {String}
   */
  get env() {
    return this._env;
  }

  /**
   * @returns {Object}
   */
  get config() {
    // @todo - create a class DeepConfig or smth, that will hold global config and expose shortcuts to different options
    return this._config;
  }

  /**
   * @returns {Microservice[]}
   */
  get microservices() {
    return this._container.get(Kernel.MICROSERVICES);
  }

  /**
   * Loads all parameters and services into DI container
   *
   * @param {Function} callback
   */
  _buildContainer(callback) {
    this._env = this._config.env;

    this._container.addParameter(
      Kernel.KERNEL,
      this
    );

    this._container.addParameter(
      Kernel.CONTEXT,
      {
        environment: this._env,
        isFrontend: this.isFrontend,
        isBackend: this.isBackend,
      }
    );

    this._container.addParameter(
      Kernel.MICROSERVICES,
      Microservice.createVector(this._config)
    );

    this._container.addParameter(
      Kernel.CONFIG,
      this._config
    );

    let bootingServices = 0;

    for (let serviceKey in this._services) {
      if (!this._services.hasOwnProperty(serviceKey)) {
        continue;
      }

      let serviceInstance = new this._services[serviceKey]();

      if (!serviceInstance instanceof ContainerAware) {
        let serviceType = typeof serviceInstance;

        throw new Exception(`Service ${serviceType} must be Kernel.ContainerAware instance`);
      }

      bootingServices++;

      serviceInstance.kernel = this;
      serviceInstance.localBackend = Core.IS_DEV_SERVER;
      serviceInstance.boot(this, function() {
        bootingServices--;
      }.bind(this));

      this._container.addService(
        serviceInstance.name,
        Kernel._createProxyIfNeeded(serviceInstance)
      );
    }

    WaitUntil()
      .interval(5)
      .times(999999) // @todo: get rid of magic here...
      .condition(function(cb) {
        process.nextTick(function() {
          cb(bootingServices <= 0);
        }.bind(this));
      }).done(function() {
        callback(this);
      }.bind(this));
  }

  /**
   * @param {ContainerAware|Object} serviceObj
   * @returns {ContainerAware|Proxy|Object}
   * @private
   */
  static _createProxyIfNeeded(serviceObj) {
    if (serviceObj === serviceObj.service) {
      return serviceObj;
    } else if(!serviceObj.hasOwnProperty('apply')) {
      return serviceObj.service;
    }

    let proxy = new Proxy(serviceObj, serviceObj.service);

    proxy.__proto__ = this.__proto__;
    proxy.constructor.prototype = this.constructor.prototype;

    return proxy;
  }

  /**
   * @returns {MicroserviceInjectable}
   */
  static get MicroserviceInjectable() {
    return MicroserviceInjectable;
  }

  /**
   * @returns {ContainerAware}
   */
  static get ContainerAware() {
    return ContainerAware;
  }

  /**
   * @returns {String}
   */
  static get FRONTEND_BOOTSTRAP_VECTOR() {
    return 'deep_frontend_bootstrap_vector';
  }

  /**
   * @returns {String}
   */
  static get CONFIG() {
    return 'deep_config';
  }

  /**
   * @returns {String}
   */
  static get KERNEL() {
    return 'deep_kernel';
  }

  /**
   * @returns {String}
   */
  static get CONTEXT() {
    return 'deep_context';
  }

  /**
   * @returns {String}
   */
  static get MICROSERVICES() {
    return 'deep_microservices';
  }

  /**
   * @returns {String}
   */
  static get FRONTEND_CONTEXT() {
    return 'frontend-ctx';
  }

  /**
   * @returns {String}
   */
  static get BACKEND_CONTEXT() {
    return 'backend-ctx';
  }

  /**
   * @returns {Array}
   */
  static get ALL_CONTEXTS() {
    return [
      Kernel.FRONTEND_CONTEXT,
      Kernel.BACKEND_CONTEXT,
    ];
  }
}