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.
199 lines
4.7 KiB
199 lines
4.7 KiB
4 years ago
|
'use strict'
|
||
|
|
||
|
const fastq = require('fastq')
|
||
|
const EE = require('events').EventEmitter
|
||
|
const inherits = require('util').inherits
|
||
|
const debug = require('debug')('avvio')
|
||
|
const CODE_PLUGIN_TIMEOUT = 'ERR_AVVIO_PLUGIN_TIMEOUT'
|
||
|
|
||
|
function getName (func) {
|
||
|
// let's see if this is a file, and in that case use that
|
||
|
// this is common for plugins
|
||
|
const cache = require.cache
|
||
|
const keys = Object.keys(cache)
|
||
|
|
||
|
for (var i = 0; i < keys.length; i++) {
|
||
|
if (cache[keys[i]].exports === func) {
|
||
|
return keys[i]
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// if not maybe it's a named function, so use that
|
||
|
if (func.name) {
|
||
|
return func.name
|
||
|
}
|
||
|
|
||
|
// takes the first two lines of the function if nothing else works
|
||
|
return func.toString().split('\n').slice(0, 2).map(s => s.trim()).join(' -- ')
|
||
|
}
|
||
|
|
||
|
function Plugin (parent, func, optsOrFunc, isAfter, timeout) {
|
||
|
this.func = func
|
||
|
this.opts = optsOrFunc
|
||
|
this.deferred = false
|
||
|
this.onFinish = null
|
||
|
this.parent = parent
|
||
|
this.timeout = timeout === undefined ? parent._timeout : timeout
|
||
|
this.name = getName(func)
|
||
|
this.isAfter = isAfter
|
||
|
|
||
|
this.q = fastq(parent, loadPlugin, 1)
|
||
|
this.q.pause()
|
||
|
this.loaded = false
|
||
|
|
||
|
// always start the queue in the next tick
|
||
|
// because we try to attach subsequent call to use()
|
||
|
// to the right plugin. we need to defer them,
|
||
|
// or they will end up at the top of _current
|
||
|
}
|
||
|
|
||
|
inherits(Plugin, EE)
|
||
|
|
||
|
Plugin.prototype.exec = function (server, cb) {
|
||
|
const func = this.func
|
||
|
var completed = false
|
||
|
var name = this.name
|
||
|
|
||
|
if (this.parent._error && !this.isAfter) {
|
||
|
debug('skipping loading of plugin as parent errored and it is not an after', name)
|
||
|
process.nextTick(cb)
|
||
|
return
|
||
|
}
|
||
|
|
||
|
try {
|
||
|
this.server = this.parent.override(server, func, this.opts)
|
||
|
} catch (err) {
|
||
|
debug('override errored', name)
|
||
|
return cb(err)
|
||
|
}
|
||
|
|
||
|
this.opts = typeof this.opts === 'function' ? this.opts(this.server) : this.opts
|
||
|
|
||
|
debug('exec', name)
|
||
|
|
||
|
var timer
|
||
|
|
||
|
if (this.timeout > 0) {
|
||
|
debug('setting up timeout', name, this.timeout)
|
||
|
timer = setTimeout(function () {
|
||
|
debug('timed out', name)
|
||
|
timer = null
|
||
|
const err = new Error(`${CODE_PLUGIN_TIMEOUT}: plugin did not start in time: ${name}`)
|
||
|
err.code = CODE_PLUGIN_TIMEOUT
|
||
|
err.fn = func
|
||
|
done(err)
|
||
|
}, this.timeout)
|
||
|
}
|
||
|
|
||
|
this.emit('start', this.server ? this.server.name : null, this.name, Date.now())
|
||
|
var promise = func(this.server, this.opts, done)
|
||
|
if (promise && typeof promise.then === 'function') {
|
||
|
debug('resolving promise', name)
|
||
|
promise.then(
|
||
|
() => process.nextTick(done),
|
||
|
(e) => process.nextTick(done, e))
|
||
|
}
|
||
|
|
||
|
function done (err) {
|
||
|
if (completed) {
|
||
|
debug('loading complete', name)
|
||
|
return
|
||
|
}
|
||
|
|
||
|
if (err) {
|
||
|
debug('exec errored', name)
|
||
|
} else {
|
||
|
debug('exec completed', name)
|
||
|
}
|
||
|
|
||
|
completed = true
|
||
|
|
||
|
if (timer) {
|
||
|
clearTimeout(timer)
|
||
|
}
|
||
|
|
||
|
cb(err)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
Plugin.prototype.enqueue = function (obj, cb) {
|
||
|
debug('enqueue', this.name, obj.name)
|
||
|
this.emit('enqueue', this.server ? this.server.name : null, this.name, Date.now())
|
||
|
this.q.push(obj, cb)
|
||
|
}
|
||
|
|
||
|
Plugin.prototype.finish = function (err, cb) {
|
||
|
debug('finish', this.name)
|
||
|
const done = () => {
|
||
|
if (this.loaded) {
|
||
|
return
|
||
|
}
|
||
|
|
||
|
debug('loaded', this.name)
|
||
|
this.emit('loaded', this.server ? this.server.name : null, this.name, Date.now())
|
||
|
this.loaded = true
|
||
|
|
||
|
cb(err)
|
||
|
}
|
||
|
|
||
|
if (err) {
|
||
|
done()
|
||
|
return
|
||
|
}
|
||
|
|
||
|
const check = () => {
|
||
|
debug('check', this.name, this.q.length(), this.q.running())
|
||
|
if (this.q.length() === 0 && this.q.running() === 0) {
|
||
|
done()
|
||
|
} else {
|
||
|
debug('delayed', this.name)
|
||
|
// finish when the queue of nested plugins to load is empty
|
||
|
this.q.drain = () => {
|
||
|
debug('drain', this.name)
|
||
|
this.q.drain = noop
|
||
|
|
||
|
// we defer the check, as a safety net for things
|
||
|
// that might be scheduled in the loading callback
|
||
|
process.nextTick(check)
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
process.nextTick(check)
|
||
|
|
||
|
// we start loading the dependents plugins only once
|
||
|
// the current level is finished
|
||
|
this.q.resume()
|
||
|
}
|
||
|
|
||
|
// loads a plugin
|
||
|
function loadPlugin (toLoad, cb) {
|
||
|
if (typeof toLoad.func.then === 'function') {
|
||
|
toLoad.func.then((fn) => {
|
||
|
if (typeof fn.default === 'function') {
|
||
|
fn = fn.default
|
||
|
}
|
||
|
toLoad.func = fn
|
||
|
loadPlugin.call(this, toLoad, cb)
|
||
|
}, cb)
|
||
|
return
|
||
|
}
|
||
|
|
||
|
const last = this._current[0]
|
||
|
|
||
|
// place the plugin at the top of _current
|
||
|
this._current.unshift(toLoad)
|
||
|
|
||
|
toLoad.exec((last && last.server) || this._server, (err) => {
|
||
|
toLoad.finish(err, (err) => {
|
||
|
this._current.shift()
|
||
|
cb(err)
|
||
|
})
|
||
|
})
|
||
|
}
|
||
|
|
||
|
function noop () {}
|
||
|
|
||
|
module.exports = Plugin
|
||
|
module.exports.loadPlugin = loadPlugin
|