@fluojs/di 1.0.0-beta.1

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.
@@ -0,0 +1,697 @@
1
+ import { InvariantError, formatTokenName } from '@fluojs/core';
2
+ import { getClassDiMetadata } from '@fluojs/core/internal';
3
+ import { CircularDependencyError, ContainerResolutionError, DuplicateProviderError, InvalidProviderError, RequestScopeResolutionError, ScopeMismatchError } from './errors.js';
4
+ import { Scope, isForwardRef, isOptionalToken } from './types.js';
5
+ function isClassConstructor(value) {
6
+ return typeof value === 'function';
7
+ }
8
+ function isValueProvider(value) {
9
+ return typeof value === 'object' && value !== null && 'useValue' in value;
10
+ }
11
+ function isFactoryProvider(value) {
12
+ return typeof value === 'object' && value !== null && 'useFactory' in value;
13
+ }
14
+ function isClassProvider(value) {
15
+ return typeof value === 'object' && value !== null && 'useClass' in value;
16
+ }
17
+ function isExistingProvider(value) {
18
+ return typeof value === 'object' && value !== null && 'useExisting' in value;
19
+ }
20
+ function normalizeInjectToken(token) {
21
+ if (token == null) {
22
+ throw new InvalidProviderError('Inject token must not be null or undefined. Check that all tokens in @Inject(...) are defined at the point of decoration (forward-reference cycles require forwardRef()).');
23
+ }
24
+ return token;
25
+ }
26
+ function normalizeProvider(provider) {
27
+ if (isClassConstructor(provider)) {
28
+ const metadata = getClassDiMetadata(provider);
29
+ return {
30
+ inject: (metadata?.inject ?? []).map(normalizeInjectToken),
31
+ provide: provider,
32
+ scope: metadata?.scope ?? Scope.DEFAULT,
33
+ type: 'class',
34
+ useClass: provider
35
+ };
36
+ }
37
+ if (isValueProvider(provider)) {
38
+ return {
39
+ inject: [],
40
+ multi: provider.multi,
41
+ provide: provider.provide,
42
+ scope: Scope.DEFAULT,
43
+ type: 'value',
44
+ useValue: provider.useValue
45
+ };
46
+ }
47
+ if (isFactoryProvider(provider)) {
48
+ const metadata = provider.resolverClass ? getClassDiMetadata(provider.resolverClass) : undefined;
49
+ return {
50
+ inject: (provider.inject ?? []).map(normalizeInjectToken),
51
+ multi: provider.multi,
52
+ provide: provider.provide,
53
+ scope: provider.scope ?? metadata?.scope ?? Scope.DEFAULT,
54
+ type: 'factory',
55
+ useFactory: provider.useFactory
56
+ };
57
+ }
58
+ if (isClassProvider(provider)) {
59
+ const metadata = getClassDiMetadata(provider.useClass);
60
+ return {
61
+ inject: (provider.inject ?? metadata?.inject ?? []).map(normalizeInjectToken),
62
+ multi: provider.multi,
63
+ provide: provider.provide,
64
+ scope: provider.scope ?? metadata?.scope ?? Scope.DEFAULT,
65
+ type: 'class',
66
+ useClass: provider.useClass
67
+ };
68
+ }
69
+ if (isExistingProvider(provider)) {
70
+ return {
71
+ inject: [],
72
+ provide: provider.provide,
73
+ scope: Scope.DEFAULT,
74
+ type: 'existing',
75
+ useExisting: provider.useExisting
76
+ };
77
+ }
78
+ throw new InvalidProviderError('Unsupported provider type.');
79
+ }
80
+
81
+ /**
82
+ * Scope-aware dependency injection container for Fluo providers.
83
+ */
84
+ export class Container {
85
+ registrations = new Map();
86
+ multiRegistrations = new Map();
87
+ multiOverriddenTokens = new Set();
88
+ requestCache = new Map();
89
+ multiRequestCache = new Map();
90
+ multiSingletonCache = new Map();
91
+ staleDisposalTasks = new Set();
92
+ staleDisposalErrors = [];
93
+ singletonCache;
94
+ childScopes = new Set();
95
+ disposePromise;
96
+ disposed = false;
97
+ constructor(parent, requestScopeEnabled = false, singletonCache) {
98
+ this.parent = parent;
99
+ this.requestScopeEnabled = requestScopeEnabled;
100
+ this.singletonCache = singletonCache ?? new Map();
101
+ }
102
+
103
+ /**
104
+ * Registers providers in the current container scope.
105
+ *
106
+ * @param providers Provider definitions to register in this container.
107
+ * @returns The same container instance for fluent registration chains.
108
+ * @throws {ContainerResolutionError} When called after the container was disposed.
109
+ * @throws {ScopeMismatchError} When registering singleton providers directly on a request scope.
110
+ * @throws {DuplicateProviderError} When registration conflicts with existing single/multi mappings.
111
+ * @throws {InvalidProviderError} When a provider definition is structurally invalid.
112
+ */
113
+ register(...providers) {
114
+ if (this.disposed) {
115
+ throw new ContainerResolutionError('Container has been disposed and can no longer register providers.', {
116
+ hint: 'Ensure providers are registered before calling container.dispose().'
117
+ });
118
+ }
119
+ for (const provider of providers) {
120
+ const normalized = normalizeProvider(provider);
121
+ if (this.requestScopeEnabled && normalized.scope === Scope.DEFAULT && normalized.multi !== true) {
122
+ throw new ScopeMismatchError(`Singleton provider ${String(normalized.provide)} cannot be registered on a request-scope container.`, {
123
+ token: normalized.provide,
124
+ scope: 'singleton',
125
+ hint: 'Register it on the root container before creating the request scope, or use container.override() within the request scope instead.'
126
+ });
127
+ }
128
+ this.assertNoRegistrationConflict(normalized.provide, normalized.multi === true);
129
+ if (normalized.multi) {
130
+ const existing = this.multiRegistrations.get(normalized.provide);
131
+ if (existing) {
132
+ existing.push(normalized);
133
+ continue;
134
+ }
135
+ this.multiRegistrations.set(normalized.provide, [normalized]);
136
+ } else {
137
+ this.registrations.set(normalized.provide, normalized);
138
+ }
139
+ }
140
+ return this;
141
+ }
142
+
143
+ /**
144
+ * Override one or more already-registered providers.
145
+ *
146
+ * **Multi-provider destructive replacement**: when the incoming provider has `multi: true`,
147
+ * the entire existing multi-registration array for that token is deleted before the new entry
148
+ * is added. There is intentionally no way to replace a single entry within a multi-provider
149
+ * set — the whole set is replaced. If you need to preserve other entries, re-register them
150
+ * together with the replacement in one `override()` call.
151
+ *
152
+ * @param providers Provider definitions that should replace existing registrations for each token.
153
+ * @returns The same container instance for fluent override chains.
154
+ * @throws {ContainerResolutionError} When called after the container was disposed.
155
+ * @throws {InvalidProviderError} When a provider definition is structurally invalid.
156
+ */
157
+ override(...providers) {
158
+ if (this.disposed) {
159
+ throw new ContainerResolutionError('Container has been disposed and can no longer override providers.', {
160
+ hint: 'Ensure overrides are applied before calling container.dispose().'
161
+ });
162
+ }
163
+ for (const provider of providers) {
164
+ const normalized = normalizeProvider(provider);
165
+ const existing = this.lookupProvider(normalized.provide);
166
+ this.registrations.delete(normalized.provide);
167
+ this.multiRegistrations.delete(normalized.provide);
168
+ this.invalidateCachedEntry(normalized.provide, existing?.scope ?? normalized.scope);
169
+ if (normalized.multi) {
170
+ this.multiRegistrations.set(normalized.provide, [normalized]);
171
+ this.multiOverriddenTokens.add(normalized.provide);
172
+ continue;
173
+ }
174
+ this.multiOverriddenTokens.add(normalized.provide);
175
+ this.registrations.set(normalized.provide, normalized);
176
+ }
177
+ return this;
178
+ }
179
+
180
+ /**
181
+ * Returns whether a token is registered in this scope chain.
182
+ *
183
+ * @param token Token to check across this container and its ancestors.
184
+ * @returns `true` when a single or multi provider exists for the token.
185
+ */
186
+ has(token) {
187
+ return this.lookupProvider(token) !== undefined || this.hasMulti(token);
188
+ }
189
+
190
+ /**
191
+ * Creates a child request-scope container that shares root singleton cache.
192
+ *
193
+ * @returns A request-scope child container bound to this container hierarchy.
194
+ * @throws {ContainerResolutionError} When called after the container was disposed.
195
+ */
196
+ createRequestScope() {
197
+ if (this.disposed) {
198
+ throw new ContainerResolutionError('Container has been disposed and can no longer create request scopes.', {
199
+ hint: 'Create request scopes before calling container.dispose().'
200
+ });
201
+ }
202
+ const child = new Container(this, true, this.root().singletonCache);
203
+ this.root().childScopes.add(child);
204
+ return child;
205
+ }
206
+
207
+ /**
208
+ * Resolves a token to an instance using scope-aware caching rules.
209
+ *
210
+ * @param token Token to resolve.
211
+ * @returns A promise that resolves to the token instance (or multi-provider instance array).
212
+ * @throws {ContainerResolutionError} When called after disposal or when no provider is registered.
213
+ * @throws {RequestScopeResolutionError} When request-scoped providers are resolved from root scope.
214
+ * @throws {ScopeMismatchError} When singleton providers depend on request-scoped providers.
215
+ * @throws {CircularDependencyError} When provider dependency resolution detects a cycle.
216
+ */
217
+ async resolve(token) {
218
+ if (this.disposed) {
219
+ throw new ContainerResolutionError('Container has been disposed and can no longer resolve providers.', {
220
+ token,
221
+ hint: 'Ensure all resolves complete before calling container.dispose().'
222
+ });
223
+ }
224
+ return this.resolveWithChain(token, [], new Set());
225
+ }
226
+
227
+ /**
228
+ * Disposes cached instances and nested request scopes.
229
+ *
230
+ * @returns A promise that settles after all cached disposable instances are torn down.
231
+ * @throws {Error} Propagates one or more disposal errors (`AggregateError` when multiple failures occur).
232
+ */
233
+ async dispose() {
234
+ if (this.disposePromise) {
235
+ await this.disposePromise;
236
+ return;
237
+ }
238
+ this.disposed = true;
239
+ this.disposePromise = this.disposeAll();
240
+ try {
241
+ await this.disposePromise;
242
+ } catch (error) {
243
+ this.disposePromise = undefined;
244
+ throw error;
245
+ }
246
+ }
247
+ async disposeAll() {
248
+ try {
249
+ // Dispose all live request-scope children first (root only)
250
+ if (!this.parent && this.childScopes.size > 0) {
251
+ await Promise.all(Array.from(this.childScopes).map(child => child.dispose()));
252
+ this.childScopes.clear();
253
+ }
254
+ await this.disposeCache(this.disposalCacheEntries());
255
+ } finally {
256
+ if (this.parent) {
257
+ this.root().childScopes.delete(this);
258
+ }
259
+ }
260
+ }
261
+ hasMulti(token) {
262
+ if (this.multiRegistrations.has(token)) return true;
263
+ return this.parent?.hasMulti(token) ?? false;
264
+ }
265
+ assertNoRegistrationConflict(token, multi) {
266
+ if (multi) {
267
+ if (this.registrations.has(token)) {
268
+ throw new DuplicateProviderError(token);
269
+ }
270
+ if (this.hasAncestorSingleRegistration(token)) {
271
+ throw new DuplicateProviderError(token);
272
+ }
273
+ return;
274
+ }
275
+ if (this.registrations.has(token) || this.multiRegistrations.has(token)) {
276
+ throw new DuplicateProviderError(token);
277
+ }
278
+ if (this.hasAncestorMultiRegistration(token)) {
279
+ throw new DuplicateProviderError(token);
280
+ }
281
+ }
282
+ hasAncestorSingleRegistration(token) {
283
+ return this.parent?.hasSingleRegistration(token) ?? false;
284
+ }
285
+ hasSingleRegistration(token) {
286
+ if (this.registrations.has(token)) return true;
287
+ return this.parent?.hasSingleRegistration(token) ?? false;
288
+ }
289
+ hasAncestorMultiRegistration(token) {
290
+ return this.parent?.hasMultiRegistration(token) ?? false;
291
+ }
292
+ hasMultiRegistration(token) {
293
+ if (this.multiRegistrations.has(token)) return true;
294
+ return this.parent?.hasMultiRegistration(token) ?? false;
295
+ }
296
+ collectMultiProviders(token) {
297
+ const local = this.multiRegistrations.get(token);
298
+ if (this.multiOverriddenTokens.has(token)) {
299
+ return local ?? [];
300
+ }
301
+ const fromParent = this.parent ? this.parent.collectMultiProviders(token) : [];
302
+ if (local) {
303
+ return [...fromParent, ...local];
304
+ }
305
+ return fromParent;
306
+ }
307
+ async resolveWithChain(token, chain, activeTokens, allowForwardRef = false) {
308
+ const cachedForwardRef = this.resolveForwardRefCircularDependency(token, chain, activeTokens, allowForwardRef);
309
+ if (cachedForwardRef !== undefined) {
310
+ return await cachedForwardRef;
311
+ }
312
+ return await this.resolveFromRegisteredProviders(token, chain, activeTokens);
313
+ }
314
+ async resolveFromRegisteredProviders(token, chain, activeTokens) {
315
+ const localSingleProvider = this.registrations.get(token);
316
+ if (!localSingleProvider) {
317
+ const multiProviders = this.collectMultiProviders(token);
318
+ if (multiProviders.length > 0) {
319
+ const instances = await this.withTokenInChain(token, chain, activeTokens, async (c, at) => this.resolveMultiProviderInstances(multiProviders, c, at));
320
+ return instances;
321
+ }
322
+ }
323
+ const provider = this.requireProvider(token);
324
+ const existingTarget = this.resolveExistingProviderTarget(provider);
325
+ if (existingTarget !== undefined) {
326
+ return await this.resolveAliasTarget(existingTarget, token, chain, activeTokens);
327
+ }
328
+ if (provider.scope === 'transient') {
329
+ return await this.withTokenInChain(token, chain, activeTokens, async (c, at) => this.instantiate(provider, c, at));
330
+ }
331
+ return await this.withTokenInChain(token, chain, activeTokens, async (c, at) => this.resolveScopedOrSingletonInstance(provider, c, at));
332
+ }
333
+ requireProvider(token) {
334
+ const provider = this.lookupProvider(token);
335
+ if (!provider) {
336
+ throw new ContainerResolutionError(`No provider registered for token ${formatTokenName(token)}.`, {
337
+ token,
338
+ hint: 'Ensure the provider is registered in a module\'s providers array, or that the module exporting it is imported by the consuming module.'
339
+ });
340
+ }
341
+ return provider;
342
+ }
343
+ async resolveAliasTarget(existingTarget, token, chain, activeTokens) {
344
+ return await this.withTokenInChain(token, chain, activeTokens, async (c, at) => this.resolveWithChain(existingTarget, c, at));
345
+ }
346
+ resolveForwardRefCircularDependency(token, chain, activeTokens, allowForwardRef) {
347
+ if (!activeTokens.has(token)) {
348
+ return undefined;
349
+ }
350
+ if (allowForwardRef) {
351
+ throw new CircularDependencyError([...chain, token], 'forwardRef only defers token lookup and does not resolve true circular construction.');
352
+ }
353
+ throw new CircularDependencyError([...chain, token]);
354
+ }
355
+ async resolveMultiProviderInstances(providers, chain, activeTokens) {
356
+ const instances = [];
357
+ for (const provider of providers) {
358
+ instances.push(await this.resolveMultiProviderInstance(provider, chain, activeTokens));
359
+ }
360
+ return instances;
361
+ }
362
+ async resolveMultiProviderInstance(provider, chain, activeTokens) {
363
+ if (provider.type === 'existing') {
364
+ return await this.resolveWithChain(provider.useExisting, chain, activeTokens);
365
+ }
366
+ if (provider.scope === 'transient') {
367
+ return await this.instantiate(provider, chain, activeTokens);
368
+ }
369
+ if (this.shouldResolveMultiProviderFromRoot(provider)) {
370
+ return await this.root().resolveMultiProviderInstance(provider, chain, activeTokens);
371
+ }
372
+ const cache = this.multiCacheFor(provider);
373
+ if (!cache.has(provider)) {
374
+ const promise = this.instantiate(provider, chain, activeTokens);
375
+ cache.set(provider, promise);
376
+ promise.catch(() => cache.delete(provider));
377
+ }
378
+ return await cache.get(provider);
379
+ }
380
+ resolveExistingProviderTarget(provider) {
381
+ if (provider.type !== 'existing') {
382
+ return undefined;
383
+ }
384
+ return provider.useExisting;
385
+ }
386
+ async resolveScopedOrSingletonInstance(provider, chain, activeTokens) {
387
+ if (this.shouldResolveFromRoot(provider)) {
388
+ return await this.root().resolveScopedOrSingletonInstance(provider, chain, activeTokens);
389
+ }
390
+ const cache = this.cacheFor(provider);
391
+ if (!cache.has(provider.provide)) {
392
+ const promise = this.instantiate(provider, chain, activeTokens).catch(error => {
393
+ cache.delete(provider.provide);
394
+ throw error;
395
+ });
396
+ cache.set(provider.provide, promise);
397
+ }
398
+ return cache.get(provider.provide);
399
+ }
400
+ shouldResolveFromRoot(provider) {
401
+ return provider.scope === Scope.DEFAULT && this.requestScopeEnabled && !this.registrations.has(provider.provide);
402
+ }
403
+ shouldResolveMultiProviderFromRoot(provider) {
404
+ return provider.scope === Scope.DEFAULT && this.requestScopeEnabled && !this.hasLocalMultiProvider(provider);
405
+ }
406
+ async resolveDepToken(depEntry, chain, activeTokens) {
407
+ if (isOptionalToken(depEntry)) {
408
+ const innerToken = depEntry.token;
409
+ if (!this.has(innerToken)) {
410
+ return undefined;
411
+ }
412
+ return this.resolveWithChain(innerToken, chain, activeTokens);
413
+ }
414
+ if (isForwardRef(depEntry)) {
415
+ const resolvedToken = depEntry.forwardRef();
416
+ return this.resolveWithChain(resolvedToken, chain, activeTokens, /* allowForwardRef */true);
417
+ }
418
+ return this.resolveWithChain(depEntry, chain, activeTokens);
419
+ }
420
+ async withTokenInChain(token, chain, activeTokens, run) {
421
+ chain.push(token);
422
+ activeTokens.add(token);
423
+ try {
424
+ return await run(chain, activeTokens);
425
+ } finally {
426
+ activeTokens.delete(token);
427
+ chain.pop();
428
+ }
429
+ }
430
+ root() {
431
+ return this.parent ? this.parent.root() : this;
432
+ }
433
+ lookupProvider(token) {
434
+ const local = this.registrations.get(token);
435
+ if (local) {
436
+ return local;
437
+ }
438
+ return this.parent?.lookupProvider(token);
439
+ }
440
+
441
+ /**
442
+ * Resolve the cache map that should hold the instance for `provider`.
443
+ *
444
+ * **Singleton-in-request-scope**: if a provider with `scope: 'singleton'` (the default) is
445
+ * registered directly on a request-scope child container (rather than the root), it is cached
446
+ * in the child's `requestCache` instead of the root's `singletonCache`. This means it behaves
447
+ * as request-scoped despite the singleton scope annotation. This is intentional — it allows
448
+ * test and override scenarios to inject short-lived values without polluting the global cache
449
+ * — but the divergence from the declared scope is a known footgun for consumers who
450
+ * inadvertently register singletons on child containers.
451
+ */
452
+ cacheFor(provider) {
453
+ if (provider.scope === Scope.DEFAULT) {
454
+ if (this.requestScopeEnabled && this.registrations.has(provider.provide)) {
455
+ return this.requestCache;
456
+ }
457
+ return this.root().singletonCache;
458
+ }
459
+ if (!this.requestScopeEnabled) {
460
+ throw new RequestScopeResolutionError(`Request-scoped provider ${formatTokenName(provider.provide)} cannot be resolved outside request scope.`, {
461
+ token: provider.provide,
462
+ scope: 'request',
463
+ hint: 'Wrap the resolve call inside a request-scoped child container created via container.createRequestScope().'
464
+ });
465
+ }
466
+ return this.requestCache;
467
+ }
468
+ multiCacheFor(provider) {
469
+ if (provider.scope === Scope.DEFAULT) {
470
+ if (this.requestScopeEnabled && this.hasLocalMultiProvider(provider)) {
471
+ return this.multiRequestCache;
472
+ }
473
+ return this.root().multiSingletonCache;
474
+ }
475
+ if (!this.requestScopeEnabled) {
476
+ throw new RequestScopeResolutionError(`Request-scoped provider ${formatTokenName(provider.provide)} cannot be resolved outside request scope.`, {
477
+ token: provider.provide,
478
+ scope: 'request',
479
+ hint: 'Wrap the resolve call inside a request-scoped child container created via container.createRequestScope().'
480
+ });
481
+ }
482
+ return this.multiRequestCache;
483
+ }
484
+ hasLocalMultiProvider(provider) {
485
+ return this.multiRegistrations.get(provider.provide)?.includes(provider) ?? false;
486
+ }
487
+ disposalCacheEntries() {
488
+ if (this.parent) {
489
+ const entries = Array.from(this.requestCache.entries());
490
+ for (const [provider, promise] of this.multiRequestCache.entries()) {
491
+ entries.push([provider, promise]);
492
+ }
493
+ return entries;
494
+ }
495
+ const entries = Array.from(this.singletonCache.entries());
496
+ for (const [provider, promise] of this.multiSingletonCache.entries()) {
497
+ entries.push([provider, promise]);
498
+ }
499
+ return entries;
500
+ }
501
+ async disposeCache(entries) {
502
+ await this.waitForStaleDisposalTasks();
503
+ const {
504
+ disposables,
505
+ errors
506
+ } = await this.collectDisposableInstances(entries);
507
+ errors.push(...this.staleDisposalErrors.splice(0, this.staleDisposalErrors.length));
508
+ errors.push(...(await this.disposeInstancesInReverseOrder(disposables)));
509
+ this.clearDisposalCaches();
510
+ this.throwDisposalErrors(errors);
511
+ }
512
+ async collectDisposableInstances(entries) {
513
+ const disposables = [];
514
+ const seenInstances = new Set();
515
+ const errors = [];
516
+ const settled = await Promise.allSettled(entries.map(([, p]) => p));
517
+ for (const result of settled) {
518
+ if (result.status === 'rejected') {
519
+ errors.push(result.reason);
520
+ continue;
521
+ }
522
+ const instance = result.value;
523
+ if (this.isDisposable(instance) && !seenInstances.has(instance)) {
524
+ seenInstances.add(instance);
525
+ disposables.push(instance);
526
+ }
527
+ }
528
+ return {
529
+ disposables,
530
+ errors
531
+ };
532
+ }
533
+ async disposeInstancesInReverseOrder(disposables) {
534
+ const errors = [];
535
+ for (const instance of [...disposables].reverse()) {
536
+ try {
537
+ await instance.onDestroy();
538
+ } catch (error) {
539
+ errors.push(error);
540
+ }
541
+ }
542
+ return errors;
543
+ }
544
+ clearDisposalCaches() {
545
+ if (this.parent) {
546
+ this.requestCache.clear();
547
+ this.multiRequestCache.clear();
548
+ return;
549
+ }
550
+ this.singletonCache.clear();
551
+ this.multiSingletonCache.clear();
552
+ }
553
+ async waitForStaleDisposalTasks() {
554
+ while (this.staleDisposalTasks.size > 0) {
555
+ await Promise.all(Array.from(this.staleDisposalTasks));
556
+ }
557
+ }
558
+ scheduleStaleDisposal(instancePromise) {
559
+ let task;
560
+ task = (async () => {
561
+ try {
562
+ const instance = await instancePromise;
563
+ if (this.isDisposable(instance)) {
564
+ await instance.onDestroy();
565
+ }
566
+ } catch (error) {
567
+ this.staleDisposalErrors.push(error);
568
+ }
569
+ })().finally(() => {
570
+ this.staleDisposalTasks.delete(task);
571
+ });
572
+ this.staleDisposalTasks.add(task);
573
+ }
574
+ throwDisposalErrors(errors) {
575
+ if (errors.length === 1) {
576
+ throw errors[0];
577
+ }
578
+ if (errors.length > 1) {
579
+ throw new AggregateError(errors, 'Container disposal failed for one or more providers.');
580
+ }
581
+ }
582
+ isDisposable(value) {
583
+ return typeof value === 'object' && value !== null && 'onDestroy' in value && typeof value.onDestroy === 'function';
584
+ }
585
+ async instantiate(provider, chain, activeTokens) {
586
+ this.assertSingletonDependencyScopes(provider);
587
+ switch (provider.type) {
588
+ case 'value':
589
+ return provider.useValue;
590
+ case 'existing':
591
+ return await this.resolveWithChain(provider.useExisting, [], new Set());
592
+ case 'factory':
593
+ {
594
+ if (!provider.useFactory) {
595
+ throw new InvariantError('Factory provider is missing useFactory.');
596
+ }
597
+ const deps = await this.resolveProviderDeps(provider, chain, activeTokens);
598
+ return provider.useFactory(...deps);
599
+ }
600
+ case 'class':
601
+ {
602
+ if (!provider.useClass) {
603
+ throw new InvariantError('Class provider is missing useClass.');
604
+ }
605
+ const deps = await this.resolveProviderDeps(provider, chain, activeTokens);
606
+ return new provider.useClass(...deps);
607
+ }
608
+ default:
609
+ throw new InvariantError('Unknown provider type.');
610
+ }
611
+ }
612
+ assertSingletonDependencyScopes(provider) {
613
+ if (provider.scope !== Scope.DEFAULT) {
614
+ return;
615
+ }
616
+ for (const depEntry of provider.inject) {
617
+ const depToken = this.resolveProviderDependencyToken(depEntry);
618
+ const effectiveProvider = this.resolveEffectiveProvider(depToken);
619
+ if (effectiveProvider?.scope === 'request') {
620
+ throw new ScopeMismatchError(`Singleton provider ${formatTokenName(provider.provide)} depends on request-scoped provider ${formatTokenName(depToken)}.`, {
621
+ token: provider.provide,
622
+ scope: 'singleton',
623
+ hint: `Singleton providers cannot depend on request-scoped providers. Either change ${formatTokenName(depToken)} to singleton/transient scope, or change ${formatTokenName(provider.provide)} to request scope.`
624
+ });
625
+ }
626
+ }
627
+ }
628
+ resolveEffectiveProvider(token, visited = new Set(), chain = []) {
629
+ let currentToken = token;
630
+ while (true) {
631
+ if (visited.has(currentToken)) {
632
+ throw new CircularDependencyError([...chain, currentToken]);
633
+ }
634
+ visited.add(currentToken);
635
+ const provider = this.lookupProvider(currentToken);
636
+ if (!provider) {
637
+ return undefined;
638
+ }
639
+ if (provider.type !== 'existing' || provider.useExisting === undefined) {
640
+ return provider;
641
+ }
642
+ chain.push(currentToken);
643
+ currentToken = provider.useExisting;
644
+ }
645
+ }
646
+ resolveProviderDependencyToken(depEntry) {
647
+ if (isForwardRef(depEntry)) {
648
+ return depEntry.forwardRef();
649
+ }
650
+ if (isOptionalToken(depEntry)) {
651
+ return depEntry.token;
652
+ }
653
+ return depEntry;
654
+ }
655
+ async resolveProviderDeps(provider, chain, activeTokens) {
656
+ const deps = new Array(provider.inject.length);
657
+ for (const [index, entry] of provider.inject.entries()) {
658
+ deps[index] = await this.resolveDepToken(entry, chain, activeTokens);
659
+ }
660
+ return deps;
661
+ }
662
+ invalidateCachedEntry(token, scope) {
663
+ if (this.requestCache.has(token)) {
664
+ const cached = this.requestCache.get(token);
665
+ if (cached) {
666
+ this.scheduleStaleDisposal(cached);
667
+ }
668
+ this.requestCache.delete(token);
669
+ }
670
+ if (!this.parent && scope === Scope.DEFAULT) {
671
+ const singletonCache = this.singletonCache;
672
+ if (singletonCache.has(token)) {
673
+ const cached = singletonCache.get(token);
674
+ if (cached) {
675
+ this.scheduleStaleDisposal(cached);
676
+ }
677
+ singletonCache.delete(token);
678
+ }
679
+ }
680
+ if (!this.parent) {
681
+ for (const [provider, cached] of this.multiSingletonCache.entries()) {
682
+ if (provider.provide !== token) {
683
+ continue;
684
+ }
685
+ this.scheduleStaleDisposal(cached);
686
+ this.multiSingletonCache.delete(provider);
687
+ }
688
+ }
689
+ for (const [provider, cached] of this.multiRequestCache.entries()) {
690
+ if (provider.provide !== token) {
691
+ continue;
692
+ }
693
+ this.scheduleStaleDisposal(cached);
694
+ this.multiRequestCache.delete(provider);
695
+ }
696
+ }
697
+ }