redweb 0.7.4 → 0.7.5
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/index.d.ts +23 -16
- package/package.json +1 -1
- package/src/http/BaseHttpServer.js +4 -4
- package/src/ws/BaseHandler.js +32 -14
- package/src/ws/SocketRoute.js +95 -58
package/index.d.ts
CHANGED
|
@@ -2,7 +2,8 @@ declare module 'redweb' {
|
|
|
2
2
|
import { Application } from 'express';
|
|
3
3
|
import { CorsOptions } from 'cors';
|
|
4
4
|
import { Server as HttpServer } from 'http';
|
|
5
|
-
import { WebSocket } from 'ws';
|
|
5
|
+
import { WebSocket, ServerOptions } from 'ws';
|
|
6
|
+
import { Buffer } from 'buffer';
|
|
6
7
|
|
|
7
8
|
/** ─────────────────── HTTP / CORE ─────────────────── */
|
|
8
9
|
|
|
@@ -34,9 +35,10 @@ declare module 'redweb' {
|
|
|
34
35
|
|
|
35
36
|
export interface SocketRouteConfig {
|
|
36
37
|
path: string;
|
|
37
|
-
handlers: Array<new () => BaseHandler>;
|
|
38
|
-
services?: Array<new () => SocketService>;
|
|
39
|
-
allowDuplicateConnections?: boolean;
|
|
38
|
+
handlers: Array<new () => BaseHandler>;
|
|
39
|
+
services?: Array<new () => SocketService>;
|
|
40
|
+
allowDuplicateConnections?: boolean;
|
|
41
|
+
websocketOptions?: ServerOptions;
|
|
40
42
|
}
|
|
41
43
|
|
|
42
44
|
/** Socket‑side autonomous service (game loops, timers, etc.) */
|
|
@@ -70,21 +72,26 @@ declare module 'redweb' {
|
|
|
70
72
|
message: any
|
|
71
73
|
): void;
|
|
72
74
|
|
|
73
|
-
onMessage(socket: WebSocket, message: any): void;
|
|
74
|
-
|
|
75
|
-
|
|
75
|
+
onMessage(socket: WebSocket, message: any): void;
|
|
76
|
+
acceptsBinary?(socket: WebSocket, buffer: Buffer): boolean;
|
|
77
|
+
handleBinaryMessage(socket: WebSocket, buffer: Buffer): void;
|
|
78
|
+
onBinaryMessage(socket: WebSocket, buffer: Buffer): void;
|
|
79
|
+
onInitialContact(socket: WebSocket): void;
|
|
80
|
+
}
|
|
76
81
|
|
|
77
82
|
export class SocketRoute {
|
|
78
83
|
path: string;
|
|
79
|
-
handlers: BaseHandler[];
|
|
80
|
-
clients: Map<string, WebSocket>;
|
|
81
|
-
allowDuplicateConnections?: boolean;
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
84
|
+
handlers: BaseHandler[];
|
|
85
|
+
clients: Map<string, WebSocket>;
|
|
86
|
+
allowDuplicateConnections?: boolean;
|
|
87
|
+
websocketOptions?: ServerOptions;
|
|
88
|
+
|
|
89
|
+
constructor(config: SocketRouteConfig);
|
|
90
|
+
|
|
91
|
+
addHandler(handler: new () => BaseHandler): void;
|
|
92
|
+
handleMessage(sock: WebSocket, data: any): void;
|
|
93
|
+
handleBinaryMessage(socket: WebSocket, buffer: Buffer): void;
|
|
94
|
+
}
|
|
88
95
|
|
|
89
96
|
/** ─────────────────── SERVER BASE ─────────────────── */
|
|
90
97
|
|
package/package.json
CHANGED
|
@@ -45,10 +45,10 @@ const HTTP_OPTIONS = {
|
|
|
45
45
|
* @param {RedWebOptions} options - Configuration options for RedWeb.
|
|
46
46
|
* @return {Object} Express application instance.
|
|
47
47
|
*/
|
|
48
|
-
function BaseHttpServer(options = {}) {
|
|
49
|
-
this.options = { ...HTTP_OPTIONS, ...options };
|
|
50
|
-
this.app =
|
|
51
|
-
Object.assign(this, this.options);
|
|
48
|
+
function BaseHttpServer(options = {}) {
|
|
49
|
+
this.options = { ...HTTP_OPTIONS, ...options };
|
|
50
|
+
this.app = this.options.server || express();
|
|
51
|
+
Object.assign(this, this.options);
|
|
52
52
|
|
|
53
53
|
// Middleware to parse request bodies based on the specified encoding
|
|
54
54
|
if (this.encoding === ENCODINGS.json) {
|
package/src/ws/BaseHandler.js
CHANGED
|
@@ -18,21 +18,39 @@ class BaseHandler {
|
|
|
18
18
|
* @param {WebSocket & {sendJson: (message: Object) => void, broadcast: (message: Object) => void}} socket - The WebSocket connection that sent the message.
|
|
19
19
|
* @param {any} message - The incoming message in parsed JSON.
|
|
20
20
|
*/
|
|
21
|
-
handleMessage(socket, message) {
|
|
22
|
-
this.onMessage(socket, message);
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
*
|
|
27
|
-
* @param {WebSocket} socket - The WebSocket connection that sent the message.
|
|
28
|
-
* @param {
|
|
21
|
+
handleMessage(socket, message) {
|
|
22
|
+
this.onMessage(socket, message);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Handles an incoming binary message.
|
|
27
|
+
* @param {WebSocket & {sendJson: (message: Object) => void}} socket - The WebSocket connection that sent the message.
|
|
28
|
+
* @param {Buffer} buffer - The incoming binary message.
|
|
29
|
+
*/
|
|
30
|
+
handleBinaryMessage(socket, buffer) {
|
|
31
|
+
this.onBinaryMessage(socket, buffer);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Method to be overriden to process messages.
|
|
36
|
+
* @param {WebSocket} socket - The WebSocket connection that sent the message.
|
|
37
|
+
* @param {any} message - The incoming message in parsed JSON.
|
|
29
38
|
*/
|
|
30
|
-
onMessage(socket, message) {
|
|
31
|
-
throw "Not yet implemented!";
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
39
|
+
onMessage(socket, message) {
|
|
40
|
+
throw "Not yet implemented!";
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Method to be overriden to process binary messages.
|
|
45
|
+
* @param {WebSocket & {sendJson: (message: Object) => void}} socket - The WebSocket connection that sent the message.
|
|
46
|
+
* @param {Buffer} buffer - The incoming binary message.
|
|
47
|
+
*/
|
|
48
|
+
onBinaryMessage(socket, buffer) {
|
|
49
|
+
socket.sendJson({ error: 'Binary messages are not supported by this handler' });
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
onInitialContact(socket) {
|
|
53
|
+
|
|
36
54
|
}
|
|
37
55
|
}
|
|
38
56
|
|
package/src/ws/SocketRoute.js
CHANGED
|
@@ -11,11 +11,12 @@ class SocketRoute {
|
|
|
11
11
|
* Creates a new instance of `SocketRoute`.
|
|
12
12
|
* @param {Object} options - Configuration options for the WebSocket route.
|
|
13
13
|
* @param {string} options.path - The path of the WebSocket route (e.g., `/chat`, `/lobby`).
|
|
14
|
-
* @param {boolean} options.allowDuplicateConnections - Whether to allow multiple connections from the same client IP address.
|
|
15
|
-
* @param {import('./BaseHandler').BaseHandler[]} options.handlers - An array of handler instances that manage connections and messages for this route.
|
|
16
|
-
* @param {Array<new () => SocketService>} [options.services]
|
|
17
|
-
|
|
18
|
-
|
|
14
|
+
* @param {boolean} options.allowDuplicateConnections - Whether to allow multiple connections from the same client IP address.
|
|
15
|
+
* @param {import('./BaseHandler').BaseHandler[]} options.handlers - An array of handler instances that manage connections and messages for this route.
|
|
16
|
+
* @param {Array<new () => SocketService>} [options.services]
|
|
17
|
+
* @param {import('ws').ServerOptions} [options.websocketOptions] - Options passed to the underlying WebSocketServer.
|
|
18
|
+
*/
|
|
19
|
+
constructor({ path, handlers, services = [], allowDuplicateConnections, websocketOptions = {} } = {}) {
|
|
19
20
|
if (!path) {
|
|
20
21
|
throw new Error('A `path` must be specified for the SocketRoute.');
|
|
21
22
|
}
|
|
@@ -27,17 +28,18 @@ class SocketRoute {
|
|
|
27
28
|
* This determines the endpoint that clients must connect to (e.g., `ws://localhost:3000/chat`).
|
|
28
29
|
* @type {string}
|
|
29
30
|
*/
|
|
30
|
-
this.path = path;
|
|
31
|
+
this.path = path;
|
|
32
|
+
this.websocketOptions = websocketOptions;
|
|
31
33
|
/**
|
|
32
34
|
* The array of handler instances associated with this route.
|
|
33
35
|
* Each handler is responsible for managing WebSocket connections and message handling logic.
|
|
34
36
|
* @type {import('./BaseHandler').BaseHandler[]}
|
|
35
37
|
*/
|
|
36
|
-
this.handlers = handlers.map(HandlerClass => new HandlerClass());
|
|
37
|
-
this.clients = new Map();
|
|
38
|
-
this.server = new WebSocketServer({ noServer: true, path });
|
|
39
|
-
this.server.on('connection', this.handleConnection.bind(this));
|
|
40
|
-
this.allowDuplicateConnections = allowDuplicateConnections;
|
|
38
|
+
this.handlers = handlers.map(HandlerClass => new HandlerClass());
|
|
39
|
+
this.clients = new Map();
|
|
40
|
+
this.server = new WebSocketServer({ noServer: true, path, ...websocketOptions });
|
|
41
|
+
this.server.on('connection', this.handleConnection.bind(this));
|
|
42
|
+
this.allowDuplicateConnections = allowDuplicateConnections;
|
|
41
43
|
|
|
42
44
|
/* ─── ROUTE‑SCOPED SERVICES ─────────────────────────── */
|
|
43
45
|
this.services = services.map(SvcClass => {
|
|
@@ -64,41 +66,51 @@ class SocketRoute {
|
|
|
64
66
|
* @param {WebSocket} socket - The WebSocket connection instance.
|
|
65
67
|
* @param {import('http').IncomingMessage} req - The HTTP request object associated with the connection.
|
|
66
68
|
*/
|
|
67
|
-
handleConnection(socket, req) {
|
|
68
|
-
const ip = req
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
socket.
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
socket.
|
|
89
|
-
socket.
|
|
90
|
-
socket.
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
return;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
|
|
69
|
+
handleConnection(socket, req) {
|
|
70
|
+
const ip = req?.socket?.remoteAddress || 'unknown';
|
|
71
|
+
const clientKey = this.allowDuplicateConnections ? randomUUID() : ip;
|
|
72
|
+
|
|
73
|
+
console.log(`New client connected: ${ip}`);
|
|
74
|
+
|
|
75
|
+
if (!this.allowDuplicateConnections) {
|
|
76
|
+
const existing = this.clients.get(clientKey);
|
|
77
|
+
if (existing) {
|
|
78
|
+
console.warn(`Client ${ip} already connected, disconnecting existing connection.`);
|
|
79
|
+
existing.send(
|
|
80
|
+
JSON.stringify({ msg: 'You are being disconnected because a new client is connected with your IP address.' })
|
|
81
|
+
);
|
|
82
|
+
existing.close();
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
this.clients.set(clientKey, socket);
|
|
87
|
+
socket.clientKey = clientKey;
|
|
88
|
+
socket.__redwebClientKey = clientKey;
|
|
89
|
+
socket.remoteAddress = socket.remoteAddress || ip;
|
|
90
|
+
socket.isAssigned = false; // Tracks whether the socket has been assigned a handler.
|
|
91
|
+
socket.sendJson = (data) => sendJson(socket, data);
|
|
92
|
+
socket.broadcast = (data) => broadcast([...this.clients.values()].filter(sock => sock !== socket), data);
|
|
93
|
+
|
|
94
|
+
this.connectionOpenCallback(socket);
|
|
95
|
+
socket.on('close', () => this.handleClose(socket));
|
|
96
|
+
socket.on('error', (error) => this.handleError(socket, error));
|
|
97
|
+
socket.on('message', (message, isBinary) => {
|
|
98
|
+
if (isBinary) {
|
|
99
|
+
this.handleBinaryMessage(socket, message);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
try {
|
|
104
|
+
const parsed = JSON.parse(message);
|
|
105
|
+
this.handleMessage(socket, parsed);
|
|
106
|
+
} catch (error) {
|
|
107
|
+
console.error(`Error parsing message from ${ip}:`, error);
|
|
108
|
+
socket.sendJson({ error: 'Invalid JSON format' });
|
|
109
|
+
socket.close();
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
}
|
|
102
114
|
|
|
103
115
|
connectionOpenCallback(socket) {
|
|
104
116
|
console.log(`Opening new connection: ${socket.remoteAddress}`);
|
|
@@ -117,19 +129,43 @@ class SocketRoute {
|
|
|
117
129
|
sendJson(sock, { error: `${error.message}` });
|
|
118
130
|
sock.close();
|
|
119
131
|
}
|
|
120
|
-
}
|
|
121
|
-
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
handleBinaryMessage(socket, buffer) {
|
|
136
|
+
const hasBinaryPredicate = this.handlers.some(handler => typeof handler.acceptsBinary === 'function');
|
|
137
|
+
const handler = hasBinaryPredicate
|
|
138
|
+
? this.handlers.find(handler => handler.acceptsBinary?.(socket, buffer))
|
|
139
|
+
: this.handlers.find(handler => typeof handler.handleBinaryMessage === 'function');
|
|
140
|
+
|
|
141
|
+
if (handler) {
|
|
142
|
+
try {
|
|
143
|
+
handler.handleBinaryMessage(socket, buffer);
|
|
144
|
+
} catch (error) {
|
|
145
|
+
console.error(`Error handling binary message in handler ${handler.name}:`, error);
|
|
146
|
+
sendJson(socket, { error: `${error.message}` });
|
|
147
|
+
socket.close();
|
|
148
|
+
}
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
sendJson(socket, {
|
|
153
|
+
error: 'Binary messages are not supported on this route'
|
|
154
|
+
});
|
|
155
|
+
}
|
|
122
156
|
|
|
123
157
|
/**
|
|
124
158
|
* Handles socket disconnection.
|
|
125
159
|
* @param {WebSocket} socket - The WebSocket connection instance.
|
|
126
160
|
* @param {string} ip - The client's IP address.
|
|
127
161
|
*/
|
|
128
|
-
handleClose(socket
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
162
|
+
handleClose(socket) {
|
|
163
|
+
const key = socket.clientKey || socket.__redwebClientKey;
|
|
164
|
+
const ip = socket.remoteAddress || 'unknown';
|
|
165
|
+
console.log(`Client disconnected: ${ip}`);
|
|
166
|
+
if (key && this.clients.get(key) === socket) this.clients.delete(key);
|
|
167
|
+
if (this.connectionCloseCallback) this.connectionCloseCallback(socket);
|
|
168
|
+
}
|
|
133
169
|
|
|
134
170
|
shutdown() {
|
|
135
171
|
this.services.forEach(svc => svc.onShutdown && svc.onShutdown());
|
|
@@ -142,9 +178,10 @@ class SocketRoute {
|
|
|
142
178
|
* @param {Error} error - The error object.
|
|
143
179
|
* @param {string} ip - The client's IP address.
|
|
144
180
|
*/
|
|
145
|
-
handleError(socket, error
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
}
|
|
181
|
+
handleError(socket, error) {
|
|
182
|
+
const ip = socket.remoteAddress || 'unknown';
|
|
183
|
+
console.error(`Socket error from ${ip}:`, error);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
149
186
|
|
|
150
|
-
module.exports = SocketRoute;
|
|
187
|
+
module.exports = SocketRoute;
|