@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.
@@ -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;