strong-tie/inbound-calls
0
1'use strict'2 3const { EventEmitter } = require('node:events')4const { inherits } = require('node:util')5const { debug } = require('./debug')6const { createPromise } = require('./create-promise')7const { AVV_ERR_PLUGIN_EXEC_TIMEOUT } = require('./errors')8const { getPluginName } = require('./get-plugin-name')9const { isPromiseLike } = require('./is-promise-like')10 11/**12 * @param {*} queue13 * @param {*} func14 * @param {*} options15 * @param {boolean} isAfter16 * @param {number} [timeout]17 */18function Plugin (queue, func, options, isAfter, timeout) {19 this.queue = queue20 this.func = func21 this.options = options22 23 /**24 * @type {boolean}25 */26 this.isAfter = isAfter27 /**28 * @type {number}29 */30 this.timeout = timeout31 32 /**33 * @type {boolean}34 */35 this.started = false36 /**37 * @type {string}38 */39 this.name = getPluginName(func, options)40 41 this.queue.pause()42 43 /**44 * @type {Error|null}45 */46 this._error = null47 /**48 * @type {boolean}49 */50 this.loaded = false51 52 this._promise = null53 54 this.startTime = null55}56 57inherits(Plugin, EventEmitter)58 59/**60 * @callback ExecCallback61 * @param {Error|null} execErr62 * @returns63 */64 65/**66 *67 * @param {*} server68 * @param {ExecCallback} callback69 * @returns70 */71Plugin.prototype.exec = function (server, callback) {72 debug('exec', this.name)73 74 this.server = server75 const func = this.func76 const name = this.name77 let completed = false78 79 this.options = typeof this.options === 'function' ? this.options(this.server) : this.options80 81 let timer = null82 83 /**84 * @param {Error} [execErr]85 */86 const done = (execErr) => {87 if (completed) {88 debug('loading complete', name)89 return90 }91 92 this._error = execErr93 94 if (execErr) {95 debug('exec errored', name)96 } else {97 debug('exec completed', name)98 }99 100 completed = true101 102 if (timer) {103 clearTimeout(timer)104 }105 106 callback(execErr)107 }108 109 if (this.timeout > 0) {110 debug('setting up timeout', name, this.timeout)111 timer = setTimeout(function () {112 debug('timed out', name)113 timer = null114 const readyTimeoutErr = new AVV_ERR_PLUGIN_EXEC_TIMEOUT(name)115 // TODO Remove reference to function116 readyTimeoutErr.fn = func117 done(readyTimeoutErr)118 }, this.timeout)119 }120 121 this.started = true122 this.startTime = Date.now()123 this.emit('start', this.server ? this.server.name : null, this.name, Date.now())124 125 const maybePromiseLike = func(this.server, this.options, done)126 127 if (isPromiseLike(maybePromiseLike)) {128 debug('exec: resolving promise', name)129 130 maybePromiseLike.then(131 () => process.nextTick(done),132 (e) => process.nextTick(done, e))133 } else if (func.length < 3) {134 done()135 }136}137 138/**139 * @returns {Promise}140 */141Plugin.prototype.loadedSoFar = function () {142 debug('loadedSoFar', this.name)143 144 if (this.loaded) {145 return Promise.resolve()146 }147 148 const setup = () => {149 this.server.after((afterErr, callback) => {150 this._error = afterErr151 this.queue.pause()152 153 if (this._promise) {154 if (afterErr) {155 debug('rejecting promise', this.name, afterErr)156 this._promise.reject(afterErr)157 } else {158 debug('resolving promise', this.name)159 this._promise.resolve()160 }161 this._promise = null162 }163 164 process.nextTick(callback, afterErr)165 })166 this.queue.resume()167 }168 169 let res170 171 if (!this._promise) {172 this._promise = createPromise()173 res = this._promise.promise174 175 if (!this.server) {176 this.on('start', setup)177 } else {178 setup()179 }180 } else {181 res = Promise.resolve()182 }183 184 return res185}186 187/**188 * @callback EnqueueCallback189 * @param {Error|null} enqueueErr190 * @param {Plugin} result191 */192 193/**194 *195 * @param {Plugin} plugin196 * @param {EnqueueCallback} callback197 */198Plugin.prototype.enqueue = function (plugin, callback) {199 debug('enqueue', this.name, plugin.name)200 201 this.emit('enqueue', this.server ? this.server.name : null, this.name, Date.now())202 this.queue.push(plugin, callback)203}204 205/**206 * @callback FinishCallback207 * @param {Error|null} finishErr208 * @returns209 */210/**211 *212 * @param {Error|null} err213 * @param {FinishCallback} callback214 * @returns215 */216Plugin.prototype.finish = function (err, callback) {217 debug('finish', this.name, err)218 219 const done = () => {220 if (this.loaded) {221 return222 }223 224 debug('loaded', this.name)225 this.emit('loaded', this.server ? this.server.name : null, this.name, Date.now())226 this.loaded = true227 228 callback(err)229 }230 231 if (err) {232 if (this._promise) {233 this._promise.reject(err)234 this._promise = null235 }236 done()237 return238 }239 240 const check = () => {241 debug('check', this.name, this.queue.length(), this.queue.running(), this._promise)242 if (this.queue.length() === 0 && this.queue.running() === 0) {243 if (this._promise) {244 const wrap = () => {245 debug('wrap')246 queueMicrotask(check)247 }248 this._promise.resolve()249 this._promise.promise.then(wrap, wrap)250 this._promise = null251 } else {252 done()253 }254 } else {255 debug('delayed', this.name)256 // finish when the queue of nested plugins to load is empty257 this.queue.drain = () => {258 debug('drain', this.name)259 this.queue.drain = noop260 261 // we defer the check, as a safety net for things262 // that might be scheduled in the loading callback263 queueMicrotask(check)264 }265 }266 }267 268 queueMicrotask(check)269 270 // we start loading the dependents plugins only once271 // the current level is finished272 this.queue.resume()273}274 275function noop () {}276 277module.exports = {278 Plugin279}280 