All files / classes App.js

97.3% Statements 72/74
92.59% Branches 50/54
100% Functions 9/9
100% Lines 63/63
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 1835x 5x 5x                   16x         16x 16x 12x   4x     16x                 2x                 9x 3x 6x   3x   6x                 20x                     19x 2x 2x                 20x 18x 18x 17x 17x 5x                           10x         10x 10x 5x 5x 5x 7x 7x 7x 6x 6x   1x         10x 9x                 10x 2x     10x   10x   9x   9x         9x 9x 14x 12x 12x 11x   10x     8x   8x 1x 1x     8x   1x                   3x 2x 2x   2x       5x  
const HooksMixin = require('hooks-mixin');
const Rubik = require('../Rubik');
const isFunction = require('lodash/isFunction');
 
/**
 * App for Rubik kubiks, main point for your app
 * @namespace Rubik
 * @class App
 * @prop {Map} kubiks kubiks of your application
 */
class App {
  constructor(kubiks) {
    this.kubiks = new Map();
    /**
     * use for calc requirements of app by dependencies of kubiks
     * @type {Map}
     */
    this._appRequirements = new Map();
    if (this.name) {
      Rubik.apps[this.name] = this;
    } else {
      Rubik.app = this;
    }
 
    Iif (kubiks) this.add(...arguments);
  }
 
  /**
   * get kubik by name
   * @param  {String} kubikName name of kubik
   * @return {Rubik.Kubik}      kubik
   */
  get(kubikName) {
    return this.kubiks.get(kubikName) || null;
  }
 
  /**
   * add new kubiks to app
   * @param {Array<Rubik.Kubik>|Rubik.Kubik} kubiks kubiks to add
   * @param {Boolean}                        check  check or not, kubik is instanceof Kubik
   */
  add(kubiks, check) {
    if (Array.isArray(kubiks)) {
      for (const kubik of kubiks) {
        this.addOne(kubik, check);
      }
      return;
    }
    return this.addOne(kubiks, check);
  }
 
  /**
   * get name of kubik
   * @param  {Mixed} kubik
   * @return {String|undefined} name
   */
  getKubikName(kubik) {
    return kubik.name
            ? kubik.name + ''
            : (kubik.constructor ? kubik.constructor.name.toLowerCase() : undefined);
  }
 
  /**
   * check is kubik correct
   * @param  {Mixed} kubik
   * @throw {TypeError} if kubik is invalid
   */
  checkKubikType(kubik) {
    if (kubik instanceof Rubik.Kubik) return;
    const name = this.getKubikName(kubik);
    throw new TypeError(`${name ? name : 'Nameless'} kubik is not Kubik's instance`);
  }
 
  /**
   * add a kubik
   * @param {Rubik.Kubik}  kubik        kubik to add
   * @param {Boolean}     [check=true] check or not, kubik is instanceof Kubik
   */
  addOne(kubik, check = true) {
    if (check) this.checkKubikType(kubik);
    const name = this.getKubikName(kubik);
    if (!name) throw new TypeError('Kubik is nameless');
    this.kubiks.set(name, kubik);
    if (Array.isArray(kubik.dependencies) && kubik.dependencies.length) {
      this._appRequirements.set(name, kubik.dependencies);
    }
  }
 
  /**
   * Find and check dependencies of kubiks
   * @param  {Set|null} [partial=null] set of kubiks to start
   * @return {Object} dependencies' hash
   */
  processRequirements(partial = null) {
    /**
     * Errors when up
     * @type {Array}
     */
    const errors = [];
    /**
     * Hash of dependencies
     * @type {Object}
     */
    const depHash = {};
    for (const [name, dependencies] of this._appRequirements) {
      Iif (partial && !partial.has(name)) continue;
      depHash[name] = {};
      for (const item of dependencies) {
        const req = item + '';
        const reqKubik = this.kubiks.get(req);
        if (reqKubik && (!partial || partial.has(req))) {
          depHash[name][req] = reqKubik;
          continue;
        }
        errors.push(
          new ReferenceError(`Can't find required kubik ${req} of ${name}`)
        );
      }
    }
    if (errors.length) throw new Rubik.Errors.AppStartError(errors);
    return depHash;
  }
 
  /**
   * up app
   * @param {Set} [partial=null] set of kubiks to start
   * @return {Promise}
   */
  async up(partial = null) {
    if (partial) {
      if (!(partial instanceof Set)) partial = null;
    }
    // Hooks atStart
    await this.processHooksAsync('atStart');
    // Find and check dependencies
    const depHash = this.processRequirements();
    // Hooks before up
    await this.processHooksAsync('before');
    // Up kubiks
    try {
      /**
       * Kubiks to call after, after up
       * @type {Set}
       */
      const after = new Set();
      for (const [name, kubik] of this.kubiks) {
        if (partial && !partial.has(name + '')) continue;
        kubik.app = this;
        await kubik.up(depHash[name] || {});
        if (isFunction(kubik.after)) after.add(kubik);
        // Clean extensions, if no “after” exists
        else kubik.extensions = [];
      }
      // Hooks after up
      await this.processHooksAsync('after');
      // Call after step of kubik
      for (const kubik of after) {
        await kubik.after();
        kubik.extensions = [];
      }
      // Hooks atEnd
      await this.processHooksAsync('atEnd');
    } catch (err) {
      throw new Rubik.Errors.AppStartError([err]);
    }
  }
 
  /**
   * use extension for app and kubiks
   * @param  {Object} extension extension
   * @return {Rubik.App}        this
   */
  use(extension) {
    if (!extension) return this;
    for (const [name, kubik] of this.kubiks) {
      if (extension[name]) kubik.use(extension[name]);
    }
    return this;
  }
}
 
module.exports = HooksMixin(App);