@veloxts/core 0.3.3 → 0.3.4
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/README.md +697 -16
- package/dist/app.d.ts +67 -10
- package/dist/app.d.ts.map +1 -1
- package/dist/app.js +79 -12
- package/dist/app.js.map +1 -1
- package/dist/di/container.d.ts +406 -0
- package/dist/di/container.d.ts.map +1 -0
- package/dist/di/container.js +693 -0
- package/dist/di/container.js.map +1 -0
- package/dist/di/decorators.d.ts +221 -0
- package/dist/di/decorators.d.ts.map +1 -0
- package/dist/di/decorators.js +297 -0
- package/dist/di/decorators.js.map +1 -0
- package/dist/di/index.d.ts +65 -0
- package/dist/di/index.d.ts.map +1 -0
- package/dist/di/index.js +73 -0
- package/dist/di/index.js.map +1 -0
- package/dist/di/providers.d.ts +357 -0
- package/dist/di/providers.d.ts.map +1 -0
- package/dist/di/providers.js +380 -0
- package/dist/di/providers.js.map +1 -0
- package/dist/di/scope.d.ts +209 -0
- package/dist/di/scope.d.ts.map +1 -0
- package/dist/di/scope.js +262 -0
- package/dist/di/scope.js.map +1 -0
- package/dist/di/tokens.d.ts +227 -0
- package/dist/di/tokens.d.ts.map +1 -0
- package/dist/di/tokens.js +192 -0
- package/dist/di/tokens.js.map +1 -0
- package/dist/index.d.ts +9 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +28 -5
- package/dist/index.js.map +1 -1
- package/package.json +3 -2
|
@@ -0,0 +1,693 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dependency Injection Container
|
|
3
|
+
*
|
|
4
|
+
* The VeloxTS DI container provides:
|
|
5
|
+
* - Service registration with multiple provider types
|
|
6
|
+
* - Automatic constructor injection via reflect-metadata
|
|
7
|
+
* - Lifecycle management (singleton, transient, request-scoped)
|
|
8
|
+
* - Circular dependency detection
|
|
9
|
+
* - Integration with Fastify for request-scoped services
|
|
10
|
+
*
|
|
11
|
+
* @module di/container
|
|
12
|
+
*/
|
|
13
|
+
import { VeloxError } from '../errors.js';
|
|
14
|
+
import { getConstructorTokens, getInjectableScope, getOptionalParams, isInjectable, } from './decorators.js';
|
|
15
|
+
import { normalizeProvider, validateProvider } from './providers.js';
|
|
16
|
+
import { Scope, ScopeManager } from './scope.js';
|
|
17
|
+
import { getTokenName, isClassToken, validateToken } from './tokens.js';
|
|
18
|
+
// ============================================================================
|
|
19
|
+
// Container Implementation
|
|
20
|
+
// ============================================================================
|
|
21
|
+
/**
|
|
22
|
+
* Dependency Injection Container
|
|
23
|
+
*
|
|
24
|
+
* The central hub for service registration and resolution.
|
|
25
|
+
* Manages service lifecycles and dependencies.
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* ```typescript
|
|
29
|
+
* // Create a container
|
|
30
|
+
* const container = new Container();
|
|
31
|
+
*
|
|
32
|
+
* // Register services
|
|
33
|
+
* container.register({ provide: UserService, useClass: UserService });
|
|
34
|
+
* container.register({
|
|
35
|
+
* provide: DATABASE,
|
|
36
|
+
* useFactory: (config) => createDb(config.dbUrl),
|
|
37
|
+
* inject: [ConfigService]
|
|
38
|
+
* });
|
|
39
|
+
*
|
|
40
|
+
* // Resolve services
|
|
41
|
+
* const userService = container.resolve(UserService);
|
|
42
|
+
* const db = container.resolve(DATABASE);
|
|
43
|
+
* ```
|
|
44
|
+
*/
|
|
45
|
+
export class Container {
|
|
46
|
+
/**
|
|
47
|
+
* Registered providers indexed by token
|
|
48
|
+
*/
|
|
49
|
+
providers = new Map();
|
|
50
|
+
/**
|
|
51
|
+
* Manages singleton and request-scoped instance caches
|
|
52
|
+
*/
|
|
53
|
+
scopeManager = new ScopeManager();
|
|
54
|
+
/**
|
|
55
|
+
* Parent container for hierarchical lookup
|
|
56
|
+
*/
|
|
57
|
+
parent;
|
|
58
|
+
/**
|
|
59
|
+
* Whether to auto-register @Injectable classes
|
|
60
|
+
*/
|
|
61
|
+
autoRegister;
|
|
62
|
+
/**
|
|
63
|
+
* Resolution stack for circular dependency detection
|
|
64
|
+
*/
|
|
65
|
+
resolutionStack = new Set();
|
|
66
|
+
/**
|
|
67
|
+
* Creates a new DI container
|
|
68
|
+
*
|
|
69
|
+
* @param options - Container configuration options
|
|
70
|
+
*
|
|
71
|
+
* @example
|
|
72
|
+
* ```typescript
|
|
73
|
+
* // Standalone container
|
|
74
|
+
* const container = new Container();
|
|
75
|
+
*
|
|
76
|
+
* // Child container (inherits from parent)
|
|
77
|
+
* const childContainer = new Container({ parent: container });
|
|
78
|
+
*
|
|
79
|
+
* // With auto-registration enabled
|
|
80
|
+
* const autoContainer = new Container({ autoRegister: true });
|
|
81
|
+
* ```
|
|
82
|
+
*/
|
|
83
|
+
constructor(options = {}) {
|
|
84
|
+
this.parent = options.parent;
|
|
85
|
+
this.autoRegister = options.autoRegister ?? false;
|
|
86
|
+
}
|
|
87
|
+
// ==========================================================================
|
|
88
|
+
// Registration
|
|
89
|
+
// ==========================================================================
|
|
90
|
+
/**
|
|
91
|
+
* Registers a service provider
|
|
92
|
+
*
|
|
93
|
+
* @param provider - The provider configuration
|
|
94
|
+
* @returns The container (for chaining)
|
|
95
|
+
* @throws {VeloxError} If the provider is invalid
|
|
96
|
+
*
|
|
97
|
+
* @example
|
|
98
|
+
* ```typescript
|
|
99
|
+
* // Class provider
|
|
100
|
+
* container.register({
|
|
101
|
+
* provide: UserService,
|
|
102
|
+
* useClass: UserService,
|
|
103
|
+
* scope: Scope.REQUEST
|
|
104
|
+
* });
|
|
105
|
+
*
|
|
106
|
+
* // Factory provider
|
|
107
|
+
* container.register({
|
|
108
|
+
* provide: DATABASE,
|
|
109
|
+
* useFactory: (config: ConfigService) => createDb(config.dbUrl),
|
|
110
|
+
* inject: [ConfigService]
|
|
111
|
+
* });
|
|
112
|
+
*
|
|
113
|
+
* // Value provider
|
|
114
|
+
* container.register({
|
|
115
|
+
* provide: CONFIG,
|
|
116
|
+
* useValue: { port: 3210, debug: true }
|
|
117
|
+
* });
|
|
118
|
+
*
|
|
119
|
+
* // Existing/alias provider
|
|
120
|
+
* container.register({
|
|
121
|
+
* provide: LOGGER,
|
|
122
|
+
* useExisting: ConsoleLogger
|
|
123
|
+
* });
|
|
124
|
+
* ```
|
|
125
|
+
*/
|
|
126
|
+
register(provider) {
|
|
127
|
+
validateProvider(provider);
|
|
128
|
+
const normalized = normalizeProvider(provider);
|
|
129
|
+
this.providers.set(provider.provide, normalized);
|
|
130
|
+
return this;
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Registers multiple providers at once
|
|
134
|
+
*
|
|
135
|
+
* @param providers - Array of provider configurations
|
|
136
|
+
* @returns The container (for chaining)
|
|
137
|
+
*
|
|
138
|
+
* @example
|
|
139
|
+
* ```typescript
|
|
140
|
+
* container.registerMany([
|
|
141
|
+
* { provide: UserService, useClass: UserService },
|
|
142
|
+
* { provide: PostService, useClass: PostService },
|
|
143
|
+
* { provide: CONFIG, useValue: appConfig }
|
|
144
|
+
* ]);
|
|
145
|
+
* ```
|
|
146
|
+
*/
|
|
147
|
+
registerMany(providers) {
|
|
148
|
+
for (const provider of providers) {
|
|
149
|
+
this.register(provider);
|
|
150
|
+
}
|
|
151
|
+
return this;
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Checks if a token is registered
|
|
155
|
+
*
|
|
156
|
+
* @param token - The token to check
|
|
157
|
+
* @returns true if the token is registered
|
|
158
|
+
*/
|
|
159
|
+
isRegistered(token) {
|
|
160
|
+
return this.providers.has(token) || (this.parent?.isRegistered(token) ?? false);
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Gets the provider for a token (without resolving)
|
|
164
|
+
*
|
|
165
|
+
* @param token - The token to get the provider for
|
|
166
|
+
* @returns The normalized provider or undefined
|
|
167
|
+
*/
|
|
168
|
+
getProvider(token) {
|
|
169
|
+
const local = this.providers.get(token);
|
|
170
|
+
if (local) {
|
|
171
|
+
return local;
|
|
172
|
+
}
|
|
173
|
+
return this.parent?.getProvider(token);
|
|
174
|
+
}
|
|
175
|
+
// ==========================================================================
|
|
176
|
+
// Resolution
|
|
177
|
+
// ==========================================================================
|
|
178
|
+
/**
|
|
179
|
+
* Resolves a service from the container
|
|
180
|
+
*
|
|
181
|
+
* @param token - The token to resolve
|
|
182
|
+
* @param context - Optional resolution context (for request scope)
|
|
183
|
+
* @returns The resolved service instance
|
|
184
|
+
* @throws {VeloxError} If the service cannot be resolved
|
|
185
|
+
*
|
|
186
|
+
* @example
|
|
187
|
+
* ```typescript
|
|
188
|
+
* // Basic resolution
|
|
189
|
+
* const userService = container.resolve(UserService);
|
|
190
|
+
*
|
|
191
|
+
* // With request context (for request-scoped services)
|
|
192
|
+
* const userContext = container.resolve(UserContext, { request });
|
|
193
|
+
* ```
|
|
194
|
+
*/
|
|
195
|
+
resolve(token, context) {
|
|
196
|
+
validateToken(token);
|
|
197
|
+
// Check for circular dependencies
|
|
198
|
+
if (this.resolutionStack.has(token)) {
|
|
199
|
+
const stack = [...this.resolutionStack].map((t) => getTokenName(t));
|
|
200
|
+
const current = getTokenName(token);
|
|
201
|
+
throw new VeloxError(`Circular dependency detected: ${[...stack, current].join(' -> ')}`, 500, 'CIRCULAR_DEPENDENCY');
|
|
202
|
+
}
|
|
203
|
+
// Get provider (local or from parent)
|
|
204
|
+
let provider = this.getProvider(token);
|
|
205
|
+
// Handle unregistered tokens
|
|
206
|
+
if (!provider) {
|
|
207
|
+
// Try auto-registration if enabled and token is a class
|
|
208
|
+
if (this.autoRegister && isClassToken(token)) {
|
|
209
|
+
provider = this.tryAutoRegister(token);
|
|
210
|
+
}
|
|
211
|
+
if (!provider) {
|
|
212
|
+
throw new VeloxError(`No provider found for: ${getTokenName(token)}`, 500, 'SERVICE_NOT_FOUND');
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
// Resolve based on scope
|
|
216
|
+
return this.resolveWithScope(token, provider, context);
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Resolves a service, returning undefined if not found
|
|
220
|
+
*
|
|
221
|
+
* @param token - The token to resolve
|
|
222
|
+
* @param context - Optional resolution context
|
|
223
|
+
* @returns The resolved service or undefined
|
|
224
|
+
*/
|
|
225
|
+
resolveOptional(token, context) {
|
|
226
|
+
try {
|
|
227
|
+
return this.resolve(token, context);
|
|
228
|
+
}
|
|
229
|
+
catch (error) {
|
|
230
|
+
if (error instanceof VeloxError && error.code === 'SERVICE_NOT_FOUND') {
|
|
231
|
+
return undefined;
|
|
232
|
+
}
|
|
233
|
+
throw error;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* Resolves all services registered for a token
|
|
238
|
+
*
|
|
239
|
+
* Useful for multi-injection patterns where multiple implementations
|
|
240
|
+
* are registered for the same token.
|
|
241
|
+
*
|
|
242
|
+
* @param token - The token to resolve
|
|
243
|
+
* @param context - Optional resolution context
|
|
244
|
+
* @returns Array of resolved service instances
|
|
245
|
+
*
|
|
246
|
+
* @example
|
|
247
|
+
* ```typescript
|
|
248
|
+
* // Register multiple validators
|
|
249
|
+
* container.register({ provide: VALIDATOR, useClass: EmailValidator });
|
|
250
|
+
* container.register({ provide: VALIDATOR, useClass: PhoneValidator });
|
|
251
|
+
*
|
|
252
|
+
* // Resolve all validators
|
|
253
|
+
* const validators = container.resolveAll(VALIDATOR);
|
|
254
|
+
* ```
|
|
255
|
+
*
|
|
256
|
+
* Note: Currently returns single instance. Multi-injection to be
|
|
257
|
+
* implemented in v1.1 with a separate multi-provider registration API.
|
|
258
|
+
*/
|
|
259
|
+
resolveAll(token, context) {
|
|
260
|
+
const instance = this.resolveOptional(token, context);
|
|
261
|
+
return instance !== undefined ? [instance] : [];
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* Resolves with scope management
|
|
265
|
+
*
|
|
266
|
+
* @internal
|
|
267
|
+
*/
|
|
268
|
+
resolveWithScope(token, provider, context) {
|
|
269
|
+
switch (provider.scope) {
|
|
270
|
+
case Scope.SINGLETON: {
|
|
271
|
+
// Check cache first
|
|
272
|
+
if (this.scopeManager.hasSingleton(token)) {
|
|
273
|
+
return this.scopeManager.getSingleton(token);
|
|
274
|
+
}
|
|
275
|
+
// Create and cache
|
|
276
|
+
const instance = this.createInstance(token, provider, context);
|
|
277
|
+
this.scopeManager.setSingleton(token, instance);
|
|
278
|
+
return instance;
|
|
279
|
+
}
|
|
280
|
+
case Scope.TRANSIENT: {
|
|
281
|
+
// Always create new instance
|
|
282
|
+
return this.createInstance(token, provider, context);
|
|
283
|
+
}
|
|
284
|
+
case Scope.REQUEST: {
|
|
285
|
+
// Validate and get request context
|
|
286
|
+
const request = this.scopeManager.ensureRequestScope(context?.request);
|
|
287
|
+
// Check request cache first
|
|
288
|
+
if (this.scopeManager.hasRequestScoped(token, request)) {
|
|
289
|
+
return this.scopeManager.getRequestScoped(token, request);
|
|
290
|
+
}
|
|
291
|
+
// Create and cache in request scope
|
|
292
|
+
const instance = this.createInstance(token, provider, context);
|
|
293
|
+
this.scopeManager.setRequestScoped(token, instance, request);
|
|
294
|
+
return instance;
|
|
295
|
+
}
|
|
296
|
+
default: {
|
|
297
|
+
throw new VeloxError(`Unknown scope: ${provider.scope}`, 500, 'SCOPE_MISMATCH');
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
/**
|
|
302
|
+
* Creates an instance based on provider type
|
|
303
|
+
*
|
|
304
|
+
* @internal
|
|
305
|
+
*/
|
|
306
|
+
createInstance(token, provider, context) {
|
|
307
|
+
// Track resolution for circular dependency detection
|
|
308
|
+
this.resolutionStack.add(token);
|
|
309
|
+
try {
|
|
310
|
+
switch (provider.type) {
|
|
311
|
+
case 'class':
|
|
312
|
+
return this.instantiateClass(provider.implementation.class, context);
|
|
313
|
+
case 'factory':
|
|
314
|
+
return this.invokeFactory(provider, context);
|
|
315
|
+
case 'value':
|
|
316
|
+
return provider.implementation.value;
|
|
317
|
+
case 'existing':
|
|
318
|
+
return this.resolve(provider.implementation.existing, context);
|
|
319
|
+
default:
|
|
320
|
+
throw new VeloxError(`Unknown provider type: ${provider.type}`, 500, 'INVALID_PROVIDER');
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
finally {
|
|
324
|
+
this.resolutionStack.delete(token);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
/**
|
|
328
|
+
* Instantiates a class with automatic dependency injection
|
|
329
|
+
*
|
|
330
|
+
* @internal
|
|
331
|
+
*/
|
|
332
|
+
instantiateClass(cls, context) {
|
|
333
|
+
// Get constructor dependency tokens
|
|
334
|
+
const tokens = getConstructorTokens(cls);
|
|
335
|
+
const optionalParams = getOptionalParams(cls);
|
|
336
|
+
// Resolve all dependencies
|
|
337
|
+
const dependencies = [];
|
|
338
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
339
|
+
const token = tokens[i];
|
|
340
|
+
const isOptional = optionalParams.has(i);
|
|
341
|
+
try {
|
|
342
|
+
// Handle Object type (unresolved interface)
|
|
343
|
+
if (token === Object) {
|
|
344
|
+
if (isOptional) {
|
|
345
|
+
dependencies.push(undefined);
|
|
346
|
+
continue;
|
|
347
|
+
}
|
|
348
|
+
throw new VeloxError(`Cannot resolve dependency at index ${i} for ${cls.name}: ` +
|
|
349
|
+
'Type resolved to Object. Use @Inject() decorator for interfaces.', 500, 'MISSING_INJECTABLE_DECORATOR');
|
|
350
|
+
}
|
|
351
|
+
const dependency = isOptional
|
|
352
|
+
? this.resolveOptional(token, context)
|
|
353
|
+
: this.resolve(token, context);
|
|
354
|
+
dependencies.push(dependency);
|
|
355
|
+
}
|
|
356
|
+
catch (error) {
|
|
357
|
+
if (isOptional && error instanceof VeloxError && error.code === 'SERVICE_NOT_FOUND') {
|
|
358
|
+
dependencies.push(undefined);
|
|
359
|
+
}
|
|
360
|
+
else {
|
|
361
|
+
throw error;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
// Instantiate the class
|
|
366
|
+
// Note: We need to spread the dependencies array into the constructor
|
|
367
|
+
// TypeScript doesn't know the exact number of parameters, so we use the
|
|
368
|
+
// constructor with a spread of unknown[]
|
|
369
|
+
return new cls(...dependencies);
|
|
370
|
+
}
|
|
371
|
+
/**
|
|
372
|
+
* Invokes a factory function with dependencies
|
|
373
|
+
*
|
|
374
|
+
* @internal
|
|
375
|
+
*/
|
|
376
|
+
invokeFactory(provider, context) {
|
|
377
|
+
const factory = provider.implementation.factory;
|
|
378
|
+
const injectTokens = provider.implementation.inject ?? [];
|
|
379
|
+
// Resolve factory dependencies
|
|
380
|
+
const dependencies = injectTokens.map((token) => this.resolve(token, context));
|
|
381
|
+
// Invoke factory
|
|
382
|
+
const result = factory(...dependencies);
|
|
383
|
+
// Handle async factories (should be avoided in sync resolution)
|
|
384
|
+
if (result instanceof Promise) {
|
|
385
|
+
throw new VeloxError('Async factory returned from sync resolve(). Use resolveAsync() for async factories.', 500, 'INVALID_PROVIDER');
|
|
386
|
+
}
|
|
387
|
+
return result;
|
|
388
|
+
}
|
|
389
|
+
/**
|
|
390
|
+
* Tries to auto-register a class if it's injectable
|
|
391
|
+
*
|
|
392
|
+
* @internal
|
|
393
|
+
*/
|
|
394
|
+
tryAutoRegister(cls) {
|
|
395
|
+
if (!isInjectable(cls)) {
|
|
396
|
+
return undefined;
|
|
397
|
+
}
|
|
398
|
+
const scope = getInjectableScope(cls);
|
|
399
|
+
const provider = {
|
|
400
|
+
provide: cls,
|
|
401
|
+
useClass: cls,
|
|
402
|
+
scope,
|
|
403
|
+
};
|
|
404
|
+
this.register(provider);
|
|
405
|
+
return this.getProvider(cls);
|
|
406
|
+
}
|
|
407
|
+
// ==========================================================================
|
|
408
|
+
// Async Resolution
|
|
409
|
+
// ==========================================================================
|
|
410
|
+
/**
|
|
411
|
+
* Resolves a service asynchronously
|
|
412
|
+
*
|
|
413
|
+
* Use this method when your providers include async factories.
|
|
414
|
+
*
|
|
415
|
+
* @param token - The token to resolve
|
|
416
|
+
* @param context - Optional resolution context
|
|
417
|
+
* @returns Promise resolving to the service instance
|
|
418
|
+
*
|
|
419
|
+
* @example
|
|
420
|
+
* ```typescript
|
|
421
|
+
* container.register({
|
|
422
|
+
* provide: DATABASE,
|
|
423
|
+
* useFactory: async (config) => {
|
|
424
|
+
* const client = createClient(config.dbUrl);
|
|
425
|
+
* await client.connect();
|
|
426
|
+
* return client;
|
|
427
|
+
* },
|
|
428
|
+
* inject: [ConfigService]
|
|
429
|
+
* });
|
|
430
|
+
*
|
|
431
|
+
* const db = await container.resolveAsync(DATABASE);
|
|
432
|
+
* ```
|
|
433
|
+
*/
|
|
434
|
+
async resolveAsync(token, context) {
|
|
435
|
+
validateToken(token);
|
|
436
|
+
// Check for circular dependencies
|
|
437
|
+
if (this.resolutionStack.has(token)) {
|
|
438
|
+
const stack = [...this.resolutionStack].map((t) => getTokenName(t));
|
|
439
|
+
const current = getTokenName(token);
|
|
440
|
+
throw new VeloxError(`Circular dependency detected: ${[...stack, current].join(' -> ')}`, 500, 'CIRCULAR_DEPENDENCY');
|
|
441
|
+
}
|
|
442
|
+
// Get provider
|
|
443
|
+
let provider = this.getProvider(token);
|
|
444
|
+
if (!provider) {
|
|
445
|
+
if (this.autoRegister && isClassToken(token)) {
|
|
446
|
+
provider = this.tryAutoRegister(token);
|
|
447
|
+
}
|
|
448
|
+
if (!provider) {
|
|
449
|
+
throw new VeloxError(`No provider found for: ${getTokenName(token)}`, 500, 'SERVICE_NOT_FOUND');
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
return this.resolveWithScopeAsync(token, provider, context);
|
|
453
|
+
}
|
|
454
|
+
/**
|
|
455
|
+
* Async scope resolution
|
|
456
|
+
*
|
|
457
|
+
* @internal
|
|
458
|
+
*/
|
|
459
|
+
async resolveWithScopeAsync(token, provider, context) {
|
|
460
|
+
switch (provider.scope) {
|
|
461
|
+
case Scope.SINGLETON: {
|
|
462
|
+
if (this.scopeManager.hasSingleton(token)) {
|
|
463
|
+
return this.scopeManager.getSingleton(token);
|
|
464
|
+
}
|
|
465
|
+
const instance = await this.createInstanceAsync(token, provider, context);
|
|
466
|
+
this.scopeManager.setSingleton(token, instance);
|
|
467
|
+
return instance;
|
|
468
|
+
}
|
|
469
|
+
case Scope.TRANSIENT: {
|
|
470
|
+
return this.createInstanceAsync(token, provider, context);
|
|
471
|
+
}
|
|
472
|
+
case Scope.REQUEST: {
|
|
473
|
+
// Validate and get request context
|
|
474
|
+
const request = this.scopeManager.ensureRequestScope(context?.request);
|
|
475
|
+
if (this.scopeManager.hasRequestScoped(token, request)) {
|
|
476
|
+
return this.scopeManager.getRequestScoped(token, request);
|
|
477
|
+
}
|
|
478
|
+
const instance = await this.createInstanceAsync(token, provider, context);
|
|
479
|
+
this.scopeManager.setRequestScoped(token, instance, request);
|
|
480
|
+
return instance;
|
|
481
|
+
}
|
|
482
|
+
default: {
|
|
483
|
+
throw new VeloxError(`Unknown scope: ${provider.scope}`, 500, 'SCOPE_MISMATCH');
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
/**
|
|
488
|
+
* Async instance creation
|
|
489
|
+
*
|
|
490
|
+
* @internal
|
|
491
|
+
*/
|
|
492
|
+
async createInstanceAsync(token, provider, context) {
|
|
493
|
+
this.resolutionStack.add(token);
|
|
494
|
+
try {
|
|
495
|
+
switch (provider.type) {
|
|
496
|
+
case 'class':
|
|
497
|
+
return await this.instantiateClassAsync(provider.implementation.class, context);
|
|
498
|
+
case 'factory':
|
|
499
|
+
return await this.invokeFactoryAsync(provider, context);
|
|
500
|
+
case 'value':
|
|
501
|
+
return provider.implementation.value;
|
|
502
|
+
case 'existing':
|
|
503
|
+
return await this.resolveAsync(provider.implementation.existing, context);
|
|
504
|
+
default:
|
|
505
|
+
throw new VeloxError(`Unknown provider type: ${provider.type}`, 500, 'INVALID_PROVIDER');
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
finally {
|
|
509
|
+
this.resolutionStack.delete(token);
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
/**
|
|
513
|
+
* Async class instantiation
|
|
514
|
+
*
|
|
515
|
+
* @internal
|
|
516
|
+
*/
|
|
517
|
+
async instantiateClassAsync(cls, context) {
|
|
518
|
+
const tokens = getConstructorTokens(cls);
|
|
519
|
+
const optionalParams = getOptionalParams(cls);
|
|
520
|
+
const dependencies = [];
|
|
521
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
522
|
+
const token = tokens[i];
|
|
523
|
+
const isOptional = optionalParams.has(i);
|
|
524
|
+
try {
|
|
525
|
+
if (token === Object) {
|
|
526
|
+
if (isOptional) {
|
|
527
|
+
dependencies.push(undefined);
|
|
528
|
+
continue;
|
|
529
|
+
}
|
|
530
|
+
throw new VeloxError(`Cannot resolve dependency at index ${i} for ${cls.name}: ` +
|
|
531
|
+
'Type resolved to Object. Use @Inject() decorator for interfaces.', 500, 'MISSING_INJECTABLE_DECORATOR');
|
|
532
|
+
}
|
|
533
|
+
const dependency = isOptional
|
|
534
|
+
? await this.resolveAsync(token, context).catch(() => undefined)
|
|
535
|
+
: await this.resolveAsync(token, context);
|
|
536
|
+
dependencies.push(dependency);
|
|
537
|
+
}
|
|
538
|
+
catch (error) {
|
|
539
|
+
if (isOptional) {
|
|
540
|
+
dependencies.push(undefined);
|
|
541
|
+
}
|
|
542
|
+
else {
|
|
543
|
+
throw error;
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
return new cls(...dependencies);
|
|
548
|
+
}
|
|
549
|
+
/**
|
|
550
|
+
* Async factory invocation
|
|
551
|
+
*
|
|
552
|
+
* @internal
|
|
553
|
+
*/
|
|
554
|
+
async invokeFactoryAsync(provider, context) {
|
|
555
|
+
const factory = provider.implementation.factory;
|
|
556
|
+
const injectTokens = provider.implementation.inject ?? [];
|
|
557
|
+
const dependencies = await Promise.all(injectTokens.map((token) => this.resolveAsync(token, context)));
|
|
558
|
+
const result = factory(...dependencies);
|
|
559
|
+
return result instanceof Promise ? result : result;
|
|
560
|
+
}
|
|
561
|
+
// ==========================================================================
|
|
562
|
+
// Fastify Integration
|
|
563
|
+
// ==========================================================================
|
|
564
|
+
/**
|
|
565
|
+
* Attaches the container to a Fastify server
|
|
566
|
+
*
|
|
567
|
+
* Sets up the request lifecycle hooks needed for request-scoped services.
|
|
568
|
+
* Must be called before resolving request-scoped services.
|
|
569
|
+
*
|
|
570
|
+
* @param server - Fastify server instance
|
|
571
|
+
* @returns The container (for chaining)
|
|
572
|
+
*
|
|
573
|
+
* @example
|
|
574
|
+
* ```typescript
|
|
575
|
+
* const app = await createVeloxApp();
|
|
576
|
+
* container.attachToFastify(app.server);
|
|
577
|
+
* ```
|
|
578
|
+
*/
|
|
579
|
+
attachToFastify(server) {
|
|
580
|
+
this.scopeManager.attachToFastify(server);
|
|
581
|
+
return this;
|
|
582
|
+
}
|
|
583
|
+
/**
|
|
584
|
+
* Creates a resolution context from a Fastify request
|
|
585
|
+
*
|
|
586
|
+
* @param request - The Fastify request
|
|
587
|
+
* @returns Resolution context for request-scoped services
|
|
588
|
+
*/
|
|
589
|
+
static createContext(request) {
|
|
590
|
+
return { request };
|
|
591
|
+
}
|
|
592
|
+
// ==========================================================================
|
|
593
|
+
// Container Management
|
|
594
|
+
// ==========================================================================
|
|
595
|
+
/**
|
|
596
|
+
* Creates a child container
|
|
597
|
+
*
|
|
598
|
+
* Child containers inherit from this container but can override registrations.
|
|
599
|
+
* Useful for testing or creating scoped containers.
|
|
600
|
+
*
|
|
601
|
+
* @param options - Options for the child container
|
|
602
|
+
* @returns New child container
|
|
603
|
+
*
|
|
604
|
+
* @example
|
|
605
|
+
* ```typescript
|
|
606
|
+
* const childContainer = container.createChild();
|
|
607
|
+
*
|
|
608
|
+
* // Override a service for testing
|
|
609
|
+
* childContainer.register({
|
|
610
|
+
* provide: UserRepository,
|
|
611
|
+
* useClass: MockUserRepository
|
|
612
|
+
* });
|
|
613
|
+
* ```
|
|
614
|
+
*/
|
|
615
|
+
createChild(options = {}) {
|
|
616
|
+
return new Container({ ...options, parent: this });
|
|
617
|
+
}
|
|
618
|
+
/**
|
|
619
|
+
* Clears all singleton instances
|
|
620
|
+
*
|
|
621
|
+
* Useful for testing or application shutdown.
|
|
622
|
+
* Does not clear registrations.
|
|
623
|
+
*/
|
|
624
|
+
clearInstances() {
|
|
625
|
+
this.scopeManager.clearSingletons();
|
|
626
|
+
}
|
|
627
|
+
/**
|
|
628
|
+
* Clears all registrations and instances
|
|
629
|
+
*
|
|
630
|
+
* @internal
|
|
631
|
+
*/
|
|
632
|
+
reset() {
|
|
633
|
+
this.providers.clear();
|
|
634
|
+
this.scopeManager.reset();
|
|
635
|
+
this.resolutionStack.clear();
|
|
636
|
+
}
|
|
637
|
+
/**
|
|
638
|
+
* Gets debug information about the container
|
|
639
|
+
*
|
|
640
|
+
* @returns Object with container statistics and registered providers
|
|
641
|
+
*/
|
|
642
|
+
getDebugInfo() {
|
|
643
|
+
return {
|
|
644
|
+
providerCount: this.providers.size,
|
|
645
|
+
providers: [...this.providers.values()].map((p) => {
|
|
646
|
+
const tokenName = getTokenName(p.provide);
|
|
647
|
+
return `${p.type}(${tokenName}, ${p.scope})`;
|
|
648
|
+
}),
|
|
649
|
+
hasParent: this.parent !== undefined,
|
|
650
|
+
autoRegister: this.autoRegister,
|
|
651
|
+
};
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
// ============================================================================
|
|
655
|
+
// Global Container
|
|
656
|
+
// ============================================================================
|
|
657
|
+
/**
|
|
658
|
+
* Default global container instance
|
|
659
|
+
*
|
|
660
|
+
* For convenience, VeloxTS provides a default container.
|
|
661
|
+
* You can also create your own containers for testing or isolation.
|
|
662
|
+
*
|
|
663
|
+
* @example
|
|
664
|
+
* ```typescript
|
|
665
|
+
* import { container } from '@veloxts/core';
|
|
666
|
+
*
|
|
667
|
+
* container.register({
|
|
668
|
+
* provide: UserService,
|
|
669
|
+
* useClass: UserService
|
|
670
|
+
* });
|
|
671
|
+
*
|
|
672
|
+
* const userService = container.resolve(UserService);
|
|
673
|
+
* ```
|
|
674
|
+
*/
|
|
675
|
+
export const container = new Container();
|
|
676
|
+
// ============================================================================
|
|
677
|
+
// Container Factory
|
|
678
|
+
// ============================================================================
|
|
679
|
+
/**
|
|
680
|
+
* Creates a new DI container
|
|
681
|
+
*
|
|
682
|
+
* @param options - Container configuration options
|
|
683
|
+
* @returns New container instance
|
|
684
|
+
*
|
|
685
|
+
* @example
|
|
686
|
+
* ```typescript
|
|
687
|
+
* const appContainer = createContainer({ autoRegister: true });
|
|
688
|
+
* ```
|
|
689
|
+
*/
|
|
690
|
+
export function createContainer(options) {
|
|
691
|
+
return new Container(options);
|
|
692
|
+
}
|
|
693
|
+
//# sourceMappingURL=container.js.map
|