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 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 simple and flexible Node.js framework built on top of Express.js and WebSocket. It allows you to quickly set up web servers and WebSocket servers with customizable options.
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
- ## Installation
9
+ ---
6
10
 
7
- To install RedWeb, use npm:
11
+ ## 📦 Installation
8
12
 
9
13
  ```bash
10
14
  npm install redweb
11
15
  ```
12
16
 
13
- ## Usage
14
-
15
- ### Basic Example
17
+ ---
16
18
 
17
- Initialize your RedWeb instance with the default options:
19
+ ## 🚀 Quick Start
18
20
 
19
- ```javascript
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
- ### Custom Configuration
28
+ ---
30
29
 
31
- #### HTTP Server
30
+ ## 🌐 HTTP Server Example (HTMX Support)
32
31
 
33
- ```javascript
32
+ ```js
34
33
  const { HttpServer, METHODS } = require('redweb');
35
34
 
36
- const services = [
35
+ new HttpServer({
36
+ port: 3000,
37
+ publicPaths: ['./public'],
38
+ enableHtmxRendering: true,
39
+ services: [
37
40
  {
38
- serviceName: '/submit-form',
39
- method: METHODS.POST,
40
- function: (req, res) => {
41
- const { name, email, message } = req.body;
42
- if (!name || !email || !message) {
43
- return res.status(400).json({ error: 'All fields are required' });
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
- #### HTTPS Server
61
-
62
- ```javascript
63
- const { HttpsServer } = require('redweb');
52
+ `.htmx` files under `public/` will render server-side.
53
+ Example:
64
54
 
65
- const options = {
66
- port: 3443,
67
- ssl: {
68
- key: './path/to/key.pem',
69
- cert: './path/to/cert.pem'
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
- ### WebSocket Server with Routes and Handlers
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
- #### Defining a Custom Handler
64
+ ## 🔌 WebSocket Broadcast Chat (🔥 Instant Testing)
82
65
 
83
- Handlers extend the `BaseHandler` class and manage their own connections and message types.
66
+ ### 1. `ChatHandler.js`
84
67
 
85
- ```javascript
68
+ ```js
86
69
  const { BaseHandler } = require('redweb');
87
70
 
88
71
  class ChatHandler extends BaseHandler {
89
- constructor() {
90
- super('chat');
91
- }
92
-
93
- onMessage(socket, message) {
94
- console.log(`Received chat message: ${message.text}`);
95
- socket.send(JSON.stringify({ type: 'chatResponse', message: 'Hello!' }));
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
- #### Defining a WebSocket Route
86
+ ---
103
87
 
104
- Routes group handlers and specify the WebSocket path.
88
+ ### 2. `ChatRoute.js`
105
89
 
106
- ```javascript
90
+ ```js
107
91
  const { SocketRoute } = require('redweb');
108
92
  const ChatHandler = require('./ChatHandler');
109
93
 
110
94
  class ChatRoute extends SocketRoute {
111
- constructor() {
112
- super({
113
- path: '/chat',
114
- handlers: [ChatHandler]
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
- #### Setting Up a WebSocket Server with Routes
107
+ ---
108
+
109
+ ### 3. `server.js`
123
110
 
124
- ```javascript
111
+ ```js
125
112
  const { SocketServer } = require('redweb');
126
113
  const ChatRoute = require('./ChatRoute');
127
114
 
128
115
  new SocketServer({
129
- port: 3000,
130
- routes: [ChatRoute]
116
+ port: 3000,
117
+ routes: [ChatRoute]
131
118
  });
132
119
  ```
133
120
 
134
- ### Adding Routes Dynamically
135
-
136
- Routes can be added to the WebSocket server after initialization.
137
-
138
- ```javascript
139
- const { SocketServer, SocketRoute } = require('redweb');
140
- const ChatHandler = require('./ChatHandler');
141
-
142
- class ChatRoute extends SocketRoute {
143
- constructor() {
144
- super({
145
- path: '/chat',
146
- handlers: [ChatHandler]
147
- });
148
- }
149
- }
150
-
151
- const socketServer = new SocketServer({ port: 3000 });
152
-
153
- // Dynamically add a new route
154
- const chatRoute = new ChatRoute();
155
- socketServer.routes.push(chatRoute);
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
- ### Client Communication with a Route
159
-
160
- The client connects to the WebSocket server using the specified route.
152
+ Open multiple tabs to test!
161
153
 
162
- ```javascript
163
- const WebSocket = require('ws');
154
+ ---
164
155
 
165
- const ws = new WebSocket('ws://localhost:3000/chat');
156
+ ## 🔧 Options
166
157
 
167
- ws.on('open', () => {
168
- ws.send(JSON.stringify({ type: 'chat', text: 'Hello there!' }));
169
- });
158
+ ### HTTP / HTTPS Server Options
170
159
 
171
- ws.on('message', (message) => {
172
- console.log('Received:', message);
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
- ### Managing Connected Clients
169
+ ---
177
170
 
178
- RedWeb's WebSocket server maintains a list of connected clients by their IP addresses for each route. This list is automatically updated when clients connect or disconnect.
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
- ## Options
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
- ### HttpServer and HttpsServer Options
179
+ ---
202
180
 
203
- - **port**: Port number (default: `80`).
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
- ### SocketServer Options
183
+ * `allowDuplicateConnections` for multi-tab testing
184
+ * ✅ Robust message validation
185
+ * ✅ `socket.broadcast()` now excludes sender
186
+ * ✅ Better error handling
212
187
 
213
- - **port**: Port number (default: `3000`).
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 License
192
+ MIT
package/index.d.ts CHANGED
@@ -1,255 +1,98 @@
1
1
  declare module 'redweb' {
2
- import { Request, Response } from 'express';
3
- import { Server as HTTPServer } from 'http';
4
- import { Server as HTTPSServer } from 'https';
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
- port?: number;
26
- bind?: string;
27
- publicPaths?: string[];
28
- services?: Service[];
29
- listenCallback?: () => void;
30
- encoding?: RedWebEncoding;
31
- ssl?: {
32
- key: string;
33
- cert: string;
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
- port?: number;
52
- connectionOpenCallback?: (socket: WebSocket) => void;
53
- connectionCloseCallback?: (socket: WebSocket) => void;
54
- messageCallback?: (socket: WebSocket, message: string) => void;
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
- path: string; // The WebSocket route path (e.g., "/chat").
70
- handlers: Array<new () => BaseHandler>; // Array of handler classes for the route.
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
- * The name of the handler (used to identify it in the server).
133
- */
134
- name: string;
135
- /**
136
- * Creates a new handler instance.
137
- * @param name - The name of the handler.
138
- */
139
- constructor(name: string);
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
- * Base WebSocket server class.
163
- */
164
- export class BaseSocketServer {
165
- /**
166
- * List of WebSocket routes.
167
- */
168
- routes: SocketRoute[];
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
- * HTTPS server class.
201
- */
202
- export class HttpsServer {
203
- constructor(options?: RedWebOptions);
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
- constructor(options?: SocketServerOptions);
68
+ constructor(options?: SocketServerOptions);
211
69
  }
212
-
213
- /**
214
- * Secure WebSocket server class.
215
- */
70
+
216
71
  export class SecureSocketServer extends BaseSocketServer {
217
- constructor(options?: SocketServerOptions);
72
+ constructor(options?: SocketServerOptions);
218
73
  }
219
-
220
- /**
221
- * SSL configuration loader.
222
- */
223
- export function loadSslConfig(sslOptions: { key: string; cert: string }): { key: string; cert: string };
224
-
225
- /**
226
- * Constants for encoding types.
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
- POST: 'post';
238
- GET: 'get';
239
- PUT: 'put';
240
- DELETE: 'delete';
241
- PATCH: 'patch';
242
- OPTIONS: 'options';
243
- HEAD: 'head';
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "redweb",
3
- "version": "0.6.8",
3
+ "version": "0.7.0",
4
4
  "description": "A way to quickly set up an express server",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -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;
@@ -0,0 +1,11 @@
1
+ class RedWebHtmxComponent {
2
+ constructor(props = {}) {
3
+ this.props = props;
4
+ }
5
+
6
+ render() {
7
+ throw new Error('Render method must be implemented in derived components');
8
+ }
9
+ }
10
+
11
+ module.exports = RedWebHtmxComponent;
@@ -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
- this.publicPaths.forEach(public_path => this.app.use(express.static(path.join(process.cwd(), public_path))));
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 => this.app[service.method](service.serviceName, service.function));
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
  };
@@ -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 {string} message - The incoming message in JSON string format.
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 {string} message - The incoming message in JSON string format.
28
+ * @param {any} message - The incoming message in parsed JSON.
29
29
  */
30
30
  onMessage(socket, message) {
31
31
  throw "Not yet implemented!";
@@ -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.clients.get(ip) !== undefined) {
60
- const oldClient = this.clients.get(ip);
61
- console.warn(`Client ${ip} already connected, disconnecting existing connection.`);
62
- oldClient.send(
63
- JSON.stringify({ msg: 'You are being disconnected because a new client is connected with your IP address.' })
64
- );
65
- oldClient.close();
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
- handler.handleMessage(sock, data);
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