@aetherframework/websocket 1.0.1 → 1.0.3

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