@fluojs/di 1.0.3 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.ko.md CHANGED
@@ -67,7 +67,7 @@ const service = await container.resolve(UserService);
67
67
 
68
68
  - **클래스 provider**: `container.register(MyService)` 또는 `{ provide, useClass }`
69
69
  - **값 provider**: `{ provide: 'API_URL', useValue: 'https://api.example.com' }`
70
- - **팩토리 provider**: `{ provide, useFactory, inject }`
70
+ - **팩토리 provider**: `{ provide, useFactory, inject }`. 팩토리가 참조 클래스의 `@Scope(...)` 같은 DI metadata를 상속해야 하고 provider `scope`를 명시하지 않았다면 `resolverClass`를 함께 지정합니다.
71
71
  - **별칭(Alias) provider**: `{ provide: ILogger, useExisting: PinoLogger }`를 사용하여 하나의 토큰을 기존에 등록된 다른 provider로 매핑할 수 있습니다.
72
72
 
73
73
  ### scope-aware 수명 주기 관리
@@ -76,11 +76,11 @@ const service = await container.resolve(UserService);
76
76
  - **request**: `createRequestScope()`마다 새로 생성됩니다.
77
77
  - **transient**: resolve할 때마다 새 인스턴스를 만듭니다.
78
78
 
79
- dispose 중에는 루트 컨테이너가 먼저 살아 있는 request scope 자식을 정리한 뒤, 자식 dispose 중 하나 이상이 실패하더라도 루트가 소유한 singleton 정리를 계속 수행합니다. 자식/루트 dispose 실패가 여러 개 발생하면 `dispose()`는 모든 shutdown 실패를 확인할 수 있도록 `AggregateError`로 보고합니다.
79
+ dispose 중에는 컨테이너가 자신이 소유한 살아 있는 request scope 자식을 먼저 재귀적으로 정리하므로, 루트가 아닌 request scope를 dispose해도 중첩 request scope를 닫은 뒤 자신의 request cache를 정리합니다. 이후 루트 dispose는 자식 dispose 중 하나 이상이 실패하더라도 루트가 소유한 singleton 정리를 계속 수행합니다. 자식/루트 dispose 실패가 여러 개 발생하면 `dispose()`는 모든 shutdown 실패를 확인할 수 있도록 `AggregateError`로 보고합니다.
80
80
 
81
81
  ### provider override
82
82
 
83
- 테스트나 request-local 경계에서 기존 등록을 의도적으로 교체해야 할 때는 `override(...providers)`를 사용합니다. override는 각 토큰의 현재 provider set을 교체하고 현재 컨테이너와 이미 materialize된 request-scope 자식의 cached instance를 무효화하며, 오래된 instance 즉시 dispose합니다. multi provider override는 해당 토큰의 전체 multi-provider set을 교체하므로 필요한 replacement provider를 한 번에 모두 전달하세요. 같은 토큰에 single replacement와 multi replacement를 한 override 호출에서 섞으면 모호한 교체로 보고 거부합니다.
83
+ 테스트나 request-local 경계에서 기존 등록을 의도적으로 교체해야 할 때는 `override(...providers)`를 사용합니다. override는 각 토큰의 현재 provider set을 교체하고 현재 컨테이너와 이미 materialize된 request-scope 자식의 cached instance를 무효화하며, 다음 replacement resolution이 계속되기 전에 오래된 instance dispose 끝나도록 보장합니다. multi provider override는 해당 토큰의 전체 multi-provider set을 교체하므로 필요한 replacement provider를 한 번에 모두 전달하세요. 같은 토큰에 single replacement와 multi replacement를 한 override 호출에서 섞으면 모호한 교체로 보고 거부합니다.
84
84
 
85
85
  ### request scope 분리
86
86
 
@@ -91,7 +91,7 @@ const scopedService = await requestContainer.resolve(RequestScopedService);
91
91
 
92
92
  request scope 컨테이너는 부모 체인의 provider를 해석할 수 있지만, request가 소유하는 등록은 새 singleton provider를 만들 수 없습니다. singleton provider는 request scope를 만들기 전에 루트 컨테이너에 등록하세요. request scope에 로컬 provider를 추가해야 한다면 `scope: 'request'`/`Scope.REQUEST`를 명시하거나 `override()`로 의도적인 request-local 교체를 표현하세요. multi provider에도 같은 규칙이 적용됩니다. 기본 scope의 multi provider는 루트 컨테이너에 등록하고, request-local multi provider는 request scope를 명시하거나 `override()`로 교체해야 합니다.
93
93
 
94
- provider 객체는 등록 시점에 검증됩니다. 모든 객체 provider는 null이 아닌 `provide` 토큰과 정확히 하나의 전략(`useClass`, `useValue`, `useFactory`, `useExisting`)을 포함해야 합니다. 잘못된 provider 형태는 컨테이너 그래프에 영향을 주기 전에 `InvalidProviderError`를 발생시킵니다.
94
+ provider 객체는 등록 시점에 검증됩니다. 모든 객체 provider는 null이 아닌 `provide` 토큰과 정확히 하나의 전략(`useClass`, `useValue`, `useFactory`, `useExisting`)을 포함해야 합니다. class provider에서 `inject`를 생략하거나 `undefined`로 지정하면 `useClass`의 `@Inject(...)` 메타데이터로 fallback하며, 그 밖의 명시적 `inject` 값은 유효한 token 또는 올바른 `forwardRef(...)` / `optional(...)` wrapper로 구성된 배열이어야 합니다. 명시적인 `scope` 값은 `singleton`, `request`, `transient` 중 하나여야 합니다. 잘못된 provider 형태는 컨테이너 그래프에 영향을 주기 전에 `InvalidProviderError`를 발생시킵니다.
95
95
 
96
96
  ## 순환 의존성 처리
97
97
 
@@ -129,22 +129,42 @@ class ServiceWithOptionalLogger {
129
129
 
130
130
  ## 테스트 및 모킹
131
131
 
132
- `useValue`를 사용하면 단위 테스트 중에 컨테이너의 provider를 모의 객체(mock)나 스텁(stub)으로 쉽게 교체할 수 있습니다.
132
+ 먼저 전체 의존성 그래프를 등록한 다음, `override(...)`와 `useValue`를 사용해 기존 provider를 mock이나 stub으로 교체하세요. `register(...)`는 새 provider를 추가하며 중복 토큰을 거부하고, `override(...)`는 지원되는 교체 API입니다.
133
133
 
134
134
  ```typescript
135
+ import { Inject } from '@fluojs/core';
135
136
  import { Container } from '@fluojs/di';
137
+ import { expect, it, vi } from 'vitest';
136
138
 
137
- const container = new Container();
138
- const mockDb = { query: vi.fn() };
139
+ class Database {
140
+ async query(): Promise<readonly string[]> {
141
+ return ['real row'];
142
+ }
143
+ }
139
144
 
140
- // 실제 Database 클래스를 모의 객체 값으로 교체
141
- container.register({
142
- provide: Database,
143
- useValue: mockDb
144
- });
145
+ @Inject(Database)
146
+ class DataService {
147
+ constructor(private readonly database: Database) {}
148
+
149
+ async load(): Promise<readonly string[]> {
150
+ return this.database.query();
151
+ }
152
+ }
153
+
154
+ it('uses a mock database', async () => {
155
+ const mockDb = { query: vi.fn().mockResolvedValue(['mock row']) };
156
+ const container = new Container().register(Database, DataService);
145
157
 
146
- const service = await container.resolve(DataService);
147
- // service는 실제 Database 인스턴스 대신 mockDb를 사용합니다.
158
+ container.override({
159
+ provide: Database,
160
+ useValue: mockDb,
161
+ });
162
+
163
+ const service = await container.resolve(DataService);
164
+
165
+ await expect(service.load()).resolves.toEqual(['mock row']);
166
+ expect(mockDb.query).toHaveBeenCalledOnce();
167
+ });
148
168
  ```
149
169
 
150
170
  ## 문제 해결
@@ -157,16 +177,17 @@ const service = await container.resolve(DataService);
157
177
 
158
178
  ## 공개 API
159
179
 
160
- | Export | 설명 |
161
- |---|---|
162
- | `Container` | 메인 DI 컨테이너 클래스입니다. |
163
- | `register(...providers)` | 하나 이상의 프로바이더를 등록합니다. |
164
- | `override(...providers)` | 기존 provider를 교체하고 cached instance를 무효화하며 오래된 instance dispose합니다. |
165
- | `resolve<T>(token)` | 토큰을 인스턴스로 비동기 해석합니다. |
166
- | `createRequestScope()` | 요청 스코프 의존성을 위한 자식 컨테이너를 생성합니다. |
167
- | `has(token)` | 컨테이너나 부모에 토큰이 등록되어 있는지 확인합니다. |
168
- | `hasRequestScopedDependency(token)` | 토큰 해석 provider 그래프에 request-scoped 의존성이나 순환이 있어 request-scope 컨테이너가 필요할 수 있는지 확인합니다. |
169
- | `dispose()` | request child와 루트가 소유한 singleton instance를 정리합니다. |
180
+ | Surface | 종류 | 설명 |
181
+ |---|---|---|
182
+ | `Container` | Root export | 메인 DI 컨테이너 클래스입니다. |
183
+ | `container.register(...providers)` | `Container` instance method | 하나 이상의 프로바이더를 등록합니다. |
184
+ | `container.override(...providers)` | `Container` instance method | 기존 provider를 교체하고 cached instance를 무효화하며 다음 replacement resolution이 계속되기 전에 오래된 instance dispose settle되도록 보장합니다. |
185
+ | `container.resolve<T>(token)` | `Container` instance method | 토큰을 인스턴스로 비동기 해석합니다. |
186
+ | `container.inspectResolutionState()` | `Container` instance method | snapshot read-only map view, frozen provider record, controlled cache adoption을 통해 cache ownership을 보존해야 하는 testing/tooling helper를 위한 지원 대상 framework-owned container introspection seam을 노출합니다. 애플리케이션 코드는 `has(...)`와 `resolve(...)`를 우선 사용하세요. |
187
+ | `container.createRequestScope()` | `Container` instance method | 요청 스코프 의존성을 위한 자식 컨테이너를 생성합니다. |
188
+ | `container.has(token)` | `Container` instance method | 컨테이너나 부모에 토큰이 등록되어 있는지 확인합니다. |
189
+ | `container.hasRequestScopedDependency(token)` | `Container` instance method | 토큰 해석 시 provider 그래프에 request-scoped 의존성이나 순환이 있어 request-scope 컨테이너가 필요할 수 있는지 확인합니다. |
190
+ | `container.dispose()` | `Container` instance method | request child와 루트가 소유한 singleton instance를 정리합니다. |
170
191
  | `forwardRef(fn)` | 선언 순서 문제를 위해 조회를 지연하는 토큰 래퍼를 반환합니다. 실제 생성자 순환을 해석 가능하게 만들지는 않습니다. |
171
192
  | `isForwardRef(value)` | `forwardRef(...)`가 만든 값인지 확인하는 type guard입니다. 커스텀 provider tooling이 DI token wrapper와 통합될 때 사용할 수 있습니다. |
172
193
  | `optional(token)` | 하나의 의존성을 optional로 표시하는 토큰 래퍼를 반환합니다. 누락된 optional dependency는 `undefined`로 해석됩니다. |
@@ -175,7 +196,9 @@ const service = await container.resolve(DataService);
175
196
  | Provider types | `Provider`, `ClassProvider`, `FactoryProvider`, `ValueProvider`, `ExistingProvider`는 `register(...)`와 `override(...)`가 받는 공개 registration shape를 설명합니다. |
176
197
  | Token wrapper types | `ForwardRefFn`과 `OptionalToken`은 `forwardRef(...)`와 `optional(...)`이 반환하는 wrapper 값을 설명합니다. |
177
198
  | Container helper types | `ClassType`, `Disposable`, `RequestScopeContainer`는 typed provider 선언, teardown hook, request-scope helper 경계를 지원합니다. |
199
+ | Container introspection helper types | `ContainerResolutionState`, `ContainerResolutionCacheOwner`, `ContainerFactoryResolutionState`는 `inspectResolutionState()`가 반환하는 read-only graph/cache view와 controlled cache adoption helper를 설명합니다. |
178
200
  | `NormalizedProvider` | 컨테이너가 검증한 provider record shape를 위한 compatibility-only 공개 타입입니다. provider를 작성할 때는 `Provider`나 구체 provider interface를 우선 사용하세요. normalized record 생성은 컨테이너가 소유합니다. |
201
+ | `@fluojs/di/internal` | sibling fluo package가 자체 순회 전에 컨테이너의 canonical provider validation을 적용할 수 있도록 `validateProviderInputs(...)`를 노출하는 package-integration seam입니다. 애플리케이션 코드는 계속 `Container`를 통해 provider를 등록해야 합니다. |
179
202
  | `DiErrorContext` | DI error에 붙는 구조화된 context입니다. 로그와 테스트가 token, scope, module, dependency chain, hint를 검사할 수 있게 합니다. |
180
203
  | 에러 클래스 | `InvalidProviderError`, `ContainerResolutionError`, `RequestScopeResolutionError`, `ScopeMismatchError`, `CircularDependencyError`, `DuplicateProviderError`. |
181
204
 
package/README.md CHANGED
@@ -67,7 +67,7 @@ const result = await service.getStatus();
67
67
  fluo DI supports four provider shapes:
68
68
  - **Class Providers**: `container.register(MyService)` or `{ provide: MyToken, useClass: MyService }`.
69
69
  - **Value Providers**: `{ provide: 'API_URL', useValue: 'https://api.example.com' }`.
70
- - **Factory Providers**: `{ provide: 'ASYNC_CONFIG', useFactory: async (db) => await db.load(), inject: [Database] }`.
70
+ - **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.
71
71
  - **Alias Providers**: `{ provide: ILogger, useExisting: PinoLogger }` allows mapping one token to another existing provider.
72
72
 
73
73
  ### Scope Management
@@ -75,11 +75,11 @@ fluo DI supports four provider shapes:
75
75
  - **Request**: Instance is created once per `createRequestScope()` call.
76
76
  - **Transient**: A new instance is created every time it is resolved.
77
77
 
78
- During disposal, the root container first tears down live request-scope children and 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.
78
+ During disposal, 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.
79
79
 
80
80
  ### Provider Overrides
81
81
 
82
- 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 immediately. 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.
82
+ 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.
83
83
 
84
84
  ### Request Scoping
85
85
  Isolated containers can be created to handle per-request state without polluting the root container.
@@ -91,7 +91,7 @@ const scopedService = await requestContainer.resolve(RequestScopedService);
91
91
 
92
92
  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'`/`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()`.
93
93
 
94
- Provider objects are validated at registration time: every object provider must include a non-null `provide` token and exactly one strategy (`useClass`, `useValue`, `useFactory`, or `useExisting`). Invalid provider shapes throw `InvalidProviderError` before they can affect the container graph.
94
+ Provider objects are validated at registration time: every object provider must include a non-null `provide` token and exactly one strategy (`useClass`, `useValue`, `useFactory`, or `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(...)` / `optional(...)` wrappers. Explicit `scope` values must be `singleton`, `request`, or `transient`. Invalid provider shapes throw `InvalidProviderError` before they can affect the container graph.
95
95
 
96
96
  ## Circular Dependency Handling
97
97
 
@@ -129,22 +129,42 @@ class ServiceWithOptionalLogger {
129
129
 
130
130
  ## Testing and Mocking
131
131
 
132
- You can easily override providers in the container to use mocks or stubs during unit testing by using `useValue`.
132
+ 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.
133
133
 
134
134
  ```typescript
135
+ import { Inject } from '@fluojs/core';
135
136
  import { Container } from '@fluojs/di';
137
+ import { expect, it, vi } from 'vitest';
136
138
 
137
- const container = new Container();
138
- const mockDb = { query: vi.fn() };
139
+ class Database {
140
+ async query(): Promise<readonly string[]> {
141
+ return ['real row'];
142
+ }
143
+ }
139
144
 
140
- // Override the real Database class with a mock value
141
- container.register({
142
- provide: Database,
143
- useValue: mockDb
144
- });
145
+ @Inject(Database)
146
+ class DataService {
147
+ constructor(private readonly database: Database) {}
148
+
149
+ async load(): Promise<readonly string[]> {
150
+ return this.database.query();
151
+ }
152
+ }
153
+
154
+ it('uses a mock database', async () => {
155
+ const mockDb = { query: vi.fn().mockResolvedValue(['mock row']) };
156
+ const container = new Container().register(Database, DataService);
145
157
 
146
- const service = await container.resolve(DataService);
147
- // service uses mockDb instead of the real Database instance
158
+ container.override({
159
+ provide: Database,
160
+ useValue: mockDb,
161
+ });
162
+
163
+ const service = await container.resolve(DataService);
164
+
165
+ await expect(service.load()).resolves.toEqual(['mock row']);
166
+ expect(mockDb.query).toHaveBeenCalledOnce();
167
+ });
148
168
  ```
149
169
 
150
170
  ## Troubleshooting
@@ -157,16 +177,17 @@ Ensure all required providers are registered in the container. If you use `creat
157
177
 
158
178
  ## Public API
159
179
 
160
- | Export | Description |
161
- |---|---|
162
- | `Container` | The main DI container class. |
163
- | `register(...providers)` | Registers one or more providers. |
164
- | `override(...providers)` | Replaces existing providers, invalidates cached instances, and disposes stale instances. |
165
- | `resolve<T>(token)` | Asynchronously resolves a token to an instance. |
166
- | `createRequestScope()` | Creates a child container for request-scoped dependencies. |
167
- | `has(token)` | Checks if a token is registered in the container or its parents. |
168
- | `hasRequestScopedDependency(token)` | Checks whether resolving a token may require a request-scope container because its provider graph contains request-scoped dependencies or is cyclic. |
169
- | `dispose()` | Disposes request children and root-owned singleton instances. |
180
+ | Surface | Kind | Description |
181
+ |---|---|---|
182
+ | `Container` | Root export | The main DI container class. |
183
+ | `container.register(...providers)` | `Container` instance method | Registers one or more providers. |
184
+ | `container.override(...providers)` | `Container` instance method | Replaces existing providers, invalidates cached instances, and ensures stale instance disposal settles before the next replacement resolution continues. |
185
+ | `container.resolve<T>(token)` | `Container` instance method | Asynchronously resolves a token to an instance. |
186
+ | `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. |
187
+ | `container.createRequestScope()` | `Container` instance method | Creates a child container for request-scoped dependencies. |
188
+ | `container.has(token)` | `Container` instance method | Checks if a token is registered in the container or its parents. |
189
+ | `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. |
190
+ | `container.dispose()` | `Container` instance method | Disposes request children and root-owned singleton instances. |
170
191
  | `forwardRef(fn)` | Returns a token wrapper that defers lookup for declaration-order issues; it does not make constructor dependency cycles resolvable. |
171
192
  | `isForwardRef(value)` | Type guard for values produced by `forwardRef(...)`; useful when integrating custom provider tooling with DI token wrappers. |
172
193
  | `optional(token)` | Returns a token wrapper that marks one dependency as optional; missing optional dependencies resolve to `undefined`. |
@@ -175,7 +196,9 @@ Ensure all required providers are registered in the container. If you use `creat
175
196
  | Provider types | `Provider`, `ClassProvider`, `FactoryProvider`, `ValueProvider`, and `ExistingProvider` describe the public registration shapes accepted by `register(...)` and `override(...)`. |
176
197
  | Token wrapper types | `ForwardRefFn` and `OptionalToken` describe the wrapper values returned by `forwardRef(...)` and `optional(...)`. |
177
198
  | Container helper types | `ClassType`, `Disposable`, and `RequestScopeContainer` support typed provider declarations, teardown hooks, and request-scope helper boundaries. |
199
+ | Container introspection helper types | `ContainerResolutionState`, `ContainerResolutionCacheOwner`, and `ContainerFactoryResolutionState` describe the read-only graph/cache views and controlled cache adoption helpers returned by `inspectResolutionState()`. |
178
200
  | `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. |
201
+ | `@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`. |
179
202
  | `DiErrorContext` | Structured context attached to DI errors so logs and tests can inspect tokens, scopes, modules, dependency chains, and hints. |
180
203
  | Error classes | `InvalidProviderError`, `ContainerResolutionError`, `RequestScopeResolutionError`, `ScopeMismatchError`, `CircularDependencyError`, `DuplicateProviderError`. |
181
204
 
@@ -1,5 +1,43 @@
1
1
  import { type Token } from '@fluojs/core';
2
- import type { Provider } from './types.js';
2
+ import type { NormalizedProvider, Provider } from './types.js';
3
+ /**
4
+ * Factory provider resolution mode recorded after a factory returns either synchronously or through a promise.
5
+ */
6
+ export type FactoryResolutionKind = 'async' | 'sync';
7
+ /**
8
+ * Controlled cache adoption seam for framework-owned testing and tooling that
9
+ * need synchronous helpers to preserve container-owned singleton disposal.
10
+ */
11
+ export interface ContainerResolutionCacheOwner {
12
+ readonly deleteMultiSingleton: (provider: NormalizedProvider) => void;
13
+ readonly deleteSingleton: (token: Token) => void;
14
+ readonly recordFactoryResolution: (provider: NormalizedProvider, kind: FactoryResolutionKind) => void;
15
+ readonly setMultiSingleton: (provider: NormalizedProvider, promise: Promise<unknown>) => void;
16
+ readonly setSingleton: (token: Token, promise: Promise<unknown>) => void;
17
+ }
18
+ /**
19
+ * Read-only factory resolution diagnostics recorded by container-owned factory
20
+ * instantiation paths.
21
+ */
22
+ export interface ContainerFactoryResolutionState {
23
+ readonly get: (provider: NormalizedProvider) => FactoryResolutionKind | undefined;
24
+ readonly has: (provider: NormalizedProvider) => boolean;
25
+ }
26
+ /**
27
+ * Public read-only seam for framework-owned testing and tooling that need to
28
+ * inspect a container's resolved provider graph without depending on private
29
+ * field names or structural casts.
30
+ */
31
+ export interface ContainerResolutionState {
32
+ readonly cacheOwner: ContainerResolutionCacheOwner;
33
+ readonly factoryResolutionKinds: ContainerFactoryResolutionState;
34
+ readonly parent?: ContainerResolutionState;
35
+ readonly registrations: ReadonlyMap<Token, NormalizedProvider>;
36
+ readonly multiRegistrations: ReadonlyMap<Token, readonly NormalizedProvider[]>;
37
+ readonly multiSingletonCache: ReadonlyMap<NormalizedProvider, Promise<unknown>>;
38
+ readonly requestScopeEnabled: boolean;
39
+ readonly singletonCache: ReadonlyMap<Token, Promise<unknown>>;
40
+ }
3
41
  /**
4
42
  * Scope-aware dependency injection container for Fluo providers.
5
43
  */
@@ -13,9 +51,9 @@ export declare class Container {
13
51
  private multiRequestCache;
14
52
  private readonly multiSingletonCache;
15
53
  private readonly staleDisposalTasks;
16
- private readonly staleDisposalErrors;
17
54
  private readonly singletonCache;
18
55
  private readonly forwardRefTokenCache;
56
+ private readonly factoryResolutionKinds;
19
57
  private readonly providerLookupPlanCache;
20
58
  private readonly multiProviderPlanCache;
21
59
  private readonly requestScopeVerdictPlanCache;
@@ -23,7 +61,7 @@ export declare class Container {
23
61
  private childScopes;
24
62
  private disposePromise;
25
63
  private disposed;
26
- private trackedByRoot;
64
+ private trackedByParent;
27
65
  private graphRevision;
28
66
  constructor(parent?: Container | undefined, requestScopeEnabled?: boolean, singletonCache?: Map<Token, Promise<unknown>>);
29
67
  /**
@@ -59,6 +97,21 @@ export declare class Container {
59
97
  * @returns `true` when a single or multi provider exists for the token.
60
98
  */
61
99
  has(token: Token): boolean;
100
+ /**
101
+ * Returns the framework-owned resolution state for testing/tooling adapters.
102
+ *
103
+ * This method is the supported introspection seam for packages such as
104
+ * `@fluojs/testing`; callers should prefer ordinary `has(...)` and
105
+ * `resolve(...)` unless they need read-only graph/cache visibility while
106
+ * implementing a framework-level helper. Cache adoption for synchronous
107
+ * helpers goes through `cacheOwner`; the returned maps are not mutable
108
+ * container internals.
109
+ *
110
+ * @returns Read-only provider registrations and resolution caches for this container scope.
111
+ */
112
+ inspectResolutionState(): ContainerResolutionState;
113
+ private createCacheOwner;
114
+ private createFactoryResolutionState;
62
115
  /**
63
116
  * Returns whether resolving a token may require a request-scope container.
64
117
  *
@@ -147,7 +200,7 @@ export declare class Container {
147
200
  private writePlanCache;
148
201
  private advanceGraphRevision;
149
202
  private clearResolutionPlanCaches;
150
- private waitForStaleDisposalTasks;
203
+ private assertStaleDisposalsSettled;
151
204
  private scheduleStaleDisposal;
152
205
  private throwDisposalErrors;
153
206
  private collectDisposalError;
@@ -161,7 +214,6 @@ export declare class Container {
161
214
  private resolveForwardRefToken;
162
215
  private resolveProviderDeps;
163
216
  private invalidateAffectedCachedEntriesInHierarchy;
164
- private isAncestorOf;
165
217
  private invalidateAffectedCachedEntries;
166
218
  private shouldInvalidateCachedToken;
167
219
  private shouldInvalidateCachedProvider;
@@ -1 +1 @@
1
- {"version":3,"file":"container.d.ts","sourceRoot":"","sources":["../src/container.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"container.d.ts","sourceRoot":"","sources":["../src/container.ts"],"names":[],"mappings":"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"}
package/dist/container.js CHANGED
@@ -1,115 +1,102 @@
1
- import { InvariantError, formatTokenName } from '@fluojs/core';
1
+ import { formatTokenName, InvariantError } from '@fluojs/core';
2
2
  import { getClassDiMetadata } from '@fluojs/core/internal';
3
- import { CircularDependencyError, ContainerResolutionError, DuplicateProviderError, InvalidProviderError, RequestScopeResolutionError, ScopeMismatchError } from './errors.js';
4
- import { Scope, isForwardRef, isOptionalToken } from './types.js';
5
- function isClassConstructor(value) {
6
- return typeof value === 'function';
7
- }
8
- function isValueProvider(value) {
9
- return typeof value === 'object' && value !== null && 'useValue' in value;
10
- }
11
- function isFactoryProvider(value) {
12
- return typeof value === 'object' && value !== null && 'useFactory' in value;
13
- }
14
- function isClassProvider(value) {
15
- return typeof value === 'object' && value !== null && 'useClass' in value;
16
- }
17
- function isExistingProvider(value) {
18
- return typeof value === 'object' && value !== null && 'useExisting' in value;
19
- }
20
- function assertProviderToken(provider) {
21
- if (!('provide' in provider) || provider.provide == null) {
22
- throw new InvalidProviderError('Provider object must include a non-null provide token.');
3
+ import { CircularDependencyError, ContainerResolutionError, DuplicateProviderError, RequestScopeResolutionError, ScopeMismatchError } from './errors.js';
4
+ import { normalizeProvider } from './provider-normalization.js';
5
+ import { isForwardRef, isOptionalToken, Scope } from './types.js';
6
+
7
+ /**
8
+ * Factory provider resolution mode recorded after a factory returns either synchronously or through a promise.
9
+ */
10
+
11
+ /**
12
+ * Controlled cache adoption seam for framework-owned testing and tooling that
13
+ * need synchronous helpers to preserve container-owned singleton disposal.
14
+ */
15
+
16
+ /**
17
+ * Read-only factory resolution diagnostics recorded by container-owned factory
18
+ * instantiation paths.
19
+ */
20
+
21
+ /**
22
+ * Public read-only seam for framework-owned testing and tooling that need to
23
+ * inspect a container's resolved provider graph without depending on private
24
+ * field names or structural casts.
25
+ */
26
+
27
+ class ReadonlyMapView {
28
+ #source;
29
+ [Symbol.toStringTag] = 'Map';
30
+ constructor(source) {
31
+ this.#source = source;
23
32
  }
24
- }
25
- function assertProviderStrategy(provider) {
26
- const strategyCount = Number('useValue' in provider) + Number('useFactory' in provider) + Number('useClass' in provider) + Number('useExisting' in provider);
27
- if (strategyCount !== 1) {
28
- throw new InvalidProviderError('Provider object must declare exactly one of useValue, useFactory, useClass, or useExisting.');
33
+ get size() {
34
+ return this.#source.size;
29
35
  }
30
- }
31
- function assertObjectProvider(provider) {
32
- assertProviderToken(provider);
33
- assertProviderStrategy(provider);
34
- }
35
- function normalizeInjectToken(token) {
36
- if (token == null) {
37
- throw new InvalidProviderError('Inject token must not be null or undefined. Check that all tokens in @Inject(...) are defined at the point of decoration (forward-reference cycles require forwardRef()).');
36
+ entries() {
37
+ return this.#source.entries();
38
+ }
39
+ forEach(callbackfn, thisArg) {
40
+ for (const [key, value] of this.#source) {
41
+ callbackfn.call(thisArg, value, key, this);
42
+ }
43
+ }
44
+ get(key) {
45
+ return this.#source.get(key);
46
+ }
47
+ has(key) {
48
+ return this.#source.has(key);
49
+ }
50
+ keys() {
51
+ return this.#source.keys();
52
+ }
53
+ values() {
54
+ return this.#source.values();
55
+ }
56
+ [Symbol.iterator]() {
57
+ return this.entries();
38
58
  }
39
- return token;
40
59
  }
41
- function normalizeProvider(provider) {
42
- if (isClassConstructor(provider)) {
43
- const metadata = getClassDiMetadata(provider);
44
- return {
45
- inject: (metadata?.inject ?? []).map(normalizeInjectToken),
46
- provide: provider,
47
- scope: metadata?.scope ?? Scope.DEFAULT,
48
- type: 'class',
49
- useClass: provider
50
- };
60
+ class ReadonlyMultiRegistrationMapView {
61
+ #source;
62
+ [Symbol.toStringTag] = 'Map';
63
+ constructor(source) {
64
+ this.#source = source;
51
65
  }
52
- if (isValueProvider(provider)) {
53
- assertObjectProvider(provider);
54
- return {
55
- inject: [],
56
- multi: provider.multi,
57
- provide: provider.provide,
58
- scope: Scope.DEFAULT,
59
- type: 'value',
60
- useValue: provider.useValue
61
- };
66
+ get size() {
67
+ return this.#source.size;
62
68
  }
63
- if (isFactoryProvider(provider)) {
64
- assertObjectProvider(provider);
65
- if (typeof provider.useFactory !== 'function') {
66
- throw new InvalidProviderError('Factory provider useFactory must be a function.', {
67
- token: provider.provide
68
- });
69
+ *entries() {
70
+ for (const [token, providers] of this.#source) {
71
+ yield [token, Object.freeze([...providers])];
69
72
  }
70
- const metadata = provider.resolverClass ? getClassDiMetadata(provider.resolverClass) : undefined;
71
- return {
72
- inject: (provider.inject ?? []).map(normalizeInjectToken),
73
- multi: provider.multi,
74
- provide: provider.provide,
75
- scope: provider.scope ?? metadata?.scope ?? Scope.DEFAULT,
76
- type: 'factory',
77
- useFactory: provider.useFactory
78
- };
79
73
  }
80
- if (isClassProvider(provider)) {
81
- assertObjectProvider(provider);
82
- if (typeof provider.useClass !== 'function') {
83
- throw new InvalidProviderError('Class provider useClass must be a constructor.', {
84
- token: provider.provide
85
- });
74
+ forEach(callbackfn, thisArg) {
75
+ for (const [key, value] of this.entries()) {
76
+ callbackfn.call(thisArg, value, key, this);
86
77
  }
87
- const metadata = getClassDiMetadata(provider.useClass);
88
- return {
89
- inject: (provider.inject ?? metadata?.inject ?? []).map(normalizeInjectToken),
90
- multi: provider.multi,
91
- provide: provider.provide,
92
- scope: provider.scope ?? metadata?.scope ?? Scope.DEFAULT,
93
- type: 'class',
94
- useClass: provider.useClass
95
- };
96
78
  }
97
- if (isExistingProvider(provider)) {
98
- assertObjectProvider(provider);
99
- if (provider.useExisting == null) {
100
- throw new InvalidProviderError('Alias provider useExisting must be a non-null token.', {
101
- token: provider.provide
102
- });
79
+ get(key) {
80
+ const providers = this.#source.get(key);
81
+ return providers ? Object.freeze([...providers]) : undefined;
82
+ }
83
+ has(key) {
84
+ return this.#source.has(key);
85
+ }
86
+ keys() {
87
+ return this.#source.keys();
88
+ }
89
+ *values() {
90
+ for (const providers of this.#source.values()) {
91
+ yield Object.freeze([...providers]);
103
92
  }
104
- return {
105
- inject: [],
106
- provide: provider.provide,
107
- scope: Scope.DEFAULT,
108
- type: 'existing',
109
- useExisting: provider.useExisting
110
- };
111
93
  }
112
- throw new InvalidProviderError('Unsupported provider type.');
94
+ [Symbol.iterator]() {
95
+ return this.entries();
96
+ }
97
+ }
98
+ function isPromiseLike(value) {
99
+ return (typeof value === 'object' || typeof value === 'function') && value !== null && typeof value.then === 'function';
113
100
  }
114
101
 
115
102
  /**
@@ -123,9 +110,9 @@ export class Container {
123
110
  multiRequestCache;
124
111
  multiSingletonCache = new Map();
125
112
  staleDisposalTasks = new Set();
126
- staleDisposalErrors = [];
127
113
  singletonCache;
128
114
  forwardRefTokenCache = new WeakMap();
115
+ factoryResolutionKinds = new WeakMap();
129
116
  providerLookupPlanCache = new Map();
130
117
  multiProviderPlanCache = new Map();
131
118
  requestScopeVerdictPlanCache = new Map();
@@ -133,7 +120,7 @@ export class Container {
133
120
  childScopes;
134
121
  disposePromise;
135
122
  disposed = false;
136
- trackedByRoot = false;
123
+ trackedByParent = false;
137
124
  graphRevision = 0;
138
125
  constructor(parent, requestScopeEnabled = false, singletonCache) {
139
126
  this.parent = parent;
@@ -251,6 +238,65 @@ export class Container {
251
238
  return this.lookupProvider(token) !== undefined || this.hasMulti(token);
252
239
  }
253
240
 
241
+ /**
242
+ * Returns the framework-owned resolution state for testing/tooling adapters.
243
+ *
244
+ * This method is the supported introspection seam for packages such as
245
+ * `@fluojs/testing`; callers should prefer ordinary `has(...)` and
246
+ * `resolve(...)` unless they need read-only graph/cache visibility while
247
+ * implementing a framework-level helper. Cache adoption for synchronous
248
+ * helpers goes through `cacheOwner`; the returned maps are not mutable
249
+ * container internals.
250
+ *
251
+ * @returns Read-only provider registrations and resolution caches for this container scope.
252
+ */
253
+ inspectResolutionState() {
254
+ const registrations = new Map(this.registrations);
255
+ const multiRegistrations = new Map(Array.from(this.multiRegistrations, ([token, providers]) => [token, Object.freeze([...providers])]));
256
+ const multiSingletonCache = new Map(this.multiSingletonCache);
257
+ const singletonCache = new Map(this.singletonCache);
258
+ return {
259
+ cacheOwner: this.createCacheOwner(singletonCache, multiSingletonCache),
260
+ factoryResolutionKinds: this.createFactoryResolutionState(),
261
+ parent: this.parent?.inspectResolutionState(),
262
+ registrations: new ReadonlyMapView(registrations),
263
+ multiRegistrations: new ReadonlyMultiRegistrationMapView(multiRegistrations),
264
+ multiSingletonCache: new ReadonlyMapView(multiSingletonCache),
265
+ requestScopeEnabled: this.requestScopeEnabled,
266
+ singletonCache: new ReadonlyMapView(singletonCache)
267
+ };
268
+ }
269
+ createCacheOwner(singletonCacheSnapshot, multiSingletonCacheSnapshot) {
270
+ return Object.freeze({
271
+ deleteMultiSingleton: provider => {
272
+ this.multiSingletonCache.delete(provider);
273
+ multiSingletonCacheSnapshot.delete(provider);
274
+ },
275
+ deleteSingleton: token => {
276
+ this.singletonCache.delete(token);
277
+ singletonCacheSnapshot.delete(token);
278
+ },
279
+ recordFactoryResolution: (provider, kind) => {
280
+ this.root().factoryResolutionKinds.set(provider, kind);
281
+ },
282
+ setMultiSingleton: (provider, promise) => {
283
+ this.multiSingletonCache.set(provider, promise);
284
+ multiSingletonCacheSnapshot.set(provider, promise);
285
+ },
286
+ setSingleton: (token, promise) => {
287
+ this.singletonCache.set(token, promise);
288
+ singletonCacheSnapshot.set(token, promise);
289
+ }
290
+ });
291
+ }
292
+ createFactoryResolutionState() {
293
+ const root = this.root();
294
+ return Object.freeze({
295
+ get: provider => root.factoryResolutionKinds.get(provider),
296
+ has: provider => root.factoryResolutionKinds.has(provider)
297
+ });
298
+ }
299
+
254
300
  /**
255
301
  * Returns whether resolving a token may require a request-scope container.
256
302
  *
@@ -297,6 +343,7 @@ export class Container {
297
343
  hint: 'Ensure all resolves complete before calling container.dispose().'
298
344
  });
299
345
  }
346
+ await this.assertStaleDisposalsSettled();
300
347
  return this.resolveWithChain(token, [], new Set());
301
348
  }
302
349
 
@@ -324,8 +371,8 @@ export class Container {
324
371
  async disposeAll() {
325
372
  const errors = [];
326
373
  try {
327
- // Dispose all live request-scope children first (root only)
328
- if (!this.parent && this.childScopes && this.childScopes.size > 0) {
374
+ // Dispose all live request-scope children before tearing down this scope's cache.
375
+ if (this.childScopes && this.childScopes.size > 0) {
329
376
  const childResults = await Promise.allSettled(Array.from(this.childScopes).map(child => child.dispose()));
330
377
  for (const result of childResults) {
331
378
  if (result.status === 'rejected') {
@@ -341,9 +388,9 @@ export class Container {
341
388
  }
342
389
  this.throwDisposalErrors(errors);
343
390
  } finally {
344
- if (this.parent && this.trackedByRoot) {
345
- this.root().childScopes?.delete(this);
346
- this.trackedByRoot = false;
391
+ if (this.parent && this.trackedByParent) {
392
+ this.parent.childScopes?.delete(this);
393
+ this.trackedByParent = false;
347
394
  }
348
395
  }
349
396
  }
@@ -589,13 +636,13 @@ export class Container {
589
636
  return this.parent ? this.parent.root() : this;
590
637
  }
591
638
  ensureTrackedRequestScope() {
592
- if (!this.requestScopeEnabled || !this.parent || this.trackedByRoot) {
639
+ if (!this.requestScopeEnabled || !this.parent || this.trackedByParent) {
593
640
  return;
594
641
  }
595
- const root = this.root();
596
- root.childScopes ??= new Set();
597
- root.childScopes.add(this);
598
- this.trackedByRoot = true;
642
+ this.parent.ensureTrackedRequestScope();
643
+ this.parent.childScopes ??= new Set();
644
+ this.parent.childScopes.add(this);
645
+ this.trackedByParent = true;
599
646
  }
600
647
  requestCacheForWrite() {
601
648
  this.ensureTrackedRequestScope();
@@ -678,12 +725,17 @@ export class Container {
678
725
  return entries;
679
726
  }
680
727
  async disposeCache(entries) {
681
- await this.waitForStaleDisposalTasks();
728
+ const errors = [];
729
+ try {
730
+ await this.assertStaleDisposalsSettled();
731
+ } catch (error) {
732
+ this.collectDisposalError(error, errors);
733
+ }
682
734
  const {
683
735
  disposables,
684
- errors
736
+ errors: resolutionErrors
685
737
  } = await this.collectDisposableInstances(entries);
686
- errors.push(...this.staleDisposalErrors.splice(0, this.staleDisposalErrors.length));
738
+ errors.push(...resolutionErrors);
687
739
  errors.push(...(await this.disposeInstancesInReverseOrder(disposables)));
688
740
  this.clearDisposalCaches();
689
741
  this.throwDisposalErrors(errors);
@@ -759,26 +811,49 @@ export class Container {
759
811
  this.requestScopeVerdictPlanCache.clear();
760
812
  this.effectiveProviderPlanCache.clear();
761
813
  }
762
- async waitForStaleDisposalTasks() {
814
+ async assertStaleDisposalsSettled() {
815
+ const errors = [];
763
816
  while (this.staleDisposalTasks.size > 0) {
764
- await Promise.all(Array.from(this.staleDisposalTasks));
817
+ const tasks = Array.from(this.staleDisposalTasks);
818
+ await Promise.all(tasks.map(task => task.promise));
819
+ for (const task of tasks) {
820
+ this.staleDisposalTasks.delete(task);
821
+ if (task.failed && !task.errorConsumed) {
822
+ task.errorConsumed = true;
823
+ errors.push(task.error);
824
+ }
825
+ }
765
826
  }
827
+ this.throwDisposalErrors(errors);
766
828
  }
767
- scheduleStaleDisposal(instancePromise) {
768
- let task;
769
- task = (async () => {
829
+ scheduleStaleDisposal(instancePromise, staleDisposalOwner) {
830
+ const observers = staleDisposalOwner === this ? [this] : [this, staleDisposalOwner];
831
+ const task = {
832
+ error: undefined,
833
+ errorConsumed: false,
834
+ failed: false,
835
+ promise: Promise.resolve()
836
+ };
837
+ task.promise = (async () => {
770
838
  try {
771
839
  const instance = await instancePromise;
772
840
  if (this.isDisposable(instance)) {
773
841
  await instance.onDestroy();
774
842
  }
775
843
  } catch (error) {
776
- this.staleDisposalErrors.push(error);
844
+ task.error = error;
845
+ task.failed = true;
777
846
  }
778
847
  })().finally(() => {
779
- this.staleDisposalTasks.delete(task);
848
+ if (!task.failed) {
849
+ for (const observer of observers) {
850
+ observer.staleDisposalTasks.delete(task);
851
+ }
852
+ }
780
853
  });
781
- this.staleDisposalTasks.add(task);
854
+ for (const observer of observers) {
855
+ observer.staleDisposalTasks.add(task);
856
+ }
782
857
  }
783
858
  throwDisposalErrors(errors) {
784
859
  if (errors.length === 1) {
@@ -811,7 +886,9 @@ export class Container {
811
886
  throw new InvariantError('Factory provider is missing useFactory.');
812
887
  }
813
888
  const deps = await this.resolveProviderDeps(provider, chain, activeTokens);
814
- return provider.useFactory(...deps);
889
+ const value = provider.useFactory(...deps);
890
+ this.root().factoryResolutionKinds.set(provider, isPromiseLike(value) ? 'async' : 'sync');
891
+ return value;
815
892
  }
816
893
  case 'class':
817
894
  {
@@ -931,34 +1008,18 @@ export class Container {
931
1008
  }
932
1009
  return deps;
933
1010
  }
934
- invalidateAffectedCachedEntriesInHierarchy(token) {
935
- this.invalidateAffectedCachedEntries(token);
936
- const childScopes = this.root().childScopes;
937
- if (!childScopes) {
938
- return;
939
- }
940
- for (const childScope of childScopes) {
941
- if (this.isAncestorOf(childScope)) {
942
- childScope.invalidateAffectedCachedEntries(token);
943
- }
944
- }
945
- }
946
- isAncestorOf(container) {
947
- let current = container.parent;
948
- while (current) {
949
- if (current === this) {
950
- return true;
951
- }
952
- current = current.parent;
1011
+ invalidateAffectedCachedEntriesInHierarchy(token, staleDisposalOwner = this) {
1012
+ this.invalidateAffectedCachedEntries(token, staleDisposalOwner);
1013
+ for (const childScope of this.childScopes ?? []) {
1014
+ childScope.invalidateAffectedCachedEntriesInHierarchy(token, staleDisposalOwner);
953
1015
  }
954
- return false;
955
1016
  }
956
- invalidateAffectedCachedEntries(token) {
1017
+ invalidateAffectedCachedEntries(token, staleDisposalOwner) {
957
1018
  for (const [cachedToken, cached] of this.requestCache?.entries() ?? []) {
958
1019
  if (!this.shouldInvalidateCachedToken(cachedToken, token)) {
959
1020
  continue;
960
1021
  }
961
- this.scheduleStaleDisposal(cached);
1022
+ this.scheduleStaleDisposal(cached, staleDisposalOwner);
962
1023
  this.requestCache?.delete(cachedToken);
963
1024
  }
964
1025
  if (!this.parent) {
@@ -966,7 +1027,7 @@ export class Container {
966
1027
  if (!this.shouldInvalidateCachedToken(cachedToken, token)) {
967
1028
  continue;
968
1029
  }
969
- this.scheduleStaleDisposal(cached);
1030
+ this.scheduleStaleDisposal(cached, staleDisposalOwner);
970
1031
  this.singletonCache.delete(cachedToken);
971
1032
  }
972
1033
  }
@@ -975,7 +1036,7 @@ export class Container {
975
1036
  if (!this.shouldInvalidateCachedProvider(provider, token)) {
976
1037
  continue;
977
1038
  }
978
- this.scheduleStaleDisposal(cached);
1039
+ this.scheduleStaleDisposal(cached, staleDisposalOwner);
979
1040
  this.multiSingletonCache.delete(provider);
980
1041
  }
981
1042
  }
@@ -987,7 +1048,7 @@ export class Container {
987
1048
  if (!this.shouldInvalidateCachedProvider(provider, token)) {
988
1049
  continue;
989
1050
  }
990
- this.scheduleStaleDisposal(cached);
1051
+ this.scheduleStaleDisposal(cached, staleDisposalOwner);
991
1052
  multiRequestCache.delete(provider);
992
1053
  }
993
1054
  }
@@ -0,0 +1,3 @@
1
+ export { validateProviderInputs } from './provider-normalization.js';
2
+ export type { Provider } from './types.js';
3
+ //# sourceMappingURL=internal.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"internal.d.ts","sourceRoot":"","sources":["../src/internal.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,sBAAsB,EAAE,MAAM,6BAA6B,CAAC;AACrE,YAAY,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC"}
@@ -0,0 +1 @@
1
+ export { validateProviderInputs } from './provider-normalization.js';
@@ -0,0 +1,17 @@
1
+ import type { NormalizedProvider, Provider } from './types.js';
2
+ /**
3
+ * Validates and snapshots a public provider declaration for container registration.
4
+ *
5
+ * @param provider Provider declaration crossing the container registration boundary.
6
+ * @returns An immutable provider record with normalized injection and scope fields.
7
+ */
8
+ export declare function normalizeProvider(provider: Provider): NormalizedProvider;
9
+ /**
10
+ * Validates provider declarations through the same normalization path used by `Container` registration.
11
+ *
12
+ * @param providers Provider declarations crossing an internal framework integration boundary.
13
+ * @returns The original provider list after every declaration has passed canonical normalization.
14
+ * @internal
15
+ */
16
+ export declare function validateProviderInputs(providers: Provider[]): Provider[];
17
+ //# sourceMappingURL=provider-normalization.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"provider-normalization.d.ts","sourceRoot":"","sources":["../src/provider-normalization.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAA2B,kBAAkB,EAAiB,QAAQ,EAAE,MAAM,YAAY,CAAC;AA6JvG;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,QAAQ,GAAG,kBAAkB,CAiFxE;AAED;;;;;;GAMG;AACH,wBAAgB,sBAAsB,CAAC,SAAS,EAAE,QAAQ,EAAE,GAAG,QAAQ,EAAE,CAMxE"}
@@ -0,0 +1,211 @@
1
+ import { getClassDiMetadata } from '@fluojs/core/internal';
2
+ import { InvalidProviderError } from './errors.js';
3
+ import { Scope } from './types.js';
4
+ function isClassConstructor(value) {
5
+ return isConstructableFunction(value);
6
+ }
7
+ function isProviderObject(value) {
8
+ return typeof value === 'object' && value !== null;
9
+ }
10
+ function isClassType(value) {
11
+ return isConstructableFunction(value);
12
+ }
13
+ function isFactoryFunction(value) {
14
+ return typeof value === 'function';
15
+ }
16
+ function isTokenResolver(value) {
17
+ return typeof value === 'function';
18
+ }
19
+ function isConstructableFunction(value) {
20
+ if (typeof value !== 'function') {
21
+ return false;
22
+ }
23
+ try {
24
+ Reflect.construct(Object, [], value);
25
+ return true;
26
+ } catch (error) {
27
+ if (error instanceof TypeError) {
28
+ return false;
29
+ }
30
+ throw error;
31
+ }
32
+ }
33
+ function isToken(value) {
34
+ return typeof value === 'string' || typeof value === 'symbol' || isConstructableFunction(value);
35
+ }
36
+ function isScope(value) {
37
+ return value === 'singleton' || value === 'request' || value === 'transient';
38
+ }
39
+ function assertProviderToken(provider) {
40
+ if (!('provide' in provider) || provider.provide == null) {
41
+ throw new InvalidProviderError('Provider object must include a non-null provide token.');
42
+ }
43
+ }
44
+ function assertProviderStrategy(provider) {
45
+ const strategyCount = Number('useValue' in provider) + Number('useFactory' in provider) + Number('useClass' in provider) + Number('useExisting' in provider);
46
+ if (strategyCount !== 1) {
47
+ throw new InvalidProviderError('Provider object must declare exactly one of useValue, useFactory, useClass, or useExisting.');
48
+ }
49
+ }
50
+ function assertObjectProvider(provider) {
51
+ assertProviderToken(provider);
52
+ assertProviderStrategy(provider);
53
+ }
54
+ function normalizeProviderScope(scope, providerToken) {
55
+ if (scope === undefined) {
56
+ return undefined;
57
+ }
58
+ if (isScope(scope)) {
59
+ return scope;
60
+ }
61
+ throw new InvalidProviderError('Provider scope must be one of singleton, request, or transient.', {
62
+ token: providerToken,
63
+ scope: String(scope),
64
+ hint: 'Use Scope.DEFAULT, Scope.REQUEST, Scope.TRANSIENT, or the matching string literal.'
65
+ });
66
+ }
67
+ function normalizeInjectToken(token, providerToken, index) {
68
+ if (typeof token === 'object' && token !== null && '__forwardRef__' in token && token.__forwardRef__ === true) {
69
+ if (!('forwardRef' in token) || !isTokenResolver(token.forwardRef)) {
70
+ throw new InvalidProviderError(`Provider inject forwardRef wrapper at index ${String(index)} must expose a callable forwardRef function.`, {
71
+ token: providerToken
72
+ });
73
+ }
74
+ return Object.freeze({
75
+ __forwardRef__: true,
76
+ forwardRef: token.forwardRef
77
+ });
78
+ }
79
+ if (typeof token === 'object' && token !== null && '__optional__' in token && token.__optional__ === true) {
80
+ if (!('token' in token) || !isToken(token.token)) {
81
+ throw new InvalidProviderError(`Provider inject optional wrapper at index ${String(index)} must contain a valid token.`, {
82
+ token: providerToken
83
+ });
84
+ }
85
+ return Object.freeze({
86
+ __optional__: true,
87
+ token: token.token
88
+ });
89
+ }
90
+ if (isToken(token)) {
91
+ return token;
92
+ }
93
+ throw new InvalidProviderError(`Provider inject entry at index ${String(index)} must be a string, symbol, class, forwardRef(), or optional() token wrapper.`, {
94
+ token: providerToken,
95
+ hint: 'Check that every dependency token is defined, and use forwardRef() for declaration-order cycles.'
96
+ });
97
+ }
98
+ function normalizeInject(inject, providerToken) {
99
+ if (inject === undefined) {
100
+ return [];
101
+ }
102
+ if (!Array.isArray(inject)) {
103
+ throw new InvalidProviderError('Provider inject must be an array.', {
104
+ token: providerToken,
105
+ hint: 'Pass dependency tokens as inject: [DependencyA, DependencyB].'
106
+ });
107
+ }
108
+ return inject.map((token, index) => normalizeInjectToken(token, providerToken, index));
109
+ }
110
+ function freezeNormalizedProvider(provider) {
111
+ return Object.freeze({
112
+ ...provider,
113
+ inject: Object.freeze([...provider.inject])
114
+ });
115
+ }
116
+
117
+ /**
118
+ * Validates and snapshots a public provider declaration for container registration.
119
+ *
120
+ * @param provider Provider declaration crossing the container registration boundary.
121
+ * @returns An immutable provider record with normalized injection and scope fields.
122
+ */
123
+ export function normalizeProvider(provider) {
124
+ if (isClassConstructor(provider)) {
125
+ const metadata = getClassDiMetadata(provider);
126
+ return freezeNormalizedProvider({
127
+ inject: normalizeInject(metadata?.inject, provider),
128
+ provide: provider,
129
+ scope: normalizeProviderScope(metadata?.scope, provider) ?? Scope.DEFAULT,
130
+ type: 'class',
131
+ useClass: provider
132
+ });
133
+ }
134
+ if (!isProviderObject(provider)) {
135
+ throw new InvalidProviderError('Unsupported provider type.');
136
+ }
137
+ const objectProvider = provider;
138
+ assertObjectProvider(objectProvider);
139
+ const explicitScope = normalizeProviderScope(objectProvider.scope, objectProvider.provide);
140
+ if ('useValue' in objectProvider) {
141
+ return freezeNormalizedProvider({
142
+ inject: [],
143
+ multi: objectProvider.multi,
144
+ provide: objectProvider.provide,
145
+ scope: Scope.DEFAULT,
146
+ type: 'value',
147
+ useValue: objectProvider.useValue
148
+ });
149
+ }
150
+ if ('useFactory' in objectProvider) {
151
+ if (!isFactoryFunction(objectProvider.useFactory)) {
152
+ throw new InvalidProviderError('Factory provider useFactory must be a function.', {
153
+ token: objectProvider.provide
154
+ });
155
+ }
156
+ const metadata = objectProvider.resolverClass ? getClassDiMetadata(objectProvider.resolverClass) : undefined;
157
+ return freezeNormalizedProvider({
158
+ inject: normalizeInject(objectProvider.inject, objectProvider.provide),
159
+ multi: objectProvider.multi,
160
+ provide: objectProvider.provide,
161
+ scope: explicitScope ?? normalizeProviderScope(metadata?.scope, objectProvider.provide) ?? Scope.DEFAULT,
162
+ type: 'factory',
163
+ useFactory: objectProvider.useFactory
164
+ });
165
+ }
166
+ if ('useClass' in objectProvider) {
167
+ if (!isClassType(objectProvider.useClass)) {
168
+ throw new InvalidProviderError('Class provider useClass must be a constructor.', {
169
+ token: objectProvider.provide
170
+ });
171
+ }
172
+ const metadata = getClassDiMetadata(objectProvider.useClass);
173
+ return freezeNormalizedProvider({
174
+ inject: normalizeInject(objectProvider.inject === undefined ? metadata?.inject : objectProvider.inject, objectProvider.provide),
175
+ multi: objectProvider.multi,
176
+ provide: objectProvider.provide,
177
+ scope: explicitScope ?? normalizeProviderScope(metadata?.scope, objectProvider.provide) ?? Scope.DEFAULT,
178
+ type: 'class',
179
+ useClass: objectProvider.useClass
180
+ });
181
+ }
182
+ if ('useExisting' in objectProvider) {
183
+ if (objectProvider.useExisting == null) {
184
+ throw new InvalidProviderError('Alias provider useExisting must be a non-null token.', {
185
+ token: objectProvider.provide
186
+ });
187
+ }
188
+ return freezeNormalizedProvider({
189
+ inject: [],
190
+ provide: objectProvider.provide,
191
+ scope: Scope.DEFAULT,
192
+ type: 'existing',
193
+ useExisting: objectProvider.useExisting
194
+ });
195
+ }
196
+ throw new InvalidProviderError('Provider object must declare exactly one of useValue, useFactory, useClass, or useExisting.');
197
+ }
198
+
199
+ /**
200
+ * Validates provider declarations through the same normalization path used by `Container` registration.
201
+ *
202
+ * @param providers Provider declarations crossing an internal framework integration boundary.
203
+ * @returns The original provider list after every declaration has passed canonical normalization.
204
+ * @internal
205
+ */
206
+ export function validateProviderInputs(providers) {
207
+ for (const provider of providers) {
208
+ normalizeProvider(provider);
209
+ }
210
+ return providers;
211
+ }
package/dist/types.d.ts CHANGED
@@ -43,6 +43,7 @@ export interface FactoryProvider<T = unknown> {
43
43
  inject?: InjectionToken[];
44
44
  scope?: Scope;
45
45
  multi?: boolean;
46
+ /** Class metadata source used when the factory should inherit `@Scope(...)` metadata. */
46
47
  resolverClass?: ClassType;
47
48
  }
48
49
  /**
@@ -94,15 +95,15 @@ export interface RequestScopeContainer {
94
95
  * {@link ValueProvider}, or {@link ExistingProvider}. The container owns construction of normalized records.
95
96
  */
96
97
  export interface NormalizedProvider<T = unknown> {
97
- inject: InjectionToken[];
98
- provide: Token<T>;
99
- scope: Scope;
100
- type: 'class' | 'factory' | 'value' | 'existing';
101
- useClass?: ClassType<T>;
102
- useFactory?: (...deps: unknown[]) => MaybePromise<T>;
103
- useValue?: T;
104
- useExisting?: Token;
105
- multi?: boolean;
98
+ readonly inject: readonly InjectionToken[];
99
+ readonly provide: Token<T>;
100
+ readonly scope: Scope;
101
+ readonly type: 'class' | 'factory' | 'value' | 'existing';
102
+ readonly useClass?: ClassType<T>;
103
+ readonly useFactory?: (...deps: unknown[]) => MaybePromise<T>;
104
+ readonly useValue?: T;
105
+ readonly useExisting?: Token;
106
+ readonly multi?: boolean;
106
107
  }
107
108
  /**
108
109
  * Wraps a token factory so DI metadata can defer token lookup until resolution time.
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
package/dist/types.js CHANGED
@@ -63,10 +63,10 @@ export let Scope;
63
63
  * ```
64
64
  */
65
65
  export function forwardRef(fn) {
66
- return {
66
+ return Object.freeze({
67
67
  __forwardRef__: true,
68
68
  forwardRef: fn
69
- };
69
+ });
70
70
  }
71
71
 
72
72
  /**
@@ -86,10 +86,10 @@ export function isForwardRef(value) {
86
86
  * @returns An optional-token wrapper understood by container resolution.
87
87
  */
88
88
  export function optional(token) {
89
- return {
89
+ return Object.freeze({
90
90
  __optional__: true,
91
91
  token
92
- };
92
+ });
93
93
  }
94
94
 
95
95
  /**
package/package.json CHANGED
@@ -9,7 +9,7 @@
9
9
  "container",
10
10
  "provider"
11
11
  ],
12
- "version": "1.0.3",
12
+ "version": "2.0.0",
13
13
  "private": false,
14
14
  "license": "MIT",
15
15
  "repository": {
@@ -28,6 +28,10 @@
28
28
  ".": {
29
29
  "types": "./dist/index.d.ts",
30
30
  "import": "./dist/index.js"
31
+ },
32
+ "./internal": {
33
+ "types": "./dist/internal.d.ts",
34
+ "import": "./dist/internal.js"
31
35
  }
32
36
  },
33
37
  "main": "./dist/index.js",
@@ -36,7 +40,7 @@
36
40
  "dist"
37
41
  ],
38
42
  "dependencies": {
39
- "@fluojs/core": "^1.0.3"
43
+ "@fluojs/core": "^1.1.0"
40
44
  },
41
45
  "devDependencies": {
42
46
  "vitest": "^3.2.4"