@hazeljs/websocket 0.2.0-beta.1
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 +530 -0
- package/dist/decorators/realtime.decorator.d.ts +113 -0
- package/dist/decorators/realtime.decorator.d.ts.map +1 -0
- package/dist/decorators/realtime.decorator.js +202 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +32 -0
- package/dist/room/room.manager.d.ts +81 -0
- package/dist/room/room.manager.d.ts.map +1 -0
- package/dist/room/room.manager.js +209 -0
- package/dist/src/decorators/realtime.decorator.d.ts +113 -0
- package/dist/src/decorators/realtime.decorator.d.ts.map +1 -0
- package/dist/src/decorators/realtime.decorator.js +202 -0
- package/dist/src/index.d.ts +10 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +32 -0
- package/dist/src/room/room.manager.d.ts +81 -0
- package/dist/src/room/room.manager.d.ts.map +1 -0
- package/dist/src/room/room.manager.js +209 -0
- package/dist/src/sse/sse.handler.d.ts +61 -0
- package/dist/src/sse/sse.handler.d.ts.map +1 -0
- package/dist/src/sse/sse.handler.js +209 -0
- package/dist/src/websocket.gateway.d.ts +94 -0
- package/dist/src/websocket.gateway.d.ts.map +1 -0
- package/dist/src/websocket.gateway.js +309 -0
- package/dist/src/websocket.module.d.ts +57 -0
- package/dist/src/websocket.module.d.ts.map +1 -0
- package/dist/src/websocket.module.js +88 -0
- package/dist/src/websocket.types.d.ts +258 -0
- package/dist/src/websocket.types.d.ts.map +1 -0
- package/dist/src/websocket.types.js +2 -0
- package/dist/sse/sse.handler.d.ts +61 -0
- package/dist/sse/sse.handler.d.ts.map +1 -0
- package/dist/sse/sse.handler.js +209 -0
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/dist/websocket.gateway.d.ts +79 -0
- package/dist/websocket.gateway.d.ts.map +1 -0
- package/dist/websocket.gateway.js +214 -0
- package/dist/websocket.module.d.ts +57 -0
- package/dist/websocket.module.d.ts.map +1 -0
- package/dist/websocket.module.js +88 -0
- package/dist/websocket.types.d.ts +258 -0
- package/dist/websocket.types.d.ts.map +1 -0
- package/dist/websocket.types.js +2 -0
- package/package.json +48 -0
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.SSEHandler = void 0;
|
|
7
|
+
exports.createSSEResponse = createSSEResponse;
|
|
8
|
+
exports.sendSSEMessage = sendSSEMessage;
|
|
9
|
+
const core_1 = __importDefault(require("@hazeljs/core"));
|
|
10
|
+
/**
|
|
11
|
+
* Server-Sent Events (SSE) handler
|
|
12
|
+
*/
|
|
13
|
+
class SSEHandler {
|
|
14
|
+
constructor() {
|
|
15
|
+
this.connections = new Map();
|
|
16
|
+
this.keepAliveIntervals = new Map();
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Initialize SSE connection
|
|
20
|
+
*/
|
|
21
|
+
initConnection(req, res, options = {}) {
|
|
22
|
+
const connectionId = this.generateConnectionId();
|
|
23
|
+
// Set SSE headers
|
|
24
|
+
res.writeHead(200, {
|
|
25
|
+
'Content-Type': 'text/event-stream',
|
|
26
|
+
'Cache-Control': 'no-cache',
|
|
27
|
+
Connection: 'keep-alive',
|
|
28
|
+
'Access-Control-Allow-Origin': '*',
|
|
29
|
+
'X-Accel-Buffering': 'no', // Disable nginx buffering
|
|
30
|
+
});
|
|
31
|
+
// Send initial retry interval
|
|
32
|
+
if (options.retry) {
|
|
33
|
+
res.write(`retry: ${options.retry}\n\n`);
|
|
34
|
+
}
|
|
35
|
+
// Store connection
|
|
36
|
+
this.connections.set(connectionId, res);
|
|
37
|
+
// Setup keep-alive
|
|
38
|
+
const keepAliveInterval = options.keepAlive || 30000;
|
|
39
|
+
const interval = setInterval(() => {
|
|
40
|
+
if (this.connections.has(connectionId)) {
|
|
41
|
+
res.write(': keep-alive\n\n');
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
clearInterval(interval);
|
|
45
|
+
}
|
|
46
|
+
}, keepAliveInterval);
|
|
47
|
+
this.keepAliveIntervals.set(connectionId, interval);
|
|
48
|
+
// Handle connection close
|
|
49
|
+
req.on('close', () => {
|
|
50
|
+
this.closeConnection(connectionId);
|
|
51
|
+
});
|
|
52
|
+
core_1.default.debug(`SSE connection initialized: ${connectionId}`);
|
|
53
|
+
return connectionId;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Send message to a specific connection
|
|
57
|
+
*/
|
|
58
|
+
send(connectionId, message) {
|
|
59
|
+
const res = this.connections.get(connectionId);
|
|
60
|
+
if (!res) {
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
try {
|
|
64
|
+
let output = '';
|
|
65
|
+
// Add event type
|
|
66
|
+
if (message.event) {
|
|
67
|
+
output += `event: ${message.event}\n`;
|
|
68
|
+
}
|
|
69
|
+
// Add ID
|
|
70
|
+
if (message.id) {
|
|
71
|
+
output += `id: ${message.id}\n`;
|
|
72
|
+
}
|
|
73
|
+
// Add retry
|
|
74
|
+
if (message.retry) {
|
|
75
|
+
output += `retry: ${message.retry}\n`;
|
|
76
|
+
}
|
|
77
|
+
// Add data (can be multi-line)
|
|
78
|
+
const data = typeof message.data === 'string' ? message.data : JSON.stringify(message.data);
|
|
79
|
+
const lines = data.split('\n');
|
|
80
|
+
for (const line of lines) {
|
|
81
|
+
output += `data: ${line}\n`;
|
|
82
|
+
}
|
|
83
|
+
output += '\n';
|
|
84
|
+
res.write(output);
|
|
85
|
+
return true;
|
|
86
|
+
}
|
|
87
|
+
catch (error) {
|
|
88
|
+
core_1.default.error(`Failed to send SSE message to ${connectionId}:`, error);
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Broadcast message to all connections
|
|
94
|
+
*/
|
|
95
|
+
broadcast(message) {
|
|
96
|
+
for (const connectionId of this.connections.keys()) {
|
|
97
|
+
this.send(connectionId, message);
|
|
98
|
+
}
|
|
99
|
+
core_1.default.debug(`SSE broadcast: ${message.event || 'message'}`);
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Close a specific connection
|
|
103
|
+
*/
|
|
104
|
+
closeConnection(connectionId) {
|
|
105
|
+
const res = this.connections.get(connectionId);
|
|
106
|
+
if (res) {
|
|
107
|
+
res.end();
|
|
108
|
+
this.connections.delete(connectionId);
|
|
109
|
+
}
|
|
110
|
+
const interval = this.keepAliveIntervals.get(connectionId);
|
|
111
|
+
if (interval) {
|
|
112
|
+
clearInterval(interval);
|
|
113
|
+
this.keepAliveIntervals.delete(connectionId);
|
|
114
|
+
}
|
|
115
|
+
core_1.default.debug(`SSE connection closed: ${connectionId}`);
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Close all connections
|
|
119
|
+
*/
|
|
120
|
+
closeAll() {
|
|
121
|
+
for (const connectionId of this.connections.keys()) {
|
|
122
|
+
this.closeConnection(connectionId);
|
|
123
|
+
}
|
|
124
|
+
core_1.default.info('All SSE connections closed');
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Get active connection count
|
|
128
|
+
*/
|
|
129
|
+
getConnectionCount() {
|
|
130
|
+
return this.connections.size;
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Check if connection exists
|
|
134
|
+
*/
|
|
135
|
+
hasConnection(connectionId) {
|
|
136
|
+
return this.connections.has(connectionId);
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Get all connection IDs
|
|
140
|
+
*/
|
|
141
|
+
getConnectionIds() {
|
|
142
|
+
return Array.from(this.connections.keys());
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Generate unique connection ID
|
|
146
|
+
*/
|
|
147
|
+
generateConnectionId() {
|
|
148
|
+
return `sse-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Create a stream for continuous data
|
|
152
|
+
*/
|
|
153
|
+
async *createStream(connectionId, dataSource, options = {}) {
|
|
154
|
+
try {
|
|
155
|
+
for await (const data of dataSource) {
|
|
156
|
+
const transformedData = options.transform ? options.transform(data) : data;
|
|
157
|
+
const sent = this.send(connectionId, {
|
|
158
|
+
event: options.event,
|
|
159
|
+
data: transformedData,
|
|
160
|
+
});
|
|
161
|
+
yield sent;
|
|
162
|
+
if (!sent) {
|
|
163
|
+
break;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
catch (error) {
|
|
168
|
+
core_1.default.error(`SSE stream error for ${connectionId}:`, error);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
exports.SSEHandler = SSEHandler;
|
|
173
|
+
/**
|
|
174
|
+
* Helper function to create SSE response
|
|
175
|
+
*/
|
|
176
|
+
function createSSEResponse(res, options = {}) {
|
|
177
|
+
res.writeHead(200, {
|
|
178
|
+
'Content-Type': 'text/event-stream',
|
|
179
|
+
'Cache-Control': 'no-cache',
|
|
180
|
+
Connection: 'keep-alive',
|
|
181
|
+
'Access-Control-Allow-Origin': '*',
|
|
182
|
+
'X-Accel-Buffering': 'no',
|
|
183
|
+
});
|
|
184
|
+
if (options.retry) {
|
|
185
|
+
res.write(`retry: ${options.retry}\n\n`);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Helper function to send SSE message
|
|
190
|
+
*/
|
|
191
|
+
function sendSSEMessage(res, message) {
|
|
192
|
+
let output = '';
|
|
193
|
+
if (message.event) {
|
|
194
|
+
output += `event: ${message.event}\n`;
|
|
195
|
+
}
|
|
196
|
+
if (message.id) {
|
|
197
|
+
output += `id: ${message.id}\n`;
|
|
198
|
+
}
|
|
199
|
+
if (message.retry) {
|
|
200
|
+
output += `retry: ${message.retry}\n`;
|
|
201
|
+
}
|
|
202
|
+
const data = typeof message.data === 'string' ? message.data : JSON.stringify(message.data);
|
|
203
|
+
const lines = data.split('\n');
|
|
204
|
+
for (const line of lines) {
|
|
205
|
+
output += `data: ${line}\n`;
|
|
206
|
+
}
|
|
207
|
+
output += '\n';
|
|
208
|
+
res.write(output);
|
|
209
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { WebSocketClient, WebSocketMessage, WebSocketStats, WebSocketServerOptions } from './websocket.types';
|
|
2
|
+
import { RoomManager } from './room/room.manager';
|
|
3
|
+
import { Server as HttpServer } from 'http';
|
|
4
|
+
import { WebSocketServer } from 'ws';
|
|
5
|
+
/**
|
|
6
|
+
* Base WebSocket Gateway class
|
|
7
|
+
*/
|
|
8
|
+
export declare class WebSocketGateway {
|
|
9
|
+
protected clients: Map<string, WebSocketClient>;
|
|
10
|
+
protected roomManager: RoomManager;
|
|
11
|
+
protected wss: WebSocketServer | null;
|
|
12
|
+
protected stats: WebSocketStats;
|
|
13
|
+
/**
|
|
14
|
+
* Attach WebSocket server to an existing HTTP server
|
|
15
|
+
*/
|
|
16
|
+
attachToServer(server: HttpServer, options?: WebSocketServerOptions): WebSocketServer;
|
|
17
|
+
/**
|
|
18
|
+
* Create a standalone WebSocket server (without HTTP server)
|
|
19
|
+
*/
|
|
20
|
+
listen(port: number, options?: WebSocketServerOptions): WebSocketServer;
|
|
21
|
+
/**
|
|
22
|
+
* Close the WebSocket server
|
|
23
|
+
*/
|
|
24
|
+
close(): Promise<void>;
|
|
25
|
+
/**
|
|
26
|
+
* Handle client connection
|
|
27
|
+
*/
|
|
28
|
+
protected handleConnection(client: WebSocketClient): void;
|
|
29
|
+
/**
|
|
30
|
+
* Handle client disconnection
|
|
31
|
+
*/
|
|
32
|
+
protected handleDisconnection(clientId: string): void;
|
|
33
|
+
/**
|
|
34
|
+
* Handle incoming message
|
|
35
|
+
*/
|
|
36
|
+
protected handleMessage(clientId: string, message: WebSocketMessage): void;
|
|
37
|
+
/**
|
|
38
|
+
* Send message to a specific client
|
|
39
|
+
*/
|
|
40
|
+
sendToClient(clientId: string, event: string, data: unknown): boolean;
|
|
41
|
+
/**
|
|
42
|
+
* Broadcast message to all clients
|
|
43
|
+
*/
|
|
44
|
+
broadcast(event: string, data: unknown, excludeClientId?: string): void;
|
|
45
|
+
/**
|
|
46
|
+
* Broadcast to a specific room
|
|
47
|
+
*/
|
|
48
|
+
broadcastToRoom(roomName: string, event: string, data: unknown, excludeClientId?: string): void;
|
|
49
|
+
/**
|
|
50
|
+
* Add client to room
|
|
51
|
+
*/
|
|
52
|
+
joinRoom(clientId: string, roomName: string): void;
|
|
53
|
+
/**
|
|
54
|
+
* Remove client from room
|
|
55
|
+
*/
|
|
56
|
+
leaveRoom(clientId: string, roomName: string): void;
|
|
57
|
+
/**
|
|
58
|
+
* Get all clients
|
|
59
|
+
*/
|
|
60
|
+
getClients(): WebSocketClient[];
|
|
61
|
+
/**
|
|
62
|
+
* Get client by ID
|
|
63
|
+
*/
|
|
64
|
+
getClient(clientId: string): WebSocketClient | undefined;
|
|
65
|
+
/**
|
|
66
|
+
* Get connected client count
|
|
67
|
+
*/
|
|
68
|
+
getClientCount(): number;
|
|
69
|
+
/**
|
|
70
|
+
* Get room clients
|
|
71
|
+
*/
|
|
72
|
+
getRoomClients(roomName: string): string[];
|
|
73
|
+
/**
|
|
74
|
+
* Get client rooms
|
|
75
|
+
*/
|
|
76
|
+
getClientRooms(clientId: string): string[];
|
|
77
|
+
/**
|
|
78
|
+
* Get statistics
|
|
79
|
+
*/
|
|
80
|
+
getStats(): WebSocketStats;
|
|
81
|
+
/**
|
|
82
|
+
* Disconnect a client
|
|
83
|
+
*/
|
|
84
|
+
disconnectClient(clientId: string): void;
|
|
85
|
+
/**
|
|
86
|
+
* Disconnect all clients
|
|
87
|
+
*/
|
|
88
|
+
disconnectAll(): void;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Create a WebSocket client wrapper
|
|
92
|
+
*/
|
|
93
|
+
export declare function createWebSocketClient(socket: unknown, id: string): WebSocketClient;
|
|
94
|
+
//# sourceMappingURL=websocket.gateway.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"websocket.gateway.d.ts","sourceRoot":"","sources":["../../src/websocket.gateway.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,gBAAgB,EAAE,cAAc,EAAE,sBAAsB,EAAE,MAAM,mBAAmB,CAAC;AAC9G,OAAO,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAElD,OAAO,EAAE,MAAM,IAAI,UAAU,EAAmB,MAAM,MAAM,CAAC;AAC7D,OAAkB,EAAE,eAAe,EAAE,MAAM,IAAI,CAAC;AAGhD;;GAEG;AACH,qBAAa,gBAAgB;IAC3B,SAAS,CAAC,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,eAAe,CAAC,CAAa;IAC5D,SAAS,CAAC,WAAW,EAAE,WAAW,CAAqB;IACvD,SAAS,CAAC,GAAG,EAAE,eAAe,GAAG,IAAI,CAAQ;IAC7C,SAAS,CAAC,KAAK,EAAE,cAAc,CAQ7B;IAEF;;OAEG;IACH,cAAc,CAAC,MAAM,EAAE,UAAU,EAAE,OAAO,GAAE,sBAA2B,GAAG,eAAe;IAyCzF;;OAEG;IACH,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,GAAE,sBAA2B,GAAG,eAAe;IAqC3E;;OAEG;IACH,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAkBtB;;OAEG;IACH,SAAS,CAAC,gBAAgB,CAAC,MAAM,EAAE,eAAe,GAAG,IAAI;IAMzD;;OAEG;IACH,SAAS,CAAC,mBAAmB,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI;IAWrD;;OAEG;IACH,SAAS,CAAC,aAAa,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,gBAAgB,GAAG,IAAI;IAM1E;;OAEG;IACH,YAAY,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,GAAG,OAAO;IAYrE;;OAEG;IACH,SAAS,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,eAAe,CAAC,EAAE,MAAM,GAAG,IAAI;IAavE;;OAEG;IACH,eAAe,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,eAAe,CAAC,EAAE,MAAM,GAAG,IAAI;IAK/F;;OAEG;IACH,QAAQ,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,IAAI;IASlD;;OAEG;IACH,SAAS,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,IAAI;IASnD;;OAEG;IACH,UAAU,IAAI,eAAe,EAAE;IAI/B;;OAEG;IACH,SAAS,CAAC,QAAQ,EAAE,MAAM,GAAG,eAAe,GAAG,SAAS;IAIxD;;OAEG;IACH,cAAc,IAAI,MAAM;IAIxB;;OAEG;IACH,cAAc,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,EAAE;IAI1C;;OAEG;IACH,cAAc,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,EAAE;IAI1C;;OAEG;IACH,QAAQ,IAAI,cAAc;IAQ1B;;OAEG;IACH,gBAAgB,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI;IAQxC;;OAEG;IACH,aAAa,IAAI,IAAI;CAUtB;AAED;;GAEG;AACH,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,OAAO,EAAE,EAAE,EAAE,MAAM,GAAG,eAAe,CAsClF"}
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.WebSocketGateway = void 0;
|
|
7
|
+
exports.createWebSocketClient = createWebSocketClient;
|
|
8
|
+
const room_manager_1 = require("./room/room.manager");
|
|
9
|
+
const core_1 = __importDefault(require("@hazeljs/core"));
|
|
10
|
+
const ws_1 = require("ws");
|
|
11
|
+
const crypto_1 = require("crypto");
|
|
12
|
+
/**
|
|
13
|
+
* Base WebSocket Gateway class
|
|
14
|
+
*/
|
|
15
|
+
class WebSocketGateway {
|
|
16
|
+
constructor() {
|
|
17
|
+
this.clients = new Map();
|
|
18
|
+
this.roomManager = new room_manager_1.RoomManager();
|
|
19
|
+
this.wss = null;
|
|
20
|
+
this.stats = {
|
|
21
|
+
connectedClients: 0,
|
|
22
|
+
totalRooms: 0,
|
|
23
|
+
messagesSent: 0,
|
|
24
|
+
messagesReceived: 0,
|
|
25
|
+
bytesSent: 0,
|
|
26
|
+
bytesReceived: 0,
|
|
27
|
+
uptime: Date.now(),
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Attach WebSocket server to an existing HTTP server
|
|
32
|
+
*/
|
|
33
|
+
attachToServer(server, options = {}) {
|
|
34
|
+
this.wss = new ws_1.WebSocketServer({
|
|
35
|
+
server,
|
|
36
|
+
path: options.path || '/ws',
|
|
37
|
+
perMessageDeflate: options.perMessageDeflate ?? false,
|
|
38
|
+
maxPayload: options.maxPayload || 1048576, // 1MB default
|
|
39
|
+
clientTracking: options.clientTracking ?? true,
|
|
40
|
+
});
|
|
41
|
+
this.wss.on('connection', (socket, request) => {
|
|
42
|
+
const clientId = (0, crypto_1.randomUUID)();
|
|
43
|
+
const client = createWebSocketClient(socket, clientId);
|
|
44
|
+
this.handleConnection(client);
|
|
45
|
+
socket.on('message', (data) => {
|
|
46
|
+
try {
|
|
47
|
+
const parsed = JSON.parse(data.toString());
|
|
48
|
+
this.handleMessage(clientId, parsed);
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
core_1.default.warn(`Invalid WebSocket message from ${clientId}`);
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
socket.on('close', () => {
|
|
55
|
+
this.handleDisconnection(clientId);
|
|
56
|
+
});
|
|
57
|
+
socket.on('error', (error) => {
|
|
58
|
+
core_1.default.error(`WebSocket error for client ${clientId}:`, error);
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
this.wss.on('error', (error) => {
|
|
62
|
+
core_1.default.error('WebSocket server error:', error);
|
|
63
|
+
});
|
|
64
|
+
core_1.default.info(`WebSocket server attached at path: ${options.path || '/ws'}`);
|
|
65
|
+
return this.wss;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Create a standalone WebSocket server (without HTTP server)
|
|
69
|
+
*/
|
|
70
|
+
listen(port, options = {}) {
|
|
71
|
+
this.wss = new ws_1.WebSocketServer({
|
|
72
|
+
port,
|
|
73
|
+
path: options.path,
|
|
74
|
+
perMessageDeflate: options.perMessageDeflate ?? false,
|
|
75
|
+
maxPayload: options.maxPayload || 1048576,
|
|
76
|
+
clientTracking: options.clientTracking ?? true,
|
|
77
|
+
});
|
|
78
|
+
this.wss.on('connection', (socket, request) => {
|
|
79
|
+
const clientId = (0, crypto_1.randomUUID)();
|
|
80
|
+
const client = createWebSocketClient(socket, clientId);
|
|
81
|
+
this.handleConnection(client);
|
|
82
|
+
socket.on('message', (data) => {
|
|
83
|
+
try {
|
|
84
|
+
const parsed = JSON.parse(data.toString());
|
|
85
|
+
this.handleMessage(clientId, parsed);
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
core_1.default.warn(`Invalid WebSocket message from ${clientId}`);
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
socket.on('close', () => {
|
|
92
|
+
this.handleDisconnection(clientId);
|
|
93
|
+
});
|
|
94
|
+
socket.on('error', (error) => {
|
|
95
|
+
core_1.default.error(`WebSocket error for client ${clientId}:`, error);
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
core_1.default.info(`WebSocket server listening on port ${port}`);
|
|
99
|
+
return this.wss;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Close the WebSocket server
|
|
103
|
+
*/
|
|
104
|
+
close() {
|
|
105
|
+
return new Promise((resolve, reject) => {
|
|
106
|
+
if (!this.wss) {
|
|
107
|
+
resolve();
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
this.disconnectAll();
|
|
111
|
+
this.wss.close((err) => {
|
|
112
|
+
if (err)
|
|
113
|
+
reject(err);
|
|
114
|
+
else {
|
|
115
|
+
this.wss = null;
|
|
116
|
+
core_1.default.info('WebSocket server closed');
|
|
117
|
+
resolve();
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Handle client connection
|
|
124
|
+
*/
|
|
125
|
+
handleConnection(client) {
|
|
126
|
+
this.clients.set(client.id, client);
|
|
127
|
+
this.stats.connectedClients = this.clients.size;
|
|
128
|
+
core_1.default.info(`WebSocket client connected: ${client.id}`);
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Handle client disconnection
|
|
132
|
+
*/
|
|
133
|
+
handleDisconnection(clientId) {
|
|
134
|
+
const client = this.clients.get(clientId);
|
|
135
|
+
if (client) {
|
|
136
|
+
// Remove from all rooms
|
|
137
|
+
this.roomManager.removeClientFromAllRooms(clientId);
|
|
138
|
+
this.clients.delete(clientId);
|
|
139
|
+
this.stats.connectedClients = this.clients.size;
|
|
140
|
+
core_1.default.info(`WebSocket client disconnected: ${clientId}`);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Handle incoming message
|
|
145
|
+
*/
|
|
146
|
+
handleMessage(clientId, message) {
|
|
147
|
+
this.stats.messagesReceived++;
|
|
148
|
+
this.stats.bytesReceived += JSON.stringify(message).length;
|
|
149
|
+
core_1.default.debug(`Message received from ${clientId}: ${message.event}`);
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Send message to a specific client
|
|
153
|
+
*/
|
|
154
|
+
sendToClient(clientId, event, data) {
|
|
155
|
+
const client = this.clients.get(clientId);
|
|
156
|
+
if (!client) {
|
|
157
|
+
return false;
|
|
158
|
+
}
|
|
159
|
+
client.send(event, data);
|
|
160
|
+
this.stats.messagesSent++;
|
|
161
|
+
this.stats.bytesSent += JSON.stringify(data).length;
|
|
162
|
+
return true;
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Broadcast message to all clients
|
|
166
|
+
*/
|
|
167
|
+
broadcast(event, data, excludeClientId) {
|
|
168
|
+
for (const [clientId, client] of this.clients) {
|
|
169
|
+
if (excludeClientId && clientId === excludeClientId) {
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
client.send(event, data);
|
|
173
|
+
}
|
|
174
|
+
this.stats.messagesSent += this.clients.size;
|
|
175
|
+
this.stats.bytesSent += JSON.stringify(data).length * this.clients.size;
|
|
176
|
+
core_1.default.debug(`Broadcast: ${event} to ${this.clients.size} clients`);
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Broadcast to a specific room
|
|
180
|
+
*/
|
|
181
|
+
broadcastToRoom(roomName, event, data, excludeClientId) {
|
|
182
|
+
this.roomManager.broadcastToRoom(roomName, event, data, this.clients, excludeClientId);
|
|
183
|
+
this.stats.messagesSent++;
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Add client to room
|
|
187
|
+
*/
|
|
188
|
+
joinRoom(clientId, roomName) {
|
|
189
|
+
const client = this.clients.get(clientId);
|
|
190
|
+
if (client) {
|
|
191
|
+
client.join(roomName);
|
|
192
|
+
this.roomManager.addClientToRoom(clientId, roomName);
|
|
193
|
+
this.stats.totalRooms = this.roomManager.getRoomCount();
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Remove client from room
|
|
198
|
+
*/
|
|
199
|
+
leaveRoom(clientId, roomName) {
|
|
200
|
+
const client = this.clients.get(clientId);
|
|
201
|
+
if (client) {
|
|
202
|
+
client.leave(roomName);
|
|
203
|
+
this.roomManager.removeClientFromRoom(clientId, roomName);
|
|
204
|
+
this.stats.totalRooms = this.roomManager.getRoomCount();
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Get all clients
|
|
209
|
+
*/
|
|
210
|
+
getClients() {
|
|
211
|
+
return Array.from(this.clients.values());
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Get client by ID
|
|
215
|
+
*/
|
|
216
|
+
getClient(clientId) {
|
|
217
|
+
return this.clients.get(clientId);
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Get connected client count
|
|
221
|
+
*/
|
|
222
|
+
getClientCount() {
|
|
223
|
+
return this.clients.size;
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* Get room clients
|
|
227
|
+
*/
|
|
228
|
+
getRoomClients(roomName) {
|
|
229
|
+
return this.roomManager.getRoomClients(roomName);
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* Get client rooms
|
|
233
|
+
*/
|
|
234
|
+
getClientRooms(clientId) {
|
|
235
|
+
return this.roomManager.getClientRooms(clientId);
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* Get statistics
|
|
239
|
+
*/
|
|
240
|
+
getStats() {
|
|
241
|
+
return {
|
|
242
|
+
...this.stats,
|
|
243
|
+
uptime: Date.now() - this.stats.uptime,
|
|
244
|
+
totalRooms: this.roomManager.getRoomCount(),
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* Disconnect a client
|
|
249
|
+
*/
|
|
250
|
+
disconnectClient(clientId) {
|
|
251
|
+
const client = this.clients.get(clientId);
|
|
252
|
+
if (client) {
|
|
253
|
+
client.disconnect();
|
|
254
|
+
this.handleDisconnection(clientId);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* Disconnect all clients
|
|
259
|
+
*/
|
|
260
|
+
disconnectAll() {
|
|
261
|
+
for (const client of this.clients.values()) {
|
|
262
|
+
client.disconnect();
|
|
263
|
+
}
|
|
264
|
+
this.clients.clear();
|
|
265
|
+
this.roomManager.clear();
|
|
266
|
+
this.stats.connectedClients = 0;
|
|
267
|
+
this.stats.totalRooms = 0;
|
|
268
|
+
core_1.default.info('All WebSocket clients disconnected');
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
exports.WebSocketGateway = WebSocketGateway;
|
|
272
|
+
/**
|
|
273
|
+
* Create a WebSocket client wrapper
|
|
274
|
+
*/
|
|
275
|
+
function createWebSocketClient(socket, id) {
|
|
276
|
+
const client = {
|
|
277
|
+
id,
|
|
278
|
+
socket,
|
|
279
|
+
metadata: new Map(),
|
|
280
|
+
rooms: new Set(),
|
|
281
|
+
send(event, data) {
|
|
282
|
+
try {
|
|
283
|
+
const message = JSON.stringify({ event, data, timestamp: Date.now() });
|
|
284
|
+
socket.send(message);
|
|
285
|
+
}
|
|
286
|
+
catch (error) {
|
|
287
|
+
core_1.default.error(`Failed to send message to client ${id}:`, error);
|
|
288
|
+
}
|
|
289
|
+
},
|
|
290
|
+
disconnect() {
|
|
291
|
+
try {
|
|
292
|
+
socket.close();
|
|
293
|
+
}
|
|
294
|
+
catch (error) {
|
|
295
|
+
core_1.default.error(`Failed to disconnect client ${id}:`, error);
|
|
296
|
+
}
|
|
297
|
+
},
|
|
298
|
+
join(room) {
|
|
299
|
+
this.rooms.add(room);
|
|
300
|
+
},
|
|
301
|
+
leave(room) {
|
|
302
|
+
this.rooms.delete(room);
|
|
303
|
+
},
|
|
304
|
+
inRoom(room) {
|
|
305
|
+
return this.rooms.has(room);
|
|
306
|
+
},
|
|
307
|
+
};
|
|
308
|
+
return client;
|
|
309
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { WebSocketServerOptions } from './websocket.types';
|
|
2
|
+
import { SSEHandler } from './sse/sse.handler';
|
|
3
|
+
import { RoomManager } from './room/room.manager';
|
|
4
|
+
/**
|
|
5
|
+
* WebSocket module options
|
|
6
|
+
*/
|
|
7
|
+
export interface WebSocketModuleOptions extends WebSocketServerOptions {
|
|
8
|
+
/**
|
|
9
|
+
* Whether this is a global module
|
|
10
|
+
* @default true
|
|
11
|
+
*/
|
|
12
|
+
isGlobal?: boolean;
|
|
13
|
+
/**
|
|
14
|
+
* Enable SSE support
|
|
15
|
+
* @default true
|
|
16
|
+
*/
|
|
17
|
+
enableSSE?: boolean;
|
|
18
|
+
/**
|
|
19
|
+
* Enable room management
|
|
20
|
+
* @default true
|
|
21
|
+
*/
|
|
22
|
+
enableRooms?: boolean;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* WebSocket module for HazelJS
|
|
26
|
+
*/
|
|
27
|
+
export declare class WebSocketModule {
|
|
28
|
+
/**
|
|
29
|
+
* Configure WebSocket module
|
|
30
|
+
*/
|
|
31
|
+
static forRoot(options?: WebSocketModuleOptions): {
|
|
32
|
+
module: typeof WebSocketModule;
|
|
33
|
+
providers: Array<{
|
|
34
|
+
provide: typeof SSEHandler | typeof RoomManager;
|
|
35
|
+
useValue: SSEHandler | RoomManager;
|
|
36
|
+
}>;
|
|
37
|
+
exports: Array<typeof SSEHandler | typeof RoomManager>;
|
|
38
|
+
global: boolean;
|
|
39
|
+
};
|
|
40
|
+
/**
|
|
41
|
+
* Configure WebSocket module asynchronously
|
|
42
|
+
*/
|
|
43
|
+
static forRootAsync(options: {
|
|
44
|
+
useFactory: (...args: unknown[]) => Promise<WebSocketModuleOptions> | WebSocketModuleOptions;
|
|
45
|
+
inject?: unknown[];
|
|
46
|
+
}): {
|
|
47
|
+
module: typeof WebSocketModule;
|
|
48
|
+
providers: Array<{
|
|
49
|
+
provide: string | typeof SSEHandler | typeof RoomManager;
|
|
50
|
+
useFactory: unknown;
|
|
51
|
+
inject?: unknown[];
|
|
52
|
+
}>;
|
|
53
|
+
exports: Array<typeof SSEHandler | typeof RoomManager>;
|
|
54
|
+
global: boolean;
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
//# sourceMappingURL=websocket.module.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"websocket.module.d.ts","sourceRoot":"","sources":["../../src/websocket.module.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,sBAAsB,EAAE,MAAM,mBAAmB,CAAC;AAC3D,OAAO,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAC/C,OAAO,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAGlD;;GAEG;AACH,MAAM,WAAW,sBAAuB,SAAQ,sBAAsB;IACpE;;;OAGG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;IAEnB;;;OAGG;IACH,SAAS,CAAC,EAAE,OAAO,CAAC;IAEpB;;;OAGG;IACH,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB;AAED;;GAEG;AACH,qBAIa,eAAe;IAC1B;;OAEG;IACH,MAAM,CAAC,OAAO,CAAC,OAAO,GAAE,sBAA2B,GAAG;QACpD,MAAM,EAAE,OAAO,eAAe,CAAC;QAC/B,SAAS,EAAE,KAAK,CAAC;YACf,OAAO,EAAE,OAAO,UAAU,GAAG,OAAO,WAAW,CAAC;YAChD,QAAQ,EAAE,UAAU,GAAG,WAAW,CAAC;SACpC,CAAC,CAAC;QACH,OAAO,EAAE,KAAK,CAAC,OAAO,UAAU,GAAG,OAAO,WAAW,CAAC,CAAC;QACvD,MAAM,EAAE,OAAO,CAAC;KACjB;IAkCD;;OAEG;IACH,MAAM,CAAC,YAAY,CAAC,OAAO,EAAE;QAC3B,UAAU,EAAE,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,OAAO,CAAC,sBAAsB,CAAC,GAAG,sBAAsB,CAAC;QAC7F,MAAM,CAAC,EAAE,OAAO,EAAE,CAAC;KACpB,GAAG;QACF,MAAM,EAAE,OAAO,eAAe,CAAC;QAC/B,SAAS,EAAE,KAAK,CAAC;YACf,OAAO,EAAE,MAAM,GAAG,OAAO,UAAU,GAAG,OAAO,WAAW,CAAC;YACzD,UAAU,EAAE,OAAO,CAAC;YACpB,MAAM,CAAC,EAAE,OAAO,EAAE,CAAC;SACpB,CAAC,CAAC;QACH,OAAO,EAAE,KAAK,CAAC,OAAO,UAAU,GAAG,OAAO,WAAW,CAAC,CAAC;QACvD,MAAM,EAAE,OAAO,CAAC;KACjB;CA4BF"}
|