@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 +47 -26
- package/README.md +47 -26
- package/dist/container.d.ts +40 -12
- package/dist/container.d.ts.map +1 -1
- package/dist/container.js +189 -156
- package/dist/internal.d.ts +3 -0
- package/dist/internal.d.ts.map +1 -0
- package/dist/internal.js +1 -0
- package/dist/provider-normalization.d.ts +17 -0
- package/dist/provider-normalization.d.ts.map +1 -0
- package/dist/provider-normalization.js +211 -0
- package/dist/types.d.ts +10 -9
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +4 -4
- package/package.json +6 -2
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 중에는
|
|
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
|
|
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`를
|
|
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
|
-
|
|
138
|
-
|
|
139
|
+
class Database {
|
|
140
|
+
async query(): Promise<readonly string[]> {
|
|
141
|
+
return ['real row'];
|
|
142
|
+
}
|
|
143
|
+
}
|
|
139
144
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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
|
-
|
|
147
|
-
|
|
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
|
-
|
|
|
161
|
-
|
|
162
|
-
| `Container` | 메인 DI 컨테이너 클래스입니다. |
|
|
163
|
-
| `register(...providers)` | 하나 이상의 프로바이더를 등록합니다. |
|
|
164
|
-
| `override(...providers)` | 기존 provider를 교체하고 cached instance를 무효화하며 오래된 instance
|
|
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
|
-
|
|
|
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,
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
138
|
-
|
|
139
|
+
class Database {
|
|
140
|
+
async query(): Promise<readonly string[]> {
|
|
141
|
+
return ['real row'];
|
|
142
|
+
}
|
|
143
|
+
}
|
|
139
144
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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
|
-
|
|
147
|
-
|
|
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
|
-
|
|
|
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
|
|
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`
|
|
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
|
|
package/dist/container.d.ts
CHANGED
|
@@ -1,17 +1,42 @@
|
|
|
1
1
|
import { type Token } from '@fluojs/core';
|
|
2
2
|
import type { NormalizedProvider, Provider } from './types.js';
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
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:
|
|
11
|
-
readonly multiRegistrations:
|
|
12
|
-
readonly multiSingletonCache:
|
|
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:
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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;
|
package/dist/container.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"container.d.ts","sourceRoot":"","sources":["../src/container.ts"],"names":[],"mappings":"AAAA,OAAO,EAAmC,KAAK,KAAK,EAAE,MAAM,cAAc,CAAC;AAW3E,OAAO,KAAK,
|
|
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 {
|
|
1
|
+
import { formatTokenName, InvariantError } from '@fluojs/core';
|
|
2
2
|
import { getClassDiMetadata } from '@fluojs/core/internal';
|
|
3
|
-
import { CircularDependencyError, ContainerResolutionError, DuplicateProviderError,
|
|
4
|
-
import {
|
|
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
|
-
*
|
|
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
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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
|
-
|
|
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
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
-
|
|
60
|
-
|
|
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
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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:
|
|
275
|
-
multiRegistrations:
|
|
276
|
-
multiSingletonCache:
|
|
262
|
+
registrations: new ReadonlyMapView(registrations),
|
|
263
|
+
multiRegistrations: new ReadonlyMultiRegistrationMapView(multiRegistrations),
|
|
264
|
+
multiSingletonCache: new ReadonlyMapView(multiSingletonCache),
|
|
277
265
|
requestScopeEnabled: this.requestScopeEnabled,
|
|
278
|
-
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
|
|
356
|
-
if (
|
|
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.
|
|
373
|
-
this.
|
|
374
|
-
this.
|
|
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.
|
|
639
|
+
if (!this.requestScopeEnabled || !this.parent || this.trackedByParent) {
|
|
621
640
|
return;
|
|
622
641
|
}
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
this.
|
|
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
|
-
|
|
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(...
|
|
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
|
|
814
|
+
async assertStaleDisposalsSettled() {
|
|
815
|
+
const errors = [];
|
|
791
816
|
while (this.staleDisposalTasks.size > 0) {
|
|
792
|
-
|
|
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
|
-
|
|
797
|
-
task =
|
|
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
|
-
|
|
844
|
+
task.error = error;
|
|
845
|
+
task.failed = true;
|
|
805
846
|
}
|
|
806
847
|
})().finally(() => {
|
|
807
|
-
|
|
848
|
+
if (!task.failed) {
|
|
849
|
+
for (const observer of observers) {
|
|
850
|
+
observer.staleDisposalTasks.delete(task);
|
|
851
|
+
}
|
|
852
|
+
}
|
|
808
853
|
});
|
|
809
|
-
|
|
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
|
-
|
|
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
|
|
965
|
-
|
|
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 @@
|
|
|
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"}
|
package/dist/internal.js
ADDED
|
@@ -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.
|
package/dist/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,eAAe,EAAE,cAAc,EAAE,YAAY,EAAE,mBAAmB,EAAE,KAAK,EAAE,MAAM,cAAc,CAAC;AAE3H;;GAEG;AACH,MAAM,MAAM,KAAK,GAAG,WAAW,GAAG,SAAS,GAAG,WAAW,CAAC;AAE1D;;GAEG;AACH,yBAAiB,KAAK,CAAC;IACrB;;OAEG;IACI,MAAM,OAAO,EAAE,KAAmB,CAAC;IAE1C;;OAEG;IACI,MAAM,OAAO,EAAE,KAAiB,CAAC;IAExC;;OAEG;IACI,MAAM,SAAS,EAAE,KAAmB,CAAC;CAC7C;AAED;;GAEG;AACH,MAAM,MAAM,SAAS,CAAC,CAAC,GAAG,OAAO,IAAI,WAAW,CAAC,CAAC,CAAC,GAAG,QAAQ,CAAC;AAE/D;;GAEG;AACH,MAAM,WAAW,aAAa,CAAC,CAAC,GAAG,OAAO;IACxC,OAAO,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC;IAClB,QAAQ,EAAE,SAAS,CAAC,CAAC,CAAC,CAAC;IACvB,MAAM,CAAC,EAAE,cAAc,EAAE,CAAC;IAC1B,KAAK,CAAC,EAAE,KAAK,CAAC;IACd,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAED;;GAEG;AACH,MAAM,WAAW,eAAe,CAAC,CAAC,GAAG,OAAO;IAC1C,OAAO,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC;IAClB,UAAU,EAAE,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,YAAY,CAAC,CAAC,CAAC,CAAC;IACpD,MAAM,CAAC,EAAE,cAAc,EAAE,CAAC;IAC1B,KAAK,CAAC,EAAE,KAAK,CAAC;IACd,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,aAAa,CAAC,EAAE,SAAS,CAAC;CAC3B;AAED;;GAEG;AACH,MAAM,WAAW,aAAa,CAAC,CAAC,GAAG,OAAO;IACxC,OAAO,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC;IAClB,QAAQ,EAAE,CAAC,CAAC;IACZ,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAED;;GAEG;AACH,MAAM,WAAW,gBAAgB,CAAC,CAAC,GAAG,OAAO;IAC3C,OAAO,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC;IAClB,WAAW,EAAE,KAAK,CAAC;CACpB;AAED;;GAEG;AACH,MAAM,MAAM,YAAY,CAAC,CAAC,GAAG,OAAO,IAAI,eAAe,CAAC,CAAC,CAAC,CAAC;AAE3D;;GAEG;AACH,MAAM,MAAM,aAAa,CAAC,CAAC,GAAG,OAAO,IAAI,mBAAmB,CAAC,CAAC,CAAC,CAAC;AAEhE;;GAEG;AACH,MAAM,MAAM,QAAQ,CAAC,CAAC,GAAG,OAAO,IAC5B,SAAS,CAAC,CAAC,CAAC,GACZ,aAAa,CAAC,CAAC,CAAC,GAChB,eAAe,CAAC,CAAC,CAAC,GAClB,aAAa,CAAC,CAAC,CAAC,GAChB,gBAAgB,CAAC,CAAC,CAAC,CAAC;AAExB;;GAEG;AACH,MAAM,WAAW,UAAU;IACzB,SAAS,IAAI,YAAY,CAAC,IAAI,CAAC,CAAC;CACjC;AAED;;GAEG;AACH,MAAM,WAAW,qBAAqB;IACpC,OAAO,CAAC,CAAC,EAAE,KAAK,EAAE,KAAK,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;IACxC,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CAC1B;AAED;;;;;;;GAOG;AACH,MAAM,WAAW,kBAAkB,CAAC,CAAC,GAAG,OAAO;IAC7C,MAAM,EAAE,cAAc,EAAE,CAAC;
|
|
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": "
|
|
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
|
|
43
|
+
"@fluojs/core": "^1.1.0"
|
|
40
44
|
},
|
|
41
45
|
"devDependencies": {
|
|
42
46
|
"vitest": "^3.2.4"
|