@stnekroman/inversityts
TypeScript icon, indicating that this package has built-in type declarations

0.0.6 • Public • Published

Stand With Ukraine

Lightweight IoC (Inversion of Control) and DI (Dependency Injection) library.

Inspired by Angular and Spring Frameworks.
Injectors serve function of IoC containers.

Features:

  • Injectors have hierarchy - everything known on parent level will be available for DI at child level.
  • Child injectors can override parent tokens for his level and for nested, if any.
  • Injectors can be detached from root.
  • Tokens can be multiple in scope of one injector - you can request singleton instance from injectable or all, it it's multi.
  • Injectables have scopes.
  • Has circular dependency detection
  • Supports @Configuration classes
  • Supprots different value providers for injectable
  • This library doesn't use reflect-metadata, the solution is prototype-based.

Table of Contents

Installation

npm

npm i @stnekroman/inversityts

yarn

yarn add @stnekroman/inversityts

And in your tsconfig.json you need to set (if you want to use decorators)

{
  "compilerOptions": {
    "experimentalDecorators": true
  }
}

Terminology

token - something, against what (or "for what") to create new instance.
Token can be literally anything - Symbol (recommended), string, object, class.
instance - result of DI process (Injector.createInstance() call) against requested token.
injectable - defined rule for creation of new instance(s) for specific token.
You can treat it as "bridge" from token to instance
injector - IoC container, which holds already created instances, holds definitions of injectables and can create instances

API Overview

@Injectable

Defines new injectable, describes how to create instance of it (single value, factory or class))

Can be called:

  • as decorator on class - then class will be used to create new instances via new
  • as decorator on class method - if class annotated with @Configuration, then annotated method will be used as factory for new instances. (works both on static and non-static method, see @Configuration` for more info)
  • as function call to manually define new injectable
Param Description
token Token for new injectable
options Optional options structure, which may contain:
* injector - direct ref to injector to use
*tags - reversed for future, not in use currently
*class - (for function call only) specifies which class to use to generate new instance
*value - (for function call only) specifies static value, which will be used as instance
*redirect - (for function call only) specifies another token, which will be used instead
*factory - (for function call only) specifies factory function which will be used to generate new instance
*dependencies - (for function call only) additional possible input dependenes for factory function - list of tokens
*scope - indicates when to re-use existing instance, when - to create new

Scopes

List of possible scopes:

Scope Description
singleton (default) Only one instance is create for injectable, cached and shared for future injects
prototype Each new inject call will cause new instance to be created
cache Internal caching map will be used, you need only specify provider, which will provide string keys to that map.
On key change - new instance will be created. In order to active this scope mode you need just pass in
a function () => string, cache key provider
weak Optional param, which is in use onlye in case of using caching scope.
When set to true - internal cache will hold WeakRefs, which won't prevent garbage collector from collecting such references.
So weak caching scope can be used when we don't want to keep created instance in memory, if that is not used (referenced) already.
custom That is possible to completely override logic of scoping and instances creation.
Via passing in class extends ScopeProvider.

@Inject

Function to create (or get, if already exists) instance against given token.

Param Description
token Token against which to create (or get) new injectable
injector Optional injectorto use

@Inject.Param

Decorator, used to annotate parameters in class constructor or factory function as injectable targets.
DI process will use this info to find/create corresponding instances and inject them as parameter.
Accepts only one argument - token

Injector

IoC container, used to register, get and create injectable instances against given tokens.

Method Description
root (static) Built-in default global root injector
getCurrentInjector (static) Method to return 'current' injector, associated with current context
register Low-level method to register injectables. Better use Injectable instead
get Resolves given token to instace.
* If injector has it internal cache - returns ready-to-use instance
* If injector has injectable (metadata to create instance) - creates it and returns ref
(despite if parent injector already has created instance, new metadata/injectable - new instance)
* If no instance and no injectable metadata - asks parent for it (he may have something)

@Configuration

Annotation for class, which may product injectables via methods (static and/or non-static) as factory methods.
Doesn't contain any input parameters so far. But it required for method-based injectable processing.
It's singleton - only one instance of such class may be created.

ForwardRef

Used for redirect tokens to pass token, which is not yet constucted.

Token

In case cases you might want to wrap your token to Token wrapper.
Cases are:

  • passing dependency to list of dependencies for raw factory function with multi: true
  • using this wrapper to pass options (like multi: true) to direct Inject call.

Usage examples

Inject example

@Injectable()
class SimpleService {
  public doSomething() {
    // ...
  };
}

class WithDependencies {
  public readonly simple : SimpleService = Inject(SimpleService);
}

const ins = new WithDependencies(); // if constructor doesn't have dependencies

constructor has dependencies example

@Injectable()
class SimpleService {
  public doSomething() {
    // ...
  };
}

class WithDependencies {
  constructor(@Inject.Param(SimpleService) simple : SimpleService) {}
}

const ins = Injector.getCurrentInjector().createInstance(WithDependencies);

Hide implementation from consumer's eyes example

const TYPES = {
  SERVICE_A: Symbol("ServiceA"),
  SERVICE_B: Symbol("ServiceB"),
};

interface ServiceA {...}
interface ServiceB {...}

@Injectable(TYPES.SERVICE_A)
class ServiceAImpl implements ServiceA {...}
@Injectable(TYPES.SERVICE_B)
class ServiceBImpl implements ServiceB { // can have inner dependencies
  constructor( @Inject.Param(TYPES.SERVICE_A) serviceA : ServiceA) {...}
}

class WithDependencies {
  constructor(
    @Inject.Param(TYPES.SERVICE_A) serviceA : ServiceA,
    @Inject.Param(TYPES.SERVICE_B) serviceB : ServiceB
  ) {...}
}

const ins = Injector.getCurrentInjector().createInstance(WithDependencies);

Manual definition of tokens example

const valueToken = Injectable(Symbol("TEST_TOKEN"), {
  value: "aval"
}); // function call returns `token` back

class WithDependencies {
  constructor(
    @Inject.Param(Symbol("TEST_TOKEN")) public readonly value1 : string,
    @Inject.Param(valueToken) public readonly value2 : string // valueToken is Symbol("TEST_TOKEN")
  ) {
    expect(value1).toBe("aval");
    expect(value2).toBe("aval");
  }
}

Factories example

const valueToken = Injectable(Symbol("TEST_TOKEN"), {
  factory: () => "aval" // just a provider function, without additional dependencies
});

const factoryToken = Injectable(Symbol("SECOND_TOKEN"), {
  dependencies: [valueToken], // this factory relies on other dependencies
  factory: (val: string) => val + "_bval"
});

Factories from class methods.
All classes, that can product injectables must be annotated with @Configuration()
Class can expose outside a static method, which will be used to generate injectable - this will work as regular factory method.
Or class instance can be instantiated/constructed and his regular methods (non-static, in order to access inner state) can be used.
Class, annotated with @Configuration() decorator can be created only once (singleton). Exception will raise on attempt to create second instance.

@Configuration()
class Config {

  constructor(private readonly prefix : string = "pref_") {}

  @Injectable(ServiceA)
  public createServiceA() : ServiceA {
    return new ServiceA(this.prefix + "a");
  }

  @Injectable(ServiceB)
  public static createServiceB(@Inject.Param(ServiceA) a: ServiceA) : ServiceB {
    return new ServiceB(a);
  }
}

new Config(); // is enough if constructor doesn't need dependencies
// OR
Injector.getCurrentInjector().createInstance(Config);

Injectors hierarchy example

There is always Injector.root built-in injector, which is global and default if you don't specify else.
When you need create new injector, you do const injector = new Injector("name", Injector.root) if you want to create child injector
or const injector = new Injector("name") if you need completely detached injector.
All tokens and instances, which are knowns in parent - will be visible for child injector.
If injector was asked to create instance for some token - it will immediately return ready-to-use instance, if such exists in internal cache.
If doesn't exists - we look for definition, if current injector can construct new instance. If can - constructs, if cannot - delegates the call to parent injector.
This way you can override tokens as:

const TOKEN = Symbol("TOKEN");

Injectable(TOKEN, {
  value: "rootVal",
  injector: Injector.root
});
Injectable(TOKEN, {
  value: "childVal",
  injector: testInjector
});

Inject(TOKEN, Injector.root); // = "rootVal"
Inject(TOKEN, testInjector); // = "childVal"

Injector context example

When defining injectable via Injectable function you can optionally pass parameters as second argument.

const TOKEN1 = Injectable(Symbol("TOKEN1"), {
  value: "aval",
  injector: customInjector
});

@Injectable(undefined, { // first argument is always `token`, if you want treat current class as token itself - pass `undefined`
  injector: customInjector
})
class SomeServiceImpl {}

Or there is another alternative- use injector.runInContext

injector.runInContext(() => {
  // do work
});

this will temporary bind injector - from which you called runInContext as currentInjector, which will be available via Injector.getCurrentInjector() call.

For more examples - look at tests

token redirect

When want/need re-use another token under different name.

const TOKEN = Symbol("TOKEN");
const REDIRECT_TOKEN = Symbol("REDIRECT_TOKEN");

Injectable(REDIRECT_TOKEN, {
  redirect: TOKEN
});

@Injectable(TOKEN)
class SimpleService {
  public a = "aval";
}

class WithDependencies {
  public readonly simple : SimpleService = Inject(TOKEN);
  public readonly simple2 : SimpleService = Inject(REDIRECT_TOKEN);
}

multiple tokens

Watch options {multi: true} - if specified - all created instances will be returned inside array, even if there is only one instance.
When not passing multi: true (so you expect only one instance), but there are many inject candidates - exception will be thrown in runtime.

const TEST_TOKEN = Symbol("TEST_TOKEN");
Injectable(TEST_TOKEN, {
  value: "aval",
  multi: true
});
Injectable(TEST_TOKEN, {
  value: "bval",
  multi: true
});

class WithDependencies {
  constructor(
    @Inject.Param(TEST_TOKEN, {multi: true}) public readonly values : string[]
  ) {...}
}

multiple tokens - service handlers use-case

Want to have set of services/handlers (hightly lickely with common interface), which can be injectables, which can be used separately.
But, additionall, can be collected somewhere in single array.

interface Handler {
  handle() : boolean;
}

Injectable("magicHandler", { // common token, which is multi
  redirect: new ForwardRef(() => ServiceHandlerA),
  multi: true
});
@Injectable() // individual injectable
class ServiceHandlerA implements Handler {
  public handle() : boolean {
    return true;
  }
}

Injectable("magicHandler", {
  redirect: new ForwardRef(() => ServiceHandlerA),
  multi: true
});
@Injectable()
class ServiceHandlerB implements Handler {
  public handle() : boolean {
    return false;
  }
}

class WithDependencies {
  constructor(
    @Inject.Param("magicHandler", {multi: true}) public readonly handlers : Handler[]
  ) {...}
}

scopes

Prototype

class SimpleService {}

Injectable("TEST_TOKEN", {
  scope: "prototype",
  factory: () => new SimpleService()
});

const service1 : SimpleService = Inject("TEST_TOKEN");
const service2 : SimpleService = Inject("TEST_TOKEN"); // prototype --> each time new instance

Caching

class SimpleService {}

let currentSessionId : string = "1"; // or requestId, or traceId, or whateverIdentifier

Injectable("TEST_TOKEN", {
  scope: () => currentSessionId,
  factory: () => new SimpleService()
});

const service1 : SimpleService = Inject("TEST_TOKEN");
const service2 : SimpleService = Inject("TEST_TOKEN"); // the same service should arrive, as currentSessionId unchanged
currentSessionId = "2"; // new request, or session, or something
const service3 : SimpleService = Inject("TEST_TOKEN");
currentSessionId = "1";
const service4 : SimpleService = Inject("TEST_TOKEN"); // reuse previous

License MIT

Package Sidebar

Install

npm i @stnekroman/inversityts

Weekly Downloads

1

Version

0.0.6

License

MIT

Unpacked Size

71.9 kB

Total Files

43

Last publish

Collaborators

  • stnekroman