@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/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,17 @@
|
|
|
1
|
+
# [1.6.0](https://github.com/bmartel/calyx/compare/v1.5.0...v1.6.0) (2026-07-01)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Features
|
|
5
|
+
|
|
6
|
+
* **websockets,microservices:** implement WebSocketGateways, TCP Microservices, and context switching for Guards/Interceptors ([a8cde0d](https://github.com/bmartel/calyx/commit/a8cde0d68411a3ab736fe1df00ae04672588b1af))
|
|
7
|
+
|
|
8
|
+
# [1.5.0](https://github.com/bmartel/calyx/compare/v1.4.0...v1.5.0) (2026-07-01)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Features
|
|
12
|
+
|
|
13
|
+
* **security,schedule:** implement ConfigModule, EventEmitterModule, HashingService, CORS, Helmet, and Task Scheduler ([6cfa8ca](https://github.com/bmartel/calyx/commit/6cfa8ca9459d73c7a7b1087953991d675d3a55db))
|
|
14
|
+
|
|
1
15
|
# [1.4.0](https://github.com/bmartel/calyx/compare/v1.3.0...v1.4.0) (2026-07-01)
|
|
2
16
|
|
|
3
17
|
|
package/package.json
CHANGED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'fs';
|
|
2
|
+
import { Module, DynamicModule } from '../core/decorators.ts';
|
|
3
|
+
import { ConfigService } from './config.service.ts';
|
|
4
|
+
|
|
5
|
+
export interface ConfigModuleOptions {
|
|
6
|
+
isGlobal?: boolean;
|
|
7
|
+
load?: (() => Record<string, any>)[];
|
|
8
|
+
envFilePath?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
@Module({
|
|
12
|
+
providers: [ConfigService],
|
|
13
|
+
exports: [ConfigService],
|
|
14
|
+
})
|
|
15
|
+
export class ConfigModule {
|
|
16
|
+
static forRoot(options: ConfigModuleOptions = {}): DynamicModule {
|
|
17
|
+
const configData: Record<string, string> = { ...(process.env as Record<string, string>) };
|
|
18
|
+
|
|
19
|
+
if (options.envFilePath && existsSync(options.envFilePath)) {
|
|
20
|
+
try {
|
|
21
|
+
const text = readFileSync(options.envFilePath, 'utf-8');
|
|
22
|
+
if (text) {
|
|
23
|
+
const lines = text.split('\n');
|
|
24
|
+
for (const line of lines) {
|
|
25
|
+
const trimmed = line.trim();
|
|
26
|
+
if (!trimmed || trimmed.startsWith('#')) continue;
|
|
27
|
+
const eqIdx = trimmed.indexOf('=');
|
|
28
|
+
if (eqIdx !== -1) {
|
|
29
|
+
const key = trimmed.substring(0, eqIdx).trim();
|
|
30
|
+
const val = trimmed.substring(eqIdx + 1).trim();
|
|
31
|
+
configData[key] = val.replace(/^['"]|['"]$/g, '');
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
} catch (err) {
|
|
36
|
+
// ignore read error
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (options.load) {
|
|
41
|
+
for (const factory of options.load) {
|
|
42
|
+
const data = factory();
|
|
43
|
+
for (const [key, val] of Object.entries(data)) {
|
|
44
|
+
configData[key] = String(val);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const configServiceProvider = {
|
|
50
|
+
provide: ConfigService,
|
|
51
|
+
useValue: new ConfigService(configData),
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
module: ConfigModule,
|
|
56
|
+
providers: [configServiceProvider],
|
|
57
|
+
exports: [ConfigService],
|
|
58
|
+
global: options.isGlobal ?? false,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { Injectable } from '../core/decorators.ts';
|
|
2
|
+
|
|
3
|
+
@Injectable()
|
|
4
|
+
export class ConfigService {
|
|
5
|
+
private readonly env: Record<string, string> = {};
|
|
6
|
+
|
|
7
|
+
constructor(internalConfig?: Record<string, string>) {
|
|
8
|
+
this.env = internalConfig ?? (process.env as Record<string, string>);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
get<T = string>(path: string, defaultValue?: T): T {
|
|
12
|
+
const val = this.env[path];
|
|
13
|
+
if (val === undefined) {
|
|
14
|
+
return defaultValue as T;
|
|
15
|
+
}
|
|
16
|
+
if (val === 'true') return true as unknown as T;
|
|
17
|
+
if (val === 'false') return false as unknown as T;
|
|
18
|
+
const num = Number(val);
|
|
19
|
+
if (!isNaN(num) && val.trim() !== '') {
|
|
20
|
+
return num as unknown as T;
|
|
21
|
+
}
|
|
22
|
+
return val as unknown as T;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import 'reflect-metadata';
|
|
2
|
+
|
|
3
|
+
export function OnEvent(event: string): MethodDecorator {
|
|
4
|
+
return (target, propertyKey) => {
|
|
5
|
+
const constructor = target.constructor;
|
|
6
|
+
const existing = Reflect.getOwnMetadata('calyx:on_event', constructor) || [];
|
|
7
|
+
existing.push({ event, propertyKey });
|
|
8
|
+
Reflect.defineMetadata('calyx:on_event', existing, constructor);
|
|
9
|
+
};
|
|
10
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { Module, DynamicModule } from '../core/decorators.ts';
|
|
2
|
+
import { EventEmitter } from './event-emitter.ts';
|
|
3
|
+
|
|
4
|
+
@Module({
|
|
5
|
+
providers: [EventEmitter],
|
|
6
|
+
exports: [EventEmitter],
|
|
7
|
+
})
|
|
8
|
+
export class EventEmitterModule {
|
|
9
|
+
static forRoot(): DynamicModule {
|
|
10
|
+
return {
|
|
11
|
+
module: EventEmitterModule,
|
|
12
|
+
providers: [EventEmitter],
|
|
13
|
+
exports: [EventEmitter],
|
|
14
|
+
global: true,
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { Injectable } from '../core/decorators.ts';
|
|
2
|
+
|
|
3
|
+
@Injectable()
|
|
4
|
+
export class EventEmitter {
|
|
5
|
+
private readonly listeners = new Map<string, Function[]>();
|
|
6
|
+
private readonly wildcardListeners: { pattern: RegExp; fn: Function }[] = [];
|
|
7
|
+
|
|
8
|
+
on(event: string, fn: Function) {
|
|
9
|
+
if (event.includes('*')) {
|
|
10
|
+
const regexStr = '^' + event.replace(/\./g, '\\.').replace(/\*/g, '.*') + '$';
|
|
11
|
+
this.wildcardListeners.push({ pattern: new RegExp(regexStr), fn });
|
|
12
|
+
} else {
|
|
13
|
+
let list = this.listeners.get(event);
|
|
14
|
+
if (!list) {
|
|
15
|
+
list = [];
|
|
16
|
+
this.listeners.set(event, list);
|
|
17
|
+
}
|
|
18
|
+
list.push(fn);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
emit(event: string, ...args: any[]) {
|
|
23
|
+
const list = this.listeners.get(event);
|
|
24
|
+
if (list) {
|
|
25
|
+
for (const fn of list) {
|
|
26
|
+
fn(...args);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
for (const item of this.wildcardListeners) {
|
|
31
|
+
if (item.pattern.test(event)) {
|
|
32
|
+
item.fn(...args);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async emitAsync(event: string, ...args: any[]): Promise<any[]> {
|
|
38
|
+
const promises: Promise<any>[] = [];
|
|
39
|
+
|
|
40
|
+
const list = this.listeners.get(event);
|
|
41
|
+
if (list) {
|
|
42
|
+
for (const fn of list) {
|
|
43
|
+
const res = fn(...args);
|
|
44
|
+
if (res instanceof Promise) {
|
|
45
|
+
promises.push(res);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
for (const item of this.wildcardListeners) {
|
|
51
|
+
if (item.pattern.test(event)) {
|
|
52
|
+
const res = item.fn(...args);
|
|
53
|
+
if (res instanceof Promise) {
|
|
54
|
+
promises.push(res);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return Promise.all(promises);
|
|
60
|
+
}
|
|
61
|
+
}
|
package/src/http/application.ts
CHANGED
|
@@ -6,6 +6,10 @@ import { HttpException, NotFoundException } from './exceptions.ts';
|
|
|
6
6
|
import { ArgumentsHost, ExecutionContext } from '../lifecycle/interfaces.ts';
|
|
7
7
|
import { CalyxArgumentsHost, CalyxExecutionContext } from '../lifecycle/context.ts';
|
|
8
8
|
import { CalyxMiddlewareConsumer, MiddlewareConfiguration, RequestMethodMap, CompiledLifecycleItem as CompiledMiddlewareItem } from './middleware.ts';
|
|
9
|
+
import { EventEmitter } from '../event-emitter/event-emitter.ts';
|
|
10
|
+
import { cors, CorsOptions } from '../security/cors.middleware.ts';
|
|
11
|
+
import { helmet, HelmetOptions } from '../security/helmet.middleware.ts';
|
|
12
|
+
import { CronMatcher } from '../schedule/cron.matcher.ts';
|
|
9
13
|
|
|
10
14
|
class ObjectPool<T> {
|
|
11
15
|
private pool: T[] = [];
|
|
@@ -95,6 +99,9 @@ export class CalyxApplication {
|
|
|
95
99
|
private middlewareConfigurations: MiddlewareConfiguration[] = [];
|
|
96
100
|
private hostPool = new ObjectPool<CalyxArgumentsHost>(() => new CalyxArgumentsHost());
|
|
97
101
|
private contextPool = new ObjectPool<CalyxExecutionContext>(() => new CalyxExecutionContext());
|
|
102
|
+
private sharedWebSockets: any[] = [];
|
|
103
|
+
private hasWebSockets = false;
|
|
104
|
+
private serverPort = 3000;
|
|
98
105
|
|
|
99
106
|
use(...middlewares: any[]) {
|
|
100
107
|
this.globalMiddlewares.push(...middlewares);
|
|
@@ -109,6 +116,16 @@ export class CalyxApplication {
|
|
|
109
116
|
this.globalInterceptors.push(...interceptors);
|
|
110
117
|
}
|
|
111
118
|
|
|
119
|
+
enableCors(options?: CorsOptions) {
|
|
120
|
+
this.use(cors(options));
|
|
121
|
+
return this;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
useHelmet(options?: HelmetOptions) {
|
|
125
|
+
this.use(helmet(options));
|
|
126
|
+
return this;
|
|
127
|
+
}
|
|
128
|
+
|
|
112
129
|
useGlobalPipes(...pipes: any[]) {
|
|
113
130
|
this.globalPipes.push(...pipes);
|
|
114
131
|
}
|
|
@@ -129,6 +146,15 @@ export class CalyxApplication {
|
|
|
129
146
|
// Build the routing table from registered controllers
|
|
130
147
|
this.buildRoutes();
|
|
131
148
|
|
|
149
|
+
// Register Event listeners
|
|
150
|
+
this.registerEventListeners();
|
|
151
|
+
|
|
152
|
+
// Register Scheduled tasks
|
|
153
|
+
this.registerScheduledTasks();
|
|
154
|
+
|
|
155
|
+
// Register WebSocket Gateways
|
|
156
|
+
this.registerWebSocketGateways();
|
|
157
|
+
|
|
132
158
|
// Call OnModuleInit hooks
|
|
133
159
|
await this.runOnModuleInit();
|
|
134
160
|
|
|
@@ -908,11 +934,43 @@ export class CalyxApplication {
|
|
|
908
934
|
}
|
|
909
935
|
|
|
910
936
|
async listen(port: number): Promise<any> {
|
|
937
|
+
this.serverPort = port;
|
|
911
938
|
await this.init();
|
|
912
|
-
|
|
939
|
+
|
|
940
|
+
const fetchHandler = (req: Request, server: any) => {
|
|
941
|
+
if (this.hasWebSockets && req.headers.get('upgrade') === 'websocket') {
|
|
942
|
+
const success = server.upgrade(req);
|
|
943
|
+
if (success) return undefined;
|
|
944
|
+
}
|
|
945
|
+
return this.handleRequest(req);
|
|
946
|
+
};
|
|
947
|
+
|
|
948
|
+
const serveOptions: any = {
|
|
913
949
|
port,
|
|
914
|
-
fetch:
|
|
915
|
-
}
|
|
950
|
+
fetch: fetchHandler,
|
|
951
|
+
};
|
|
952
|
+
|
|
953
|
+
if (this.hasWebSockets) {
|
|
954
|
+
serveOptions.websocket = {
|
|
955
|
+
open: (ws: any) => {
|
|
956
|
+
for (const gateway of this.sharedWebSockets) {
|
|
957
|
+
this.dispatchWsConnection(gateway, ws);
|
|
958
|
+
}
|
|
959
|
+
},
|
|
960
|
+
message: (ws: any, message: any) => {
|
|
961
|
+
for (const gateway of this.sharedWebSockets) {
|
|
962
|
+
this.dispatchWsMessage(gateway, ws, message);
|
|
963
|
+
}
|
|
964
|
+
},
|
|
965
|
+
close: (ws: any) => {
|
|
966
|
+
for (const gateway of this.sharedWebSockets) {
|
|
967
|
+
this.dispatchWsDisconnect(gateway, ws);
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
};
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
this.server = Bun.serve(serveOptions);
|
|
916
974
|
return this.server;
|
|
917
975
|
}
|
|
918
976
|
|
|
@@ -989,4 +1047,279 @@ export class CalyxApplication {
|
|
|
989
1047
|
}
|
|
990
1048
|
}
|
|
991
1049
|
}
|
|
1050
|
+
|
|
1051
|
+
private registerEventListeners() {
|
|
1052
|
+
let eventEmitter: EventEmitter;
|
|
1053
|
+
try {
|
|
1054
|
+
eventEmitter = this.container.getGlobalOrAnyInstance(EventEmitter);
|
|
1055
|
+
} catch {
|
|
1056
|
+
return;
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
const instances = this.container.getProviderAndControllerInstances();
|
|
1060
|
+
for (const instance of instances) {
|
|
1061
|
+
if (!instance || !instance.constructor) continue;
|
|
1062
|
+
const listeners: { event: string; propertyKey: string | symbol }[] =
|
|
1063
|
+
Reflect.getMetadata('calyx:on_event', instance.constructor) || [];
|
|
1064
|
+
|
|
1065
|
+
for (const listener of listeners) {
|
|
1066
|
+
eventEmitter.on(listener.event, (...args: any[]) => {
|
|
1067
|
+
return instance[listener.propertyKey](...args);
|
|
1068
|
+
});
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
private registerScheduledTasks() {
|
|
1074
|
+
let hasScheduleModule = false;
|
|
1075
|
+
for (const moduleClass of this.container.getModules().keys()) {
|
|
1076
|
+
if (moduleClass.name === 'ScheduleModule') {
|
|
1077
|
+
hasScheduleModule = true;
|
|
1078
|
+
break;
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
if (!hasScheduleModule) {
|
|
1082
|
+
return;
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
const instances = this.container.getProviderAndControllerInstances();
|
|
1086
|
+
for (const instance of instances) {
|
|
1087
|
+
if (!instance || !instance.constructor) continue;
|
|
1088
|
+
|
|
1089
|
+
const crons: { expression: string; propertyKey: string | symbol }[] =
|
|
1090
|
+
Reflect.getMetadata('calyx:cron', instance.constructor) || [];
|
|
1091
|
+
for (const cron of crons) {
|
|
1092
|
+
const parts = cron.expression.split(' ');
|
|
1093
|
+
const isSecondLevel = parts.length === 6;
|
|
1094
|
+
|
|
1095
|
+
let lastRan = 0;
|
|
1096
|
+
const tick = () => {
|
|
1097
|
+
const now = new Date();
|
|
1098
|
+
const nowMs = now.getTime();
|
|
1099
|
+
const timeUnit = isSecondLevel ? Math.floor(nowMs / 1000) : Math.floor(nowMs / 60000);
|
|
1100
|
+
if (timeUnit === lastRan) return;
|
|
1101
|
+
|
|
1102
|
+
if (CronMatcher.match(cron.expression, now)) {
|
|
1103
|
+
lastRan = timeUnit;
|
|
1104
|
+
try {
|
|
1105
|
+
instance[cron.propertyKey]();
|
|
1106
|
+
} catch (err) {
|
|
1107
|
+
console.error(`Error executing cron task ${String(cron.propertyKey)}:`, err);
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
};
|
|
1111
|
+
|
|
1112
|
+
const intervalMs = isSecondLevel ? 1000 : 20000;
|
|
1113
|
+
const timer = setInterval(tick, intervalMs);
|
|
1114
|
+
this.cleanupListeners.push(() => clearInterval(timer));
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
const intervals: { ms: number; propertyKey: string | symbol }[] =
|
|
1118
|
+
Reflect.getMetadata('calyx:interval', instance.constructor) || [];
|
|
1119
|
+
for (const interval of intervals) {
|
|
1120
|
+
const timer = setInterval(() => {
|
|
1121
|
+
try {
|
|
1122
|
+
instance[interval.propertyKey]();
|
|
1123
|
+
} catch (err) {
|
|
1124
|
+
console.error(`Error executing interval task ${String(interval.propertyKey)}:`, err);
|
|
1125
|
+
}
|
|
1126
|
+
}, interval.ms);
|
|
1127
|
+
this.cleanupListeners.push(() => clearInterval(timer));
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
const timeouts: { ms: number; propertyKey: string | symbol }[] =
|
|
1131
|
+
Reflect.getMetadata('calyx:timeout', instance.constructor) || [];
|
|
1132
|
+
for (const timeout of timeouts) {
|
|
1133
|
+
const timer = setTimeout(() => {
|
|
1134
|
+
try {
|
|
1135
|
+
instance[timeout.propertyKey]();
|
|
1136
|
+
} catch (err) {
|
|
1137
|
+
console.error(`Error executing timeout task ${String(timeout.propertyKey)}:`, err);
|
|
1138
|
+
}
|
|
1139
|
+
}, timeout.ms);
|
|
1140
|
+
this.cleanupListeners.push(() => clearTimeout(timer));
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
private registerWebSocketGateways() {
|
|
1146
|
+
const instances = this.container.getProviderAndControllerInstances();
|
|
1147
|
+
for (const instance of instances) {
|
|
1148
|
+
if (!instance || !instance.constructor) continue;
|
|
1149
|
+
const metadata = Reflect.getMetadata('calyx:websocket_gateway', instance.constructor);
|
|
1150
|
+
if (!metadata) continue;
|
|
1151
|
+
|
|
1152
|
+
const handlers = new Map<string, { propertyKey: string | symbol; paramMapping: any[] }>();
|
|
1153
|
+
|
|
1154
|
+
const subMessages: { event: string; propertyKey: string | symbol }[] =
|
|
1155
|
+
Reflect.getMetadata('calyx:subscribe_message', instance.constructor) || [];
|
|
1156
|
+
|
|
1157
|
+
const bodyParams: { propertyKey: string | symbol; parameterIndex: number }[] =
|
|
1158
|
+
Reflect.getMetadata('calyx:message_body', instance.constructor) || [];
|
|
1159
|
+
|
|
1160
|
+
const socketParams: { propertyKey: string | symbol; parameterIndex: number }[] =
|
|
1161
|
+
Reflect.getMetadata('calyx:connected_socket', instance.constructor) || [];
|
|
1162
|
+
|
|
1163
|
+
for (const sub of subMessages) {
|
|
1164
|
+
const paramMapping: any[] = [];
|
|
1165
|
+
|
|
1166
|
+
for (const bp of bodyParams) {
|
|
1167
|
+
if (bp.propertyKey === sub.propertyKey) {
|
|
1168
|
+
paramMapping[bp.parameterIndex] = 'body';
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
for (const sp of socketParams) {
|
|
1172
|
+
if (sp.propertyKey === sub.propertyKey) {
|
|
1173
|
+
paramMapping[sp.parameterIndex] = 'socket';
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
const classGuards = Reflect.getMetadata(METADATA_KEYS.GUARDS, instance.constructor) || [];
|
|
1178
|
+
const methodGuards = Reflect.getMetadata(METADATA_KEYS.GUARDS, instance.constructor.prototype, sub.propertyKey) || [];
|
|
1179
|
+
const guards = this.compileLifecycleItems(this.rootModule, [...this.globalGuards, ...classGuards, ...methodGuards]);
|
|
1180
|
+
|
|
1181
|
+
const classInterceptors = Reflect.getMetadata(METADATA_KEYS.INTERCEPTORS, instance.constructor) || [];
|
|
1182
|
+
const methodInterceptors = Reflect.getMetadata(METADATA_KEYS.INTERCEPTORS, instance.constructor.prototype, sub.propertyKey) || [];
|
|
1183
|
+
const interceptors = this.compileLifecycleItems(this.rootModule, [...this.globalInterceptors, ...classInterceptors, ...methodInterceptors]);
|
|
1184
|
+
|
|
1185
|
+
handlers.set(sub.event, {
|
|
1186
|
+
propertyKey: sub.propertyKey,
|
|
1187
|
+
paramMapping,
|
|
1188
|
+
guards,
|
|
1189
|
+
interceptors,
|
|
1190
|
+
gatewayClass: instance.constructor,
|
|
1191
|
+
});
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
const gatewayInfo = {
|
|
1195
|
+
instance,
|
|
1196
|
+
metadata,
|
|
1197
|
+
handlers,
|
|
1198
|
+
};
|
|
1199
|
+
|
|
1200
|
+
if (metadata.port && metadata.port !== this.serverPort) {
|
|
1201
|
+
this.startDedicatedWebSocketServer(gatewayInfo);
|
|
1202
|
+
} else {
|
|
1203
|
+
this.sharedWebSockets.push(gatewayInfo);
|
|
1204
|
+
this.hasWebSockets = true;
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
private startDedicatedWebSocketServer(gateway: any) {
|
|
1210
|
+
const port = gateway.metadata.port;
|
|
1211
|
+
const self = this;
|
|
1212
|
+
const wsServer = Bun.serve({
|
|
1213
|
+
port,
|
|
1214
|
+
fetch(req, server) {
|
|
1215
|
+
if (server.upgrade(req)) {
|
|
1216
|
+
return undefined;
|
|
1217
|
+
}
|
|
1218
|
+
return new Response('Expected upgrade to websocket', { status: 400 });
|
|
1219
|
+
},
|
|
1220
|
+
websocket: {
|
|
1221
|
+
open: (ws: any) => {
|
|
1222
|
+
self.dispatchWsConnection(gateway, ws);
|
|
1223
|
+
},
|
|
1224
|
+
message: (ws: any, message: any) => {
|
|
1225
|
+
self.dispatchWsMessage(gateway, ws, message);
|
|
1226
|
+
},
|
|
1227
|
+
close: (ws: any) => {
|
|
1228
|
+
self.dispatchWsDisconnect(gateway, ws);
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
});
|
|
1232
|
+
|
|
1233
|
+
this.cleanupListeners.push(() => wsServer.stop());
|
|
1234
|
+
|
|
1235
|
+
if (typeof gateway.instance.afterInit === 'function') {
|
|
1236
|
+
gateway.instance.afterInit(wsServer);
|
|
1237
|
+
}
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
private dispatchWsConnection(gateway: any, ws: any) {
|
|
1241
|
+
if (typeof gateway.instance.handleConnection === 'function') {
|
|
1242
|
+
gateway.instance.handleConnection(ws);
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
private dispatchWsDisconnect(gateway: any, ws: any) {
|
|
1247
|
+
if (typeof gateway.instance.handleDisconnect === 'function') {
|
|
1248
|
+
gateway.instance.handleDisconnect(ws);
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
private async dispatchWsMessage(gateway: any, ws: any, message: any) {
|
|
1253
|
+
let messageStr = '';
|
|
1254
|
+
if (typeof message === 'string') {
|
|
1255
|
+
messageStr = message;
|
|
1256
|
+
} else if (message instanceof Uint8Array || message instanceof ArrayBuffer) {
|
|
1257
|
+
messageStr = new TextDecoder().decode(message);
|
|
1258
|
+
} else {
|
|
1259
|
+
return;
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
try {
|
|
1263
|
+
const parsed = JSON.parse(messageStr);
|
|
1264
|
+
const event = parsed.event;
|
|
1265
|
+
const data = parsed.data;
|
|
1266
|
+
|
|
1267
|
+
const handlerInfo = gateway.handlers.get(event);
|
|
1268
|
+
if (handlerInfo) {
|
|
1269
|
+
const args: any[] = [];
|
|
1270
|
+
for (let i = 0; i < handlerInfo.paramMapping.length; i++) {
|
|
1271
|
+
const type = handlerInfo.paramMapping[i];
|
|
1272
|
+
if (type === 'body') {
|
|
1273
|
+
args[i] = data;
|
|
1274
|
+
} else if (type === 'socket') {
|
|
1275
|
+
args[i] = ws;
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1279
|
+
const context = this.contextPool.acquire();
|
|
1280
|
+
context.resetContextWs(ws, data, handlerInfo.gatewayClass, gateway.instance[handlerInfo.propertyKey]);
|
|
1281
|
+
|
|
1282
|
+
try {
|
|
1283
|
+
for (const guard of handlerInfo.guards) {
|
|
1284
|
+
const canActive = await guard.instance.canActivate(context);
|
|
1285
|
+
if (!canActive) {
|
|
1286
|
+
return; // Block event
|
|
1287
|
+
}
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
const nextCall = {
|
|
1291
|
+
handle: async () => {
|
|
1292
|
+
return gateway.instance[handlerInfo.propertyKey](...args);
|
|
1293
|
+
}
|
|
1294
|
+
};
|
|
1295
|
+
|
|
1296
|
+
let chain = nextCall.handle;
|
|
1297
|
+
for (let i = handlerInfo.interceptors.length - 1; i >= 0; i--) {
|
|
1298
|
+
const interceptor = handlerInfo.interceptors[i];
|
|
1299
|
+
const currentChain = chain;
|
|
1300
|
+
chain = async () => {
|
|
1301
|
+
return interceptor.instance.intercept(context, {
|
|
1302
|
+
handle: () => currentChain()
|
|
1303
|
+
});
|
|
1304
|
+
};
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1307
|
+
const result = await chain();
|
|
1308
|
+
|
|
1309
|
+
if (result !== undefined) {
|
|
1310
|
+
ws.send(JSON.stringify(result));
|
|
1311
|
+
}
|
|
1312
|
+
} catch (err: any) {
|
|
1313
|
+
// ignore or log
|
|
1314
|
+
} finally {
|
|
1315
|
+
context.clearContext();
|
|
1316
|
+
this.contextPool.release(context);
|
|
1317
|
+
}
|
|
1318
|
+
}
|
|
1319
|
+
} catch {
|
|
1320
|
+
// ignore non-json
|
|
1321
|
+
}
|
|
1322
|
+
}
|
|
992
1323
|
}
|
|
1324
|
+
|
|
1325
|
+
|
package/src/http/factory.ts
CHANGED
|
@@ -1,8 +1,15 @@
|
|
|
1
1
|
import { CalyxApplication } from './application.ts';
|
|
2
|
+
import { CalyxMicroservice } from '../microservices/microservice.ts';
|
|
3
|
+
import { MicroserviceOptions } from '../microservices/interfaces.ts';
|
|
2
4
|
|
|
3
5
|
export class CalyxFactory {
|
|
4
6
|
static async create(rootModule: any): Promise<CalyxApplication> {
|
|
5
7
|
const app = new CalyxApplication(rootModule);
|
|
6
8
|
return app;
|
|
7
9
|
}
|
|
10
|
+
|
|
11
|
+
static async createMicroservice(rootModule: any, options?: MicroserviceOptions): Promise<CalyxMicroservice> {
|
|
12
|
+
const app = new CalyxMicroservice(rootModule, options);
|
|
13
|
+
return app;
|
|
14
|
+
}
|
|
8
15
|
}
|
package/src/index.ts
CHANGED
|
@@ -2,3 +2,9 @@ import 'reflect-metadata';
|
|
|
2
2
|
export * from './core/index.ts';
|
|
3
3
|
export * from './http/index.ts';
|
|
4
4
|
export * from './lifecycle/index.ts';
|
|
5
|
+
export * from './config/index.ts';
|
|
6
|
+
export * from './event-emitter/index.ts';
|
|
7
|
+
export * from './security/index.ts';
|
|
8
|
+
export * from './schedule/index.ts';
|
|
9
|
+
export * from './websockets/index.ts';
|
|
10
|
+
export * from './microservices/index.ts';
|