@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.
Files changed (70) hide show
  1. package/README.md +61 -7
  2. package/dist/core/config/types.d.ts +147 -0
  3. package/dist/core/config/types.js +124 -0
  4. package/dist/core/config/types.js.map +1 -0
  5. package/dist/core/config/typescript-loader.d.ts +6 -0
  6. package/dist/core/config/typescript-loader.js +268 -0
  7. package/dist/core/config/typescript-loader.js.map +1 -0
  8. package/dist/core/config/validation.d.ts +18 -0
  9. package/dist/core/config/validation.js +134 -0
  10. package/dist/core/config/validation.js.map +1 -0
  11. package/dist/core/docs/openapi-generator.js +6 -6
  12. package/dist/core/docs/openapi-generator.js.map +1 -1
  13. package/dist/core/docs/schema-to-openapi.d.ts +7 -0
  14. package/dist/core/docs/schema-to-openapi.js +124 -0
  15. package/dist/core/docs/schema-to-openapi.js.map +1 -0
  16. package/dist/core/docs/zod-to-openapi.d.ts +2 -0
  17. package/dist/core/docs/zod-to-openapi.js.map +1 -1
  18. package/dist/core/framework.d.ts +29 -6
  19. package/dist/core/framework.js +117 -18
  20. package/dist/core/framework.js.map +1 -1
  21. package/dist/core/networking/adapters/index.d.ts +3 -0
  22. package/dist/core/networking/adapters/index.js +10 -0
  23. package/dist/core/networking/adapters/index.js.map +1 -0
  24. package/dist/core/networking/adapters/socketio-adapter.d.ts +16 -0
  25. package/dist/core/networking/adapters/socketio-adapter.js +244 -0
  26. package/dist/core/networking/adapters/socketio-adapter.js.map +1 -0
  27. package/dist/core/networking/adapters/ws-adapter.d.ts +54 -0
  28. package/dist/core/networking/adapters/ws-adapter.js +383 -0
  29. package/dist/core/networking/adapters/ws-adapter.js.map +1 -0
  30. package/dist/core/networking/websocket-adapter.d.ts +171 -0
  31. package/dist/core/networking/websocket-adapter.js +5 -0
  32. package/dist/core/networking/websocket-adapter.js.map +1 -0
  33. package/dist/core/networking/websocket-manager.d.ts +53 -17
  34. package/dist/core/networking/websocket-manager.js +166 -108
  35. package/dist/core/networking/websocket-manager.js.map +1 -1
  36. package/dist/core/routing/index.d.ts +13 -13
  37. package/dist/core/routing/index.js.map +1 -1
  38. package/dist/core/validation/adapters.d.ts +51 -0
  39. package/dist/core/validation/adapters.js +135 -0
  40. package/dist/core/validation/adapters.js.map +1 -0
  41. package/dist/core/validation/index.d.ts +14 -11
  42. package/dist/core/validation/index.js +37 -26
  43. package/dist/core/validation/index.js.map +1 -1
  44. package/dist/core/validation/schema-interface.d.ts +36 -0
  45. package/dist/core/validation/schema-interface.js +68 -0
  46. package/dist/core/validation/schema-interface.js.map +1 -0
  47. package/dist/index.d.ts +6 -1
  48. package/dist/index.js +14 -3
  49. package/dist/index.js.map +1 -1
  50. package/dist/moro.js +8 -2
  51. package/dist/moro.js.map +1 -1
  52. package/package.json +31 -7
  53. package/src/core/config/types.ts +277 -0
  54. package/src/core/config/typescript-loader.ts +571 -0
  55. package/src/core/config/validation.ts +145 -0
  56. package/src/core/docs/openapi-generator.ts +7 -6
  57. package/src/core/docs/schema-to-openapi.ts +148 -0
  58. package/src/core/docs/zod-to-openapi.ts +2 -0
  59. package/src/core/framework.ts +121 -28
  60. package/src/core/networking/adapters/index.ts +16 -0
  61. package/src/core/networking/adapters/socketio-adapter.ts +252 -0
  62. package/src/core/networking/adapters/ws-adapter.ts +425 -0
  63. package/src/core/networking/websocket-adapter.ts +217 -0
  64. package/src/core/networking/websocket-manager.ts +185 -127
  65. package/src/core/routing/index.ts +13 -13
  66. package/src/core/validation/adapters.ts +147 -0
  67. package/src/core/validation/index.ts +60 -38
  68. package/src/core/validation/schema-interface.ts +100 -0
  69. package/src/index.ts +25 -2
  70. 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
+ }