@di-framework/di-framework 2.0.4 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +57 -26
- package/dist/container.d.ts +19 -3
- package/dist/container.js +224 -5
- package/dist/decorators.d.ts +61 -1
- package/dist/decorators.js +82 -1
- package/package.json +17 -4
package/README.md
CHANGED
|
@@ -6,12 +6,12 @@
|
|
|
6
6
|
|
|
7
7
|
A lightweight, type-safe Dependency Injection framework for TypeScript using decorators. This framework automatically manages service instantiation, dependency resolution, and lifecycle management.
|
|
8
8
|
|
|
9
|
-
|
|
10
9
|
## Installation
|
|
11
10
|
|
|
12
11
|
No external dependencies required! The framework works with SWC and TypeScript's native decorator support.
|
|
13
12
|
|
|
14
13
|
Just ensure you have:
|
|
14
|
+
|
|
15
15
|
- TypeScript 5.0+
|
|
16
16
|
- SWC or TypeScript compiler with `experimentalDecorators` and `emitDecoratorMetadata` enabled
|
|
17
17
|
|
|
@@ -22,7 +22,7 @@ The decorators are fully integrated with SWC's native support - no need for `ref
|
|
|
22
22
|
### 1. Basic Service
|
|
23
23
|
|
|
24
24
|
```typescript
|
|
25
|
-
import { Container } from 'di-framework/decorators';
|
|
25
|
+
import { Container } from '@di-framework/di-framework/decorators';
|
|
26
26
|
|
|
27
27
|
@Container()
|
|
28
28
|
export class DatabaseService {
|
|
@@ -35,7 +35,7 @@ export class DatabaseService {
|
|
|
35
35
|
### 2. Service with Dependencies
|
|
36
36
|
|
|
37
37
|
```typescript
|
|
38
|
-
import { Container, Component } from 'di-framework/decorators';
|
|
38
|
+
import { Container, Component } from '@di-framework/di-framework/decorators';
|
|
39
39
|
import { DatabaseService } from './services/DatabaseService';
|
|
40
40
|
|
|
41
41
|
@Container()
|
|
@@ -56,7 +56,7 @@ Note: Property injection is used for all dependencies. This works seamlessly wit
|
|
|
56
56
|
### 3. Resolve Services
|
|
57
57
|
|
|
58
58
|
```typescript
|
|
59
|
-
import { useContainer } from 'di-framework/container';
|
|
59
|
+
import { useContainer } from '@di-framework/di-framework/container';
|
|
60
60
|
import { UserService } from './services/UserService';
|
|
61
61
|
|
|
62
62
|
const container = useContainer();
|
|
@@ -73,11 +73,13 @@ userService.getUser('123');
|
|
|
73
73
|
Marks a class as injectable and automatically registers it with the DI container.
|
|
74
74
|
|
|
75
75
|
**Options:**
|
|
76
|
+
|
|
76
77
|
- `singleton?: boolean` (default: `true`) - Create a new instance each time or reuse the same instance
|
|
77
78
|
- `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
|
+
- Note: Import as `import { Container as DIContainer } from '@di-framework/di-framework/container'` to avoid name collision with the `@Container` decorator.
|
|
79
80
|
|
|
80
81
|
**Example:**
|
|
82
|
+
|
|
81
83
|
```typescript
|
|
82
84
|
@Container({ singleton: false })
|
|
83
85
|
export class RequestScopedService {
|
|
@@ -90,9 +92,11 @@ export class RequestScopedService {
|
|
|
90
92
|
Marks a constructor parameter or property for dependency injection.
|
|
91
93
|
|
|
92
94
|
**Parameters:**
|
|
95
|
+
|
|
93
96
|
- `target` - The class to inject or a string identifier for factory-registered services
|
|
94
97
|
|
|
95
98
|
**Example - Constructor Parameter:**
|
|
99
|
+
|
|
96
100
|
```typescript
|
|
97
101
|
@Container()
|
|
98
102
|
export class OrderService {
|
|
@@ -101,6 +105,7 @@ export class OrderService {
|
|
|
101
105
|
```
|
|
102
106
|
|
|
103
107
|
**Example - Property Injection:**
|
|
108
|
+
|
|
104
109
|
```typescript
|
|
105
110
|
@Container()
|
|
106
111
|
export class ReportService {
|
|
@@ -114,9 +119,11 @@ export class ReportService {
|
|
|
114
119
|
Marks a method for telemetry tracking. When called, it emits a `telemetry` event on the container. Works with both synchronous and asynchronous methods.
|
|
115
120
|
|
|
116
121
|
**Options:**
|
|
122
|
+
|
|
117
123
|
- `logging?: boolean` (default: `false`) - If true, logs the method execution details (status and duration) to the console.
|
|
118
124
|
|
|
119
125
|
**Example:**
|
|
126
|
+
|
|
120
127
|
```typescript
|
|
121
128
|
@Container()
|
|
122
129
|
export class ApiService {
|
|
@@ -132,12 +139,15 @@ export class ApiService {
|
|
|
132
139
|
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
140
|
|
|
134
141
|
**Example:**
|
|
142
|
+
|
|
135
143
|
```typescript
|
|
136
144
|
@Container()
|
|
137
145
|
export class MonitoringService {
|
|
138
146
|
@TelemetryListener()
|
|
139
147
|
onTelemetry(event: any) {
|
|
140
|
-
console.log(
|
|
148
|
+
console.log(
|
|
149
|
+
`Method ${event.className}.${event.methodName} took ${event.endTime - event.startTime}ms`,
|
|
150
|
+
);
|
|
141
151
|
}
|
|
142
152
|
}
|
|
143
153
|
```
|
|
@@ -147,7 +157,7 @@ export class MonitoringService {
|
|
|
147
157
|
Returns the global DI container instance.
|
|
148
158
|
|
|
149
159
|
```typescript
|
|
150
|
-
import { useContainer } from 'di-framework/container';
|
|
160
|
+
import { useContainer } from '@di-framework/di-framework/container';
|
|
151
161
|
|
|
152
162
|
const container = useContainer();
|
|
153
163
|
```
|
|
@@ -165,10 +175,14 @@ container.register(UserService, { singleton: true });
|
|
|
165
175
|
Register a service using a factory function.
|
|
166
176
|
|
|
167
177
|
```typescript
|
|
168
|
-
container.registerFactory(
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
178
|
+
container.registerFactory(
|
|
179
|
+
'config',
|
|
180
|
+
() => ({
|
|
181
|
+
apiKey: process.env.API_KEY,
|
|
182
|
+
dbUrl: process.env.DATABASE_URL,
|
|
183
|
+
}),
|
|
184
|
+
{ singleton: true },
|
|
185
|
+
);
|
|
172
186
|
```
|
|
173
187
|
|
|
174
188
|
### `container.resolve(serviceClass)`
|
|
@@ -205,12 +219,14 @@ console.log(names); // ['DatabaseService', 'UserService', ...]
|
|
|
205
219
|
Subscribe to DI container lifecycle events (observer pattern).
|
|
206
220
|
|
|
207
221
|
**Events:**
|
|
222
|
+
|
|
208
223
|
- `registered` - fired when a class or factory is registered
|
|
209
224
|
- `resolved` - fired whenever a service is resolved (cached or fresh)
|
|
210
225
|
- `constructed` - fired when `construct()` creates a new instance
|
|
211
226
|
- `cleared` - fired when the container is cleared
|
|
212
227
|
|
|
213
228
|
**Example:**
|
|
229
|
+
|
|
214
230
|
```typescript
|
|
215
231
|
const unsubscribe = container.on('resolved', ({ key, fromCache }) => {
|
|
216
232
|
console.log(`Resolved ${typeof key === 'string' ? key : key.name} (fromCache=${fromCache})`);
|
|
@@ -224,11 +240,14 @@ unsubscribe(); // stop listening
|
|
|
224
240
|
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
241
|
|
|
226
242
|
```typescript
|
|
227
|
-
import { Component } from 'di-framework/decorators';
|
|
228
|
-
import { LoggerService } from 'di-framework/services/LoggerService';
|
|
243
|
+
import { Component } from '@di-framework/di-framework/decorators';
|
|
244
|
+
import { LoggerService } from '@di-framework/di-framework/services/LoggerService';
|
|
229
245
|
|
|
230
246
|
class Greeter {
|
|
231
|
-
constructor(
|
|
247
|
+
constructor(
|
|
248
|
+
@Component(LoggerService) private logger: LoggerService,
|
|
249
|
+
private greeting: string,
|
|
250
|
+
) {}
|
|
232
251
|
}
|
|
233
252
|
|
|
234
253
|
const greeter = container.construct(Greeter, { 1: 'hello world' });
|
|
@@ -252,7 +271,7 @@ export class ApplicationContext {
|
|
|
252
271
|
constructor(
|
|
253
272
|
@Component(DatabaseService) private db: DatabaseService,
|
|
254
273
|
@Component(LoggerService) private logger: LoggerService,
|
|
255
|
-
@Component(AuthService) private auth: AuthService
|
|
274
|
+
@Component(AuthService) private auth: AuthService,
|
|
256
275
|
) {}
|
|
257
276
|
|
|
258
277
|
async initialize() {
|
|
@@ -314,12 +333,16 @@ db.connect();
|
|
|
314
333
|
### Factory Functions
|
|
315
334
|
|
|
316
335
|
```typescript
|
|
317
|
-
container.registerFactory(
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
336
|
+
container.registerFactory(
|
|
337
|
+
'apiClient',
|
|
338
|
+
() => {
|
|
339
|
+
return new HttpClient({
|
|
340
|
+
baseUrl: process.env.API_URL,
|
|
341
|
+
timeout: 5000,
|
|
342
|
+
});
|
|
343
|
+
},
|
|
344
|
+
{ singleton: true },
|
|
345
|
+
);
|
|
323
346
|
|
|
324
347
|
// Use in services
|
|
325
348
|
@Container()
|
|
@@ -343,9 +366,10 @@ export class UserService {
|
|
|
343
366
|
## Comparison with SAMPLE.ts
|
|
344
367
|
|
|
345
368
|
### Before (Manual - SAMPLE.ts)
|
|
369
|
+
|
|
346
370
|
```typescript
|
|
347
371
|
const createServerContext = (env, ctx) => {
|
|
348
|
-
if(!instanceState.member) {
|
|
372
|
+
if (!instanceState.member) {
|
|
349
373
|
const contextInstance = Context.create({
|
|
350
374
|
contactService: ContactService.create({}),
|
|
351
375
|
assetService: AssetService.create({}),
|
|
@@ -363,20 +387,22 @@ const createServerContext = (env, ctx) => {
|
|
|
363
387
|
instanceState.member.setCtx(ctx);
|
|
364
388
|
// ... manual dependency wiring
|
|
365
389
|
instanceState.member.knowledgeService.setAttachmentService(
|
|
366
|
-
instanceState.member.attachmentService
|
|
390
|
+
instanceState.member.attachmentService,
|
|
367
391
|
);
|
|
368
392
|
return instanceState.member;
|
|
369
393
|
};
|
|
370
394
|
```
|
|
371
395
|
|
|
372
396
|
### After (DI Framework)
|
|
397
|
+
|
|
373
398
|
```typescript
|
|
374
399
|
@Container()
|
|
375
400
|
export class ApplicationContext {
|
|
376
401
|
constructor(
|
|
377
402
|
@Component(ContactService) private contactService: ContactService,
|
|
378
403
|
@Component(AssetService) private assetService: AssetService,
|
|
379
|
-
@Component(TransactionService)
|
|
404
|
+
@Component(TransactionService)
|
|
405
|
+
private transactionService: TransactionService,
|
|
380
406
|
// ... all services automatically injected
|
|
381
407
|
@Component(ChatService) private chatService: ChatService,
|
|
382
408
|
) {}
|
|
@@ -399,6 +425,7 @@ appContext.setCtx(ctx);
|
|
|
399
425
|
```
|
|
400
426
|
|
|
401
427
|
**Benefits:**
|
|
428
|
+
|
|
402
429
|
- No manual service instantiation
|
|
403
430
|
- No manual dependency wiring
|
|
404
431
|
- Automatic singleton management
|
|
@@ -409,6 +436,7 @@ appContext.setCtx(ctx);
|
|
|
409
436
|
## Error Handling
|
|
410
437
|
|
|
411
438
|
### Circular Dependencies
|
|
439
|
+
|
|
412
440
|
```typescript
|
|
413
441
|
// This will be detected and throw an error:
|
|
414
442
|
@Container()
|
|
@@ -425,6 +453,7 @@ class ServiceB {
|
|
|
425
453
|
```
|
|
426
454
|
|
|
427
455
|
### Unregistered Services
|
|
456
|
+
|
|
428
457
|
```typescript
|
|
429
458
|
@Container()
|
|
430
459
|
class MyService {
|
|
@@ -448,13 +477,15 @@ class MyService {
|
|
|
448
477
|
|
|
449
478
|
```typescript
|
|
450
479
|
// Create a test container
|
|
451
|
-
import { Container as DIContainer } from 'di-framework/container';
|
|
480
|
+
import { Container as DIContainer } from '@di-framework/di-framework/container';
|
|
452
481
|
|
|
453
482
|
const testContainer = new DIContainer();
|
|
454
483
|
|
|
455
484
|
// Register mock implementations
|
|
456
485
|
class MockDatabaseService {
|
|
457
|
-
query() {
|
|
486
|
+
query() {
|
|
487
|
+
return { mock: true };
|
|
488
|
+
}
|
|
458
489
|
}
|
|
459
490
|
|
|
460
491
|
testContainer.register(MockDatabaseService);
|
package/dist/container.d.ts
CHANGED
|
@@ -40,6 +40,9 @@ type ContainerEventPayloads = {
|
|
|
40
40
|
type Listener<T> = (payload: T) => void;
|
|
41
41
|
export declare const TELEMETRY_METADATA_KEY = "di:telemetry";
|
|
42
42
|
export declare const TELEMETRY_LISTENER_METADATA_KEY = "di:telemetry-listener";
|
|
43
|
+
export declare const PUBLISHER_METADATA_KEY = "di:publisher";
|
|
44
|
+
export declare const SUBSCRIBER_METADATA_KEY = "di:subscriber";
|
|
45
|
+
export declare const CRON_METADATA_KEY = "di:cron";
|
|
43
46
|
declare function defineMetadata(key: string | symbol, value: any, target: any): void;
|
|
44
47
|
declare function getMetadata(key: string | symbol, target: any): any;
|
|
45
48
|
declare function hasMetadata(key: string | symbol, target: any): boolean;
|
|
@@ -48,6 +51,7 @@ export declare class Container {
|
|
|
48
51
|
private services;
|
|
49
52
|
private resolutionStack;
|
|
50
53
|
private listeners;
|
|
54
|
+
private cronJobs;
|
|
51
55
|
/**
|
|
52
56
|
* Register a service class as injectable
|
|
53
57
|
*/
|
|
@@ -78,6 +82,10 @@ export declare class Container {
|
|
|
78
82
|
* Clear all registered services
|
|
79
83
|
*/
|
|
80
84
|
clear(): void;
|
|
85
|
+
/**
|
|
86
|
+
* Stop all active cron jobs
|
|
87
|
+
*/
|
|
88
|
+
stopCronJobs(): void;
|
|
81
89
|
/**
|
|
82
90
|
* Get all registered service names
|
|
83
91
|
*/
|
|
@@ -93,12 +101,20 @@ export declare class Container {
|
|
|
93
101
|
* Subscribe to container lifecycle events (observer pattern).
|
|
94
102
|
* Returns an unsubscribe function.
|
|
95
103
|
*/
|
|
96
|
-
on<K extends keyof ContainerEventPayloads>(event: K, listener: Listener<ContainerEventPayloads[K]>): () => void;
|
|
104
|
+
on<K extends keyof ContainerEventPayloads | (string & {})>(event: K, listener: Listener<K extends keyof ContainerEventPayloads ? ContainerEventPayloads[K] : any>): () => void;
|
|
97
105
|
/**
|
|
98
106
|
* Remove a previously registered listener
|
|
99
107
|
*/
|
|
100
|
-
off<K extends keyof ContainerEventPayloads>(event: K, listener: Listener<ContainerEventPayloads[K]>): void;
|
|
101
|
-
|
|
108
|
+
off<K extends keyof ContainerEventPayloads | (string & {})>(event: K, listener: Listener<K extends keyof ContainerEventPayloads ? ContainerEventPayloads[K] : any>): void;
|
|
109
|
+
emit<K extends keyof ContainerEventPayloads | (string & {})>(event: K, payload: K extends keyof ContainerEventPayloads ? ContainerEventPayloads[K] : any): void;
|
|
110
|
+
/**
|
|
111
|
+
* Apply event publishers and subscribers defined via decorators
|
|
112
|
+
*/
|
|
113
|
+
private applyEvents;
|
|
114
|
+
/**
|
|
115
|
+
* Apply cron schedules defined via @Cron decorator
|
|
116
|
+
*/
|
|
117
|
+
private applyCron;
|
|
102
118
|
/**
|
|
103
119
|
* Private method to instantiate a service
|
|
104
120
|
*/
|
package/dist/container.js
CHANGED
|
@@ -10,6 +10,9 @@ const INJECT_METADATA_KEY = 'di:inject';
|
|
|
10
10
|
const DESIGN_PARAM_TYPES_KEY = 'design:paramtypes';
|
|
11
11
|
export const TELEMETRY_METADATA_KEY = 'di:telemetry';
|
|
12
12
|
export const TELEMETRY_LISTENER_METADATA_KEY = 'di:telemetry-listener';
|
|
13
|
+
export const PUBLISHER_METADATA_KEY = 'di:publisher';
|
|
14
|
+
export const SUBSCRIBER_METADATA_KEY = 'di:subscriber';
|
|
15
|
+
export const CRON_METADATA_KEY = 'di:cron';
|
|
13
16
|
/**
|
|
14
17
|
* Simple metadata storage that doesn't require reflect-metadata
|
|
15
18
|
* Works with SWC's native decorator support
|
|
@@ -30,10 +33,70 @@ function hasMetadata(key, target) {
|
|
|
30
33
|
function getOwnMetadata(key, target) {
|
|
31
34
|
return getMetadata(key, target);
|
|
32
35
|
}
|
|
36
|
+
// Parse a single cron field into an array of matching values.
|
|
37
|
+
// Supports: * (any), star/N (step), N (exact), N,M (list), N-M (range)
|
|
38
|
+
function parseCronField(field, min, max) {
|
|
39
|
+
if (field === '*') {
|
|
40
|
+
const out = [];
|
|
41
|
+
for (let i = min; i <= max; i++)
|
|
42
|
+
out.push(i);
|
|
43
|
+
return out;
|
|
44
|
+
}
|
|
45
|
+
if (field.startsWith('*/')) {
|
|
46
|
+
const step = parseInt(field.slice(2), 10);
|
|
47
|
+
const out = [];
|
|
48
|
+
for (let i = min; i <= max; i++) {
|
|
49
|
+
if (i % step === 0)
|
|
50
|
+
out.push(i);
|
|
51
|
+
}
|
|
52
|
+
return out;
|
|
53
|
+
}
|
|
54
|
+
if (field.includes(',')) {
|
|
55
|
+
return field.split(',').map((s) => parseInt(s.trim(), 10));
|
|
56
|
+
}
|
|
57
|
+
if (field.includes('-')) {
|
|
58
|
+
const [lo = 0, hi = 0] = field.split('-').map((s) => parseInt(s.trim(), 10));
|
|
59
|
+
const out = [];
|
|
60
|
+
for (let i = lo; i <= hi; i++)
|
|
61
|
+
out.push(i);
|
|
62
|
+
return out;
|
|
63
|
+
}
|
|
64
|
+
return [parseInt(field, 10)];
|
|
65
|
+
}
|
|
66
|
+
function parseCronExpression(expr) {
|
|
67
|
+
const parts = expr.trim().split(/\s+/);
|
|
68
|
+
if (parts.length !== 5)
|
|
69
|
+
throw new Error(`Invalid cron expression "${expr}": expected 5 fields (minute hour dayOfMonth month dayOfWeek)`);
|
|
70
|
+
return {
|
|
71
|
+
minute: parseCronField(parts[0], 0, 59),
|
|
72
|
+
hour: parseCronField(parts[1], 0, 23),
|
|
73
|
+
dayOfMonth: parseCronField(parts[2], 1, 31),
|
|
74
|
+
month: parseCronField(parts[3], 1, 12),
|
|
75
|
+
dayOfWeek: parseCronField(parts[4], 0, 6),
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
function getNextCronTime(fields, from) {
|
|
79
|
+
const next = new Date(from);
|
|
80
|
+
next.setSeconds(0, 0);
|
|
81
|
+
next.setMinutes(next.getMinutes() + 1);
|
|
82
|
+
// Search forward up to ~2 years of minutes
|
|
83
|
+
for (let i = 0; i < 1_051_920; i++) {
|
|
84
|
+
if (fields.minute.includes(next.getMinutes()) &&
|
|
85
|
+
fields.hour.includes(next.getHours()) &&
|
|
86
|
+
fields.dayOfMonth.includes(next.getDate()) &&
|
|
87
|
+
fields.month.includes(next.getMonth() + 1) &&
|
|
88
|
+
fields.dayOfWeek.includes(next.getDay())) {
|
|
89
|
+
return next;
|
|
90
|
+
}
|
|
91
|
+
next.setMinutes(next.getMinutes() + 1);
|
|
92
|
+
}
|
|
93
|
+
throw new Error(`No matching cron time found for expression within 2 years`);
|
|
94
|
+
}
|
|
33
95
|
export class Container {
|
|
34
96
|
services = new Map();
|
|
35
97
|
resolutionStack = new Set();
|
|
36
98
|
listeners = new Map();
|
|
99
|
+
cronJobs = [];
|
|
37
100
|
/**
|
|
38
101
|
* Register a service class as injectable
|
|
39
102
|
*/
|
|
@@ -145,9 +208,19 @@ export class Container {
|
|
|
145
208
|
*/
|
|
146
209
|
clear() {
|
|
147
210
|
const count = this.services.size;
|
|
211
|
+
this.stopCronJobs();
|
|
148
212
|
this.services.clear();
|
|
149
213
|
this.emit('cleared', { count });
|
|
150
214
|
}
|
|
215
|
+
/**
|
|
216
|
+
* Stop all active cron jobs
|
|
217
|
+
*/
|
|
218
|
+
stopCronJobs() {
|
|
219
|
+
for (const job of this.cronJobs) {
|
|
220
|
+
job.stop();
|
|
221
|
+
}
|
|
222
|
+
this.cronJobs = [];
|
|
223
|
+
}
|
|
151
224
|
/**
|
|
152
225
|
* Get all registered service names
|
|
153
226
|
*/
|
|
@@ -200,7 +273,148 @@ export class Container {
|
|
|
200
273
|
listener(payload);
|
|
201
274
|
}
|
|
202
275
|
catch (err) {
|
|
203
|
-
console.error(`[Container] listener for '${event}' threw`, err);
|
|
276
|
+
console.error(`[Container] listener for '${String(event)}' threw`, err);
|
|
277
|
+
}
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
/**
|
|
281
|
+
* Apply event publishers and subscribers defined via decorators
|
|
282
|
+
*/
|
|
283
|
+
applyEvents(instance, constructor) {
|
|
284
|
+
const className = constructor.name;
|
|
285
|
+
// Handle @Subscriber(event)
|
|
286
|
+
const subscriberMap = getMetadata(SUBSCRIBER_METADATA_KEY, constructor.prototype) || {};
|
|
287
|
+
Object.entries(subscriberMap).forEach(([event, methods]) => {
|
|
288
|
+
methods.forEach((methodName) => {
|
|
289
|
+
const method = instance[methodName];
|
|
290
|
+
if (typeof method === 'function') {
|
|
291
|
+
this.on(event, (payload) => {
|
|
292
|
+
try {
|
|
293
|
+
method.call(instance, payload);
|
|
294
|
+
}
|
|
295
|
+
catch (err) {
|
|
296
|
+
console.error(`[Container] Subscriber '${className}.${methodName}' for event '${event}' threw`, err);
|
|
297
|
+
}
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
});
|
|
301
|
+
});
|
|
302
|
+
// Handle @Publisher(options)
|
|
303
|
+
const publisherMethods = getMetadata(PUBLISHER_METADATA_KEY, constructor.prototype) || {};
|
|
304
|
+
Object.entries(publisherMethods).forEach(([methodName, options]) => {
|
|
305
|
+
const originalMethod = instance[methodName];
|
|
306
|
+
if (typeof originalMethod === 'function') {
|
|
307
|
+
const self = this;
|
|
308
|
+
const phase = options.phase ?? 'after';
|
|
309
|
+
instance[methodName] = function (...args) {
|
|
310
|
+
const startTime = Date.now();
|
|
311
|
+
const emit = (result, error) => {
|
|
312
|
+
const payload = {
|
|
313
|
+
className,
|
|
314
|
+
methodName,
|
|
315
|
+
args,
|
|
316
|
+
startTime,
|
|
317
|
+
endTime: Date.now(),
|
|
318
|
+
result,
|
|
319
|
+
error,
|
|
320
|
+
};
|
|
321
|
+
if (options.logging) {
|
|
322
|
+
const duration = payload.endTime - payload.startTime;
|
|
323
|
+
const status = error
|
|
324
|
+
? `ERROR: ${error && error.message ? error.message : String(error)}`
|
|
325
|
+
: 'SUCCESS';
|
|
326
|
+
console.log(`[Publisher] ${className}.${methodName} -> '${options.event}' - ${status} (${duration}ms)`);
|
|
327
|
+
}
|
|
328
|
+
self.emit(options.event, payload);
|
|
329
|
+
};
|
|
330
|
+
try {
|
|
331
|
+
if (phase === 'before' || phase === 'both') {
|
|
332
|
+
// Emit before invocation (no result yet)
|
|
333
|
+
emit(undefined, undefined);
|
|
334
|
+
}
|
|
335
|
+
const result = originalMethod.apply(this, args);
|
|
336
|
+
if (result instanceof Promise) {
|
|
337
|
+
return result
|
|
338
|
+
.then((val) => {
|
|
339
|
+
if (phase === 'after' || phase === 'both') {
|
|
340
|
+
emit(val, undefined);
|
|
341
|
+
}
|
|
342
|
+
return val;
|
|
343
|
+
})
|
|
344
|
+
.catch((err) => {
|
|
345
|
+
// Always emit on error to allow subscribers to react
|
|
346
|
+
emit(undefined, err);
|
|
347
|
+
throw err;
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
if (phase === 'after' || phase === 'both') {
|
|
351
|
+
emit(result, undefined);
|
|
352
|
+
}
|
|
353
|
+
return result;
|
|
354
|
+
}
|
|
355
|
+
catch (err) {
|
|
356
|
+
emit(undefined, err);
|
|
357
|
+
throw err;
|
|
358
|
+
}
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
/**
|
|
364
|
+
* Apply cron schedules defined via @Cron decorator
|
|
365
|
+
*/
|
|
366
|
+
applyCron(instance, constructor) {
|
|
367
|
+
const cronMethods = getMetadata(CRON_METADATA_KEY, constructor.prototype) || {};
|
|
368
|
+
Object.entries(cronMethods).forEach(([methodName, schedule]) => {
|
|
369
|
+
const method = instance[methodName];
|
|
370
|
+
if (typeof method !== 'function')
|
|
371
|
+
return;
|
|
372
|
+
if (typeof schedule === 'number') {
|
|
373
|
+
// Simple interval in ms
|
|
374
|
+
const timer = setInterval(() => {
|
|
375
|
+
try {
|
|
376
|
+
method.call(instance);
|
|
377
|
+
}
|
|
378
|
+
catch (err) {
|
|
379
|
+
console.error(`[Cron] ${constructor.name}.${methodName} threw`, err);
|
|
380
|
+
}
|
|
381
|
+
}, schedule);
|
|
382
|
+
this.cronJobs.push({ stop: () => clearInterval(timer) });
|
|
383
|
+
}
|
|
384
|
+
else {
|
|
385
|
+
// Cron expression
|
|
386
|
+
const fields = parseCronExpression(schedule);
|
|
387
|
+
let stopped = false;
|
|
388
|
+
const scheduleNext = () => {
|
|
389
|
+
if (stopped)
|
|
390
|
+
return;
|
|
391
|
+
const now = new Date();
|
|
392
|
+
const next = getNextCronTime(fields, now);
|
|
393
|
+
const delay = next.getTime() - now.getTime();
|
|
394
|
+
const timer = setTimeout(() => {
|
|
395
|
+
if (stopped)
|
|
396
|
+
return;
|
|
397
|
+
try {
|
|
398
|
+
method.call(instance);
|
|
399
|
+
}
|
|
400
|
+
catch (err) {
|
|
401
|
+
console.error(`[Cron] ${constructor.name}.${methodName} threw`, err);
|
|
402
|
+
}
|
|
403
|
+
scheduleNext();
|
|
404
|
+
}, delay);
|
|
405
|
+
// Update the stop function to clear the latest timer
|
|
406
|
+
job.stop = () => {
|
|
407
|
+
stopped = true;
|
|
408
|
+
clearTimeout(timer);
|
|
409
|
+
};
|
|
410
|
+
};
|
|
411
|
+
const job = {
|
|
412
|
+
stop: () => {
|
|
413
|
+
stopped = true;
|
|
414
|
+
},
|
|
415
|
+
};
|
|
416
|
+
this.cronJobs.push(job);
|
|
417
|
+
scheduleNext();
|
|
204
418
|
}
|
|
205
419
|
});
|
|
206
420
|
}
|
|
@@ -255,11 +469,18 @@ export class Container {
|
|
|
255
469
|
const instance = new type(...dependencies);
|
|
256
470
|
// Apply Telemetry and TelemetryListener
|
|
257
471
|
this.applyTelemetry(instance, type);
|
|
472
|
+
// Apply custom event publishers and subscribers
|
|
473
|
+
this.applyEvents(instance, type);
|
|
474
|
+
// Apply cron schedules
|
|
475
|
+
this.applyCron(instance, type);
|
|
258
476
|
// Call @Component() decorators on properties
|
|
259
477
|
// Check both the instance and the constructor prototype for metadata
|
|
260
478
|
const injectProperties = getMetadata(INJECT_METADATA_KEY, type) || {};
|
|
261
479
|
const protoInjectProperties = getMetadata(INJECT_METADATA_KEY, type.prototype) || {};
|
|
262
|
-
const allInjectProperties = {
|
|
480
|
+
const allInjectProperties = {
|
|
481
|
+
...injectProperties,
|
|
482
|
+
...protoInjectProperties,
|
|
483
|
+
};
|
|
263
484
|
Object.entries(allInjectProperties).forEach(([propName, targetType]) => {
|
|
264
485
|
if (!propName.startsWith('param_') && targetType) {
|
|
265
486
|
try {
|
|
@@ -345,9 +566,7 @@ export class Container {
|
|
|
345
566
|
* Check if a function is a class constructor
|
|
346
567
|
*/
|
|
347
568
|
isClass(func) {
|
|
348
|
-
return
|
|
349
|
-
func.prototype &&
|
|
350
|
-
func.prototype.constructor === func);
|
|
569
|
+
return typeof func === 'function' && func.prototype && func.prototype.constructor === func;
|
|
351
570
|
}
|
|
352
571
|
/**
|
|
353
572
|
* Extract parameter names from constructor
|
package/dist/decorators.d.ts
CHANGED
|
@@ -31,6 +31,54 @@ export declare function Telemetry(options?: TelemetryOptions): (target: any, pro
|
|
|
31
31
|
* The method will be automatically registered to the container's 'telemetry' event.
|
|
32
32
|
*/
|
|
33
33
|
export declare function TelemetryListener(): (target: any, propertyKey: string | symbol, descriptor: PropertyDescriptor) => void;
|
|
34
|
+
/**
|
|
35
|
+
* Options for the @Publisher decorator
|
|
36
|
+
*/
|
|
37
|
+
export interface PublisherOptions {
|
|
38
|
+
/** The custom event name to emit on the container */
|
|
39
|
+
event: string;
|
|
40
|
+
/** When to emit relative to the method invocation. Defaults to 'after'. */
|
|
41
|
+
phase?: 'before' | 'after' | 'both';
|
|
42
|
+
/** Optional console logging for debug purposes. Defaults to false. */
|
|
43
|
+
logging?: boolean;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Marks a method to publish a custom event on invocation.
|
|
47
|
+
* Useful for cross-platform event-driven architectures.
|
|
48
|
+
*
|
|
49
|
+
* Example:
|
|
50
|
+
* @Container()
|
|
51
|
+
* class UserService {
|
|
52
|
+
* @Publisher('user.created')
|
|
53
|
+
* createUser(dto: CreateUserDto) { ... }
|
|
54
|
+
* }
|
|
55
|
+
*/
|
|
56
|
+
export declare function Publisher(optionsOrEvent: string | PublisherOptions): (target: any, propertyKey: string | symbol, descriptor: PropertyDescriptor) => void;
|
|
57
|
+
/**
|
|
58
|
+
* Marks a method to subscribe to a custom event emitted on the container.
|
|
59
|
+
* The decorated method will receive the published payload.
|
|
60
|
+
*
|
|
61
|
+
* Example:
|
|
62
|
+
* @Container()
|
|
63
|
+
* class AuditService {
|
|
64
|
+
* @Subscriber('user.created')
|
|
65
|
+
* onUserCreated(payload: any) { ... }
|
|
66
|
+
* }
|
|
67
|
+
*/
|
|
68
|
+
export declare function Subscriber(event: string): (target: any, propertyKey: string | symbol, descriptor: PropertyDescriptor) => void;
|
|
69
|
+
/**
|
|
70
|
+
* Marks a method to run on a cron schedule.
|
|
71
|
+
* The schedule starts automatically when the service is resolved.
|
|
72
|
+
* Jobs are stopped when container.clear() is called.
|
|
73
|
+
*
|
|
74
|
+
* @param schedule A cron expression (5 fields: minute hour dayOfMonth month dayOfWeek)
|
|
75
|
+
* or an interval in milliseconds.
|
|
76
|
+
*
|
|
77
|
+
* @example
|
|
78
|
+
* Cron('0 * * * *') // every hour
|
|
79
|
+
* Cron(30000) // every 30 seconds
|
|
80
|
+
*/
|
|
81
|
+
export declare function Cron(schedule: string | number): (target: any, propertyKey: string | symbol, descriptor: PropertyDescriptor) => void;
|
|
34
82
|
/**
|
|
35
83
|
* Marks a class as injectable and registers it with the DI container
|
|
36
84
|
*
|
|
@@ -54,6 +102,18 @@ export declare function Container(options?: {
|
|
|
54
102
|
}): <T extends {
|
|
55
103
|
new (...args: any[]): {};
|
|
56
104
|
}>(constructor: T) => T;
|
|
105
|
+
/**
|
|
106
|
+
* Eagerly resolves a class once at definition time.
|
|
107
|
+
*
|
|
108
|
+
* Useful for startup-only classes (e.g. HTTP controllers) whose constructors
|
|
109
|
+
* register routes or side effects and should run before handling requests.
|
|
110
|
+
*/
|
|
111
|
+
export declare function Bootstrap(options?: {
|
|
112
|
+
singleton?: boolean;
|
|
113
|
+
container?: DIContainer;
|
|
114
|
+
}): <T extends {
|
|
115
|
+
new (...args: any[]): {};
|
|
116
|
+
}>(constructor: T) => T;
|
|
57
117
|
/**
|
|
58
118
|
* Marks a constructor parameter or property for dependency injection
|
|
59
119
|
*
|
|
@@ -82,7 +142,7 @@ export declare function Container(options?: {
|
|
|
82
142
|
* constructor(@Component('apiKey') apiKey: string) {}
|
|
83
143
|
* }
|
|
84
144
|
*/
|
|
85
|
-
export declare function Component(target: any): (targetClass:
|
|
145
|
+
export declare function Component(target: any): (targetClass: object | any, propertyKey?: string | symbol, parameterIndex?: number) => void;
|
|
86
146
|
/**
|
|
87
147
|
* Check if a class is marked as injectable
|
|
88
148
|
*/
|
package/dist/decorators.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* Works with SWC and TypeScript's native decorator support.
|
|
8
8
|
* No external dependencies required (no reflect-metadata needed).
|
|
9
9
|
*/
|
|
10
|
-
import { useContainer, Container as DIContainer, defineMetadata, getOwnMetadata, getMetadata, TELEMETRY_METADATA_KEY, TELEMETRY_LISTENER_METADATA_KEY } from './container';
|
|
10
|
+
import { useContainer, Container as DIContainer, defineMetadata, getOwnMetadata, getMetadata, TELEMETRY_METADATA_KEY, TELEMETRY_LISTENER_METADATA_KEY, PUBLISHER_METADATA_KEY, SUBSCRIBER_METADATA_KEY, CRON_METADATA_KEY, } from './container';
|
|
11
11
|
const INJECTABLE_METADATA_KEY = 'di:injectable';
|
|
12
12
|
const INJECT_METADATA_KEY = 'di:inject';
|
|
13
13
|
/**
|
|
@@ -35,6 +35,68 @@ export function TelemetryListener() {
|
|
|
35
35
|
defineMetadata(TELEMETRY_LISTENER_METADATA_KEY, listeners, target);
|
|
36
36
|
};
|
|
37
37
|
}
|
|
38
|
+
/**
|
|
39
|
+
* Marks a method to publish a custom event on invocation.
|
|
40
|
+
* Useful for cross-platform event-driven architectures.
|
|
41
|
+
*
|
|
42
|
+
* Example:
|
|
43
|
+
* @Container()
|
|
44
|
+
* class UserService {
|
|
45
|
+
* @Publisher('user.created')
|
|
46
|
+
* createUser(dto: CreateUserDto) { ... }
|
|
47
|
+
* }
|
|
48
|
+
*/
|
|
49
|
+
export function Publisher(optionsOrEvent) {
|
|
50
|
+
return function (target, propertyKey, descriptor) {
|
|
51
|
+
const options = typeof optionsOrEvent === 'string' ? { event: optionsOrEvent } : optionsOrEvent;
|
|
52
|
+
const methods = getOwnMetadata(PUBLISHER_METADATA_KEY, target) || {};
|
|
53
|
+
methods[propertyKey] = {
|
|
54
|
+
event: options.event,
|
|
55
|
+
phase: options.phase ?? 'after',
|
|
56
|
+
logging: options.logging ?? false,
|
|
57
|
+
};
|
|
58
|
+
defineMetadata(PUBLISHER_METADATA_KEY, methods, target);
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Marks a method to subscribe to a custom event emitted on the container.
|
|
63
|
+
* The decorated method will receive the published payload.
|
|
64
|
+
*
|
|
65
|
+
* Example:
|
|
66
|
+
* @Container()
|
|
67
|
+
* class AuditService {
|
|
68
|
+
* @Subscriber('user.created')
|
|
69
|
+
* onUserCreated(payload: any) { ... }
|
|
70
|
+
* }
|
|
71
|
+
*/
|
|
72
|
+
export function Subscriber(event) {
|
|
73
|
+
return function (target, propertyKey, descriptor) {
|
|
74
|
+
const map = getOwnMetadata(SUBSCRIBER_METADATA_KEY, target) || {};
|
|
75
|
+
if (!map[event])
|
|
76
|
+
map[event] = [];
|
|
77
|
+
map[event].push(propertyKey);
|
|
78
|
+
defineMetadata(SUBSCRIBER_METADATA_KEY, map, target);
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Marks a method to run on a cron schedule.
|
|
83
|
+
* The schedule starts automatically when the service is resolved.
|
|
84
|
+
* Jobs are stopped when container.clear() is called.
|
|
85
|
+
*
|
|
86
|
+
* @param schedule A cron expression (5 fields: minute hour dayOfMonth month dayOfWeek)
|
|
87
|
+
* or an interval in milliseconds.
|
|
88
|
+
*
|
|
89
|
+
* @example
|
|
90
|
+
* Cron('0 * * * *') // every hour
|
|
91
|
+
* Cron(30000) // every 30 seconds
|
|
92
|
+
*/
|
|
93
|
+
export function Cron(schedule) {
|
|
94
|
+
return function (target, propertyKey, descriptor) {
|
|
95
|
+
const methods = getOwnMetadata(CRON_METADATA_KEY, target) || {};
|
|
96
|
+
methods[propertyKey] = schedule;
|
|
97
|
+
defineMetadata(CRON_METADATA_KEY, methods, target);
|
|
98
|
+
};
|
|
99
|
+
}
|
|
38
100
|
/**
|
|
39
101
|
* Marks a class as injectable and registers it with the DI container
|
|
40
102
|
*
|
|
@@ -63,6 +125,25 @@ export function Container(options = {}) {
|
|
|
63
125
|
return constructor;
|
|
64
126
|
};
|
|
65
127
|
}
|
|
128
|
+
/**
|
|
129
|
+
* Eagerly resolves a class once at definition time.
|
|
130
|
+
*
|
|
131
|
+
* Useful for startup-only classes (e.g. HTTP controllers) whose constructors
|
|
132
|
+
* register routes or side effects and should run before handling requests.
|
|
133
|
+
*/
|
|
134
|
+
export function Bootstrap(options = {}) {
|
|
135
|
+
return function (constructor) {
|
|
136
|
+
const container = options.container ?? useContainer();
|
|
137
|
+
// Allow bootstrap to be used with or without @Container().
|
|
138
|
+
if (!container.has(constructor)) {
|
|
139
|
+
const singleton = options.singleton ?? true;
|
|
140
|
+
defineMetadata(INJECTABLE_METADATA_KEY, true, constructor);
|
|
141
|
+
container.register(constructor, { singleton });
|
|
142
|
+
}
|
|
143
|
+
container.resolve(constructor);
|
|
144
|
+
return constructor;
|
|
145
|
+
};
|
|
146
|
+
}
|
|
66
147
|
/**
|
|
67
148
|
* Marks a constructor parameter or property for dependency injection
|
|
68
149
|
*
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@di-framework/di-framework",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.0.0",
|
|
4
4
|
"description": "Lightweight, zero-dependency TypeScript Dependency Injection framework using decorators. Works seamlessly with SWC and TypeScript's native decorator support.",
|
|
5
5
|
"main": "./dist/container.js",
|
|
6
6
|
"types": "./dist/container.d.ts",
|
|
@@ -19,12 +19,25 @@
|
|
|
19
19
|
],
|
|
20
20
|
"exports": {
|
|
21
21
|
".": {
|
|
22
|
+
"bun": "./container.ts",
|
|
22
23
|
"import": "./dist/container.js",
|
|
23
24
|
"types": "./dist/container.d.ts"
|
|
24
25
|
},
|
|
25
|
-
"./container":
|
|
26
|
-
|
|
27
|
-
|
|
26
|
+
"./container": {
|
|
27
|
+
"bun": "./container.ts",
|
|
28
|
+
"import": "./dist/container.js",
|
|
29
|
+
"types": "./dist/container.d.ts"
|
|
30
|
+
},
|
|
31
|
+
"./decorators": {
|
|
32
|
+
"bun": "./decorators.ts",
|
|
33
|
+
"import": "./dist/decorators.js",
|
|
34
|
+
"types": "./dist/decorators.d.ts"
|
|
35
|
+
},
|
|
36
|
+
"./types": {
|
|
37
|
+
"bun": "./types.ts",
|
|
38
|
+
"import": "./dist/types.js",
|
|
39
|
+
"types": "./dist/types.d.ts"
|
|
40
|
+
}
|
|
28
41
|
},
|
|
29
42
|
"license": "MIT",
|
|
30
43
|
"repository": {
|