Dioma Dependency Injection Container

repository·main·Indexed 42 days ago

https://github.com/zheksoon/dioma

A lightweight dependency injection container for vanilla JavaScript and TypeScript (v0.4.6). Dioma provides async injection, dependency cycle detection, and hierarchical child containers without the use of decorators. It supports multiple lifecycles via Scopes (Singleton, Transient, Container, and Resolution) and allows for decoupled injection using Class, Value, and Factory tokens.

Tokens
6.3K
Snippets
23
Records
31
Agent score
54%

What's inside Dioma

  1. Configure Dependency Scopes

    main

    Dioma provides several scope types to control the lifecycle and instantiation of classes via the static scope property:

    • Scopes.Singleton(): Creates a single instance for the entire application (stored in the global container).
    • Scopes.Transient(): Creates a new instance on every injection.
    • Scopes.Container(): Creates a single instance per container (also referred to as Scopes.Scoped()).
    • Scopes.Resolution(): Creates a new instance every time, but the same instance is reused for the duration of a single resolution (useful for request-scoped data).
    import { Scopes } from "dioma";
    
    class MyService {
      static scope = Scopes.Singleton(); // or Transient(), Container(), Resolution()
    }
  2. Inject using Tokens (Class, Value, and Factory)

    main

    Tokens allow you to inject abstractions (interfaces/abstract classes), constant values, or results from factory functions instead of concrete classes.

    Class Tokens

    Useful for injecting an implementation for an interface or abstract class.

    Value Tokens

    Used to inject constant values.

    Factory Tokens

    Used to inject a value produced by a function. The factory function receives the container as its first argument.

    import { Token, Container } from "dioma";
    
    // Value Token
    const token = new Token<string>("Value token");
    container.register({ token, value: "Value" });
    const value = container.inject(token);
    
    // Factory Token
    const factoryToken = new Token<string>("Factory token");
    container.register({
      token: factoryToken,
      factory: (container, a: string, b: string): string => a + b,
    });
    const result = container.inject(factoryToken, "Hello, ", "world!");
  3. Resolve circular dependencies with async injection

    main

    When a circular dependency is detected (throwing DependencyCycleError), you can resolve it by using injectAsync instead of inject. This allows dependencies to be resolved asynchronously, breaking the immediate synchronous loop.

    Important Lifecycle Note: Even when using await injectAsync(...), you must call container.waitAsync() (or wait for the next tick) to ensure all internal instance promises are fully resolved before using the instances.

    Warning: Avoid using async injection with loops involving transient dependencies, as this can lead to undefined behavior or AsyncDependencyCycleError.

    import { inject, injectAsync, Scopes, globalContainer } from "dioma";
    
    class A {
      constructor(private instanceB = inject(B)) {}
      static scope = Scopes.Singleton();
    }
    
    class B {
      private declare instanceA: A;
    
      // injectAsync returns a promise of the A instance
      constructor(private promiseA = injectAsync(A)) {
        this.promiseA.then((instance) => {
          this.instanceA = instance;
        });
      }
    
      help() { console.log("helping"); }
    }
    
    const a = await injectAsync(A);
    const b = await injectAsync(B);
    
    // Crucial: Wait until all promises are resolved
    await globalContainer.waitAsync();
    
    a.doWork();
  4. Basic Usage of Dioma

    main

    To use Dioma, add a static scope property to your class and use the inject function to retrieve instances. By default, classes become 'sticky' to the container where they are first injected. You can define different lifecycles using Scopes.

    import { inject, Scopes } from "dioma";
    
    class Garage {
      open() {
        console.log("garage opened");
      }
      static scope = Scopes.Singleton();
    }
    
    class Car {
      constructor(private garage = inject(Garage)) {}
    
      park() {
        this.garage.open();
        console.log("car parked");
      }
      static scope = Scopes.Transient();
    }
    
    const car = inject(Car);
    car.park();
  5. Manage Hierarchical Child Containers

    main

    You can create a hierarchy of containers using container.childContainer(). Dioma searches for instances top-down from the current container to the root. If an instance is not found, it is created in the current container or the container where the class was originally registered.

    import { Container, Scopes } from "dioma";
    
    const parent = new Container("Parent");
    const child = parent.childContainer("Child");
    
    class SharedService {
      static scope = Scopes.Container();
    }
    
    // Register in parent
    parent.register({ class: SharedService });
    
    // Child can access the parent's instance
    const instance = child.inject(SharedService);
  6. Token API Reference

    main

    Tokens allow for decoupled dependency injection where you can inject based on an abstract identifier rather than a concrete class.

    • new Token<T>(name?): Creates a new token of type T. Tokens can be used with both inject and injectAsync.
    import { Token, injectAsync } from "dioma";
    
    const token = new Token<A>("A");
    
    class B {
      constructor(private promiseA = injectAsync(token)) {
        // ...
      }
    }
  7. Inject with Constructor Arguments

    main

    For Transient and Resolution scopes, you can pass additional arguments to the constructor during injection by providing them as subsequent arguments to the inject function.

    import { inject, Scopes } from "dioma";
    
    class Pet {
      constructor(public name: string, public owner = inject(Owner)) {}
      static scope = Scopes.Transient();
    }
    
    class Owner {
      static scope = Scopes.Singleton();
    }
    
    // Injecting Pet with the argument "Fluffy"
    const pet = inject(Pet, "Fluffy");
  8. Register and Unregister Classes in a Container

    main

    Use a Container instance to explicitly manage class lifecycles. Registering a class allows you to control its scope or ensure it is shared across child containers. You can also unregister classes to remove them from the container and its children.

    import { Container, Scopes } from "dioma";
    
    const container = new Container();
    
    // Register a class with a specific scope
    container.register({ class: FooBar, scope: Scopes.Transient() });
    
    // Unregister a class
    container.unregister(FooBar);
  9. Use the Injectable interface for type safety

    main

    Dioma uses the Injectable<T> interface to ensure that every class intended for injection has a static scope defined. If a class implements Injectable but lacks a static scope, TypeScript will flag an error. This prevents runtime errors caused by missing scope definitions.

    import { Scopes, Injectable } from "dioma";
    
    class Database implements Injectable<typeof Database> {
      constructor(private url: string) {}
      static scope = Scopes.Singleton();
    }
    import { inject, Scopes, Injectable } from "dioma";
    
    class Database implements Injectable<typeof Database> {
      constructor(private url: string) {}
      connect() {
        console.log(`Connected to ${this.url}`);
      }
    
      static scope = Scopes.Singleton();
    }
    
    // Error, scope is not specified
    class Repository implements Injectable<typeof Repository> {
      constructor(private db = inject(Database)) {}
    }
  10. Use Injection Hooks during Registration

    main

    When registering a class in a container, you can provide beforeInject and beforeCreate hooks to execute logic before the instance is created or injected.

    container.register({
      class: MyClass,
      beforeInject: (container, descriptor, args) => {
        console.log("Before inject");
      },
      beforeCreate: (container, descriptor, args) => {
        console.log("Before create");
      },
    });
  11. Container API Reference

    main

    The Container class is the core of Dioma's dependency injection system. It manages registration, resolution, and lifecycle of dependencies.

    Methods

    • new Container(parent?, name?): Creates a new container. Optionally accepts a parent container for hierarchical resolution.
    • container.inject(classOrToken, ...args): Injects the instance of the class or token. Supports providing constructor arguments.
    • container.injectAsync(classOrToken, ...args): Injects a Promise of the instance.
    • container.waitAsync(): Returns a promise that resolves when all current async injections are resolved.
    • container.register({ class, token?, scope? }): Registers a class with an optional token and scope.
    • container.register({ token, value }): Registers a specific value associated with a token.
    • container.register({ token, factory }): Registers a factory function associated with a token.
    • container.unregister(classOrToken): Removes the class or token from the container.
    • container.childContainer(name?): Creates a new child container.