@martel/calyx 1.5.0 → 1.7.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/cache/cache.interceptor.ts +32 -0
- package/src/cache/cache.module.ts +31 -0
- package/src/cache/cache.service.ts +86 -0
- package/src/cache/index.ts +3 -0
- package/src/http/application.ts +231 -3
- package/src/http/factory.ts +7 -0
- package/src/index.ts +4 -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/validation/compiler.ts +124 -0
- package/src/validation/decorators.ts +47 -0
- package/src/validation/index.ts +3 -0
- package/src/validation/pipe.ts +31 -0
- package/src/websockets/decorators.ts +49 -0
- package/src/websockets/gateway.ts +11 -0
- package/src/websockets/index.ts +2 -0
- package/tests/cache.test.ts +93 -0
- package/tests/microservices.test.ts +105 -0
- package/tests/rpc-ws-context.test.ts +135 -0
- package/tests/validation-serialization.test.ts +134 -0
- package/tests/websockets.test.ts +125 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,17 @@
|
|
|
1
|
+
# [1.7.0](https://github.com/bmartel/calyx/compare/v1.6.0...v1.7.0) (2026-07-01)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Features
|
|
5
|
+
|
|
6
|
+
* **cache,validation:** implement SQLite CacheModule, JIT ValidationPipe, and JIT Response Serializer ([1d37b68](https://github.com/bmartel/calyx/commit/1d37b68c2a7ba52e7878810b63f75ee31becc433))
|
|
7
|
+
|
|
8
|
+
# [1.6.0](https://github.com/bmartel/calyx/compare/v1.5.0...v1.6.0) (2026-07-01)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Features
|
|
12
|
+
|
|
13
|
+
* **websockets,microservices:** implement WebSocketGateways, TCP Microservices, and context switching for Guards/Interceptors ([a8cde0d](https://github.com/bmartel/calyx/commit/a8cde0d68411a3ab736fe1df00ae04672588b1af))
|
|
14
|
+
|
|
1
15
|
# [1.5.0](https://github.com/bmartel/calyx/compare/v1.4.0...v1.5.0) (2026-07-01)
|
|
2
16
|
|
|
3
17
|
|
package/package.json
CHANGED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { NestInterceptor, ExecutionContext, CallHandler } from '../lifecycle/interfaces.ts';
|
|
2
|
+
import { Injectable } from '../core/decorators.ts';
|
|
3
|
+
import { CacheService } from './cache.service.ts';
|
|
4
|
+
|
|
5
|
+
@Injectable()
|
|
6
|
+
export class CacheInterceptor implements NestInterceptor {
|
|
7
|
+
constructor(private readonly cacheService: CacheService) {}
|
|
8
|
+
|
|
9
|
+
async intercept(context: ExecutionContext, next: CallHandler): Promise<any> {
|
|
10
|
+
const type = context.getType();
|
|
11
|
+
if (type !== 'http') {
|
|
12
|
+
return next.handle();
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const req = context.switchToHttp().getRequest<Request>();
|
|
16
|
+
if (req.method !== 'GET') {
|
|
17
|
+
return next.handle();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const url = new URL(req.url);
|
|
21
|
+
const key = `http_cache::${url.pathname}${url.search}`;
|
|
22
|
+
|
|
23
|
+
const cached = await this.cacheService.get(key);
|
|
24
|
+
if (cached !== undefined) {
|
|
25
|
+
return cached;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const result = await next.handle();
|
|
29
|
+
await this.cacheService.set(key, result);
|
|
30
|
+
return result;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { Module, DynamicModule } from '../core/decorators.ts';
|
|
2
|
+
import { CacheService } from './cache.service.ts';
|
|
3
|
+
import { CacheInterceptor } from './cache.interceptor.ts';
|
|
4
|
+
|
|
5
|
+
export interface CacheModuleOptions {
|
|
6
|
+
dbPath?: string;
|
|
7
|
+
defaultTtl?: number;
|
|
8
|
+
isGlobal?: boolean;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
@Module({
|
|
12
|
+
providers: [CacheService, CacheInterceptor],
|
|
13
|
+
exports: [CacheService, CacheInterceptor],
|
|
14
|
+
})
|
|
15
|
+
export class CacheModule {
|
|
16
|
+
static register(options: CacheModuleOptions = {}): DynamicModule {
|
|
17
|
+
const cacheServiceInstance = new CacheService(options);
|
|
18
|
+
|
|
19
|
+
const cacheServiceProvider = {
|
|
20
|
+
provide: CacheService,
|
|
21
|
+
useValue: cacheServiceInstance,
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
return {
|
|
25
|
+
module: CacheModule,
|
|
26
|
+
providers: [cacheServiceProvider, CacheInterceptor],
|
|
27
|
+
exports: [CacheService, CacheInterceptor],
|
|
28
|
+
global: options.isGlobal ?? false,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { Database } from 'bun:sqlite';
|
|
2
|
+
import { Injectable } from '../core/decorators.ts';
|
|
3
|
+
|
|
4
|
+
@Injectable()
|
|
5
|
+
export class CacheService {
|
|
6
|
+
private db!: Database;
|
|
7
|
+
private pruneTimer: any;
|
|
8
|
+
private readonly dbPath: string;
|
|
9
|
+
private readonly defaultTtl: number;
|
|
10
|
+
|
|
11
|
+
constructor(options: { dbPath?: string; defaultTtl?: number } = {}) {
|
|
12
|
+
this.dbPath = options.dbPath ?? ':memory:';
|
|
13
|
+
this.defaultTtl = options.defaultTtl ?? 5;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
onModuleInit() {
|
|
17
|
+
this.db = new Database(this.dbPath);
|
|
18
|
+
this.db.run(`
|
|
19
|
+
CREATE TABLE IF NOT EXISTS cache (
|
|
20
|
+
key TEXT PRIMARY KEY,
|
|
21
|
+
value TEXT,
|
|
22
|
+
expires_at INTEGER
|
|
23
|
+
)
|
|
24
|
+
`);
|
|
25
|
+
this.db.run(`CREATE INDEX IF NOT EXISTS idx_expires_at ON cache (expires_at)`);
|
|
26
|
+
|
|
27
|
+
this.pruneTimer = setInterval(() => this.prune(), 30000);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
onModuleDestroy() {
|
|
31
|
+
if (this.pruneTimer) {
|
|
32
|
+
clearInterval(this.pruneTimer);
|
|
33
|
+
}
|
|
34
|
+
if (this.db) {
|
|
35
|
+
this.db.close();
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async get<T = any>(key: string): Promise<T | undefined> {
|
|
40
|
+
const row = this.db.query('SELECT value, expires_at FROM cache WHERE key = $key').get({ $key: key }) as any;
|
|
41
|
+
if (!row) return undefined;
|
|
42
|
+
|
|
43
|
+
if (row.expires_at !== null && row.expires_at < Date.now()) {
|
|
44
|
+
this.db.query('DELETE FROM cache WHERE key = $key').run({ $key: key });
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
try {
|
|
49
|
+
return JSON.parse(row.value) as T;
|
|
50
|
+
} catch {
|
|
51
|
+
return row.value as unknown as T;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async set<T = any>(key: string, value: T, ttl?: number): Promise<void> {
|
|
56
|
+
const ttlSeconds = ttl !== undefined ? ttl : this.defaultTtl;
|
|
57
|
+
const expiresAt = ttlSeconds > 0 ? Date.now() + ttlSeconds * 1000 : null;
|
|
58
|
+
const valStr = JSON.stringify(value);
|
|
59
|
+
|
|
60
|
+
this.db.query(`
|
|
61
|
+
INSERT INTO cache (key, value, expires_at)
|
|
62
|
+
VALUES ($key, $val, $expiresAt)
|
|
63
|
+
ON CONFLICT(key) DO UPDATE SET value = $val, expires_at = $expiresAt
|
|
64
|
+
`).run({
|
|
65
|
+
$key: key,
|
|
66
|
+
$val: valStr,
|
|
67
|
+
$expiresAt: expiresAt,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async del(key: string): Promise<void> {
|
|
72
|
+
this.db.query('DELETE FROM cache WHERE key = $key').run({ $key: key });
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async reset(): Promise<void> {
|
|
76
|
+
this.db.run('DELETE FROM cache');
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
private prune() {
|
|
80
|
+
try {
|
|
81
|
+
this.db.query('DELETE FROM cache WHERE expires_at IS NOT NULL AND expires_at < $now').run({ $now: Date.now() });
|
|
82
|
+
} catch {
|
|
83
|
+
// ignore
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
package/src/http/application.ts
CHANGED
|
@@ -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 { SerializationCompiler } from '../validation/compiler.ts';
|
|
13
14
|
|
|
14
15
|
class ObjectPool<T> {
|
|
15
16
|
private pool: T[] = [];
|
|
@@ -99,6 +100,9 @@ export class CalyxApplication {
|
|
|
99
100
|
private middlewareConfigurations: MiddlewareConfiguration[] = [];
|
|
100
101
|
private hostPool = new ObjectPool<CalyxArgumentsHost>(() => new CalyxArgumentsHost());
|
|
101
102
|
private contextPool = new ObjectPool<CalyxExecutionContext>(() => new CalyxExecutionContext());
|
|
103
|
+
private sharedWebSockets: any[] = [];
|
|
104
|
+
private hasWebSockets = false;
|
|
105
|
+
private serverPort = 3000;
|
|
102
106
|
|
|
103
107
|
use(...middlewares: any[]) {
|
|
104
108
|
this.globalMiddlewares.push(...middlewares);
|
|
@@ -149,6 +153,9 @@ export class CalyxApplication {
|
|
|
149
153
|
// Register Scheduled tasks
|
|
150
154
|
this.registerScheduledTasks();
|
|
151
155
|
|
|
156
|
+
// Register WebSocket Gateways
|
|
157
|
+
this.registerWebSocketGateways();
|
|
158
|
+
|
|
152
159
|
// Call OnModuleInit hooks
|
|
153
160
|
await this.runOnModuleInit();
|
|
154
161
|
|
|
@@ -882,6 +889,15 @@ export class CalyxApplication {
|
|
|
882
889
|
|
|
883
890
|
if (typeof result === 'object') {
|
|
884
891
|
responseHeaders['content-type'] = 'application/json';
|
|
892
|
+
const constructor = result.constructor;
|
|
893
|
+
if (constructor) {
|
|
894
|
+
const hasRules = Reflect.hasMetadata('calyx:validation_rules', constructor);
|
|
895
|
+
const hasExpose = Reflect.hasMetadata('calyx:expose_properties', constructor);
|
|
896
|
+
if (hasRules || hasExpose) {
|
|
897
|
+
const serialize = SerializationCompiler.compile(constructor);
|
|
898
|
+
return new Response(serialize(result), { status, headers: responseHeaders });
|
|
899
|
+
}
|
|
900
|
+
}
|
|
885
901
|
return new Response(JSON.stringify(result), { status, headers: responseHeaders });
|
|
886
902
|
}
|
|
887
903
|
|
|
@@ -928,11 +944,43 @@ export class CalyxApplication {
|
|
|
928
944
|
}
|
|
929
945
|
|
|
930
946
|
async listen(port: number): Promise<any> {
|
|
947
|
+
this.serverPort = port;
|
|
931
948
|
await this.init();
|
|
932
|
-
|
|
949
|
+
|
|
950
|
+
const fetchHandler = (req: Request, server: any) => {
|
|
951
|
+
if (this.hasWebSockets && req.headers.get('upgrade') === 'websocket') {
|
|
952
|
+
const success = server.upgrade(req);
|
|
953
|
+
if (success) return undefined;
|
|
954
|
+
}
|
|
955
|
+
return this.handleRequest(req);
|
|
956
|
+
};
|
|
957
|
+
|
|
958
|
+
const serveOptions: any = {
|
|
933
959
|
port,
|
|
934
|
-
fetch:
|
|
935
|
-
}
|
|
960
|
+
fetch: fetchHandler,
|
|
961
|
+
};
|
|
962
|
+
|
|
963
|
+
if (this.hasWebSockets) {
|
|
964
|
+
serveOptions.websocket = {
|
|
965
|
+
open: (ws: any) => {
|
|
966
|
+
for (const gateway of this.sharedWebSockets) {
|
|
967
|
+
this.dispatchWsConnection(gateway, ws);
|
|
968
|
+
}
|
|
969
|
+
},
|
|
970
|
+
message: (ws: any, message: any) => {
|
|
971
|
+
for (const gateway of this.sharedWebSockets) {
|
|
972
|
+
this.dispatchWsMessage(gateway, ws, message);
|
|
973
|
+
}
|
|
974
|
+
},
|
|
975
|
+
close: (ws: any) => {
|
|
976
|
+
for (const gateway of this.sharedWebSockets) {
|
|
977
|
+
this.dispatchWsDisconnect(gateway, ws);
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
};
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
this.server = Bun.serve(serveOptions);
|
|
936
984
|
return this.server;
|
|
937
985
|
}
|
|
938
986
|
|
|
@@ -1103,5 +1151,185 @@ export class CalyxApplication {
|
|
|
1103
1151
|
}
|
|
1104
1152
|
}
|
|
1105
1153
|
}
|
|
1154
|
+
|
|
1155
|
+
private registerWebSocketGateways() {
|
|
1156
|
+
const instances = this.container.getProviderAndControllerInstances();
|
|
1157
|
+
for (const instance of instances) {
|
|
1158
|
+
if (!instance || !instance.constructor) continue;
|
|
1159
|
+
const metadata = Reflect.getMetadata('calyx:websocket_gateway', instance.constructor);
|
|
1160
|
+
if (!metadata) continue;
|
|
1161
|
+
|
|
1162
|
+
const handlers = new Map<string, { propertyKey: string | symbol; paramMapping: any[] }>();
|
|
1163
|
+
|
|
1164
|
+
const subMessages: { event: string; propertyKey: string | symbol }[] =
|
|
1165
|
+
Reflect.getMetadata('calyx:subscribe_message', instance.constructor) || [];
|
|
1166
|
+
|
|
1167
|
+
const bodyParams: { propertyKey: string | symbol; parameterIndex: number }[] =
|
|
1168
|
+
Reflect.getMetadata('calyx:message_body', instance.constructor) || [];
|
|
1169
|
+
|
|
1170
|
+
const socketParams: { propertyKey: string | symbol; parameterIndex: number }[] =
|
|
1171
|
+
Reflect.getMetadata('calyx:connected_socket', instance.constructor) || [];
|
|
1172
|
+
|
|
1173
|
+
for (const sub of subMessages) {
|
|
1174
|
+
const paramMapping: any[] = [];
|
|
1175
|
+
|
|
1176
|
+
for (const bp of bodyParams) {
|
|
1177
|
+
if (bp.propertyKey === sub.propertyKey) {
|
|
1178
|
+
paramMapping[bp.parameterIndex] = 'body';
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
for (const sp of socketParams) {
|
|
1182
|
+
if (sp.propertyKey === sub.propertyKey) {
|
|
1183
|
+
paramMapping[sp.parameterIndex] = 'socket';
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
const classGuards = Reflect.getMetadata(METADATA_KEYS.GUARDS, instance.constructor) || [];
|
|
1188
|
+
const methodGuards = Reflect.getMetadata(METADATA_KEYS.GUARDS, instance.constructor.prototype, sub.propertyKey) || [];
|
|
1189
|
+
const guards = this.compileLifecycleItems(this.rootModule, [...this.globalGuards, ...classGuards, ...methodGuards]);
|
|
1190
|
+
|
|
1191
|
+
const classInterceptors = Reflect.getMetadata(METADATA_KEYS.INTERCEPTORS, instance.constructor) || [];
|
|
1192
|
+
const methodInterceptors = Reflect.getMetadata(METADATA_KEYS.INTERCEPTORS, instance.constructor.prototype, sub.propertyKey) || [];
|
|
1193
|
+
const interceptors = this.compileLifecycleItems(this.rootModule, [...this.globalInterceptors, ...classInterceptors, ...methodInterceptors]);
|
|
1194
|
+
|
|
1195
|
+
handlers.set(sub.event, {
|
|
1196
|
+
propertyKey: sub.propertyKey,
|
|
1197
|
+
paramMapping,
|
|
1198
|
+
guards,
|
|
1199
|
+
interceptors,
|
|
1200
|
+
gatewayClass: instance.constructor,
|
|
1201
|
+
});
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
const gatewayInfo = {
|
|
1205
|
+
instance,
|
|
1206
|
+
metadata,
|
|
1207
|
+
handlers,
|
|
1208
|
+
};
|
|
1209
|
+
|
|
1210
|
+
if (metadata.port && metadata.port !== this.serverPort) {
|
|
1211
|
+
this.startDedicatedWebSocketServer(gatewayInfo);
|
|
1212
|
+
} else {
|
|
1213
|
+
this.sharedWebSockets.push(gatewayInfo);
|
|
1214
|
+
this.hasWebSockets = true;
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
private startDedicatedWebSocketServer(gateway: any) {
|
|
1220
|
+
const port = gateway.metadata.port;
|
|
1221
|
+
const self = this;
|
|
1222
|
+
const wsServer = Bun.serve({
|
|
1223
|
+
port,
|
|
1224
|
+
fetch(req, server) {
|
|
1225
|
+
if (server.upgrade(req)) {
|
|
1226
|
+
return undefined;
|
|
1227
|
+
}
|
|
1228
|
+
return new Response('Expected upgrade to websocket', { status: 400 });
|
|
1229
|
+
},
|
|
1230
|
+
websocket: {
|
|
1231
|
+
open: (ws: any) => {
|
|
1232
|
+
self.dispatchWsConnection(gateway, ws);
|
|
1233
|
+
},
|
|
1234
|
+
message: (ws: any, message: any) => {
|
|
1235
|
+
self.dispatchWsMessage(gateway, ws, message);
|
|
1236
|
+
},
|
|
1237
|
+
close: (ws: any) => {
|
|
1238
|
+
self.dispatchWsDisconnect(gateway, ws);
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1241
|
+
});
|
|
1242
|
+
|
|
1243
|
+
this.cleanupListeners.push(() => wsServer.stop());
|
|
1244
|
+
|
|
1245
|
+
if (typeof gateway.instance.afterInit === 'function') {
|
|
1246
|
+
gateway.instance.afterInit(wsServer);
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
private dispatchWsConnection(gateway: any, ws: any) {
|
|
1251
|
+
if (typeof gateway.instance.handleConnection === 'function') {
|
|
1252
|
+
gateway.instance.handleConnection(ws);
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1256
|
+
private dispatchWsDisconnect(gateway: any, ws: any) {
|
|
1257
|
+
if (typeof gateway.instance.handleDisconnect === 'function') {
|
|
1258
|
+
gateway.instance.handleDisconnect(ws);
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
private async dispatchWsMessage(gateway: any, ws: any, message: any) {
|
|
1263
|
+
let messageStr = '';
|
|
1264
|
+
if (typeof message === 'string') {
|
|
1265
|
+
messageStr = message;
|
|
1266
|
+
} else if (message instanceof Uint8Array || message instanceof ArrayBuffer) {
|
|
1267
|
+
messageStr = new TextDecoder().decode(message);
|
|
1268
|
+
} else {
|
|
1269
|
+
return;
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
try {
|
|
1273
|
+
const parsed = JSON.parse(messageStr);
|
|
1274
|
+
const event = parsed.event;
|
|
1275
|
+
const data = parsed.data;
|
|
1276
|
+
|
|
1277
|
+
const handlerInfo = gateway.handlers.get(event);
|
|
1278
|
+
if (handlerInfo) {
|
|
1279
|
+
const args: any[] = [];
|
|
1280
|
+
for (let i = 0; i < handlerInfo.paramMapping.length; i++) {
|
|
1281
|
+
const type = handlerInfo.paramMapping[i];
|
|
1282
|
+
if (type === 'body') {
|
|
1283
|
+
args[i] = data;
|
|
1284
|
+
} else if (type === 'socket') {
|
|
1285
|
+
args[i] = ws;
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
const context = this.contextPool.acquire();
|
|
1290
|
+
context.resetContextWs(ws, data, handlerInfo.gatewayClass, gateway.instance[handlerInfo.propertyKey]);
|
|
1291
|
+
|
|
1292
|
+
try {
|
|
1293
|
+
for (const guard of handlerInfo.guards) {
|
|
1294
|
+
const canActive = await guard.instance.canActivate(context);
|
|
1295
|
+
if (!canActive) {
|
|
1296
|
+
return; // Block event
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
1299
|
+
|
|
1300
|
+
const nextCall = {
|
|
1301
|
+
handle: async () => {
|
|
1302
|
+
return gateway.instance[handlerInfo.propertyKey](...args);
|
|
1303
|
+
}
|
|
1304
|
+
};
|
|
1305
|
+
|
|
1306
|
+
let chain = nextCall.handle;
|
|
1307
|
+
for (let i = handlerInfo.interceptors.length - 1; i >= 0; i--) {
|
|
1308
|
+
const interceptor = handlerInfo.interceptors[i];
|
|
1309
|
+
const currentChain = chain;
|
|
1310
|
+
chain = async () => {
|
|
1311
|
+
return interceptor.instance.intercept(context, {
|
|
1312
|
+
handle: () => currentChain()
|
|
1313
|
+
});
|
|
1314
|
+
};
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
const result = await chain();
|
|
1318
|
+
|
|
1319
|
+
if (result !== undefined) {
|
|
1320
|
+
ws.send(JSON.stringify(result));
|
|
1321
|
+
}
|
|
1322
|
+
} catch (err: any) {
|
|
1323
|
+
// ignore or log
|
|
1324
|
+
} finally {
|
|
1325
|
+
context.clearContext();
|
|
1326
|
+
this.contextPool.release(context);
|
|
1327
|
+
}
|
|
1328
|
+
}
|
|
1329
|
+
} catch {
|
|
1330
|
+
// ignore non-json
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1106
1333
|
}
|
|
1107
1334
|
|
|
1335
|
+
|
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
|
@@ -6,3 +6,7 @@ export * from './config/index.ts';
|
|
|
6
6
|
export * from './event-emitter/index.ts';
|
|
7
7
|
export * from './security/index.ts';
|
|
8
8
|
export * from './schedule/index.ts';
|
|
9
|
+
export * from './websockets/index.ts';
|
|
10
|
+
export * from './microservices/index.ts';
|
|
11
|
+
export * from './cache/index.ts';
|
|
12
|
+
export * from './validation/index.ts';
|
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
|
+
}
|