@aetherframework/websocket 1.0.0 → 1.0.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/LICENSE +21 -0
- package/package.json +1 -2
- package/src/core/ConnectionManager.js +0 -213
- package/src/core/FrameParser.js +0 -115
- package/src/core/HandshakeHandler.js +0 -93
- package/src/core/ProtocolHandler.js +0 -186
- package/src/core/WebSocketFactory.js +0 -293
- package/src/drivers/http-driver.js +0 -576
- package/src/drivers/index.js +0 -29
- package/src/drivers/memory-driver.js +0 -422
- package/src/drivers/tcp-driver.js +0 -471
- package/src/drivers/tls-driver.js +0 -502
- package/src/middleware/auth-middleware.js +0 -37
- package/src/middleware/broadcast-manager.js +0 -173
- package/src/middleware/compression.js +0 -194
- package/src/middleware/message-logger.js +0 -322
- package/src/middleware/rate-limiter.js +0 -142
- package/src/utils/config-loader.js +0 -183
- package/src/utils/connection-pool.js +0 -110
- package/src/utils/error-handler.js +0 -59
- package/src/utils/frame-encoder.js +0 -211
- package/src/utils/heartbeat-manager.js +0 -91
|
@@ -1,183 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @license MIT
|
|
3
|
-
* Copyright (c) 2026-present AetherFramework Contributors.
|
|
4
|
-
* SPDX-License-Identifier: MIT
|
|
5
|
-
* @module @aetherframework/src/utils/config-loader
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import { readFileSync } from 'fs';
|
|
9
|
-
import { resolve } from 'path';
|
|
10
|
-
|
|
11
|
-
class ConfigLoader {
|
|
12
|
-
constructor(defaults = {}) {
|
|
13
|
-
this.defaults = defaults;
|
|
14
|
-
this.config = { ...defaults };
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Load config from .env file (simple parser, no deps)
|
|
19
|
-
* @param {string} path - Path to .env file
|
|
20
|
-
*/
|
|
21
|
-
loadEnvFile(path = '.env') {
|
|
22
|
-
try {
|
|
23
|
-
const content = readFileSync(resolve(process.cwd(), path), 'utf8');
|
|
24
|
-
const lines = content.split('\n');
|
|
25
|
-
|
|
26
|
-
for (const line of lines) {
|
|
27
|
-
const trimmed = line.trim();
|
|
28
|
-
if (!trimmed || trimmed.startsWith('#')) continue;
|
|
29
|
-
|
|
30
|
-
const eqIndex = trimmed.indexOf('=');
|
|
31
|
-
if (eqIndex === -1) continue;
|
|
32
|
-
|
|
33
|
-
const key = trimmed.substring(0, eqIndex).trim();
|
|
34
|
-
let value = trimmed.substring(eqIndex + 1).trim();
|
|
35
|
-
|
|
36
|
-
// Remove quotes if present
|
|
37
|
-
if ((value.startsWith('"') && value.endsWith('"')) ||
|
|
38
|
-
(value.startsWith("'") && value.endsWith("'"))) {
|
|
39
|
-
value = value.substring(1, value.length - 1);
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
// Only set if not already set by process.env (priority: Env Var > .env > Default)
|
|
43
|
-
if (process.env[key] === undefined) {
|
|
44
|
-
process.env[key] = value;
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
} catch (e) {
|
|
48
|
-
// Ignore if file not found, rely on process.env or defaults
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* Get a configuration value
|
|
54
|
-
* @param {string} key - Dot notation key e.g., 'server.port'
|
|
55
|
-
* @param {any} defaultValue - Fallback value
|
|
56
|
-
* @returns {any}
|
|
57
|
-
*/
|
|
58
|
-
get(key, defaultValue) {
|
|
59
|
-
// Check process.env first (flattened keys usually UPPER_SNAKE_CASE)
|
|
60
|
-
const envKey = key.replace(/\./g, '_').toUpperCase();
|
|
61
|
-
|
|
62
|
-
if (process.env[envKey] !== undefined) {
|
|
63
|
-
return this._parseValue(process.env[envKey]);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
// Check nested defaults
|
|
67
|
-
const keys = key.split('.');
|
|
68
|
-
let val = this.defaults;
|
|
69
|
-
for (const k of keys) {
|
|
70
|
-
if (val === undefined || val === null) return defaultValue;
|
|
71
|
-
val = val[k];
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
return val !== undefined ? val : defaultValue;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* Get number config
|
|
79
|
-
*/
|
|
80
|
-
getNumber(key, defaultValue) {
|
|
81
|
-
const val = this.get(key, defaultValue);
|
|
82
|
-
const num = Number(val);
|
|
83
|
-
return isNaN(num) ? defaultValue : num;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
/**
|
|
87
|
-
* Get boolean config
|
|
88
|
-
*/
|
|
89
|
-
getBoolean(key, defaultValue) {
|
|
90
|
-
const val = this.get(key, defaultValue);
|
|
91
|
-
if (typeof val === 'boolean') return val;
|
|
92
|
-
if (typeof val === 'string') {
|
|
93
|
-
return ['true', '1', 'yes'].includes(val.toLowerCase());
|
|
94
|
-
}
|
|
95
|
-
return !!val;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
_parseValue(val) {
|
|
99
|
-
if (val === 'true') return true;
|
|
100
|
-
if (val === 'false') return false;
|
|
101
|
-
if (val === 'null') return null;
|
|
102
|
-
if (!isNaN(val) && val !== '') return Number(val);
|
|
103
|
-
return val;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
/**
|
|
107
|
-
* Static method to load configuration
|
|
108
|
-
* @param {Object} userConfig - User-provided configuration
|
|
109
|
-
* @returns {Object} Merged configuration
|
|
110
|
-
*/
|
|
111
|
-
static load(userConfig = {}) {
|
|
112
|
-
// Default configuration
|
|
113
|
-
const defaults = {
|
|
114
|
-
driver: process.env.WS_DRIVER || 'tcp',
|
|
115
|
-
port: parseInt(process.env.WS_PORT) || 8080,
|
|
116
|
-
host: process.env.WS_HOST || '0.0.0.0',
|
|
117
|
-
maxPayload: parseInt(process.env.WS_MAX_PAYLOAD) || 1024 * 1024,
|
|
118
|
-
pingInterval: parseInt(process.env.WS_PING_INTERVAL) || 30000,
|
|
119
|
-
pingTimeout: parseInt(process.env.WS_PING_TIMEOUT) || 10000,
|
|
120
|
-
compression: process.env.WS_COMPRESSION === 'true',
|
|
121
|
-
tls: process.env.WS_TLS === 'true',
|
|
122
|
-
tlsOptions: {
|
|
123
|
-
key: process.env.WS_TLS_KEY || null,
|
|
124
|
-
cert: process.env.WS_TLS_CERT || null,
|
|
125
|
-
ca: process.env.WS_TLS_CA || null
|
|
126
|
-
},
|
|
127
|
-
socketTimeout: parseInt(process.env.WS_SOCKET_TIMEOUT) || 30000,
|
|
128
|
-
maxConnections: parseInt(process.env.WS_MAX_CONNECTIONS) || 10000,
|
|
129
|
-
enableStats: process.env.WS_ENABLE_STATS !== 'false'
|
|
130
|
-
};
|
|
131
|
-
|
|
132
|
-
// Merge configurations (user config overrides env, env overrides defaults)
|
|
133
|
-
const config = { ...defaults, ...userConfig };
|
|
134
|
-
|
|
135
|
-
// Validate configuration
|
|
136
|
-
this._validateConfig(config);
|
|
137
|
-
|
|
138
|
-
return config;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
/**
|
|
142
|
-
* Validate configuration
|
|
143
|
-
* @param {Object} config - Configuration to validate
|
|
144
|
-
* @throws {Error} If configuration is invalid
|
|
145
|
-
* @private
|
|
146
|
-
*/
|
|
147
|
-
static _validateConfig(config) {
|
|
148
|
-
// Validate port
|
|
149
|
-
if (config.port < 1 || config.port > 65535) {
|
|
150
|
-
throw new Error(`Invalid port: ${config.port}. Must be between 1 and 65535`);
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
// Validate maxPayload
|
|
154
|
-
if (config.maxPayload < 1 || config.maxPayload > 100 * 1024 * 1024) {
|
|
155
|
-
throw new Error(`Invalid maxPayload: ${config.maxPayload}. Must be between 1 and 100MB`);
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
// Validate pingInterval
|
|
159
|
-
if (config.pingInterval < 1000) {
|
|
160
|
-
throw new Error(`Invalid pingInterval: ${config.pingInterval}. Must be at least 1000ms`);
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
// Validate pingTimeout
|
|
164
|
-
if (config.pingTimeout < 1000) {
|
|
165
|
-
throw new Error(`Invalid pingTimeout: ${config.pingTimeout}. Must be at least 1000ms`);
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
// Validate driver
|
|
169
|
-
const validDrivers = ['tcp', 'tls', 'http', 'memory'];
|
|
170
|
-
if (!validDrivers.includes(config.driver)) {
|
|
171
|
-
throw new Error(`Invalid driver: ${config.driver}. Must be one of: ${validDrivers.join(', ')}`);
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
// Validate TLS options if TLS is enabled
|
|
175
|
-
if (config.tls && config.driver === 'tls') {
|
|
176
|
-
if (!config.tlsOptions.key || !config.tlsOptions.cert) {
|
|
177
|
-
throw new Error('TLS requires both key and certificate');
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
export default ConfigLoader;
|
|
@@ -1,110 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @license MIT
|
|
3
|
-
* Copyright (c) 2026-present AetherFramework Contributors.
|
|
4
|
-
* SPDX-License-Identifier: MIT
|
|
5
|
-
* @module @aetherframework/src/utils/connection-pool
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
class ConnectionPool {
|
|
10
|
-
constructor(factory, options = {}) {
|
|
11
|
-
this.factory = factory; // Function to create new connections
|
|
12
|
-
this.maxSize = options.maxSize || 10;
|
|
13
|
-
this.acquireTimeout = options.acquireTimeout || 5000;
|
|
14
|
-
|
|
15
|
-
this.available = [];
|
|
16
|
-
this.inUse = new Set();
|
|
17
|
-
this.waitingQueue = [];
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Acquire a connection from the pool
|
|
22
|
-
* @returns {Promise<Object>}
|
|
23
|
-
*/
|
|
24
|
-
async acquire() {
|
|
25
|
-
// Try to get an available healthy connection
|
|
26
|
-
while (this.available.length > 0) {
|
|
27
|
-
const conn = this.available.pop();
|
|
28
|
-
if (conn.readyState === 1) {
|
|
29
|
-
this.inUse.add(conn);
|
|
30
|
-
return conn;
|
|
31
|
-
}
|
|
32
|
-
// If dead, just discard and try next
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
// If pool not full, create new
|
|
36
|
-
if (this.inUse.size + this.available.length < this.maxSize) {
|
|
37
|
-
const conn = await this.factory();
|
|
38
|
-
this.inUse.add(conn);
|
|
39
|
-
return conn;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
// Wait for a connection to be released
|
|
43
|
-
return new Promise((resolve, reject) => {
|
|
44
|
-
const timeout = setTimeout(() => {
|
|
45
|
-
reject(new Error('Connection pool acquisition timeout'));
|
|
46
|
-
}, this.acquireTimeout);
|
|
47
|
-
|
|
48
|
-
this.waitingQueue.push({ resolve, reject, timeout });
|
|
49
|
-
});
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* Release a connection back to the pool
|
|
54
|
-
* @param {Object} conn
|
|
55
|
-
*/
|
|
56
|
-
release(conn) {
|
|
57
|
-
this.inUse.delete(conn);
|
|
58
|
-
|
|
59
|
-
// If there are waiters, give it to them
|
|
60
|
-
if (this.waitingQueue.length > 0) {
|
|
61
|
-
const { resolve, timeout } = this.waitingQueue.shift();
|
|
62
|
-
clearTimeout(timeout);
|
|
63
|
-
this.inUse.add(conn);
|
|
64
|
-
resolve(conn);
|
|
65
|
-
return;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
// Otherwise put back in available if healthy
|
|
69
|
-
if (conn.readyState === 1) {
|
|
70
|
-
this.available.push(conn);
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
* Close all connections
|
|
76
|
-
*/
|
|
77
|
-
async drain() {
|
|
78
|
-
const closePromises = [];
|
|
79
|
-
|
|
80
|
-
for (const conn of this.available) {
|
|
81
|
-
closePromises.push(conn.close());
|
|
82
|
-
}
|
|
83
|
-
for (const conn of this.inUse) {
|
|
84
|
-
closePromises.push(conn.close());
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
this.available = [];
|
|
88
|
-
this.inUse.clear();
|
|
89
|
-
|
|
90
|
-
// Reject all waiters
|
|
91
|
-
for (const { reject, timeout } of this.waitingQueue) {
|
|
92
|
-
clearTimeout(timeout);
|
|
93
|
-
reject(new Error('Pool drained'));
|
|
94
|
-
}
|
|
95
|
-
this.waitingQueue = [];
|
|
96
|
-
|
|
97
|
-
await Promise.all(closePromises);
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
getStats() {
|
|
101
|
-
return {
|
|
102
|
-
available: this.available.length,
|
|
103
|
-
inUse: this.inUse.size,
|
|
104
|
-
waiting: this.waitingQueue.length,
|
|
105
|
-
maxSize: this.maxSize
|
|
106
|
-
};
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
export default ConnectionPool;
|
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @license MIT
|
|
3
|
-
* Copyright (c) 2026-present AetherFramework Contributors.
|
|
4
|
-
* SPDX-License-Identifier: MIT
|
|
5
|
-
* @module @aetherframework/src/utils/error-handler
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
class WebSocketError extends Error {
|
|
10
|
-
constructor(message, code = 1011, isOperational = true) {
|
|
11
|
-
super(message);
|
|
12
|
-
this.name = 'WebSocketError';
|
|
13
|
-
this.code = code;
|
|
14
|
-
this.isOperational = isOperational; // True if expected (e.g., validation fail), False if bug
|
|
15
|
-
Error.captureStackTrace(this, this.constructor);
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
class ErrorHandler {
|
|
20
|
-
constructor(logger = console) {
|
|
21
|
-
this.logger = logger;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Handle error in async context
|
|
26
|
-
* @param {Error} err
|
|
27
|
-
* @param {Object} connection - Optional connection to close
|
|
28
|
-
*/
|
|
29
|
-
handle(err, connection = null) {
|
|
30
|
-
if (err instanceof WebSocketError) {
|
|
31
|
-
this.logger.warn(`Operational Error [${err.code}]: ${err.message}`);
|
|
32
|
-
if (connection && connection.readyState === 1) {
|
|
33
|
-
connection.close(err.code, err.message);
|
|
34
|
-
}
|
|
35
|
-
} else {
|
|
36
|
-
this.logger.error(`Unexpected Error: ${err.message}\n${err.stack}`);
|
|
37
|
-
if (connection && connection.readyState === 1) {
|
|
38
|
-
// 1011 Internal Error
|
|
39
|
-
connection.close(1011, 'Internal Server Error');
|
|
40
|
-
}
|
|
41
|
-
// In a real app, you might want to restart the process here for non-operational errors
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* Create a standardized error response object
|
|
47
|
-
*/
|
|
48
|
-
createErrorResponse(message, code = 400) {
|
|
49
|
-
return {
|
|
50
|
-
type: 'error',
|
|
51
|
-
code,
|
|
52
|
-
message,
|
|
53
|
-
timestamp: Date.now()
|
|
54
|
-
};
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
export { WebSocketError, ErrorHandler };
|
|
59
|
-
export default ErrorHandler;
|
|
@@ -1,211 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @license MIT
|
|
3
|
-
* Copyright (c) 2026-present AetherFramework Contributors.
|
|
4
|
-
* SPDX-License-Identifier: MIT
|
|
5
|
-
* @module @aetherframework/src/utils/frame-encoder
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
class FrameEncoder {
|
|
10
|
-
/**
|
|
11
|
-
* Create a WebSocket frame
|
|
12
|
-
* @param {number} opcode - Operation code (1=text, 2=binary, 8=close, 9=ping, 10=pong)
|
|
13
|
-
* @param {Buffer|string} data - Payload data
|
|
14
|
-
* @param {boolean} fin - FIN bit (default true)
|
|
15
|
-
* @param {boolean} mask - Mask bit (default false for server->client)
|
|
16
|
-
* @returns {Buffer} Encoded frame
|
|
17
|
-
*/
|
|
18
|
-
/**
|
|
19
|
-
* Parse a WebSocket frame from buffer
|
|
20
|
-
* @param {Buffer} buffer - Raw WebSocket frame buffer
|
|
21
|
-
* @param {number} maxPayload - Maximum allowed payload size
|
|
22
|
-
* @returns {Object} Parsed frame object
|
|
23
|
-
*/
|
|
24
|
-
static parse(buffer, maxPayload = 1024 * 1024) {
|
|
25
|
-
if (!Buffer.isBuffer(buffer) || buffer.length < 2) {
|
|
26
|
-
throw new Error('Invalid WebSocket frame: buffer too short or not a buffer');
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
let offset = 0;
|
|
30
|
-
|
|
31
|
-
// Byte 1: FIN + RSV1-3 + Opcode
|
|
32
|
-
const firstByte = buffer[offset++];
|
|
33
|
-
const fin = (firstByte & 0x80) !== 0;
|
|
34
|
-
const rsv1 = (firstByte & 0x40) !== 0;
|
|
35
|
-
const rsv2 = (firstByte & 0x20) !== 0;
|
|
36
|
-
const rsv3 = (firstByte & 0x10) !== 0;
|
|
37
|
-
const opcode = firstByte & 0x0F;
|
|
38
|
-
|
|
39
|
-
// Byte 2: Mask + Payload Length
|
|
40
|
-
const secondByte = buffer[offset++];
|
|
41
|
-
const masked = (secondByte & 0x80) !== 0;
|
|
42
|
-
let payloadLength = secondByte & 0x7F;
|
|
43
|
-
|
|
44
|
-
// Extended payload length
|
|
45
|
-
if (payloadLength === 126) {
|
|
46
|
-
if (buffer.length < offset + 2) {
|
|
47
|
-
throw new Error('Invalid WebSocket frame: insufficient data for extended length');
|
|
48
|
-
}
|
|
49
|
-
payloadLength = buffer.readUInt16BE(offset);
|
|
50
|
-
offset += 2;
|
|
51
|
-
} else if (payloadLength === 127) {
|
|
52
|
-
if (buffer.length < offset + 8) {
|
|
53
|
-
throw new Error('Invalid WebSocket frame: insufficient data for 64-bit length');
|
|
54
|
-
}
|
|
55
|
-
const bigLength = buffer.readBigUInt64BE(offset);
|
|
56
|
-
if (bigLength > Number.MAX_SAFE_INTEGER) {
|
|
57
|
-
throw new Error('WebSocket frame too large for JavaScript safe integer');
|
|
58
|
-
}
|
|
59
|
-
payloadLength = Number(bigLength);
|
|
60
|
-
offset += 8;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
// Check payload size limit
|
|
64
|
-
if (payloadLength > maxPayload) {
|
|
65
|
-
throw new Error(`Payload exceeds maximum size: ${payloadLength} > ${maxPayload}`);
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
// Masking Key
|
|
69
|
-
let maskingKey = null;
|
|
70
|
-
if (masked) {
|
|
71
|
-
if (buffer.length < offset + 4) {
|
|
72
|
-
throw new Error('Invalid WebSocket frame: insufficient data for masking key');
|
|
73
|
-
}
|
|
74
|
-
maskingKey = buffer.slice(offset, offset + 4);
|
|
75
|
-
offset += 4;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
// Payload
|
|
79
|
-
if (buffer.length < offset + payloadLength) {
|
|
80
|
-
throw new Error('Invalid WebSocket frame: insufficient data for payload');
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
let payload = buffer.slice(offset, offset + payloadLength);
|
|
84
|
-
|
|
85
|
-
// Unmask payload if masked
|
|
86
|
-
if (masked && maskingKey) {
|
|
87
|
-
for (let i = 0; i < payload.length; i++) {
|
|
88
|
-
payload[i] = payload[i]^ maskingKey[i % 4];
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
return {
|
|
93
|
-
fin,
|
|
94
|
-
rsv1,
|
|
95
|
-
rsv2,
|
|
96
|
-
rsv3,
|
|
97
|
-
opcode,
|
|
98
|
-
masked,
|
|
99
|
-
payloadLength,
|
|
100
|
-
maskingKey,
|
|
101
|
-
payload,
|
|
102
|
-
totalLength: offset + payloadLength
|
|
103
|
-
};
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
static encode(opcode, data, fin = true, mask = false) {
|
|
107
|
-
let payload;
|
|
108
|
-
if (typeof data === 'string') {
|
|
109
|
-
payload = Buffer.from(data, 'utf8');
|
|
110
|
-
} else if (Buffer.isBuffer(data)) {
|
|
111
|
-
payload = data;
|
|
112
|
-
} else {
|
|
113
|
-
payload = Buffer.from(String(data));
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
const payloadLength = payload.length;
|
|
117
|
-
let headerLength = 2;
|
|
118
|
-
|
|
119
|
-
if (payloadLength <= 125) {
|
|
120
|
-
// No extra length bytes
|
|
121
|
-
} else if (payloadLength <= 65535) {
|
|
122
|
-
headerLength += 2;
|
|
123
|
-
} else {
|
|
124
|
-
headerLength += 8;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
if (mask) {
|
|
128
|
-
headerLength += 4;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
const frame = Buffer.allocUnsafe(headerLength + payloadLength);
|
|
132
|
-
let offset = 0;
|
|
133
|
-
|
|
134
|
-
// Byte 1: FIN + Opcode
|
|
135
|
-
frame[offset++] = (fin ? 0x80 : 0) | (opcode & 0x0F);
|
|
136
|
-
|
|
137
|
-
// Byte 2: Mask + Payload Length
|
|
138
|
-
let lengthByte = mask ? 0x80 : 0;
|
|
139
|
-
if (payloadLength <= 125) {
|
|
140
|
-
lengthByte |= payloadLength;
|
|
141
|
-
frame[offset++] = lengthByte;
|
|
142
|
-
} else if (payloadLength <= 65535) {
|
|
143
|
-
lengthByte |= 126;
|
|
144
|
-
frame[offset++] = lengthByte;
|
|
145
|
-
frame.writeUInt16BE(payloadLength, offset);
|
|
146
|
-
offset += 2;
|
|
147
|
-
} else {
|
|
148
|
-
lengthByte |= 127;
|
|
149
|
-
frame[offset++] = lengthByte;
|
|
150
|
-
// Write 64-bit length (only lower 53 bits safely used in JS usually, but spec allows 64)
|
|
151
|
-
frame.writeBigUInt64BE(BigInt(payloadLength), offset);
|
|
152
|
-
offset += 8;
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
// Masking Key (if masked)
|
|
156
|
-
if (mask) {
|
|
157
|
-
const maskingKey = crypto.randomBytes(4);
|
|
158
|
-
frame.set(maskingKey, offset);
|
|
159
|
-
offset += 4;
|
|
160
|
-
|
|
161
|
-
// Mask payload
|
|
162
|
-
for (let i = 0; i < payloadLength; i++) {
|
|
163
|
-
frame[offset + i] = payload[i]^ maskingKey[i % 4];
|
|
164
|
-
}
|
|
165
|
-
} else {
|
|
166
|
-
// Copy payload directly
|
|
167
|
-
payload.copy(frame, offset);
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
return frame;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
/**
|
|
174
|
-
* Create a Close frame
|
|
175
|
-
* @param {number} code - Status code
|
|
176
|
-
* @param {string} reason - Reason string
|
|
177
|
-
* @returns {Buffer}
|
|
178
|
-
*/
|
|
179
|
-
static createCloseFrame(code = 1000, reason = '') {
|
|
180
|
-
const reasonBuf = Buffer.from(reason, 'utf8');
|
|
181
|
-
const payload = Buffer.allocUnsafe(2 + reasonBuf.length);
|
|
182
|
-
payload.writeUInt16BE(code, 0);
|
|
183
|
-
if (reasonBuf.length > 0) {
|
|
184
|
-
reasonBuf.copy(payload, 2);
|
|
185
|
-
}
|
|
186
|
-
return this.encode(0x8, payload);
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
/**
|
|
190
|
-
* Create a Ping frame
|
|
191
|
-
* @param {Buffer} data - Optional ping data
|
|
192
|
-
* @returns {Buffer}
|
|
193
|
-
*/
|
|
194
|
-
static createPingFrame(data = Buffer.alloc(0)) {
|
|
195
|
-
return this.encode(0x9, data);
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
/**
|
|
199
|
-
* Create a Pong frame
|
|
200
|
-
* @param {Buffer} data - Pong data (usually echo of ping)
|
|
201
|
-
* @returns {Buffer}
|
|
202
|
-
*/
|
|
203
|
-
static createPongFrame(data = Buffer.alloc(0)) {
|
|
204
|
-
return this.encode(0xA, data);
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
// Import crypto dynamically to avoid circular deps if needed, but top-level is fine here
|
|
209
|
-
import crypto from 'crypto';
|
|
210
|
-
|
|
211
|
-
export default FrameEncoder;
|
|
@@ -1,91 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @license MIT
|
|
3
|
-
* Copyright (c) 2026-present AetherFramework Contributors.
|
|
4
|
-
* SPDX-License-Identifier: MIT
|
|
5
|
-
* @module @aetherframework/src/utils/heartbeat-manager
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
class HeartbeatManager {
|
|
9
|
-
constructor(options = {}) {
|
|
10
|
-
this.interval = options.interval || 30000; // ms
|
|
11
|
-
this.timeout = options.timeout || 5000; // ms to wait for pong
|
|
12
|
-
this.timers = new Map(); // connId -> timerId
|
|
13
|
-
this.isRunning = false;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Start monitoring a connection
|
|
18
|
-
* @param {Object} connection
|
|
19
|
-
*/
|
|
20
|
-
monitor(connection) {
|
|
21
|
-
if (this.timers.has(connection.id)) return;
|
|
22
|
-
|
|
23
|
-
const timer = setInterval(() => {
|
|
24
|
-
if (connection.readyState !== 1) {
|
|
25
|
-
this.stop(connection.id);
|
|
26
|
-
return;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
// If we are waiting for a pong and didn't get it
|
|
30
|
-
if (connection.isWaitingForPong) {
|
|
31
|
-
connection.terminate();
|
|
32
|
-
this.stop(connection.id);
|
|
33
|
-
return;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
// Send Ping
|
|
37
|
-
connection.isWaitingForPong = true;
|
|
38
|
-
connection.ping();
|
|
39
|
-
|
|
40
|
-
// Set timeout for Pong
|
|
41
|
-
const timeoutTimer = setTimeout(() => {
|
|
42
|
-
if (connection.isWaitingForPong) {
|
|
43
|
-
connection.terminate();
|
|
44
|
-
this.stop(connection.id);
|
|
45
|
-
}
|
|
46
|
-
}, this.timeout);
|
|
47
|
-
|
|
48
|
-
// Store timeout ID to clear it when pong arrives
|
|
49
|
-
connection.pongTimeout = timeoutTimer;
|
|
50
|
-
|
|
51
|
-
}, this.interval);
|
|
52
|
-
|
|
53
|
-
this.timers.set(connection.id, timer);
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* Handle Pong received
|
|
58
|
-
* @param {Object} connection
|
|
59
|
-
*/
|
|
60
|
-
acknowledge(connection) {
|
|
61
|
-
connection.isWaitingForPong = false;
|
|
62
|
-
if (connection.pongTimeout) {
|
|
63
|
-
clearTimeout(connection.pongTimeout);
|
|
64
|
-
connection.pongTimeout = null;
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* Stop monitoring
|
|
70
|
-
* @param {string} connId
|
|
71
|
-
*/
|
|
72
|
-
stop(connId) {
|
|
73
|
-
const timer = this.timers.get(connId);
|
|
74
|
-
if (timer) {
|
|
75
|
-
clearInterval(timer);
|
|
76
|
-
this.timers.delete(connId);
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
/**
|
|
81
|
-
* Cleanup all
|
|
82
|
-
*/
|
|
83
|
-
shutdown() {
|
|
84
|
-
for (const timer of this.timers.values()) {
|
|
85
|
-
clearInterval(timer);
|
|
86
|
-
}
|
|
87
|
-
this.timers.clear();
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
export default HeartbeatManager;
|