Skip to content

Commit

Permalink
feat: impl boot methods
Browse files Browse the repository at this point in the history
1. add `Boot` Class, developer can inherit from it.
2. add `startBoot`, application like egg should call
`startBoot` after all files loaded.

Refs: eggjs/egg#2520
  • Loading branch information
killagu committed Aug 30, 2018
1 parent ae38fa4 commit 74d2137
Show file tree
Hide file tree
Showing 20 changed files with 609 additions and 13 deletions.
183 changes: 178 additions & 5 deletions lib/egg.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,11 @@ const debug = require('debug')('egg-core');
const is = require('is-type-of');
const co = require('co');
const BaseContextClass = require('./utils/base_context_class');
const BootClass = require('./utils/boot_class');
const utils = require('./utils');
const Router = require('./utils/router');
const Timing = require('./utils/timing');
const { Ready } = require('ready-callback');

const DEPRECATE = Symbol('EggCore#deprecate');
const CLOSESET = Symbol('EggCore#closeSet');
Expand All @@ -19,6 +21,23 @@ const CLOSE_PROMISE = Symbol('EggCore#closePromise');
const ROUTER = Symbol('EggCore#router');
const EGG_LOADER = Symbol.for('egg#loader');
const INIT_READY = Symbol('EggCore#initReady');
const INIT_BOOT_READY = Symbol('EggCore#initBootReady');
const READY_TIMEOUT = Symbol('EggCore#readyTimeout');
const DELEGATE_READY_EVENT = Symbol('EggCore#delegateReadyEvent');
const BOOTS = Symbol.for('EggCore#boots');

const TRIGGER_CONFIG_DID_LOAD = Symbol.for('EggCore#triggerConfigDidLoad');
const TRIGGER_DID_READY = Symbol.for('EggCore#triggerDidReady');
const TRIGGER_SERVER_DID_READY = Symbol.for('EggCore#triggerServerDidReady');
const START_BOOT = Symbol.for('EggCore#startBoot');

const LOAD_READY = Symbol('EggCore#loadReady');
const BOOT_READY = Symbol('EggCore#bootReady');
const READY_CALLBACKS = Symbol('EggCore#readyCallbacks');

const REGISTER_DID_LOAD = Symbol('EggCore#registerDidLoad');
const REGISTER_WILL_READY = Symbol('EggCore#registerWillReady');
const REGISTER_BEFORE_CLOSE = Symbol('EggCore#reigsterBeforeClose');


class EggCore extends KoaApplication {
Expand Down Expand Up @@ -108,6 +127,26 @@ class EggCore extends KoaApplication {
*/
this.Service = Service;

/**
* @member {BootClass} EggCore#BootClass
*/
this.BootClass = BootClass;

/**
* Base boot to be extended by boot in `app.boot`
* @class Boot
* @extends BootClass
* @example Boot extends app.Boot {}
*/
const Boot = this.BootClass;

/**
* Retrieve base boot
* @member {Boot} EggCore#Boot
*/
this.Boot = Boot;
this[BOOTS] = [];

/**
* The loader instance, the default class is {@link EggLoader}.
* If you want define
Expand Down Expand Up @@ -207,17 +246,21 @@ class EggCore extends KoaApplication {
* @param {Function|GeneratorFunction|AsyncFunction} scope function will execute before app start
*/
beforeStart(scope) {
this.registerReadyCallback(scope, this[LOAD_READY], 'Before Start');
}

registerReadyCallback(scope, ready, timingKeyPrefix) {
if (!is.function(scope)) {
throw new Error('beforeStart only support function');
throw new Error('boot only support function');
}

// get filename from stack
const name = utils.getCalleeFromStack(true);
const timingkey = 'Before Start in ' + utils.getResolvedFilename(name, this.options.baseDir);
const name = utils.getCalleeFromStack(true, 3);
const timingkey = `${timingKeyPrefix} in ` + utils.getResolvedFilename(name, this.options.baseDir);

this.timing.start(timingkey);

const done = this.readyCallback(name);
const done = ready.readyCallback(name);

// ensure scope executes after load completed
process.nextTick(() => {
Expand Down Expand Up @@ -291,6 +334,7 @@ class EggCore extends KoaApplication {
// 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[READY_TIMEOUT] = eggReadyTimeoutEnv;

/**
* If a client starts asynchronously, you can register `readyCallback`,
Expand All @@ -305,7 +349,13 @@ class EggCore extends KoaApplication {
* const done = app.readyCallback('mysql');
* mysql.ready(done);
*/
require('ready-callback')({ timeout: eggReadyTimeoutEnv }).mixin(this);
this[LOAD_READY] = new Ready({ timeout: this[READY_TIMEOUT] });
// init after didLoad
this[BOOT_READY] = null;

this.readyCallback = this[LOAD_READY].readyCallback.bind(this[LOAD_READY]);
this[READY_CALLBACKS] = [];
this[DELEGATE_READY_EVENT](this[LOAD_READY]);

this.on('ready_stat', data => {
this.console.info('[egg:core:ready_stat] end ready task %s, remain %j', data.id, data.remain);
Expand All @@ -314,6 +364,48 @@ class EggCore extends KoaApplication {
});
}

[INIT_BOOT_READY]() {
if (this[BOOT_READY]) return;
this[BOOT_READY] = new Ready({ timeout: this[READY_TIMEOUT] });
this[DELEGATE_READY_EVENT](this[BOOT_READY]);
this[BOOT_READY].ready(err => {
debug('willReady done');
this[TRIGGER_DID_READY](err);
});
}

ready(flagOrFunction) {
if (flagOrFunction === undefined || is.function(flagOrFunction)) {
// boot ready have not init
if (!this[BOOT_READY]) {
// push function to callback cache
if (is.function(flagOrFunction)) {
this[READY_CALLBACKS].push(flagOrFunction);
} else {
// return a promise and create function to callback cache
return new Promise((resolve, reject) => {
const fn = err => (err ? reject(err) : resolve());
this[READY_CALLBACKS].push(fn);
});
}
} else {
// use boot ready
return this[BOOT_READY].ready(flagOrFunction);
}
} else {
// boot ready have not init
this[INIT_BOOT_READY]();
return this[BOOT_READY].ready(flagOrFunction);
}
}

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

/**
* get router
* @member {Router} EggCore#router
Expand Down Expand Up @@ -393,6 +485,87 @@ class EggCore extends KoaApplication {
return yield obj;
});
}

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

[TRIGGER_DID_READY](err) {
debug('trigger didReady');
const didReadys = this[BOOTS]
.filter(t => t.didReady)
.map(t => t.didReady.bind(t));
const readyCbs = [
...this[READY_CALLBACKS],
...didReadys,
];
(async () => {
for (const cb of readyCbs) {
try {
await utils.callFn(cb, [ err ]);
} catch (e) {
this.emit('error', e);
}
}
debug('trigger didReady done');
})();
}

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

[START_BOOT]() {
this[REGISTER_DID_LOAD]();
this[REGISTER_BEFORE_CLOSE]();
}

[REGISTER_DID_LOAD]() {
debug('register didLoad');
for (const boot of this[BOOTS]) {
if (boot.didLoad) {
this.registerReadyCallback(boot.didLoad, this[LOAD_READY], 'Did Load');
}
}
this[LOAD_READY].ready(error => {
debug('didLoad done');
if (error) {
this.ready(error);
} else {
this[REGISTER_WILL_READY]();
}
});
}

[REGISTER_WILL_READY]() {
debug('register willReady');
this[INIT_BOOT_READY]();
for (const boot of this[BOOTS]) {
if (boot.willReady) {
this.registerReadyCallback(boot.willReady, this[BOOT_READY], 'Will Ready');
}
}
}

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

// delegate all router method to application
Expand Down
18 changes: 14 additions & 4 deletions lib/loader/mixin/custom.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
'use strict';

const path = require('path');
const is = require('is-type-of');
const BOOTS = Symbol.for('EggCore#boots');


module.exports = {
Expand All @@ -23,19 +25,27 @@ module.exports = {
*/
loadCustomApp() {
this.timing.start('Load app.js');
this.getLoadUnits()
.forEach(unit => this.loadFile(this.resolveModule(path.join(unit.path, 'app'))));
this.app[BOOTS] = this.getLoadUnits()
.map(unit =>
this.loadFile(this.resolveModule(path.join(unit.path, 'app')))
)
.filter(t => is.class(t))
.map(t => new t(this.app));
this.timing.end('Load app.js');
this.app[Symbol.for('EggCore#triggerConfigDidLoad')]();
},

/**
* Load agent.js, same as {@link EggLoader#loadCustomApp}
*/
loadCustomAgent() {
this.timing.start('Load agent.js');
this.getLoadUnits()
.forEach(unit => this.loadFile(this.resolveModule(path.join(unit.path, 'agent'))));
this.app[BOOTS] = this.getLoadUnits()
.map(unit => this.loadFile(this.resolveModule(path.join(unit.path, 'agent'))))
.filter(t => is.class(t))
.map(t => new t(this.app));
this.timing.end('Load agent.js');
this.app[Symbol.for('EggCore#triggerConfigDidLoad')]();
},

};
50 changes: 50 additions & 0 deletions lib/utils/boot_class.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
'use strict';

class BootClass {
/**
* @constructor
* @param {EggApplication} app - app instance
*/
constructor(app) {
this.app = app;

/**
* will be triggered after plugin.js, config.js,
* extends, app.js/agent.js did loaded
* @method configDidLoad
*/

/**
* will be triggered after all files did loaded
* @async
* @method didLoad
*/

/**
* all plugins are ready
* @async
* @method willReady
*/

/**
* after all `willReady` ready
* @async
* @method didReady
*/

/**
* all workers, agent is ready
* @async
* @method serverDidReady
*/

/**
* before process exit
* not safe
* @async
* @method beforeClose
*/
}
}

module.exports = BootClass;
7 changes: 4 additions & 3 deletions lib/utils/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@ module.exports = {
return is.generatorFunction(fn) ? convert(fn) : fn;
},

getCalleeFromStack(withLine) {
getCalleeFromStack(withLine, stackIndex) {
stackIndex = stackIndex === undefined ? 2 : stackIndex;
const limit = Error.stackTraceLimit;
const prep = Error.prepareStackTrace;

Expand All @@ -50,7 +51,7 @@ module.exports = {
// capture the stack
const obj = {};
Error.captureStackTrace(obj);
let callSite = obj.stack[2];
let callSite = obj.stack[stackIndex];
let fileName;
/* istanbul ignore else */
if (callSite) {
Expand All @@ -60,7 +61,7 @@ module.exports = {
/* istanbul ignore if */
if (fileName && fileName.endsWith('egg-mock/lib/app.js')) {
// TODO: add test
callSite = obj.stack[3];
callSite = obj.stack[stackIndex + 1];
fileName = callSite.getFileName();
}
}
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
},
"devDependencies": {
"autod": "^3.0.1",
"await-event": "^2.1.0",
"coffee": "^4.1.0",
"egg-bin": "^4.7.0",
"egg-ci": "^1.8.0",
Expand Down
Loading

0 comments on commit 74d2137

Please sign in to comment.