redweb 0.6.9 → 0.7.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 +116 -163
- package/index.d.ts +78 -157
- package/package.json +1 -1
- package/src/ws/BaseHandler.js +2 -2
- package/src/ws/SocketRoute.js +35 -12
package/README.md
CHANGED
|
@@ -1,239 +1,192 @@
|
|
|
1
|
+
Here is the **fully updated and cleaned-up `README.md`** for RedWeb, now featuring a **working broadcast chat example** using `allowDuplicateConnections`:
|
|
2
|
+
|
|
3
|
+
---
|
|
4
|
+
|
|
1
5
|
# RedWeb
|
|
2
6
|
|
|
3
|
-
RedWeb is a
|
|
7
|
+
**RedWeb** is a flexible Node.js framework built on top of **Express.js** and **WebSocket**. It enables quick setup of HTTP(S) and WebSocket servers with a modular route and handler system.
|
|
4
8
|
|
|
5
|
-
|
|
9
|
+
---
|
|
6
10
|
|
|
7
|
-
|
|
11
|
+
## 📦 Installation
|
|
8
12
|
|
|
9
13
|
```bash
|
|
10
14
|
npm install redweb
|
|
11
15
|
```
|
|
12
16
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
### Basic Example
|
|
17
|
+
---
|
|
16
18
|
|
|
17
|
-
|
|
19
|
+
## 🚀 Quick Start
|
|
18
20
|
|
|
19
|
-
```
|
|
21
|
+
```js
|
|
20
22
|
const { HttpServer, SocketServer } = require('redweb');
|
|
21
23
|
|
|
22
|
-
// HTTP server with default configuration
|
|
23
24
|
const httpServer = new HttpServer();
|
|
24
|
-
|
|
25
|
-
// WebSocket server with default configuration
|
|
26
25
|
const socketServer = new SocketServer();
|
|
27
26
|
```
|
|
28
27
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
#### HTTP Server with HTMX Rendering
|
|
28
|
+
---
|
|
32
29
|
|
|
33
|
-
|
|
30
|
+
## 🌐 HTTP Server Example (HTMX Support)
|
|
34
31
|
|
|
35
|
-
```
|
|
32
|
+
```js
|
|
36
33
|
const { HttpServer, METHODS } = require('redweb');
|
|
37
34
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
}
|
|
54
|
-
]
|
|
55
|
-
};
|
|
56
|
-
|
|
57
|
-
const app = new HttpServer(options);
|
|
35
|
+
new HttpServer({
|
|
36
|
+
port: 3000,
|
|
37
|
+
publicPaths: ['./public'],
|
|
38
|
+
enableHtmxRendering: true,
|
|
39
|
+
services: [
|
|
40
|
+
{
|
|
41
|
+
serviceName: '/submit',
|
|
42
|
+
method: METHODS.POST,
|
|
43
|
+
function: (req, res) => {
|
|
44
|
+
if (!req.body.name) return res.status(400).json({ error: 'Missing name' });
|
|
45
|
+
res.status(200).json({ message: `Thanks, ${req.body.name}!` });
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
]
|
|
49
|
+
});
|
|
58
50
|
```
|
|
59
51
|
|
|
60
|
-
|
|
61
|
-
|
|
52
|
+
`.htmx` files under `public/` will render server-side.
|
|
62
53
|
Example:
|
|
63
54
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
const name = 'RedWeb User';
|
|
67
|
-
|
|
55
|
+
```html
|
|
56
|
+
<!-- public/hello.htmx -->
|
|
68
57
|
<@>
|
|
69
|
-
|
|
58
|
+
<h1>Hello, {{name}}!</h1>
|
|
70
59
|
<@/>
|
|
71
60
|
```
|
|
72
61
|
|
|
73
|
-
|
|
74
|
-
```html
|
|
75
|
-
<h1>Hello, RedWeb User!</h1>
|
|
76
|
-
```
|
|
77
|
-
|
|
78
|
-
#### HTTPS Server
|
|
79
|
-
|
|
80
|
-
```javascript
|
|
81
|
-
const { HttpsServer } = require('redweb');
|
|
82
|
-
|
|
83
|
-
const options = {
|
|
84
|
-
port: 3443,
|
|
85
|
-
ssl: {
|
|
86
|
-
key: './path/to/key.pem',
|
|
87
|
-
cert: './path/to/cert.pem'
|
|
88
|
-
},
|
|
89
|
-
publicPaths: ['./public'],
|
|
90
|
-
enableHtmxRendering: true // Enable .htmx rendering
|
|
91
|
-
};
|
|
92
|
-
|
|
93
|
-
const app = new HttpsServer(options);
|
|
94
|
-
```
|
|
95
|
-
|
|
96
|
-
### WebSocket Server with Routes and Handlers
|
|
62
|
+
---
|
|
97
63
|
|
|
98
|
-
|
|
64
|
+
## 🔌 WebSocket Broadcast Chat (🔥 Instant Testing)
|
|
99
65
|
|
|
100
|
-
|
|
66
|
+
### 1. `ChatHandler.js`
|
|
101
67
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
```javascript
|
|
68
|
+
```js
|
|
105
69
|
const { BaseHandler } = require('redweb');
|
|
106
70
|
|
|
107
71
|
class ChatHandler extends BaseHandler {
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
}
|
|
72
|
+
constructor() {
|
|
73
|
+
super('chat');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
onMessage(socket, message) {
|
|
77
|
+
const text = message.text;
|
|
78
|
+
console.log(`Broadcasting: ${text}`);
|
|
79
|
+
socket.broadcast({ type: 'chat', text });
|
|
80
|
+
}
|
|
116
81
|
}
|
|
117
82
|
|
|
118
83
|
module.exports = ChatHandler;
|
|
119
84
|
```
|
|
120
85
|
|
|
121
|
-
|
|
86
|
+
---
|
|
122
87
|
|
|
123
|
-
|
|
88
|
+
### 2. `ChatRoute.js`
|
|
124
89
|
|
|
125
|
-
```
|
|
90
|
+
```js
|
|
126
91
|
const { SocketRoute } = require('redweb');
|
|
127
92
|
const ChatHandler = require('./ChatHandler');
|
|
128
93
|
|
|
129
94
|
class ChatRoute extends SocketRoute {
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
}
|
|
95
|
+
constructor() {
|
|
96
|
+
super({
|
|
97
|
+
path: '/chat',
|
|
98
|
+
handlers: [ChatHandler],
|
|
99
|
+
allowDuplicateConnections: true // key for local testing!
|
|
100
|
+
});
|
|
101
|
+
}
|
|
136
102
|
}
|
|
137
103
|
|
|
138
104
|
module.exports = ChatRoute;
|
|
139
105
|
```
|
|
140
106
|
|
|
141
|
-
|
|
107
|
+
---
|
|
108
|
+
|
|
109
|
+
### 3. `server.js`
|
|
142
110
|
|
|
143
|
-
```
|
|
111
|
+
```js
|
|
144
112
|
const { SocketServer } = require('redweb');
|
|
145
113
|
const ChatRoute = require('./ChatRoute');
|
|
146
114
|
|
|
147
115
|
new SocketServer({
|
|
148
|
-
|
|
149
|
-
|
|
116
|
+
port: 3000,
|
|
117
|
+
routes: [ChatRoute]
|
|
150
118
|
});
|
|
151
119
|
```
|
|
152
120
|
|
|
153
|
-
|
|
121
|
+
---
|
|
154
122
|
|
|
155
|
-
|
|
123
|
+
### 4. `client.html`
|
|
156
124
|
|
|
157
|
-
```
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
125
|
+
```html
|
|
126
|
+
<!DOCTYPE html>
|
|
127
|
+
<html>
|
|
128
|
+
<body>
|
|
129
|
+
<h1>Broadcast Chat</h1>
|
|
130
|
+
<input id="msg" placeholder="Type message..." />
|
|
131
|
+
<button onclick="send()">Send</button>
|
|
132
|
+
<pre id="log"></pre>
|
|
133
|
+
|
|
134
|
+
<script>
|
|
135
|
+
const log = document.getElementById('log');
|
|
136
|
+
const ws = new WebSocket('ws://localhost:3000/chat');
|
|
137
|
+
|
|
138
|
+
ws.onmessage = (e) => {
|
|
139
|
+
const msg = JSON.parse(e.data);
|
|
140
|
+
log.textContent += `\n${msg.text}`;
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
function send() {
|
|
144
|
+
const text = document.getElementById('msg').value;
|
|
145
|
+
ws.send(JSON.stringify({ type: 'chat', text }));
|
|
146
|
+
}
|
|
147
|
+
</script>
|
|
148
|
+
</body>
|
|
149
|
+
</html>
|
|
175
150
|
```
|
|
176
151
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
The client connects to the WebSocket server using the specified route.
|
|
152
|
+
Open multiple tabs to test!
|
|
180
153
|
|
|
181
|
-
|
|
182
|
-
const WebSocket = require('ws');
|
|
154
|
+
---
|
|
183
155
|
|
|
184
|
-
|
|
156
|
+
## 🔧 Options
|
|
185
157
|
|
|
186
|
-
|
|
187
|
-
ws.send(JSON.stringify({ type: 'chat', text: 'Hello there!' }));
|
|
188
|
-
});
|
|
158
|
+
### HTTP / HTTPS Server Options
|
|
189
159
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
160
|
+
| Option | Type | Default | Description |
|
|
161
|
+
| --------------------- | --------- | -------------- | ------------------------------ |
|
|
162
|
+
| `port` | number | `80` | Port to listen on |
|
|
163
|
+
| `bind` | string | `'0.0.0.0'` | Bind address |
|
|
164
|
+
| `publicPaths` | string\[] | `['./public']` | Serve static and `.htmx` files |
|
|
165
|
+
| `services` | object\[] | `[]` | REST endpoints |
|
|
166
|
+
| `enableHtmxRendering` | boolean | `false` | Enables `.htmx` file rendering |
|
|
167
|
+
| `ssl` | object | `undefined` | Used in `HttpsServer` |
|
|
194
168
|
|
|
195
|
-
|
|
169
|
+
---
|
|
196
170
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
```javascript
|
|
200
|
-
const { SocketRoute } = require('redweb');
|
|
201
|
-
|
|
202
|
-
class ChatRoute extends SocketRoute {
|
|
203
|
-
constructor() {
|
|
204
|
-
super({
|
|
205
|
-
path: '/chat',
|
|
206
|
-
handlers: []
|
|
207
|
-
});
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
onConnection(socket) {
|
|
211
|
-
console.log('New client connected:', socket.remoteAddress);
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
module.exports = ChatRoute;
|
|
216
|
-
```
|
|
171
|
+
### WebSocket Server Options
|
|
217
172
|
|
|
218
|
-
|
|
173
|
+
| Option | Type | Default | Description |
|
|
174
|
+
| --------------------------- | ------------------- | ------- | ----------------------------------- |
|
|
175
|
+
| `port` | number | `3000` | WebSocket port |
|
|
176
|
+
| `routes` | `SocketRoute[]` | `[]` | List of custom route classes |
|
|
177
|
+
| `allowDuplicateConnections` | boolean (per route) | `false` | Allow multiple clients from same IP |
|
|
219
178
|
|
|
220
|
-
|
|
179
|
+
---
|
|
221
180
|
|
|
222
|
-
|
|
223
|
-
- **bind**: Bind address (default: `0.0.0.0`).
|
|
224
|
-
- **publicPaths**: Array of paths to serve static files (default: `['./public']`).
|
|
225
|
-
- **services**: Array of services with endpoints and handlers (default: `[]`).
|
|
226
|
-
- **listenCallback**: Function to execute once the server starts listening.
|
|
227
|
-
- **encoding**: Encoding type for request bodies (`'json'` or `'urlencoded'`).
|
|
228
|
-
- **ssl**: SSL configuration for HTTPS server (`{ key: './path/to/key.pem', cert: './path/to/cert.pem' }`).
|
|
229
|
-
- **enableHtmxRendering**: Enable dynamic rendering of `.htmx` files (default: `false`).
|
|
181
|
+
## 🆕 0.7.0 Update Highlights
|
|
230
182
|
|
|
231
|
-
|
|
183
|
+
* ✅ `allowDuplicateConnections` for multi-tab testing
|
|
184
|
+
* ✅ Robust message validation
|
|
185
|
+
* ✅ `socket.broadcast()` now excludes sender
|
|
186
|
+
* ✅ Better error handling
|
|
232
187
|
|
|
233
|
-
|
|
234
|
-
- **routes**: Array of `SocketRoute` classes to define WebSocket routes and handlers.
|
|
235
|
-
- **ssl**: SSL configuration for SecureSocketServer (`{ key: './path/to/key.pem', cert: './path/to/cert.pem' }`).
|
|
188
|
+
---
|
|
236
189
|
|
|
237
|
-
## License
|
|
190
|
+
## 🪪 License
|
|
238
191
|
|
|
239
|
-
MIT
|
|
192
|
+
MIT
|
package/index.d.ts
CHANGED
|
@@ -1,177 +1,98 @@
|
|
|
1
1
|
declare module 'redweb' {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import { Server as
|
|
5
|
-
import WebSocket from 'ws';
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* RedWeb encoding types.
|
|
9
|
-
*/
|
|
2
|
+
import { Application } from 'express';
|
|
3
|
+
import { CorsOptions } from 'cors';
|
|
4
|
+
import { Server as HttpServer } from 'http';
|
|
5
|
+
import { WebSocket } from 'ws';
|
|
6
|
+
|
|
10
7
|
export type RedWebEncoding = 'json' | 'urlencoded';
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Service configuration for RedWeb.
|
|
14
|
-
*/
|
|
15
|
-
export interface Service {
|
|
16
|
-
serviceName: string;
|
|
17
|
-
method: 'get' | 'post' | 'put' | 'delete' | 'patch' | 'options' | 'head';
|
|
18
|
-
function: (req: Request, res: Response) => void;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* Options for configuring a RedWeb server.
|
|
23
|
-
*/
|
|
8
|
+
|
|
24
9
|
export interface RedWebOptions {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
corsOptions?: import('cors').CorsOptions;
|
|
36
|
-
enableHtmxRendering?: boolean; // New flag to enable HTMX rendering
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* Configuration for a WebSocket handler.
|
|
41
|
-
*/
|
|
42
|
-
export interface HandlerConfig {
|
|
43
|
-
name: string;
|
|
44
|
-
handlers: {
|
|
45
|
-
[type: string]: (socket: WebSocket, data: any) => void;
|
|
46
|
-
};
|
|
10
|
+
port?: number;
|
|
11
|
+
bind?: string;
|
|
12
|
+
publicPaths?: string[];
|
|
13
|
+
services?: Array<{ serviceName: string; method: string; function: Function }>;
|
|
14
|
+
listenCallback?: () => void;
|
|
15
|
+
encoding?: RedWebEncoding;
|
|
16
|
+
ssl?: { key: string; cert: string };
|
|
17
|
+
server?: Application;
|
|
18
|
+
corsOptions?: CorsOptions;
|
|
19
|
+
enableHtmxRendering?: boolean;
|
|
47
20
|
}
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* Options for configuring a WebSocket server.
|
|
51
|
-
*/
|
|
21
|
+
|
|
52
22
|
export interface SocketServerOptions {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
messageHandlers?: {
|
|
58
|
-
[type: string]: (socket: WebSocket, data: any) => void;
|
|
59
|
-
};
|
|
60
|
-
ssl?: {
|
|
61
|
-
key: string;
|
|
62
|
-
cert: string;
|
|
63
|
-
};
|
|
64
|
-
routes?: Array<new () => SocketRoute>;
|
|
23
|
+
server?: HttpServer;
|
|
24
|
+
port?: number;
|
|
25
|
+
routes?: Array<new () => SocketRoute>;
|
|
26
|
+
ssl?: { key: string; cert: string };
|
|
65
27
|
}
|
|
66
|
-
|
|
67
|
-
/**
|
|
68
|
-
* WebSocket route configuration.
|
|
69
|
-
*/
|
|
28
|
+
|
|
70
29
|
export interface SocketRouteConfig {
|
|
71
|
-
|
|
72
|
-
|
|
30
|
+
path: string;
|
|
31
|
+
handlers: Array<new () => BaseHandler>;
|
|
32
|
+
allowDuplicateConnections?: boolean;
|
|
73
33
|
}
|
|
74
|
-
|
|
75
|
-
/**
|
|
76
|
-
* Represents a WebSocket route.
|
|
77
|
-
*/
|
|
78
|
-
export class SocketRoute {
|
|
79
|
-
path: string;
|
|
80
|
-
handlers: BaseHandler[];
|
|
81
|
-
|
|
82
|
-
constructor(config: SocketRouteConfig);
|
|
83
|
-
|
|
84
|
-
addHandler(HandlerClass: new () => BaseHandler): void;
|
|
85
|
-
handleConnection(socket: WebSocket, req: import('http').IncomingMessage): void;
|
|
86
|
-
handleMessage(socket: WebSocket, data: any): void;
|
|
87
|
-
handleClose(socket: WebSocket): void;
|
|
88
|
-
handleError(socket: WebSocket, error: Error): void;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
/**
|
|
92
|
-
* Base class for WebSocket handlers.
|
|
93
|
-
*/
|
|
34
|
+
|
|
94
35
|
export class BaseHandler {
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
36
|
+
name: string;
|
|
37
|
+
constructor(name: string);
|
|
38
|
+
handleMessage(
|
|
39
|
+
socket: WebSocket & {
|
|
40
|
+
sendJson: (message: object) => void;
|
|
41
|
+
broadcast: (message: object) => void;
|
|
42
|
+
},
|
|
43
|
+
message: any
|
|
44
|
+
): void;
|
|
45
|
+
onMessage(socket: WebSocket, message: any): void;
|
|
46
|
+
onInitialContact(socket: WebSocket): void;
|
|
101
47
|
}
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
addRoute(route: new () => SocketRoute): void;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
/**
|
|
115
|
-
* HTTP server class.
|
|
116
|
-
*/
|
|
117
|
-
export class HttpServer {
|
|
118
|
-
constructor(options?: RedWebOptions);
|
|
48
|
+
|
|
49
|
+
export class SocketRoute {
|
|
50
|
+
path: string;
|
|
51
|
+
handlers: BaseHandler[];
|
|
52
|
+
clients: Map<string, WebSocket>;
|
|
53
|
+
allowDuplicateConnections?: boolean;
|
|
54
|
+
constructor(config: SocketRouteConfig);
|
|
55
|
+
addHandler(handler: new () => BaseHandler): void;
|
|
56
|
+
handleMessage(sock: WebSocket, data: any): void;
|
|
119
57
|
}
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
58
|
+
|
|
59
|
+
export class BaseSocketServer {
|
|
60
|
+
clients: Map<string, WebSocket>;
|
|
61
|
+
server: HttpServer;
|
|
62
|
+
routes: SocketRoute[];
|
|
63
|
+
constructor(server: HttpServer, options?: SocketServerOptions);
|
|
64
|
+
addRoute(route: new () => SocketRoute): void;
|
|
126
65
|
}
|
|
127
|
-
|
|
128
|
-
/**
|
|
129
|
-
* WebSocket server class.
|
|
130
|
-
*/
|
|
66
|
+
|
|
131
67
|
export class SocketServer extends BaseSocketServer {
|
|
132
|
-
|
|
68
|
+
constructor(options?: SocketServerOptions);
|
|
133
69
|
}
|
|
134
|
-
|
|
135
|
-
/**
|
|
136
|
-
* Secure WebSocket server class.
|
|
137
|
-
*/
|
|
70
|
+
|
|
138
71
|
export class SecureSocketServer extends BaseSocketServer {
|
|
139
|
-
|
|
72
|
+
constructor(options?: SocketServerOptions);
|
|
140
73
|
}
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
export const ENCODINGS: {
|
|
151
|
-
json: 'json';
|
|
152
|
-
urlencoded: 'urlencoded';
|
|
153
|
-
};
|
|
154
|
-
|
|
155
|
-
/**
|
|
156
|
-
* Constants for HTTP methods.
|
|
157
|
-
*/
|
|
74
|
+
|
|
75
|
+
export class HttpServer {
|
|
76
|
+
constructor(options?: RedWebOptions);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export class HttpsServer {
|
|
80
|
+
constructor(options?: RedWebOptions);
|
|
81
|
+
}
|
|
82
|
+
|
|
158
83
|
export const METHODS: {
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
84
|
+
GET: 'get';
|
|
85
|
+
POST: 'post';
|
|
86
|
+
PUT: 'put';
|
|
87
|
+
DELETE: 'delete';
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
export const ENCODINGS: {
|
|
91
|
+
json: 'json';
|
|
92
|
+
urlencoded: 'urlencoded';
|
|
166
93
|
};
|
|
167
|
-
|
|
168
|
-
/**
|
|
169
|
-
* Default HTTP options.
|
|
170
|
-
*/
|
|
94
|
+
|
|
171
95
|
export const HTTP_OPTIONS: RedWebOptions;
|
|
172
|
-
|
|
173
|
-
/**
|
|
174
|
-
* Default socket options.
|
|
175
|
-
*/
|
|
176
96
|
export const SOCKET_OPTIONS: SocketServerOptions;
|
|
177
|
-
}
|
|
97
|
+
}
|
|
98
|
+
|
package/package.json
CHANGED
package/src/ws/BaseHandler.js
CHANGED
|
@@ -16,7 +16,7 @@ class BaseHandler {
|
|
|
16
16
|
/**
|
|
17
17
|
* Handles an incoming message and routes it to the appropriate handler function.
|
|
18
18
|
* @param {WebSocket & {sendJson: (message: Object) => void, broadcast: (message: Object) => void}} socket - The WebSocket connection that sent the message.
|
|
19
|
-
* @param {
|
|
19
|
+
* @param {any} message - The incoming message in parsed JSON.
|
|
20
20
|
*/
|
|
21
21
|
handleMessage(socket, message) {
|
|
22
22
|
this.onMessage(socket, message);
|
|
@@ -25,7 +25,7 @@ class BaseHandler {
|
|
|
25
25
|
/**
|
|
26
26
|
* Method to be overriden to process messages.
|
|
27
27
|
* @param {WebSocket} socket - The WebSocket connection that sent the message.
|
|
28
|
-
* @param {
|
|
28
|
+
* @param {any} message - The incoming message in parsed JSON.
|
|
29
29
|
*/
|
|
30
30
|
onMessage(socket, message) {
|
|
31
31
|
throw "Not yet implemented!";
|
package/src/ws/SocketRoute.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const { WebSocketServer } = require("ws");
|
|
2
2
|
const { sendJson, broadcast } = require("./util");
|
|
3
|
+
const { randomUUID } = require("crypto");
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* Represents a WebSocket route configuration.
|
|
@@ -10,9 +11,10 @@ class SocketRoute {
|
|
|
10
11
|
* Creates a new instance of `SocketRoute`.
|
|
11
12
|
* @param {Object} options - Configuration options for the WebSocket route.
|
|
12
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.
|
|
13
15
|
* @param {import('./BaseHandler').BaseHandler[]} options.handlers - An array of handler instances that manage connections and messages for this route.
|
|
14
16
|
*/
|
|
15
|
-
constructor({path, handlers } = {}) {
|
|
17
|
+
constructor({path, handlers, allowDuplicateConnections } = {}) {
|
|
16
18
|
if (!path) {
|
|
17
19
|
throw new Error('A `path` must be specified for the SocketRoute.');
|
|
18
20
|
}
|
|
@@ -34,6 +36,7 @@ class SocketRoute {
|
|
|
34
36
|
this.clients = new Map();
|
|
35
37
|
this.server = new WebSocketServer({ noServer: true, path });
|
|
36
38
|
this.server.on('connection', this.handleConnection.bind(this));
|
|
39
|
+
this.allowDuplicateConnections = allowDuplicateConnections;
|
|
37
40
|
}
|
|
38
41
|
/**
|
|
39
42
|
* Adds a new handler to the WebSocket server.
|
|
@@ -56,23 +59,37 @@ class SocketRoute {
|
|
|
56
59
|
handleConnection(socket, req) {
|
|
57
60
|
const ip = req.socket.remoteAddress;
|
|
58
61
|
console.log(`New client connected: ${ip}`);
|
|
59
|
-
if (this.
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
62
|
+
if (this.allowDuplicateConnections) {
|
|
63
|
+
this.clients.set(randomUUID(), socket);
|
|
64
|
+
} else {
|
|
65
|
+
if (this.clients.get(ip) !== undefined) {
|
|
66
|
+
const oldClient = this.clients.get(ip);
|
|
67
|
+
console.warn(`Client ${ip} already connected, disconnecting existing connection.`);
|
|
68
|
+
oldClient.send(
|
|
69
|
+
JSON.stringify({ msg: 'You are being disconnected because a new client is connected with your IP address.' })
|
|
70
|
+
);
|
|
71
|
+
oldClient.close();
|
|
72
|
+
}
|
|
73
|
+
this.clients.set(ip, socket);
|
|
66
74
|
}
|
|
67
|
-
this.clients.set(ip, socket);
|
|
68
75
|
socket.isAssigned = false; // Tracks whether the socket has been assigned a handler.
|
|
69
76
|
socket.sendJson = (data) => sendJson(socket, data);
|
|
70
|
-
socket.broadcast = (data) => broadcast([...this.clients.values()], data);
|
|
77
|
+
socket.broadcast = (data) => broadcast([...this.clients.values()].filter(sock => sock !== socket), data);
|
|
71
78
|
|
|
72
79
|
this.connectionOpenCallback(socket);
|
|
73
|
-
socket.on('message', (message) => this.handleMessage(socket, JSON.parse(message)));
|
|
74
80
|
socket.on('close', this.handleClose.bind(this));
|
|
75
81
|
socket.on('error', this.handleError.bind(this));
|
|
82
|
+
socket.on('message', (message) => {
|
|
83
|
+
try {
|
|
84
|
+
const parsed = JSON.parse(message);
|
|
85
|
+
this.handleMessage(socket, parsed);
|
|
86
|
+
} catch (error) {
|
|
87
|
+
console.error(`Error parsing message from ${ip}:`, error);
|
|
88
|
+
socket.sendJson({ error: 'Invalid JSON format' });
|
|
89
|
+
socket.close();
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
});
|
|
76
93
|
}
|
|
77
94
|
|
|
78
95
|
connectionOpenCallback(socket) {
|
|
@@ -85,7 +102,13 @@ class SocketRoute {
|
|
|
85
102
|
sendJson(sock, {error: `No such handler ${data.type}`});
|
|
86
103
|
sock.close();
|
|
87
104
|
} else {
|
|
88
|
-
|
|
105
|
+
try {
|
|
106
|
+
handler.handleMessage(sock, data);
|
|
107
|
+
} catch (error) {
|
|
108
|
+
console.error(`Error handling message in handler ${handler.name}:`, error);
|
|
109
|
+
sendJson(sock, { error: `${error.message}` });
|
|
110
|
+
sock.close();
|
|
111
|
+
}
|
|
89
112
|
}
|
|
90
113
|
}
|
|
91
114
|
|