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.

286 lines
6.5 KiB

3 years ago
'use strict'
const queueMicrotask = require('queue-microtask')
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)
// eslint-disable-next-line no-var
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 promise () {
const obj = {}
obj.promise = new Promise((resolve, reject) => {
obj.resolve = resolve
obj.reject = reject
})
return obj
}
function Plugin (parent, func, optsOrFunc, isAfter, timeout) {
this.started = false
this.func = func
this.opts = optsOrFunc
this.onFinish = null
this.parent = parent
this.timeout = timeout === undefined ? parent._timeout : timeout
this.name = getName(func)
this.isAfter = isAfter
this.q = fastq(parent, loadPluginNextTick, 1)
this.q.pause()
this._error = null
this.loaded = false
this._promise = null
// 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
let completed = false
const 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
}
if (!this.isAfter) {
// Skip override for after
try {
this.server = this.parent.override(server, func, this.opts)
} catch (err) {
debug('override errored', name)
return cb(err)
}
} else {
this.server = server
}
this.opts = typeof this.opts === 'function' ? this.opts(this.server) : this.opts
debug('exec', name)
let timer
const done = (err) => {
if (completed) {
debug('loading complete', name)
return
}
this._error = err
if (err) {
debug('exec errored', name)
} else {
debug('exec completed', name)
}
completed = true
if (timer) {
clearTimeout(timer)
}
cb(err)
}
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}. You may have forgotten to call 'done' function or to resolve a Promise`)
err.code = CODE_PLUGIN_TIMEOUT
err.fn = func
done(err)
}, this.timeout)
}
this.started = true
this.emit('start', this.server ? this.server.name : null, this.name, Date.now())
const promise = func(this.server, this.opts, done)
if (promise && typeof promise.then === 'function') {
debug('exec: resolving promise', name)
promise.then(
() => process.nextTick(done),
(e) => process.nextTick(done, e))
}
}
Plugin.prototype.loadedSoFar = function () {
if (this.loaded) {
return Promise.resolve()
}
const setup = () => {
this.server.after((err, cb) => {
this._error = err
this.q.pause()
if (err) {
debug('rejecting promise', this.name, err)
this._promise.reject(err)
} else {
debug('resolving promise', this.name)
this._promise.resolve()
}
this._promise = null
process.nextTick(cb, err)
})
this.q.resume()
}
let res
if (!this._promise) {
this._promise = promise()
res = this._promise.promise
if (!this.server) {
this.on('start', setup)
} else {
setup()
}
} else {
res = Promise.resolve()
}
return res
}
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, err)
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) {
if (this._promise) {
this._promise.reject(err)
this._promise = null
}
done()
return
}
const check = () => {
debug('check', this.name, this.q.length(), this.q.running(), this._promise)
if (this.q.length() === 0 && this.q.running() === 0) {
if (this._promise) {
const wrap = () => {
debug('wrap')
queueMicrotask(check)
}
this._promise.resolve()
this._promise.promise.then(wrap, wrap)
this._promise = null
} else {
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
queueMicrotask(check)
}
}
}
queueMicrotask(check)
// we start loading the dependents plugins only once
// the current level is finished
this.q.resume()
}
// delays plugin loading until the next tick to ensure any bound `_after` callbacks have a chance
// to run prior to executing the next plugin
function loadPluginNextTick (toLoad, cb) {
const parent = this
process.nextTick(loadPlugin.bind(parent), toLoad, cb)
}
// 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