@aetherframework/websocket 1.0.1 → 1.0.3
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/examples/basic-server.js +95 -0
- package/examples/chat-server.js +416 -0
- package/examples/client-test.js +58 -0
- package/examples/realtime-api.js +98 -0
- package/examples//342/200/214stress-test-server.js +495 -0
- package/package.json +3 -1
- package/src/core/ConnectionManager.js +213 -0
- package/src/core/FrameParser.js +115 -0
- package/src/core/HandshakeHandler.js +93 -0
- package/src/core/ProtocolHandler.js +186 -0
- package/src/core/WebSocketFactory.js +293 -0
- package/src/drivers/http-driver.js +576 -0
- package/src/drivers/index.js +29 -0
- package/src/drivers/memory-driver.js +422 -0
- package/src/drivers/tcp-driver.js +471 -0
- package/src/drivers/tls-driver.js +502 -0
- package/src/middleware/auth-middleware.js +37 -0
- package/src/middleware/broadcast-manager.js +173 -0
- package/src/middleware/compression.js +194 -0
- package/src/middleware/message-logger.js +322 -0
- package/src/middleware/rate-limiter.js +142 -0
- package/src/utils/config-loader.js +183 -0
- package/src/utils/connection-pool.js +110 -0
- package/src/utils/error-handler.js +59 -0
- package/src/utils/frame-encoder.js +211 -0
- package/src/utils/heartbeat-manager.js +91 -0
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license MIT
|
|
3
|
+
* Copyright (c) 2026-present AetherFramework Contributors.
|
|
4
|
+
* SPDX-License-Identifier: MIT
|
|
5
|
+
* @module @aetherframework/src/core/ConnectionManager
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
class ConnectionManager {
|
|
9
|
+
constructor() {
|
|
10
|
+
this.connections = new Map();
|
|
11
|
+
this.groups = new Map();
|
|
12
|
+
this.stats = {
|
|
13
|
+
totalConnections: 0,
|
|
14
|
+
activeConnections: 0,
|
|
15
|
+
messagesSent: 0,
|
|
16
|
+
messagesReceived: 0,
|
|
17
|
+
bytesSent: 0,
|
|
18
|
+
bytesReceived: 0
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Add a new connection
|
|
24
|
+
* @param {Object} connection - Connection object
|
|
25
|
+
* @returns {Object} Enhanced connection object
|
|
26
|
+
*/
|
|
27
|
+
add(connection) {
|
|
28
|
+
const enhancedConnection = {
|
|
29
|
+
...connection,
|
|
30
|
+
groups: new Set(),
|
|
31
|
+
metadata: {},
|
|
32
|
+
createdAt: Date.now(),
|
|
33
|
+
lastActivity: Date.now(),
|
|
34
|
+
|
|
35
|
+
// Enhanced send method with statistics
|
|
36
|
+
send: (data, options = {}) => {
|
|
37
|
+
const result = connection.send(data, options);
|
|
38
|
+
if (result) {
|
|
39
|
+
this.stats.messagesSent++;
|
|
40
|
+
this.stats.bytesSent += Buffer.byteLength(data);
|
|
41
|
+
connection.lastActivity = Date.now();
|
|
42
|
+
}
|
|
43
|
+
return result;
|
|
44
|
+
},
|
|
45
|
+
|
|
46
|
+
// Enhanced close method
|
|
47
|
+
close: (code = 1000, reason = '') => {
|
|
48
|
+
this.remove(connection.id);
|
|
49
|
+
return connection.close(code, reason);
|
|
50
|
+
},
|
|
51
|
+
|
|
52
|
+
// Join a group
|
|
53
|
+
join: (groupName) => {
|
|
54
|
+
if (!this.groups.has(groupName)) {
|
|
55
|
+
this.groups.set(groupName, new Set());
|
|
56
|
+
}
|
|
57
|
+
this.groups.get(groupName).add(connection.id);
|
|
58
|
+
enhancedConnection.groups.add(groupName);
|
|
59
|
+
return true;
|
|
60
|
+
},
|
|
61
|
+
|
|
62
|
+
// Leave a group
|
|
63
|
+
leave: (groupName) => {
|
|
64
|
+
const group = this.groups.get(groupName);
|
|
65
|
+
if (group) {
|
|
66
|
+
group.delete(connection.id);
|
|
67
|
+
enhancedConnection.groups.delete(groupName);
|
|
68
|
+
return true;
|
|
69
|
+
}
|
|
70
|
+
return false;
|
|
71
|
+
},
|
|
72
|
+
|
|
73
|
+
// Get connection statistics
|
|
74
|
+
getStats: () => ({
|
|
75
|
+
id: connection.id,
|
|
76
|
+
readyState: connection.readyState,
|
|
77
|
+
uptime: Date.now() - enhancedConnection.createdAt,
|
|
78
|
+
lastActivity: enhancedConnection.lastActivity,
|
|
79
|
+
groups: Array.from(enhancedConnection.groups),
|
|
80
|
+
metadata: enhancedConnection.metadata,
|
|
81
|
+
remoteAddress: connection.remoteAddress,
|
|
82
|
+
remotePort: connection.remotePort
|
|
83
|
+
})
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
this.connections.set(connection.id, enhancedConnection);
|
|
87
|
+
this.stats.totalConnections++;
|
|
88
|
+
this.stats.activeConnections++;
|
|
89
|
+
|
|
90
|
+
// Set up cleanup on close
|
|
91
|
+
connection.on('close', () => {
|
|
92
|
+
this.remove(connection.id);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
return enhancedConnection;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Remove a connection
|
|
100
|
+
* @param {string} connectionId - Connection ID
|
|
101
|
+
* @returns {boolean} Success status
|
|
102
|
+
*/
|
|
103
|
+
remove(connectionId) {
|
|
104
|
+
const connection = this.connections.get(connectionId);
|
|
105
|
+
if (connection) {
|
|
106
|
+
// Remove from all groups
|
|
107
|
+
connection.groups.forEach(groupName => {
|
|
108
|
+
const group = this.groups.get(groupName);
|
|
109
|
+
if (group) {
|
|
110
|
+
group.delete(connectionId);
|
|
111
|
+
if (group.size === 0) {
|
|
112
|
+
this.groups.delete(groupName);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
this.connections.delete(connectionId);
|
|
118
|
+
this.stats.activeConnections--;
|
|
119
|
+
return true;
|
|
120
|
+
}
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Get connection by ID
|
|
126
|
+
* @param {string} connectionId - Connection ID
|
|
127
|
+
* @returns {Object|null} Connection object or null
|
|
128
|
+
*/
|
|
129
|
+
get(connectionId) {
|
|
130
|
+
return this.connections.get(connectionId) || null;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Get all connections
|
|
135
|
+
* @returns {Array} Array of connection objects
|
|
136
|
+
*/
|
|
137
|
+
getAll() {
|
|
138
|
+
return Array.from(this.connections.values());
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Get connections by group
|
|
143
|
+
* @param {string} groupName - Group name
|
|
144
|
+
* @returns {Array} Array of connections in the group
|
|
145
|
+
*/
|
|
146
|
+
getGroup(groupName) {
|
|
147
|
+
const group = this.groups.get(groupName);
|
|
148
|
+
if (!group) return [];
|
|
149
|
+
|
|
150
|
+
return Array.from(group)
|
|
151
|
+
.map(id => this.get(id))
|
|
152
|
+
.filter(conn => conn !== null && conn.readyState === 1);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Broadcast to a specific group
|
|
157
|
+
* @param {string} groupName - Group name
|
|
158
|
+
* @param {string|Buffer} message - Message to send
|
|
159
|
+
* @returns {number} Number of connections that received the message
|
|
160
|
+
*/
|
|
161
|
+
broadcastToGroup(groupName, message) {
|
|
162
|
+
const connections = this.getGroup(groupName);
|
|
163
|
+
let successCount = 0;
|
|
164
|
+
|
|
165
|
+
connections.forEach(connection => {
|
|
166
|
+
if (connection.send(message)) {
|
|
167
|
+
successCount++;
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
return successCount;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Filter connections based on criteria
|
|
176
|
+
* @param {Function} filterFn - Filter function
|
|
177
|
+
* @returns {Array} Filtered connections
|
|
178
|
+
*/
|
|
179
|
+
filter(filterFn) {
|
|
180
|
+
return this.getAll().filter(filterFn);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Get connection statistics
|
|
185
|
+
* @returns {Object} Statistics object
|
|
186
|
+
*/
|
|
187
|
+
getStats() {
|
|
188
|
+
return {
|
|
189
|
+
...this.stats,
|
|
190
|
+
groups: this.groups.size,
|
|
191
|
+
timestamp: Date.now()
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Clear all connections
|
|
197
|
+
*/
|
|
198
|
+
clear() {
|
|
199
|
+
this.connections.clear();
|
|
200
|
+
this.groups.clear();
|
|
201
|
+
this.stats.activeConnections = 0;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Get connection count
|
|
206
|
+
* @returns {number} Number of connections
|
|
207
|
+
*/
|
|
208
|
+
count() {
|
|
209
|
+
return this.connections.size;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export default ConnectionManager;
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license MIT
|
|
3
|
+
* Copyright (c) 2026-present AetherFramework Contributors.
|
|
4
|
+
* SPDX-License-Identifier: MIT
|
|
5
|
+
* @module @aetherframework/src/core/FrameParser
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
class FrameParser {
|
|
9
|
+
/**
|
|
10
|
+
* Parse raw buffer data into WebSocket frames
|
|
11
|
+
* @param {Buffer} buffer - Raw incoming data
|
|
12
|
+
* @param {number} maxPayload - Maximum allowed payload size
|
|
13
|
+
* @returns {Object} { frames: Array, remaining: Buffer }
|
|
14
|
+
*/
|
|
15
|
+
static parse(buffer, maxPayload = 1024 * 1024) {
|
|
16
|
+
const frames = [];
|
|
17
|
+
let offset = 0;
|
|
18
|
+
|
|
19
|
+
while (offset < buffer.length) {
|
|
20
|
+
// Need at least 2 bytes for basic header
|
|
21
|
+
if (buffer.length - offset < 2) break;
|
|
22
|
+
|
|
23
|
+
const byte1 = buffer[offset];
|
|
24
|
+
const byte2 = buffer[offset + 1];
|
|
25
|
+
|
|
26
|
+
const fin = (byte1 & 0x80) !== 0;
|
|
27
|
+
const opcode = byte1 & 0x0F;
|
|
28
|
+
const mask = (byte2 & 0x80) !== 0;
|
|
29
|
+
let payloadLength = byte2 & 0x7F;
|
|
30
|
+
|
|
31
|
+
let headerSize = 2;
|
|
32
|
+
let payloadOffset = offset + 2;
|
|
33
|
+
|
|
34
|
+
// Handle extended payload length
|
|
35
|
+
if (payloadLength === 126) {
|
|
36
|
+
if (buffer.length - offset < 4) break;
|
|
37
|
+
payloadLength = buffer.readUInt16BE(offset + 2);
|
|
38
|
+
headerSize += 2;
|
|
39
|
+
payloadOffset += 2;
|
|
40
|
+
} else if (payloadLength === 127) {
|
|
41
|
+
if (buffer.length - offset < 10) break;
|
|
42
|
+
// JavaScript numbers are safe up to 2^53, but WS spec says 2^63
|
|
43
|
+
// We use BigInt for safety then convert if safe, or throw if too large
|
|
44
|
+
const bigLen = buffer.readBigUInt64BE(offset + 2);
|
|
45
|
+
if (bigLen > BigInt(Number.MAX_SAFE_INTEGER)) {
|
|
46
|
+
throw new Error('Payload too large');
|
|
47
|
+
}
|
|
48
|
+
payloadLength = Number(bigLen);
|
|
49
|
+
headerSize += 8;
|
|
50
|
+
payloadOffset += 8;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Check payload size limit
|
|
54
|
+
if (payloadLength > maxPayload) {
|
|
55
|
+
throw new Error(`Payload exceeds maximum size: ${payloadLength} > ${maxPayload}`);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Handle masking key
|
|
59
|
+
let maskingKey = null;
|
|
60
|
+
if (mask) {
|
|
61
|
+
if (buffer.length - offset < headerSize + 4) break;
|
|
62
|
+
maskingKey = buffer.slice(offset + headerSize, offset + headerSize + 4);
|
|
63
|
+
headerSize += 4;
|
|
64
|
+
payloadOffset += 4;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Check if full payload is available
|
|
68
|
+
if (buffer.length - offset < headerSize + payloadLength) break;
|
|
69
|
+
|
|
70
|
+
// Extract payload
|
|
71
|
+
let payload = buffer.slice(payloadOffset, payloadOffset + payloadLength);
|
|
72
|
+
|
|
73
|
+
// Unmask if necessary
|
|
74
|
+
if (mask && maskingKey) {
|
|
75
|
+
payload = this._unmask(payload, maskingKey);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
frames.push({
|
|
79
|
+
fin,
|
|
80
|
+
opcode,
|
|
81
|
+
mask,
|
|
82
|
+
payloadLength,
|
|
83
|
+
payload
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
offset += headerSize + payloadLength;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
frames,
|
|
91
|
+
remaining: buffer.slice(offset)
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Unmask payload using XOR operation
|
|
97
|
+
* @param {Buffer} payload
|
|
98
|
+
* @param {Buffer} maskingKey
|
|
99
|
+
* @returns {Buffer} Unmasked payload
|
|
100
|
+
* @private
|
|
101
|
+
*/
|
|
102
|
+
static _unmask(payload, maskingKey) {
|
|
103
|
+
const len = payload.length;
|
|
104
|
+
const unmasked = Buffer.allocUnsafe(len);
|
|
105
|
+
|
|
106
|
+
// Optimized loop for unmasking
|
|
107
|
+
for (let i = 0; i < len; i++) {
|
|
108
|
+
unmasked[i] = payload[i] ^ maskingKey[i % 4];
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return unmasked;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export default FrameParser;
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license MIT
|
|
3
|
+
* Copyright (c) 2026-present AetherFramework Contributors.
|
|
4
|
+
* SPDX-License-Identifier: MIT
|
|
5
|
+
* @module @aetherframework/src/core/HandshakeHandler
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
import crypto from 'crypto';
|
|
10
|
+
|
|
11
|
+
class HandshakeHandler {
|
|
12
|
+
static GUID = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Parse HTTP headers from raw buffer
|
|
16
|
+
* @param {Buffer} buffer
|
|
17
|
+
* @returns {Object|null} Parsed request or null if incomplete
|
|
18
|
+
*/
|
|
19
|
+
static parseRequest(buffer) {
|
|
20
|
+
const str = buffer.toString('utf8');
|
|
21
|
+
const endOfHeaders = str.indexOf('\r\n\r\n');
|
|
22
|
+
|
|
23
|
+
if (endOfHeaders === -1) return null;
|
|
24
|
+
|
|
25
|
+
const lines = str.slice(0, endOfHeaders).split('\r\n');
|
|
26
|
+
const requestLine = lines;
|
|
27
|
+
const headers = {};
|
|
28
|
+
|
|
29
|
+
// Parse method and path
|
|
30
|
+
const [method, path] = requestLine.split(' ');
|
|
31
|
+
|
|
32
|
+
// Parse headers
|
|
33
|
+
for (let i = 1; i < lines.length; i++) {
|
|
34
|
+
const index = lines[i].indexOf(':');
|
|
35
|
+
if (index > 0) {
|
|
36
|
+
const key = lines[i].slice(0, index).trim().toLowerCase();
|
|
37
|
+
const value = lines[i].slice(index + 1).trim();
|
|
38
|
+
headers[key] = value;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
method,
|
|
44
|
+
path,
|
|
45
|
+
headers,
|
|
46
|
+
rawHeaderSize: endOfHeaders + 4
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Validate WebSocket upgrade request
|
|
52
|
+
* @param {Object} request
|
|
53
|
+
* @returns {boolean}
|
|
54
|
+
*/
|
|
55
|
+
static isValidUpgrade(request) {
|
|
56
|
+
return (
|
|
57
|
+
request.method === 'GET' &&
|
|
58
|
+
request.headers['upgrade']?.toLowerCase() === 'websocket' &&
|
|
59
|
+
request.headers['connection']?.toLowerCase().includes('upgrade') &&
|
|
60
|
+
request.headers['sec-websocket-key'] &&
|
|
61
|
+
request.headers['sec-websocket-version'] === '13'
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Generate handshake response headers
|
|
67
|
+
* @param {string} key - Sec-WebSocket-Key from client
|
|
68
|
+
* @param {string} protocol - Optional subprotocol
|
|
69
|
+
* @returns {Buffer} Response headers
|
|
70
|
+
*/
|
|
71
|
+
static createResponse(key, protocol = null) {
|
|
72
|
+
const accept = crypto
|
|
73
|
+
.createHash('sha1')
|
|
74
|
+
.update(key + this.GUID)
|
|
75
|
+
.digest('base64');
|
|
76
|
+
|
|
77
|
+
let response = [
|
|
78
|
+
'HTTP/1.1 101 Switching Protocols',
|
|
79
|
+
'Upgrade: websocket',
|
|
80
|
+
'Connection: Upgrade',
|
|
81
|
+
`Sec-WebSocket-Accept: ${accept}`
|
|
82
|
+
];
|
|
83
|
+
|
|
84
|
+
if (protocol) {
|
|
85
|
+
response.push(`Sec-WebSocket-Protocol: ${protocol}`);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
response.push('', ''); // Empty line to end headers
|
|
89
|
+
return Buffer.from(response.join('\r\n'), 'utf8');
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export default HandshakeHandler;
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license MIT
|
|
3
|
+
* Copyright (c) 2026-present AetherFramework Contributors.
|
|
4
|
+
* SPDX-License-Identifier: MIT
|
|
5
|
+
* @module @aetherframework/src/core/ProtocolHandler
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class ProtocolHandler {
|
|
10
|
+
constructor(options = {}) {
|
|
11
|
+
this.options = {
|
|
12
|
+
supportedProtocols: options.supportedProtocols || [],
|
|
13
|
+
maxMessageSize: options.maxMessageSize || 1024 * 1024, // 1MB
|
|
14
|
+
autoPing: options.autoPing !== false,
|
|
15
|
+
pingInterval: options.pingInterval || 30000,
|
|
16
|
+
pingTimeout: options.pingTimeout || 10000,
|
|
17
|
+
...options
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
this.messageHandlers = new Map();
|
|
21
|
+
this.protocols = new Set(this.options.supportedProtocols);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Handle WebSocket handshake protocol negotiation
|
|
26
|
+
* @param {Object} request - HTTP request object
|
|
27
|
+
* @returns {string|null} Selected protocol or null
|
|
28
|
+
*/
|
|
29
|
+
negotiateProtocol(request) {
|
|
30
|
+
const clientProtocols = request.headers['sec-websocket-protocol'];
|
|
31
|
+
if (!clientProtocols) return null;
|
|
32
|
+
|
|
33
|
+
const requestedProtocols = clientProtocols.split(',').map(p => p.trim());
|
|
34
|
+
|
|
35
|
+
// Find first matching protocol
|
|
36
|
+
for (const protocol of requestedProtocols) {
|
|
37
|
+
if (this.protocols.has(protocol)) {
|
|
38
|
+
return protocol;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Register message handler for specific protocol
|
|
47
|
+
* @param {string} protocol - Protocol name
|
|
48
|
+
* @param {Function} handler - Message handler function
|
|
49
|
+
*/
|
|
50
|
+
registerHandler(protocol, handler) {
|
|
51
|
+
this.messageHandlers.set(protocol, handler);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Process incoming WebSocket message
|
|
56
|
+
* @param {Object} connection - WebSocket connection
|
|
57
|
+
* @param {Buffer|string} message - Raw message
|
|
58
|
+
* @param {boolean} isBinary - Whether message is binary
|
|
59
|
+
* @returns {Promise<any>} Processing result
|
|
60
|
+
*/
|
|
61
|
+
async processMessage(connection, message, isBinary = false) {
|
|
62
|
+
const protocol = connection.protocol;
|
|
63
|
+
const handler = this.messageHandlers.get(protocol) || this.messageHandlers.get('*');
|
|
64
|
+
|
|
65
|
+
if (!handler) {
|
|
66
|
+
throw new Error(`No handler registered for protocol: ${protocol}`);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Validate message size
|
|
70
|
+
const messageSize = isBinary ? message.length : Buffer.byteLength(message);
|
|
71
|
+
if (messageSize > this.options.maxMessageSize) {
|
|
72
|
+
throw new Error(`Message size ${messageSize} exceeds maximum ${this.options.maxMessageSize}`);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Parse message based on protocol
|
|
76
|
+
let parsedMessage;
|
|
77
|
+
if (protocol === 'json') {
|
|
78
|
+
try {
|
|
79
|
+
parsedMessage = JSON.parse(message.toString());
|
|
80
|
+
} catch (error) {
|
|
81
|
+
throw new Error(`Invalid JSON: ${error.message}`);
|
|
82
|
+
}
|
|
83
|
+
} else if (protocol === 'binary') {
|
|
84
|
+
parsedMessage = message;
|
|
85
|
+
} else {
|
|
86
|
+
parsedMessage = message.toString();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Execute handler
|
|
90
|
+
return await handler(connection, parsedMessage, isBinary);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Handle ping/pong heartbeat
|
|
95
|
+
* @param {Object} connection - WebSocket connection
|
|
96
|
+
* @param {Buffer} data - Ping data
|
|
97
|
+
* @returns {Buffer} Pong response
|
|
98
|
+
*/
|
|
99
|
+
handlePing(connection, data) {
|
|
100
|
+
connection.lastPingTime = Date.now();
|
|
101
|
+
return data; // Echo back the same data for pong
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Handle pong response
|
|
106
|
+
* @param {Object} connection - WebSocket connection
|
|
107
|
+
* @param {Buffer} data - Pong data
|
|
108
|
+
*/
|
|
109
|
+
handlePong(connection, data) {
|
|
110
|
+
connection.lastPongTime = Date.now();
|
|
111
|
+
connection.isAlive = true;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Handle close frame
|
|
116
|
+
* @param {Object} connection - WebSocket connection
|
|
117
|
+
* @param {number} code - Close code
|
|
118
|
+
* @param {string} reason - Close reason
|
|
119
|
+
* @returns {Object} Close response
|
|
120
|
+
*/
|
|
121
|
+
handleClose(connection, code, reason) {
|
|
122
|
+
const validCodes = [
|
|
123
|
+
1000, 1001, 1002, 1003, 1007, 1008, 1009, 1010, 1011,
|
|
124
|
+
3000, 3999, 4000, 4999
|
|
125
|
+
];
|
|
126
|
+
|
|
127
|
+
if (!validCodes.includes(code)) {
|
|
128
|
+
code = 1002; // Protocol error
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return {
|
|
132
|
+
code,
|
|
133
|
+
reason: reason || this.getCloseReason(code)
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Get human-readable close reason
|
|
139
|
+
* @param {number} code - Close code
|
|
140
|
+
* @returns {string} Reason description
|
|
141
|
+
*/
|
|
142
|
+
getCloseReason(code) {
|
|
143
|
+
const reasons = {
|
|
144
|
+
1000: 'Normal closure',
|
|
145
|
+
1001: 'Going away',
|
|
146
|
+
1002: 'Protocol error',
|
|
147
|
+
1003: 'Unsupported data',
|
|
148
|
+
1007: 'Invalid frame payload data',
|
|
149
|
+
1008: 'Policy violation',
|
|
150
|
+
1009: 'Message too big',
|
|
151
|
+
1010: 'Mandatory extension',
|
|
152
|
+
1011: 'Internal server error',
|
|
153
|
+
3000: 'Custom code start',
|
|
154
|
+
3999: 'Custom code end',
|
|
155
|
+
4000: 'Reserved'
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
return reasons[code] || 'Unknown reason';
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Validate WebSocket version
|
|
163
|
+
* @param {string} version - WebSocket version
|
|
164
|
+
* @returns {boolean} Whether version is supported
|
|
165
|
+
*/
|
|
166
|
+
validateVersion(version) {
|
|
167
|
+
return version === '13'; // RFC 6455
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Get protocol statistics
|
|
172
|
+
* @returns {Object} Statistics object
|
|
173
|
+
*/
|
|
174
|
+
getStats() {
|
|
175
|
+
return {
|
|
176
|
+
supportedProtocols: Array.from(this.protocols),
|
|
177
|
+
registeredHandlers: this.messageHandlers.size,
|
|
178
|
+
maxMessageSize: this.options.maxMessageSize,
|
|
179
|
+
autoPing: this.options.autoPing,
|
|
180
|
+
pingInterval: this.options.pingInterval,
|
|
181
|
+
pingTimeout: this.options.pingTimeout
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export default ProtocolHandler;
|