@fluojs/di 1.1.0 → 3.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 +129 -26
- package/README.md +129 -26
- package/dist/container.d.ts +79 -17
- package/dist/container.d.ts.map +1 -1
- package/dist/container.js +564 -197
- package/dist/errors.d.ts +2 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +3 -2
- package/dist/internal.d.ts +18 -0
- package/dist/internal.d.ts.map +1 -0
- package/dist/internal.js +26 -0
- package/dist/multi-contribution-registry.d.ts +26 -0
- package/dist/multi-contribution-registry.d.ts.map +1 -0
- package/dist/multi-contribution-registry.js +29 -0
- package/dist/provider-normalization.d.ts +17 -0
- package/dist/provider-normalization.d.ts.map +1 -0
- package/dist/provider-normalization.js +216 -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 +9 -5
package/dist/container.js
CHANGED
|
@@ -1,138 +1,191 @@
|
|
|
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 { registerMultiContributionResolver } from './multi-contribution-registry.js';
|
|
5
|
+
import { normalizeProvider } from './provider-normalization.js';
|
|
6
|
+
import { isForwardRef, isOptionalToken, Scope } from './types.js';
|
|
5
7
|
|
|
6
8
|
/**
|
|
7
|
-
*
|
|
9
|
+
* Factory provider resolution mode recorded after a factory returns either synchronously or through a promise.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Controlled cache adoption seam for framework-owned testing and tooling that
|
|
14
|
+
* need synchronous helpers to preserve container-owned singleton disposal.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Read-only factory resolution diagnostics recorded by container-owned factory
|
|
19
|
+
* instantiation paths.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Public read-only seam for framework-owned testing and tooling that need to
|
|
8
24
|
* inspect a container's resolved provider graph without depending on private
|
|
9
25
|
* field names or structural casts.
|
|
10
26
|
*/
|
|
11
27
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
28
|
+
class ReadonlyMapView {
|
|
29
|
+
#source;
|
|
30
|
+
[Symbol.toStringTag] = 'Map';
|
|
31
|
+
constructor(source) {
|
|
32
|
+
this.#source = source;
|
|
33
|
+
}
|
|
34
|
+
get size() {
|
|
35
|
+
return this.#source.size;
|
|
36
|
+
}
|
|
37
|
+
entries() {
|
|
38
|
+
return this.#source.entries();
|
|
39
|
+
}
|
|
40
|
+
forEach(callbackfn, thisArg) {
|
|
41
|
+
for (const [key, value] of this.#source) {
|
|
42
|
+
callbackfn.call(thisArg, value, key, this);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
get(key) {
|
|
46
|
+
return this.#source.get(key);
|
|
47
|
+
}
|
|
48
|
+
has(key) {
|
|
49
|
+
return this.#source.has(key);
|
|
50
|
+
}
|
|
51
|
+
keys() {
|
|
52
|
+
return this.#source.keys();
|
|
53
|
+
}
|
|
54
|
+
values() {
|
|
55
|
+
return this.#source.values();
|
|
56
|
+
}
|
|
57
|
+
[Symbol.iterator]() {
|
|
58
|
+
return this.entries();
|
|
30
59
|
}
|
|
31
60
|
}
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
61
|
+
class ReadonlyMultiRegistrationMapView {
|
|
62
|
+
#source;
|
|
63
|
+
[Symbol.toStringTag] = 'Map';
|
|
64
|
+
constructor(source) {
|
|
65
|
+
this.#source = source;
|
|
36
66
|
}
|
|
67
|
+
get size() {
|
|
68
|
+
return this.#source.size;
|
|
69
|
+
}
|
|
70
|
+
*entries() {
|
|
71
|
+
for (const [token, providers] of this.#source) {
|
|
72
|
+
yield [token, Object.freeze([...providers])];
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
forEach(callbackfn, thisArg) {
|
|
76
|
+
for (const [key, value] of this.entries()) {
|
|
77
|
+
callbackfn.call(thisArg, value, key, this);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
get(key) {
|
|
81
|
+
const providers = this.#source.get(key);
|
|
82
|
+
return providers ? Object.freeze([...providers]) : undefined;
|
|
83
|
+
}
|
|
84
|
+
has(key) {
|
|
85
|
+
return this.#source.has(key);
|
|
86
|
+
}
|
|
87
|
+
keys() {
|
|
88
|
+
return this.#source.keys();
|
|
89
|
+
}
|
|
90
|
+
*values() {
|
|
91
|
+
for (const providers of this.#source.values()) {
|
|
92
|
+
yield Object.freeze([...providers]);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
[Symbol.iterator]() {
|
|
96
|
+
return this.entries();
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
function isPromiseLike(value) {
|
|
100
|
+
return (typeof value === 'object' || typeof value === 'function') && value !== null && typeof value.then === 'function';
|
|
37
101
|
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
102
|
+
const pendingResolutionContexts = new WeakMap();
|
|
103
|
+
const pendingResolutionOwners = new WeakMap();
|
|
104
|
+
function trackPendingResolution(promise, activeTokens) {
|
|
105
|
+
const context = pendingResolutionContexts.get(activeTokens) ?? {
|
|
106
|
+
dependencies: new Map(),
|
|
107
|
+
pendingPromises: 0
|
|
108
|
+
};
|
|
109
|
+
context.pendingPromises += 1;
|
|
110
|
+
pendingResolutionContexts.set(activeTokens, context);
|
|
111
|
+
pendingResolutionOwners.set(promise, activeTokens);
|
|
41
112
|
}
|
|
42
|
-
function
|
|
43
|
-
|
|
44
|
-
|
|
113
|
+
function untrackPendingResolution(promise, activeTokens) {
|
|
114
|
+
const context = pendingResolutionContexts.get(activeTokens);
|
|
115
|
+
pendingResolutionOwners.delete(promise);
|
|
116
|
+
if (!context) {
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
context.pendingPromises -= 1;
|
|
120
|
+
if (context.pendingPromises === 0 && context.dependencies.size === 0) {
|
|
121
|
+
pendingResolutionContexts.delete(activeTokens);
|
|
45
122
|
}
|
|
46
|
-
return token;
|
|
47
123
|
}
|
|
48
|
-
function
|
|
49
|
-
if (
|
|
50
|
-
|
|
51
|
-
return {
|
|
52
|
-
inject: (metadata?.inject ?? []).map(normalizeInjectToken),
|
|
53
|
-
provide: provider,
|
|
54
|
-
scope: metadata?.scope ?? Scope.DEFAULT,
|
|
55
|
-
type: 'class',
|
|
56
|
-
useClass: provider
|
|
57
|
-
};
|
|
124
|
+
function findPendingResolutionPath(start, target, visited) {
|
|
125
|
+
if (start === target) {
|
|
126
|
+
return [start];
|
|
58
127
|
}
|
|
59
|
-
if (
|
|
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
|
-
};
|
|
128
|
+
if (visited.has(start)) {
|
|
129
|
+
return undefined;
|
|
69
130
|
}
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
});
|
|
131
|
+
visited.add(start);
|
|
132
|
+
for (const dependency of pendingResolutionContexts.get(start)?.dependencies.keys() ?? []) {
|
|
133
|
+
const path = findPendingResolutionPath(dependency, target, visited);
|
|
134
|
+
if (path) {
|
|
135
|
+
return [start, ...path];
|
|
76
136
|
}
|
|
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
137
|
}
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
}
|
|
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
|
-
};
|
|
138
|
+
return undefined;
|
|
139
|
+
}
|
|
140
|
+
function linkPendingResolution(promise, activeTokens, token) {
|
|
141
|
+
const owner = pendingResolutionOwners.get(promise);
|
|
142
|
+
if (!owner || activeTokens.size === 0) {
|
|
143
|
+
return undefined;
|
|
103
144
|
}
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
145
|
+
const cyclePath = findPendingResolutionPath(owner, activeTokens, new Set());
|
|
146
|
+
if (cyclePath) {
|
|
147
|
+
throw new CircularDependencyError([...activeTokens, token, ...cyclePath.flatMap(context => [...context])]);
|
|
148
|
+
}
|
|
149
|
+
const context = pendingResolutionContexts.get(activeTokens) ?? {
|
|
150
|
+
dependencies: new Map(),
|
|
151
|
+
pendingPromises: 0
|
|
152
|
+
};
|
|
153
|
+
context.dependencies.set(owner, (context.dependencies.get(owner) ?? 0) + 1);
|
|
154
|
+
pendingResolutionContexts.set(activeTokens, context);
|
|
155
|
+
return () => {
|
|
156
|
+
const dependencyCount = context.dependencies.get(owner);
|
|
157
|
+
if (dependencyCount === undefined) {
|
|
158
|
+
return;
|
|
110
159
|
}
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
160
|
+
if (dependencyCount === 1) {
|
|
161
|
+
context.dependencies.delete(owner);
|
|
162
|
+
} else {
|
|
163
|
+
context.dependencies.set(owner, dependencyCount - 1);
|
|
164
|
+
}
|
|
165
|
+
if (context.pendingPromises === 0 && context.dependencies.size === 0) {
|
|
166
|
+
pendingResolutionContexts.delete(activeTokens);
|
|
167
|
+
}
|
|
168
|
+
};
|
|
120
169
|
}
|
|
121
170
|
|
|
122
171
|
/**
|
|
123
172
|
* Scope-aware dependency injection container for Fluo providers.
|
|
124
173
|
*/
|
|
174
|
+
// allow: SIZE_OK — Container is the package's existing DI lifecycle state machine.
|
|
125
175
|
export class Container {
|
|
176
|
+
static #childScopeConstruction;
|
|
126
177
|
registrations = new Map();
|
|
127
178
|
multiRegistrations = new Map();
|
|
128
179
|
multiOverriddenTokens = new Set();
|
|
129
180
|
requestCache;
|
|
130
181
|
multiRequestCache;
|
|
131
182
|
multiSingletonCache = new Map();
|
|
183
|
+
materializedCachePromises = [];
|
|
184
|
+
pendingDisposables = [];
|
|
132
185
|
staleDisposalTasks = new Set();
|
|
133
|
-
staleDisposalErrors = [];
|
|
134
186
|
singletonCache;
|
|
135
187
|
forwardRefTokenCache = new WeakMap();
|
|
188
|
+
factoryResolutionKinds = new WeakMap();
|
|
136
189
|
providerLookupPlanCache = new Map();
|
|
137
190
|
multiProviderPlanCache = new Map();
|
|
138
191
|
requestScopeVerdictPlanCache = new Map();
|
|
@@ -140,12 +193,40 @@ export class Container {
|
|
|
140
193
|
childScopes;
|
|
141
194
|
disposePromise;
|
|
142
195
|
disposed = false;
|
|
143
|
-
|
|
196
|
+
trackedByParent = false;
|
|
144
197
|
graphRevision = 0;
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
198
|
+
parent;
|
|
199
|
+
requestScopeEnabled;
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Creates a root container that owns its own singleton cache.
|
|
203
|
+
*
|
|
204
|
+
* Child request scopes are package-owned and must be created with
|
|
205
|
+
* {@link Container.createRequestScope}; caller-supplied parent, request-scope,
|
|
206
|
+
* or singleton-cache wiring is rejected.
|
|
207
|
+
*
|
|
208
|
+
* @throws {ContainerResolutionError} When any constructor argument is supplied.
|
|
209
|
+
*/
|
|
210
|
+
constructor(...construction) {
|
|
211
|
+
if (construction.length > 0) {
|
|
212
|
+
throw new ContainerResolutionError('Container child-scope construction is package-owned and cannot be invoked directly.', {
|
|
213
|
+
hint: 'Construct root containers with new Container() and create child scopes with container.createRequestScope().'
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
const childScope = Container.#childScopeConstruction;
|
|
217
|
+
this.parent = childScope?.parent;
|
|
218
|
+
this.requestScopeEnabled = childScope?.requestScopeEnabled ?? false;
|
|
219
|
+
this.singletonCache = childScope?.singletonCache ?? new Map();
|
|
220
|
+
registerMultiContributionResolver(this, (token, contributionIndex) => this.resolveMultiContribution(token, contributionIndex));
|
|
221
|
+
}
|
|
222
|
+
static #createChildScope(construction) {
|
|
223
|
+
const parentConstruction = Container.#childScopeConstruction;
|
|
224
|
+
Container.#childScopeConstruction = construction;
|
|
225
|
+
try {
|
|
226
|
+
return new Container();
|
|
227
|
+
} finally {
|
|
228
|
+
Container.#childScopeConstruction = parentConstruction;
|
|
229
|
+
}
|
|
149
230
|
}
|
|
150
231
|
|
|
151
232
|
/**
|
|
@@ -199,10 +280,16 @@ export class Container {
|
|
|
199
280
|
* set — the whole set is replaced. If you need to preserve other entries, re-register them
|
|
200
281
|
* together with the replacement in one `override()` call.
|
|
201
282
|
*
|
|
283
|
+
* **Batch atomicity**: the whole batch is validated before any registration or cache is touched,
|
|
284
|
+
* so a rejected `override()` call leaves every provider, cached instance, and disposal ownership
|
|
285
|
+
* exactly as it was before the call.
|
|
286
|
+
*
|
|
202
287
|
* @param providers Provider definitions that should replace existing registrations for each token.
|
|
203
288
|
* @returns The same container instance for fluent override chains.
|
|
204
289
|
* @throws {ContainerResolutionError} When called after the container was disposed.
|
|
290
|
+
* @throws {ScopeMismatchError} When a request-scope override would introduce a new singleton token.
|
|
205
291
|
* @throws {InvalidProviderError} When a provider definition is structurally invalid.
|
|
292
|
+
* @throws {DuplicateProviderError} When one token mixes single and multi replacements or repeats a single replacement.
|
|
206
293
|
*/
|
|
207
294
|
override(...providers) {
|
|
208
295
|
if (this.isDisposedInHierarchy()) {
|
|
@@ -220,6 +307,19 @@ export class Container {
|
|
|
220
307
|
}
|
|
221
308
|
normalizedByToken.set(normalized.provide, [normalized]);
|
|
222
309
|
}
|
|
310
|
+
if (this.requestScopeEnabled) {
|
|
311
|
+
for (const [token, normalizedProviders] of normalizedByToken) {
|
|
312
|
+
const introducesSingleton = normalizedProviders.some(normalized => normalized.scope === Scope.DEFAULT);
|
|
313
|
+
if (introducesSingleton && !this.has(token)) {
|
|
314
|
+
throw new ScopeMismatchError(`Singleton provider ${formatTokenName(token)} cannot be introduced by override() on a request-scope container.`, {
|
|
315
|
+
token,
|
|
316
|
+
scope: 'singleton',
|
|
317
|
+
hint: 'Register it on the root container before creating the request scope, or register a request/transient provider in the request scope.'
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
const plannedOverrides = new Map();
|
|
223
323
|
for (const [token, normalizedProviders] of normalizedByToken) {
|
|
224
324
|
const firstProvider = normalizedProviders[0];
|
|
225
325
|
if (!firstProvider) {
|
|
@@ -232,6 +332,17 @@ export class Container {
|
|
|
232
332
|
if (!containsMultiProvider && normalizedProviders.length > 1) {
|
|
233
333
|
throw new DuplicateProviderError(token);
|
|
234
334
|
}
|
|
335
|
+
plannedOverrides.set(token, {
|
|
336
|
+
containsMultiProvider,
|
|
337
|
+
firstProvider,
|
|
338
|
+
normalizedProviders
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
for (const [token, {
|
|
342
|
+
containsMultiProvider,
|
|
343
|
+
firstProvider,
|
|
344
|
+
normalizedProviders
|
|
345
|
+
}] of plannedOverrides) {
|
|
235
346
|
this.invalidateAffectedCachedEntriesInHierarchy(token);
|
|
236
347
|
this.registrations.delete(token);
|
|
237
348
|
this.multiRegistrations.delete(token);
|
|
@@ -263,21 +374,61 @@ export class Container {
|
|
|
263
374
|
*
|
|
264
375
|
* This method is the supported introspection seam for packages such as
|
|
265
376
|
* `@fluojs/testing`; callers should prefer ordinary `has(...)` and
|
|
266
|
-
* `resolve(...)` unless they need
|
|
267
|
-
* implementing a framework-level helper.
|
|
377
|
+
* `resolve(...)` unless they need read-only graph/cache visibility while
|
|
378
|
+
* implementing a framework-level helper. Cache adoption for synchronous
|
|
379
|
+
* helpers goes through `cacheOwner`; the returned maps are not mutable
|
|
380
|
+
* container internals.
|
|
268
381
|
*
|
|
269
|
-
* @returns
|
|
382
|
+
* @returns Read-only provider registrations and resolution caches for this container scope.
|
|
270
383
|
*/
|
|
271
384
|
inspectResolutionState() {
|
|
385
|
+
const registrations = new Map(this.registrations);
|
|
386
|
+
const multiRegistrations = new Map(Array.from(this.multiRegistrations, ([token, providers]) => [token, Object.freeze([...providers])]));
|
|
387
|
+
const multiSingletonCache = new Map(this.multiSingletonCache);
|
|
388
|
+
const singletonCache = new Map(this.singletonCache);
|
|
272
389
|
return {
|
|
390
|
+
cacheOwner: this.createCacheOwner(singletonCache, multiSingletonCache),
|
|
391
|
+
factoryResolutionKinds: this.createFactoryResolutionState(),
|
|
273
392
|
parent: this.parent?.inspectResolutionState(),
|
|
274
|
-
registrations:
|
|
275
|
-
multiRegistrations:
|
|
276
|
-
multiSingletonCache:
|
|
393
|
+
registrations: new ReadonlyMapView(registrations),
|
|
394
|
+
multiRegistrations: new ReadonlyMultiRegistrationMapView(multiRegistrations),
|
|
395
|
+
multiSingletonCache: new ReadonlyMapView(multiSingletonCache),
|
|
277
396
|
requestScopeEnabled: this.requestScopeEnabled,
|
|
278
|
-
singletonCache:
|
|
397
|
+
singletonCache: new ReadonlyMapView(singletonCache)
|
|
279
398
|
};
|
|
280
399
|
}
|
|
400
|
+
createCacheOwner(singletonCacheSnapshot, multiSingletonCacheSnapshot) {
|
|
401
|
+
return Object.freeze({
|
|
402
|
+
deleteMultiSingleton: provider => {
|
|
403
|
+
this.multiSingletonCache.delete(provider);
|
|
404
|
+
multiSingletonCacheSnapshot.delete(provider);
|
|
405
|
+
},
|
|
406
|
+
deleteSingleton: token => {
|
|
407
|
+
this.singletonCache.delete(token);
|
|
408
|
+
singletonCacheSnapshot.delete(token);
|
|
409
|
+
},
|
|
410
|
+
recordFactoryResolution: (provider, kind) => {
|
|
411
|
+
this.root().factoryResolutionKinds.set(provider, kind);
|
|
412
|
+
},
|
|
413
|
+
setMultiSingleton: (provider, promise) => {
|
|
414
|
+
this.multiSingletonCache.set(provider, promise);
|
|
415
|
+
multiSingletonCacheSnapshot.set(provider, promise);
|
|
416
|
+
this.trackCacheMaterialization(promise);
|
|
417
|
+
},
|
|
418
|
+
setSingleton: (token, promise) => {
|
|
419
|
+
this.singletonCache.set(token, promise);
|
|
420
|
+
singletonCacheSnapshot.set(token, promise);
|
|
421
|
+
this.trackCacheMaterialization(promise);
|
|
422
|
+
}
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
createFactoryResolutionState() {
|
|
426
|
+
const root = this.root();
|
|
427
|
+
return Object.freeze({
|
|
428
|
+
get: provider => root.factoryResolutionKinds.get(provider),
|
|
429
|
+
has: provider => root.factoryResolutionKinds.has(provider)
|
|
430
|
+
});
|
|
431
|
+
}
|
|
281
432
|
|
|
282
433
|
/**
|
|
283
434
|
* Returns whether resolving a token may require a request-scope container.
|
|
@@ -305,7 +456,11 @@ export class Container {
|
|
|
305
456
|
hint: 'Create request scopes before calling container.dispose().'
|
|
306
457
|
});
|
|
307
458
|
}
|
|
308
|
-
return
|
|
459
|
+
return Container.#createChildScope({
|
|
460
|
+
parent: this,
|
|
461
|
+
requestScopeEnabled: true,
|
|
462
|
+
singletonCache: this.root().singletonCache
|
|
463
|
+
});
|
|
309
464
|
}
|
|
310
465
|
|
|
311
466
|
/**
|
|
@@ -325,42 +480,79 @@ export class Container {
|
|
|
325
480
|
hint: 'Ensure all resolves complete before calling container.dispose().'
|
|
326
481
|
});
|
|
327
482
|
}
|
|
483
|
+
await this.assertStaleDisposalsSettled();
|
|
328
484
|
return this.resolveWithChain(token, [], new Set());
|
|
329
485
|
}
|
|
486
|
+
async resolveMultiContribution(token, contributionIndex) {
|
|
487
|
+
if (this.isDisposedInHierarchy()) {
|
|
488
|
+
throw new ContainerResolutionError('Container has been disposed and can no longer resolve providers.', {
|
|
489
|
+
token,
|
|
490
|
+
hint: 'Ensure all resolves complete before calling container.dispose().'
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
await this.assertStaleDisposalsSettled();
|
|
494
|
+
const providers = this.collectMultiProviders(token);
|
|
495
|
+
const provider = providers[contributionIndex];
|
|
496
|
+
if (!provider) {
|
|
497
|
+
throw new ContainerResolutionError(`Multi-provider contribution ${contributionIndex} is not registered for ${formatTokenName(token)}.`, {
|
|
498
|
+
token,
|
|
499
|
+
hint: 'Resolve a contribution index returned by the registered multi-provider order.'
|
|
500
|
+
});
|
|
501
|
+
}
|
|
502
|
+
this.assertSingletonDependencyScopes(provider);
|
|
503
|
+
return await this.withTokenInChain(token, [], new Set(), async (chain, activeTokens) => this.resolveMultiProviderInstance(provider, chain, activeTokens));
|
|
504
|
+
}
|
|
330
505
|
|
|
331
506
|
/**
|
|
332
507
|
* Disposes cached instances and nested request scopes.
|
|
333
508
|
*
|
|
509
|
+
* Concurrent callers share the active disposal attempt. After a failed attempt,
|
|
510
|
+
* a later explicit call retries only `onDestroy()` hooks that did not complete;
|
|
511
|
+
* successfully completed hooks are never repeated. Disposal remains terminal
|
|
512
|
+
* for registration, resolution, overrides, and child-scope creation. A directly
|
|
513
|
+
* disposed child owns its remaining retries after that attempt settles, while a
|
|
514
|
+
* parent-started failed attempt remains owned by the parent hierarchy.
|
|
515
|
+
*
|
|
334
516
|
* @returns A promise that settles after all cached disposable instances are torn down.
|
|
335
517
|
* @throws {Error} Propagates one or more disposal errors (`AggregateError` when multiple failures occur).
|
|
336
518
|
*/
|
|
337
519
|
async dispose() {
|
|
520
|
+
await this.disposeWithOrigin('direct');
|
|
521
|
+
}
|
|
522
|
+
async disposeFromParent() {
|
|
523
|
+
await this.disposeWithOrigin('parent');
|
|
524
|
+
}
|
|
525
|
+
async disposeWithOrigin(origin) {
|
|
338
526
|
if (this.disposePromise) {
|
|
339
527
|
await this.disposePromise;
|
|
340
528
|
return;
|
|
341
529
|
}
|
|
342
530
|
this.disposed = true;
|
|
343
531
|
this.advanceGraphRevision();
|
|
344
|
-
|
|
532
|
+
const activeDispose = this.disposeAll(origin);
|
|
533
|
+
this.disposePromise = activeDispose;
|
|
345
534
|
try {
|
|
346
|
-
await
|
|
535
|
+
await activeDispose;
|
|
536
|
+
if (this.disposePromise === activeDispose && this.hasRetainedStaleDisposalTasksInSubtree()) {
|
|
537
|
+
this.disposePromise = undefined;
|
|
538
|
+
}
|
|
347
539
|
} catch (error) {
|
|
348
540
|
this.disposePromise = undefined;
|
|
349
541
|
throw error;
|
|
350
542
|
}
|
|
351
543
|
}
|
|
352
|
-
async disposeAll() {
|
|
544
|
+
async disposeAll(origin) {
|
|
353
545
|
const errors = [];
|
|
546
|
+
let completed = false;
|
|
354
547
|
try {
|
|
355
|
-
// Dispose all live request-scope children
|
|
356
|
-
if (
|
|
357
|
-
const childResults = await Promise.allSettled(Array.from(this.childScopes).map(child => child.
|
|
548
|
+
// Dispose all live request-scope children before tearing down this scope's cache.
|
|
549
|
+
if (this.childScopes && this.childScopes.size > 0) {
|
|
550
|
+
const childResults = await Promise.allSettled(Array.from(this.childScopes).map(child => child.disposeFromParent()));
|
|
358
551
|
for (const result of childResults) {
|
|
359
552
|
if (result.status === 'rejected') {
|
|
360
553
|
this.collectDisposalError(result.reason, errors);
|
|
361
554
|
}
|
|
362
555
|
}
|
|
363
|
-
this.childScopes.clear();
|
|
364
556
|
}
|
|
365
557
|
try {
|
|
366
558
|
await this.disposeCache(this.disposalCacheEntries());
|
|
@@ -368,10 +560,15 @@ export class Container {
|
|
|
368
560
|
this.collectDisposalError(error, errors);
|
|
369
561
|
}
|
|
370
562
|
this.throwDisposalErrors(errors);
|
|
563
|
+
completed = true;
|
|
371
564
|
} finally {
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
565
|
+
const retainsStaleRetries = this.hasRetainedStaleDisposalTasksInSubtree();
|
|
566
|
+
if ((origin === 'direct' || completed && !retainsStaleRetries) && this.parent && this.trackedByParent) {
|
|
567
|
+
if (origin === 'direct') {
|
|
568
|
+
this.releaseNonOwnerStaleTaskObserversInSubtree();
|
|
569
|
+
}
|
|
570
|
+
this.parent.childScopes?.delete(this);
|
|
571
|
+
this.trackedByParent = false;
|
|
375
572
|
}
|
|
376
573
|
}
|
|
377
574
|
}
|
|
@@ -503,7 +700,12 @@ export class Container {
|
|
|
503
700
|
}
|
|
504
701
|
const cachedInstance = this.getCachedScopedOrSingletonInstance(provider);
|
|
505
702
|
if (cachedInstance) {
|
|
506
|
-
|
|
703
|
+
const releasePendingResolution = linkPendingResolution(cachedInstance, activeTokens, token);
|
|
704
|
+
try {
|
|
705
|
+
return await cachedInstance;
|
|
706
|
+
} finally {
|
|
707
|
+
releasePendingResolution?.();
|
|
708
|
+
}
|
|
507
709
|
}
|
|
508
710
|
return await this.withTokenInChain(token, chain, activeTokens, async (c, at) => this.resolveScopedOrSingletonInstance(provider, c, at));
|
|
509
711
|
}
|
|
@@ -543,16 +745,29 @@ export class Container {
|
|
|
543
745
|
if (provider.scope === 'transient') {
|
|
544
746
|
return await this.instantiate(provider, chain, activeTokens);
|
|
545
747
|
}
|
|
546
|
-
|
|
547
|
-
|
|
748
|
+
const cacheOwner = this.cacheOwnerFor(provider);
|
|
749
|
+
if (cacheOwner !== this) {
|
|
750
|
+
return await cacheOwner.resolveMultiProviderInstance(provider, chain, activeTokens);
|
|
548
751
|
}
|
|
549
752
|
const cache = this.multiCacheFor(provider);
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
}
|
|
555
|
-
|
|
753
|
+
const cachedInstance = cache.get(provider);
|
|
754
|
+
if (cachedInstance) {
|
|
755
|
+
const releasePendingResolution = linkPendingResolution(cachedInstance, activeTokens, provider.provide);
|
|
756
|
+
return releasePendingResolution ? cachedInstance.finally(releasePendingResolution) : cachedInstance;
|
|
757
|
+
}
|
|
758
|
+
let promise;
|
|
759
|
+
promise = this.instantiate(provider, chain, activeTokens).then(value => {
|
|
760
|
+
untrackPendingResolution(promise, activeTokens);
|
|
761
|
+
return value;
|
|
762
|
+
}, error => {
|
|
763
|
+
cache.delete(provider);
|
|
764
|
+
untrackPendingResolution(promise, activeTokens);
|
|
765
|
+
throw error;
|
|
766
|
+
});
|
|
767
|
+
trackPendingResolution(promise, activeTokens);
|
|
768
|
+
cache.set(provider, promise);
|
|
769
|
+
this.trackCacheMaterialization(promise);
|
|
770
|
+
return promise;
|
|
556
771
|
}
|
|
557
772
|
resolveExistingProviderTarget(provider) {
|
|
558
773
|
if (provider.type !== 'existing') {
|
|
@@ -561,33 +776,48 @@ export class Container {
|
|
|
561
776
|
return provider.useExisting;
|
|
562
777
|
}
|
|
563
778
|
async resolveScopedOrSingletonInstance(provider, chain, activeTokens) {
|
|
564
|
-
|
|
565
|
-
|
|
779
|
+
const cacheOwner = this.cacheOwnerFor(provider);
|
|
780
|
+
if (cacheOwner !== this) {
|
|
781
|
+
return await cacheOwner.resolveScopedOrSingletonInstance(provider, chain, activeTokens);
|
|
566
782
|
}
|
|
567
783
|
const cache = this.cacheFor(provider);
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
784
|
+
const cachedInstance = cache.get(provider.provide);
|
|
785
|
+
if (cachedInstance) {
|
|
786
|
+
const releasePendingResolution = linkPendingResolution(cachedInstance, activeTokens, provider.provide);
|
|
787
|
+
return releasePendingResolution ? cachedInstance.finally(releasePendingResolution) : cachedInstance;
|
|
788
|
+
}
|
|
789
|
+
let promise;
|
|
790
|
+
promise = this.instantiate(provider, chain, activeTokens).then(value => {
|
|
791
|
+
untrackPendingResolution(promise, activeTokens);
|
|
792
|
+
return value;
|
|
793
|
+
}, error => {
|
|
794
|
+
cache.delete(provider.provide);
|
|
795
|
+
untrackPendingResolution(promise, activeTokens);
|
|
796
|
+
throw error;
|
|
797
|
+
});
|
|
798
|
+
trackPendingResolution(promise, activeTokens);
|
|
799
|
+
cache.set(provider.provide, promise);
|
|
800
|
+
this.trackCacheMaterialization(promise);
|
|
801
|
+
return promise;
|
|
576
802
|
}
|
|
577
803
|
getCachedScopedOrSingletonInstance(provider) {
|
|
578
804
|
if (provider.scope !== Scope.DEFAULT) {
|
|
579
805
|
return undefined;
|
|
580
806
|
}
|
|
581
|
-
|
|
582
|
-
|
|
807
|
+
const cacheOwner = this.cacheOwnerFor(provider);
|
|
808
|
+
if (cacheOwner !== this) {
|
|
809
|
+
return cacheOwner.getCachedScopedOrSingletonInstance(provider);
|
|
583
810
|
}
|
|
584
811
|
return this.cacheFor(provider).get(provider.provide);
|
|
585
812
|
}
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
813
|
+
cacheOwnerFor(provider) {
|
|
814
|
+
if (provider.scope !== Scope.DEFAULT || !this.requestScopeEnabled) {
|
|
815
|
+
return this;
|
|
816
|
+
}
|
|
817
|
+
if (this.registrations.get(provider.provide) === provider || this.hasLocalMultiProvider(provider)) {
|
|
818
|
+
return this;
|
|
819
|
+
}
|
|
820
|
+
return this.parent?.cacheOwnerFor(provider) ?? this;
|
|
591
821
|
}
|
|
592
822
|
async resolveDepToken(depEntry, chain, activeTokens) {
|
|
593
823
|
if (isOptionalToken(depEntry)) {
|
|
@@ -617,13 +847,13 @@ export class Container {
|
|
|
617
847
|
return this.parent ? this.parent.root() : this;
|
|
618
848
|
}
|
|
619
849
|
ensureTrackedRequestScope() {
|
|
620
|
-
if (!this.requestScopeEnabled || !this.parent || this.
|
|
850
|
+
if (!this.requestScopeEnabled || !this.parent || this.trackedByParent) {
|
|
621
851
|
return;
|
|
622
852
|
}
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
this.
|
|
853
|
+
this.parent.ensureTrackedRequestScope();
|
|
854
|
+
this.parent.childScopes ??= new Set();
|
|
855
|
+
this.parent.childScopes.add(this);
|
|
856
|
+
this.trackedByParent = true;
|
|
627
857
|
}
|
|
628
858
|
requestCacheForWrite() {
|
|
629
859
|
this.ensureTrackedRequestScope();
|
|
@@ -706,12 +936,22 @@ export class Container {
|
|
|
706
936
|
return entries;
|
|
707
937
|
}
|
|
708
938
|
async disposeCache(entries) {
|
|
709
|
-
|
|
939
|
+
const errors = [];
|
|
940
|
+
const retryableStaleTasks = this.retainedStaleDisposalTasks();
|
|
941
|
+
const attemptedStaleInstances = new Set();
|
|
942
|
+
try {
|
|
943
|
+
await this.assertStaleDisposalsSettled();
|
|
944
|
+
} catch (error) {
|
|
945
|
+
this.collectDisposalError(error, errors);
|
|
946
|
+
}
|
|
947
|
+
errors.push(...(await this.retryFailedStaleDisposals(retryableStaleTasks, attemptedStaleInstances)));
|
|
710
948
|
const {
|
|
711
|
-
disposables,
|
|
712
|
-
errors
|
|
949
|
+
disposables: materializedDisposables,
|
|
950
|
+
errors: resolutionErrors
|
|
713
951
|
} = await this.collectDisposableInstances(entries);
|
|
714
|
-
|
|
952
|
+
const disposalCandidates = this.pendingDisposables.length > 0 ? this.pendingDisposables : materializedDisposables;
|
|
953
|
+
const disposables = disposalCandidates.filter(instance => !attemptedStaleInstances.has(instance));
|
|
954
|
+
errors.push(...resolutionErrors);
|
|
715
955
|
errors.push(...(await this.disposeInstancesInReverseOrder(disposables)));
|
|
716
956
|
this.clearDisposalCaches();
|
|
717
957
|
this.throwDisposalErrors(errors);
|
|
@@ -720,13 +960,18 @@ export class Container {
|
|
|
720
960
|
const disposables = [];
|
|
721
961
|
const seenInstances = new Set();
|
|
722
962
|
const errors = [];
|
|
963
|
+
const activePromises = new Set(entries.map(([, promise]) => promise));
|
|
723
964
|
const settled = await Promise.allSettled(entries.map(([, p]) => p));
|
|
724
965
|
for (const result of settled) {
|
|
725
966
|
if (result.status === 'rejected') {
|
|
726
967
|
errors.push(result.reason);
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
for (const promise of this.materializedCachePromises) {
|
|
971
|
+
if (!activePromises.has(promise)) {
|
|
727
972
|
continue;
|
|
728
973
|
}
|
|
729
|
-
const instance =
|
|
974
|
+
const instance = await promise;
|
|
730
975
|
if (this.isDisposable(instance) && !seenInstances.has(instance)) {
|
|
731
976
|
seenInstances.add(instance);
|
|
732
977
|
disposables.push(instance);
|
|
@@ -739,16 +984,20 @@ export class Container {
|
|
|
739
984
|
}
|
|
740
985
|
async disposeInstancesInReverseOrder(disposables) {
|
|
741
986
|
const errors = [];
|
|
987
|
+
const pendingDisposables = [];
|
|
742
988
|
for (const instance of [...disposables].reverse()) {
|
|
743
989
|
try {
|
|
744
990
|
await instance.onDestroy();
|
|
745
991
|
} catch (error) {
|
|
746
992
|
errors.push(error);
|
|
993
|
+
pendingDisposables.unshift(instance);
|
|
747
994
|
}
|
|
748
995
|
}
|
|
996
|
+
this.pendingDisposables.splice(0, this.pendingDisposables.length, ...pendingDisposables);
|
|
749
997
|
return errors;
|
|
750
998
|
}
|
|
751
999
|
clearDisposalCaches() {
|
|
1000
|
+
this.materializedCachePromises.length = 0;
|
|
752
1001
|
if (this.parent) {
|
|
753
1002
|
this.requestCache?.clear();
|
|
754
1003
|
this.multiRequestCache?.clear();
|
|
@@ -759,6 +1008,9 @@ export class Container {
|
|
|
759
1008
|
this.multiSingletonCache.clear();
|
|
760
1009
|
this.clearResolutionPlanCaches();
|
|
761
1010
|
}
|
|
1011
|
+
trackCacheMaterialization(promise) {
|
|
1012
|
+
void promise.then(() => this.materializedCachePromises.push(promise), () => undefined);
|
|
1013
|
+
}
|
|
762
1014
|
currentLineageRevision() {
|
|
763
1015
|
const parentRevision = this.parent?.currentLineageRevision();
|
|
764
1016
|
return parentRevision ? `${parentRevision}/${this.graphRevision}` : String(this.graphRevision);
|
|
@@ -787,26 +1039,129 @@ export class Container {
|
|
|
787
1039
|
this.requestScopeVerdictPlanCache.clear();
|
|
788
1040
|
this.effectiveProviderPlanCache.clear();
|
|
789
1041
|
}
|
|
790
|
-
async
|
|
791
|
-
|
|
792
|
-
|
|
1042
|
+
async assertStaleDisposalsSettled() {
|
|
1043
|
+
const errors = [];
|
|
1044
|
+
const settled = new Set();
|
|
1045
|
+
while (true) {
|
|
1046
|
+
const tasks = Array.from(this.staleDisposalTasks).filter(task => !settled.has(task));
|
|
1047
|
+
if (tasks.length === 0) {
|
|
1048
|
+
break;
|
|
1049
|
+
}
|
|
1050
|
+
await Promise.all(tasks.map(task => task.promise));
|
|
1051
|
+
for (const task of tasks) {
|
|
1052
|
+
settled.add(task);
|
|
1053
|
+
if (!task.failed) {
|
|
1054
|
+
this.staleDisposalTasks.delete(task);
|
|
1055
|
+
continue;
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
// A rejected materialization has no hook to retry. Deliver its error
|
|
1059
|
+
// once, then release it from every observer's stale-task ledger.
|
|
1060
|
+
if (!task.errorConsumed) {
|
|
1061
|
+
task.errorConsumed = true;
|
|
1062
|
+
errors.push(task.error);
|
|
1063
|
+
}
|
|
1064
|
+
if (!task.retryInstance) {
|
|
1065
|
+
for (const observer of task.observers) {
|
|
1066
|
+
observer.staleDisposalTasks.delete(task);
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
793
1070
|
}
|
|
1071
|
+
this.throwDisposalErrors(errors);
|
|
1072
|
+
}
|
|
1073
|
+
retainedStaleDisposalTasks() {
|
|
1074
|
+
// Only the scheduling container retries a stale hook, and only when the
|
|
1075
|
+
// failure was already delivered to an earlier caller. A failure first
|
|
1076
|
+
// surfaced by this attempt is reported, not retried within the same pass.
|
|
1077
|
+
return Array.from(this.staleDisposalTasks).filter(task => task.retryOwner === this && task.failed && task.errorConsumed);
|
|
1078
|
+
}
|
|
1079
|
+
hasRetainedStaleDisposalTasksInSubtree() {
|
|
1080
|
+
return this.retainedStaleDisposalTasks().length > 0 || Array.from(this.childScopes ?? []).some(childScope => childScope.hasRetainedStaleDisposalTasksInSubtree());
|
|
794
1081
|
}
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
1082
|
+
releaseNonOwnerStaleTaskObservers() {
|
|
1083
|
+
for (const task of this.staleDisposalTasks) {
|
|
1084
|
+
if (task.retryOwner !== this) {
|
|
1085
|
+
continue;
|
|
1086
|
+
}
|
|
1087
|
+
for (const observer of task.observers) {
|
|
1088
|
+
if (observer !== this) {
|
|
1089
|
+
observer.staleDisposalTasks.delete(task);
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
releaseNonOwnerStaleTaskObserversInSubtree() {
|
|
1095
|
+
this.releaseNonOwnerStaleTaskObservers();
|
|
1096
|
+
for (const childScope of this.childScopes ?? []) {
|
|
1097
|
+
childScope.releaseNonOwnerStaleTaskObserversInSubtree();
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
async retryFailedStaleDisposals(tasks, attemptedInstances) {
|
|
1101
|
+
const errors = [];
|
|
1102
|
+
for (const task of tasks) {
|
|
1103
|
+
const instance = task.retryInstance;
|
|
1104
|
+
if (!task.failed || !instance) {
|
|
1105
|
+
continue;
|
|
1106
|
+
}
|
|
1107
|
+
attemptedInstances.add(instance);
|
|
798
1108
|
try {
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
1109
|
+
await instance.onDestroy();
|
|
1110
|
+
task.failed = false;
|
|
1111
|
+
task.retryInstance = undefined;
|
|
1112
|
+
for (const observer of task.observers) {
|
|
1113
|
+
observer.staleDisposalTasks.delete(task);
|
|
802
1114
|
}
|
|
803
1115
|
} catch (error) {
|
|
804
|
-
|
|
1116
|
+
task.error = error;
|
|
1117
|
+
task.errorConsumed = true;
|
|
1118
|
+
errors.push(error);
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
return errors;
|
|
1122
|
+
}
|
|
1123
|
+
scheduleStaleDisposal(instancePromise, staleDisposalOwner) {
|
|
1124
|
+
const observers = staleDisposalOwner === this ? [this] : [this, staleDisposalOwner];
|
|
1125
|
+
const task = {
|
|
1126
|
+
error: undefined,
|
|
1127
|
+
errorConsumed: false,
|
|
1128
|
+
failed: false,
|
|
1129
|
+
observers,
|
|
1130
|
+
promise: Promise.resolve(),
|
|
1131
|
+
retryInstance: undefined,
|
|
1132
|
+
retryOwner: this
|
|
1133
|
+
};
|
|
1134
|
+
task.promise = (async () => {
|
|
1135
|
+
let instance;
|
|
1136
|
+
try {
|
|
1137
|
+
instance = await instancePromise;
|
|
1138
|
+
} catch (error) {
|
|
1139
|
+
task.error = error;
|
|
1140
|
+
task.failed = true;
|
|
1141
|
+
return;
|
|
1142
|
+
}
|
|
1143
|
+
if (!this.isDisposable(instance)) {
|
|
1144
|
+
return;
|
|
1145
|
+
}
|
|
1146
|
+
try {
|
|
1147
|
+
await instance.onDestroy();
|
|
1148
|
+
} catch (error) {
|
|
1149
|
+
task.error = error;
|
|
1150
|
+
task.failed = true;
|
|
1151
|
+
task.retryInstance = instance;
|
|
805
1152
|
}
|
|
806
1153
|
})().finally(() => {
|
|
807
|
-
this.
|
|
1154
|
+
const retainedMaterializations = this.materializedCachePromises.filter(promise => promise !== instancePromise);
|
|
1155
|
+
this.materializedCachePromises.splice(0, this.materializedCachePromises.length, ...retainedMaterializations);
|
|
1156
|
+
if (!task.failed) {
|
|
1157
|
+
for (const observer of observers) {
|
|
1158
|
+
observer.staleDisposalTasks.delete(task);
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
808
1161
|
});
|
|
809
|
-
|
|
1162
|
+
for (const observer of observers) {
|
|
1163
|
+
observer.staleDisposalTasks.add(task);
|
|
1164
|
+
}
|
|
810
1165
|
}
|
|
811
1166
|
throwDisposalErrors(errors) {
|
|
812
1167
|
if (errors.length === 1) {
|
|
@@ -839,7 +1194,9 @@ export class Container {
|
|
|
839
1194
|
throw new InvariantError('Factory provider is missing useFactory.');
|
|
840
1195
|
}
|
|
841
1196
|
const deps = await this.resolveProviderDeps(provider, chain, activeTokens);
|
|
842
|
-
|
|
1197
|
+
const value = provider.useFactory(...deps);
|
|
1198
|
+
this.root().factoryResolutionKinds.set(provider, isPromiseLike(value) ? 'async' : 'sync');
|
|
1199
|
+
return value;
|
|
843
1200
|
}
|
|
844
1201
|
case 'class':
|
|
845
1202
|
{
|
|
@@ -885,6 +1242,10 @@ export class Container {
|
|
|
885
1242
|
}
|
|
886
1243
|
visited.add(token);
|
|
887
1244
|
try {
|
|
1245
|
+
const multiRequestScopedToken = this.findRequestScopedMultiContribution(token, visited);
|
|
1246
|
+
if (multiRequestScopedToken) {
|
|
1247
|
+
return multiRequestScopedToken;
|
|
1248
|
+
}
|
|
888
1249
|
const provider = this.resolveEffectiveProvider(token);
|
|
889
1250
|
if (provider) {
|
|
890
1251
|
if (provider.scope === Scope.REQUEST) {
|
|
@@ -904,6 +1265,28 @@ export class Container {
|
|
|
904
1265
|
visited.delete(token);
|
|
905
1266
|
}
|
|
906
1267
|
}
|
|
1268
|
+
findRequestScopedMultiContribution(token, visited) {
|
|
1269
|
+
const aliasChain = new Set();
|
|
1270
|
+
let currentToken = token;
|
|
1271
|
+
while (!aliasChain.has(currentToken)) {
|
|
1272
|
+
aliasChain.add(currentToken);
|
|
1273
|
+
for (const multiProvider of this.collectMultiProviders(currentToken)) {
|
|
1274
|
+
if (multiProvider.scope === Scope.REQUEST) {
|
|
1275
|
+
return multiProvider.provide;
|
|
1276
|
+
}
|
|
1277
|
+
const requestScopedToken = multiProvider.type === 'existing' && multiProvider.useExisting !== undefined ? this.findRequestScopedDependencyToken(multiProvider.useExisting, visited) : this.findRequestScopedDependency(multiProvider.inject, visited);
|
|
1278
|
+
if (requestScopedToken) {
|
|
1279
|
+
return requestScopedToken;
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1282
|
+
const aliasProvider = this.lookupProvider(currentToken);
|
|
1283
|
+
if (aliasProvider?.type !== 'existing' || aliasProvider.useExisting === undefined) {
|
|
1284
|
+
return undefined;
|
|
1285
|
+
}
|
|
1286
|
+
currentToken = aliasProvider.useExisting;
|
|
1287
|
+
}
|
|
1288
|
+
return undefined;
|
|
1289
|
+
}
|
|
907
1290
|
resolveEffectiveProvider(token, visited = new Set(), chain = []) {
|
|
908
1291
|
const cacheable = visited.size === 0 && chain.length === 0;
|
|
909
1292
|
if (cacheable) {
|
|
@@ -959,34 +1342,18 @@ export class Container {
|
|
|
959
1342
|
}
|
|
960
1343
|
return deps;
|
|
961
1344
|
}
|
|
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;
|
|
1345
|
+
invalidateAffectedCachedEntriesInHierarchy(token, staleDisposalOwner = this) {
|
|
1346
|
+
this.invalidateAffectedCachedEntries(token, staleDisposalOwner);
|
|
1347
|
+
for (const childScope of this.childScopes ?? []) {
|
|
1348
|
+
childScope.invalidateAffectedCachedEntriesInHierarchy(token, staleDisposalOwner);
|
|
981
1349
|
}
|
|
982
|
-
return false;
|
|
983
1350
|
}
|
|
984
|
-
invalidateAffectedCachedEntries(token) {
|
|
1351
|
+
invalidateAffectedCachedEntries(token, staleDisposalOwner) {
|
|
985
1352
|
for (const [cachedToken, cached] of this.requestCache?.entries() ?? []) {
|
|
986
1353
|
if (!this.shouldInvalidateCachedToken(cachedToken, token)) {
|
|
987
1354
|
continue;
|
|
988
1355
|
}
|
|
989
|
-
this.scheduleStaleDisposal(cached);
|
|
1356
|
+
this.scheduleStaleDisposal(cached, staleDisposalOwner);
|
|
990
1357
|
this.requestCache?.delete(cachedToken);
|
|
991
1358
|
}
|
|
992
1359
|
if (!this.parent) {
|
|
@@ -994,7 +1361,7 @@ export class Container {
|
|
|
994
1361
|
if (!this.shouldInvalidateCachedToken(cachedToken, token)) {
|
|
995
1362
|
continue;
|
|
996
1363
|
}
|
|
997
|
-
this.scheduleStaleDisposal(cached);
|
|
1364
|
+
this.scheduleStaleDisposal(cached, staleDisposalOwner);
|
|
998
1365
|
this.singletonCache.delete(cachedToken);
|
|
999
1366
|
}
|
|
1000
1367
|
}
|
|
@@ -1003,7 +1370,7 @@ export class Container {
|
|
|
1003
1370
|
if (!this.shouldInvalidateCachedProvider(provider, token)) {
|
|
1004
1371
|
continue;
|
|
1005
1372
|
}
|
|
1006
|
-
this.scheduleStaleDisposal(cached);
|
|
1373
|
+
this.scheduleStaleDisposal(cached, staleDisposalOwner);
|
|
1007
1374
|
this.multiSingletonCache.delete(provider);
|
|
1008
1375
|
}
|
|
1009
1376
|
}
|
|
@@ -1015,7 +1382,7 @@ export class Container {
|
|
|
1015
1382
|
if (!this.shouldInvalidateCachedProvider(provider, token)) {
|
|
1016
1383
|
continue;
|
|
1017
1384
|
}
|
|
1018
|
-
this.scheduleStaleDisposal(cached);
|
|
1385
|
+
this.scheduleStaleDisposal(cached, staleDisposalOwner);
|
|
1019
1386
|
multiRequestCache.delete(provider);
|
|
1020
1387
|
}
|
|
1021
1388
|
}
|