@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,202 @@
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 {
6
+ AnyInjectableType,
7
+ InjectionTokenSchemaType,
8
+ } from '../../token/injection-token.mjs'
9
+ import type { IContainer } from '../../interfaces/container.interface.mjs'
10
+ import type { Registry } from '../../token/registry.mjs'
11
+ import type { ScopedContainer } from '../../container/scoped-container.mjs'
12
+ import type { ClearAllOptions } from './invalidator.mjs'
13
+ import type { Injectors } from '../../utils/index.mjs'
14
+
15
+ import { defaultInjectors } from '../../injectors.mjs'
16
+ import { InstanceResolver } from './instance-resolver.mjs'
17
+ import { globalRegistry } from '../../token/registry.mjs'
18
+ import { Instantiator } from './instantiator.mjs'
19
+ import { Invalidator } from './invalidator.mjs'
20
+ import { LifecycleEventBus } from '../lifecycle/lifecycle-event-bus.mjs'
21
+ import { HolderManager } from '../holder/holder-manager.mjs'
22
+ import { TokenProcessor } from './token-processor.mjs'
23
+
24
+ /**
25
+ * Core DI engine that coordinates service instantiation, resolution, and lifecycle.
26
+ *
27
+ * Acts as the central orchestrator for dependency injection operations,
28
+ * delegating to specialized components (InstanceResolver, Instantiator, Invalidator)
29
+ * for specific tasks.
30
+ */
31
+ export class ServiceLocator {
32
+ private readonly eventBus: LifecycleEventBus
33
+ private readonly manager: HolderManager
34
+ private readonly instantiator: Instantiator
35
+ private readonly tokenProcessor: TokenProcessor
36
+ private readonly invalidator: Invalidator
37
+ private readonly instanceResolver: InstanceResolver
38
+
39
+ constructor(
40
+ private readonly registry: Registry = globalRegistry,
41
+ private readonly logger: Console | null = null,
42
+ private readonly injectors: Injectors = defaultInjectors,
43
+ ) {
44
+ this.eventBus = new LifecycleEventBus(logger)
45
+ this.manager = new HolderManager(logger)
46
+ this.instantiator = new Instantiator(injectors)
47
+ this.tokenProcessor = new TokenProcessor(logger)
48
+ this.invalidator = new Invalidator(
49
+ this.manager,
50
+ this.eventBus,
51
+ logger,
52
+ )
53
+ this.instanceResolver = new InstanceResolver(
54
+ this.registry,
55
+ this.manager,
56
+ this.instantiator,
57
+ this.tokenProcessor,
58
+ logger,
59
+ this,
60
+ )
61
+ }
62
+
63
+ // ============================================================================
64
+ // PUBLIC METHODS
65
+ // ============================================================================
66
+
67
+ getEventBus() {
68
+ return this.eventBus
69
+ }
70
+
71
+ getManager() {
72
+ return this.manager
73
+ }
74
+
75
+ getInvalidator() {
76
+ return this.invalidator
77
+ }
78
+
79
+ getTokenProcessor() {
80
+ return this.tokenProcessor
81
+ }
82
+
83
+ public getInstanceIdentifier(token: AnyInjectableType, args?: any): string {
84
+ const [err, { actualToken, validatedArgs }] =
85
+ this.tokenProcessor.validateAndResolveTokenArgs(token, args)
86
+ if (err) {
87
+ throw err
88
+ }
89
+ return this.tokenProcessor.generateInstanceName(actualToken, validatedArgs)
90
+ }
91
+
92
+ /**
93
+ * Gets or creates an instance for the given token.
94
+ * @param token The injection token
95
+ * @param args Optional arguments
96
+ * @param contextContainer The container to use for creating FactoryContext
97
+ */
98
+ public async getInstance(
99
+ token: AnyInjectableType,
100
+ args: any,
101
+ contextContainer: IContainer,
102
+ ) {
103
+ const [err, data] = await this.instanceResolver.resolveInstance(
104
+ token,
105
+ args,
106
+ contextContainer,
107
+ )
108
+ if (err) {
109
+ return [err]
110
+ }
111
+
112
+ return [undefined, data]
113
+ }
114
+
115
+ /**
116
+ * Gets or throws an instance for the given token.
117
+ * @param token The injection token
118
+ * @param args Optional arguments
119
+ * @param contextContainer The container to use for creating FactoryContext
120
+ */
121
+ public async getOrThrowInstance<Instance>(
122
+ token: AnyInjectableType,
123
+ args: any,
124
+ contextContainer: IContainer,
125
+ ): Promise<Instance> {
126
+ const [error, instance] = await this.getInstance(token, args, contextContainer)
127
+ if (error) {
128
+ throw error
129
+ }
130
+ return instance
131
+ }
132
+
133
+ /**
134
+ * Resolves a request-scoped service for a ScopedContainer.
135
+ * The service will be stored in the ScopedContainer's request context.
136
+ *
137
+ * @param token The injection token
138
+ * @param args Optional arguments
139
+ * @param scopedContainer The ScopedContainer that owns the request context
140
+ */
141
+ public async resolveRequestScoped(
142
+ token: AnyInjectableType,
143
+ args: any,
144
+ scopedContainer: ScopedContainer,
145
+ ): Promise<any> {
146
+ const [err, data] = await this.instanceResolver.resolveRequestScopedInstance(
147
+ token,
148
+ args,
149
+ scopedContainer,
150
+ )
151
+ if (err) {
152
+ throw err
153
+ }
154
+ return data
155
+ }
156
+
157
+ public getSyncInstance<
158
+ Instance,
159
+ Schema extends InjectionTokenSchemaType | undefined,
160
+ >(
161
+ token: AnyInjectableType,
162
+ args: Schema extends ZodObject
163
+ ? z.input<Schema>
164
+ : Schema extends ZodOptional<ZodObject>
165
+ ? z.input<Schema> | undefined
166
+ : undefined,
167
+ contextContainer: IContainer,
168
+ ): Instance | null {
169
+ return this.instanceResolver.getSyncInstance(token, args as any, contextContainer)
170
+ }
171
+
172
+ invalidate(service: string, round = 1): Promise<any> {
173
+ return this.invalidator.invalidate(service, round)
174
+ }
175
+
176
+ /**
177
+ * Gracefully clears all services in the ServiceLocator using invalidation logic.
178
+ * This method respects service dependencies and ensures proper cleanup order.
179
+ * Services that depend on others will be invalidated first, then their dependencies.
180
+ *
181
+ * @param options Optional configuration for the clearing process
182
+ * @returns Promise that resolves when all services have been cleared
183
+ */
184
+ async clearAll(options: ClearAllOptions = {}): Promise<void> {
185
+ return this.invalidator.clearAll(options)
186
+ }
187
+
188
+ /**
189
+ * Waits for all services to settle (either created, destroyed, or error state).
190
+ */
191
+ async ready(): Promise<void> {
192
+ return this.invalidator.ready()
193
+ }
194
+
195
+ /**
196
+ * Helper method for InstanceResolver to generate instance names.
197
+ * This is needed for the factory context creation.
198
+ */
199
+ generateInstanceName(token: any, args: any): string {
200
+ return this.tokenProcessor.generateInstanceName(token, args)
201
+ }
202
+ }
@@ -1,29 +1,81 @@
1
1
  /* eslint-disable @typescript-eslint/no-explicit-any */
2
2
  /* eslint-disable @typescript-eslint/no-empty-object-type */
3
3
 
4
- import type { FactoryContext } from './factory-context.mjs'
4
+ import type { FactoryContext } from '../context/factory-context.mjs'
5
5
  import type {
6
6
  AnyInjectableType,
7
7
  InjectionTokenType,
8
- } from './injection-token.mjs'
9
- import type { RequestContextHolder } from './request-context-holder.mjs'
10
- import type { ServiceLocator } from './service-locator.mjs'
8
+ } from '../../token/injection-token.mjs'
9
+ import type { IContainer } from '../../interfaces/container.interface.mjs'
11
10
 
12
- import { DIError } from './errors/index.mjs'
11
+ import { DIError } from '../../errors/index.mjs'
13
12
  import {
14
13
  BoundInjectionToken,
15
14
  FactoryInjectionToken,
16
15
  InjectionToken,
17
- } from './injection-token.mjs'
18
- import { getInjectableToken } from './utils/index.mjs'
16
+ } from '../../token/injection-token.mjs'
17
+ import { getInjectableToken } from '../../utils/index.mjs'
19
18
 
20
19
  /**
21
- * TokenProcessor handles token validation, resolution, and instance name generation.
22
- * Extracted from ServiceLocator to improve separation of concerns.
20
+ * Handles token validation, normalization, and instance name generation.
21
+ *
22
+ * Provides utilities for resolving tokens to their underlying InjectionToken,
23
+ * validating arguments against schemas, and generating unique instance identifiers.
23
24
  */
24
25
  export class TokenProcessor {
25
26
  constructor(private readonly logger: Console | null = null) {}
26
27
 
28
+ // ============================================================================
29
+ // TOKEN NORMALIZATION
30
+ // ============================================================================
31
+
32
+ /**
33
+ * Normalizes a token to an InjectionToken.
34
+ * Handles class constructors by getting their injectable token.
35
+ *
36
+ * @param token A class constructor, InjectionToken, BoundInjectionToken, or FactoryInjectionToken
37
+ * @returns The normalized InjectionTokenType
38
+ */
39
+ normalizeToken(token: AnyInjectableType): InjectionTokenType {
40
+ if (typeof token === 'function') {
41
+ return getInjectableToken(token)
42
+ }
43
+ return token as InjectionTokenType
44
+ }
45
+
46
+ /**
47
+ * Gets the underlying "real" token from wrapped tokens.
48
+ * For BoundInjectionToken and FactoryInjectionToken, returns the wrapped token.
49
+ * For other tokens, returns the token itself.
50
+ *
51
+ * @param token The token to unwrap
52
+ * @returns The underlying InjectionToken
53
+ */
54
+ getRealToken<T = unknown>(token: InjectionTokenType): InjectionToken<T> {
55
+ if (
56
+ token instanceof BoundInjectionToken ||
57
+ token instanceof FactoryInjectionToken
58
+ ) {
59
+ return token.token as InjectionToken<T>
60
+ }
61
+ return token as InjectionToken<T>
62
+ }
63
+
64
+ /**
65
+ * Convenience method that normalizes a token and then gets the real token.
66
+ * Useful for checking registry entries where you need the actual registered token.
67
+ *
68
+ * @param token Any injectable type
69
+ * @returns The underlying InjectionToken
70
+ */
71
+ getRegistryToken<T = unknown>(token: AnyInjectableType): InjectionToken<T> {
72
+ return this.getRealToken(this.normalizeToken(token))
73
+ }
74
+
75
+ // ============================================================================
76
+ // TOKEN VALIDATION
77
+ // ============================================================================
78
+
27
79
  /**
28
80
  * Validates and resolves token arguments, handling factory token resolution and validation.
29
81
  */
@@ -93,10 +145,12 @@ export class TokenProcessor {
93
145
 
94
146
  /**
95
147
  * Creates a factory context for dependency injection during service instantiation.
96
- * @param serviceLocator Reference to the service locator for dependency resolution
148
+ * @param container The container instance (Container or ScopedContainer) for dependency resolution
149
+ * @param onDependencyResolved Callback when a dependency is resolved, receives the instance name
97
150
  */
98
151
  createFactoryContext(
99
- serviceLocator: ServiceLocator, // ServiceLocator reference for dependency resolution
152
+ container: IContainer,
153
+ onDependencyResolved?: (instanceName: string) => void,
100
154
  ): FactoryContext & {
101
155
  getDestroyListeners: () => (() => void)[]
102
156
  deps: Set<string>
@@ -112,63 +166,28 @@ export class TokenProcessor {
112
166
  return Array.from(destroyListeners)
113
167
  }
114
168
 
169
+ const self = this
170
+
115
171
  return {
116
172
  // @ts-expect-error This is correct type
117
173
  async inject(token, args) {
118
- // Fall back to normal resolution
119
- const [error, instance] = await serviceLocator.getInstance(
120
- token,
121
- args,
122
- ({ instanceName }: { instanceName: string }) => {
123
- deps.add(instanceName)
124
- },
125
- )
126
- if (error) {
127
- throw error
174
+ // Get the instance name for dependency tracking
175
+ const actualToken =
176
+ typeof token === 'function' ? getInjectableToken(token) : token
177
+ const instanceName = self.generateInstanceName(actualToken, args)
178
+ deps.add(instanceName)
179
+
180
+ if (onDependencyResolved) {
181
+ onDependencyResolved(instanceName)
128
182
  }
129
- return instance
183
+
184
+ // Use the container's get method for resolution
185
+ return container.get(token, args)
130
186
  },
131
187
  addDestroyListener,
132
188
  getDestroyListeners,
133
- locator: serviceLocator,
189
+ container,
134
190
  deps,
135
191
  }
136
192
  }
137
-
138
- /**
139
- * Tries to get a pre-prepared instance from request contexts.
140
- */
141
- tryGetPrePreparedInstance(
142
- instanceName: string,
143
- contextHolder: RequestContextHolder | undefined,
144
- deps: Set<string>,
145
- currentRequestContext: RequestContextHolder | null,
146
- ): any {
147
- // Check provided context holder first (if has higher priority)
148
- if (contextHolder && contextHolder.priority > 0) {
149
- const prePreparedInstance = contextHolder.get(instanceName)?.instance
150
- if (prePreparedInstance !== undefined) {
151
- this.logger?.debug(
152
- `[TokenProcessor] Using pre-prepared instance ${instanceName} from request context ${contextHolder.requestId}`,
153
- )
154
- deps.add(instanceName)
155
- return prePreparedInstance
156
- }
157
- }
158
-
159
- // Check current request context (if different from provided contextHolder)
160
- if (currentRequestContext && currentRequestContext !== contextHolder) {
161
- const prePreparedInstance =
162
- currentRequestContext.get(instanceName)?.instance
163
- if (prePreparedInstance !== undefined) {
164
- this.logger?.debug(
165
- `[TokenProcessor] Using pre-prepared instance ${instanceName} from current request context ${currentRequestContext.requestId}`,
166
- )
167
- deps.add(instanceName)
168
- return prePreparedInstance
169
- }
170
- }
171
-
172
- return undefined
173
- }
174
193
  }
@@ -1,14 +1,24 @@
1
- import type { ServiceLocatorInstanceHolder } from './service-locator-instance-holder.mjs'
1
+ import type { InstanceHolder } from './instance-holder.mjs'
2
2
 
3
- import { InjectableScope, InjectableType } from './enums/index.mjs'
4
- import { ServiceLocatorInstanceHolderStatus } from './service-locator-instance-holder.mjs'
3
+ import { InjectableScope, InjectableType } from '../../enums/index.mjs'
4
+ import { DIError } from '../../errors/index.mjs'
5
+ import { CircularDetector } from '../lifecycle/circular-detector.mjs'
6
+ import { InstanceStatus } from './instance-holder.mjs'
5
7
 
6
8
  /**
7
- * Abstract base class that provides common functionality for managing ServiceLocatorInstanceHolder objects.
8
- * This class contains shared patterns used by both RequestContextHolder and ServiceLocatorManager.
9
+ * Result type for waitForHolderReady.
10
+ * Returns either [undefined, holder] on success or [error] on failure.
9
11
  */
10
- export abstract class BaseInstanceHolderManager {
11
- protected readonly _holders: Map<string, ServiceLocatorInstanceHolder>
12
+ export type HolderReadyResult<T> = [undefined, InstanceHolder<T>] | [DIError]
13
+
14
+ /**
15
+ * Abstract base class providing common functionality for managing InstanceHolder objects.
16
+ *
17
+ * Provides shared patterns for holder storage, creation, and lifecycle management
18
+ * used by both singleton (HolderManager) and request-scoped (RequestContext) managers.
19
+ */
20
+ export abstract class BaseHolderManager {
21
+ protected readonly _holders: Map<string, InstanceHolder>
12
22
 
13
23
  constructor(protected readonly logger: Console | null = null) {
14
24
  this._holders = new Map()
@@ -17,7 +27,7 @@ export abstract class BaseInstanceHolderManager {
17
27
  /**
18
28
  * Protected getter for accessing the holders map from subclasses.
19
29
  */
20
- protected get holders(): Map<string, ServiceLocatorInstanceHolder> {
30
+ protected get holders(): Map<string, InstanceHolder> {
21
31
  return this._holders
22
32
  }
23
33
 
@@ -30,7 +40,7 @@ export abstract class BaseInstanceHolderManager {
30
40
  /**
31
41
  * Abstract method to set a holder by name. Each implementation may have different validation logic.
32
42
  */
33
- abstract set(name: string, holder: ServiceLocatorInstanceHolder): void
43
+ abstract set(name: string, holder: InstanceHolder): void
34
44
 
35
45
  /**
36
46
  * Abstract method to check if a holder exists. Each implementation may have different validation logic.
@@ -52,11 +62,8 @@ export abstract class BaseInstanceHolderManager {
52
62
  * @returns A new Map containing only the holders that match the predicate
53
63
  */
54
64
  filter(
55
- predicate: (
56
- value: ServiceLocatorInstanceHolder<any>,
57
- key: string,
58
- ) => boolean,
59
- ): Map<string, ServiceLocatorInstanceHolder> {
65
+ predicate: (value: InstanceHolder<any>, key: string) => boolean,
66
+ ): Map<string, InstanceHolder> {
60
67
  return new Map(
61
68
  [...this._holders].filter(([key, value]) => predicate(value, key)),
62
69
  )
@@ -92,12 +99,12 @@ export abstract class BaseInstanceHolderManager {
92
99
  deps: Set<string> = new Set(),
93
100
  ): [
94
101
  ReturnType<typeof Promise.withResolvers<[undefined, Instance]>>,
95
- ServiceLocatorInstanceHolder<Instance>,
102
+ InstanceHolder<Instance>,
96
103
  ] {
97
104
  const deferred = Promise.withResolvers<[undefined, Instance]>()
98
105
 
99
- const holder: ServiceLocatorInstanceHolder<Instance> = {
100
- status: ServiceLocatorInstanceHolderStatus.Creating,
106
+ const holder: InstanceHolder<Instance> = {
107
+ status: InstanceStatus.Creating,
101
108
  name,
102
109
  instance: null,
103
110
  creationPromise: deferred.promise,
@@ -107,6 +114,7 @@ export abstract class BaseInstanceHolderManager {
107
114
  deps,
108
115
  destroyListeners: [],
109
116
  createdAt: Date.now(),
117
+ waitingFor: new Set(),
110
118
  }
111
119
 
112
120
  return [deferred, holder]
@@ -128,9 +136,9 @@ export abstract class BaseInstanceHolderManager {
128
136
  type: InjectableType,
129
137
  scope: InjectableScope,
130
138
  deps: Set<string> = new Set(),
131
- ): ServiceLocatorInstanceHolder<Instance> {
132
- const holder: ServiceLocatorInstanceHolder<Instance> = {
133
- status: ServiceLocatorInstanceHolderStatus.Created,
139
+ ): InstanceHolder<Instance> {
140
+ const holder: InstanceHolder<Instance> = {
141
+ status: InstanceStatus.Created,
134
142
  name,
135
143
  instance,
136
144
  creationPromise: null,
@@ -140,6 +148,7 @@ export abstract class BaseInstanceHolderManager {
140
148
  deps,
141
149
  destroyListeners: [],
142
150
  createdAt: Date.now(),
151
+ waitingFor: new Set(),
143
152
  }
144
153
 
145
154
  return holder
@@ -155,7 +164,7 @@ export abstract class BaseInstanceHolderManager {
155
164
  /**
156
165
  * Gets all holders currently managed.
157
166
  */
158
- getAllHolders(): ServiceLocatorInstanceHolder[] {
167
+ getAllHolders(): InstanceHolder[] {
159
168
  return Array.from(this._holders.values())
160
169
  }
161
170
 
@@ -165,4 +174,65 @@ export abstract class BaseInstanceHolderManager {
165
174
  isEmpty(): boolean {
166
175
  return this._holders.size === 0
167
176
  }
177
+
178
+ /**
179
+ * Waits for a holder to be ready and returns the appropriate result.
180
+ * This is a shared utility used by both singleton and request-scoped resolution.
181
+ *
182
+ * @param holder The holder to wait for
183
+ * @param waiterHolder Optional holder that is doing the waiting (for circular dependency detection)
184
+ * @param getHolder Optional function to retrieve holders by name (required if waiterHolder is provided)
185
+ * @returns A promise that resolves with [undefined, holder] on success or [DIError] on failure
186
+ */
187
+ static async waitForHolderReady<T>(
188
+ holder: InstanceHolder<T>,
189
+ waiterHolder?: InstanceHolder,
190
+ getHolder?: (name: string) => InstanceHolder | undefined,
191
+ ): Promise<HolderReadyResult<T>> {
192
+ switch (holder.status) {
193
+ case InstanceStatus.Creating: {
194
+ // Check for circular dependency before waiting
195
+ if (waiterHolder && getHolder) {
196
+ const cycle = CircularDetector.detectCycle(
197
+ waiterHolder.name,
198
+ holder.name,
199
+ getHolder,
200
+ )
201
+ if (cycle) {
202
+ return [DIError.circularDependency(cycle)]
203
+ }
204
+
205
+ // Track the waiting relationship
206
+ waiterHolder.waitingFor.add(holder.name)
207
+ }
208
+
209
+ try {
210
+ await holder.creationPromise
211
+ } finally {
212
+ // Clean up the waiting relationship
213
+ if (waiterHolder) {
214
+ waiterHolder.waitingFor.delete(holder.name)
215
+ }
216
+ }
217
+
218
+ return BaseHolderManager.waitForHolderReady(
219
+ holder,
220
+ waiterHolder,
221
+ getHolder,
222
+ )
223
+ }
224
+
225
+ case InstanceStatus.Destroying:
226
+ return [DIError.instanceDestroying(holder.name)]
227
+
228
+ case InstanceStatus.Error:
229
+ return [holder.instance as unknown as DIError]
230
+
231
+ case InstanceStatus.Created:
232
+ return [undefined, holder]
233
+
234
+ default:
235
+ return [DIError.instanceNotFound('unknown')]
236
+ }
237
+ }
168
238
  }
@@ -0,0 +1,85 @@
1
+ import type { InstanceHolder } from './instance-holder.mjs'
2
+
3
+ import { InjectableScope, InjectableType } from '../../enums/index.mjs'
4
+ import { DIError, DIErrorCode } from '../../errors/index.mjs'
5
+ import { BaseHolderManager } from './base-holder-manager.mjs'
6
+ import { InstanceStatus } from './instance-holder.mjs'
7
+
8
+ /**
9
+ * Manages the storage and retrieval of singleton instance holders.
10
+ *
11
+ * Provides CRUD operations and filtering for the holder map.
12
+ * Handles holder state validation (destroying, error states) on retrieval.
13
+ */
14
+ export class HolderManager extends BaseHolderManager {
15
+ constructor(logger: Console | null = null) {
16
+ super(logger)
17
+ }
18
+
19
+ get(
20
+ name: string,
21
+ ): [DIError, InstanceHolder] | [DIError] | [undefined, InstanceHolder] {
22
+ const holder = this._holders.get(name)
23
+ if (holder) {
24
+ if (holder.status === InstanceStatus.Destroying) {
25
+ this.logger?.log(
26
+ `[HolderManager]#get() Instance ${holder.name} is destroying`,
27
+ )
28
+ return [DIError.instanceDestroying(holder.name), holder]
29
+ } else if (holder.status === InstanceStatus.Error) {
30
+ this.logger?.log(
31
+ `[HolderManager]#get() Instance ${holder.name} is in error state`,
32
+ )
33
+ return [holder.instance as unknown as DIError, holder]
34
+ }
35
+
36
+ return [undefined, holder]
37
+ } else {
38
+ this.logger?.log(`[HolderManager]#get() Instance ${name} not found`)
39
+ return [DIError.instanceNotFound(name)]
40
+ }
41
+ }
42
+
43
+ set(name: string, holder: InstanceHolder): void {
44
+ this._holders.set(name, holder)
45
+ }
46
+
47
+ has(name: string): [DIError] | [undefined, boolean] {
48
+ const [error, holder] = this.get(name)
49
+ if (!error) {
50
+ return [undefined, true]
51
+ }
52
+ if (error.code === DIErrorCode.InstanceDestroying) {
53
+ return [error]
54
+ }
55
+ return [undefined, !!holder]
56
+ }
57
+
58
+ // delete and filter methods are inherited from BaseHolderManager
59
+
60
+ // createCreatingHolder method is inherited from BaseHolderManager
61
+
62
+ /**
63
+ * Creates a new holder with Created status and stores it.
64
+ * This is useful for creating holders that already have their instance ready.
65
+ * @param name The name of the instance
66
+ * @param instance The actual instance to store
67
+ * @param type The injectable type
68
+ * @param scope The injectable scope
69
+ * @param deps Optional set of dependencies
70
+ * @returns The created holder
71
+ */
72
+ storeCreatedHolder<Instance>(
73
+ name: string,
74
+ instance: Instance,
75
+ type: InjectableType,
76
+ scope: InjectableScope,
77
+ deps: Set<string> = new Set(),
78
+ ): InstanceHolder<Instance> {
79
+ const holder = this.createCreatedHolder(name, instance, type, scope, deps)
80
+
81
+ this._holders.set(name, holder)
82
+
83
+ return holder
84
+ }
85
+ }