npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@fluojs/di

v3.1.2

Published

Minimal token-based dependency injection container powering every Fluo application.

Readme

@fluojs/di

Node.js support is >=24.0.0 <27. See Node.js support and migration before upgrading.

Minimal token-based dependency injection container powering every fluo application.

Table of Contents

Installation

npm install @fluojs/di

When to Use

Use this package when you need to:

  • Resolve classes and their dependencies at runtime.
  • Manage object lifetimes (Singleton, Request, Transient).
  • Override implementations for testing or environment-specific needs.
  • Create isolated request-scoped containers for HTTP or background tasks.

Quick Start

The container resolves tokens into instances based on their registered providers.

import { Container } from '@fluojs/di';
import { Inject, Scope } from '@fluojs/core';

class Logger {
  log(msg: string) { console.log(msg); }
}

@Inject(Logger)
@Scope('singleton')
class UserService {
  constructor(private logger: Logger) {}
  
  async getStatus() {
    this.logger.log('Checking status...');
    return { status: 'active' };
  }
}

const container = new Container();
container.register(Logger, UserService);

const service = await container.resolve(UserService);
const result = await service.getStatus();

Key Capabilities

Provider Types

fluo DI supports four provider shapes:

  • Class Providers: container.register(MyService) or { provide: MyToken, useClass: MyService }.
  • Value Providers: { provide: 'API_URL', useValue: 'https://api.example.com' }.
  • Factory Providers: { provide: 'ASYNC_CONFIG', useFactory: async (db) => await db.load(), inject: [Database] }. Add resolverClass when the factory should inherit the referenced class's DI metadata, such as @Scope(...), unless an explicit provider scope is set.
  • Alias Providers: { provide: ILogger, useExisting: PinoLogger } allows mapping one token to another existing provider.

Typed public token resolution

Symbols created by publicToken<T>() from @fluojs/core infer the return type of resolve(). Names and metadata registries do not unify distinct constructors; an explicit alias points to the existing provider.

import { publicToken } from '@fluojs/core';
import { Container } from '@fluojs/di';

class PostsService { title() { return 'FluoBlog'; } }
const POSTS = publicToken<PostsService>('my-blog/posts/v1');
const container = new Container().register(
  PostsService,
  { provide: POSTS, useExisting: PostsService },
);
try {
  const posts = await container.resolve(POSTS); // PostsService
  posts.title();
} finally {
  await container.dispose();
}

Injection from another module requires exporting the alias from its owning module and importing that module. The original class need not be exported, but each exposed token needs its own alias/exports declaration. Manual Symbol.for('my-blog/posts/v1') and resolve<PostsService>(token) can still use the same alias. Tokens do not change scope. Keep request/actor/session providers in per-request child scopes, dispose them in finally, and never put them into root singletons or global caches. For multi-provider tokens, declare T as the actual returned array type.

Scope Management

  • Singleton (Default): Instance is created once and shared across the entire container.
  • Request: Instance is created once per createRequestScope() call.
  • Transient: A new instance is created every time it is resolved.

A singleton provider must not depend on a request-scoped provider. That mismatch throws ScopeMismatchError before any provider factory or constructor in the graph runs, and the check covers single, alias (useExisting), and multi-provider registrations. A singleton that injects a multi token fails the same way when any contribution under that token is request-scoped, so no partial contribution set is materialized first.

During disposal, each container tears down successfully materialized cached instances in reverse creation order across single-provider and multi-provider caches, so dependents are destroyed before their dependencies. Each container first recursively tears down live request-scope children it owns, so disposing a non-root request scope also closes nested request scopes before its own request cache. Root disposal then continues with root-owned singleton cleanup even if one or more child disposals fail. When multiple child/root disposals fail, dispose() reports an AggregateError so callers can inspect every shutdown failure without losing cleanup progress.

Starting dispose() is terminal for resolve(), register(), override(), and createRequestScope(). Concurrent callers share the active disposal attempt. If an onDestroy() hook fails, the container retains only that failed hook for a later explicit dispose() retry, preserving child-before-parent/root and reverse-creation ordering. Hooks that completed successfully are never run again, and disposal becomes idempotent after every retained hook succeeds.

Disposal retry ownership

Disposal retries follow five ownership rules:

  1. Calling public child.dispose() directly detaches the request child from its parent graph after the active attempt settles, even when retained onDestroy() hooks failed.
  2. A retained child reference can call dispose() again to retry only that child's failed hooks. Successful sibling hooks are not repeated.
  3. A child first reached through parent or root disposal remains parent-tracked after failure, so a later parent or root dispose() retries it before retained hooks in the parent or root.
  4. Concurrent direct and parent callers share one active attempt. The caller that starts the shared attempt sets its direct or parent ownership, and later callers cannot change it.
  5. A later direct retry detaches a parent-retained child after settlement, even when that retry fails.

Executable evidence lives in packages/di/src/container-disposal-ownership.test.ts for graph ownership and packages/di/src/container-disposal-retry.test.ts for failed-hook ordering and idempotency.

Migrating disposal from 2.x to 3.x

In @fluojs/di 2.x, a failed container-managed onDestroy() hook was attempted once. In 3.x, a later explicit Container.dispose() call or application/application-context close() that reaches the same container retries only hooks that failed. Hooks that already completed successfully remain exactly-once. Before upgrading, make cleanup hooks that can fail safe to attempt again: preserve enough state to finish partial cleanup, tolerate resources that were already released, and surface a repeated failure to the shutdown caller.

Direct child.dispose() now detaches the request child from its parent after the attempt settles, including a failed attempt. Retain the child reference when the direct caller must inspect or retry that failure. A failure from parent- or root-started disposal remains owned by the parent hierarchy until cleanup succeeds or a later direct child attempt settles. When direct and parent callers overlap, the caller that starts the shared attempt owns those detach and retry semantics.

Provider Overrides

Use override(...providers) when a test or request-local boundary needs to replace existing registrations deliberately. Overrides replace the current provider set for each token, invalidate cached instances in the current container and already-materialized request-scope descendants, and dispose stale instances before the next replacement resolution continues. Multi-provider overrides replace the full multi-provider set for that token, so pass every replacement provider together; mixing single and multi replacements for the same token in one override call is rejected as ambiguous. An override call is atomic: the whole batch is validated before any registration or cache changes, so a rejected call leaves every provider, cached instance, and disposal ownership exactly as it was.

Container Construction Boundary

new Container() is the only supported public construction form, and it always creates a root container that owns its own singleton cache. Child request scopes are package-owned: parent linkage, the request-scope flag, and singleton-cache sharing use a private construction path reachable only through createRequestScope().

const root = new Container();
const requestScope = root.createRequestScope();

Supplying constructor arguments is rejected. The emitted declaration accepts no assignable argument type, and at runtime a caller-supplied argument throws ContainerResolutionError rather than producing a container with borrowed cache ownership.

// Rejected: child-scope wiring is package-owned.
Reflect.construct(Container, [root]);

Migrating container construction from 2.x to 3.x

In @fluojs/di 2.x, the emitted Container declaration exposed parent, requestScopeEnabled, and singletonCache constructor parameters even though caller-supplied child wiring was never a supported application workflow. In 3.x that surface is sealed: the constructor accepts no arguments, and passing any argument throws ContainerResolutionError.

Zero-argument new Container() and createRequestScope() are unchanged, so supported code needs no migration. If you constructed child containers directly, replace that call with parent.createRequestScope(), which supplies the same parent linkage, request-scope flag, and shared root singleton cache while keeping disposal ownership intact.

Executable evidence lives in packages/di/src/container-construction-boundary.test.ts.

A failed stale onDestroy() hook follows the same retained-retry contract as ordinary disposal. The next resolution on an observing container surfaces that failure once so the replacement can continue, and the failed instance stays retained by the container that scheduled its cleanup until a later explicit dispose() on that container invokes the hook again. Stale hooks that already completed successfully are never repeated.

Request Scoping

Isolated containers can be created to handle per-request state without polluting the root container.

const requestContainer = container.createRequestScope();
const scopedService = await requestContainer.resolve(RequestScopedService);

Request-scope containers may resolve providers from their parent chain, but request-owned registrations must not introduce new singleton providers. Register singleton providers on the root container before creating request scopes. If a request scope needs local additions, declare them with scope: 'request' or use override() for an explicit request-local replacement. The same rule applies to multi providers: default-scope multi providers belong on the root container, while request-local multi providers must opt into request scope or be replaced through override().

Provider objects are validated at registration time: every object provider must include a string, symbol, or constructable class provide token and exactly one strategy (useClass, useValue, useFactory, or useExisting). Alias providers require the same valid token forms for useExisting. For class providers, an omitted or undefined inject value falls back to the useClass @Inject(...) metadata; any other explicit inject value must be an array containing valid tokens or well-formed ForwardRef.create(...) / Optional.create(...) wrappers. Value providers must omit inject; declaring it as an own property is rejected even when its value is undefined. Explicit scope values must be singleton, request, or transient. Invalid provider shapes throw InvalidProviderError before they can affect the container graph.

NestJS Scope and Optional Dependency Migration

NestJS @Injectable({ scope: Scope.REQUEST }) and @Injectable({ scope: Scope.TRANSIENT }) map to a fluo provider with @Scope('request') / @Scope('transient'), or an explicit provider scope: 'request' / scope: 'transient'. Singleton remains the default.

fluo does not implement NestJS scope bubbling. Resolve request-scoped providers from a createRequestScope() child container: root resolution throws RequestScopeResolutionError, and a singleton that depends on a request-scoped provider throws ScopeMismatchError.

NestJS @Optional() maps to Optional.create(Token) in a class-level @Inject(...) list or a provider inject array. Optional.create(...) is a token wrapper, not a decorator, and a missing registration resolves to undefined.

import { Inject, Scope } from '@fluojs/core';
import { Optional } from '@fluojs/di';

class AuditLogger {}

@Scope('request')
@Inject(Optional.create(AuditLogger))
class RequestAuditService {
  constructor(private readonly auditLogger: AuditLogger | undefined) {}
}

Circular Dependency Handling

The container automatically detects circular dependencies and throws a CircularDependencyError to prevent infinite loops. This includes direct (A→A), two-node (A→B→A), and deep (A→B→C→A) cycles.

Use ForwardRef.create() when a token is referenced before its declaration. It defers token lookup for declaration-order issues, but it does not make true constructor cycles resolvable; those cycles are still rejected with CircularDependencyError.

import { ForwardRef } from '@fluojs/di';
import { Inject } from '@fluojs/core';

@Inject(ForwardRef.create(() => ServiceB))
class ServiceA {
  constructor(private readonly serviceB: ServiceB) {}
}

class ServiceB {
  getStatus() {
    return 'ready';
  }
}

ForwardRef.create(...) and Optional.create(...) are token wrappers used inside the class-level @Inject(...) token list or provider-level inject arrays. They are not decorators and do not attach to constructor parameters.

import { Optional } from '@fluojs/di';
import { Inject } from '@fluojs/core';

@Inject(Optional.create(AuditLogger))
class ServiceWithOptionalLogger {
  constructor(private readonly auditLogger: AuditLogger | undefined) {}
}

Testing and Mocking

Register the complete dependency graph first, then use override(...) with useValue to replace an existing provider with a mock or stub. register(...) adds new providers and rejects duplicate tokens; override(...) is the supported replacement API.

import { Inject } from '@fluojs/core';
import { Container } from '@fluojs/di';
import { expect, it, vi } from 'vitest';

class Database {
  async query(): Promise<readonly string[]> {
    return ['real row'];
  }
}

@Inject(Database)
class DataService {
  constructor(private readonly database: Database) {}

  async load(): Promise<readonly string[]> {
    return this.database.query();
  }
}

it('uses a mock database', async () => {
  const mockDb = { query: vi.fn().mockResolvedValue(['mock row']) };
  const container = new Container().register(Database, DataService);

  container.override({
    provide: Database,
    useValue: mockDb,
  });

  const service = await container.resolve(DataService);

  await expect(service.load()).resolves.toEqual(['mock row']);
  expect(mockDb.query).toHaveBeenCalledOnce();
});

Internal Package Integrations

@fluojs/di/internal is a typed integration seam for first-party framework packages. It resolves one ordered multi: true contribution through the owning container, preserving the container's scope, cache, cycle, ordering, and disposal semantics. Application code must use Container.resolve(...); contribution indexes are not part of the root Container API.

Compatible same-realm @fluojs/di copies can resolve an ordered contribution only through the exact container that registered it. The resolver map shares container-bound entries, not containers, caches, request scopes, or application graphs.

Troubleshooting

CircularDependencyError

Thrown when the container detects a cycle in the dependency graph. Check your constructor injections and remove the cycle by extracting shared state, introducing a mediator, or changing the lifetime boundary. ForwardRef.create() only defers token lookup for declaration-order issues; it does not break true constructor cycles.

Token Not Found

Ensure all required providers are registered in the container. If you use createRequestScope(), the child container can resolve tokens from the parent, but not vice versa.

Public API

ForwardRef.create(fn) and Optional.create(token) own wrapper creation and return frozen plain records, preserving resolver and token identity without resolving them. Metadata and provider registration still snapshot wrapper records. Both Core and DI export the shared ForwardRefToken<T> and OptionalInjectToken<T> types. The removed creation functions, runtime scope namespace, and old wrapper type names have no compatibility aliases. Follow the Core and DI migration guide. Optional.create is a DI token factory, not the unrelated HTTP Optional field decorator.

| Surface | Kind | Description | |---|---|---| | Container | Root export | The main DI container class. new Container() takes no arguments and creates a root container; child request scopes are package-owned and created with createRequestScope(). Supplying constructor arguments throws ContainerResolutionError. | | container.register(...providers) | Container instance method | Registers one or more providers. | | container.override(...providers) | Container instance method | Replaces existing providers atomically per call, invalidates cached instances, and ensures stale instance disposal settles before the next replacement resolution continues. | | container.resolve<T>(token) | Container instance method | Asynchronously resolves a token to an instance. | | container.inspectResolutionState() | Container instance method | Exposes the supported framework-owned container introspection seam for testing/tooling helpers that must preserve cache ownership through snapshot read-only map views, frozen provider records, and controlled cache adoption. Prefer has(...) and resolve(...) for application code. | | container.createRequestScope() | Container instance method | Creates a child container for request-scoped dependencies. This is the only supported path to parent-linked, request-scope-enabled containers that share the root singleton cache. | | container.has(token) | Container instance method | Checks if a token is registered in the container or its parents. | | container.hasRequestScopedDependency(token) | Container instance method | Checks whether resolving a token may require a request-scope container because its provider graph contains request-scoped dependencies or is cyclic. | | container.dispose() | Container instance method | Disposes request children before parent/root caches, shares an active attempt, and retries only failed onDestroy() hooks on a later explicit call. | | ForwardRef.create(fn) | Returns a token wrapper that defers lookup for declaration-order issues; it does not make constructor dependency cycles resolvable. | | isForwardRef(value) | Type guard for values produced by ForwardRef.create(...); useful when integrating custom provider tooling with DI token wrappers. | | Optional.create(token) | Returns a token wrapper that marks one dependency as optional; missing optional dependencies resolve to undefined. | | isOptionalToken(value) | Type guard for values produced by Optional.create(...); useful when inspecting provider-level inject arrays. | | Scope | Type-only union of 'singleton', 'request', and 'transient'. Import the decorator from @fluojs/core. | | Provider types | Provider, ClassProvider, FactoryProvider, ValueProvider, and ExistingProvider describe the public registration shapes accepted by register(...) and override(...). | | Token wrapper types | ForwardRefToken and OptionalInjectToken describe the wrapper values returned by ForwardRef.create(...) and Optional.create(...). | | Container helper types | ClassType, Disposable, and RequestScopeContainer support typed provider declarations, teardown hooks, and request-scope helper boundaries. | | Container introspection helper types | ContainerResolutionState, ContainerResolutionCacheOwner, and ContainerFactoryResolutionState describe the read-only graph/cache views and controlled cache adoption helpers returned by inspectResolutionState(). | | FactoryResolutionKind | Root export | Classifies whether a factory provider returned synchronously (sync) or through a promise (async) for container diagnostics and introspection. | | NormalizedProvider | Compatibility-only public type for the container's validated provider record shape. Prefer authoring providers with Provider or the specific provider interfaces; the container owns normalized record construction. | | @fluojs/di/internal | Package-integration seam exposing validateProviderInputs(...) so sibling fluo packages can apply the container's canonical provider validation before their own traversal. Application code should continue to register providers through Container. | | DiErrorContext | Structured context attached to DI errors so logs and tests can inspect tokens, scopes, modules, dependency chains, and hints. | | Error classes | InvalidProviderError, ContainerResolutionError, RequestScopeResolutionError, ScopeMismatchError, CircularDependencyError, DuplicateProviderError. Use isDiError(...) at package boundaries; it validates the versioned DI owner contract and required common fields across compatible duplicate copies. |

Resolving a multi-provider token returns an array of resolved values in registration order.

Related Packages

  • @fluojs/core: Defines the @Inject() and @Scope() decorators used to annotate classes.
  • @fluojs/runtime: Handles automatic registration of providers during application bootstrap.
  • @fluojs/http: Creates a request scope for every incoming HTTP request.

Example Sources

  • packages/di/src/container.ts
  • packages/di/src/container.test.ts
  • examples/minimal/src/app.ts

Compatible framework service copies

Explicitly designated framework service classes resolve across compatible same-realm package copies through ordinary class injection. Unmarked classes, named tokens, scopes, overrides, and module visibility retain their existing semantics.