Diesis is a declarative dependency injection library. It allows to define a group of functions in a graph of dependencies, removing the manual wiring necessary to connect the functions one another. It also ensures every dependency is executed at most once. It works with functions returning synchronously or asynchronously (returning a promise). It also makes testing way easier.
Diesis is written in ES2015 and uses Promises and Maps. It uses commonjs to ensure the broadest compatibility. It runs on all supported node versions and modern browsers without transpilation or polyfilling. It has no dependencies and has a minimal footprint.
You can import diesis as ES module or using commonjs:
import { dependency, run } from 'diesis'
// or
const { dependency, run } = require('diesis')
Let's start with 2 simple functions, one depending on another:
const hello = () => 'hello'
const world = (s) => `${s} world`
Assuming that you want to call "world" with the result of the function "hello". You need to write:
const greeting = hello()
world(greeting) // it returns 'hello world'
diesis can be used to automate the dependency resolution. Let's write the functions like this instead:
const { dependency } = require('diesis')
const hello = dependency(() => 'hello')
const world = dependency([hello], (s) => `${s} world`)
world()
.then((res) => res) // res is 'hello world'
In that way I declared that the first argument of "world" will always be the result of the "hello" function. The resulting function returns always asynchronously.
You can override some dependency (for testing for example). In that case you need to call the function with a ES2015 Map:
const deps = new Map()
deps.set(hello, () => 'bye')
world(deps)
.then((res) => res) // res is 'bye world'
To make thing easier you can also pass an array or key/value pairs. This will be converted to a Map:
world([[hello, () => 'bye']])
.then((res) => res) // res is 'bye world'
You can use the same feature to inject a different sets of dependencies. If these dependencies are defined with a string, you can pass an object instead of a map:
const hello = dependency(() => 'hello')
const world = dependency([hello, 'emphasis'], (s, emphasis) => `${s} world${emphasis && '!'}`)
world({ emphasis: true })
.then((res) => res) // res is 'hello world!'
world({ emphasis: false })
.then((res) => res) // res is 'hello world'
If there are no dependencies you can omit the first argument. Or just use a function. You can pass any value instead of a function, this will be converted to a function returning that value. For example:
const hello = dependency([], () => 'hello')
// is equivalent to
const hello = dependency(() => 'hello')
// is equivalent to
const hello = () => 'hello')
// is also equivalent to
const hello = dependency('hello')
If you want to run multiple dependencies you might be tempted to use Promise.all:
Promise.all([dependency1(), dependency2(), dependency3()])
.then((res) => res) // res is an array containing the 3 resolved dependencies
This might return the correct result (if the dependencies are pure functions). But common dependencies can be executed multiple times. To avoid this, you can use run:
const run = require('diesis').run
run([dependency1, dependency2, dependency3]) // this can take an additional argument to override dependencies
.then((res) => res) // res is an array containing the 3 resolved dependencies
You can use run
to run a single dependency as well:
run(dependency1)
.then((res) => res)
// is equivalent to
dependency1()
.then((res) => res)
Often times you want to execute a dependency once and store the result. For example, creating a connection to a database.
This is the behaviour of dependencyMemo
:
const dbConnection = dependencyMemo([config], (config) => {
// this function is executed only once. The result is memoized.
})
A dependency is only executed when necessary, so config
is not executed when the result of dbConnection is memoized.
You can clear the previously saved result using the reset
method (on the dependency object).
dbConnection.dep.reset()
A piece of advice: there is a caveat, exporting a memoized dependency with a side effect (for example, opening a network connection). Because of the way node resolve dependencies, different dependencies might be used. Let's say that your application "A" dependes on packages "B" and "C". "B" and "C" depends on "D" which exports a memoized dependency. But "B" and "C" requires a different incompatible version of "D".
In this case node will resolve 2 distinct version of "D" and they will be memoized independently. You can verify this with npm list
.
If you are still struggling to understand how this can be useful, here's a more articulated example. For making it simpler I'll omit the implementation of these functions, providing only a description.
const dependency = require('diesis').dependency
const dbConnection = dependencyMemo([],() => {
// this function returns the connection to a db
// this function is memoized to avoid creating multiple connections
})
const userData = dependency(['userId'], (userId) => {
// this function returns user informations, from an external service
})
const resourceData = dependency([dbConnection, 'resourceId'], (dbConnection, resourceId) => {
// this function returns resource informations, from the db
})
const template = dependency([], () => {
// return the compiled template
})
const renderTemplate = dependency([template, userData, resourceData], (template, userData, resourceData) => {
// render the template with the given informations
})
renderTemplate({ userId: 12345, resourceId: 23456 })
.then(markup => {
//
})
the "dependency" object contains the original function in the "func" attribute. This can be used to test the function in isolation:
const hello = dependency(() => 'hello')
const world = dependency([hello], (s) => `${s} world`)
world.dep.func('test') // returns 'test world'