avvio

repository·main·Indexed 19 days ago

https://github.com/fastify/avvio

An asynchronous bootstrapping library for Node applications designed to handle plugin loading, error handling, and load order. It features a graph-based, reentrant bootstrapper that supports callbacks, Promises, and async/await, allowing plugins to be loaded in a specific order even when nested. Version 9.3.0 provides tools for lifecycle management via .use(), .after(), and .ready(), as well as shutdown procedures using .close() and Symbol.asyncDispose.

Tokens
3.9K
Snippets
17
Records
17
Agent score
17%

What's inside avvio

  1. Basic usage of avvio for asynchronous bootstrapping

    main

    Avvio is a graph-based, reentrant asynchronous bootstrapper. It allows you to load plugins or components in a specific order, even when loading plugins from within other plugins. It supports both callback-style and async/await functions.

    Key features:

    • Reentrancy: You can call .use() inside a plugin being loaded by avvio.
    • Graph-based: Ensures correct load order and dependency management.
    • Flexible API: Supports callbacks, Promises, and async/await.
    'use strict'
    
    const app = require('avvio')()
    
    app
      .use(first, { hello: 'world' })
      .after((err, cb) => {
        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 function third (instance, opts) {
      console.log('third loaded')
    }
  2. Initialize Avvio with avvio()

    main

    To start the Avvio sequence, call avvio([instance], [options], [started]). The instance is the object representing your application (e.g., a server object). Avvio will augment this instance with the use, after, and ready methods. You can also use avvio as a constructor to inherit from it.

    Configuration Options

    • expose: A key/value property to change how use, after, and ready are exposed on the instance.
    • autostart: If set to false, Avvio will not start loading plugins automatically; it will wait for a call to .start() or .ready().
    • timeout: The number of milliseconds to wait for a plugin to load before erroring with ERR_AVVIO_PLUGIN_TIMEOUT. Default is 0 (disabled).

    Events

    • 'start': Emitted when the application starts.
    • 'preReady': Emitted before the ready queue is run.
    const server = {}
    
    require('avvio')(server)
    
    server.use(function first (s, opts, cb) {
      // s is the same as server
      s.use(function second (s, opts, cb) {
        cb()
      })
      cb()
    }).after(function (err, cb) {
      // after first and second are finished
      cb()
    })
  3. Override server instances with app.override()

    main

    The app.override(server, plugin, options) method allows you to customize the server instance passed to each loading plugin. This enables creating an inheritance chain of server instances.

    To use this, you must provide an implementation of override on your Avvio instance. When a plugin is loaded, Avvio will call your override function to generate the instance that the plugin will receive.

    const server = { count: 0 }
    const app = require('avvio')(server)
    
    app.override = function (s, fn, opts) {
      // create a new instance with the server as the prototype
      const res = Object.create(s)
      res.count = res.count + 1
      return res
    }
    
    app.use(function first (s1, opts, cb) {
      // s1 is the overridden instance
      cb()
    })
  4. Execute logic after plugins with app.after()

    main

    The app.after(func) method calls a function after all previously defined plugins (and their dependencies) have loaded. Note that the 'start' event is not yet emitted.

    Callback Signatures

    The callback behavior changes based on the number of arguments provided:

    1. 0 args: (err) => void. If an error occurs, it is passed to the next error handler.
    2. 1 arg: (err) => void. The parameter is the error object.
    3. 2 args: (err, done) => void. The first is the error, the second is the done callback.
    4. 3 args: (err, context, done) => void. The first is the error, the second is the top-level context, and the third is the done callback.

    Awaitable Alternative

    You can use await app.after() (with no arguments) as a shorthand to load all previously registered plugins and wait for them to finish. Unlike the callback version, await app.after() is not chainable.

    // async after with one parameter
    app.after(async function (err) {
      await sleep(10)
      if (err) {
        throw err
      }
    })
    
    // await after (shorthand for loading previous plugins)
    await app.after()
  5. Finalize loading with app.ready()

    main

    The app.ready([callback]) method ensures all plugins and after calls are completed. It is executed before the 'start' event is emitted.

    Callback Signatures

    1. 0 args: Returns a Promise that resolves when plugins/after calls are complete.
    2. 1 arg: (err) => void. The parameter is the error object.
    3. 2 args: (err, done) => void. The first is the error, the second is the done callback.
    4. 3 args: (err, context, done) => void. The first is the error, the second is the top-level context, and the third is the done callback.

    If autostart: false was used during initialization, calling .ready() will also trigger the boot sequence.

    // ready with Promise
    app.ready()
      .then(() => console.log('Ready'))
      .catch(err => {
        console.error(err)
        process.exit(1)
      })
    
    // await ready
    async function main () {
      try {
        await app.ready()
        console.log('Ready')
      } catch(err) {
        console.error(err)
        process.exit(1)
      }
    }
  6. Inspect plugin loading with toJSON() and prettyPrint()

    main

    Avvio provides tools to inspect the plugin loading tree and timing.

    • avvio.toJSON(): Returns a JSON tree representing the state of the plugins and their loading times. It is best called during the 'preReady' event to ensure the tree is complete.
    • avvio.prettyPrint(): Returns a human-readable string representation of the plugin tree (similar to the output of toJSON).
    const avvio = require('avvio')()
    avvio.on('preReady', () => {
      console.log(avvio.prettyPrint())
    })
  7. Load plugins with app.use()

    main

    The app.use(func, [optsOrFunc]) method loads one or more functions asynchronously.

    Plugin Signature

    Plugins must follow one of these patterns:

    1. Callback style: (instance, options, done) => void. You must call done() exactly once when the plugin is ready.
    2. Promise style: async (instance, options) => void. If the function returns a Promise, the signature is not required.
    3. Immediate style: If the plugin is ready immediately, you can omit done from the signature.

    Advanced Usage

    • Chainable/Awaitable: use returns a thenable wrapped instance, allowing you to chain calls or await app.use(...).
    • Dynamic Options: If the second argument to use is a function, it receives the parent instance as its first argument. This allows a plugin to inject variables into subsequent plugins.
    • ESM Support: You can use import() to load ESM modules: app.use(import('./plugin.mjs')).

    Error Handling

    To catch errors during plugin loading, you must use the .ready() method.

    function plugin (server, opts, done) {
      done()
    }
    
    app.use(plugin)
  8. Shutdown with app.close() and Symbol.asyncDispose

    main

    To shut down the application and clean up resources, use app.close() or the [Symbol.asyncDispose]() method.

    app.close()

    Starts the shutdown procedure. It executes all registered onClose callbacks.

    • If no callback is provided, it returns a Promise.
    • If a callback is provided, it follows the standard error/context/done signature patterns.

    Symbol.asyncDispose

    For Node.js 20+, app[Symbol.asyncDispose]() is an alias for app.close(). This allows using the await using syntax for automatic resource cleanup when a scope is exited, which is highly recommended for unit tests.

    app.onClose(func)

    Registers a callback to be fired once close is called. If the callback returns a Promise, the shutdown will wait for that Promise to resolve/reject before proceeding to the next onClose callback or finishing the close procedure.

    // Using Symbol.asyncDispose for automatic cleanup
    test('my test', async () => {
      await using app = avvio()
    
      app.use(function (server, opts, done) {
        done()
      })
    
      await app.ready()
      // app.close() will be called automatically when exiting this scope
    })
  9. Initialize Avvio for plugin management

    main

    Avvio is used to manage the loading order and lifecycle of plugins. You can instantiate it directly or use it to wrap an existing server instance. When wrapping a server, Avvio can expose its API methods (like use, ready, onClose) directly onto that server via the expose option.

    Constructor Signature: new Boot(server, opts, done)

    • server: An optional existing instance (e.g., a Fastify server) that you want to attach Avio's management capabilities to.
    • opts: Configuration object:
      • autostart (boolean, default: true): Whether to automatically call .start().
      • timeout (number, default: 0): Timeout in milliseconds for plugin loading and ready states.
      • expose (object): Configuration to map Avvio methods to the server instance. Keys include use, after, ready, onClose, and close.
    const Boot = require('avvio');
    
    // Option 1: Create a standalone Avvio instance
    const avvio = new Boot();
    
    // Option 2: Wrap an existing server
    const server = {}; // your server instance
    const avvio = new Boot(server, {
      expose: {
        use: 'use',
        ready: 'ready',
        onClose: 'onClose'
      }
    });
  10. Register a plugin with use()

    main

    Use the .use(plugin, [opts]) method to register a plugin for loading. Plugins can be standard functions or Promise-based functions. If a plugin is a bundled or TypeScript module, Avvio will automatically handle the .default property.

    Returns the Avvio instance to allow chaining.

    avvio.use(myPlugin, { someOption: true });
  11. Execute logic after plugins load with after()

    main

    The .after(func) method allows you to register a callback that executes after the currently registered plugins have been loaded. This is useful for performing setup tasks that depend on the state initialized by plugins.

    If no function is passed to .after(), it returns a Promise that resolves when the currently registered plugins are loaded.

    // Using a callback
    avvio.after((err, context, done) => {
      if (err) return done(err);
      // Do something after plugins are loaded
      done();
    });
    
    // Using a Promise
    await avvio.after();