diff --git a/README.md b/README.md index c6c0913..f5a1f87 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@ npm install avvio --save ## Example -The example below can be found [here][example] and ran using `node example.js`. +The example below can be found [here][example] and ran using `node example.js`. It demonstrates how to use `avvio` to load functions / plugins in order. @@ -66,9 +66,9 @@ function second (instance, opts, cb) { process.nextTick(cb) } -function third (instance, opts, cb) { +// async/await or Promise support +async function third (instance, opts) { console.log('third loaded') - cb() } ``` @@ -89,8 +89,8 @@ function third (instance, opts, cb) { ### avvio([instance], [started]) -Starts the avvio sequence. -As the name suggest, `instance` is the object representing your application. +Starts the avvio sequence. +As the name suggest, `instance` is the object representing your application. Avvio will add the functions `use`, `after` and `ready` to the instance. ```js @@ -138,7 +138,7 @@ app.on('start', () => { ### app.use(func, [opts], [cb]) -Loads one or more functions asynchronously. +Loads one or more functions asynchronously. The function **must** have the signature: `instance, options, done` Plugin example: @@ -151,6 +151,15 @@ app.use(plugin) ``` `done` must be called only once, when your plugin is ready to go. +async/await is also supported: + +```js +async function plugin (server, opts) { + await sleep(10) +} +app.use(plugin) +``` + `use` returns the instance on which `use` is called, to support a chainable API. If you need to add more than a function and you don't need to use a different options object or callback, you can pass an array of functions to `.use`. @@ -188,11 +197,11 @@ app.ready(function (err) { ### app.after(func(error, [context], [done]), [cb]) Calls a function after all the previously defined plugins are loaded, including -all their dependencies. The `'start'` event is not emitted yet. +all their dependencies. The `'start'` event is not emitted yet. The callback changes basing on the parameters your are giving: -1. If one parameter is given to the callback, that parameter will be the `error` object. -2. If two parameters are given to the callback, the first will be the `error` object, the second will be the `done` callback. +1. If one parameter is given to the callback, that parameter will be the `error` object. +2. If two parameters are given to the callback, the first will be the `error` object, the second will be the `done` callback. 3. If three parameters are given to the callback, the first will be the `error` object, the second will be the top level `context` unless you have specified both server and override, in that case the `context` will be what the override returns, and the third the `done` callback. ```js @@ -226,11 +235,11 @@ Returns the instance on which `after` is called, to support a chainable API. ### app.ready(func(error, [context], [done])) -Calls a function after all the plugins and `after` call are completed, but before `'start'` is emitted. `ready` callbacks are executed one at a time. +Calls a function after all the plugins and `after` call are completed, but before `'start'` is emitted. `ready` callbacks are executed one at a time. The callback changes basing on the parameters your are giving: -1. If one parameter is given to the callback, that parameter will be the `error` object. -2. If two parameters are given to the callback, the first will be the `error` object, the second will be the `done` callback. +1. If one parameter is given to the callback, that parameter will be the `error` object. +2. If two parameters are given to the callback, the first will be the `error` object, the second will be the `done` callback. 3. If three parameters are given to the callback, the first will be the `error` object, the second will be the top level `context` unless you have specified both server and override, in that case the `context` will be what the override returns, and the third the `done` callback. ```js @@ -281,8 +290,8 @@ boot(app, { ### app.override(server, plugin, options) -Allows to override the instance of the server for each loading plugin. -It allows the creation of an inheritance chain for the server instances. +Allows to override the instance of the server for each loading plugin. +It allows the creation of an inheritance chain for the server instances. The first parameter is the server instance and the second is the plugin function while the third is the options object that you give to use. ```js @@ -324,8 +333,8 @@ app.use(function first (s1, opts, cb) { Registers a new callback that will be fired once then `close` api is called. The callback changes basing on the parameters your are giving: -1. If one parameter is given to the callback, that parameter will be the `context`. -2. If two parameters are given to the callback, the first will be the top level `context` unless you have specified both server and override, in that case the `context` will be what the override returns, the second will be the `done` callback. +1. If one parameter is given to the callback, that parameter will be the `context`. +2. If two parameters are given to the callback, the first will be the top level `context` unless you have specified both server and override, in that case the `context` will be what the override returns, the second will be the `done` callback. ```js const server = {} @@ -353,8 +362,8 @@ Returns the instance on which `onClose` is called, to support a chainable API. Starts the shotdown procedure, the callback is called once all the registered callbacks with `onClose` has been executed. The callback changes basing on the parameters your are giving: -1. If one parameter is given to the callback, that parameter will be the `error` object. -2. If two parameters are given to the callback, the first will be the `error` object, the second will be the `done` callback. +1. If one parameter is given to the callback, that parameter will be the `error` object. +2. If two parameters are given to the callback, the first will be the `error` object, the second will be the `done` callback. 3. If three parameters are given to the callback, the first will be the `error` object, the second will be the top level `context` unless you have specified both server and override, in that case the `context` will be what the override returns, and the third the `done` callback. ```js diff --git a/package.json b/package.json index fcf573c..493155c 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "description": "Asynchronous bootstrapping of Node applications", "main": "boot.js", "scripts": { - "test": "standard && tap test/*.js" + "test": "standard && tap test/*test.js" }, "precommit": "test", "repository": { @@ -33,7 +33,8 @@ "express": "^4.15.4", "pre-commit": "^1.2.2", "standard": "^10.0.3", - "tap": "^10.7.2" + "tap": "^10.7.2", + "then-sleep": "^1.0.1" }, "dependencies": { "fastq": "^1.5.0" diff --git a/plugin.js b/plugin.js index 2d13397..35c6a00 100644 --- a/plugin.js +++ b/plugin.js @@ -22,13 +22,27 @@ function Plugin (parent, func, opts, callback) { Plugin.prototype.exec = function (server, cb) { const func = this.func + var completed = false this.server = this.parent.override(server, func, this.opts) // we must defer the loading of the plugin until the // current execution has ended process.nextTick(() => { - func(this.server, this.opts, cb) + var promise = func(this.server, this.opts, done) + if (promise && typeof promise.then === 'function') { + promise.then(() => done()).catch(done) + } }) + + function done (err) { + if (completed) { + return + } + + completed = true + + cb(err) + } } Plugin.prototype.finish = function (err, cb) { diff --git a/test/after-and-ready.js b/test/after-and-ready.test.js similarity index 100% rename from test/after-and-ready.js rename to test/after-and-ready.test.js diff --git a/test/async-await.js b/test/async-await.js new file mode 100644 index 0000000..0321a2a --- /dev/null +++ b/test/async-await.js @@ -0,0 +1,119 @@ +'use strict' + +const test = require('tap').test +const sleep = require('then-sleep') + +const boot = require('..') + +test('one level', (t) => { + t.plan(13) + + const app = boot() + let firstLoaded = false + let secondLoaded = false + let thirdLoaded = false + + app.use(first) + app.use(third) + + async function first (s, opts) { + 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) + } + + async function second (s, opts) { + t.ok(firstLoaded, 'first is loaded') + t.notOk(secondLoaded, 'second is not loaded') + t.notOk(thirdLoaded, 'third is not loaded') + secondLoaded = true + } + + async function third (s, opts) { + t.ok(firstLoaded, 'first is loaded') + t.ok(secondLoaded, 'second is loaded') + t.notOk(thirdLoaded, 'third is not loaded') + thirdLoaded = true + } + + app.on('start', () => { + t.ok(firstLoaded, 'first is loaded') + t.ok(secondLoaded, 'second is loaded') + t.ok(thirdLoaded, 'third is loaded') + t.pass('booted') + }) +}) + +test('multiple reentrant plugin loading', (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.notOk(firstLoaded, 'first is not loaded') + t.notOk(secondLoaded, 'second is not loaded') + t.notOk(thirdLoaded, 'third is not loaded') + t.notOk(fourthLoaded, 'fourth is not loaded') + t.notOk(fifthLoaded, 'fifth is not loaded') + firstLoaded = true + s.use(second) + } + + async function second (s, opts) { + 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') + t.notOk(fifthLoaded, 'fifth is not loaded') + secondLoaded = true + s.use(third) + await sleep(10) + s.use(fourth) + } + + async function third (s, opts) { + t.ok(firstLoaded, 'first is loaded') + t.ok(secondLoaded, 'second is loaded') + t.notOk(thirdLoaded, 'third is not loaded') + t.notOk(fourthLoaded, 'fourth is not loaded') + t.notOk(fifthLoaded, 'fifth is not loaded') + thirdLoaded = true + } + + async function fourth (s, opts) { + t.ok(firstLoaded, 'first is loaded') + t.ok(secondLoaded, 'second is loaded') + t.ok(thirdLoaded, 'third is loaded') + t.notOk(fourthLoaded, 'fourth is not loaded') + t.notOk(fifthLoaded, 'fifth is not loaded') + fourthLoaded = true + } + + async function fifth (s, opts) { + t.ok(firstLoaded, 'first is loaded') + t.ok(secondLoaded, 'second is loaded') + t.ok(thirdLoaded, 'third is loaded') + t.ok(fourthLoaded, 'fourth is loaded') + t.notOk(fifthLoaded, 'fifth is not loaded') + fifthLoaded = true + } + + app.on('start', () => { + t.ok(firstLoaded, 'first is loaded') + t.ok(secondLoaded, 'second is loaded') + t.ok(thirdLoaded, 'third is loaded') + t.ok(fourthLoaded, 'fourth is loaded') + t.ok(fifthLoaded, 'fifth is loaded') + t.pass('booted') + }) +}) diff --git a/test/async-await.test.js b/test/async-await.test.js new file mode 100644 index 0000000..c9d78cd --- /dev/null +++ b/test/async-await.test.js @@ -0,0 +1,10 @@ +'use strict' + +const tap = require('tap') + +if (Number(process.versions.node[0]) >= 8) { + require('./async-await') +} else { + tap.pass('Skip because Node version < 8') + tap.end() +} diff --git a/test/basic.js b/test/basic.test.js similarity index 88% rename from test/basic.js rename to test/basic.test.js index e1980d1..589b233 100644 --- a/test/basic.js +++ b/test/basic.test.js @@ -31,6 +31,26 @@ test('boot an app with a plugin', (t) => { }) }) +test('boot an app with a promisified plugin', (t) => { + t.plan(4) + + const app = boot() + var after = false + + app.use(function (server, opts) { + t.equal(server, app, 'the first argument is the server') + t.deepEqual(opts, {}, 'no options') + t.ok(after, 'delayed execution') + return Promise.resolve() + }) + + after = true + + app.on('start', () => { + t.pass('booted') + }) +}) + test('boot an app with a plugin and a callback', (t) => { t.plan(2) diff --git a/test/callbacks.js b/test/callbacks.test.js similarity index 100% rename from test/callbacks.js rename to test/callbacks.test.js diff --git a/test/chainable.js b/test/chainable.test.js similarity index 100% rename from test/chainable.js rename to test/chainable.test.js diff --git a/test/close.js b/test/close.test.js similarity index 100% rename from test/close.js rename to test/close.test.js diff --git a/test/express.js b/test/express.test.js similarity index 100% rename from test/express.js rename to test/express.test.js diff --git a/test/override.js b/test/override.test.js similarity index 100% rename from test/override.js rename to test/override.test.js diff --git a/test/reentrant.js b/test/reentrant.test.js similarity index 100% rename from test/reentrant.js rename to test/reentrant.test.js diff --git a/test/use-array.js b/test/use-array.test.js similarity index 100% rename from test/use-array.js rename to test/use-array.test.js