From ab1649f1d528ddc29e9cbf429718dcf1d421b3b5 Mon Sep 17 00:00:00 2001 From: KaKa Date: Tue, 21 Jan 2025 15:06:21 +0800 Subject: [PATCH 1/2] feat: rewrite --- avvio.js | 386 ++++++++ boot.js | 607 ------------ examples/example.js | 27 +- lib/create-promise.js | 45 - lib/debug.js | 19 - lib/errors.js | 42 +- lib/execute-with-thenable.js | 28 - lib/get-plugin-name.js | 34 - lib/is-bundled-or-typescript-plugin.js | 23 - lib/is-promise-like.js | 17 - lib/plugin.js | 301 ++---- lib/promise.js | 48 + lib/symbols.js | 42 +- lib/thenify.js | 60 -- lib/time-tree.js | 200 ---- lib/utils.js | 58 ++ lib/validate-plugin.js | 26 - lib/workers.js | 57 ++ package.json | 2 +- test/after-and-ready.test.js | 864 ------------------ test/after-pass-through.test.js | 33 - test/after-self-promise.test.js | 20 - test/after-throw.test.js | 24 - test/after-use-after.test.js | 90 -- test/async-await.test.js | 323 ------- test/avvio-on-close.test.js | 85 ++ test/avvio-ready.test.js | 83 ++ test/await-after.test.js | 448 --------- test/await-self.test.js | 31 - test/await-use.test.js | 294 ------ test/basic.test.js | 467 ++-------- test/callbacks.test.js | 116 --- test/catch-override-exception.test.js | 26 - test/chainable.test.js | 67 -- test/close.test.js | 544 ----------- test/errors.test.js | 26 - test/esm.mjs | 12 - test/esm.test.js | 14 - test/events-listeners.test.js | 23 - test/expose.test.js | 80 -- test/fixtures/dummy.txt | 1 - test/fixtures/esm.mjs | 3 - test/fixtures/plugin-no-next.js | 5 - test/gh-issues/bug-205.test.js | 16 - test/inheritance.test.js | 15 + test/lib/create-promise.test.js | 55 -- test/lib/execute-with-thenable.test.js | 82 -- test/lib/get-plugin-name.test.js | 67 -- .../is-bundled-or-typescript-plugin.test.js | 20 - test/lib/is-promise-like.test.js | 20 - test/lib/thenify.test.js | 123 --- test/lib/time-tree.test.js | 391 -------- test/lib/validate-plugin.test.js | 19 - test/load-plugin.test.js | 132 --- test/no-done.test.js | 19 - test/on-ready-timeout-await.test.js | 33 - test/override.test.js | 385 -------- test/plugin-loaded-so-far.test.js | 84 -- test/plugin-name.test.js | 69 -- test/plugin-timeout-await.test.js | 33 - test/plugin-timeout.test.js | 218 ----- test/plugin.test.js | 325 +++++++ test/pretty-print.test.js | 75 -- test/reentrant.test.js | 126 --- test/to-json.test.js | 151 --- test/twice-done.test.js | 23 - test/types/index.ts | 412 --------- test/types/tsconfig.json | 9 - 68 files changed, 1247 insertions(+), 7356 deletions(-) create mode 100644 avvio.js delete mode 100644 boot.js delete mode 100644 lib/create-promise.js delete mode 100644 lib/debug.js delete mode 100644 lib/execute-with-thenable.js delete mode 100644 lib/get-plugin-name.js delete mode 100644 lib/is-bundled-or-typescript-plugin.js delete mode 100644 lib/is-promise-like.js create mode 100644 lib/promise.js delete mode 100644 lib/thenify.js delete mode 100644 lib/time-tree.js create mode 100644 lib/utils.js delete mode 100644 lib/validate-plugin.js create mode 100644 lib/workers.js delete mode 100644 test/after-and-ready.test.js delete mode 100644 test/after-pass-through.test.js delete mode 100644 test/after-self-promise.test.js delete mode 100644 test/after-throw.test.js delete mode 100644 test/after-use-after.test.js delete mode 100644 test/async-await.test.js create mode 100644 test/avvio-on-close.test.js create mode 100644 test/avvio-ready.test.js delete mode 100644 test/await-after.test.js delete mode 100644 test/await-self.test.js delete mode 100644 test/await-use.test.js delete mode 100644 test/callbacks.test.js delete mode 100644 test/catch-override-exception.test.js delete mode 100644 test/chainable.test.js delete mode 100644 test/close.test.js delete mode 100644 test/errors.test.js delete mode 100644 test/esm.mjs delete mode 100644 test/esm.test.js delete mode 100644 test/events-listeners.test.js delete mode 100644 test/expose.test.js delete mode 100644 test/fixtures/dummy.txt delete mode 100644 test/fixtures/esm.mjs delete mode 100644 test/fixtures/plugin-no-next.js delete mode 100644 test/gh-issues/bug-205.test.js create mode 100644 test/inheritance.test.js delete mode 100644 test/lib/create-promise.test.js delete mode 100644 test/lib/execute-with-thenable.test.js delete mode 100644 test/lib/get-plugin-name.test.js delete mode 100644 test/lib/is-bundled-or-typescript-plugin.test.js delete mode 100644 test/lib/is-promise-like.test.js delete mode 100644 test/lib/thenify.test.js delete mode 100644 test/lib/time-tree.test.js delete mode 100644 test/lib/validate-plugin.test.js delete mode 100644 test/load-plugin.test.js delete mode 100644 test/no-done.test.js delete mode 100644 test/on-ready-timeout-await.test.js delete mode 100644 test/override.test.js delete mode 100644 test/plugin-loaded-so-far.test.js delete mode 100644 test/plugin-name.test.js delete mode 100644 test/plugin-timeout-await.test.js delete mode 100644 test/plugin-timeout.test.js create mode 100644 test/plugin.test.js delete mode 100644 test/pretty-print.test.js delete mode 100644 test/reentrant.test.js delete mode 100644 test/to-json.test.js delete mode 100644 test/twice-done.test.js delete mode 100644 test/types/index.ts delete mode 100644 test/types/tsconfig.json diff --git a/avvio.js b/avvio.js new file mode 100644 index 0000000..86a5fe8 --- /dev/null +++ b/avvio.js @@ -0,0 +1,386 @@ +'use strict' + +const { Plugin } = require('./lib/plugin') +const { isPromiseLike, withResolvers } = require('./lib/promise') +const { kContext, kOptions, kExpose, kAvvio, kPluginRoot, kAddPlugin, kStart, kPluginQueue, kError, kLoadPlugin, kLoadPluginNext, kReadyQueue, kCloseQueue, kOnCloseFunction, kWrappedThen } = require('./lib/symbols') +const { resolveBundledFunction, noop } = require('./lib/utils') +const EventEmitter = require('node:events').EventEmitter +const inherits = require('node:util').inherits +const fastq = require('fastq') +const { readyWorker, closeWorker } = require('./lib/workers') +const { AVV_ERR_CALLBACK_NOT_FN } = require('./lib/errors') + +/** + * + * @signature `Avvio()` + * @signature `Avvio(done)` + * @signature `Avvio(instance, done)` + * @signature `Avvio(instance, options, done)` + * @param {object} [instance] The instance to be exposed with Avvio methods. + * @param {*} [options] Options to change the behavior. + * @param {Function} [done] Function that called when ready. + * @returns {Avvio} + * + * @example Boot without any params + * const avvio = Avvio() + * avvio.ready() + * + * @example Boot with instance + * const server = {} + * const avvio = Avvio(server) + * avvio.ready() + * + * @example Boot with callback + * const avvio = Avvio(function() { + * console.log('ready') + * }) + */ +function Avvio (instance, options, done) { + // supports multiple signatures + // Avvio(done) + if (typeof instance === 'function' && arguments.length === 1) { + done = instance + options = {} + instance = null + } + + // Avvio(instance, done) + if (typeof options === 'function') { + done = options + options = {} + } + + options ??= {} + const o = { + autostart: options.autostart !== false, + timeout: Number(options.timeout) || 0, + expose: options.expose ?? {} + } + + // allows to use without new + if (!new.target) { + return new Avvio(instance, o, done) + } + + this[kContext] = instance ?? this + this[kOptions] = options + + // override instance when it is suppied + if (instance) { + this[kExpose]() + } + + // prevent memory leak warning + this.setMaxListeners(0) + + if (done) { + this.once('start', done) + } + + this[kReadyQueue] = fastq(this, readyWorker, 1) + this[kReadyQueue].pause() + this[kReadyQueue].drain = () => { + this.emit('start') + // prevent emit multiple start event + this[kReadyQueue].drain = noop + } + + this[kCloseQueue] = fastq(this, closeWorker, 1) + this[kCloseQueue].pause() + this[kCloseQueue].drain = () => { + this.emit('close') + // prevent emit multiple close event + this[kCloseQueue].drain = noop + } + + // status + this.started = false // true when called start + this.booted = false // true when ready + + this[kPluginQueue] = [] + this[kStart] = null + this[kError] = null + + const self = this + this[kPluginRoot] = new Plugin( + fastq(this, this[kLoadPluginNext], 1), + function root (_, options, done) { + self[kStart] = done + options.autostart && self.start() + }, + options, + 0 + ) + + this[kLoadPlugin](this[kPluginRoot], (error) => { + try { + this.emit('preReady') + this[kPluginRoot] = null + } catch (preReadyError) { + error = error ?? this[kError] ?? preReadyError + } + + if (error) { + this[kError] = error + if (this[kReadyQueue].length() === 0) { + throw error + } + } else { + this.booted = true + } + this[kReadyQueue].resume() + }) +} + +inherits(Avvio, EventEmitter) + +Avvio.prototype.start = function start () { + this.started = true + + process.nextTick(this[kStart]) + + // allows method chaining + return this +} + +Avvio.prototype.ready = function ready (fn) { + if (fn) { + if (typeof fn !== 'function') { + throw AVV_ERR_CALLBACK_NOT_FN('ready', typeof fn) + } + this[kReadyQueue].push(fn) + queueMicrotask(this.start.bind(this)) + return this + } else { + const promise = withResolvers() + const lastContext = this[kPluginQueue][0].context + this[kReadyQueue].push(function ready (error, context, done) { + if (error) { + promise.reject(error) + } else { + promise.resolve(lastContext) + } + process.nextTick(done) + }) + queueMicrotask(this.start.bind(this)) + return promise.promise + } +} + +/** + * onClose registered in reverse order. + * + * @param {Function} fn + * @returns + */ +Avvio.prototype.onClose = function onClose (fn) { + if (typeof fn !== 'function') { + throw AVV_ERR_CALLBACK_NOT_FN('onClose', typeof fn) + } + + // used to distinguish between onClose and close + fn[kOnCloseFunction] = true + this[kCloseQueue].unshift(fn, (error) => { error && (this[kError] = error) }) + + return this +} + +/** + * close registered in sequantial order + * + * @param {Function} fn + * @returns + */ +Avvio.prototype.close = function close (fn) { + const resolvers = withResolvers() + + if (fn) { + if (typeof fn !== 'function') { + throw AVV_ERR_CALLBACK_NOT_FN('close', typeof fn) + } + } else { + fn = function close (error) { + if (error) { + resolvers.reject(error) + } else { + resolvers.resolve() + } + } + } + + // we need to start and fininalize before closing + this.ready(() => { + this.emit('preClose') + this[kError] = null + this[kCloseQueue].push(fn) + process.nextTick(this[kCloseQueue].resume.bind(this[kCloseQueue])) + }) + + return resolvers.promise +} + +Avvio.prototype.use = function use (plugin, options) { + this[kAddPlugin](plugin, options) + // allows method chaining + return this +} + +Avvio.prototype.override = function (context, fn, options) { + return context +} + +Object.defineProperties(Avvio.prototype, { + then: { + get () { + // when instance is ready, there is nothing + // to await. + if (this.booted) { + return + } + + // to prevent recursion of promise resolving + // due to resolve(this[kContext]), we need + // an indicator to break the recursion. + if (this[kWrappedThen]) { + this[kWrappedThen] = false + return + } + + const plugin = this[kPluginQueue][0] + const needToStart = !this.started && !this.booted + + // if the root plugin is not loaded, resume + if (needToStart) { + process.nextTick(() => { + this[kPluginRoot].queue.resume() + }) + } + + let promise = null + + if (!plugin) { + // when no plugin is registered, + // immediately resolve + promise = Promise.resolve(this[kContext]) + } else { + // wait until the plugin is fully loaded. + promise = plugin.promise() + } + + return (resolve, reject) => promise.then(() => { + this[kWrappedThen] = true + return resolve(this[kContext]) + }, reject) + } + } +}) + +/** + * Used to identify the Avvio instance + * and skip some logic when appropriate + */ +Avvio.prototype[kAvvio] = true + +Avvio.prototype[kExpose] = function expose () { + const self = this + const instance = this[kContext] + const { + use: useKey = 'use' + } = this[kOptions].expose + + if (instance[useKey]) { + throw Error() + } + instance[useKey] = function (fn, options) { + self.use(fn, options) + return this + } + + instance[kAvvio] = true +} + +Avvio.prototype[kAddPlugin] = function (fn, options) { + fn = resolveBundledFunction(fn) + Plugin.validate(fn) + + options = options ?? {} + + if (this.booted) { + throw Error() + } + + const parent = this[kPluginQueue][0] + + let timeout = this[kOptions].timeout + + if (!parent.loaded && parent.timeout > 0) { + const delta = Date.now() - parent.startTime + // decrease the timeout by 3ms to ensure the child + // timeout is triggered earlier than parent + timeout = parent.timeout - (delta + 3) + } + + const plugin = new Plugin( + fastq(this, this[kLoadPluginNext], 1), + fn, + options, + timeout + ) + + if (parent.loaded) { + throw Error(plugin.name, parent.name) + } + + parent.enqueue(plugin, (error) => { error && (this[kError] = error) }) + + return plugin +} + +Avvio.prototype[kLoadPlugin] = function (plugin, callback) { + const self = this + if (isPromiseLike(plugin.fn)) { + plugin.fn.then((fn) => { + fn = resolveBundledFunction(fn) + plugin.fn = fn + this[kLoadPlugin](plugin, callback) + }, callback) + return + } + + // prev added plugin + const prev = self[kPluginQueue][0] + self[kPluginQueue].unshift(plugin) + + let context = prev?.context ?? self[kContext] + try { + context = self.override(context, plugin.fn, plugin.options) + } catch (overrideError) { + return executeCallback(overrideError) + } + + plugin.execute(context, executeCallback) + + function executeCallback (error) { + plugin.finish(error, (error) => { + self[kPluginQueue].shift() + callback(error) + }) + } +} + +Avvio.prototype[kLoadPluginNext] = function (plugin, callback) { + process.nextTick(this[kLoadPlugin].bind(this), plugin, callback) +} + +/** + * supports the following import + * 1. const Avvio = require('avvio') + * 2. const { Avvio } = require('avvio') + * 3. const { default: Avvio } = require('avvio') + * 4. import Avvio from 'avvio' + * 5. import { Avvio } from 'avvio' + * 6. import { default as Avvio } from 'avvio' + */ +module.exports = Avvio +module.exports.default = Avvio +module.exports.Avvio = Avvio diff --git a/boot.js b/boot.js deleted file mode 100644 index 7897b65..0000000 --- a/boot.js +++ /dev/null @@ -1,607 +0,0 @@ -'use strict' - -const fastq = require('fastq') -const EE = require('node:events').EventEmitter -const inherits = require('node:util').inherits -const { - AVV_ERR_EXPOSE_ALREADY_DEFINED, - AVV_ERR_CALLBACK_NOT_FN, - AVV_ERR_ROOT_PLG_BOOTED, - AVV_ERR_READY_TIMEOUT, - AVV_ERR_ATTRIBUTE_ALREADY_DEFINED -} = require('./lib/errors') -const { - kAvvio, - kIsOnCloseHandler -} = require('./lib/symbols') -const { TimeTree } = require('./lib/time-tree') -const { Plugin } = require('./lib/plugin') -const { debug } = require('./lib/debug') -const { validatePlugin } = require('./lib/validate-plugin') -const { isBundledOrTypescriptPlugin } = require('./lib/is-bundled-or-typescript-plugin') -const { isPromiseLike } = require('./lib/is-promise-like') -const { thenify } = require('./lib/thenify') -const { executeWithThenable } = require('./lib/execute-with-thenable') - -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 || {} - opts.autostart = opts.autostart !== false - opts.timeout = Number(opts.timeout) || 0 - opts.expose = opts.expose || {} - - if (!new.target) { - return new Boot(server, opts, done) - } - - this._server = server || this - this._opts = opts - - if (server) { - this._expose() - } - - /** - * @type {Array} - */ - this._current = [] - - this._error = null - - this._lastUsed = null - - 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 close only once - this._closeQ.drain = noop - } - - this._doStart = null - - const instance = this - this._root = new Plugin(fastq(this, this._loadPluginNextTick, 1), function root (server, opts, done) { - instance._doStart = done - opts.autostart && instance.start() - }, opts, false, 0) - - this._trackPluginLoading(this._root) - - this._loadPlugin(this._root, (err) => { - debug('root plugin ready') - try { - this.emit('preReady') - this._root = null - } 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() - }) -} - -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 -} - -Boot.prototype[kAvvio] = true - -// load a plugin -Boot.prototype.use = function (plugin, opts) { - this._lastUsed = this._addPlugin(plugin, opts, false) - return this -} - -Boot.prototype._loadRegistered = function () { - const plugin = this._current[0] - const weNeedToStart = !this.started && !this.booted - - // if the root plugin is not loaded, let's resume that - // so one can use after() before calling ready - if (weNeedToStart) { - process.nextTick(() => this._root.queue.resume()) - } - - if (!plugin) { - return Promise.resolve() - } - - return plugin.loadedSoFar() -} - -Object.defineProperty(Boot.prototype, 'then', { get: thenify }) - -Boot.prototype._addPlugin = function (pluginFn, opts, isAfter) { - if (isBundledOrTypescriptPlugin(pluginFn)) { - pluginFn = pluginFn.default - } - validatePlugin(pluginFn) - opts = opts || {} - - if (this.booted) { - throw new AVV_ERR_ROOT_PLG_BOOTED() - } - - // we always add plugins to load at the current element - const current = this._current[0] - - let timeout = this._opts.timeout - - if (!current.loaded && current.timeout > 0) { - const delta = Date.now() - current.startTime - // We need to decrease it by 3ms to make sure the internal timeout - // is triggered earlier than the parent - timeout = current.timeout - (delta + 3) - } - - const plugin = new Plugin(fastq(this, this._loadPluginNextTick, 1), pluginFn, opts, isAfter, timeout) - this._trackPluginLoading(plugin) - - if (current.loaded) { - throw new Error(plugin.name, current.name) - } - - // we add the plugin to be loaded at the end of the current queue - current.enqueue(plugin, (err) => { err && (this._error = err) }) - - return plugin -} - -Boot.prototype._expose = function _expose () { - const instance = this - const server = instance._server - const { - use: useKey = 'use', - after: afterKey = 'after', - ready: readyKey = 'ready', - onClose: onCloseKey = 'onClose', - close: closeKey = 'close' - } = this._opts.expose - - if (server[useKey]) { - throw new AVV_ERR_EXPOSE_ALREADY_DEFINED(useKey, 'use') - } - server[useKey] = function (fn, opts) { - instance.use(fn, opts) - return this - } - - if (server[afterKey]) { - throw new AVV_ERR_EXPOSE_ALREADY_DEFINED(afterKey, 'after') - } - server[afterKey] = function (func) { - if (typeof func !== 'function') { - return instance._loadRegistered() - } - instance.after(encapsulateThreeParam(func, this)) - return this - } - - if (server[readyKey]) { - throw new AVV_ERR_EXPOSE_ALREADY_DEFINED(readyKey, 'ready') - } - server[readyKey] = function (func) { - if (func && typeof func !== 'function') { - throw new AVV_ERR_CALLBACK_NOT_FN(readyKey, typeof func) - } - return instance.ready(func ? encapsulateThreeParam(func, this) : undefined) - } - - if (server[onCloseKey]) { - throw new AVV_ERR_EXPOSE_ALREADY_DEFINED(onCloseKey, 'onClose') - } - server[onCloseKey] = function (func) { - if (typeof func !== 'function') { - throw new AVV_ERR_CALLBACK_NOT_FN(onCloseKey, typeof func) - } - instance.onClose(encapsulateTwoParam(func, this)) - return this - } - - if (server[closeKey]) { - throw new AVV_ERR_EXPOSE_ALREADY_DEFINED(closeKey, 'close') - } - server[closeKey] = function (func) { - if (func && typeof func !== 'function') { - throw new AVV_ERR_CALLBACK_NOT_FN(closeKey, typeof func) - } - - if (func) { - instance.close(encapsulateThreeParam(func, this)) - return this - } - - // this is a Promise - return instance.close() - } - - if (server.then) { - throw new AVV_ERR_ATTRIBUTE_ALREADY_DEFINED('then') - } - Object.defineProperty(server, 'then', { get: thenify.bind(instance) }) - - server[kAvvio] = true -} - -Boot.prototype.after = function (func) { - if (!func) { - return this._loadRegistered() - } - - 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 AVV_ERR_CALLBACK_NOT_FN('onClose', typeof func) - } - - func[kIsOnCloseHandler] = true - this._closeQ.unshift(func, (err) => { err && (this._error = err) }) - - return this -} - -Boot.prototype.close = function (func) { - let promise - - if (func) { - if (typeof func !== 'function') { - throw new AVV_ERR_CALLBACK_NOT_FN('close', typeof func) - } - } 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 AVV_ERR_CALLBACK_NOT_FN('ready', typeof func) - } - this._readyQ.push(func) - queueMicrotask(this.start.bind(this)) - return - } - - return new Promise((resolve, reject) => { - this._readyQ.push(readyPromiseCB) - this.start() - - /** - * The `encapsulateThreeParam` let callback function - * bind to the right server instance. - * In promises we need to track the last server - * instance loaded, the first one in the _current queue. - */ - const relativeContext = this._current[0].server - - function readyPromiseCB (err, context, done) { - // the context is always binded to the root server - if (err) { - reject(err) - } else { - resolve(relativeContext) - } - process.nextTick(done) - } - }) -} - -/** - * @param {Plugin} plugin - * @returns {void} - */ -Boot.prototype._trackPluginLoading = function (plugin) { - const parentName = this._current[0]?.name || null - plugin.once('start', (serverName, funcName, time) => { - const nodeId = this.pluginTree.start(parentName || null, funcName, time) - plugin.once('loaded', (serverName, funcName, time) => { - this.pluginTree.stop(nodeId, time) - }) - }) -} - -Boot.prototype.prettyPrint = function () { - return this.pluginTree.prettyPrint() -} - -Boot.prototype.toJSON = function () { - return this.pluginTree.toJSON() -} - -/** - * @callback LoadPluginCallback - * @param {Error} [err] - */ - -/** - * Load a plugin - * - * @param {Plugin} plugin - * @param {LoadPluginCallback} callback - */ -Boot.prototype._loadPlugin = function (plugin, callback) { - const instance = this - if (isPromiseLike(plugin.func)) { - plugin.func.then((fn) => { - if (typeof fn.default === 'function') { - fn = fn.default - } - plugin.func = fn - this._loadPlugin(plugin, callback) - }, callback) - return - } - - const last = instance._current[0] - - // place the plugin at the top of _current - instance._current.unshift(plugin) - - if (instance._error && !plugin.isAfter) { - debug('skipping loading of plugin as instance errored and it is not an after', plugin.name) - process.nextTick(execCallback) - return - } - - let server = last?.server || instance._server - - if (!plugin.isAfter) { - // Skip override for after - try { - server = instance.override(server, plugin.func, plugin.options) - } catch (overrideErr) { - debug('override errored', plugin.name) - return execCallback(overrideErr) - } - } - - plugin.exec(server, execCallback) - - function execCallback (err) { - plugin.finish(err, (err) => { - instance._current.shift() - callback(err) - }) - } -} - -/** -* Delays plugin loading until the next tick to ensure any bound `_after` callbacks have a chance -* to run prior to executing the next plugin -*/ -Boot.prototype._loadPluginNextTick = function (plugin, callback) { - process.nextTick(this._loadPlugin.bind(this), plugin, callback) -} - -function noop () { } - -function callWithCbOrNextTick (func, cb) { - const context = this._server - const err = this._error - - // with this the error will appear just in the next after/ready callback - this._error = null - if (func.length === 0) { - this._error = err - executeWithThenable(func, [], cb) - } else if (func.length === 1) { - executeWithThenable(func, [err], cb) - } else { - if (this._opts.timeout === 0) { - const wrapCb = (err) => { - this._error = err - cb(this._error) - } - - if (func.length === 2) { - func(err, wrapCb) - } else { - func(err, context, wrapCb) - } - } else { - timeoutCall.call(this, func, err, context, cb) - } - } -} - -function timeoutCall (func, rootErr, context, cb) { - const name = func.unwrappedName ?? func.name - debug('setting up ready timeout', name, this._opts.timeout) - let timer = setTimeout(() => { - debug('timed out', name) - timer = null - const toutErr = new AVV_ERR_READY_TIMEOUT(name) - toutErr.fn = func - this._error = toutErr - cb(toutErr) - }, this._opts.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) { - const context = this._server - const isOnCloseHandler = func[kIsOnCloseHandler] - if (func.length === 0 || func.length === 1) { - let 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) { - let res - if (func.length === 0) { - res = func() - if (res?.then) { - res.then(function () { - process.nextTick(cb) - }, cb) - } else { - process.nextTick(cb) - } - } else if (func.length === 1) { - res = func(this) - - if (res?.then) { - res.then(function () { - process.nextTick(cb) - }, cb) - } else { - process.nextTick(cb) - } - } else { - func(this, cb) - } - } -} - -function encapsulateThreeParam (func, that) { - const wrapped = _encapsulateThreeParam.bind(that) - wrapped.unwrappedName = func.name - return wrapped - function _encapsulateThreeParam (err, cb) { - let res - if (!func) { - process.nextTick(cb) - } else if (func.length === 0) { - res = func() - if (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?.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 diff --git a/examples/example.js b/examples/example.js index cbba32b..8cf315c 100644 --- a/examples/example.js +++ b/examples/example.js @@ -26,21 +26,20 @@ avvio .use(b) setTimeout(cb, 42) }) - .after(function (err, cb) { - if (err) { - console.log('something bad happened') - console.log(err) - } + .then((avvio) => { console.log('after first and second') - cb() - }) - .use(duplicate, { count: 4 }) - .use(third) - .ready(function (err) { - if (err) { - throw err - } - console.log('application booted!') + avvio + .use(duplicate, { count: 4 }) + .use(third) + .ready(function (err) { + if (err) { + throw err + } + console.log('application booted!') + }) + }, (error) => { + console.log('something bad happened') + console.log(error) }) avvio.on('preReady', () => { diff --git a/lib/create-promise.js b/lib/create-promise.js deleted file mode 100644 index 4b10c0b..0000000 --- a/lib/create-promise.js +++ /dev/null @@ -1,45 +0,0 @@ -'use strict' - -/** - * @callback PromiseResolve - * @param {any|PromiseLike} value - * @returns {void} - */ - -/** - * @callback PromiseReject - * @param {any} reason - * @returns {void} - */ - -/** - * @typedef PromiseObject - * @property {Promise} promise - * @property {PromiseResolve} resolve - * @property {PromiseReject} reject - */ - -/** - * @returns {PromiseObject} - */ -function createPromise () { - /** - * @type {PromiseObject} - */ - const obj = { - resolve: null, - reject: null, - promise: null - } - - obj.promise = new Promise((resolve, reject) => { - obj.resolve = resolve - obj.reject = reject - }) - - return obj -} - -module.exports = { - createPromise -} diff --git a/lib/debug.js b/lib/debug.js deleted file mode 100644 index e7cdc6f..0000000 --- a/lib/debug.js +++ /dev/null @@ -1,19 +0,0 @@ -'use strict' - -const { debuglog } = require('node:util') - -/** - * @callback DebugLogger - * @param {string} msg - * @param {...unknown} param - * @returns {void} - */ - -/** - * @type {DebugLogger} - */ -const debug = debuglog('avvio') - -module.exports = { - debug -} diff --git a/lib/errors.js b/lib/errors.js index 9aa4c8a..7b3b05c 100644 --- a/lib/errors.js +++ b/lib/errors.js @@ -2,37 +2,11 @@ const { createError } = require('@fastify/error') -module.exports = { - AVV_ERR_EXPOSE_ALREADY_DEFINED: createError( - 'AVV_ERR_EXPOSE_ALREADY_DEFINED', - "'%s' is already defined, specify an expose option for '%s'" - ), - AVV_ERR_ATTRIBUTE_ALREADY_DEFINED: createError( - 'AVV_ERR_ATTRIBUTE_ALREADY_DEFINED', - "'%s' is already defined" - ), - AVV_ERR_CALLBACK_NOT_FN: createError( - 'AVV_ERR_CALLBACK_NOT_FN', - "Callback for '%s' hook is not a function. Received: '%s'" - ), - AVV_ERR_PLUGIN_NOT_VALID: createError( - 'AVV_ERR_PLUGIN_NOT_VALID', - "Plugin must be a function or a promise. Received: '%s'" - ), - AVV_ERR_ROOT_PLG_BOOTED: createError( - 'AVV_ERR_ROOT_PLG_BOOTED', - 'Root plugin has already booted' - ), - AVV_ERR_PARENT_PLG_LOADED: createError( - 'AVV_ERR_PARENT_PLG_LOADED', - "Impossible to load '%s' plugin because the parent '%s' was already loaded" - ), - AVV_ERR_READY_TIMEOUT: createError( - 'AVV_ERR_READY_TIMEOUT', - "Plugin did not start in time: '%s'. You may have forgotten to call 'done' function or to resolve a Promise" - ), - AVV_ERR_PLUGIN_EXEC_TIMEOUT: createError( - 'AVV_ERR_PLUGIN_EXEC_TIMEOUT', - "Plugin did not start in time: '%s'. You may have forgotten to call 'done' function or to resolve a Promise" - ) -} +module.exports.AVV_ERR_EXPOSE_ALREADY_DEFINED = createError( + 'AVV_ERR_EXPOSE_ALREADY_DEFINED', + "'%s' is already defined, specify an expose option for '%s'" +) +module.exports.AVV_ERR_CALLBACK_NOT_FN = createError( + 'AVV_ERR_CALLBACK_NOT_FN', + "Callback for '%s' hook is not a function. Received: '%s'" +) diff --git a/lib/execute-with-thenable.js b/lib/execute-with-thenable.js deleted file mode 100644 index 6e2c809..0000000 --- a/lib/execute-with-thenable.js +++ /dev/null @@ -1,28 +0,0 @@ -'use strict' -const { isPromiseLike } = require('./is-promise-like') -const { kAvvio } = require('./symbols') - -/** - * @callback ExecuteWithThenableCallback - * @param {Error} error - * @returns {void} - */ - -/** - * @param {Function} func - * @param {Array} args - * @param {ExecuteWithThenableCallback} [callback] - */ -function executeWithThenable (func, args, callback) { - const result = func.apply(func, args) - if (isPromiseLike(result) && !result[kAvvio]) { - // process promise but not avvio mock thenable - result.then(() => process.nextTick(callback), (error) => process.nextTick(callback, error)) - } else if (callback) { - process.nextTick(callback) - } -} - -module.exports = { - executeWithThenable -} diff --git a/lib/get-plugin-name.js b/lib/get-plugin-name.js deleted file mode 100644 index dad82a9..0000000 --- a/lib/get-plugin-name.js +++ /dev/null @@ -1,34 +0,0 @@ -'use strict' - -// this symbol is assigned by fastify-plugin -const { kPluginMeta } = require('./symbols') - -/** - * @param {function} plugin - * @param {object} [options] - * @param {string} [options.name] - * @returns {string} - */ -function getPluginName (plugin, options) { - // use explicit function metadata if set - if (plugin[kPluginMeta]?.name) { - return plugin[kPluginMeta].name - } - - // use explicit name option if set - if (options?.name) { - return options.name - } - - // determine from the function - if (plugin.name) { - return plugin.name - } else { - // takes the first two lines of the function if nothing else works - return plugin.toString().split('\n').slice(0, 2).map(s => s.trim()).join(' -- ') - } -} - -module.exports = { - getPluginName -} diff --git a/lib/is-bundled-or-typescript-plugin.js b/lib/is-bundled-or-typescript-plugin.js deleted file mode 100644 index 42003dd..0000000 --- a/lib/is-bundled-or-typescript-plugin.js +++ /dev/null @@ -1,23 +0,0 @@ -'use strict' - -/** - * bundled or typescript plugin - * @typedef {object} BundledOrTypescriptPlugin - * @property {function} default - */ - -/** - * @param {any} maybeBundledOrTypescriptPlugin - * @returns {plugin is BundledOrTypescriptPlugin} - */ -function isBundledOrTypescriptPlugin (maybeBundledOrTypescriptPlugin) { - return ( - maybeBundledOrTypescriptPlugin !== null && - typeof maybeBundledOrTypescriptPlugin === 'object' && - typeof maybeBundledOrTypescriptPlugin.default === 'function' - ) -} - -module.exports = { - isBundledOrTypescriptPlugin -} diff --git a/lib/is-promise-like.js b/lib/is-promise-like.js deleted file mode 100644 index 909f8db..0000000 --- a/lib/is-promise-like.js +++ /dev/null @@ -1,17 +0,0 @@ -'use strict' - -/** - * @param {any} maybePromiseLike - * @returns {maybePromiseLike is PromiseLike} - */ -function isPromiseLike (maybePromiseLike) { - return ( - maybePromiseLike !== null && - typeof maybePromiseLike === 'object' && - typeof maybePromiseLike.then === 'function' - ) -} - -module.exports = { - isPromiseLike -} diff --git a/lib/plugin.js b/lib/plugin.js index a3f248d..8dccfeb 100644 --- a/lib/plugin.js +++ b/lib/plugin.js @@ -1,279 +1,166 @@ 'use strict' -const { EventEmitter } = require('node:events') -const { inherits } = require('node:util') -const { debug } = require('./debug') -const { createPromise } = require('./create-promise') -const { AVV_ERR_PLUGIN_EXEC_TIMEOUT } = require('./errors') -const { getPluginName } = require('./get-plugin-name') -const { isPromiseLike } = require('./is-promise-like') +const EventEmitter = require('node:events').EventEmitter +const inherits = require('node:util').inherits +const { withResolvers } = require('./promise') +const { resolvePluginName, noop, executeFn } = require('./utils') -/** - * @param {*} queue - * @param {*} func - * @param {*} options - * @param {boolean} isAfter - * @param {number} [timeout] - */ -function Plugin (queue, func, options, isAfter, timeout) { +// Plugin is used to manage encapsulation levels + +function Plugin (queue, fn, options, timeout) { this.queue = queue - this.func = func + this.fn = fn this.options = options - - /** - * @type {boolean} - */ - this.isAfter = isAfter - /** - * @type {number} - */ this.timeout = timeout - /** - * @type {boolean} - */ - this.started = false - /** - * @type {string} - */ - this.name = getPluginName(func, options) - this.queue.pause() - /** - * @type {Error|null} - */ - this._error = null - /** - * @type {boolean} - */ - this.loaded = false - - this._promise = null - + // status + this.context = null + this.contextName = null + this.name = resolvePluginName(fn, options) + this.error = null + this.started = false this.startTime = null + this.loaded = false + this.resolvers = null } inherits(Plugin, EventEmitter) -/** - * @callback ExecCallback - * @param {Error|null} execErr - * @returns - */ - -/** - * - * @param {*} server - * @param {ExecCallback} callback - * @returns - */ -Plugin.prototype.exec = function (server, callback) { - debug('exec', this.name) +// static function +Plugin.validate = function validate (maybePlugin) { + if (!(maybePlugin && (typeof maybePlugin === 'function' || typeof maybePlugin.then === 'function'))) { + if (Array.isArray(maybePlugin)) { + throw Error() + } else if (maybePlugin === null) { + throw Error() + } else { + throw Error() + } + } +} - this.server = server - const func = this.func - const name = this.name +Plugin.prototype.execute = function execute (context, callback) { + this.context = context + this.contextName = context ? context.name : null + // resolve function options + this.options = typeof this.options === 'function' ? this.options(context) : this.options + + const { + fn, + name, + timeout + } = this let completed = false + let timerId = null - this.options = typeof this.options === 'function' ? this.options(this.server) : this.options - - let timer = null - - /** - * @param {Error} [execErr] - */ - const done = (execErr) => { + const done = (executeError) => { + // prevent execute twice if (completed) { - debug('loading complete', name) return } - this._error = execErr - - if (execErr) { - debug('exec errored', name) - } else { - debug('exec completed', name) - } + this.error = executeError completed = true - if (timer) { - clearTimeout(timer) + if (timerId) { + clearTimeout(timerId) } - callback(execErr) + callback(executeError) } - if (this.timeout > 0) { - debug('setting up timeout', name, this.timeout) - timer = setTimeout(function () { - debug('timed out', name) - timer = null - const readyTimeoutErr = new AVV_ERR_PLUGIN_EXEC_TIMEOUT(name) - // TODO Remove reference to function - readyTimeoutErr.fn = func - done(readyTimeoutErr) - }, this.timeout) + if (timeout > 0) { + timerId = setTimeout(function () { + timerId = null + done(Error()) + }, timeout) } + // execute the fn this.started = true this.startTime = Date.now() - this.emit('start', this.server ? this.server.name : null, this.name, Date.now()) - - const maybePromiseLike = func(this.server, this.options, done) - - if (isPromiseLike(maybePromiseLike)) { - debug('exec: resolving promise', name) - - maybePromiseLike.then( - () => process.nextTick(done), - (e) => process.nextTick(done, e)) - } else if (func.length < 3) { - done() - } + this.emit('start', this.contextName, name, this.startTime) + executeFn(fn, [context, this.options], done) } -/** - * @returns {Promise} - */ -Plugin.prototype.loadedSoFar = function () { - debug('loadedSoFar', this.name) - - if (this.loaded) { - return Promise.resolve() - } - - const setup = () => { - this.server.after((afterErr, callback) => { - this._error = afterErr - this.queue.pause() - - if (this._promise) { - if (afterErr) { - debug('rejecting promise', this.name, afterErr) - this._promise.reject(afterErr) - } else { - debug('resolving promise', this.name) - this._promise.resolve() - } - this._promise = null - } - - process.nextTick(callback, afterErr) - }) - this.queue.resume() - } - - let res - - if (!this._promise) { - this._promise = createPromise() - res = this._promise.promise - - if (!this.server) { - this.on('start', setup) - } else { - setup() - } - } else { - res = Promise.resolve() - } - - return res -} - -/** - * @callback EnqueueCallback - * @param {Error|null} enqueueErr - * @param {Plugin} result - */ - -/** - * - * @param {Plugin} plugin - * @param {EnqueueCallback} callback - */ -Plugin.prototype.enqueue = function (plugin, callback) { - debug('enqueue', this.name, plugin.name) - - this.emit('enqueue', this.server ? this.server.name : null, this.name, Date.now()) +Plugin.prototype.enqueue = function enqueue (plugin, callback) { + this.emit('enqueue', this.contextName, this.name, Date.now()) this.queue.push(plugin, callback) } -/** - * @callback FinishCallback - * @param {Error|null} finishErr - * @returns - */ -/** - * - * @param {Error|null} err - * @param {FinishCallback} callback - * @returns - */ -Plugin.prototype.finish = function (err, callback) { - debug('finish', this.name, err) - +Plugin.prototype.finish = function finish (error, callback) { const done = () => { if (this.loaded) { return } - debug('loaded', this.name) - this.emit('loaded', this.server ? this.server.name : null, this.name, Date.now()) + this.emit('loaded', this.contextName, this.name, Date.now()) this.loaded = true - callback(err) + callback(error) } - if (err) { - if (this._promise) { - this._promise.reject(err) - this._promise = null - } + if (error) { + this.reject(error) done() return } const check = () => { - debug('check', this.name, this.queue.length(), this.queue.running(), this._promise) if (this.queue.length() === 0 && this.queue.running() === 0) { - if (this._promise) { - const wrap = () => { - debug('wrap') + if (this.resolvers) { + function queue () { queueMicrotask(check) } - this._promise.resolve() - this._promise.promise.then(wrap, wrap) - this._promise = null + this.resolve().then(queue, queue) } else { done() } } else { - debug('delayed', this.name) - // finish when the queue of nested plugins to load is empty this.queue.drain = () => { - debug('drain', this.name) this.queue.drain = noop - - // we defer the check, as a safety net for things - // that might be scheduled in the loading callback queueMicrotask(check) } } } queueMicrotask(check) + this.queue.resume() +} + +Plugin.prototype.promise = function promise () { + if (this.loaded) { + return Promise.resolve() + } - // we start loading the dependents plugins only once - // the current level is finished this.queue.resume() + + if (!this.resolvers) { + this.resolvers = withResolvers() + } + + return this.resolvers.promise } -function noop () {} +Plugin.prototype.resolve = function resolve () { + if (this.resolvers) { + const resolvers = this.resolvers + resolvers.resolve() + this.resolvers = null + return resolvers.promise + } +} -module.exports = { - Plugin +Plugin.prototype.reject = function reject (error) { + if (this.resolvers) { + const resolvers = this.resolvers + resolvers.reject(error) + this.resolvers = null + return resolvers.promise + } } + +module.exports.Plugin = Plugin diff --git a/lib/promise.js b/lib/promise.js new file mode 100644 index 0000000..7baf784 --- /dev/null +++ b/lib/promise.js @@ -0,0 +1,48 @@ +'use strict' + +function withResolvers () { + // use native if avaiable + if (Promise.withResolvers) return Promise.withResolvers() + + const resolvers = { + resolve: null, + reject: null, + promise: null + } + + resolvers.promise = new Promise((resolve, reject) => { + resolvers.resolve = resolve + resolvers.reject = reject + }) + + return resolvers +} +module.exports.withResolvers = withResolvers + +function _try (fn, ...args) { + // use native if avaiable + if (Promise.try) return Promise.try(fn, ...args) + + const resolvers = withResolvers() + + try { + const result = fn.call(fn, ...args) + resolvers.resolve(result) + } catch (error) { + resolvers.reject(error) + } + + return resolvers.promise +} + +module.exports.try = _try + +function isPromiseLike (maybePromiseLike) { + return ( + maybePromiseLike !== null && + typeof maybePromiseLike === 'object' && + typeof maybePromiseLike.then === 'function' + ) +} + +module.exports.isPromiseLike = isPromiseLike diff --git a/lib/symbols.js b/lib/symbols.js index 62cdf38..47d0494 100644 --- a/lib/symbols.js +++ b/lib/symbols.js @@ -1,26 +1,26 @@ 'use strict' -// Internal Symbols -const kAvvio = Symbol('avvio.Boot') -const kIsOnCloseHandler = Symbol('isOnCloseHandler') -const kThenifyDoNotWrap = Symbol('avvio.ThenifyDoNotWrap') -const kUntrackNode = Symbol('avvio.TimeTree.untrackNode') -const kTrackNode = Symbol('avvio.TimeTree.trackNode') -const kGetParent = Symbol('avvio.TimeTree.getParent') -const kGetNode = Symbol('avvio.TimeTree.getNode') -const kAddNode = Symbol('avvio.TimeTree.addNode') +module.exports = { + // private properties + kAvvio: Symbol('Avvio#this'), + kError: Symbol('Avvio#error'), + kContext: Symbol('Avvio#context'), + kOptions: Symbol('Avvio#options'), + kPluginRoot: Symbol('Avvio#root'), + kPluginQueue: Symbol('Avvio#plugin-queue'), + kReadyQueue: Symbol('Avvio#ready-queue'), + kCloseQueue: Symbol('Avvio#close-queue'), + kResolvers: Symbol('Avvio#resolvers'), + kWrappedThen: Symbol('Avvio#wrapped-then'), + // private functions + kExpose: Symbol('Avvio#expose'), + kAddPlugin: Symbol('Avvio#add-plugin'), + kLoadPlugin: Symbol('Avvio#load-plugin'), + kLoadPluginNext: Symbol('Avvio#load-plugin-next'), + kStart: Symbol('Avvio#start'), -// Public Symbols -const kPluginMeta = Symbol.for('plugin-meta') + kOnCloseFunction: Symbol('Function#on-close'), -module.exports = { - kAvvio, - kIsOnCloseHandler, - kThenifyDoNotWrap, - kUntrackNode, - kTrackNode, - kGetParent, - kGetNode, - kAddNode, - kPluginMeta + // public properties + kPluginMeta: Symbol.for('plugin-meta') } diff --git a/lib/thenify.js b/lib/thenify.js deleted file mode 100644 index e1b614d..0000000 --- a/lib/thenify.js +++ /dev/null @@ -1,60 +0,0 @@ -'use strict' - -const { debug } = require('./debug') -const { kThenifyDoNotWrap } = require('./symbols') - -/** - * @callback PromiseConstructorLikeResolve - * @param {any} value - * @returns {void} - */ - -/** - * @callback PromiseConstructorLikeReject - * @param {reason} error - * @returns {void} - */ - -/** - * @callback PromiseConstructorLike - * @param {PromiseConstructorLikeResolve} resolve - * @param {PromiseConstructorLikeReject} reject - * @returns {void} - */ - -/** - * @returns {PromiseConstructorLike} - */ -function thenify () { - // If the instance is ready, then there is - // nothing to await. This is true during - // await server.ready() as ready() resolves - // with the server, end we will end up here - // because of automatic promise chaining. - if (this.booted) { - debug('thenify returning undefined because we are already booted') - return - } - - // Calling resolve(this._server) would fetch the then - // property on the server, which will lead it here. - // If we do not break the recursion, we will loop - // forever. - if (this[kThenifyDoNotWrap]) { - this[kThenifyDoNotWrap] = false - return - } - - debug('thenify') - return (resolve, reject) => { - const p = this._loadRegistered() - return p.then(() => { - this[kThenifyDoNotWrap] = true - return resolve(this._server) - }, reject) - } -} - -module.exports = { - thenify -} diff --git a/lib/time-tree.js b/lib/time-tree.js deleted file mode 100644 index 5f02d68..0000000 --- a/lib/time-tree.js +++ /dev/null @@ -1,200 +0,0 @@ -'use strict' - -const { - kUntrackNode, - kTrackNode, - kGetParent, - kGetNode, - kAddNode -} = require('./symbols') - -/** - * Node of the TimeTree - * @typedef {object} TimeTreeNode - * @property {string} id - * @property {string|null} parent - * @property {string} label - * @property {Array} nodes - * @property {number} start - * @property {number|undefined} stop - * @property {number|undefined} diff - */ - -class TimeTree { - constructor () { - /** - * @type {TimeTreeNode|null} root - * @public - */ - this.root = null - - /** - * @type {Map} tableId - * @public - */ - this.tableId = new Map() - - /** - * @type {Map>} tableLabel - * @public - */ - this.tableLabel = new Map() - } - - /** - * @param {TimeTreeNode} node - */ - [kTrackNode] (node) { - this.tableId.set(node.id, node) - if (this.tableLabel.has(node.label)) { - this.tableLabel.get(node.label).push(node) - } else { - this.tableLabel.set(node.label, [node]) - } - } - - /** - * @param {TimeTreeNode} node - */ - [kUntrackNode] (node) { - this.tableId.delete(node.id) - - const labelNode = this.tableLabel.get(node.label) - labelNode.pop() - - if (labelNode.length === 0) { - this.tableLabel.delete(node.label) - } - } - - /** - * @param {string} parent - * @returns {TimeTreeNode} - */ - [kGetParent] (parent) { - if (parent === null) { - return null - } else if (this.tableLabel.has(parent)) { - const parentNode = this.tableLabel.get(parent) - return parentNode[parentNode.length - 1] - } else { - return null - } - } - - /** - * - * @param {string} nodeId - * @returns {TimeTreeNode} - */ - [kGetNode] (nodeId) { - return this.tableId.get(nodeId) - } - - /** - * @param {string} parent - * @param {string} label - * @param {number} start - * @returns {TimeTreeNode["id"]} - */ - [kAddNode] (parent, label, start) { - const parentNode = this[kGetParent](parent) - const isRoot = parentNode === null - - if (isRoot) { - this.root = { - parent: null, - id: 'root', - label, - nodes: [], - start, - stop: null, - diff: -1 - } - this[kTrackNode](this.root) - return this.root.id - } - - const nodeId = `${label}-${Math.random()}` - /** - * @type {TimeTreeNode} - */ - const childNode = { - parent, - id: nodeId, - label, - nodes: [], - start, - stop: null, - diff: -1 - } - parentNode.nodes.push(childNode) - this[kTrackNode](childNode) - return nodeId - } - - /** - * @param {string} parent - * @param {string} label - * @param {number|undefined} start - * @returns {TimeTreeNode["id"]} - */ - start (parent, label, start = Date.now()) { - return this[kAddNode](parent, label, start) - } - - /** - * @param {string} nodeId - * @param {number|undefined} stop - */ - stop (nodeId, stop = Date.now()) { - const node = this[kGetNode](nodeId) - if (node) { - node.stop = stop - node.diff = (node.stop - node.start) || 0 - this[kUntrackNode](node) - } - } - - /** - * @returns {TimeTreeNode} - */ - toJSON () { - return Object.assign({}, this.root) - } - - /** - * @returns {string} - */ - prettyPrint () { - return prettyPrintTimeTree(this.toJSON()) - } -} - -/** - * @param {TimeTreeNode} obj - * @param {string|undefined} prefix - * @returns {string} - */ -function prettyPrintTimeTree (obj, prefix = '') { - let result = prefix - - const nodesCount = obj.nodes.length - const lastIndex = nodesCount - 1 - result += `${obj.label} ${obj.diff} ms\n` - - for (let i = 0; i < nodesCount; ++i) { - const node = obj.nodes[i] - const prefix_ = prefix + (i === lastIndex ? ' ' : '│ ') - - result += prefix - result += (i === lastIndex ? '└─' : '├─') - result += (node.nodes.length === 0 ? '─ ' : '┬ ') - result += prettyPrintTimeTree(node, prefix_).slice(prefix.length + 2) - } - return result -} - -module.exports = { - TimeTree -} diff --git a/lib/utils.js b/lib/utils.js new file mode 100644 index 0000000..9150dab --- /dev/null +++ b/lib/utils.js @@ -0,0 +1,58 @@ +'use strict' + +const { isPromiseLike } = require('./promise') +const { kPluginMeta } = require('./symbols') +const promise = require('./promise') + +module.exports.noop = function noop () { } + +module.exports.resolvePluginName = function resolvePluginName (plugin, options) { + // use explicit function metadata if available + if (plugin[kPluginMeta]?.name) { + return plugin[kPluginMeta].name + } + + // use explicit name option if available + if (options?.name) { + return options.name + } + + // determine from the function + if (plugin.name) { + return plugin.name + } else { + // takes the first two lines of the function if nothing else works + return plugin.toString().split('\n').slice(0, 2).map(s => s.trim()).join(' -- ') + } +} + +module.exports.resolveBundledFunction = function resolveBundledFunction (maybeBundled) { + if ( + maybeBundled !== null && + typeof maybeBundled === 'object' && + typeof maybeBundled.default === 'function' + ) { + return maybeBundled.default + } else { + return maybeBundled + } +} + +module.exports.executeFn = function executeFn (fn, args, callback) { + let thenable = null + const length = args.length + 1 + + if (fn.length < length) { + thenable = promise.try(fn, ...args) + } else { + const maybePromiseLike = fn.call(fn, ...args, callback) + isPromiseLike(maybePromiseLike) && (thenable = maybePromiseLike) + } + + if (isPromiseLike(thenable)) { + thenable.then( + () => process.nextTick(callback), + (error) => process.nextTick(callback, error) + ) + } +} diff --git a/lib/validate-plugin.js b/lib/validate-plugin.js deleted file mode 100644 index 374ee56..0000000 --- a/lib/validate-plugin.js +++ /dev/null @@ -1,26 +0,0 @@ -'use strict' - -const { AVV_ERR_PLUGIN_NOT_VALID } = require('./errors') - -/** - * @param {any} maybePlugin - * @throws {AVV_ERR_PLUGIN_NOT_VALID} - * - * @returns {asserts plugin is Function|PromiseLike} - */ -function validatePlugin (maybePlugin) { - // validate if plugin is a function or Promise - if (!(maybePlugin && (typeof maybePlugin === 'function' || typeof maybePlugin.then === 'function'))) { - if (Array.isArray(maybePlugin)) { - throw new AVV_ERR_PLUGIN_NOT_VALID('array') - } else if (maybePlugin === null) { - throw new AVV_ERR_PLUGIN_NOT_VALID('null') - } else { - throw new AVV_ERR_PLUGIN_NOT_VALID(typeof maybePlugin) - } - } -} - -module.exports = { - validatePlugin -} diff --git a/lib/workers.js b/lib/workers.js new file mode 100644 index 0000000..e9d0b69 --- /dev/null +++ b/lib/workers.js @@ -0,0 +1,57 @@ +'use strict' + +const { kContext, kError, kOptions, kOnCloseFunction } = require('./symbols') +const { executeFn } = require('./utils') + +/** + * + * @param {Function} fn ready function + * @param {Function} done indicate the current task is done + */ +module.exports.readyWorker = function readyWorker (fn, done) { + const context = this[kContext] + const error = this[kError] + + // prevent the error appear outside of the next ready callback + this[kError] = null + + if (this[kOptions].timeout === 0) { + executeFn(fn, [error, context], (error) => { + this[kError] = error + done(this[kError]) + }) + } else { + let timerId = setTimeout(() => { + timerId = null + this[kError] = Error() + done(this[kError]) + }, this[kOptions].timeout) + + executeFn(fn, [error, context], (error) => { + if (timerId) { + clearTimeout(timerId) + this[kError] = error + done(this[kError]) + } + }) + } +} + +/** + * + * @param {Function} fn + * @param {Function} done + */ +module.exports.closeWorker = function closeWorker (fn, done) { + const context = this[kContext] + const error = this[kError] + + // close worker is shared between, onClose and close + // the arguments accepted by two function is different + const isOnCloseFn = fn[kOnCloseFunction] + const args = isOnCloseFn + ? [context] + : [error, context] + + executeFn(fn, args, done) +} diff --git a/package.json b/package.json index f539c2f..b7dc154 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "avvio", "version": "9.1.0", "description": "Asynchronous bootstrapping of Node applications", - "main": "boot.js", + "main": "avvio.js", "type": "commonjs", "types": "index.d.ts", "scripts": { diff --git a/test/after-and-ready.test.js b/test/after-and-ready.test.js deleted file mode 100644 index ff3d730..0000000 --- a/test/after-and-ready.test.js +++ /dev/null @@ -1,864 +0,0 @@ -'use strict' - -const { test } = require('tap') -const boot = require('..') - -test('boot a plugin and then execute a call after that', (t) => { - t.plan(5) - - const app = boot() - let pluginLoaded = false - let afterCalled = false - - app.use(function (s, opts, done) { - t.notOk(afterCalled, 'after not called') - pluginLoaded = true - done() - }) - - app.after(function (err, cb) { - t.error(err) - t.ok(pluginLoaded, 'afterred!') - afterCalled = true - cb() - }) - - app.on('start', () => { - t.ok(afterCalled, 'after called') - t.ok(pluginLoaded, 'plugin loaded') - }) -}) - -test('after without a done callback', (t) => { - t.plan(5) - - const app = boot() - let pluginLoaded = false - let afterCalled = false - - app.use(function (s, opts, done) { - t.notOk(afterCalled, 'after not called') - pluginLoaded = true - done() - }) - - app.after(function (err) { - t.error(err) - t.ok(pluginLoaded, 'afterred!') - afterCalled = true - }) - - app.on('start', () => { - t.ok(afterCalled, 'after called') - t.ok(pluginLoaded, 'plugin loaded') - }) -}) - -test('verify when a afterred call happens', (t) => { - t.plan(3) - - const app = boot() - - app.use(function (s, opts, done) { - done() - }) - - app.after(function (err, cb) { - t.error(err) - t.pass('afterred finished') - cb() - }) - - app.on('start', () => { - t.pass('booted') - }) -}) - -test('internal after', (t) => { - t.plan(18) - - const app = boot() - let firstLoaded = false - let secondLoaded = false - let thirdLoaded = false - let afterCalled = false - - app.use(first) - app.use(third) - - function first (s, opts, done) { - t.notOk(firstLoaded, 'first is not loaded') - t.notOk(secondLoaded, 'second is not loaded') - t.notOk(thirdLoaded, 'third is not loaded') - firstLoaded = true - s.use(second) - s.after(function (err, cb) { - t.error(err) - t.notOk(afterCalled, 'after was not called') - afterCalled = true - cb() - }) - done() - } - - function second (s, opts, done) { - t.ok(firstLoaded, 'first is loaded') - t.notOk(secondLoaded, 'second is not loaded') - t.notOk(thirdLoaded, 'third is not loaded') - t.notOk(afterCalled, 'after was not called') - secondLoaded = true - done() - } - - function third (s, opts, done) { - t.ok(firstLoaded, 'first is loaded') - t.ok(secondLoaded, 'second is loaded') - t.ok(afterCalled, 'after was called') - t.notOk(thirdLoaded, 'third is not loaded') - thirdLoaded = true - done() - } - - app.on('start', () => { - t.ok(firstLoaded, 'first is loaded') - t.ok(secondLoaded, 'second is loaded') - t.ok(thirdLoaded, 'third is loaded') - t.ok(afterCalled, 'after was called') - t.pass('booted') - }) -}) - -test('ready adds at the end of the queue', (t) => { - t.plan(14) - - const app = boot() - let pluginLoaded = false - let afterCalled = false - let readyCalled = false - - app.ready(function (err, cb) { - t.error(err) - t.ok(pluginLoaded, 'after the plugin') - t.ok(afterCalled, 'after after') - readyCalled = true - process.nextTick(cb) - }) - - app.use(function (s, opts, done) { - t.notOk(afterCalled, 'after not called') - t.notOk(readyCalled, 'ready not called') - pluginLoaded = true - - app.ready(function (err) { - t.error(err) - t.ok(readyCalled, 'after the first ready') - t.ok(afterCalled, 'after the after callback') - }) - - done() - }) - - app.after(function (err, cb) { - t.error(err) - t.ok(pluginLoaded, 'executing after!') - t.notOk(readyCalled, 'ready not called') - afterCalled = true - cb() - }) - - app.on('start', () => { - t.ok(afterCalled, 'after called') - t.ok(pluginLoaded, 'plugin loaded') - t.ok(readyCalled, 'ready called') - }) -}) - -test('if the after/ready callback has two parameters, the first one must be the context', (t) => { - t.plan(4) - - const server = { my: 'server' } - const app = boot(server) - - app.use(function (s, opts, done) { - done() - }) - - app.after(function (err, context, cb) { - t.error(err) - t.equal(server, context) - cb() - }) - - app.ready(function (err, context, cb) { - t.error(err) - t.equal(server, context) - cb() - }) -}) - -test('if the after/ready async, the returns must be the context generated', (t) => { - t.plan(3) - - const server = { my: 'server', index: 0 } - const app = boot(server) - app.override = function (old) { - return { ...old, index: old.index + 1 } - } - - app.use(function (s, opts, done) { - s.use(function (s, opts, done) { - s.ready().then(itself => t.same(itself, s, 'deep deep')) - done() - }) - s.ready().then(itself => t.same(itself, s, 'deep')) - done() - }) - - app.ready().then(itself => t.same(itself, server, 'outer')) -}) - -test('if the after/ready callback, the returns must be the context generated', (t) => { - t.plan(3) - - const server = { my: 'server', index: 0 } - const app = boot(server) - app.override = function (old) { - return { ...old, index: old.index + 1 } - } - - app.use(function (s, opts, done) { - s.use(function (s, opts, done) { - s.ready((_, itself, done) => { - t.same(itself, s, 'deep deep') - done() - }) - done() - }) - s.ready((_, itself, done) => { - t.same(itself, s, 'deep') - done() - }) - done() - }) - - app.ready((_, itself, done) => { - t.same(itself, server, 'outer') - done() - }) -}) - -test('error should come in the first after - one parameter', (t) => { - t.plan(3) - - const server = { my: 'server' } - const app = boot(server) - - app.use(function (s, opts, done) { - done(new Error('err')) - }) - - app.after(function (err) { - t.ok(err instanceof Error) - t.equal(err.message, 'err') - }) - - app.ready(function (err) { - t.error(err) - }) -}) - -test('error should come in the first after - two parameters', (t) => { - t.plan(3) - - const server = { my: 'server' } - const app = boot(server) - - app.use(function (s, opts, done) { - done(new Error('err')) - }) - - app.after(function (err, cb) { - t.ok(err instanceof Error) - t.equal(err.message, 'err') - cb() - }) - - app.ready(function (err) { - t.error(err) - }) -}) - -test('error should come in the first after - three parameter', (t) => { - t.plan(4) - - const server = { my: 'server' } - const app = boot(server) - - app.use(function (s, opts, done) { - done(new Error('err')) - }) - - app.after(function (err, context, cb) { - t.ok(err instanceof Error) - t.equal(err.message, 'err') - t.equal(context, server) - cb() - }) - - app.ready(function (err) { - t.error(err) - }) -}) - -test('error should come in the first ready - one parameter', (t) => { - t.plan(2) - - const server = { my: 'server' } - const app = boot(server) - - app.use(function (s, opts, done) { - done(new Error('err')) - }) - - app.ready(function (err) { - t.ok(err instanceof Error) - t.equal(err.message, 'err') - }) -}) - -test('error should come in the first ready - two parameters', (t) => { - t.plan(2) - - const server = { my: 'server' } - const app = boot(server) - - app.use(function (s, opts, done) { - done(new Error('err')) - }) - - app.ready(function (err, cb) { - t.ok(err instanceof Error) - t.equal(err.message, 'err') - cb() - }) -}) - -test('error should come in the first ready - three parameters', (t) => { - t.plan(3) - - const server = { my: 'server' } - const app = boot(server) - - app.use(function (s, opts, done) { - done(new Error('err')) - }) - - app.ready(function (err, context, cb) { - t.ok(err instanceof Error) - t.equal(err.message, 'err') - t.equal(context, server) - cb() - }) -}) - -test('if `use` has a callback with more then one parameter, the error must not reach ready', (t) => { - t.plan(1) - - const server = { my: 'server' } - const app = boot(server) - - app.use(function (s, opts, done) { - done(new Error('err')) - }) - - app.ready(function (err) { - t.ok(err) - }) -}) - -test('if `use` has a callback without parameters, the error must reach ready', (t) => { - t.plan(1) - - const server = { my: 'server' } - const app = boot(server) - - app.use(function (s, opts, done) { - done(new Error('err')) - }, () => {}) - - app.ready(function (err) { - t.ok(err) - }) -}) - -test('should pass the errors from after to ready', (t) => { - t.plan(6) - - const server = {} - const app = boot(server, {}) - - server.use(function (s, opts, done) { - t.equal(s, server, 'the first argument is the server') - t.same(opts, {}, 'no options') - done() - }).after((err, done) => { - t.error(err) - done(new Error('some error')) - }) - - server.onClose(() => { - t.ok('onClose called') - }) - - server.ready(err => { - t.equal(err.message, 'some error') - }) - - app.on('start', () => { - server.close(() => { - t.pass('booted') - }) - }) -}) - -test('after no encapsulation', t => { - t.plan(4) - - const app = boot() - app.override = function (s, fn, opts) { - s = Object.create(s) - return s - } - - app.use(function (instance, opts, next) { - instance.test = true - instance.after(function (err, i, done) { - t.error(err) - t.notOk(i.test) - done() - }) - next() - }) - - app.after(function (err, i, done) { - t.error(err) - t.notOk(i.test) - done() - }) -}) - -test('ready no encapsulation', t => { - t.plan(4) - - const app = boot() - app.override = function (s, fn, opts) { - s = Object.create(s) - return s - } - - app.use(function (instance, opts, next) { - instance.test = true - instance.ready(function (err, i, done) { - t.error(err) - t.notOk(i.test) - done() - }) - next() - }) - - app.ready(function (err, i, done) { - t.error(err) - t.notOk(i.test) - done() - }) -}) - -test('after encapsulation with a server', t => { - t.plan(4) - - const server = { my: 'server' } - const app = boot(server) - app.override = function (s, fn, opts) { - s = Object.create(s) - return s - } - - app.use(function (instance, opts, next) { - instance.test = true - instance.after(function (err, i, done) { - t.error(err) - t.ok(i.test) - done() - }) - next() - }) - - app.after(function (err, i, done) { - t.error(err) - t.notOk(i.test) - done() - }) -}) - -test('ready encapsulation with a server', t => { - t.plan(4) - - const server = { my: 'server' } - const app = boot(server) - app.override = function (s, fn, opts) { - s = Object.create(s) - return s - } - - app.use(function (instance, opts, next) { - instance.test = true - instance.ready(function (err, i, done) { - t.error(err) - t.ok(i.test) - done() - }) - next() - }) - - app.ready(function (err, i, done) { - t.error(err) - t.notOk(i.test) - done() - }) -}) - -test('after should passthrough the errors', (t) => { - t.plan(5) - - const app = boot() - let pluginLoaded = false - let afterCalled = false - - app.use(function (s, opts, done) { - t.notOk(afterCalled, 'after not called') - pluginLoaded = true - done(new Error('kaboom')) - }) - - app.after(function () { - t.ok(pluginLoaded, 'afterred!') - afterCalled = true - }) - - app.ready(function (err) { - t.ok(err) - t.ok(afterCalled, 'after called') - t.ok(pluginLoaded, 'plugin loaded') - }) -}) - -test('stop loading plugins if it errors', (t) => { - t.plan(2) - - const app = boot() - - app.use(function first (server, opts, done) { - t.pass('first called') - done(new Error('kaboom')) - }) - - app.use(function second (server, opts, done) { - t.fail('this should never be called') - }) - - app.ready((err) => { - t.equal(err.message, 'kaboom') - }) -}) - -test('keep loading if there is an .after', (t) => { - t.plan(4) - - const app = boot() - - app.use(function first (server, opts, done) { - t.pass('first called') - done(new Error('kaboom')) - }) - - app.after(function (err) { - t.equal(err.message, 'kaboom') - }) - - app.use(function second (server, opts, done) { - t.pass('second called') - done() - }) - - app.ready((err) => { - t.error(err) - }) -}) - -test('do not load nested plugin if parent errors', (t) => { - t.plan(4) - - const app = boot() - - app.use(function first (server, opts, done) { - t.pass('first called') - - server.use(function second (_, opts, done) { - t.fail('this should never be called') - }) - - done(new Error('kaboom')) - }) - - app.after(function (err) { - t.equal(err.message, 'kaboom') - }) - - app.use(function third (server, opts, done) { - t.pass('third called') - done() - }) - - app.ready((err) => { - t.error(err) - }) -}) - -test('.after nested', (t) => { - t.plan(4) - - const app = boot() - - app.use(function outer (app, opts, done) { - app.use(function first (app, opts, done) { - t.pass('first called') - done(new Error('kaboom')) - }) - - app.after(function (err) { - t.equal(err.message, 'kaboom') - }) - - app.use(function second (app, opts, done) { - t.pass('second called') - done() - }) - - done() - }) - - app.ready((err) => { - t.error(err) - }) -}) - -test('nested error', (t) => { - t.plan(4) - - const app = boot() - - app.use(function outer (app, opts, done) { - app.use(function first (app, opts, done) { - t.pass('first called') - done(new Error('kaboom')) - }) - - app.use(function second (app, opts, done) { - t.fail('this should never be called') - }) - - done() - }) - - app.after(function (err) { - t.equal(err.message, 'kaboom') - }) - - app.use(function third (server, opts, done) { - t.pass('third called') - done() - }) - - app.ready((err) => { - t.error(err) - }) -}) - -test('preReady event', (t) => { - t.plan(4) - - const app = boot() - const order = [1, 2] - - app.use(function first (server, opts, done) { - t.pass('first called') - done() - }) - - app.use(function second (server, opts, done) { - t.pass('second called') - done() - }) - - app.on('preReady', () => { - t.equal(order.shift(), 1) - }) - - app.ready(() => { - t.equal(order.shift(), 2) - }) -}) - -test('preReady event (multiple)', (t) => { - t.plan(6) - - const app = boot() - const order = [1, 2, 3, 4] - - app.use(function first (server, opts, done) { - t.pass('first called') - done() - }) - - app.use(function second (server, opts, done) { - t.pass('second called') - done() - }) - - app.on('preReady', () => { - t.equal(order.shift(), 1) - }) - - app.on('preReady', () => { - t.equal(order.shift(), 2) - }) - - app.on('preReady', () => { - t.equal(order.shift(), 3) - }) - - app.ready(() => { - t.equal(order.shift(), 4) - }) -}) - -test('preReady event (nested)', (t) => { - t.plan(6) - - const app = boot() - const order = [1, 2, 3, 4] - - app.use(function first (server, opts, done) { - t.pass('first called') - done() - }) - - app.use(function second (server, opts, done) { - t.pass('second called') - - server.on('preReady', () => { - t.equal(order.shift(), 3) - }) - - done() - }) - - app.on('preReady', () => { - t.equal(order.shift(), 1) - }) - - app.on('preReady', () => { - t.equal(order.shift(), 2) - }) - - app.ready(() => { - t.equal(order.shift(), 4) - }) -}) - -test('preReady event (errored)', (t) => { - t.plan(5) - - const app = boot() - const order = [1, 2, 3] - - app.use(function first (server, opts, done) { - t.pass('first called') - done(new Error('kaboom')) - }) - - app.use(function second (server, opts, done) { - t.fail('We should not be here') - }) - - app.on('preReady', () => { - t.equal(order.shift(), 1) - }) - - app.on('preReady', () => { - t.equal(order.shift(), 2) - }) - - app.ready((err) => { - t.ok(err) - t.equal(order.shift(), 3) - }) -}) - -test('after return self', (t) => { - t.plan(6) - - const app = boot() - let pluginLoaded = false - let afterCalled = false - let second = false - - app.use(function (s, opts, done) { - t.notOk(afterCalled, 'after not called') - pluginLoaded = true - done() - }) - - app.after(function () { - t.ok(pluginLoaded, 'afterred!') - afterCalled = true - // happens with after(() => app.use(..)) - return app - }) - - app.use(function (s, opts, done) { - t.ok(afterCalled, 'after called') - second = true - done() - }) - - app.on('start', () => { - t.ok(afterCalled, 'after called') - t.ok(pluginLoaded, 'plugin loaded') - t.ok(second, 'second plugin loaded') - }) -}) - -test('after 1 param swallows errors with server and timeout', (t) => { - t.plan(3) - - const server = {} - boot(server, { autostart: false, timeout: 1000 }) - - server.use(function first (server, opts, done) { - t.pass('first called') - done(new Error('kaboom')) - }) - - server.use(function second (server, opts, done) { - t.fail('We should not be here') - }) - - server.after(function (err) { - t.ok(err) - }) - - server.ready(function (err) { - t.error(err) - }) -}) diff --git a/test/after-pass-through.test.js b/test/after-pass-through.test.js deleted file mode 100644 index 34efeaa..0000000 --- a/test/after-pass-through.test.js +++ /dev/null @@ -1,33 +0,0 @@ -'use strict' - -const { test } = require('node:test') -const boot = require('..') - -test('proper support for after with a passed async function in wrapped mode', (t, testCompleted) => { - const app = {} - boot(app) - - t.plan(5) - - const e = new Error('kaboom') - - app.use(function (f, opts) { - return Promise.reject(e) - }).after(function (err, cb) { - t.assert.deepStrictEqual(err, e) - cb(err) - }).after(function () { - t.assert.ok('this is just called') - }).after(function (err, cb) { - t.assert.deepStrictEqual(err, e) - cb(err) - }) - - app.ready().then(() => { - t.assert.fail('this should not be called') - }).catch(err => { - t.assert.ok(err) - t.assert.strictEqual(err.message, 'kaboom') - testCompleted() - }) -}) diff --git a/test/after-self-promise.test.js b/test/after-self-promise.test.js deleted file mode 100644 index 70791c5..0000000 --- a/test/after-self-promise.test.js +++ /dev/null @@ -1,20 +0,0 @@ -'use strict' - -const { test } = require('node:test') -const boot = require('..') - -test('after does not await itself', async (t) => { - t.plan(3) - - const app = {} - boot(app) - - app.use(async (app) => { - t.assert.ok('plugin init') - }) - app.after(() => app) - t.assert.ok('reachable') - - await app.ready() - t.assert.ok('reachable') -}) diff --git a/test/after-throw.test.js b/test/after-throw.test.js deleted file mode 100644 index c06e725..0000000 --- a/test/after-throw.test.js +++ /dev/null @@ -1,24 +0,0 @@ -'use strict' - -const { test } = require('tap') -const boot = require('..') - -test('catched error by Promise.reject', (t) => { - const app = boot() - t.plan(2) - - t.threw = function (err) { - t.equal(err.message, 'kaboom2') - } - - app.use(function (f, opts) { - return Promise.reject(new Error('kaboom')) - }).after(function (err) { - t.equal(err.message, 'kaboom') - throw new Error('kaboom2') - }) - - app.ready(function () { - t.fail('the ready callback should never be called') - }) -}) diff --git a/test/after-use-after.test.js b/test/after-use-after.test.js deleted file mode 100644 index d0ab596..0000000 --- a/test/after-use-after.test.js +++ /dev/null @@ -1,90 +0,0 @@ -'use strict' - -const { test } = require('tap') -const boot = require('..') -const app = {} - -boot(app) - -test('multi after', (t) => { - t.plan(6) - - app.use(function (f, opts, cb) { - cb() - }).after(() => { - t.pass('this is just called') - - app.use(function (f, opts, cb) { - t.pass('this is just called') - cb() - }) - }).after(function () { - t.pass('this is just called') - app.use(function (f, opts, cb) { - t.pass('this is just called') - cb() - }) - }).after(function (err, cb) { - t.pass('this is just called') - cb(err) - }) - - app.ready().then(() => { - t.pass('ready') - }).catch(() => { - t.fail('this should not be called') - }) -}) - -test('after grouping - use called after after called', (t) => { - t.plan(9) - const app = {} - boot(app) - - const TEST_VALUE = {} - const OTHER_TEST_VALUE = {} - const NEW_TEST_VALUE = {} - - const sO = (fn) => { - fn[Symbol.for('skip-override')] = true - return fn - } - - app.use(sO(function (f, options, next) { - f.test = TEST_VALUE - - next() - })) - - app.after(function (err, f, done) { - t.error(err) - t.equal(f.test, TEST_VALUE) - - f.test2 = OTHER_TEST_VALUE - done() - }) - - app.use(sO(function (f, options, next) { - t.equal(f.test, TEST_VALUE) - t.equal(f.test2, OTHER_TEST_VALUE) - - f.test3 = NEW_TEST_VALUE - - next() - })) - - app.after(function (err, f, done) { - t.error(err) - t.equal(f.test, TEST_VALUE) - t.equal(f.test2, OTHER_TEST_VALUE) - t.equal(f.test3, NEW_TEST_VALUE) - done() - }) - - app.ready().then(() => { - t.pass('ready') - }).catch((e) => { - console.log(e) - t.fail('this should not be called') - }) -}) diff --git a/test/async-await.test.js b/test/async-await.test.js deleted file mode 100644 index fa2386f..0000000 --- a/test/async-await.test.js +++ /dev/null @@ -1,323 +0,0 @@ -'use strict' - -/* eslint no-prototype-builtins: off */ - -const { test } = require('node:test') -const { setTimeout: sleep } = require('node:timers/promises') - -const boot = require('..') - -test('one level', async (t) => { - t.plan(14) - - const app = boot() - let firstLoaded = false - let secondLoaded = false - let thirdLoaded = false - - app.use(first) - app.use(third) - - async function first (s, opts) { - t.assert.strictEqual(firstLoaded, false, 'first is not loaded') - t.assert.strictEqual(secondLoaded, false, 'second is not loaded') - t.assert.strictEqual(thirdLoaded, false, 'third is not loaded') - firstLoaded = true - s.use(second) - } - - async function second (s, opts) { - t.assert.ok(firstLoaded, 'first is loaded') - t.assert.strictEqual(secondLoaded, false, 'second is not loaded') - t.assert.strictEqual(thirdLoaded, false, 'third is not loaded') - secondLoaded = true - } - - async function third (s, opts) { - t.assert.ok(firstLoaded, 'first is loaded') - t.assert.ok(secondLoaded, 'second is loaded') - t.assert.strictEqual(thirdLoaded, false, 'third is not loaded') - thirdLoaded = true - } - - const readyContext = await app.ready() - - t.assert.deepStrictEqual(app, readyContext) - t.assert.ok(firstLoaded, 'first is loaded') - t.assert.ok(secondLoaded, 'second is loaded') - t.assert.ok(thirdLoaded, 'third is loaded') - t.assert.ok('booted') -}) - -test('multiple reentrant plugin loading', async (t) => { - t.plan(31) - - const app = boot() - let firstLoaded = false - let secondLoaded = false - let thirdLoaded = false - let fourthLoaded = false - let fifthLoaded = false - - app.use(first) - app.use(fifth) - - async function first (s, opts) { - t.assert.strictEqual(firstLoaded, false, 'first is not loaded') - t.assert.strictEqual(secondLoaded, false, 'second is not loaded') - t.assert.strictEqual(thirdLoaded, false, 'third is not loaded') - t.assert.strictEqual(fourthLoaded, false, 'fourth is not loaded') - t.assert.strictEqual(fifthLoaded, false, 'fifth is not loaded') - firstLoaded = true - s.use(second) - } - - async function second (s, opts) { - t.assert.ok(firstLoaded, 'first is loaded') - t.assert.strictEqual(secondLoaded, false, 'second is not loaded') - t.assert.strictEqual(thirdLoaded, false, 'third is not loaded') - t.assert.strictEqual(fourthLoaded, false, 'fourth is not loaded') - t.assert.strictEqual(fifthLoaded, false, 'fifth is not loaded') - secondLoaded = true - s.use(third) - await sleep(10) - s.use(fourth) - } - - async function third (s, opts) { - t.assert.ok(firstLoaded, 'first is loaded') - t.assert.ok(secondLoaded, 'second is loaded') - t.assert.strictEqual(thirdLoaded, false, 'third is not loaded') - t.assert.strictEqual(fourthLoaded, false, 'fourth is not loaded') - t.assert.strictEqual(fifthLoaded, false, 'fifth is not loaded') - thirdLoaded = true - } - - async function fourth (s, opts) { - t.assert.ok(firstLoaded, 'first is loaded') - t.assert.ok(secondLoaded, 'second is loaded') - t.assert.ok(thirdLoaded, 'third is loaded') - t.assert.strictEqual(fourthLoaded, false, 'fourth is not loaded') - t.assert.strictEqual(fifthLoaded, false, 'fifth is not loaded') - fourthLoaded = true - } - - async function fifth (s, opts) { - t.assert.ok(firstLoaded, 'first is loaded') - t.assert.ok(secondLoaded, 'second is loaded') - t.assert.ok(thirdLoaded, 'third is loaded') - t.assert.ok(fourthLoaded, 'fourth is loaded') - t.assert.strictEqual(fifthLoaded, false, 'fifth is not loaded') - fifthLoaded = true - } - - await app.ready() - t.assert.ok(firstLoaded, 'first is loaded') - t.assert.ok(secondLoaded, 'second is loaded') - t.assert.ok(thirdLoaded, 'third is loaded') - t.assert.ok(fourthLoaded, 'fourth is loaded') - t.assert.ok(fifthLoaded, 'fifth is loaded') - t.assert.ok('booted') -}) - -test('async ready plugin registration (errored)', async (t) => { - t.plan(1) - - const app = boot() - - app.use(async (server, opts) => { - await sleep(10) - throw new Error('kaboom') - }) - - try { - await app.ready() - t.assert.fail('we should not be here') - } catch (err) { - t.assert.strictEqual(err.message, 'kaboom') - } -}) - -test('after', async (t) => { - t.plan(15) - - const app = boot() - let firstLoaded = false - let secondLoaded = false - let thirdLoaded = false - - app.use(first) - - async function first (s, opts) { - t.assert.strictEqual(firstLoaded, false, 'first is not loaded') - t.assert.strictEqual(secondLoaded, false, 'second is not loaded') - t.assert.strictEqual(thirdLoaded, false, 'third is not loaded') - firstLoaded = true - s.after(second) - s.after(third) - } - - async function second (err) { - t.assert.ifError(err) - t.assert.ok(firstLoaded, 'first is loaded') - t.assert.strictEqual(secondLoaded, false, 'second is not loaded') - t.assert.strictEqual(thirdLoaded, false, 'third is not loaded') - await sleep(10) - secondLoaded = true - } - - async function third () { - t.assert.ok(firstLoaded, 'first is loaded') - t.assert.ok(secondLoaded, 'second is loaded') - t.assert.strictEqual(thirdLoaded, false, 'third is not loaded') - await sleep(10) - thirdLoaded = true - } - - const readyContext = await app.ready() - - t.assert.deepStrictEqual(app, readyContext) - t.assert.ok(firstLoaded, 'first is loaded') - t.assert.ok(secondLoaded, 'second is loaded') - t.assert.ok(thirdLoaded, 'third is loaded') - t.assert.ok('booted') -}) - -test('after wrapped', async (t) => { - t.plan(15) - - const app = {} - boot(app) - let firstLoaded = false - let secondLoaded = false - let thirdLoaded = false - - app.use(first) - - async function first (s, opts) { - t.assert.strictEqual(firstLoaded, false, 'first is not loaded') - t.assert.strictEqual(secondLoaded, false, 'second is not loaded') - t.assert.strictEqual(thirdLoaded, false, 'third is not loaded') - firstLoaded = true - s.after(second) - s.after(third) - } - - async function second (err) { - t.assert.ifError(err) - t.assert.ok(firstLoaded, 'first is loaded') - t.assert.strictEqual(secondLoaded, false, 'second is not loaded') - t.assert.strictEqual(thirdLoaded, false, 'third is not loaded') - await sleep(10) - secondLoaded = true - } - - async function third () { - t.assert.ok(firstLoaded, 'first is loaded') - t.assert.ok(secondLoaded, 'second is loaded') - t.assert.strictEqual(thirdLoaded, false, 'third is not loaded') - await sleep(10) - thirdLoaded = true - } - - const readyContext = await app.ready() - - t.assert.deepStrictEqual(app, readyContext) - t.assert.ok(firstLoaded, 'first is loaded') - t.assert.ok(secondLoaded, 'second is loaded') - t.assert.ok(thirdLoaded, 'third is loaded') - t.assert.ok('booted') -}) - -test('promise plugins', async (t) => { - t.plan(14) - - const app = boot() - let firstLoaded = false - let secondLoaded = false - let thirdLoaded = false - - app.use(first()) - app.use(third()) - - async function first () { - return async function (s, opts) { - t.assert.strictEqual(firstLoaded, false, 'first is not loaded') - t.assert.strictEqual(secondLoaded, false, 'second is not loaded') - t.assert.strictEqual(thirdLoaded, false, 'third is not loaded') - firstLoaded = true - s.use(second()) - } - } - - async function second () { - return async function (s, opts) { - t.assert.ok(firstLoaded, 'first is loaded') - t.assert.strictEqual(secondLoaded, false, 'second is not loaded') - t.assert.strictEqual(thirdLoaded, false, 'third is not loaded') - secondLoaded = true - } - } - - async function third () { - return async function (s, opts) { - t.assert.ok(firstLoaded, 'first is loaded') - t.assert.ok(secondLoaded, 'second is loaded') - t.assert.strictEqual(thirdLoaded, false, 'third is not loaded') - thirdLoaded = true - } - } - - const readyContext = await app.ready() - - t.assert.deepStrictEqual(app, readyContext) - t.assert.ok(firstLoaded, 'first is loaded') - t.assert.ok(secondLoaded, 'second is loaded') - t.assert.ok(thirdLoaded, 'third is loaded') - t.assert.ok('booted') -}) - -test('skip override with promise', async (t) => { - t.plan(3) - - const server = { my: 'server' } - const app = boot(server) - - app.override = function (s, func) { - t.assert.ok('override called') - - if (func[Symbol.for('skip-override')]) { - return s - } - return Object.create(s) - } - - app.use(first()) - - async function first () { - async function fn (s, opts) { - t.assert.deepStrictEqual(s, server) - t.assert.strictEqual(Object.prototype.isPrototypeOf.call(server, s), false) - } - - fn[Symbol.for('skip-override')] = true - - return fn - } - - await app.ready() -}) - -test('ready queue error', async (t) => { - const app = boot() - app.use(first) - - async function first (s, opts) {} - - app.ready(function (_, worker, done) { - const error = new Error('kaboom') - done(error) - }) - - await t.assert.rejects(app.ready(), { message: 'kaboom' }) -}) diff --git a/test/avvio-on-close.test.js b/test/avvio-on-close.test.js new file mode 100644 index 0000000..c105145 --- /dev/null +++ b/test/avvio-on-close.test.js @@ -0,0 +1,85 @@ +const test = require('node:test').test +const Avvio = require('../avvio') + +test('Avvio#onClose', async (t) => { + const test = t.test.bind(t) + + await test('() => void', (t, done) => { + t.plan(1) + const avvio = Avvio() + avvio.onClose(() => { + t.assert.ok(true, 'executed') + }) + avvio.close(() => done()) + }) + + await test('() => Promise', (t, done) => { + t.plan(1) + const avvio = Avvio() + avvio.onClose(() => { + t.assert.ok(true, 'executed') + return Promise.resolve() + }) + avvio.close(() => done()) + }) + + await test('(context) => void', (t, done) => { + t.plan(2) + const avvio = Avvio() + avvio.onClose((context) => { + t.assert.strictEqual(context, avvio) + t.assert.ok(true, 'executed') + }) + avvio.close(() => done()) + }) + + await test('(context) => Promise', (t, done) => { + t.plan(2) + const avvio = Avvio() + avvio.onClose((context) => { + t.assert.strictEqual(context, avvio) + t.assert.ok(true, 'executed') + return Promise.resolve() + }) + avvio.close(() => done()) + }) + + await test('(context, done) => void', (t, done) => { + t.plan(2) + const avvio = Avvio() + avvio.onClose((context, done) => { + t.assert.strictEqual(context, avvio) + t.assert.ok(true, 'executed') + setImmediate(done) + }) + avvio.close(() => done()) + }) + + await test('(context, done) => Promise', (t, done) => { + t.plan(2) + const avvio = Avvio() + avvio.onClose((context, done) => { + t.assert.strictEqual(context, avvio) + t.assert.ok(true, 'executed') + return Promise.resolve() + }) + avvio.close(() => done()) + }) + + await test('execution order', (t, done) => { + t.plan(3) + const counter = 0 + const avvio = Avvio() + avvio.onClose(() => { + t.assert.strictEqual(counter, 3) + }) + avvio.onClose(async () => { + t.assert.strictEqual(counter, 2) + }) + avvio.onClose(() => { + t.assert.strictEqual(counter, 1) + return Promise.resolve() + }) + avvio.close(() => done()) + }) +}) diff --git a/test/avvio-ready.test.js b/test/avvio-ready.test.js new file mode 100644 index 0000000..1a170dd --- /dev/null +++ b/test/avvio-ready.test.js @@ -0,0 +1,83 @@ +const test = require('node:test').test +const Avvio = require('../avvio') + +test('Avvio#ready', async (t) => { + const test = t.test.bind(t) + await test('await ready()', async (t) => { + t.plan(4) + let counter = 0 + const avvio = Avvio() + avvio.use((context, _, done) => { + counter++ + t.assert.strictEqual(counter, 1) + context.use((context, _, done) => { + counter++ + t.assert.strictEqual(counter, 2) + done() + }) + done() + }) + const context = await avvio.ready() + t.assert.strictEqual(counter, 2) + t.assert.deepStrictEqual(context, avvio) + }) + + await test('await ready()', async (t) => { + t.plan(4) + let counter = 0 + const avvio = Avvio() + avvio.use((context, _, done) => { + counter++ + t.assert.strictEqual(counter, 1) + context.use((context, _, done) => { + counter++ + t.assert.strictEqual(counter, 2) + setImmediate(done) + }) + setImmediate(done) + }) + const context = await avvio.ready() + t.assert.strictEqual(counter, 2) + t.assert.deepStrictEqual(context, avvio) + }) + + await test('ready(callback)', (t, done) => { + t.plan(3) + let counter = 0 + const avvio = Avvio() + avvio.use((context, _, done) => { + counter++ + t.assert.strictEqual(counter, 1) + context.use((context, _, done) => { + counter++ + t.assert.strictEqual(counter, 2) + done() + }) + done() + }) + avvio.ready(() => { + t.assert.strictEqual(counter, 2) + done() + }) + }) + + await test('ready(callback)', (t, done) => { + t.plan(3) + let counter = 0 + const avvio = Avvio() + avvio.use((context, _, done) => { + counter++ + t.assert.strictEqual(counter, 1) + context.use((context, _, done) => { + counter++ + t.assert.strictEqual(counter, 2) + setImmediate(done) + }) + setImmediate(done) + }) + avvio.ready(() => { + t.assert.strictEqual(counter, 2) + done() + }) + }) +}) diff --git a/test/await-after.test.js b/test/await-after.test.js deleted file mode 100644 index b3c3b21..0000000 --- a/test/await-after.test.js +++ /dev/null @@ -1,448 +0,0 @@ -'use strict' - -const { test } = require('node:test') -const boot = require('..') -const { setTimeout: sleep } = require('node:timers/promises') -const fs = require('node:fs/promises') -const path = require('node:path') - -test('await after - nested plugins with same tick callbacks', async (t) => { - const app = {} - boot(app) - - let secondLoaded = false - - app.use(async (app) => { - t.assert.ok('plugin init') - app.use(async () => { - t.assert.ok('plugin2 init') - await sleep(1) - secondLoaded = true - }) - }) - await app.after() - t.assert.ok('reachable') - t.assert.ok(secondLoaded) - - await app.ready() - t.assert.ok('reachable') -}) - -test('await after without server', async (t) => { - const app = boot() - - let secondLoaded = false - - app.use(async (app) => { - t.assert.ok('plugin init') - app.use(async () => { - t.assert.ok('plugin2 init') - await sleep(1) - secondLoaded = true - }) - }) - await app.after() - t.assert.ok('reachable') - t.assert.ok(secondLoaded) - - await app.ready() - t.assert.ok('reachable') -}) - -test('await after with cb functions', async (t) => { - const app = boot() - let secondLoaded = false - let record = '' - - app.use(async (app) => { - t.assert.ok('plugin init') - record += 'plugin|' - app.use(async () => { - t.assert.ok('plugin2 init') - record += 'plugin2|' - await sleep(1) - secondLoaded = true - }) - }) - await app.after(() => { - record += 'after|' - }) - t.assert.ok('reachable') - t.assert.ok(secondLoaded) - record += 'ready' - await app.ready() - t.assert.ok('reachable') - t.assert.strictEqual(record, 'plugin|plugin2|after|ready') -}) - -test('await after - nested plugins with future tick callbacks', async (t) => { - const app = {} - boot(app) - - t.plan(4) - - app.use((f, opts, cb) => { - t.assert.ok('plugin init') - app.use((f, opts, cb) => { - t.assert.ok('plugin2 init') - setImmediate(cb) - }) - setImmediate(cb) - }) - await app.after() - t.assert.ok('reachable') - - await app.ready() - t.assert.ok('reachable') -}) - -test('await after - nested async function plugins', async (t) => { - const app = {} - boot(app) - - t.plan(5) - - app.use(async (f, opts) => { - t.assert.ok('plugin init') - await app.use(async (f, opts) => { - t.assert.ok('plugin2 init') - }) - t.assert.ok('reachable') - }) - await app.after() - t.assert.ok('reachable') - - await app.ready() - t.assert.ok('reachable') -}) - -test('await after - promise resolves to undefined', async (t) => { - const app = {} - boot(app) - - t.plan(4) - - app.use(async (f, opts, cb) => { - app.use((f, opts, cb) => { - t.assert.ok('plugin init') - cb() - }) - const instance = await app.after() - t.assert.strictEqual(instance, undefined) - }) - t.assert.ok('reachable') - - await app.ready() - t.assert.ok('reachable') -}) - -test('await after - promise returning function plugins + promise chaining', async (t) => { - const app = {} - boot(app) - - t.plan(6) - - app.use((f, opts) => { - t.assert.ok('plugin init') - return app.use((f, opts) => { - t.assert.ok('plugin2 init') - return Promise.resolve() - }).then((f2) => { - t.assert.deepStrictEqual(f2, f) - return 'test' - }).then((val) => { - t.assert.strictEqual(val, 'test') - }) - }) - await app.after() - t.assert.ok('reachable') - - await app.ready() - t.assert.ok('reachable') -}) - -test('await after - error handling, async throw', async (t) => { - const app = {} - boot(app) - - t.plan(2) - - const e = new Error('kaboom') - - app.use(async (f, opts) => { - throw Error('kaboom') - }) - - await t.assert.rejects(app.after(), e) - - await t.assert.rejects(() => app.ready(), Error('kaboom')) -}) - -test('await after - error handling, async throw, nested', async (t) => { - const app = {} - boot(app) - - t.plan(2) - - const e = new Error('kaboom') - - app.use(async (f, opts) => { - app.use(async (f, opts) => { - throw e - }) - }) - - await t.assert.rejects(app.after()) - await t.assert.rejects(() => app.ready(), e) -}) - -test('await after - error handling, same tick cb err', async (t) => { - const app = {} - boot(app) - - t.plan(2) - - app.use((f, opts, cb) => { - cb(Error('kaboom')) - }) - await t.assert.rejects(app.after()) - await t.assert.rejects(app.ready(), Error('kaboom')) -}) - -test('await after - error handling, same tick cb err, nested', async (t) => { - const app = {} - boot(app) - - t.plan(2) - - app.use((f, opts, cb) => { - app.use((f, opts, cb) => { - cb(Error('kaboom')) - }) - cb() - }) - - await t.assert.rejects(app.after()) - await t.assert.rejects(app.ready(), Error('kaboom')) -}) - -test('await after - error handling, future tick cb err', async (t) => { - const app = {} - boot(app) - - t.plan(2) - - app.use((f, opts, cb) => { - setImmediate(() => { cb(Error('kaboom')) }) - }) - - await t.assert.rejects(app.after()) - await t.assert.rejects(app.ready(), Error('kaboom')) -}) - -test('await after - error handling, future tick cb err, nested', async (t) => { - const app = {} - boot(app) - - t.plan(2) - - app.use((f, opts, cb) => { - app.use((f, opts, cb) => { - setImmediate(() => { cb(Error('kaboom')) }) - }) - cb() - }) - await t.assert.rejects(app.after(), Error('kaboom')) - await t.assert.rejects(app.ready(), Error('kaboom')) -}) - -test('await after complex scenario', async (t) => { - const app = {} - boot(app) - t.plan(16) - - let firstLoaded = false - let secondLoaded = false - let thirdLoaded = false - let fourthLoaded = false - - app.use(first) - await app.after() - t.assert.ok(firstLoaded, 'first is loaded') - t.assert.strictEqual(secondLoaded, false, 'second is not loaded') - t.assert.strictEqual(thirdLoaded, false, 'third is not loaded') - t.assert.strictEqual(fourthLoaded, false, 'fourth is not loaded') - app.use(second) - t.assert.ok(firstLoaded, 'first is loaded') - t.assert.strictEqual(secondLoaded, false, 'second is not loaded') - t.assert.strictEqual(thirdLoaded, false, 'third is not loaded') - t.assert.strictEqual(fourthLoaded, false, 'fourth is not loaded') - app.use(third) - await app.after() - t.assert.ok(firstLoaded, 'first is loaded') - t.assert.ok(secondLoaded, 'second is loaded') - t.assert.ok(thirdLoaded, 'third is loaded') - t.assert.ok(fourthLoaded, 'fourth is loaded') - await app.ready() - t.assert.ok(firstLoaded, 'first is loaded') - t.assert.ok(secondLoaded, 'second is loaded') - t.assert.ok(thirdLoaded, 'third is loaded') - t.assert.ok(fourthLoaded, 'fourth is loaded') - - async function first () { - firstLoaded = true - } - - async function second () { - secondLoaded = true - } - - async function third (app) { - thirdLoaded = true - app.use(fourth) - } - - async function fourth () { - fourthLoaded = true - } -}) - -test('without autostart and sync/async plugin mix', async (t) => { - const app = {} - boot(app, { autostart: false }) - t.plan(21) - - let firstLoaded = false - let secondLoaded = false - let thirdLoaded = false - let fourthLoaded = false - - app.use(first) - await app.after() - t.assert.ok(firstLoaded, 'first is loaded') - t.assert.strictEqual(secondLoaded, false, 'second is not loaded') - t.assert.strictEqual(thirdLoaded, false, 'third is not loaded') - t.assert.strictEqual(fourthLoaded, false, 'fourth is not loaded') - - app.use(second) - await app.after() - t.assert.ok(firstLoaded, 'first is loaded') - t.assert.ok(secondLoaded, 'second is loaded') - t.assert.strictEqual(thirdLoaded, false, 'third is not loaded') - t.assert.strictEqual(fourthLoaded, false, 'fourth is not loaded') - - await sleep(10) - - app.use(third) - await app.after() - t.assert.ok(firstLoaded, 'first is loaded') - t.assert.ok(secondLoaded, 'second is loaded') - t.assert.ok(thirdLoaded, 'third is loaded') - t.assert.strictEqual(fourthLoaded, false, 'fourth is not loaded') - - app.use(fourth) - t.assert.ok(firstLoaded, 'first is loaded') - t.assert.ok(secondLoaded, 'second is loaded') - t.assert.ok(thirdLoaded, 'third is loaded') - t.assert.strictEqual(fourthLoaded, false, 'fourth is not loaded') - - await app.after() - t.assert.ok(firstLoaded, 'first is loaded') - t.assert.ok(secondLoaded, 'second is loaded') - t.assert.ok(thirdLoaded, 'third is loaded') - t.assert.ok(fourthLoaded, 'fourth is loaded') - - await app.ready() - - async function first () { - firstLoaded = true - } - - async function second () { - const contents = await fs.readFile(path.join(__dirname, 'fixtures', 'dummy.txt'), 'utf-8') - t.assert.strictEqual(contents, 'hello, world!') - secondLoaded = true - } - - async function third () { - await sleep(10) - thirdLoaded = true - } - - function fourth (server, opts, done) { - fourthLoaded = true - done() - } -}) - -test('without autostart', async (t) => { - const app = {} - boot(app, { autostart: false }) - let firstLoaded = false - let secondLoaded = false - let thirdLoaded = false - - app.use(async function first (app) { - firstLoaded = true - app.use(async () => { - await sleep(1) - secondLoaded = true - }) - }) - - await app.after() - t.assert.ok(firstLoaded) - t.assert.ok(secondLoaded) - - await app.use(async () => { - thirdLoaded = true - }) - - t.assert.ok(thirdLoaded) - - await app.ready() -}) - -test('without autostart and with override', async (t) => { - const app = {} - const _ = boot(app, { autostart: false }) - let count = 0 - - _.override = function (s) { - const res = Object.create(s) - res.count = ++count - - return res - } - - app.use(async function first (app) { - t.assert.strictEqual(app.count, 1) - app.use(async (app) => { - t.assert.strictEqual(app.count, 2) - await app.after() - }) - }) - - await app.after() - - await app.use(async (app) => { - t.assert.strictEqual(app.count, 3) - }) - - await app.ready() -}) - -test('stop processing after errors', async (t) => { - t.plan(2) - - const app = boot() - - try { - await app.use(async function first (app) { - t.assert.ok('first should be loaded') - throw new Error('kaboom') - }) - } catch (e) { - t.assert.strictEqual(e.message, 'kaboom') - } -}) diff --git a/test/await-self.test.js b/test/await-self.test.js deleted file mode 100644 index 8d8c990..0000000 --- a/test/await-self.test.js +++ /dev/null @@ -1,31 +0,0 @@ -'use strict' - -const { test } = require('tap') -const boot = require('..') - -test('await self', async (t) => { - const app = {} - boot(app) - - t.equal(await app, app) -}) - -test('await self three times', async (t) => { - const app = {} - boot(app) - - t.equal(await app, app) - t.equal(await app, app) - t.equal(await app, app) -}) - -test('await self within plugin', async (t) => { - const app = {} - boot(app) - - app.use(async (f) => { - t.equal(await f, f) - }) - - t.equal(await app, app) -}) diff --git a/test/await-use.test.js b/test/await-use.test.js deleted file mode 100644 index f8adee8..0000000 --- a/test/await-use.test.js +++ /dev/null @@ -1,294 +0,0 @@ -'use strict' - -const { test } = require('tap') -const { promisify } = require('node:util') -const sleep = promisify(setTimeout) -const boot = require('..') - -test('await use - nested plugins with same tick callbacks', async (t) => { - const app = {} - boot(app) - - t.plan(4) - - await app.use((f, opts, cb) => { - t.pass('plugin init') - app.use((f, opts, cb) => { - t.pass('plugin2 init') - cb() - }) - cb() - }) - t.pass('reachable') - - await app.ready() - t.pass('reachable') -}) - -test('await use - nested plugins with future tick callbacks', async (t) => { - const app = {} - boot(app) - - t.plan(4) - - await app.use((f, opts, cb) => { - t.pass('plugin init') - app.use((f, opts, cb) => { - t.pass('plugin2 init') - setImmediate(cb) - }) - setImmediate(cb) - }) - t.pass('reachable') - - await app.ready() - t.pass('reachable') -}) - -test('await use - nested async function plugins', async (t) => { - const app = {} - boot(app) - - t.plan(5) - - await app.use(async (f, opts) => { - t.pass('plugin init') - await app.use(async (f, opts) => { - t.pass('plugin2 init') - }) - t.pass('reachable') - }) - t.pass('reachable') - - await app.ready() - t.pass('reachable') -}) - -test('await use - promise returning function plugins + promise chaining', async (t) => { - const app = {} - boot(app) - - t.plan(6) - - await app.use((f, opts) => { - t.pass('plugin init') - return app.use((f, opts) => { - t.pass('plugin2 init') - return Promise.resolve() - }).then(() => { - t.pass('reachable') - return 'test' - }).then((val) => { - t.equal(val, 'test') - }) - }) - t.pass('reachable') - - await app.ready() - t.pass('reachable') -}) - -test('await use - await and use chaining', async (t) => { - const app = {} - boot(app) - - t.plan(3) - - app.use(async (f, opts, cb) => { - await app.use(async (f, opts) => { - t.pass('plugin init') - }).use(async (f, opts) => { - t.pass('plugin2 init') - }) - }) - - await app.ready() - t.pass('reachable') -}) - -function thenableRejects (t, thenable, err, msg) { - return t.rejects(async () => { await thenable }, err, msg) -} - -test('await use - error handling, async throw', async (t) => { - const app = {} - boot(app) - - t.plan(2) - - await thenableRejects(t, app.use(async (f, opts) => { - throw Error('kaboom') - }), Error('kaboom')) - - await t.rejects(app.ready(), Error('kaboom')) -}) - -test('await use - error handling, async throw, nested', async (t) => { - const app = {} - boot(app) - - t.plan(2) - - await thenableRejects(t, app.use(async function a (f, opts) { - await app.use(async function b (f, opts) { - throw Error('kaboom') - }) - }, Error('kaboom')), 'b') - - t.rejects(() => app.ready(), Error('kaboom')) -}) - -test('await use - error handling, same tick cb err', async (t) => { - const app = {} - boot(app) - - t.plan(2) - - await thenableRejects(t, app.use((f, opts, cb) => { - cb(Error('kaboom')) - }), Error('kaboom')) - - t.rejects(() => app.ready(), Error('kaboom')) -}) - -test('await use - error handling, same tick cb err, nested', async (t) => { - const app = {} - boot(app) - - t.plan(2) - - await thenableRejects(t, app.use((f, opts, cb) => { - app.use((f, opts, cb) => { - cb(Error('kaboom')) - }) - cb() - }), Error('kaboom')) - - t.rejects(() => app.ready(), Error('kaboom')) -}) - -test('await use - error handling, future tick cb err', async (t) => { - const app = {} - boot(app) - - t.plan(2) - - await thenableRejects(t, app.use((f, opts, cb) => { - setImmediate(() => { cb(Error('kaboom')) }) - }), Error('kaboom')) - - t.rejects(() => app.ready(), Error('kaboom')) -}) - -test('await use - error handling, future tick cb err, nested', async (t) => { - const app = {} - boot(app) - - t.plan(2) - - await thenableRejects(t, app.use((f, opts, cb) => { - app.use((f, opts, cb) => { - setImmediate(() => { cb(Error('kaboom')) }) - }) - cb() - }), Error('kaboom')) - - t.rejects(() => app.ready(), Error('kaboom')) -}) - -test('mixed await use and non-awaited use ', async (t) => { - const app = {} - boot(app) - t.plan(16) - - let firstLoaded = false - let secondLoaded = false - let thirdLoaded = false - let fourthLoaded = false - - await app.use(first) - t.ok(firstLoaded, 'first is loaded') - t.notOk(secondLoaded, 'second is not loaded') - t.notOk(thirdLoaded, 'third is not loaded') - t.notOk(fourthLoaded, 'fourth is not loaded') - app.use(second) - t.ok(firstLoaded, 'first is loaded') - t.notOk(secondLoaded, 'second is not loaded') - t.notOk(thirdLoaded, 'third is not loaded') - t.notOk(fourthLoaded, 'fourth is not loaded') - await app.use(third) - t.ok(firstLoaded, 'first is loaded') - t.ok(secondLoaded, 'second is loaded') - t.ok(thirdLoaded, 'third is loaded') - t.ok(fourthLoaded, 'fourth is loaded') - await app.ready() - t.ok(firstLoaded, 'first is loaded') - t.ok(secondLoaded, 'second is loaded') - t.ok(thirdLoaded, 'third is loaded') - t.ok(fourthLoaded, 'fourth is loaded') - - async function first () { - firstLoaded = true - } - - async function second () { - secondLoaded = true - } - - async function third (app) { - thirdLoaded = true - app.use(fourth) - } - - async function fourth () { - fourthLoaded = true - } -}) - -test('await use - mix of same and future tick callbacks', async (t) => { - const app = {} - boot(app, { autostart: false }) - let record = '' - - t.plan(4) - - await app.use(async function plugin0 () { - t.pass('plugin0 init') - record += 'plugin0|' - }) - await app.use(async function plugin1 () { - t.pass('plugin1 init') - await sleep(500) - record += 'plugin1|' - }) - await sleep(1) - await app.use(async function plugin2 () { - t.pass('plugin2 init') - await sleep(500) - record += 'plugin2|' - }) - record += 'ready' - t.equal(record, 'plugin0|plugin1|plugin2|ready') -}) - -test('await use - fork the promise chain', (t) => { - t.plan(3) - const app = {} - boot(app, { autostart: false }) - - async function setup () { - let set = false - await app.use(async function plugin0 () { - t.pass('plugin0 init') - await sleep(500) - set = true - }) - t.equal(set, true) - } - setup() - - app.ready((err, done) => { - t.error(err) - done() - }) -}) diff --git a/test/basic.test.js b/test/basic.test.js index 9a56e4f..d6b3957 100644 --- a/test/basic.test.js +++ b/test/basic.test.js @@ -1,459 +1,98 @@ -'use strict' - const { test } = require('node:test') -const boot = require('..') - -test('boot an empty app', (t, testCompleted) => { - t.plan(1) - const app = boot() - app.on('start', () => { - t.assert.ok(true, 'booted') - testCompleted() - }) -}) - -test('start returns app', (t, testCompleted) => { - t.plan(1) - const app = boot({}, { autostart: false }) - app - .start() - .ready((err) => { - t.assert.ifError(err) - testCompleted() - }) -}) - -test('boot an app with a plugin', (t, testCompleted) => { - t.plan(4) - - const app = boot() - let after = false - - app.use(function (server, opts, done) { - t.assert.deepStrictEqual(server, app, 'the first argument is the server') - t.assert.deepStrictEqual(opts, {}, 'no options') - t.assert.ok(after, 'delayed execution') - done() - }) - - after = true - - app.on('start', () => { - t.assert.ok(true, 'booted') - testCompleted() - }) -}) - -test('boot an app with a promisified plugin', (t, testCompleted) => { - t.plan(4) - - const app = boot() - let after = false - - app.use(function (server, opts) { - t.assert.deepStrictEqual(server, app, 'the first argument is the server') - t.assert.deepStrictEqual(opts, {}, 'no options') - t.assert.ok(after, 'delayed execution') - return Promise.resolve() - }) - - after = true - - app.on('start', () => { - t.assert.ok(true, 'booted') - testCompleted() - }) -}) - -test('boot an app with a plugin and a callback /1', (t, testCompleted) => { - t.plan(2) - - const app = boot(() => { - t.assert.ok(true, 'booted') - }) - - app.use(function (server, opts, done) { - t.assert.ok(true, 'plugin loaded') - done() - testCompleted() - }) -}) - -test('boot an app with a plugin and a callback /2', (t, testCompleted) => { - t.plan(2) +const { Avvio } = require('../avvio') - const app = boot({}, () => { - t.assert.ok(true, 'booted') - }) - - app.use(function (server, opts, done) { - t.assert.ok(true, 'plugin loaded') - done() - testCompleted() - }) -}) - -test('boot a plugin with a custom server', (t, testCompleted) => { - t.plan(4) - - const server = {} - const app = boot(server) - - app.use(function (s, opts, done) { - t.assert.deepStrictEqual(s, server, 'the first argument is the server') - t.assert.deepStrictEqual(opts, {}, 'no options') - done() - }) +test('Avvio#constructor', async (t) => { + const test = t.test.bind(t) + await test('()', (t, done) => { + t.plan(1) - app.onClose(() => { - t.assert.ok(true, 'onClose called') - testCompleted() - }) - - app.on('start', () => { - app.close(() => { + const avvio = Avvio() + avvio.once('start', () => { t.assert.ok(true, 'booted') + done() }) }) -}) - -test('custom instance should inherits avvio methods /1', (t, testCompleted) => { - t.plan(6) - const server = {} - const app = boot(server, {}) + await test('({ autostart: false })', (t, done) => { + t.plan(2) - server.use(function (s, opts, done) { - t.assert.deepStrictEqual(s, server, 'the first argument is the server') - t.assert.deepStrictEqual(opts, {}, 'no options') - done() - }).after(() => { - t.assert.ok(true, 'after called') - }) - - server.onClose(() => { - t.assert.ok(true, 'onClose called') - testCompleted() - }) - - server.ready(() => { - t.assert.ok(true, 'ready called') - }) - - app.on('start', () => { - server.close(() => { + const avvio = Avvio({}, { autostart: false }) + avvio.once('start', () => { t.assert.ok(true, 'booted') + done() }) - }) -}) - -test('custom instance should inherits avvio methods /2', (t, testCompleted) => { - t.plan(6) - - const server = {} - const app = new boot(server, {}) // eslint-disable-line new-cap - - server.use(function (s, opts, done) { - t.assert.deepStrictEqual(s, server, 'the first argument is the server') - t.assert.deepStrictEqual(opts, {}, 'no options') - done() - }).after(() => { - t.assert.ok(true, 'after called') - }) - - server.onClose(() => { - t.assert.ok(true, 'onClose called') - testCompleted() - }) - - server.ready(() => { - t.assert.ok(true, 'ready called') + const app = avvio.start() + t.assert.deepStrictEqual(avvio, app, 'chainable') }) - app.on('start', () => { - server.close(() => { + await test('(callback)', (t, done) => { + t.plan(3) + const avvio = Avvio(() => { t.assert.ok(true, 'booted') + done() }) - }) -}) - -test('boot a plugin with options', (t, testCompleted) => { - t.plan(3) - - const server = {} - const app = boot(server) - const myOpts = { - hello: 'world' - } - - app.use(function (s, opts, done) { - t.assert.deepStrictEqual(s, server, 'the first argument is the server') - t.assert.deepStrictEqual(opts, myOpts, 'passed options') - done() - }, myOpts) - - app.on('start', () => { - t.assert.ok(true, 'booted') - testCompleted() - }) -}) - -test('boot a plugin with a function that returns the options', (t, testCompleted) => { - t.plan(4) - - const server = {} - const app = boot(server) - const myOpts = { - hello: 'world' - } - const myOptsAsFunc = parent => { - t.assert.deepStrictEqual(parent, server) - return parent.myOpts - } - - app.use(function (s, opts, done) { - s.myOpts = opts - done() - }, myOpts) - - app.use(function (s, opts, done) { - t.assert.deepStrictEqual(s, server, 'the first argument is the server') - t.assert.deepStrictEqual(opts, myOpts, 'passed options via function accessing parent injected variable') - done() - }, myOptsAsFunc) - - app.on('start', () => { - t.assert.ok(true, 'booted') - testCompleted() - }) -}) -test('throw on non-function use', (t) => { - t.plan(1) - const app = boot() - t.assert.throws(() => { - app.use({}) - }) -}) - -// https://github.com/mcollina/avvio/issues/20 -test('ready and nextTick', (t, testCompleted) => { - const app = boot() - process.nextTick(() => { - app.ready(() => { - testCompleted() + avvio.use(function (context, options, done) { + t.assert.deepStrictEqual(avvio, context, 'context is the same as avvio') + t.assert.deepStrictEqual(options, {}, 'provides empty options') + done() }) }) -}) -// https://github.com/mcollina/avvio/issues/20 -test('promises and microtask', (t, testCompleted) => { - const app = boot() - Promise.resolve() - .then(() => { - app.ready(function () { - testCompleted() - }) + await test('(context, callback)', (t, done) => { + t.plan(3) + const instance = {} + const avvio = Avvio(instance, () => { + t.assert.ok(true, 'booted') + done() }) -}) - -test('always loads nested plugins after the current one', (t, testCompleted) => { - t.plan(2) - - const server = {} - const app = boot(server) - - let second = false - app.use(function (s, opts, done) { - app.use(function (s, opts, done) { - second = true + avvio.use(function (context, options, done) { + t.assert.deepStrictEqual(instance, context, 'context is the same as specified one') + t.assert.deepStrictEqual(options, {}, 'provides empty options') done() }) - t.assert.ok(!second) - - done() - }) - - app.on('start', () => { - t.assert.ok(true, second) - testCompleted() - }) -}) - -test('promise long resolve', (t, testCompleted) => { - t.plan(2) - - const app = boot() - - setTimeout(function () { - t.assert.throws(() => { - app.use((s, opts, done) => { - done() - }) - }, 'root plugin has already booted') - testCompleted() - }) - - app.ready(function (err) { - t.assert.ok(!err) - }) -}) - -test('do not autostart', (t) => { - const app = boot(null, { - autostart: false - }) - app.on('start', () => { - t.assert.fail() - }) -}) - -test('start with ready', (t, testCompleted) => { - t.plan(2) - - const app = boot(null, { - autostart: false - }) - - app.on('start', () => { - t.assert.ok(true) - testCompleted() - }) - - app.ready(function (err) { - t.assert.ifError(err) }) }) -test('load a plugin after start()', (t, testCompleted) => { - t.plan(1) +test('automatic start with plugin', (t, done) => { + t.plan(4) - let startCalled = false - const app = boot(null, { - autostart: false - }) + const avvio = Avvio() + let after = false - app.use((s, opts, done) => { - t.assert.ok(startCalled) + avvio.use(function (context, options, done) { + t.assert.deepStrictEqual(avvio, context, 'context is the same as avvio') + t.assert.deepStrictEqual(options, {}, 'provides empty options') + t.assert.ok(after, 'delayed execution') done() - testCompleted() - }) - - // we use a timer because - // it is more reliable than - // nextTick and setImmediate - // this almost always will come - // after those are completed - setTimeout(() => { - app.start() - startCalled = true - }, 2) -}) - -test('booted should be set before ready', (t, testCompleted) => { - t.plan(2) - - const app = boot() - - app.ready(function (err) { - t.assert.ifError(err) - t.assert.ok(app.booted) - testCompleted() }) -}) -test('start should be emitted after ready resolves', (t, testCompleted) => { - t.plan(1) - - const app = boot() - let ready = false - - app.ready().then(function () { - ready = true - }) - - app.on('start', function () { - t.assert.ok(ready) - testCompleted() - }) -}) - -test('throws correctly if registering after ready', (t, testCompleted) => { - t.plan(1) - - const app = boot() - - app.ready(function () { - t.assert.throws(() => { - app.use((a, b, done) => done()) - }, 'root plugin has already booted') - testCompleted() - }) -}) - -test('preReady errors must be managed', (t, testCompleted) => { - t.plan(2) - - const app = boot() - - app.use((f, opts, cb) => { - cb() - }) - - app.on('preReady', () => { - throw new Error('boom') - }) - - app.ready(err => { - t.assert.ok(true, 'ready function is called') - t.assert.equal(err.message, 'boom') - testCompleted() - }) -}) - -test('preReady errors do not override plugin\'s errors', (t, testCompleted) => { - t.plan(3) - - const app = boot() - - app.use((f, opts, cb) => { - cb(new Error('baam')) - }) - - app.on('preReady', () => { - t.assert.ok(true, 'preReady is executed') - throw new Error('boom') - }) + after = true - app.ready(err => { - t.assert.ok(true, 'ready function is called') - t.assert.equal(err.message, 'baam') - testCompleted() + avvio.once('start', () => { + t.assert.ok(true, 'booted') + done() }) }) -test('support faux modules', (t, testCompleted) => { +test('automatic start with promisified plugin', (t, done) => { t.plan(4) - const app = boot() + const avvio = Avvio() let after = false - // Faux modules are modules built with TypeScript - // or Babel that they export a .default property. - app.use({ - default: function (server, opts, done) { - t.assert.deepStrictEqual(server, app, 'the first argument is the server') - t.assert.deepStrictEqual(opts, {}, 'no options') - t.assert.ok(true, after, 'delayed execution') - done() - } + avvio.use(function (context, options) { + t.assert.deepStrictEqual(avvio, context, 'context is the same as avvio') + t.assert.deepStrictEqual(options, {}, 'provides empty options') + t.assert.ok(after, 'delayed execution') + return Promise.resolve() }) after = true - app.on('start', () => { + avvio.once('start', () => { t.assert.ok(true, 'booted') - testCompleted() + done() }) }) diff --git a/test/callbacks.test.js b/test/callbacks.test.js deleted file mode 100644 index 3ae90bd..0000000 --- a/test/callbacks.test.js +++ /dev/null @@ -1,116 +0,0 @@ -'use strict' - -const { test } = require('node:test') -const boot = require('..') - -test('reentrant', (t, testCompleted) => { - t.plan(7) - - const app = boot() - let firstLoaded = false - let secondLoaded = false - - app - .use(first) - .after(() => { - t.assert.ok(firstLoaded, 'first is loaded') - t.assert.ok(secondLoaded, 'second is loaded') - t.assert.ok(true) - testCompleted() - }) - - function first (s, opts, done) { - t.assert.strictEqual(firstLoaded, false, 'first is not loaded') - t.assert.strictEqual(secondLoaded, false, 'second is not loaded') - firstLoaded = true - s.use(second) - done() - } - - function second (s, opts, done) { - t.assert.ok(firstLoaded, 'first is loaded') - t.assert.strictEqual(secondLoaded, false, 'second is not loaded') - secondLoaded = true - done() - } -}) - -test('reentrant with callbacks deferred', (t, testCompleted) => { - t.plan(11) - - const app = boot() - let firstLoaded = false - let secondLoaded = false - let thirdLoaded = false - - app.use(first) - - function first (s, opts, done) { - t.assert.strictEqual(firstLoaded, false, 'first is not loaded') - t.assert.strictEqual(secondLoaded, false, 'second is not loaded') - t.assert.strictEqual(thirdLoaded, false, 'third is not loaded') - firstLoaded = true - s.use(second) - setTimeout(() => { - try { - s.use(third) - } catch (err) { - t.assert.strictEqual(err.message, 'Root plugin has already booted') - } - testCompleted() - }, 500) - done() - } - - function second (s, opts, done) { - t.assert.ok(firstLoaded, 'first is loaded') - t.assert.strictEqual(secondLoaded, false, 'second is not loaded') - t.assert.strictEqual(thirdLoaded, false, 'third is not loaded') - secondLoaded = true - done() - } - - function third (s, opts, done) { - thirdLoaded = true - done() - } - - app.on('start', () => { - t.assert.ok(firstLoaded, 'first is loaded') - t.assert.ok(secondLoaded, 'second is loaded') - t.assert.strictEqual(thirdLoaded, false, 'third is not loaded') - t.assert.ok(true) - }) -}) - -test('multiple loading time', (t, testCompleted) => { - t.plan(1) - const app = boot() - - function a (instance, opts, done) { - (opts.use || []).forEach(_ => { instance.use(_, { use: opts.subUse || [] }) }) - setTimeout(done, 10) - } - const pointer = a - - function b (instance, opts, done) { - (opts.use || []).forEach(_ => { instance.use(_, { use: opts.subUse || [] }) }) - setTimeout(done, 20) - } - - function c (instance, opts, done) { - (opts.use || []).forEach(_ => { instance.use(_, { use: opts.subUse || [] }) }) - setTimeout(done, 30) - } - - app - .use(function a (instance, opts, done) { - instance.use(pointer, { use: [b], subUse: [c] }) - .use(b) - setTimeout(done, 0) - }) - .after(() => { - t.assert.ok(true) - testCompleted() - }) -}) diff --git a/test/catch-override-exception.test.js b/test/catch-override-exception.test.js deleted file mode 100644 index fdcd58a..0000000 --- a/test/catch-override-exception.test.js +++ /dev/null @@ -1,26 +0,0 @@ -'use strict' - -const { test } = require('tap') -const boot = require('..') - -test('catch exceptions in parent.override', (t) => { - t.plan(2) - - const server = {} - - const app = boot(server, { - autostart: false - }) - app.override = function () { - throw Error('catch it') - } - - app - .use(function () {}) - .start() - - app.ready(function (err) { - t.type(err, Error) - t.match(err, /catch it/) - }) -}) diff --git a/test/chainable.test.js b/test/chainable.test.js deleted file mode 100644 index d07a7ac..0000000 --- a/test/chainable.test.js +++ /dev/null @@ -1,67 +0,0 @@ -'use strict' - -const { test } = require('tap') -const boot = require('..') - -test('chainable standalone', (t) => { - t.plan(5) - - const readyResult = boot() - .use(function (ctx, opts, done) { - t.pass('1st plugin') - done() - }).after(function (err, done) { - t.error(err) - t.pass('2nd after') - done() - }).ready(function () { - t.pass('we are ready') - }) - t.equal(readyResult, undefined) -}) - -test('chainable automatically binded', (t) => { - t.plan(5) - - const app = {} - boot(app) - - const readyResult = app - .use(function (ctx, opts, done) { - t.pass('1st plugin') - done() - }).after(function (err, done) { - t.error(err) - t.pass('2nd after') - done() - }).ready(function () { - t.pass('we are ready') - }) - t.equal(readyResult, undefined) -}) - -test('chainable standalone with server', (t) => { - t.plan(6) - - const server = {} - boot(server, { - expose: { - use: 'register' - } - }) - - const readyResult = server.register(function (ctx, opts, done) { - t.pass('1st plugin') - done() - }).after(function (err, done) { - t.error(err) - t.pass('2nd after') - done() - }).register(function (ctx, opts, done) { - t.pass('3rd plugin') - done() - }).ready(function () { - t.pass('we are ready') - }) - t.equal(readyResult, undefined) -}) diff --git a/test/close.test.js b/test/close.test.js deleted file mode 100644 index a02914b..0000000 --- a/test/close.test.js +++ /dev/null @@ -1,544 +0,0 @@ -'use strict' - -const { test } = require('tap') -const boot = require('..') -const { AVV_ERR_CALLBACK_NOT_FN } = require('../lib/errors') - -test('boot an app with a plugin', (t) => { - t.plan(4) - - const app = boot() - let last = false - - app.use(function (server, opts, done) { - app.onClose(() => { - t.ok('onClose called') - t.notOk(last) - last = true - }) - done() - }) - - app.on('start', () => { - app.close(() => { - t.ok(last) - t.pass('Closed in the correct order') - }) - }) -}) - -test('onClose arguments', (t) => { - t.plan(5) - - const app = boot() - - app.use(function (server, opts, next) { - server.onClose((instance, done) => { - t.ok('called') - t.equal(server, instance) - done() - }) - next() - }) - - app.use(function (server, opts, next) { - server.onClose((instance) => { - t.ok('called') - t.equal(server, instance) - }) - next() - }) - - app.on('start', () => { - app.close(() => { - t.pass('Closed in the correct order') - }) - }) -}) - -test('onClose arguments - fastify encapsulation test case', (t) => { - t.plan(5) - - const server = { my: 'server' } - const app = boot(server) - - app.override = function (s, fn, opts) { - s = Object.create(s) - return s - } - - app.use(function (instance, opts, next) { - instance.test = true - instance.onClose((i, done) => { - t.ok(i.test) - done() - }) - next() - }) - - app.use(function (instance, opts, next) { - t.notOk(instance.test) - instance.onClose((i, done) => { - t.notOk(i.test) - done() - }) - next() - }) - - app.on('start', () => { - t.notOk(app.test) - app.close(() => { - t.pass('Closed in the correct order') - }) - }) -}) - -test('onClose arguments - fastify encapsulation test case / 2', (t) => { - t.plan(5) - - const server = { my: 'server' } - const app = boot(server) - - app.override = function (s, fn, opts) { - s = Object.create(s) - return s - } - - server.use(function (instance, opts, next) { - instance.test = true - instance.onClose((i, done) => { - t.ok(i.test) - done() - }) - next() - }) - - server.use(function (instance, opts, next) { - t.notOk(instance.test) - instance.onClose((i, done) => { - t.notOk(i.test) - done() - }) - next() - }) - - app.on('start', () => { - t.notOk(server.test) - try { - server.close() - t.pass() - } catch (err) { - t.fail(err) - } - }) -}) - -test('onClose arguments - encapsulation test case no server', (t) => { - t.plan(5) - - const app = boot() - - app.override = function (s, fn, opts) { - s = Object.create(s) - return s - } - - app.use(function (instance, opts, next) { - instance.test = true - instance.onClose((i, done) => { - t.notOk(i.test) - done() - }) - next() - }) - - app.use(function (instance, opts, next) { - t.notOk(instance.test) - instance.onClose((i) => { - t.notOk(i.test) - }) - next() - }) - - app.on('start', () => { - t.notOk(app.test) - app.close(() => { - t.pass('Closed in the correct order') - }) - }) -}) - -test('onClose should handle errors', (t) => { - t.plan(3) - - const app = boot() - - app.use(function (server, opts, done) { - app.onClose((instance, done) => { - t.ok('called') - done(new Error('some error')) - }) - done() - }) - - app.on('start', () => { - app.close(err => { - t.equal(err.message, 'some error') - t.pass('Closed in the correct order') - }) - }) -}) - -test('#54 close handlers should receive same parameters when queue is not empty', (t) => { - t.plan(6) - - const context = { test: true } - const app = boot(context) - - app.use(function (server, opts, done) { - done() - }) - app.on('start', () => { - app.close((err, done) => { - t.equal(err, null) - t.pass('Closed in the correct order') - setImmediate(done) - }) - app.close(err => { - t.equal(err, null) - t.pass('Closed in the correct order') - }) - app.close(err => { - t.equal(err, null) - t.pass('Closed in the correct order') - }) - }) -}) - -test('onClose should handle errors / 2', (t) => { - t.plan(4) - - const app = boot() - - app.onClose((instance, done) => { - t.ok('called') - done(new Error('some error')) - }) - - app.use(function (server, opts, done) { - app.onClose((instance, done) => { - t.ok('called') - done() - }) - done() - }) - - app.on('start', () => { - app.close(err => { - t.equal(err.message, 'some error') - t.pass('Closed in the correct order') - }) - }) -}) - -test('close arguments', (t) => { - t.plan(4) - - const app = boot() - - app.use(function (server, opts, done) { - app.onClose((instance, done) => { - t.ok('called') - done() - }) - done() - }) - - app.on('start', () => { - app.close((err, instance, done) => { - t.error(err) - t.equal(instance, app) - done() - t.pass('Closed in the correct order') - }) - }) -}) - -test('close event', (t) => { - t.plan(3) - - const app = boot() - let last = false - - app.on('start', () => { - app.close(() => { - t.notOk(last) - last = true - }) - }) - - app.on('close', () => { - t.ok(last) - t.pass('event fired') - }) -}) - -test('close order', (t) => { - t.plan(5) - - const app = boot() - const order = [1, 2, 3, 4] - - app.use(function (server, opts, done) { - app.onClose(() => { - t.equal(order.shift(), 3) - }) - - app.use(function (server, opts, done) { - app.onClose(() => { - t.equal(order.shift(), 2) - }) - done() - }) - done() - }) - - app.use(function (server, opts, done) { - app.onClose(() => { - t.equal(order.shift(), 1) - }) - done() - }) - - app.on('start', () => { - app.close(() => { - t.equal(order.shift(), 4) - t.pass('Closed in the correct order') - }) - }) -}) - -test('close without a cb', (t) => { - t.plan(1) - - const app = boot() - - app.onClose((instance, done) => { - t.ok('called') - done() - }) - - app.close() -}) - -test('onClose with 0 parameters', (t) => { - t.plan(4) - - const server = { my: 'server' } - const app = boot(server) - - app.use(function (instance, opts, next) { - instance.onClose(function () { - t.ok('called') - t.equal(arguments.length, 0) - }) - next() - }) - - app.close(err => { - t.error(err) - t.pass('Closed') - }) -}) - -test('onClose with 1 parameter', (t) => { - t.plan(3) - - const server = { my: 'server' } - const app = boot(server) - - app.use(function (instance, opts, next) { - instance.onClose(function (context) { - t.equal(arguments.length, 1) - }) - next() - }) - - app.close(err => { - t.error(err) - t.pass('Closed') - }) -}) - -test('close passing not a function', (t) => { - t.plan(1) - - const app = boot() - - app.onClose((instance, done) => { - t.ok('called') - done() - }) - - t.throws(() => app.close({}), { message: 'not a function' }) -}) - -test('close passing not a function', (t) => { - t.plan(1) - - const app = boot() - - app.onClose((instance, done) => { - t.ok('called') - done() - }) - - t.throws(() => app.close({}), { message: 'not a function' }) -}) - -test('close passing not a function when wrapping', (t) => { - t.plan(1) - - const app = {} - boot(app) - - app.onClose((instance, done) => { - t.ok('called') - done() - }) - - t.throws(() => app.close({}), { message: 'not a function' }) -}) - -test('close should trigger ready()', (t) => { - t.plan(2) - - const app = boot(null, { - autostart: false - }) - - app.on('start', () => { - // this will be emitted after the - // callback in close() is fired - t.pass('started') - }) - - app.close(() => { - t.pass('closed') - }) -}) - -test('close without a cb returns a promise', (t) => { - t.plan(1) - - const app = boot() - app.close().then(() => { - t.pass('promise resolves') - }) -}) - -test('close without a cb returns a promise when attaching to a server', (t) => { - t.plan(1) - - const server = {} - boot(server) - server.close().then(() => { - t.pass('promise resolves') - }) -}) - -test('close with async onClose handlers', t => { - t.plan(7) - - const app = boot() - const order = [1, 2, 3, 4, 5, 6] - - app.onClose(() => { - return new Promise(resolve => setTimeout(resolve, 500)).then(() => { - t.equal(order.shift(), 5) - }) - }) - - app.onClose(() => { - t.equal(order.shift(), 4) - }) - - app.onClose(instance => { - return new Promise(resolve => setTimeout(resolve, 500)).then(() => { - t.equal(order.shift(), 3) - }) - }) - - app.onClose(async instance => { - return new Promise(resolve => setTimeout(resolve, 500)).then(() => { - t.equal(order.shift(), 2) - }) - }) - - app.onClose(async () => { - return new Promise(resolve => setTimeout(resolve, 500)).then(() => { - t.equal(order.shift(), 1) - }) - }) - - app.on('start', () => { - app.close(() => { - t.equal(order.shift(), 6) - t.pass('Closed in the correct order') - }) - }) -}) - -test('onClose callback must be a function', (t) => { - t.plan(1) - - const app = boot() - - app.use(function (server, opts, done) { - t.throws(() => app.onClose({}), new AVV_ERR_CALLBACK_NOT_FN('onClose', 'object')) - done() - }) -}) - -test('close custom server with async onClose handlers', t => { - t.plan(7) - - const server = {} - const app = boot(server) - const order = [1, 2, 3, 4, 5, 6] - - server.onClose(() => { - return new Promise(resolve => setTimeout(resolve, 500)).then(() => { - t.equal(order.shift(), 5) - }) - }) - - server.onClose(() => { - t.equal(order.shift(), 4) - }) - - server.onClose(instance => { - return new Promise(resolve => setTimeout(resolve, 500)).then(() => { - t.equal(order.shift(), 3) - }) - }) - - server.onClose(async instance => { - return new Promise(resolve => setTimeout(resolve, 500)).then(() => { - t.equal(order.shift(), 2) - }) - }) - - server.onClose(async () => { - return new Promise(resolve => setTimeout(resolve, 500)).then(() => { - t.equal(order.shift(), 1) - }) - }) - - app.on('start', () => { - app.close(() => { - t.equal(order.shift(), 6) - t.pass('Closed in the correct order') - }) - }) -}) diff --git a/test/errors.test.js b/test/errors.test.js deleted file mode 100644 index 44c0787..0000000 --- a/test/errors.test.js +++ /dev/null @@ -1,26 +0,0 @@ -'use strict' - -const { test } = require('node:test') -const errors = require('../lib/errors') - -test('Correct codes of AvvioErrors', t => { - const testcases = [ - 'AVV_ERR_EXPOSE_ALREADY_DEFINED', - 'AVV_ERR_ATTRIBUTE_ALREADY_DEFINED', - 'AVV_ERR_CALLBACK_NOT_FN', - 'AVV_ERR_PLUGIN_NOT_VALID', - 'AVV_ERR_ROOT_PLG_BOOTED', - 'AVV_ERR_PARENT_PLG_LOADED', - 'AVV_ERR_READY_TIMEOUT', - 'AVV_ERR_PLUGIN_EXEC_TIMEOUT' - ] - - t.plan(testcases.length + 1) - // errors.js exposes errors and the createError fn - t.assert.strictEqual(testcases.length, Object.keys(errors).length) - - for (const testcase of testcases) { - const error = new errors[testcase]() - t.assert.strictEqual(error.code, testcase) - } -}) diff --git a/test/esm.mjs b/test/esm.mjs deleted file mode 100644 index df53d64..0000000 --- a/test/esm.mjs +++ /dev/null @@ -1,12 +0,0 @@ -import { test } from 'tap' -import boot from '../boot.js' - -test('support import', async (t) => { - const app = boot() - - app.use(import('./fixtures/esm.mjs')) - - await app.ready() - - t.equal(app.loaded, true) -}) diff --git a/test/esm.test.js b/test/esm.test.js deleted file mode 100644 index d930881..0000000 --- a/test/esm.test.js +++ /dev/null @@ -1,14 +0,0 @@ -'use strict' - -const { test } = require('tap') - -test('support esm import', (t) => { - import('./esm.mjs').then(() => { - t.pass('esm is supported') - t.end() - }).catch((err) => { - process.nextTick(() => { - throw err - }) - }) -}) diff --git a/test/events-listeners.test.js b/test/events-listeners.test.js deleted file mode 100644 index 556ea67..0000000 --- a/test/events-listeners.test.js +++ /dev/null @@ -1,23 +0,0 @@ -'use strict' - -const { test } = require('node:test') -const boot = require('..') -const noop = () => {} - -test('boot a plugin and then execute a call after that', (t, testDone) => { - t.plan(1) - - process.on('warning', (warning) => { - t.assert.fail('we should not get a warning') - }) - - const app = boot() - for (let i = 0; i < 12; i++) { - app.on('preReady', noop) - } - - setTimeout(() => { - t.assert.ok('Everything ok') - testDone() - }, 500) -}) diff --git a/test/expose.test.js b/test/expose.test.js deleted file mode 100644 index 422524e..0000000 --- a/test/expose.test.js +++ /dev/null @@ -1,80 +0,0 @@ -'use strict' - -const { test } = require('tap') -const boot = require('..') -const { AVV_ERR_EXPOSE_ALREADY_DEFINED, AVV_ERR_ATTRIBUTE_ALREADY_DEFINED } = require('../lib/errors') -const { kAvvio } = require('../lib/symbols') - -for (const key of ['use', 'after', 'ready', 'onClose', 'close']) { - test('throws if ' + key + ' is by default already there', (t) => { - t.plan(1) - - const app = {} - app[key] = () => { } - - t.throws(() => boot(app), new AVV_ERR_EXPOSE_ALREADY_DEFINED(key, key)) - }) - - test('throws if ' + key + ' is already there', (t) => { - t.plan(1) - - const app = {} - app['cust' + key] = () => { } - - t.throws(() => boot(app, { expose: { [key]: 'cust' + key } }), new AVV_ERR_EXPOSE_ALREADY_DEFINED('cust' + key, key)) - }) - - test('support expose for ' + key, (t) => { - const app = {} - app[key] = () => { } - - const expose = {} - expose[key] = 'muahah' - - boot(app, { - expose - }) - - t.end() - }) -} - -test('set the kAvvio to true on the server', (t) => { - t.plan(1) - - const server = {} - boot(server) - - t.ok(server[kAvvio]) -}) - -test('.then()', t => { - t.plan(3) - - t.test('.then() can not be overwritten', (t) => { - t.plan(1) - - const server = { - then: () => {} - } - t.throws(() => boot(server), AVV_ERR_ATTRIBUTE_ALREADY_DEFINED('then')) - }) - - t.test('.then() is a function', (t) => { - t.plan(1) - - const server = {} - boot(server) - - t.type(server.then, 'function') - }) - - t.test('.then() can not be overwritten', (t) => { - t.plan(1) - - const server = {} - boot(server) - - t.throws(() => { server.then = 'invalid' }, TypeError('Cannot set property then of # which has only a getter')) - }) -}) diff --git a/test/fixtures/dummy.txt b/test/fixtures/dummy.txt deleted file mode 100644 index 30f51a3..0000000 --- a/test/fixtures/dummy.txt +++ /dev/null @@ -1 +0,0 @@ -hello, world! \ No newline at end of file diff --git a/test/fixtures/esm.mjs b/test/fixtures/esm.mjs deleted file mode 100644 index ae3574d..0000000 --- a/test/fixtures/esm.mjs +++ /dev/null @@ -1,3 +0,0 @@ -export default async function (app) { - app.loaded = true -} diff --git a/test/fixtures/plugin-no-next.js b/test/fixtures/plugin-no-next.js deleted file mode 100644 index cc19b06..0000000 --- a/test/fixtures/plugin-no-next.js +++ /dev/null @@ -1,5 +0,0 @@ -'use strict' - -module.exports = function noNext (app, opts, next) { - // no call to next -} diff --git a/test/gh-issues/bug-205.test.js b/test/gh-issues/bug-205.test.js deleted file mode 100644 index 1dbd4df..0000000 --- a/test/gh-issues/bug-205.test.js +++ /dev/null @@ -1,16 +0,0 @@ -'use strict' - -const { test } = require('tap') -const boot = require('../..') - -test('should print the time tree', (t) => { - t.plan(2) - const app = boot() - - app.use(function first (instance, opts, cb) { - const out = instance.prettyPrint().split('\n') - t.equal(out[0], 'root -1 ms') - t.equal(out[1], '└── first -1 ms') - cb() - }) -}) diff --git a/test/inheritance.test.js b/test/inheritance.test.js new file mode 100644 index 0000000..9180f23 --- /dev/null +++ b/test/inheritance.test.js @@ -0,0 +1,15 @@ +const test = require('node:test').test +const EventEmitter = require('node:events').EventEmitter +const Avvio = require('../avvio') + +test('avvio instanceof Avvio', function (t) { + t.plan(1) + const avvio = new Avvio() + t.assert.ok(avvio instanceof Avvio) +}) + +test('avvio instanceof EventEmitter', function (t) { + t.plan(1) + const avvio = new Avvio() + t.assert.ok(avvio instanceof EventEmitter) +}) diff --git a/test/lib/create-promise.test.js b/test/lib/create-promise.test.js deleted file mode 100644 index 99ed5c6..0000000 --- a/test/lib/create-promise.test.js +++ /dev/null @@ -1,55 +0,0 @@ -'use strict' - -const { test } = require('tap') -const { createPromise } = require('../../lib/create-promise') - -test('createPromise() returns an object', (t) => { - t.plan(3) - t.type(createPromise(), 'object') - t.equal(Array.isArray(createPromise()), false) - t.notOk(Array.isArray(createPromise() !== null)) -}) - -test('createPromise() returns an attribute with attribute resolve', (t) => { - t.plan(1) - t.ok('resolve' in createPromise()) -}) - -test('createPromise() returns an attribute with attribute reject', (t) => { - t.plan(1) - t.ok('reject' in createPromise()) -}) - -test('createPromise() returns an attribute with attribute createPromise', (t) => { - t.plan(1) - t.ok('promise' in createPromise()) -}) - -test('when resolve is called, createPromise attribute is resolved', (t) => { - t.plan(1) - const p = createPromise() - - p.promise - .then(() => { - t.pass() - }) - .catch(() => { - t.fail() - }) - p.resolve() -}) - -test('when reject is called, createPromise attribute is rejected', (t) => { - t.plan(1) - const p = createPromise() - - p.promise - .then(() => { - t.fail() - }) - .catch(() => { - t.pass() - }) - - p.reject() -}) diff --git a/test/lib/execute-with-thenable.test.js b/test/lib/execute-with-thenable.test.js deleted file mode 100644 index f51ac4e..0000000 --- a/test/lib/execute-with-thenable.test.js +++ /dev/null @@ -1,82 +0,0 @@ -'use strict' - -const { test } = require('tap') -const { executeWithThenable } = require('../../lib/execute-with-thenable') -const { kAvvio } = require('../../lib/symbols') - -test('executeWithThenable', (t) => { - t.plan(6) - - t.test('passes the arguments to the function', (t) => { - t.plan(5) - - executeWithThenable((...args) => { - t.equal(args.length, 3) - t.equal(args[0], 1) - t.equal(args[1], 2) - t.equal(args[2], 3) - }, [1, 2, 3], (err) => { - t.error(err) - }) - }) - - t.test('function references this to itself', (t) => { - t.plan(2) - - const func = function () { - t.equal(this, func) - } - executeWithThenable(func, [], (err) => { - t.error(err) - }) - }) - - t.test('handle resolving Promise of func', (t) => { - t.plan(1) - - const fn = function () { - return Promise.resolve(42) - } - - executeWithThenable(fn, [], (err) => { - t.error(err) - }) - }) - - t.test('handle rejecting Promise of func', (t) => { - t.plan(1) - - const fn = function () { - return Promise.reject(new Error('Arbitrary Error')) - } - - executeWithThenable(fn, [], (err) => { - t.equal(err.message, 'Arbitrary Error') - }) - }) - - t.test('dont handle avvio mocks PromiseLike results but use callback if provided', (t) => { - t.plan(1) - - const fn = function () { - const result = Promise.resolve(42) - result[kAvvio] = true - } - - executeWithThenable(fn, [], (err) => { - t.error(err) - }) - }) - - t.test('dont handle avvio mocks Promises and if no callback is provided', (t) => { - t.plan(1) - - const fn = function () { - t.pass(1) - const result = Promise.resolve(42) - result[kAvvio] = true - } - - executeWithThenable(fn, []) - }) -}) diff --git a/test/lib/get-plugin-name.test.js b/test/lib/get-plugin-name.test.js deleted file mode 100644 index 2cb3853..0000000 --- a/test/lib/get-plugin-name.test.js +++ /dev/null @@ -1,67 +0,0 @@ -'use strict' - -const { test } = require('tap') -const { getPluginName } = require('../../lib/get-plugin-name') -const { kPluginMeta } = require('../../lib/symbols') - -test('getPluginName of function', (t) => { - t.plan(1) - - t.equal(getPluginName(function aPlugin () { }), 'aPlugin') -}) - -test('getPluginName of async function', (t) => { - t.plan(1) - - t.equal(getPluginName(async function aPlugin () { }), 'aPlugin') -}) - -test('getPluginName of arrow function without name', (t) => { - t.plan(2) - - t.equal(getPluginName(() => { }), '() => { }') - t.equal(getPluginName(() => { return 'random' }), '() => { return \'random\' }') -}) - -test('getPluginName of arrow function assigned to variable', (t) => { - t.plan(1) - - const namedArrowFunction = () => { } - t.equal(getPluginName(namedArrowFunction), 'namedArrowFunction') -}) - -test("getPluginName based on Symbol 'plugin-meta' /1", (t) => { - t.plan(1) - - function plugin () { - - } - - plugin[kPluginMeta] = {} - t.equal(getPluginName(plugin), 'plugin') -}) - -test("getPluginName based on Symbol 'plugin-meta' /2", (t) => { - t.plan(1) - - function plugin () { - - } - - plugin[kPluginMeta] = { - name: 'fastify-non-existent' - } - t.equal(getPluginName(plugin), 'fastify-non-existent') -}) - -test('getPluginName if null is provided as options', (t) => { - t.plan(1) - - t.equal(getPluginName(function a () {}, null), 'a') -}) - -test('getPluginName if name is provided in options', (t) => { - t.plan(1) - - t.equal(getPluginName(function defaultName () {}, { name: 'providedName' }), 'providedName') -}) diff --git a/test/lib/is-bundled-or-typescript-plugin.test.js b/test/lib/is-bundled-or-typescript-plugin.test.js deleted file mode 100644 index b81029b..0000000 --- a/test/lib/is-bundled-or-typescript-plugin.test.js +++ /dev/null @@ -1,20 +0,0 @@ -'use strict' - -const { test } = require('tap') -const { isBundledOrTypescriptPlugin } = require('../../lib/is-bundled-or-typescript-plugin') - -test('isBundledOrTypescriptPlugin', (t) => { - t.plan(9) - - t.equal(isBundledOrTypescriptPlugin(1), false) - t.equal(isBundledOrTypescriptPlugin('function'), false) - t.equal(isBundledOrTypescriptPlugin({}), false) - t.equal(isBundledOrTypescriptPlugin([]), false) - t.equal(isBundledOrTypescriptPlugin(null), false) - - t.equal(isBundledOrTypescriptPlugin(function () {}), false) - t.equal(isBundledOrTypescriptPlugin(new Promise((resolve) => resolve)), false) - t.equal(isBundledOrTypescriptPlugin(Promise.resolve()), false) - - t.equal(isBundledOrTypescriptPlugin({ default: () => {} }), true) -}) diff --git a/test/lib/is-promise-like.test.js b/test/lib/is-promise-like.test.js deleted file mode 100644 index f140766..0000000 --- a/test/lib/is-promise-like.test.js +++ /dev/null @@ -1,20 +0,0 @@ -'use strict' - -const { test } = require('tap') -const { isPromiseLike } = require('../../lib/is-promise-like') - -test('isPromiseLike', (t) => { - t.plan(9) - - t.equal(isPromiseLike(1), false) - t.equal(isPromiseLike('function'), false) - t.equal(isPromiseLike({}), false) - t.equal(isPromiseLike([]), false) - t.equal(isPromiseLike(null), false) - - t.equal(isPromiseLike(function () {}), false) - t.equal(isPromiseLike(new Promise((resolve) => resolve)), true) - t.equal(isPromiseLike(Promise.resolve()), true) - - t.equal(isPromiseLike({ then: () => {} }), true) -}) diff --git a/test/lib/thenify.test.js b/test/lib/thenify.test.js deleted file mode 100644 index 2004a0a..0000000 --- a/test/lib/thenify.test.js +++ /dev/null @@ -1,123 +0,0 @@ -'use strict' - -const { test, mockRequire } = require('tap') -const { kThenifyDoNotWrap } = require('../../lib/symbols') - -test('thenify', (t) => { - t.plan(7) - - t.test('return undefined if booted', (t) => { - t.plan(2) - - const { thenify } = mockRequire('../../lib/thenify', { - '../../lib/debug': { - debug: (message) => { t.equal(message, 'thenify returning undefined because we are already booted') } - } - }) - const result = thenify.call({ - booted: true - }) - t.equal(result, undefined) - }) - - t.test('return undefined if kThenifyDoNotWrap is true', (t) => { - t.plan(1) - - const { thenify } = require('../../lib/thenify') - const result = thenify.call({ - [kThenifyDoNotWrap]: true - }) - t.equal(result, undefined) - }) - - t.test('return PromiseConstructorLike if kThenifyDoNotWrap is false', (t) => { - t.plan(3) - - const { thenify } = mockRequire('../../lib/thenify', { - '../../lib/debug': { - debug: (message) => { t.equal(message, 'thenify') } - } - }) - const promiseContructorLike = thenify.call({ - [kThenifyDoNotWrap]: false - }) - - t.type(promiseContructorLike, 'function') - t.equal(promiseContructorLike.length, 2) - }) - - t.test('return PromiseConstructorLike', (t) => { - t.plan(3) - - const { thenify } = mockRequire('../../lib/thenify', { - '../../lib/debug': { - debug: (message) => { t.equal(message, 'thenify') } - } - }) - const promiseContructorLike = thenify.call({}) - - t.type(promiseContructorLike, 'function') - t.equal(promiseContructorLike.length, 2) - }) - - t.test('resolve should return _server', async (t) => { - t.plan(1) - - const { thenify } = require('../../lib/thenify') - - const server = { - _loadRegistered: () => { - return Promise.resolve() - }, - _server: 'server' - } - const promiseContructorLike = thenify.call(server) - - promiseContructorLike(function (value) { - t.equal(value, 'server') - }, function (reason) { - t.error(reason) - }) - }) - - t.test('resolving should set kThenifyDoNotWrap to true', async (t) => { - t.plan(1) - - const { thenify } = require('../../lib/thenify') - - const server = { - _loadRegistered: () => { - return Promise.resolve() - }, - [kThenifyDoNotWrap]: false, - _server: 'server' - } - const promiseContructorLike = thenify.call(server) - - promiseContructorLike(function (value) { - t.equal(server[kThenifyDoNotWrap], true) - }, function (reason) { - t.error(reason) - }) - }) - - t.test('rejection should pass through to reject', async (t) => { - t.plan(1) - - const { thenify } = require('../../lib/thenify') - - const server = { - _loadRegistered: () => { - return Promise.reject(new Error('Arbitrary rejection')) - }, - _server: 'server' - } - const promiseContructorLike = thenify.call(server) - - promiseContructorLike(function (value) { - t.error(value) - }, function (reason) { - t.equal(reason.message, 'Arbitrary rejection') - }) - }) -}) diff --git a/test/lib/time-tree.test.js b/test/lib/time-tree.test.js deleted file mode 100644 index 8bef526..0000000 --- a/test/lib/time-tree.test.js +++ /dev/null @@ -1,391 +0,0 @@ -'use strict' - -const { test } = require('tap') -const { TimeTree } = require('../../lib/time-tree') - -test('TimeTree is constructed with a root attribute, set to null', t => { - t.plan(1) - - const tree = new TimeTree() - t.equal(tree.root, null) -}) - -test('TimeTree is constructed with an empty tableId-Map', t => { - t.plan(2) - - const tree = new TimeTree() - t.ok(tree.tableId instanceof Map) - t.equal(tree.tableId.size, 0) -}) - -test('TimeTree is constructed with an empty tableLabel-Map', t => { - t.plan(2) - - const tree = new TimeTree() - t.ok(tree.tableLabel instanceof Map) - t.equal(tree.tableLabel.size, 0) -}) - -test('TimeTree#toJSON dumps the content of the TimeTree', t => { - t.plan(1) - - const tree = new TimeTree() - t.same(tree.toJSON(), {}) -}) - -test('TimeTree#toJSON is creating new instances of its content, ensuring being immutable', t => { - t.plan(1) - - const tree = new TimeTree() - t.not(tree.toJSON(), tree.toJSON()) -}) - -test('TimeTree#start is adding a node with correct shape, root-node', t => { - t.plan(15) - - const tree = new TimeTree() - tree.start(null, 'root') - - const rootNode = tree.root - - t.equal(Object.keys(rootNode).length, 7) - t.ok('parent' in rootNode) - t.equal(rootNode.parent, null) - t.ok('id' in rootNode) - t.type(rootNode.id, 'string') - t.ok('label' in rootNode) - t.type(rootNode.label, 'string') - t.ok('nodes' in rootNode) - t.ok(Array.isArray(rootNode.nodes)) - t.ok('start' in rootNode) - t.ok(Number.isInteger(rootNode.start)) - t.ok('stop' in rootNode) - t.type(rootNode.stop, 'null') - t.ok('diff' in rootNode) - t.type(rootNode.diff, 'number') -}) - -test('TimeTree#start is adding a node with correct shape, child-node', t => { - t.plan(16) - - const tree = new TimeTree() - tree.start(null, 'root') - tree.start('root', 'child') - - const rootNode = tree.root - - t.equal(rootNode.nodes.length, 1) - - const childNode = rootNode.nodes[0] - - t.equal(Object.keys(childNode).length, 7) - t.ok('parent' in childNode) - t.type(childNode.parent, 'string') - t.ok('id' in childNode) - t.type(childNode.id, 'string') - t.ok('label' in childNode) - t.type(childNode.label, 'string') - t.ok('nodes' in childNode) - t.ok(Array.isArray(childNode.nodes)) - t.ok('start' in childNode) - t.ok(Number.isInteger(childNode.start)) - t.ok('stop' in childNode) - t.type(childNode.stop, 'null') - t.ok('diff' in childNode) - t.type(childNode.diff, 'number') -}) - -test('TimeTree#start is adding a root element when parent is null', t => { - t.plan(9) - - const tree = new TimeTree() - tree.start(null, 'root') - - const rootNode = tree.root - - t.type(rootNode, 'object') - t.equal(Object.keys(rootNode).length, 7) - t.equal(rootNode.parent, null) - t.equal(rootNode.id, 'root') - t.equal(rootNode.label, 'root') - t.ok(Array.isArray(rootNode.nodes)) - t.equal(rootNode.nodes.length, 0) - t.ok(Number.isInteger(rootNode.start)) - t.type(rootNode.diff, 'number') -}) - -test('TimeTree#start is adding a root element when parent does not exist', t => { - t.plan(9) - - const tree = new TimeTree() - tree.start('invalid', 'root') - - const rootNode = tree.root - - t.type(rootNode, 'object') - t.equal(Object.keys(rootNode).length, 7) - t.equal(rootNode.parent, null) - t.equal(rootNode.id, 'root') - t.equal(rootNode.label, 'root') - t.ok(Array.isArray(rootNode.nodes)) - t.equal(rootNode.nodes.length, 0) - t.ok(Number.isInteger(rootNode.start)) - t.type(rootNode.diff, 'number') -}) - -test('TimeTree#start parameter start can override automatically generated start time', t => { - t.plan(1) - - const tree = new TimeTree() - tree.start(null, 'root', 1337) - - t.ok(tree.root.start, 1337) -}) - -test('TimeTree#start returns id of root, when adding a root node /1', t => { - t.plan(1) - - const tree = new TimeTree() - t.equal(tree.start(null, 'root'), 'root') -}) - -test('TimeTree#start returns id of root, when adding a root node /2', t => { - t.plan(1) - - const tree = new TimeTree() - t.equal(tree.start(null, '/'), 'root') -}) - -test('TimeTree#start returns id of child, when adding a child node', t => { - t.plan(1) - - const tree = new TimeTree() - tree.start(null, 'root') - t.match(tree.start('root', 'child'), /^child-[0-9.]+$/) -}) - -test('TimeTree tracks node ids /1', t => { - t.plan(3) - - const tree = new TimeTree() - tree.start(null, 'root') - tree.start('root', 'child') - - t.equal(tree.tableId.size, 2) - t.ok(tree.tableId.has('root')) - t.ok(tree.tableId.has(tree.root.nodes[0].id)) -}) - -test('TimeTree tracks node ids /2', t => { - t.plan(4) - - const tree = new TimeTree() - tree.start(null, 'root') - tree.start('root', 'child') - tree.start('child', 'grandchild') - - t.equal(tree.tableId.size, 3) - t.ok(tree.tableId.has('root')) - t.ok(tree.tableId.has(tree.root.nodes[0].id)) - t.ok(tree.tableId.has(tree.root.nodes[0].nodes[0].id)) -}) - -test('TimeTree tracks node ids /3', t => { - t.plan(4) - - const tree = new TimeTree() - tree.start(null, 'root') - tree.start('root', 'child') - tree.start('root', 'child') - - t.equal(tree.tableId.size, 3) - t.ok(tree.tableId.has('root')) - t.ok(tree.tableId.has(tree.root.nodes[0].id)) - t.ok(tree.tableId.has(tree.root.nodes[1].id)) -}) - -test('TimeTree tracks node labels /1', t => { - t.plan(4) - - const tree = new TimeTree() - tree.start(null, 'root') - tree.start('root', 'child') - tree.start('root', 'sibling') - - t.equal(tree.tableLabel.size, 3) - t.ok(tree.tableLabel.has('root')) - t.ok(tree.tableLabel.has('child')) - t.ok(tree.tableLabel.has('sibling')) -}) - -test('TimeTree tracks node labels /2', t => { - t.plan(3) - - const tree = new TimeTree() - tree.start(null, 'root') - tree.start('root', 'child') - tree.start('root', 'child') - - t.equal(tree.tableLabel.size, 2) - t.ok(tree.tableLabel.has('root')) - t.ok(tree.tableLabel.has('child')) -}) - -test('TimeTree#stop returns undefined', t => { - t.plan(1) - - const tree = new TimeTree() - tree.start(null, 'root') - - t.type(tree.stop('root'), 'undefined') -}) - -test('TimeTree#stop sets stop value of node', t => { - t.plan(3) - - const tree = new TimeTree() - tree.start(null, 'root') - t.type(tree.root.stop, 'null') - - tree.stop('root') - t.type(tree.root.stop, 'number') - t.ok(Number.isInteger(tree.root.stop)) -}) - -test('TimeTree#stop parameter stop is used as stop value of node', t => { - t.plan(3) - - const tree = new TimeTree() - tree.start(null, 'root') - t.type(tree.root.stop, 'null') - - tree.stop('root', 1337) - t.type(tree.root.stop, 'number') - t.equal(tree.root.stop, 1337) -}) - -test('TimeTree#stop calculates the diff', t => { - t.plan(4) - - const tree = new TimeTree() - tree.start(null, 'root', 1) - t.type(tree.root.diff, 'number') - t.equal(tree.root.diff, -1) - tree.stop('root', 5) - - t.type(tree.root.diff, 'number') - t.equal(tree.root.diff, 4) -}) - -test('TimeTree#stop does nothing when node is not found', t => { - t.plan(2) - - const tree = new TimeTree() - tree.start(null, 'root') - t.type(tree.root.stop, 'null') - - tree.stop('invalid') - t.type(tree.root.stop, 'null') -}) - -test('TimeTree untracks node ids /1', t => { - t.plan(2) - - const tree = new TimeTree() - tree.start(null, 'root') - tree.start('root', 'child') - - tree.stop(tree.root.nodes[0].id) - t.equal(tree.tableId.size, 1) - t.ok(tree.tableId.has('root')) -}) - -test('TimeTree untracks node ids /2', t => { - t.plan(3) - - const tree = new TimeTree() - tree.start(null, 'root') - tree.start('root', 'child') - tree.start('child', 'grandchild') - - tree.stop(tree.root.nodes[0].nodes[0].id) - - t.equal(tree.tableId.size, 2) - t.ok(tree.tableId.has('root')) - t.ok(tree.tableId.has(tree.root.nodes[0].id)) -}) - -test('TimeTree untracks node ids /3', t => { - t.plan(3) - - const tree = new TimeTree() - tree.start(null, 'root') - tree.start('root', 'child') - tree.start('root', 'child') - - tree.stop(tree.root.nodes[0].id) - - t.equal(tree.tableId.size, 2) - t.ok(tree.tableId.has('root')) - t.ok(tree.tableId.has(tree.root.nodes[1].id)) -}) - -test('TimeTree untracks node ids /4', t => { - t.plan(3) - - const tree = new TimeTree() - tree.start(null, 'root') - tree.start('root', 'child') - tree.start('root', 'child') - - tree.stop(tree.root.nodes[1].id) - - t.equal(tree.tableId.size, 2) - t.ok(tree.tableId.has('root')) - t.ok(tree.tableId.has(tree.root.nodes[0].id)) -}) - -test('TimeTree untracks node labels /1', t => { - t.plan(3) - - const tree = new TimeTree() - tree.start(null, 'root') - tree.start('root', 'child') - tree.start('root', 'sibling') - - tree.stop(tree.root.nodes[1].id) - - t.equal(tree.tableLabel.size, 2) - t.ok(tree.tableLabel.has('root')) - t.ok(tree.tableLabel.has('child')) -}) - -test('TimeTree untracks node labels /2', t => { - t.plan(3) - - const tree = new TimeTree() - tree.start(null, 'root') - tree.start('root', 'child') - tree.start('root', 'sibling') - - tree.stop(tree.root.nodes[0].id) - - t.equal(tree.tableLabel.size, 2) - t.ok(tree.tableLabel.has('root')) - t.ok(tree.tableLabel.has('sibling')) -}) - -test('TimeTree does not untrack label if used by other node', t => { - t.plan(3) - - const tree = new TimeTree() - tree.start(null, 'root') - tree.start('root', 'child') - tree.start('root', 'child') - - tree.stop(tree.root.nodes[0].id) - - t.equal(tree.tableLabel.size, 2) - t.ok(tree.tableLabel.has('root')) - t.ok(tree.tableLabel.has('child')) -}) diff --git a/test/lib/validate-plugin.test.js b/test/lib/validate-plugin.test.js deleted file mode 100644 index a3aa956..0000000 --- a/test/lib/validate-plugin.test.js +++ /dev/null @@ -1,19 +0,0 @@ -'use strict' - -const { test } = require('tap') -const { validatePlugin } = require('../../lib/validate-plugin') -const { AVV_ERR_PLUGIN_NOT_VALID } = require('../../lib/errors') - -test('validatePlugin', (t) => { - t.plan(8) - - t.throws(() => validatePlugin(1), new AVV_ERR_PLUGIN_NOT_VALID('number')) - t.throws(() => validatePlugin('function'), new AVV_ERR_PLUGIN_NOT_VALID('string')) - t.throws(() => validatePlugin({}), new AVV_ERR_PLUGIN_NOT_VALID('object')) - t.throws(() => validatePlugin([]), new AVV_ERR_PLUGIN_NOT_VALID('array')) - t.throws(() => validatePlugin(null), new AVV_ERR_PLUGIN_NOT_VALID('null')) - - t.doesNotThrow(() => validatePlugin(function () {})) - t.doesNotThrow(() => validatePlugin(new Promise((resolve) => resolve))) - t.doesNotThrow(() => validatePlugin(Promise.resolve())) -}) diff --git a/test/load-plugin.test.js b/test/load-plugin.test.js deleted file mode 100644 index cab5346..0000000 --- a/test/load-plugin.test.js +++ /dev/null @@ -1,132 +0,0 @@ -'use strict' - -const fastq = require('fastq') -const boot = require('..') -const { test } = require('node:test') -const { Plugin } = require('../lib/plugin') - -test('successfully load a plugin with sync function', (t, testDone) => { - t.plan(1) - const app = boot({}) - - const plugin = new Plugin(fastq(app, app._loadPluginNextTick, 1), function (instance, opts, done) { - done() - }, false, 0) - - app._loadPlugin(plugin, function (err) { - t.assert.ifError(err) - testDone() - }) -}) - -test('catch an error when loading a plugin with sync function', (t, testDone) => { - t.plan(1) - const app = boot({}) - - const plugin = new Plugin(fastq(app, app._loadPluginNextTick, 1), function (instance, opts, done) { - done(Error('ArbitraryError')) - }, false, 0) - - app._loadPlugin(plugin, function (err) { - t.assert.strictEqual(err.message, 'ArbitraryError') - testDone() - }) -}) - -test('successfully load a plugin with sync function without done as a parameter', (t, testDone) => { - t.plan(1) - const app = boot({}) - - const plugin = new Plugin(fastq(app, app._loadPluginNextTick, 1), function (instance, opts) { }, false, 0) - - app._loadPlugin(plugin, function (err) { - t.assert.ifError(err) - testDone() - }) -}) - -test('successfully load a plugin with async function', (t, testDone) => { - t.plan(1) - const app = boot({}) - - const plugin = new Plugin(fastq(app, app._loadPluginNextTick, 1), async function (instance, opts) { }, false, 0) - - app._loadPlugin(plugin, function (err) { - t.assert.ifError(err) - testDone() - }) -}) - -test('catch an error when loading a plugin with async function', (t, testDone) => { - t.plan(1) - const app = boot({}) - - const plugin = new Plugin(fastq(app, app._loadPluginNextTick, 1), async function (instance, opts) { - throw Error('ArbitraryError') - }, false, 0) - - app._loadPlugin(plugin, function (err) { - t.assert.strictEqual(err.message, 'ArbitraryError') - testDone() - }) -}) - -test('successfully load a plugin when function is a Promise, which resolves to a function', (t, testDone) => { - t.plan(1) - const app = boot({}) - - const plugin = new Plugin(fastq(app, app._loadPluginNextTick, 1), new Promise(resolve => resolve(function (instance, opts, done) { - done() - })), false, 0) - - app._loadPlugin(plugin, function (err) { - t.assert.ifError(err) - testDone() - }) -}) - -test('catch an error when loading a plugin when function is a Promise, which resolves to a function', (t, testDone) => { - t.plan(1) - const app = boot({}) - - const plugin = new Plugin(fastq(app, app._loadPluginNextTick, 1), new Promise(resolve => resolve(function (instance, opts, done) { - done(Error('ArbitraryError')) - })), false, 0) - - app._loadPlugin(plugin, function (err) { - t.assert.strictEqual(err.message, 'ArbitraryError') - testDone() - }) -}) - -test('successfully load a plugin when function is a Promise, which resolves to a function, which is wrapped in default', (t, testDone) => { - t.plan(1) - const app = boot({}) - - const plugin = new Plugin(fastq(app, app._loadPluginNextTick, 1), new Promise(resolve => resolve({ - default: function (instance, opts, done) { - done() - } - })), false, 0) - - app._loadPlugin(plugin, function (err) { - t.assert.ifError(err) - testDone() - }) -}) - -test('catch an error when loading a plugin when function is a Promise, which resolves to a function, which is wrapped in default', (t, testDone) => { - t.plan(1) - const app = boot({}) - - const plugin = new Plugin(fastq(app, app._loadPluginNextTick, 1), new Promise(resolve => resolve({ - default: function (instance, opts, done) { - done(Error('ArbitraryError')) - } - })), false, 0) - - app._loadPlugin(plugin, function (err) { - t.assert.strictEqual(err.message, 'ArbitraryError') - testDone() - }) -}) diff --git a/test/no-done.test.js b/test/no-done.test.js deleted file mode 100644 index a3ca5b8..0000000 --- a/test/no-done.test.js +++ /dev/null @@ -1,19 +0,0 @@ -'use strict' - -const { test } = require('node:test') -const boot = require('..') - -test('not taking done does not throw error.', (t, testDone) => { - t.plan(2) - - const app = boot() - - app.use(noDone).ready((err) => { - t.assert.strictEqual(err, null, 'no error') - testDone() - }) - - function noDone (s, opts) { - t.assert.ok('did not throw') - } -}) diff --git a/test/on-ready-timeout-await.test.js b/test/on-ready-timeout-await.test.js deleted file mode 100644 index 6f90575..0000000 --- a/test/on-ready-timeout-await.test.js +++ /dev/null @@ -1,33 +0,0 @@ -'use strict' - -/* eslint no-prototype-builtins: off */ - -const { test } = require('node:test') -const boot = require('../boot') - -test('onReadyTimeout', async (t) => { - const app = boot({}, { - timeout: 10, // 10 ms - autostart: false - }) - - app.use(function one (innerApp, opts, next) { - t.assert.ok('loaded') - innerApp.ready(function readyNoResolve (err, done) { - t.assert.ifError(err) - t.assert.ok('first ready called') - // Do not call done() to timeout - }) - next() - }) - - await app.start() - - try { - await app.ready() - t.assert.fail('should throw') - } catch (err) { - t.assert.strictEqual(err.message, 'Plugin did not start in time: \'readyNoResolve\'. You may have forgotten to call \'done\' function or to resolve a Promise') - // And not Plugin did not start in time: 'bound _encapsulateThreeParam'. You may have forgotten to call 'done' function or to resolve a Promise - } -}) diff --git a/test/override.test.js b/test/override.test.js deleted file mode 100644 index b92f61e..0000000 --- a/test/override.test.js +++ /dev/null @@ -1,385 +0,0 @@ -'use strict' - -/* eslint no-prototype-builtins: off */ - -const { test } = require('node:test') -const boot = require('..') - -test('custom inheritance', (t, testDone) => { - t.plan(3) - - const server = { my: 'server' } - const app = boot(server) - - app.override = function (s) { - t.assert.deepStrictEqual(s, server) - - const res = Object.create(s) - res.b = 42 - - return res - } - - app.use(function first (s, opts, cb) { - t.assert.notDeepStrictEqual(s, server) - t.assert.ok(Object.prototype.isPrototypeOf.call(server, s)) - cb() - testDone() - }) -}) - -test('custom inheritance multiple levels', (t, testDone) => { - t.plan(6) - - const server = { count: 0 } - const app = boot(server) - - app.override = function (s) { - const res = Object.create(s) - res.count = res.count + 1 - - return res - } - - app.use(function first (s1, opts, cb) { - t.assert.notDeepStrictEqual(s1, server) - t.assert.ok(Object.prototype.isPrototypeOf.call(server, s1)) - t.assert.strictEqual(s1.count, 1) - s1.use(second) - - cb() - - function second (s2, opts, cb) { - t.assert.notDeepStrictEqual(s2, s1) - t.assert.ok(Object.prototype.isPrototypeOf.call(s1, s2)) - t.assert.strictEqual(s2.count, 2) - cb() - testDone() - } - }) -}) - -test('custom inheritance multiple levels twice', (t, testDone) => { - t.plan(10) - - const server = { count: 0 } - const app = boot(server) - - app.override = function (s) { - const res = Object.create(s) - res.count = res.count + 1 - - return res - } - - app.use(function first (s1, opts, cb) { - t.assert.notDeepStrictEqual(s1, server) - t.assert.ok(Object.prototype.isPrototypeOf.call(server, s1)) - t.assert.strictEqual(s1.count, 1) - s1.use(second) - s1.use(third) - let prev - - cb() - - function second (s2, opts, cb) { - prev = s2 - t.assert.notDeepStrictEqual(s2, s1) - t.assert.ok(Object.prototype.isPrototypeOf.call(s1, s2)) - t.assert.strictEqual(s2.count, 2) - cb() - } - - function third (s3, opts, cb) { - t.assert.notDeepStrictEqual(s3, s1) - t.assert.ok(Object.prototype.isPrototypeOf.call(s1, s3)) - t.assert.strictEqual(Object.prototype.isPrototypeOf.call(prev, s3), false) - t.assert.strictEqual(s3.count, 2) - cb() - testDone() - } - }) -}) - -test('custom inheritance multiple levels with multiple heads', (t, testDone) => { - t.plan(13) - - const server = { count: 0 } - const app = boot(server) - - app.override = function (s) { - const res = Object.create(s) - res.count = res.count + 1 - - return res - } - - app.use(function first (s1, opts, cb) { - t.assert.notDeepStrictEqual(s1, server) - t.assert.ok(Object.prototype.isPrototypeOf.call(server, s1)) - t.assert.strictEqual(s1.count, 1) - s1.use(second) - - cb() - - function second (s2, opts, cb) { - t.assert.notDeepStrictEqual(s2, s1) - t.assert.ok(Object.prototype.isPrototypeOf.call(s1, s2)) - t.assert.strictEqual(s2.count, 2) - cb() - } - }) - - app.use(function third (s1, opts, cb) { - t.assert.notDeepStrictEqual(s1, server) - t.assert.ok(Object.prototype.isPrototypeOf.call(server, s1)) - t.assert.strictEqual(s1.count, 1) - s1.use(fourth) - - cb() - - function fourth (s2, opts, cb) { - t.assert.notDeepStrictEqual(s2, s1) - t.assert.ok(Object.prototype.isPrototypeOf.call(s1, s2)) - t.assert.strictEqual(s2.count, 2) - cb() - } - }) - - app.ready(function () { - t.assert.strictEqual(server.count, 0) - testDone() - }) -}) - -test('fastify test case', (t, testDone) => { - t.plan(7) - - const noop = () => {} - - function build () { - const app = boot(server, {}) - app.override = function (s) { - return Object.create(s) - } - - server.add = function (name, fn, cb) { - if (this[name]) return cb(new Error('already existent')) - this[name] = fn - cb() - } - - return server - - function server (req, res) {} - } - - const instance = build() - t.assert.ok(instance.add) - t.assert.ok(instance.use) - - instance.use((i, opts, cb) => { - t.assert.notDeepStrictEqual(i, instance) - t.assert.ok(Object.prototype.isPrototypeOf.call(instance, i)) - - i.add('test', noop, (err) => { - t.assert.ifError(err) - t.assert.ok(i.test) - cb() - }) - }) - - instance.ready(() => { - t.assert.strictEqual(instance.test, undefined) - testDone() - }) -}) - -test('override should pass also the plugin function', (t, testDone) => { - t.plan(3) - - const server = { my: 'server' } - const app = boot(server) - - app.override = function (s, fn) { - t.assert.strictEqual(typeof fn, 'function') - t.assert.deepStrictEqual(fn, first) - return s - } - - app.use(first) - - function first (s, opts, cb) { - t.assert.deepStrictEqual(s, server) - cb() - testDone() - } -}) - -test('skip override - fastify test case', (t, testDone) => { - t.plan(2) - - const server = { my: 'server' } - const app = boot(server) - - app.override = function (s, func) { - if (func[Symbol.for('skip-override')]) { - return s - } - return Object.create(s) - } - - first[Symbol.for('skip-override')] = true - app.use(first) - - function first (s, opts, cb) { - t.assert.deepStrictEqual(s, server) - t.assert.strictEqual(Object.prototype.isPrototypeOf.call(server, s), false) - cb() - testDone() - } -}) - -test('override can receive options object', (t, testDone) => { - t.plan(4) - - const server = { my: 'server' } - const options = { hello: 'world' } - const app = boot(server) - - app.override = function (s, fn, opts) { - t.assert.deepStrictEqual(s, server) - t.assert.deepStrictEqual(opts, options) - - const res = Object.create(s) - res.b = 42 - - return res - } - - app.use(function first (s, opts, cb) { - t.assert.notDeepStrictEqual(s, server) - t.assert.ok(Object.prototype.isPrototypeOf.call(server, s)) - cb() - testDone() - }, options) -}) - -test('override can receive options function', (t, testDone) => { - t.plan(8) - - const server = { my: 'server' } - const options = { hello: 'world' } - const app = boot(server) - - app.override = function (s, fn, opts) { - t.assert.deepStrictEqual(s, server) - if (typeof opts !== 'function') { - t.assert.deepStrictEqual(opts, options) - } - - const res = Object.create(s) - res.b = 42 - res.bar = 'world' - - return res - } - - app.use(function first (s, opts, cb) { - t.assert.notDeepStrictEqual(s, server) - t.assert.ok(Object.prototype.isPrototypeOf.call(server, s)) - s.foo = 'bar' - cb() - }, options) - - app.use(function second (s, opts, cb) { - t.assert.strictEqual(s.foo, undefined) - t.assert.deepStrictEqual(opts, { hello: 'world' }) - t.assert.ok(Object.prototype.isPrototypeOf.call(server, s)) - cb() - testDone() - }, p => ({ hello: p.bar })) -}) - -test('after trigger override', (t, testDone) => { - t.plan(8) - - const server = { count: 0 } - const app = boot(server) - - let overrideCalls = 0 - app.override = function (s, fn, opts) { - overrideCalls++ - const res = Object.create(s) - res.count = res.count + 1 - return res - } - - app - .use(function first (s, opts, cb) { - t.assert.strictEqual(s.count, 1, 'should trigger override') - cb() - }) - .after(function () { - t.assert.strictEqual(overrideCalls, 1, 'after with 0 parameter should not trigger override') - }) - .after(function (err) { - if (err) throw err - t.assert.strictEqual(overrideCalls, 1, 'after with 1 parameter should not trigger override') - }) - .after(function (err, done) { - if (err) throw err - t.assert.strictEqual(overrideCalls, 1, 'after with 2 parameters should not trigger override') - done() - }) - .after(function (err, context, done) { - if (err) throw err - t.assert.strictEqual(overrideCalls, 1, 'after with 3 parameters should not trigger override') - done() - }) - .after(async function () { - t.assert.strictEqual(overrideCalls, 1, 'async after with 0 parameter should not trigger override') - }) - .after(async function (err) { - if (err) throw err - t.assert.strictEqual(overrideCalls, 1, 'async after with 1 parameter should not trigger override') - }) - .after(async function (err, context) { - if (err) throw err - t.assert.strictEqual(overrideCalls, 1, 'async after with 2 parameters should not trigger override') - testDone() - }) -}) - -test('custom inheritance override in after', (t, testDone) => { - t.plan(6) - - const server = { count: 0 } - const app = boot(server) - - app.override = function (s) { - const res = Object.create(s) - res.count = res.count + 1 - - return res - } - - app.use(function first (s1, opts, cb) { - t.assert.notDeepStrictEqual(s1, server) - t.assert.ok(Object.prototype.isPrototypeOf.call(server, s1)) - t.assert.strictEqual(s1.count, 1) - s1.after(() => { - s1.use(second) - }) - - cb() - - function second (s2, opts, cb) { - t.assert.notDeepStrictEqual(s2, s1) - t.assert.ok(Object.prototype.isPrototypeOf.call(s1, s2)) - t.assert.strictEqual(s2.count, 2) - cb() - testDone() - } - }) -}) diff --git a/test/plugin-loaded-so-far.test.js b/test/plugin-loaded-so-far.test.js deleted file mode 100644 index bdad133..0000000 --- a/test/plugin-loaded-so-far.test.js +++ /dev/null @@ -1,84 +0,0 @@ -'use strict' - -const { test } = require('tap') -const fastq = require('fastq') -const boot = require('..') -const { Plugin } = require('../lib/plugin') - -test('loadedSoFar resolves a Promise, if plugin.loaded is set to true', async (t) => { - t.plan(1) - const app = boot({}) - - const plugin = new Plugin(fastq(app, app._loadPluginNextTick, 1), function (instance, opts, done) { - done() - }, false, 0) - - plugin.loaded = true - - await t.resolves(plugin.loadedSoFar()) -}) - -test('loadedSoFar resolves a Promise, if plugin was loaded by avvio', async (t) => { - t.plan(2) - const app = boot({}) - - const plugin = new Plugin(fastq(app, app._loadPluginNextTick, 1), function (instance, opts, done) { - done() - }, false, 0) - - app._loadPlugin(plugin, function (err) { - t.equal(err, undefined) - }) - - await app.ready() - - await t.resolves(plugin.loadedSoFar()) -}) - -test('loadedSoFar resolves a Promise, if .after() has no error', async t => { - t.plan(1) - const app = boot() - - app.after = function (callback) { - callback(null, () => {}) - } - - const plugin = new Plugin(fastq(app, app._loadPluginNextTick, 1), function (instance, opts, done) { - done() - }, false, 0) - - app._loadPlugin(plugin, function () {}) - - await t.resolves(plugin.loadedSoFar()) -}) - -test('loadedSoFar rejects a Promise, if .after() has an error', async t => { - t.plan(1) - const app = boot() - - app.after = function (fn) { - fn(new Error('ArbitraryError'), () => {}) - } - - const plugin = new Plugin(fastq(app, app._loadPluginNextTick, 1), function (instance, opts, done) { - done() - }, false, 0) - - app._loadPlugin(plugin, function () {}) - - await t.rejects(plugin.loadedSoFar(), new Error('ArbitraryError')) -}) - -test('loadedSoFar resolves a Promise, if Plugin is attached to avvio after it the Plugin was instantiated', async t => { - t.plan(1) - - const plugin = new Plugin(fastq(null, null, 1), function (instance, opts, done) { - done() - }, false, 0) - - const promise = plugin.loadedSoFar() - - plugin.server = boot() - plugin.emit('start') - await t.resolves(promise) -}) diff --git a/test/plugin-name.test.js b/test/plugin-name.test.js deleted file mode 100644 index fa129a5..0000000 --- a/test/plugin-name.test.js +++ /dev/null @@ -1,69 +0,0 @@ -'use strict' - -const { test } = require('tap') -const boot = require('..') -const { kPluginMeta } = require('../lib/symbols') - -test('plugins get a name from the plugin metadata if it is set', async (t) => { - t.plan(1) - const app = boot() - - const func = (app, opts, next) => next() - func[kPluginMeta] = { name: 'a-test-plugin' } - app.use(func) - await app.ready() - - t.match(app.toJSON(), { - label: 'root', - nodes: [ - { label: 'a-test-plugin' } - ] - }) -}) - -test('plugins get a name from the options if theres no metadata', async (t) => { - t.plan(1) - const app = boot() - - function testPlugin (app, opts, next) { next() } - app.use(testPlugin, { name: 'test registration options name' }) - await app.ready() - - t.match(app.toJSON(), { - label: 'root', - nodes: [ - { label: 'test registration options name' } - ] - }) -}) - -test('plugins get a name from the function name if theres no name in the options and no metadata', async (t) => { - t.plan(1) - const app = boot() - - function testPlugin (app, opts, next) { next() } - app.use(testPlugin) - await app.ready() - - t.match(app.toJSON(), { - label: 'root', - nodes: [ - { label: 'testPlugin' } - ] - }) -}) - -test('plugins get a name from the function source if theres no other option', async (t) => { - t.plan(1) - const app = boot() - - app.use((app, opts, next) => next()) - await app.ready() - - t.match(app.toJSON(), { - label: 'root', - nodes: [ - { label: '(app, opts, next) => next()' } - ] - }) -}) diff --git a/test/plugin-timeout-await.test.js b/test/plugin-timeout-await.test.js deleted file mode 100644 index 13ecdd8..0000000 --- a/test/plugin-timeout-await.test.js +++ /dev/null @@ -1,33 +0,0 @@ -'use strict' - -/* eslint no-prototype-builtins: off */ - -const { test } = require('tap') -const boot = require('..') - -test('do not load', async (t) => { - const app = boot({}, { timeout: 10 }) - - app.use(first) - - async function first (s, opts) { - await s.use(second) - } - - async function second (s, opts) { - await s.use(third) - } - - function third (s, opts) { - return new Promise((resolve, reject) => { - // no resolve - }) - } - - try { - await app.start() - t.fail('should throw') - } catch (err) { - t.equal(err.message, 'Plugin did not start in time: \'third\'. You may have forgotten to call \'done\' function or to resolve a Promise') - } -}) diff --git a/test/plugin-timeout.test.js b/test/plugin-timeout.test.js deleted file mode 100644 index 22b97ca..0000000 --- a/test/plugin-timeout.test.js +++ /dev/null @@ -1,218 +0,0 @@ -'use strict' - -const { test } = require('tap') -const boot = require('..') - -const message = (name) => `Plugin did not start in time: '${name}'. You may have forgotten to call 'done' function or to resolve a Promise` - -test('timeout without calling next - callbacks', (t) => { - t.plan(4) - const app = boot({}, { - timeout: 10 // 10 ms - }) - app.use(one) - function one (app, opts, next) { - // do not call next on purpose - } - app.ready((err) => { - t.ok(err) - t.equal(err.fn, one) - t.equal(err.message, message('one')) - t.equal(err.code, 'AVV_ERR_PLUGIN_EXEC_TIMEOUT') - }) -}) - -test('timeout without calling next - promises', (t) => { - t.plan(4) - const app = boot({}, { - timeout: 10 // 10 ms - }) - app.use(two) - function two (app, opts) { - return new Promise(function (resolve) { - // do not call resolve on purpose - }) - } - app.ready((err) => { - t.ok(err) - t.equal(err.fn, two) - t.equal(err.message, message('two')) - t.equal(err.code, 'AVV_ERR_PLUGIN_EXEC_TIMEOUT') - }) -}) - -test('timeout without calling next - use file as name', (t) => { - t.plan(3) - const app = boot({}, { - timeout: 10 // 10 ms - }) - app.use(require('./fixtures/plugin-no-next')) - app.ready((err) => { - t.ok(err) - t.equal(err.message, message('noNext')) - t.equal(err.code, 'AVV_ERR_PLUGIN_EXEC_TIMEOUT') - }) -}) - -test('timeout without calling next - use code as name', (t) => { - t.plan(3) - const app = boot({}, { - timeout: 10 // 10 ms - }) - app.use(function (app, opts, next) { - // do not call next on purpose - code as name - }) - - app.ready((err) => { - t.ok(err) - t.equal(err.message, message('function (app, opts, next) { -- // do not call next on purpose - code as name')) - t.equal(err.code, 'AVV_ERR_PLUGIN_EXEC_TIMEOUT') - }) -}) - -test('does not keep going', (t) => { - t.plan(2) - const app = boot({}, { - timeout: 10 // 10 ms - }) - app.use(function three (app, opts, next) { - next(new Error('kaboom')) - }) - app.ready((err) => { - t.ok(err) - t.equal(err.message, 'kaboom') - }) -}) - -test('throw in override without autostart', (t) => { - t.plan(2) - - const server = { my: 'server' } - const app = boot(server, { - timeout: 10, - autostart: false - }) - - app.override = function (s) { - throw new Error('kaboom') - } - - app.use(function (s, opts, cb) { - t.fail('this is never reached') - }) - - setTimeout(function () { - app.ready((err) => { - t.ok(err) - t.equal(err.message, 'kaboom') - }) - }, 20) -}) - -test('timeout without calling next in ready and ignoring the error', (t) => { - t.plan(11) - const app = boot({}, { - timeout: 10, // 10 ms - autostart: false - }) - - let preReady = false - - app.use(function one (app, opts, next) { - t.pass('loaded') - app.ready(function readyOk (err, done) { - t.notOk(err) - t.pass('first ready called') - done() - }) - next() - }) - - app.on('preReady', () => { - t.pass('preReady should be called') - preReady = true - }) - - app.on('start', () => { - t.pass('start should be called') - }) - - app.ready(function onReadyWithoutDone (err, done) { - t.pass('wrong ready called') - t.ok(preReady, 'preReady already called') - t.notOk(err) - // done() // Don't call done - }) - - app.ready(function onReadyTwo (err) { - t.ok(err) - t.equal(err.message, message('onReadyWithoutDone')) - t.equal(err.code, 'AVV_ERR_READY_TIMEOUT') - // don't rethrow the error - }) - - app.start() -}) - -test('timeout without calling next in ready and rethrowing the error', (t) => { - t.plan(11) - const app = boot({}, { - timeout: 10, // 10 ms - autostart: true - }) - - app.use(function one (app, opts, next) { - t.pass('loaded') - app.ready(function readyOk (err, done) { - t.ok(err) - t.equal(err.message, message('onReadyWithoutDone')) - t.equal(err.code, 'AVV_ERR_READY_TIMEOUT') - done(err) - }) - next() - }) - - app.on('preReady', () => { - t.pass('preReady should be called') - }) - - app.on('start', () => { - t.pass('start should be called in any case') - }) - - app.ready(function onReadyWithoutDone (err, done) { - t.pass('wrong ready called') - t.notOk(err) - // done() // Don't call done - }) - - app.ready(function onReadyTwo (err, done) { - t.ok(err) - t.equal(err.message, message('onReadyWithoutDone')) - t.equal(err.code, 'AVV_ERR_READY_TIMEOUT') - done(err) - }) - - app.start() -}) - -test('nested timeout do not crash - await', (t) => { - t.plan(4) - const app = boot({}, { - timeout: 10 // 10 ms - }) - app.use(one) - async function one (app, opts) { - await app.use(two) - } - - function two (app, opts, next) { - // do not call next on purpose - } - app.ready((err) => { - t.ok(err) - t.equal(err.fn, two) - t.equal(err.message, message('two')) - t.equal(err.code, 'AVV_ERR_PLUGIN_EXEC_TIMEOUT') - }) -}) diff --git a/test/plugin.test.js b/test/plugin.test.js new file mode 100644 index 0000000..14aeb76 --- /dev/null +++ b/test/plugin.test.js @@ -0,0 +1,325 @@ +const test = require('node:test').test +const Avvio = require('../avvio') + +test('plugin signatures', async (t) => { + const test = t.test.bind(t) + await test('() => void', (t, done) => { + t.plan(2) + const avvio = Avvio() + avvio.use(() => { + t.assert.ok(true, 'plugin executed') + }) + avvio.ready((error) => { + t.assert.ifError(error) + done() + }) + }) + + await test('() => Promise', (t, done) => { + t.plan(2) + const avvio = Avvio() + avvio.use(() => { + t.assert.ok(true, 'plugin executed') + return Promise.resolve() + }) + avvio.ready((error) => { + t.assert.ifError(error) + done() + }) + }) + + await test('(context) => void', (t, done) => { + t.plan(3) + const avvio = Avvio() + avvio.use((context) => { + t.assert.deepStrictEqual(context, avvio) + t.assert.ok(true, 'plugin executed') + }) + avvio.ready((error) => { + t.assert.ifError(error) + done() + }) + }) + + await test('(context) => Promise', (t, done) => { + t.plan(3) + const avvio = Avvio() + avvio.use((context) => { + t.assert.deepStrictEqual(context, avvio) + t.assert.ok(true, 'plugin executed') + return Promise.resolve() + }) + avvio.ready((error) => { + t.assert.ifError(error) + done() + }) + }) + + await test('(context, options) => void', (t, done) => { + t.plan(4) + const avvio = Avvio() + avvio.use((context, options) => { + t.assert.deepStrictEqual(context, avvio) + t.assert.deepStrictEqual(options, {}) + t.assert.ok(true, 'plugin executed') + }) + avvio.ready((error) => { + t.assert.ifError(error) + done() + }) + }) + + await test('(context, options) => Promise', (t, done) => { + t.plan(4) + const avvio = Avvio() + avvio.use((context, options) => { + t.assert.deepStrictEqual(context, avvio) + t.assert.deepStrictEqual(options, {}) + t.assert.ok(true, 'plugin executed') + return Promise.resolve() + }) + avvio.ready((error) => { + t.assert.ifError(error) + done() + }) + }) + + await test('(context, options, done) => void', (t, done) => { + t.plan(4) + const avvio = Avvio() + avvio.use((context, options, done) => { + t.assert.deepStrictEqual(context, avvio) + t.assert.deepStrictEqual(options, {}) + t.assert.ok(true, 'plugin executed') + done() + }) + avvio.ready((error) => { + t.assert.ifError(error) + done() + }) + }) + + await test('(context, options, done) => Promise', (t, done) => { + t.plan(4) + const avvio = Avvio() + avvio.use((context, options, done) => { + t.assert.deepStrictEqual(context, avvio) + t.assert.deepStrictEqual(options, {}) + t.assert.ok(true, 'plugin executed') + return Promise.resolve() + }) + avvio.ready((error) => { + t.assert.ifError(error) + done() + }) + }) + + await test('async () => Promise', (t, done) => { + t.plan(2) + const avvio = Avvio() + avvio.use(async () => { + t.assert.ok(true, 'plugin executed') + }) + avvio.ready((error) => { + t.assert.ifError(error) + done() + }) + }) + + await test('async (context) => Promise', (t, done) => { + t.plan(3) + const avvio = Avvio() + avvio.use(async (context) => { + t.assert.deepStrictEqual(context, avvio) + t.assert.ok(true, 'plugin executed') + }) + avvio.ready((error) => { + t.assert.ifError(error) + done() + }) + }) + + await test('async (context, options) => Promise', (t, done) => { + t.plan(4) + const avvio = Avvio() + avvio.use(async (context, options) => { + t.assert.deepStrictEqual(context, avvio) + t.assert.deepStrictEqual(options, {}) + t.assert.ok(true, 'plugin executed') + }) + avvio.ready((error) => { + t.assert.ifError(error) + done() + }) + }) + + await test('async (context, options, done) => Promise', (t, done) => { + t.plan(4) + const avvio = Avvio() + avvio.use(async (context, options, done) => { + t.assert.deepStrictEqual(context, avvio) + t.assert.deepStrictEqual(options, {}) + t.assert.ok(true, 'plugin executed') + }) + avvio.ready((error) => { + t.assert.ifError(error) + done() + }) + }) +}) + +test('plugin orders', async (t) => { + const test = t.test.bind(t) + await test('sequential on same level', (t, done) => { + t.plan(5) + let counter = 0 + const avvio = Avvio() + avvio.use(() => { + counter++ + t.assert.strictEqual(counter, 1) + }) + avvio.use(() => { + counter++ + t.assert.strictEqual(counter, 2) + return Promise.resolve() + }) + avvio.use(async () => { + counter++ + t.assert.strictEqual(counter, 3) + }) + avvio.ready((error) => { + t.assert.ifError(error) + t.assert.strictEqual(counter, 3) + done() + }) + }) + + await test('sequential on nested level', (t, done) => { + t.plan(14) + let counter = 0 + const avvio = Avvio() + avvio.use((context) => { + counter++ + t.assert.strictEqual(counter, 1) + context.use(() => { + counter++ + t.assert.strictEqual(counter, 2) + }) + context.use(() => { + counter++ + t.assert.strictEqual(counter, 3) + return Promise.resolve() + }) + context.use(async () => { + counter++ + t.assert.strictEqual(counter, 4) + }) + }) + avvio.use((context) => { + counter++ + t.assert.strictEqual(counter, 5) + context.use(() => { + counter++ + t.assert.strictEqual(counter, 6) + }) + context.use(() => { + counter++ + t.assert.strictEqual(counter, 7) + return Promise.resolve() + }) + context.use(async () => { + counter++ + t.assert.strictEqual(counter, 8) + }) + }) + avvio.use(async (context) => { + counter++ + t.assert.strictEqual(counter, 9) + context.use(() => { + counter++ + t.assert.strictEqual(counter, 10) + }) + context.use(() => { + counter++ + t.assert.strictEqual(counter, 11) + return Promise.resolve() + }) + context.use(async () => { + counter++ + t.assert.strictEqual(counter, 12) + }) + }) + avvio.ready((error) => { + t.assert.ifError(error) + t.assert.strictEqual(counter, 12) + done() + }) + }) + + await test('sequential on deep nested', (t, done) => { + t.plan(15) + let counter = 0 + const avvio = Avvio() + avvio.use((context) => { + counter++ + t.assert.strictEqual(counter, 1) + context.use((context) => { + counter++ + t.assert.strictEqual(counter, 2) + context.use(() => { + counter++ + t.assert.strictEqual(counter, 3) + }) + context.use(() => { + counter++ + t.assert.strictEqual(counter, 4) + return Promise.resolve() + }) + context.use(async () => { + counter++ + t.assert.strictEqual(counter, 5) + }) + }) + context.use(() => { + counter++ + t.assert.strictEqual(counter, 6) + context.use(() => { + counter++ + t.assert.strictEqual(counter, 7) + }) + context.use(() => { + counter++ + t.assert.strictEqual(counter, 8) + return Promise.resolve() + }) + context.use(async () => { + counter++ + t.assert.strictEqual(counter, 9) + context.use(() => { + counter++ + t.assert.strictEqual(counter, 10) + }) + context.use(() => { + counter++ + t.assert.strictEqual(counter, 11) + return Promise.resolve() + }) + context.use(async () => { + counter++ + t.assert.strictEqual(counter, 12) + }) + }) + return Promise.resolve() + }) + context.use(async () => { + counter++ + t.assert.strictEqual(counter, 13) + }) + }) + avvio.ready((error) => { + t.assert.ifError(error) + t.assert.strictEqual(counter, 13) + done() + }) + }) +}) diff --git a/test/pretty-print.test.js b/test/pretty-print.test.js deleted file mode 100644 index 734ab08..0000000 --- a/test/pretty-print.test.js +++ /dev/null @@ -1,75 +0,0 @@ -'use strict' - -const { test } = require('tap') -const boot = require('..') - -test('pretty print', t => { - t.plan(19) - - const app = boot() - app - .use(first) - .use(duplicate, { count: 3 }) - .use(second).after(afterUse).after(after) - .use(duplicate, { count: 2 }) - .use(third).after(after) - .use(duplicate, { count: 1 }) - - const linesExpected = [/^root \d+ ms$/, - /^├── first \d+ ms$/, - /^├─┬ duplicate \d+ ms$/, - /^│ └─┬ duplicate \d+ ms$/, - /^│ {3}└─┬ duplicate \d+ ms$/, - /^│ {5}└── duplicate \d+ ms$/, - /^├── second \d+ ms$/, - /^├─┬ bound _after \d+ ms$/, - /^│ └── afterInsider \d+ ms$/, - /^├── bound _after \d+ ms$/, - /^├─┬ duplicate \d+ ms$/, - /^│ └─┬ duplicate \d+ ms$/, - /^│ {3}└── duplicate \d+ ms$/, - /^├── third \d+ ms$/, - /^├── bound _after \d+ ms$/, - /^└─┬ duplicate \d+ ms$/, - /^ {2}└── duplicate \d+ ms$/, - '' - ] - - app.on('preReady', function show () { - const print = app.prettyPrint() - const lines = print.split('\n') - - t.equal(lines.length, linesExpected.length) - lines.forEach((l, i) => { - t.match(l, linesExpected[i]) - }) - }) - - function first (s, opts, done) { - done() - } - function second (s, opts, done) { - done() - } - function third (s, opts, done) { - done() - } - function after (err, cb) { - cb(err) - } - function afterUse (err, cb) { - app.use(afterInsider) - cb(err) - } - - function afterInsider (s, opts, done) { - done() - } - - function duplicate (instance, opts, cb) { - if (opts.count > 0) { - instance.use(duplicate, { count: opts.count - 1 }) - } - setTimeout(cb, 20) - } -}) diff --git a/test/reentrant.test.js b/test/reentrant.test.js deleted file mode 100644 index 20c7e44..0000000 --- a/test/reentrant.test.js +++ /dev/null @@ -1,126 +0,0 @@ -'use strict' - -const { test } = require('node:test') -const boot = require('..') - -test('one level', (t, testDone) => { - t.plan(13) - - const app = boot() - let firstLoaded = false - let secondLoaded = false - let thirdLoaded = false - - app.use(first) - app.use(third) - - function first (s, opts, done) { - t.assert.strictEqual(firstLoaded, false, 'first is not loaded') - t.assert.strictEqual(secondLoaded, false, 'second is not loaded') - t.assert.strictEqual(thirdLoaded, false, 'third is not loaded') - firstLoaded = true - s.use(second) - done() - } - - function second (s, opts, done) { - t.assert.ok(firstLoaded, 'first is loaded') - t.assert.strictEqual(secondLoaded, false, 'second is not loaded') - t.assert.strictEqual(thirdLoaded, false, 'third is not loaded') - secondLoaded = true - done() - } - - function third (s, opts, done) { - t.assert.ok(firstLoaded, 'first is loaded') - t.assert.ok(secondLoaded, 'second is loaded') - t.assert.strictEqual(thirdLoaded, false, 'third is not loaded') - thirdLoaded = true - done() - } - - app.on('start', () => { - t.assert.ok(firstLoaded, 'first is loaded') - t.assert.ok(secondLoaded, 'second is loaded') - t.assert.ok(thirdLoaded, 'third is loaded') - t.assert.ok('booted') - testDone() - }) -}) - -test('multiple reentrant plugin loading', (t, testDone) => { - t.plan(31) - - const app = boot() - let firstLoaded = false - let secondLoaded = false - let thirdLoaded = false - let fourthLoaded = false - let fifthLoaded = false - - app.use(first) - app.use(fifth) - - function first (s, opts, done) { - t.assert.strictEqual(firstLoaded, false, 'first is not loaded') - t.assert.strictEqual(secondLoaded, false, 'second is not loaded') - t.assert.strictEqual(thirdLoaded, false, 'third is not loaded') - t.assert.strictEqual(fourthLoaded, false, 'fourth is not loaded') - t.assert.strictEqual(fifthLoaded, false, 'fifth is not loaded') - firstLoaded = true - s.use(second) - done() - } - - function second (s, opts, done) { - t.assert.ok(firstLoaded, 'first is loaded') - t.assert.strictEqual(secondLoaded, false, 'second is not loaded') - t.assert.strictEqual(thirdLoaded, false, 'third is not loaded') - t.assert.strictEqual(fourthLoaded, false, 'fourth is not loaded') - t.assert.strictEqual(fifthLoaded, false, 'fifth is not loaded') - secondLoaded = true - s.use(third) - s.use(fourth) - done() - } - - function third (s, opts, done) { - t.assert.ok(firstLoaded, 'first is loaded') - t.assert.ok(secondLoaded, 'second is loaded') - t.assert.strictEqual(thirdLoaded, false, 'third is not loaded') - t.assert.strictEqual(fourthLoaded, false, 'fourth is not loaded') - t.assert.strictEqual(fifthLoaded, false, 'fifth is not loaded') - thirdLoaded = true - done() - } - - function fourth (s, opts, done) { - t.assert.ok(firstLoaded, 'first is loaded') - t.assert.ok(secondLoaded, 'second is loaded') - t.assert.ok(thirdLoaded, 'third is loaded') - t.assert.strictEqual(fourthLoaded, false, 'fourth is not loaded') - t.assert.strictEqual(fifthLoaded, false, 'fifth is not loaded') - fourthLoaded = true - done() - } - - function fifth (s, opts, done) { - t.assert.ok(firstLoaded, 'first is loaded') - t.assert.ok(secondLoaded, 'second is loaded') - t.assert.ok(thirdLoaded, 'third is loaded') - t.assert.ok(fourthLoaded, 'fourth is loaded') - t.assert.strictEqual(fifthLoaded, false, 'fifth is not loaded') - fifthLoaded = true - done() - } - - app.on('start', () => { - t.assert.ok(firstLoaded, 'first is loaded') - t.assert.ok(secondLoaded, 'second is loaded') - t.assert.ok(thirdLoaded, 'third is loaded') - t.assert.ok(fourthLoaded, 'fourth is loaded') - t.assert.ok(fifthLoaded, 'fifth is loaded') - t.assert.ok('booted') - testDone() - }) -}) diff --git a/test/to-json.test.js b/test/to-json.test.js deleted file mode 100644 index d37725a..0000000 --- a/test/to-json.test.js +++ /dev/null @@ -1,151 +0,0 @@ -'use strict' - -const { test } = require('tap') -const boot = require('..') - -test('to json', (t) => { - t.plan(4) - - const app = boot() - app - .use(one) - .use(two) - .use(three) - - const outJson = { - id: 'root', - label: 'root', - start: /\d+/, - nodes: [] - } - - app.on('preReady', function show () { - const json = app.toJSON() - outJson.stop = /\d*/ - outJson.diff = /\d*/ - t.match(json, outJson) - }) - - function one (s, opts, done) { - const json = app.toJSON() - outJson.nodes.push({ - id: /.+/, - parent: outJson.label, - label: 'one', - start: /\d+/ - }) - t.match(json, outJson) - done() - } - function two (s, opts, done) { - const json = app.toJSON() - outJson.nodes.push({ - id: /.+/, - parent: outJson.label, - label: 'two', - start: /\d+/ - }) - t.match(json, outJson) - done() - } - function three (s, opts, done) { - const json = app.toJSON() - outJson.nodes.push({ - id: /.+/, - parent: outJson.label, - label: 'three', - start: /\d+/ - }) - t.match(json, outJson) - done() - } -}) - -test('to json multi-level hierarchy', (t) => { - t.plan(4) - - const server = { name: 'asd', count: 0 } - const app = boot(server) - - const outJson = { - id: 'root', - label: 'root', - start: /\d+/, - nodes: [ - { - id: /.+/, - parent: 'root', - start: /\d+/, - label: 'first', - nodes: [ - { - id: /.+/, - parent: 'first', - start: /\d+/, - label: 'second', - nodes: [], - stop: /\d+/, - diff: /\d+/ - }, - { - id: /.+/, - parent: 'first', - start: /\d+/, - label: 'third', - nodes: [ - { - id: /.+/, - parent: 'third', - start: /\d+/, - label: 'fourth', - nodes: [], - stop: /\d+/, - diff: /\d+/ - } - ], - stop: /\d+/, - diff: /\d+/ - } - ], - stop: /\d+/, - diff: /\d+/ - } - ], - stop: /\d+/, - diff: /\d+/ - } - - app.on('preReady', function show () { - const json = app.toJSON() - t.match(json, outJson) - }) - - app.override = function (s) { - const res = Object.create(s) - res.count = res.count + 1 - res.name = 'qwe' - return res - } - - app.use(function first (s1, opts, cb) { - s1.use(second) - s1.use(third) - cb() - - function second (s2, opts, cb) { - t.equal(s2.count, 2) - cb() - } - - function third (s3, opts, cb) { - s3.use(fourth) - t.equal(s3.count, 2) - cb() - } - - function fourth (s4, opts, cb) { - t.equal(s4.count, 3) - cb() - } - }) -}) diff --git a/test/twice-done.test.js b/test/twice-done.test.js deleted file mode 100644 index a5d65fb..0000000 --- a/test/twice-done.test.js +++ /dev/null @@ -1,23 +0,0 @@ -'use strict' - -const { test } = require('node:test') -const boot = require('..') - -test('calling done twice does not throw error', (t, testDone) => { - t.plan(2) - - const app = boot() - - app - .use(twiceDone) - .ready((err) => { - t.assert.ifError(err) - testDone() - }) - - function twiceDone (s, opts, done) { - done() - done() - t.assert.ok('did not throw') - } -}) diff --git a/test/types/index.ts b/test/types/index.ts deleted file mode 100644 index 77592c9..0000000 --- a/test/types/index.ts +++ /dev/null @@ -1,412 +0,0 @@ -/* eslint-disable @typescript-eslint/no-unused-expressions */ -import * as avvio from '../../' - -{ - // avvio with no argument - const app = avvio() - - app.override = (server, fn, options) => server - - app.use( - (server, opts, done) => { - server.use - server.after - server.ready - server.on - server.start - server.override - server.onClose - server.close - - opts.mySuper - - done() - }, - { mySuper: 'option' } - ) - - app.use(async (server, options) => { - server.use - server.after - server.ready - server.on - server.start - server.override - server.onClose - server.close - }) - - app.use(async (server, options) => {}, - (server) => { - server.use - server.after - server.ready - server.on - server.start - server.override - server.onClose - server.close - }) - - app.after(err => { - if (err) throw err - }) - - app.after((_err: Error, done: Function) => { - done() - }) - - app.after((_err: Error, context: avvio.context, done: Function) => { - context.use - context.after - context.ready - context.on - context.start - context.override - context.onClose - context.close - - done() - }) - - app.ready().then(context => { - context.use - context.after - context.ready - context.on - context.start - context.override - context.onClose - context.close - }) - - app.ready(err => { - if (err) throw err - }) - - app.ready((_err: Error, done: Function) => { - done() - }) - - app.ready((_err: Error, context: avvio.context, done: Function) => { - context.use - context.after - context.ready - context.on - context.start - context.override - context.onClose - context.close - - done() - }) - - app.close(err => { - if (err) throw err - }) - - app.close((_err: Error, done: Function) => { - done() - }) - - app.close((_err: Error, context: avvio.context, done: Function) => { - context.use - context.after - context.ready - context.on - context.start - context.override - context.onClose - context.close - - done() - }) - - app.onClose((context, done) => { - context.use - context.after - context.ready - context.on - context.start - context.override - context.onClose - context.close - - done() - }) -} - -{ - // avvio with done - const app = avvio(() => undefined) - - app.use( - (server, opts, done) => { - server.use - server.after - server.ready - server.on - server.start - server.override - server.onClose - server.close - - opts.mySuper - - done() - }, - { mySuper: 'option' } - ) - - app.use(async (server, options) => { - server.use - server.after - server.ready - server.on - server.start - server.override - server.onClose - server.close - }) - - app.use(async (server, options) => {}, - (server) => { - server.use - server.after - server.ready - server.on - server.start - server.override - server.onClose - server.close - }) - - app.after(err => { - if (err) throw err - }) - - app.after((_err: Error, done: Function) => { - done() - }) - - app.after((_err: Error, context: avvio.context, done: Function) => { - context.use - context.after - context.ready - context.on - context.start - context.override - context.onClose - context.close - - done() - }) - - app.ready().then(context => { - context.use - context.after - context.ready - context.on - context.start - context.override - context.onClose - context.close - }) - - app.ready(err => { - if (err) throw err - }) - - app.ready((_err: Error, done: Function) => { - done() - }) - - app.ready((_err: Error, context: avvio.context, done: Function) => { - context.use - context.after - context.ready - context.on - context.start - context.override - context.onClose - context.close - - done() - }) - - app.close(err => { - if (err) throw err - }) - - app.close((_err: Error, done: Function) => { - done() - }) - - app.close((_err: Error, context: avvio.context, done: Function) => { - context.use - context.after - context.ready - context.on - context.start - context.override - context.onClose - context.close - - done() - }) - - app.onClose((context, done) => { - context.use - context.after - context.ready - context.on - context.start - context.override - context.onClose - context.close - - done() - }) -} - -{ - const server = { typescriptIs: 'amazing' } - // avvio with server - const app = avvio(server) - - app.use( - (server, opts, done) => { - server.use - server.after - server.ready - server.typescriptIs - - opts.mySuper - - done() - }, - { mySuper: 'option' } - ) - - app.use(async (server, options) => { - server.use - server.after - server.ready - server.typescriptIs - }) - - app.use(async (server, options) => {}, - (server) => { - server.use - server.after - server.ready - server.typescriptIs - }) - - app.after(err => { - if (err) throw err - }) - - app.after((_err: Error, done: Function) => { - done() - }) - - app.after( - (_err: Error, context: avvio.context, done: Function) => { - context.use - context.after - context.ready - context.typescriptIs - - done() - } - ) - - app.ready().then(context => { - context.use - context.after - context.ready - context.typescriptIs - }) - - app.ready(err => { - if (err) throw err - }) - - app.ready((_err: Error, done: Function) => { - done() - }) - - app.ready( - (_err: Error, context: avvio.context, done: Function) => { - context.use - context.after - context.ready - context.close - context.onClose - context.typescriptIs - - done() - } - ) - - app.close(err => { - if (err) throw err - }) - - app.close((_err: Error, done: Function) => { - done() - }) - - app.close( - (_err: Error, context: avvio.context, done: Function) => { - context.use - context.after - context.ready - context.close - context.onClose - context.typescriptIs - - done() - } - ) - - app.onClose((context, done) => { - context.use - context.after - context.ready - context.close - context.onClose - context.typescriptIs - - done() - }) -} - -{ - const server = { hello: 'world' } - const options = { - autostart: false, - expose: { after: 'after', ready: 'ready', use: 'use', close: 'close', onClose: 'onClose' }, - timeout: 50000 - } - // avvio with server and options - avvio(server, options) -} - -{ - const server = { hello: 'world' } - const options = { - autostart: false, - expose: { after: 'after', ready: 'ready', use: 'use' } - } - // avvio with server, options and done callback - avvio(server, options, () => undefined) -} - -{ - const app = avvio() - const plugin: avvio.Plugin = async (): Promise => {} - const promise = plugin(app, {}, undefined as any); - (promise instanceof Promise) -} diff --git a/test/types/tsconfig.json b/test/types/tsconfig.json deleted file mode 100644 index b170f36..0000000 --- a/test/types/tsconfig.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "compilerOptions": { - "target": "es6", - "module": "commonjs", - "noEmit": true, - "strict": true - }, - "files": ["./index.ts"] -} From 0651ee55a15e797cec00b9ad8725b06c99492d13 Mon Sep 17 00:00:00 2001 From: KaKa Date: Tue, 21 Jan 2025 15:42:14 +0800 Subject: [PATCH 2/2] feat: tracker --- avvio.js | 26 ++++++++++- lib/symbols.js | 2 + lib/tracker.js | 122 +++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 148 insertions(+), 2 deletions(-) create mode 100644 lib/tracker.js diff --git a/avvio.js b/avvio.js index 86a5fe8..8d23373 100644 --- a/avvio.js +++ b/avvio.js @@ -2,13 +2,14 @@ const { Plugin } = require('./lib/plugin') const { isPromiseLike, withResolvers } = require('./lib/promise') -const { kContext, kOptions, kExpose, kAvvio, kPluginRoot, kAddPlugin, kStart, kPluginQueue, kError, kLoadPlugin, kLoadPluginNext, kReadyQueue, kCloseQueue, kOnCloseFunction, kWrappedThen } = require('./lib/symbols') +const { kContext, kOptions, kExpose, kAvvio, kPluginRoot, kAddPlugin, kStart, kPluginQueue, kError, kLoadPlugin, kLoadPluginNext, kReadyQueue, kCloseQueue, kOnCloseFunction, kWrappedThen, kTracker, kTrackPlugin } = require('./lib/symbols') const { resolveBundledFunction, noop } = require('./lib/utils') const EventEmitter = require('node:events').EventEmitter const inherits = require('node:util').inherits const fastq = require('fastq') const { readyWorker, closeWorker } = require('./lib/workers') const { AVV_ERR_CALLBACK_NOT_FN } = require('./lib/errors') +const { Tracker } = require('./lib/tracker') /** * @@ -96,6 +97,7 @@ function Avvio (instance, options, done) { // status this.started = false // true when called start this.booted = false // true when ready + this[kTracker] = new Tracker() this[kPluginQueue] = [] this[kStart] = null @@ -111,6 +113,7 @@ function Avvio (instance, options, done) { options, 0 ) + this[kTrackPlugin](this[kPluginRoot]) this[kLoadPlugin](this[kPluginRoot], (error) => { try { @@ -225,10 +228,18 @@ Avvio.prototype.use = function use (plugin, options) { return this } -Avvio.prototype.override = function (context, fn, options) { +Avvio.prototype.override = function override (context, fn, options) { return context } +Avvio.prototype.prettyPrint = function prettyPrint () { + return this[kTracker].prettyPrint() +} + +Avvio.prototype.toJSON = function toJSON () { + return this[kTracker].toJSON() +} + Object.defineProperties(Avvio.prototype, { then: { get () { @@ -326,6 +337,7 @@ Avvio.prototype[kAddPlugin] = function (fn, options) { options, timeout ) + this[kTrackPlugin](plugin) if (parent.loaded) { throw Error(plugin.name, parent.name) @@ -372,6 +384,16 @@ Avvio.prototype[kLoadPluginNext] = function (plugin, callback) { process.nextTick(this[kLoadPlugin].bind(this), plugin, callback) } +Avvio.prototype[kTrackPlugin] = function (plugin) { + const parentName = this[kPluginQueue][0]?.name ?? null + plugin.once('start', (contextName, fnName, startTime) => { + const nodeId = this[kTracker].start(parentName, fnName, startTime) + plugin.once('loaded', (contextName, fnName, endTime) => { + this[kTracker].stop(nodeId, endTime) + }) + }) +} + /** * supports the following import * 1. const Avvio = require('avvio') diff --git a/lib/symbols.js b/lib/symbols.js index 47d0494..6312389 100644 --- a/lib/symbols.js +++ b/lib/symbols.js @@ -12,12 +12,14 @@ module.exports = { kCloseQueue: Symbol('Avvio#close-queue'), kResolvers: Symbol('Avvio#resolvers'), kWrappedThen: Symbol('Avvio#wrapped-then'), + kTracker: Symbol('Avvio#tracker'), // private functions kExpose: Symbol('Avvio#expose'), kAddPlugin: Symbol('Avvio#add-plugin'), kLoadPlugin: Symbol('Avvio#load-plugin'), kLoadPluginNext: Symbol('Avvio#load-plugin-next'), kStart: Symbol('Avvio#start'), + kTrackPlugin: Symbol('Avvio#track-plugin'), kOnCloseFunction: Symbol('Function#on-close'), diff --git a/lib/tracker.js b/lib/tracker.js new file mode 100644 index 0000000..f92e01e --- /dev/null +++ b/lib/tracker.js @@ -0,0 +1,122 @@ +'use strict' + +class Tracker { + constructor () { + this.root = null + this.tableId = new Map() + this.tableLabel = new Map() + } + + track (node) { + this.tableId.set(node.id, node) + if (this.tableLabel.has(node.label)) { + this.tableLabel.get(node.label).push(node) + } else { + this.tableLabel.set(node.label, [node]) + } + } + + untrack (node) { + this.tableId.delete(node.id) + + const labelNode = this.tableLabel.get(node.label) + labelNode.pop() + + if (labelNode.length === 0) { + this.tableLabel.delete(node.label) + } + } + + findParentByLabel (label) { + if (label === null) { + return null + } else if (this.tableLabel.has(label)) { + const parent = this.tableLabel.get(label) + return parent[parent.length - 1] + } else { + return null + } + } + + findNodeById (nodeId) { + return this.tableId.get(nodeId) + } + + add (parent, label, start) { + const parentNode = this.findParentByLabel(parent) + const isRoot = parentNode === null + + if (isRoot) { + this.root = { + parent: null, + id: 'root', + label, + nodes: [], + start, + stop: null, + diff: -1 + } + this.track(this.root) + return this.root.id + } + + const nodeId = `${label}-${Math.random()}` + /** + * @type {TimeTreeNode} + */ + const childNode = { + parent, + id: nodeId, + label, + nodes: [], + start, + stop: null, + diff: -1 + } + parentNode.nodes.push(childNode) + this.track(childNode) + return nodeId + } + + start (parent, label, start = Date.now()) { + return this.add(parent, label, start) + } + + stop (nodeId, stop = Date.now()) { + const node = this.findNodeById(nodeId) + if (node) { + node.stop = stop + node.diff = (node.stop - node.start) || 0 + this.untrack(node) + } + } + + toJSON () { + return Object.assign({}, this.root) + } + + prettyPrint () { + return prettyPrintTimeTree(this.toJSON()) + } +} + +function prettyPrintTimeTree (obj, prefix = '') { + let result = prefix + + const nodesCount = obj.nodes.length + const lastIndex = nodesCount - 1 + result += `${obj.label} ${obj.diff} ms\n` + + for (let i = 0; i < nodesCount; ++i) { + const node = obj.nodes[i] + const prefix_ = prefix + (i === lastIndex ? ' ' : '│ ') + + result += prefix + result += (i === lastIndex ? '└─' : '├─') + result += (node.nodes.length === 0 ? '─ ' : '┬ ') + result += prettyPrintTimeTree(node, prefix_).slice(prefix.length + 2) + } + return result +} + +module.exports.Tracker = Tracker