@navios/di 0.4.1 → 0.5.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.
Files changed (128) hide show
  1. package/README.md +211 -1
  2. package/coverage/clover.xml +1912 -1277
  3. package/coverage/coverage-final.json +37 -28
  4. package/coverage/docs/examples/basic-usage.mts.html +1 -1
  5. package/coverage/docs/examples/factory-pattern.mts.html +1 -1
  6. package/coverage/docs/examples/index.html +1 -1
  7. package/coverage/docs/examples/injection-tokens.mts.html +1 -1
  8. package/coverage/docs/examples/request-scope-example.mts.html +1 -1
  9. package/coverage/docs/examples/service-lifecycle.mts.html +1 -1
  10. package/coverage/index.html +71 -41
  11. package/coverage/lib/_tsup-dts-rollup.d.mts.html +682 -43
  12. package/coverage/lib/index.d.mts.html +7 -4
  13. package/coverage/lib/index.html +5 -5
  14. package/coverage/lib/testing/index.d.mts.html +91 -0
  15. package/coverage/lib/testing/index.html +116 -0
  16. package/coverage/src/base-instance-holder-manager.mts.html +589 -0
  17. package/coverage/src/container.mts.html +257 -74
  18. package/coverage/src/decorators/factory.decorator.mts.html +1 -1
  19. package/coverage/src/decorators/index.html +1 -1
  20. package/coverage/src/decorators/index.mts.html +1 -1
  21. package/coverage/src/decorators/injectable.decorator.mts.html +20 -20
  22. package/coverage/src/enums/index.html +1 -1
  23. package/coverage/src/enums/index.mts.html +1 -1
  24. package/coverage/src/enums/injectable-scope.enum.mts.html +1 -1
  25. package/coverage/src/enums/injectable-type.enum.mts.html +1 -1
  26. package/coverage/src/errors/di-error.mts.html +292 -0
  27. package/coverage/src/errors/errors.enum.mts.html +30 -21
  28. package/coverage/src/errors/factory-not-found.mts.html +31 -22
  29. package/coverage/src/errors/factory-token-not-resolved.mts.html +29 -26
  30. package/coverage/src/errors/index.html +56 -41
  31. package/coverage/src/errors/index.mts.html +15 -9
  32. package/coverage/src/errors/instance-destroying.mts.html +31 -22
  33. package/coverage/src/errors/instance-expired.mts.html +31 -22
  34. package/coverage/src/errors/instance-not-found.mts.html +31 -22
  35. package/coverage/src/errors/unknown-error.mts.html +31 -43
  36. package/coverage/src/event-emitter.mts.html +14 -14
  37. package/coverage/src/factory-context.mts.html +1 -1
  38. package/coverage/src/index.html +121 -46
  39. package/coverage/src/index.mts.html +7 -4
  40. package/coverage/src/injection-token.mts.html +28 -28
  41. package/coverage/src/injector.mts.html +1 -1
  42. package/coverage/src/instance-resolver.mts.html +1762 -0
  43. package/coverage/src/interfaces/factory.interface.mts.html +1 -1
  44. package/coverage/src/interfaces/index.html +1 -1
  45. package/coverage/src/interfaces/index.mts.html +1 -1
  46. package/coverage/src/interfaces/on-service-destroy.interface.mts.html +1 -1
  47. package/coverage/src/interfaces/on-service-init.interface.mts.html +1 -1
  48. package/coverage/src/registry.mts.html +28 -28
  49. package/coverage/src/request-context-holder.mts.html +183 -102
  50. package/coverage/src/request-context-manager.mts.html +532 -0
  51. package/coverage/src/service-instantiator.mts.html +49 -49
  52. package/coverage/src/service-invalidator.mts.html +1372 -0
  53. package/coverage/src/service-locator-event-bus.mts.html +48 -48
  54. package/coverage/src/service-locator-instance-holder.mts.html +2 -14
  55. package/coverage/src/service-locator-manager.mts.html +71 -335
  56. package/coverage/src/service-locator.mts.html +240 -2328
  57. package/coverage/src/symbols/index.html +1 -1
  58. package/coverage/src/symbols/index.mts.html +1 -1
  59. package/coverage/src/symbols/injectable-token.mts.html +1 -1
  60. package/coverage/src/testing/index.html +131 -0
  61. package/coverage/src/testing/index.mts.html +88 -0
  62. package/coverage/src/testing/test-container.mts.html +445 -0
  63. package/coverage/src/token-processor.mts.html +607 -0
  64. package/coverage/src/utils/defer.mts.html +28 -214
  65. package/coverage/src/utils/get-injectable-token.mts.html +7 -7
  66. package/coverage/src/utils/get-injectors.mts.html +99 -99
  67. package/coverage/src/utils/index.html +15 -15
  68. package/coverage/src/utils/index.mts.html +4 -7
  69. package/coverage/src/utils/types.mts.html +1 -1
  70. package/docs/injectable.md +51 -11
  71. package/docs/scopes.md +63 -29
  72. package/lib/_tsup-dts-rollup.d.mts +447 -212
  73. package/lib/_tsup-dts-rollup.d.ts +447 -212
  74. package/lib/chunk-44F3LXW5.mjs +2043 -0
  75. package/lib/chunk-44F3LXW5.mjs.map +1 -0
  76. package/lib/index.d.mts +6 -4
  77. package/lib/index.d.ts +6 -4
  78. package/lib/index.js +1199 -773
  79. package/lib/index.js.map +1 -1
  80. package/lib/index.mjs +4 -1599
  81. package/lib/index.mjs.map +1 -1
  82. package/lib/testing/index.d.mts +2 -0
  83. package/lib/testing/index.d.ts +2 -0
  84. package/lib/testing/index.js +2060 -0
  85. package/lib/testing/index.js.map +1 -0
  86. package/lib/testing/index.mjs +73 -0
  87. package/lib/testing/index.mjs.map +1 -0
  88. package/package.json +11 -1
  89. package/src/__tests__/container.spec.mts +47 -13
  90. package/src/__tests__/errors.spec.mts +53 -27
  91. package/src/__tests__/injectable.spec.mts +73 -0
  92. package/src/__tests__/request-scope.spec.mts +0 -2
  93. package/src/__tests__/service-locator-manager.spec.mts +12 -82
  94. package/src/__tests__/service-locator.spec.mts +1009 -1
  95. package/src/__type-tests__/inject.spec-d.mts +30 -7
  96. package/src/__type-tests__/injectable.spec-d.mts +76 -37
  97. package/src/base-instance-holder-manager.mts +2 -9
  98. package/src/container.mts +70 -10
  99. package/src/decorators/injectable.decorator.mts +29 -5
  100. package/src/errors/di-error.mts +69 -0
  101. package/src/errors/index.mts +9 -7
  102. package/src/injection-token.mts +1 -0
  103. package/src/injector.mts +2 -0
  104. package/src/instance-resolver.mts +559 -0
  105. package/src/request-context-holder.mts +0 -2
  106. package/src/request-context-manager.mts +149 -0
  107. package/src/service-invalidator.mts +429 -0
  108. package/src/service-locator-instance-holder.mts +0 -4
  109. package/src/service-locator-manager.mts +10 -40
  110. package/src/service-locator.mts +86 -782
  111. package/src/testing/README.md +80 -0
  112. package/src/testing/__tests__/test-container.spec.mts +173 -0
  113. package/src/testing/index.mts +1 -0
  114. package/src/testing/test-container.mts +120 -0
  115. package/src/token-processor.mts +174 -0
  116. package/src/utils/get-injectors.mts +161 -24
  117. package/src/utils/index.mts +0 -1
  118. package/src/utils/types.mts +12 -8
  119. package/tsup.config.mts +1 -1
  120. package/src/__tests__/defer.spec.mts +0 -166
  121. package/src/errors/errors.enum.mts +0 -8
  122. package/src/errors/factory-not-found.mts +0 -8
  123. package/src/errors/factory-token-not-resolved.mts +0 -10
  124. package/src/errors/instance-destroying.mts +0 -8
  125. package/src/errors/instance-expired.mts +0 -8
  126. package/src/errors/instance-not-found.mts +0 -8
  127. package/src/errors/unknown-error.mts +0 -15
  128. package/src/utils/defer.mts +0 -73
@@ -0,0 +1,149 @@
1
+ import type { RequestContextHolder } from './request-context-holder.mjs'
2
+
3
+ import { DefaultRequestContextHolder } from './request-context-holder.mjs'
4
+
5
+ /**
6
+ * RequestContextManager handles request context lifecycle management.
7
+ * Extracted from ServiceLocator to improve separation of concerns.
8
+ */
9
+ export class RequestContextManager {
10
+ private readonly requestContexts = new Map<string, RequestContextHolder>()
11
+ private currentRequestContext: RequestContextHolder | null = null
12
+
13
+ constructor(private readonly logger: Console | null = null) {}
14
+
15
+ /**
16
+ * Begins a new request context with the given parameters.
17
+ * @param requestId Unique identifier for this request
18
+ * @param metadata Optional metadata for the request
19
+ * @param priority Priority for resolution (higher = more priority)
20
+ * @returns The created request context holder
21
+ */
22
+ beginRequest(
23
+ requestId: string,
24
+ metadata?: Record<string, any>,
25
+ priority: number = 100,
26
+ ): RequestContextHolder {
27
+ if (this.requestContexts.has(requestId)) {
28
+ throw new Error(
29
+ `[RequestContextManager] Request context ${requestId} already exists`,
30
+ )
31
+ }
32
+
33
+ const contextHolder = new DefaultRequestContextHolder(
34
+ requestId,
35
+ priority,
36
+ metadata,
37
+ )
38
+ this.requestContexts.set(requestId, contextHolder)
39
+ this.currentRequestContext = contextHolder
40
+
41
+ this.logger?.log(
42
+ `[RequestContextManager] Started request context: ${requestId}`,
43
+ )
44
+ return contextHolder
45
+ }
46
+
47
+ /**
48
+ * Ends a request context and cleans up all associated instances.
49
+ * @param requestId The request ID to end
50
+ */
51
+ async endRequest(requestId: string): Promise<void> {
52
+ const contextHolder = this.requestContexts.get(requestId)
53
+ if (!contextHolder) {
54
+ this.logger?.warn(
55
+ `[RequestContextManager] Request context ${requestId} not found`,
56
+ )
57
+ return
58
+ }
59
+
60
+ this.logger?.log(
61
+ `[RequestContextManager] Ending request context: ${requestId}`,
62
+ )
63
+
64
+ // Clean up all request-scoped instances
65
+ const cleanupPromises: Promise<any>[] = []
66
+ for (const [, holder] of contextHolder.holders) {
67
+ if (holder.destroyListeners.length > 0) {
68
+ cleanupPromises.push(
69
+ Promise.all(holder.destroyListeners.map((listener) => listener())),
70
+ )
71
+ }
72
+ }
73
+
74
+ await Promise.all(cleanupPromises)
75
+
76
+ // Clear the context
77
+ contextHolder.clear()
78
+ this.requestContexts.delete(requestId)
79
+
80
+ // Reset current context if it was the one being ended
81
+ if (this.currentRequestContext === contextHolder) {
82
+ this.currentRequestContext =
83
+ Array.from(this.requestContexts.values()).at(-1) ?? null
84
+ }
85
+
86
+ this.logger?.log(
87
+ `[RequestContextManager] Request context ${requestId} ended`,
88
+ )
89
+ }
90
+
91
+ /**
92
+ * Gets the current request context.
93
+ * @returns The current request context holder or null
94
+ */
95
+ getCurrentRequestContext(): RequestContextHolder | null {
96
+ return this.currentRequestContext
97
+ }
98
+
99
+ /**
100
+ * Sets the current request context.
101
+ * @param requestId The request ID to set as current
102
+ */
103
+ setCurrentRequestContext(requestId: string): void {
104
+ const contextHolder = this.requestContexts.get(requestId)
105
+ if (!contextHolder) {
106
+ throw new Error(
107
+ `[RequestContextManager] Request context ${requestId} not found`,
108
+ )
109
+ }
110
+ this.currentRequestContext = contextHolder
111
+ }
112
+
113
+ /**
114
+ * Gets all request contexts.
115
+ * @returns Map of request contexts
116
+ */
117
+ getRequestContexts(): Map<string, RequestContextHolder> {
118
+ return this.requestContexts
119
+ }
120
+
121
+ /**
122
+ * Clears all request contexts.
123
+ */
124
+ async clearAllRequestContexts(): Promise<void> {
125
+ const requestIds = Array.from(this.requestContexts.keys())
126
+
127
+ if (requestIds.length === 0) {
128
+ this.logger?.log('[RequestContextManager] No request contexts to clear')
129
+ return
130
+ }
131
+
132
+ this.logger?.log(
133
+ `[RequestContextManager] Clearing ${requestIds.length} request contexts: ${requestIds.join(', ')}`,
134
+ )
135
+
136
+ // Clear request contexts sequentially to avoid race conditions
137
+ for (const requestId of requestIds) {
138
+ try {
139
+ await this.endRequest(requestId)
140
+ } catch (error) {
141
+ this.logger?.error(
142
+ `[RequestContextManager] Error clearing request context ${requestId}:`,
143
+ error,
144
+ )
145
+ // Continue with other request contexts even if one fails
146
+ }
147
+ }
148
+ }
149
+ }
@@ -0,0 +1,429 @@
1
+ /* eslint-disable @typescript-eslint/no-explicit-any */
2
+ import type { RequestContextManager } from './request-context-manager.mjs'
3
+ import type { ServiceLocatorEventBus } from './service-locator-event-bus.mjs'
4
+ import type { ServiceLocatorInstanceHolder } from './service-locator-instance-holder.mjs'
5
+ import type { ServiceLocatorManager } from './service-locator-manager.mjs'
6
+
7
+ import { ServiceLocatorInstanceHolderStatus } from './service-locator-instance-holder.mjs'
8
+
9
+ export interface ClearAllOptions {
10
+ /** Whether to also clear request contexts (default: true) */
11
+ clearRequestContexts?: boolean
12
+ /** Maximum number of invalidation rounds to prevent infinite loops (default: 10) */
13
+ maxRounds?: number
14
+ /** Whether to wait for all services to settle before starting (default: true) */
15
+ waitForSettlement?: boolean
16
+ }
17
+
18
+ /**
19
+ * ServiceInvalidator handles service invalidation, cleanup, and graceful clearing.
20
+ * Extracted from ServiceLocator to improve separation of concerns.
21
+ */
22
+ export class ServiceInvalidator {
23
+ constructor(
24
+ private readonly manager: ServiceLocatorManager,
25
+ private readonly requestContextManager: RequestContextManager,
26
+ private readonly eventBus: ServiceLocatorEventBus,
27
+ private readonly logger: Console | null = null,
28
+ ) {}
29
+
30
+ /**
31
+ * Invalidates a service and all its dependencies.
32
+ */
33
+ invalidate(service: string, round = 1): Promise<any> {
34
+ this.logger?.log(
35
+ `[ServiceInvalidator] Starting invalidation process for ${service}`,
36
+ )
37
+ const [, toInvalidate] = this.manager.get(service)
38
+
39
+ const promises = []
40
+ if (toInvalidate) {
41
+ promises.push(this.invalidateHolder(service, toInvalidate, round))
42
+ }
43
+
44
+ // Also invalidate request-scoped instances that depend on the service or match the service name
45
+ const requestContexts = this.requestContextManager.getRequestContexts()
46
+ for (const [requestId, requestContext] of requestContexts.entries()) {
47
+ const holder = requestContext.get(service)
48
+ if (holder) {
49
+ this.logger?.log(
50
+ `[ServiceInvalidator] Invalidating request-scoped instance ${service} in request ${requestId}`,
51
+ )
52
+ promises.push(
53
+ this.invalidateRequestHolder(requestId, service, holder, round),
54
+ )
55
+ }
56
+ }
57
+
58
+ return Promise.all(promises)
59
+ }
60
+
61
+ /**
62
+ * Gracefully clears all services in the ServiceLocator using invalidation logic.
63
+ * This method respects service dependencies and ensures proper cleanup order.
64
+ * Services that depend on others will be invalidated first, then their dependencies.
65
+ */
66
+ async clearAll(options: ClearAllOptions = {}): Promise<void> {
67
+ const {
68
+ clearRequestContexts = true,
69
+ maxRounds = 10,
70
+ waitForSettlement = true,
71
+ } = options
72
+
73
+ this.logger?.log(
74
+ '[ServiceInvalidator] Starting graceful clearing of all services',
75
+ )
76
+
77
+ // Wait for all services to settle if requested
78
+ if (waitForSettlement) {
79
+ this.logger?.log(
80
+ '[ServiceInvalidator] Waiting for all services to settle...',
81
+ )
82
+ await this.ready()
83
+ }
84
+
85
+ // Get all service names that need to be cleared
86
+ const allServiceNames = this.getAllServiceNames()
87
+
88
+ if (allServiceNames.length === 0) {
89
+ this.logger?.log('[ServiceInvalidator] No singleton services to clear')
90
+ } else {
91
+ this.logger?.log(
92
+ `[ServiceInvalidator] Found ${allServiceNames.length} services to clear: ${allServiceNames.join(', ')}`,
93
+ )
94
+
95
+ // Clear services using dependency-aware invalidation
96
+ await this.clearServicesWithDependencyAwareness(
97
+ allServiceNames,
98
+ maxRounds,
99
+ )
100
+ }
101
+
102
+ // Clear request contexts if requested
103
+ if (clearRequestContexts) {
104
+ await this.requestContextManager.clearAllRequestContexts()
105
+ }
106
+
107
+ this.logger?.log('[ServiceInvalidator] Graceful clearing completed')
108
+ }
109
+
110
+ /**
111
+ * Waits for all services to settle (either created, destroyed, or error state).
112
+ */
113
+ async ready(): Promise<void> {
114
+ const holders = Array.from(this.manager.filter(() => true)).map(
115
+ ([, holder]) => holder,
116
+ )
117
+ await Promise.all(
118
+ holders.map((holder) => this.waitForHolderToSettle(holder)),
119
+ )
120
+ }
121
+
122
+ /**
123
+ * Invalidates a single holder based on its current status.
124
+ */
125
+ private async invalidateHolder(
126
+ key: string,
127
+ holder: ServiceLocatorInstanceHolder<any>,
128
+ round: number,
129
+ ): Promise<void> {
130
+ await this.invalidateHolderByStatus(holder, round, {
131
+ context: key,
132
+ isRequestScoped: false,
133
+ onCreationError: () =>
134
+ this.logger?.error(
135
+ `[ServiceInvalidator] ${key} creation triggered too many invalidation rounds`,
136
+ ),
137
+ onRecursiveInvalidate: () => this.invalidate(key, round + 1),
138
+ onDestroy: () => this.destroyHolder(key, holder),
139
+ })
140
+ }
141
+
142
+ /**
143
+ * Invalidates a request-scoped holder based on its current status.
144
+ */
145
+ private async invalidateRequestHolder(
146
+ requestId: string,
147
+ instanceName: string,
148
+ holder: ServiceLocatorInstanceHolder<any>,
149
+ round: number,
150
+ ): Promise<void> {
151
+ await this.invalidateHolderByStatus(holder, round, {
152
+ context: `Request-scoped ${instanceName} in ${requestId}`,
153
+ isRequestScoped: true,
154
+ onCreationError: () =>
155
+ this.logger?.error(
156
+ `[ServiceInvalidator] Request-scoped ${instanceName} in ${requestId} creation triggered too many invalidation rounds`,
157
+ ),
158
+ onRecursiveInvalidate: () =>
159
+ this.invalidateRequestHolder(
160
+ requestId,
161
+ instanceName,
162
+ holder,
163
+ round + 1,
164
+ ),
165
+ onDestroy: () =>
166
+ this.destroyRequestHolder(requestId, instanceName, holder),
167
+ })
168
+ }
169
+
170
+ /**
171
+ * Common invalidation logic for holders based on their status.
172
+ */
173
+ private async invalidateHolderByStatus(
174
+ holder: ServiceLocatorInstanceHolder<any>,
175
+ round: number,
176
+ options: {
177
+ context: string
178
+ isRequestScoped: boolean
179
+ onCreationError: () => void
180
+ onRecursiveInvalidate: () => Promise<void>
181
+ onDestroy: () => Promise<void>
182
+ },
183
+ ): Promise<void> {
184
+ switch (holder.status) {
185
+ case ServiceLocatorInstanceHolderStatus.Destroying:
186
+ await holder.destroyPromise
187
+ break
188
+
189
+ case ServiceLocatorInstanceHolderStatus.Creating:
190
+ await holder.creationPromise
191
+ if (round > 3) {
192
+ options.onCreationError()
193
+ return
194
+ }
195
+ await options.onRecursiveInvalidate()
196
+ break
197
+
198
+ default:
199
+ await options.onDestroy()
200
+ break
201
+ }
202
+ }
203
+
204
+ /**
205
+ * Destroys a holder and cleans up its resources.
206
+ */
207
+ private async destroyHolder(
208
+ key: string,
209
+ holder: ServiceLocatorInstanceHolder<any>,
210
+ ): Promise<void> {
211
+ await this.destroyHolderWithCleanup(holder, {
212
+ context: key,
213
+ logMessage: `[ServiceInvalidator] Invalidating ${key} and notifying listeners`,
214
+ cleanup: () => this.manager.delete(key),
215
+ eventName: key,
216
+ })
217
+ }
218
+
219
+ /**
220
+ * Destroys a request-scoped holder and cleans up its resources.
221
+ */
222
+ private async destroyRequestHolder(
223
+ requestId: string,
224
+ instanceName: string,
225
+ holder: ServiceLocatorInstanceHolder<any>,
226
+ ): Promise<void> {
227
+ await this.destroyHolderWithCleanup(holder, {
228
+ context: `Request-scoped ${instanceName} in ${requestId}`,
229
+ logMessage: `[ServiceInvalidator] Invalidating request-scoped ${instanceName} in ${requestId} and notifying listeners`,
230
+ cleanup: () => {
231
+ const requestContext = this.requestContextManager
232
+ .getRequestContexts()
233
+ .get(requestId)
234
+ if (requestContext) {
235
+ requestContext.delete(instanceName)
236
+ }
237
+ },
238
+ eventName: instanceName,
239
+ })
240
+ }
241
+
242
+ /**
243
+ * Common destroy logic for holders with customizable cleanup.
244
+ */
245
+ private async destroyHolderWithCleanup(
246
+ holder: ServiceLocatorInstanceHolder<any>,
247
+ options: {
248
+ context: string
249
+ logMessage: string
250
+ cleanup: () => void
251
+ eventName: string
252
+ },
253
+ ): Promise<void> {
254
+ holder.status = ServiceLocatorInstanceHolderStatus.Destroying
255
+ this.logger?.log(options.logMessage)
256
+
257
+ holder.destroyPromise = Promise.all(
258
+ holder.destroyListeners.map((listener) => listener()),
259
+ ).then(async () => {
260
+ holder.destroyListeners = []
261
+ holder.deps.clear()
262
+ options.cleanup()
263
+ await this.emitInstanceEvent(options.eventName, 'destroy')
264
+ })
265
+
266
+ await holder.destroyPromise
267
+ }
268
+
269
+ /**
270
+ * Waits for a holder to settle (either created, destroyed, or error state).
271
+ */
272
+ private async waitForHolderToSettle(
273
+ holder: ServiceLocatorInstanceHolder<any>,
274
+ ): Promise<void> {
275
+ switch (holder.status) {
276
+ case ServiceLocatorInstanceHolderStatus.Creating:
277
+ await holder.creationPromise
278
+ break
279
+ case ServiceLocatorInstanceHolderStatus.Destroying:
280
+ await holder.destroyPromise
281
+ break
282
+ // Already settled states
283
+ case ServiceLocatorInstanceHolderStatus.Created:
284
+ case ServiceLocatorInstanceHolderStatus.Error:
285
+ break
286
+ }
287
+ }
288
+
289
+ /**
290
+ * Clears services with dependency awareness, ensuring proper cleanup order.
291
+ * Services with no dependencies are cleared first, then services that depend on them.
292
+ */
293
+ private async clearServicesWithDependencyAwareness(
294
+ serviceNames: string[],
295
+ maxRounds: number,
296
+ ): Promise<void> {
297
+ const clearedServices = new Set<string>()
298
+ let round = 1
299
+
300
+ while (clearedServices.size < serviceNames.length && round <= maxRounds) {
301
+ this.logger?.log(
302
+ `[ServiceInvalidator] Clearing round ${round}/${maxRounds}, ${clearedServices.size}/${serviceNames.length} services cleared`,
303
+ )
304
+
305
+ // Find services that can be cleared in this round
306
+ const servicesToClearThisRound = this.findServicesReadyForClearing(
307
+ serviceNames,
308
+ clearedServices,
309
+ )
310
+
311
+ if (servicesToClearThisRound.length === 0) {
312
+ // If no services can be cleared, try to clear remaining services anyway
313
+ // This handles circular dependencies or other edge cases
314
+ const remainingServices = serviceNames.filter(
315
+ (name) => !clearedServices.has(name),
316
+ )
317
+
318
+ if (remainingServices.length > 0) {
319
+ this.logger?.warn(
320
+ `[ServiceInvalidator] No services ready for clearing, forcing cleanup of remaining: ${remainingServices.join(', ')}`,
321
+ )
322
+ await this.forceClearServices(remainingServices)
323
+ remainingServices.forEach((name) => clearedServices.add(name))
324
+ }
325
+ break
326
+ }
327
+
328
+ // Clear services in this round
329
+ const clearPromises = servicesToClearThisRound.map(
330
+ async (serviceName) => {
331
+ try {
332
+ await this.invalidate(serviceName, round)
333
+ clearedServices.add(serviceName)
334
+ this.logger?.log(
335
+ `[ServiceInvalidator] Successfully cleared service: ${serviceName}`,
336
+ )
337
+ } catch (error) {
338
+ this.logger?.error(
339
+ `[ServiceInvalidator] Error clearing service ${serviceName}:`,
340
+ error,
341
+ )
342
+ // Still mark as cleared to avoid infinite loops
343
+ clearedServices.add(serviceName)
344
+ }
345
+ },
346
+ )
347
+
348
+ await Promise.all(clearPromises)
349
+ round++
350
+ }
351
+
352
+ if (clearedServices.size < serviceNames.length) {
353
+ this.logger?.warn(
354
+ `[ServiceInvalidator] Clearing completed after ${maxRounds} rounds, but ${serviceNames.length - clearedServices.size} services may not have been properly cleared`,
355
+ )
356
+ }
357
+ }
358
+
359
+ /**
360
+ * Finds services that are ready to be cleared in the current round.
361
+ * A service is ready if all its dependencies have already been cleared.
362
+ */
363
+ private findServicesReadyForClearing(
364
+ allServiceNames: string[],
365
+ clearedServices: Set<string>,
366
+ ): string[] {
367
+ return allServiceNames.filter((serviceName) => {
368
+ if (clearedServices.has(serviceName)) {
369
+ return false // Already cleared
370
+ }
371
+
372
+ // Check if this service has any dependencies that haven't been cleared yet
373
+ const [error, holder] = this.manager.get(serviceName)
374
+ if (error) {
375
+ return true // Service not found or in error state, can be cleared
376
+ }
377
+
378
+ // Check if all dependencies have been cleared
379
+ const hasUnclearedDependencies = Array.from(holder.deps).some(
380
+ (dep) => !clearedServices.has(dep),
381
+ )
382
+
383
+ return !hasUnclearedDependencies
384
+ })
385
+ }
386
+
387
+ /**
388
+ * Force clears services that couldn't be cleared through normal dependency resolution.
389
+ * This handles edge cases like circular dependencies.
390
+ */
391
+ private async forceClearServices(serviceNames: string[]): Promise<void> {
392
+ const promises = serviceNames.map(async (serviceName) => {
393
+ try {
394
+ // Directly destroy the holder without going through normal invalidation
395
+ const [error, holder] = this.manager.get(serviceName)
396
+ if (!error && holder) {
397
+ await this.destroyHolder(serviceName, holder)
398
+ }
399
+ } catch (error) {
400
+ this.logger?.error(
401
+ `[ServiceInvalidator] Error force clearing service ${serviceName}:`,
402
+ error,
403
+ )
404
+ }
405
+ })
406
+
407
+ await Promise.all(promises)
408
+ }
409
+
410
+ /**
411
+ * Gets all service names currently managed by the ServiceLocator.
412
+ */
413
+ private getAllServiceNames(): string[] {
414
+ return this.manager.getAllNames()
415
+ }
416
+
417
+ /**
418
+ * Emits events to listeners for instance lifecycle events.
419
+ */
420
+ private emitInstanceEvent(
421
+ name: string,
422
+ event: 'create' | 'destroy' = 'create',
423
+ ) {
424
+ this.logger?.log(
425
+ `[ServiceInvalidator]#emitInstanceEvent() Notifying listeners for ${name} with event ${event}`,
426
+ )
427
+ return this.eventBus.emit(name, event)
428
+ }
429
+ }
@@ -22,7 +22,6 @@ export interface ServiceLocatorInstanceHolderCreating<Instance> {
22
22
  deps: Set<string>
23
23
  destroyListeners: ServiceLocatorInstanceDestroyListener[]
24
24
  createdAt: number
25
- ttl: number
26
25
  }
27
26
 
28
27
  export interface ServiceLocatorInstanceHolderCreated<Instance> {
@@ -36,7 +35,6 @@ export interface ServiceLocatorInstanceHolderCreated<Instance> {
36
35
  deps: Set<string>
37
36
  destroyListeners: ServiceLocatorInstanceDestroyListener[]
38
37
  createdAt: number
39
- ttl: number
40
38
  }
41
39
 
42
40
  export interface ServiceLocatorInstanceHolderDestroying<Instance> {
@@ -50,7 +48,6 @@ export interface ServiceLocatorInstanceHolderDestroying<Instance> {
50
48
  deps: Set<string>
51
49
  destroyListeners: ServiceLocatorInstanceDestroyListener[]
52
50
  createdAt: number
53
- ttl: number
54
51
  }
55
52
 
56
53
  export interface ServiceLocatorInstanceHolderError {
@@ -64,7 +61,6 @@ export interface ServiceLocatorInstanceHolderError {
64
61
  deps: Set<string>
65
62
  destroyListeners: ServiceLocatorInstanceDestroyListener[]
66
63
  createdAt: number
67
- ttl: number
68
64
  }
69
65
 
70
66
  export type ServiceLocatorInstanceHolder<Instance = unknown> =