@di-framework/di-framework 2.0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Geoff Seemueller
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,472 @@
1
+ # di-framework
2
+
3
+ [![CI](https://github.com/geoffsee/di-framework/actions/workflows/ci.yml/badge.svg)](https://github.com/geoffsee/di-framework/actions/workflows/ci.yml)
4
+ [![npm version](https://img.shields.io/npm/v/di-framework.svg)](https://www.npmjs.com/package/di-framework)
5
+ [![license](https://img.shields.io/github/license/geoffsee/di-framework.svg)](LICENSE)
6
+
7
+ A lightweight, type-safe Dependency Injection framework for TypeScript using decorators. This framework automatically manages service instantiation, dependency resolution, and lifecycle management.
8
+
9
+
10
+ ## Installation
11
+
12
+ No external dependencies required! The framework works with SWC and TypeScript's native decorator support.
13
+
14
+ Just ensure you have:
15
+ - TypeScript 5.0+
16
+ - SWC or TypeScript compiler with `experimentalDecorators` and `emitDecoratorMetadata` enabled
17
+
18
+ The decorators are fully integrated with SWC's native support - no need for `reflect-metadata` or any other polyfill.
19
+
20
+ ## Quick Start
21
+
22
+ ### 1. Basic Service
23
+
24
+ ```typescript
25
+ import { Container } from 'di-framework/decorators';
26
+
27
+ @Container()
28
+ export class DatabaseService {
29
+ connect(): void {
30
+ console.log('Connected to database');
31
+ }
32
+ }
33
+ ```
34
+
35
+ ### 2. Service with Dependencies
36
+
37
+ ```typescript
38
+ import { Container, Component } from 'di-framework/decorators';
39
+ import { DatabaseService } from './services/DatabaseService';
40
+
41
+ @Container()
42
+ export class UserService {
43
+ @Component(DatabaseService)
44
+ private db!: DatabaseService;
45
+
46
+ constructor() {}
47
+
48
+ getUser(id: string) {
49
+ return this.db.query(`SELECT * FROM users WHERE id = '${id}'`);
50
+ }
51
+ }
52
+ ```
53
+
54
+ Note: Property injection is used for all dependencies. This works seamlessly with SWC and TypeScript's native decorator support.
55
+
56
+ ### 3. Resolve Services
57
+
58
+ ```typescript
59
+ import { useContainer } from 'di-framework/container';
60
+ import { UserService } from './services/UserService';
61
+
62
+ const container = useContainer();
63
+ const userService = container.resolve<UserService>(UserService);
64
+
65
+ // All dependencies are automatically injected!
66
+ userService.getUser('123');
67
+ ```
68
+
69
+ ## API Reference
70
+
71
+ ### `@Container(options?)`
72
+
73
+ Marks a class as injectable and automatically registers it with the DI container.
74
+
75
+ **Options:**
76
+ - `singleton?: boolean` (default: `true`) - Create a new instance each time or reuse the same instance
77
+ - `container?: DIContainer` - Specify a custom container (defaults to global container)
78
+ - Note: Import as `import { Container as DIContainer } from 'di-framework/container'` to avoid name collision with the `@Container` decorator.
79
+
80
+ **Example:**
81
+ ```typescript
82
+ @Container({ singleton: false })
83
+ export class RequestScopedService {
84
+ // New instance created for each resolution
85
+ }
86
+ ```
87
+
88
+ ### `@Component(target)`
89
+
90
+ Marks a constructor parameter or property for dependency injection.
91
+
92
+ **Parameters:**
93
+ - `target` - The class to inject or a string identifier for factory-registered services
94
+
95
+ **Example - Constructor Parameter:**
96
+ ```typescript
97
+ @Container()
98
+ export class OrderService {
99
+ constructor(@Component(DatabaseService) private db: DatabaseService) {}
100
+ }
101
+ ```
102
+
103
+ **Example - Property Injection:**
104
+ ```typescript
105
+ @Container()
106
+ export class ReportService {
107
+ @Component(DatabaseService)
108
+ private db: DatabaseService;
109
+ }
110
+ ```
111
+
112
+ ### `@Telemetry(options?)`
113
+
114
+ Marks a method for telemetry tracking. When called, it emits a `telemetry` event on the container. Works with both synchronous and asynchronous methods.
115
+
116
+ **Options:**
117
+ - `logging?: boolean` (default: `false`) - If true, logs the method execution details (status and duration) to the console.
118
+
119
+ **Example:**
120
+ ```typescript
121
+ @Container()
122
+ export class ApiService {
123
+ @Telemetry({ logging: true })
124
+ async fetchData(id: string) {
125
+ // ...
126
+ }
127
+ }
128
+ ```
129
+
130
+ ### `@TelemetryListener()`
131
+
132
+ Marks a method as a listener for telemetry events. The method will be automatically registered to the container's `telemetry` event when the service is instantiated.
133
+
134
+ **Example:**
135
+ ```typescript
136
+ @Container()
137
+ export class MonitoringService {
138
+ @TelemetryListener()
139
+ onTelemetry(event: any) {
140
+ console.log(`Method ${event.className}.${event.methodName} took ${event.endTime - event.startTime}ms`);
141
+ }
142
+ }
143
+ ```
144
+
145
+ ### `useContainer()`
146
+
147
+ Returns the global DI container instance.
148
+
149
+ ```typescript
150
+ import { useContainer } from 'di-framework/container';
151
+
152
+ const container = useContainer();
153
+ ```
154
+
155
+ ### `container.register(serviceClass, options?)`
156
+
157
+ Manually register a service class.
158
+
159
+ ```typescript
160
+ container.register(UserService, { singleton: true });
161
+ ```
162
+
163
+ ### `container.registerFactory(name, factory, options?)`
164
+
165
+ Register a service using a factory function.
166
+
167
+ ```typescript
168
+ container.registerFactory('config', () => ({
169
+ apiKey: process.env.API_KEY,
170
+ dbUrl: process.env.DATABASE_URL
171
+ }), { singleton: true });
172
+ ```
173
+
174
+ ### `container.resolve(serviceClass)`
175
+
176
+ Resolve and get an instance of a service.
177
+
178
+ ```typescript
179
+ const userService = container.resolve<UserService>(UserService);
180
+ // or by name
181
+ const config = container.resolve('config');
182
+ ```
183
+
184
+ ### `container.has(serviceClass)`
185
+
186
+ Check if a service is registered.
187
+
188
+ ```typescript
189
+ if (container.has(UserService)) {
190
+ const service = container.resolve(UserService);
191
+ }
192
+ ```
193
+
194
+ ### `container.getServiceNames()`
195
+
196
+ Get all registered service names.
197
+
198
+ ```typescript
199
+ const names = container.getServiceNames();
200
+ console.log(names); // ['DatabaseService', 'UserService', ...]
201
+ ```
202
+
203
+ ### `container.on(event, listener)`
204
+
205
+ Subscribe to DI container lifecycle events (observer pattern).
206
+
207
+ **Events:**
208
+ - `registered` - fired when a class or factory is registered
209
+ - `resolved` - fired whenever a service is resolved (cached or fresh)
210
+ - `constructed` - fired when `construct()` creates a new instance
211
+ - `cleared` - fired when the container is cleared
212
+
213
+ **Example:**
214
+ ```typescript
215
+ const unsubscribe = container.on('resolved', ({ key, fromCache }) => {
216
+ console.log(`Resolved ${typeof key === 'string' ? key : key.name} (fromCache=${fromCache})`);
217
+ });
218
+
219
+ unsubscribe(); // stop listening
220
+ ```
221
+
222
+ ### `container.construct(serviceClass, overrides?)`
223
+
224
+ Create a fresh instance without registering it, while still honoring dependency injection. Useful for constructor-pattern scenarios where you need to supply specific primitives/config values.
225
+
226
+ ```typescript
227
+ import { Component } from 'di-framework/decorators';
228
+ import { LoggerService } from 'di-framework/services/LoggerService';
229
+
230
+ class Greeter {
231
+ constructor(@Component(LoggerService) private logger: LoggerService, private greeting: string) {}
232
+ }
233
+
234
+ const greeter = container.construct(Greeter, { 1: 'hello world' });
235
+ ```
236
+
237
+ ### `container.fork(options?)`
238
+
239
+ Clone the container registrations (prototype pattern) into a new container. Pass `{ carrySingletons: true }` to reuse existing singleton instances; default is to start with fresh instances.
240
+
241
+ ```typescript
242
+ const testContainer = container.fork({ carrySingletons: false });
243
+ ```
244
+
245
+ ## Advanced Examples
246
+
247
+ ### Multiple Dependencies
248
+
249
+ ```typescript
250
+ @Container()
251
+ export class ApplicationContext {
252
+ constructor(
253
+ @Component(DatabaseService) private db: DatabaseService,
254
+ @Component(LoggerService) private logger: LoggerService,
255
+ @Component(AuthService) private auth: AuthService
256
+ ) {}
257
+
258
+ async initialize() {
259
+ this.logger.log('Initializing application...');
260
+ await this.db.connect();
261
+ this.auth.setup();
262
+ }
263
+ }
264
+ ```
265
+
266
+ ### Transient (Non-Singleton) Services
267
+
268
+ ```typescript
269
+ @Container({ singleton: false })
270
+ export class RequestContext {
271
+ id = Math.random().toString();
272
+
273
+ constructor(@Component(LoggerService) private logger: LoggerService) {
274
+ this.logger.log(`Request context created: ${this.id}`);
275
+ }
276
+ }
277
+
278
+ // Each resolve creates a new instance
279
+ const ctx1 = container.resolve(RequestContext); // new instance
280
+ const ctx2 = container.resolve(RequestContext); // different instance
281
+ ```
282
+
283
+ ### Lifecycle Methods
284
+
285
+ Services can optionally implement lifecycle methods:
286
+
287
+ ```typescript
288
+ @Container()
289
+ export class DatabaseService {
290
+ private connected = false;
291
+
292
+ setEnv(env: Record<string, any>) {
293
+ // Called to initialize environment-specific config
294
+ console.log('DB URL:', env.DATABASE_URL);
295
+ }
296
+
297
+ setCtx(context: any) {
298
+ // Called to set execution context
299
+ console.log('Context:', context);
300
+ }
301
+
302
+ connect() {
303
+ this.connected = true;
304
+ }
305
+ }
306
+
307
+ // Calling lifecycle methods
308
+ const db = container.resolve(DatabaseService);
309
+ db.setEnv(process.env);
310
+ db.setCtx({ userId: '123' });
311
+ db.connect();
312
+ ```
313
+
314
+ ### Factory Functions
315
+
316
+ ```typescript
317
+ container.registerFactory('apiClient', () => {
318
+ return new HttpClient({
319
+ baseUrl: process.env.API_URL,
320
+ timeout: 5000
321
+ });
322
+ }, { singleton: true });
323
+
324
+ // Use in services
325
+ @Container()
326
+ export class UserService {
327
+ constructor(@Component('apiClient') private api: any) {}
328
+ }
329
+ ```
330
+
331
+ ## How It Works
332
+
333
+ 1. **Decoration**: When you decorate a class with `@Container()`, the decorator registers it with the global container
334
+ 2. **Registration**: The class is stored in the container with metadata about its dependencies
335
+ 3. **Resolution**: When you call `container.resolve(ServiceClass)`:
336
+ - The container creates a new instance (or returns existing singleton)
337
+ - It examines the constructor parameters and their types
338
+ - It recursively resolves each dependency
339
+ - Dependencies are injected into the constructor
340
+ - The configured instance is returned
341
+ 4. **Caching**: Singleton instances are cached and reused
342
+
343
+ ## Comparison with SAMPLE.ts
344
+
345
+ ### Before (Manual - SAMPLE.ts)
346
+ ```typescript
347
+ const createServerContext = (env, ctx) => {
348
+ if(!instanceState.member) {
349
+ const contextInstance = Context.create({
350
+ contactService: ContactService.create({}),
351
+ assetService: AssetService.create({}),
352
+ transactionService: TransactionService.create({}),
353
+ // ... 20+ more services manually created and wired
354
+ chatService: ChatService.create({
355
+ openAIApiKey: env.OPENAI_API_KEY,
356
+ // ... manual configuration
357
+ }),
358
+ });
359
+ instanceState.member = contextInstance;
360
+ }
361
+
362
+ instanceState.member.setEnv(env);
363
+ instanceState.member.setCtx(ctx);
364
+ // ... manual dependency wiring
365
+ instanceState.member.knowledgeService.setAttachmentService(
366
+ instanceState.member.attachmentService
367
+ );
368
+ return instanceState.member;
369
+ };
370
+ ```
371
+
372
+ ### After (DI Framework)
373
+ ```typescript
374
+ @Container()
375
+ export class ApplicationContext {
376
+ constructor(
377
+ @Component(ContactService) private contactService: ContactService,
378
+ @Component(AssetService) private assetService: AssetService,
379
+ @Component(TransactionService) private transactionService: TransactionService,
380
+ // ... all services automatically injected
381
+ @Component(ChatService) private chatService: ChatService,
382
+ ) {}
383
+
384
+ setEnv(env: Record<string, any>) {
385
+ // Services are already available via constructor injection
386
+ this.chatService.initialize(env.OPENAI_API_KEY);
387
+ }
388
+
389
+ setCtx(ctx: any) {
390
+ // All services have access to context
391
+ }
392
+ }
393
+
394
+ // Usage
395
+ const container = useContainer();
396
+ const appContext = container.resolve(ApplicationContext);
397
+ appContext.setEnv(env);
398
+ appContext.setCtx(ctx);
399
+ ```
400
+
401
+ **Benefits:**
402
+ - No manual service instantiation
403
+ - No manual dependency wiring
404
+ - Automatic singleton management
405
+ - Type-safe dependency resolution
406
+ - Easier to test (mock services simply by registering test implementations)
407
+ - Scales better as services grow
408
+
409
+ ## Error Handling
410
+
411
+ ### Circular Dependencies
412
+ ```typescript
413
+ // This will be detected and throw an error:
414
+ @Container()
415
+ class ServiceA {
416
+ constructor(@Component(ServiceB) private b: ServiceB) {}
417
+ }
418
+
419
+ @Container()
420
+ class ServiceB {
421
+ constructor(@Component(ServiceA) private a: ServiceA) {}
422
+ }
423
+
424
+ // Error: Circular dependency detected while resolving ServiceA
425
+ ```
426
+
427
+ ### Unregistered Services
428
+ ```typescript
429
+ @Container()
430
+ class MyService {
431
+ constructor(@Component(UnregisteredService) private s: UnregisteredService) {}
432
+ }
433
+
434
+ // Error: Service 'UnregisteredService' is not registered in the DI container
435
+ ```
436
+
437
+ ## Best Practices
438
+
439
+ 1. **Mark all services with `@Container()`** - Makes dependency management explicit
440
+ 2. **Use constructor injection** - Preferred over property injection for mandatory dependencies
441
+ 3. **Use property injection for optional dependencies** - Keep it minimal
442
+ 4. **No need to import reflect-metadata** - This framework uses a lightweight metadata store
443
+ 5. **Separate service interfaces from implementations** - For easier testing
444
+ 6. **Use singletons for stateless services** - Most services should be singletons
445
+ 7. **Use transient (non-singleton) for stateful services** - Request/session scoped services
446
+
447
+ ## Testing
448
+
449
+ ```typescript
450
+ // Create a test container
451
+ import { Container as DIContainer } from 'di-framework/container';
452
+
453
+ const testContainer = new DIContainer();
454
+
455
+ // Register mock implementations
456
+ class MockDatabaseService {
457
+ query() { return { mock: true }; }
458
+ }
459
+
460
+ testContainer.register(MockDatabaseService);
461
+
462
+ // Register dependencies
463
+ testContainer.register(UserService);
464
+
465
+ // Test the service with mocked dependencies
466
+ const userService = testContainer.resolve(UserService);
467
+ expect(userService.getUser('1')).toEqual({ mock: true });
468
+ ```
469
+
470
+ ## License
471
+
472
+ MIT
@@ -0,0 +1,136 @@
1
+ /**
2
+ * Dependency Injection Container
3
+ *
4
+ * Manages service registration, dependency resolution, and instance lifecycle.
5
+ * Supports singleton pattern and automatic dependency injection via decorators.
6
+ * Works with SWC and TypeScript's native decorator support.
7
+ */
8
+ type Constructor<T = any> = new (...args: any[]) => T;
9
+ type ServiceFactory<T = any> = () => T;
10
+ type ContainerEventPayloads = {
11
+ registered: {
12
+ key: string | Constructor;
13
+ singleton: boolean;
14
+ kind: 'class' | 'factory';
15
+ };
16
+ resolved: {
17
+ key: string | Constructor;
18
+ instance: any;
19
+ singleton: boolean;
20
+ fromCache: boolean;
21
+ };
22
+ constructed: {
23
+ key: Constructor;
24
+ instance: any;
25
+ overrides: Record<number, any>;
26
+ };
27
+ cleared: {
28
+ count: number;
29
+ };
30
+ telemetry: {
31
+ className: string;
32
+ methodName: string;
33
+ args: any[];
34
+ startTime: number;
35
+ endTime?: number;
36
+ result?: any;
37
+ error?: any;
38
+ };
39
+ };
40
+ type Listener<T> = (payload: T) => void;
41
+ export declare const TELEMETRY_METADATA_KEY = "di:telemetry";
42
+ export declare const TELEMETRY_LISTENER_METADATA_KEY = "di:telemetry-listener";
43
+ declare function defineMetadata(key: string | symbol, value: any, target: any): void;
44
+ declare function getMetadata(key: string | symbol, target: any): any;
45
+ declare function hasMetadata(key: string | symbol, target: any): boolean;
46
+ declare function getOwnMetadata(key: string | symbol, target: any): any;
47
+ export declare class Container {
48
+ private services;
49
+ private resolutionStack;
50
+ private listeners;
51
+ /**
52
+ * Register a service class as injectable
53
+ */
54
+ register<T>(serviceClass: Constructor<T>, options?: {
55
+ singleton?: boolean;
56
+ }): this;
57
+ /**
58
+ * Register a service using a factory function
59
+ */
60
+ registerFactory<T>(name: string, factory: ServiceFactory<T>, options?: {
61
+ singleton?: boolean;
62
+ }): this;
63
+ /**
64
+ * Get or create a service instance
65
+ */
66
+ resolve<T>(serviceClass: Constructor<T> | string): T;
67
+ /**
68
+ * Construct a new instance without registering it in the container.
69
+ * Supports constructor overrides for primitives/config (constructor pattern).
70
+ * Always returns a fresh instance (no caching).
71
+ */
72
+ construct<T>(serviceClass: Constructor<T>, overrides?: Record<number, any>): T;
73
+ /**
74
+ * Check if a service is registered
75
+ */
76
+ has(serviceClass: Constructor | string): boolean;
77
+ /**
78
+ * Clear all registered services
79
+ */
80
+ clear(): void;
81
+ /**
82
+ * Get all registered service names
83
+ */
84
+ getServiceNames(): string[];
85
+ /**
86
+ * Fork the container (prototype pattern): clone registrations into a new container.
87
+ * Optionally carry over existing singleton instances.
88
+ */
89
+ fork(options?: {
90
+ carrySingletons?: boolean;
91
+ }): Container;
92
+ /**
93
+ * Subscribe to container lifecycle events (observer pattern).
94
+ * Returns an unsubscribe function.
95
+ */
96
+ on<K extends keyof ContainerEventPayloads>(event: K, listener: Listener<ContainerEventPayloads[K]>): () => void;
97
+ /**
98
+ * Remove a previously registered listener
99
+ */
100
+ off<K extends keyof ContainerEventPayloads>(event: K, listener: Listener<ContainerEventPayloads[K]>): void;
101
+ private emit;
102
+ /**
103
+ * Private method to instantiate a service
104
+ */
105
+ private instantiate;
106
+ /**
107
+ * Apply telemetry tracking and listeners to an instance
108
+ */
109
+ private applyTelemetry;
110
+ /**
111
+ * Check if a function is a class constructor
112
+ */
113
+ private isClass;
114
+ /**
115
+ * Extract parameter names from constructor
116
+ */
117
+ private getConstructorParamNames;
118
+ /**
119
+ * Extract parameter types from TypeScript compiled code
120
+ * Looks for type annotations in the compiled constructor signature
121
+ */
122
+ private extractParamTypesFromSource;
123
+ }
124
+ /**
125
+ * Global DI container instance
126
+ */
127
+ export declare const container: Container;
128
+ /**
129
+ * Get the global DI container
130
+ */
131
+ export declare function useContainer(): Container;
132
+ /**
133
+ * Export metadata functions for use in decorators
134
+ * These provide a simple, reflect-metadata-free way to store and access metadata
135
+ */
136
+ export { defineMetadata, getMetadata, hasMetadata, getOwnMetadata };