@martel/calyx 1.4.0 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +14 -0
- package/package.json +1 -1
- package/src/config/config.module.ts +61 -0
- package/src/config/config.service.ts +24 -0
- package/src/config/index.ts +2 -0
- package/src/event-emitter/decorators.ts +10 -0
- package/src/event-emitter/event-emitter.module.ts +17 -0
- package/src/event-emitter/event-emitter.ts +61 -0
- package/src/event-emitter/index.ts +3 -0
- package/src/http/application.ts +336 -3
- package/src/http/factory.ts +7 -0
- package/src/index.ts +6 -0
- package/src/lifecycle/context.ts +75 -0
- package/src/lifecycle/interfaces.ts +3 -0
- package/src/microservices/client-proxy.ts +7 -0
- package/src/microservices/client-tcp.ts +127 -0
- package/src/microservices/decorators.ts +19 -0
- package/src/microservices/index.ts +6 -0
- package/src/microservices/interfaces.ts +11 -0
- package/src/microservices/microservice.ts +114 -0
- package/src/microservices/server-tcp.ts +210 -0
- package/src/schedule/cron.matcher.ts +45 -0
- package/src/schedule/decorators.ts +28 -0
- package/src/schedule/index.ts +3 -0
- package/src/schedule/schedule.module.ts +13 -0
- package/src/security/cors.middleware.ts +50 -0
- package/src/security/hashing.service.ts +12 -0
- package/src/security/helmet.middleware.ts +45 -0
- package/src/security/index.ts +3 -0
- package/src/websockets/decorators.ts +49 -0
- package/src/websockets/gateway.ts +11 -0
- package/src/websockets/index.ts +2 -0
- package/tests/config-event.test.ts +145 -0
- package/tests/microservices.test.ts +105 -0
- package/tests/rpc-ws-context.test.ts +135 -0
- package/tests/schedule.test.ts +64 -0
- package/tests/security.test.ts +89 -0
- package/tests/websockets.test.ts +125 -0
package/src/lifecycle/context.ts
CHANGED
|
@@ -4,6 +4,10 @@ import { Type } from '../core/metadata.ts';
|
|
|
4
4
|
export class CalyxArgumentsHost implements ArgumentsHost {
|
|
5
5
|
protected req!: Request;
|
|
6
6
|
protected res!: any;
|
|
7
|
+
protected type: 'http' | 'ws' | 'rpc' = 'http';
|
|
8
|
+
protected wsClient: any = null;
|
|
9
|
+
protected rpcContext: any = null;
|
|
10
|
+
protected data: any = null;
|
|
7
11
|
|
|
8
12
|
constructor(req?: Request, res?: any) {
|
|
9
13
|
if (req && res) {
|
|
@@ -12,16 +16,47 @@ export class CalyxArgumentsHost implements ArgumentsHost {
|
|
|
12
16
|
}
|
|
13
17
|
|
|
14
18
|
reset(req: Request, res: any) {
|
|
19
|
+
this.type = 'http';
|
|
15
20
|
this.req = req;
|
|
16
21
|
this.res = res;
|
|
22
|
+
this.wsClient = null;
|
|
23
|
+
this.rpcContext = null;
|
|
24
|
+
this.data = null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
resetWs(client: any, data: any) {
|
|
28
|
+
this.type = 'ws';
|
|
29
|
+
this.wsClient = client;
|
|
30
|
+
this.data = data;
|
|
31
|
+
this.req = null as any;
|
|
32
|
+
this.res = null;
|
|
33
|
+
this.rpcContext = null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
resetRpc(ctx: any, data: any) {
|
|
37
|
+
this.type = 'rpc';
|
|
38
|
+
this.rpcContext = ctx;
|
|
39
|
+
this.data = data;
|
|
40
|
+
this.req = null as any;
|
|
41
|
+
this.res = null;
|
|
42
|
+
this.wsClient = null;
|
|
17
43
|
}
|
|
18
44
|
|
|
19
45
|
clear() {
|
|
20
46
|
this.req = null as any;
|
|
21
47
|
this.res = null;
|
|
48
|
+
this.wsClient = null;
|
|
49
|
+
this.rpcContext = null;
|
|
50
|
+
this.data = null;
|
|
22
51
|
}
|
|
23
52
|
|
|
24
53
|
getArgs<T extends any[] = any[]>(): T {
|
|
54
|
+
if (this.type === 'ws') {
|
|
55
|
+
return [this.wsClient, this.data] as unknown as T;
|
|
56
|
+
}
|
|
57
|
+
if (this.type === 'rpc') {
|
|
58
|
+
return [this.data, this.rpcContext] as unknown as T;
|
|
59
|
+
}
|
|
25
60
|
return [this.req, this.res] as unknown as T;
|
|
26
61
|
}
|
|
27
62
|
|
|
@@ -36,6 +71,24 @@ export class CalyxArgumentsHost implements ArgumentsHost {
|
|
|
36
71
|
getNext: <T = any>() => (() => {}) as unknown as T,
|
|
37
72
|
};
|
|
38
73
|
}
|
|
74
|
+
|
|
75
|
+
switchToWs(): any {
|
|
76
|
+
return {
|
|
77
|
+
getClient: <T = any>() => this.wsClient as T,
|
|
78
|
+
getData: <T = any>() => this.data as T,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
switchToRpc(): any {
|
|
83
|
+
return {
|
|
84
|
+
getContext: <T = any>() => this.rpcContext as T,
|
|
85
|
+
getData: <T = any>() => this.data as T,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
getType<TContextType extends string = string>(): TContextType {
|
|
90
|
+
return this.type as TContextType;
|
|
91
|
+
}
|
|
39
92
|
}
|
|
40
93
|
|
|
41
94
|
export class CalyxExecutionContext extends CalyxArgumentsHost implements ExecutionContext {
|
|
@@ -65,6 +118,28 @@ export class CalyxExecutionContext extends CalyxArgumentsHost implements Executi
|
|
|
65
118
|
this.handlerMethod = handlerMethod;
|
|
66
119
|
}
|
|
67
120
|
|
|
121
|
+
resetContextWs(
|
|
122
|
+
client: any,
|
|
123
|
+
data: any,
|
|
124
|
+
targetClass: Type<any>,
|
|
125
|
+
handlerMethod: Function
|
|
126
|
+
) {
|
|
127
|
+
this.resetWs(client, data);
|
|
128
|
+
this.targetClass = targetClass;
|
|
129
|
+
this.handlerMethod = handlerMethod;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
resetContextRpc(
|
|
133
|
+
ctx: any,
|
|
134
|
+
data: any,
|
|
135
|
+
targetClass: Type<any>,
|
|
136
|
+
handlerMethod: Function
|
|
137
|
+
) {
|
|
138
|
+
this.resetRpc(ctx, data);
|
|
139
|
+
this.targetClass = targetClass;
|
|
140
|
+
this.handlerMethod = handlerMethod;
|
|
141
|
+
}
|
|
142
|
+
|
|
68
143
|
clearContext() {
|
|
69
144
|
this.clear();
|
|
70
145
|
this.targetClass = null as any;
|
|
@@ -4,6 +4,9 @@ export interface ArgumentsHost {
|
|
|
4
4
|
getArgs<T extends any[] = any[]>(): T;
|
|
5
5
|
getArgByIndex<T = any>(index: number): T;
|
|
6
6
|
switchToHttp(): HttpArgumentsHost;
|
|
7
|
+
switchToWs(): any;
|
|
8
|
+
switchToRpc(): any;
|
|
9
|
+
getType<TContextType extends string = string>(): TContextType;
|
|
7
10
|
}
|
|
8
11
|
|
|
9
12
|
export interface HttpArgumentsHost {
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { Observable } from 'rxjs';
|
|
2
|
+
|
|
3
|
+
export abstract class ClientProxy {
|
|
4
|
+
abstract send<TResult = any, TInput = any>(pattern: any, data: TInput): Observable<TResult>;
|
|
5
|
+
abstract emit<TResult = any, TInput = any>(pattern: any, data: TInput): Observable<TResult>;
|
|
6
|
+
abstract close(): void;
|
|
7
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { ClientProxy } from './client-proxy.ts';
|
|
2
|
+
import { Observable } from 'rxjs';
|
|
3
|
+
|
|
4
|
+
export class ClientTcp extends ClientProxy {
|
|
5
|
+
private socket: any;
|
|
6
|
+
private readonly host: string;
|
|
7
|
+
private readonly port: number;
|
|
8
|
+
private messageId = 0;
|
|
9
|
+
private pendingRequests = new Map<string, { next: (val: any) => void; error: (err: any) => void; complete: () => void }>();
|
|
10
|
+
private socketPromise: Promise<any> | null = null;
|
|
11
|
+
private buffer = '';
|
|
12
|
+
|
|
13
|
+
constructor(options: { host?: string; port?: number }) {
|
|
14
|
+
super();
|
|
15
|
+
this.host = options.host ?? '127.0.0.1';
|
|
16
|
+
this.port = options.port ?? 3000;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
private async getSocket(): Promise<any> {
|
|
20
|
+
if (this.socket) return this.socket;
|
|
21
|
+
if (this.socketPromise) return this.socketPromise;
|
|
22
|
+
|
|
23
|
+
this.socketPromise = (async () => {
|
|
24
|
+
const self = this;
|
|
25
|
+
const socket = await Bun.connect({
|
|
26
|
+
hostname: this.host,
|
|
27
|
+
port: this.port,
|
|
28
|
+
socket: {
|
|
29
|
+
data(socket, data) {
|
|
30
|
+
self.handleData(data);
|
|
31
|
+
},
|
|
32
|
+
error(socket, error) {
|
|
33
|
+
self.handleError(error);
|
|
34
|
+
},
|
|
35
|
+
close(socket) {
|
|
36
|
+
self.socket = null;
|
|
37
|
+
self.socketPromise = null;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
this.socket = socket;
|
|
42
|
+
return socket;
|
|
43
|
+
})();
|
|
44
|
+
|
|
45
|
+
return this.socketPromise;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
private handleData(data: Uint8Array) {
|
|
49
|
+
this.buffer += new TextDecoder().decode(data);
|
|
50
|
+
let boundary = this.buffer.indexOf('\n');
|
|
51
|
+
while (boundary !== -1) {
|
|
52
|
+
const messageStr = this.buffer.substring(0, boundary).trim();
|
|
53
|
+
this.buffer = this.buffer.substring(boundary + 1);
|
|
54
|
+
|
|
55
|
+
if (messageStr) {
|
|
56
|
+
try {
|
|
57
|
+
const parsed = JSON.parse(messageStr);
|
|
58
|
+
const { id, response, error, isDisposed } = parsed;
|
|
59
|
+
|
|
60
|
+
const pending = this.pendingRequests.get(id);
|
|
61
|
+
if (pending) {
|
|
62
|
+
if (error) {
|
|
63
|
+
pending.error(new Error(error));
|
|
64
|
+
this.pendingRequests.delete(id);
|
|
65
|
+
} else if (isDisposed) {
|
|
66
|
+
pending.complete();
|
|
67
|
+
this.pendingRequests.delete(id);
|
|
68
|
+
} else {
|
|
69
|
+
pending.next(response);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
} catch (err) {
|
|
73
|
+
// ignore parsing errors for partial or malformed frames
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
boundary = this.buffer.indexOf('\n');
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
private handleError(error: any) {
|
|
81
|
+
for (const pending of this.pendingRequests.values()) {
|
|
82
|
+
pending.error(error);
|
|
83
|
+
}
|
|
84
|
+
this.pendingRequests.clear();
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
send<TResult = any, TInput = any>(pattern: any, data: TInput): Observable<TResult> {
|
|
88
|
+
return new Observable<TResult>((observer) => {
|
|
89
|
+
this.messageId++;
|
|
90
|
+
const id = String(this.messageId);
|
|
91
|
+
this.pendingRequests.set(id, observer);
|
|
92
|
+
|
|
93
|
+
this.getSocket().then((socket) => {
|
|
94
|
+
const packet = JSON.stringify({ pattern, data, id }) + '\n';
|
|
95
|
+
socket.write(packet);
|
|
96
|
+
}).catch((err) => {
|
|
97
|
+
observer.error(err);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
return () => {
|
|
101
|
+
this.pendingRequests.delete(id);
|
|
102
|
+
};
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
emit<TResult = any, TInput = any>(pattern: any, data: TInput): Observable<TResult> {
|
|
107
|
+
return new Observable<TResult>((observer) => {
|
|
108
|
+
this.getSocket().then((socket) => {
|
|
109
|
+
const packet = JSON.stringify({ pattern, data }) + '\n';
|
|
110
|
+
socket.write(packet);
|
|
111
|
+
observer.next(undefined as any);
|
|
112
|
+
observer.complete();
|
|
113
|
+
}).catch((err) => {
|
|
114
|
+
observer.error(err);
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
close() {
|
|
120
|
+
if (this.socket) {
|
|
121
|
+
this.socket.end();
|
|
122
|
+
this.socket = null;
|
|
123
|
+
}
|
|
124
|
+
this.socketPromise = null;
|
|
125
|
+
this.pendingRequests.clear();
|
|
126
|
+
}
|
|
127
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import 'reflect-metadata';
|
|
2
|
+
|
|
3
|
+
export function MessagePattern(pattern: any): MethodDecorator {
|
|
4
|
+
return (target, propertyKey) => {
|
|
5
|
+
const constructor = target.constructor;
|
|
6
|
+
const existing = Reflect.getOwnMetadata('calyx:message_pattern', constructor) || [];
|
|
7
|
+
existing.push({ pattern, propertyKey });
|
|
8
|
+
Reflect.defineMetadata('calyx:message_pattern', existing, constructor);
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function EventPattern(pattern: any): MethodDecorator {
|
|
13
|
+
return (target, propertyKey) => {
|
|
14
|
+
const constructor = target.constructor;
|
|
15
|
+
const existing = Reflect.getOwnMetadata('calyx:event_pattern', constructor) || [];
|
|
16
|
+
existing.push({ pattern, propertyKey });
|
|
17
|
+
Reflect.defineMetadata('calyx:event_pattern', existing, constructor);
|
|
18
|
+
};
|
|
19
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { CalyxContainer } from '../core/container.ts';
|
|
2
|
+
import { ServerTcp } from './server-tcp.ts';
|
|
3
|
+
import { MicroserviceOptions, Transport } from './interfaces.ts';
|
|
4
|
+
|
|
5
|
+
export class CalyxMicroservice {
|
|
6
|
+
private readonly container = new CalyxContainer();
|
|
7
|
+
private readonly server: ServerTcp;
|
|
8
|
+
private isListening = false;
|
|
9
|
+
private cleanupListeners: (() => void)[] = [];
|
|
10
|
+
private readonly globalGuards: any[] = [];
|
|
11
|
+
private readonly globalInterceptors: any[] = [];
|
|
12
|
+
|
|
13
|
+
constructor(private readonly rootModule: any, options: MicroserviceOptions = {}) {
|
|
14
|
+
const transport = options.transport ?? Transport.TCP;
|
|
15
|
+
if (transport !== Transport.TCP) {
|
|
16
|
+
throw new Error(`Calyx microservice: Transport algorithm "${transport}" not supported`);
|
|
17
|
+
}
|
|
18
|
+
this.server = new ServerTcp(options.options);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
useGlobalGuards(...guards: any[]) {
|
|
22
|
+
this.globalGuards.push(...guards);
|
|
23
|
+
return this;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
useGlobalInterceptors(...interceptors: any[]) {
|
|
27
|
+
this.globalInterceptors.push(...interceptors);
|
|
28
|
+
return this;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async listen(): Promise<any> {
|
|
32
|
+
if (this.isListening) return;
|
|
33
|
+
|
|
34
|
+
this.container.bootstrap(this.rootModule);
|
|
35
|
+
this.server.registerHandlers(this.container, this.globalGuards, this.globalInterceptors);
|
|
36
|
+
|
|
37
|
+
const hostInfo = await this.server.listen();
|
|
38
|
+
this.isListening = true;
|
|
39
|
+
|
|
40
|
+
await this.runOnModuleInit();
|
|
41
|
+
await this.runOnApplicationBootstrap();
|
|
42
|
+
|
|
43
|
+
return hostInfo;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async close() {
|
|
47
|
+
if (!this.isListening) return;
|
|
48
|
+
|
|
49
|
+
await this.runShutdownHooks();
|
|
50
|
+
|
|
51
|
+
for (const cleanup of this.cleanupListeners) {
|
|
52
|
+
cleanup();
|
|
53
|
+
}
|
|
54
|
+
this.cleanupListeners = [];
|
|
55
|
+
|
|
56
|
+
this.server.close();
|
|
57
|
+
this.isListening = false;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
enableShutdownHooks(signals: string[] = ['SIGTERM', 'SIGINT']) {
|
|
61
|
+
const handler = async (signal: string) => {
|
|
62
|
+
await this.close();
|
|
63
|
+
process.exit(0);
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
for (const signal of signals) {
|
|
67
|
+
const listener = () => handler(signal);
|
|
68
|
+
process.on(signal as any, listener);
|
|
69
|
+
this.cleanupListeners.push(() => {
|
|
70
|
+
process.off(signal as any, listener);
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
private async runOnModuleInit() {
|
|
76
|
+
const instances = this.container.getProviderAndControllerInstances();
|
|
77
|
+
for (const instance of instances) {
|
|
78
|
+
if (instance && typeof instance.onModuleInit === 'function') {
|
|
79
|
+
await instance.onModuleInit();
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
private async runOnApplicationBootstrap() {
|
|
85
|
+
const instances = this.container.getProviderAndControllerInstances();
|
|
86
|
+
for (const instance of instances) {
|
|
87
|
+
if (instance && typeof instance.onApplicationBootstrap === 'function') {
|
|
88
|
+
await instance.onApplicationBootstrap();
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
private async runShutdownHooks() {
|
|
94
|
+
const instances = this.container.getProviderAndControllerInstances();
|
|
95
|
+
|
|
96
|
+
for (const instance of instances) {
|
|
97
|
+
if (instance && typeof instance.onModuleDestroy === 'function') {
|
|
98
|
+
await instance.onModuleDestroy();
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
for (const instance of instances) {
|
|
103
|
+
if (instance && typeof instance.beforeApplicationShutdown === 'function') {
|
|
104
|
+
await instance.beforeApplicationShutdown();
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
for (const instance of instances) {
|
|
109
|
+
if (instance && typeof instance.onApplicationShutdown === 'function') {
|
|
110
|
+
await instance.onApplicationShutdown();
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { CalyxContainer } from '../core/container.ts';
|
|
2
|
+
import { METADATA_KEYS } from '../core/metadata.ts';
|
|
3
|
+
import { CalyxExecutionContext } from '../lifecycle/context.ts';
|
|
4
|
+
|
|
5
|
+
export class ServerTcp {
|
|
6
|
+
private server: any;
|
|
7
|
+
private readonly host: string;
|
|
8
|
+
private readonly port: number;
|
|
9
|
+
private readonly messageHandlers = new Map<string, {
|
|
10
|
+
instance: any;
|
|
11
|
+
propertyKey: string | symbol;
|
|
12
|
+
guards: any[];
|
|
13
|
+
interceptors: any[];
|
|
14
|
+
controllerClass: any;
|
|
15
|
+
}>();
|
|
16
|
+
private readonly eventHandlers = new Map<string, { instance: any; propertyKey: string | symbol }>();
|
|
17
|
+
|
|
18
|
+
constructor(options: { host?: string; port?: number } = {}) {
|
|
19
|
+
this.host = options.host ?? '127.0.0.1';
|
|
20
|
+
this.port = options.port ?? 3000;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
registerHandlers(
|
|
24
|
+
container: CalyxContainer,
|
|
25
|
+
globalGuards: any[] = [],
|
|
26
|
+
globalInterceptors: any[] = []
|
|
27
|
+
) {
|
|
28
|
+
const instances = container.getProviderAndControllerInstances();
|
|
29
|
+
for (const instance of instances) {
|
|
30
|
+
if (!instance || !instance.constructor) continue;
|
|
31
|
+
|
|
32
|
+
const msgPatterns: { pattern: any; propertyKey: string | symbol }[] =
|
|
33
|
+
Reflect.getMetadata('calyx:message_pattern', instance.constructor) || [];
|
|
34
|
+
for (const handler of msgPatterns) {
|
|
35
|
+
const patternStr = typeof handler.pattern === 'string' ? handler.pattern : JSON.stringify(handler.pattern);
|
|
36
|
+
|
|
37
|
+
const classGuards = Reflect.getMetadata(METADATA_KEYS.GUARDS, instance.constructor) || [];
|
|
38
|
+
const methodGuards = Reflect.getMetadata(METADATA_KEYS.GUARDS, instance.constructor.prototype, handler.propertyKey) || [];
|
|
39
|
+
const guards = this.compileLifecycleItems(container, [...globalGuards, ...classGuards, ...methodGuards]);
|
|
40
|
+
|
|
41
|
+
const classInterceptors = Reflect.getMetadata(METADATA_KEYS.INTERCEPTORS, instance.constructor) || [];
|
|
42
|
+
const methodInterceptors = Reflect.getMetadata(METADATA_KEYS.INTERCEPTORS, instance.constructor.prototype, handler.propertyKey) || [];
|
|
43
|
+
const interceptors = this.compileLifecycleItems(container, [...globalInterceptors, ...classInterceptors, ...methodInterceptors]);
|
|
44
|
+
|
|
45
|
+
this.messageHandlers.set(patternStr, {
|
|
46
|
+
instance,
|
|
47
|
+
propertyKey: handler.propertyKey,
|
|
48
|
+
guards,
|
|
49
|
+
interceptors,
|
|
50
|
+
controllerClass: instance.constructor,
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const eventPatterns: { pattern: any; propertyKey: string | symbol }[] =
|
|
55
|
+
Reflect.getMetadata('calyx:event_pattern', instance.constructor) || [];
|
|
56
|
+
for (const handler of eventPatterns) {
|
|
57
|
+
const patternStr = typeof handler.pattern === 'string' ? handler.pattern : JSON.stringify(handler.pattern);
|
|
58
|
+
this.eventHandlers.set(patternStr, { instance, propertyKey: handler.propertyKey });
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
private compileLifecycleItems(
|
|
64
|
+
container: CalyxContainer,
|
|
65
|
+
items: any[]
|
|
66
|
+
): any[] {
|
|
67
|
+
return items.map((item) => {
|
|
68
|
+
if (typeof item === 'function') {
|
|
69
|
+
const proto = item.prototype;
|
|
70
|
+
const hasLifecycle = proto && (proto.canActivate || proto.intercept);
|
|
71
|
+
if (hasLifecycle) {
|
|
72
|
+
try {
|
|
73
|
+
return { instance: container.resolveTokenGlobally(item) };
|
|
74
|
+
} catch {
|
|
75
|
+
return { instance: new item() };
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return { instance: { canActivate: item, intercept: item } };
|
|
79
|
+
}
|
|
80
|
+
return { instance: item };
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
listen(): Promise<any> {
|
|
85
|
+
const self = this;
|
|
86
|
+
return new Promise((resolve, reject) => {
|
|
87
|
+
try {
|
|
88
|
+
this.server = Bun.listen({
|
|
89
|
+
hostname: this.host,
|
|
90
|
+
port: this.port,
|
|
91
|
+
socket: {
|
|
92
|
+
data(socket, data) {
|
|
93
|
+
self.handleSocketData(socket, data);
|
|
94
|
+
},
|
|
95
|
+
error(socket, err) {
|
|
96
|
+
console.error(`Microservice socket error:`, err);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
resolve(this.server);
|
|
101
|
+
} catch (err) {
|
|
102
|
+
reject(err);
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
private handleSocketData(socket: any, data: Uint8Array) {
|
|
108
|
+
if (!socket.data) {
|
|
109
|
+
socket.data = { buffer: '' };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
socket.data.buffer += new TextDecoder().decode(data);
|
|
113
|
+
let boundary = socket.data.buffer.indexOf('\n');
|
|
114
|
+
while (boundary !== -1) {
|
|
115
|
+
const messageStr = socket.data.buffer.substring(0, boundary).trim();
|
|
116
|
+
socket.data.buffer = socket.data.buffer.substring(boundary + 1);
|
|
117
|
+
|
|
118
|
+
if (messageStr) {
|
|
119
|
+
this.handleMessage(socket, messageStr);
|
|
120
|
+
}
|
|
121
|
+
boundary = socket.data.buffer.indexOf('\n');
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
private async handleMessage(socket: any, messageStr: string) {
|
|
126
|
+
try {
|
|
127
|
+
const parsed = JSON.parse(messageStr);
|
|
128
|
+
const { pattern, data, id } = parsed;
|
|
129
|
+
const patternStr = typeof pattern === 'string' ? pattern : JSON.stringify(pattern);
|
|
130
|
+
|
|
131
|
+
if (id !== undefined) {
|
|
132
|
+
const handler = this.messageHandlers.get(patternStr);
|
|
133
|
+
if (!handler) {
|
|
134
|
+
socket.write(JSON.stringify({ id, error: `Handler for pattern "${patternStr}" not found` }) + '\n');
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const context = new CalyxExecutionContext();
|
|
139
|
+
context.resetContextRpc(socket, data, handler.controllerClass, handler.instance[handler.propertyKey]);
|
|
140
|
+
|
|
141
|
+
try {
|
|
142
|
+
for (const guard of handler.guards) {
|
|
143
|
+
const canActive = await guard.instance.canActivate(context);
|
|
144
|
+
if (!canActive) {
|
|
145
|
+
throw new Error('Forbidden resource');
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const nextCall = {
|
|
150
|
+
handle: async () => {
|
|
151
|
+
return handler.instance[handler.propertyKey](data);
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
let chain = nextCall.handle;
|
|
156
|
+
for (let i = handler.interceptors.length - 1; i >= 0; i--) {
|
|
157
|
+
const interceptor = handler.interceptors[i];
|
|
158
|
+
const currentChain = chain;
|
|
159
|
+
chain = async () => {
|
|
160
|
+
return interceptor.instance.intercept(context, {
|
|
161
|
+
handle: () => currentChain()
|
|
162
|
+
});
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const result = await chain();
|
|
167
|
+
|
|
168
|
+
if (result && typeof result.subscribe === 'function') {
|
|
169
|
+
result.subscribe({
|
|
170
|
+
next: (val: any) => {
|
|
171
|
+
socket.write(JSON.stringify({ id, response: val }) + '\n');
|
|
172
|
+
},
|
|
173
|
+
error: (err: any) => {
|
|
174
|
+
socket.write(JSON.stringify({ id, error: err.message }) + '\n');
|
|
175
|
+
},
|
|
176
|
+
complete: () => {
|
|
177
|
+
socket.write(JSON.stringify({ id, isDisposed: true }) + '\n');
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
} else {
|
|
181
|
+
socket.write(JSON.stringify({ id, response: result }) + '\n');
|
|
182
|
+
socket.write(JSON.stringify({ id, isDisposed: true }) + '\n');
|
|
183
|
+
}
|
|
184
|
+
} catch (err: any) {
|
|
185
|
+
socket.write(JSON.stringify({ id, error: err.message }) + '\n');
|
|
186
|
+
} finally {
|
|
187
|
+
context.clearContext();
|
|
188
|
+
}
|
|
189
|
+
} else {
|
|
190
|
+
const handler = this.eventHandlers.get(patternStr);
|
|
191
|
+
if (handler) {
|
|
192
|
+
try {
|
|
193
|
+
handler.instance[handler.propertyKey](data);
|
|
194
|
+
} catch (err) {
|
|
195
|
+
console.error(`Error executing EventPattern handler for pattern "${patternStr}":`, err);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
} catch {
|
|
200
|
+
// ignore
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
close() {
|
|
205
|
+
if (this.server) {
|
|
206
|
+
this.server.stop();
|
|
207
|
+
this.server = null;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
export class CronMatcher {
|
|
2
|
+
static match(pattern: string, date: Date): boolean {
|
|
3
|
+
const parts = pattern.split(' ');
|
|
4
|
+
if (parts.length < 5) return false;
|
|
5
|
+
|
|
6
|
+
const hasSeconds = parts.length === 6;
|
|
7
|
+
const sec = hasSeconds ? date.getSeconds() : 0;
|
|
8
|
+
const min = date.getMinutes();
|
|
9
|
+
const hour = date.getHours();
|
|
10
|
+
const dom = date.getDate();
|
|
11
|
+
const month = date.getMonth() + 1; // 1-12
|
|
12
|
+
const dow = date.getDay(); // 0-6 (Sunday-Saturday)
|
|
13
|
+
|
|
14
|
+
const matchPart = (part: string, val: number): boolean => {
|
|
15
|
+
if (part === '*') return true;
|
|
16
|
+
if (part.includes('/')) {
|
|
17
|
+
const [left, right] = part.split('/');
|
|
18
|
+
const step = Number(right);
|
|
19
|
+
if (left === '*') {
|
|
20
|
+
return val % step === 0;
|
|
21
|
+
}
|
|
22
|
+
const start = Number(left);
|
|
23
|
+
return val >= start && (val - start) % step === 0;
|
|
24
|
+
}
|
|
25
|
+
if (part.includes(',')) {
|
|
26
|
+
return part.split(',').map(Number).includes(val);
|
|
27
|
+
}
|
|
28
|
+
if (part.includes('-')) {
|
|
29
|
+
const [start, end] = part.split('-').map(Number);
|
|
30
|
+
return val >= start && val <= end;
|
|
31
|
+
}
|
|
32
|
+
return Number(part) === val;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
if (hasSeconds && !matchPart(parts[0], sec)) return false;
|
|
36
|
+
const offset = hasSeconds ? 1 : 0;
|
|
37
|
+
if (!matchPart(parts[offset + 0], min)) return false;
|
|
38
|
+
if (!matchPart(parts[offset + 1], hour)) return false;
|
|
39
|
+
if (!matchPart(parts[offset + 2], dom)) return false;
|
|
40
|
+
if (!matchPart(parts[offset + 3], month)) return false;
|
|
41
|
+
if (!matchPart(parts[offset + 4], dow)) return false;
|
|
42
|
+
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import 'reflect-metadata';
|
|
2
|
+
|
|
3
|
+
export function Cron(expression: string): MethodDecorator {
|
|
4
|
+
return (target, propertyKey) => {
|
|
5
|
+
const constructor = target.constructor;
|
|
6
|
+
const existing = Reflect.getOwnMetadata('calyx:cron', constructor) || [];
|
|
7
|
+
existing.push({ expression, propertyKey });
|
|
8
|
+
Reflect.defineMetadata('calyx:cron', existing, constructor);
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function Interval(ms: number): MethodDecorator {
|
|
13
|
+
return (target, propertyKey) => {
|
|
14
|
+
const constructor = target.constructor;
|
|
15
|
+
const existing = Reflect.getOwnMetadata('calyx:interval', constructor) || [];
|
|
16
|
+
existing.push({ ms, propertyKey });
|
|
17
|
+
Reflect.defineMetadata('calyx:interval', existing, constructor);
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function Timeout(ms: number): MethodDecorator {
|
|
22
|
+
return (target, propertyKey) => {
|
|
23
|
+
const constructor = target.constructor;
|
|
24
|
+
const existing = Reflect.getOwnMetadata('calyx:timeout', constructor) || [];
|
|
25
|
+
existing.push({ ms, propertyKey });
|
|
26
|
+
Reflect.defineMetadata('calyx:timeout', existing, constructor);
|
|
27
|
+
};
|
|
28
|
+
}
|