Iniettore

WARNING: API is significantly changed in v2.0
See documentation in tagged versions for previous usage.
TODO
Table of Content
Features
Extreme late binding
With exception of eager singletons, all instances and dependencies are resolved only when requested rather when registered into the context.
Functional Programming support
FUNCTION
, PROVIDER
and INSTANCE
mappings are the ideal solution to have DI in Functional Programming.
Lifecycle management
iniettore provides contexts and singleton lifecycle management.
Predictable
iniettore handles all operation in a syncronous way so at any point in time you know what is instanciated and what is not.
ECMA Script 5 required features
iniettore assumes that the following ES5 features are available. If you want to use the library in a no-ES5 compatible environment please provide a polyfill. For example see es5-shim.
Object.create
Function.prototype.bind
Quick start
Installation
npm install iniettore --save
Simple usage
import iniettore from 'iniettore'
import { VALUE, LAZY, SINGLETON, CONSTRUCTOR } from 'iniettore'
class UltimateQuestion {
constructor(answer) {
console.log(answer)
}
}
var rootContext = iniettore.create(function (map) {
map('answer').to(42).as(VALUE)
map('question').to(UltimateQuestion).as(LAZY, SINGLETON, CONSTRUCTOR).injecting('answer')
})
var question = rootContext.get('question')
console.log(question instanceof UltimateQuestion)
Concepts
Context
A context is a JS Object that contains the collection of mappings. During the creation of the context it is possible to register several mappings using the registration API provided inside the configuration function. After the context has been fully created it's only possible to request mapping from it using the query API.
import iniettore from 'iniettore'
import { VALUE } from 'iniettore'
var rootContext = iniettore.create(function (map) {
map('answer').to(42).as(VALUE)
})
var answer = rootContext.get('answer')
console.log(answer)
Child context
Contexts can be organized in a hierarchy. Given one context query interface it's possible to create a child context and provide a separate configuration function for registering child specific mappings. The child context can access all the mappings of his parent and ancestor in the same fashion as JS execution context can access parent ones.
A parente cannot see/use any of the mapping registered in its child contexts. A mapping registered in a child context that has the same name of a mapping in the parent context (or any of its ancestor contexts) will shadow the corresponding value in the same way that in JS a variable in a nested scope can shadows a variable defined in a "parent" scope. See Child contexts for more details.
Continuing with the previouse example:
import iniettore from 'iniettore'
import { VALUE, PROVIDER } from 'iniettore'
function questionProvider(answer) {
return {
question: 'What is the Answer to the Ultimate Question of Life, the Universe, and Everything?',
answer
}
}
var rootContext = iniettore.create(function (map) {
map('answer').to(42).as(VALUE)
})
var childContext = rootContext.createChild(function (map) {
map('question').to(questionProvider).as(PROVIDER).injecting('answer')
})
var question = childContext.get('question')
console.log(question)
Advanced usage
Values and instances
import iniettore from 'iniettore'
import { VALUE, INSTANCE } from 'iniettore'
var drone = {
fly: function () { }
}
var rootContext = iniettore.create(function (map) {
map('answer').to(42).as(VALUE)
map('drone').to(drone).as(INSTANCE)
})
var answer = rootContext.get('answer')
console.log(rootContext.get('drone') === drone)
Functions
You can register a function into the context and specify the its dependencies. When requesting the function you will get a partial application of it with all dependencies already satisfied.
import iniettore from 'iniettore'
import { VALUE, FUNCTION } from 'iniettore'
function fooFunction(bar, baz) {
console.log(bar, baz)
}
var rootContext = iniettore.create(function (map) {
map('bar').to('BAR').as(VALUE)
map('foo').to(fooFunction).as(FUNCTION).injecting('bar')
})
var foo = rootContext.get('foo')
foo(42)
Providers
Providers are generic functions that returns object or values specific for your application domain. A factory function can be seen as a special use case of the provider pattern.
Every request will invoke the provider function and return a new value. The returned value depends on the nature of the registered provider function.
import iniettore from 'iniettore'
import { PROVIDER } from 'iniettore'
var idx = 0
function fooProvider(bar) {
idx++
return { idx, bar }
}
var rootContext = iniettore.create(function (map) {
map('bar').to(42).as(VALUE)
map('foo').to(fooProvider).as(PROVIDER).injecting('bar')
})
console.log(rootContext.get('foo'))
console.log(rootContext.get('foo'))
Constructors
You can register constructors specifying the constructor dependencies. Every request will receive a new instance of the specified constructor.
Note: no setters injection is supported at the moment.
import iniettore from 'iniettore'
import { CONSTRUCTOR } from 'iniettore'
var idx = 0
class Bar {
constructor() {
this.idx = ++idx
}
}
var rootContext = iniettore.create(function (map) {
map('bar').to(Bar).as(CONSTRUCTOR)
})
console.log(rootContext.get('bar'))
console.log(rootContext.get('bar'))
Child contexts
Containers can be organized in a hierarchy. Given a context you can create a child context invoking context.createChild(configure :Function) :Object
and providing the configuration function.
A child context can make use all the mappings of the parent context and ancestor contexts.
import iniettore from 'iniettore'
import { VALUE, PROVIDER } from 'iniettore'
function fooProvider(bar, baz) {
return { bar, baz }
}
var rootContext = iniettore.create(function (map) {
map('bar').to(42).as(VALUE)
map('baz').to('pluto').as(VALUE)
})
var childContext = rootContext.createChild(function (map) {
map('bar').to(84).as(VALUE)
map('foo').to(fooProvider).as(PROVIDER).injecting('bar', 'baz')
})
console.log(rootContext.get('bar'))
console.log(childContext.get('bar'))
console.log(childContext.get('foo'))
Blueprints
Blueprint is effectively a convenient way to register a child context factory. The mapping value is the configuration function for the child context. Every time you request the blueprint mapping name you will get a new child context.
import iniettore from 'iniettore'
import { VALUE, BLUEPRINT } from 'iniettore'
function configureChildContext(map) {
map('baz').to('pluto').as(VALUE)
}
var rootContext = iniettore.create(function (map) {
map('bar').to(42).as(VALUE)
map('foo').to(configureChildContext).as(BLUEPRINT)
})
var childContext = rootContext.get('foo')
console.log(childContext.get('bar'))
console.log(childContext.get('baz'))
In case you are interested in only one mapping in the child context you can specify the exported alias. See example below.
import iniettore from 'iniettore'
import { VALUE, FUNCTION, BLUEPRINT } from 'iniettore'
function baz(bar) {
console.log(bar)
}
function configureChildContext(map) {
map('baz').to(baz).as(FUNCTION).injecting('bar')
}
var rootContext = iniettore.create(function (map) {
map('bar').to(42).as(VALUE)
map('foo').to(configureChildContext).as(BLUEPRINT).exports('baz')
})
var baz = rootContext.get('foo')
console.log(baz())
Transient dependencies
EXPERIMENTAL FEATURE: don't abuse of it.
While requesting an alias it's possible to provide temporary dependencies to satisfy dependencies of the requested mapping or one of his dependency.
Note: Transient dependencies cannot be used to satisfy dependencies in the ancestor contexts.
import iniettore from 'iniettore'
import { VALUE, PROVIDER } from 'iniettore'
function fooProvider(bar, baz) {
return { bar, baz }
}
var rootContext = iniettore.create(function (map) {
map('bar').to(42).as(VALUE)
map('foo').to(fooProvider).as(PROVIDER).injecting('bar', 'baz')
})
var transientDependencies = {
baz: 'pluto'
}
var foo = rootContext.using(transientDependencies).get('foo')
console.log(foo)
Service locator
TBC
Singletons
Constructors and Providers can also be marked as singletons. A function registered as SINGLETON, PROVIDER
will be used as singleton instance factory. A constructor registered as SINGLETON, CONSTRUCTOR
will be used to create only once instance of the constructor type.
Singletons can be marked as: LAZY
, EAGER
or TRANSIENT
.
Lazy singletons
A mapping marked as LAZY, SINGLETON
produce a singleton instance that gets created at the first time it is requested. It gets destroyed only when the context it has been registered into gets destroyed. See context.dispose
.
Lazy Singleton Provider
import iniettore from 'iniettore'
import { LAZY, SINGLETON, PROVIDER } from 'iniettore'
var idx = 0
function fooProvider() {
return {
id: ++idx
}
}
var rootContext = iniettore.create(function (map) {
map('foo').to(fooProvider).as(LAZY, SINGLETON, PROVIDER)
})
var foo1 = rootContext.get('foo')
var foo2 = rootContext.get('foo')
console.log(foo1)
console.log(foo1 === foo2)
Lazy Singleton Constructor
import iniettore from 'iniettore'
import { LAZY, SINGLETON, CONSTRUCTOR } from 'iniettore'
var idx = 0
class Bar {
constructor() {
this.idx = ++idx
}
}
var rootContext = iniettore.create(function (map) {
map('bar').to(Bar).as(LAZY, SINGLETON, CONSTRUCTOR)
})
var bar1 = rootContext.get('bar')
var bar2 = rootContext.get('bar')
console.log(bar1)
console.log(bar1 === bar2)
Eager singletons
A mapping marked as EAGER, SINGLETON
gets created at registration time.
All the required dependencies must be already registered in the current context or in one of its ancestors.
Eager singletons gets destroyed when the corresponding context is destoroyed.
import iniettore from 'iniettore'
import { EAGER, SINGLETON, PROVIDER, CONSTRUCTOR, VALUE } from 'iniettore'
var idx = 0
function fooProvider(answer) {
console.log('foo provider invoked:', answer)
return {
id: ++idx
}
}
class Bar {
constructor(answer) {
console.log('Bar instance created', answer)
}
}
var rootContext = iniettore.create(function (map) {
map('answer').to(42).as(VALUE)
map('foo').to(fooProvider).as(EAGER, SINGLETON, PROVIDER).injecting('answer')
map('bar').to(Bar).as(EAGER, SINGLETON, CONSTRUCTOR).injecting('answer')
})
Transient singletons
A mapping marked as TRANSIENT, SINGLETON
produce a temporary lazy singleton instance. The instance gets created at the first time it is requested (directly or as dependency of another mapping) and gets destroyed when is not used anymore.
A transient singleton allows to gurantee that at any given point in time there are no more than one instance of the respective mapping (whetever has been created using a constructor or a provider function).
In order to announce that a singleton is not used anymore you can invoke context.release(name:string):void
method. The instance gets released (i.e. all references to it gets removed) when context.release
is invoked as many time as it has been requested. See examples below.
Transient Singleton Provider
import iniettore from 'iniettore'
import { TRANSIENT, SINGLETON, PROVIDER } from 'iniettore'
var idx = 0
function fooProvider() {
return {
id: ++idx
}
}
var rootContext = iniettore.create(function (map) {
map('foo').to(fooProvider).as(TRANSIENT, SINGLETON, PROVIDER)
})
var foo1 = rootContext.get('foo')
var foo2 = rootContext.get('foo')
console.log(foo1 === foo2)
rootContext.release('foo')
rootContext.release('foo')
var foo3 = rootContext.get('foo')
console.log(foo1 === foo3)
Transient Singleton Constructor
import iniettore from 'iniettore'
import { TRANSIENT, SINGLETON, CONSTRUCTOR } from 'iniettore'
var idx = 0
class Bar {
constructor() {
this.idx = ++idx
}
}
var rootContext = iniettore.create(function (map) {
map('bar').to(Bar).as(TRANSIENT, SINGLETON, CONSTRUCTOR)
})
var bar1 = rootContext.get('bar')
var bar2 = rootContext.get('bar')
console.log(bar1 === bar2)
rootContext.release('bar')
rootContext.release('bar')
var bar3 = rootContext.get('bar')
console.log(bar1 === bar3)
Transient singleton dependencies
import iniettore from 'iniettore'
import { TRANSIENT, SINGLETON, CONSTRUCTOR, PROVIDER } from 'iniettore'
var idx = 0
class Bar {
constructor() {
this.idx = ++idx
}
}
function fooProvider(bar) {
return {
bar,
method: function () {}
}
}
var rootContext = iniettore.create(function (map) {
map('bar').to(Bar).as(TRANSIENT, SINGLETON, CONSTRUCTOR)
map('foo').to(fooProvider).as(TRANSIENT, SINGLETON, PROVIDER).injecting('bar')
})
var foo1 = rootContext.get('foo')
console.log(foo1)
var foo2 = rootContext.get('foo')
console.log(foo1 === foo2)
console.log(foo1.bar === foo2.bar)
rootContext.release('foo')
rootContext.release('foo')
var foo3 = rootContext.get('foo')
console.log(foo3)
console.log(foo1 === foo3)
console.log(foo1.bar === foo3.bar)
Lifecycle
iniettore offers a simple concept of lifecycle management for singleton instances and contexts. Let's see what it means for instances and contexts.
instance.dispose
Given a LAZY, SINGLETON
or TRANSIENT, SINGLETON
instance that implements a method called dispose():void
when the instance gets released the context will invoke it. This allow you to cleanup any hanging reference (e.g. remove event listeners) so the instance can be properly garbage collected.
import iniettore from 'iniettore'
import { LAZY, SINGLETON, CONSTRUCTOR } from 'iniettore'
import { EventEmitter } from 'events'
class Foo {
constructor(events) {
this._events = events
this._events.on('message', this._onMessage)
}
_onMessage(evt) { }
dispose() {
this._events.off('message', this._onMessage)
}
}
var rootContext = iniettore.create(function (map) {
map('events').to(EventEmitter).as(EAGER, SINGLETON, CONSTRUCTOR)
map('foo').to(Foo).as(LAZY, SINGLETON, CONSTRUCTOR).injecting('events')
})
var events = rootContext.get('events')
console.log(events.listeners('message').length)
var foo = rootContext.get('foo')
console.log(events.listeners('message').length)
rootContext.release('foo')
console.log(events.listeners('message').length)
context.dispose
TBC
Troubleshooting
TBC