@fluojs/di 1.1.0 → 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,17 +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
- | `inspectResolutionState()` | cache ownership을 보존해야 하는 testing/tooling helper를 위한 지원 대상 framework-owned container introspection seam을 노출합니다. 애플리케이션 코드는 `has(...)`와 `resolve(...)`를 우선 사용하세요. |
167
- | `createRequestScope()` | 요청 스코프 의존성을 위한 자식 컨테이너를 생성합니다. |
168
- | `has(token)` | 컨테이너나 부모에 토큰이 등록되어 있는지 확인합니다. |
169
- | `hasRequestScopedDependency(token)` | 토큰 해석 시 provider 그래프에 request-scoped 의존성이나 순환이 있어 request-scope 컨테이너가 필요할 수 있는지 확인합니다. |
170
- | `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를 정리합니다. |
171
191
  | `forwardRef(fn)` | 선언 순서 문제를 위해 조회를 지연하는 토큰 래퍼를 반환합니다. 실제 생성자 순환을 해석 가능하게 만들지는 않습니다. |
172
192
  | `isForwardRef(value)` | `forwardRef(...)`가 만든 값인지 확인하는 type guard입니다. 커스텀 provider tooling이 DI token wrapper와 통합될 때 사용할 수 있습니다. |
173
193
  | `optional(token)` | 하나의 의존성을 optional로 표시하는 토큰 래퍼를 반환합니다. 누락된 optional dependency는 `undefined`로 해석됩니다. |
@@ -176,8 +196,9 @@ const service = await container.resolve(DataService);
176
196
  | Provider types | `Provider`, `ClassProvider`, `FactoryProvider`, `ValueProvider`, `ExistingProvider`는 `register(...)`와 `override(...)`가 받는 공개 registration shape를 설명합니다. |
177
197
  | Token wrapper types | `ForwardRefFn`과 `OptionalToken`은 `forwardRef(...)`와 `optional(...)`이 반환하는 wrapper 값을 설명합니다. |
178
198
  | Container helper types | `ClassType`, `Disposable`, `RequestScopeContainer`는 typed provider 선언, teardown hook, request-scope helper 경계를 지원합니다. |
179
- | `ContainerResolutionState` | framework testing/tooling integration을 위해 `inspectResolutionState()`가 반환하는 공개 introspection record입니다. |
199
+ | Container introspection helper types | `ContainerResolutionState`, `ContainerResolutionCacheOwner`, `ContainerFactoryResolutionState`는 `inspectResolutionState()`가 반환하는 read-only graph/cache view와 controlled cache adoption helper를 설명합니다. |
180
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를 등록해야 합니다. |
181
202
  | `DiErrorContext` | DI error에 붙는 구조화된 context입니다. 로그와 테스트가 token, scope, module, dependency chain, hint를 검사할 수 있게 합니다. |
182
203
  | 에러 클래스 | `InvalidProviderError`, `ContainerResolutionError`, `RequestScopeResolutionError`, `ScopeMismatchError`, `CircularDependencyError`, `DuplicateProviderError`. |
183
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,17 +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
- | `inspectResolutionState()` | Exposes the supported framework-owned container introspection seam for testing/tooling helpers that must preserve cache ownership. Prefer `has(...)` and `resolve(...)` for application code. |
167
- | `createRequestScope()` | Creates a child container for request-scoped dependencies. |
168
- | `has(token)` | Checks if a token is registered in the container or its parents. |
169
- | `hasRequestScopedDependency(token)` | Checks whether resolving a token may require a request-scope container because its provider graph contains request-scoped dependencies or is cyclic. |
170
- | `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. |
171
191
  | `forwardRef(fn)` | Returns a token wrapper that defers lookup for declaration-order issues; it does not make constructor dependency cycles resolvable. |
172
192
  | `isForwardRef(value)` | Type guard for values produced by `forwardRef(...)`; useful when integrating custom provider tooling with DI token wrappers. |
173
193
  | `optional(token)` | Returns a token wrapper that marks one dependency as optional; missing optional dependencies resolve to `undefined`. |
@@ -176,8 +196,9 @@ Ensure all required providers are registered in the container. If you use `creat
176
196
  | Provider types | `Provider`, `ClassProvider`, `FactoryProvider`, `ValueProvider`, and `ExistingProvider` describe the public registration shapes accepted by `register(...)` and `override(...)`. |
177
197
  | Token wrapper types | `ForwardRefFn` and `OptionalToken` describe the wrapper values returned by `forwardRef(...)` and `optional(...)`. |
178
198
  | Container helper types | `ClassType`, `Disposable`, and `RequestScopeContainer` support typed provider declarations, teardown hooks, and request-scope helper boundaries. |
179
- | `ContainerResolutionState` | Public introspection record returned by `inspectResolutionState()` for framework testing/tooling integrations. |
199
+ | Container introspection helper types | `ContainerResolutionState`, `ContainerResolutionCacheOwner`, and `ContainerFactoryResolutionState` describe the read-only graph/cache views and controlled cache adoption helpers returned by `inspectResolutionState()`. |
180
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`. |
181
202
  | `DiErrorContext` | Structured context attached to DI errors so logs and tests can inspect tokens, scopes, modules, dependency chains, and hints. |
182
203
  | Error classes | `InvalidProviderError`, `ContainerResolutionError`, `RequestScopeResolutionError`, `ScopeMismatchError`, `CircularDependencyError`, `DuplicateProviderError`. |
183
204
 
@@ -1,17 +1,42 @@
1
1
  import { type Token } from '@fluojs/core';
2
2
  import type { NormalizedProvider, Provider } from './types.js';
3
3
  /**
4
- * Public read/write seam for framework-owned testing and tooling that need to
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
5
28
  * inspect a container's resolved provider graph without depending on private
6
29
  * field names or structural casts.
7
30
  */
8
31
  export interface ContainerResolutionState {
32
+ readonly cacheOwner: ContainerResolutionCacheOwner;
33
+ readonly factoryResolutionKinds: ContainerFactoryResolutionState;
9
34
  readonly parent?: ContainerResolutionState;
10
- readonly registrations: Map<Token, NormalizedProvider>;
11
- readonly multiRegistrations: Map<Token, NormalizedProvider[]>;
12
- readonly multiSingletonCache: Map<NormalizedProvider, Promise<unknown>>;
35
+ readonly registrations: ReadonlyMap<Token, NormalizedProvider>;
36
+ readonly multiRegistrations: ReadonlyMap<Token, readonly NormalizedProvider[]>;
37
+ readonly multiSingletonCache: ReadonlyMap<NormalizedProvider, Promise<unknown>>;
13
38
  readonly requestScopeEnabled: boolean;
14
- readonly singletonCache: Map<Token, Promise<unknown>>;
39
+ readonly singletonCache: ReadonlyMap<Token, Promise<unknown>>;
15
40
  }
16
41
  /**
17
42
  * Scope-aware dependency injection container for Fluo providers.
@@ -26,9 +51,9 @@ export declare class Container {
26
51
  private multiRequestCache;
27
52
  private readonly multiSingletonCache;
28
53
  private readonly staleDisposalTasks;
29
- private readonly staleDisposalErrors;
30
54
  private readonly singletonCache;
31
55
  private readonly forwardRefTokenCache;
56
+ private readonly factoryResolutionKinds;
32
57
  private readonly providerLookupPlanCache;
33
58
  private readonly multiProviderPlanCache;
34
59
  private readonly requestScopeVerdictPlanCache;
@@ -36,7 +61,7 @@ export declare class Container {
36
61
  private childScopes;
37
62
  private disposePromise;
38
63
  private disposed;
39
- private trackedByRoot;
64
+ private trackedByParent;
40
65
  private graphRevision;
41
66
  constructor(parent?: Container | undefined, requestScopeEnabled?: boolean, singletonCache?: Map<Token, Promise<unknown>>);
42
67
  /**
@@ -77,12 +102,16 @@ export declare class Container {
77
102
  *
78
103
  * This method is the supported introspection seam for packages such as
79
104
  * `@fluojs/testing`; callers should prefer ordinary `has(...)` and
80
- * `resolve(...)` unless they need to preserve container cache ownership while
81
- * implementing a framework-level helper.
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.
82
109
  *
83
- * @returns Provider registrations and resolution caches for this container scope.
110
+ * @returns Read-only provider registrations and resolution caches for this container scope.
84
111
  */
85
112
  inspectResolutionState(): ContainerResolutionState;
113
+ private createCacheOwner;
114
+ private createFactoryResolutionState;
86
115
  /**
87
116
  * Returns whether resolving a token may require a request-scope container.
88
117
  *
@@ -171,7 +200,7 @@ export declare class Container {
171
200
  private writePlanCache;
172
201
  private advanceGraphRevision;
173
202
  private clearResolutionPlanCaches;
174
- private waitForStaleDisposalTasks;
203
+ private assertStaleDisposalsSettled;
175
204
  private scheduleStaleDisposal;
176
205
  private throwDisposalErrors;
177
206
  private collectDisposalError;
@@ -185,7 +214,6 @@ export declare class Container {
185
214
  private resolveForwardRefToken;
186
215
  private resolveProviderDeps;
187
216
  private invalidateAffectedCachedEntriesInHierarchy;
188
- private isAncestorOf;
189
217
  private invalidateAffectedCachedEntries;
190
218
  private shouldInvalidateCachedToken;
191
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,122 +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';
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';
5
6
 
6
7
  /**
7
- * Public read/write seam for framework-owned testing and tooling that need to
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
8
23
  * inspect a container's resolved provider graph without depending on private
9
24
  * field names or structural casts.
10
25
  */
11
26
 
12
- function isClassConstructor(value) {
13
- return typeof value === 'function';
14
- }
15
- function isValueProvider(value) {
16
- return typeof value === 'object' && value !== null && 'useValue' in value;
17
- }
18
- function isFactoryProvider(value) {
19
- return typeof value === 'object' && value !== null && 'useFactory' in value;
20
- }
21
- function isClassProvider(value) {
22
- return typeof value === 'object' && value !== null && 'useClass' in value;
23
- }
24
- function isExistingProvider(value) {
25
- return typeof value === 'object' && value !== null && 'useExisting' in value;
26
- }
27
- function assertProviderToken(provider) {
28
- if (!('provide' in provider) || provider.provide == null) {
29
- throw new InvalidProviderError('Provider object must include a non-null provide token.');
27
+ class ReadonlyMapView {
28
+ #source;
29
+ [Symbol.toStringTag] = 'Map';
30
+ constructor(source) {
31
+ this.#source = source;
30
32
  }
31
- }
32
- function assertProviderStrategy(provider) {
33
- const strategyCount = Number('useValue' in provider) + Number('useFactory' in provider) + Number('useClass' in provider) + Number('useExisting' in provider);
34
- if (strategyCount !== 1) {
35
- throw new InvalidProviderError('Provider object must declare exactly one of useValue, useFactory, useClass, or useExisting.');
33
+ get size() {
34
+ return this.#source.size;
36
35
  }
37
- }
38
- function assertObjectProvider(provider) {
39
- assertProviderToken(provider);
40
- assertProviderStrategy(provider);
41
- }
42
- function normalizeInjectToken(token) {
43
- if (token == null) {
44
- 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();
45
58
  }
46
- return token;
47
59
  }
48
- function normalizeProvider(provider) {
49
- if (isClassConstructor(provider)) {
50
- const metadata = getClassDiMetadata(provider);
51
- return {
52
- inject: (metadata?.inject ?? []).map(normalizeInjectToken),
53
- provide: provider,
54
- scope: metadata?.scope ?? Scope.DEFAULT,
55
- type: 'class',
56
- useClass: provider
57
- };
60
+ class ReadonlyMultiRegistrationMapView {
61
+ #source;
62
+ [Symbol.toStringTag] = 'Map';
63
+ constructor(source) {
64
+ this.#source = source;
58
65
  }
59
- if (isValueProvider(provider)) {
60
- assertObjectProvider(provider);
61
- return {
62
- inject: [],
63
- multi: provider.multi,
64
- provide: provider.provide,
65
- scope: Scope.DEFAULT,
66
- type: 'value',
67
- useValue: provider.useValue
68
- };
66
+ get size() {
67
+ return this.#source.size;
69
68
  }
70
- if (isFactoryProvider(provider)) {
71
- assertObjectProvider(provider);
72
- if (typeof provider.useFactory !== 'function') {
73
- throw new InvalidProviderError('Factory provider useFactory must be a function.', {
74
- token: provider.provide
75
- });
69
+ *entries() {
70
+ for (const [token, providers] of this.#source) {
71
+ yield [token, Object.freeze([...providers])];
76
72
  }
77
- const metadata = provider.resolverClass ? getClassDiMetadata(provider.resolverClass) : undefined;
78
- return {
79
- inject: (provider.inject ?? []).map(normalizeInjectToken),
80
- multi: provider.multi,
81
- provide: provider.provide,
82
- scope: provider.scope ?? metadata?.scope ?? Scope.DEFAULT,
83
- type: 'factory',
84
- useFactory: provider.useFactory
85
- };
86
73
  }
87
- if (isClassProvider(provider)) {
88
- assertObjectProvider(provider);
89
- if (typeof provider.useClass !== 'function') {
90
- throw new InvalidProviderError('Class provider useClass must be a constructor.', {
91
- token: provider.provide
92
- });
74
+ forEach(callbackfn, thisArg) {
75
+ for (const [key, value] of this.entries()) {
76
+ callbackfn.call(thisArg, value, key, this);
93
77
  }
94
- const metadata = getClassDiMetadata(provider.useClass);
95
- return {
96
- inject: (provider.inject ?? metadata?.inject ?? []).map(normalizeInjectToken),
97
- multi: provider.multi,
98
- provide: provider.provide,
99
- scope: provider.scope ?? metadata?.scope ?? Scope.DEFAULT,
100
- type: 'class',
101
- useClass: provider.useClass
102
- };
103
78
  }
104
- if (isExistingProvider(provider)) {
105
- assertObjectProvider(provider);
106
- if (provider.useExisting == null) {
107
- throw new InvalidProviderError('Alias provider useExisting must be a non-null token.', {
108
- token: provider.provide
109
- });
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]);
110
92
  }
111
- return {
112
- inject: [],
113
- provide: provider.provide,
114
- scope: Scope.DEFAULT,
115
- type: 'existing',
116
- useExisting: provider.useExisting
117
- };
118
93
  }
119
- 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';
120
100
  }
121
101
 
122
102
  /**
@@ -130,9 +110,9 @@ export class Container {
130
110
  multiRequestCache;
131
111
  multiSingletonCache = new Map();
132
112
  staleDisposalTasks = new Set();
133
- staleDisposalErrors = [];
134
113
  singletonCache;
135
114
  forwardRefTokenCache = new WeakMap();
115
+ factoryResolutionKinds = new WeakMap();
136
116
  providerLookupPlanCache = new Map();
137
117
  multiProviderPlanCache = new Map();
138
118
  requestScopeVerdictPlanCache = new Map();
@@ -140,7 +120,7 @@ export class Container {
140
120
  childScopes;
141
121
  disposePromise;
142
122
  disposed = false;
143
- trackedByRoot = false;
123
+ trackedByParent = false;
144
124
  graphRevision = 0;
145
125
  constructor(parent, requestScopeEnabled = false, singletonCache) {
146
126
  this.parent = parent;
@@ -263,21 +243,59 @@ export class Container {
263
243
  *
264
244
  * This method is the supported introspection seam for packages such as
265
245
  * `@fluojs/testing`; callers should prefer ordinary `has(...)` and
266
- * `resolve(...)` unless they need to preserve container cache ownership while
267
- * implementing a framework-level helper.
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.
268
250
  *
269
- * @returns Provider registrations and resolution caches for this container scope.
251
+ * @returns Read-only provider registrations and resolution caches for this container scope.
270
252
  */
271
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);
272
258
  return {
259
+ cacheOwner: this.createCacheOwner(singletonCache, multiSingletonCache),
260
+ factoryResolutionKinds: this.createFactoryResolutionState(),
273
261
  parent: this.parent?.inspectResolutionState(),
274
- registrations: this.registrations,
275
- multiRegistrations: this.multiRegistrations,
276
- multiSingletonCache: this.multiSingletonCache,
262
+ registrations: new ReadonlyMapView(registrations),
263
+ multiRegistrations: new ReadonlyMultiRegistrationMapView(multiRegistrations),
264
+ multiSingletonCache: new ReadonlyMapView(multiSingletonCache),
277
265
  requestScopeEnabled: this.requestScopeEnabled,
278
- singletonCache: this.singletonCache
266
+ singletonCache: new ReadonlyMapView(singletonCache)
279
267
  };
280
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
+ }
281
299
 
282
300
  /**
283
301
  * Returns whether resolving a token may require a request-scope container.
@@ -325,6 +343,7 @@ export class Container {
325
343
  hint: 'Ensure all resolves complete before calling container.dispose().'
326
344
  });
327
345
  }
346
+ await this.assertStaleDisposalsSettled();
328
347
  return this.resolveWithChain(token, [], new Set());
329
348
  }
330
349
 
@@ -352,8 +371,8 @@ export class Container {
352
371
  async disposeAll() {
353
372
  const errors = [];
354
373
  try {
355
- // Dispose all live request-scope children first (root only)
356
- 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) {
357
376
  const childResults = await Promise.allSettled(Array.from(this.childScopes).map(child => child.dispose()));
358
377
  for (const result of childResults) {
359
378
  if (result.status === 'rejected') {
@@ -369,9 +388,9 @@ export class Container {
369
388
  }
370
389
  this.throwDisposalErrors(errors);
371
390
  } finally {
372
- if (this.parent && this.trackedByRoot) {
373
- this.root().childScopes?.delete(this);
374
- this.trackedByRoot = false;
391
+ if (this.parent && this.trackedByParent) {
392
+ this.parent.childScopes?.delete(this);
393
+ this.trackedByParent = false;
375
394
  }
376
395
  }
377
396
  }
@@ -617,13 +636,13 @@ export class Container {
617
636
  return this.parent ? this.parent.root() : this;
618
637
  }
619
638
  ensureTrackedRequestScope() {
620
- if (!this.requestScopeEnabled || !this.parent || this.trackedByRoot) {
639
+ if (!this.requestScopeEnabled || !this.parent || this.trackedByParent) {
621
640
  return;
622
641
  }
623
- const root = this.root();
624
- root.childScopes ??= new Set();
625
- root.childScopes.add(this);
626
- this.trackedByRoot = true;
642
+ this.parent.ensureTrackedRequestScope();
643
+ this.parent.childScopes ??= new Set();
644
+ this.parent.childScopes.add(this);
645
+ this.trackedByParent = true;
627
646
  }
628
647
  requestCacheForWrite() {
629
648
  this.ensureTrackedRequestScope();
@@ -706,12 +725,17 @@ export class Container {
706
725
  return entries;
707
726
  }
708
727
  async disposeCache(entries) {
709
- await this.waitForStaleDisposalTasks();
728
+ const errors = [];
729
+ try {
730
+ await this.assertStaleDisposalsSettled();
731
+ } catch (error) {
732
+ this.collectDisposalError(error, errors);
733
+ }
710
734
  const {
711
735
  disposables,
712
- errors
736
+ errors: resolutionErrors
713
737
  } = await this.collectDisposableInstances(entries);
714
- errors.push(...this.staleDisposalErrors.splice(0, this.staleDisposalErrors.length));
738
+ errors.push(...resolutionErrors);
715
739
  errors.push(...(await this.disposeInstancesInReverseOrder(disposables)));
716
740
  this.clearDisposalCaches();
717
741
  this.throwDisposalErrors(errors);
@@ -787,26 +811,49 @@ export class Container {
787
811
  this.requestScopeVerdictPlanCache.clear();
788
812
  this.effectiveProviderPlanCache.clear();
789
813
  }
790
- async waitForStaleDisposalTasks() {
814
+ async assertStaleDisposalsSettled() {
815
+ const errors = [];
791
816
  while (this.staleDisposalTasks.size > 0) {
792
- 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
+ }
793
826
  }
827
+ this.throwDisposalErrors(errors);
794
828
  }
795
- scheduleStaleDisposal(instancePromise) {
796
- let task;
797
- 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 () => {
798
838
  try {
799
839
  const instance = await instancePromise;
800
840
  if (this.isDisposable(instance)) {
801
841
  await instance.onDestroy();
802
842
  }
803
843
  } catch (error) {
804
- this.staleDisposalErrors.push(error);
844
+ task.error = error;
845
+ task.failed = true;
805
846
  }
806
847
  })().finally(() => {
807
- this.staleDisposalTasks.delete(task);
848
+ if (!task.failed) {
849
+ for (const observer of observers) {
850
+ observer.staleDisposalTasks.delete(task);
851
+ }
852
+ }
808
853
  });
809
- this.staleDisposalTasks.add(task);
854
+ for (const observer of observers) {
855
+ observer.staleDisposalTasks.add(task);
856
+ }
810
857
  }
811
858
  throwDisposalErrors(errors) {
812
859
  if (errors.length === 1) {
@@ -839,7 +886,9 @@ export class Container {
839
886
  throw new InvariantError('Factory provider is missing useFactory.');
840
887
  }
841
888
  const deps = await this.resolveProviderDeps(provider, chain, activeTokens);
842
- return provider.useFactory(...deps);
889
+ const value = provider.useFactory(...deps);
890
+ this.root().factoryResolutionKinds.set(provider, isPromiseLike(value) ? 'async' : 'sync');
891
+ return value;
843
892
  }
844
893
  case 'class':
845
894
  {
@@ -959,34 +1008,18 @@ export class Container {
959
1008
  }
960
1009
  return deps;
961
1010
  }
962
- invalidateAffectedCachedEntriesInHierarchy(token) {
963
- this.invalidateAffectedCachedEntries(token);
964
- const childScopes = this.root().childScopes;
965
- if (!childScopes) {
966
- return;
967
- }
968
- for (const childScope of childScopes) {
969
- if (this.isAncestorOf(childScope)) {
970
- childScope.invalidateAffectedCachedEntries(token);
971
- }
972
- }
973
- }
974
- isAncestorOf(container) {
975
- let current = container.parent;
976
- while (current) {
977
- if (current === this) {
978
- return true;
979
- }
980
- 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);
981
1015
  }
982
- return false;
983
1016
  }
984
- invalidateAffectedCachedEntries(token) {
1017
+ invalidateAffectedCachedEntries(token, staleDisposalOwner) {
985
1018
  for (const [cachedToken, cached] of this.requestCache?.entries() ?? []) {
986
1019
  if (!this.shouldInvalidateCachedToken(cachedToken, token)) {
987
1020
  continue;
988
1021
  }
989
- this.scheduleStaleDisposal(cached);
1022
+ this.scheduleStaleDisposal(cached, staleDisposalOwner);
990
1023
  this.requestCache?.delete(cachedToken);
991
1024
  }
992
1025
  if (!this.parent) {
@@ -994,7 +1027,7 @@ export class Container {
994
1027
  if (!this.shouldInvalidateCachedToken(cachedToken, token)) {
995
1028
  continue;
996
1029
  }
997
- this.scheduleStaleDisposal(cached);
1030
+ this.scheduleStaleDisposal(cached, staleDisposalOwner);
998
1031
  this.singletonCache.delete(cachedToken);
999
1032
  }
1000
1033
  }
@@ -1003,7 +1036,7 @@ export class Container {
1003
1036
  if (!this.shouldInvalidateCachedProvider(provider, token)) {
1004
1037
  continue;
1005
1038
  }
1006
- this.scheduleStaleDisposal(cached);
1039
+ this.scheduleStaleDisposal(cached, staleDisposalOwner);
1007
1040
  this.multiSingletonCache.delete(provider);
1008
1041
  }
1009
1042
  }
@@ -1015,7 +1048,7 @@ export class Container {
1015
1048
  if (!this.shouldInvalidateCachedProvider(provider, token)) {
1016
1049
  continue;
1017
1050
  }
1018
- this.scheduleStaleDisposal(cached);
1051
+ this.scheduleStaleDisposal(cached, staleDisposalOwner);
1019
1052
  multiRequestCache.delete(provider);
1020
1053
  }
1021
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.1.0",
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"