@navios/di 0.5.1 → 0.6.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.
Files changed (122) hide show
  1. package/CHANGELOG.md +145 -0
  2. package/README.md +196 -219
  3. package/docs/README.md +69 -11
  4. package/docs/api-reference.md +281 -117
  5. package/docs/container.md +220 -56
  6. package/docs/examples/request-scope-example.mts +2 -2
  7. package/docs/factory.md +3 -8
  8. package/docs/getting-started.md +37 -8
  9. package/docs/migration.md +318 -37
  10. package/docs/request-contexts.md +263 -175
  11. package/docs/scopes.md +79 -42
  12. package/lib/browser/index.d.mts +1577 -0
  13. package/lib/browser/index.d.mts.map +1 -0
  14. package/lib/browser/index.mjs +3013 -0
  15. package/lib/browser/index.mjs.map +1 -0
  16. package/lib/index-7jfWsiG4.d.mts +1211 -0
  17. package/lib/index-7jfWsiG4.d.mts.map +1 -0
  18. package/lib/index-DW3K5sOX.d.cts +1206 -0
  19. package/lib/index-DW3K5sOX.d.cts.map +1 -0
  20. package/lib/index.cjs +389 -0
  21. package/lib/index.cjs.map +1 -0
  22. package/lib/index.d.cts +376 -0
  23. package/lib/index.d.cts.map +1 -0
  24. package/lib/index.d.mts +371 -78
  25. package/lib/index.d.mts.map +1 -0
  26. package/lib/index.mjs +325 -63
  27. package/lib/index.mjs.map +1 -1
  28. package/lib/testing/index.cjs +9 -0
  29. package/lib/testing/index.d.cts +2 -0
  30. package/lib/testing/index.d.mts +2 -2
  31. package/lib/testing/index.mjs +2 -72
  32. package/lib/testing-BG_fa9TJ.mjs +2656 -0
  33. package/lib/testing-BG_fa9TJ.mjs.map +1 -0
  34. package/lib/testing-DIaIRiJz.cjs +2896 -0
  35. package/lib/testing-DIaIRiJz.cjs.map +1 -0
  36. package/package.json +29 -7
  37. package/project.json +2 -2
  38. package/src/__tests__/async-local-storage.browser.spec.mts +240 -0
  39. package/src/__tests__/async-local-storage.spec.mts +333 -0
  40. package/src/__tests__/container.spec.mts +30 -25
  41. package/src/__tests__/e2e.browser.spec.mts +790 -0
  42. package/src/__tests__/e2e.spec.mts +1222 -0
  43. package/src/__tests__/factory.spec.mts +1 -1
  44. package/src/__tests__/get-injectors.spec.mts +1 -1
  45. package/src/__tests__/injectable.spec.mts +1 -1
  46. package/src/__tests__/injection-token.spec.mts +1 -1
  47. package/src/__tests__/library-findings.spec.mts +563 -0
  48. package/src/__tests__/registry.spec.mts +2 -2
  49. package/src/__tests__/request-scope.spec.mts +266 -274
  50. package/src/__tests__/service-instantiator.spec.mts +18 -17
  51. package/src/__tests__/service-locator-event-bus.spec.mts +9 -9
  52. package/src/__tests__/service-locator-manager.spec.mts +15 -15
  53. package/src/__tests__/service-locator.spec.mts +167 -244
  54. package/src/__tests__/unified-api.spec.mts +27 -27
  55. package/src/__type-tests__/factory.spec-d.mts +2 -2
  56. package/src/__type-tests__/inject.spec-d.mts +2 -2
  57. package/src/__type-tests__/injectable.spec-d.mts +1 -1
  58. package/src/browser.mts +16 -0
  59. package/src/container/container.mts +319 -0
  60. package/src/container/index.mts +2 -0
  61. package/src/container/scoped-container.mts +350 -0
  62. package/src/decorators/factory.decorator.mts +4 -4
  63. package/src/decorators/injectable.decorator.mts +5 -5
  64. package/src/errors/di-error.mts +12 -5
  65. package/src/errors/index.mts +0 -8
  66. package/src/index.mts +156 -15
  67. package/src/interfaces/container.interface.mts +82 -0
  68. package/src/interfaces/factory.interface.mts +2 -2
  69. package/src/interfaces/index.mts +1 -0
  70. package/src/internal/context/async-local-storage.mts +120 -0
  71. package/src/internal/context/factory-context.mts +18 -0
  72. package/src/internal/context/index.mts +3 -0
  73. package/src/{request-context-holder.mts → internal/context/request-context.mts} +40 -27
  74. package/src/internal/context/resolution-context.mts +63 -0
  75. package/src/internal/context/sync-local-storage.mts +51 -0
  76. package/src/internal/core/index.mts +5 -0
  77. package/src/internal/core/instance-resolver.mts +641 -0
  78. package/src/{service-instantiator.mts → internal/core/instantiator.mts} +31 -27
  79. package/src/internal/core/invalidator.mts +437 -0
  80. package/src/internal/core/service-locator.mts +202 -0
  81. package/src/{token-processor.mts → internal/core/token-processor.mts} +79 -60
  82. package/src/{base-instance-holder-manager.mts → internal/holder/base-holder-manager.mts} +91 -21
  83. package/src/internal/holder/holder-manager.mts +85 -0
  84. package/src/internal/holder/holder-storage.interface.mts +116 -0
  85. package/src/internal/holder/index.mts +6 -0
  86. package/src/internal/holder/instance-holder.mts +109 -0
  87. package/src/internal/holder/request-storage.mts +134 -0
  88. package/src/internal/holder/singleton-storage.mts +105 -0
  89. package/src/internal/index.mts +4 -0
  90. package/src/internal/lifecycle/circular-detector.mts +77 -0
  91. package/src/internal/lifecycle/index.mts +2 -0
  92. package/src/{service-locator-event-bus.mts → internal/lifecycle/lifecycle-event-bus.mts} +11 -4
  93. package/src/testing/__tests__/test-container.spec.mts +2 -2
  94. package/src/testing/test-container.mts +4 -4
  95. package/src/token/index.mts +2 -0
  96. package/src/{injection-token.mts → token/injection-token.mts} +1 -1
  97. package/src/{registry.mts → token/registry.mts} +1 -1
  98. package/src/utils/get-injectable-token.mts +1 -1
  99. package/src/utils/get-injectors.mts +32 -15
  100. package/src/utils/types.mts +1 -1
  101. package/tsdown.config.mts +67 -0
  102. package/lib/_tsup-dts-rollup.d.mts +0 -1283
  103. package/lib/_tsup-dts-rollup.d.ts +0 -1283
  104. package/lib/chunk-2M576LCC.mjs +0 -2043
  105. package/lib/chunk-2M576LCC.mjs.map +0 -1
  106. package/lib/index.d.ts +0 -78
  107. package/lib/index.js +0 -2127
  108. package/lib/index.js.map +0 -1
  109. package/lib/testing/index.d.ts +0 -2
  110. package/lib/testing/index.js +0 -2060
  111. package/lib/testing/index.js.map +0 -1
  112. package/lib/testing/index.mjs.map +0 -1
  113. package/src/container.mts +0 -227
  114. package/src/factory-context.mts +0 -8
  115. package/src/instance-resolver.mts +0 -559
  116. package/src/request-context-manager.mts +0 -149
  117. package/src/service-invalidator.mts +0 -429
  118. package/src/service-locator-instance-holder.mts +0 -70
  119. package/src/service-locator-manager.mts +0 -85
  120. package/src/service-locator.mts +0 -246
  121. package/tsup.config.mts +0 -12
  122. /package/src/{injector.mts → injectors.mts} +0 -0
@@ -1,14 +1,18 @@
1
- import type { FactoryContext } from './factory-context.mjs'
2
- import type { FactoryRecord } from './registry.mjs'
3
- import type { Injectors } from './utils/get-injectors.mjs'
1
+ import type { FactoryRecord } from '../../token/registry.mjs'
2
+ import type { Injectors } from '../../utils/get-injectors.mjs'
3
+ import type { FactoryContext } from '../context/factory-context.mjs'
4
4
 
5
- import { InjectableType } from './enums/index.mjs'
5
+ import { InjectableType } from '../../enums/index.mjs'
6
+ import { DIError } from '../../errors/index.mjs'
6
7
 
7
8
  /**
8
- * ServiceInstantiator handles the instantiation of services based on registry records.
9
- * It replaces the hard-coded logic in Injectable and Factory decorators.
9
+ * Creates service instances from registry records.
10
+ *
11
+ * Handles both class-based (@Injectable) and factory-based (@Factory) services,
12
+ * managing the instantiation lifecycle including sync initialization retries
13
+ * and lifecycle hook invocation (onServiceInit, onServiceDestroy).
10
14
  */
11
- export class ServiceInstantiator {
15
+ export class Instantiator {
12
16
  constructor(private readonly injectors: Injectors) {}
13
17
 
14
18
  /**
@@ -22,7 +26,7 @@ export class ServiceInstantiator {
22
26
  ctx: FactoryContext,
23
27
  record: FactoryRecord<T, any>,
24
28
  args: any = undefined,
25
- ): Promise<[undefined, T] | [Error]> {
29
+ ): Promise<[undefined, T] | [DIError]> {
26
30
  try {
27
31
  switch (record.type) {
28
32
  case InjectableType.Class:
@@ -30,12 +34,12 @@ export class ServiceInstantiator {
30
34
  case InjectableType.Factory:
31
35
  return this.instantiateFactory(ctx, record, args)
32
36
  default:
33
- throw new Error(
34
- `[ServiceInstantiator] Unknown service type: ${record.type}`,
37
+ throw DIError.unknown(
38
+ `[Instantiator] Unknown service type: ${record.type}`,
35
39
  )
36
40
  }
37
41
  } catch (error) {
38
- return [error instanceof Error ? error : new Error(String(error))]
42
+ return [error instanceof DIError ? error : DIError.unknown(String(error))]
39
43
  }
40
44
  }
41
45
 
@@ -50,7 +54,7 @@ export class ServiceInstantiator {
50
54
  ctx: FactoryContext,
51
55
  record: FactoryRecord<T, any>,
52
56
  args: any,
53
- ): Promise<[undefined, T] | [Error]> {
57
+ ): Promise<[undefined, T] | [DIError]> {
54
58
  try {
55
59
  const tryLoad = this.injectors.wrapSyncInit(() => {
56
60
  const original = this.injectors.provideFactoryContext(ctx)
@@ -63,8 +67,8 @@ export class ServiceInstantiator {
63
67
  if (promises.length > 0) {
64
68
  const results = await Promise.allSettled(promises)
65
69
  if (results.some((result) => result.status === 'rejected')) {
66
- throw new Error(
67
- `[ServiceInstantiator] Service ${record.target.name} cannot be instantiated.`,
70
+ throw DIError.unknown(
71
+ `[Instantiator] Service ${record.target.name} cannot be instantiated.`,
68
72
  )
69
73
  }
70
74
  const newRes = tryLoad(injectState)
@@ -73,13 +77,13 @@ export class ServiceInstantiator {
73
77
  }
74
78
 
75
79
  if (promises.length > 0) {
76
- console.error(`[ServiceInstantiator] ${record.target.name} has problem with it's definition.
80
+ console.error(`[Instantiator] ${record.target.name} has problem with it's definition.
77
81
 
78
82
  One or more of the dependencies are registered as a InjectableScope.Instance and are used with inject.
79
83
 
80
84
  Please use inject asyncInject of inject to load those dependencies.`)
81
- throw new Error(
82
- `[ServiceInstantiator] Service ${record.target.name} cannot be instantiated.`,
85
+ throw DIError.unknown(
86
+ `[Instantiator] Service ${record.target.name} cannot be instantiated.`,
83
87
  )
84
88
  }
85
89
 
@@ -95,7 +99,7 @@ export class ServiceInstantiator {
95
99
 
96
100
  return [undefined, instance]
97
101
  } catch (error) {
98
- return [error instanceof Error ? error : new Error(String(error))]
102
+ return [error instanceof DIError ? error : DIError.unknown(String(error))]
99
103
  }
100
104
  }
101
105
 
@@ -110,7 +114,7 @@ export class ServiceInstantiator {
110
114
  ctx: FactoryContext,
111
115
  record: FactoryRecord<T, any>,
112
116
  args: any,
113
- ): Promise<[undefined, T] | [Error]> {
117
+ ): Promise<[undefined, T] | [DIError]> {
114
118
  try {
115
119
  const tryLoad = this.injectors.wrapSyncInit(() => {
116
120
  const original = this.injectors.provideFactoryContext(ctx)
@@ -123,8 +127,8 @@ export class ServiceInstantiator {
123
127
  if (promises.length > 0) {
124
128
  const results = await Promise.allSettled(promises)
125
129
  if (results.some((result) => result.status === 'rejected')) {
126
- throw new Error(
127
- `[ServiceInstantiator] Service ${record.target.name} cannot be instantiated.`,
130
+ throw DIError.unknown(
131
+ `[Instantiator] Service ${record.target.name} cannot be instantiated.`,
128
132
  )
129
133
  }
130
134
  const newRes = tryLoad(injectState)
@@ -133,26 +137,26 @@ export class ServiceInstantiator {
133
137
  }
134
138
 
135
139
  if (promises.length > 0) {
136
- console.error(`[ServiceInstantiator] ${record.target.name} has problem with it's definition.
140
+ console.error(`[Instantiator] ${record.target.name} has problem with it's definition.
137
141
 
138
142
  One or more of the dependencies are registered as a InjectableScope.Instance and are used with inject.
139
143
 
140
144
  Please use asyncInject instead of inject to load those dependencies.`)
141
- throw new Error(
142
- `[ServiceInstantiator] Service ${record.target.name} cannot be instantiated.`,
145
+ throw DIError.unknown(
146
+ `[Instantiator] Service ${record.target.name} cannot be instantiated.`,
143
147
  )
144
148
  }
145
149
 
146
150
  if (typeof builder.create !== 'function') {
147
- throw new Error(
148
- `[ServiceInstantiator] Factory ${record.target.name} does not implement the create method.`,
151
+ throw DIError.unknown(
152
+ `[Instantiator] Factory ${record.target.name} does not implement the create method.`,
149
153
  )
150
154
  }
151
155
 
152
156
  const instance = await builder.create(ctx, args)
153
157
  return [undefined, instance]
154
158
  } catch (error) {
155
- return [error instanceof Error ? error : new Error(String(error))]
159
+ return [error instanceof DIError ? error : DIError.unknown(String(error))]
156
160
  }
157
161
  }
158
162
  }
@@ -0,0 +1,437 @@
1
+ /* eslint-disable @typescript-eslint/no-explicit-any */
2
+ import type { IHolderStorage } from '../holder/holder-storage.interface.mjs'
3
+ import type { LifecycleEventBus } from '../lifecycle/lifecycle-event-bus.mjs'
4
+ import type { InstanceHolder } from '../holder/instance-holder.mjs'
5
+ import type { HolderManager } from '../holder/holder-manager.mjs'
6
+
7
+ import { InstanceStatus } from '../holder/instance-holder.mjs'
8
+ import { SingletonStorage } from '../holder/singleton-storage.mjs'
9
+
10
+ export interface ClearAllOptions {
11
+ /** Maximum number of invalidation rounds to prevent infinite loops (default: 10) */
12
+ maxRounds?: number
13
+ /** Whether to wait for all services to settle before starting (default: true) */
14
+ waitForSettlement?: boolean
15
+ }
16
+
17
+ export interface InvalidationOptions {
18
+ /** Whether to emit events after invalidation (default: true for singletons) */
19
+ emitEvents?: boolean
20
+ /** Custom event emitter function */
21
+ onInvalidated?: (instanceName: string) => Promise<void>
22
+ /** Whether to cascade invalidation to dependents (default: true) */
23
+ cascade?: boolean
24
+ /** Internal: tracks services being invalidated in the current call chain to prevent circular loops */
25
+ _invalidating?: Set<string>
26
+ }
27
+
28
+ /**
29
+ * Manages graceful service cleanup with dependency-aware invalidation.
30
+ *
31
+ * Ensures services are destroyed in the correct order based on their dependencies.
32
+ * Works with any IHolderStorage implementation, enabling unified invalidation
33
+ * for both singleton and request-scoped services.
34
+ */
35
+ export class Invalidator {
36
+ private readonly storage: IHolderStorage
37
+
38
+ constructor(
39
+ manager: HolderManager,
40
+ private readonly eventBus: LifecycleEventBus | null,
41
+ private readonly logger: Console | null = null,
42
+ ) {
43
+ this.storage = new SingletonStorage(manager)
44
+ }
45
+
46
+ /**
47
+ * Invalidates a service and all its dependencies.
48
+ * Works with the configured storage (singleton by default).
49
+ */
50
+ invalidate(service: string, round = 1): Promise<any> {
51
+ return this.invalidateWithStorage(service, this.storage, round)
52
+ }
53
+
54
+ /**
55
+ * Invalidates a service using a specific storage.
56
+ * This allows request-scoped invalidation using a RequestStorage.
57
+ *
58
+ * @param service The instance name to invalidate
59
+ * @param storage The storage to use for this invalidation
60
+ * @param round Current invalidation round (for recursion limiting)
61
+ * @param options Additional options for invalidation behavior
62
+ */
63
+ async invalidateWithStorage(
64
+ service: string,
65
+ storage: IHolderStorage,
66
+ round = 1,
67
+ options: InvalidationOptions = {},
68
+ ): Promise<void> {
69
+ const { cascade = true, _invalidating = new Set<string>() } = options
70
+
71
+ // Prevent infinite recursion from circular dependencies
72
+ if (_invalidating.has(service)) {
73
+ this.logger?.log(
74
+ `[Invalidator] Skipping ${service} - already being invalidated in this chain`,
75
+ )
76
+ return
77
+ }
78
+
79
+ this.logger?.log(
80
+ `[Invalidator] Starting invalidation process for ${service}`,
81
+ )
82
+
83
+ const result = storage.get(service)
84
+ if (result === null) {
85
+ return
86
+ }
87
+
88
+ // Mark this service as being invalidated
89
+ _invalidating.add(service)
90
+
91
+ // Pass the tracking set to cascaded invalidations
92
+ const optionsWithTracking = { ...options, _invalidating }
93
+
94
+ // Cascade invalidation: first invalidate all services that depend on this one
95
+ if (cascade) {
96
+ const dependents = storage.findDependents(service)
97
+ for (const dependentName of dependents) {
98
+ await this.invalidateWithStorage(dependentName, storage, round, optionsWithTracking)
99
+ }
100
+ }
101
+
102
+ const [, holder] = result
103
+ if (holder) {
104
+ await this.invalidateHolderWithStorage(service, holder, storage, round, optionsWithTracking)
105
+ }
106
+ }
107
+
108
+ /**
109
+ * Gracefully clears all services using invalidation logic.
110
+ * This method respects service dependencies and ensures proper cleanup order.
111
+ * Services that depend on others will be invalidated first, then their dependencies.
112
+ */
113
+ async clearAll(options: ClearAllOptions = {}): Promise<void> {
114
+ return this.clearAllWithStorage(this.storage, options)
115
+ }
116
+
117
+ /**
118
+ * Gracefully clears all services in a specific storage.
119
+ * This allows clearing request-scoped services using a RequestStorage.
120
+ */
121
+ async clearAllWithStorage(
122
+ storage: IHolderStorage,
123
+ options: ClearAllOptions = {},
124
+ ): Promise<void> {
125
+ const { maxRounds = 10, waitForSettlement = true } = options
126
+
127
+ this.logger?.log(
128
+ '[Invalidator] Starting graceful clearing of all services',
129
+ )
130
+
131
+ // Wait for all services to settle if requested
132
+ if (waitForSettlement) {
133
+ this.logger?.log(
134
+ '[Invalidator] Waiting for all services to settle...',
135
+ )
136
+ await this.readyWithStorage(storage)
137
+ }
138
+
139
+ // Get all service names that need to be cleared
140
+ const allServiceNames = storage.getAllNames()
141
+
142
+ if (allServiceNames.length === 0) {
143
+ this.logger?.log('[Invalidator] No services to clear')
144
+ } else {
145
+ this.logger?.log(
146
+ `[Invalidator] Found ${allServiceNames.length} services to clear: ${allServiceNames.join(', ')}`,
147
+ )
148
+
149
+ // Clear services using dependency-aware invalidation
150
+ await this.clearServicesWithDependencyAwarenessForStorage(
151
+ allServiceNames,
152
+ maxRounds,
153
+ storage,
154
+ )
155
+ }
156
+
157
+ this.logger?.log('[Invalidator] Graceful clearing completed')
158
+ }
159
+
160
+ /**
161
+ * Waits for all services to settle (either created, destroyed, or error state).
162
+ */
163
+ async ready(): Promise<void> {
164
+ return this.readyWithStorage(this.storage)
165
+ }
166
+
167
+ /**
168
+ * Waits for all services in a specific storage to settle.
169
+ */
170
+ async readyWithStorage(storage: IHolderStorage): Promise<void> {
171
+ const holders: InstanceHolder<any>[] = []
172
+ storage.forEach((_: string, holder: InstanceHolder) => holders.push(holder))
173
+ await Promise.all(
174
+ holders.map((holder) => this.waitForHolderToSettle(holder)),
175
+ )
176
+ }
177
+
178
+ // ============================================================================
179
+ // INTERNAL INVALIDATION HELPERS
180
+ // ============================================================================
181
+
182
+ /**
183
+ * Invalidates a single holder using a specific storage.
184
+ */
185
+ private async invalidateHolderWithStorage(
186
+ key: string,
187
+ holder: InstanceHolder<any>,
188
+ storage: IHolderStorage,
189
+ round: number,
190
+ options: InvalidationOptions = {},
191
+ ): Promise<void> {
192
+ const { emitEvents = true, onInvalidated } = options
193
+
194
+ await this.invalidateHolderByStatus(holder, round, {
195
+ context: key,
196
+ onCreationError: () =>
197
+ this.logger?.error(
198
+ `[Invalidator] ${key} creation triggered too many invalidation rounds`,
199
+ ),
200
+ onRecursiveInvalidate: () =>
201
+ this.invalidateWithStorage(key, storage, round + 1, options),
202
+ onDestroy: () =>
203
+ this.destroyHolderWithStorage(key, holder, storage, emitEvents, onInvalidated),
204
+ })
205
+ }
206
+
207
+ /**
208
+ * Common invalidation logic for holders based on their status.
209
+ */
210
+ private async invalidateHolderByStatus(
211
+ holder: InstanceHolder<any>,
212
+ round: number,
213
+ options: {
214
+ context: string
215
+ onCreationError: () => void
216
+ onRecursiveInvalidate: () => Promise<void>
217
+ onDestroy: () => Promise<void>
218
+ },
219
+ ): Promise<void> {
220
+ switch (holder.status) {
221
+ case InstanceStatus.Destroying:
222
+ await holder.destroyPromise
223
+ break
224
+
225
+ case InstanceStatus.Creating:
226
+ await holder.creationPromise
227
+ if (round > 3) {
228
+ options.onCreationError()
229
+ return
230
+ }
231
+ await options.onRecursiveInvalidate()
232
+ break
233
+
234
+ default:
235
+ await options.onDestroy()
236
+ break
237
+ }
238
+ }
239
+
240
+ /**
241
+ * Destroys a holder using a specific storage.
242
+ */
243
+ private async destroyHolderWithStorage(
244
+ key: string,
245
+ holder: InstanceHolder<any>,
246
+ storage: IHolderStorage,
247
+ emitEvents: boolean,
248
+ onInvalidated?: (instanceName: string) => Promise<void>,
249
+ ): Promise<void> {
250
+ holder.status = InstanceStatus.Destroying
251
+ this.logger?.log(`[Invalidator] Invalidating ${key} and notifying listeners`)
252
+
253
+ holder.destroyPromise = Promise.all(
254
+ holder.destroyListeners.map((listener) => listener()),
255
+ ).then(async () => {
256
+ holder.destroyListeners = []
257
+ holder.deps.clear()
258
+ storage.delete(key)
259
+
260
+ // Emit events if enabled and event bus exists
261
+ if (emitEvents && this.eventBus) {
262
+ await this.emitInstanceEvent(key, 'destroy')
263
+ }
264
+
265
+ // Call custom callback if provided
266
+ if (onInvalidated) {
267
+ await onInvalidated(key)
268
+ }
269
+ })
270
+
271
+ await holder.destroyPromise
272
+ }
273
+
274
+ /**
275
+ * Waits for a holder to settle (either created, destroyed, or error state).
276
+ */
277
+ private async waitForHolderToSettle(
278
+ holder: InstanceHolder<any>,
279
+ ): Promise<void> {
280
+ switch (holder.status) {
281
+ case InstanceStatus.Creating:
282
+ await holder.creationPromise
283
+ break
284
+ case InstanceStatus.Destroying:
285
+ await holder.destroyPromise
286
+ break
287
+ // Already settled states
288
+ case InstanceStatus.Created:
289
+ case InstanceStatus.Error:
290
+ break
291
+ }
292
+ }
293
+
294
+ /**
295
+ * Clears services with dependency awareness for a specific storage.
296
+ */
297
+ private async clearServicesWithDependencyAwarenessForStorage(
298
+ serviceNames: string[],
299
+ maxRounds: number,
300
+ storage: IHolderStorage,
301
+ ): Promise<void> {
302
+ const clearedServices = new Set<string>()
303
+ let round = 1
304
+
305
+ while (clearedServices.size < serviceNames.length && round <= maxRounds) {
306
+ this.logger?.log(
307
+ `[Invalidator] Clearing round ${round}/${maxRounds}, ${clearedServices.size}/${serviceNames.length} services cleared`,
308
+ )
309
+
310
+ // Find services that can be cleared in this round
311
+ const servicesToClearThisRound = this.findServicesReadyForClearingInStorage(
312
+ serviceNames,
313
+ clearedServices,
314
+ storage,
315
+ )
316
+
317
+ if (servicesToClearThisRound.length === 0) {
318
+ // If no services can be cleared, try to clear remaining services anyway
319
+ // This handles circular dependencies or other edge cases
320
+ const remainingServices = serviceNames.filter(
321
+ (name) => !clearedServices.has(name),
322
+ )
323
+
324
+ if (remainingServices.length > 0) {
325
+ this.logger?.warn(
326
+ `[Invalidator] No services ready for clearing, forcing cleanup of remaining: ${remainingServices.join(', ')}`,
327
+ )
328
+ await this.forceClearServicesInStorage(remainingServices, storage)
329
+ remainingServices.forEach((name) => clearedServices.add(name))
330
+ }
331
+ break
332
+ }
333
+
334
+ // Clear services in this round
335
+ const clearPromises = servicesToClearThisRound.map(
336
+ async (serviceName) => {
337
+ try {
338
+ await this.invalidateWithStorage(serviceName, storage, round)
339
+ clearedServices.add(serviceName)
340
+ this.logger?.log(
341
+ `[Invalidator] Successfully cleared service: ${serviceName}`,
342
+ )
343
+ } catch (error) {
344
+ this.logger?.error(
345
+ `[Invalidator] Error clearing service ${serviceName}:`,
346
+ error,
347
+ )
348
+ // Still mark as cleared to avoid infinite loops
349
+ clearedServices.add(serviceName)
350
+ }
351
+ },
352
+ )
353
+
354
+ await Promise.all(clearPromises)
355
+ round++
356
+ }
357
+
358
+ if (clearedServices.size < serviceNames.length) {
359
+ this.logger?.warn(
360
+ `[Invalidator] Clearing completed after ${maxRounds} rounds, but ${serviceNames.length - clearedServices.size} services may not have been properly cleared`,
361
+ )
362
+ }
363
+ }
364
+
365
+ /**
366
+ * Finds services that are ready to be cleared in the current round.
367
+ * A service is ready if all its dependencies have already been cleared.
368
+ */
369
+ private findServicesReadyForClearingInStorage(
370
+ allServiceNames: string[],
371
+ clearedServices: Set<string>,
372
+ storage: IHolderStorage,
373
+ ): string[] {
374
+ return allServiceNames.filter((serviceName) => {
375
+ if (clearedServices.has(serviceName)) {
376
+ return false // Already cleared
377
+ }
378
+
379
+ // Check if this service has any dependencies that haven't been cleared yet
380
+ const result = storage.get(serviceName)
381
+ if (result === null || result[0]) {
382
+ return true // Service not found or in error state, can be cleared
383
+ }
384
+
385
+ const [, holder] = result
386
+ // Check if all dependencies have been cleared
387
+ const hasUnclearedDependencies = Array.from(holder!.deps).some(
388
+ (dep) => !clearedServices.has(dep),
389
+ )
390
+
391
+ return !hasUnclearedDependencies
392
+ })
393
+ }
394
+
395
+ /**
396
+ * Force clears services that couldn't be cleared through normal dependency resolution.
397
+ * This handles edge cases like circular dependencies.
398
+ */
399
+ private async forceClearServicesInStorage(
400
+ serviceNames: string[],
401
+ storage: IHolderStorage,
402
+ ): Promise<void> {
403
+ const promises = serviceNames.map(async (serviceName) => {
404
+ try {
405
+ // Directly destroy the holder without going through normal invalidation
406
+ const result = storage.get(serviceName)
407
+ if (result !== null && !result[0]) {
408
+ const [, holder] = result
409
+ await this.destroyHolderWithStorage(serviceName, holder!, storage, true)
410
+ }
411
+ } catch (error) {
412
+ this.logger?.error(
413
+ `[Invalidator] Error force clearing service ${serviceName}:`,
414
+ error,
415
+ )
416
+ }
417
+ })
418
+
419
+ await Promise.all(promises)
420
+ }
421
+
422
+ /**
423
+ * Emits events to listeners for instance lifecycle events.
424
+ */
425
+ private emitInstanceEvent(
426
+ name: string,
427
+ event: 'create' | 'destroy' = 'create',
428
+ ) {
429
+ if (!this.eventBus) {
430
+ return Promise.resolve()
431
+ }
432
+ this.logger?.log(
433
+ `[Invalidator]#emitInstanceEvent() Notifying listeners for ${name} with event ${event}`,
434
+ )
435
+ return this.eventBus.emit(name, event)
436
+ }
437
+ }