@fluojs/di 1.0.0-beta.5 → 1.0.0-beta.7
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 +34 -7
- package/README.md +35 -8
- package/dist/container.d.ts +13 -0
- package/dist/container.d.ts.map +1 -1
- package/dist/container.js +168 -30
- package/package.json +2 -2
package/README.ko.md
CHANGED
|
@@ -76,6 +76,12 @@ const service = await container.resolve(UserService);
|
|
|
76
76
|
- **request**: `createRequestScope()`마다 새로 생성됩니다.
|
|
77
77
|
- **transient**: resolve할 때마다 새 인스턴스를 만듭니다.
|
|
78
78
|
|
|
79
|
+
dispose 중에는 루트 컨테이너가 먼저 살아 있는 request scope 자식을 정리한 뒤, 자식 dispose 중 하나 이상이 실패하더라도 루트가 소유한 singleton 정리를 계속 수행합니다. 자식/루트 dispose 실패가 여러 개 발생하면 `dispose()`는 모든 shutdown 실패를 확인할 수 있도록 `AggregateError`로 보고합니다.
|
|
80
|
+
|
|
81
|
+
### provider override
|
|
82
|
+
|
|
83
|
+
테스트나 request-local 경계에서 기존 등록을 의도적으로 교체해야 할 때는 `override(...providers)`를 사용합니다. override는 각 토큰의 현재 provider set을 교체하고 cached instance를 무효화하며, 오래된 instance를 즉시 dispose합니다. multi provider override는 해당 토큰의 전체 multi-provider set을 교체하므로 필요한 replacement provider를 한 번에 모두 전달하세요. 같은 토큰에 single replacement와 multi replacement를 한 override 호출에서 섞으면 모호한 교체로 보고 거부합니다.
|
|
84
|
+
|
|
79
85
|
### request scope 분리
|
|
80
86
|
|
|
81
87
|
```ts
|
|
@@ -91,7 +97,7 @@ provider 객체는 등록 시점에 검증됩니다. 모든 객체 provider는 n
|
|
|
91
97
|
|
|
92
98
|
컨테이너는 순환 의존성을 자동으로 감지하고 `CircularDependencyError`를 발생시켜 무한 루프를 방지합니다. 여기에는 직접 참조(A→A), 이중 노드(A→B→A), 깊은 순환(A→B→C→A)이 모두 포함됩니다.
|
|
93
99
|
|
|
94
|
-
|
|
100
|
+
선언 순서 때문에 아직 정의되지 않은 토큰을 참조해야 한다면 `forwardRef()`를 사용하세요. `forwardRef()`는 선언 순서 문제를 위해 토큰 조회를 지연할 뿐이며, 실제 생성자 순환을 해소하지는 않습니다. 그런 순환은 여전히 `CircularDependencyError`로 거부됩니다.
|
|
95
101
|
|
|
96
102
|
```typescript
|
|
97
103
|
import { forwardRef } from '@fluojs/di';
|
|
@@ -99,12 +105,25 @@ import { Inject } from '@fluojs/core';
|
|
|
99
105
|
|
|
100
106
|
@Inject(forwardRef(() => ServiceB))
|
|
101
107
|
class ServiceA {
|
|
102
|
-
constructor(private serviceB:
|
|
108
|
+
constructor(private readonly serviceB: ServiceB) {}
|
|
103
109
|
}
|
|
104
110
|
|
|
105
|
-
@Inject(forwardRef(() => ServiceA))
|
|
106
111
|
class ServiceB {
|
|
107
|
-
|
|
112
|
+
getStatus() {
|
|
113
|
+
return 'ready';
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
`forwardRef(...)`와 `optional(...)`은 클래스 수준 `@Inject(...)` 토큰 목록이나 provider 수준 `inject` 배열 안에서 쓰는 토큰 래퍼입니다. 이들은 데코레이터가 아니며 constructor parameter에 붙이지 않습니다.
|
|
119
|
+
|
|
120
|
+
```typescript
|
|
121
|
+
import { optional } from '@fluojs/di';
|
|
122
|
+
import { Inject } from '@fluojs/core';
|
|
123
|
+
|
|
124
|
+
@Inject(optional(AuditLogger))
|
|
125
|
+
class ServiceWithOptionalLogger {
|
|
126
|
+
constructor(private readonly auditLogger: AuditLogger | undefined) {}
|
|
108
127
|
}
|
|
109
128
|
```
|
|
110
129
|
|
|
@@ -116,7 +135,7 @@ class ServiceB {
|
|
|
116
135
|
import { Container } from '@fluojs/di';
|
|
117
136
|
|
|
118
137
|
const container = new Container();
|
|
119
|
-
const mockDb = { query:
|
|
138
|
+
const mockDb = { query: vi.fn() };
|
|
120
139
|
|
|
121
140
|
// 실제 Database 클래스를 모의 객체 값으로 교체
|
|
122
141
|
container.register({
|
|
@@ -125,13 +144,13 @@ container.register({
|
|
|
125
144
|
});
|
|
126
145
|
|
|
127
146
|
const service = await container.resolve(DataService);
|
|
128
|
-
//
|
|
147
|
+
// service는 실제 Database 인스턴스 대신 mockDb를 사용합니다.
|
|
129
148
|
```
|
|
130
149
|
|
|
131
150
|
## 문제 해결
|
|
132
151
|
|
|
133
152
|
### CircularDependencyError
|
|
134
|
-
의존성 그래프에서 순환이 감지될 때 발생합니다. 생성자 주입 항목을 확인하고
|
|
153
|
+
의존성 그래프에서 순환이 감지될 때 발생합니다. 생성자 주입 항목을 확인하고 공유 상태 추출, 중재자 도입, 수명 주기 경계 변경 등으로 순환을 제거하세요. `forwardRef()`는 선언 순서 문제를 위해 토큰 조회만 지연하며, 실제 생성자 순환을 끊지는 않습니다.
|
|
135
154
|
|
|
136
155
|
### 토큰을 찾을 수 없음 (Token Not Found)
|
|
137
156
|
필요한 모든 provider가 컨테이너에 등록되어 있는지 확인하세요. `createRequestScope()`를 사용하는 경우 자식 컨테이너는 부모의 토큰을 해석할 수 있지만, 그 반대는 불가능합니다.
|
|
@@ -142,10 +161,18 @@ const service = await container.resolve(DataService);
|
|
|
142
161
|
|---|---|
|
|
143
162
|
| `Container` | 메인 DI 컨테이너 클래스입니다. |
|
|
144
163
|
| `register(...providers)` | 하나 이상의 프로바이더를 등록합니다. |
|
|
164
|
+
| `override(...providers)` | 기존 provider를 교체하고 cached instance를 무효화하며 오래된 instance를 dispose합니다. |
|
|
145
165
|
| `resolve<T>(token)` | 토큰을 인스턴스로 비동기 해석합니다. |
|
|
146
166
|
| `createRequestScope()` | 요청 스코프 의존성을 위한 자식 컨테이너를 생성합니다. |
|
|
147
167
|
| `has(token)` | 컨테이너나 부모에 토큰이 등록되어 있는지 확인합니다. |
|
|
148
168
|
| `hasRequestScopedDependency(token)` | 토큰 해석 시 provider 그래프에 request-scoped 의존성이나 순환이 있어 request-scope 컨테이너가 필요할 수 있는지 확인합니다. |
|
|
169
|
+
| `dispose()` | request child와 루트가 소유한 singleton instance를 정리합니다. |
|
|
170
|
+
| `forwardRef(fn)` | 선언 순서 문제를 위해 조회를 지연하는 토큰 래퍼를 반환합니다. 실제 생성자 순환을 해석 가능하게 만들지는 않습니다. |
|
|
171
|
+
| `optional(token)` | 하나의 의존성을 optional로 표시하는 토큰 래퍼를 반환합니다. 누락된 optional dependency는 `undefined`로 해석됩니다. |
|
|
172
|
+
| `Scope` | `DEFAULT`, `REQUEST`, `TRANSIENT` scope 상수를 제공합니다. |
|
|
173
|
+
| 에러 클래스 | `InvalidProviderError`, `ContainerResolutionError`, `RequestScopeResolutionError`, `ScopeMismatchError`, `CircularDependencyError`, `DuplicateProviderError`. |
|
|
174
|
+
|
|
175
|
+
multi-provider 토큰을 resolve하면 등록 순서대로 해석된 값의 배열이 반환됩니다.
|
|
149
176
|
|
|
150
177
|
## 관련 패키지
|
|
151
178
|
|
package/README.md
CHANGED
|
@@ -64,7 +64,7 @@ const result = await service.getStatus();
|
|
|
64
64
|
## Key Capabilities
|
|
65
65
|
|
|
66
66
|
### Provider Types
|
|
67
|
-
fluo DI supports
|
|
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
70
|
- **Factory Providers**: `{ provide: 'ASYNC_CONFIG', useFactory: async (db) => await db.load(), inject: [Database] }`.
|
|
@@ -75,6 +75,12 @@ fluo DI supports three main provider shapes:
|
|
|
75
75
|
- **Request**: Instance is created once per `createRequestScope()` call.
|
|
76
76
|
- **Transient**: A new instance is created every time it is resolved.
|
|
77
77
|
|
|
78
|
+
During disposal, the root container first tears down live request-scope children and then continues with root-owned singleton cleanup even if one or more child disposals fail. When multiple child/root disposals fail, `dispose()` reports an `AggregateError` so callers can inspect every shutdown failure without losing cleanup progress.
|
|
79
|
+
|
|
80
|
+
### Provider Overrides
|
|
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, and dispose stale instances immediately. Multi-provider overrides replace the full multi-provider set for that token, so pass every replacement provider together; mixing single and multi replacements for the same token in one override call is rejected as ambiguous.
|
|
83
|
+
|
|
78
84
|
### Request Scoping
|
|
79
85
|
Isolated containers can be created to handle per-request state without polluting the root container.
|
|
80
86
|
|
|
@@ -91,7 +97,7 @@ Provider objects are validated at registration time: every object provider must
|
|
|
91
97
|
|
|
92
98
|
The container automatically detects circular dependencies and throws a `CircularDependencyError` to prevent infinite loops. This includes direct (A→A), two-node (A→B→A), and deep (A→B→C→A) cycles.
|
|
93
99
|
|
|
94
|
-
|
|
100
|
+
Use `forwardRef()` when a token is referenced before its declaration. It defers token lookup for declaration-order issues, but it does not make true constructor cycles resolvable; those cycles are still rejected with `CircularDependencyError`.
|
|
95
101
|
|
|
96
102
|
```typescript
|
|
97
103
|
import { forwardRef } from '@fluojs/di';
|
|
@@ -99,12 +105,25 @@ import { Inject } from '@fluojs/core';
|
|
|
99
105
|
|
|
100
106
|
@Inject(forwardRef(() => ServiceB))
|
|
101
107
|
class ServiceA {
|
|
102
|
-
constructor(private serviceB:
|
|
108
|
+
constructor(private readonly serviceB: ServiceB) {}
|
|
103
109
|
}
|
|
104
110
|
|
|
105
|
-
@Inject(forwardRef(() => ServiceA))
|
|
106
111
|
class ServiceB {
|
|
107
|
-
|
|
112
|
+
getStatus() {
|
|
113
|
+
return 'ready';
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
`forwardRef(...)` and `optional(...)` are token wrappers used inside the class-level `@Inject(...)` token list or provider-level `inject` arrays. They are not decorators and do not attach to constructor parameters.
|
|
119
|
+
|
|
120
|
+
```typescript
|
|
121
|
+
import { optional } from '@fluojs/di';
|
|
122
|
+
import { Inject } from '@fluojs/core';
|
|
123
|
+
|
|
124
|
+
@Inject(optional(AuditLogger))
|
|
125
|
+
class ServiceWithOptionalLogger {
|
|
126
|
+
constructor(private readonly auditLogger: AuditLogger | undefined) {}
|
|
108
127
|
}
|
|
109
128
|
```
|
|
110
129
|
|
|
@@ -116,7 +135,7 @@ You can easily override providers in the container to use mocks or stubs during
|
|
|
116
135
|
import { Container } from '@fluojs/di';
|
|
117
136
|
|
|
118
137
|
const container = new Container();
|
|
119
|
-
const mockDb = { query:
|
|
138
|
+
const mockDb = { query: vi.fn() };
|
|
120
139
|
|
|
121
140
|
// Override the real Database class with a mock value
|
|
122
141
|
container.register({
|
|
@@ -125,13 +144,13 @@ container.register({
|
|
|
125
144
|
});
|
|
126
145
|
|
|
127
146
|
const service = await container.resolve(DataService);
|
|
128
|
-
// service
|
|
147
|
+
// service uses mockDb instead of the real Database instance
|
|
129
148
|
```
|
|
130
149
|
|
|
131
150
|
## Troubleshooting
|
|
132
151
|
|
|
133
152
|
### CircularDependencyError
|
|
134
|
-
Thrown when the container detects a cycle in the dependency graph. Check your constructor injections and
|
|
153
|
+
Thrown when the container detects a cycle in the dependency graph. Check your constructor injections and remove the cycle by extracting shared state, introducing a mediator, or changing the lifetime boundary. `forwardRef()` only defers token lookup for declaration-order issues; it does not break true constructor cycles.
|
|
135
154
|
|
|
136
155
|
### Token Not Found
|
|
137
156
|
Ensure all required providers are registered in the container. If you use `createRequestScope()`, the child container can resolve tokens from the parent, but not vice versa.
|
|
@@ -142,10 +161,18 @@ Ensure all required providers are registered in the container. If you use `creat
|
|
|
142
161
|
|---|---|
|
|
143
162
|
| `Container` | The main DI container class. |
|
|
144
163
|
| `register(...providers)` | Registers one or more providers. |
|
|
164
|
+
| `override(...providers)` | Replaces existing providers, invalidates cached instances, and disposes stale instances. |
|
|
145
165
|
| `resolve<T>(token)` | Asynchronously resolves a token to an instance. |
|
|
146
166
|
| `createRequestScope()` | Creates a child container for request-scoped dependencies. |
|
|
147
167
|
| `has(token)` | Checks if a token is registered in the container or its parents. |
|
|
148
168
|
| `hasRequestScopedDependency(token)` | Checks whether resolving a token may require a request-scope container because its provider graph contains request-scoped dependencies or is cyclic. |
|
|
169
|
+
| `dispose()` | Disposes request children and root-owned singleton instances. |
|
|
170
|
+
| `forwardRef(fn)` | Returns a token wrapper that defers lookup for declaration-order issues; it does not make constructor dependency cycles resolvable. |
|
|
171
|
+
| `optional(token)` | Returns a token wrapper that marks one dependency as optional; missing optional dependencies resolve to `undefined`. |
|
|
172
|
+
| `Scope` | Exposes `DEFAULT`, `REQUEST`, and `TRANSIENT` scope constants. |
|
|
173
|
+
| Error classes | `InvalidProviderError`, `ContainerResolutionError`, `RequestScopeResolutionError`, `ScopeMismatchError`, `CircularDependencyError`, `DuplicateProviderError`. |
|
|
174
|
+
|
|
175
|
+
Resolving a multi-provider token returns an array of resolved values in registration order.
|
|
149
176
|
|
|
150
177
|
## Related Packages
|
|
151
178
|
|
package/dist/container.d.ts
CHANGED
|
@@ -16,10 +16,15 @@ export declare class Container {
|
|
|
16
16
|
private readonly staleDisposalErrors;
|
|
17
17
|
private readonly singletonCache;
|
|
18
18
|
private readonly forwardRefTokenCache;
|
|
19
|
+
private readonly providerLookupPlanCache;
|
|
20
|
+
private readonly multiProviderPlanCache;
|
|
21
|
+
private readonly requestScopeVerdictPlanCache;
|
|
22
|
+
private readonly effectiveProviderPlanCache;
|
|
19
23
|
private childScopes;
|
|
20
24
|
private disposePromise;
|
|
21
25
|
private disposed;
|
|
22
26
|
private trackedByRoot;
|
|
27
|
+
private graphRevision;
|
|
23
28
|
constructor(parent?: Container | undefined, requestScopeEnabled?: boolean, singletonCache?: Map<Token, Promise<unknown>>);
|
|
24
29
|
/**
|
|
25
30
|
* Registers providers in the current container scope.
|
|
@@ -137,12 +142,20 @@ export declare class Container {
|
|
|
137
142
|
private collectDisposableInstances;
|
|
138
143
|
private disposeInstancesInReverseOrder;
|
|
139
144
|
private clearDisposalCaches;
|
|
145
|
+
private currentLineageRevision;
|
|
146
|
+
private readCachedPlan;
|
|
147
|
+
private writePlanCache;
|
|
148
|
+
private advanceGraphRevision;
|
|
149
|
+
private clearResolutionPlanCaches;
|
|
140
150
|
private waitForStaleDisposalTasks;
|
|
141
151
|
private scheduleStaleDisposal;
|
|
142
152
|
private throwDisposalErrors;
|
|
153
|
+
private collectDisposalError;
|
|
143
154
|
private isDisposable;
|
|
144
155
|
private instantiate;
|
|
145
156
|
private assertSingletonDependencyScopes;
|
|
157
|
+
private findRequestScopedDependency;
|
|
158
|
+
private findRequestScopedDependencyToken;
|
|
146
159
|
private resolveEffectiveProvider;
|
|
147
160
|
private resolveProviderDependencyToken;
|
|
148
161
|
private resolveForwardRefToken;
|
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,EASV,QAAQ,EAET,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"container.d.ts","sourceRoot":"","sources":["../src/container.ts"],"names":[],"mappings":"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"}
|
package/dist/container.js
CHANGED
|
@@ -126,10 +126,15 @@ export class Container {
|
|
|
126
126
|
staleDisposalErrors = [];
|
|
127
127
|
singletonCache;
|
|
128
128
|
forwardRefTokenCache = new WeakMap();
|
|
129
|
+
providerLookupPlanCache = new Map();
|
|
130
|
+
multiProviderPlanCache = new Map();
|
|
131
|
+
requestScopeVerdictPlanCache = new Map();
|
|
132
|
+
effectiveProviderPlanCache = new Map();
|
|
129
133
|
childScopes;
|
|
130
134
|
disposePromise;
|
|
131
135
|
disposed = false;
|
|
132
136
|
trackedByRoot = false;
|
|
137
|
+
graphRevision = 0;
|
|
133
138
|
constructor(parent, requestScopeEnabled = false, singletonCache) {
|
|
134
139
|
this.parent = parent;
|
|
135
140
|
this.requestScopeEnabled = requestScopeEnabled;
|
|
@@ -166,12 +171,14 @@ export class Container {
|
|
|
166
171
|
const existing = this.multiRegistrations.get(normalized.provide);
|
|
167
172
|
if (existing) {
|
|
168
173
|
existing.push(normalized);
|
|
174
|
+
this.advanceGraphRevision();
|
|
169
175
|
continue;
|
|
170
176
|
}
|
|
171
177
|
this.multiRegistrations.set(normalized.provide, [normalized]);
|
|
172
178
|
} else {
|
|
173
179
|
this.registrations.set(normalized.provide, normalized);
|
|
174
180
|
}
|
|
181
|
+
this.advanceGraphRevision();
|
|
175
182
|
}
|
|
176
183
|
return this;
|
|
177
184
|
}
|
|
@@ -196,19 +203,42 @@ export class Container {
|
|
|
196
203
|
hint: 'Ensure overrides are applied before calling container.dispose().'
|
|
197
204
|
});
|
|
198
205
|
}
|
|
206
|
+
const normalizedByToken = new Map();
|
|
199
207
|
for (const provider of providers) {
|
|
200
208
|
const normalized = normalizeProvider(provider);
|
|
201
|
-
const
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
this.invalidateCachedEntry(normalized.provide, existing?.scope ?? normalized.scope);
|
|
205
|
-
if (normalized.multi) {
|
|
206
|
-
this.multiRegistrations.set(normalized.provide, [normalized]);
|
|
207
|
-
this.multiOverriddenTokens.add(normalized.provide);
|
|
209
|
+
const normalizedProviders = normalizedByToken.get(normalized.provide);
|
|
210
|
+
if (normalizedProviders) {
|
|
211
|
+
normalizedProviders.push(normalized);
|
|
208
212
|
continue;
|
|
209
213
|
}
|
|
210
|
-
|
|
211
|
-
|
|
214
|
+
normalizedByToken.set(normalized.provide, [normalized]);
|
|
215
|
+
}
|
|
216
|
+
for (const [token, normalizedProviders] of normalizedByToken) {
|
|
217
|
+
const firstProvider = normalizedProviders[0];
|
|
218
|
+
if (!firstProvider) {
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
const containsMultiProvider = normalizedProviders.some(normalized => normalized.multi === true);
|
|
222
|
+
if (containsMultiProvider && normalizedProviders.some(normalized => normalized.multi !== true)) {
|
|
223
|
+
throw new DuplicateProviderError(token);
|
|
224
|
+
}
|
|
225
|
+
if (!containsMultiProvider && normalizedProviders.length > 1) {
|
|
226
|
+
throw new DuplicateProviderError(token);
|
|
227
|
+
}
|
|
228
|
+
const existing = this.lookupProvider(token);
|
|
229
|
+
const existingMultiProviders = this.collectMultiProviders(token);
|
|
230
|
+
this.registrations.delete(token);
|
|
231
|
+
this.multiRegistrations.delete(token);
|
|
232
|
+
this.invalidateCachedEntry(token, existing?.scope ?? existingMultiProviders[0]?.scope ?? firstProvider.scope);
|
|
233
|
+
if (containsMultiProvider) {
|
|
234
|
+
this.multiRegistrations.set(token, normalizedProviders);
|
|
235
|
+
this.multiOverriddenTokens.add(token);
|
|
236
|
+
this.advanceGraphRevision();
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
this.multiOverriddenTokens.add(token);
|
|
240
|
+
this.registrations.set(token, firstProvider);
|
|
241
|
+
this.advanceGraphRevision();
|
|
212
242
|
}
|
|
213
243
|
return this;
|
|
214
244
|
}
|
|
@@ -230,7 +260,11 @@ export class Container {
|
|
|
230
260
|
* @returns `true` when the provider graph contains request-scoped dependencies or is cyclic.
|
|
231
261
|
*/
|
|
232
262
|
hasRequestScopedDependency(token) {
|
|
233
|
-
|
|
263
|
+
const cached = this.readCachedPlan(this.requestScopeVerdictPlanCache, token);
|
|
264
|
+
if (cached) {
|
|
265
|
+
return cached.value;
|
|
266
|
+
}
|
|
267
|
+
return this.writePlanCache(this.requestScopeVerdictPlanCache, token, this.providerGraphRequiresRequestScope(token, new Set()));
|
|
234
268
|
}
|
|
235
269
|
|
|
236
270
|
/**
|
|
@@ -280,6 +314,7 @@ export class Container {
|
|
|
280
314
|
return;
|
|
281
315
|
}
|
|
282
316
|
this.disposed = true;
|
|
317
|
+
this.advanceGraphRevision();
|
|
283
318
|
this.disposePromise = this.disposeAll();
|
|
284
319
|
try {
|
|
285
320
|
await this.disposePromise;
|
|
@@ -289,13 +324,24 @@ export class Container {
|
|
|
289
324
|
}
|
|
290
325
|
}
|
|
291
326
|
async disposeAll() {
|
|
327
|
+
const errors = [];
|
|
292
328
|
try {
|
|
293
329
|
// Dispose all live request-scope children first (root only)
|
|
294
330
|
if (!this.parent && this.childScopes && this.childScopes.size > 0) {
|
|
295
|
-
await Promise.
|
|
331
|
+
const childResults = await Promise.allSettled(Array.from(this.childScopes).map(child => child.dispose()));
|
|
332
|
+
for (const result of childResults) {
|
|
333
|
+
if (result.status === 'rejected') {
|
|
334
|
+
this.collectDisposalError(result.reason, errors);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
296
337
|
this.childScopes.clear();
|
|
297
338
|
}
|
|
298
|
-
|
|
339
|
+
try {
|
|
340
|
+
await this.disposeCache(this.disposalCacheEntries());
|
|
341
|
+
} catch (error) {
|
|
342
|
+
this.collectDisposalError(error, errors);
|
|
343
|
+
}
|
|
344
|
+
this.throwDisposalErrors(errors);
|
|
299
345
|
} finally {
|
|
300
346
|
if (this.parent && this.trackedByRoot) {
|
|
301
347
|
this.root().childScopes?.delete(this);
|
|
@@ -345,15 +391,20 @@ export class Container {
|
|
|
345
391
|
return this.parent?.hasMultiRegistration(token) ?? false;
|
|
346
392
|
}
|
|
347
393
|
collectMultiProviders(token) {
|
|
394
|
+
const cached = this.readCachedPlan(this.multiProviderPlanCache, token);
|
|
395
|
+
if (cached) {
|
|
396
|
+
return [...cached.value];
|
|
397
|
+
}
|
|
348
398
|
const local = this.multiRegistrations.get(token);
|
|
399
|
+
let providers;
|
|
349
400
|
if (this.multiOverriddenTokens.has(token)) {
|
|
350
|
-
|
|
401
|
+
providers = Object.freeze([...(local ?? [])]);
|
|
402
|
+
} else {
|
|
403
|
+
const fromParent = this.parent ? this.parent.collectMultiProviders(token) : [];
|
|
404
|
+
providers = Object.freeze(local ? [...fromParent, ...local] : [...fromParent]);
|
|
351
405
|
}
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
return [...fromParent, ...local];
|
|
355
|
-
}
|
|
356
|
-
return fromParent;
|
|
406
|
+
this.writePlanCache(this.multiProviderPlanCache, token, providers);
|
|
407
|
+
return [...providers];
|
|
357
408
|
}
|
|
358
409
|
providerGraphRequiresRequestScope(token, visited) {
|
|
359
410
|
if (visited.has(token)) {
|
|
@@ -559,11 +610,13 @@ export class Container {
|
|
|
559
610
|
return this.multiRequestCache;
|
|
560
611
|
}
|
|
561
612
|
lookupProvider(token) {
|
|
562
|
-
const
|
|
563
|
-
if (
|
|
564
|
-
return
|
|
613
|
+
const cached = this.readCachedPlan(this.providerLookupPlanCache, token);
|
|
614
|
+
if (cached) {
|
|
615
|
+
return cached.value;
|
|
565
616
|
}
|
|
566
|
-
|
|
617
|
+
const local = this.registrations.get(token);
|
|
618
|
+
const provider = local ?? this.parent?.lookupProvider(token);
|
|
619
|
+
return this.writePlanCache(this.providerLookupPlanCache, token, provider);
|
|
567
620
|
}
|
|
568
621
|
|
|
569
622
|
/**
|
|
@@ -673,10 +726,40 @@ export class Container {
|
|
|
673
726
|
if (this.parent) {
|
|
674
727
|
this.requestCache?.clear();
|
|
675
728
|
this.multiRequestCache?.clear();
|
|
729
|
+
this.clearResolutionPlanCaches();
|
|
676
730
|
return;
|
|
677
731
|
}
|
|
678
732
|
this.singletonCache.clear();
|
|
679
733
|
this.multiSingletonCache.clear();
|
|
734
|
+
this.clearResolutionPlanCaches();
|
|
735
|
+
}
|
|
736
|
+
currentLineageRevision() {
|
|
737
|
+
const parentRevision = this.parent?.currentLineageRevision();
|
|
738
|
+
return parentRevision ? `${parentRevision}/${this.graphRevision}` : String(this.graphRevision);
|
|
739
|
+
}
|
|
740
|
+
readCachedPlan(cache, token) {
|
|
741
|
+
const cached = cache.get(token);
|
|
742
|
+
if (!cached || cached.lineageRevision !== this.currentLineageRevision()) {
|
|
743
|
+
return undefined;
|
|
744
|
+
}
|
|
745
|
+
return cached;
|
|
746
|
+
}
|
|
747
|
+
writePlanCache(cache, token, value) {
|
|
748
|
+
cache.set(token, {
|
|
749
|
+
lineageRevision: this.currentLineageRevision(),
|
|
750
|
+
value
|
|
751
|
+
});
|
|
752
|
+
return value;
|
|
753
|
+
}
|
|
754
|
+
advanceGraphRevision() {
|
|
755
|
+
this.graphRevision += 1;
|
|
756
|
+
this.clearResolutionPlanCaches();
|
|
757
|
+
}
|
|
758
|
+
clearResolutionPlanCaches() {
|
|
759
|
+
this.providerLookupPlanCache.clear();
|
|
760
|
+
this.multiProviderPlanCache.clear();
|
|
761
|
+
this.requestScopeVerdictPlanCache.clear();
|
|
762
|
+
this.effectiveProviderPlanCache.clear();
|
|
680
763
|
}
|
|
681
764
|
async waitForStaleDisposalTasks() {
|
|
682
765
|
while (this.staleDisposalTasks.size > 0) {
|
|
@@ -707,6 +790,13 @@ export class Container {
|
|
|
707
790
|
throw new AggregateError(errors, 'Container disposal failed for one or more providers.');
|
|
708
791
|
}
|
|
709
792
|
}
|
|
793
|
+
collectDisposalError(error, errors) {
|
|
794
|
+
if (error instanceof AggregateError) {
|
|
795
|
+
errors.push(...error.errors);
|
|
796
|
+
return;
|
|
797
|
+
}
|
|
798
|
+
errors.push(error);
|
|
799
|
+
}
|
|
710
800
|
isDisposable(value) {
|
|
711
801
|
return typeof value === 'object' && value !== null && 'onDestroy' in value && typeof value.onDestroy === 'function';
|
|
712
802
|
}
|
|
@@ -741,19 +831,61 @@ export class Container {
|
|
|
741
831
|
if (provider.scope !== Scope.DEFAULT) {
|
|
742
832
|
return;
|
|
743
833
|
}
|
|
744
|
-
|
|
834
|
+
const requestScopedDependency = this.findRequestScopedDependency(provider.inject, new Set([provider.provide]));
|
|
835
|
+
if (requestScopedDependency) {
|
|
836
|
+
throw new ScopeMismatchError(`Singleton provider ${formatTokenName(provider.provide)} depends on request-scoped provider ${formatTokenName(requestScopedDependency)}.`, {
|
|
837
|
+
token: provider.provide,
|
|
838
|
+
scope: 'singleton',
|
|
839
|
+
hint: `Singleton providers cannot depend on request-scoped providers. Either change ${formatTokenName(requestScopedDependency)} to singleton/transient scope, or change ${formatTokenName(provider.provide)} to request scope.`
|
|
840
|
+
});
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
findRequestScopedDependency(depEntries, visited) {
|
|
844
|
+
for (const depEntry of depEntries) {
|
|
745
845
|
const depToken = this.resolveProviderDependencyToken(depEntry);
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
846
|
+
if (isOptionalToken(depEntry) && !this.has(depToken)) {
|
|
847
|
+
continue;
|
|
848
|
+
}
|
|
849
|
+
const requestScopedToken = this.findRequestScopedDependencyToken(depToken, visited);
|
|
850
|
+
if (requestScopedToken) {
|
|
851
|
+
return requestScopedToken;
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
return undefined;
|
|
855
|
+
}
|
|
856
|
+
findRequestScopedDependencyToken(token, visited) {
|
|
857
|
+
if (visited.has(token)) {
|
|
858
|
+
return undefined;
|
|
859
|
+
}
|
|
860
|
+
visited.add(token);
|
|
861
|
+
try {
|
|
862
|
+
const provider = this.resolveEffectiveProvider(token);
|
|
863
|
+
if (provider) {
|
|
864
|
+
if (provider.scope === Scope.REQUEST) {
|
|
865
|
+
return provider.provide;
|
|
866
|
+
}
|
|
867
|
+
return this.findRequestScopedDependency(provider.inject, visited);
|
|
868
|
+
}
|
|
869
|
+
if (typeof token !== 'function') {
|
|
870
|
+
return undefined;
|
|
871
|
+
}
|
|
872
|
+
const metadata = getClassDiMetadata(token);
|
|
873
|
+
if (metadata?.scope === Scope.REQUEST) {
|
|
874
|
+
return token;
|
|
753
875
|
}
|
|
876
|
+
return this.findRequestScopedDependency(metadata?.inject ?? [], visited);
|
|
877
|
+
} finally {
|
|
878
|
+
visited.delete(token);
|
|
754
879
|
}
|
|
755
880
|
}
|
|
756
881
|
resolveEffectiveProvider(token, visited = new Set(), chain = []) {
|
|
882
|
+
const cacheable = visited.size === 0 && chain.length === 0;
|
|
883
|
+
if (cacheable) {
|
|
884
|
+
const cached = this.readCachedPlan(this.effectiveProviderPlanCache, token);
|
|
885
|
+
if (cached) {
|
|
886
|
+
return cached.value;
|
|
887
|
+
}
|
|
888
|
+
}
|
|
757
889
|
let currentToken = token;
|
|
758
890
|
while (true) {
|
|
759
891
|
if (visited.has(currentToken)) {
|
|
@@ -762,9 +894,15 @@ export class Container {
|
|
|
762
894
|
visited.add(currentToken);
|
|
763
895
|
const provider = this.lookupProvider(currentToken);
|
|
764
896
|
if (!provider) {
|
|
897
|
+
if (cacheable) {
|
|
898
|
+
return this.writePlanCache(this.effectiveProviderPlanCache, token, undefined);
|
|
899
|
+
}
|
|
765
900
|
return undefined;
|
|
766
901
|
}
|
|
767
902
|
if (provider.type !== 'existing' || provider.useExisting === undefined) {
|
|
903
|
+
if (cacheable) {
|
|
904
|
+
return this.writePlanCache(this.effectiveProviderPlanCache, token, provider);
|
|
905
|
+
}
|
|
768
906
|
return provider;
|
|
769
907
|
}
|
|
770
908
|
chain.push(currentToken);
|
package/package.json
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"container",
|
|
10
10
|
"provider"
|
|
11
11
|
],
|
|
12
|
-
"version": "1.0.0-beta.
|
|
12
|
+
"version": "1.0.0-beta.7",
|
|
13
13
|
"private": false,
|
|
14
14
|
"license": "MIT",
|
|
15
15
|
"repository": {
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
"dist"
|
|
37
37
|
],
|
|
38
38
|
"dependencies": {
|
|
39
|
-
"@fluojs/core": "^1.0.0-beta.
|
|
39
|
+
"@fluojs/core": "^1.0.0-beta.5"
|
|
40
40
|
},
|
|
41
41
|
"devDependencies": {
|
|
42
42
|
"vitest": "^3.2.4"
|