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 +267 -152
- package/index.d.ts +111 -116
- package/index.js +3 -1
- package/package.json +1 -1
- package/src/ws/BaseHandler.js +2 -2
- package/src/ws/BaseSocketServer.js +47 -42
- package/src/ws/SocketRegistry.js +37 -0
- package/src/ws/SocketRoute.js +50 -14
- package/src/ws/SocketService.js +33 -0
- package/src/ws/index.js +2 -0
package/README.md
CHANGED
|
@@ -1,239 +1,354 @@
|
|
|
1
1
|
# RedWeb
|
|
2
2
|
|
|
3
|
-
RedWeb is a
|
|
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
|
-
|
|
5
|
+
---
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
## π¦ Installation
|
|
8
8
|
|
|
9
9
|
```bash
|
|
10
10
|
npm install redweb
|
|
11
11
|
```
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
---
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
## π Quick Start
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
```javascript
|
|
17
|
+
```js
|
|
20
18
|
const { HttpServer, SocketServer } = require('redweb');
|
|
21
19
|
|
|
22
|
-
//
|
|
23
|
-
|
|
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
|
-
|
|
30
|
-
|
|
31
|
-
#### HTTP Server with HTMX Rendering
|
|
24
|
+
---
|
|
32
25
|
|
|
33
|
-
|
|
26
|
+
## π HTTP Server Example (HTMX Support)
|
|
34
27
|
|
|
35
|
-
```
|
|
28
|
+
```js
|
|
36
29
|
const { HttpServer, METHODS } = require('redweb');
|
|
37
30
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
}
|
|
54
|
-
]
|
|
55
|
-
};
|
|
56
|
-
|
|
57
|
-
const app = new HttpServer(options);
|
|
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
|
-
|
|
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
|
-
|
|
53
|
+
<h1>Hello, {{name}}!</h1>
|
|
70
54
|
<@/>
|
|
71
55
|
```
|
|
72
56
|
|
|
73
|
-
|
|
74
|
-
```html
|
|
75
|
-
<h1>Hello, RedWeb User!</h1>
|
|
76
|
-
```
|
|
77
|
-
|
|
78
|
-
#### HTTPS Server
|
|
57
|
+
---
|
|
79
58
|
|
|
80
|
-
|
|
81
|
-
const { HttpsServer } = require('redweb');
|
|
59
|
+
## π WebSocket Broadcast Chat (π₯ Instant Testing)
|
|
82
60
|
|
|
83
|
-
|
|
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
|
-
|
|
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
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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
|
-
|
|
80
|
+
---
|
|
122
81
|
|
|
123
|
-
|
|
82
|
+
### 2. `ChatRoute.js`
|
|
124
83
|
|
|
125
|
-
```
|
|
84
|
+
```js
|
|
126
85
|
const { SocketRoute } = require('redweb');
|
|
127
86
|
const ChatHandler = require('./ChatHandler');
|
|
128
87
|
|
|
129
88
|
class ChatRoute extends SocketRoute {
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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
|
-
|
|
101
|
+
---
|
|
102
|
+
|
|
103
|
+
### 3. `server.js`
|
|
142
104
|
|
|
143
|
-
```
|
|
105
|
+
```js
|
|
144
106
|
const { SocketServer } = require('redweb');
|
|
145
107
|
const ChatRoute = require('./ChatRoute');
|
|
146
108
|
|
|
147
109
|
new SocketServer({
|
|
148
|
-
|
|
149
|
-
|
|
110
|
+
port: 3000,
|
|
111
|
+
routes: [ChatRoute]
|
|
150
112
|
});
|
|
151
113
|
```
|
|
152
114
|
|
|
153
|
-
|
|
115
|
+
---
|
|
154
116
|
|
|
155
|
-
|
|
117
|
+
### 4. `client.html`
|
|
156
118
|
|
|
157
|
-
```
|
|
158
|
-
|
|
159
|
-
|
|
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
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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
|
-
|
|
189
|
+
```js
|
|
190
|
+
class MatchService extends SocketService {
|
|
191
|
+
constructor() {
|
|
192
|
+
super('match', 1000); // tick every 1s
|
|
193
|
+
}
|
|
171
194
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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
|
-
###
|
|
209
|
+
### π¦ `SocketRegistry` (NEW) β Event-Driven Socket Object Store
|
|
178
210
|
|
|
179
|
-
|
|
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
|
-
|
|
182
|
-
const WebSocket = require('ws');
|
|
213
|
+
Useful for managing players, NPCs, chat members, rooms, etc.
|
|
183
214
|
|
|
184
|
-
|
|
215
|
+
---
|
|
185
216
|
|
|
186
|
-
|
|
187
|
-
ws.send(JSON.stringify({ type: 'chat', text: 'Hello there!' }));
|
|
188
|
-
});
|
|
217
|
+
### π§ Basic Usage
|
|
189
218
|
|
|
190
|
-
|
|
191
|
-
|
|
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
|
-
|
|
238
|
+
---
|
|
196
239
|
|
|
197
|
-
|
|
240
|
+
### π Extending `SocketRegistry` to Create a Player Registry
|
|
198
241
|
|
|
199
|
-
```
|
|
200
|
-
|
|
242
|
+
```js
|
|
243
|
+
class PlayerRegistry extends SocketRegistry {
|
|
244
|
+
create(socket, id) {
|
|
245
|
+
return new Player(socket, id);
|
|
246
|
+
}
|
|
201
247
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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
|
-
|
|
211
|
-
|
|
261
|
+
broadcastToAll(message) {
|
|
262
|
+
this.items.forEach(player => player.send(message.type, message));
|
|
212
263
|
}
|
|
213
264
|
}
|
|
265
|
+
```
|
|
214
266
|
|
|
215
|
-
|
|
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
|
-
|
|
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
|
+
|
|
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
|
-
|
|
340
|
+
* Includes `.add()`, `.remove()`, `.getById()`, `.broadcast()`
|
|
341
|
+
* Fully compatible with custom socket wrappers and `EventEmitter`
|
|
221
342
|
|
|
222
|
-
|
|
223
|
-
- **bind**: Bind address (default: `0.0.0.0`).
|
|
224
|
-
- **publicPaths**: Array of paths to serve static files (default: `['./public']`).
|
|
225
|
-
- **services**: Array of services with endpoints and handlers (default: `[]`).
|
|
226
|
-
- **listenCallback**: Function to execute once the server starts listening.
|
|
227
|
-
- **encoding**: Encoding type for request bodies (`'json'` or `'urlencoded'`).
|
|
228
|
-
- **ssl**: SSL configuration for HTTPS server (`{ key: './path/to/key.pem', cert: './path/to/cert.pem' }`).
|
|
229
|
-
- **enableHtmxRendering**: Enable dynamic rendering of `.htmx` files (default: `false`).
|
|
343
|
+
## 0.7.0 Update Highlights
|
|
230
344
|
|
|
231
|
-
|
|
345
|
+
* allowDuplicateConnections for multi-tab testing
|
|
346
|
+
* Robust message validation
|
|
347
|
+
* socket.broadcast() now excludes sender
|
|
348
|
+
* Better error handling
|
|
232
349
|
|
|
233
|
-
|
|
234
|
-
- **routes**: Array of `SocketRoute` classes to define WebSocket routes and handlers.
|
|
235
|
-
- **ssl**: SSL configuration for SecureSocketServer (`{ key: './path/to/key.pem', cert: './path/to/cert.pem' }`).
|
|
350
|
+
---
|
|
236
351
|
|
|
237
|
-
## License
|
|
352
|
+
## πͺͺ License
|
|
238
353
|
|
|
239
|
-
MIT
|
|
354
|
+
MIT
|
package/index.d.ts
CHANGED
|
@@ -1,177 +1,172 @@
|
|
|
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
|
-
*/
|
|
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
|
-
|
|
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?:
|
|
15
|
+
services?: Array<{ serviceName: string; method: string; function: Function }>;
|
|
29
16
|
listenCallback?: () => void;
|
|
30
17
|
encoding?: RedWebEncoding;
|
|
31
|
-
ssl?: {
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
|
|
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
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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(
|
|
48
|
+
constructor(name: string, tickRateMs?: number);
|
|
83
49
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
110
|
-
|
|
96
|
+
constructor(server: HttpServer, options?: SocketServerOptions);
|
|
97
|
+
|
|
111
98
|
addRoute(route: new () => SocketRoute): void;
|
|
112
99
|
}
|
|
113
100
|
|
|
114
|
-
/**
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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
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!";
|
|
@@ -1,57 +1,62 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @typedef {Object} SocketServerOptions
|
|
3
|
-
* @property {import('http').Server} [server]
|
|
4
|
-
* @property {number}
|
|
5
|
-
* @property {Array<new () => import('./SocketRoute').SocketRoute>} [routes]
|
|
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
|
-
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
port: 3000,
|
|
12
|
+
ssl: null,
|
|
13
|
+
routes: []
|
|
14
14
|
};
|
|
15
15
|
|
|
16
16
|
/**
|
|
17
|
-
*
|
|
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
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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;
|
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,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
|
-
|
|
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.
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
|
|
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
|
}
|