@fluojs/di 2.0.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 +89 -7
- package/README.md +89 -7
- package/dist/container.d.ts +39 -5
- package/dist/container.d.ts.map +1 -1
- package/dist/container.js +382 -48
- 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 +15 -0
- package/dist/internal.d.ts.map +1 -1
- package/dist/internal.js +25 -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.map +1 -1
- package/dist/provider-normalization.js +9 -4
- package/package.json +5 -5
package/dist/container.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { formatTokenName, InvariantError } from '@fluojs/core';
|
|
2
2
|
import { getClassDiMetadata } from '@fluojs/core/internal';
|
|
3
3
|
import { CircularDependencyError, ContainerResolutionError, DuplicateProviderError, RequestScopeResolutionError, ScopeMismatchError } from './errors.js';
|
|
4
|
+
import { registerMultiContributionResolver } from './multi-contribution-registry.js';
|
|
4
5
|
import { normalizeProvider } from './provider-normalization.js';
|
|
5
6
|
import { isForwardRef, isOptionalToken, Scope } from './types.js';
|
|
6
7
|
|
|
@@ -98,17 +99,89 @@ class ReadonlyMultiRegistrationMapView {
|
|
|
98
99
|
function isPromiseLike(value) {
|
|
99
100
|
return (typeof value === 'object' || typeof value === 'function') && value !== null && typeof value.then === 'function';
|
|
100
101
|
}
|
|
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);
|
|
112
|
+
}
|
|
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);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
function findPendingResolutionPath(start, target, visited) {
|
|
125
|
+
if (start === target) {
|
|
126
|
+
return [start];
|
|
127
|
+
}
|
|
128
|
+
if (visited.has(start)) {
|
|
129
|
+
return undefined;
|
|
130
|
+
}
|
|
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];
|
|
136
|
+
}
|
|
137
|
+
}
|
|
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;
|
|
144
|
+
}
|
|
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;
|
|
159
|
+
}
|
|
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
|
+
};
|
|
169
|
+
}
|
|
101
170
|
|
|
102
171
|
/**
|
|
103
172
|
* Scope-aware dependency injection container for Fluo providers.
|
|
104
173
|
*/
|
|
174
|
+
// allow: SIZE_OK — Container is the package's existing DI lifecycle state machine.
|
|
105
175
|
export class Container {
|
|
176
|
+
static #childScopeConstruction;
|
|
106
177
|
registrations = new Map();
|
|
107
178
|
multiRegistrations = new Map();
|
|
108
179
|
multiOverriddenTokens = new Set();
|
|
109
180
|
requestCache;
|
|
110
181
|
multiRequestCache;
|
|
111
182
|
multiSingletonCache = new Map();
|
|
183
|
+
materializedCachePromises = [];
|
|
184
|
+
pendingDisposables = [];
|
|
112
185
|
staleDisposalTasks = new Set();
|
|
113
186
|
singletonCache;
|
|
114
187
|
forwardRefTokenCache = new WeakMap();
|
|
@@ -122,10 +195,38 @@ export class Container {
|
|
|
122
195
|
disposed = false;
|
|
123
196
|
trackedByParent = false;
|
|
124
197
|
graphRevision = 0;
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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
|
+
}
|
|
129
230
|
}
|
|
130
231
|
|
|
131
232
|
/**
|
|
@@ -179,10 +280,16 @@ export class Container {
|
|
|
179
280
|
* set — the whole set is replaced. If you need to preserve other entries, re-register them
|
|
180
281
|
* together with the replacement in one `override()` call.
|
|
181
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
|
+
*
|
|
182
287
|
* @param providers Provider definitions that should replace existing registrations for each token.
|
|
183
288
|
* @returns The same container instance for fluent override chains.
|
|
184
289
|
* @throws {ContainerResolutionError} When called after the container was disposed.
|
|
290
|
+
* @throws {ScopeMismatchError} When a request-scope override would introduce a new singleton token.
|
|
185
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.
|
|
186
293
|
*/
|
|
187
294
|
override(...providers) {
|
|
188
295
|
if (this.isDisposedInHierarchy()) {
|
|
@@ -200,6 +307,19 @@ export class Container {
|
|
|
200
307
|
}
|
|
201
308
|
normalizedByToken.set(normalized.provide, [normalized]);
|
|
202
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();
|
|
203
323
|
for (const [token, normalizedProviders] of normalizedByToken) {
|
|
204
324
|
const firstProvider = normalizedProviders[0];
|
|
205
325
|
if (!firstProvider) {
|
|
@@ -212,6 +332,17 @@ export class Container {
|
|
|
212
332
|
if (!containsMultiProvider && normalizedProviders.length > 1) {
|
|
213
333
|
throw new DuplicateProviderError(token);
|
|
214
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) {
|
|
215
346
|
this.invalidateAffectedCachedEntriesInHierarchy(token);
|
|
216
347
|
this.registrations.delete(token);
|
|
217
348
|
this.multiRegistrations.delete(token);
|
|
@@ -282,10 +413,12 @@ export class Container {
|
|
|
282
413
|
setMultiSingleton: (provider, promise) => {
|
|
283
414
|
this.multiSingletonCache.set(provider, promise);
|
|
284
415
|
multiSingletonCacheSnapshot.set(provider, promise);
|
|
416
|
+
this.trackCacheMaterialization(promise);
|
|
285
417
|
},
|
|
286
418
|
setSingleton: (token, promise) => {
|
|
287
419
|
this.singletonCache.set(token, promise);
|
|
288
420
|
singletonCacheSnapshot.set(token, promise);
|
|
421
|
+
this.trackCacheMaterialization(promise);
|
|
289
422
|
}
|
|
290
423
|
});
|
|
291
424
|
}
|
|
@@ -323,7 +456,11 @@ export class Container {
|
|
|
323
456
|
hint: 'Create request scopes before calling container.dispose().'
|
|
324
457
|
});
|
|
325
458
|
}
|
|
326
|
-
return
|
|
459
|
+
return Container.#createChildScope({
|
|
460
|
+
parent: this,
|
|
461
|
+
requestScopeEnabled: true,
|
|
462
|
+
singletonCache: this.root().singletonCache
|
|
463
|
+
});
|
|
327
464
|
}
|
|
328
465
|
|
|
329
466
|
/**
|
|
@@ -346,40 +483,76 @@ export class Container {
|
|
|
346
483
|
await this.assertStaleDisposalsSettled();
|
|
347
484
|
return this.resolveWithChain(token, [], new Set());
|
|
348
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
|
+
}
|
|
349
505
|
|
|
350
506
|
/**
|
|
351
507
|
* Disposes cached instances and nested request scopes.
|
|
352
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
|
+
*
|
|
353
516
|
* @returns A promise that settles after all cached disposable instances are torn down.
|
|
354
517
|
* @throws {Error} Propagates one or more disposal errors (`AggregateError` when multiple failures occur).
|
|
355
518
|
*/
|
|
356
519
|
async dispose() {
|
|
520
|
+
await this.disposeWithOrigin('direct');
|
|
521
|
+
}
|
|
522
|
+
async disposeFromParent() {
|
|
523
|
+
await this.disposeWithOrigin('parent');
|
|
524
|
+
}
|
|
525
|
+
async disposeWithOrigin(origin) {
|
|
357
526
|
if (this.disposePromise) {
|
|
358
527
|
await this.disposePromise;
|
|
359
528
|
return;
|
|
360
529
|
}
|
|
361
530
|
this.disposed = true;
|
|
362
531
|
this.advanceGraphRevision();
|
|
363
|
-
|
|
532
|
+
const activeDispose = this.disposeAll(origin);
|
|
533
|
+
this.disposePromise = activeDispose;
|
|
364
534
|
try {
|
|
365
|
-
await
|
|
535
|
+
await activeDispose;
|
|
536
|
+
if (this.disposePromise === activeDispose && this.hasRetainedStaleDisposalTasksInSubtree()) {
|
|
537
|
+
this.disposePromise = undefined;
|
|
538
|
+
}
|
|
366
539
|
} catch (error) {
|
|
367
540
|
this.disposePromise = undefined;
|
|
368
541
|
throw error;
|
|
369
542
|
}
|
|
370
543
|
}
|
|
371
|
-
async disposeAll() {
|
|
544
|
+
async disposeAll(origin) {
|
|
372
545
|
const errors = [];
|
|
546
|
+
let completed = false;
|
|
373
547
|
try {
|
|
374
548
|
// Dispose all live request-scope children before tearing down this scope's cache.
|
|
375
549
|
if (this.childScopes && this.childScopes.size > 0) {
|
|
376
|
-
const childResults = await Promise.allSettled(Array.from(this.childScopes).map(child => child.
|
|
550
|
+
const childResults = await Promise.allSettled(Array.from(this.childScopes).map(child => child.disposeFromParent()));
|
|
377
551
|
for (const result of childResults) {
|
|
378
552
|
if (result.status === 'rejected') {
|
|
379
553
|
this.collectDisposalError(result.reason, errors);
|
|
380
554
|
}
|
|
381
555
|
}
|
|
382
|
-
this.childScopes.clear();
|
|
383
556
|
}
|
|
384
557
|
try {
|
|
385
558
|
await this.disposeCache(this.disposalCacheEntries());
|
|
@@ -387,8 +560,13 @@ export class Container {
|
|
|
387
560
|
this.collectDisposalError(error, errors);
|
|
388
561
|
}
|
|
389
562
|
this.throwDisposalErrors(errors);
|
|
563
|
+
completed = true;
|
|
390
564
|
} finally {
|
|
391
|
-
|
|
565
|
+
const retainsStaleRetries = this.hasRetainedStaleDisposalTasksInSubtree();
|
|
566
|
+
if ((origin === 'direct' || completed && !retainsStaleRetries) && this.parent && this.trackedByParent) {
|
|
567
|
+
if (origin === 'direct') {
|
|
568
|
+
this.releaseNonOwnerStaleTaskObserversInSubtree();
|
|
569
|
+
}
|
|
392
570
|
this.parent.childScopes?.delete(this);
|
|
393
571
|
this.trackedByParent = false;
|
|
394
572
|
}
|
|
@@ -522,7 +700,12 @@ export class Container {
|
|
|
522
700
|
}
|
|
523
701
|
const cachedInstance = this.getCachedScopedOrSingletonInstance(provider);
|
|
524
702
|
if (cachedInstance) {
|
|
525
|
-
|
|
703
|
+
const releasePendingResolution = linkPendingResolution(cachedInstance, activeTokens, token);
|
|
704
|
+
try {
|
|
705
|
+
return await cachedInstance;
|
|
706
|
+
} finally {
|
|
707
|
+
releasePendingResolution?.();
|
|
708
|
+
}
|
|
526
709
|
}
|
|
527
710
|
return await this.withTokenInChain(token, chain, activeTokens, async (c, at) => this.resolveScopedOrSingletonInstance(provider, c, at));
|
|
528
711
|
}
|
|
@@ -562,16 +745,29 @@ export class Container {
|
|
|
562
745
|
if (provider.scope === 'transient') {
|
|
563
746
|
return await this.instantiate(provider, chain, activeTokens);
|
|
564
747
|
}
|
|
565
|
-
|
|
566
|
-
|
|
748
|
+
const cacheOwner = this.cacheOwnerFor(provider);
|
|
749
|
+
if (cacheOwner !== this) {
|
|
750
|
+
return await cacheOwner.resolveMultiProviderInstance(provider, chain, activeTokens);
|
|
567
751
|
}
|
|
568
752
|
const cache = this.multiCacheFor(provider);
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
}
|
|
574
|
-
|
|
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;
|
|
575
771
|
}
|
|
576
772
|
resolveExistingProviderTarget(provider) {
|
|
577
773
|
if (provider.type !== 'existing') {
|
|
@@ -580,33 +776,48 @@ export class Container {
|
|
|
580
776
|
return provider.useExisting;
|
|
581
777
|
}
|
|
582
778
|
async resolveScopedOrSingletonInstance(provider, chain, activeTokens) {
|
|
583
|
-
|
|
584
|
-
|
|
779
|
+
const cacheOwner = this.cacheOwnerFor(provider);
|
|
780
|
+
if (cacheOwner !== this) {
|
|
781
|
+
return await cacheOwner.resolveScopedOrSingletonInstance(provider, chain, activeTokens);
|
|
585
782
|
}
|
|
586
783
|
const cache = this.cacheFor(provider);
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
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;
|
|
595
802
|
}
|
|
596
803
|
getCachedScopedOrSingletonInstance(provider) {
|
|
597
804
|
if (provider.scope !== Scope.DEFAULT) {
|
|
598
805
|
return undefined;
|
|
599
806
|
}
|
|
600
|
-
|
|
601
|
-
|
|
807
|
+
const cacheOwner = this.cacheOwnerFor(provider);
|
|
808
|
+
if (cacheOwner !== this) {
|
|
809
|
+
return cacheOwner.getCachedScopedOrSingletonInstance(provider);
|
|
602
810
|
}
|
|
603
811
|
return this.cacheFor(provider).get(provider.provide);
|
|
604
812
|
}
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
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;
|
|
610
821
|
}
|
|
611
822
|
async resolveDepToken(depEntry, chain, activeTokens) {
|
|
612
823
|
if (isOptionalToken(depEntry)) {
|
|
@@ -726,15 +937,20 @@ export class Container {
|
|
|
726
937
|
}
|
|
727
938
|
async disposeCache(entries) {
|
|
728
939
|
const errors = [];
|
|
940
|
+
const retryableStaleTasks = this.retainedStaleDisposalTasks();
|
|
941
|
+
const attemptedStaleInstances = new Set();
|
|
729
942
|
try {
|
|
730
943
|
await this.assertStaleDisposalsSettled();
|
|
731
944
|
} catch (error) {
|
|
732
945
|
this.collectDisposalError(error, errors);
|
|
733
946
|
}
|
|
947
|
+
errors.push(...(await this.retryFailedStaleDisposals(retryableStaleTasks, attemptedStaleInstances)));
|
|
734
948
|
const {
|
|
735
|
-
disposables,
|
|
949
|
+
disposables: materializedDisposables,
|
|
736
950
|
errors: resolutionErrors
|
|
737
951
|
} = await this.collectDisposableInstances(entries);
|
|
952
|
+
const disposalCandidates = this.pendingDisposables.length > 0 ? this.pendingDisposables : materializedDisposables;
|
|
953
|
+
const disposables = disposalCandidates.filter(instance => !attemptedStaleInstances.has(instance));
|
|
738
954
|
errors.push(...resolutionErrors);
|
|
739
955
|
errors.push(...(await this.disposeInstancesInReverseOrder(disposables)));
|
|
740
956
|
this.clearDisposalCaches();
|
|
@@ -744,13 +960,18 @@ export class Container {
|
|
|
744
960
|
const disposables = [];
|
|
745
961
|
const seenInstances = new Set();
|
|
746
962
|
const errors = [];
|
|
963
|
+
const activePromises = new Set(entries.map(([, promise]) => promise));
|
|
747
964
|
const settled = await Promise.allSettled(entries.map(([, p]) => p));
|
|
748
965
|
for (const result of settled) {
|
|
749
966
|
if (result.status === 'rejected') {
|
|
750
967
|
errors.push(result.reason);
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
for (const promise of this.materializedCachePromises) {
|
|
971
|
+
if (!activePromises.has(promise)) {
|
|
751
972
|
continue;
|
|
752
973
|
}
|
|
753
|
-
const instance =
|
|
974
|
+
const instance = await promise;
|
|
754
975
|
if (this.isDisposable(instance) && !seenInstances.has(instance)) {
|
|
755
976
|
seenInstances.add(instance);
|
|
756
977
|
disposables.push(instance);
|
|
@@ -763,16 +984,20 @@ export class Container {
|
|
|
763
984
|
}
|
|
764
985
|
async disposeInstancesInReverseOrder(disposables) {
|
|
765
986
|
const errors = [];
|
|
987
|
+
const pendingDisposables = [];
|
|
766
988
|
for (const instance of [...disposables].reverse()) {
|
|
767
989
|
try {
|
|
768
990
|
await instance.onDestroy();
|
|
769
991
|
} catch (error) {
|
|
770
992
|
errors.push(error);
|
|
993
|
+
pendingDisposables.unshift(instance);
|
|
771
994
|
}
|
|
772
995
|
}
|
|
996
|
+
this.pendingDisposables.splice(0, this.pendingDisposables.length, ...pendingDisposables);
|
|
773
997
|
return errors;
|
|
774
998
|
}
|
|
775
999
|
clearDisposalCaches() {
|
|
1000
|
+
this.materializedCachePromises.length = 0;
|
|
776
1001
|
if (this.parent) {
|
|
777
1002
|
this.requestCache?.clear();
|
|
778
1003
|
this.multiRequestCache?.clear();
|
|
@@ -783,6 +1008,9 @@ export class Container {
|
|
|
783
1008
|
this.multiSingletonCache.clear();
|
|
784
1009
|
this.clearResolutionPlanCaches();
|
|
785
1010
|
}
|
|
1011
|
+
trackCacheMaterialization(promise) {
|
|
1012
|
+
void promise.then(() => this.materializedCachePromises.push(promise), () => undefined);
|
|
1013
|
+
}
|
|
786
1014
|
currentLineageRevision() {
|
|
787
1015
|
const parentRevision = this.parent?.currentLineageRevision();
|
|
788
1016
|
return parentRevision ? `${parentRevision}/${this.graphRevision}` : String(this.graphRevision);
|
|
@@ -813,38 +1041,118 @@ export class Container {
|
|
|
813
1041
|
}
|
|
814
1042
|
async assertStaleDisposalsSettled() {
|
|
815
1043
|
const errors = [];
|
|
816
|
-
|
|
817
|
-
|
|
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
|
+
}
|
|
818
1050
|
await Promise.all(tasks.map(task => task.promise));
|
|
819
1051
|
for (const task of tasks) {
|
|
820
|
-
|
|
821
|
-
if (task.failed
|
|
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) {
|
|
822
1061
|
task.errorConsumed = true;
|
|
823
1062
|
errors.push(task.error);
|
|
824
1063
|
}
|
|
1064
|
+
if (!task.retryInstance) {
|
|
1065
|
+
for (const observer of task.observers) {
|
|
1066
|
+
observer.staleDisposalTasks.delete(task);
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
825
1069
|
}
|
|
826
1070
|
}
|
|
827
1071
|
this.throwDisposalErrors(errors);
|
|
828
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());
|
|
1081
|
+
}
|
|
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);
|
|
1108
|
+
try {
|
|
1109
|
+
await instance.onDestroy();
|
|
1110
|
+
task.failed = false;
|
|
1111
|
+
task.retryInstance = undefined;
|
|
1112
|
+
for (const observer of task.observers) {
|
|
1113
|
+
observer.staleDisposalTasks.delete(task);
|
|
1114
|
+
}
|
|
1115
|
+
} catch (error) {
|
|
1116
|
+
task.error = error;
|
|
1117
|
+
task.errorConsumed = true;
|
|
1118
|
+
errors.push(error);
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
return errors;
|
|
1122
|
+
}
|
|
829
1123
|
scheduleStaleDisposal(instancePromise, staleDisposalOwner) {
|
|
830
1124
|
const observers = staleDisposalOwner === this ? [this] : [this, staleDisposalOwner];
|
|
831
1125
|
const task = {
|
|
832
1126
|
error: undefined,
|
|
833
1127
|
errorConsumed: false,
|
|
834
1128
|
failed: false,
|
|
835
|
-
|
|
1129
|
+
observers,
|
|
1130
|
+
promise: Promise.resolve(),
|
|
1131
|
+
retryInstance: undefined,
|
|
1132
|
+
retryOwner: this
|
|
836
1133
|
};
|
|
837
1134
|
task.promise = (async () => {
|
|
1135
|
+
let instance;
|
|
838
1136
|
try {
|
|
839
|
-
|
|
840
|
-
if (this.isDisposable(instance)) {
|
|
841
|
-
await instance.onDestroy();
|
|
842
|
-
}
|
|
1137
|
+
instance = await instancePromise;
|
|
843
1138
|
} catch (error) {
|
|
844
1139
|
task.error = error;
|
|
845
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;
|
|
846
1152
|
}
|
|
847
1153
|
})().finally(() => {
|
|
1154
|
+
const retainedMaterializations = this.materializedCachePromises.filter(promise => promise !== instancePromise);
|
|
1155
|
+
this.materializedCachePromises.splice(0, this.materializedCachePromises.length, ...retainedMaterializations);
|
|
848
1156
|
if (!task.failed) {
|
|
849
1157
|
for (const observer of observers) {
|
|
850
1158
|
observer.staleDisposalTasks.delete(task);
|
|
@@ -934,6 +1242,10 @@ export class Container {
|
|
|
934
1242
|
}
|
|
935
1243
|
visited.add(token);
|
|
936
1244
|
try {
|
|
1245
|
+
const multiRequestScopedToken = this.findRequestScopedMultiContribution(token, visited);
|
|
1246
|
+
if (multiRequestScopedToken) {
|
|
1247
|
+
return multiRequestScopedToken;
|
|
1248
|
+
}
|
|
937
1249
|
const provider = this.resolveEffectiveProvider(token);
|
|
938
1250
|
if (provider) {
|
|
939
1251
|
if (provider.scope === Scope.REQUEST) {
|
|
@@ -953,6 +1265,28 @@ export class Container {
|
|
|
953
1265
|
visited.delete(token);
|
|
954
1266
|
}
|
|
955
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
|
+
}
|
|
956
1290
|
resolveEffectiveProvider(token, visited = new Set(), chain = []) {
|
|
957
1291
|
const cacheable = visited.size === 0 && chain.length === 0;
|
|
958
1292
|
if (cacheable) {
|
package/dist/errors.d.ts
CHANGED
|
@@ -48,7 +48,8 @@ export declare class ScopeMismatchError extends FluoCodeError {
|
|
|
48
48
|
*
|
|
49
49
|
* @remarks
|
|
50
50
|
* The formatted message includes the full dependency path plus a first-party hint that points callers toward
|
|
51
|
-
* extracting shared logic or
|
|
51
|
+
* extracting shared logic, introducing a mediator, or moving the interaction to a later boundary.
|
|
52
|
+
* `forwardRef()` only defers declaration-time token lookup and cannot resolve a true constructor cycle.
|
|
52
53
|
*/
|
|
53
54
|
export declare class CircularDependencyError extends FluoCodeError {
|
|
54
55
|
constructor(chain: readonly unknown[], detail?: string);
|
package/dist/errors.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAmB,MAAM,cAAc,CAAC;AAE9D;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,CAAC;IACzB,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,eAAe,CAAC,EAAE,SAAS,OAAO,EAAE,CAAC;IAC9C,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;CACxB;AAgCD;;;;;;GAMG;AACH,qBAAa,oBAAqB,SAAQ,aAAa;gBACzC,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,cAAc;CAOtD;AAED;;;;;GAKG;AACH,qBAAa,wBAAyB,SAAQ,aAAa;gBAC7C,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,cAAc;CAOtD;AAED;;;;;GAKG;AACH,qBAAa,2BAA4B,SAAQ,aAAa;gBAChD,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,cAAc;CAOtD;AAED;;GAEG;AACH,qBAAa,kBAAmB,SAAQ,aAAa;gBACvC,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,cAAc;CAOtD;AAED
|
|
1
|
+
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAmB,MAAM,cAAc,CAAC;AAE9D;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,CAAC;IACzB,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,eAAe,CAAC,EAAE,SAAS,OAAO,EAAE,CAAC;IAC9C,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;CACxB;AAgCD;;;;;;GAMG;AACH,qBAAa,oBAAqB,SAAQ,aAAa;gBACzC,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,cAAc;CAOtD;AAED;;;;;GAKG;AACH,qBAAa,wBAAyB,SAAQ,aAAa;gBAC7C,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,cAAc;CAOtD;AAED;;;;;GAKG;AACH,qBAAa,2BAA4B,SAAQ,aAAa;gBAChD,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,cAAc;CAOtD;AAED;;GAEG;AACH,qBAAa,kBAAmB,SAAQ,aAAa;gBACvC,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,cAAc;CAOtD;AAED;;;;;;;GAOG;AACH,qBAAa,uBAAwB,SAAQ,aAAa;gBAC5C,KAAK,EAAE,SAAS,OAAO,EAAE,EAAE,MAAM,CAAC,EAAE,MAAM;CAWvD;AAED;;GAEG;AACH,qBAAa,sBAAuB,SAAQ,aAAa;gBAC3C,KAAK,EAAE,OAAO;CAW3B"}
|