@fluojs/di 1.0.0-beta.4 → 1.0.0-beta.6

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 CHANGED
@@ -76,6 +76,8 @@ const service = await container.resolve(UserService);
76
76
  - **request**: `createRequestScope()`마다 새로 생성됩니다.
77
77
  - **transient**: resolve할 때마다 새 인스턴스를 만듭니다.
78
78
 
79
+ dispose 중에는 루트 컨테이너가 먼저 살아 있는 request scope 자식을 정리한 뒤, 자식 dispose 중 하나 이상이 실패하더라도 루트가 소유한 singleton 정리를 계속 수행합니다. 자식/루트 dispose 실패가 여러 개 발생하면 `dispose()`는 모든 shutdown 실패를 확인할 수 있도록 `AggregateError`로 보고합니다.
80
+
79
81
  ### request scope 분리
80
82
 
81
83
  ```ts
@@ -91,7 +93,7 @@ provider 객체는 등록 시점에 검증됩니다. 모든 객체 provider는 n
91
93
 
92
94
  컨테이너는 순환 의존성을 자동으로 감지하고 `CircularDependencyError`를 발생시켜 무한 루프를 방지합니다. 여기에는 직접 참조(A→A), 이중 노드(A→B→A), 깊은 순환(A→B→C→A)이 모두 포함됩니다.
93
95
 
94
- 순환 의존성을 해결하려면 `forwardRef()`를 사용하여 의존성 토큰의 해석을 지연시키세요.
96
+ 선언 순서 때문에 아직 정의되지 않은 토큰을 참조해야 한다면 `forwardRef()`를 사용하세요. `forwardRef()`는 선언 순서 문제를 위해 토큰 조회를 지연할 뿐이며, 실제 생성자 순환을 해소하지는 않습니다. 그런 순환은 여전히 `CircularDependencyError`로 거부됩니다.
95
97
 
96
98
  ```typescript
97
99
  import { forwardRef } from '@fluojs/di';
@@ -99,12 +101,13 @@ import { Inject } from '@fluojs/core';
99
101
 
100
102
  @Inject(forwardRef(() => ServiceB))
101
103
  class ServiceA {
102
- constructor(private serviceB: any) {}
104
+ constructor(private readonly serviceB: ServiceB) {}
103
105
  }
104
106
 
105
- @Inject(forwardRef(() => ServiceA))
106
107
  class ServiceB {
107
- constructor(private serviceA: any) {}
108
+ getStatus() {
109
+ return 'ready';
110
+ }
108
111
  }
109
112
  ```
110
113
 
@@ -131,7 +134,7 @@ const service = await container.resolve(DataService);
131
134
  ## 문제 해결
132
135
 
133
136
  ### CircularDependencyError
134
- 의존성 그래프에서 순환이 감지될 때 발생합니다. 생성자 주입 항목을 확인하고 필요한 경우 `forwardRef()`를 사용하여 순환을 끊으세요.
137
+ 의존성 그래프에서 순환이 감지될 때 발생합니다. 생성자 주입 항목을 확인하고 공유 상태 추출, 중재자 도입, 수명 주기 경계 변경 등으로 순환을 제거하세요. `forwardRef()`는 선언 순서 문제를 위해 토큰 조회만 지연하며, 실제 생성자 순환을 끊지는 않습니다.
135
138
 
136
139
  ### 토큰을 찾을 수 없음 (Token Not Found)
137
140
  필요한 모든 provider가 컨테이너에 등록되어 있는지 확인하세요. `createRequestScope()`를 사용하는 경우 자식 컨테이너는 부모의 토큰을 해석할 수 있지만, 그 반대는 불가능합니다.
@@ -145,6 +148,7 @@ const service = await container.resolve(DataService);
145
148
  | `resolve<T>(token)` | 토큰을 인스턴스로 비동기 해석합니다. |
146
149
  | `createRequestScope()` | 요청 스코프 의존성을 위한 자식 컨테이너를 생성합니다. |
147
150
  | `has(token)` | 컨테이너나 부모에 토큰이 등록되어 있는지 확인합니다. |
151
+ | `hasRequestScopedDependency(token)` | 토큰 해석 시 provider 그래프에 request-scoped 의존성이나 순환이 있어 request-scope 컨테이너가 필요할 수 있는지 확인합니다. |
148
152
 
149
153
  ## 관련 패키지
150
154
 
package/README.md CHANGED
@@ -75,6 +75,8 @@ fluo DI supports three main provider shapes:
75
75
  - **Request**: Instance is created once per `createRequestScope()` call.
76
76
  - **Transient**: A new instance is created every time it is resolved.
77
77
 
78
+ During disposal, the root container first tears down live request-scope children and then continues with root-owned singleton cleanup even if one or more child disposals fail. When multiple child/root disposals fail, `dispose()` reports an `AggregateError` so callers can inspect every shutdown failure without losing cleanup progress.
79
+
78
80
  ### Request Scoping
79
81
  Isolated containers can be created to handle per-request state without polluting the root container.
80
82
 
@@ -91,7 +93,7 @@ Provider objects are validated at registration time: every object provider must
91
93
 
92
94
  The container automatically detects circular dependencies and throws a `CircularDependencyError` to prevent infinite loops. This includes direct (A→A), two-node (A→B→A), and deep (A→B→C→A) cycles.
93
95
 
94
- To resolve a circular dependency, use `forwardRef()` to defer the resolution of the dependent token.
96
+ Use `forwardRef()` when a token is referenced before its declaration. It defers token lookup for declaration-order issues, but it does not make true constructor cycles resolvable; those cycles are still rejected with `CircularDependencyError`.
95
97
 
96
98
  ```typescript
97
99
  import { forwardRef } from '@fluojs/di';
@@ -99,12 +101,13 @@ import { Inject } from '@fluojs/core';
99
101
 
100
102
  @Inject(forwardRef(() => ServiceB))
101
103
  class ServiceA {
102
- constructor(private serviceB: any) {}
104
+ constructor(private readonly serviceB: ServiceB) {}
103
105
  }
104
106
 
105
- @Inject(forwardRef(() => ServiceA))
106
107
  class ServiceB {
107
- constructor(private serviceA: any) {}
108
+ getStatus() {
109
+ return 'ready';
110
+ }
108
111
  }
109
112
  ```
110
113
 
@@ -131,7 +134,7 @@ const service = await container.resolve(DataService);
131
134
  ## Troubleshooting
132
135
 
133
136
  ### CircularDependencyError
134
- Thrown when the container detects a cycle in the dependency graph. Check your constructor injections and use `forwardRef()` where necessary to break the cycle.
137
+ Thrown when the container detects a cycle in the dependency graph. Check your constructor injections and remove the cycle by extracting shared state, introducing a mediator, or changing the lifetime boundary. `forwardRef()` only defers token lookup for declaration-order issues; it does not break true constructor cycles.
135
138
 
136
139
  ### Token Not Found
137
140
  Ensure all required providers are registered in the container. If you use `createRequestScope()`, the child container can resolve tokens from the parent, but not vice versa.
@@ -145,6 +148,7 @@ Ensure all required providers are registered in the container. If you use `creat
145
148
  | `resolve<T>(token)` | Asynchronously resolves a token to an instance. |
146
149
  | `createRequestScope()` | Creates a child container for request-scoped dependencies. |
147
150
  | `has(token)` | Checks if a token is registered in the container or its parents. |
151
+ | `hasRequestScopedDependency(token)` | Checks whether resolving a token may require a request-scope container because its provider graph contains request-scoped dependencies or is cyclic. |
148
152
 
149
153
  ## Related Packages
150
154
 
@@ -16,10 +16,15 @@ export declare class Container {
16
16
  private readonly staleDisposalErrors;
17
17
  private readonly singletonCache;
18
18
  private readonly forwardRefTokenCache;
19
+ private readonly providerLookupPlanCache;
20
+ private readonly multiProviderPlanCache;
21
+ private readonly requestScopeVerdictPlanCache;
22
+ private readonly effectiveProviderPlanCache;
19
23
  private childScopes;
20
24
  private disposePromise;
21
25
  private disposed;
22
26
  private trackedByRoot;
27
+ private graphRevision;
23
28
  constructor(parent?: Container | undefined, requestScopeEnabled?: boolean, singletonCache?: Map<Token, Promise<unknown>>);
24
29
  /**
25
30
  * Registers providers in the current container scope.
@@ -54,6 +59,13 @@ export declare class Container {
54
59
  * @returns `true` when a single or multi provider exists for the token.
55
60
  */
56
61
  has(token: Token): boolean;
62
+ /**
63
+ * Returns whether resolving a token may require a request-scope container.
64
+ *
65
+ * @param token Provider token to inspect through aliases, multi providers, and dependencies.
66
+ * @returns `true` when the provider graph contains request-scoped dependencies or is cyclic.
67
+ */
68
+ hasRequestScopedDependency(token: Token): boolean;
57
69
  /**
58
70
  * Creates a child request-scope container that shares root singleton cache.
59
71
  *
@@ -88,6 +100,10 @@ export declare class Container {
88
100
  private hasAncestorMultiRegistration;
89
101
  private hasMultiRegistration;
90
102
  private collectMultiProviders;
103
+ private providerGraphRequiresRequestScope;
104
+ private unregisteredClassRequiresRequestScope;
105
+ private normalizedProviderRequiresRequestScope;
106
+ private dependencyEntryRequiresRequestScope;
91
107
  private resolveWithChain;
92
108
  private resolveFromRegisteredProviders;
93
109
  private requireProvider;
@@ -126,12 +142,20 @@ export declare class Container {
126
142
  private collectDisposableInstances;
127
143
  private disposeInstancesInReverseOrder;
128
144
  private clearDisposalCaches;
145
+ private currentLineageRevision;
146
+ private readCachedPlan;
147
+ private writePlanCache;
148
+ private advanceGraphRevision;
149
+ private clearResolutionPlanCaches;
129
150
  private waitForStaleDisposalTasks;
130
151
  private scheduleStaleDisposal;
131
152
  private throwDisposalErrors;
153
+ private collectDisposalError;
132
154
  private isDisposable;
133
155
  private instantiate;
134
156
  private assertSingletonDependencyScopes;
157
+ private findRequestScopedDependency;
158
+ private findRequestScopedDependencyToken;
135
159
  private resolveEffectiveProvider;
136
160
  private resolveProviderDependencyToken;
137
161
  private resolveForwardRefToken;
@@ -1 +1 @@
1
- {"version":3,"file":"container.d.ts","sourceRoot":"","sources":["../src/container.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"container.d.ts","sourceRoot":"","sources":["../src/container.ts"],"names":[],"mappings":"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"}
package/dist/container.js CHANGED
@@ -126,10 +126,15 @@ export class Container {
126
126
  staleDisposalErrors = [];
127
127
  singletonCache;
128
128
  forwardRefTokenCache = new WeakMap();
129
+ providerLookupPlanCache = new Map();
130
+ multiProviderPlanCache = new Map();
131
+ requestScopeVerdictPlanCache = new Map();
132
+ effectiveProviderPlanCache = new Map();
129
133
  childScopes;
130
134
  disposePromise;
131
135
  disposed = false;
132
136
  trackedByRoot = false;
137
+ graphRevision = 0;
133
138
  constructor(parent, requestScopeEnabled = false, singletonCache) {
134
139
  this.parent = parent;
135
140
  this.requestScopeEnabled = requestScopeEnabled;
@@ -166,12 +171,14 @@ export class Container {
166
171
  const existing = this.multiRegistrations.get(normalized.provide);
167
172
  if (existing) {
168
173
  existing.push(normalized);
174
+ this.advanceGraphRevision();
169
175
  continue;
170
176
  }
171
177
  this.multiRegistrations.set(normalized.provide, [normalized]);
172
178
  } else {
173
179
  this.registrations.set(normalized.provide, normalized);
174
180
  }
181
+ this.advanceGraphRevision();
175
182
  }
176
183
  return this;
177
184
  }
@@ -205,10 +212,12 @@ export class Container {
205
212
  if (normalized.multi) {
206
213
  this.multiRegistrations.set(normalized.provide, [normalized]);
207
214
  this.multiOverriddenTokens.add(normalized.provide);
215
+ this.advanceGraphRevision();
208
216
  continue;
209
217
  }
210
218
  this.multiOverriddenTokens.add(normalized.provide);
211
219
  this.registrations.set(normalized.provide, normalized);
220
+ this.advanceGraphRevision();
212
221
  }
213
222
  return this;
214
223
  }
@@ -223,6 +232,20 @@ export class Container {
223
232
  return this.lookupProvider(token) !== undefined || this.hasMulti(token);
224
233
  }
225
234
 
235
+ /**
236
+ * Returns whether resolving a token may require a request-scope container.
237
+ *
238
+ * @param token Provider token to inspect through aliases, multi providers, and dependencies.
239
+ * @returns `true` when the provider graph contains request-scoped dependencies or is cyclic.
240
+ */
241
+ hasRequestScopedDependency(token) {
242
+ const cached = this.readCachedPlan(this.requestScopeVerdictPlanCache, token);
243
+ if (cached) {
244
+ return cached.value;
245
+ }
246
+ return this.writePlanCache(this.requestScopeVerdictPlanCache, token, this.providerGraphRequiresRequestScope(token, new Set()));
247
+ }
248
+
226
249
  /**
227
250
  * Creates a child request-scope container that shares root singleton cache.
228
251
  *
@@ -270,6 +293,7 @@ export class Container {
270
293
  return;
271
294
  }
272
295
  this.disposed = true;
296
+ this.advanceGraphRevision();
273
297
  this.disposePromise = this.disposeAll();
274
298
  try {
275
299
  await this.disposePromise;
@@ -279,13 +303,24 @@ export class Container {
279
303
  }
280
304
  }
281
305
  async disposeAll() {
306
+ const errors = [];
282
307
  try {
283
308
  // Dispose all live request-scope children first (root only)
284
309
  if (!this.parent && this.childScopes && this.childScopes.size > 0) {
285
- await Promise.all(Array.from(this.childScopes).map(child => child.dispose()));
310
+ const childResults = await Promise.allSettled(Array.from(this.childScopes).map(child => child.dispose()));
311
+ for (const result of childResults) {
312
+ if (result.status === 'rejected') {
313
+ this.collectDisposalError(result.reason, errors);
314
+ }
315
+ }
286
316
  this.childScopes.clear();
287
317
  }
288
- await this.disposeCache(this.disposalCacheEntries());
318
+ try {
319
+ await this.disposeCache(this.disposalCacheEntries());
320
+ } catch (error) {
321
+ this.collectDisposalError(error, errors);
322
+ }
323
+ this.throwDisposalErrors(errors);
289
324
  } finally {
290
325
  if (this.parent && this.trackedByRoot) {
291
326
  this.root().childScopes?.delete(this);
@@ -335,15 +370,65 @@ export class Container {
335
370
  return this.parent?.hasMultiRegistration(token) ?? false;
336
371
  }
337
372
  collectMultiProviders(token) {
373
+ const cached = this.readCachedPlan(this.multiProviderPlanCache, token);
374
+ if (cached) {
375
+ return [...cached.value];
376
+ }
338
377
  const local = this.multiRegistrations.get(token);
378
+ let providers;
339
379
  if (this.multiOverriddenTokens.has(token)) {
340
- return local ?? [];
380
+ providers = Object.freeze([...(local ?? [])]);
381
+ } else {
382
+ const fromParent = this.parent ? this.parent.collectMultiProviders(token) : [];
383
+ providers = Object.freeze(local ? [...fromParent, ...local] : [...fromParent]);
384
+ }
385
+ this.writePlanCache(this.multiProviderPlanCache, token, providers);
386
+ return [...providers];
387
+ }
388
+ providerGraphRequiresRequestScope(token, visited) {
389
+ if (visited.has(token)) {
390
+ return true;
391
+ }
392
+ visited.add(token);
393
+ try {
394
+ const provider = this.lookupProvider(token);
395
+ const multiProviders = this.collectMultiProviders(token);
396
+ if (!provider && multiProviders.length === 0) {
397
+ return this.unregisteredClassRequiresRequestScope(token, visited);
398
+ }
399
+ if (provider && this.normalizedProviderRequiresRequestScope(provider, visited)) {
400
+ return true;
401
+ }
402
+ return multiProviders.some(multiProvider => this.normalizedProviderRequiresRequestScope(multiProvider, visited));
403
+ } finally {
404
+ visited.delete(token);
341
405
  }
342
- const fromParent = this.parent ? this.parent.collectMultiProviders(token) : [];
343
- if (local) {
344
- return [...fromParent, ...local];
406
+ }
407
+ unregisteredClassRequiresRequestScope(token, visited) {
408
+ if (typeof token !== 'function') {
409
+ return false;
410
+ }
411
+ const metadata = getClassDiMetadata(token);
412
+ if (metadata?.scope === Scope.REQUEST) {
413
+ return true;
345
414
  }
346
- return fromParent;
415
+ return (metadata?.inject ?? []).some(depEntry => this.dependencyEntryRequiresRequestScope(depEntry, visited));
416
+ }
417
+ normalizedProviderRequiresRequestScope(provider, visited) {
418
+ if (provider.scope === Scope.REQUEST) {
419
+ return true;
420
+ }
421
+ if (provider.type === 'existing' && provider.useExisting !== undefined) {
422
+ return this.providerGraphRequiresRequestScope(provider.useExisting, visited);
423
+ }
424
+ return provider.inject.some(depEntry => this.dependencyEntryRequiresRequestScope(depEntry, visited));
425
+ }
426
+ dependencyEntryRequiresRequestScope(depEntry, visited) {
427
+ const depToken = this.resolveProviderDependencyToken(depEntry);
428
+ if (isOptionalToken(depEntry) && !this.has(depToken)) {
429
+ return false;
430
+ }
431
+ return this.providerGraphRequiresRequestScope(depToken, visited);
347
432
  }
348
433
  async resolveWithChain(token, chain, activeTokens, allowForwardRef = false) {
349
434
  const cachedForwardRef = this.resolveForwardRefCircularDependency(token, chain, activeTokens, allowForwardRef);
@@ -504,11 +589,13 @@ export class Container {
504
589
  return this.multiRequestCache;
505
590
  }
506
591
  lookupProvider(token) {
507
- const local = this.registrations.get(token);
508
- if (local) {
509
- return local;
592
+ const cached = this.readCachedPlan(this.providerLookupPlanCache, token);
593
+ if (cached) {
594
+ return cached.value;
510
595
  }
511
- return this.parent?.lookupProvider(token);
596
+ const local = this.registrations.get(token);
597
+ const provider = local ?? this.parent?.lookupProvider(token);
598
+ return this.writePlanCache(this.providerLookupPlanCache, token, provider);
512
599
  }
513
600
 
514
601
  /**
@@ -618,10 +705,40 @@ export class Container {
618
705
  if (this.parent) {
619
706
  this.requestCache?.clear();
620
707
  this.multiRequestCache?.clear();
708
+ this.clearResolutionPlanCaches();
621
709
  return;
622
710
  }
623
711
  this.singletonCache.clear();
624
712
  this.multiSingletonCache.clear();
713
+ this.clearResolutionPlanCaches();
714
+ }
715
+ currentLineageRevision() {
716
+ const parentRevision = this.parent?.currentLineageRevision();
717
+ return parentRevision ? `${parentRevision}/${this.graphRevision}` : String(this.graphRevision);
718
+ }
719
+ readCachedPlan(cache, token) {
720
+ const cached = cache.get(token);
721
+ if (!cached || cached.lineageRevision !== this.currentLineageRevision()) {
722
+ return undefined;
723
+ }
724
+ return cached;
725
+ }
726
+ writePlanCache(cache, token, value) {
727
+ cache.set(token, {
728
+ lineageRevision: this.currentLineageRevision(),
729
+ value
730
+ });
731
+ return value;
732
+ }
733
+ advanceGraphRevision() {
734
+ this.graphRevision += 1;
735
+ this.clearResolutionPlanCaches();
736
+ }
737
+ clearResolutionPlanCaches() {
738
+ this.providerLookupPlanCache.clear();
739
+ this.multiProviderPlanCache.clear();
740
+ this.requestScopeVerdictPlanCache.clear();
741
+ this.effectiveProviderPlanCache.clear();
625
742
  }
626
743
  async waitForStaleDisposalTasks() {
627
744
  while (this.staleDisposalTasks.size > 0) {
@@ -652,6 +769,13 @@ export class Container {
652
769
  throw new AggregateError(errors, 'Container disposal failed for one or more providers.');
653
770
  }
654
771
  }
772
+ collectDisposalError(error, errors) {
773
+ if (error instanceof AggregateError) {
774
+ errors.push(...error.errors);
775
+ return;
776
+ }
777
+ errors.push(error);
778
+ }
655
779
  isDisposable(value) {
656
780
  return typeof value === 'object' && value !== null && 'onDestroy' in value && typeof value.onDestroy === 'function';
657
781
  }
@@ -686,19 +810,61 @@ export class Container {
686
810
  if (provider.scope !== Scope.DEFAULT) {
687
811
  return;
688
812
  }
689
- for (const depEntry of provider.inject) {
813
+ const requestScopedDependency = this.findRequestScopedDependency(provider.inject, new Set([provider.provide]));
814
+ if (requestScopedDependency) {
815
+ throw new ScopeMismatchError(`Singleton provider ${formatTokenName(provider.provide)} depends on request-scoped provider ${formatTokenName(requestScopedDependency)}.`, {
816
+ token: provider.provide,
817
+ scope: 'singleton',
818
+ hint: `Singleton providers cannot depend on request-scoped providers. Either change ${formatTokenName(requestScopedDependency)} to singleton/transient scope, or change ${formatTokenName(provider.provide)} to request scope.`
819
+ });
820
+ }
821
+ }
822
+ findRequestScopedDependency(depEntries, visited) {
823
+ for (const depEntry of depEntries) {
690
824
  const depToken = this.resolveProviderDependencyToken(depEntry);
691
- const effectiveProvider = this.resolveEffectiveProvider(depToken);
692
- if (effectiveProvider?.scope === 'request') {
693
- throw new ScopeMismatchError(`Singleton provider ${formatTokenName(provider.provide)} depends on request-scoped provider ${formatTokenName(depToken)}.`, {
694
- token: provider.provide,
695
- scope: 'singleton',
696
- 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.`
697
- });
825
+ if (isOptionalToken(depEntry) && !this.has(depToken)) {
826
+ continue;
827
+ }
828
+ const requestScopedToken = this.findRequestScopedDependencyToken(depToken, visited);
829
+ if (requestScopedToken) {
830
+ return requestScopedToken;
698
831
  }
699
832
  }
833
+ return undefined;
834
+ }
835
+ findRequestScopedDependencyToken(token, visited) {
836
+ if (visited.has(token)) {
837
+ return undefined;
838
+ }
839
+ visited.add(token);
840
+ try {
841
+ const provider = this.resolveEffectiveProvider(token);
842
+ if (provider) {
843
+ if (provider.scope === Scope.REQUEST) {
844
+ return provider.provide;
845
+ }
846
+ return this.findRequestScopedDependency(provider.inject, visited);
847
+ }
848
+ if (typeof token !== 'function') {
849
+ return undefined;
850
+ }
851
+ const metadata = getClassDiMetadata(token);
852
+ if (metadata?.scope === Scope.REQUEST) {
853
+ return token;
854
+ }
855
+ return this.findRequestScopedDependency(metadata?.inject ?? [], visited);
856
+ } finally {
857
+ visited.delete(token);
858
+ }
700
859
  }
701
860
  resolveEffectiveProvider(token, visited = new Set(), chain = []) {
861
+ const cacheable = visited.size === 0 && chain.length === 0;
862
+ if (cacheable) {
863
+ const cached = this.readCachedPlan(this.effectiveProviderPlanCache, token);
864
+ if (cached) {
865
+ return cached.value;
866
+ }
867
+ }
702
868
  let currentToken = token;
703
869
  while (true) {
704
870
  if (visited.has(currentToken)) {
@@ -707,9 +873,15 @@ export class Container {
707
873
  visited.add(currentToken);
708
874
  const provider = this.lookupProvider(currentToken);
709
875
  if (!provider) {
876
+ if (cacheable) {
877
+ return this.writePlanCache(this.effectiveProviderPlanCache, token, undefined);
878
+ }
710
879
  return undefined;
711
880
  }
712
881
  if (provider.type !== 'existing' || provider.useExisting === undefined) {
882
+ if (cacheable) {
883
+ return this.writePlanCache(this.effectiveProviderPlanCache, token, provider);
884
+ }
713
885
  return provider;
714
886
  }
715
887
  chain.push(currentToken);
package/package.json CHANGED
@@ -9,7 +9,7 @@
9
9
  "container",
10
10
  "provider"
11
11
  ],
12
- "version": "1.0.0-beta.4",
12
+ "version": "1.0.0-beta.6",
13
13
  "private": false,
14
14
  "license": "MIT",
15
15
  "repository": {
@@ -36,7 +36,7 @@
36
36
  "dist"
37
37
  ],
38
38
  "dependencies": {
39
- "@fluojs/core": "^1.0.0-beta.2"
39
+ "@fluojs/core": "^1.0.0-beta.3"
40
40
  },
41
41
  "devDependencies": {
42
42
  "vitest": "^3.2.4"