Config providers

Config providers

Some configuration files like (config/hash.ts) do not export config as a plain object. Instead, they export a config provider. The config provider provides a transparent API for packages to lazily compute the configuration after the application is booted.

Without config providers

To understand config providers, let's see what the config/hash.ts file would look like if we were not using config providers.

import { Scrypt } from '@adonisjs/core/hash/drivers/scrypt'
export default {
default: 'scrypt',
list: {
scrypt: () => new Scrypt({
cost: 16384,
blockSize: 8,
parallelization: 1,
maxMemory: 33554432,
})
}
}

So far, so good. Instead of referencing the scrypt driver from the drivers collection. We are importing it directly and returning an instance using a factory function.

Let's say the Scrypt driver needs an instance of the Emitter class to emit an event every time it hashes a value.

import { Scrypt } from '@adonisjs/core/hash/drivers/scrypt'
import emitter from '@adonisjs/core/services/emitter'
export default {
default: 'scrypt',
list: {
scrypt: () => new Scrypt({
cost: 16384,
blockSize: 8,
parallelization: 1,
maxMemory: 33554432,
}, emitter)
}
}

🚨 The above example will fail because the AdonisJS container services are unavailable until the application has been booted and the config files are imported before the application boot phase.

Well, that's a problem with AdonisJS architecture 🤷🏻‍♂️

Not really. Let's not use the container service and create an instance of the Emitter class directly within the config file.

import { Scrypt } from '@adonisjs/core/hash/drivers/scrypt'
import emitter from '@adonisjs/core/services/emitter'
import { Emitter } from '@adonisjs/core/events'
const emitter = new Emitter()
export default {
default: 'scrypt',
list: {
scrypt: () => new Scrypt({
cost: 16384,
blockSize: 8,
parallelization: 1,
maxMemory: 33554432,
}, emitter)
}
}

Now, we have a new problem. The emitter instance we have created for the Scrypt driver is not globally available for us to import and listen for events emitted by the driver.

Therefore, you might want to move the construction of the Emitter class to its file and export an instance of it. This way, you can pass the emitter instance to the driver and use it to listen to events.

start/emitter.ts
import { Emitter } from '@adonisjs/core/events'
export const emitter = new Emitter()
import { Scrypt } from '@adonisjs/core/hash/drivers/scrypt'
import { Emitter } from '@adonisjs/core/events'
import { emitter } from '#start/emitter'
const emitter = new Emitter()
export default {
default: 'scrypt',
list: {
scrypt: () => new Scrypt({
cost: 16384,
blockSize: 8,
parallelization: 1,
maxMemory: 33554432,
}, emitter)
}
}

The above code will work fine. However, you are manually constructing the dependencies your application needs this time. As a result, your application will have a lot of boilerplate code to wire everything together.

With AdonisJS, we strive to write minimal boilerplate code and use the IoC container for lookup dependencies.

With config provider

Now, let's re-write the config/hash.ts file and use a config provider this time. Config provider is a fancy name for a function that accepts an instance of the Application class and resolves its dependencies using the container.

import { configProvider } from '@adonisjs/core'
import { Scrypt } from '@adonisjs/core/hash/drivers/scrypt'
export default {
default: 'scrypt',
list: {
scrypt: configProvider.create(async (app) => {
const emitter = await app.container.make('emitter')
return () => new Scrypt({
cost: 16384,
blockSize: 8,
parallelization: 1,
maxMemory: 33554432,
}, emitter)
})
}
}

Once you use the hash service, the config provider for the scrypt driver will be executed to resolve its dependencies. As a result, we do not attempt to look up the emitter until we use the hash service elsewhere inside our code.

Since config providers are async, you might want to import the Scrypt driver lazily via dynamic import.

import { configProvider } from '@adonisjs/core'
import { Scrypt } from '@adonisjs/core/hash/drivers/scrypt'
export default {
default: 'scrypt',
list: {
scrypt: configProvider.create(async (app) => {
const { Scrypt } = await import('@adonisjs/core/hash/drivers/scrypt')
const emitter = await app.container.make('emitter')
return () => new Scrypt({
cost: 16384,
blockSize: 8,
parallelization: 1,
maxMemory: 33554432,
}, emitter)
})
}
}

How do I access the resolved config?

You may access the resolved config from the service directly. For example, in the case of the hash service, you can get a reference to the resolved config as follows.

import hash from '@adonisjs/core/services/hash'
console.log(hash.config)