jluchiji/node-ignis

View on GitHub
src/core.js

Summary

Maintainability
A
3 hrs
Test Coverage
/**
 * core.js
 *
 * @author  Denis Luchkin-Zhou <denis@ricepo.com>
 * @license MIT
 */

/* eslint no-loop-func:0 */

import _           from 'lodash';
import Debug       from 'debug';
import Chalk       from 'chalk';
import Toposort    from 'toposort';
import Monologue   from 'monologue.js';


import Service     from './service';
import ServiceName from './util/service-name';

const debug = Debug('ignis:core');


/* Symbol to hide services */
const $$base = Symbol();
const $$services = Symbol();


/*
 * Ignis class.
 * The service manager.
 */
export default class Ignis extends Monologue {

  constructor() {
    super();
    this[$$services] = new Map();
    this.startup = null;
  }


  /**
   * Uses an extension module.
   */
  use(service) {

    /* Handle ES6 modules */
    if (service.__esModule) { service = service.default; }

    /* If this is a Service, register it */
    if (service.prototype instanceof Service) {

      if (Service.meta(service, 'abstract')) {
        throw new Error(`${service.name} is abstract, you need to extend it first.`);
      }

      if (Ignis[$$services].has(service)) {
        debug(Chalk.bold.cyan('skip') + ` ${service.name}`);
        return;
      }

      debug(Chalk.bold.cyan('register') + ` ${service.name}`);
      Ignis[$$services].add(service);

      this.import(service);

      /* Call onregister callback if specified */
      if (typeof service.onregister === 'function') {
        service.onregister(this);
      }

      return;
    }

    /* If this is an old-style callback,invoke it */
    if (typeof service === 'function') {
      debug(Chalk.bold.cyan('invoke') + ` ${service.name || '<anonymous>'}`);
      service(this);
      return;
    }

    throw new Error('Unexpected service type.');
  }


  /**
   * Exposes properties exported by services.
   */
  import(service) {
    const exps = Service.meta(service, 'exports') || { };
    _.forEach(exps, (options, name) => {
      debug(Chalk.bold.yellow('export ') + name);
      debug(options);

      if (options.static) {
        _.set(Ignis.exports, options.path || name, service[name]);
      } else {
        const descriptor = {
          configurable: false,
          enumerable: options.enumerable,
          readonly: options.readonly,
          get: () => service[name],
          set: options.readonly ? undefined : v => { service[name] = v; }
        };
        Object.defineProperty(Ignis.prototype, name, descriptor);
      }
    });
  }


  /**
   * Finds a service.
   */
  service(name) {
    const service = this[$$services].get(name);

    /* Fail if there is no such service */
    if (!service) {
      throw new Error(`Service [${name}] is not defined.`);
    }

    /* Fail if service is not ready */
    if (!service.ready) {
      throw new Error(`Service [${name}] is not ready.`);
    }

    return service;
  }


  /**
   * Initializes all registered services.
   * Wraps the __init().
   */
  init() {
    this.startup = this.__init();
    return this.startup;
  }


  /**
   * @private Actual initialization function.
   * Do not call yourself, or you can seriously mess up the startup sequence.
   */
  async __init() {

    /* Prepare to toposort */
    const graph = [ ];

    /* Instantiate and toposort services */
    for (const service of Ignis[$$services]) {

      /* Normalize service name */
      const name = ServiceName(service);
      debug(Chalk.bold.cyan('create') + ` ${name}`);

      /* Instantiate and save service reference */
      const svc = new service(this);
      svc[$$base] = service;

      this[$$services].set(name, svc);

      /* Push dependency info into toposort */
      const deps = Service.meta(service, 'deps') || [ ];
      debug(deps);
      for (const dep of deps) { graph.push([ dep, name ]); }
      if (deps.length === 0) { graph.push([ name ]); }
    }
    const sorted = _.compact(Toposort(graph));
    debug(sorted);

    /* Initialize services */
    for (const name of sorted) {
      debug(Chalk.bold.yellow('init') + ` ${name}`);
      const service = this[$$services].get(name);

      if (!service) {
        throw new Error(`Service [${name}] is not defined.`);
      }

      /* Extract dependencies */
      let deps = Service.meta(service[$$base], 'deps') || [ ];
      deps = deps.map(i => this.service(i));

      /* Invoke initialization callback */
      await service.init(...deps);
      service.ready = true;
      debug(Chalk.bold.green('success') + ` ${name}`);
    }

    /* Execute post-initialization operations */
    for (const name of sorted) {
      debug(Chalk.bold.green('postinit') + ` ${name}`);
      const service = this[$$services].get(name);
      service.postinit();
    }

  }


  /**
   * Tests if environment matches the expression.
   * If {expr} is not a RegExp, it is compiled into a RegExp with 'i' flag.
   */
  env(expr) {
    if (!(expr instanceof RegExp)) {
      expr = new RegExp(`(${expr})`, 'i');
    }
    return expr.test(process.env.NODE_ENV);
  }


}


/**
 * Registered services
 */
Ignis[$$services] = new Set();


/**
 * Expose symbols
 */
Ignis.$$base = $$base;
Ignis.$$services = $$services;