@miracledevs/paradigm-web-di

A minimal dependency injection library for the web

Usage no npm install needed!

<script type="module">
  import miracledevsParadigmWebDi from 'https://cdn.skypack.dev/@miracledevs/paradigm-web-di';
</script>

README

Paradigm Web DI Build Status

A minimal dependency injection framework for the web.

Installing

$ npm i @miracledevs/paradigm-web-di

How to use

The library was written to be easy to use. You can decorate your classes, or manually register them for later use.

If you want to register your classes, just decorate your class with the @Injectable decorator. You'll need to provide the life time of your class:

import { Injectable, DependencyLifeTime } from "@miracledevs/paradigm-web-di";

@Injectable({ lifeTime: DependencyLifeTime.Singleton })
export class ExampleService
{
    getValues(): number[]
    {
        return [1, 2, 3, 4];
    }
}

@Injectable({ lifeTime: DependencyLifeTime.Singleton })
export class AnotherService
{
    constructor(private readonly example: ExampleService)
    {
    }

    getValues(): number[]
    {
        return this.example.getValues().map(x => x + 1);
    }
}

Or, if you prefer to have all the dependencies registered at the same time onto a single point, you can also use the procedural approach:

import { DependencyCollection } from "@miracledevs/paradigm-web-di";

DependencyCollection.globalCollection.registerSingleton(ExampleService);
DependencyCollection.globalCollection.registerSingleton(AnotherService, [ ExampleService ]);

We recommend the first approach because we can automatically extract metadata when executing the decorator, and you don't need to manually describe the class dependencies. But, if you need to manually register a class, you can do it.

To resolve your services, you need a reference to a service container. The easiest way to obtain a container is to build one from the global collection DependencyCollection.globalCollection.buildContainer():

import { DependencyCollection } from "@miracledevs/paradigm-web-di";

const container = DependencyCollection.globalCollection.buildContainer();
const service = container.resolve(AnotherService);
console.log(service.getValues());

Lifetimes

The framework works with 3 dependency types:

Lifetime Meaning
Transient Every time a class request a transient service, the framework will create a new instance. So, each class will have their own instance of the given transient service.
Singleton The framework will allow only one instance of a given type per application life cycle. Every time a class request a singleton reference, the framework will return the same reference.
Scoped The global dependency container allows you to create scoped containers. Scoped containers are containers isolated from the global container. When you mark a service a scoped, that service will exist as a singleton inside the scope asking to resolve the class. This may be difficult to understand at first, but let's present an example: On a web server, each request should have their own instances, and they shouldn't know about each other. In this case, you can create a new scope per request, and scope your singleton services if you want the same reference along the request, but not globally accessible to every request.

Important: Scoped classes can not be resolved in the global container.

Scoped Containers

A Scoped container can be created from a parent container, and they isolate scoped services from other containers. When resolving dependencies, they can look on their parental hierarchy form already instantiated classes if required.

const scopedContainer = container.createScopedInjector("my scope");

Once you have your scoped container, you can used it the same way you would use the global container:

const service = scopedContainer.resolve(AnotherService);
console.log(service.getValues());

Building and Validating the Dependency Tree

The framework can validate if your dependency tree is consistent, to prevent runtime errors:

import { DependencyCollection } from "@miracledevs/paradigm-web-di";

const container = DependencyCollection.globalCollection.buildContainer(true);

The dependency collection is where you register your services, either by calling the DependencyCollection directly, or by using the decorator @Injectable({ ... }). The dependency collection contains the dependency tree you registered, plus some reflection data extracted at declaration time.

When validating the tree, the framework can validate:

  • Circular References: When two classes reference each other, this can derivate into a stack overflow. The dependency can validate this and throw an error informing about this circular reference.

  • Missing Registration: If one of your classes is expecting a service on its constructor, but the latter is not registered, the validation will throw an error telling which classes are failing.

  • Scoped Dependencies on Singletons: If one of your global singletons depends on a scoped service, the framework will fail. Remember, scoped services can only be resolved inside a scoped containers, and singleton live in the global context.

Custom Collections

You can opt out from using the global collection when writing your program. If for some reason you want to use your own collection, or have multiple trees, you can specify the collection when declaring classes:

import { DependencyCollection } from "@miracledevs/paradigm-web-di";

const customCollection = new DependencyCollection();

@Injectable({ collection: customCollection })
class ClassA
{
}

@Injectable({ collection: customCollection })
class ClassB
{
    constructor(private readonly a: ClassA)
    {
    }
}

const container = customContainer.buildContainer(true);
const b = container.resolve(ClassB);

Building and Testing

To build the library:

$ npm run build

To watch-build the library:

$ npm run watch

To watch for changes and build after every change:

$ npm run watch

To test the solution:

$ npm run test

To watch-test the solution:

$ npm run watch-test

To see the test coverage:

$ npm run coverage