@aetherframework/websocket 1.0.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 +213 -0
- package/index.js +59 -0
- package/package.json +51 -0
- 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,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;
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license MIT
|
|
3
|
+
* Copyright (c) 2026-present AetherFramework Contributors.
|
|
4
|
+
* SPDX-License-Identifier: MIT
|
|
5
|
+
* @module @aetherframework/src/core/WebSocketFactory
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import EventEmitter from 'events';
|
|
9
|
+
import ConnectionManager from './ConnectionManager.js';
|
|
10
|
+
import FrameParser from './FrameParser.js';
|
|
11
|
+
import ProtocolHandler from './ProtocolHandler.js';
|
|
12
|
+
import ConfigLoader from '../utils/config-loader.js';
|
|
13
|
+
|
|
14
|
+
class WebSocketFactory extends EventEmitter {
|
|
15
|
+
/**
|
|
16
|
+
* Create a new WebSocketFactory instance
|
|
17
|
+
* @param {Object} options - Configuration options
|
|
18
|
+
*/
|
|
19
|
+
constructor(options = {}) {
|
|
20
|
+
super();
|
|
21
|
+
|
|
22
|
+
// Load configuration from environment and options
|
|
23
|
+
this.config = ConfigLoader.load({
|
|
24
|
+
// Default configuration
|
|
25
|
+
driver: 'tcp',
|
|
26
|
+
port: 8080,
|
|
27
|
+
host: '0.0.0.0',
|
|
28
|
+
maxPayload: 1024 * 1024, // 1MB
|
|
29
|
+
pingInterval: 30000, // 30 seconds
|
|
30
|
+
pingTimeout: 10000, // 10 seconds
|
|
31
|
+
compression: false,
|
|
32
|
+
tls: false,
|
|
33
|
+
tlsOptions: {},
|
|
34
|
+
// Merge with user options
|
|
35
|
+
...options
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
this.connectionManager = new ConnectionManager();
|
|
39
|
+
this.frameParser = new FrameParser(this.config);
|
|
40
|
+
this.protocolHandler = new ProtocolHandler(this.config);
|
|
41
|
+
this.driver = null;
|
|
42
|
+
this.server = null;
|
|
43
|
+
this.middleware = [];
|
|
44
|
+
|
|
45
|
+
// Initialize driver
|
|
46
|
+
this._initializeDriver();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Initialize the selected transport driver
|
|
51
|
+
* @private
|
|
52
|
+
*/
|
|
53
|
+
async _initializeDriver() {
|
|
54
|
+
try {
|
|
55
|
+
const driverName = this.config.driver || 'tcp';
|
|
56
|
+
const driverModule = await import(`../drivers/${driverName}-driver.js`);
|
|
57
|
+
this.driver = new driverModule.default(this.config);
|
|
58
|
+
|
|
59
|
+
// Set up driver event forwarding
|
|
60
|
+
this._setupDriverEvents();
|
|
61
|
+
|
|
62
|
+
this.emit('driver:initialized', { driver: driverName });
|
|
63
|
+
} catch (error) {
|
|
64
|
+
this.emit('error', {
|
|
65
|
+
type: 'driver_initialization',
|
|
66
|
+
message: `Failed to initialize driver: ${error.message}`,
|
|
67
|
+
error
|
|
68
|
+
});
|
|
69
|
+
throw error;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Set up event forwarding from driver to factory
|
|
75
|
+
* @private
|
|
76
|
+
*/
|
|
77
|
+
_setupDriverEvents() {
|
|
78
|
+
if (!this.driver) return;
|
|
79
|
+
|
|
80
|
+
// Forward all driver events to factory
|
|
81
|
+
const eventsToForward = [
|
|
82
|
+
'server:listening',
|
|
83
|
+
'server:closed',
|
|
84
|
+
'server:error',
|
|
85
|
+
'connection',
|
|
86
|
+
'message',
|
|
87
|
+
'close',
|
|
88
|
+
'error',
|
|
89
|
+
'ping',
|
|
90
|
+
'pong'
|
|
91
|
+
];
|
|
92
|
+
|
|
93
|
+
eventsToForward.forEach(event => {
|
|
94
|
+
this.driver.on(event, (...args) => {
|
|
95
|
+
this.emit(event, ...args);
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Create a WebSocket server
|
|
102
|
+
* @param {Object} options - Server-specific options
|
|
103
|
+
* @returns {Promise<Object>} Server instance
|
|
104
|
+
*/
|
|
105
|
+
async createServer(options = {}) {
|
|
106
|
+
if (!this.driver) {
|
|
107
|
+
await this._initializeDriver();
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const serverOptions = {
|
|
111
|
+
...this.config,
|
|
112
|
+
...options
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
try {
|
|
116
|
+
this.server = await this.driver.createServer(serverOptions);
|
|
117
|
+
|
|
118
|
+
// CRITICAL FIX: Ensure server:listening event is emitted
|
|
119
|
+
// Get server address information
|
|
120
|
+
let addressInfo;
|
|
121
|
+
if (this.server.address && typeof this.server.address === 'function') {
|
|
122
|
+
addressInfo = this.server.address();
|
|
123
|
+
} else if (this.server.address) {
|
|
124
|
+
addressInfo = this.server.address;
|
|
125
|
+
} else {
|
|
126
|
+
addressInfo = {
|
|
127
|
+
address: serverOptions.host || '0.0.0.0',
|
|
128
|
+
port: serverOptions.port || 8080,
|
|
129
|
+
family: 'IPv4'
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Emit server:listening event with address information
|
|
134
|
+
// Use setTimeout to ensure event listeners are registered
|
|
135
|
+
setTimeout(() => {
|
|
136
|
+
this.emit('server:listening', {
|
|
137
|
+
host: addressInfo.address,
|
|
138
|
+
port: addressInfo.port,
|
|
139
|
+
family: addressInfo.family || 'IPv4'
|
|
140
|
+
});
|
|
141
|
+
}, 0);
|
|
142
|
+
|
|
143
|
+
this.emit('server:created', this.server);
|
|
144
|
+
return this.server;
|
|
145
|
+
} catch (error) {
|
|
146
|
+
this.emit('error', {
|
|
147
|
+
type: 'server_creation',
|
|
148
|
+
message: `Failed to create server: ${error.message}`,
|
|
149
|
+
error
|
|
150
|
+
});
|
|
151
|
+
throw error;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Create a WebSocket client connection
|
|
157
|
+
* @param {string} url - WebSocket URL (ws:// or wss://)
|
|
158
|
+
* @param {Object} options - Client options
|
|
159
|
+
* @returns {Promise<Object>} Client connection
|
|
160
|
+
*/
|
|
161
|
+
async createClient(url, options = {}) {
|
|
162
|
+
if (!this.driver) {
|
|
163
|
+
await this._initializeDriver();
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const clientOptions = {
|
|
167
|
+
...this.config,
|
|
168
|
+
...options
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
try {
|
|
172
|
+
const client = await this.driver.createClient(url, clientOptions);
|
|
173
|
+
this.emit('client:created', client);
|
|
174
|
+
return client;
|
|
175
|
+
} catch (error) {
|
|
176
|
+
this.emit('error', {
|
|
177
|
+
type: 'client_creation',
|
|
178
|
+
message: `Failed to create client: ${error.message}`,
|
|
179
|
+
error
|
|
180
|
+
});
|
|
181
|
+
throw error;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Add middleware to the WebSocket pipeline
|
|
187
|
+
* @param {Function} middleware - Middleware function
|
|
188
|
+
*/
|
|
189
|
+
use(middleware) {
|
|
190
|
+
this.middleware.push(middleware);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Broadcast message to all connected clients
|
|
195
|
+
* @param {string|Buffer} message - Message to broadcast
|
|
196
|
+
* @param {Function} filter - Optional filter function
|
|
197
|
+
*/
|
|
198
|
+
broadcast(message, filter = null) {
|
|
199
|
+
const connections = this.connectionManager.getAll();
|
|
200
|
+
|
|
201
|
+
connections.forEach(connection => {
|
|
202
|
+
if (!filter || filter(connection)) {
|
|
203
|
+
try {
|
|
204
|
+
connection.send(message);
|
|
205
|
+
} catch (error) {
|
|
206
|
+
this.emit('error', {
|
|
207
|
+
type: 'broadcast',
|
|
208
|
+
message: `Failed to broadcast to connection ${connection.id}`,
|
|
209
|
+
error,
|
|
210
|
+
connection
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Send message to specific connection
|
|
219
|
+
* @param {string} connectionId - Connection ID
|
|
220
|
+
* @param {string|Buffer} message - Message to send
|
|
221
|
+
* @returns {boolean} Success status
|
|
222
|
+
*/
|
|
223
|
+
sendTo(connectionId, message) {
|
|
224
|
+
const connection = this.connectionManager.get(connectionId);
|
|
225
|
+
if (connection && connection.readyState === 1) { // OPEN
|
|
226
|
+
return connection.send(message);
|
|
227
|
+
}
|
|
228
|
+
return false;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Get connection by ID
|
|
233
|
+
* @param {string} connectionId - Connection ID
|
|
234
|
+
* @returns {Object|null} Connection object or null
|
|
235
|
+
*/
|
|
236
|
+
getConnection(connectionId) {
|
|
237
|
+
return this.connectionManager.get(connectionId);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Get all connections
|
|
242
|
+
* @returns {Array} Array of connection objects
|
|
243
|
+
*/
|
|
244
|
+
getConnections() {
|
|
245
|
+
return this.connectionManager.getAll();
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Get connection statistics
|
|
250
|
+
* @returns {Object} Statistics object
|
|
251
|
+
*/
|
|
252
|
+
getStats() {
|
|
253
|
+
const connections = this.connectionManager.getAll();
|
|
254
|
+
|
|
255
|
+
return {
|
|
256
|
+
totalConnections: connections.length,
|
|
257
|
+
activeConnections: connections.filter(c => c.readyState === 1).length,
|
|
258
|
+
driver: this.config.driver,
|
|
259
|
+
uptime: this.server ? Date.now() - this.server.startTime : 0,
|
|
260
|
+
memoryUsage: process.memoryUsage(),
|
|
261
|
+
config: {
|
|
262
|
+
maxPayload: this.config.maxPayload,
|
|
263
|
+
pingInterval: this.config.pingInterval,
|
|
264
|
+
pingTimeout: this.config.pingTimeout,
|
|
265
|
+
compression: this.config.compression
|
|
266
|
+
}
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Close the WebSocket server
|
|
272
|
+
* @returns {Promise<void>}
|
|
273
|
+
*/
|
|
274
|
+
async close() {
|
|
275
|
+
if (this.server) {
|
|
276
|
+
await this.driver.closeServer(this.server);
|
|
277
|
+
this.emit('server:closed');
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Close all connections
|
|
281
|
+
const connections = this.connectionManager.getAll();
|
|
282
|
+
connections.forEach(connection => {
|
|
283
|
+
if (connection.readyState === 1) {
|
|
284
|
+
connection.close(1000, 'Server shutting down');
|
|
285
|
+
}
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
this.connectionManager.clear();
|
|
289
|
+
this.emit('closed');
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
export default WebSocketFactory;
|