Asynchronous Listeners

Antora calls listeners synchronously in the order they are registered. This is true even if a listener is marked asynchronous using the async keyword or the listener returns a Promise. Antora will await the completion of the listener invocation before calling the next listener (and thus before continuing its own operation). This behavior differs from the behavior of the built-in NodeEmitter in Node.js.

The benefit of marking a listener as async, or returning a Promise, is that the listener can perform asynchronous operations. Of course, these operations will all be resolved before Antora proceeds, so they are made to behave in a synchronous fashion outside the boundaries of the function.

A Promise ends when the program calls await on it. Obviously, that requirement bubbles all the way up to the top-level function of the program. Antora hides away this detail by allowing you to either define an extension listener as async or return a Promise.

Let’s look at an example of fetching a file from a URL and publishing it to the site.

Example 1. fetch-and-publish-readme-extension.js
module.exports.register = function () {
  this.on('beforePublish', async ({ siteCatalog }) => {
    const https = require('https')
    const contents = await new Promise((resolve, reject) => {
      const buffer = []
      https
        .get('https://gitlab.com/antora/antora/-/raw/HEAD/README.adoc', (response) => {
          response.on('data', (chunk) => buffer.push(chunk.toString()))
          response.on('end', () => resolve(buffer.join('').trimRight()))
        })
        .on('error', reject)
    })
    siteCatalog.addFile({ contents: Buffer.from(contents), out: { path: 'README.adoc' } })
  })
}

Notice that we have added the async keyword to the listener function. This allows us to the use the await keyword inside the function.

As an exercise, you could try retrieving a file from each branch of each content source and adding it to the published site. To give you a hint, you would need to access the playbook variable to get a list of content sources.

If you don’t want Antora to wait for the completion of your asynchronous listener, you can either return an empty promise (e.g, return Promise.resolve()), or you can remove the async keyword from your listener. However, if you do so, you’ll need to add a listener that listens for an event that is emitted later in the generator, such as contextClosed, so you can resolve the promise before Antora completes.

Let’s look at the same example as earlier, except it downloads the README.adoc in the background while the site is being generated. To help manage the state of the pending promise, it has also been rewritten as a class-based extension.

Example 2. background-fetch-and-publish-readme-extension.js
const https = require('https')

class FetchAndPublishReadmeExtension {
  // alternate way to export register method
  //static register ({ config }) {
  //  return new FetchAndPublishReadmeExtension(this, config)
  //}

  constructor (context, config) {
    ;(this.context = context)
      .on('playbookBuilt', this.onPlaybookBuilt.bind(this))
      .on('beforePublish', this.onBeforePublish.bind(this))
    this.readmeUrl = config.readmeUrl || 'https://gitlab.com/antora/antora/-/raw/HEAD/README.adoc'
    this.contentsPromise = undefined
  }

  onPlaybookBuilt ({ siteCatalog }) {
    this.contentsPromise = new Promise((resolve, reject) => {
      const buffer = []
      https
        .get(this.readmeUrl, (response) => {
          response.on('data', (chunk) => buffer.push(chunk.toString()))
          response.on('end', () => resolve(buffer.join('').trimRight()))
        })
        .on('error', reject)
    })
  }

  async onBeforePublish ({ siteCatalog }) {
    const contents = await this.contentsPromise
    siteCatalog.addFile({ contents: Buffer.from(contents), out: { path: 'README.adoc' } })
  }
}

FetchAndPublishReadmeExtension.register = function ({ config }) {
  return new FetchAndPublishReadmeExtension(this, config)
}
// or
//FetchAndPublishReadmeExtension.register = (context, { config }) => new FetchAndPublishReadmeExtension(context, config)

module.exports = FetchAndPublishReadmeExtension

Notice that only the onBeforePublish listener function is async so it can wait for the promise started by the onPlaybookBuilt listener function. The extension also now accepts the URL of the README as a configuration key named readme_url.