@morojs/moro 1.3.0 → 1.4.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/README.md +61 -7
- package/dist/core/config/types.d.ts +147 -0
- package/dist/core/config/types.js +124 -0
- package/dist/core/config/types.js.map +1 -0
- package/dist/core/config/typescript-loader.d.ts +6 -0
- package/dist/core/config/typescript-loader.js +268 -0
- package/dist/core/config/typescript-loader.js.map +1 -0
- package/dist/core/config/validation.d.ts +18 -0
- package/dist/core/config/validation.js +134 -0
- package/dist/core/config/validation.js.map +1 -0
- package/dist/core/docs/openapi-generator.js +6 -6
- package/dist/core/docs/openapi-generator.js.map +1 -1
- package/dist/core/docs/schema-to-openapi.d.ts +7 -0
- package/dist/core/docs/schema-to-openapi.js +124 -0
- package/dist/core/docs/schema-to-openapi.js.map +1 -0
- package/dist/core/docs/zod-to-openapi.d.ts +2 -0
- package/dist/core/docs/zod-to-openapi.js.map +1 -1
- package/dist/core/framework.d.ts +29 -6
- package/dist/core/framework.js +117 -18
- package/dist/core/framework.js.map +1 -1
- package/dist/core/networking/adapters/index.d.ts +3 -0
- package/dist/core/networking/adapters/index.js +10 -0
- package/dist/core/networking/adapters/index.js.map +1 -0
- package/dist/core/networking/adapters/socketio-adapter.d.ts +16 -0
- package/dist/core/networking/adapters/socketio-adapter.js +244 -0
- package/dist/core/networking/adapters/socketio-adapter.js.map +1 -0
- package/dist/core/networking/adapters/ws-adapter.d.ts +54 -0
- package/dist/core/networking/adapters/ws-adapter.js +383 -0
- package/dist/core/networking/adapters/ws-adapter.js.map +1 -0
- package/dist/core/networking/websocket-adapter.d.ts +171 -0
- package/dist/core/networking/websocket-adapter.js +5 -0
- package/dist/core/networking/websocket-adapter.js.map +1 -0
- package/dist/core/networking/websocket-manager.d.ts +53 -17
- package/dist/core/networking/websocket-manager.js +166 -108
- package/dist/core/networking/websocket-manager.js.map +1 -1
- package/dist/core/routing/index.d.ts +13 -13
- package/dist/core/routing/index.js.map +1 -1
- package/dist/core/validation/adapters.d.ts +51 -0
- package/dist/core/validation/adapters.js +135 -0
- package/dist/core/validation/adapters.js.map +1 -0
- package/dist/core/validation/index.d.ts +14 -11
- package/dist/core/validation/index.js +37 -26
- package/dist/core/validation/index.js.map +1 -1
- package/dist/core/validation/schema-interface.d.ts +36 -0
- package/dist/core/validation/schema-interface.js +68 -0
- package/dist/core/validation/schema-interface.js.map +1 -0
- package/dist/index.d.ts +6 -1
- package/dist/index.js +14 -3
- package/dist/index.js.map +1 -1
- package/dist/moro.js +8 -2
- package/dist/moro.js.map +1 -1
- package/package.json +31 -7
- package/src/core/config/types.ts +277 -0
- package/src/core/config/typescript-loader.ts +571 -0
- package/src/core/config/validation.ts +145 -0
- package/src/core/docs/openapi-generator.ts +7 -6
- package/src/core/docs/schema-to-openapi.ts +148 -0
- package/src/core/docs/zod-to-openapi.ts +2 -0
- package/src/core/framework.ts +121 -28
- package/src/core/networking/adapters/index.ts +16 -0
- package/src/core/networking/adapters/socketio-adapter.ts +252 -0
- package/src/core/networking/adapters/ws-adapter.ts +425 -0
- package/src/core/networking/websocket-adapter.ts +217 -0
- package/src/core/networking/websocket-manager.ts +185 -127
- package/src/core/routing/index.ts +13 -13
- package/src/core/validation/adapters.ts +147 -0
- package/src/core/validation/index.ts +60 -38
- package/src/core/validation/schema-interface.ts +100 -0
- package/src/index.ts +25 -2
- package/src/moro.ts +11 -2
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
// Socket.IO WebSocket Adapter for Moro Framework
|
|
2
|
+
// Implements the WebSocket adapter interface using Socket.IO
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
WebSocketAdapter,
|
|
6
|
+
WebSocketAdapterOptions,
|
|
7
|
+
WebSocketNamespace,
|
|
8
|
+
WebSocketConnection,
|
|
9
|
+
WebSocketEmitter,
|
|
10
|
+
WebSocketMiddleware,
|
|
11
|
+
} from '../websocket-adapter';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Socket.IO adapter implementation
|
|
15
|
+
*/
|
|
16
|
+
export class SocketIOAdapter implements WebSocketAdapter {
|
|
17
|
+
private io: any; // Socket.IO server instance
|
|
18
|
+
private customIdGenerator?: () => string;
|
|
19
|
+
|
|
20
|
+
async initialize(httpServer: any, options: WebSocketAdapterOptions = {}): Promise<void> {
|
|
21
|
+
try {
|
|
22
|
+
// Dynamic import to avoid requiring socket.io as a hard dependency
|
|
23
|
+
const { Server } = await import('socket.io');
|
|
24
|
+
|
|
25
|
+
this.io = new Server(httpServer, {
|
|
26
|
+
cors: options.cors || { origin: '*' },
|
|
27
|
+
path: options.path || '/socket.io/',
|
|
28
|
+
compression: options.compression !== false,
|
|
29
|
+
maxHttpBufferSize: options.maxPayloadLength,
|
|
30
|
+
...options,
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
// Apply custom ID generator if set
|
|
34
|
+
if (this.customIdGenerator) {
|
|
35
|
+
(this.io.engine as any).generateId = this.customIdGenerator;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Setup compression if enabled
|
|
39
|
+
if (options.compression) {
|
|
40
|
+
this.setCompression(true);
|
|
41
|
+
}
|
|
42
|
+
} catch (error) {
|
|
43
|
+
throw new Error(
|
|
44
|
+
'Socket.IO not found. Install it with: npm install socket.io\n' +
|
|
45
|
+
'Or use a different WebSocket adapter.'
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
createNamespace(namespace: string): WebSocketNamespace {
|
|
51
|
+
if (!this.io) {
|
|
52
|
+
throw new Error('Socket.IO adapter not initialized');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const ns = this.io.of(namespace);
|
|
56
|
+
return new SocketIONamespaceWrapper(ns);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
getDefaultNamespace(): WebSocketNamespace {
|
|
60
|
+
return this.createNamespace('/');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async close(): Promise<void> {
|
|
64
|
+
if (this.io) {
|
|
65
|
+
return new Promise(resolve => {
|
|
66
|
+
this.io.close(() => resolve());
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
setCompression(enabled: boolean, options: any = {}): void {
|
|
72
|
+
if (this.io && enabled) {
|
|
73
|
+
(this.io.engine as any).compression = true;
|
|
74
|
+
(this.io.engine as any).perMessageDeflate = {
|
|
75
|
+
threshold: 1024,
|
|
76
|
+
concurrencyLimit: 10,
|
|
77
|
+
memLevel: 8,
|
|
78
|
+
...options,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
setCustomIdGenerator(generator: () => string): void {
|
|
84
|
+
this.customIdGenerator = generator;
|
|
85
|
+
if (this.io) {
|
|
86
|
+
(this.io.engine as any).generateId = generator;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
getAdapterName(): string {
|
|
91
|
+
return 'socket.io';
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
getConnectionCount(): number {
|
|
95
|
+
if (!this.io) return 0;
|
|
96
|
+
return this.io.engine.clientsCount || 0;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Socket.IO namespace wrapper
|
|
102
|
+
*/
|
|
103
|
+
class SocketIONamespaceWrapper implements WebSocketNamespace {
|
|
104
|
+
constructor(private namespace: any) {}
|
|
105
|
+
|
|
106
|
+
on(event: 'connection', handler: (socket: WebSocketConnection) => void): void {
|
|
107
|
+
this.namespace.on(event, (socket: any) => {
|
|
108
|
+
handler(new SocketIOConnectionWrapper(socket));
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
emit(event: string, data: any): void {
|
|
113
|
+
this.namespace.emit(event, data);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
to(room: string | string[]): WebSocketEmitter {
|
|
117
|
+
const target = Array.isArray(room)
|
|
118
|
+
? room.reduce((acc, r) => acc.to(r), this.namespace)
|
|
119
|
+
: this.namespace.to(room);
|
|
120
|
+
return new SocketIOEmitterWrapper(target);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
except(room: string | string[]): WebSocketEmitter {
|
|
124
|
+
const target = Array.isArray(room)
|
|
125
|
+
? room.reduce((acc, r) => acc.except(r), this.namespace)
|
|
126
|
+
: this.namespace.except(room);
|
|
127
|
+
return new SocketIOEmitterWrapper(target);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
getSockets(): WebSocketConnection[] {
|
|
131
|
+
const sockets = this.namespace.sockets;
|
|
132
|
+
return Array.from(sockets.values()).map((socket: any) => new SocketIOConnectionWrapper(socket));
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
getConnectionCount(): number {
|
|
136
|
+
return this.namespace.sockets.size;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
use(middleware: WebSocketMiddleware): void {
|
|
140
|
+
this.namespace.use((socket: any, next: any) => {
|
|
141
|
+
middleware(new SocketIOConnectionWrapper(socket), next);
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Socket.IO connection wrapper
|
|
148
|
+
*/
|
|
149
|
+
class SocketIOConnectionWrapper implements WebSocketConnection {
|
|
150
|
+
public data: Record<string, any> = {};
|
|
151
|
+
|
|
152
|
+
constructor(private socket: any) {
|
|
153
|
+
// Map socket.data to our data property
|
|
154
|
+
this.data = socket.data || {};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
get id(): string {
|
|
158
|
+
return this.socket.id;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
get ip(): string | undefined {
|
|
162
|
+
return this.socket.handshake?.address;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
get headers(): Record<string, string> | undefined {
|
|
166
|
+
return this.socket.handshake?.headers;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
get connected(): boolean {
|
|
170
|
+
return this.socket.connected;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
get broadcast(): WebSocketEmitter {
|
|
174
|
+
return new SocketIOEmitterWrapper(this.socket.broadcast);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
on(event: string, handler: (data: any, callback?: (response?: any) => void) => void): void {
|
|
178
|
+
this.socket.on(event, handler);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
onAny(handler: (event: string, ...args: any[]) => void): void {
|
|
182
|
+
this.socket.onAny(handler);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
emit(event: string, data: any): void {
|
|
186
|
+
this.socket.emit(event, data);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
compressedEmit(event: string, data: any): void {
|
|
190
|
+
this.socket.compress(true).emit(event, data);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
join(room: string | string[]): void {
|
|
194
|
+
if (Array.isArray(room)) {
|
|
195
|
+
room.forEach(r => this.socket.join(r));
|
|
196
|
+
} else {
|
|
197
|
+
this.socket.join(room);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
leave(room: string | string[]): void {
|
|
202
|
+
if (Array.isArray(room)) {
|
|
203
|
+
room.forEach(r => this.socket.leave(r));
|
|
204
|
+
} else {
|
|
205
|
+
this.socket.leave(room);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
to(room: string | string[]): WebSocketEmitter {
|
|
210
|
+
const target = Array.isArray(room)
|
|
211
|
+
? room.reduce((acc, r) => acc.to(r), this.socket)
|
|
212
|
+
: this.socket.to(room);
|
|
213
|
+
return new SocketIOEmitterWrapper(target);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
getRooms(): Set<string> {
|
|
217
|
+
return new Set(this.socket.rooms);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
disconnect(close?: boolean): void {
|
|
221
|
+
this.socket.disconnect(close);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Socket.IO emitter wrapper
|
|
227
|
+
*/
|
|
228
|
+
class SocketIOEmitterWrapper implements WebSocketEmitter {
|
|
229
|
+
constructor(private emitter: any) {}
|
|
230
|
+
|
|
231
|
+
emit(event: string, data: any): void {
|
|
232
|
+
this.emitter.emit(event, data);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
to(room: string | string[]): WebSocketEmitter {
|
|
236
|
+
const target = Array.isArray(room)
|
|
237
|
+
? room.reduce((acc, r) => acc.to(r), this.emitter)
|
|
238
|
+
: this.emitter.to(room);
|
|
239
|
+
return new SocketIOEmitterWrapper(target);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
except(room: string | string[]): WebSocketEmitter {
|
|
243
|
+
const target = Array.isArray(room)
|
|
244
|
+
? room.reduce((acc, r) => acc.except(r), this.emitter)
|
|
245
|
+
: this.emitter.except(room);
|
|
246
|
+
return new SocketIOEmitterWrapper(target);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
compress(compress: boolean): WebSocketEmitter {
|
|
250
|
+
return new SocketIOEmitterWrapper(this.emitter.compress(compress));
|
|
251
|
+
}
|
|
252
|
+
}
|
|
@@ -0,0 +1,425 @@
|
|
|
1
|
+
// Native WebSocket Adapter for Moro Framework
|
|
2
|
+
// Implements the WebSocket adapter interface using the 'ws' library
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
WebSocketAdapter,
|
|
6
|
+
WebSocketAdapterOptions,
|
|
7
|
+
WebSocketNamespace,
|
|
8
|
+
WebSocketConnection,
|
|
9
|
+
WebSocketEmitter,
|
|
10
|
+
WebSocketMiddleware,
|
|
11
|
+
} from '../websocket-adapter';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Native WebSocket adapter using the 'ws' library
|
|
15
|
+
* Provides a lightweight, standards-compliant WebSocket implementation
|
|
16
|
+
*/
|
|
17
|
+
export class WSAdapter implements WebSocketAdapter {
|
|
18
|
+
private wss: any; // WebSocket server instance
|
|
19
|
+
private namespaces = new Map<string, WSNamespaceWrapper>();
|
|
20
|
+
private connections = new Map<string, WSConnectionWrapper>();
|
|
21
|
+
private customIdGenerator?: () => string;
|
|
22
|
+
private connectionCounter = 0;
|
|
23
|
+
|
|
24
|
+
async initialize(httpServer: any, options: WebSocketAdapterOptions = {}): Promise<void> {
|
|
25
|
+
try {
|
|
26
|
+
// Dynamic import to avoid requiring ws as a hard dependency
|
|
27
|
+
const { WebSocketServer } = await import('ws');
|
|
28
|
+
|
|
29
|
+
this.wss = new WebSocketServer({
|
|
30
|
+
server: httpServer,
|
|
31
|
+
path: options.path || '/ws',
|
|
32
|
+
maxPayload: options.maxPayloadLength || 100 * 1024 * 1024, // 100MB default
|
|
33
|
+
// Note: ws doesn't have built-in compression like socket.io
|
|
34
|
+
// but browsers handle compression at the transport level
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
// Setup connection handling
|
|
38
|
+
this.wss.on('connection', (ws: any, request: any) => {
|
|
39
|
+
this.handleConnection(ws, request);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
// Setup default namespace
|
|
43
|
+
this.createNamespace('/');
|
|
44
|
+
} catch (error) {
|
|
45
|
+
throw new Error(
|
|
46
|
+
'ws library not found. Install it with: npm install ws @types/ws\n' +
|
|
47
|
+
'Or use a different WebSocket adapter.'
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
private handleConnection(ws: any, request: any): void {
|
|
53
|
+
const id = this.generateId();
|
|
54
|
+
const connection = new WSConnectionWrapper(id, ws, request);
|
|
55
|
+
|
|
56
|
+
this.connections.set(id, connection);
|
|
57
|
+
|
|
58
|
+
// Parse namespace from URL path or default to '/'
|
|
59
|
+
const url = new URL(request.url || '/', `http://${request.headers.host}`);
|
|
60
|
+
const namespacePath = url.pathname === '/ws' ? '/' : url.pathname.replace('/ws', '') || '/';
|
|
61
|
+
|
|
62
|
+
const namespace = this.namespaces.get(namespacePath);
|
|
63
|
+
if (namespace) {
|
|
64
|
+
namespace.handleConnection(connection);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Clean up on disconnect
|
|
68
|
+
ws.on('close', () => {
|
|
69
|
+
this.connections.delete(id);
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
createNamespace(namespace: string): WebSocketNamespace {
|
|
74
|
+
if (!this.namespaces.has(namespace)) {
|
|
75
|
+
const ns = new WSNamespaceWrapper(namespace, this);
|
|
76
|
+
this.namespaces.set(namespace, ns);
|
|
77
|
+
}
|
|
78
|
+
return this.namespaces.get(namespace)!;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
getDefaultNamespace(): WebSocketNamespace {
|
|
82
|
+
return this.createNamespace('/');
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async close(): Promise<void> {
|
|
86
|
+
if (this.wss) {
|
|
87
|
+
return new Promise(resolve => {
|
|
88
|
+
this.wss.close(() => {
|
|
89
|
+
this.connections.clear();
|
|
90
|
+
this.namespaces.clear();
|
|
91
|
+
resolve();
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
setCompression(enabled: boolean, _options: any = {}): void {
|
|
98
|
+
// ws library handles compression at the browser level
|
|
99
|
+
// This is a no-op but kept for interface compatibility
|
|
100
|
+
if (enabled) {
|
|
101
|
+
console.warn('Compression is handled automatically by the ws library and browsers');
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
setCustomIdGenerator(generator: () => string): void {
|
|
106
|
+
this.customIdGenerator = generator;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
getAdapterName(): string {
|
|
110
|
+
return 'ws';
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
getConnectionCount(): number {
|
|
114
|
+
return this.connections.size;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
generateId(): string {
|
|
118
|
+
if (this.customIdGenerator) {
|
|
119
|
+
return this.customIdGenerator();
|
|
120
|
+
}
|
|
121
|
+
return `ws_${++this.connectionCounter}_${Date.now()}`;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
addConnection(id: string, connection: WSConnectionWrapper): void {
|
|
125
|
+
this.connections.set(id, connection);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
removeConnection(id: string): void {
|
|
129
|
+
this.connections.delete(id);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
getAllConnections(): Map<string, WSConnectionWrapper> {
|
|
133
|
+
return this.connections;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* WebSocket namespace wrapper
|
|
139
|
+
*/
|
|
140
|
+
class WSNamespaceWrapper implements WebSocketNamespace {
|
|
141
|
+
private connectionHandlers: ((socket: WebSocketConnection) => void)[] = [];
|
|
142
|
+
private middlewares: WebSocketMiddleware[] = [];
|
|
143
|
+
private connections = new Map<string, WSConnectionWrapper>();
|
|
144
|
+
|
|
145
|
+
constructor(
|
|
146
|
+
private namespacePath: string,
|
|
147
|
+
private adapter: WSAdapter
|
|
148
|
+
) {}
|
|
149
|
+
|
|
150
|
+
handleConnection(connection: WSConnectionWrapper): void {
|
|
151
|
+
this.connections.set(connection.id, connection);
|
|
152
|
+
|
|
153
|
+
// Run middlewares
|
|
154
|
+
this.runMiddlewares(connection, () => {
|
|
155
|
+
// Notify connection handlers
|
|
156
|
+
this.connectionHandlers.forEach(handler => handler(connection));
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
// Clean up on disconnect
|
|
160
|
+
connection.on('close', () => {
|
|
161
|
+
this.connections.delete(connection.id);
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
private runMiddlewares(connection: WSConnectionWrapper, callback: () => void): void {
|
|
166
|
+
let index = 0;
|
|
167
|
+
|
|
168
|
+
const next = (err?: Error) => {
|
|
169
|
+
if (err || index >= this.middlewares.length) {
|
|
170
|
+
if (!err) callback();
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const middleware = this.middlewares[index++];
|
|
175
|
+
middleware(connection, next);
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
next();
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
on(event: 'connection', handler: (socket: WebSocketConnection) => void): void {
|
|
182
|
+
this.connectionHandlers.push(handler);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
emit(event: string, data: any): void {
|
|
186
|
+
const message = JSON.stringify({ event, data });
|
|
187
|
+
for (const connection of this.connections.values()) {
|
|
188
|
+
if (connection.connected) {
|
|
189
|
+
connection.ws.send(message);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
to(room: string | string[]): WebSocketEmitter {
|
|
195
|
+
return new WSEmitterWrapper(this.connections, room);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
except(room: string | string[]): WebSocketEmitter {
|
|
199
|
+
return new WSEmitterWrapper(this.connections, undefined, room);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
getSockets(): WebSocketConnection[] {
|
|
203
|
+
return Array.from(this.connections.values());
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
getConnectionCount(): number {
|
|
207
|
+
return this.connections.size;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
use(middleware: WebSocketMiddleware): void {
|
|
211
|
+
this.middlewares.push(middleware);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* WebSocket connection wrapper
|
|
217
|
+
*/
|
|
218
|
+
class WSConnectionWrapper implements WebSocketConnection {
|
|
219
|
+
public data: Record<string, any> = {};
|
|
220
|
+
private eventHandlers = new Map<string, Function[]>();
|
|
221
|
+
private anyHandlers: Function[] = [];
|
|
222
|
+
private rooms = new Set<string>();
|
|
223
|
+
private _connected = true;
|
|
224
|
+
|
|
225
|
+
constructor(
|
|
226
|
+
public readonly id: string,
|
|
227
|
+
public readonly ws: any,
|
|
228
|
+
private request: any
|
|
229
|
+
) {
|
|
230
|
+
// Setup message handling
|
|
231
|
+
this.ws.on('message', (data: Buffer) => {
|
|
232
|
+
this.handleMessage(data);
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
this.ws.on('close', () => {
|
|
236
|
+
this._connected = false;
|
|
237
|
+
this.emit('close');
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
this.ws.on('error', (error: Error) => {
|
|
241
|
+
this.emit('error', error);
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
get ip(): string | undefined {
|
|
246
|
+
return (
|
|
247
|
+
this.request.socket?.remoteAddress || this.request.headers['x-forwarded-for']?.split(',')[0]
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
get headers(): Record<string, string> | undefined {
|
|
252
|
+
return this.request.headers;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
get connected(): boolean {
|
|
256
|
+
return this._connected && this.ws.readyState === 1; // WebSocket.OPEN
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
get broadcast(): WebSocketEmitter {
|
|
260
|
+
// Get all connections except this one
|
|
261
|
+
const allConnections = new Map();
|
|
262
|
+
// This would need access to adapter's connections
|
|
263
|
+
return new WSEmitterWrapper(allConnections, undefined, undefined, this.id);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
on(event: string, handler: (data: any, callback?: (response?: any) => void) => void): void {
|
|
267
|
+
if (event === 'close' || event === 'error') {
|
|
268
|
+
// Special internal events
|
|
269
|
+
if (!this.eventHandlers.has(event)) {
|
|
270
|
+
this.eventHandlers.set(event, []);
|
|
271
|
+
}
|
|
272
|
+
this.eventHandlers.get(event)!.push(handler);
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
if (!this.eventHandlers.has(event)) {
|
|
277
|
+
this.eventHandlers.set(event, []);
|
|
278
|
+
}
|
|
279
|
+
this.eventHandlers.get(event)!.push(handler);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
onAny(handler: (event: string, ...args: any[]) => void): void {
|
|
283
|
+
this.anyHandlers.push(handler);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
emit(event: string, data?: any): void {
|
|
287
|
+
if (event === 'close' || event === 'error') {
|
|
288
|
+
// Internal events
|
|
289
|
+
const handlers = this.eventHandlers.get(event);
|
|
290
|
+
if (handlers) {
|
|
291
|
+
handlers.forEach(handler => handler(data));
|
|
292
|
+
}
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
if (this.connected) {
|
|
297
|
+
const message = JSON.stringify({ event, data });
|
|
298
|
+
this.ws.send(message);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
compressedEmit(event: string, data: any): void {
|
|
303
|
+
// ws library handles compression automatically
|
|
304
|
+
this.emit(event, data);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
join(room: string | string[]): void {
|
|
308
|
+
if (Array.isArray(room)) {
|
|
309
|
+
room.forEach(r => this.rooms.add(r));
|
|
310
|
+
} else {
|
|
311
|
+
this.rooms.add(room);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
leave(room: string | string[]): void {
|
|
316
|
+
if (Array.isArray(room)) {
|
|
317
|
+
room.forEach(r => this.rooms.delete(r));
|
|
318
|
+
} else {
|
|
319
|
+
this.rooms.delete(room);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
to(room: string | string[]): WebSocketEmitter {
|
|
324
|
+
const connections = new Map([[this.id, this]]);
|
|
325
|
+
return new WSEmitterWrapper(connections, room);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
getRooms(): Set<string> {
|
|
329
|
+
return new Set(this.rooms);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
disconnect(close?: boolean): void {
|
|
333
|
+
if (close !== false && this.ws.readyState === 1) {
|
|
334
|
+
this.ws.close();
|
|
335
|
+
}
|
|
336
|
+
this._connected = false;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
private handleMessage(data: Buffer): void {
|
|
340
|
+
try {
|
|
341
|
+
const text = data.toString();
|
|
342
|
+
const parsed = JSON.parse(text);
|
|
343
|
+
const { event, data: messageData, callback: callbackId } = parsed;
|
|
344
|
+
|
|
345
|
+
// Create callback function if callback ID is provided
|
|
346
|
+
const callback = callbackId
|
|
347
|
+
? (response: any) => {
|
|
348
|
+
this.emit('callback', { id: callbackId, data: response });
|
|
349
|
+
}
|
|
350
|
+
: undefined;
|
|
351
|
+
|
|
352
|
+
// Call any handlers
|
|
353
|
+
this.anyHandlers.forEach(handler => handler(event, messageData));
|
|
354
|
+
|
|
355
|
+
// Call specific event handlers
|
|
356
|
+
const handlers = this.eventHandlers.get(event);
|
|
357
|
+
if (handlers) {
|
|
358
|
+
handlers.forEach(handler => handler(messageData, callback));
|
|
359
|
+
}
|
|
360
|
+
} catch (error) {
|
|
361
|
+
// Invalid message format - ignore
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* WebSocket emitter wrapper
|
|
368
|
+
*/
|
|
369
|
+
class WSEmitterWrapper implements WebSocketEmitter {
|
|
370
|
+
constructor(
|
|
371
|
+
private connections: Map<string, WSConnectionWrapper>,
|
|
372
|
+
private targetRooms?: string | string[],
|
|
373
|
+
private excludeRooms?: string | string[],
|
|
374
|
+
private excludeId?: string
|
|
375
|
+
) {}
|
|
376
|
+
|
|
377
|
+
emit(event: string, data: any): void {
|
|
378
|
+
const message = JSON.stringify({ event, data });
|
|
379
|
+
|
|
380
|
+
for (const connection of this.connections.values()) {
|
|
381
|
+
if (this.excludeId && connection.id === this.excludeId) {
|
|
382
|
+
continue;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
if (this.shouldIncludeConnection(connection) && connection.connected) {
|
|
386
|
+
connection.ws.send(message);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
to(room: string | string[]): WebSocketEmitter {
|
|
392
|
+
return new WSEmitterWrapper(this.connections, room, this.excludeRooms, this.excludeId);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
except(room: string | string[]): WebSocketEmitter {
|
|
396
|
+
return new WSEmitterWrapper(this.connections, this.targetRooms, room, this.excludeId);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
compress(_compress: boolean): WebSocketEmitter {
|
|
400
|
+
// ws library handles compression automatically
|
|
401
|
+
return this;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
private shouldIncludeConnection(connection: WSConnectionWrapper): boolean {
|
|
405
|
+
const rooms = connection.getRooms();
|
|
406
|
+
|
|
407
|
+
// Check target rooms
|
|
408
|
+
if (this.targetRooms) {
|
|
409
|
+
const targets = Array.isArray(this.targetRooms) ? this.targetRooms : [this.targetRooms];
|
|
410
|
+
if (!targets.some(room => rooms.has(room))) {
|
|
411
|
+
return false;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// Check exclude rooms
|
|
416
|
+
if (this.excludeRooms) {
|
|
417
|
+
const excludes = Array.isArray(this.excludeRooms) ? this.excludeRooms : [this.excludeRooms];
|
|
418
|
+
if (excludes.some(room => rooms.has(room))) {
|
|
419
|
+
return false;
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
return true;
|
|
424
|
+
}
|
|
425
|
+
}
|