@navios/di 0.5.0 → 0.6.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 (123) hide show
  1. package/CHANGELOG.md +146 -0
  2. package/README.md +196 -219
  3. package/docs/README.md +69 -11
  4. package/docs/api-reference.md +281 -117
  5. package/docs/container.md +220 -56
  6. package/docs/examples/request-scope-example.mts +2 -2
  7. package/docs/factory.md +3 -8
  8. package/docs/getting-started.md +37 -8
  9. package/docs/migration.md +318 -37
  10. package/docs/request-contexts.md +263 -175
  11. package/docs/scopes.md +79 -42
  12. package/lib/browser/index.d.mts +1577 -0
  13. package/lib/browser/index.d.mts.map +1 -0
  14. package/lib/browser/index.mjs +3012 -0
  15. package/lib/browser/index.mjs.map +1 -0
  16. package/lib/index-S_qX2VLI.d.mts +1211 -0
  17. package/lib/index-S_qX2VLI.d.mts.map +1 -0
  18. package/lib/index-fKPuT65j.d.cts +1206 -0
  19. package/lib/index-fKPuT65j.d.cts.map +1 -0
  20. package/lib/index.cjs +389 -0
  21. package/lib/index.cjs.map +1 -0
  22. package/lib/index.d.cts +376 -0
  23. package/lib/index.d.cts.map +1 -0
  24. package/lib/index.d.mts +371 -78
  25. package/lib/index.d.mts.map +1 -0
  26. package/lib/index.mjs +325 -63
  27. package/lib/index.mjs.map +1 -1
  28. package/lib/testing/index.cjs +9 -0
  29. package/lib/testing/index.d.cts +2 -0
  30. package/lib/testing/index.d.mts +2 -2
  31. package/lib/testing/index.mjs +2 -72
  32. package/lib/testing-BMGmmxH7.cjs +2895 -0
  33. package/lib/testing-BMGmmxH7.cjs.map +1 -0
  34. package/lib/testing-DCXz8AJD.mjs +2655 -0
  35. package/lib/testing-DCXz8AJD.mjs.map +1 -0
  36. package/package.json +26 -4
  37. package/project.json +2 -2
  38. package/src/__tests__/async-local-storage.browser.spec.mts +240 -0
  39. package/src/__tests__/async-local-storage.spec.mts +333 -0
  40. package/src/__tests__/container.spec.mts +30 -25
  41. package/src/__tests__/e2e.browser.spec.mts +790 -0
  42. package/src/__tests__/e2e.spec.mts +1222 -0
  43. package/src/__tests__/errors.spec.mts +6 -6
  44. package/src/__tests__/factory.spec.mts +1 -1
  45. package/src/__tests__/get-injectors.spec.mts +1 -1
  46. package/src/__tests__/injectable.spec.mts +1 -1
  47. package/src/__tests__/injection-token.spec.mts +1 -1
  48. package/src/__tests__/library-findings.spec.mts +563 -0
  49. package/src/__tests__/registry.spec.mts +2 -2
  50. package/src/__tests__/request-scope.spec.mts +266 -274
  51. package/src/__tests__/service-instantiator.spec.mts +19 -17
  52. package/src/__tests__/service-locator-event-bus.spec.mts +9 -9
  53. package/src/__tests__/service-locator-manager.spec.mts +15 -15
  54. package/src/__tests__/service-locator.spec.mts +167 -244
  55. package/src/__tests__/unified-api.spec.mts +27 -27
  56. package/src/__type-tests__/factory.spec-d.mts +2 -2
  57. package/src/__type-tests__/inject.spec-d.mts +2 -2
  58. package/src/__type-tests__/injectable.spec-d.mts +1 -1
  59. package/src/browser.mts +16 -0
  60. package/src/container/container.mts +319 -0
  61. package/src/container/index.mts +2 -0
  62. package/src/container/scoped-container.mts +350 -0
  63. package/src/decorators/factory.decorator.mts +4 -4
  64. package/src/decorators/injectable.decorator.mts +5 -5
  65. package/src/errors/di-error.mts +13 -7
  66. package/src/errors/index.mts +0 -8
  67. package/src/index.mts +156 -15
  68. package/src/interfaces/container.interface.mts +82 -0
  69. package/src/interfaces/factory.interface.mts +2 -2
  70. package/src/interfaces/index.mts +1 -0
  71. package/src/internal/context/async-local-storage.mts +120 -0
  72. package/src/internal/context/factory-context.mts +18 -0
  73. package/src/internal/context/index.mts +3 -0
  74. package/src/{request-context-holder.mts → internal/context/request-context.mts} +40 -27
  75. package/src/internal/context/resolution-context.mts +63 -0
  76. package/src/internal/context/sync-local-storage.mts +51 -0
  77. package/src/internal/core/index.mts +5 -0
  78. package/src/internal/core/instance-resolver.mts +641 -0
  79. package/src/{service-instantiator.mts → internal/core/instantiator.mts} +31 -27
  80. package/src/internal/core/invalidator.mts +437 -0
  81. package/src/internal/core/service-locator.mts +202 -0
  82. package/src/{token-processor.mts → internal/core/token-processor.mts} +79 -60
  83. package/src/{base-instance-holder-manager.mts → internal/holder/base-holder-manager.mts} +91 -21
  84. package/src/internal/holder/holder-manager.mts +85 -0
  85. package/src/internal/holder/holder-storage.interface.mts +116 -0
  86. package/src/internal/holder/index.mts +6 -0
  87. package/src/internal/holder/instance-holder.mts +109 -0
  88. package/src/internal/holder/request-storage.mts +134 -0
  89. package/src/internal/holder/singleton-storage.mts +105 -0
  90. package/src/internal/index.mts +4 -0
  91. package/src/internal/lifecycle/circular-detector.mts +77 -0
  92. package/src/internal/lifecycle/index.mts +2 -0
  93. package/src/{service-locator-event-bus.mts → internal/lifecycle/lifecycle-event-bus.mts} +12 -5
  94. package/src/testing/__tests__/test-container.spec.mts +2 -2
  95. package/src/testing/test-container.mts +4 -4
  96. package/src/token/index.mts +2 -0
  97. package/src/{injection-token.mts → token/injection-token.mts} +1 -1
  98. package/src/{registry.mts → token/registry.mts} +1 -1
  99. package/src/utils/get-injectable-token.mts +1 -1
  100. package/src/utils/get-injectors.mts +32 -15
  101. package/src/utils/types.mts +1 -1
  102. package/tsdown.config.mts +67 -0
  103. package/lib/_tsup-dts-rollup.d.mts +0 -1283
  104. package/lib/_tsup-dts-rollup.d.ts +0 -1283
  105. package/lib/chunk-44F3LXW5.mjs +0 -2043
  106. package/lib/chunk-44F3LXW5.mjs.map +0 -1
  107. package/lib/index.d.ts +0 -78
  108. package/lib/index.js +0 -2127
  109. package/lib/index.js.map +0 -1
  110. package/lib/testing/index.d.ts +0 -2
  111. package/lib/testing/index.js +0 -2060
  112. package/lib/testing/index.js.map +0 -1
  113. package/lib/testing/index.mjs.map +0 -1
  114. package/src/container.mts +0 -227
  115. package/src/factory-context.mts +0 -8
  116. package/src/instance-resolver.mts +0 -559
  117. package/src/request-context-manager.mts +0 -149
  118. package/src/service-invalidator.mts +0 -429
  119. package/src/service-locator-instance-holder.mts +0 -70
  120. package/src/service-locator-manager.mts +0 -85
  121. package/src/service-locator.mts +0 -246
  122. package/tsup.config.mts +0 -12
  123. /package/src/{injector.mts → injectors.mts} +0 -0
@@ -0,0 +1,641 @@
1
+ /* eslint-disable @typescript-eslint/no-explicit-any */
2
+ /* eslint-disable @typescript-eslint/no-empty-object-type */
3
+ import type { z, ZodObject, ZodOptional } from 'zod/v4'
4
+
5
+ import type { ScopedContainer } from '../../container/scoped-container.mjs'
6
+ import type { IContainer } from '../../interfaces/container.interface.mjs'
7
+ import type {
8
+ AnyInjectableType,
9
+ InjectionTokenSchemaType,
10
+ InjectionTokenType,
11
+ } from '../../token/injection-token.mjs'
12
+ import type { Registry } from '../../token/registry.mjs'
13
+ import type { FactoryContext } from '../context/factory-context.mjs'
14
+ import type { HolderManager } from '../holder/holder-manager.mjs'
15
+ import type { IHolderStorage } from '../holder/holder-storage.interface.mjs'
16
+ import type { InstanceHolder } from '../holder/instance-holder.mjs'
17
+ import type { Instantiator } from './instantiator.mjs'
18
+ import type { ServiceLocator } from './service-locator.mjs'
19
+ import type { TokenProcessor } from './token-processor.mjs'
20
+
21
+ import { InjectableScope } from '../../enums/index.mjs'
22
+ import { DIError, DIErrorCode } from '../../errors/index.mjs'
23
+ import {
24
+ BoundInjectionToken,
25
+ FactoryInjectionToken,
26
+ InjectionToken,
27
+ } from '../../token/injection-token.mjs'
28
+ import {
29
+ getCurrentResolutionContext,
30
+ withResolutionContext,
31
+ } from '../context/resolution-context.mjs'
32
+ import { BaseHolderManager } from '../holder/base-holder-manager.mjs'
33
+ import { InstanceStatus } from '../holder/instance-holder.mjs'
34
+ import { SingletonStorage } from '../holder/singleton-storage.mjs'
35
+
36
+ /**
37
+ * Resolves instances from tokens, handling caching, creation, and scope rules.
38
+ *
39
+ * Uses the Storage Strategy pattern for unified singleton/request-scoped handling.
40
+ * Coordinates with Instantiator for actual service creation.
41
+ */
42
+ export class InstanceResolver {
43
+ private readonly singletonStorage: IHolderStorage
44
+
45
+ constructor(
46
+ private readonly registry: Registry,
47
+ private readonly manager: HolderManager,
48
+ private readonly instantiator: Instantiator,
49
+ private readonly tokenProcessor: TokenProcessor,
50
+ private readonly logger: Console | null = null,
51
+ private readonly serviceLocator: ServiceLocator,
52
+ ) {
53
+ this.singletonStorage = new SingletonStorage(manager)
54
+ }
55
+
56
+ // ============================================================================
57
+ // PUBLIC RESOLUTION METHODS
58
+ // ============================================================================
59
+
60
+ /**
61
+ * Resolves an instance for the given token and arguments.
62
+ * This method is used for singleton and transient services.
63
+ *
64
+ * @param token The injection token
65
+ * @param args Optional arguments
66
+ * @param contextContainer The container to use for creating FactoryContext
67
+ */
68
+ async resolveInstance(
69
+ token: AnyInjectableType,
70
+ args: any,
71
+ contextContainer: IContainer,
72
+ ): Promise<[undefined, any] | [DIError]> {
73
+ return this.resolveWithStorage(
74
+ token,
75
+ args,
76
+ contextContainer,
77
+ this.singletonStorage,
78
+ )
79
+ }
80
+
81
+ /**
82
+ * Resolves a request-scoped instance for a ScopedContainer.
83
+ * The service will be stored in the ScopedContainer's request context.
84
+ *
85
+ * @param token The injection token
86
+ * @param args Optional arguments
87
+ * @param scopedContainer The ScopedContainer that owns the request context
88
+ */
89
+ async resolveRequestScopedInstance(
90
+ token: AnyInjectableType,
91
+ args: any,
92
+ scopedContainer: ScopedContainer,
93
+ ): Promise<[undefined, any] | [DIError]> {
94
+ // Use the cached storage from ScopedContainer
95
+ return this.resolveWithStorage(
96
+ token,
97
+ args,
98
+ scopedContainer,
99
+ scopedContainer.getHolderStorage(),
100
+ scopedContainer,
101
+ )
102
+ }
103
+
104
+ // ============================================================================
105
+ // UNIFIED RESOLUTION (Storage Strategy Pattern)
106
+ // ============================================================================
107
+
108
+ /**
109
+ * Unified resolution method that works with any IHolderStorage.
110
+ * This eliminates duplication between singleton and request-scoped resolution.
111
+ *
112
+ * IMPORTANT: The check-and-store logic is carefully designed to avoid race conditions.
113
+ * The storage check and holder creation must happen synchronously (no awaits between).
114
+ *
115
+ * @param token The injection token
116
+ * @param args Optional arguments
117
+ * @param contextContainer The container for FactoryContext
118
+ * @param storage The storage strategy to use
119
+ * @param scopedContainer Optional scoped container for request-scoped services
120
+ */
121
+ private async resolveWithStorage(
122
+ token: AnyInjectableType,
123
+ args: any,
124
+ contextContainer: IContainer,
125
+ storage: IHolderStorage,
126
+ scopedContainer?: ScopedContainer,
127
+ ): Promise<[undefined, any] | [DIError]> {
128
+ // Step 1: Resolve token and prepare instance name
129
+ const [err, data] = await this.resolveTokenAndPrepareInstanceName(
130
+ token,
131
+ args,
132
+ contextContainer,
133
+ )
134
+ if (err) {
135
+ return [err]
136
+ }
137
+
138
+ const { instanceName, validatedArgs, realToken } = data!
139
+
140
+ // Step 2: Check for existing holder SYNCHRONOUSLY (no await between check and store)
141
+ // This is critical for preventing race conditions with concurrent resolution
142
+ const getResult = storage.get(instanceName)
143
+
144
+ if (getResult !== null) {
145
+ const [error, holder] = getResult
146
+ if (!error && holder) {
147
+ // Found existing holder - wait for it to be ready
148
+ const readyResult = await this.waitForInstanceReady(holder)
149
+ if (readyResult[0]) {
150
+ return [readyResult[0]]
151
+ }
152
+ return [undefined, readyResult[1]!.instance]
153
+ }
154
+ // Handle error states (destroying, etc.)
155
+ if (error) {
156
+ const handledResult = await this.handleStorageError(
157
+ instanceName,
158
+ error,
159
+ holder,
160
+ storage,
161
+ )
162
+ if (handledResult) {
163
+ return handledResult
164
+ }
165
+ }
166
+ }
167
+
168
+ // Step 3: Create new instance and store it
169
+ // NOTE: Holder is stored synchronously inside createAndStoreInstance before any await
170
+ const [createError, holder] = await this.createAndStoreInstance(
171
+ instanceName,
172
+ realToken,
173
+ validatedArgs,
174
+ contextContainer,
175
+ storage,
176
+ scopedContainer,
177
+ )
178
+ if (createError) {
179
+ return [createError]
180
+ }
181
+
182
+ return [undefined, holder!.instance]
183
+ }
184
+
185
+ /**
186
+ * Handles storage error states (destroying, error, etc.).
187
+ * Returns a result if handled, null if should proceed with creation.
188
+ */
189
+ private async handleStorageError(
190
+ instanceName: string,
191
+ error: DIError,
192
+ holder: InstanceHolder | undefined,
193
+ storage: IHolderStorage,
194
+ ): Promise<[undefined, any] | [DIError] | null> {
195
+ switch (error.code) {
196
+ case DIErrorCode.InstanceDestroying:
197
+ // Wait for destruction then retry
198
+ this.logger?.log(
199
+ `[InstanceResolver] Instance ${instanceName} is being destroyed, waiting...`,
200
+ )
201
+ if (holder?.destroyPromise) {
202
+ await holder.destroyPromise
203
+ }
204
+ // Re-check after destruction
205
+ const newResult = storage.get(instanceName)
206
+ if (newResult !== null && !newResult[0]) {
207
+ const readyResult = await this.waitForInstanceReady(newResult[1]!)
208
+ if (readyResult[0]) {
209
+ return [readyResult[0]]
210
+ }
211
+ return [undefined, readyResult[1]!.instance]
212
+ }
213
+ return null // Proceed with creation
214
+
215
+ default:
216
+ return [error]
217
+ }
218
+ }
219
+
220
+ /**
221
+ * Creates a new instance and stores it using the provided storage strategy.
222
+ * This unified method replaces instantiateServiceFromRegistry and createRequestScopedInstance.
223
+ *
224
+ * For transient services, the instance is created but not stored (no caching).
225
+ */
226
+ private async createAndStoreInstance<Instance>(
227
+ instanceName: string,
228
+ realToken: InjectionToken<Instance, any>,
229
+ args: any,
230
+ contextContainer: IContainer,
231
+ storage: IHolderStorage,
232
+ scopedContainer?: ScopedContainer,
233
+ ): Promise<[undefined, InstanceHolder<Instance>] | [DIError]> {
234
+ this.logger?.log(
235
+ `[InstanceResolver]#createAndStoreInstance() Creating instance for ${instanceName}`,
236
+ )
237
+
238
+ if (!this.registry.has(realToken)) {
239
+ return [DIError.factoryNotFound(realToken.name.toString())]
240
+ }
241
+
242
+ const ctx = this.createFactoryContext(contextContainer)
243
+ const record = this.registry.get<Instance, any>(realToken)
244
+ const { scope, type } = record
245
+
246
+ // For transient services, don't use storage locking - create directly
247
+ if (scope === InjectableScope.Transient) {
248
+ return this.createTransientInstance(instanceName, record, args, ctx)
249
+ }
250
+
251
+ // Create holder in "Creating" state using registry scope, not storage scope
252
+ const [deferred, holder] = this.manager.createCreatingHolder<Instance>(
253
+ instanceName,
254
+ type,
255
+ scope,
256
+ ctx.deps,
257
+ )
258
+
259
+ // Store holder immediately (for lock mechanism)
260
+ storage.set(instanceName, holder)
261
+
262
+ // Create a getHolder function that looks up holders from both the manager and storage
263
+ const getHolder = (name: string): InstanceHolder | undefined => {
264
+ // First check the storage (which might be request-scoped)
265
+ const storageResult = storage.get(name)
266
+ if (storageResult !== null) {
267
+ const [, storageHolder] = storageResult
268
+ if (storageHolder) return storageHolder
269
+ }
270
+ // Fall back to the singleton manager
271
+ const [, managerHolder] = this.manager.get(name)
272
+ return managerHolder
273
+ }
274
+
275
+ // Start async instantiation within the resolution context
276
+ // This allows circular dependency detection to track the waiter
277
+ withResolutionContext(holder, getHolder, () => {
278
+ this.instantiator
279
+ .instantiateService(ctx, record, args)
280
+ .then(async (result: [undefined, Instance] | [DIError]) => {
281
+ const [error, instance] =
282
+ result.length === 2 ? result : [result[0], undefined]
283
+ await this.handleInstantiationResult(
284
+ instanceName,
285
+ holder,
286
+ ctx,
287
+ deferred,
288
+ scope,
289
+ error,
290
+ instance,
291
+ scopedContainer,
292
+ )
293
+ })
294
+ .catch(async (error: Error) => {
295
+ await this.handleInstantiationError(
296
+ instanceName,
297
+ holder,
298
+ deferred,
299
+ scope,
300
+ error,
301
+ )
302
+ })
303
+ .catch(() => {
304
+ // Suppress unhandled rejections from the async chain.
305
+ // Errors are communicated to awaiters via deferred.reject() which
306
+ // rejects holder.creationPromise. This catch is a safety net for
307
+ // any errors that might occur in the error handling itself.
308
+ })
309
+ })
310
+
311
+ // Wait for instance to be ready
312
+ return this.waitForInstanceReady(holder)
313
+ }
314
+
315
+ /**
316
+ * Creates a transient instance without storage or locking.
317
+ * Each call creates a new instance.
318
+ */
319
+ private async createTransientInstance<Instance>(
320
+ instanceName: string,
321
+ record: any,
322
+ args: any,
323
+ ctx: FactoryContext & {
324
+ deps: Set<string>
325
+ getDestroyListeners: () => (() => void)[]
326
+ },
327
+ ): Promise<[undefined, InstanceHolder<Instance>] | [DIError]> {
328
+ this.logger?.log(
329
+ `[InstanceResolver]#createTransientInstance() Creating transient instance for ${instanceName}`,
330
+ )
331
+
332
+ // Create a temporary holder for resolution context (transient instances can still have deps)
333
+ const tempHolder: InstanceHolder<Instance> = {
334
+ status: InstanceStatus.Creating,
335
+ name: instanceName,
336
+ instance: null,
337
+ creationPromise: null,
338
+ destroyPromise: null,
339
+ type: record.type,
340
+ scope: InjectableScope.Transient,
341
+ deps: ctx.deps,
342
+ destroyListeners: [],
343
+ createdAt: Date.now(),
344
+ waitingFor: new Set(),
345
+ }
346
+
347
+ // Create a getHolder function for resolution context
348
+ const getHolder = (name: string): InstanceHolder | undefined => {
349
+ const [, managerHolder] = this.manager.get(name)
350
+ return managerHolder
351
+ }
352
+
353
+ // Run instantiation within resolution context for cycle detection
354
+ const [error, instance] = await withResolutionContext(
355
+ tempHolder,
356
+ getHolder,
357
+ () => this.instantiator.instantiateService(ctx, record, args),
358
+ )
359
+
360
+ if (error) {
361
+ return [error as DIError]
362
+ }
363
+
364
+ // Create a holder for the transient instance (not stored, just for return consistency)
365
+ const holder: InstanceHolder<Instance> = {
366
+ status: InstanceStatus.Created,
367
+ name: instanceName,
368
+ instance: instance as Instance,
369
+ creationPromise: null,
370
+ destroyPromise: null,
371
+ type: record.type,
372
+ scope: InjectableScope.Transient,
373
+ deps: ctx.deps,
374
+ destroyListeners: ctx.getDestroyListeners(),
375
+ createdAt: Date.now(),
376
+ waitingFor: new Set(),
377
+ }
378
+
379
+ return [undefined, holder]
380
+ }
381
+
382
+ /**
383
+ * Gets a synchronous instance (for sync operations).
384
+ */
385
+ getSyncInstance<
386
+ Instance,
387
+ Schema extends InjectionTokenSchemaType | undefined,
388
+ >(
389
+ token: AnyInjectableType,
390
+ args: Schema extends ZodObject
391
+ ? z.input<Schema>
392
+ : Schema extends ZodOptional<ZodObject>
393
+ ? z.input<Schema> | undefined
394
+ : undefined,
395
+ contextContainer: IContainer,
396
+ ): Instance | null {
397
+ const [err, { actualToken, validatedArgs }] =
398
+ this.tokenProcessor.validateAndResolveTokenArgs(token, args)
399
+ if (err) {
400
+ return null
401
+ }
402
+ const instanceName = this.tokenProcessor.generateInstanceName(
403
+ actualToken,
404
+ validatedArgs,
405
+ )
406
+
407
+ // Check if this is a ScopedContainer and the service is request-scoped
408
+ if ('getRequestInstance' in contextContainer) {
409
+ const scopedContainer = contextContainer as ScopedContainer
410
+ const requestHolder = scopedContainer.getRequestInstance(instanceName)
411
+ if (requestHolder) {
412
+ return requestHolder.instance as Instance
413
+ }
414
+ }
415
+
416
+ // Try singleton manager
417
+ const [error, holder] = this.manager.get(instanceName)
418
+ if (error) {
419
+ return null
420
+ }
421
+ return holder.instance as Instance
422
+ }
423
+
424
+ /**
425
+ * Internal method to resolve token args and create instance name.
426
+ * Handles factory token resolution and validation.
427
+ */
428
+ private async resolveTokenAndPrepareInstanceName(
429
+ token: AnyInjectableType,
430
+ args: any,
431
+ contextContainer: IContainer,
432
+ ): Promise<
433
+ | [
434
+ undefined,
435
+ {
436
+ instanceName: string
437
+ validatedArgs: any
438
+ actualToken: InjectionTokenType
439
+ realToken: InjectionToken<any, any>
440
+ },
441
+ ]
442
+ | [DIError]
443
+ > {
444
+ const [err, { actualToken, validatedArgs }] =
445
+ this.tokenProcessor.validateAndResolveTokenArgs(token, args)
446
+ if (err instanceof DIError && err.code === DIErrorCode.UnknownError) {
447
+ return [err]
448
+ } else if (
449
+ err instanceof DIError &&
450
+ err.code === DIErrorCode.FactoryTokenNotResolved &&
451
+ actualToken instanceof FactoryInjectionToken
452
+ ) {
453
+ this.logger?.log(
454
+ `[InstanceResolver]#resolveTokenAndPrepareInstanceName() Factory token not resolved, resolving it`,
455
+ )
456
+ await actualToken.resolve(this.createFactoryContext(contextContainer))
457
+ return this.resolveTokenAndPrepareInstanceName(
458
+ token,
459
+ undefined,
460
+ contextContainer,
461
+ )
462
+ }
463
+ const instanceName = this.tokenProcessor.generateInstanceName(
464
+ actualToken,
465
+ validatedArgs,
466
+ )
467
+ // Determine the real token (the actual InjectionToken that will be used for resolution)
468
+ const realToken =
469
+ actualToken instanceof BoundInjectionToken ||
470
+ actualToken instanceof FactoryInjectionToken
471
+ ? actualToken.token
472
+ : actualToken
473
+ return [undefined, { instanceName, validatedArgs, actualToken, realToken }]
474
+ }
475
+
476
+ // ============================================================================
477
+ // INSTANTIATION HANDLERS
478
+ // ============================================================================
479
+
480
+ /**
481
+ * Waits for an instance holder to be ready and returns the appropriate result.
482
+ * Uses the shared utility from BaseHolderManager.
483
+ * Passes the current resolution context for circular dependency detection.
484
+ */
485
+ private waitForInstanceReady<T>(
486
+ holder: InstanceHolder<T>,
487
+ ): Promise<[undefined, InstanceHolder<T>] | [DIError]> {
488
+ // Get the current resolution context (if we're inside an instantiation)
489
+ const ctx = getCurrentResolutionContext()
490
+
491
+ return BaseHolderManager.waitForHolderReady(
492
+ holder,
493
+ ctx?.waiterHolder,
494
+ ctx?.getHolder,
495
+ )
496
+ }
497
+
498
+ /**
499
+ * Handles the result of service instantiation.
500
+ */
501
+ private async handleInstantiationResult(
502
+ instanceName: string,
503
+ holder: InstanceHolder<any>,
504
+ ctx: FactoryContext & {
505
+ deps: Set<string>
506
+ getDestroyListeners: () => (() => void)[]
507
+ },
508
+ deferred: any,
509
+ scope: InjectableScope,
510
+ error: any,
511
+ instance: any,
512
+ scopedContainer?: ScopedContainer,
513
+ ): Promise<void> {
514
+ holder.destroyListeners = ctx.getDestroyListeners()
515
+ holder.creationPromise = null
516
+
517
+ if (error) {
518
+ await this.handleInstantiationError(
519
+ instanceName,
520
+ holder,
521
+ deferred,
522
+ scope,
523
+ error,
524
+ )
525
+ } else {
526
+ await this.handleInstantiationSuccess(
527
+ instanceName,
528
+ holder,
529
+ ctx,
530
+ deferred,
531
+ instance,
532
+ scopedContainer,
533
+ )
534
+ }
535
+ }
536
+
537
+ /**
538
+ * Handles successful service instantiation.
539
+ */
540
+ private async handleInstantiationSuccess(
541
+ instanceName: string,
542
+ holder: InstanceHolder<any>,
543
+ ctx: FactoryContext & {
544
+ deps: Set<string>
545
+ getDestroyListeners: () => (() => void)[]
546
+ },
547
+ deferred: any,
548
+ instance: any,
549
+ scopedContainer?: ScopedContainer,
550
+ ): Promise<void> {
551
+ holder.instance = instance
552
+ holder.status = InstanceStatus.Created
553
+
554
+ // Set up dependency invalidation listeners
555
+ if (ctx.deps.size > 0) {
556
+ ctx.deps.forEach((dependency: string) => {
557
+ holder.destroyListeners.push(
558
+ this.serviceLocator.getEventBus().on(dependency, 'destroy', () => {
559
+ this.logger?.log(
560
+ `[InstanceResolver] Dependency ${dependency} destroyed, invalidating ${instanceName}`,
561
+ )
562
+ this.serviceLocator.getInvalidator().invalidate(instanceName)
563
+ }),
564
+ )
565
+
566
+ // For request-scoped services, also listen with prefixed event name
567
+ if (scopedContainer) {
568
+ const prefixedDependency =
569
+ scopedContainer.getPrefixedEventName(dependency)
570
+ holder.destroyListeners.push(
571
+ this.serviceLocator
572
+ .getEventBus()
573
+ .on(prefixedDependency, 'destroy', () => {
574
+ this.logger?.log(
575
+ `[InstanceResolver] Request-scoped dependency ${dependency} destroyed, invalidating ${instanceName}`,
576
+ )
577
+ // For request-scoped, we need to invalidate within the scoped container
578
+ scopedContainer.invalidate(instance)
579
+ }),
580
+ )
581
+ }
582
+ })
583
+ }
584
+
585
+ // Note: Event emission would need access to the event bus
586
+ this.logger?.log(
587
+ `[InstanceResolver] Instance ${instanceName} created successfully`,
588
+ )
589
+ deferred.resolve([undefined, instance])
590
+ }
591
+
592
+ /**
593
+ * Handles service instantiation errors.
594
+ */
595
+ private async handleInstantiationError(
596
+ instanceName: string,
597
+ holder: InstanceHolder<any>,
598
+ deferred: any,
599
+ scope: InjectableScope,
600
+ error: any,
601
+ ): Promise<void> {
602
+ this.logger?.error(
603
+ `[InstanceResolver] Error creating instance for ${instanceName}`,
604
+ error,
605
+ )
606
+
607
+ holder.status = InstanceStatus.Error
608
+ holder.instance = error
609
+ holder.creationPromise = null
610
+
611
+ if (scope === InjectableScope.Singleton) {
612
+ this.logger?.log(
613
+ `[InstanceResolver] Singleton ${instanceName} failed, will be invalidated`,
614
+ )
615
+ // Fire-and-forget invalidation - don't await as it could cause deadlocks
616
+ // Suppress any potential rejections since the primary error is already handled
617
+ this.serviceLocator
618
+ .getInvalidator()
619
+ .invalidate(instanceName)
620
+ .catch(() => {
621
+ // Suppress - primary error is communicated via deferred.reject()
622
+ })
623
+ }
624
+
625
+ deferred.reject(error)
626
+ }
627
+
628
+ // ============================================================================
629
+ // FACTORY CONTEXT
630
+ // ============================================================================
631
+
632
+ /**
633
+ * Creates a factory context for dependency injection during service instantiation.
634
+ */
635
+ private createFactoryContext(contextContainer: IContainer): FactoryContext & {
636
+ getDestroyListeners: () => (() => void)[]
637
+ deps: Set<string>
638
+ } {
639
+ return this.tokenProcessor.createFactoryContext(contextContainer)
640
+ }
641
+ }