@fluojs/di 1.0.3 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.ko.md +47 -24
- package/README.md +47 -24
- package/dist/container.d.ts +57 -5
- package/dist/container.d.ts.map +1 -1
- package/dist/container.js +209 -148
- 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,16 +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
|
-
| `
|
|
167
|
-
| `
|
|
168
|
-
| `
|
|
169
|
-
| `
|
|
180
|
+
| Surface | 종류 | 설명 |
|
|
181
|
+
|---|---|---|
|
|
182
|
+
| `Container` | Root export | 메인 DI 컨테이너 클래스입니다. |
|
|
183
|
+
| `container.register(...providers)` | `Container` instance method | 하나 이상의 프로바이더를 등록합니다. |
|
|
184
|
+
| `container.override(...providers)` | `Container` instance method | 기존 provider를 교체하고 cached instance를 무효화하며 다음 replacement resolution이 계속되기 전에 오래된 instance dispose가 settle되도록 보장합니다. |
|
|
185
|
+
| `container.resolve<T>(token)` | `Container` instance method | 토큰을 인스턴스로 비동기 해석합니다. |
|
|
186
|
+
| `container.inspectResolutionState()` | `Container` instance method | snapshot read-only map view, frozen provider record, controlled cache adoption을 통해 cache ownership을 보존해야 하는 testing/tooling helper를 위한 지원 대상 framework-owned container introspection seam을 노출합니다. 애플리케이션 코드는 `has(...)`와 `resolve(...)`를 우선 사용하세요. |
|
|
187
|
+
| `container.createRequestScope()` | `Container` instance method | 요청 스코프 의존성을 위한 자식 컨테이너를 생성합니다. |
|
|
188
|
+
| `container.has(token)` | `Container` instance method | 컨테이너나 부모에 토큰이 등록되어 있는지 확인합니다. |
|
|
189
|
+
| `container.hasRequestScopedDependency(token)` | `Container` instance method | 토큰 해석 시 provider 그래프에 request-scoped 의존성이나 순환이 있어 request-scope 컨테이너가 필요할 수 있는지 확인합니다. |
|
|
190
|
+
| `container.dispose()` | `Container` instance method | request child와 루트가 소유한 singleton instance를 정리합니다. |
|
|
170
191
|
| `forwardRef(fn)` | 선언 순서 문제를 위해 조회를 지연하는 토큰 래퍼를 반환합니다. 실제 생성자 순환을 해석 가능하게 만들지는 않습니다. |
|
|
171
192
|
| `isForwardRef(value)` | `forwardRef(...)`가 만든 값인지 확인하는 type guard입니다. 커스텀 provider tooling이 DI token wrapper와 통합될 때 사용할 수 있습니다. |
|
|
172
193
|
| `optional(token)` | 하나의 의존성을 optional로 표시하는 토큰 래퍼를 반환합니다. 누락된 optional dependency는 `undefined`로 해석됩니다. |
|
|
@@ -175,7 +196,9 @@ const service = await container.resolve(DataService);
|
|
|
175
196
|
| Provider types | `Provider`, `ClassProvider`, `FactoryProvider`, `ValueProvider`, `ExistingProvider`는 `register(...)`와 `override(...)`가 받는 공개 registration shape를 설명합니다. |
|
|
176
197
|
| Token wrapper types | `ForwardRefFn`과 `OptionalToken`은 `forwardRef(...)`와 `optional(...)`이 반환하는 wrapper 값을 설명합니다. |
|
|
177
198
|
| Container helper types | `ClassType`, `Disposable`, `RequestScopeContainer`는 typed provider 선언, teardown hook, request-scope helper 경계를 지원합니다. |
|
|
199
|
+
| Container introspection helper types | `ContainerResolutionState`, `ContainerResolutionCacheOwner`, `ContainerFactoryResolutionState`는 `inspectResolutionState()`가 반환하는 read-only graph/cache view와 controlled cache adoption helper를 설명합니다. |
|
|
178
200
|
| `NormalizedProvider` | 컨테이너가 검증한 provider record shape를 위한 compatibility-only 공개 타입입니다. provider를 작성할 때는 `Provider`나 구체 provider interface를 우선 사용하세요. normalized record 생성은 컨테이너가 소유합니다. |
|
|
201
|
+
| `@fluojs/di/internal` | sibling fluo package가 자체 순회 전에 컨테이너의 canonical provider validation을 적용할 수 있도록 `validateProviderInputs(...)`를 노출하는 package-integration seam입니다. 애플리케이션 코드는 계속 `Container`를 통해 provider를 등록해야 합니다. |
|
|
179
202
|
| `DiErrorContext` | DI error에 붙는 구조화된 context입니다. 로그와 테스트가 token, scope, module, dependency chain, hint를 검사할 수 있게 합니다. |
|
|
180
203
|
| 에러 클래스 | `InvalidProviderError`, `ContainerResolutionError`, `RequestScopeResolutionError`, `ScopeMismatchError`, `CircularDependencyError`, `DuplicateProviderError`. |
|
|
181
204
|
|
package/README.md
CHANGED
|
@@ -67,7 +67,7 @@ const result = await service.getStatus();
|
|
|
67
67
|
fluo DI supports four provider shapes:
|
|
68
68
|
- **Class Providers**: `container.register(MyService)` or `{ provide: MyToken, useClass: MyService }`.
|
|
69
69
|
- **Value Providers**: `{ provide: 'API_URL', useValue: 'https://api.example.com' }`.
|
|
70
|
-
- **Factory Providers**: `{ provide: 'ASYNC_CONFIG', useFactory: async (db) => await db.load(), inject: [Database] }`.
|
|
70
|
+
- **Factory Providers**: `{ provide: 'ASYNC_CONFIG', useFactory: async (db) => await db.load(), inject: [Database] }`. Add `resolverClass` when the factory should inherit the referenced class's DI metadata, such as `@Scope(...)`, unless an explicit provider `scope` is set.
|
|
71
71
|
- **Alias Providers**: `{ provide: ILogger, useExisting: PinoLogger }` allows mapping one token to another existing provider.
|
|
72
72
|
|
|
73
73
|
### Scope Management
|
|
@@ -75,11 +75,11 @@ fluo DI supports four provider shapes:
|
|
|
75
75
|
- **Request**: Instance is created once per `createRequestScope()` call.
|
|
76
76
|
- **Transient**: A new instance is created every time it is resolved.
|
|
77
77
|
|
|
78
|
-
During disposal,
|
|
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,16 +177,17 @@ Ensure all required providers are registered in the container. If you use `creat
|
|
|
157
177
|
|
|
158
178
|
## Public API
|
|
159
179
|
|
|
160
|
-
|
|
|
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
|
-
| `
|
|
167
|
-
| `
|
|
168
|
-
| `
|
|
169
|
-
| `
|
|
180
|
+
| Surface | Kind | Description |
|
|
181
|
+
|---|---|---|
|
|
182
|
+
| `Container` | Root export | The main DI container class. |
|
|
183
|
+
| `container.register(...providers)` | `Container` instance method | Registers one or more providers. |
|
|
184
|
+
| `container.override(...providers)` | `Container` instance method | Replaces existing providers, invalidates cached instances, and ensures stale instance disposal settles before the next replacement resolution continues. |
|
|
185
|
+
| `container.resolve<T>(token)` | `Container` instance method | Asynchronously resolves a token to an instance. |
|
|
186
|
+
| `container.inspectResolutionState()` | `Container` instance method | Exposes the supported framework-owned container introspection seam for testing/tooling helpers that must preserve cache ownership through snapshot read-only map views, frozen provider records, and controlled cache adoption. Prefer `has(...)` and `resolve(...)` for application code. |
|
|
187
|
+
| `container.createRequestScope()` | `Container` instance method | Creates a child container for request-scoped dependencies. |
|
|
188
|
+
| `container.has(token)` | `Container` instance method | Checks if a token is registered in the container or its parents. |
|
|
189
|
+
| `container.hasRequestScopedDependency(token)` | `Container` instance method | Checks whether resolving a token may require a request-scope container because its provider graph contains request-scoped dependencies or is cyclic. |
|
|
190
|
+
| `container.dispose()` | `Container` instance method | Disposes request children and root-owned singleton instances. |
|
|
170
191
|
| `forwardRef(fn)` | Returns a token wrapper that defers lookup for declaration-order issues; it does not make constructor dependency cycles resolvable. |
|
|
171
192
|
| `isForwardRef(value)` | Type guard for values produced by `forwardRef(...)`; useful when integrating custom provider tooling with DI token wrappers. |
|
|
172
193
|
| `optional(token)` | Returns a token wrapper that marks one dependency as optional; missing optional dependencies resolve to `undefined`. |
|
|
@@ -175,7 +196,9 @@ Ensure all required providers are registered in the container. If you use `creat
|
|
|
175
196
|
| Provider types | `Provider`, `ClassProvider`, `FactoryProvider`, `ValueProvider`, and `ExistingProvider` describe the public registration shapes accepted by `register(...)` and `override(...)`. |
|
|
176
197
|
| Token wrapper types | `ForwardRefFn` and `OptionalToken` describe the wrapper values returned by `forwardRef(...)` and `optional(...)`. |
|
|
177
198
|
| Container helper types | `ClassType`, `Disposable`, and `RequestScopeContainer` support typed provider declarations, teardown hooks, and request-scope helper boundaries. |
|
|
199
|
+
| Container introspection helper types | `ContainerResolutionState`, `ContainerResolutionCacheOwner`, and `ContainerFactoryResolutionState` describe the read-only graph/cache views and controlled cache adoption helpers returned by `inspectResolutionState()`. |
|
|
178
200
|
| `NormalizedProvider` | Compatibility-only public type for the container's validated provider record shape. Prefer authoring providers with `Provider` or the specific provider interfaces; the container owns normalized record construction. |
|
|
201
|
+
| `@fluojs/di/internal` | Package-integration seam exposing `validateProviderInputs(...)` so sibling fluo packages can apply the container's canonical provider validation before their own traversal. Application code should continue to register providers through `Container`. |
|
|
179
202
|
| `DiErrorContext` | Structured context attached to DI errors so logs and tests can inspect tokens, scopes, modules, dependency chains, and hints. |
|
|
180
203
|
| Error classes | `InvalidProviderError`, `ContainerResolutionError`, `RequestScopeResolutionError`, `ScopeMismatchError`, `CircularDependencyError`, `DuplicateProviderError`. |
|
|
181
204
|
|
package/dist/container.d.ts
CHANGED
|
@@ -1,5 +1,43 @@
|
|
|
1
1
|
import { type Token } from '@fluojs/core';
|
|
2
|
-
import type { Provider } from './types.js';
|
|
2
|
+
import type { NormalizedProvider, Provider } from './types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Factory provider resolution mode recorded after a factory returns either synchronously or through a promise.
|
|
5
|
+
*/
|
|
6
|
+
export type FactoryResolutionKind = 'async' | 'sync';
|
|
7
|
+
/**
|
|
8
|
+
* Controlled cache adoption seam for framework-owned testing and tooling that
|
|
9
|
+
* need synchronous helpers to preserve container-owned singleton disposal.
|
|
10
|
+
*/
|
|
11
|
+
export interface ContainerResolutionCacheOwner {
|
|
12
|
+
readonly deleteMultiSingleton: (provider: NormalizedProvider) => void;
|
|
13
|
+
readonly deleteSingleton: (token: Token) => void;
|
|
14
|
+
readonly recordFactoryResolution: (provider: NormalizedProvider, kind: FactoryResolutionKind) => void;
|
|
15
|
+
readonly setMultiSingleton: (provider: NormalizedProvider, promise: Promise<unknown>) => void;
|
|
16
|
+
readonly setSingleton: (token: Token, promise: Promise<unknown>) => void;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Read-only factory resolution diagnostics recorded by container-owned factory
|
|
20
|
+
* instantiation paths.
|
|
21
|
+
*/
|
|
22
|
+
export interface ContainerFactoryResolutionState {
|
|
23
|
+
readonly get: (provider: NormalizedProvider) => FactoryResolutionKind | undefined;
|
|
24
|
+
readonly has: (provider: NormalizedProvider) => boolean;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Public read-only seam for framework-owned testing and tooling that need to
|
|
28
|
+
* inspect a container's resolved provider graph without depending on private
|
|
29
|
+
* field names or structural casts.
|
|
30
|
+
*/
|
|
31
|
+
export interface ContainerResolutionState {
|
|
32
|
+
readonly cacheOwner: ContainerResolutionCacheOwner;
|
|
33
|
+
readonly factoryResolutionKinds: ContainerFactoryResolutionState;
|
|
34
|
+
readonly parent?: ContainerResolutionState;
|
|
35
|
+
readonly registrations: ReadonlyMap<Token, NormalizedProvider>;
|
|
36
|
+
readonly multiRegistrations: ReadonlyMap<Token, readonly NormalizedProvider[]>;
|
|
37
|
+
readonly multiSingletonCache: ReadonlyMap<NormalizedProvider, Promise<unknown>>;
|
|
38
|
+
readonly requestScopeEnabled: boolean;
|
|
39
|
+
readonly singletonCache: ReadonlyMap<Token, Promise<unknown>>;
|
|
40
|
+
}
|
|
3
41
|
/**
|
|
4
42
|
* Scope-aware dependency injection container for Fluo providers.
|
|
5
43
|
*/
|
|
@@ -13,9 +51,9 @@ export declare class Container {
|
|
|
13
51
|
private multiRequestCache;
|
|
14
52
|
private readonly multiSingletonCache;
|
|
15
53
|
private readonly staleDisposalTasks;
|
|
16
|
-
private readonly staleDisposalErrors;
|
|
17
54
|
private readonly singletonCache;
|
|
18
55
|
private readonly forwardRefTokenCache;
|
|
56
|
+
private readonly factoryResolutionKinds;
|
|
19
57
|
private readonly providerLookupPlanCache;
|
|
20
58
|
private readonly multiProviderPlanCache;
|
|
21
59
|
private readonly requestScopeVerdictPlanCache;
|
|
@@ -23,7 +61,7 @@ export declare class Container {
|
|
|
23
61
|
private childScopes;
|
|
24
62
|
private disposePromise;
|
|
25
63
|
private disposed;
|
|
26
|
-
private
|
|
64
|
+
private trackedByParent;
|
|
27
65
|
private graphRevision;
|
|
28
66
|
constructor(parent?: Container | undefined, requestScopeEnabled?: boolean, singletonCache?: Map<Token, Promise<unknown>>);
|
|
29
67
|
/**
|
|
@@ -59,6 +97,21 @@ export declare class Container {
|
|
|
59
97
|
* @returns `true` when a single or multi provider exists for the token.
|
|
60
98
|
*/
|
|
61
99
|
has(token: Token): boolean;
|
|
100
|
+
/**
|
|
101
|
+
* Returns the framework-owned resolution state for testing/tooling adapters.
|
|
102
|
+
*
|
|
103
|
+
* This method is the supported introspection seam for packages such as
|
|
104
|
+
* `@fluojs/testing`; callers should prefer ordinary `has(...)` and
|
|
105
|
+
* `resolve(...)` unless they need read-only graph/cache visibility while
|
|
106
|
+
* implementing a framework-level helper. Cache adoption for synchronous
|
|
107
|
+
* helpers goes through `cacheOwner`; the returned maps are not mutable
|
|
108
|
+
* container internals.
|
|
109
|
+
*
|
|
110
|
+
* @returns Read-only provider registrations and resolution caches for this container scope.
|
|
111
|
+
*/
|
|
112
|
+
inspectResolutionState(): ContainerResolutionState;
|
|
113
|
+
private createCacheOwner;
|
|
114
|
+
private createFactoryResolutionState;
|
|
62
115
|
/**
|
|
63
116
|
* Returns whether resolving a token may require a request-scope container.
|
|
64
117
|
*
|
|
@@ -147,7 +200,7 @@ export declare class Container {
|
|
|
147
200
|
private writePlanCache;
|
|
148
201
|
private advanceGraphRevision;
|
|
149
202
|
private clearResolutionPlanCaches;
|
|
150
|
-
private
|
|
203
|
+
private assertStaleDisposalsSettled;
|
|
151
204
|
private scheduleStaleDisposal;
|
|
152
205
|
private throwDisposalErrors;
|
|
153
206
|
private collectDisposalError;
|
|
@@ -161,7 +214,6 @@ export declare class Container {
|
|
|
161
214
|
private resolveForwardRefToken;
|
|
162
215
|
private resolveProviderDeps;
|
|
163
216
|
private invalidateAffectedCachedEntriesInHierarchy;
|
|
164
|
-
private isAncestorOf;
|
|
165
217
|
private invalidateAffectedCachedEntries;
|
|
166
218
|
private shouldInvalidateCachedToken;
|
|
167
219
|
private shouldInvalidateCachedProvider;
|
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,115 +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 {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
3
|
+
import { CircularDependencyError, ContainerResolutionError, DuplicateProviderError, RequestScopeResolutionError, ScopeMismatchError } from './errors.js';
|
|
4
|
+
import { normalizeProvider } from './provider-normalization.js';
|
|
5
|
+
import { isForwardRef, isOptionalToken, Scope } from './types.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Factory provider resolution mode recorded after a factory returns either synchronously or through a promise.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Controlled cache adoption seam for framework-owned testing and tooling that
|
|
13
|
+
* need synchronous helpers to preserve container-owned singleton disposal.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Read-only factory resolution diagnostics recorded by container-owned factory
|
|
18
|
+
* instantiation paths.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Public read-only seam for framework-owned testing and tooling that need to
|
|
23
|
+
* inspect a container's resolved provider graph without depending on private
|
|
24
|
+
* field names or structural casts.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
class ReadonlyMapView {
|
|
28
|
+
#source;
|
|
29
|
+
[Symbol.toStringTag] = 'Map';
|
|
30
|
+
constructor(source) {
|
|
31
|
+
this.#source = source;
|
|
23
32
|
}
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
const strategyCount = Number('useValue' in provider) + Number('useFactory' in provider) + Number('useClass' in provider) + Number('useExisting' in provider);
|
|
27
|
-
if (strategyCount !== 1) {
|
|
28
|
-
throw new InvalidProviderError('Provider object must declare exactly one of useValue, useFactory, useClass, or useExisting.');
|
|
33
|
+
get size() {
|
|
34
|
+
return this.#source.size;
|
|
29
35
|
}
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
36
|
+
entries() {
|
|
37
|
+
return this.#source.entries();
|
|
38
|
+
}
|
|
39
|
+
forEach(callbackfn, thisArg) {
|
|
40
|
+
for (const [key, value] of this.#source) {
|
|
41
|
+
callbackfn.call(thisArg, value, key, this);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
get(key) {
|
|
45
|
+
return this.#source.get(key);
|
|
46
|
+
}
|
|
47
|
+
has(key) {
|
|
48
|
+
return this.#source.has(key);
|
|
49
|
+
}
|
|
50
|
+
keys() {
|
|
51
|
+
return this.#source.keys();
|
|
52
|
+
}
|
|
53
|
+
values() {
|
|
54
|
+
return this.#source.values();
|
|
55
|
+
}
|
|
56
|
+
[Symbol.iterator]() {
|
|
57
|
+
return this.entries();
|
|
38
58
|
}
|
|
39
|
-
return token;
|
|
40
59
|
}
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
provide: provider,
|
|
47
|
-
scope: metadata?.scope ?? Scope.DEFAULT,
|
|
48
|
-
type: 'class',
|
|
49
|
-
useClass: provider
|
|
50
|
-
};
|
|
60
|
+
class ReadonlyMultiRegistrationMapView {
|
|
61
|
+
#source;
|
|
62
|
+
[Symbol.toStringTag] = 'Map';
|
|
63
|
+
constructor(source) {
|
|
64
|
+
this.#source = source;
|
|
51
65
|
}
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
return {
|
|
55
|
-
inject: [],
|
|
56
|
-
multi: provider.multi,
|
|
57
|
-
provide: provider.provide,
|
|
58
|
-
scope: Scope.DEFAULT,
|
|
59
|
-
type: 'value',
|
|
60
|
-
useValue: provider.useValue
|
|
61
|
-
};
|
|
66
|
+
get size() {
|
|
67
|
+
return this.#source.size;
|
|
62
68
|
}
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
throw new InvalidProviderError('Factory provider useFactory must be a function.', {
|
|
67
|
-
token: provider.provide
|
|
68
|
-
});
|
|
69
|
+
*entries() {
|
|
70
|
+
for (const [token, providers] of this.#source) {
|
|
71
|
+
yield [token, Object.freeze([...providers])];
|
|
69
72
|
}
|
|
70
|
-
const metadata = provider.resolverClass ? getClassDiMetadata(provider.resolverClass) : undefined;
|
|
71
|
-
return {
|
|
72
|
-
inject: (provider.inject ?? []).map(normalizeInjectToken),
|
|
73
|
-
multi: provider.multi,
|
|
74
|
-
provide: provider.provide,
|
|
75
|
-
scope: provider.scope ?? metadata?.scope ?? Scope.DEFAULT,
|
|
76
|
-
type: 'factory',
|
|
77
|
-
useFactory: provider.useFactory
|
|
78
|
-
};
|
|
79
73
|
}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
throw new InvalidProviderError('Class provider useClass must be a constructor.', {
|
|
84
|
-
token: provider.provide
|
|
85
|
-
});
|
|
74
|
+
forEach(callbackfn, thisArg) {
|
|
75
|
+
for (const [key, value] of this.entries()) {
|
|
76
|
+
callbackfn.call(thisArg, value, key, this);
|
|
86
77
|
}
|
|
87
|
-
const metadata = getClassDiMetadata(provider.useClass);
|
|
88
|
-
return {
|
|
89
|
-
inject: (provider.inject ?? metadata?.inject ?? []).map(normalizeInjectToken),
|
|
90
|
-
multi: provider.multi,
|
|
91
|
-
provide: provider.provide,
|
|
92
|
-
scope: provider.scope ?? metadata?.scope ?? Scope.DEFAULT,
|
|
93
|
-
type: 'class',
|
|
94
|
-
useClass: provider.useClass
|
|
95
|
-
};
|
|
96
78
|
}
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
79
|
+
get(key) {
|
|
80
|
+
const providers = this.#source.get(key);
|
|
81
|
+
return providers ? Object.freeze([...providers]) : undefined;
|
|
82
|
+
}
|
|
83
|
+
has(key) {
|
|
84
|
+
return this.#source.has(key);
|
|
85
|
+
}
|
|
86
|
+
keys() {
|
|
87
|
+
return this.#source.keys();
|
|
88
|
+
}
|
|
89
|
+
*values() {
|
|
90
|
+
for (const providers of this.#source.values()) {
|
|
91
|
+
yield Object.freeze([...providers]);
|
|
103
92
|
}
|
|
104
|
-
return {
|
|
105
|
-
inject: [],
|
|
106
|
-
provide: provider.provide,
|
|
107
|
-
scope: Scope.DEFAULT,
|
|
108
|
-
type: 'existing',
|
|
109
|
-
useExisting: provider.useExisting
|
|
110
|
-
};
|
|
111
93
|
}
|
|
112
|
-
|
|
94
|
+
[Symbol.iterator]() {
|
|
95
|
+
return this.entries();
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
function isPromiseLike(value) {
|
|
99
|
+
return (typeof value === 'object' || typeof value === 'function') && value !== null && typeof value.then === 'function';
|
|
113
100
|
}
|
|
114
101
|
|
|
115
102
|
/**
|
|
@@ -123,9 +110,9 @@ export class Container {
|
|
|
123
110
|
multiRequestCache;
|
|
124
111
|
multiSingletonCache = new Map();
|
|
125
112
|
staleDisposalTasks = new Set();
|
|
126
|
-
staleDisposalErrors = [];
|
|
127
113
|
singletonCache;
|
|
128
114
|
forwardRefTokenCache = new WeakMap();
|
|
115
|
+
factoryResolutionKinds = new WeakMap();
|
|
129
116
|
providerLookupPlanCache = new Map();
|
|
130
117
|
multiProviderPlanCache = new Map();
|
|
131
118
|
requestScopeVerdictPlanCache = new Map();
|
|
@@ -133,7 +120,7 @@ export class Container {
|
|
|
133
120
|
childScopes;
|
|
134
121
|
disposePromise;
|
|
135
122
|
disposed = false;
|
|
136
|
-
|
|
123
|
+
trackedByParent = false;
|
|
137
124
|
graphRevision = 0;
|
|
138
125
|
constructor(parent, requestScopeEnabled = false, singletonCache) {
|
|
139
126
|
this.parent = parent;
|
|
@@ -251,6 +238,65 @@ export class Container {
|
|
|
251
238
|
return this.lookupProvider(token) !== undefined || this.hasMulti(token);
|
|
252
239
|
}
|
|
253
240
|
|
|
241
|
+
/**
|
|
242
|
+
* Returns the framework-owned resolution state for testing/tooling adapters.
|
|
243
|
+
*
|
|
244
|
+
* This method is the supported introspection seam for packages such as
|
|
245
|
+
* `@fluojs/testing`; callers should prefer ordinary `has(...)` and
|
|
246
|
+
* `resolve(...)` unless they need read-only graph/cache visibility while
|
|
247
|
+
* implementing a framework-level helper. Cache adoption for synchronous
|
|
248
|
+
* helpers goes through `cacheOwner`; the returned maps are not mutable
|
|
249
|
+
* container internals.
|
|
250
|
+
*
|
|
251
|
+
* @returns Read-only provider registrations and resolution caches for this container scope.
|
|
252
|
+
*/
|
|
253
|
+
inspectResolutionState() {
|
|
254
|
+
const registrations = new Map(this.registrations);
|
|
255
|
+
const multiRegistrations = new Map(Array.from(this.multiRegistrations, ([token, providers]) => [token, Object.freeze([...providers])]));
|
|
256
|
+
const multiSingletonCache = new Map(this.multiSingletonCache);
|
|
257
|
+
const singletonCache = new Map(this.singletonCache);
|
|
258
|
+
return {
|
|
259
|
+
cacheOwner: this.createCacheOwner(singletonCache, multiSingletonCache),
|
|
260
|
+
factoryResolutionKinds: this.createFactoryResolutionState(),
|
|
261
|
+
parent: this.parent?.inspectResolutionState(),
|
|
262
|
+
registrations: new ReadonlyMapView(registrations),
|
|
263
|
+
multiRegistrations: new ReadonlyMultiRegistrationMapView(multiRegistrations),
|
|
264
|
+
multiSingletonCache: new ReadonlyMapView(multiSingletonCache),
|
|
265
|
+
requestScopeEnabled: this.requestScopeEnabled,
|
|
266
|
+
singletonCache: new ReadonlyMapView(singletonCache)
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
createCacheOwner(singletonCacheSnapshot, multiSingletonCacheSnapshot) {
|
|
270
|
+
return Object.freeze({
|
|
271
|
+
deleteMultiSingleton: provider => {
|
|
272
|
+
this.multiSingletonCache.delete(provider);
|
|
273
|
+
multiSingletonCacheSnapshot.delete(provider);
|
|
274
|
+
},
|
|
275
|
+
deleteSingleton: token => {
|
|
276
|
+
this.singletonCache.delete(token);
|
|
277
|
+
singletonCacheSnapshot.delete(token);
|
|
278
|
+
},
|
|
279
|
+
recordFactoryResolution: (provider, kind) => {
|
|
280
|
+
this.root().factoryResolutionKinds.set(provider, kind);
|
|
281
|
+
},
|
|
282
|
+
setMultiSingleton: (provider, promise) => {
|
|
283
|
+
this.multiSingletonCache.set(provider, promise);
|
|
284
|
+
multiSingletonCacheSnapshot.set(provider, promise);
|
|
285
|
+
},
|
|
286
|
+
setSingleton: (token, promise) => {
|
|
287
|
+
this.singletonCache.set(token, promise);
|
|
288
|
+
singletonCacheSnapshot.set(token, promise);
|
|
289
|
+
}
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
createFactoryResolutionState() {
|
|
293
|
+
const root = this.root();
|
|
294
|
+
return Object.freeze({
|
|
295
|
+
get: provider => root.factoryResolutionKinds.get(provider),
|
|
296
|
+
has: provider => root.factoryResolutionKinds.has(provider)
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
|
|
254
300
|
/**
|
|
255
301
|
* Returns whether resolving a token may require a request-scope container.
|
|
256
302
|
*
|
|
@@ -297,6 +343,7 @@ export class Container {
|
|
|
297
343
|
hint: 'Ensure all resolves complete before calling container.dispose().'
|
|
298
344
|
});
|
|
299
345
|
}
|
|
346
|
+
await this.assertStaleDisposalsSettled();
|
|
300
347
|
return this.resolveWithChain(token, [], new Set());
|
|
301
348
|
}
|
|
302
349
|
|
|
@@ -324,8 +371,8 @@ export class Container {
|
|
|
324
371
|
async disposeAll() {
|
|
325
372
|
const errors = [];
|
|
326
373
|
try {
|
|
327
|
-
// Dispose all live request-scope children
|
|
328
|
-
if (
|
|
374
|
+
// Dispose all live request-scope children before tearing down this scope's cache.
|
|
375
|
+
if (this.childScopes && this.childScopes.size > 0) {
|
|
329
376
|
const childResults = await Promise.allSettled(Array.from(this.childScopes).map(child => child.dispose()));
|
|
330
377
|
for (const result of childResults) {
|
|
331
378
|
if (result.status === 'rejected') {
|
|
@@ -341,9 +388,9 @@ export class Container {
|
|
|
341
388
|
}
|
|
342
389
|
this.throwDisposalErrors(errors);
|
|
343
390
|
} finally {
|
|
344
|
-
if (this.parent && this.
|
|
345
|
-
this.
|
|
346
|
-
this.
|
|
391
|
+
if (this.parent && this.trackedByParent) {
|
|
392
|
+
this.parent.childScopes?.delete(this);
|
|
393
|
+
this.trackedByParent = false;
|
|
347
394
|
}
|
|
348
395
|
}
|
|
349
396
|
}
|
|
@@ -589,13 +636,13 @@ export class Container {
|
|
|
589
636
|
return this.parent ? this.parent.root() : this;
|
|
590
637
|
}
|
|
591
638
|
ensureTrackedRequestScope() {
|
|
592
|
-
if (!this.requestScopeEnabled || !this.parent || this.
|
|
639
|
+
if (!this.requestScopeEnabled || !this.parent || this.trackedByParent) {
|
|
593
640
|
return;
|
|
594
641
|
}
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
this.
|
|
642
|
+
this.parent.ensureTrackedRequestScope();
|
|
643
|
+
this.parent.childScopes ??= new Set();
|
|
644
|
+
this.parent.childScopes.add(this);
|
|
645
|
+
this.trackedByParent = true;
|
|
599
646
|
}
|
|
600
647
|
requestCacheForWrite() {
|
|
601
648
|
this.ensureTrackedRequestScope();
|
|
@@ -678,12 +725,17 @@ export class Container {
|
|
|
678
725
|
return entries;
|
|
679
726
|
}
|
|
680
727
|
async disposeCache(entries) {
|
|
681
|
-
|
|
728
|
+
const errors = [];
|
|
729
|
+
try {
|
|
730
|
+
await this.assertStaleDisposalsSettled();
|
|
731
|
+
} catch (error) {
|
|
732
|
+
this.collectDisposalError(error, errors);
|
|
733
|
+
}
|
|
682
734
|
const {
|
|
683
735
|
disposables,
|
|
684
|
-
errors
|
|
736
|
+
errors: resolutionErrors
|
|
685
737
|
} = await this.collectDisposableInstances(entries);
|
|
686
|
-
errors.push(...
|
|
738
|
+
errors.push(...resolutionErrors);
|
|
687
739
|
errors.push(...(await this.disposeInstancesInReverseOrder(disposables)));
|
|
688
740
|
this.clearDisposalCaches();
|
|
689
741
|
this.throwDisposalErrors(errors);
|
|
@@ -759,26 +811,49 @@ export class Container {
|
|
|
759
811
|
this.requestScopeVerdictPlanCache.clear();
|
|
760
812
|
this.effectiveProviderPlanCache.clear();
|
|
761
813
|
}
|
|
762
|
-
async
|
|
814
|
+
async assertStaleDisposalsSettled() {
|
|
815
|
+
const errors = [];
|
|
763
816
|
while (this.staleDisposalTasks.size > 0) {
|
|
764
|
-
|
|
817
|
+
const tasks = Array.from(this.staleDisposalTasks);
|
|
818
|
+
await Promise.all(tasks.map(task => task.promise));
|
|
819
|
+
for (const task of tasks) {
|
|
820
|
+
this.staleDisposalTasks.delete(task);
|
|
821
|
+
if (task.failed && !task.errorConsumed) {
|
|
822
|
+
task.errorConsumed = true;
|
|
823
|
+
errors.push(task.error);
|
|
824
|
+
}
|
|
825
|
+
}
|
|
765
826
|
}
|
|
827
|
+
this.throwDisposalErrors(errors);
|
|
766
828
|
}
|
|
767
|
-
scheduleStaleDisposal(instancePromise) {
|
|
768
|
-
|
|
769
|
-
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 () => {
|
|
770
838
|
try {
|
|
771
839
|
const instance = await instancePromise;
|
|
772
840
|
if (this.isDisposable(instance)) {
|
|
773
841
|
await instance.onDestroy();
|
|
774
842
|
}
|
|
775
843
|
} catch (error) {
|
|
776
|
-
|
|
844
|
+
task.error = error;
|
|
845
|
+
task.failed = true;
|
|
777
846
|
}
|
|
778
847
|
})().finally(() => {
|
|
779
|
-
|
|
848
|
+
if (!task.failed) {
|
|
849
|
+
for (const observer of observers) {
|
|
850
|
+
observer.staleDisposalTasks.delete(task);
|
|
851
|
+
}
|
|
852
|
+
}
|
|
780
853
|
});
|
|
781
|
-
|
|
854
|
+
for (const observer of observers) {
|
|
855
|
+
observer.staleDisposalTasks.add(task);
|
|
856
|
+
}
|
|
782
857
|
}
|
|
783
858
|
throwDisposalErrors(errors) {
|
|
784
859
|
if (errors.length === 1) {
|
|
@@ -811,7 +886,9 @@ export class Container {
|
|
|
811
886
|
throw new InvariantError('Factory provider is missing useFactory.');
|
|
812
887
|
}
|
|
813
888
|
const deps = await this.resolveProviderDeps(provider, chain, activeTokens);
|
|
814
|
-
|
|
889
|
+
const value = provider.useFactory(...deps);
|
|
890
|
+
this.root().factoryResolutionKinds.set(provider, isPromiseLike(value) ? 'async' : 'sync');
|
|
891
|
+
return value;
|
|
815
892
|
}
|
|
816
893
|
case 'class':
|
|
817
894
|
{
|
|
@@ -931,34 +1008,18 @@ export class Container {
|
|
|
931
1008
|
}
|
|
932
1009
|
return deps;
|
|
933
1010
|
}
|
|
934
|
-
invalidateAffectedCachedEntriesInHierarchy(token) {
|
|
935
|
-
this.invalidateAffectedCachedEntries(token);
|
|
936
|
-
const
|
|
937
|
-
|
|
938
|
-
return;
|
|
939
|
-
}
|
|
940
|
-
for (const childScope of childScopes) {
|
|
941
|
-
if (this.isAncestorOf(childScope)) {
|
|
942
|
-
childScope.invalidateAffectedCachedEntries(token);
|
|
943
|
-
}
|
|
944
|
-
}
|
|
945
|
-
}
|
|
946
|
-
isAncestorOf(container) {
|
|
947
|
-
let current = container.parent;
|
|
948
|
-
while (current) {
|
|
949
|
-
if (current === this) {
|
|
950
|
-
return true;
|
|
951
|
-
}
|
|
952
|
-
current = current.parent;
|
|
1011
|
+
invalidateAffectedCachedEntriesInHierarchy(token, staleDisposalOwner = this) {
|
|
1012
|
+
this.invalidateAffectedCachedEntries(token, staleDisposalOwner);
|
|
1013
|
+
for (const childScope of this.childScopes ?? []) {
|
|
1014
|
+
childScope.invalidateAffectedCachedEntriesInHierarchy(token, staleDisposalOwner);
|
|
953
1015
|
}
|
|
954
|
-
return false;
|
|
955
1016
|
}
|
|
956
|
-
invalidateAffectedCachedEntries(token) {
|
|
1017
|
+
invalidateAffectedCachedEntries(token, staleDisposalOwner) {
|
|
957
1018
|
for (const [cachedToken, cached] of this.requestCache?.entries() ?? []) {
|
|
958
1019
|
if (!this.shouldInvalidateCachedToken(cachedToken, token)) {
|
|
959
1020
|
continue;
|
|
960
1021
|
}
|
|
961
|
-
this.scheduleStaleDisposal(cached);
|
|
1022
|
+
this.scheduleStaleDisposal(cached, staleDisposalOwner);
|
|
962
1023
|
this.requestCache?.delete(cachedToken);
|
|
963
1024
|
}
|
|
964
1025
|
if (!this.parent) {
|
|
@@ -966,7 +1027,7 @@ export class Container {
|
|
|
966
1027
|
if (!this.shouldInvalidateCachedToken(cachedToken, token)) {
|
|
967
1028
|
continue;
|
|
968
1029
|
}
|
|
969
|
-
this.scheduleStaleDisposal(cached);
|
|
1030
|
+
this.scheduleStaleDisposal(cached, staleDisposalOwner);
|
|
970
1031
|
this.singletonCache.delete(cachedToken);
|
|
971
1032
|
}
|
|
972
1033
|
}
|
|
@@ -975,7 +1036,7 @@ export class Container {
|
|
|
975
1036
|
if (!this.shouldInvalidateCachedProvider(provider, token)) {
|
|
976
1037
|
continue;
|
|
977
1038
|
}
|
|
978
|
-
this.scheduleStaleDisposal(cached);
|
|
1039
|
+
this.scheduleStaleDisposal(cached, staleDisposalOwner);
|
|
979
1040
|
this.multiSingletonCache.delete(provider);
|
|
980
1041
|
}
|
|
981
1042
|
}
|
|
@@ -987,7 +1048,7 @@ export class Container {
|
|
|
987
1048
|
if (!this.shouldInvalidateCachedProvider(provider, token)) {
|
|
988
1049
|
continue;
|
|
989
1050
|
}
|
|
990
|
-
this.scheduleStaleDisposal(cached);
|
|
1051
|
+
this.scheduleStaleDisposal(cached, staleDisposalOwner);
|
|
991
1052
|
multiRequestCache.delete(provider);
|
|
992
1053
|
}
|
|
993
1054
|
}
|
|
@@ -0,0 +1 @@
|
|
|
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"
|