An inversion-of-control container for managing dependencies. Supports constructor, property and method injection
npm install saharaSahara is an inversion of control container. It supports constructor,
property and method injection, parsing the method signature automatically
to determine dependencies.

npm install sahara.Requires Node >= v8.0.0.
``javascript
class Container {
registerType(ctor, options) {}
registerTypeAndArgAlias(ctor, key, argAlias) {}
registerInstance(instance, options) {}
registerInstanceAndArgAlias(instance, key, argAlias = null) {}
registerFactory(factory, options = {}) {}
registerFactoryAndArgAlias(instance, key, argAlias = null) {}
isRegistered(key) {}
resolve(key) {}
resolveSync(key) {}
tryResolve(key) {}
tryResolveSync(key) {}
inject(instance, key = null) {}
injectSync(instance, key = null) {}
createChildContainer(withEvents = false) {}
}
sahara.inject = {
property: (name, key) => PropertyInjection,
propertyValue: (name, value) => PropertyValueInjection,
method: (name, args) => MethodInjection
};
sahara.lifetime = {
transient: () => TransientLifetime,
memory: () => MemoryLifetime
};
`
Of note: there are two ways to register something in the container. You can
explicitly specify the key, or pass an options object.
`javascript
container.registerInstance({}, {
key: 'foo',
lifetime: lifetime.memory(),
injections: [inject.property('foo', 'bar'), inject.method('doStuff', ['arg1', 'arg2'])]
});
// or just the key
container.registerInstance({}, 'foo');
// or without a key if the name of the type can be deduced
class foo {}
container.registerType(foo);
`
and retrieve it
using resolveSync() (or, asynchronously using Promises with resolve()):`javascript
const Container = require('sahara').Container;
const container = new Container();const myObject = { oh: 'hi mark' };
container.registerInstance(myObject, 'MyObject');
const instance = container.resolveSync('MyObject');
console.log(myObject === instance); //true, they are literally the same instance
`But that's not abundantly interesting. The magic comes when you want to inject
your
myObject instance somewhere else. Like into another object via a constructor
argument:`javascript
function AnotherObject(/* MyObject /theObject) {
this.obj = theObject;
}container.registerType(AnotherObject);
const anotherObject = container.resolveSync(AnotherObject);
console.log(anotherObject.obj); // { oh: 'hai mark' }
`This will dynamically create an instance of
AnotherObject, using the
container to resolve its dependencies (in this case, it's dependency was
of type MyObject).$3
You might have noticed there was a little bit of hand-waving going on up
above. When registering a type, Sahara will do some fancy regular expression
voodoo to ferret out the constructor signature, as well as the types of
the constructor arguments.Since JavaScript isn't even close to a statically typed language, and doesn't
have a reflection API, we have to use doc comments to specify the types.
Specifically, they must be
inline doc comments.
`javascript
function Foo(message) { this.message = message; }
function Bar(/* Foo /foo) { this.foo = foo; }
function Baz(/* Bar /bar) { this.bar = bar; }container
.registerInstance(new Foo('oh hi mark'))
.registerType(Bar)
.registerType(Baz)
.resolveSync(Baz)
.bar.foo.message; //"oh hi mark"
`NOTE: When using
registerType, you MUST register a class or a function that
is a constructor.#### Registering arguments
__As of v6.0.0__ you can use the
register*AndArgAlias() methods to register a specific
_argument name_. This will eliminate the need for doc comments and play nice with bundlers
that remove comments. The one caveat is that these registrations apply to _all_ resolutions
where the argument name matches. So _any_ method with a parameter named foo will be
resolved by whatever you registered with registerTypeAndArgAlias(Foo, 'foo').Note that you __cannot use doc comments with register*AsArg() functions__. In other
words, your function signature must not have a doc comment for arguments that are
registered via
register*AndArgAlias(). Doc comments take precedence and will be used instead
of the named argument key.For example:
`javascript
class Foo {
constructor(arg1, arg2) {}
}class Bar {}
class Baz {}
container
.registerType(Foo)
.registerTypeAndArgAlias(Bar, 'arg1')
.registerInstanceAndArgAlias(new Baz(), 'myBarInstance', 'arg2')
.resolveSync(Foo);
`Internally this actually registers the
Bar type with resolution key $arg:arg1. So you
can also do container.resolve('$arg:arg1'). This is an internal implementation detail and
should not be relied upon (i.e. the $arg: prefix could change at any time).#### Register aliases
__As of v6.0.0__ you can use
registerAlias() to register an alias of a previously
registered type/instance/object.`javascript
const instance = {};container
.registerInstance(instance, 'myInstance')
.registerAlias('myInstance', 'somethingElse');
console.log(container.resolveSync('myInstance') === container.resolveSync('somethingElse')); // true
`This is the same mechanism by which __arg aliases__ work as described above.
#### Named functions
By default, Sahara will use the name of the class or constructor as the resolution key.
As a means of convenience (as you can see by most of the examples on this page),
you can also pass the constructor to the
resolveSync() function instead of
the resolution key.If you pass a constructor to
resolveSync(), it'll use ctor.name to deduce the
resolution key.Otherwise, you need to pass in a
key property in the options argument
to the register*() methods. Alternatively, as a means of convenience,
you can also just pass a straight-up string as the second argument in
lieu of the options object, and it will be used as the key.`javascript
function Foo() {}//the following registrations are equivalent
container.registerType(Foo);
container.registerType(Foo, { key: 'Foo' });
container.registerType(Foo, 'Foo');
//the following resolutions are equivalent
container.resolveSync(Foo); //uses Foo.name
container.resolveSync('Foo');
`When registering instances, it'll try and use
instance.constructor.name to
get the resolution key.`javascript
function Foo() {}const instance = new Foo();
//the following registrations are equivalent
container.registerInstance(instance)
container.registerInstance(instance, { key: 'Foo' });
container.registerInstance(instance, 'Foo');
`#### Anonymous functions
If you don't have a named function, you can also register an anonymous
function, but you must provide a resolution key for it:
`javascript
const foo = function() {};
container.registerType(foo, 'MySpecialName');
const fooInstance = container.resolveSync('MySpecialName');//with an instance
container.registerInstance(fooInstance, 'AnotherSpecialName');
const sameInstance = container.resolveSync('AnotherSpecialName');
`#### Classes
As of v2.0.0, sahara supports classes with or without a
constructor method.`javascript
class Foo {
constructor() {}
}
class Bar {}container
.registerType(Foo)
.registerType(Bar);
`$3
In some cases you'll want to defer the creation of an object until
it's absolutely needed. You can do this by using container.registerFactory().
Your factory function should take in one argument, the container.This is mostly used as a replacement for
registerInstance(), but for
the times when you don't want to create the instance immediately.Note that the
key option is required when using registerFactory().`javascript
function Foo() {}container.registerFactory(container => new Foo(), 'MyKey');
container.resolveSync('MyKey');
`$3
Use the container.isRegistered() function.`javascript
function foo() {}container.registerType(foo);
console.log(container.isRegistered(foo)); //true
console.log(container.isRegistered('foo')); //true
console.log(container.isRegistered('bar')); //false
`$3
Note: as of v5.0.0 sahara now uses Promises instead of callbacksAll examples given are synchronous. However, if you need to resolve something
asynchronously, you can use the
resolve(key) function. Note that
everything (from resolution, to injection, to object building) will be
asynchronous, so bear that in mind.This is most relevant for
registerFactory(), because you either have to be
very careful, or make sure your factory function can handle both async
and synchronous code paths.Here is an asynchronous example:
`javascript
function createThingAsync(container) {
return new Promise(resolve => setTimeout(() => resolve({}), 1000));
}const container = new Container()
.registerFactory(createThingAsync, 'Thing');
container.resolve('Thing')
.then((thing) => { / ... / })
.catch((err) => console.error(err));
`Note that if you try to resolve
Thing synchronously, a promise is still returned.
This behavior was changed in v5.0.0; previously it would return undefined unless
explicitly handled.$3
...are bad.`javascript
function Foo(/* Bar /bar) {}
function Bar(/* Foo /foo) {}new Container().registerType(Foo); //throws "Cyclic dependency from Foo to Bar"
`$3
But wait, there's more configuration! To give you more fine-grained control
over the objects that Sahara creates, you can specify a Lifetime. This
basically tells Sahara how to store the object, in effect governing the
lifetime of the object.The default lifetime is the
TransientLifetime, which means that every time
you call resolve() Sahara will pretend like it's never seen this type
before and run the object building sequence each time.The
MemoryLifetime will store the
object instance in memory, and reuse that instance every time the container
attempts to resolve that type. This is useful when you have an object
with an expensive construction time (e.g. a database connection) that
you would want to reuse for the duration of your script.`javascript
const lifetime = require('sahara').lifetime;function DbConnection() {
this.client = db.connect({...});
}
container.registerType(DbConnection, { lifetime: lifetime.memory() });
`$3
By default, Sahara performs constructor injection. That is, it resolves
dependencies that are specified in the constructor. What if you have
dependencies that are not in the constructor? Well, there are a few ways
to alleviate that problem as well.NOTE: cyclic dependencies are not detected when performing property
and method injection, because I haven't figured out a clever way of
doing it yet. So if you see a "maximum stack size exceeded" error,
you probably created a cyclic dependency.
#### Property injection
Property injection simply sets the value of a property on an object
when it is
resolve()'d.There are two ways to do it. You can simply give the value of the
property:
`javascript
const inject = sahara.inject;function Foo() {
this.value = 'foo';
}
container.registerType(Foo, { injections: [ inject.propertyValue('value', 'bar') ] });
console.log(container.resolveSync(Foo).value); //"bar"
`Or you can have the container resolve the type. In this case, you
must specify the property's type:
`javascript
function Foo() {
this.value = 'foo';
}
function Bar() {
this.toString = function() { return 'I am Bar'; };
}container
.registerType(Foo, { injections: [ inject.property('value', 'Bar') ] })
.registerType(Bar);
console.log(container.resolveSync(Foo).value); //"I am Bar"
`#### Method injection
Method injection invokes the specified method with optional arguments
during resolution. Again, you can specify the arguments explicitly:
`javascript
function Foo() {
this.value = 'foo';
this.setValue = function(newValue) {
this.value = newValue;
};
}container.registerType(Foo, { injections: [ inject.method('setValue', [ 'bar' ]) ] });
console.log(container.resolveSync(Foo).value); //"bar"
`Or you can let the container resolve the method's arguments. To
accomplish this, simply omit the optional array of arguments to
inject.method(). Note that this uses the same procedure as
container.registerType(), so you'll need to specify the
types of each parameter with a comment.`javascript
class Foo {
constructor() {
this.value = 'foo';
this.setValue = (/* TheNewValue /newValue) => {
this.value = newValue;
};
}
}
container
.registerType(Foo, { injections: [ inject.method('setValue') ] })
.registerInstance('This is the new value', 'TheNewValue');
console.log(container.resolveSync(Foo).value); //"This is the new value"
`#### Manual injection
The container also provides a method for performing injection on an
already existing object. This can be useful if an object was created
without using the container.
`javascript
class Foo {
constructor() {
this.value = 'foo';
}
}container.registerType(Foo, { injections: [ inject.propertyValue('value', 'bar') ] });
const instance = new Foo();
console.log(instance.value); //"foo"
container.injectSync(instance);
console.log(instance.value); //"bar"
//specify the key explicitly
container.injectSync(instance, 'Foo');
`The async way:
`javascript
container.inject(instance, 'Foo')
.then(() => {
//injection completed successfully
})
.catch((err) => {
console.error(err);
});
`$3
Interception was removed as of v5.0.0. View one of the older releases' README
for documentation.$3
Occasionally the need arises to create a new container that inherits all of the
configurations from another container. This can be accomplished with the
createChildContainer() function.`javascript
class Foo {}const parent = new Container().registerType(Foo);
const child = parent.createChildContainer();
const fooInstance = child.resolveSync(Foo); // instance of Foo
`Anything you do on the parent container will not affect the state of the
child container, and vice versa. They are completely independent.
If you want the child container to inherit the events as well, pass
true
to createChildContainer().`javascript
child = parent.createChildContainer(true);
`$3
Events emitted by a Container instance:-
registering - when a type/instance/factory is being registered
1. arguments[0]: the registration key
1. arguments[1]: the registration type (type, instance or factory)
- resolving - when an object is being resolved
1. arguments[0]: the registration key
- resolved - when an object has been resolved
1. arguments[0]: the registration key
2. arguments[1]: the resolved objectEvents emitted by an
ObjectBuilder instance:-
building - when an object is being built
1. arguments[0]: metadata for the type: { args: [], ctor: Function, name: string }
- built - when an object has been built
1. arguments[0]: metadata for the type (see above)
2. arguments[1]: the object instanceExample:
`javascript
const container = new Container();container
.on('registering', key => console.log(key + ' is being registered'))
.on('resolving', key => console.log(key + ' is being resolved'))
.on('resolved', key => console.log(key + ' has been resolved'));
container.builder
.on('building', info => console.log('building ' + info.name))
.on('built', info => console.log('built ' + info.name));
``