node_modules/egg-core/lib/lifecycle.js

'use strict';

const is = require('is-type-of');
const assert = require('assert');
const getReady = require('get-ready');
const { Ready } = require('ready-callback');
const { EventEmitter } = require('events');
const debug = require('debug')('egg-core:lifecycle');
const INIT = Symbol('Lifycycle#init');
const INIT_READY = Symbol('Lifecycle#initReady');
const DELEGATE_READY_EVENT = Symbol('Lifecycle#delegateReadyEvent');
const REGISTER_BEFORE_CLOSE = Symbol('Lifecycle#registerBeforeClose');
const REGISTER_READY_CALLBACK = Symbol('Lifecycle#registerReadyCallback');
const CLOSE_SET = Symbol('Lifecycle#closeSet');
const IS_CLOSED = Symbol('Lifecycle#isClosed');
const BOOT_HOOKS = Symbol('Lifecycle#bootHooks');
const BOOTS = Symbol('Lifecycle#boots');

const utils = require('./utils');

class Lifecycle extends EventEmitter {

  /**
   * @param {object} options - options
   * @param {String} options.baseDir - the directory of application
   * @param {EggCore} options.app - Application instance
   * @param {Logger} options.logger - logger
   */
  constructor(options) {
    super();
    this.options = options;
    this[BOOT_HOOKS] = [];
    this[BOOTS] = [];
    this[CLOSE_SET] = new Set();
    this[IS_CLOSED] = false;
    this[INIT] = false;
    getReady.mixin(this);

    this.timing.start('Application Start');
    // get app timeout from env or use default timeout 10 second
    const eggReadyTimeoutEnv = Number.parseInt(process.env.EGG_READY_TIMEOUT_ENV || 10000);
    assert(
      Number.isInteger(eggReadyTimeoutEnv),
      `process.env.EGG_READY_TIMEOUT_ENV ${process.env.EGG_READY_TIMEOUT_ENV} should be able to parseInt.`);
    this.readyTimeout = eggReadyTimeoutEnv;

    this[INIT_READY]();
    this
      .on('ready_stat', data => {
        this.logger.info('[egg:core:ready_stat] end ready task %s, remain %j', data.id, data.remain);
      })
      .on('ready_timeout', id => {
        this.logger.warn('[egg:core:ready_timeout] %s seconds later %s was still unable to finish.', this.readyTimeout / 1000, id);
      });

    this.ready(err => {
      this.triggerDidReady(err);
      this.timing.end('Application Start');
    });
  }

  get app() {
    return this.options.app;
  }

  get logger() {
    return this.options.logger;
  }

  get timing() {
    return this.app.timing;
  }

  legacyReadyCallback(name, opt) {
    return this.loadReady.readyCallback(name, opt);
  }

  addBootHook(hook) {
    assert(this[INIT] === false, 'do not add hook when lifecycle has been initialized');
    this[BOOT_HOOKS].push(hook);
  }

  addFunctionAsBootHook(hook) {
    assert(this[INIT] === false, 'do not add hook when lifecycle has been initialized');
    // app.js is exported as a function
    // call this function in configDidLoad
    this[BOOT_HOOKS].push(class Hook {
      constructor(app) {
        this.app = app;
      }
      configDidLoad() {
        hook(this.app);
      }
    });
  }

  /**
   * init boots and trigger config did config
   */
  init() {
    assert(this[INIT] === false, 'lifecycle have been init');
    this[INIT] = true;
    this[BOOTS] = this[BOOT_HOOKS].map(t => new t(this.app));
    this[REGISTER_BEFORE_CLOSE]();
  }

  registerBeforeStart(scope) {
    this[REGISTER_READY_CALLBACK]({
      scope,
      ready: this.loadReady,
      timingKeyPrefix: 'Before Start',
    });
  }

  registerBeforeClose(fn) {
    assert(is.function(fn), 'argument should be function');
    assert(this[IS_CLOSED] === false, 'app has been closed');
    this[CLOSE_SET].add(fn);
  }

  async close() {
    // close in reverse order: first created, last closed
    const closeFns = Array.from(this[CLOSE_SET]);
    for (const fn of closeFns.reverse()) {
      await utils.callFn(fn);
      this[CLOSE_SET].delete(fn);
    }
    // Be called after other close callbacks
    this.app.emit('close');
    this.removeAllListeners();
    this.app.removeAllListeners();
    this[IS_CLOSED] = true;
  }

  triggerConfigWillLoad() {
    for (const boot of this[BOOTS]) {
      if (boot.configWillLoad) {
        boot.configWillLoad();
      }
    }
    this.triggerConfigDidLoad();
  }

  triggerConfigDidLoad() {
    for (const boot of this[BOOTS]) {
      if (boot.configDidLoad) {
        boot.configDidLoad();
      }
    }
    this.triggerDidLoad();
  }

  triggerDidLoad() {
    debug('register didLoad');
    for (const boot of this[BOOTS]) {
      const didLoad = boot.didLoad && boot.didLoad.bind(boot);
      if (didLoad) {
        this[REGISTER_READY_CALLBACK]({
          scope: didLoad,
          ready: this.loadReady,
          timingKeyPrefix: 'Did Load',
          scopeFullName: boot.fullPath + ':didLoad',
        });
      }
    }
  }

  triggerWillReady() {
    debug('register willReady');
    this.bootReady.start();
    for (const boot of this[BOOTS]) {
      const willReady = boot.willReady && boot.willReady.bind(boot);
      if (willReady) {
        this[REGISTER_READY_CALLBACK]({
          scope: willReady,
          ready: this.bootReady,
          timingKeyPrefix: 'Will Ready',
          scopeFullName: boot.fullPath + ':willReady',
        });
      }
    }
  }

  triggerDidReady(err) {
    debug('trigger didReady');
    (async () => {
      for (const boot of this[BOOTS]) {
        if (boot.didReady) {
          try {
            await boot.didReady(err);
          } catch (e) {
            this.emit('error', e);
          }
        }
      }
      debug('trigger didReady done');
    })();
  }

  triggerServerDidReady() {
    (async () => {
      for (const boot of this[BOOTS]) {
        try {
          await utils.callFn(boot.serverDidReady, null, boot);
        } catch (e) {
          this.emit('error', e);
        }
      }
    })();
  }

  [INIT_READY]() {
    this.loadReady = new Ready({ timeout: this.readyTimeout });
    this[DELEGATE_READY_EVENT](this.loadReady);
    this.loadReady.ready(err => {
      debug('didLoad done');
      if (err) {
        this.ready(err);
      } else {
        this.triggerWillReady();
      }
    });

    this.bootReady = new Ready({ timeout: this.readyTimeout, lazyStart: true });
    this[DELEGATE_READY_EVENT](this.bootReady);
    this.bootReady.ready(err => {
      this.ready(err || true);
    });
  }

  [DELEGATE_READY_EVENT](ready) {
    ready.once('error', err => ready.ready(err));
    ready.on('ready_timeout', id => this.emit('ready_timeout', id));
    ready.on('ready_stat', data => this.emit('ready_stat', data));
    ready.on('error', err => this.emit('error', err));
  }

  [REGISTER_BEFORE_CLOSE]() {
    for (const boot of this[BOOTS]) {
      const beforeClose = boot.beforeClose && boot.beforeClose.bind(boot);
      if (beforeClose) {
        this.registerBeforeClose(beforeClose);
      }
    }
  }

  [REGISTER_READY_CALLBACK]({ scope, ready, timingKeyPrefix, scopeFullName }) {
    if (!is.function(scope)) {
      throw new Error('boot only support function');
    }

    // get filename from stack if scopeFullName is undefined
    const name = scopeFullName || utils.getCalleeFromStack(true, 4);
    const timingkey = `${timingKeyPrefix} in ` + utils.getResolvedFilename(name, this.app.baseDir);

    this.timing.start(timingkey);

    const done = ready.readyCallback(name);

    // ensure scope executes after load completed
    process.nextTick(() => {
      utils.callFn(scope).then(() => {
        done();
        this.timing.end(timingkey);
      }, err => {
        done(err);
        this.timing.end(timingkey);
      });
    });
  }
}

module.exports = Lifecycle;