@di-framework/di-framework 0.0.0-prerelease.308 → 0.0.0-prerelease.310
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 +23 -25
- package/dist/container.d.ts +11 -1
- package/dist/container.js +171 -45
- package/dist/decorators.d.ts +28 -3
- package/dist/decorators.js +43 -7
- package/dist/types.js +6 -6
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -22,12 +22,12 @@ 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
|
|
25
|
+
import { Container } from '@di-framework/di-framework/decorators';
|
|
26
26
|
|
|
27
27
|
@Container()
|
|
28
28
|
export class DatabaseService {
|
|
29
29
|
connect(): void {
|
|
30
|
-
console.log(
|
|
30
|
+
console.log('Connected to database');
|
|
31
31
|
}
|
|
32
32
|
}
|
|
33
33
|
```
|
|
@@ -35,8 +35,8 @@ export class DatabaseService {
|
|
|
35
35
|
### 2. Service with Dependencies
|
|
36
36
|
|
|
37
37
|
```typescript
|
|
38
|
-
import { Container, Component } from
|
|
39
|
-
import { DatabaseService } from
|
|
38
|
+
import { Container, Component } from '@di-framework/di-framework/decorators';
|
|
39
|
+
import { DatabaseService } from './services/DatabaseService';
|
|
40
40
|
|
|
41
41
|
@Container()
|
|
42
42
|
export class UserService {
|
|
@@ -56,14 +56,14 @@ 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
|
|
60
|
-
import { UserService } from
|
|
59
|
+
import { useContainer } from '@di-framework/di-framework/container';
|
|
60
|
+
import { UserService } from './services/UserService';
|
|
61
61
|
|
|
62
62
|
const container = useContainer();
|
|
63
63
|
const userService = container.resolve<UserService>(UserService);
|
|
64
64
|
|
|
65
65
|
// All dependencies are automatically injected!
|
|
66
|
-
userService.getUser(
|
|
66
|
+
userService.getUser('123');
|
|
67
67
|
```
|
|
68
68
|
|
|
69
69
|
## API Reference
|
|
@@ -157,7 +157,7 @@ export class MonitoringService {
|
|
|
157
157
|
Returns the global DI container instance.
|
|
158
158
|
|
|
159
159
|
```typescript
|
|
160
|
-
import { useContainer } from
|
|
160
|
+
import { useContainer } from '@di-framework/di-framework/container';
|
|
161
161
|
|
|
162
162
|
const container = useContainer();
|
|
163
163
|
```
|
|
@@ -176,7 +176,7 @@ Register a service using a factory function.
|
|
|
176
176
|
|
|
177
177
|
```typescript
|
|
178
178
|
container.registerFactory(
|
|
179
|
-
|
|
179
|
+
'config',
|
|
180
180
|
() => ({
|
|
181
181
|
apiKey: process.env.API_KEY,
|
|
182
182
|
dbUrl: process.env.DATABASE_URL,
|
|
@@ -192,7 +192,7 @@ Resolve and get an instance of a service.
|
|
|
192
192
|
```typescript
|
|
193
193
|
const userService = container.resolve<UserService>(UserService);
|
|
194
194
|
// or by name
|
|
195
|
-
const config = container.resolve(
|
|
195
|
+
const config = container.resolve('config');
|
|
196
196
|
```
|
|
197
197
|
|
|
198
198
|
### `container.has(serviceClass)`
|
|
@@ -228,10 +228,8 @@ Subscribe to DI container lifecycle events (observer pattern).
|
|
|
228
228
|
**Example:**
|
|
229
229
|
|
|
230
230
|
```typescript
|
|
231
|
-
const unsubscribe = container.on(
|
|
232
|
-
console.log(
|
|
233
|
-
`Resolved ${typeof key === "string" ? key : key.name} (fromCache=${fromCache})`,
|
|
234
|
-
);
|
|
231
|
+
const unsubscribe = container.on('resolved', ({ key, fromCache }) => {
|
|
232
|
+
console.log(`Resolved ${typeof key === 'string' ? key : key.name} (fromCache=${fromCache})`);
|
|
235
233
|
});
|
|
236
234
|
|
|
237
235
|
unsubscribe(); // stop listening
|
|
@@ -242,8 +240,8 @@ unsubscribe(); // stop listening
|
|
|
242
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.
|
|
243
241
|
|
|
244
242
|
```typescript
|
|
245
|
-
import { Component } from
|
|
246
|
-
import { LoggerService } from
|
|
243
|
+
import { Component } from '@di-framework/di-framework/decorators';
|
|
244
|
+
import { LoggerService } from '@di-framework/di-framework/services/LoggerService';
|
|
247
245
|
|
|
248
246
|
class Greeter {
|
|
249
247
|
constructor(
|
|
@@ -252,7 +250,7 @@ class Greeter {
|
|
|
252
250
|
) {}
|
|
253
251
|
}
|
|
254
252
|
|
|
255
|
-
const greeter = container.construct(Greeter, { 1:
|
|
253
|
+
const greeter = container.construct(Greeter, { 1: 'hello world' });
|
|
256
254
|
```
|
|
257
255
|
|
|
258
256
|
### `container.fork(options?)`
|
|
@@ -277,7 +275,7 @@ export class ApplicationContext {
|
|
|
277
275
|
) {}
|
|
278
276
|
|
|
279
277
|
async initialize() {
|
|
280
|
-
this.logger.log(
|
|
278
|
+
this.logger.log('Initializing application...');
|
|
281
279
|
await this.db.connect();
|
|
282
280
|
this.auth.setup();
|
|
283
281
|
}
|
|
@@ -312,12 +310,12 @@ export class DatabaseService {
|
|
|
312
310
|
|
|
313
311
|
setEnv(env: Record<string, any>) {
|
|
314
312
|
// Called to initialize environment-specific config
|
|
315
|
-
console.log(
|
|
313
|
+
console.log('DB URL:', env.DATABASE_URL);
|
|
316
314
|
}
|
|
317
315
|
|
|
318
316
|
setCtx(context: any) {
|
|
319
317
|
// Called to set execution context
|
|
320
|
-
console.log(
|
|
318
|
+
console.log('Context:', context);
|
|
321
319
|
}
|
|
322
320
|
|
|
323
321
|
connect() {
|
|
@@ -328,7 +326,7 @@ export class DatabaseService {
|
|
|
328
326
|
// Calling lifecycle methods
|
|
329
327
|
const db = container.resolve(DatabaseService);
|
|
330
328
|
db.setEnv(process.env);
|
|
331
|
-
db.setCtx({ userId:
|
|
329
|
+
db.setCtx({ userId: '123' });
|
|
332
330
|
db.connect();
|
|
333
331
|
```
|
|
334
332
|
|
|
@@ -336,7 +334,7 @@ db.connect();
|
|
|
336
334
|
|
|
337
335
|
```typescript
|
|
338
336
|
container.registerFactory(
|
|
339
|
-
|
|
337
|
+
'apiClient',
|
|
340
338
|
() => {
|
|
341
339
|
return new HttpClient({
|
|
342
340
|
baseUrl: process.env.API_URL,
|
|
@@ -349,7 +347,7 @@ container.registerFactory(
|
|
|
349
347
|
// Use in services
|
|
350
348
|
@Container()
|
|
351
349
|
export class UserService {
|
|
352
|
-
constructor(@Component(
|
|
350
|
+
constructor(@Component('apiClient') private api: any) {}
|
|
353
351
|
}
|
|
354
352
|
```
|
|
355
353
|
|
|
@@ -479,7 +477,7 @@ class MyService {
|
|
|
479
477
|
|
|
480
478
|
```typescript
|
|
481
479
|
// Create a test container
|
|
482
|
-
import { Container as DIContainer } from
|
|
480
|
+
import { Container as DIContainer } from '@di-framework/di-framework/container';
|
|
483
481
|
|
|
484
482
|
const testContainer = new DIContainer();
|
|
485
483
|
|
|
@@ -497,7 +495,7 @@ testContainer.register(UserService);
|
|
|
497
495
|
|
|
498
496
|
// Test the service with mocked dependencies
|
|
499
497
|
const userService = testContainer.resolve(UserService);
|
|
500
|
-
expect(userService.getUser(
|
|
498
|
+
expect(userService.getUser('1')).toEqual({ mock: true });
|
|
501
499
|
```
|
|
502
500
|
|
|
503
501
|
## License
|
package/dist/container.d.ts
CHANGED
|
@@ -11,7 +11,7 @@ type ContainerEventPayloads = {
|
|
|
11
11
|
registered: {
|
|
12
12
|
key: string | Constructor;
|
|
13
13
|
singleton: boolean;
|
|
14
|
-
kind:
|
|
14
|
+
kind: 'class' | 'factory';
|
|
15
15
|
};
|
|
16
16
|
resolved: {
|
|
17
17
|
key: string | Constructor;
|
|
@@ -42,6 +42,7 @@ export declare const TELEMETRY_METADATA_KEY = "di:telemetry";
|
|
|
42
42
|
export declare const TELEMETRY_LISTENER_METADATA_KEY = "di:telemetry-listener";
|
|
43
43
|
export declare const PUBLISHER_METADATA_KEY = "di:publisher";
|
|
44
44
|
export declare const SUBSCRIBER_METADATA_KEY = "di:subscriber";
|
|
45
|
+
export declare const CRON_METADATA_KEY = "di:cron";
|
|
45
46
|
declare function defineMetadata(key: string | symbol, value: any, target: any): void;
|
|
46
47
|
declare function getMetadata(key: string | symbol, target: any): any;
|
|
47
48
|
declare function hasMetadata(key: string | symbol, target: any): boolean;
|
|
@@ -50,6 +51,7 @@ export declare class Container {
|
|
|
50
51
|
private services;
|
|
51
52
|
private resolutionStack;
|
|
52
53
|
private listeners;
|
|
54
|
+
private cronJobs;
|
|
53
55
|
/**
|
|
54
56
|
* Register a service class as injectable
|
|
55
57
|
*/
|
|
@@ -80,6 +82,10 @@ export declare class Container {
|
|
|
80
82
|
* Clear all registered services
|
|
81
83
|
*/
|
|
82
84
|
clear(): void;
|
|
85
|
+
/**
|
|
86
|
+
* Stop all active cron jobs
|
|
87
|
+
*/
|
|
88
|
+
stopCronJobs(): void;
|
|
83
89
|
/**
|
|
84
90
|
* Get all registered service names
|
|
85
91
|
*/
|
|
@@ -105,6 +111,10 @@ export declare class Container {
|
|
|
105
111
|
* Apply event publishers and subscribers defined via decorators
|
|
106
112
|
*/
|
|
107
113
|
private applyEvents;
|
|
114
|
+
/**
|
|
115
|
+
* Apply cron schedules defined via @Cron decorator
|
|
116
|
+
*/
|
|
117
|
+
private applyCron;
|
|
108
118
|
/**
|
|
109
119
|
* Private method to instantiate a service
|
|
110
120
|
*/
|
package/dist/container.js
CHANGED
|
@@ -5,13 +5,14 @@
|
|
|
5
5
|
* Supports singleton pattern and automatic dependency injection via decorators.
|
|
6
6
|
* Works with SWC and TypeScript's native decorator support.
|
|
7
7
|
*/
|
|
8
|
-
const INJECTABLE_METADATA_KEY =
|
|
9
|
-
const INJECT_METADATA_KEY =
|
|
10
|
-
const DESIGN_PARAM_TYPES_KEY =
|
|
11
|
-
export const TELEMETRY_METADATA_KEY =
|
|
12
|
-
export const TELEMETRY_LISTENER_METADATA_KEY =
|
|
13
|
-
export const PUBLISHER_METADATA_KEY =
|
|
14
|
-
export const SUBSCRIBER_METADATA_KEY =
|
|
8
|
+
const INJECTABLE_METADATA_KEY = 'di:injectable';
|
|
9
|
+
const INJECT_METADATA_KEY = 'di:inject';
|
|
10
|
+
const DESIGN_PARAM_TYPES_KEY = 'design:paramtypes';
|
|
11
|
+
export const TELEMETRY_METADATA_KEY = 'di:telemetry';
|
|
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';
|
|
15
16
|
/**
|
|
16
17
|
* Simple metadata storage that doesn't require reflect-metadata
|
|
17
18
|
* Works with SWC's native decorator support
|
|
@@ -32,10 +33,70 @@ function hasMetadata(key, target) {
|
|
|
32
33
|
function getOwnMetadata(key, target) {
|
|
33
34
|
return getMetadata(key, target);
|
|
34
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
|
+
}
|
|
35
95
|
export class Container {
|
|
36
96
|
services = new Map();
|
|
37
97
|
resolutionStack = new Set();
|
|
38
98
|
listeners = new Map();
|
|
99
|
+
cronJobs = [];
|
|
39
100
|
/**
|
|
40
101
|
* Register a service class as injectable
|
|
41
102
|
*/
|
|
@@ -49,10 +110,10 @@ export class Container {
|
|
|
49
110
|
type: serviceClass,
|
|
50
111
|
singleton: options.singleton ?? true,
|
|
51
112
|
});
|
|
52
|
-
this.emit(
|
|
113
|
+
this.emit('registered', {
|
|
53
114
|
key: serviceClass,
|
|
54
115
|
singleton: options.singleton ?? true,
|
|
55
|
-
kind:
|
|
116
|
+
kind: 'class',
|
|
56
117
|
});
|
|
57
118
|
return this;
|
|
58
119
|
}
|
|
@@ -64,10 +125,10 @@ export class Container {
|
|
|
64
125
|
type: factory,
|
|
65
126
|
singleton: options.singleton ?? true,
|
|
66
127
|
});
|
|
67
|
-
this.emit(
|
|
128
|
+
this.emit('registered', {
|
|
68
129
|
key: name,
|
|
69
130
|
singleton: options.singleton ?? true,
|
|
70
|
-
kind:
|
|
131
|
+
kind: 'factory',
|
|
71
132
|
});
|
|
72
133
|
return this;
|
|
73
134
|
}
|
|
@@ -75,11 +136,11 @@ export class Container {
|
|
|
75
136
|
* Get or create a service instance
|
|
76
137
|
*/
|
|
77
138
|
resolve(serviceClass) {
|
|
78
|
-
const key = typeof serviceClass ===
|
|
79
|
-
const keyStr = typeof serviceClass ===
|
|
139
|
+
const key = typeof serviceClass === 'string' ? serviceClass : serviceClass;
|
|
140
|
+
const keyStr = typeof serviceClass === 'string' ? serviceClass : serviceClass.name;
|
|
80
141
|
// Check for circular dependencies
|
|
81
142
|
if (this.resolutionStack.has(key)) {
|
|
82
|
-
throw new Error(`Circular dependency detected while resolving ${keyStr}. Stack: ${Array.from(this.resolutionStack).join(
|
|
143
|
+
throw new Error(`Circular dependency detected while resolving ${keyStr}. Stack: ${Array.from(this.resolutionStack).join(' -> ')} -> ${keyStr}`);
|
|
83
144
|
}
|
|
84
145
|
const definition = this.services.get(key);
|
|
85
146
|
if (!definition) {
|
|
@@ -88,7 +149,7 @@ export class Container {
|
|
|
88
149
|
const wasCached = definition.singleton && !!definition.instance;
|
|
89
150
|
// Return cached singleton
|
|
90
151
|
if (definition.singleton && definition.instance) {
|
|
91
|
-
this.emit(
|
|
152
|
+
this.emit('resolved', {
|
|
92
153
|
key,
|
|
93
154
|
instance: definition.instance,
|
|
94
155
|
singleton: true,
|
|
@@ -104,7 +165,7 @@ export class Container {
|
|
|
104
165
|
if (definition.singleton) {
|
|
105
166
|
definition.instance = instance;
|
|
106
167
|
}
|
|
107
|
-
this.emit(
|
|
168
|
+
this.emit('resolved', {
|
|
108
169
|
key,
|
|
109
170
|
instance,
|
|
110
171
|
singleton: definition.singleton,
|
|
@@ -124,12 +185,12 @@ export class Container {
|
|
|
124
185
|
construct(serviceClass, overrides = {}) {
|
|
125
186
|
const keyStr = serviceClass.name;
|
|
126
187
|
if (this.resolutionStack.has(serviceClass)) {
|
|
127
|
-
throw new Error(`Circular dependency detected while constructing ${keyStr}. Stack: ${Array.from(this.resolutionStack).join(
|
|
188
|
+
throw new Error(`Circular dependency detected while constructing ${keyStr}. Stack: ${Array.from(this.resolutionStack).join(' -> ')} -> ${keyStr}`);
|
|
128
189
|
}
|
|
129
190
|
this.resolutionStack.add(serviceClass);
|
|
130
191
|
try {
|
|
131
192
|
const instance = this.instantiate(serviceClass, overrides);
|
|
132
|
-
this.emit(
|
|
193
|
+
this.emit('constructed', { key: serviceClass, instance, overrides });
|
|
133
194
|
return instance;
|
|
134
195
|
}
|
|
135
196
|
finally {
|
|
@@ -147,8 +208,18 @@ export class Container {
|
|
|
147
208
|
*/
|
|
148
209
|
clear() {
|
|
149
210
|
const count = this.services.size;
|
|
211
|
+
this.stopCronJobs();
|
|
150
212
|
this.services.clear();
|
|
151
|
-
this.emit(
|
|
213
|
+
this.emit('cleared', { count });
|
|
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 = [];
|
|
152
223
|
}
|
|
153
224
|
/**
|
|
154
225
|
* Get all registered service names
|
|
@@ -156,7 +227,7 @@ export class Container {
|
|
|
156
227
|
getServiceNames() {
|
|
157
228
|
const names = new Set();
|
|
158
229
|
this.services.forEach((_, key) => {
|
|
159
|
-
if (typeof key ===
|
|
230
|
+
if (typeof key === 'string') {
|
|
160
231
|
names.add(key);
|
|
161
232
|
}
|
|
162
233
|
});
|
|
@@ -216,7 +287,7 @@ export class Container {
|
|
|
216
287
|
Object.entries(subscriberMap).forEach(([event, methods]) => {
|
|
217
288
|
methods.forEach((methodName) => {
|
|
218
289
|
const method = instance[methodName];
|
|
219
|
-
if (typeof method ===
|
|
290
|
+
if (typeof method === 'function') {
|
|
220
291
|
this.on(event, (payload) => {
|
|
221
292
|
try {
|
|
222
293
|
method.call(instance, payload);
|
|
@@ -232,9 +303,9 @@ export class Container {
|
|
|
232
303
|
const publisherMethods = getMetadata(PUBLISHER_METADATA_KEY, constructor.prototype) || {};
|
|
233
304
|
Object.entries(publisherMethods).forEach(([methodName, options]) => {
|
|
234
305
|
const originalMethod = instance[methodName];
|
|
235
|
-
if (typeof originalMethod ===
|
|
306
|
+
if (typeof originalMethod === 'function') {
|
|
236
307
|
const self = this;
|
|
237
|
-
const phase = options.phase ??
|
|
308
|
+
const phase = options.phase ?? 'after';
|
|
238
309
|
instance[methodName] = function (...args) {
|
|
239
310
|
const startTime = Date.now();
|
|
240
311
|
const emit = (result, error) => {
|
|
@@ -251,13 +322,13 @@ export class Container {
|
|
|
251
322
|
const duration = payload.endTime - payload.startTime;
|
|
252
323
|
const status = error
|
|
253
324
|
? `ERROR: ${error && error.message ? error.message : String(error)}`
|
|
254
|
-
:
|
|
325
|
+
: 'SUCCESS';
|
|
255
326
|
console.log(`[Publisher] ${className}.${methodName} -> '${options.event}' - ${status} (${duration}ms)`);
|
|
256
327
|
}
|
|
257
328
|
self.emit(options.event, payload);
|
|
258
329
|
};
|
|
259
330
|
try {
|
|
260
|
-
if (phase ===
|
|
331
|
+
if (phase === 'before' || phase === 'both') {
|
|
261
332
|
// Emit before invocation (no result yet)
|
|
262
333
|
emit(undefined, undefined);
|
|
263
334
|
}
|
|
@@ -265,7 +336,7 @@ export class Container {
|
|
|
265
336
|
if (result instanceof Promise) {
|
|
266
337
|
return result
|
|
267
338
|
.then((val) => {
|
|
268
|
-
if (phase ===
|
|
339
|
+
if (phase === 'after' || phase === 'both') {
|
|
269
340
|
emit(val, undefined);
|
|
270
341
|
}
|
|
271
342
|
return val;
|
|
@@ -276,7 +347,7 @@ export class Container {
|
|
|
276
347
|
throw err;
|
|
277
348
|
});
|
|
278
349
|
}
|
|
279
|
-
if (phase ===
|
|
350
|
+
if (phase === 'after' || phase === 'both') {
|
|
280
351
|
emit(result, undefined);
|
|
281
352
|
}
|
|
282
353
|
return result;
|
|
@@ -289,12 +360,70 @@ export class Container {
|
|
|
289
360
|
}
|
|
290
361
|
});
|
|
291
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();
|
|
418
|
+
}
|
|
419
|
+
});
|
|
420
|
+
}
|
|
292
421
|
/**
|
|
293
422
|
* Private method to instantiate a service
|
|
294
423
|
*/
|
|
295
424
|
instantiate(type, overrides = {}) {
|
|
296
|
-
if (typeof type !==
|
|
297
|
-
throw new Error(
|
|
425
|
+
if (typeof type !== 'function') {
|
|
426
|
+
throw new Error('Service type must be a constructor or factory function');
|
|
298
427
|
}
|
|
299
428
|
// If it's a factory function (not a class), just call it
|
|
300
429
|
if (!this.isClass(type)) {
|
|
@@ -342,17 +471,18 @@ export class Container {
|
|
|
342
471
|
this.applyTelemetry(instance, type);
|
|
343
472
|
// Apply custom event publishers and subscribers
|
|
344
473
|
this.applyEvents(instance, type);
|
|
474
|
+
// Apply cron schedules
|
|
475
|
+
this.applyCron(instance, type);
|
|
345
476
|
// Call @Component() decorators on properties
|
|
346
477
|
// Check both the instance and the constructor prototype for metadata
|
|
347
478
|
const injectProperties = getMetadata(INJECT_METADATA_KEY, type) || {};
|
|
348
|
-
const protoInjectProperties = getMetadata(INJECT_METADATA_KEY, type.prototype) ||
|
|
349
|
-
{};
|
|
479
|
+
const protoInjectProperties = getMetadata(INJECT_METADATA_KEY, type.prototype) || {};
|
|
350
480
|
const allInjectProperties = {
|
|
351
481
|
...injectProperties,
|
|
352
482
|
...protoInjectProperties,
|
|
353
483
|
};
|
|
354
484
|
Object.entries(allInjectProperties).forEach(([propName, targetType]) => {
|
|
355
|
-
if (!propName.startsWith(
|
|
485
|
+
if (!propName.startsWith('param_') && targetType) {
|
|
356
486
|
try {
|
|
357
487
|
instance[propName] = this.resolve(targetType);
|
|
358
488
|
}
|
|
@@ -372,8 +502,8 @@ export class Container {
|
|
|
372
502
|
const listenerMethods = getMetadata(TELEMETRY_LISTENER_METADATA_KEY, constructor.prototype) || [];
|
|
373
503
|
listenerMethods.forEach((methodName) => {
|
|
374
504
|
const method = instance[methodName];
|
|
375
|
-
if (typeof method ===
|
|
376
|
-
this.on(
|
|
505
|
+
if (typeof method === 'function') {
|
|
506
|
+
this.on('telemetry', (payload) => {
|
|
377
507
|
try {
|
|
378
508
|
method.call(instance, payload);
|
|
379
509
|
}
|
|
@@ -387,7 +517,7 @@ export class Container {
|
|
|
387
517
|
const telemetryMethods = getMetadata(TELEMETRY_METADATA_KEY, constructor.prototype) || {};
|
|
388
518
|
Object.entries(telemetryMethods).forEach(([methodName, options]) => {
|
|
389
519
|
const originalMethod = instance[methodName];
|
|
390
|
-
if (typeof originalMethod ===
|
|
520
|
+
if (typeof originalMethod === 'function') {
|
|
391
521
|
const self = this;
|
|
392
522
|
instance[methodName] = function (...args) {
|
|
393
523
|
const startTime = Date.now();
|
|
@@ -403,12 +533,10 @@ export class Container {
|
|
|
403
533
|
};
|
|
404
534
|
if (options.logging) {
|
|
405
535
|
const duration = payload.endTime - payload.startTime;
|
|
406
|
-
const status = error
|
|
407
|
-
? `ERROR: ${error.message || error}`
|
|
408
|
-
: "SUCCESS";
|
|
536
|
+
const status = error ? `ERROR: ${error.message || error}` : 'SUCCESS';
|
|
409
537
|
console.log(`[Telemetry] ${className}.${methodName} - ${status} (${duration}ms)`);
|
|
410
538
|
}
|
|
411
|
-
self.emit(
|
|
539
|
+
self.emit('telemetry', payload);
|
|
412
540
|
};
|
|
413
541
|
try {
|
|
414
542
|
const result = originalMethod.apply(this, args);
|
|
@@ -438,9 +566,7 @@ export class Container {
|
|
|
438
566
|
* Check if a function is a class constructor
|
|
439
567
|
*/
|
|
440
568
|
isClass(func) {
|
|
441
|
-
return
|
|
442
|
-
func.prototype &&
|
|
443
|
-
func.prototype.constructor === func);
|
|
569
|
+
return typeof func === 'function' && func.prototype && func.prototype.constructor === func;
|
|
444
570
|
}
|
|
445
571
|
/**
|
|
446
572
|
* Extract parameter names from constructor
|
|
@@ -452,11 +578,11 @@ export class Container {
|
|
|
452
578
|
return [];
|
|
453
579
|
const paramsStr = match[1];
|
|
454
580
|
return paramsStr
|
|
455
|
-
.split(
|
|
581
|
+
.split(',')
|
|
456
582
|
.map((param) => {
|
|
457
583
|
const trimmed = param.trim();
|
|
458
|
-
const withoutDefault = trimmed.split(
|
|
459
|
-
const withoutType = withoutDefault.split(
|
|
584
|
+
const withoutDefault = trimmed.split('=')[0] || '';
|
|
585
|
+
const withoutType = withoutDefault.split(':')[0] || '';
|
|
460
586
|
return withoutType.trim();
|
|
461
587
|
})
|
|
462
588
|
.filter((param) => param);
|
package/dist/decorators.d.ts
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 { Container as DIContainer } from
|
|
10
|
+
import { Container as DIContainer } from './container';
|
|
11
11
|
/**
|
|
12
12
|
* Options for the @Telemetry decorator
|
|
13
13
|
*/
|
|
@@ -38,7 +38,7 @@ export interface PublisherOptions {
|
|
|
38
38
|
/** The custom event name to emit on the container */
|
|
39
39
|
event: string;
|
|
40
40
|
/** When to emit relative to the method invocation. Defaults to 'after'. */
|
|
41
|
-
phase?:
|
|
41
|
+
phase?: 'before' | 'after' | 'both';
|
|
42
42
|
/** Optional console logging for debug purposes. Defaults to false. */
|
|
43
43
|
logging?: boolean;
|
|
44
44
|
}
|
|
@@ -66,6 +66,19 @@ export declare function Publisher(optionsOrEvent: string | PublisherOptions): (t
|
|
|
66
66
|
* }
|
|
67
67
|
*/
|
|
68
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;
|
|
69
82
|
/**
|
|
70
83
|
* Marks a class as injectable and registers it with the DI container
|
|
71
84
|
*
|
|
@@ -89,6 +102,18 @@ export declare function Container(options?: {
|
|
|
89
102
|
}): <T extends {
|
|
90
103
|
new (...args: any[]): {};
|
|
91
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;
|
|
92
117
|
/**
|
|
93
118
|
* Marks a constructor parameter or property for dependency injection
|
|
94
119
|
*
|
|
@@ -117,7 +142,7 @@ export declare function Container(options?: {
|
|
|
117
142
|
* constructor(@Component('apiKey') apiKey: string) {}
|
|
118
143
|
* }
|
|
119
144
|
*/
|
|
120
|
-
export declare function Component(target: any): (targetClass:
|
|
145
|
+
export declare function Component(target: any): (targetClass: object | any, propertyKey?: string | symbol, parameterIndex?: number) => void;
|
|
121
146
|
/**
|
|
122
147
|
* Check if a class is marked as injectable
|
|
123
148
|
*/
|
package/dist/decorators.js
CHANGED
|
@@ -7,9 +7,9 @@
|
|
|
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, PUBLISHER_METADATA_KEY, SUBSCRIBER_METADATA_KEY, } from
|
|
11
|
-
const INJECTABLE_METADATA_KEY =
|
|
12
|
-
const INJECT_METADATA_KEY =
|
|
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
|
+
const INJECTABLE_METADATA_KEY = 'di:injectable';
|
|
12
|
+
const INJECT_METADATA_KEY = 'di:inject';
|
|
13
13
|
/**
|
|
14
14
|
* Marks a method for telemetry tracking.
|
|
15
15
|
* When called, it will emit a 'telemetry' event on the container.
|
|
@@ -48,13 +48,11 @@ export function TelemetryListener() {
|
|
|
48
48
|
*/
|
|
49
49
|
export function Publisher(optionsOrEvent) {
|
|
50
50
|
return function (target, propertyKey, descriptor) {
|
|
51
|
-
const options = typeof optionsOrEvent ===
|
|
52
|
-
? { event: optionsOrEvent }
|
|
53
|
-
: optionsOrEvent;
|
|
51
|
+
const options = typeof optionsOrEvent === 'string' ? { event: optionsOrEvent } : optionsOrEvent;
|
|
54
52
|
const methods = getOwnMetadata(PUBLISHER_METADATA_KEY, target) || {};
|
|
55
53
|
methods[propertyKey] = {
|
|
56
54
|
event: options.event,
|
|
57
|
-
phase: options.phase ??
|
|
55
|
+
phase: options.phase ?? 'after',
|
|
58
56
|
logging: options.logging ?? false,
|
|
59
57
|
};
|
|
60
58
|
defineMetadata(PUBLISHER_METADATA_KEY, methods, target);
|
|
@@ -80,6 +78,25 @@ export function Subscriber(event) {
|
|
|
80
78
|
defineMetadata(SUBSCRIBER_METADATA_KEY, map, target);
|
|
81
79
|
};
|
|
82
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
|
+
}
|
|
83
100
|
/**
|
|
84
101
|
* Marks a class as injectable and registers it with the DI container
|
|
85
102
|
*
|
|
@@ -108,6 +125,25 @@ export function Container(options = {}) {
|
|
|
108
125
|
return constructor;
|
|
109
126
|
};
|
|
110
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
|
+
}
|
|
111
147
|
/**
|
|
112
148
|
* Marks a constructor parameter or property for dependency injection
|
|
113
149
|
*
|
package/dist/types.js
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
export class DIError extends Error {
|
|
10
10
|
constructor(message) {
|
|
11
11
|
super(message);
|
|
12
|
-
this.name =
|
|
12
|
+
this.name = 'DIError';
|
|
13
13
|
}
|
|
14
14
|
}
|
|
15
15
|
/**
|
|
@@ -18,7 +18,7 @@ export class DIError extends Error {
|
|
|
18
18
|
export class CircularDependencyError extends DIError {
|
|
19
19
|
constructor(message) {
|
|
20
20
|
super(message);
|
|
21
|
-
this.name =
|
|
21
|
+
this.name = 'CircularDependencyError';
|
|
22
22
|
}
|
|
23
23
|
}
|
|
24
24
|
/**
|
|
@@ -27,7 +27,7 @@ export class CircularDependencyError extends DIError {
|
|
|
27
27
|
export class ServiceNotFoundError extends DIError {
|
|
28
28
|
constructor(serviceName) {
|
|
29
29
|
super(`Service '${serviceName}' is not registered in the DI container`);
|
|
30
|
-
this.name =
|
|
30
|
+
this.name = 'ServiceNotFoundError';
|
|
31
31
|
}
|
|
32
32
|
}
|
|
33
33
|
/**
|
|
@@ -76,21 +76,21 @@ export var ServiceUtils;
|
|
|
76
76
|
* Check if an object is a service class
|
|
77
77
|
*/
|
|
78
78
|
function isServiceClass(obj) {
|
|
79
|
-
return typeof obj ===
|
|
79
|
+
return typeof obj === 'function' && obj.prototype;
|
|
80
80
|
}
|
|
81
81
|
ServiceUtils.isServiceClass = isServiceClass;
|
|
82
82
|
/**
|
|
83
83
|
* Check if an object is a factory function
|
|
84
84
|
*/
|
|
85
85
|
function isFactory(obj) {
|
|
86
|
-
return typeof obj ===
|
|
86
|
+
return typeof obj === 'function' && !isServiceClass(obj);
|
|
87
87
|
}
|
|
88
88
|
ServiceUtils.isFactory = isFactory;
|
|
89
89
|
/**
|
|
90
90
|
* Get the service name
|
|
91
91
|
*/
|
|
92
92
|
function getServiceName(service) {
|
|
93
|
-
if (typeof service ===
|
|
93
|
+
if (typeof service === 'string') {
|
|
94
94
|
return service;
|
|
95
95
|
}
|
|
96
96
|
return service.name;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@di-framework/di-framework",
|
|
3
|
-
"version": "0.0.0-prerelease.
|
|
3
|
+
"version": "0.0.0-prerelease.310",
|
|
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",
|