@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
package/README.md
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
Aether WebSocket API ð
|
|
2
|
+
|
|
3
|
+
High-Performance, Zero-Dependency WebSocket Server & Client for Node.js
|
|
4
|
+
|
|
5
|
+
Aether is a lightweight, robust, and highly efficient WebSocket implementation built from the ground up for Node.js. Unlike heavy frameworks that bundle unnecessary features, Aether focuses on raw speed, memory efficiency, and strict RFC 6455 compliance.
|
|
6
|
+
|
|
7
|
+
âĻ Why Choose Aether?
|
|
8
|
+
|
|
9
|
+
1. ⥠Blazing Fast Performance
|
|
10
|
+
Built with a custom binary frame parser (`FrameParser`) and encoder (`FrameEncoder`), Aether minimizes garbage collection pressure and maximizes throughput. It handles thousands of concurrent connections with minimal CPU overhead.
|
|
11
|
+
|
|
12
|
+
2. ðŠķ Zero Dependencies
|
|
13
|
+
Aether relies only on Node.js built-in modules (`net`, `http`, `crypto`, `events`). No `npm install` bloat, no security vulnerabilities from third-party packages, and a tiny footprint ideal for microservices and serverless environments.
|
|
14
|
+
|
|
15
|
+
3. ðĄïļ Robust & Secure
|
|
16
|
+
- Strict Protocol Compliance: Fully adheres to RFC 6455.
|
|
17
|
+
- Memory Safety: Built-in protection against memory leaks via efficient buffer management.
|
|
18
|
+
- Error Resilience: Graceful handling of malformed frames, unexpected disconnections, and protocol errors.
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
> Best For: Developers who need a reliable, lightweight, and transparent WebSocket solution without the baggage of large frameworks.
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
ðĶ Installation
|
|
26
|
+
|
|
27
|
+
Install Aether WebSocket via npm:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
npm install aetherframework-websocket
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
ð Quick Start
|
|
36
|
+
|
|
37
|
+
1. Create a WebSocket Server
|
|
38
|
+
|
|
39
|
+
```javascript
|
|
40
|
+
import { createWebSocketFactory } from 'aetherframework-websocket';
|
|
41
|
+
|
|
42
|
+
// Create a WebSocket factory instance
|
|
43
|
+
const factory = createWebSocketFactory({
|
|
44
|
+
port: 8080,
|
|
45
|
+
host: '0.0.0.0',
|
|
46
|
+
maxPayload: 1024 * 1024, // 1MB max message size
|
|
47
|
+
pingInterval: 30000, // Send ping every 30s
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
// Handle new connections
|
|
51
|
+
factory.on('connection', (connection) => {
|
|
52
|
+
console.log(`â
New connection: ${connection.id}`);
|
|
53
|
+
|
|
54
|
+
// Send a welcome message
|
|
55
|
+
connection.socket.write(factory._encodeFrame(0x1, Buffer.from('Welcome to Aether!')));
|
|
56
|
+
|
|
57
|
+
// Handle incoming messages
|
|
58
|
+
factory.on('message', (conn, data, isBinary) => {
|
|
59
|
+
console.log(`ðĻ Received from ${conn.id}:`, data.toString());
|
|
60
|
+
|
|
61
|
+
// Echo back
|
|
62
|
+
conn.socket.write(factory._encodeFrame(0x1, Buffer.from('Echo: ' + data)));
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
// Handle disconnection
|
|
66
|
+
factory.on('close', (conn, code, reason) => {
|
|
67
|
+
console.log(`â Disconnected: ${conn.id} (Code: ${code})`);
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
// Start the server
|
|
72
|
+
async function start() {
|
|
73
|
+
try {
|
|
74
|
+
const server = await factory.createServer();
|
|
75
|
+
console.log(`ð Aether Server running on ws://${server.address.address}:${server.address.port}`);
|
|
76
|
+
} catch (error) {
|
|
77
|
+
console.error('Failed to start server:', error);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
start();
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
2. Create a WebSocket Client
|
|
85
|
+
|
|
86
|
+
```javascript
|
|
87
|
+
import { createWebSocketFactory } from 'aetherframework-websocket';
|
|
88
|
+
|
|
89
|
+
const factory = createWebSocketFactory();
|
|
90
|
+
|
|
91
|
+
async function connect() {
|
|
92
|
+
try {
|
|
93
|
+
const client = await factory.createClient('ws://localhost:8080');
|
|
94
|
+
|
|
95
|
+
console.log('â
Connected to server');
|
|
96
|
+
|
|
97
|
+
// Listen for messages
|
|
98
|
+
factory.on('message', (conn, data, isBinary) => {
|
|
99
|
+
console.log('ðĻ Server says:', data.toString());
|
|
100
|
+
|
|
101
|
+
// Close connection after receiving
|
|
102
|
+
client.close(1000, 'Goodbye');
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
// Send a message
|
|
106
|
+
client.send('Hello Aether!');
|
|
107
|
+
|
|
108
|
+
} catch (error) {
|
|
109
|
+
console.error('Connection failed:', error);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
connect();
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
---
|
|
117
|
+
|
|
118
|
+
ð API Reference
|
|
119
|
+
|
|
120
|
+
`createWebSocketFactory` Function
|
|
121
|
+
|
|
122
|
+
```javascript
|
|
123
|
+
createWebSocketFactory(config)
|
|
124
|
+
```
|
|
125
|
+
- `config.port` (number): Server port (default: `80`).
|
|
126
|
+
- `config.host` (string): Server host (default: `'0.0.0.0'`).
|
|
127
|
+
- `config.maxPayload` (number): Max message size in bytes (default: `1048576`).
|
|
128
|
+
- `config.pingInterval` (number): Milliseconds between pings (default: `null`).
|
|
129
|
+
|
|
130
|
+
Available Methods
|
|
131
|
+
|
|
132
|
+
| Method | Description | Returns |
|
|
133
|
+
| :--- | :--- | :--- |
|
|
134
|
+
| `createServer(options)` | Starts the HTTP/WebSocket server. | `Promise<{ type, address, close }>` |
|
|
135
|
+
| `createClient(url, options)` | Creates a WebSocket client connection. | `Promise<{ id, socket, send, close }>` |
|
|
136
|
+
| `closeServer()` | Gracefully shuts down the server. | `Promise<void>` |
|
|
137
|
+
| `getStats()` | Returns current server statistics. | `Object` |
|
|
138
|
+
|
|
139
|
+
Events
|
|
140
|
+
|
|
141
|
+
| Event | Callback Arguments | Description |
|
|
142
|
+
| :--- | :--- | :--- |
|
|
143
|
+
| `connection` | `(connection)` | Fired when a new client connects. |
|
|
144
|
+
| `message` | `(connection, data, isBinary)` | Fired when a message is received. |
|
|
145
|
+
| `close` | `(connection, code, reason)` | Fired when a connection closes. |
|
|
146
|
+
| `error` | `(errorObj)` | Fired on parsing or socket errors. |
|
|
147
|
+
| `ping` | `(connection, payload)` | Fired when a ping is received. |
|
|
148
|
+
| `pong` | `(connection, payload)` | Fired when a pong is received. |
|
|
149
|
+
|
|
150
|
+
---
|
|
151
|
+
|
|
152
|
+
ð ïļ Advanced Configuration
|
|
153
|
+
|
|
154
|
+
Handling Binary Data
|
|
155
|
+
Aether seamlessly handles both text and binary frames.
|
|
156
|
+
|
|
157
|
+
```javascript
|
|
158
|
+
factory.on('message', (conn, data, isBinary) => {
|
|
159
|
+
if (isBinary) {
|
|
160
|
+
console.log('Received binary data:', data.length, 'bytes');
|
|
161
|
+
// Process Buffer directly
|
|
162
|
+
} else {
|
|
163
|
+
console.log('Received text:', data.toString());
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
Custom Heartbeat Logic
|
|
169
|
+
If you disable `pingInterval` in config, you can implement custom health checks:
|
|
170
|
+
|
|
171
|
+
```javascript
|
|
172
|
+
factory.on('connection', (conn) => {
|
|
173
|
+
conn.isAlive = true;
|
|
174
|
+
|
|
175
|
+
const interval = setInterval(() => {
|
|
176
|
+
if (!conn.isAlive) {
|
|
177
|
+
console.log('Terminating inactive connection');
|
|
178
|
+
conn.socket.destroy();
|
|
179
|
+
return clearInterval(interval);
|
|
180
|
+
}
|
|
181
|
+
conn.isAlive = false;
|
|
182
|
+
factory._sendPing(conn.socket);
|
|
183
|
+
}, 30000);
|
|
184
|
+
|
|
185
|
+
factory.on('pong', () => {
|
|
186
|
+
conn.isAlive = true;
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
---
|
|
192
|
+
|
|
193
|
+
ð Statistics & Monitoring
|
|
194
|
+
|
|
195
|
+
Use `getStats()` to monitor server health in real-time:
|
|
196
|
+
|
|
197
|
+
```javascript
|
|
198
|
+
setInterval(() => {
|
|
199
|
+
const stats = factory.getStats();
|
|
200
|
+
console.log(`Active Connections: ${stats.connections}`);
|
|
201
|
+
console.log(`Uptime: ${stats.uptime}ms`);
|
|
202
|
+
console.log(`Memory RSS: ${stats.memory.rss} bytes`);
|
|
203
|
+
}, 10000);
|
|
204
|
+
```
|
|
205
|
+
---
|
|
206
|
+
|
|
207
|
+
ð License
|
|
208
|
+
|
|
209
|
+
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
|
210
|
+
|
|
211
|
+
---
|
|
212
|
+
|
|
213
|
+
Made with âĪïļ by the Aether Team
|
package/index.js
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license MIT
|
|
3
|
+
* Copyright (c) 2026-present AetherFramework Contributors.
|
|
4
|
+
* SPDX-License-Identifier: MIT
|
|
5
|
+
* @module @aetherframework/index
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
import WebSocketFactory from './src/core/WebSocketFactory.js';
|
|
10
|
+
import ConfigLoader from './src/utils/config-loader.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Create a WebSocket factory instance
|
|
14
|
+
* @param {Object} options - Configuration options
|
|
15
|
+
* @returns {WebSocketFactory} Factory instance
|
|
16
|
+
*/
|
|
17
|
+
export function createWebSocketFactory(options = {}) {
|
|
18
|
+
// Load environment configuration
|
|
19
|
+
const config = ConfigLoader.load(options);
|
|
20
|
+
return new WebSocketFactory(config);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Create a WebSocket server
|
|
25
|
+
* @param {Object} options - Server options
|
|
26
|
+
* @returns {Promise<Object>} WebSocket server instance
|
|
27
|
+
*/
|
|
28
|
+
export async function createServer(options = {}) {
|
|
29
|
+
const factory = createWebSocketFactory(options);
|
|
30
|
+
return await factory.createServer();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Create a WebSocket client
|
|
35
|
+
* @param {string} url - WebSocket URL
|
|
36
|
+
* @param {Object} options - Client options
|
|
37
|
+
* @returns {Promise<Object>} WebSocket client instance
|
|
38
|
+
*/
|
|
39
|
+
export async function createClient(url, options = {}) {
|
|
40
|
+
const factory = createWebSocketFactory(options);
|
|
41
|
+
return await factory.createClient(url);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Export core components
|
|
45
|
+
export { default as WebSocketFactory } from './src/core/WebSocketFactory.js';
|
|
46
|
+
export { default as ConnectionManager } from './src/core/ConnectionManager.js';
|
|
47
|
+
export { default as FrameParser } from './src/core/FrameParser.js';
|
|
48
|
+
export { default as ProtocolHandler } from './src/core/ProtocolHandler.js';
|
|
49
|
+
|
|
50
|
+
// Export middleware
|
|
51
|
+
export { default as AuthMiddleware } from './src/middleware/auth-middleware.js';
|
|
52
|
+
export { default as RateLimiter } from './src/middleware/rate-limiter.js';
|
|
53
|
+
export { default as Compression } from './src/middleware/compression.js';
|
|
54
|
+
|
|
55
|
+
// Export utilities
|
|
56
|
+
export { default as ConfigLoader } from './src/utils/config-loader.js';
|
|
57
|
+
export { default as HeartbeatManager } from './src/utils/heartbeat-manager.js';
|
|
58
|
+
|
|
59
|
+
export default createWebSocketFactory;
|
package/package.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@aetherframework/websocket",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Zero-dependency WebSocket server implementation for AetherJS",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"types": "index.d.ts",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"scripts": {
|
|
9
|
+
"start:basic": "node examples/basic-server.js",
|
|
10
|
+
"start:chat": "node examples/chat-server.js",
|
|
11
|
+
"start:api": "node examples/realtime-api.js",
|
|
12
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
13
|
+
},
|
|
14
|
+
"keywords": [
|
|
15
|
+
"websocket",
|
|
16
|
+
"factory-pattern",
|
|
17
|
+
"zero-dependency",
|
|
18
|
+
"high-performance",
|
|
19
|
+
"esm"
|
|
20
|
+
],
|
|
21
|
+
"devDependencies": {
|
|
22
|
+
"@types/node": "^20.0.0",
|
|
23
|
+
"dotenv": "^16.6.1",
|
|
24
|
+
"eslint": "^8.0.0",
|
|
25
|
+
"jest": "^29.0.0",
|
|
26
|
+
"jsdoc": "^4.0.0",
|
|
27
|
+
"prettier": "^3.0.0"
|
|
28
|
+
},
|
|
29
|
+
"engines": {
|
|
30
|
+
"node": ">=18.0.0"
|
|
31
|
+
},
|
|
32
|
+
"author": "Aether Framework Team",
|
|
33
|
+
"license": "MIT",
|
|
34
|
+
"repository": {
|
|
35
|
+
"type": "git",
|
|
36
|
+
"url": "https://github.com/aetherjs/aetherframework-websocket.git"
|
|
37
|
+
},
|
|
38
|
+
"bugs": {
|
|
39
|
+
"url": "https://github.com/aetherjs/aetherframework-websocket/issues",
|
|
40
|
+
"email": "support@aetherjs.org"
|
|
41
|
+
},
|
|
42
|
+
"homepage": "https://www.aetherjs.org",
|
|
43
|
+
"files": [
|
|
44
|
+
"index.js",
|
|
45
|
+
"server.js",
|
|
46
|
+
"src/",
|
|
47
|
+
"connection.js",
|
|
48
|
+
"README.md",
|
|
49
|
+
"LICENSE"
|
|
50
|
+
]
|
|
51
|
+
}
|
|
@@ -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;
|