redweb 0.6.9 β†’ 0.7.1

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,239 +1,354 @@
1
1
  # RedWeb
2
2
 
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.
3
+ **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
4
 
5
- ## Installation
5
+ ---
6
6
 
7
- To install RedWeb, use npm:
7
+ ## πŸ“¦ Installation
8
8
 
9
9
  ```bash
10
10
  npm install redweb
11
11
  ```
12
12
 
13
- ## Usage
13
+ ---
14
14
 
15
- ### Basic Example
15
+ ## πŸš€ Quick Start
16
16
 
17
- Initialize your RedWeb instance with the default options:
18
-
19
- ```javascript
17
+ ```js
20
18
  const { HttpServer, SocketServer } = require('redweb');
21
19
 
22
- // HTTP server with default configuration
23
- const httpServer = new HttpServer();
24
-
25
- // WebSocket server with default configuration
26
- const socketServer = new SocketServer();
20
+ new HttpServer(); // serves public/ by default
21
+ new SocketServer(); // starts WS on :3000
27
22
  ```
28
23
 
29
- ### Custom Configuration
30
-
31
- #### HTTP Server with HTMX Rendering
24
+ ---
32
25
 
33
- RedWeb now supports dynamic rendering of `.htmx` files using `enableHtmxRendering`.
26
+ ## 🌐 HTTP Server Example (HTMX Support)
34
27
 
35
- ```javascript
28
+ ```js
36
29
  const { HttpServer, METHODS } = require('redweb');
37
30
 
38
- const options = {
39
- port: 3000,
40
- publicPaths: ['./public'],
41
- enableHtmxRendering: true, // Enable .htmx rendering
42
- services: [
43
- {
44
- serviceName: '/submit-form',
45
- method: METHODS.POST,
46
- function: (req, res) => {
47
- const { name, email, message } = req.body;
48
- if (!name || !email || !message) {
49
- return res.status(400).json({ error: 'All fields are required' });
50
- }
51
- res.status(200).json({ success: 'Form submitted successfully' });
52
- }
53
- }
54
- ]
55
- };
56
-
57
- const app = new HttpServer(options);
31
+ new HttpServer({
32
+ port: 3000,
33
+ publicPaths: ['./public'],
34
+ enableHtmxRendering: true,
35
+ services: [
36
+ {
37
+ serviceName: '/submit',
38
+ method: METHODS.POST,
39
+ function: (req, res) => {
40
+ if (!req.body.name) return res.status(400).json({ error: 'Missing name' });
41
+ res.status(200).json({ message: `Thanks, ${req.body.name}!` });
42
+ }
43
+ }
44
+ ]
45
+ });
58
46
  ```
59
47
 
60
- Place `.htmx` files in the specified `publicPaths`, and they will be dynamically rendered.
61
-
62
- Example:
63
-
64
- **File: `public/index.htmx`**
65
- ```javascript
66
- const name = 'RedWeb User';
48
+ `.htmx` files under `public/` will render server-side. Example:
67
49
 
50
+ ```html
51
+ <!-- public/hello.htmx -->
68
52
  <@>
69
- <h1>Hello, {{name}}!</h1>
53
+ <h1>Hello, {{name}}!</h1>
70
54
  <@/>
71
55
  ```
72
56
 
73
- Accessing `/index.htmx` will render:
74
- ```html
75
- <h1>Hello, RedWeb User!</h1>
76
- ```
77
-
78
- #### HTTPS Server
57
+ ---
79
58
 
80
- ```javascript
81
- const { HttpsServer } = require('redweb');
59
+ ## πŸ”Œ WebSocket Broadcast Chat (πŸ”₯ Instant Testing)
82
60
 
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
- };
61
+ ### 1. `ChatHandler.js`
92
62
 
93
- const app = new HttpsServer(options);
94
- ```
95
-
96
- ### WebSocket Server with Routes and Handlers
97
-
98
- RedWeb uses **route-based architecture** for WebSocket connections, allowing you to modularize and secure your WebSocket message handling logic.
99
-
100
- #### Defining a Custom Handler
101
-
102
- Handlers extend the `BaseHandler` class and manage their own connections and message types.
103
-
104
- ```javascript
63
+ ```js
105
64
  const { BaseHandler } = require('redweb');
106
65
 
107
66
  class ChatHandler extends BaseHandler {
108
- constructor() {
109
- super('chat');
110
- }
111
-
112
- onMessage(socket, message) {
113
- console.log(`Received chat message: ${message.text}`);
114
- socket.send(JSON.stringify({ type: 'chatResponse', message: 'Hello!' }));
115
- }
67
+ constructor() {
68
+ super('chat');
69
+ }
70
+
71
+ onMessage(socket, message) {
72
+ const text = message.text;
73
+ socket.broadcast({ type: 'chat', text });
74
+ }
116
75
  }
117
76
 
118
77
  module.exports = ChatHandler;
119
78
  ```
120
79
 
121
- #### Defining a WebSocket Route
80
+ ---
122
81
 
123
- Routes group handlers and specify the WebSocket path.
82
+ ### 2. `ChatRoute.js`
124
83
 
125
- ```javascript
84
+ ```js
126
85
  const { SocketRoute } = require('redweb');
127
86
  const ChatHandler = require('./ChatHandler');
128
87
 
129
88
  class ChatRoute extends SocketRoute {
130
- constructor() {
131
- super({
132
- path: '/chat',
133
- handlers: [ChatHandler]
134
- });
135
- }
89
+ constructor() {
90
+ super({
91
+ path: '/chat',
92
+ handlers: [ChatHandler],
93
+ allowDuplicateConnections: true
94
+ });
95
+ }
136
96
  }
137
97
 
138
98
  module.exports = ChatRoute;
139
99
  ```
140
100
 
141
- #### Setting Up a WebSocket Server with Routes
101
+ ---
102
+
103
+ ### 3. `server.js`
142
104
 
143
- ```javascript
105
+ ```js
144
106
  const { SocketServer } = require('redweb');
145
107
  const ChatRoute = require('./ChatRoute');
146
108
 
147
109
  new SocketServer({
148
- port: 3000,
149
- routes: [ChatRoute]
110
+ port: 3000,
111
+ routes: [ChatRoute]
150
112
  });
151
113
  ```
152
114
 
153
- ### Adding Routes Dynamically
115
+ ---
154
116
 
155
- Routes can be added to the WebSocket server after initialization.
117
+ ### 4. `client.html`
156
118
 
157
- ```javascript
158
- const { SocketServer, SocketRoute } = require('redweb');
159
- const ChatHandler = require('./ChatHandler');
119
+ ```html
120
+ <!DOCTYPE html>
121
+ <html>
122
+ <body>
123
+ <h1>Broadcast Chat</h1>
124
+ <input id="msg" placeholder="Type message..." />
125
+ <button onclick="send()">Send</button>
126
+ <pre id="log"></pre>
127
+
128
+ <script>
129
+ const log = document.getElementById('log');
130
+ const ws = new WebSocket('ws://localhost:3000/chat');
131
+
132
+ ws.onmessage = (e) => {
133
+ const msg = JSON.parse(e.data);
134
+ log.textContent += `\n${msg.text}`;
135
+ };
136
+
137
+ function send() {
138
+ const text = document.getElementById('msg').value;
139
+ ws.send(JSON.stringify({ type: 'chat', text }));
140
+ }
141
+ </script>
142
+ </body>
143
+ </html>
144
+ ```
160
145
 
161
- class ChatRoute extends SocketRoute {
162
- constructor() {
163
- super({
164
- path: '/chat',
165
- handlers: [ChatHandler]
166
- });
167
- }
146
+ Open multiple tabs to test.
147
+
148
+ ---
149
+
150
+ ## 🧩 Socket Architecture
151
+
152
+ ### `SocketRoute`
153
+
154
+ Defines a WebSocket path, handlers, and optional route-scoped services:
155
+
156
+ ```js
157
+ new SocketRoute({
158
+ path: '/game',
159
+ handlers: [ChatHandler, MoveHandler],
160
+ services: [MatchService], // βœ… Scoped only to this route
161
+ allowDuplicateConnections: true
162
+ });
163
+ ```
164
+
165
+ ---
166
+
167
+ ### `BaseHandler`
168
+
169
+ Handlers are message-type keyed classes:
170
+
171
+ ```js
172
+ class MoveHandler extends BaseHandler {
173
+ constructor() {
174
+ super('move');
175
+ }
176
+
177
+ onMessage(socket, message) {
178
+ // handle movement logic
179
+ }
168
180
  }
181
+ ```
182
+
183
+ ---
184
+
185
+ ### `SocketService` (NEW)
186
+
187
+ Socket services run alongside handlers on a route. Use for timers, logic, cleanup.
169
188
 
170
- const socketServer = new SocketServer({ port: 3000 });
189
+ ```js
190
+ class MatchService extends SocketService {
191
+ constructor() {
192
+ super('match', 1000); // tick every 1s
193
+ }
171
194
 
172
- // Dynamically add a new route
173
- const chatRoute = new ChatRoute();
174
- socketServer.routes.push(chatRoute);
195
+ onInit(route) {
196
+ route.registry.on('maxPlayersReached', () => this.startMatch());
197
+ }
198
+
199
+ onTick() {
200
+ // tick logic
201
+ }
202
+
203
+ onShutdown() {
204
+ // cleanup
205
+ }
206
+ }
175
207
  ```
176
208
 
177
- ### Client Communication with a Route
209
+ ### πŸ“¦ `SocketRegistry` (NEW) – Event-Driven Socket Object Store
178
210
 
179
- The client connects to the WebSocket server using the specified route.
211
+ `SocketRegistry` is a lightweight, extendable class for managing WebSocket-connected clients (or any socket-bound object). It provides add/remove/get/broadcast utilities with full `EventEmitter` support.
180
212
 
181
- ```javascript
182
- const WebSocket = require('ws');
213
+ Useful for managing players, NPCs, chat members, rooms, etc.
183
214
 
184
- const ws = new WebSocket('ws://localhost:3000/chat');
215
+ ---
185
216
 
186
- ws.on('open', () => {
187
- ws.send(JSON.stringify({ type: 'chat', text: 'Hello there!' }));
188
- });
217
+ ### πŸ”§ Basic Usage
189
218
 
190
- ws.on('message', (message) => {
191
- console.log('Received:', message);
192
- });
219
+ ```js
220
+ const { SocketRegistry } = require('redweb');
221
+
222
+ class Player {
223
+ constructor(socket, id) {
224
+ this.socket = socket;
225
+ this.id = id;
226
+ }
227
+
228
+ send(type, payload) {
229
+ this.socket.send(JSON.stringify({ type, ...payload }));
230
+ }
231
+
232
+ getSanitized() {
233
+ return { id: this.id };
234
+ }
235
+ }
193
236
  ```
194
237
 
195
- ### Managing Connected Clients
238
+ ---
196
239
 
197
- 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.
240
+ ### πŸš€ Extending `SocketRegistry` to Create a Player Registry
198
241
 
199
- ```javascript
200
- const { SocketRoute } = require('redweb');
242
+ ```js
243
+ class PlayerRegistry extends SocketRegistry {
244
+ create(socket, id) {
245
+ return new Player(socket, id);
246
+ }
201
247
 
202
- class ChatRoute extends SocketRoute {
203
- constructor() {
204
- super({
205
- path: '/chat',
206
- handlers: []
207
- });
248
+ addPlayer(socket, id) {
249
+ const player = this.create(socket, id);
250
+ const success = this.add(player);
251
+ if (success) this.emit('playerJoined', player);
252
+ return success;
253
+ }
254
+
255
+ removePlayer(id) {
256
+ const success = this.remove(id);
257
+ if (success) this.emit('playerLeft', id);
258
+ return success;
208
259
  }
209
260
 
210
- onConnection(socket) {
211
- console.log('New client connected:', socket.remoteAddress);
261
+ broadcastToAll(message) {
262
+ this.items.forEach(player => player.send(message.type, message));
212
263
  }
213
264
  }
265
+ ```
214
266
 
215
- module.exports = ChatRoute;
267
+ ---
268
+
269
+ ### πŸ“£ Built-in Events
270
+
271
+ You can listen to events:
272
+
273
+ ```js
274
+ const registry = new PlayerRegistry();
275
+
276
+ registry.on('playerJoined', player => {
277
+ console.log('New player:', player.id);
278
+ });
279
+
280
+ registry.on('playerLeft', id => {
281
+ console.log('Player left:', id);
282
+ });
216
283
  ```
217
284
 
218
- ## Options
285
+ ---
286
+
287
+ ### πŸ”„ Built-in Methods
288
+
289
+ * `add(player)`
290
+ * `remove(id)`
291
+ * `getById(id)`
292
+ * `getBySocket(socket)`
293
+ * `all()`
294
+ * `count()`
295
+ * `broadcast(message, excludeSocket?)`
296
+ * `getSanitizedList()`
297
+
298
+ ---
299
+
300
+ ## πŸ”§ Configuration
301
+
302
+ ### HTTP / HTTPS Options
303
+
304
+ | Option | Type | Default | Description |
305
+ | --------------------- | --------- | -------------- | ------------------------------ |
306
+ | `port` | number | `80` | Port to listen on |
307
+ | `bind` | string | `'0.0.0.0'` | Bind address |
308
+ | `publicPaths` | string\[] | `['./public']` | Serve static and `.htmx` files |
309
+ | `services` | object\[] | `[]` | REST endpoints |
310
+ | `enableHtmxRendering` | boolean | `false` | Enables `.htmx` file rendering |
311
+ | `ssl` | object | `undefined` | Used in `HttpsServer` |
312
+
313
+ ---
314
+
315
+ ### WebSocket Server Options
316
+
317
+ | Option | Type | Default | Description |
318
+ | -------- | --------------- | ------- | ---------------------------- |
319
+ | `port` | number | `3000` | WebSocket port |
320
+ | `routes` | `SocketRoute[]` | `[]` | List of custom route classes |
321
+
322
+ &nbsp;
323
+
324
+ # Changelog
325
+ Here’s the updated `CHANGELOG.md` entry for **RedWeb v0.7.1**, written professionally and focused only on the framework-level additions:
326
+
327
+ ---
328
+
329
+ ## πŸ“¦ RedWeb v0.7.1 – Socket Services & Registries
330
+
331
+ ### ✨ Added
332
+
333
+ * `SocketService`: A new class for running autonomous, lifecycle-aware logic alongside a `SocketRoute`. Ideal for game loops, timers, state machines, or server-side AI.
334
+
335
+ * Hooks: `onInit(route)`, `onTick()`, `onShutdown()`
336
+ * Optional `tickRateMs` support for periodic execution
337
+
338
+ * `SocketRegistry`: A generic, event-driven registry for managing WebSocket-bound entities
219
339
 
220
- ### HttpServer and HttpsServer Options
340
+ * Includes `.add()`, `.remove()`, `.getById()`, `.broadcast()`
341
+ * Fully compatible with custom socket wrappers and `EventEmitter`
221
342
 
222
- - **port**: Port number (default: `80`).
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`).
343
+ ## 0.7.0 Update Highlights
230
344
 
231
- ### SocketServer Options
345
+ * allowDuplicateConnections for multi-tab testing
346
+ * Robust message validation
347
+ * socket.broadcast() now excludes sender
348
+ * Better error handling
232
349
 
233
- - **port**: Port number (default: `3000`).
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' }`).
350
+ ---
236
351
 
237
- ## License
352
+ ## πŸͺͺ License
238
353
 
239
- MIT License
354
+ MIT
package/index.d.ts CHANGED
@@ -1,177 +1,172 @@
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
- */
10
- export type RedWebEncoding = 'json' | 'urlencoded';
2
+ import { Application } from 'express';
3
+ import { CorsOptions } from 'cors';
4
+ import { Server as HttpServer } from 'http';
5
+ import { WebSocket } from 'ws';
11
6
 
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
- }
7
+ /** ─────────────────── HTTP / CORE ─────────────────── */
8
+
9
+ export type RedWebEncoding = 'json' | 'urlencoded';
20
10
 
21
- /**
22
- * Options for configuring a RedWeb server.
23
- */
24
11
  export interface RedWebOptions {
25
12
  port?: number;
26
13
  bind?: string;
27
14
  publicPaths?: string[];
28
- services?: Service[];
15
+ services?: Array<{ serviceName: string; method: string; function: Function }>;
29
16
  listenCallback?: () => void;
30
17
  encoding?: RedWebEncoding;
31
- ssl?: {
32
- key: string;
33
- cert: string;
34
- };
35
- corsOptions?: import('cors').CorsOptions;
36
- enableHtmxRendering?: boolean; // New flag to enable HTMX rendering
18
+ ssl?: { key: string; cert: string };
19
+ server?: Application;
20
+ corsOptions?: CorsOptions;
21
+ enableHtmxRendering?: boolean;
37
22
  }
38
23
 
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
- };
47
- }
24
+ /** ─────────────────── SOCKET SERVER ─────────────────── */
48
25
 
49
- /**
50
- * Options for configuring a WebSocket server.
51
- */
52
26
  export interface SocketServerOptions {
27
+ server?: HttpServer;
53
28
  port?: number;
54
- connectionOpenCallback?: (socket: WebSocket) => void;
55
- connectionCloseCallback?: (socket: WebSocket) => void;
56
- messageCallback?: (socket: WebSocket, message: string) => void;
57
- messageHandlers?: {
58
- [type: string]: (socket: WebSocket, data: any) => void;
59
- };
60
- ssl?: {
61
- key: string;
62
- cert: string;
63
- };
64
29
  routes?: Array<new () => SocketRoute>;
30
+ ssl?: { key: string; cert: string };
65
31
  }
66
32
 
67
- /**
68
- * WebSocket route configuration.
69
- */
33
+ /** ─────────────────── ROUTES & HANDLERS ─────────────────── */
34
+
70
35
  export interface SocketRouteConfig {
71
36
  path: string;
72
37
  handlers: Array<new () => BaseHandler>;
38
+ services?: Array<new () => SocketService>;
39
+ allowDuplicateConnections?: boolean;
73
40
  }
74
41
 
75
- /**
76
- * Represents a WebSocket route.
77
- */
78
- export class SocketRoute {
79
- path: string;
80
- handlers: BaseHandler[];
42
+ /** Socket‑side autonomous service (game loops, timers, etc.) */
43
+ export abstract class SocketService {
44
+ name: string;
45
+ tickRateMs?: number;
46
+ protected _tickHandle?: NodeJS.Timeout;
81
47
 
82
- constructor(config: SocketRouteConfig);
48
+ constructor(name: string, tickRateMs?: number);
83
49
 
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;
50
+ /** Called once when the route is initialised */
51
+ onInit(route: SocketRoute): void;
52
+
53
+ /** Optional recurring tick (respecting tickRateMs) */
54
+ onTick?(): void;
55
+
56
+ /** Called on process shutdown / route removal */
57
+ onShutdown(): void;
89
58
  }
90
59
 
91
- /**
92
- * Base class for WebSocket handlers.
93
- */
60
+ /** Message handler, triggered by client messages */
94
61
  export class BaseHandler {
95
62
  name: string;
96
-
97
63
  constructor(name: string);
98
- onMessage(socket: WebSocket, message: Object): void;
64
+
65
+ handleMessage(
66
+ socket: WebSocket & {
67
+ sendJson: (message: object) => void;
68
+ broadcast: (message: object) => void;
69
+ },
70
+ message: any
71
+ ): void;
72
+
73
+ onMessage(socket: WebSocket, message: any): void;
99
74
  onInitialContact(socket: WebSocket): void;
100
- onClose(socket: WebSocket): void;
101
75
  }
102
76
 
103
- /**
104
- * Base WebSocket server class.
105
- */
77
+ export class SocketRoute {
78
+ path: string;
79
+ handlers: BaseHandler[];
80
+ clients: Map<string, WebSocket>;
81
+ allowDuplicateConnections?: boolean;
82
+
83
+ constructor(config: SocketRouteConfig);
84
+
85
+ addHandler(handler: new () => BaseHandler): void;
86
+ handleMessage(sock: WebSocket, data: any): void;
87
+ }
88
+
89
+ /** ─────────────────── SERVER BASE ─────────────────── */
90
+
106
91
  export class BaseSocketServer {
92
+ clients: Map<string, WebSocket>;
93
+ server: HttpServer;
107
94
  routes: SocketRoute[];
108
95
 
109
- constructor(server: HTTPServer | HTTPSServer, options?: SocketServerOptions);
110
- handleUpgrade(req: import('http').IncomingMessage, socket: import('net').Socket, head: Buffer): void;
96
+ constructor(server: HttpServer, options?: SocketServerOptions);
97
+
111
98
  addRoute(route: new () => SocketRoute): void;
112
99
  }
113
100
 
114
- /**
115
- * HTTP server class.
116
- */
117
- export class HttpServer {
118
- constructor(options?: RedWebOptions);
101
+ /** ─────────────────── REGISTRY & UTIL TYPES ─────────────────── */
102
+
103
+ export interface SocketWrapper {
104
+ socket: WebSocket;
105
+ id: string;
106
+ send: (type: string, payload: Record<string, any>) => void;
107
+ getSanitized?(): Record<string, any>;
119
108
  }
120
109
 
121
- /**
122
- * HTTPS server class.
123
- */
124
- export class HttpsServer {
125
- constructor(options?: RedWebOptions);
110
+ export type SocketMessage = {
111
+ type: string;
112
+ [key: string]: any;
113
+ };
114
+
115
+ /** Generic event‑driven registry for socket objects */
116
+ export class SocketRegistry<T extends SocketWrapper = SocketWrapper> {
117
+ protected clients: T[];
118
+
119
+ onCreate?: (client: T) => boolean | void;
120
+ onRemove?: (client: T) => boolean | void;
121
+
122
+ constructor();
123
+
124
+ setCreateValidator(fn: (client: T) => boolean | void): void;
125
+ setRemoveValidator(fn: (client: T) => boolean | void): void;
126
+
127
+ add(client: T): boolean;
128
+ removeById(id: string): boolean;
129
+
130
+ getById(id: string): T | undefined;
131
+ getBySocket(socket: WebSocket): T | undefined;
132
+ allWithout(socket: WebSocket): T[];
133
+
134
+ broadcast(message: SocketMessage, excludeSocket?: WebSocket | null): void;
135
+ getSanitizedList(): Record<string, any>[];
126
136
  }
127
137
 
128
- /**
129
- * WebSocket server class.
130
- */
138
+ /** ─────────────────── CONCRETE SERVERS ─────────────────── */
139
+
131
140
  export class SocketServer extends BaseSocketServer {
132
141
  constructor(options?: SocketServerOptions);
133
142
  }
134
143
 
135
- /**
136
- * Secure WebSocket server class.
137
- */
138
144
  export class SecureSocketServer extends BaseSocketServer {
139
145
  constructor(options?: SocketServerOptions);
140
146
  }
141
147
 
142
- /**
143
- * SSL configuration loader.
144
- */
145
- export function loadSslConfig(sslOptions: { key: string; cert: string }): { key: string; cert: string };
148
+ export class HttpServer {
149
+ constructor(options?: RedWebOptions);
150
+ }
146
151
 
147
- /**
148
- * Constants for encoding types.
149
- */
150
- export const ENCODINGS: {
151
- json: 'json';
152
- urlencoded: 'urlencoded';
153
- };
152
+ export class HttpsServer {
153
+ constructor(options?: RedWebOptions);
154
+ }
155
+
156
+ /** ─────────────────── CONSTANTS ─────────────────── */
154
157
 
155
- /**
156
- * Constants for HTTP methods.
157
- */
158
158
  export const METHODS: {
159
- POST: 'post';
160
159
  GET: 'get';
160
+ POST: 'post';
161
161
  PUT: 'put';
162
162
  DELETE: 'delete';
163
- PATCH: 'patch';
164
- OPTIONS: 'options';
165
- HEAD: 'head';
166
163
  };
167
164
 
168
- /**
169
- * Default HTTP options.
170
- */
171
- export const HTTP_OPTIONS: RedWebOptions;
165
+ export const ENCODINGS: {
166
+ json: 'json';
167
+ urlencoded: 'urlencoded';
168
+ };
172
169
 
173
- /**
174
- * Default socket options.
175
- */
170
+ export const HTTP_OPTIONS: RedWebOptions;
176
171
  export const SOCKET_OPTIONS: SocketServerOptions;
177
172
  }
package/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  const { METHODS } = require('./src/http');
2
2
  const { sendJson } = require('./src/ws/util');
3
- const { SocketServer, SecureSocketServer, SOCKET_OPTIONS, SocketRoute } = require('./src/ws');
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');
@@ -11,6 +11,8 @@ module.exports = {
11
11
  SecureSocketServer,
12
12
  BaseHandler,
13
13
  SocketRoute,
14
+ SocketService,
15
+ SocketRegistry,
14
16
  sendJson,
15
17
  SOCKET_OPTIONS,
16
18
  METHODS
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "redweb",
3
- "version": "0.6.9",
3
+ "version": "0.7.1",
4
4
  "description": "A way to quickly set up an express server",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -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,57 +1,62 @@
1
1
  /**
2
2
  * @typedef {Object} SocketServerOptions
3
- * @property {import('http').Server} [server] - The HTTP server instance to bind the WebSocket server to.
4
- * @property {number} [port=3000] - The port number for the WebSocket server.
5
- * @property {Array<new () => import('./SocketRoute').SocketRoute>} [routes] - An array of handler classes to use for routing.
3
+ * @property {import('http').Server} [server] HTTP server to bind to
4
+ * @property {number} [port=3000] Port to listen on
5
+ * @property {Array<new () => import('./SocketRoute').SocketRoute>} [routes]
6
6
  */
7
7
 
8
8
  const DefaultRoute = require('./DefaultRoute');
9
9
 
10
10
  const SOCKET_OPTIONS = {
11
- port: 3000,
12
- ssl: null,
13
- routes: []
11
+ port: 3000,
12
+ ssl: null,
13
+ routes: []
14
14
  };
15
15
 
16
16
  /**
17
- * Broadcasts a message to all connected clients.
18
- * @param {BaseSocketServer} socketServer - The WebSocket server instance.
19
- * @param {object} message - The message to broadcast.
20
- */
21
- function broadcast(socketServer, message) {
22
- socketServer.clients.forEach(client => client.send(JSON.stringify(message)));
23
- }
24
-
25
- /**
26
- * Represents the base WebSocket server.
17
+ * Base WebSocket server
27
18
  */
28
19
  class BaseSocketServer {
29
- /**
30
- * @param {import('http').Server} server - The HTTP server instance to bind the WebSocket server to.
31
- * @param {SocketServerOptions} options - The configuration options for the WebSocket server.
32
- */
33
- constructor(server, options = {}) {
34
- this.clients = new Map(); // Map of clients by their IP addresses.
35
- Object.assign(this, { ...SOCKET_OPTIONS, ...options });
36
- this.server = server;
37
- if (!options.routes?.length) options.routes = [ DefaultRoute ];
38
- this.routes = options.routes.map((route) => new route(server));
39
- this.server.on('upgrade', this.handleUpgrade.bind(this));
40
- }
41
-
42
- handleUpgrade(req, sock, head) {
43
- const route = this.routes.find(route => route.path == req.url);
44
- if (!route) sock.destroy();
45
- else route.server.handleUpgrade(req, sock, head, (s, r) => route.server.emit('connection', s, r));
46
- }
47
-
48
- /**
49
- *
50
- * @param {new () => import('./SocketRoute')} route
51
- */
52
- addRoute(route) {
53
- this.routes.push(new route(this.server));
54
- }
20
+ /**
21
+ * @param {import('http').Server} server
22
+ * @param {SocketServerOptions} [options]
23
+ */
24
+ constructor(server, options = {}) {
25
+ this.clients = new Map();
26
+ Object.assign(this, { ...SOCKET_OPTIONS, ...options });
27
+ this.server = server;
28
+
29
+ /* ─── ROUTE INITIALISATION ─────────────────────────── */
30
+ if (!options.routes?.length) options.routes = [DefaultRoute];
31
+ this.routes = options.routes.map(RouteClass => new RouteClass(server));
32
+
33
+ this.server.on('upgrade', this.handleUpgrade.bind(this));
34
+ }
35
+
36
+ handleUpgrade(req, sock, head) {
37
+ const route = this.routes.find(r => r.path === req.url);
38
+ if (!route) return sock.destroy();
39
+
40
+ route.server.handleUpgrade(req, sock, head, (s, r) =>
41
+ route.server.emit('connection', s, r)
42
+ );
43
+ }
44
+
45
+ /**
46
+ * Dynamically attach a new route at runtime
47
+ * @param {new () => import('./SocketRoute').SocketRoute} RouteClass
48
+ */
49
+ addRoute(RouteClass) {
50
+ this.routes.push(new RouteClass(this.server));
51
+ }
52
+
53
+ /**
54
+ * Gracefully tear down all routes (and their services)
55
+ */
56
+ shutdown() {
57
+ this.routes.forEach(route => route.shutdown?.());
58
+ this.server.close();
59
+ }
55
60
  }
56
61
 
57
62
  module.exports = { BaseSocketServer, SOCKET_OPTIONS };
@@ -0,0 +1,37 @@
1
+ // handlers/SocketRegistry.js
2
+ const { EventEmitter } = require("events");
3
+
4
+ class SocketRegistry extends EventEmitter {
5
+ constructor() {
6
+ super();
7
+ this.items = [];
8
+ }
9
+
10
+ add(item) {
11
+ this.items.push(item);
12
+ this.emit("added", item);
13
+ }
14
+
15
+ remove(itemOrId, by = "id") {
16
+ const idx = typeof itemOrId === "object"
17
+ ? this.items.findIndex(i => i === itemOrId)
18
+ : this.items.findIndex(i => i[by] === itemOrId);
19
+
20
+ if (idx !== -1) {
21
+ const [removed] = this.items.splice(idx, 1);
22
+ this.emit("removed", removed);
23
+ return true;
24
+ }
25
+ return false;
26
+ }
27
+
28
+ all() {
29
+ return [...this.items];
30
+ }
31
+
32
+ count() {
33
+ return this.all().length;
34
+ }
35
+ }
36
+
37
+ module.exports = SocketRegistry;
@@ -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,11 @@ 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
- */
15
- constructor({path, handlers } = {}) {
16
+ * @param {Array<new () => SocketService>} [options.services]
17
+ */
18
+ constructor({ path, handlers, services = [], allowDuplicateConnections } = {}) {
16
19
  if (!path) {
17
20
  throw new Error('A `path` must be specified for the SocketRoute.');
18
21
  }
@@ -34,6 +37,14 @@ class SocketRoute {
34
37
  this.clients = new Map();
35
38
  this.server = new WebSocketServer({ noServer: true, path });
36
39
  this.server.on('connection', this.handleConnection.bind(this));
40
+ this.allowDuplicateConnections = allowDuplicateConnections;
41
+
42
+ /* ─── ROUTE‑SCOPED SERVICES ─────────────────────────── */
43
+ this.services = services.map(SvcClass => {
44
+ const svc = new SvcClass();
45
+ if (typeof svc.onInit === 'function') svc.onInit(this);
46
+ return svc;
47
+ });
37
48
  }
38
49
  /**
39
50
  * Adds a new handler to the WebSocket server.
@@ -56,23 +67,37 @@ class SocketRoute {
56
67
  handleConnection(socket, req) {
57
68
  const ip = req.socket.remoteAddress;
58
69
  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();
70
+ if (this.allowDuplicateConnections) {
71
+ this.clients.set(randomUUID(), socket);
72
+ } else {
73
+ if (this.clients.get(ip) !== undefined) {
74
+ const oldClient = this.clients.get(ip);
75
+ console.warn(`Client ${ip} already connected, disconnecting existing connection.`);
76
+ oldClient.send(
77
+ JSON.stringify({ msg: 'You are being disconnected because a new client is connected with your IP address.' })
78
+ );
79
+ oldClient.close();
80
+ }
81
+ this.clients.set(ip, socket);
66
82
  }
67
- this.clients.set(ip, socket);
68
83
  socket.isAssigned = false; // Tracks whether the socket has been assigned a handler.
69
84
  socket.sendJson = (data) => sendJson(socket, data);
70
- socket.broadcast = (data) => broadcast([...this.clients.values()], data);
85
+ socket.broadcast = (data) => broadcast([...this.clients.values()].filter(sock => sock !== socket), data);
71
86
 
72
87
  this.connectionOpenCallback(socket);
73
- socket.on('message', (message) => this.handleMessage(socket, JSON.parse(message)));
74
88
  socket.on('close', this.handleClose.bind(this));
75
89
  socket.on('error', this.handleError.bind(this));
90
+ socket.on('message', (message) => {
91
+ try {
92
+ const parsed = JSON.parse(message);
93
+ this.handleMessage(socket, parsed);
94
+ } catch (error) {
95
+ console.error(`Error parsing message from ${ip}:`, error);
96
+ socket.sendJson({ error: 'Invalid JSON format' });
97
+ socket.close();
98
+ return;
99
+ }
100
+ });
76
101
  }
77
102
 
78
103
  connectionOpenCallback(socket) {
@@ -82,10 +107,16 @@ class SocketRoute {
82
107
  handleMessage(sock, data) {
83
108
  const handler = this.handlers.find((handler) => handler.name == data.type);
84
109
  if (!handler) {
85
- sendJson(sock, {error: `No such handler ${data.type}`});
110
+ sendJson(sock, { error: `No such handler ${data.type}` });
86
111
  sock.close();
87
112
  } else {
88
- handler.handleMessage(sock, data);
113
+ try {
114
+ handler.handleMessage(sock, data);
115
+ } catch (error) {
116
+ console.error(`Error handling message in handler ${handler.name}:`, error);
117
+ sendJson(sock, { error: `${error.message}` });
118
+ sock.close();
119
+ }
89
120
  }
90
121
  }
91
122
 
@@ -100,6 +131,11 @@ class SocketRoute {
100
131
  if (this.connectionCloseCallback) this.connectionCloseCallback(socket);
101
132
  }
102
133
 
134
+ shutdown() {
135
+ this.services.forEach(svc => svc.onShutdown && svc.onShutdown());
136
+ this.server.close();
137
+ }
138
+
103
139
  /**
104
140
  * Handles socket errors.
105
141
  * @param {WebSocket} socket - The WebSocket connection instance.
@@ -0,0 +1,33 @@
1
+ // handlers/SocketService.js
2
+ class SocketService {
3
+ /**
4
+ * @param {string} name – service identifier
5
+ * @param {?number} tickRateMs – optional tick interval (ms)
6
+ */
7
+ constructor(name, tickRateMs = null) {
8
+ this.name = name;
9
+ this.tickRateMs = tickRateMs;
10
+ this._tickHandle = null;
11
+ }
12
+
13
+ /**
14
+ * Called once by the owning SocketRoute.
15
+ * @param {import('../SocketRoute')} route – the route this service belongs to
16
+ */
17
+ onInit(route) {
18
+ this.route = route; // full access to route, clients, broadcast…
19
+ if (this.tickRateMs && this.onTick) {
20
+ this._tickHandle = setInterval(() => this.onTick(), this.tickRateMs);
21
+ }
22
+ }
23
+
24
+ /* Optional */
25
+ // onTick() {}
26
+
27
+ onShutdown() {
28
+ if (this._tickHandle) clearInterval(this._tickHandle);
29
+ }
30
+ }
31
+
32
+ module.exports = SocketService;
33
+
package/src/ws/index.js CHANGED
@@ -3,5 +3,7 @@ module.exports = {
3
3
  SecureSocketServer: require('./SecureSocketServer'),
4
4
  SocketServer: require('./SocketServer'),
5
5
  SocketRoute: require('./SocketRoute'),
6
+ SocketService: require('./SocketService'),
7
+ SocketRegistry: require('./SocketRegistry'),
6
8
  SOCKET_OPTIONS
7
9
  }