@navios/di 0.5.1 → 0.6.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.
- package/CHANGELOG.md +145 -0
- package/README.md +196 -219
- package/docs/README.md +69 -11
- package/docs/api-reference.md +281 -117
- package/docs/container.md +220 -56
- package/docs/examples/request-scope-example.mts +2 -2
- package/docs/factory.md +3 -8
- package/docs/getting-started.md +37 -8
- package/docs/migration.md +318 -37
- package/docs/request-contexts.md +263 -175
- package/docs/scopes.md +79 -42
- package/lib/browser/index.d.mts +1577 -0
- package/lib/browser/index.d.mts.map +1 -0
- package/lib/browser/index.mjs +3013 -0
- package/lib/browser/index.mjs.map +1 -0
- package/lib/index-7jfWsiG4.d.mts +1211 -0
- package/lib/index-7jfWsiG4.d.mts.map +1 -0
- package/lib/index-DW3K5sOX.d.cts +1206 -0
- package/lib/index-DW3K5sOX.d.cts.map +1 -0
- package/lib/index.cjs +389 -0
- package/lib/index.cjs.map +1 -0
- package/lib/index.d.cts +376 -0
- package/lib/index.d.cts.map +1 -0
- package/lib/index.d.mts +371 -78
- package/lib/index.d.mts.map +1 -0
- package/lib/index.mjs +325 -63
- package/lib/index.mjs.map +1 -1
- package/lib/testing/index.cjs +9 -0
- package/lib/testing/index.d.cts +2 -0
- package/lib/testing/index.d.mts +2 -2
- package/lib/testing/index.mjs +2 -72
- package/lib/testing-BG_fa9TJ.mjs +2656 -0
- package/lib/testing-BG_fa9TJ.mjs.map +1 -0
- package/lib/testing-DIaIRiJz.cjs +2896 -0
- package/lib/testing-DIaIRiJz.cjs.map +1 -0
- package/package.json +29 -7
- package/project.json +2 -2
- package/src/__tests__/async-local-storage.browser.spec.mts +240 -0
- package/src/__tests__/async-local-storage.spec.mts +333 -0
- package/src/__tests__/container.spec.mts +30 -25
- package/src/__tests__/e2e.browser.spec.mts +790 -0
- package/src/__tests__/e2e.spec.mts +1222 -0
- package/src/__tests__/factory.spec.mts +1 -1
- package/src/__tests__/get-injectors.spec.mts +1 -1
- package/src/__tests__/injectable.spec.mts +1 -1
- package/src/__tests__/injection-token.spec.mts +1 -1
- package/src/__tests__/library-findings.spec.mts +563 -0
- package/src/__tests__/registry.spec.mts +2 -2
- package/src/__tests__/request-scope.spec.mts +266 -274
- package/src/__tests__/service-instantiator.spec.mts +18 -17
- package/src/__tests__/service-locator-event-bus.spec.mts +9 -9
- package/src/__tests__/service-locator-manager.spec.mts +15 -15
- package/src/__tests__/service-locator.spec.mts +167 -244
- package/src/__tests__/unified-api.spec.mts +27 -27
- package/src/__type-tests__/factory.spec-d.mts +2 -2
- package/src/__type-tests__/inject.spec-d.mts +2 -2
- package/src/__type-tests__/injectable.spec-d.mts +1 -1
- package/src/browser.mts +16 -0
- package/src/container/container.mts +319 -0
- package/src/container/index.mts +2 -0
- package/src/container/scoped-container.mts +350 -0
- package/src/decorators/factory.decorator.mts +4 -4
- package/src/decorators/injectable.decorator.mts +5 -5
- package/src/errors/di-error.mts +12 -5
- package/src/errors/index.mts +0 -8
- package/src/index.mts +156 -15
- package/src/interfaces/container.interface.mts +82 -0
- package/src/interfaces/factory.interface.mts +2 -2
- package/src/interfaces/index.mts +1 -0
- package/src/internal/context/async-local-storage.mts +120 -0
- package/src/internal/context/factory-context.mts +18 -0
- package/src/internal/context/index.mts +3 -0
- package/src/{request-context-holder.mts → internal/context/request-context.mts} +40 -27
- package/src/internal/context/resolution-context.mts +63 -0
- package/src/internal/context/sync-local-storage.mts +51 -0
- package/src/internal/core/index.mts +5 -0
- package/src/internal/core/instance-resolver.mts +641 -0
- package/src/{service-instantiator.mts → internal/core/instantiator.mts} +31 -27
- package/src/internal/core/invalidator.mts +437 -0
- package/src/internal/core/service-locator.mts +202 -0
- package/src/{token-processor.mts → internal/core/token-processor.mts} +79 -60
- package/src/{base-instance-holder-manager.mts → internal/holder/base-holder-manager.mts} +91 -21
- package/src/internal/holder/holder-manager.mts +85 -0
- package/src/internal/holder/holder-storage.interface.mts +116 -0
- package/src/internal/holder/index.mts +6 -0
- package/src/internal/holder/instance-holder.mts +109 -0
- package/src/internal/holder/request-storage.mts +134 -0
- package/src/internal/holder/singleton-storage.mts +105 -0
- package/src/internal/index.mts +4 -0
- package/src/internal/lifecycle/circular-detector.mts +77 -0
- package/src/internal/lifecycle/index.mts +2 -0
- package/src/{service-locator-event-bus.mts → internal/lifecycle/lifecycle-event-bus.mts} +11 -4
- package/src/testing/__tests__/test-container.spec.mts +2 -2
- package/src/testing/test-container.mts +4 -4
- package/src/token/index.mts +2 -0
- package/src/{injection-token.mts → token/injection-token.mts} +1 -1
- package/src/{registry.mts → token/registry.mts} +1 -1
- package/src/utils/get-injectable-token.mts +1 -1
- package/src/utils/get-injectors.mts +32 -15
- package/src/utils/types.mts +1 -1
- package/tsdown.config.mts +67 -0
- package/lib/_tsup-dts-rollup.d.mts +0 -1283
- package/lib/_tsup-dts-rollup.d.ts +0 -1283
- package/lib/chunk-2M576LCC.mjs +0 -2043
- package/lib/chunk-2M576LCC.mjs.map +0 -1
- package/lib/index.d.ts +0 -78
- package/lib/index.js +0 -2127
- package/lib/index.js.map +0 -1
- package/lib/testing/index.d.ts +0 -2
- package/lib/testing/index.js +0 -2060
- package/lib/testing/index.js.map +0 -1
- package/lib/testing/index.mjs.map +0 -1
- package/src/container.mts +0 -227
- package/src/factory-context.mts +0 -8
- package/src/instance-resolver.mts +0 -559
- package/src/request-context-manager.mts +0 -149
- package/src/service-invalidator.mts +0 -429
- package/src/service-locator-instance-holder.mts +0 -70
- package/src/service-locator-manager.mts +0 -85
- package/src/service-locator.mts +0 -246
- package/tsup.config.mts +0 -12
- /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
|
+
}
|