@veloxts/core 0.3.2 → 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.
@@ -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