@navios/di 0.3.1 → 0.4.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 (97) hide show
  1. package/README.md +67 -6
  2. package/coverage/base.css +224 -0
  3. package/coverage/block-navigation.js +87 -0
  4. package/coverage/clover.xml +2659 -0
  5. package/coverage/coverage-final.json +46 -0
  6. package/coverage/docs/examples/basic-usage.mts.html +376 -0
  7. package/coverage/docs/examples/factory-pattern.mts.html +1039 -0
  8. package/coverage/docs/examples/index.html +176 -0
  9. package/coverage/docs/examples/injection-tokens.mts.html +760 -0
  10. package/coverage/docs/examples/request-scope-example.mts.html +847 -0
  11. package/coverage/docs/examples/service-lifecycle.mts.html +1162 -0
  12. package/coverage/favicon.png +0 -0
  13. package/coverage/index.html +236 -0
  14. package/coverage/lib/_tsup-dts-rollup.d.mts.html +2806 -0
  15. package/coverage/lib/index.d.mts.html +310 -0
  16. package/coverage/lib/index.html +131 -0
  17. package/coverage/prettify.css +1 -0
  18. package/coverage/prettify.js +2 -0
  19. package/coverage/sort-arrow-sprite.png +0 -0
  20. package/coverage/sorter.js +196 -0
  21. package/coverage/src/container.mts.html +586 -0
  22. package/coverage/src/decorators/factory.decorator.mts.html +322 -0
  23. package/coverage/src/decorators/index.html +146 -0
  24. package/coverage/src/decorators/index.mts.html +91 -0
  25. package/coverage/src/decorators/injectable.decorator.mts.html +394 -0
  26. package/coverage/src/enums/index.html +146 -0
  27. package/coverage/src/enums/index.mts.html +91 -0
  28. package/coverage/src/enums/injectable-scope.enum.mts.html +127 -0
  29. package/coverage/src/enums/injectable-type.enum.mts.html +97 -0
  30. package/coverage/src/errors/errors.enum.mts.html +109 -0
  31. package/coverage/src/errors/factory-not-found.mts.html +109 -0
  32. package/coverage/src/errors/factory-token-not-resolved.mts.html +115 -0
  33. package/coverage/src/errors/index.html +221 -0
  34. package/coverage/src/errors/index.mts.html +106 -0
  35. package/coverage/src/errors/instance-destroying.mts.html +109 -0
  36. package/coverage/src/errors/instance-expired.mts.html +109 -0
  37. package/coverage/src/errors/instance-not-found.mts.html +109 -0
  38. package/coverage/src/errors/unknown-error.mts.html +130 -0
  39. package/coverage/src/event-emitter.mts.html +400 -0
  40. package/coverage/src/factory-context.mts.html +109 -0
  41. package/coverage/src/index.html +296 -0
  42. package/coverage/src/index.mts.html +139 -0
  43. package/coverage/src/injection-token.mts.html +571 -0
  44. package/coverage/src/injector.mts.html +133 -0
  45. package/coverage/src/interfaces/factory.interface.mts.html +121 -0
  46. package/coverage/src/interfaces/index.html +161 -0
  47. package/coverage/src/interfaces/index.mts.html +94 -0
  48. package/coverage/src/interfaces/on-service-destroy.interface.mts.html +94 -0
  49. package/coverage/src/interfaces/on-service-init.interface.mts.html +94 -0
  50. package/coverage/src/registry.mts.html +247 -0
  51. package/coverage/src/request-context-holder.mts.html +607 -0
  52. package/coverage/src/service-instantiator.mts.html +559 -0
  53. package/coverage/src/service-locator-event-bus.mts.html +289 -0
  54. package/coverage/src/service-locator-instance-holder.mts.html +307 -0
  55. package/coverage/src/service-locator-manager.mts.html +604 -0
  56. package/coverage/src/service-locator.mts.html +2911 -0
  57. package/coverage/src/symbols/index.html +131 -0
  58. package/coverage/src/symbols/index.mts.html +88 -0
  59. package/coverage/src/symbols/injectable-token.mts.html +88 -0
  60. package/coverage/src/utils/defer.mts.html +304 -0
  61. package/coverage/src/utils/get-injectable-token.mts.html +142 -0
  62. package/coverage/src/utils/get-injectors.mts.html +691 -0
  63. package/coverage/src/utils/index.html +176 -0
  64. package/coverage/src/utils/index.mts.html +97 -0
  65. package/coverage/src/utils/types.mts.html +241 -0
  66. package/docs/README.md +5 -2
  67. package/docs/api-reference.md +38 -0
  68. package/docs/container.md +75 -0
  69. package/docs/getting-started.md +4 -3
  70. package/docs/injectable.md +4 -3
  71. package/docs/migration.md +177 -0
  72. package/docs/request-contexts.md +364 -0
  73. package/lib/_tsup-dts-rollup.d.mts +182 -41
  74. package/lib/_tsup-dts-rollup.d.ts +182 -41
  75. package/lib/index.d.mts +1 -0
  76. package/lib/index.d.ts +1 -0
  77. package/lib/index.js +480 -294
  78. package/lib/index.js.map +1 -1
  79. package/lib/index.mjs +480 -295
  80. package/lib/index.mjs.map +1 -1
  81. package/package.json +1 -1
  82. package/src/__tests__/defer.spec.mts +166 -0
  83. package/src/__tests__/errors.spec.mts +61 -0
  84. package/src/__tests__/event-emitter.spec.mts +163 -0
  85. package/src/__tests__/get-injectors.spec.mts +70 -0
  86. package/src/__tests__/registry.spec.mts +335 -0
  87. package/src/__tests__/request-scope.spec.mts +34 -35
  88. package/src/__tests__/service-instantiator.spec.mts +408 -0
  89. package/src/__tests__/service-locator-event-bus.spec.mts +242 -0
  90. package/src/__tests__/service-locator-manager.spec.mts +370 -0
  91. package/src/__tests__/unified-api.spec.mts +130 -0
  92. package/src/base-instance-holder-manager.mts +175 -0
  93. package/src/event-emitter.mts +5 -5
  94. package/src/index.mts +1 -0
  95. package/src/request-context-holder.mts +73 -44
  96. package/src/service-locator-manager.mts +12 -70
  97. package/src/service-locator.mts +421 -226
@@ -90,10 +90,10 @@ export class ServiceLocator {
90
90
  if (err) {
91
91
  return [err]
92
92
  }
93
+
93
94
  const { instanceName, validatedArgs, actualToken, realToken } = data
94
- if (onPrepare) {
95
- onPrepare({ instanceName, actualToken, validatedArgs })
96
- }
95
+ onPrepare?.({ instanceName, actualToken, validatedArgs })
96
+
97
97
  const [error, holder] = await this.retrieveOrCreateInstanceByInstanceName(
98
98
  instanceName,
99
99
  realToken,
@@ -133,12 +133,16 @@ export class ServiceLocator {
133
133
  return null
134
134
  }
135
135
  const instanceName = this.generateInstanceName(actualToken, validatedArgs)
136
+
137
+ // Try request context first
136
138
  if (this.currentRequestContext) {
137
- const requestHolder = this.currentRequestContext.getHolder(instanceName)
139
+ const requestHolder = this.currentRequestContext.get(instanceName)
138
140
  if (requestHolder) {
139
141
  return requestHolder.instance as Instance
140
142
  }
141
143
  }
144
+
145
+ // Try singleton manager
142
146
  const [error, holder] = this.manager.get(instanceName)
143
147
  if (error) {
144
148
  return null
@@ -148,66 +152,101 @@ export class ServiceLocator {
148
152
 
149
153
  invalidate(service: string, round = 1): Promise<any> {
150
154
  this.logger?.log(
151
- `[ServiceLocator]#invalidate(): Starting Invalidating process of ${service}`,
155
+ `[ServiceLocator] Starting invalidation process for ${service}`,
152
156
  )
153
157
  const toInvalidate = this.manager.filter(
154
158
  (holder) => holder.name === service || holder.deps.has(service),
155
159
  )
156
160
  const promises = []
157
161
  for (const [key, holder] of toInvalidate.entries()) {
158
- if (holder.status === ServiceLocatorInstanceHolderStatus.Destroying) {
159
- this.logger?.trace(
160
- `[ServiceLocator]#invalidate(): ${key} is already being destroyed`,
161
- )
162
- promises.push(holder.destroyPromise)
163
- continue
164
- }
165
- if (holder.status === ServiceLocatorInstanceHolderStatus.Creating) {
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:
166
182
  this.logger?.trace(
167
- `[ServiceLocator]#invalidate(): ${key} is being created, waiting for creation to finish`,
168
- )
169
- promises.push(
170
- holder.creationPromise?.then(() => {
171
- if (round > 3) {
172
- this.logger?.error(
173
- `[ServiceLocator]#invalidate(): ${key} creation is triggering a new invalidation round, but it is still not created`,
174
- )
175
- return
176
- }
177
- return this.invalidate(key, round + 1)
178
- }),
183
+ `[ServiceLocator] ${key} is being created, waiting...`,
179
184
  )
180
- continue
181
- }
182
- // @ts-expect-error TS2322 we are changing the status
183
- holder.status = ServiceLocatorInstanceHolderStatus.Destroying
184
- this.logger?.log(
185
- `[ServiceLocator]#invalidate(): Invalidating ${key} and notifying listeners`,
186
- )
187
- // @ts-expect-error TS2322 we are changing the status
188
- holder.destroyPromise = Promise.all(
189
- holder.destroyListeners.map((listener) => listener()),
190
- ).then(async () => {
191
- this.manager.delete(key)
192
- await this.emitInstanceEvent(key, 'destroy')
193
- })
194
- promises.push(holder.destroyPromise)
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
195
198
  }
196
- return Promise.all(promises)
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`,
211
+ )
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
197
221
  }
198
222
 
199
223
  async ready() {
200
- return Promise.all(
201
- Array.from(this.manager.filter(() => true)).map(([, holder]) => {
202
- if (holder.status === ServiceLocatorInstanceHolderStatus.Creating) {
203
- return holder.creationPromise?.then(() => null)
204
- }
205
- if (holder.status === ServiceLocatorInstanceHolderStatus.Destroying) {
206
- return holder.destroyPromise.then(() => null)
207
- }
208
- return Promise.resolve(null)
209
- }),
210
- ).then(() => null)
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
+ )
230
+ }
231
+
232
+ /**
233
+ * Waits for a holder to settle (either created, destroyed, or error state).
234
+ */
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
+ }
211
250
  }
212
251
 
213
252
  // ============================================================================
@@ -400,99 +439,148 @@ export class ServiceLocator {
400
439
  | [undefined, ServiceLocatorInstanceHolder<any>]
401
440
  | [UnknownError | FactoryNotFound]
402
441
  > {
403
- // Check if this is a request-scoped service and we have a current request context
404
- if (this.registry.has(realToken)) {
405
- const record = this.registry.get(realToken)
406
- if (record.scope === InjectableScope.Request) {
407
- if (!this.currentRequestContext) {
408
- this.logger?.log(
409
- `[ServiceLocator]#retrieveOrCreateInstanceByInstanceName() No current request context available for request-scoped service ${instanceName}`,
410
- )
411
- return [new UnknownError(ErrorsEnum.InstanceNotFound)]
412
- }
413
- const requestHolder = this.currentRequestContext.getHolder(instanceName)
414
- if (requestHolder) {
415
- if (
416
- requestHolder.status === ServiceLocatorInstanceHolderStatus.Creating
417
- ) {
418
- await requestHolder.creationPromise
419
- return this.retrieveOrCreateInstanceByInstanceName(
420
- instanceName,
421
- realToken,
422
- realArgs,
423
- )
424
- } else if (
425
- requestHolder.status ===
426
- ServiceLocatorInstanceHolderStatus.Destroying
427
- ) {
428
- return [new UnknownError(ErrorsEnum.InstanceDestroying)]
429
- }
430
- return [undefined, requestHolder]
431
- }
432
- }
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
433
449
  }
434
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
+ > {
435
529
  const [error, holder] = this.manager.get(instanceName)
530
+
436
531
  if (!error) {
437
- if (holder.status === ServiceLocatorInstanceHolderStatus.Creating) {
438
- await holder.creationPromise
439
- return this.retrieveOrCreateInstanceByInstanceName(
440
- instanceName,
441
- realToken,
442
- realArgs,
443
- )
444
- } else if (
445
- holder.status === ServiceLocatorInstanceHolderStatus.Destroying
446
- ) {
447
- // Should never happen
448
- return [new UnknownError(ErrorsEnum.InstanceDestroying)]
449
- }
450
- return [undefined, holder]
532
+ return this.waitForInstanceReady(holder)
451
533
  }
534
+
535
+ // Handle recovery scenarios
452
536
  switch (error.code) {
453
537
  case ErrorsEnum.InstanceDestroying:
454
538
  this.logger?.log(
455
- `[ServiceLocator]#retrieveOrCreateInstanceByInstanceName() TTL expired for ${holder?.name}`,
539
+ `[ServiceLocator] Instance ${instanceName} is being destroyed, waiting...`,
456
540
  )
457
541
  await holder?.destroyPromise
458
- //Maybe we already have a new instance
459
- return this.retrieveOrCreateInstanceByInstanceName(
460
- instanceName,
461
- realToken,
462
- realArgs,
463
- )
542
+ // Retry after destruction is complete
543
+ return this.tryGetSingletonInstance(instanceName)
464
544
 
465
545
  case ErrorsEnum.InstanceExpired:
466
546
  this.logger?.log(
467
- `[ServiceLocator]#retrieveOrCreateInstanceByInstanceName() TTL expired for ${holder?.name}`,
547
+ `[ServiceLocator] Instance ${instanceName} expired, invalidating...`,
468
548
  )
469
549
  await this.invalidate(instanceName)
470
- //Maybe we already have a new instance
471
- return this.retrieveOrCreateInstanceByInstanceName(
472
- instanceName,
473
- realToken,
474
- realArgs,
475
- )
550
+ // Retry after invalidation
551
+ return this.tryGetSingletonInstance(instanceName)
552
+
476
553
  case ErrorsEnum.InstanceNotFound:
477
- break
554
+ return null // Instance doesn't exist, should create new one
555
+
478
556
  default:
479
557
  return [error]
480
558
  }
481
- const result = await this.createNewInstance(
482
- instanceName,
483
- realToken,
484
- realArgs,
485
- )
486
- if (result[0]) {
487
- return [result[0]]
488
- }
489
- if (result[1].status === ServiceLocatorInstanceHolderStatus.Creating) {
490
- await result[1].creationPromise
491
- }
492
- if (result[1].status === ServiceLocatorInstanceHolderStatus.Error) {
493
- return [result[1].instance] as [UnknownError | FactoryNotFound]
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)]
494
583
  }
495
- return [undefined, result[1]]
496
584
  }
497
585
 
498
586
  /**
@@ -578,71 +666,191 @@ export class ServiceLocator {
578
666
  this.serviceInstantiator
579
667
  .instantiateService(ctx, record, args)
580
668
  .then(async ([error, instance]) => {
581
- holder.destroyListeners = ctx.getDestroyListeners()
582
- holder.creationPromise = null
583
- if (error) {
584
- this.logger?.error(
585
- `[ServiceLocator]#instantiateServiceFromRegistry(): Error creating instance for ${instanceName}`,
586
- error,
587
- )
588
- holder.status = ServiceLocatorInstanceHolderStatus.Error
589
- holder.instance = error
590
- if (scope === InjectableScope.Singleton) {
591
- setTimeout(() => this.invalidate(instanceName), 10)
592
- }
593
- deferred.reject(error)
594
- } else {
595
- holder.instance = instance
596
- holder.status = ServiceLocatorInstanceHolderStatus.Created
597
- if (ctx.deps.size > 0) {
598
- ctx.deps.forEach((dependency) => {
599
- holder.destroyListeners.push(
600
- this.eventBus.on(dependency, 'destroy', () =>
601
- this.invalidate(instanceName),
602
- ),
603
- )
604
- })
605
- }
606
- await this.emitInstanceEvent(instanceName)
607
- deferred.resolve([undefined, instance])
608
- }
669
+ await this.handleInstantiationResult(
670
+ instanceName,
671
+ holder,
672
+ ctx,
673
+ deferred,
674
+ scope,
675
+ error,
676
+ instance,
677
+ )
609
678
  })
610
- .catch((error) => {
611
- this.logger?.error(
612
- `[ServiceLocator]#instantiateServiceFromRegistry(): Unexpected error creating instance for ${instanceName}`,
679
+ .catch(async (error) => {
680
+ await this.handleInstantiationError(
681
+ instanceName,
682
+ holder,
683
+ deferred,
684
+ scope,
613
685
  error,
614
686
  )
615
- holder.status = ServiceLocatorInstanceHolderStatus.Error
616
- holder.instance = error
617
- holder.creationPromise = null
618
- if (scope === InjectableScope.Singleton) {
619
- setTimeout(() => this.invalidate(instanceName), 10)
620
- }
621
- deferred.reject(error)
622
687
  })
623
688
 
624
- if (scope === InjectableScope.Singleton) {
625
- this.logger?.debug(
626
- `[ServiceLocator]#instantiateServiceFromRegistry(): Setting instance for ${instanceName}`,
627
- )
628
- this.manager.set(instanceName, holder)
629
- } else if (
630
- scope === InjectableScope.Request &&
631
- this.currentRequestContext
632
- ) {
633
- this.logger?.debug(
634
- `[ServiceLocator]#instantiateServiceFromRegistry(): Setting request-scoped instance for ${instanceName}`,
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,
635
716
  )
636
- // For request-scoped services, we don't store them in the global manager
637
- // They will be managed by the request context holder
638
- this.currentRequestContext.addInstance(
717
+ } else {
718
+ await this.handleInstantiationSuccess(
639
719
  instanceName,
640
- holder.instance,
641
720
  holder,
721
+ ctx,
722
+ deferred,
723
+ instance,
642
724
  )
643
725
  }
644
- // @ts-expect-error TS2322 This is correct type
645
- return [undefined, holder]
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(
821
+ instanceName: string,
822
+ contextHolder: RequestContextHolder | undefined,
823
+ deps: Set<string>,
824
+ ): 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
646
854
  }
647
855
 
648
856
  /**
@@ -671,37 +879,19 @@ export class ServiceLocator {
671
879
  return {
672
880
  // @ts-expect-error This is correct type
673
881
  async inject(token, args) {
674
- // 1. Check RequestContextHolder first (if provided and has higher priority)
675
- if (contextHolder && contextHolder.priority > 0) {
676
- const instanceName = self.generateInstanceName(token, args)
677
- const prePreparedInstance = contextHolder.getInstance(instanceName)
678
- if (prePreparedInstance !== undefined) {
679
- self.logger?.debug(
680
- `[ServiceLocator] Using pre-prepared instance ${instanceName} from request context ${contextHolder.requestId}`,
681
- )
682
- deps.add(instanceName)
683
- return prePreparedInstance
684
- }
685
- }
882
+ const instanceName = self.generateInstanceName(token, args)
686
883
 
687
- // 2. Check current request context (if different from provided contextHolder)
688
- if (
689
- self.currentRequestContext &&
690
- self.currentRequestContext !== contextHolder
691
- ) {
692
- const instanceName = self.generateInstanceName(token, args)
693
- const prePreparedInstance =
694
- self.currentRequestContext.getInstance(instanceName)
695
- if (prePreparedInstance !== undefined) {
696
- self.logger?.debug(
697
- `[ServiceLocator] Using pre-prepared instance ${instanceName} from current request context ${self.currentRequestContext.requestId}`,
698
- )
699
- deps.add(instanceName)
700
- return prePreparedInstance
701
- }
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
702
892
  }
703
893
 
704
- // 3. Fall back to normal resolution
894
+ // Fall back to normal resolution
705
895
  const [error, instance] = await self.getInstance(
706
896
  token,
707
897
  args,
@@ -725,23 +915,28 @@ export class ServiceLocator {
725
915
  * Generates a unique instance name based on token and arguments.
726
916
  */
727
917
  private generateInstanceName(token: InjectionTokenType, args: any) {
728
- const formattedArgs = args
729
- ? ':' +
730
- Object.entries(args ?? {})
731
- .sort(([keyA], [keyB]) => keyA.localeCompare(keyB))
732
- .map(([key, value]) => {
733
- if (typeof value === 'function') {
734
- return `${key}=fn_${value.name}(${value.length})`
735
- }
736
- if (typeof value === 'symbol') {
737
- return `${key}=${value.toString()}`
738
- }
739
- return `${key}=${JSON.stringify(value).slice(0, 40)}`
740
- })
741
- .join(',')
742
- .replaceAll(/"/g, '')
743
- .replaceAll(/:/g, '=')
744
- : ''
745
- return `${token.toString()}${formattedArgs}`
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, '=')}`
928
+ }
929
+
930
+ /**
931
+ * Formats a single argument value for instance name generation.
932
+ */
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)
746
941
  }
747
942
  }