redweb 0.6.8 → 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 +119 -146
- package/index.d.ts +78 -235
- package/package.json +1 -1
- package/src/htmx/HtmxRenderer.js +65 -0
- package/src/htmx/RedWebHtmxComponent.js +11 -0
- package/src/http/BaseHttpServer.js +39 -3
- package/src/ws/BaseHandler.js +2 -2
- package/src/ws/SocketRoute.js +35 -12
package/README.md
CHANGED
|
@@ -1,219 +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
|
-
|
|
28
|
+
---
|
|
30
29
|
|
|
31
|
-
|
|
30
|
+
## 🌐 HTTP Server Example (HTMX Support)
|
|
32
31
|
|
|
33
|
-
```
|
|
32
|
+
```js
|
|
34
33
|
const { HttpServer, METHODS } = require('redweb');
|
|
35
34
|
|
|
36
|
-
|
|
35
|
+
new HttpServer({
|
|
36
|
+
port: 3000,
|
|
37
|
+
publicPaths: ['./public'],
|
|
38
|
+
enableHtmxRendering: true,
|
|
39
|
+
services: [
|
|
37
40
|
{
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
}
|
|
45
|
-
res.status(200).json({ success: 'Form submitted successfully' });
|
|
46
|
-
}
|
|
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
47
|
}
|
|
48
|
-
]
|
|
49
|
-
|
|
50
|
-
const options = {
|
|
51
|
-
port: 3000,
|
|
52
|
-
publicPaths: ['./public'],
|
|
53
|
-
encoding: 'urlencoded',
|
|
54
|
-
services: services
|
|
55
|
-
};
|
|
56
|
-
|
|
57
|
-
const app = new HttpServer(options);
|
|
48
|
+
]
|
|
49
|
+
});
|
|
58
50
|
```
|
|
59
51
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
```javascript
|
|
63
|
-
const { HttpsServer } = require('redweb');
|
|
52
|
+
`.htmx` files under `public/` will render server-side.
|
|
53
|
+
Example:
|
|
64
54
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
},
|
|
71
|
-
publicPaths: ['./public']
|
|
72
|
-
};
|
|
73
|
-
|
|
74
|
-
const app = new HttpsServer(options);
|
|
55
|
+
```html
|
|
56
|
+
<!-- public/hello.htmx -->
|
|
57
|
+
<@>
|
|
58
|
+
<h1>Hello, {{name}}!</h1>
|
|
59
|
+
<@/>
|
|
75
60
|
```
|
|
76
61
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
RedWeb uses **route-based architecture** for WebSocket connections, allowing you to modularize and secure your WebSocket message handling logic.
|
|
62
|
+
---
|
|
80
63
|
|
|
81
|
-
|
|
64
|
+
## 🔌 WebSocket Broadcast Chat (🔥 Instant Testing)
|
|
82
65
|
|
|
83
|
-
|
|
66
|
+
### 1. `ChatHandler.js`
|
|
84
67
|
|
|
85
|
-
```
|
|
68
|
+
```js
|
|
86
69
|
const { BaseHandler } = require('redweb');
|
|
87
70
|
|
|
88
71
|
class ChatHandler extends BaseHandler {
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
}
|
|
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
|
+
}
|
|
97
81
|
}
|
|
98
82
|
|
|
99
83
|
module.exports = ChatHandler;
|
|
100
84
|
```
|
|
101
85
|
|
|
102
|
-
|
|
86
|
+
---
|
|
103
87
|
|
|
104
|
-
|
|
88
|
+
### 2. `ChatRoute.js`
|
|
105
89
|
|
|
106
|
-
```
|
|
90
|
+
```js
|
|
107
91
|
const { SocketRoute } = require('redweb');
|
|
108
92
|
const ChatHandler = require('./ChatHandler');
|
|
109
93
|
|
|
110
94
|
class ChatRoute extends SocketRoute {
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
}
|
|
95
|
+
constructor() {
|
|
96
|
+
super({
|
|
97
|
+
path: '/chat',
|
|
98
|
+
handlers: [ChatHandler],
|
|
99
|
+
allowDuplicateConnections: true // key for local testing!
|
|
100
|
+
});
|
|
101
|
+
}
|
|
117
102
|
}
|
|
118
103
|
|
|
119
104
|
module.exports = ChatRoute;
|
|
120
105
|
```
|
|
121
106
|
|
|
122
|
-
|
|
107
|
+
---
|
|
108
|
+
|
|
109
|
+
### 3. `server.js`
|
|
123
110
|
|
|
124
|
-
```
|
|
111
|
+
```js
|
|
125
112
|
const { SocketServer } = require('redweb');
|
|
126
113
|
const ChatRoute = require('./ChatRoute');
|
|
127
114
|
|
|
128
115
|
new SocketServer({
|
|
129
|
-
|
|
130
|
-
|
|
116
|
+
port: 3000,
|
|
117
|
+
routes: [ChatRoute]
|
|
131
118
|
});
|
|
132
119
|
```
|
|
133
120
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
```
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
121
|
+
---
|
|
122
|
+
|
|
123
|
+
### 4. `client.html`
|
|
124
|
+
|
|
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>
|
|
156
150
|
```
|
|
157
151
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
The client connects to the WebSocket server using the specified route.
|
|
152
|
+
Open multiple tabs to test!
|
|
161
153
|
|
|
162
|
-
|
|
163
|
-
const WebSocket = require('ws');
|
|
154
|
+
---
|
|
164
155
|
|
|
165
|
-
|
|
156
|
+
## 🔧 Options
|
|
166
157
|
|
|
167
|
-
|
|
168
|
-
ws.send(JSON.stringify({ type: 'chat', text: 'Hello there!' }));
|
|
169
|
-
});
|
|
158
|
+
### HTTP / HTTPS Server Options
|
|
170
159
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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` |
|
|
175
168
|
|
|
176
|
-
|
|
169
|
+
---
|
|
177
170
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
```javascript
|
|
181
|
-
const { SocketRoute } = require('redweb');
|
|
182
|
-
|
|
183
|
-
class ChatRoute extends SocketRoute {
|
|
184
|
-
constructor() {
|
|
185
|
-
super({
|
|
186
|
-
path: '/chat',
|
|
187
|
-
handlers: []
|
|
188
|
-
});
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
onConnection(socket) {
|
|
192
|
-
console.log('New client connected:', socket.remoteAddress);
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
module.exports = ChatRoute;
|
|
197
|
-
```
|
|
171
|
+
### WebSocket Server Options
|
|
198
172
|
|
|
199
|
-
|
|
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 |
|
|
200
178
|
|
|
201
|
-
|
|
179
|
+
---
|
|
202
180
|
|
|
203
|
-
|
|
204
|
-
- **bind**: Bind address (default: `0.0.0.0`).
|
|
205
|
-
- **publicPaths**: Array of paths to serve static files (default: `['./public']`).
|
|
206
|
-
- **services**: Array of services with endpoints and handlers (default: `[]`).
|
|
207
|
-
- **listenCallback**: Function to execute once the server starts listening.
|
|
208
|
-
- **encoding**: Encoding type for request bodies (`'json'` or `'urlencoded'`).
|
|
209
|
-
- **ssl**: SSL configuration for HTTPS server (`{ key: './path/to/key.pem', cert: './path/to/cert.pem' }`).
|
|
181
|
+
## 🆕 0.7.0 Update Highlights
|
|
210
182
|
|
|
211
|
-
|
|
183
|
+
* ✅ `allowDuplicateConnections` for multi-tab testing
|
|
184
|
+
* ✅ Robust message validation
|
|
185
|
+
* ✅ `socket.broadcast()` now excludes sender
|
|
186
|
+
* ✅ Better error handling
|
|
212
187
|
|
|
213
|
-
|
|
214
|
-
- **routes**: Array of `SocketRoute` classes to define WebSocket routes and handlers.
|
|
215
|
-
- **ssl**: SSL configuration for SecureSocketServer (`{ key: './path/to/key.pem', cert: './path/to/cert.pem' }`).
|
|
188
|
+
---
|
|
216
189
|
|
|
217
|
-
## License
|
|
190
|
+
## 🪪 License
|
|
218
191
|
|
|
219
|
-
MIT
|
|
192
|
+
MIT
|
package/index.d.ts
CHANGED
|
@@ -1,255 +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
|
-
}
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* Configuration for a WebSocket handler.
|
|
39
|
-
*/
|
|
40
|
-
export interface HandlerConfig {
|
|
41
|
-
name: string;
|
|
42
|
-
handlers: {
|
|
43
|
-
[type: string]: (socket: WebSocket, data: any) => void;
|
|
44
|
-
};
|
|
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;
|
|
45
20
|
}
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* Options for configuring a WebSocket server.
|
|
49
|
-
*/
|
|
21
|
+
|
|
50
22
|
export interface SocketServerOptions {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
messageHandlers?: {
|
|
56
|
-
[type: string]: (socket: WebSocket, data: any) => void;
|
|
57
|
-
};
|
|
58
|
-
ssl?: {
|
|
59
|
-
key: string;
|
|
60
|
-
cert: string;
|
|
61
|
-
};
|
|
62
|
-
routes?: Array<new () => SocketRoute>;
|
|
23
|
+
server?: HttpServer;
|
|
24
|
+
port?: number;
|
|
25
|
+
routes?: Array<new () => SocketRoute>;
|
|
26
|
+
ssl?: { key: string; cert: string };
|
|
63
27
|
}
|
|
64
|
-
|
|
65
|
-
/**
|
|
66
|
-
* WebSocket route configuration.
|
|
67
|
-
*/
|
|
28
|
+
|
|
68
29
|
export interface SocketRouteConfig {
|
|
69
|
-
|
|
70
|
-
|
|
30
|
+
path: string;
|
|
31
|
+
handlers: Array<new () => BaseHandler>;
|
|
32
|
+
allowDuplicateConnections?: boolean;
|
|
71
33
|
}
|
|
72
|
-
|
|
73
|
-
/**
|
|
74
|
-
* Represents a WebSocket route.
|
|
75
|
-
*/
|
|
76
|
-
export class SocketRoute {
|
|
77
|
-
/**
|
|
78
|
-
* The path for the WebSocket route.
|
|
79
|
-
*/
|
|
80
|
-
path: string;
|
|
81
|
-
|
|
82
|
-
/**
|
|
83
|
-
* Handlers associated with the route.
|
|
84
|
-
*/
|
|
85
|
-
handlers: BaseHandler[];
|
|
86
|
-
|
|
87
|
-
/**
|
|
88
|
-
* Creates a new `SocketRoute` instance.
|
|
89
|
-
* @param config - Configuration options for the route.
|
|
90
|
-
*/
|
|
91
|
-
constructor(config: SocketRouteConfig);
|
|
92
|
-
|
|
93
|
-
/**
|
|
94
|
-
* Adds a new handler dynamically.
|
|
95
|
-
* @param HandlerClass - A class extending `BaseHandler`.
|
|
96
|
-
*/
|
|
97
|
-
addHandler(HandlerClass: new () => BaseHandler): void;
|
|
98
|
-
|
|
99
|
-
/**
|
|
100
|
-
* Handles a new WebSocket connection.
|
|
101
|
-
* @param socket - The WebSocket connection instance.
|
|
102
|
-
* @param req - The HTTP request associated with the connection.
|
|
103
|
-
*/
|
|
104
|
-
handleConnection(socket: WebSocket, req: import('http').IncomingMessage): void;
|
|
105
|
-
|
|
106
|
-
/**
|
|
107
|
-
* Handles incoming WebSocket messages.
|
|
108
|
-
* @param socket - The WebSocket connection instance.
|
|
109
|
-
* @param data - The message data.
|
|
110
|
-
*/
|
|
111
|
-
handleMessage(socket: WebSocket, data: any): void;
|
|
112
|
-
|
|
113
|
-
/**
|
|
114
|
-
* Handles WebSocket disconnections.
|
|
115
|
-
* @param socket - The WebSocket connection instance.
|
|
116
|
-
*/
|
|
117
|
-
handleClose(socket: WebSocket): void;
|
|
118
|
-
|
|
119
|
-
/**
|
|
120
|
-
* Handles WebSocket errors.
|
|
121
|
-
* @param socket - The WebSocket connection instance.
|
|
122
|
-
* @param error - The error object.
|
|
123
|
-
*/
|
|
124
|
-
handleError(socket: WebSocket, error: Error): void;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
/**
|
|
128
|
-
* Base class for WebSocket handlers.
|
|
129
|
-
*/
|
|
34
|
+
|
|
130
35
|
export class BaseHandler {
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
* Handles an incoming message.
|
|
143
|
-
* @param socket - The WebSocket connection that sent the message.
|
|
144
|
-
* @param message - The message data.
|
|
145
|
-
*/
|
|
146
|
-
onMessage(socket: WebSocket, message: Object): void;
|
|
147
|
-
|
|
148
|
-
/**
|
|
149
|
-
* Called during the first contact with a new WebSocket connection.
|
|
150
|
-
* @param socket - The WebSocket connection instance.
|
|
151
|
-
*/
|
|
152
|
-
onInitialContact(socket: WebSocket): void;
|
|
153
|
-
|
|
154
|
-
/**
|
|
155
|
-
* Called when a WebSocket connection closes.
|
|
156
|
-
* @param socket - The WebSocket connection instance.
|
|
157
|
-
*/
|
|
158
|
-
onClose(socket: WebSocket): void;
|
|
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;
|
|
159
47
|
}
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
/**
|
|
171
|
-
* Creates a new `BaseSocketServer`.
|
|
172
|
-
* @param server - The HTTP server instance.
|
|
173
|
-
* @param options - Configuration options.
|
|
174
|
-
*/
|
|
175
|
-
constructor(server: HTTPServer | HTTPSServer, options?: SocketServerOptions);
|
|
176
|
-
|
|
177
|
-
/**
|
|
178
|
-
* Handles WebSocket upgrade requests.
|
|
179
|
-
* @param req - The incoming HTTP upgrade request.
|
|
180
|
-
* @param socket - The raw network socket.
|
|
181
|
-
* @param head - The initial data chunk.
|
|
182
|
-
*/
|
|
183
|
-
handleUpgrade(req: import('http').IncomingMessage, socket: import('net').Socket, head: Buffer): void;
|
|
184
|
-
|
|
185
|
-
/**
|
|
186
|
-
*
|
|
187
|
-
* @param route
|
|
188
|
-
*/
|
|
189
|
-
addRoute(route: new () => SocketRoute);
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
/**
|
|
193
|
-
* HTTP server class.
|
|
194
|
-
*/
|
|
195
|
-
export class HttpServer {
|
|
196
|
-
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;
|
|
197
57
|
}
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
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;
|
|
204
65
|
}
|
|
205
|
-
|
|
206
|
-
/**
|
|
207
|
-
* WebSocket server class.
|
|
208
|
-
*/
|
|
66
|
+
|
|
209
67
|
export class SocketServer extends BaseSocketServer {
|
|
210
|
-
|
|
68
|
+
constructor(options?: SocketServerOptions);
|
|
211
69
|
}
|
|
212
|
-
|
|
213
|
-
/**
|
|
214
|
-
* Secure WebSocket server class.
|
|
215
|
-
*/
|
|
70
|
+
|
|
216
71
|
export class SecureSocketServer extends BaseSocketServer {
|
|
217
|
-
|
|
72
|
+
constructor(options?: SocketServerOptions);
|
|
218
73
|
}
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
export const ENCODINGS: {
|
|
229
|
-
json: 'json';
|
|
230
|
-
urlencoded: 'urlencoded';
|
|
231
|
-
};
|
|
232
|
-
|
|
233
|
-
/**
|
|
234
|
-
* Constants for HTTP methods.
|
|
235
|
-
*/
|
|
74
|
+
|
|
75
|
+
export class HttpServer {
|
|
76
|
+
constructor(options?: RedWebOptions);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export class HttpsServer {
|
|
80
|
+
constructor(options?: RedWebOptions);
|
|
81
|
+
}
|
|
82
|
+
|
|
236
83
|
export const METHODS: {
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
84
|
+
GET: 'get';
|
|
85
|
+
POST: 'post';
|
|
86
|
+
PUT: 'put';
|
|
87
|
+
DELETE: 'delete';
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
export const ENCODINGS: {
|
|
91
|
+
json: 'json';
|
|
92
|
+
urlencoded: 'urlencoded';
|
|
244
93
|
};
|
|
245
|
-
|
|
246
|
-
/**
|
|
247
|
-
* Default HTTP options.
|
|
248
|
-
*/
|
|
94
|
+
|
|
249
95
|
export const HTTP_OPTIONS: RedWebOptions;
|
|
250
|
-
|
|
251
|
-
/**
|
|
252
|
-
* Default socket options.
|
|
253
|
-
*/
|
|
254
96
|
export const SOCKET_OPTIONS: SocketServerOptions;
|
|
255
|
-
}
|
|
97
|
+
}
|
|
98
|
+
|
package/package.json
CHANGED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const vm = require('vm');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
|
|
5
|
+
class HtmxRenderer {
|
|
6
|
+
/**
|
|
7
|
+
* Render an .htmx file as JavaScript with embedded print statements.
|
|
8
|
+
* @param {string} filePath - Path to the .htmx file.
|
|
9
|
+
* @returns {string} Rendered HTML string with normalized whitespace.
|
|
10
|
+
*/
|
|
11
|
+
static render(filePath) {
|
|
12
|
+
if (!fs.existsSync(filePath)) {
|
|
13
|
+
throw new Error(`Template file not found: ${filePath}`);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
let output = '';
|
|
17
|
+
const templateContent = fs.readFileSync(filePath, 'utf-8');
|
|
18
|
+
|
|
19
|
+
// Transform <@ ... @/> blocks into print() calls
|
|
20
|
+
const transformedTemplate = templateContent.replace(
|
|
21
|
+
/<@>([\s\S]*?)<@\/>/g,
|
|
22
|
+
(_, content) => `print(\`${content.replace(/{{\s*(.*?)\s*}}/g, '${$1}')}\`);`
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
// Wrap the script in an IIFE
|
|
26
|
+
const wrappedScript = `
|
|
27
|
+
(() => {
|
|
28
|
+
const print = (html) => output += html;
|
|
29
|
+
${transformedTemplate}
|
|
30
|
+
return output;
|
|
31
|
+
})();
|
|
32
|
+
`;
|
|
33
|
+
|
|
34
|
+
// Create a custom require function that resolves paths relative to the template
|
|
35
|
+
const customRequire = (modulePath) => {
|
|
36
|
+
const absolutePath = path.resolve(path.dirname(filePath), modulePath);
|
|
37
|
+
return require(absolutePath);
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
// Execute the script in a sandbox
|
|
41
|
+
const script = new vm.Script(wrappedScript);
|
|
42
|
+
const sandbox = {
|
|
43
|
+
output: '',
|
|
44
|
+
require: customRequire, // Add custom require
|
|
45
|
+
__dirname: path.dirname(filePath),
|
|
46
|
+
__filename: filePath,
|
|
47
|
+
};
|
|
48
|
+
vm.createContext(sandbox);
|
|
49
|
+
|
|
50
|
+
// Get the rendered output
|
|
51
|
+
let result = script.runInContext(sandbox);
|
|
52
|
+
|
|
53
|
+
// Normalize spaces but preserve those in content
|
|
54
|
+
result = result
|
|
55
|
+
.replace(/>\s+</g, '><') // Remove spaces between tags
|
|
56
|
+
.replace(/\s+/g, ' ') // Collapse multiple spaces to one
|
|
57
|
+
.replace(/>\s+/g, '>') // Remove spaces after tags
|
|
58
|
+
.replace(/\s+</g, '<') // Remove spaces before tags
|
|
59
|
+
.trim(); // Trim leading and trailing spaces
|
|
60
|
+
|
|
61
|
+
return result;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
module.exports = HtmxRenderer;
|
|
@@ -2,6 +2,8 @@ const express = require('express');
|
|
|
2
2
|
const bodyParser = require('body-parser');
|
|
3
3
|
const path = require('path');
|
|
4
4
|
const cors = require('cors');
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
const HtmxRenderer = require('../htmx/HtmxRenderer'); // Import the HtmxRenderer module
|
|
5
7
|
|
|
6
8
|
/**
|
|
7
9
|
* @typedef {'json' | 'urlencoded'} RedWebEncoding
|
|
@@ -21,6 +23,7 @@ const cors = require('cors');
|
|
|
21
23
|
* @property {string} [ssl.cert] - Path to the SSL certificate file.
|
|
22
24
|
* @property {import('express').Application} [server] - Whether to automatically start listening.
|
|
23
25
|
* @property {import('cors').CorsOptions} [corsOptions] - The CORS Options.
|
|
26
|
+
* @property {boolean} [enableHtmxRendering=false] - Enable dynamic HTMX file rendering.
|
|
24
27
|
*/
|
|
25
28
|
|
|
26
29
|
const ENCODINGS = { json: 'json', urlencoded: 'urlencoded' };
|
|
@@ -34,6 +37,7 @@ const HTTP_OPTIONS = {
|
|
|
34
37
|
ssl: null,
|
|
35
38
|
server: undefined,
|
|
36
39
|
corsOptions: undefined,
|
|
40
|
+
enableHtmxRendering: false, // New option for HTMX rendering
|
|
37
41
|
};
|
|
38
42
|
|
|
39
43
|
/**
|
|
@@ -52,18 +56,50 @@ function BaseHttpServer(options = {}) {
|
|
|
52
56
|
} else if (this.encoding === ENCODINGS.urlencoded) {
|
|
53
57
|
this.app.use(bodyParser.urlencoded({ extended: true }));
|
|
54
58
|
}
|
|
59
|
+
|
|
55
60
|
this.app.use(cors(this.options.corsOptions));
|
|
56
|
-
|
|
61
|
+
|
|
62
|
+
// Enable HTMX rendering if the flag is set
|
|
63
|
+
if (this.enableHtmxRendering) {
|
|
64
|
+
this.app.get('*.htmx', (req, res) => {
|
|
65
|
+
// Find the file in one of the publicPaths
|
|
66
|
+
const filePath = this.publicPaths
|
|
67
|
+
.map(publicPath => path.join(process.cwd(), publicPath, req.path))
|
|
68
|
+
.find(fullPath => fs.existsSync(fullPath)); // Check if the file exists
|
|
69
|
+
|
|
70
|
+
if (!filePath) {
|
|
71
|
+
return res.status(404).send(`Error rendering HTMX file: Template file not found: ${req.path}`);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
try {
|
|
75
|
+
const renderedContent = HtmxRenderer.render(filePath);
|
|
76
|
+
res.type('html').send(renderedContent);
|
|
77
|
+
} catch (error) {
|
|
78
|
+
res.status(500).send(`Error rendering HTMX file: ${error.message}`);
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
// Serve static files from public paths
|
|
85
|
+
this.publicPaths.forEach((publicPath) =>
|
|
86
|
+
this.app.use(express.static(path.join(process.cwd(), publicPath)))
|
|
87
|
+
);
|
|
88
|
+
|
|
57
89
|
const catchAll = this.services.find((service) => service.serviceName === '*');
|
|
58
90
|
if (catchAll) this.services.splice(this.services.indexOf(catchAll), 1);
|
|
59
|
-
this.services.forEach(service =>
|
|
91
|
+
this.services.forEach((service) =>
|
|
92
|
+
this.app[service.method](service.serviceName, service.function)
|
|
93
|
+
);
|
|
60
94
|
if (catchAll) this.app[catchAll.method](catchAll.serviceName, catchAll.function);
|
|
95
|
+
|
|
61
96
|
return this;
|
|
62
97
|
}
|
|
63
98
|
|
|
99
|
+
|
|
64
100
|
module.exports = {
|
|
65
101
|
BaseHttpServer,
|
|
66
102
|
ENCODINGS,
|
|
67
103
|
HTTP_OPTIONS,
|
|
68
|
-
METHODS: {GET: 'get', POST: 'post', PUT: 'put', DELETE: 'delete'}
|
|
104
|
+
METHODS: { GET: 'get', POST: 'post', PUT: 'put', DELETE: 'delete' },
|
|
69
105
|
};
|
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
|
|