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