# @fluojs/di

<p><strong><kbd>English</kbd></strong> <a href="./README.ko.md"><kbd>한국어</kbd></a></p>

Node.js support is `>=24.0.0 <27`. See [Node.js support and migration](../../docs/reference/node-support.md) before upgrading.

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

## Table of Contents

- [Installation](#installation)
- [When to Use](#when-to-use)
- [Quick Start](#quick-start)
- [Key Capabilities](#key-capabilities)
- [NestJS Scope and Optional Dependency Migration](#nestjs-scope-and-optional-dependency-migration)
- [Circular Dependency Handling](#circular-dependency-handling)
- [Testing and Mocking](#testing-and-mocking)
- [Internal Package Integrations](#internal-package-integrations)
- [Troubleshooting](#troubleshooting)
- [Public API](#public-api)
- [Related Packages](#related-packages)
- [Example Sources](#example-sources)

## Installation

```bash
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.

```typescript
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.

```ts
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()`.

```typescript
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.

```typescript
// 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.

```typescript
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`.

```typescript
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`.

```typescript
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.

```typescript
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.

```typescript
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.

## 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](../../docs/getting-started/migrate-core-di-declarations.md).
`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`. |

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`
