redweb 0.7.4 → 0.7.6
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 +95 -8
- package/index.d.ts +47 -32
- package/index.js +5 -4
- package/package.json +1 -1
- package/src/http/BaseHttpServer.js +12 -10
- package/src/http/HttpServer.js +9 -5
- package/src/http/HttpsServer.js +11 -6
- package/src/http/index.js +2 -2
- package/src/ws/BaseHandler.js +32 -14
- package/src/ws/BaseSocketServer.js +10 -8
- package/src/ws/SecureSocketServer.js +12 -9
- package/src/ws/SocketRoute.js +95 -58
- package/src/ws/SocketServer.js +11 -8
package/README.md
CHANGED
|
@@ -19,6 +19,7 @@ const {
|
|
|
19
19
|
SocketRoute, // Per-path WebSocket routing
|
|
20
20
|
SocketService, // Route-scoped background/tick logic
|
|
21
21
|
SocketRegistry, // Evented in-memory store
|
|
22
|
+
BaseHttpServer, // Express app builder for advanced composition
|
|
22
23
|
BaseHandler, // WebSocket message handler base
|
|
23
24
|
sendJson, // Utility to stringify+send
|
|
24
25
|
SOCKET_OPTIONS, // Defaults for socket servers
|
|
@@ -36,6 +37,7 @@ Options:
|
|
|
36
37
|
- `bind` (string): defaults to `0.0.0.0`.
|
|
37
38
|
- `publicPaths` (string[]): folders served as static assets.
|
|
38
39
|
- `services` (array): `{ serviceName, method, function }` for REST endpoints.
|
|
40
|
+
- `listen` (boolean): defaults to `true`; set `false` to build `.app` without binding a port.
|
|
39
41
|
- `listenCallback` (function): invoked after `.listen`.
|
|
40
42
|
- `encoding` (`'json' | 'urlencoded'`): body parser selection.
|
|
41
43
|
- `corsOptions`: passed to `cors`.
|
|
@@ -129,21 +131,105 @@ Each connected socket gets:
|
|
|
129
131
|
|
|
130
132
|
Invalid JSON triggers an error response and closes the socket.
|
|
131
133
|
|
|
134
|
+
### Binary WebSocket messages
|
|
135
|
+
|
|
136
|
+
Text frames are still parsed as JSON and routed by `message.type`. Binary frames are dispatched separately, so handlers can receive raw `Buffer` payloads without triggering JSON parse errors.
|
|
137
|
+
|
|
138
|
+
```js
|
|
139
|
+
const { BaseHandler, SocketRoute } = require('redweb');
|
|
140
|
+
|
|
141
|
+
class UploadHandler extends BaseHandler {
|
|
142
|
+
constructor() { super('upload'); }
|
|
143
|
+
|
|
144
|
+
onMessage(socket, message) {
|
|
145
|
+
socket.sendJson({ type: 'upload:control', action: message.action });
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
onBinaryMessage(socket, buffer) {
|
|
149
|
+
socket.sendJson({ type: 'upload:chunk', bytes: buffer.length });
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
class UploadRoute extends SocketRoute {
|
|
154
|
+
constructor() {
|
|
155
|
+
super({
|
|
156
|
+
path: '/upload',
|
|
157
|
+
handlers: [UploadHandler],
|
|
158
|
+
allowDuplicateConnections: true,
|
|
159
|
+
websocketOptions: {
|
|
160
|
+
maxPayload: 2 * 1024 * 1024
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
`BaseHandler` provides `handleBinaryMessage(socket, buffer)` and `onBinaryMessage(socket, buffer)`. Override `onBinaryMessage` for normal use. If a handler does not override it, RedWeb sends:
|
|
168
|
+
|
|
169
|
+
```json
|
|
170
|
+
{ "error": "Binary messages are not supported by this handler" }
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
Routes may also select a binary-capable handler with `acceptsBinary(socket, buffer)`:
|
|
174
|
+
|
|
175
|
+
```js
|
|
176
|
+
class ImageHandler extends BaseHandler {
|
|
177
|
+
constructor() { super('image'); }
|
|
178
|
+
|
|
179
|
+
acceptsBinary(socket, buffer) {
|
|
180
|
+
return buffer.length > 0;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
onMessage(socket, message) {}
|
|
184
|
+
onBinaryMessage(socket, buffer) {}
|
|
185
|
+
}
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
### WebSocket route options
|
|
189
|
+
|
|
190
|
+
`SocketRoute` accepts `websocketOptions`, which are passed to `new WebSocketServer(...)`. Use this for `ws` server settings such as `maxPayload` or `perMessageDeflate`.
|
|
191
|
+
|
|
192
|
+
```js
|
|
193
|
+
class ClipboardRoute extends SocketRoute {
|
|
194
|
+
constructor() {
|
|
195
|
+
super({
|
|
196
|
+
path: '/clipboard',
|
|
197
|
+
handlers: [ClipboardHandler],
|
|
198
|
+
websocketOptions: {
|
|
199
|
+
maxPayload: 1024 * 1024,
|
|
200
|
+
perMessageDeflate: false
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
```
|
|
206
|
+
|
|
132
207
|
### Sharing an HTTP/HTTPS server
|
|
133
208
|
|
|
134
|
-
`
|
|
209
|
+
Use `listen: false` on `HttpServer` to build the Express app without binding a port. Then create one Node server from `httpServer.app` and pass it to `SocketServer`. When `SocketServer` receives a prebuilt `server`, it attaches upgrade handling but does not call `.listen()` unless you explicitly set `listen: true`.
|
|
135
210
|
|
|
136
211
|
```js
|
|
137
212
|
const http = require('http');
|
|
138
|
-
const
|
|
139
|
-
const { SocketServer } = require('redweb');
|
|
213
|
+
const { HttpServer, METHODS, SocketServer } = require('redweb');
|
|
140
214
|
|
|
141
|
-
const
|
|
142
|
-
|
|
215
|
+
const httpServer = new HttpServer({
|
|
216
|
+
port: 3030,
|
|
217
|
+
listen: false,
|
|
218
|
+
publicPaths: ['./public'],
|
|
219
|
+
services: [
|
|
220
|
+
{ serviceName: '/health', method: METHODS.GET, function: (req, res) => res.json({ ok: true }) },
|
|
221
|
+
{ serviceName: '/session', method: METHODS.POST, function: createSession }
|
|
222
|
+
]
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
const server = http.createServer(httpServer.app);
|
|
143
226
|
|
|
144
|
-
|
|
227
|
+
new SocketServer({
|
|
228
|
+
server,
|
|
229
|
+
routes: [ClipboardRoute]
|
|
230
|
+
});
|
|
145
231
|
|
|
146
|
-
|
|
232
|
+
server.listen(3030, () => console.log('HTTP and WebSocket server listening on 3030'));
|
|
147
233
|
```
|
|
148
234
|
|
|
149
235
|
### Socket services
|
|
@@ -182,8 +268,9 @@ Helpers: `add`, `remove(itemOrId, byKey = 'id')`, `all()`, `count()`.
|
|
|
182
268
|
|
|
183
269
|
## Defaults and lifecycle
|
|
184
270
|
|
|
185
|
-
- HTTP defaults: port `80`, bind `0.0.0.0`.
|
|
271
|
+
- HTTP defaults: port `80`, bind `0.0.0.0`, `listen: true`.
|
|
186
272
|
- WebSocket defaults: port `3000`, single connection per IP unless `allowDuplicateConnections` is set.
|
|
273
|
+
- `SocketServer` owns and listens on its own server by default; if you pass `server`, you own calling `.listen()` unless you also pass `listen: true`.
|
|
187
274
|
- If you do not supply `routes`, `SocketServer` registers a default route at `/` with `DefaultHandler` (it expects messages with `type: 'DefaultHandler'`).
|
|
188
275
|
- `BaseSocketServer.shutdown()` closes all routes, services, and the underlying server.
|
|
189
276
|
|
package/index.d.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
declare module 'redweb' {
|
|
2
2
|
import { Application } from 'express';
|
|
3
3
|
import { CorsOptions } from 'cors';
|
|
4
|
-
import { Server as
|
|
5
|
-
import { WebSocket } from 'ws';
|
|
4
|
+
import { Server as NodeHttpServer } from 'http';
|
|
5
|
+
import { WebSocket, ServerOptions } from 'ws';
|
|
6
|
+
import { Buffer } from 'buffer';
|
|
6
7
|
|
|
7
8
|
/** ─────────────────── HTTP / CORE ─────────────────── */
|
|
8
9
|
|
|
@@ -11,9 +12,10 @@ declare module 'redweb' {
|
|
|
11
12
|
export interface RedWebOptions {
|
|
12
13
|
port?: number;
|
|
13
14
|
bind?: string;
|
|
14
|
-
publicPaths?: string[];
|
|
15
|
-
services?: Array<{ serviceName: string; method: string; function: Function }>;
|
|
16
|
-
|
|
15
|
+
publicPaths?: string[];
|
|
16
|
+
services?: Array<{ serviceName: string; method: string; function: Function }>;
|
|
17
|
+
listen?: boolean;
|
|
18
|
+
listenCallback?: () => void;
|
|
17
19
|
encoding?: RedWebEncoding;
|
|
18
20
|
ssl?: { key: string; cert: string };
|
|
19
21
|
server?: Application;
|
|
@@ -24,9 +26,10 @@ declare module 'redweb' {
|
|
|
24
26
|
/** ─────────────────── SOCKET SERVER ─────────────────── */
|
|
25
27
|
|
|
26
28
|
export interface SocketServerOptions {
|
|
27
|
-
server?:
|
|
28
|
-
port?: number;
|
|
29
|
-
|
|
29
|
+
server?: NodeHttpServer;
|
|
30
|
+
port?: number;
|
|
31
|
+
listen?: boolean;
|
|
32
|
+
routes?: Array<new () => SocketRoute>;
|
|
30
33
|
ssl?: { key: string; cert: string };
|
|
31
34
|
}
|
|
32
35
|
|
|
@@ -34,9 +37,10 @@ declare module 'redweb' {
|
|
|
34
37
|
|
|
35
38
|
export interface SocketRouteConfig {
|
|
36
39
|
path: string;
|
|
37
|
-
handlers: Array<new () => BaseHandler>;
|
|
38
|
-
services?: Array<new () => SocketService>;
|
|
39
|
-
allowDuplicateConnections?: boolean;
|
|
40
|
+
handlers: Array<new () => BaseHandler>;
|
|
41
|
+
services?: Array<new () => SocketService>;
|
|
42
|
+
allowDuplicateConnections?: boolean;
|
|
43
|
+
websocketOptions?: ServerOptions;
|
|
40
44
|
}
|
|
41
45
|
|
|
42
46
|
/** Socket‑side autonomous service (game loops, timers, etc.) */
|
|
@@ -70,30 +74,35 @@ declare module 'redweb' {
|
|
|
70
74
|
message: any
|
|
71
75
|
): void;
|
|
72
76
|
|
|
73
|
-
onMessage(socket: WebSocket, message: any): void;
|
|
74
|
-
|
|
75
|
-
|
|
77
|
+
onMessage(socket: WebSocket, message: any): void;
|
|
78
|
+
acceptsBinary?(socket: WebSocket, buffer: Buffer): boolean;
|
|
79
|
+
handleBinaryMessage(socket: WebSocket, buffer: Buffer): void;
|
|
80
|
+
onBinaryMessage(socket: WebSocket, buffer: Buffer): void;
|
|
81
|
+
onInitialContact(socket: WebSocket): void;
|
|
82
|
+
}
|
|
76
83
|
|
|
77
84
|
export class SocketRoute {
|
|
78
85
|
path: string;
|
|
79
|
-
handlers: BaseHandler[];
|
|
80
|
-
clients: Map<string, WebSocket>;
|
|
81
|
-
allowDuplicateConnections?: boolean;
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
86
|
+
handlers: BaseHandler[];
|
|
87
|
+
clients: Map<string, WebSocket>;
|
|
88
|
+
allowDuplicateConnections?: boolean;
|
|
89
|
+
websocketOptions?: ServerOptions;
|
|
90
|
+
|
|
91
|
+
constructor(config: SocketRouteConfig);
|
|
92
|
+
|
|
93
|
+
addHandler(handler: new () => BaseHandler): void;
|
|
94
|
+
handleMessage(sock: WebSocket, data: any): void;
|
|
95
|
+
handleBinaryMessage(socket: WebSocket, buffer: Buffer): void;
|
|
96
|
+
}
|
|
88
97
|
|
|
89
98
|
/** ─────────────────── SERVER BASE ─────────────────── */
|
|
90
99
|
|
|
91
100
|
export class BaseSocketServer {
|
|
92
101
|
clients: Map<string, WebSocket>;
|
|
93
|
-
server:
|
|
102
|
+
server: NodeHttpServer;
|
|
94
103
|
routes: SocketRoute[];
|
|
95
104
|
|
|
96
|
-
constructor(server:
|
|
105
|
+
constructor(server: NodeHttpServer, options?: SocketServerOptions);
|
|
97
106
|
|
|
98
107
|
addRoute(route: new () => SocketRoute): void;
|
|
99
108
|
}
|
|
@@ -146,13 +155,19 @@ declare module 'redweb' {
|
|
|
146
155
|
constructor(options?: SocketServerOptions);
|
|
147
156
|
}
|
|
148
157
|
|
|
149
|
-
export class
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
158
|
+
export class BaseHttpServer {
|
|
159
|
+
app: Application;
|
|
160
|
+
server?: NodeHttpServer;
|
|
161
|
+
constructor(options?: RedWebOptions);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export class HttpServer extends BaseHttpServer {
|
|
165
|
+
constructor(options?: RedWebOptions);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export class HttpsServer extends BaseHttpServer {
|
|
169
|
+
constructor(options?: RedWebOptions);
|
|
170
|
+
}
|
|
156
171
|
|
|
157
172
|
/** ─────────────────── CONSTANTS ─────────────────── */
|
|
158
173
|
|
package/index.js
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
|
-
const { METHODS } = require('./src/http');
|
|
1
|
+
const { BaseHttpServer, METHODS } = require('./src/http');
|
|
2
2
|
const { sendJson } = require('./src/ws/util');
|
|
3
3
|
const { SocketServer, SecureSocketServer, SOCKET_OPTIONS, SocketRoute, SocketService, SocketRegistry } = require('./src/ws');
|
|
4
4
|
const { BaseHandler } = require('./src/ws/BaseHandler');
|
|
5
5
|
const HttpServer = require('./src/http/HttpServer');
|
|
6
6
|
const HttpsServer = require('./src/http/HttpsServer');
|
|
7
7
|
module.exports = {
|
|
8
|
-
HttpServer,
|
|
9
|
-
HttpsServer,
|
|
10
|
-
|
|
8
|
+
HttpServer,
|
|
9
|
+
HttpsServer,
|
|
10
|
+
BaseHttpServer,
|
|
11
|
+
SocketServer,
|
|
11
12
|
SecureSocketServer,
|
|
12
13
|
BaseHandler,
|
|
13
14
|
SocketRoute,
|
package/package.json
CHANGED
|
@@ -15,13 +15,14 @@ const HtmxRenderer = require('../htmx/HtmxRenderer'); // Import the HtmxRenderer
|
|
|
15
15
|
* @property {number} [port=80] - The port number to bind the server.
|
|
16
16
|
* @property {string} [bind='0.0.0.0'] - The bind address for the server.
|
|
17
17
|
* @property {string[]} [publicPaths=['./public']] - An array of paths to serve static files from.
|
|
18
|
-
* @property {Array<{serviceName: string, method: string, function: Function}>} [services=[]] - An array of services with their endpoints and handlers.
|
|
19
|
-
* @property {
|
|
18
|
+
* @property {Array<{serviceName: string, method: string, function: Function}>} [services=[]] - An array of services with their endpoints and handlers.
|
|
19
|
+
* @property {boolean} [listen=true] - Whether HttpServer/HttpsServer should automatically start listening.
|
|
20
|
+
* @property {Function} [listenCallback] - Callback function to execute once the server starts listening.
|
|
20
21
|
* @property {RedWebEncoding} [encoding='json'] - The encoding type for the request bodies ('json' or 'urlencoded').
|
|
21
22
|
* @property {Object} [ssl] - SSL configuration for HTTPS server.
|
|
22
23
|
* @property {string} [ssl.key] - Path to the SSL key file.
|
|
23
24
|
* @property {string} [ssl.cert] - Path to the SSL certificate file.
|
|
24
|
-
* @property {import('express').Application} [server] -
|
|
25
|
+
* @property {import('express').Application} [server] - Existing Express application to configure.
|
|
25
26
|
* @property {import('cors').CorsOptions} [corsOptions] - The CORS Options.
|
|
26
27
|
* @property {boolean} [enableHtmxRendering=false] - Enable dynamic HTMX file rendering.
|
|
27
28
|
*/
|
|
@@ -30,9 +31,10 @@ const ENCODINGS = { json: 'json', urlencoded: 'urlencoded' };
|
|
|
30
31
|
const HTTP_OPTIONS = {
|
|
31
32
|
port: 80,
|
|
32
33
|
bind: '0.0.0.0',
|
|
33
|
-
publicPaths: ['./public'],
|
|
34
|
-
services: [],
|
|
35
|
-
|
|
34
|
+
publicPaths: ['./public'],
|
|
35
|
+
services: [],
|
|
36
|
+
listen: true,
|
|
37
|
+
listenCallback: undefined,
|
|
36
38
|
encoding: ENCODINGS.json,
|
|
37
39
|
ssl: null,
|
|
38
40
|
server: undefined,
|
|
@@ -45,10 +47,10 @@ const HTTP_OPTIONS = {
|
|
|
45
47
|
* @param {RedWebOptions} options - Configuration options for RedWeb.
|
|
46
48
|
* @return {Object} Express application instance.
|
|
47
49
|
*/
|
|
48
|
-
function BaseHttpServer(options = {}) {
|
|
49
|
-
this.options = { ...HTTP_OPTIONS, ...options };
|
|
50
|
-
this.app =
|
|
51
|
-
Object.assign(this, this.options);
|
|
50
|
+
function BaseHttpServer(options = {}) {
|
|
51
|
+
this.options = { ...HTTP_OPTIONS, ...options };
|
|
52
|
+
this.app = this.options.server || express();
|
|
53
|
+
Object.assign(this, this.options);
|
|
52
54
|
|
|
53
55
|
// Middleware to parse request bodies based on the specified encoding
|
|
54
56
|
if (this.encoding === ENCODINGS.json) {
|
package/src/http/HttpServer.js
CHANGED
|
@@ -5,10 +5,14 @@ const { BaseHttpServer } = require('./BaseHttpServer');
|
|
|
5
5
|
* @param {RedWebOptions} options - Configuration options for RedWeb.
|
|
6
6
|
* @return {Object} Express application instance.
|
|
7
7
|
*/
|
|
8
|
-
function HttpServer(options = {}) {
|
|
9
|
-
BaseHttpServer.call(this, options);
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
}
|
|
8
|
+
function HttpServer(options = {}) {
|
|
9
|
+
BaseHttpServer.call(this, options);
|
|
10
|
+
if (this.listen === false) {
|
|
11
|
+
return this;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
this.server = this.app.listen(this.port, this.listenCallback ? this.listenCallback : () => console.log(`RedWeb HttpServer listening on port ${this.port}`));
|
|
15
|
+
return this;
|
|
16
|
+
}
|
|
13
17
|
|
|
14
18
|
module.exports = HttpServer;
|
package/src/http/HttpsServer.js
CHANGED
|
@@ -7,11 +7,16 @@ const loadSslConfig = require('../sslConfig');
|
|
|
7
7
|
* @param {RedWebOptions} options - Configuration options for RedWeb.
|
|
8
8
|
* @return {Object} Express application instance.
|
|
9
9
|
*/
|
|
10
|
-
function HttpsServer(options = {}) {
|
|
11
|
-
BaseHttpServer.call(this, options);
|
|
12
|
-
const sslOptions = loadSslConfig(this.ssl);
|
|
13
|
-
https.createServer(sslOptions, this.app)
|
|
14
|
-
|
|
15
|
-
|
|
10
|
+
function HttpsServer(options = {}) {
|
|
11
|
+
BaseHttpServer.call(this, options);
|
|
12
|
+
const sslOptions = loadSslConfig(this.ssl);
|
|
13
|
+
this.server = https.createServer(sslOptions, this.app);
|
|
14
|
+
if (this.listen === false) {
|
|
15
|
+
return this;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
this.server.listen(this.port, this.listenCallback ? this.listenCallback : () => console.log(`RedWeb HttpsServer listening on port ${this.port}`));
|
|
19
|
+
return this;
|
|
20
|
+
}
|
|
16
21
|
|
|
17
22
|
module.exports = HttpsServer;
|
package/src/http/index.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
const { METHODS } = require('./BaseHttpServer');
|
|
2
|
-
module.exports = { HttpServer: require('./HttpServer'), HttpsServer: require('./HttpsServer'), METHODS}
|
|
1
|
+
const { BaseHttpServer, METHODS } = require('./BaseHttpServer');
|
|
2
|
+
module.exports = { BaseHttpServer, HttpServer: require('./HttpServer'), HttpsServer: require('./HttpsServer'), METHODS}
|
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
|
|
|
@@ -1,17 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @typedef {Object} SocketServerOptions
|
|
3
|
-
* @property {import('http').Server} [server] HTTP server to bind to
|
|
4
|
-
* @property {number} [port=3000] Port to listen on
|
|
5
|
-
* @property {
|
|
6
|
-
|
|
3
|
+
* @property {import('http').Server} [server] HTTP server to bind to
|
|
4
|
+
* @property {number} [port=3000] Port to listen on
|
|
5
|
+
* @property {boolean} [listen=true] Whether owned servers should automatically start listening
|
|
6
|
+
* @property {Array<new () => import('./SocketRoute').SocketRoute>} [routes]
|
|
7
|
+
*/
|
|
7
8
|
|
|
8
9
|
const DefaultRoute = require('./DefaultRoute');
|
|
9
10
|
|
|
10
11
|
const SOCKET_OPTIONS = {
|
|
11
|
-
port: 3000,
|
|
12
|
-
ssl: null,
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
port: 3000,
|
|
13
|
+
ssl: null,
|
|
14
|
+
listen: true,
|
|
15
|
+
routes: []
|
|
16
|
+
};
|
|
15
17
|
|
|
16
18
|
/**
|
|
17
19
|
* Base WebSocket server
|
|
@@ -7,14 +7,17 @@ const { BaseSocketServer } = require('./BaseSocketServer');
|
|
|
7
7
|
* @param {SocketServerOptions} options - Configuration options for SecureSocketServer.
|
|
8
8
|
* @return {Object} WebSocket server instance.
|
|
9
9
|
*/
|
|
10
|
-
class SecureSocketServer extends BaseSocketServer {
|
|
11
|
-
constructor(options = {}) {
|
|
12
|
-
const
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
server
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
}
|
|
10
|
+
class SecureSocketServer extends BaseSocketServer {
|
|
11
|
+
constructor(options = {}) {
|
|
12
|
+
const ownsServer = !options?.server;
|
|
13
|
+
const sslOptions = ownsServer ? loadSslConfig(options.ssl) : null;
|
|
14
|
+
const server = options?.server || https.createServer(sslOptions);
|
|
15
|
+
super(server, options);
|
|
16
|
+
if ((ownsServer && this.listen !== false) || (!ownsServer && options.listen === true)) {
|
|
17
|
+
server.listen(this.port, () => console.log(`RedWeb SecureSocketServer listening on port ${this.port}`));
|
|
18
|
+
}
|
|
19
|
+
return this;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
19
22
|
|
|
20
23
|
module.exports = SecureSocketServer;
|
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;
|
package/src/ws/SocketServer.js
CHANGED
|
@@ -6,13 +6,16 @@ const { BaseSocketServer } = require('./BaseSocketServer');
|
|
|
6
6
|
* @param {SocketServerOptions} options - Configuration options for SocketServer.
|
|
7
7
|
* @return {Object} WebSocket server instance.
|
|
8
8
|
*/
|
|
9
|
-
class SocketServer extends BaseSocketServer {
|
|
10
|
-
constructor(options = {}) {
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
server
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
}
|
|
9
|
+
class SocketServer extends BaseSocketServer {
|
|
10
|
+
constructor(options = {}) {
|
|
11
|
+
const ownsServer = !options?.server;
|
|
12
|
+
const server = options?.server || http.createServer();
|
|
13
|
+
super(server, options);
|
|
14
|
+
if ((ownsServer && this.listen !== false) || (!ownsServer && options.listen === true)) {
|
|
15
|
+
server.listen(this.port, () => console.log(`RedWeb SocketServer listening on port ${this.port}`));
|
|
16
|
+
}
|
|
17
|
+
return this;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
17
20
|
|
|
18
21
|
module.exports = SocketServer;
|