You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
474 lines
11 KiB
474 lines
11 KiB
'use strict'
|
|
|
|
const fastq = require('fastq')
|
|
const EE = require('events').EventEmitter
|
|
const inherits = require('util').inherits
|
|
const TimeTree = require('./time-tree')
|
|
const Plugin = require('./plugin')
|
|
const debug = require('debug')('avvio')
|
|
|
|
function wrap (server, opts, instance) {
|
|
const expose = opts.expose || {}
|
|
const useKey = expose.use || 'use'
|
|
const afterKey = expose.after || 'after'
|
|
const readyKey = expose.ready || 'ready'
|
|
const onCloseKey = expose.onClose || 'onClose'
|
|
const closeKey = expose.close || 'close'
|
|
|
|
if (server[useKey]) {
|
|
throw new Error(useKey + '() is already defined, specify an expose option')
|
|
}
|
|
|
|
if (server[afterKey]) {
|
|
throw new Error(afterKey + '() is already defined, specify an expose option')
|
|
}
|
|
|
|
if (server[readyKey]) {
|
|
throw new Error(readyKey + '() is already defined, specify an expose option')
|
|
}
|
|
|
|
server[useKey] = function (a, b, c) {
|
|
instance.use(a, b, c)
|
|
return this
|
|
}
|
|
|
|
server[afterKey] = function (func) {
|
|
if (typeof func !== 'function') {
|
|
throw new Error('not a function')
|
|
}
|
|
instance.after(encapsulateThreeParam(func, this))
|
|
return this
|
|
}
|
|
|
|
server[readyKey] = function (func) {
|
|
if (func && typeof func !== 'function') {
|
|
throw new Error('not a function')
|
|
}
|
|
return instance.ready(func ? encapsulateThreeParam(func, this) : undefined)
|
|
}
|
|
|
|
server[onCloseKey] = function (func) {
|
|
if (typeof func !== 'function') {
|
|
throw new Error('not a function')
|
|
}
|
|
instance.onClose(encapsulateTwoParam(func, this))
|
|
return this
|
|
}
|
|
|
|
server[closeKey] = function (func) {
|
|
if (func && typeof func !== 'function') {
|
|
throw new Error('not a function')
|
|
}
|
|
|
|
if (func) {
|
|
instance.close(encapsulateThreeParam(func, this))
|
|
return this
|
|
}
|
|
|
|
// this is a Promise
|
|
return instance.close()
|
|
}
|
|
}
|
|
|
|
function Boot (server, opts, done) {
|
|
if (typeof server === 'function' && arguments.length === 1) {
|
|
done = server
|
|
opts = {}
|
|
server = null
|
|
}
|
|
|
|
if (typeof opts === 'function') {
|
|
done = opts
|
|
opts = {}
|
|
}
|
|
|
|
opts = opts || {}
|
|
|
|
if (!(this instanceof Boot)) {
|
|
const instance = new Boot(server, opts, done)
|
|
|
|
if (server) {
|
|
wrap(server, opts, instance)
|
|
}
|
|
|
|
return instance
|
|
}
|
|
|
|
if (opts.autostart !== false) {
|
|
opts.autostart = true
|
|
}
|
|
|
|
server = server || this
|
|
|
|
this._timeout = Number(opts.timeout) || 0
|
|
this._server = server
|
|
this._current = []
|
|
this._error = null
|
|
this._isOnCloseHandlerKey = Symbol('isOnCloseHandler')
|
|
|
|
this.setMaxListeners(0)
|
|
|
|
if (done) {
|
|
this.once('start', done)
|
|
}
|
|
|
|
this.started = false
|
|
this.booted = false
|
|
this.pluginTree = new TimeTree()
|
|
|
|
this._readyQ = fastq(this, callWithCbOrNextTick, 1)
|
|
this._readyQ.pause()
|
|
this._readyQ.drain = () => {
|
|
this.emit('start')
|
|
// nooping this, we want to emit start only once
|
|
this._readyQ.drain = noop
|
|
}
|
|
|
|
this._closeQ = fastq(this, closeWithCbOrNextTick, 1)
|
|
this._closeQ.pause()
|
|
this._closeQ.drain = () => {
|
|
this.emit('close')
|
|
// nooping this, we want to emit start only once
|
|
this._closeQ.drain = noop
|
|
}
|
|
|
|
this._doStart = null
|
|
const main = new Plugin(this, root.bind(this), opts, noop, 0)
|
|
|
|
main.once('start', (serverName, funcName, time) => {
|
|
const nodeId = this.pluginTree.start(null, funcName, time)
|
|
main.once('loaded', (serverName, funcName, time) => {
|
|
this.pluginTree.stop(nodeId, time)
|
|
})
|
|
})
|
|
|
|
Plugin.loadPlugin.call(this, main, (err) => {
|
|
debug('root plugin ready')
|
|
try {
|
|
this.emit('preReady')
|
|
} catch (prereadyError) {
|
|
err = err || this._error || prereadyError
|
|
}
|
|
|
|
if (err) {
|
|
this._error = err
|
|
if (this._readyQ.length() === 0) {
|
|
throw err
|
|
}
|
|
} else {
|
|
this.booted = true
|
|
}
|
|
this._readyQ.resume()
|
|
})
|
|
}
|
|
|
|
function root (s, opts, done) {
|
|
this._doStart = done
|
|
if (opts.autostart) {
|
|
this.start()
|
|
}
|
|
}
|
|
|
|
inherits(Boot, EE)
|
|
|
|
Boot.prototype.start = function () {
|
|
this.started = true
|
|
|
|
// we need to wait any call to use() to happen
|
|
process.nextTick(this._doStart)
|
|
return this
|
|
}
|
|
|
|
// allows to override the instance of a server, given a plugin
|
|
Boot.prototype.override = function (server, func, opts) {
|
|
return server
|
|
}
|
|
|
|
function assertPlugin (plugin) {
|
|
if (!(plugin && (typeof plugin === 'function' || typeof plugin.then === 'function'))) {
|
|
throw new Error('plugin must be a function or a promise')
|
|
}
|
|
}
|
|
|
|
// load a plugin
|
|
Boot.prototype.use = function (plugin, opts) {
|
|
this._addPlugin(plugin, opts, false)
|
|
|
|
return this
|
|
}
|
|
|
|
Boot.prototype._addPlugin = function (plugin, opts, isAfter) {
|
|
assertPlugin(plugin)
|
|
opts = opts || {}
|
|
|
|
if (this.booted) {
|
|
throw new Error('root plugin has already booted')
|
|
}
|
|
|
|
// we always add plugins to load at the current element
|
|
const current = this._current[0]
|
|
|
|
const obj = new Plugin(this, plugin, opts, isAfter)
|
|
obj.once('start', (serverName, funcName, time) => {
|
|
const nodeId = this.pluginTree.start(current.name, funcName, time)
|
|
obj.once('loaded', (serverName, funcName, time) => {
|
|
this.pluginTree.stop(nodeId, time)
|
|
})
|
|
})
|
|
|
|
if (current.loaded) {
|
|
throw new Error(`Impossible to load "${obj.name}" plugin because the parent "${current.name}" was already loaded`)
|
|
}
|
|
|
|
// we add the plugin to be loaded at the end of the current queue
|
|
current.enqueue(obj, (err) => {
|
|
if (err) {
|
|
this._error = err
|
|
}
|
|
})
|
|
}
|
|
|
|
Boot.prototype.after = function (func) {
|
|
this._addPlugin(_after.bind(this), {}, true)
|
|
|
|
function _after (s, opts, done) {
|
|
callWithCbOrNextTick.call(this, func, done)
|
|
}
|
|
|
|
return this
|
|
}
|
|
|
|
Boot.prototype.onClose = function (func) {
|
|
// this is used to distinguish between onClose and close handlers
|
|
// because they share the same queue but must be called with different signatures
|
|
|
|
if (typeof func !== 'function') {
|
|
throw new Error('not a function')
|
|
}
|
|
|
|
func[this._isOnCloseHandlerKey] = true
|
|
this._closeQ.unshift(func, callback.bind(this))
|
|
|
|
function callback (err) {
|
|
if (err) this._error = err
|
|
}
|
|
|
|
return this
|
|
}
|
|
|
|
Boot.prototype.close = function (func) {
|
|
var promise
|
|
|
|
if (func) {
|
|
if (typeof func !== 'function') {
|
|
throw new Error('not a function')
|
|
}
|
|
} else {
|
|
promise = new Promise(function (resolve, reject) {
|
|
func = function (err) {
|
|
if (err) {
|
|
return reject(err)
|
|
}
|
|
resolve()
|
|
}
|
|
})
|
|
}
|
|
|
|
this.ready(() => {
|
|
this._error = null
|
|
this._closeQ.push(func)
|
|
process.nextTick(this._closeQ.resume.bind(this._closeQ))
|
|
})
|
|
|
|
return promise
|
|
}
|
|
|
|
Boot.prototype.ready = function (func) {
|
|
if (func) {
|
|
if (typeof func !== 'function') {
|
|
throw new Error('not a function')
|
|
}
|
|
this._readyQ.push(func)
|
|
this.start()
|
|
return
|
|
}
|
|
|
|
return new Promise((resolve, reject) => {
|
|
this._readyQ.push(readyPromiseCB)
|
|
this.start()
|
|
|
|
function readyPromiseCB (err, context, done) {
|
|
if (err) {
|
|
reject(err)
|
|
} else {
|
|
resolve(context)
|
|
}
|
|
process.nextTick(done)
|
|
}
|
|
})
|
|
}
|
|
|
|
Boot.prototype.prettyPrint = function () {
|
|
return this.pluginTree.prittyPrint()
|
|
}
|
|
|
|
Boot.prototype.toJSON = function () {
|
|
return this.pluginTree.toJSON()
|
|
}
|
|
|
|
function noop () { }
|
|
|
|
function callWithCbOrNextTick (func, cb, context) {
|
|
context = this._server
|
|
var err = this._error
|
|
var res
|
|
|
|
// with this the error will appear just in the next after/ready callback
|
|
this._error = null
|
|
if (func.length === 0) {
|
|
this._error = err
|
|
res = func()
|
|
if (res && typeof res.then === 'function') {
|
|
res.then(() => process.nextTick(cb), (e) => process.nextTick(cb, e))
|
|
} else {
|
|
process.nextTick(cb)
|
|
}
|
|
} else if (func.length === 1) {
|
|
res = func(err)
|
|
if (res && typeof res.then === 'function') {
|
|
res.then(() => process.nextTick(cb), (e) => process.nextTick(cb, e))
|
|
} else {
|
|
process.nextTick(cb)
|
|
}
|
|
} else {
|
|
if (this._timeout === 0) {
|
|
if (func.length === 2) {
|
|
func(err, cb)
|
|
} else {
|
|
func(err, context, cb)
|
|
}
|
|
} else {
|
|
timeoutCall.call(this, func, err, context, cb)
|
|
}
|
|
}
|
|
}
|
|
|
|
function timeoutCall (func, rootErr, context, cb) {
|
|
const name = func.name
|
|
debug('setting up ready timeout', name, this._timeout)
|
|
let timer = setTimeout(() => {
|
|
debug('timed out', name)
|
|
timer = null
|
|
const toutErr = new Error(`ERR_AVVIO_READY_TIMEOUT: plugin did not start in time: ${name}`)
|
|
toutErr.code = 'ERR_AVVIO_READY_TIMEOUT'
|
|
toutErr.fn = func
|
|
this._error = toutErr
|
|
cb(toutErr)
|
|
}, this._timeout)
|
|
|
|
if (func.length === 2) {
|
|
func(rootErr, timeoutCb.bind(this))
|
|
} else {
|
|
func(rootErr, context, timeoutCb.bind(this))
|
|
}
|
|
|
|
function timeoutCb (err) {
|
|
if (timer) {
|
|
clearTimeout(timer)
|
|
this._error = err
|
|
cb(this._error)
|
|
} else {
|
|
// timeout has been triggered
|
|
// can not call cb twice
|
|
}
|
|
}
|
|
}
|
|
|
|
function closeWithCbOrNextTick (func, cb, context) {
|
|
context = this._server
|
|
var isOnCloseHandler = func[this._isOnCloseHandlerKey]
|
|
if (func.length === 0 || func.length === 1) {
|
|
var promise
|
|
if (isOnCloseHandler) {
|
|
promise = func(context)
|
|
} else {
|
|
promise = func(this._error)
|
|
}
|
|
if (promise && typeof promise.then === 'function') {
|
|
debug('resolving close/onClose promise')
|
|
promise.then(
|
|
() => process.nextTick(cb),
|
|
(e) => process.nextTick(cb, e))
|
|
} else {
|
|
process.nextTick(cb)
|
|
}
|
|
} else if (func.length === 2) {
|
|
if (isOnCloseHandler) {
|
|
func(context, cb)
|
|
} else {
|
|
func(this._error, cb)
|
|
}
|
|
} else {
|
|
if (isOnCloseHandler) {
|
|
func(context, cb)
|
|
} else {
|
|
func(this._error, context, cb)
|
|
}
|
|
}
|
|
}
|
|
|
|
function encapsulateTwoParam (func, that) {
|
|
return _encapsulateTwoParam.bind(that)
|
|
function _encapsulateTwoParam (context, cb) {
|
|
if (func.length === 0) {
|
|
func()
|
|
process.nextTick(cb)
|
|
} else if (func.length === 1) {
|
|
func(cb)
|
|
} else {
|
|
func(this, cb)
|
|
}
|
|
}
|
|
}
|
|
|
|
function encapsulateThreeParam (func, that) {
|
|
return _encapsulateThreeParam.bind(that)
|
|
function _encapsulateThreeParam (err, cb) {
|
|
var res
|
|
if (!func) {
|
|
process.nextTick(cb)
|
|
} else if (func.length === 0) {
|
|
res = func()
|
|
if (res && res.then) {
|
|
res.then(function () {
|
|
process.nextTick(cb, err)
|
|
}, cb)
|
|
} else {
|
|
process.nextTick(cb, err)
|
|
}
|
|
} else if (func.length === 1) {
|
|
res = func(err)
|
|
if (res && res.then) {
|
|
res.then(function () {
|
|
process.nextTick(cb)
|
|
}, cb)
|
|
} else {
|
|
process.nextTick(cb)
|
|
}
|
|
} else if (func.length === 2) {
|
|
func(err, cb)
|
|
} else {
|
|
func(err, this, cb)
|
|
}
|
|
}
|
|
}
|
|
|
|
module.exports = Boot
|
|
module.exports.express = function (app) {
|
|
return Boot(app, {
|
|
expose: {
|
|
use: 'load'
|
|
}
|
|
})
|
|
}
|