Asynchronous bootstrapping is hard, different things can go wrong, error handling and load order just to name a few. The module aims to make it simple.
avvio is fully reentrant and graph-based. You can load
components/plugins within plugins, and be still sure that things will
happen in the right order. At the end of the loading, your application will start.
To install avvio, simply use npm:
npm i avvioTypeScript type definitions are bundled with the package (index.d.ts), so no extra @types package is needed.
To enable internal debug logging, run your application with the NODE_DEBUG=avvio environment variable set:
NODE_DEBUG=avvio node app.jsThe example below demonstrates how to use avvio to load functions and plugins in order.
'use strict'
const app = require('avvio')()
app
.use(first, { hello: 'world' })
.after((err, cb) => {
if (err) {
throw err
}
console.log('after first and second')
cb()
})
app.use(third)
app.ready(function (err) {
if (err) {
throw err
}
console.log('application booted!')
})
function first (instance, opts, cb) {
console.log('first loaded', opts)
instance.use(second)
cb()
}
function second (instance, opts, cb) {
console.log('second loaded')
process.nextTick(cb)
}
// async/await or Promise support
async function third (instance, opts) {
console.log('third loaded')
}The important part of this example is that first registers second while it is loading.
This creates the following plugin tree:
root
├── first
│ └── second
└── third
The plugins are loaded in this order:
first
second
third
Only after the required plugins have completed can the application become ready.
Avvio builds a tree of plugins and loads that tree in a predictable order.
The main concepts are:
- plugins can register other plugins;
- nested plugins become dependencies of their parent;
- plugins are loaded according to their position in the tree;
after()waits for plugins registered before it;ready()waits for the complete boot process;- errors stop the boot process until they are handled or propagated to
ready(); onClose()andclose()provide the shutdown lifecycle.
A call to use() adds a plugin to the current context.
A plugin can register additional plugins while it is loading. Those plugins become children of the current plugin.
For example:
app.use(first)
app.use(third)
function first (instance, opts, done) {
instance.use(second)
done()
}creates this tree:
root
├── first
│ └── second
└── third
The nested second plugin belongs to first, because it was registered while first was loading.
This is what makes Avvio reentrant: a plugin can use the same application instance to register more plugins without losing the correct execution order.
Avvio loads plugins according to their position in the plugin tree.
Given:
app.use(first)
app.use(third)
function first (instance, opts, done) {
instance.use(second)
done()
}
function second (instance, opts, done) {
done()
}
function third (instance, opts, done) {
done()
}the execution order is:
first
second
third
second must finish before Avvio can continue with third, because second is a dependency of first.
This allows plugins to register the components they depend on without requiring the application to manually calculate the complete load order.
The boot process can be thought of as a sequence of phases:
use()
│
│ register plugins
▼
plugin loading
│
│ nested plugins are loaded
▼
after()
│
│ wait for plugins registered so far
▼
ready()
│
│ wait for the complete boot process
▼
start
A plugin can register more plugins while it is loading:
app.use(database)
function database (instance, opts, done) {
instance.use(connectionPool)
done()
}Avvio waits for the nested plugin before considering the parent plugin complete.
The three methods have different purposes.
Use use() to register a plugin:
app.use(database)
app.use(routes)A plugin can register additional plugins:
function database (instance, opts, done) {
instance.use(connectionPool)
done()
}Use after() when you want to wait for all plugins registered before that point, including their dependencies:
app.use(database)
app.after(function (err) {
if (err) {
throw err
}
// database and its dependencies are ready here
})The application has not emitted the start event yet.
With async/await, after() can be awaited:
app.use(database)
await app.after()
// database and its dependencies are ready hereUse ready() when you want to wait for the complete boot process:
app.use(database)
app.use(routes)
await app.ready()
// the application is readyready() runs after all plugins and after() callbacks have completed, but before the start event is emitted.
A useful way to think about the difference is:
use()
Register work
after()
Wait for work registered so far
ready()
Wait for the complete boot process
Errors are part of the boot lifecycle.
If a plugin fails:
app.use(function (instance, opts, done) {
done(new Error('database failed'))
})Avvio stops loading until the error is handled.
An after() callback can handle or propagate the error:
app.after(function (err) {
if (err) {
throw err
}
// continue
})If the error is not handled by an after() callback, it is propagated to ready():
app.ready(function (err) {
if (err) {
console.error(err)
return
}
console.log('application booted')
})The general flow is:
plugin
│
├── success ──────────────> continue
│
└── error
│
▼
after()
│
├── handled ─────────> continue
│
└── not handled
│
▼
ready()
Plugins and callbacks can also fail by not completing within the configured timeout.
See Errors for the available error codes.
Bootstrapping is only one part of an application's lifecycle. Avvio also provides a shutdown mechanism.
Use onClose() to register cleanup functions:
app.onClose(async function () {
await database.close()
})Call close() to start the shutdown process:
await app.close()The registered onClose() callbacks are executed before the close() operation completes.
The lifecycle is therefore:
boot
│
▼
ready
│
▼
application running
│
│ close()
▼
onClose()
│
▼
closed
- avvio
- Install
- Example
- How Avvio works
- API
avvio([instance], [options], [started])app.use(func, [optsOrFunc]) => Thenable- Error handling
app.after(func(error, [context], [done]))await app.after() | app.after() => Promiseapp.ready([func(error, [context], [done])])app.start()app.override(server, plugin, options)app.onClose(func([context], [done]))app.close(func(error, [context], [done]))app[Symbol.asyncDispose]()avvio.toJSON()avvio.prettyPrint()
- Errors
- Acknowledgments
- License
Starts the Avvio boot sequence.
As the name suggests, instance is the object representing your application. Avvio adds the functions use, after, ready, onClose, and close to the instance.
const server = {}
require('avvio')(server)
server
.use(function first (s, opts, cb) {
s.use(function second (s, opts, cb) {
cb()
})
cb()
})
.after(function (err, cb) {
if (err) {
throw err
}
// first and second are finished
cb()
})A key/value property to change how use, after, ready, onClose, and close are exposed on the instance.
This is useful when one of the default names is already taken.
const server = {}
require('avvio')(server, {
expose: {
use: 'register',
ready: 'onReady'
}
})
server.register(function (s, opts, done) {
done()
})
server.onReady(function (err) {
if (err) {
throw err
}
})When autostart is false, Avvio does not start loading plugins automatically. The boot sequence starts when .start() or .ready() is called.
const app = require('avvio')({
autostart: false
})
app.use(plugin)
// boot starts here
app.start()The number of milliseconds to wait for a plugin or lifecycle callback to complete.
If the timeout is reached, Avvio emits:
AVV_ERR_PLUGIN_EXEC_TIMEOUTfor plugins;AVV_ERR_READY_TIMEOUTforready()andafter()callbacks.
The default is 0, which disables the timeout.
See Errors.
Emitted when the application starts.
Emitted before the ready queue is run.
Emitted when the application has completed the shutdown procedure started with close().
true once start() has been called, or once ready() starts the boot sequence when autostart is enabled.
It does not mean that loading has completed.
true once the root plugin and all of its dependencies have finished loading successfully.
The avvio function can also be used as a constructor to inherit from.
function Server () {}
const app = require('avvio')(new Server())
app.use(function (s, opts, done) {
done()
})
app.on('start', () => {
// your application can start
})Loads one or more functions asynchronously.
The function can use the following callback signature:
function plugin (instance, options, done) {
done()
}done should be called only once, when the plugin is ready.
Additional calls to done are ignored.
If the plugin is ready immediately after the function is evaluated, done can be omitted:
function plugin (instance, options) {
// synchronous plugin
}If the function returns a Promise, an async function can be used instead:
async function plugin (instance, options) {
await initialize()
}A plugin can register additional plugins:
function first (instance, opts, done) {
instance.use(second)
done()
}
function second (instance, opts, done) {
done()
}
app.use(first)Avvio waits for second before considering first complete.
use() returns a thenable wrapped instance, so it can be awaited:
async function main () {
await app.use(async function (server, opts) {
await sleep(10)
console.log('this first')
})
console.log('then this')
await app.ready()
console.log('ready')
}
main().catch((err) => console.error(err))The options argument can also be a function.
The function receives the parent instance and returns the options for the plugin:
function first (server, opts, done) {
server.foo = 'bar'
done()
}
function second (server, opts, done) {
console.log(opts.foo === 'bar')
done()
}
const options = parent => {
return {
foo: parent.foo
}
}
app.use(first)
app.use(second, options)This is useful when a value created by a plugin needs to be made available as options to another plugin.
ES modules can be loaded using a dynamic import:
import boot from 'avvio'
const app = boot()
app.use(import('./fixtures/esm.mjs'))To handle errors in plugins, use ready():
app.use(function (instance, opts, done) {
done(new Error('error'))
})
app.ready(function (err) {
if (err) {
throw err
}
})When an error happens, plugin loading stops until the error is handled by an after() callback.
If it is not handled, the error is propagated to ready().
Calls a function after all previously defined plugins have loaded, including all their dependencies.
The start event has not been emitted yet.
after() can be used as a callback or awaited as a Promise.
The callback signature changes based on the number of parameters it declares:
- If no parameter is given, an error is passed to the next error handler.
- If one parameter is given, it receives the
error. - If two parameters are given, the first receives the
errorand the second is thedonecallback. - If three parameters are given, the first receives the
error, the second receives the top-levelcontext, and the third is thedonecallback.
In the zero- and one-parameter variants, the callback can return a Promise.
const server = {}
const app = require('avvio')(server)
// one parameter
app.after(function (err) {
if (err) {
throw err
}
})
// two parameters
app.after(function (err, done) {
if (err) {
throw err
}
done()
})
// three parameters
app.after(function (err, context, done) {
if (err) {
throw err
}
assert.equal(context, server)
done()
})
// async
app.after(async function (err) {
if (err) {
throw err
}
await sleep(10)
})done must be called only once.
When called with a function, after() returns the instance on which it was called, allowing chaining.
Calling after() without a function loads all plugins previously registered with use() and returns a Promise.
The Promise resolves when all plugins registered so far have loaded.
async function main () {
app.use(async function (server, opts) {
await sleep(10)
console.log('this first')
})
app.use(async function (server, opts) {
await sleep(10)
console.log('this second')
})
console.log('before after')
await app.after()
console.log('after after')
app.use(async function (server, opts) {
await sleep(10)
console.log('this third')
})
await app.ready()
console.log('ready')
}
main().catch((err) => console.error(err))Unlike callback-based after() and use(), await after is not chainable.
Calls a function after all plugins and after() callbacks have completed, but before the start event is emitted.
ready() callbacks are executed one at a time.
The callback signature changes based on the number of parameters it declares:
- If no parameter is given, an error is passed to the next error handler.
- If one parameter is given, it receives the
error. - If two parameters are given, the first receives the
errorand the second is thedonecallback. - If three parameters are given, the first receives the
error, the second receives the top-levelcontext, and the third is thedonecallback.
When both server and override are specified, the context is the value returned by override.
const server = {}
const app = require('avvio')(server)
// one parameter
app.ready(function (err) {
if (err) {
throw err
}
})
// two parameters
app.ready(function (err, done) {
if (err) {
throw err
}
done()
})
// three parameters
app.ready(function (err, context, done) {
if (err) {
throw err
}
assert.equal(context, server)
done()
})If no callback is provided, ready() returns a Promise:
app.ready()
.then(() => console.log('Ready'))
.catch(err => {
console.error(err)
process.exit(1)
})It can also be awaited:
async function main () {
try {
await app.ready()
console.log('Ready')
} catch (err) {
console.error(err)
process.exit(1)
}
}
main()done must be called only once.
The callback form of this function has no return value.
If autostart: false is passed as an option, calling ready() also starts the boot sequence.
Starts the boot sequence if it has not started yet.
Returns the app instance.
const app = require('avvio')({
autostart: false
})
app.use(plugin)
app.start()Allows overriding the instance passed to each loading plugin.
It can be used to create an inheritance chain between server instances.
The first parameter is the server instance, the second is the plugin function, and the third is the options object passed to use().
For example:
const assert = require('node:assert')
const server = {
count: 0
}
const app = require('avvio')(server)
app.override = function (s, fn, opts) {
const res = Object.create(s)
res.count = res.count + 1
return res
}
app.use(function first (s1, opts, cb) {
assert(s1 !== server)
assert(Object.prototype.isPrototypeOf.call(server, s1))
assert(s1.count === 1)
s1.use(second)
cb()
function second (s2, opts, cb) {
assert(s2 !== s1)
assert(Object.prototype.isPrototypeOf.call(s1, s2))
assert(s2.count === 2)
cb()
}
})The resulting relationship is:
server
│
▼
instance for first
│
▼
instance for second
Each child instance inherits from its parent.
Registers a callback that is executed when the close() API is called.
The callback can use one of the following signatures:
- One parameter: receives the
context. - Zero or one parameter: may return a Promise.
- Two parameters: receives the
contextanddonecallback.
const server = {}
const app = require('avvio')(server)
// one parameter
app.onClose(function (context) {
// ...
})
// Promise
app.onClose(function (context) {
return new Promise((resolve, reject) => {
// ...
})
})
// async
app.onClose(async function (context) {
await cleanup()
})
// callback
app.onClose(function (context, done) {
// ...
done()
})If the callback returns a Promise, the next onClose() callback and the close() callback wait until the Promise resolves or rejects.
done must be called only once.
Returns the instance on which onClose() was called, allowing chaining.
Starts the shutdown procedure.
The callback is called once all registered onClose() callbacks have completed.
The callback can use one of the following signatures:
- One parameter: receives the
error. - Two parameters: receives the
erroranddonecallback. - Three parameters: receives the
error,context, anddonecallback.
const server = {}
const app = require('avvio')(server)
// one parameter
app.close(function (err) {
if (err) {
throw err
}
})
// two parameters
app.close(function (err, done) {
if (err) {
throw err
}
done()
})
// three parameters
app.close(function (err, context, done) {
if (err) {
throw err
}
assert.equal(context, server)
done()
})When no callback is provided, close() returns a Promise:
app.close()
.then(() => console.log('Closed'))
.catch(err => {
console.error(err)
process.exit(1)
})done must be called only once.
This method is an alias for app.close() and supports ECMAScript Explicit Resource Management.
It allows the use of await using to automatically close the Avvio instance when it goes out of scope.
This is especially useful in unit tests and short-lived processes where resources need to be cleaned up automatically.
test('my test', async () => {
await using app = avvio()
app.use(function (server, opts, done) {
done()
})
await app.ready()
// app.close() is called automatically
})This feature requires Node.js 20 or later.
Returns a JSON tree representing the state of the plugins and their loading times.
Call it on preReady to get the complete tree.
const app = require('avvio')()
app.on('preReady', () => {
console.log(app.toJSON())
})The output is similar to:
{
"label": "root",
"start": 1550245184665,
"nodes": [
{
"parent": "root",
"start": 1550245184665,
"label": "first",
"nodes": [
{
"parent": "first",
"start": 1550245184708,
"label": "second",
"nodes": [],
"stop": 1550245184709,
"diff": 1
}
],
"stop": 1550245184709,
"diff": 44
}
],
"stop": 1550245184709,
"diff": 44
}Returns a printable representation of the tree returned by toJSON().
const app = require('avvio')()
app.on('preReady', () => {
console.log(app.prettyPrint())
})Example output:
avvio 56 ms
├── first 52 ms
├── second 1 ms
└── third 2 ms
avvio uses @fastify/error to generate its errors.
Every error exposes a stable code and message.
| Code | Message | Notes |
|---|---|---|
AVV_ERR_EXPOSE_ALREADY_DEFINED |
'%s' is already defined, specify an expose option for '%s' |
Thrown when a property that Avvio needs to expose on the instance (use, after, ready, onClose, close) is already taken. Use the expose option to rename it. |
AVV_ERR_ATTRIBUTE_ALREADY_DEFINED |
'%s' is already defined |
Thrown when the instance already has a then property, which Avvio needs to make itself awaitable. |
AVV_ERR_CALLBACK_NOT_FN |
Callback for '%s' hook is not a function. Received: '%s' |
Thrown when a non-function value is passed where a callback (ready, close, onClose, etc.) is expected. |
AVV_ERR_PLUGIN_NOT_VALID |
Plugin must be a function or a promise. Received: '%s' |
Thrown when the value passed to use() is neither a function nor a thenable. |
AVV_ERR_ROOT_PLG_BOOTED |
Root plugin has already booted |
Thrown when calling use() after the instance has already booted. |
AVV_ERR_PARENT_PLG_LOADED |
Impossible to load '%s' plugin because the parent '%s' was already loaded |
Thrown when trying to register a plugin on a parent that has already finished loading. |
AVV_ERR_READY_TIMEOUT |
Plugin did not start in time: '%s'. You may have forgotten to call 'done' function or to resolve a Promise |
Thrown when a ready() or after() callback does not complete before the configured timeout elapses. |
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 |
Thrown when a plugin passed to use() does not complete before the configured timeout elapses. |
The plugin or callback name used in '%s' is derived from the function name, an explicit name option, or, for fastify-plugin-wrapped plugins, the metadata attached to the plugin.
This is also the name shown by prettyPrint() and toJSON().
When a timeout occurs, the error message includes the name of the plugin or callback that did not complete.
If a plugin uses the callback API, make sure done() is called:
app.use(function plugin (instance, opts, done) {
initializeSomething()
done()
})If a plugin uses async/await, make sure its Promise resolves:
app.use(async function plugin (instance, opts) {
await initializeSomething()
})This project was kindly sponsored by nearForm.
Copyright Matteo Collina 2016-2020, Licensed under MIT.