Skip to content

Releases: SansarJs/dependency

v0.0.5

04 Sep 06:23
620ceb5

Choose a tag to compare

@sansar/dependency

A lightweight, TypeScript-native dependency injection (DI) library for modern
JavaScript environments. It supports inversion of control (IoC) with decorators,
scoped dependencies, and flexible registration strategies (values, resolvers,
generators). Built with Stage 3 ECMAScript decorators in mind, it cannot [yet]
rely on reflect-metadata for true IoC.

Features

  • Decorator-Based Injection: Use @Inject() and @Scope() for
    declarative dependency management
  • Functional Style Providers: Register dependencies as static values,
    lazy resolvers, or dynamic generators on Containers
  • Scoped Dependencies: Cache instances per scope (e.g., request-scoped
    services)
  • Circular Dependency Detection: Automatically detects and throws errors
    on cycles
  • Token-Based Abstractions: Use Token subclasses for type-safe
    abstract dependencies
  • Hierarchical Containers: Support nested containers for modular
    applications
  • Error Handling: Descriptive errors for undefined keys, duplicate
    registrations, missing dependencies, and more
  • No External Dependencies
  • Token default values
  • Containers as resources, and dependencies with disposal hooks
  • Context as parameters to resolver and generator defintion functions
  • Support a Configuration abstraction for defining, importing & excluding
    some sets of dependency definitions (think a full set of configuration for
    Redis, Datadog, logging, etc)
  • Introduce properties, environments, conditional dependency definitions

Installation

# Deno
deno add jsr:@sansar/dependency

# PNPM
pnpm i jsr:@sansar/dependency

# YARN
yarn add jsr:@sansar/dependency

# VLT
vlt install jsr:@sansar/dependency

# NPM
npx jsr add @sansar/dependency

# Bun
bunx jsr add @sansar/dependency

Quick Start

Basic Injection

Define and inject dependencies:

import { Container, Inject } from "@sansar/dependency";

@Inject()
class Logger {
  log(message: string) {
    console.log( message );
  }
}

@Inject( Logger )
class Service {
  constructor(readonly logger: Logger) {}
}

const container = new Container();
const service = container.get( Service );

service.logger.log( "Hello, DI!" ); // Outputs: Hello, DI!

Using Tokens for Abstractions

Tokens allow injecting interfaces or abstract types:

import { Container, Inject, Token } from "@sansar/dependency";

class LogLevel extends Token<"debug" | "info" | "error"> {}

@Inject( LogLevel )
class Logger {
  constructor(readonly level: "debug" | "info" | "error") {}
}

const container = new Container()
  .register( LogLevel, { value: "info" } );
const logger = container.get( Logger );

console.log( logger.level ); // Outputs: info

Scoped Dependencies

Scope instances to specific containers (e.g., per HTTP request):

import { Container, Inject, Scope } from "@sansar/dependency";

const REQUEST_SCOPE = Symbol("REQUEST");

@Scope( REQUEST_SCOPE )
@Inject()
class RequestService {}

const root = new Container();
const requestContainer = new Container({ scope: REQUEST_SCOPE, parent: root });
const someOtherContainer = new Container( requestContainer );

const service1 = someOtherContainer.get( RequestService );
const service2 = requestContainer.get( RequestService );

console.log(service1 === service2); // true (cached in scoped container)

Concepts

Container

The core class for registering and resolving dependencies.

  • Registration:
    • register(key, { value }): Provides a static value.
    • register(key, { resolver: () => value }): Lazily resolves and caches the
      value.
    • register(key, { generator: () => value }): Generates a new value on each
      request.
    • Optional scope for scoped resolver/generator caching.
  • Resolution:
    • get(key): Retrieves the dependency, falling back to parent containers if
      needed.

NOTE: Containers can be nested, forming a hierarchy for modular apps.

Token

A base class for type-safe tokens. Extend it to represent abstract dependencies:

import { Token } from "@sansar/dependency";

class DatabaseUrl extends Token<string> {}

NOTE: Tokens cannot be instantiated.

@Inject(...tokens)

A class decorator to declare constructor dependencies.
Supports forward references via arrow functions.

  • Throws InjectDuplicationError if applied multiple times.
  • Throws InjectMissingDependencyError or InjectCircularDependencyError on
    resolution issues.

@Scope(scope: symbol)

Marks a class for scoped caching. Must be used with @Inject() and applied before
it.

  • Throws ScopeDuplicationError if applied multiple times.
  • Throws ScopeInjectUsageError if misused with @Inject().
  • Resolution throws ContainerUndefinedScopeError if no matching scope exists.

Advanced Usage

Hierarchical and Scoped Resolution

Dependencies resolve from the invoking container, caching at the appropriate
level:

import { Container } from "@sansar/dependency";

const APP_SCOPE = Symbol( "APP" );

const root = new Container({ scope: APP_SCOPE });
root.register( Date, { resolver: () => new Date(), scope: APP_SCOPE } );

const child = new Container( root );
console.log(child.get( Date ) === root.get(Date)); // true

Forward References:

When a token is defined later than its usage, leverage forward reference:

import { Container, Inject } from "@sansar/dependency";

@Inject(() => B)
class A {
  constructor(readonly b: B) {}
}

@Inject()
class B {}

console.log(new Container().get( A ) instanceof A); // true
console.log(new Container().get( A ).b instanceof B); // true

Resources Management

The Container class is EcmaScript-compliant by implementing the
[Symbol.dispose] method. Futhermore, any dependency object, whether a static
value, injected, resolver- or generator-based, that implements the
[Symbol.dispose] method will be invoked when the container resource is cleaned
up.

I case you do control the dependent value implementation, you can:

  • pass an onDestroy callback to the dependency provider when registering it
  • call onDestroy( hook ) with your disposal hook yourself from the Context
    object passed to resolvers and generators.

NOTE: A dependency lifecycle is tied to its related container lifetime.
This is relevant for scoped dependencies which are tied to the contained with
the matching scope.

NOTE: When a container is disposed, it propagates to descendant
containers. We hope this helps preventing some weird inconsistencies.

import { Container, Inject, Token } from "@sansar/dependency";

class A extends Token<number> {}

class B {}

@Inject(A, B)
class C {
  [Symbol.dispose]() {/* 1 */}
}

const scope = Symbol();
const root = new Container()
  .register(A, { value: Math.PI, onDestroy() {/* 2 */} })
  .register(B, {
    scope,
    resolver: ctx => {
      ctx.onDestroy(bValue => {/* 3 */})
      return Math.random();
    }});
const container = new Container({ scope, parent: root });

container.get( C );
(() => { using _ = container; });
// when container goes out of scope, its [Symbol.dispose] is called
// so it the B instance cached there, through the #3 onDestroy hook

// when root goes out of scope or is disposed of, so will A (#2) and C (#1).

Contributing

Contributions are welcome! Fork the repository, make changes, and submit a pull
request. Ensure tests pass and follow the existing code style.

License

This project is licensed under the MIT License. See LICENSE for
details.

Copyright (c) 2025 SansarJs

v0.0.4

03 Sep 02:03
fed31e5

Choose a tag to compare

@sansar/dependency

A lightweight, TypeScript-native dependency injection (DI) library for modern
JavaScript environments. It supports inversion of control (IoC) with decorators,
scoped dependencies, and flexible registration strategies (values, resolvers,
generators). Built with Stage 3 ECMAScript decorators in mind, it cannot [yet]
rely on reflect-metadata for true IoC.

Features

  • Decorator-Based Injection: Use @Inject() and @Scope() for declarative
    dependency management
  • Functional Style Providers: Register dependencies as static values, lazy
    resolvers, or dynamic generators on Containers
  • Scoped Dependencies: Cache instances per scope (e.g., request-scoped
    services)
  • Circular Dependency Detection: Automatically detects and throws errors
    on cycles
  • Token-Based Abstractions: Use Token subclasses for type-safe abstract
    dependencies
  • Hierarchical Containers: Support nested containers for modular
    applications
  • Error Handling: Descriptive errors for undefined keys, duplicate
    registrations, missing dependencies, and more
  • No External Dependencies

Installation

# Deno
deno add jsr:@sansar/dependency

# PNPM
pnpm i jsr:@sansar/dependency

# YARN
yarn add jsr:@sansar/dependency

# VLT
vlt install jsr:@sansar/dependency

# NPM
npx jsr add @sansar/dependency

# Bun
bunx jsr add @sansar/dependency

Quick Start

Basic Injection

Define and inject dependencies:

import { Container, Inject } from "@sansar/dependency";

@Inject()
class Logger {
  log(message: string) {
    console.log( message );
  }
}

@Inject( Logger )
class Service {
  constructor(readonly logger: Logger) {}
}

const container = new Container();
const service = container.get( Service );

service.logger.log( "Hello, DI!" ); // Outputs: Hello, DI!

Using Tokens for Abstractions

Tokens allow injecting interfaces or abstract types:

import { Container, Inject, Token } from "@sansar/dependency";

class LogLevel extends Token<"debug" | "info" | "error"> {}

@Inject( LogLevel )
class Logger {
  constructor(readonly level: "debug" | "info" | "error") {}
}

const container = new Container()
  .register( LogLevel, { value: "info" } );
const logger = container.get( Logger );

console.log( logger.level ); // Outputs: info

Scoped Dependencies

Scope instances to specific containers (e.g., per HTTP request):

import { Container, Inject, Scope } from "@sansar/dependency";

const REQUEST_SCOPE = Symbol("REQUEST");

@Scope( REQUEST_SCOPE )
@Inject()
class RequestService {}

const root = new Container();
const requestContainer = new Container({ scope: REQUEST_SCOPE, parent: root });
const someOtherContainer = new Container( requestContainer );

const service1 = someOtherContainer.get( RequestService );
const service2 = requestContainer.get( RequestService );

console.log(service1 === service2); // true (cached in scoped container)

Concepts

Container

The core class for registering and resolving dependencies.

  • Registration:
    • register(key, { value }): Provides a static value.
    • register(key, { resolver: () => value }): Lazily resolves and caches the
      value.
    • register(key, { generator: () => value }): Generates a new value on each
      request.
    • Optional scope for scoped resolver/generator caching.
  • Resolution:
    • get(key): Retrieves the dependency, falling back to parent containers if
      needed.

NOTE: Containers can be nested, forming a hierarchy for modular apps.

Token

A base class for type-safe tokens. Extend it to represent abstract dependencies:

import { Token } from "@sansar/dependency";

class DatabaseUrl extends Token<string> {}

NOTE: Tokens cannot be instantiated.

@Inject(...tokens)

A class decorator to declare constructor dependencies.
Supports forward references via arrow functions.

  • Throws InjectDuplicationError if applied multiple times.
  • Throws InjectMissingDependencyError or InjectCircularDependencyError on
    resolution issues.

@Scope(scope: symbol)

Marks a class for scoped caching. Must be used with @Inject() and applied before
it.

  • Throws ScopeDuplicationError if applied multiple times.
  • Throws ScopeInjectUsageError if misused with @Inject().
  • Resolution throws ContainerUndefinedScopeError if no matching scope exists.

Advanced Usage

Hierarchical and Scoped Resolution

Dependencies resolve from the invoking container, caching at the appropriate
level:

import { Container } from "@sansar/dependency";

const APP_SCOPE = Symbol( "APP" );

const root = new Container({ scope: APP_SCOPE });
root.register( Date, { resolver: () => new Date(), scope: APP_SCOPE } );

const child = new Container( root );
console.log(child.get( Date ) === root.get(Date)); // true

Forward References:

import { Container, Inject } from "@sansar/dependency";

@Inject(() => B)
class A {
  constructor(readonly b: B) {}
}

@Inject()
class B {}

console.log(new Container().get( A ) instanceof A); // true
console.log(new Container().get( A ).b instanceof B); // true

Contributing

Contributions are welcome! Fork the repository, make changes, and submit a pull
request. Ensure tests pass and follow the existing code style.

License

This project is licensed under the MIT License. See LICENSE for
details.

Copyright (c) 2025 SansarJs

v0.0.3

31 Aug 21:47
ff97fb2

Choose a tag to compare

@sansar/dependency

A strait forward dependency container in JavaScript, implemented for
environment agnosis and compliance to web standards.

deno add @sansar/dependency

API Surface

Definitions & concepts:

  • Key: a class they serves to store dependency definition and later
    retrieve a value from a container. It could be a normal constructor like
    Date or a sub-class of Token.

    Token sub-classes are useful when multiple dependencies share a common
    type (eg.: StartupDateToken, ShutdownDateToken) or, for types that
    are not constructor-based (eg.:
    class ConfigToken extends Token<{value: string, radix?: number}>)
    .

  • Provider: a definition to resolve a dependency value. It could be:
    • a static value: { value: T }
    • a resolver to lazily compute the value, once, cached: {resolver: () => T}
    • a generator to lazily compute a new value, each time: {resolver: () => T}
  • Container: a class which instances organise dependency definitions, value
    retrieval and caching.
  • Parent container: A container can have a parent. Dependency definitions
    on children hoist that of their ancestors. Similarly, at resolution time,
    containers are travelled up the tree
  • Scope: an optional, unique tag for some containers, that force them as the
    layers to evaluate and eventually cache scoped resolver- and generator-based
    dependency definitions.

Classes:

  • Token: a base class for identifying values in a container
  • Container: the class to contain dependency definitions and cache
    dependencies when applicable

Usages

Ordinary constructor key, with a generator dependency definition:

import { Container } from "@sansar/dependency";

const container = new Container()
  .register(Date, { generator: () => new Date() });

// Later
container.get(Date) // get a date
container.get(Date) // get another date
container.get(Date) // get yet another date

Contrustor key, with a resolver dependency definition:

import { Container } from "@sansar/dependency";

class TemplateEngine { /** heavy stuff in here **/ }

const container = new Container()
  .register(TemplateEngine, { resolver: () => new TemplateEngine() });

// Later
container.get(TemplateEngine) // get the template engine (initialized lazily)
container.get(TemplateEngine) // get the same template engine
container.get(TemplateEngine) // still the same

Token-based key, with a value dependency definition:

import { Container, Token } from "@sansar/dependency";

class DbConfig extends Token<Record<'uri'|'username'|'password', string>> {
}

const container = new Container()
  .register(DbConfig, {
    value: {
      username: '',
      password: '',
      uri: 'jdbc://localhost:5432/app'
    }
  });

// Later
container.get(DbConfig) // get the registered DbConfig
container.get(DbConfig) // get the registered DbConfig
container.get(DbConfig) // get the registered DbConfig

Now with a ancestry:

import {Container} from "@sansar/dependency";

const root = new Container();
const parent = new Container(root);
const container = new Container(parent);

parent.register(Date, { resolver: () => new Date() });

container.get(Date) === parent.get(Date); // true
// root.get(Date) // throw: ContainerUndefinedKeyError

A scope with downstream definition:

import {Container} from "@sansar/dependency";

const MATH = Symbol('MATH');
const root = new Container();
const parent = new Container({scope: MATH, parent: root});
const container = new Container(parent);

container.register(Number, { scope: MATH, resolver: () => -1 });

// root.get(Number); // throw: ContainerUndefinedKeyError --------|
// parent.get(Number); // throw: ContainerUndefinedKeyError ---|  |
container.get(Number); // -1                                   |  |
parent.get(Number); // -1 -------------------------------------|  |
// root.get(Number); // throw: ContainerUndefinedKeyError --------|

A scope with upstream definition:

import {Container} from "@sansar/dependency";

const MATH = Symbol('MATH');
const root = new Container();
const parent = new Container({scope: MATH, parent: root});
const container = new Container(parent);

const values = [-1, 0, 1]
root.register(Number, { scope: MATH, resolver: () => values.shift() });

// root.get(Number); // throw: ContainerUndefinedScopeError ----|
parent.get(Number); // -1 -----------------------------------|  |
container.get(Number); // -1                                 |  |
parent.get(Number); // -1 -----------------------------------|  |
// root.get(Number); // throw: ContainerUndefinedScopeError ----|

License

MIT