@martel/calyx 1.11.0 → 1.12.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.
@@ -0,0 +1,175 @@
1
+ import { Module, Injectable } from '../core/decorators.ts';
2
+ import { Type } from '../core/metadata.ts';
3
+ import { ModuleRef } from '../core/module-ref.ts';
4
+
5
+ export interface ICommand {}
6
+ export interface ICommandHandler<TCommand extends ICommand = any, TResult = any> {
7
+ execute(command: TCommand): Promise<TResult>;
8
+ }
9
+
10
+ export interface IQuery {}
11
+ export interface IQueryHandler<TQuery extends IQuery = any, TResult = any> {
12
+ execute(query: TQuery): Promise<TResult>;
13
+ }
14
+
15
+ export interface IEvent {}
16
+ export interface IEventHandler<TEvent extends IEvent = any> {
17
+ handle(event: TEvent): any;
18
+ }
19
+
20
+ import { METADATA_KEYS } from '../core/metadata.ts';
21
+
22
+ export const COMMAND_HANDLER_METADATA = 'cqrs:command_handler';
23
+ export const QUERY_HANDLER_METADATA = 'cqrs:query_handler';
24
+ export const EVENT_HANDLER_METADATA = 'cqrs:event_handler';
25
+
26
+ export const CommandHandler = (command: Type<ICommand>): ClassDecorator => {
27
+ return (target) => {
28
+ Reflect.defineMetadata(COMMAND_HANDLER_METADATA, command, target);
29
+ Reflect.defineMetadata(METADATA_KEYS.INJECTABLE, true, target);
30
+ };
31
+ };
32
+
33
+ export const QueryHandler = (query: Type<IQuery>): ClassDecorator => {
34
+ return (target) => {
35
+ Reflect.defineMetadata(QUERY_HANDLER_METADATA, query, target);
36
+ Reflect.defineMetadata(METADATA_KEYS.INJECTABLE, true, target);
37
+ };
38
+ };
39
+
40
+ export const EventsHandler = (...events: Type<IEvent>[]): ClassDecorator => {
41
+ return (target) => {
42
+ Reflect.defineMetadata(EVENT_HANDLER_METADATA, events, target);
43
+ Reflect.defineMetadata(METADATA_KEYS.INJECTABLE, true, target);
44
+ };
45
+ };
46
+
47
+ @Injectable()
48
+ export class CommandBus {
49
+ private handlers = new Map<any, ICommandHandler>();
50
+
51
+ register(command: any, handler: ICommandHandler) {
52
+ this.handlers.set(command, handler);
53
+ }
54
+
55
+ async execute<T extends ICommand, R = any>(command: T): Promise<R> {
56
+ const commandClass = command.constructor;
57
+ const handler = this.handlers.get(commandClass);
58
+ if (!handler) {
59
+ throw new Error(`CommandHandler not found for command: ${commandClass.name}`);
60
+ }
61
+ return (await handler.execute(command)) as R;
62
+ }
63
+ }
64
+
65
+ @Injectable()
66
+ export class QueryBus {
67
+ private handlers = new Map<any, IQueryHandler>();
68
+
69
+ register(query: any, handler: IQueryHandler) {
70
+ this.handlers.set(query, handler);
71
+ }
72
+
73
+ async execute<T extends IQuery, R = any>(query: T): Promise<R> {
74
+ const queryClass = query.constructor;
75
+ const handler = this.handlers.get(queryClass);
76
+ if (!handler) {
77
+ throw new Error(`QueryHandler not found for query: ${queryClass.name}`);
78
+ }
79
+ return (await handler.execute(query)) as R;
80
+ }
81
+ }
82
+
83
+ @Injectable()
84
+ export class EventBus {
85
+ private handlers = new Map<any, IEventHandler[]>();
86
+
87
+ register(event: any, handler: IEventHandler) {
88
+ const existing = this.handlers.get(event) || [];
89
+ existing.push(handler);
90
+ this.handlers.set(event, existing);
91
+ }
92
+
93
+ publish<T extends IEvent>(event: T) {
94
+ const eventClass = event.constructor;
95
+ const eventHandlers = this.handlers.get(eventClass) || [];
96
+ for (const handler of eventHandlers) {
97
+ try {
98
+ handler.handle(event);
99
+ } catch (err) {
100
+ console.error(`Error in event handler:`, err);
101
+ }
102
+ }
103
+ }
104
+ }
105
+
106
+ @Injectable()
107
+ export class EventPublisher {
108
+ constructor(private readonly eventBus: EventBus) {}
109
+
110
+ mergeClassContext<T extends Type<any>>(metatype: T): T {
111
+ const eventBus = this.eventBus;
112
+ return class extends metatype {
113
+ publish(event: IEvent) {
114
+ eventBus.publish(event);
115
+ }
116
+ publishAll(events: IEvent[]) {
117
+ for (const event of events) {
118
+ eventBus.publish(event);
119
+ }
120
+ }
121
+ };
122
+ }
123
+
124
+ mergeObjectContext<T extends Object>(object: T): T {
125
+ const eventBus = this.eventBus;
126
+ return Object.assign(object, {
127
+ publish: (event: IEvent) => eventBus.publish(event),
128
+ publishAll: (events: IEvent[]) => {
129
+ for (const event of events) {
130
+ eventBus.publish(event);
131
+ }
132
+ },
133
+ });
134
+ }
135
+ }
136
+
137
+ @Module({
138
+ providers: [CommandBus, QueryBus, EventBus, EventPublisher],
139
+ exports: [CommandBus, QueryBus, EventBus, EventPublisher],
140
+ })
141
+ export class CqrsModule {
142
+ constructor(
143
+ private readonly moduleRef: ModuleRef,
144
+ private readonly commandBus: CommandBus,
145
+ private readonly queryBus: QueryBus,
146
+ private readonly eventBus: EventBus
147
+ ) {}
148
+
149
+ onModuleInit() {
150
+ const container = (this.moduleRef as any).container;
151
+ if (!container) return;
152
+
153
+ const instances = container.getProviderAndControllerInstances();
154
+ for (const inst of instances) {
155
+ if (!inst || !inst.constructor) continue;
156
+
157
+ const command = Reflect.getMetadata(COMMAND_HANDLER_METADATA, inst.constructor);
158
+ if (command) {
159
+ this.commandBus.register(command, inst);
160
+ }
161
+
162
+ const query = Reflect.getMetadata(QUERY_HANDLER_METADATA, inst.constructor);
163
+ if (query) {
164
+ this.queryBus.register(query, inst);
165
+ }
166
+
167
+ const events = Reflect.getMetadata(EVENT_HANDLER_METADATA, inst.constructor);
168
+ if (events && Array.isArray(events)) {
169
+ for (const event of events) {
170
+ this.eventBus.register(event, inst);
171
+ }
172
+ }
173
+ }
174
+ }
175
+ }
@@ -47,6 +47,22 @@ export function Parent(): ParameterDecorator {
47
47
  };
48
48
  }
49
49
 
50
+ export function Context(name?: string): ParameterDecorator {
51
+ return (target, propertyKey, parameterIndex) => {
52
+ const contextParams = Reflect.getOwnMetadata('calyx:context', target, propertyKey!) || [];
53
+ contextParams.push({ parameterIndex, name });
54
+ Reflect.defineMetadata('calyx:context', contextParams, target, propertyKey!);
55
+ };
56
+ }
57
+
58
+ export function Info(): ParameterDecorator {
59
+ return (target, propertyKey, parameterIndex) => {
60
+ const infoParams = Reflect.getOwnMetadata('calyx:info', target, propertyKey!) || [];
61
+ infoParams.push(parameterIndex);
62
+ Reflect.defineMetadata('calyx:info', infoParams, target, propertyKey!);
63
+ };
64
+ }
65
+
50
66
  export function ObjectType(): ClassDecorator {
51
67
  return (target) => {
52
68
  Reflect.defineMetadata('calyx:object_type', true, target);
@@ -313,6 +313,14 @@ export class GraphQLModule {
313
313
  for (const idx of parentParams) {
314
314
  params[idx] = parent;
315
315
  }
316
+ const contextParams = Reflect.getMetadata('calyx:context', resolverInstance, fieldMeta.propertyKey) || [];
317
+ for (const item of contextParams) {
318
+ params[item.parameterIndex] = item.name ? context?.[item.name] : context;
319
+ }
320
+ const infoParams = Reflect.getMetadata('calyx:info', resolverInstance, fieldMeta.propertyKey) || [];
321
+ for (const idx of infoParams) {
322
+ params[idx] = info;
323
+ }
316
324
  return resolverInstance[fieldMeta.propertyKey](...params);
317
325
  };
318
326
 
@@ -473,6 +481,14 @@ export class GraphQLModule {
473
481
  for (const idx of parentParams) {
474
482
  params[idx] = parent;
475
483
  }
484
+ const contextParams = Reflect.getMetadata('calyx:context', resolverInstance, fieldRes.propertyKey) || [];
485
+ for (const item of contextParams) {
486
+ params[item.parameterIndex] = item.name ? context?.[item.name] : context;
487
+ }
488
+ const infoParams = Reflect.getMetadata('calyx:info', resolverInstance, fieldRes.propertyKey) || [];
489
+ for (const idx of infoParams) {
490
+ params[idx] = info;
491
+ }
476
492
  return resolverInstance[fieldRes.propertyKey](...params);
477
493
  };
478
494
 
@@ -10,6 +10,7 @@ import { EventEmitter } from '../event-emitter/event-emitter.ts';
10
10
  import { cors, CorsOptions } from '../security/cors.middleware.ts';
11
11
  import { helmet, HelmetOptions } from '../security/helmet.middleware.ts';
12
12
  import { CronMatcher } from '../schedule/cron.matcher.ts';
13
+ import { SchedulerRegistry } from '../schedule/scheduler-registry.ts';
13
14
  import { SerializationCompiler } from '../validation/compiler.ts';
14
15
  import { VersioningOptions, VersioningType, VersionExtractor, VERSION_METADATA_KEY } from '../versioning/versioning.ts';
15
16
  import { QueueManager, PROCESSOR_METADATA_KEY } from '../queue/queue.module.ts';
@@ -66,6 +67,41 @@ export class CalyxResponse {
66
67
  this.cookiesList.push(formatCookie(name, value, options));
67
68
  return this;
68
69
  }
70
+
71
+ header(name: string, value: string) {
72
+ return this.set(name, value);
73
+ }
74
+
75
+ type(contentType: string) {
76
+ return this.set('content-type', contentType);
77
+ }
78
+
79
+ redirect(url: string, status?: number) {
80
+ this.statusCode = status ?? 302;
81
+ this.set('location', url);
82
+ this.sent = true;
83
+ return this;
84
+ }
85
+
86
+ end() {
87
+ this.sent = true;
88
+ return this;
89
+ }
90
+
91
+ get(name: string): string | undefined {
92
+ return this.headers[name.toLowerCase()];
93
+ }
94
+
95
+ append(name: string, value: string) {
96
+ const key = name.toLowerCase();
97
+ const existing = this.headers[key];
98
+ this.headers[key] = existing ? `${existing}, ${value}` : value;
99
+ return this;
100
+ }
101
+
102
+ clearCookie(name: string, options?: any) {
103
+ return this.cookie(name, '', { ...options, expires: new Date(0) });
104
+ }
69
105
  }
70
106
 
71
107
  interface CompiledLifecycleItem {
@@ -186,7 +222,7 @@ export class CalyxApplication {
186
222
  this.isInitialized = true;
187
223
 
188
224
  // Bootstrap the dependency injection container
189
- this.container.bootstrap(this.rootModule);
225
+ await this.container.bootstrap(this.rootModule);
190
226
 
191
227
  // Resolve registered modules middlewares
192
228
  this.resolveMiddleware();
@@ -685,6 +721,10 @@ export class CalyxApplication {
685
721
  case 'query': val = config.name ? query?.[config.name] : query; break;
686
722
  case 'body': val = config.name ? body?.[config.name] : body; break;
687
723
  case 'headers': val = config.name ? req.headers.get(config.name) : Object.fromEntries(req.headers.entries()); break;
724
+ case 'session': val = (req as any).session; break;
725
+ case 'ip': val = this.server ? (this.server.requestIP(req)?.address ?? req.headers.get('x-forwarded-for') ?? '') : (req.headers.get('x-forwarded-for') ?? ''); break;
726
+ case 'hostparam': val = config.name ? req.headers.get('host') : req.headers.get('host'); break;
727
+ case 'next': val = next; break;
688
728
  case 'custom': val = config.factory ? config.factory(config.name, context) : undefined; break;
689
729
  }
690
730
 
@@ -939,6 +979,18 @@ export class CalyxApplication {
939
979
  case 'headers':
940
980
  val = config.name ? req.headers.get(config.name) : Object.fromEntries(req.headers.entries());
941
981
  break;
982
+ case 'session':
983
+ val = (req as any).session;
984
+ break;
985
+ case 'ip':
986
+ val = this.server ? (this.server.requestIP(req)?.address ?? req.headers.get('x-forwarded-for') ?? '') : (req.headers.get('x-forwarded-for') ?? '');
987
+ break;
988
+ case 'hostparam':
989
+ val = config.name ? req.headers.get('host') : req.headers.get('host');
990
+ break;
991
+ case 'next':
992
+ val = () => {};
993
+ break;
942
994
  case 'custom':
943
995
  val = config.factory ? config.factory(config.name, context!) : undefined;
944
996
  break;
@@ -1554,11 +1606,18 @@ export class CalyxApplication {
1554
1606
  return;
1555
1607
  }
1556
1608
 
1609
+ let registry: SchedulerRegistry | null = null;
1610
+ try {
1611
+ registry = this.container.getGlobalOrAnyInstance(SchedulerRegistry);
1612
+ } catch {
1613
+ // ignore
1614
+ }
1615
+
1557
1616
  const instances = this.container.getProviderAndControllerInstances();
1558
1617
  for (const instance of instances) {
1559
1618
  if (!instance || !instance.constructor) continue;
1560
1619
 
1561
- const crons: { expression: string; propertyKey: string | symbol }[] =
1620
+ const crons: { expression: string; propertyKey: string | symbol; name?: string }[] =
1562
1621
  Reflect.getMetadata('calyx:cron', instance.constructor) || [];
1563
1622
  for (const cron of crons) {
1564
1623
  const parts = cron.expression.split(' ');
@@ -1584,9 +1643,16 @@ export class CalyxApplication {
1584
1643
  const intervalMs = isSecondLevel ? 1000 : 20000;
1585
1644
  const timer = setInterval(tick, intervalMs);
1586
1645
  this.cleanupListeners.push(() => clearInterval(timer));
1646
+
1647
+ if (registry && cron.name) {
1648
+ registry.addCronJob(cron.name, {
1649
+ start: () => {},
1650
+ stop: () => clearInterval(timer),
1651
+ });
1652
+ }
1587
1653
  }
1588
1654
 
1589
- const intervals: { ms: number; propertyKey: string | symbol }[] =
1655
+ const intervals: { ms: number; propertyKey: string | symbol; name?: string }[] =
1590
1656
  Reflect.getMetadata('calyx:interval', instance.constructor) || [];
1591
1657
  for (const interval of intervals) {
1592
1658
  const timer = setInterval(() => {
@@ -1597,9 +1663,13 @@ export class CalyxApplication {
1597
1663
  }
1598
1664
  }, interval.ms);
1599
1665
  this.cleanupListeners.push(() => clearInterval(timer));
1666
+
1667
+ if (registry && interval.name) {
1668
+ registry.addInterval(interval.name, timer);
1669
+ }
1600
1670
  }
1601
1671
 
1602
- const timeouts: { ms: number; propertyKey: string | symbol }[] =
1672
+ const timeouts: { ms: number; propertyKey: string | symbol; name?: string }[] =
1603
1673
  Reflect.getMetadata('calyx:timeout', instance.constructor) || [];
1604
1674
  for (const timeout of timeouts) {
1605
1675
  const timer = setTimeout(() => {
@@ -1610,6 +1680,10 @@ export class CalyxApplication {
1610
1680
  }
1611
1681
  }, timeout.ms);
1612
1682
  this.cleanupListeners.push(() => clearTimeout(timer));
1683
+
1684
+ if (registry && timeout.name) {
1685
+ registry.addTimeout(timeout.name, timer);
1686
+ }
1613
1687
  }
1614
1688
  }
1615
1689
  }
@@ -1635,14 +1709,24 @@ export class CalyxApplication {
1635
1709
  for (const sub of subMessages) {
1636
1710
  const paramMapping: any[] = [];
1637
1711
 
1712
+ const classPipes = Reflect.getMetadata(METADATA_KEYS.PIPES, instance.constructor) || [];
1713
+ const methodPipes = Reflect.getMetadata(METADATA_KEYS.PIPES, instance.constructor.prototype, sub.propertyKey) || [];
1714
+ const methodPipesCompiled = this.compileLifecycleItems(this.rootModule, [...this.globalPipes, ...classPipes, ...methodPipes]);
1715
+
1638
1716
  for (const bp of bodyParams) {
1639
1717
  if (bp.propertyKey === sub.propertyKey) {
1640
- paramMapping[bp.parameterIndex] = 'body';
1718
+ paramMapping[bp.parameterIndex] = {
1719
+ type: 'body',
1720
+ name: bp.name,
1721
+ pipes: [...methodPipesCompiled, ...this.compileLifecycleItems(this.rootModule, bp.pipes || [])],
1722
+ };
1641
1723
  }
1642
1724
  }
1643
1725
  for (const sp of socketParams) {
1644
1726
  if (sp.propertyKey === sub.propertyKey) {
1645
- paramMapping[sp.parameterIndex] = 'socket';
1727
+ paramMapping[sp.parameterIndex] = {
1728
+ type: 'socket',
1729
+ };
1646
1730
  }
1647
1731
  }
1648
1732
 
@@ -1654,11 +1738,16 @@ export class CalyxApplication {
1654
1738
  const methodInterceptors = Reflect.getMetadata(METADATA_KEYS.INTERCEPTORS, instance.constructor.prototype, sub.propertyKey) || [];
1655
1739
  const interceptors = this.compileLifecycleItems(this.rootModule, [...this.globalInterceptors, ...classInterceptors, ...methodInterceptors]);
1656
1740
 
1741
+ const classFilters = Reflect.getMetadata(METADATA_KEYS.FILTERS, instance.constructor) || [];
1742
+ const methodFilters = Reflect.getMetadata(METADATA_KEYS.FILTERS, instance.constructor.prototype, sub.propertyKey) || [];
1743
+ const filters = this.compileLifecycleItems(this.rootModule, [...this.globalFilters, ...classFilters, ...methodFilters]);
1744
+
1657
1745
  handlers.set(sub.event, {
1658
1746
  propertyKey: sub.propertyKey,
1659
1747
  paramMapping,
1660
1748
  guards,
1661
1749
  interceptors,
1750
+ filters,
1662
1751
  gatewayClass: instance.constructor,
1663
1752
  });
1664
1753
  }
@@ -1738,16 +1827,6 @@ export class CalyxApplication {
1738
1827
 
1739
1828
  const handlerInfo = gateway.handlers.get(event);
1740
1829
  if (handlerInfo) {
1741
- const args: any[] = [];
1742
- for (let i = 0; i < handlerInfo.paramMapping.length; i++) {
1743
- const type = handlerInfo.paramMapping[i];
1744
- if (type === 'body') {
1745
- args[i] = data;
1746
- } else if (type === 'socket') {
1747
- args[i] = ws;
1748
- }
1749
- }
1750
-
1751
1830
  const context = this.contextPool.acquire();
1752
1831
  context.resetContextWs(ws, data, handlerInfo.gatewayClass, gateway.instance[handlerInfo.propertyKey]);
1753
1832
 
@@ -1759,6 +1838,26 @@ export class CalyxApplication {
1759
1838
  }
1760
1839
  }
1761
1840
 
1841
+ const args: any[] = [];
1842
+ for (let i = 0; i < handlerInfo.paramMapping.length; i++) {
1843
+ const mapping = handlerInfo.paramMapping[i];
1844
+ if (!mapping) continue;
1845
+ if (mapping.type === 'body') {
1846
+ let val = mapping.name ? data?.[mapping.name] : data;
1847
+ for (const pipe of mapping.pipes || []) {
1848
+ const transformed = pipe.instance.transform(val, {
1849
+ type: 'body',
1850
+ metatype: undefined,
1851
+ data: mapping.name,
1852
+ });
1853
+ val = transformed instanceof Promise ? await transformed : transformed;
1854
+ }
1855
+ args[i] = val;
1856
+ } else if (mapping.type === 'socket') {
1857
+ args[i] = ws;
1858
+ }
1859
+ }
1860
+
1762
1861
  const nextCall = {
1763
1862
  handle: async () => {
1764
1863
  return gateway.instance[handlerInfo.propertyKey](...args);
@@ -1782,7 +1881,19 @@ export class CalyxApplication {
1782
1881
  ws.send(JSON.stringify(result));
1783
1882
  }
1784
1883
  } catch (err: any) {
1785
- // ignore or log
1884
+ if (handlerInfo.filters && handlerInfo.filters.length > 0) {
1885
+ for (const filter of handlerInfo.filters) {
1886
+ const catchException = Reflect.getMetadata('calyx:catch', filter.token) || [];
1887
+ if (catchException.length === 0 || catchException.some((exc: any) => err instanceof exc)) {
1888
+ const filterResult = filter.instance.catch(err, context);
1889
+ if (filterResult !== undefined) {
1890
+ ws.send(JSON.stringify(filterResult));
1891
+ return;
1892
+ }
1893
+ }
1894
+ }
1895
+ }
1896
+ ws.send(JSON.stringify({ event: 'error', data: err.message || 'Internal error' }));
1786
1897
  } finally {
1787
1898
  context.clearContext();
1788
1899
  this.contextPool.release(context);
@@ -82,6 +82,10 @@ export const Headers = (first?: any, ...pipes: any[]) => {
82
82
  return createHttpParamDecorator('headers', name, parsedPipes);
83
83
  };
84
84
 
85
+ export const Ip = () => createHttpParamDecorator('ip');
86
+ export const HostParam = (name?: string) => createHttpParamDecorator('hostparam', name);
87
+ export const Next = () => createHttpParamDecorator('next');
88
+
85
89
  export function HttpCode(code: number): MethodDecorator {
86
90
  return (target, propertyKey) => {
87
91
  Reflect.defineMetadata(METADATA_KEYS.HTTP_CODE, code, target, propertyKey);
package/src/index.ts CHANGED
@@ -24,4 +24,6 @@ export * from './http-client/index.ts';
24
24
  export * from './session/index.ts';
25
25
  export * from './mvc/index.ts';
26
26
  export * from './sse/index.ts';
27
+ export * from './terminus/terminus.ts';
28
+ export * from './cqrs/cqrs.ts';
27
29
 
@@ -0,0 +1,47 @@
1
+ import { Module, DynamicModule } from '../core/decorators.ts';
2
+ import { ClientTcp } from './client-tcp.ts';
3
+
4
+ export function Client(options: any): PropertyDecorator {
5
+ return (target: any, propertyKey: string | symbol) => {
6
+ let clientInstance: any = null;
7
+ Object.defineProperty(target, propertyKey, {
8
+ get() {
9
+ if (!clientInstance) {
10
+ clientInstance = new ClientTcp(options?.options || options || {});
11
+ }
12
+ return clientInstance;
13
+ },
14
+ configurable: true,
15
+ enumerable: true,
16
+ });
17
+ };
18
+ }
19
+
20
+ export interface ClientProviderConfig {
21
+ name: string;
22
+ transport?: any;
23
+ options?: {
24
+ host?: string;
25
+ port?: number;
26
+ };
27
+ }
28
+
29
+ @Module({})
30
+ export class ClientsModule {
31
+ static register(clients: ClientProviderConfig[]): DynamicModule {
32
+ const providers = clients.map((client) => {
33
+ return {
34
+ provide: client.name,
35
+ useFactory: () => {
36
+ return new ClientTcp(client.options || {});
37
+ },
38
+ };
39
+ });
40
+
41
+ return {
42
+ module: ClientsModule,
43
+ providers,
44
+ exports: clients.map((c) => c.name),
45
+ };
46
+ }
47
+ }
@@ -4,3 +4,4 @@ export * from './client-tcp.ts';
4
4
  export * from './decorators.ts';
5
5
  export * from './server-tcp.ts';
6
6
  export * from './microservice.ts';
7
+ export * from './clients.module.ts';
@@ -31,7 +31,7 @@ export class CalyxMicroservice {
31
31
  async listen(): Promise<any> {
32
32
  if (this.isListening) return;
33
33
 
34
- this.container.bootstrap(this.rootModule);
34
+ await this.container.bootstrap(this.rootModule);
35
35
  this.server.registerHandlers(this.container, this.globalGuards, this.globalInterceptors);
36
36
 
37
37
  const hostInfo = await this.server.listen();
@@ -1,28 +1,32 @@
1
1
  import 'reflect-metadata';
2
2
 
3
- export function Cron(expression: string): MethodDecorator {
3
+ export function Cron(expression: string, options?: { name?: string }): MethodDecorator {
4
4
  return (target, propertyKey) => {
5
5
  const constructor = target.constructor;
6
6
  const existing = Reflect.getOwnMetadata('calyx:cron', constructor) || [];
7
- existing.push({ expression, propertyKey });
7
+ existing.push({ expression, propertyKey, name: options?.name });
8
8
  Reflect.defineMetadata('calyx:cron', existing, constructor);
9
9
  };
10
10
  }
11
11
 
12
- export function Interval(ms: number): MethodDecorator {
12
+ export function Interval(nameOrMs: string | number, ms?: number): MethodDecorator {
13
13
  return (target, propertyKey) => {
14
14
  const constructor = target.constructor;
15
+ const name = typeof nameOrMs === 'string' ? nameOrMs : undefined;
16
+ const intervalMs = typeof nameOrMs === 'number' ? nameOrMs : ms!;
15
17
  const existing = Reflect.getOwnMetadata('calyx:interval', constructor) || [];
16
- existing.push({ ms, propertyKey });
18
+ existing.push({ ms: intervalMs, propertyKey, name });
17
19
  Reflect.defineMetadata('calyx:interval', existing, constructor);
18
20
  };
19
21
  }
20
22
 
21
- export function Timeout(ms: number): MethodDecorator {
23
+ export function Timeout(nameOrMs: string | number, ms?: number): MethodDecorator {
22
24
  return (target, propertyKey) => {
23
25
  const constructor = target.constructor;
26
+ const name = typeof nameOrMs === 'string' ? nameOrMs : undefined;
27
+ const timeoutMs = typeof nameOrMs === 'number' ? nameOrMs : ms!;
24
28
  const existing = Reflect.getOwnMetadata('calyx:timeout', constructor) || [];
25
- existing.push({ ms, propertyKey });
29
+ existing.push({ ms: timeoutMs, propertyKey, name });
26
30
  Reflect.defineMetadata('calyx:timeout', existing, constructor);
27
31
  };
28
32
  }
@@ -1,3 +1,4 @@
1
1
  export * from './decorators.ts';
2
2
  export * from './schedule.module.ts';
3
+ export * from './scheduler-registry.ts';
3
4
  export * from './cron.matcher.ts';
@@ -1,12 +1,13 @@
1
1
  import { Module, DynamicModule } from '../core/decorators.ts';
2
+ import { SchedulerRegistry } from './scheduler-registry.ts';
2
3
 
3
4
  @Module({})
4
5
  export class ScheduleModule {
5
6
  static forRoot(): DynamicModule {
6
7
  return {
7
8
  module: ScheduleModule,
8
- providers: [],
9
- exports: [],
9
+ providers: [SchedulerRegistry],
10
+ exports: [SchedulerRegistry],
10
11
  global: true,
11
12
  };
12
13
  }
@@ -0,0 +1,50 @@
1
+ import { Injectable } from '../core/decorators.ts';
2
+
3
+ @Injectable()
4
+ export class SchedulerRegistry {
5
+ private cronJobs = new Map<string, any>();
6
+ private intervals = new Map<string, any>();
7
+ private timeouts = new Map<string, any>();
8
+
9
+ // Cron
10
+ getCronJob(name: string) {
11
+ const job = this.cronJobs.get(name);
12
+ if (!job) throw new Error(`No Cron Job was found with the given name (${name})`);
13
+ return job;
14
+ }
15
+ getCronJobs(): Map<string, any> { return this.cronJobs; }
16
+ addCronJob(name: string, job: any) { this.cronJobs.set(name, job); }
17
+ deleteCronJob(name: string) {
18
+ const job = this.cronJobs.get(name);
19
+ if (job && typeof job.stop === 'function') job.stop();
20
+ this.cronJobs.delete(name);
21
+ }
22
+
23
+ // Interval
24
+ getInterval(name: string) {
25
+ const interval = this.intervals.get(name);
26
+ if (!interval) throw new Error(`No Interval was found with the given name (${name})`);
27
+ return interval;
28
+ }
29
+ getIntervals(): string[] { return Array.from(this.intervals.keys()); }
30
+ addInterval(name: string, intervalId: any) { this.intervals.set(name, intervalId); }
31
+ deleteInterval(name: string) {
32
+ const interval = this.intervals.get(name);
33
+ if (interval) clearInterval(interval);
34
+ this.intervals.delete(name);
35
+ }
36
+
37
+ // Timeout
38
+ getTimeout(name: string) {
39
+ const timeout = this.timeouts.get(name);
40
+ if (!timeout) throw new Error(`No Timeout was found with the given name (${name})`);
41
+ return timeout;
42
+ }
43
+ getTimeouts(): string[] { return Array.from(this.timeouts.keys()); }
44
+ addTimeout(name: string, timeoutId: any) { this.timeouts.set(name, timeoutId); }
45
+ deleteTimeout(name: string) {
46
+ const timeout = this.timeouts.get(name);
47
+ if (timeout) clearTimeout(timeout);
48
+ this.timeouts.delete(name);
49
+ }
50
+ }