@navios/di 0.4.2 → 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 (120) 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 +376 -213
  73. package/lib/_tsup-dts-rollup.d.ts +376 -213
  74. package/lib/{chunk-3NLYPYBY.mjs → chunk-44F3LXW5.mjs} +1021 -605
  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 +1192 -776
  79. package/lib/index.js.map +1 -1
  80. package/lib/index.mjs +2 -2
  81. package/lib/testing/index.js +1258 -840
  82. package/lib/testing/index.js.map +1 -1
  83. package/lib/testing/index.mjs +1 -1
  84. package/package.json +1 -1
  85. package/src/__tests__/container.spec.mts +47 -13
  86. package/src/__tests__/errors.spec.mts +53 -27
  87. package/src/__tests__/injectable.spec.mts +73 -0
  88. package/src/__tests__/request-scope.spec.mts +0 -2
  89. package/src/__tests__/service-locator-manager.spec.mts +12 -82
  90. package/src/__tests__/service-locator.spec.mts +1009 -1
  91. package/src/__type-tests__/inject.spec-d.mts +30 -7
  92. package/src/__type-tests__/injectable.spec-d.mts +76 -37
  93. package/src/base-instance-holder-manager.mts +2 -9
  94. package/src/container.mts +61 -9
  95. package/src/decorators/injectable.decorator.mts +29 -5
  96. package/src/errors/di-error.mts +69 -0
  97. package/src/errors/index.mts +9 -7
  98. package/src/injection-token.mts +1 -0
  99. package/src/injector.mts +2 -0
  100. package/src/instance-resolver.mts +559 -0
  101. package/src/request-context-holder.mts +0 -2
  102. package/src/request-context-manager.mts +149 -0
  103. package/src/service-invalidator.mts +429 -0
  104. package/src/service-locator-instance-holder.mts +0 -4
  105. package/src/service-locator-manager.mts +10 -40
  106. package/src/service-locator.mts +86 -782
  107. package/src/token-processor.mts +174 -0
  108. package/src/utils/get-injectors.mts +161 -24
  109. package/src/utils/index.mts +0 -1
  110. package/src/utils/types.mts +12 -8
  111. package/lib/chunk-3NLYPYBY.mjs.map +0 -1
  112. package/src/__tests__/defer.spec.mts +0 -166
  113. package/src/errors/errors.enum.mts +0 -8
  114. package/src/errors/factory-not-found.mts +0 -8
  115. package/src/errors/factory-token-not-resolved.mts +0 -10
  116. package/src/errors/instance-destroying.mts +0 -8
  117. package/src/errors/instance-expired.mts +0 -8
  118. package/src/errors/instance-not-found.mts +0 -8
  119. package/src/errors/unknown-error.mts +0 -15
  120. package/src/utils/defer.mts +0 -73
@@ -2,46 +2,33 @@
2
2
  /* eslint-disable @typescript-eslint/no-empty-object-type */
3
3
  import type { z, ZodObject, ZodOptional } from 'zod/v4'
4
4
 
5
- import type { FactoryContext } from './factory-context.mjs'
6
5
  import type {
7
6
  AnyInjectableType,
8
- BaseInjectionTokenSchemaType,
9
7
  InjectionTokenSchemaType,
10
- InjectionTokenType,
11
- OptionalInjectionTokenSchemaType,
12
8
  } from './injection-token.mjs'
13
9
  import type { Registry } from './registry.mjs'
14
10
  import type { RequestContextHolder } from './request-context-holder.mjs'
15
- import type { ServiceLocatorInstanceHolder } from './service-locator-instance-holder.mjs'
11
+ import type { ClearAllOptions } from './service-invalidator.mjs'
16
12
  import type { Injectors } from './utils/index.mjs'
17
13
 
18
- import { InjectableScope } from './enums/index.mjs'
19
- import {
20
- ErrorsEnum,
21
- FactoryNotFound,
22
- FactoryTokenNotResolved,
23
- UnknownError,
24
- } from './errors/index.mjs'
25
- import {
26
- BoundInjectionToken,
27
- FactoryInjectionToken,
28
- InjectionToken,
29
- } from './injection-token.mjs'
30
14
  import { defaultInjectors } from './injector.mjs'
15
+ import { InstanceResolver } from './instance-resolver.mjs'
31
16
  import { globalRegistry } from './registry.mjs'
32
- import { DefaultRequestContextHolder } from './request-context-holder.mjs'
17
+ import { RequestContextManager } from './request-context-manager.mjs'
33
18
  import { ServiceInstantiator } from './service-instantiator.mjs'
19
+ import { ServiceInvalidator } from './service-invalidator.mjs'
34
20
  import { ServiceLocatorEventBus } from './service-locator-event-bus.mjs'
35
- import { ServiceLocatorInstanceHolderStatus } from './service-locator-instance-holder.mjs'
36
21
  import { ServiceLocatorManager } from './service-locator-manager.mjs'
37
- import { getInjectableToken } from './utils/index.mjs'
22
+ import { TokenProcessor } from './token-processor.mjs'
38
23
 
39
24
  export class ServiceLocator {
40
25
  private readonly eventBus: ServiceLocatorEventBus
41
26
  private readonly manager: ServiceLocatorManager
42
27
  private readonly serviceInstantiator: ServiceInstantiator
43
- private readonly requestContexts = new Map<string, RequestContextHolder>()
44
- private currentRequestContext: RequestContextHolder | null = null
28
+ private readonly tokenProcessor: TokenProcessor
29
+ private readonly requestContextManager: RequestContextManager
30
+ private readonly serviceInvalidator: ServiceInvalidator
31
+ private readonly instanceResolver: InstanceResolver
45
32
 
46
33
  constructor(
47
34
  private readonly registry: Registry = globalRegistry,
@@ -51,6 +38,22 @@ export class ServiceLocator {
51
38
  this.eventBus = new ServiceLocatorEventBus(logger)
52
39
  this.manager = new ServiceLocatorManager(logger)
53
40
  this.serviceInstantiator = new ServiceInstantiator(injectors)
41
+ this.tokenProcessor = new TokenProcessor(logger)
42
+ this.requestContextManager = new RequestContextManager(logger)
43
+ this.serviceInvalidator = new ServiceInvalidator(
44
+ this.manager,
45
+ this.requestContextManager,
46
+ this.eventBus,
47
+ logger,
48
+ )
49
+ this.instanceResolver = new InstanceResolver(
50
+ this.registry,
51
+ this.manager,
52
+ this.serviceInstantiator,
53
+ this.tokenProcessor,
54
+ logger,
55
+ this,
56
+ )
54
57
  }
55
58
 
56
59
  // ============================================================================
@@ -65,13 +68,25 @@ export class ServiceLocator {
65
68
  return this.manager
66
69
  }
67
70
 
71
+ getRequestContexts() {
72
+ return this.requestContextManager.getRequestContexts()
73
+ }
74
+
75
+ getRequestContextManager() {
76
+ return this.requestContextManager
77
+ }
78
+
79
+ getServiceInvalidator() {
80
+ return this.serviceInvalidator
81
+ }
82
+
68
83
  public getInstanceIdentifier(token: AnyInjectableType, args?: any): string {
69
84
  const [err, { actualToken, validatedArgs }] =
70
- this.validateAndResolveTokenArgs(token, args)
85
+ this.tokenProcessor.validateAndResolveTokenArgs(token, args)
71
86
  if (err) {
72
87
  throw err
73
88
  }
74
- return this.generateInstanceName(actualToken, validatedArgs)
89
+ return this.tokenProcessor.generateInstanceName(actualToken, validatedArgs)
75
90
  }
76
91
 
77
92
  public async getInstance(
@@ -79,30 +94,30 @@ export class ServiceLocator {
79
94
  args?: any,
80
95
  onPrepare?: (data: {
81
96
  instanceName: string
82
- actualToken: InjectionTokenType
97
+ actualToken: any
83
98
  validatedArgs?: any
84
99
  }) => void,
85
100
  ) {
86
- const [err, data] = await this.resolveTokenAndPrepareInstanceName(
101
+ const [err, data] = await this.instanceResolver.resolveInstance(
87
102
  token,
88
103
  args,
104
+ this.requestContextManager.getCurrentRequestContext() || undefined,
89
105
  )
90
106
  if (err) {
91
107
  return [err]
92
108
  }
93
109
 
94
- const { instanceName, validatedArgs, actualToken, realToken } = data
95
- onPrepare?.({ instanceName, actualToken, validatedArgs })
96
-
97
- const [error, holder] = await this.retrieveOrCreateInstanceByInstanceName(
98
- instanceName,
99
- realToken,
100
- validatedArgs,
101
- )
102
- if (error) {
103
- return [error]
110
+ // Call onPrepare callback if provided
111
+ if (onPrepare) {
112
+ const instanceName = this.getInstanceIdentifier(token, args)
113
+ const [tokenErr, { actualToken, validatedArgs }] =
114
+ this.tokenProcessor.validateAndResolveTokenArgs(token, args)
115
+ if (!tokenErr) {
116
+ onPrepare({ instanceName, actualToken, validatedArgs })
117
+ }
104
118
  }
105
- return [undefined, holder.instance]
119
+
120
+ return [undefined, data]
106
121
  }
107
122
 
108
123
  public async getOrThrowInstance<Instance>(
@@ -127,126 +142,27 @@ export class ServiceLocator {
127
142
  ? z.input<Schema> | undefined
128
143
  : undefined,
129
144
  ): Instance | null {
130
- const [err, { actualToken, validatedArgs }] =
131
- this.validateAndResolveTokenArgs(token, args)
132
- if (err) {
133
- return null
134
- }
135
- const instanceName = this.generateInstanceName(actualToken, validatedArgs)
136
-
137
- // Try request context first
138
- if (this.currentRequestContext) {
139
- const requestHolder = this.currentRequestContext.get(instanceName)
140
- if (requestHolder) {
141
- return requestHolder.instance as Instance
142
- }
143
- }
144
-
145
- // Try singleton manager
146
- const [error, holder] = this.manager.get(instanceName)
147
- if (error) {
148
- return null
149
- }
150
- return holder.instance as Instance
151
- }
152
-
153
- invalidate(service: string, round = 1): Promise<any> {
154
- this.logger?.log(
155
- `[ServiceLocator] Starting invalidation process for ${service}`,
156
- )
157
- const toInvalidate = this.manager.filter(
158
- (holder) => holder.name === service || holder.deps.has(service),
159
- )
160
- const promises = []
161
- for (const [key, holder] of toInvalidate.entries()) {
162
- promises.push(this.invalidateHolder(key, holder, round))
163
- }
164
- return Promise.all(promises)
165
- }
166
-
167
- /**
168
- * Invalidates a single holder based on its current status.
169
- */
170
- private async invalidateHolder(
171
- key: string,
172
- holder: ServiceLocatorInstanceHolder<any>,
173
- round: number,
174
- ): Promise<void> {
175
- switch (holder.status) {
176
- case ServiceLocatorInstanceHolderStatus.Destroying:
177
- this.logger?.trace(`[ServiceLocator] ${key} is already being destroyed`)
178
- await holder.destroyPromise
179
- break
180
-
181
- case ServiceLocatorInstanceHolderStatus.Creating:
182
- this.logger?.trace(
183
- `[ServiceLocator] ${key} is being created, waiting...`,
184
- )
185
- await holder.creationPromise
186
- if (round > 3) {
187
- this.logger?.error(
188
- `[ServiceLocator] ${key} creation triggered too many invalidation rounds`,
189
- )
190
- return
191
- }
192
- await this.invalidate(key, round + 1)
193
- break
194
-
195
- default:
196
- await this.destroyHolder(key, holder)
197
- break
198
- }
199
- }
200
-
201
- /**
202
- * Destroys a holder and cleans up its resources.
203
- */
204
- private async destroyHolder(
205
- key: string,
206
- holder: ServiceLocatorInstanceHolder<any>,
207
- ): Promise<void> {
208
- holder.status = ServiceLocatorInstanceHolderStatus.Destroying
209
- this.logger?.log(
210
- `[ServiceLocator] Invalidating ${key} and notifying listeners`,
145
+ return this.instanceResolver.getSyncInstance(
146
+ token,
147
+ args as any,
148
+ this.requestContextManager.getCurrentRequestContext(),
211
149
  )
212
-
213
- holder.destroyPromise = Promise.all(
214
- holder.destroyListeners.map((listener) => listener()),
215
- ).then(async () => {
216
- this.manager.delete(key)
217
- await this.emitInstanceEvent(key, 'destroy')
218
- })
219
-
220
- await holder.destroyPromise
221
150
  }
222
151
 
223
- async ready() {
224
- const holders = Array.from(this.manager.filter(() => true)).map(
225
- ([, holder]) => holder,
226
- )
227
- await Promise.all(
228
- holders.map((holder) => this.waitForHolderToSettle(holder)),
229
- )
152
+ invalidate(service: string, round = 1): Promise<any> {
153
+ return this.serviceInvalidator.invalidate(service, round)
230
154
  }
231
155
 
232
156
  /**
233
- * Waits for a holder to settle (either created, destroyed, or error state).
157
+ * Gracefully clears all services in the ServiceLocator using invalidation logic.
158
+ * This method respects service dependencies and ensures proper cleanup order.
159
+ * Services that depend on others will be invalidated first, then their dependencies.
160
+ *
161
+ * @param options Optional configuration for the clearing process
162
+ * @returns Promise that resolves when all services have been cleared
234
163
  */
235
- private async waitForHolderToSettle(
236
- holder: ServiceLocatorInstanceHolder<any>,
237
- ): Promise<void> {
238
- switch (holder.status) {
239
- case ServiceLocatorInstanceHolderStatus.Creating:
240
- await holder.creationPromise
241
- break
242
- case ServiceLocatorInstanceHolderStatus.Destroying:
243
- await holder.destroyPromise
244
- break
245
- // Already settled states
246
- case ServiceLocatorInstanceHolderStatus.Created:
247
- case ServiceLocatorInstanceHolderStatus.Error:
248
- break
249
- }
164
+ async clearAll(options: ClearAllOptions = {}): Promise<void> {
165
+ return this.serviceInvalidator.clearAll(options)
250
166
  }
251
167
 
252
168
  // ============================================================================
@@ -265,22 +181,11 @@ export class ServiceLocator {
265
181
  metadata?: Record<string, any>,
266
182
  priority: number = 100,
267
183
  ): RequestContextHolder {
268
- if (this.requestContexts.has(requestId)) {
269
- throw new Error(
270
- `[ServiceLocator] Request context ${requestId} already exists`,
271
- )
272
- }
273
-
274
- const contextHolder = new DefaultRequestContextHolder(
184
+ return this.requestContextManager.beginRequest(
275
185
  requestId,
276
- priority,
277
186
  metadata,
187
+ priority,
278
188
  )
279
- this.requestContexts.set(requestId, contextHolder)
280
- this.currentRequestContext = contextHolder
281
-
282
- this.logger?.log(`[ServiceLocator] Started request context: ${requestId}`)
283
- return contextHolder
284
189
  }
285
190
 
286
191
  /**
@@ -288,39 +193,7 @@ export class ServiceLocator {
288
193
  * @param requestId The request ID to end
289
194
  */
290
195
  async endRequest(requestId: string): Promise<void> {
291
- const contextHolder = this.requestContexts.get(requestId)
292
- if (!contextHolder) {
293
- this.logger?.warn(
294
- `[ServiceLocator] Request context ${requestId} not found`,
295
- )
296
- return
297
- }
298
-
299
- this.logger?.log(`[ServiceLocator] Ending request context: ${requestId}`)
300
-
301
- // Clean up all request-scoped instances
302
- const cleanupPromises: Promise<any>[] = []
303
- for (const [, holder] of contextHolder.holders) {
304
- if (holder.destroyListeners.length > 0) {
305
- cleanupPromises.push(
306
- Promise.all(holder.destroyListeners.map((listener) => listener())),
307
- )
308
- }
309
- }
310
-
311
- await Promise.all(cleanupPromises)
312
-
313
- // Clear the context
314
- contextHolder.clear()
315
- this.requestContexts.delete(requestId)
316
-
317
- // Reset current context if it was the one being ended
318
- if (this.currentRequestContext === contextHolder) {
319
- this.currentRequestContext =
320
- Array.from(this.requestContexts.values()).at(-1) ?? null
321
- }
322
-
323
- this.logger?.log(`[ServiceLocator] Request context ${requestId} ended`)
196
+ return this.requestContextManager.endRequest(requestId)
324
197
  }
325
198
 
326
199
  /**
@@ -328,7 +201,7 @@ export class ServiceLocator {
328
201
  * @returns The current request context holder or null
329
202
  */
330
203
  getCurrentRequestContext(): RequestContextHolder | null {
331
- return this.currentRequestContext
204
+ return this.requestContextManager.getCurrentRequestContext()
332
205
  }
333
206
 
334
207
  /**
@@ -336,607 +209,38 @@ export class ServiceLocator {
336
209
  * @param requestId The request ID to set as current
337
210
  */
338
211
  setCurrentRequestContext(requestId: string): void {
339
- const contextHolder = this.requestContexts.get(requestId)
340
- if (!contextHolder) {
341
- throw new Error(`[ServiceLocator] Request context ${requestId} not found`)
342
- }
343
- this.currentRequestContext = contextHolder
344
- }
345
-
346
- // ============================================================================
347
- // PRIVATE METHODS
348
- // ============================================================================
349
-
350
- /**
351
- * Validates and resolves token arguments, handling factory token resolution and validation.
352
- */
353
- private validateAndResolveTokenArgs(
354
- token: AnyInjectableType,
355
- args?: any,
356
- ): [
357
- FactoryTokenNotResolved | UnknownError | undefined,
358
- { actualToken: InjectionTokenType; validatedArgs?: any },
359
- ] {
360
- let actualToken = token as InjectionToken<any, any>
361
- if (typeof token === 'function') {
362
- actualToken = getInjectableToken(token)
363
- }
364
- let realArgs = args
365
- if (actualToken instanceof BoundInjectionToken) {
366
- realArgs = actualToken.value
367
- } else if (actualToken instanceof FactoryInjectionToken) {
368
- if (actualToken.resolved) {
369
- realArgs = actualToken.value
370
- } else {
371
- return [new FactoryTokenNotResolved(token.name), { actualToken }]
372
- }
373
- }
374
- if (!actualToken.schema) {
375
- return [undefined, { actualToken, validatedArgs: realArgs }]
376
- }
377
- const validatedArgs = actualToken.schema?.safeParse(realArgs)
378
- if (validatedArgs && !validatedArgs.success) {
379
- this.logger?.error(
380
- `[ServiceLocator]#validateAndResolveTokenArgs(): Error validating args for ${actualToken.name.toString()}`,
381
- validatedArgs.error,
382
- )
383
- return [new UnknownError(validatedArgs.error), { actualToken }]
384
- }
385
- return [undefined, { actualToken, validatedArgs: validatedArgs?.data }]
212
+ return this.requestContextManager.setCurrentRequestContext(requestId)
386
213
  }
387
214
 
388
215
  /**
389
- * Internal method to resolve token args and create instance name.
390
- * Handles factory token resolution and validation.
216
+ * Waits for all services to settle (either created, destroyed, or error state).
391
217
  */
392
- private async resolveTokenAndPrepareInstanceName(
393
- token: AnyInjectableType,
394
- args?: any,
395
- ): Promise<
396
- | [
397
- undefined,
398
- {
399
- instanceName: string
400
- validatedArgs: any
401
- actualToken: InjectionTokenType
402
- realToken: InjectionToken<any, any>
403
- },
404
- ]
405
- | [UnknownError | FactoryTokenNotResolved]
406
- > {
407
- const [err, { actualToken, validatedArgs }] =
408
- this.validateAndResolveTokenArgs(token, args)
409
- if (err instanceof UnknownError) {
410
- return [err]
411
- } else if (
412
- (err as any) instanceof FactoryTokenNotResolved &&
413
- actualToken instanceof FactoryInjectionToken
414
- ) {
415
- this.logger?.log(
416
- `[ServiceLocator]#resolveTokenAndPrepareInstanceName() Factory token not resolved, resolving it`,
417
- )
418
- await actualToken.resolve(this.createFactoryContext())
419
- return this.resolveTokenAndPrepareInstanceName(token)
420
- }
421
- const instanceName = this.generateInstanceName(actualToken, validatedArgs)
422
- // Determine the real token (the actual InjectionToken that will be used for resolution)
423
- const realToken =
424
- actualToken instanceof BoundInjectionToken ||
425
- actualToken instanceof FactoryInjectionToken
426
- ? actualToken.token
427
- : actualToken
428
- return [undefined, { instanceName, validatedArgs, actualToken, realToken }]
218
+ async ready(): Promise<void> {
219
+ return this.serviceInvalidator.ready()
429
220
  }
430
221
 
431
222
  /**
432
- * Gets an instance by its instance name, handling all the logic after instance name creation.
223
+ * Helper method for TokenProcessor to access pre-prepared instances.
224
+ * This is needed for the factory context creation.
433
225
  */
434
- private async retrieveOrCreateInstanceByInstanceName(
435
- instanceName: string,
436
- realToken: InjectionToken<any, any>,
437
- realArgs: any,
438
- ): Promise<
439
- | [undefined, ServiceLocatorInstanceHolder<any>]
440
- | [UnknownError | FactoryNotFound]
441
- > {
442
- // Try to get existing instance (handles both request-scoped and singleton)
443
- const existingHolder = await this.tryGetExistingInstance(
444
- instanceName,
445
- realToken,
446
- )
447
- if (existingHolder) {
448
- return existingHolder
449
- }
450
-
451
- // No existing instance found, create a new one
452
- const result = await this.createNewInstance(
453
- instanceName,
454
- realToken,
455
- realArgs,
456
- )
457
- if (result[0]) {
458
- return [result[0]]
459
- }
460
-
461
- const [, holder] = result
462
- return this.waitForInstanceReady(holder)
463
- }
464
-
465
- /**
466
- * Attempts to retrieve an existing instance, handling request-scoped and singleton instances.
467
- * Returns null if no instance exists and a new one should be created.
468
- */
469
- private async tryGetExistingInstance(
470
- instanceName: string,
471
- realToken: InjectionToken<any, any>,
472
- ): Promise<
473
- [undefined, ServiceLocatorInstanceHolder<any>] | [UnknownError] | null
474
- > {
475
- // Check request-scoped instances first
476
- const requestResult = await this.tryGetRequestScopedInstance(
477
- instanceName,
478
- realToken,
479
- )
480
- if (requestResult) {
481
- return requestResult
482
- }
483
-
484
- // Check singleton instances
485
- return this.tryGetSingletonInstance(instanceName)
486
- }
487
-
488
- /**
489
- * Attempts to get a request-scoped instance if applicable.
490
- */
491
- private async tryGetRequestScopedInstance(
492
- instanceName: string,
493
- realToken: InjectionToken<any, any>,
494
- ): Promise<
495
- [undefined, ServiceLocatorInstanceHolder<any>] | [UnknownError] | null
496
- > {
497
- if (!this.registry.has(realToken)) {
498
- return null
499
- }
500
-
501
- const record = this.registry.get(realToken)
502
- if (record.scope !== InjectableScope.Request) {
503
- return null
504
- }
505
-
506
- if (!this.currentRequestContext) {
507
- this.logger?.log(
508
- `[ServiceLocator] No current request context available for request-scoped service ${instanceName}`,
509
- )
510
- return [new UnknownError(ErrorsEnum.InstanceNotFound)]
511
- }
512
-
513
- const requestHolder = this.currentRequestContext.get(instanceName)
514
- if (!requestHolder) {
515
- return null
516
- }
517
-
518
- return this.waitForInstanceReady(requestHolder)
519
- }
520
-
521
- /**
522
- * Attempts to get a singleton instance from the manager.
523
- */
524
- private async tryGetSingletonInstance(
525
- instanceName: string,
526
- ): Promise<
527
- [undefined, ServiceLocatorInstanceHolder<any>] | [UnknownError] | null
528
- > {
529
- const [error, holder] = this.manager.get(instanceName)
530
-
531
- if (!error) {
532
- return this.waitForInstanceReady(holder)
533
- }
534
-
535
- // Handle recovery scenarios
536
- switch (error.code) {
537
- case ErrorsEnum.InstanceDestroying:
538
- this.logger?.log(
539
- `[ServiceLocator] Instance ${instanceName} is being destroyed, waiting...`,
540
- )
541
- await holder?.destroyPromise
542
- // Retry after destruction is complete
543
- return this.tryGetSingletonInstance(instanceName)
544
-
545
- case ErrorsEnum.InstanceExpired:
546
- this.logger?.log(
547
- `[ServiceLocator] Instance ${instanceName} expired, invalidating...`,
548
- )
549
- await this.invalidate(instanceName)
550
- // Retry after invalidation
551
- return this.tryGetSingletonInstance(instanceName)
552
-
553
- case ErrorsEnum.InstanceNotFound:
554
- return null // Instance doesn't exist, should create new one
555
-
556
- default:
557
- return [error]
558
- }
559
- }
560
-
561
- /**
562
- * Waits for an instance holder to be ready and returns the appropriate result.
563
- */
564
- private async waitForInstanceReady<T>(
565
- holder: ServiceLocatorInstanceHolder<T>,
566
- ): Promise<[undefined, ServiceLocatorInstanceHolder<T>] | [UnknownError]> {
567
- switch (holder.status) {
568
- case ServiceLocatorInstanceHolderStatus.Creating:
569
- await holder.creationPromise
570
- return this.waitForInstanceReady(holder)
571
-
572
- case ServiceLocatorInstanceHolderStatus.Destroying:
573
- return [new UnknownError(ErrorsEnum.InstanceDestroying)]
574
-
575
- case ServiceLocatorInstanceHolderStatus.Error:
576
- return [holder.instance as UnknownError]
577
-
578
- case ServiceLocatorInstanceHolderStatus.Created:
579
- return [undefined, holder]
580
-
581
- default:
582
- return [new UnknownError(ErrorsEnum.InstanceNotFound)]
583
- }
584
- }
585
-
586
- /**
587
- * Emits events to listeners for instance lifecycle events.
588
- */
589
- private emitInstanceEvent(
590
- name: string,
591
- event: 'create' | 'destroy' = 'create',
592
- ) {
593
- this.logger?.log(
594
- `[ServiceLocator]#emitInstanceEvent() Notifying listeners for ${name} with event ${event}`,
595
- )
596
- return this.eventBus.emit(name, event)
597
- }
598
-
599
- /**
600
- * Creates a new instance for the given token and arguments.
601
- */
602
- private async createNewInstance<
603
- Instance,
604
- Schema extends InjectionTokenSchemaType | undefined,
605
- >(
606
- instanceName: string,
607
- realToken: InjectionToken<Instance, Schema>,
608
- args: Schema extends ZodObject
609
- ? z.output<Schema>
610
- : Schema extends ZodOptional<ZodObject>
611
- ? z.output<Schema> | undefined
612
- : undefined,
613
- ): Promise<
614
- | [undefined, ServiceLocatorInstanceHolder<Instance>]
615
- | [FactoryNotFound | UnknownError]
616
- > {
617
- this.logger?.log(
618
- `[ServiceLocator]#createNewInstance() Creating instance for ${instanceName}`,
619
- )
620
- if (this.registry.has(realToken)) {
621
- return this.instantiateServiceFromRegistry<Instance, Schema, any>(
622
- instanceName,
623
- realToken,
624
- args,
625
- )
626
- } else {
627
- return [new FactoryNotFound(realToken.name.toString())]
628
- }
629
- }
630
-
631
- /**
632
- * Instantiates a service from the registry using the service instantiator.
633
- */
634
- private instantiateServiceFromRegistry<
635
- Instance,
636
- Schema extends InjectionTokenSchemaType | undefined,
637
- Args extends Schema extends BaseInjectionTokenSchemaType
638
- ? z.output<Schema>
639
- : Schema extends OptionalInjectionTokenSchemaType
640
- ? z.output<Schema> | undefined
641
- : undefined,
642
- >(
643
- instanceName: string,
644
- token: InjectionToken<Instance, Schema>,
645
- args: Args,
646
- ): Promise<[undefined, ServiceLocatorInstanceHolder<Instance>]> {
647
- this.logger?.log(
648
- `[ServiceLocator]#instantiateServiceFromRegistry(): Creating instance for ${instanceName} from abstract factory`,
649
- )
650
- const ctx = this.createFactoryContext(
651
- this.currentRequestContext || undefined,
652
- )
653
- let record = this.registry.get<Instance, Schema>(token)
654
- let { scope, type } = record
655
-
656
- // Use createCreatingHolder from manager
657
- const [deferred, holder] = this.manager.createCreatingHolder<Instance>(
658
- instanceName,
659
- type,
660
- scope,
661
- ctx.deps,
662
- Infinity,
663
- )
664
-
665
- // Start the instantiation process
666
- this.serviceInstantiator
667
- .instantiateService(ctx, record, args)
668
- .then(async ([error, instance]) => {
669
- await this.handleInstantiationResult(
670
- instanceName,
671
- holder,
672
- ctx,
673
- deferred,
674
- scope,
675
- error,
676
- instance,
677
- )
678
- })
679
- .catch(async (error) => {
680
- await this.handleInstantiationError(
681
- instanceName,
682
- holder,
683
- deferred,
684
- scope,
685
- error,
686
- )
687
- })
688
-
689
- this.storeInstanceByScope(scope, instanceName, holder)
690
- // @ts-expect-error TS2322 This is correct type
691
- return [undefined, holder]
692
- }
693
-
694
- /**
695
- * Handles the result of service instantiation.
696
- */
697
- private async handleInstantiationResult(
698
- instanceName: string,
699
- holder: ServiceLocatorInstanceHolder<any>,
700
- ctx: any,
701
- deferred: any,
702
- scope: InjectableScope,
703
- error: any,
704
- instance: any,
705
- ): Promise<void> {
706
- holder.destroyListeners = ctx.getDestroyListeners()
707
- holder.creationPromise = null
708
-
709
- if (error) {
710
- await this.handleInstantiationError(
711
- instanceName,
712
- holder,
713
- deferred,
714
- scope,
715
- error,
716
- )
717
- } else {
718
- await this.handleInstantiationSuccess(
719
- instanceName,
720
- holder,
721
- ctx,
722
- deferred,
723
- instance,
724
- )
725
- }
726
- }
727
-
728
- /**
729
- * Handles successful service instantiation.
730
- */
731
- private async handleInstantiationSuccess(
732
- instanceName: string,
733
- holder: ServiceLocatorInstanceHolder<any>,
734
- ctx: any,
735
- deferred: any,
736
- instance: any,
737
- ): Promise<void> {
738
- holder.instance = instance
739
- holder.status = ServiceLocatorInstanceHolderStatus.Created
740
-
741
- // Set up dependency invalidation listeners
742
- if (ctx.deps.size > 0) {
743
- ctx.deps.forEach((dependency: string) => {
744
- holder.destroyListeners.push(
745
- this.eventBus.on(dependency, 'destroy', () =>
746
- this.invalidate(instanceName),
747
- ),
748
- )
749
- })
750
- }
751
-
752
- await this.emitInstanceEvent(instanceName)
753
- deferred.resolve([undefined, instance])
754
- }
755
-
756
- /**
757
- * Handles service instantiation errors.
758
- */
759
- private async handleInstantiationError(
760
- instanceName: string,
761
- holder: ServiceLocatorInstanceHolder<any>,
762
- deferred: any,
763
- scope: InjectableScope,
764
- error: any,
765
- ): Promise<void> {
766
- this.logger?.error(
767
- `[ServiceLocator] Error creating instance for ${instanceName}`,
768
- error,
769
- )
770
-
771
- holder.status = ServiceLocatorInstanceHolderStatus.Error
772
- holder.instance = error
773
- holder.creationPromise = null
774
-
775
- if (scope === InjectableScope.Singleton) {
776
- setTimeout(() => this.invalidate(instanceName), 10)
777
- }
778
-
779
- deferred.reject(error)
780
- }
781
-
782
- /**
783
- * Stores an instance holder based on its scope.
784
- */
785
- private storeInstanceByScope(
786
- scope: InjectableScope,
787
- instanceName: string,
788
- holder: ServiceLocatorInstanceHolder<any>,
789
- ): void {
790
- switch (scope) {
791
- case InjectableScope.Singleton:
792
- this.logger?.debug(
793
- `[ServiceLocator] Setting singleton instance for ${instanceName}`,
794
- )
795
- this.manager.set(instanceName, holder)
796
- break
797
-
798
- case InjectableScope.Request:
799
- if (this.currentRequestContext) {
800
- this.logger?.debug(
801
- `[ServiceLocator] Setting request-scoped instance for ${instanceName}`,
802
- )
803
- this.currentRequestContext.addInstance(
804
- instanceName,
805
- holder.instance,
806
- holder,
807
- )
808
- }
809
- break
810
-
811
- case InjectableScope.Transient:
812
- // Transient instances are not stored anywhere
813
- break
814
- }
815
- }
816
-
817
- /**
818
- * Tries to get a pre-prepared instance from request contexts.
819
- */
820
- private tryGetPrePreparedInstance(
226
+ tryGetPrePreparedInstance(
821
227
  instanceName: string,
822
228
  contextHolder: RequestContextHolder | undefined,
823
229
  deps: Set<string>,
824
230
  ): any {
825
- // Check provided context holder first (if has higher priority)
826
- if (contextHolder && contextHolder.priority > 0) {
827
- const prePreparedInstance = contextHolder.get(instanceName)?.instance
828
- if (prePreparedInstance !== undefined) {
829
- this.logger?.debug(
830
- `[ServiceLocator] Using pre-prepared instance ${instanceName} from request context ${contextHolder.requestId}`,
831
- )
832
- deps.add(instanceName)
833
- return prePreparedInstance
834
- }
835
- }
836
-
837
- // Check current request context (if different from provided contextHolder)
838
- if (
839
- this.currentRequestContext &&
840
- this.currentRequestContext !== contextHolder
841
- ) {
842
- const prePreparedInstance =
843
- this.currentRequestContext.get(instanceName)?.instance
844
- if (prePreparedInstance !== undefined) {
845
- this.logger?.debug(
846
- `[ServiceLocator] Using pre-prepared instance ${instanceName} from current request context ${this.currentRequestContext.requestId}`,
847
- )
848
- deps.add(instanceName)
849
- return prePreparedInstance
850
- }
851
- }
852
-
853
- return undefined
854
- }
855
-
856
- /**
857
- * Creates a factory context for dependency injection during service instantiation.
858
- * @param contextHolder Optional request context holder for priority-based resolution
859
- */
860
- private createFactoryContext(
861
- contextHolder?: RequestContextHolder,
862
- ): FactoryContext & {
863
- getDestroyListeners: () => (() => void)[]
864
- deps: Set<string>
865
- } {
866
- const destroyListeners = new Set<() => void>()
867
- const deps = new Set<string>()
868
- // eslint-disable-next-line @typescript-eslint/no-this-alias
869
- const self = this
870
-
871
- function addDestroyListener(listener: () => void) {
872
- destroyListeners.add(listener)
873
- }
874
-
875
- function getDestroyListeners() {
876
- return Array.from(destroyListeners)
877
- }
878
-
879
- return {
880
- // @ts-expect-error This is correct type
881
- async inject(token, args) {
882
- const instanceName = self.generateInstanceName(token, args)
883
-
884
- // Check request contexts for pre-prepared instances
885
- const prePreparedInstance = self.tryGetPrePreparedInstance(
886
- instanceName,
887
- contextHolder,
888
- deps,
889
- )
890
- if (prePreparedInstance !== undefined) {
891
- return prePreparedInstance
892
- }
893
-
894
- // Fall back to normal resolution
895
- const [error, instance] = await self.getInstance(
896
- token,
897
- args,
898
- ({ instanceName }) => {
899
- deps.add(instanceName)
900
- },
901
- )
902
- if (error) {
903
- throw error
904
- }
905
- return instance
906
- },
907
- addDestroyListener,
908
- getDestroyListeners,
909
- locator: self,
231
+ return this.tokenProcessor.tryGetPrePreparedInstance(
232
+ instanceName,
233
+ contextHolder,
910
234
  deps,
911
- }
912
- }
913
-
914
- /**
915
- * Generates a unique instance name based on token and arguments.
916
- */
917
- private generateInstanceName(token: InjectionTokenType, args: any) {
918
- if (!args) {
919
- return token.toString()
920
- }
921
-
922
- const formattedArgs = Object.entries(args)
923
- .sort(([keyA], [keyB]) => keyA.localeCompare(keyB))
924
- .map(([key, value]) => `${key}=${this.formatArgValue(value)}`)
925
- .join(',')
926
-
927
- return `${token.toString()}:${formattedArgs.replaceAll(/"/g, '').replaceAll(/:/g, '=')}`
235
+ this.requestContextManager.getCurrentRequestContext(),
236
+ )
928
237
  }
929
238
 
930
239
  /**
931
- * Formats a single argument value for instance name generation.
240
+ * Helper method for InstanceResolver to generate instance names.
241
+ * This is needed for the factory context creation.
932
242
  */
933
- private formatArgValue(value: any): string {
934
- if (typeof value === 'function') {
935
- return `fn_${value.name}(${value.length})`
936
- }
937
- if (typeof value === 'symbol') {
938
- return value.toString()
939
- }
940
- return JSON.stringify(value).slice(0, 40)
243
+ generateInstanceName(token: any, args: any): string {
244
+ return this.tokenProcessor.generateInstanceName(token, args)
941
245
  }
942
246
  }