'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_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)); } 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(); } // function boot hook register after configDidLoad trigger const beforeClose = boot.beforeClose && boot.beforeClose.bind(boot); if (beforeClose) { this.registerBeforeClose(beforeClose); } } 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_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;