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 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
- onInitialContact(socket: WebSocket): void;
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
- constructor(config: SocketRouteConfig);
84
-
85
- addHandler(handler: new () => BaseHandler): void;
86
- handleMessage(sock: WebSocket, data: any): void;
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "redweb",
3
- "version": "0.7.4",
3
+ "version": "0.7.5",
4
4
  "description": "A way to quickly set up an express server",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -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 = express() || this.options.server;
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) {
@@ -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
- * Method to be overriden to process messages.
27
- * @param {WebSocket} socket - The WebSocket connection that sent the message.
28
- * @param {any} message - The incoming message in parsed JSON.
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
- onInitialContact(socket) {
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
 
@@ -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
- constructor({ path, handlers, services = [], allowDuplicateConnections } = {}) {
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.socket.remoteAddress;
69
- console.log(`New client connected: ${ip}`);
70
- if (this.allowDuplicateConnections) {
71
- this.clients.set(randomUUID(), socket);
72
- } else {
73
- if (this.clients.get(ip) !== undefined) {
74
- const oldClient = this.clients.get(ip);
75
- console.warn(`Client ${ip} already connected, disconnecting existing connection.`);
76
- oldClient.send(
77
- JSON.stringify({ msg: 'You are being disconnected because a new client is connected with your IP address.' })
78
- );
79
- oldClient.close();
80
- }
81
- this.clients.set(ip, socket);
82
- }
83
- socket.isAssigned = false; // Tracks whether the socket has been assigned a handler.
84
- socket.sendJson = (data) => sendJson(socket, data);
85
- socket.broadcast = (data) => broadcast([...this.clients.values()].filter(sock => sock !== socket), data);
86
-
87
- this.connectionOpenCallback(socket);
88
- socket.on('close', this.handleClose.bind(this));
89
- socket.on('error', this.handleError.bind(this));
90
- socket.on('message', (message) => {
91
- try {
92
- const parsed = JSON.parse(message);
93
- this.handleMessage(socket, parsed);
94
- } catch (error) {
95
- console.error(`Error parsing message from ${ip}:`, error);
96
- socket.sendJson({ error: 'Invalid JSON format' });
97
- socket.close();
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, ip) {
129
- console.log(`Client disconnected: ${ip}`);
130
- this.clients.delete(ip);
131
- if (this.connectionCloseCallback) this.connectionCloseCallback(socket);
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, ip) {
146
- console.error(`Socket error from ${ip}:`, error);
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;