@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 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 "@di-framework/di-framework/decorators";
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("Connected to database");
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 "@di-framework/di-framework/decorators";
39
- import { DatabaseService } from "./services/DatabaseService";
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 "@di-framework/di-framework/container";
60
- import { UserService } from "./services/UserService";
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("123");
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 "@di-framework/di-framework/container";
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
- "config",
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("config");
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("resolved", ({ key, fromCache }) => {
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 "@di-framework/di-framework/decorators";
246
- import { LoggerService } from "@di-framework/di-framework/services/LoggerService";
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: "hello world" });
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("Initializing application...");
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("DB URL:", env.DATABASE_URL);
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("Context:", context);
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: "123" });
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
- "apiClient",
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("apiClient") private api: any) {}
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 "@di-framework/di-framework/container";
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("1")).toEqual({ mock: true });
498
+ expect(userService.getUser('1')).toEqual({ mock: true });
501
499
  ```
502
500
 
503
501
  ## License
@@ -11,7 +11,7 @@ type ContainerEventPayloads = {
11
11
  registered: {
12
12
  key: string | Constructor;
13
13
  singleton: boolean;
14
- kind: "class" | "factory";
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 = "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";
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("registered", {
113
+ this.emit('registered', {
53
114
  key: serviceClass,
54
115
  singleton: options.singleton ?? true,
55
- kind: "class",
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("registered", {
128
+ this.emit('registered', {
68
129
  key: name,
69
130
  singleton: options.singleton ?? true,
70
- kind: "factory",
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 === "string" ? serviceClass : serviceClass;
79
- const keyStr = typeof serviceClass === "string" ? serviceClass : serviceClass.name;
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(" -> ")} -> ${keyStr}`);
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("resolved", {
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("resolved", {
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(" -> ")} -> ${keyStr}`);
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("constructed", { key: serviceClass, instance, overrides });
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("cleared", { count });
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 === "string") {
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 === "function") {
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 === "function") {
306
+ if (typeof originalMethod === 'function') {
236
307
  const self = this;
237
- const phase = options.phase ?? "after";
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
- : "SUCCESS";
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 === "before" || phase === "both") {
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 === "after" || phase === "both") {
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 === "after" || phase === "both") {
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 !== "function") {
297
- throw new Error("Service type must be a constructor or factory function");
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("param_") && targetType) {
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 === "function") {
376
- this.on("telemetry", (payload) => {
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 === "function") {
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("telemetry", payload);
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 (typeof func === "function" &&
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("=")[0] || "";
459
- const withoutType = withoutDefault.split(":")[0] || "";
584
+ const withoutDefault = trimmed.split('=')[0] || '';
585
+ const withoutType = withoutDefault.split(':')[0] || '';
460
586
  return withoutType.trim();
461
587
  })
462
588
  .filter((param) => param);
@@ -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 "./container";
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?: "before" | "after" | "both";
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: Object | any, propertyKey?: string | symbol, parameterIndex?: number) => void;
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
  */
@@ -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 "./container";
11
- const INJECTABLE_METADATA_KEY = "di:injectable";
12
- const INJECT_METADATA_KEY = "di:inject";
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 === "string"
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 ?? "after",
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 = "DIError";
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 = "CircularDependencyError";
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 = "ServiceNotFoundError";
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 === "function" && obj.prototype;
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 === "function" && !isServiceClass(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 === "string") {
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.308",
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",