redweb 0.7.0 β 0.7.2
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 +185 -23
- package/index.d.ts +141 -66
- package/index.js +3 -1
- package/package.json +1 -1
- package/src/ws/BaseSocketServer.js +47 -42
- package/src/ws/SocketRegistry.js +37 -0
- package/src/ws/SocketRoute.js +16 -3
- package/src/ws/SocketService.js +33 -0
- package/src/ws/index.js +2 -0
package/README.md
CHANGED
|
@@ -1,7 +1,3 @@
|
|
|
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
|
-
|
|
5
1
|
# RedWeb
|
|
6
2
|
|
|
7
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.
|
|
@@ -21,8 +17,8 @@ npm install redweb
|
|
|
21
17
|
```js
|
|
22
18
|
const { HttpServer, SocketServer } = require('redweb');
|
|
23
19
|
|
|
24
|
-
|
|
25
|
-
|
|
20
|
+
new HttpServer(); // serves public/ by default
|
|
21
|
+
new SocketServer(); // starts WS on :3000
|
|
26
22
|
```
|
|
27
23
|
|
|
28
24
|
---
|
|
@@ -49,8 +45,7 @@ new HttpServer({
|
|
|
49
45
|
});
|
|
50
46
|
```
|
|
51
47
|
|
|
52
|
-
`.htmx` files under `public/` will render server-side.
|
|
53
|
-
Example:
|
|
48
|
+
`.htmx` files under `public/` will render server-side. Example:
|
|
54
49
|
|
|
55
50
|
```html
|
|
56
51
|
<!-- public/hello.htmx -->
|
|
@@ -75,7 +70,6 @@ class ChatHandler extends BaseHandler {
|
|
|
75
70
|
|
|
76
71
|
onMessage(socket, message) {
|
|
77
72
|
const text = message.text;
|
|
78
|
-
console.log(`Broadcasting: ${text}`);
|
|
79
73
|
socket.broadcast({ type: 'chat', text });
|
|
80
74
|
}
|
|
81
75
|
}
|
|
@@ -96,7 +90,7 @@ class ChatRoute extends SocketRoute {
|
|
|
96
90
|
super({
|
|
97
91
|
path: '/chat',
|
|
98
92
|
handlers: [ChatHandler],
|
|
99
|
-
allowDuplicateConnections: true
|
|
93
|
+
allowDuplicateConnections: true
|
|
100
94
|
});
|
|
101
95
|
}
|
|
102
96
|
}
|
|
@@ -149,13 +143,163 @@ new SocketServer({
|
|
|
149
143
|
</html>
|
|
150
144
|
```
|
|
151
145
|
|
|
152
|
-
Open multiple tabs to test
|
|
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
|
+
}
|
|
180
|
+
}
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
---
|
|
184
|
+
|
|
185
|
+
### `SocketService` (NEW)
|
|
186
|
+
|
|
187
|
+
Socket services run alongside handlers on a route. Use for timers, logic, cleanup.
|
|
188
|
+
|
|
189
|
+
```js
|
|
190
|
+
class MatchService extends SocketService {
|
|
191
|
+
constructor() {
|
|
192
|
+
super('match', 1000); // tick every 1s
|
|
193
|
+
}
|
|
194
|
+
|
|
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
|
+
}
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
### π¦ `SocketRegistry` (NEW) β Event-Driven Socket Object Store
|
|
210
|
+
|
|
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.
|
|
212
|
+
|
|
213
|
+
Useful for managing players, NPCs, chat members, rooms, etc.
|
|
214
|
+
|
|
215
|
+
---
|
|
216
|
+
|
|
217
|
+
### π§ Basic Usage
|
|
218
|
+
|
|
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
|
+
}
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
---
|
|
239
|
+
|
|
240
|
+
### π Extending `SocketRegistry` to Create a Player Registry
|
|
241
|
+
|
|
242
|
+
```js
|
|
243
|
+
class PlayerRegistry extends SocketRegistry {
|
|
244
|
+
create(socket, id) {
|
|
245
|
+
return new Player(socket, id);
|
|
246
|
+
}
|
|
247
|
+
|
|
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;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
broadcastToAll(message) {
|
|
262
|
+
this.items.forEach(player => player.send(message.type, message));
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
```
|
|
153
266
|
|
|
154
267
|
---
|
|
155
268
|
|
|
156
|
-
|
|
269
|
+
### π£ Built-in Events
|
|
157
270
|
|
|
158
|
-
|
|
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
|
+
});
|
|
283
|
+
```
|
|
284
|
+
|
|
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
|
|
159
303
|
|
|
160
304
|
| Option | Type | Default | Description |
|
|
161
305
|
| --------------------- | --------- | -------------- | ------------------------------ |
|
|
@@ -170,20 +314,38 @@ Open multiple tabs to test!
|
|
|
170
314
|
|
|
171
315
|
### WebSocket Server Options
|
|
172
316
|
|
|
173
|
-
| Option
|
|
174
|
-
|
|
|
175
|
-
| `port`
|
|
176
|
-
| `routes`
|
|
177
|
-
|
|
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:
|
|
178
326
|
|
|
179
327
|
---
|
|
180
328
|
|
|
181
|
-
##
|
|
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
|
|
339
|
+
|
|
340
|
+
* Includes `.add()`, `.remove()`, `.getById()`, `.broadcast()`
|
|
341
|
+
* Fully compatible with custom socket wrappers and `EventEmitter`
|
|
342
|
+
|
|
343
|
+
## 0.7.0 Update Highlights
|
|
182
344
|
|
|
183
|
-
*
|
|
184
|
-
*
|
|
185
|
-
*
|
|
186
|
-
*
|
|
345
|
+
* allowDuplicateConnections for multi-tab testing
|
|
346
|
+
* Robust message validation
|
|
347
|
+
* socket.broadcast() now excludes sender
|
|
348
|
+
* Better error handling
|
|
187
349
|
|
|
188
350
|
---
|
|
189
351
|
|
package/index.d.ts
CHANGED
|
@@ -3,96 +3,171 @@ declare module 'redweb' {
|
|
|
3
3
|
import { CorsOptions } from 'cors';
|
|
4
4
|
import { Server as HttpServer } from 'http';
|
|
5
5
|
import { WebSocket } from 'ws';
|
|
6
|
-
|
|
6
|
+
|
|
7
|
+
/** βββββββββββββββββββ HTTP / CORE βββββββββββββββββββ */
|
|
8
|
+
|
|
7
9
|
export type RedWebEncoding = 'json' | 'urlencoded';
|
|
8
|
-
|
|
10
|
+
|
|
9
11
|
export interface RedWebOptions {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
12
|
+
port?: number;
|
|
13
|
+
bind?: string;
|
|
14
|
+
publicPaths?: string[];
|
|
15
|
+
services?: Array<{ serviceName: string; method: string; function: Function }>;
|
|
16
|
+
listenCallback?: () => void;
|
|
17
|
+
encoding?: RedWebEncoding;
|
|
18
|
+
ssl?: { key: string; cert: string };
|
|
19
|
+
server?: Application;
|
|
20
|
+
corsOptions?: CorsOptions;
|
|
21
|
+
enableHtmxRendering?: boolean;
|
|
20
22
|
}
|
|
21
|
-
|
|
23
|
+
|
|
24
|
+
/** βββββββββββββββββββ SOCKET SERVER βββββββββββββββββββ */
|
|
25
|
+
|
|
22
26
|
export interface SocketServerOptions {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
+
server?: HttpServer;
|
|
28
|
+
port?: number;
|
|
29
|
+
routes?: Array<new () => SocketRoute>;
|
|
30
|
+
ssl?: { key: string; cert: string };
|
|
27
31
|
}
|
|
28
|
-
|
|
32
|
+
|
|
33
|
+
/** βββββββββββββββββββ ROUTES & HANDLERS βββββββββββββββββββ */
|
|
34
|
+
|
|
29
35
|
export interface SocketRouteConfig {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
36
|
+
path: string;
|
|
37
|
+
handlers: Array<new () => BaseHandler>;
|
|
38
|
+
services?: Array<new () => SocketService>;
|
|
39
|
+
allowDuplicateConnections?: boolean;
|
|
33
40
|
}
|
|
34
|
-
|
|
41
|
+
|
|
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;
|
|
47
|
+
|
|
48
|
+
constructor(name: string, tickRateMs?: number);
|
|
49
|
+
|
|
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;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Message handler, triggered by client messages */
|
|
35
61
|
export class BaseHandler {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
62
|
+
name: string;
|
|
63
|
+
constructor(name: string);
|
|
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;
|
|
74
|
+
onInitialContact(socket: WebSocket): void;
|
|
47
75
|
}
|
|
48
|
-
|
|
76
|
+
|
|
49
77
|
export class SocketRoute {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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;
|
|
57
87
|
}
|
|
58
|
-
|
|
88
|
+
|
|
89
|
+
/** βββββββββββββββββββ SERVER BASE βββββββββββββββββββ */
|
|
90
|
+
|
|
59
91
|
export class BaseSocketServer {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
92
|
+
clients: Map<string, WebSocket>;
|
|
93
|
+
server: HttpServer;
|
|
94
|
+
routes: SocketRoute[];
|
|
95
|
+
|
|
96
|
+
constructor(server: HttpServer, options?: SocketServerOptions);
|
|
97
|
+
|
|
98
|
+
addRoute(route: new () => SocketRoute): void;
|
|
99
|
+
}
|
|
100
|
+
|
|
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>;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export type SocketMessage = {
|
|
111
|
+
type: string;
|
|
112
|
+
[key: string]: any;
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
/** Generic eventβdriven registry for socket objects */
|
|
116
|
+
/** Generic event-driven registry for socket objects */
|
|
117
|
+
export class SocketRegistry<T extends SocketWrapper = SocketWrapper> extends EventEmitter {
|
|
118
|
+
protected items: T[];
|
|
119
|
+
|
|
120
|
+
constructor();
|
|
121
|
+
|
|
122
|
+
/** Adds a socket-bound object to the registry */
|
|
123
|
+
add(item: T): void;
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Removes a socket-bound object by reference or id (default key: 'id')
|
|
127
|
+
* @param itemOrId Object or ID string
|
|
128
|
+
* @param by Key name to match against (default is 'id')
|
|
129
|
+
*/
|
|
130
|
+
remove(itemOrId: T | string, by?: keyof T): boolean;
|
|
131
|
+
|
|
132
|
+
/** Returns a shallow copy of all registered items */
|
|
133
|
+
all(): T[];
|
|
134
|
+
|
|
135
|
+
/** Returns the number of registered items */
|
|
136
|
+
count(): number;
|
|
65
137
|
}
|
|
66
|
-
|
|
138
|
+
|
|
139
|
+
/** βββββββββββββββββββ CONCRETE SERVERS βββββββββββββββββββ */
|
|
140
|
+
|
|
67
141
|
export class SocketServer extends BaseSocketServer {
|
|
68
|
-
|
|
142
|
+
constructor(options?: SocketServerOptions);
|
|
69
143
|
}
|
|
70
|
-
|
|
144
|
+
|
|
71
145
|
export class SecureSocketServer extends BaseSocketServer {
|
|
72
|
-
|
|
146
|
+
constructor(options?: SocketServerOptions);
|
|
73
147
|
}
|
|
74
|
-
|
|
148
|
+
|
|
75
149
|
export class HttpServer {
|
|
76
|
-
|
|
150
|
+
constructor(options?: RedWebOptions);
|
|
77
151
|
}
|
|
78
|
-
|
|
152
|
+
|
|
79
153
|
export class HttpsServer {
|
|
80
|
-
|
|
154
|
+
constructor(options?: RedWebOptions);
|
|
81
155
|
}
|
|
82
|
-
|
|
156
|
+
|
|
157
|
+
/** βββββββββββββββββββ CONSTANTS βββββββββββββββββββ */
|
|
158
|
+
|
|
83
159
|
export const METHODS: {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
160
|
+
GET: 'get';
|
|
161
|
+
POST: 'post';
|
|
162
|
+
PUT: 'put';
|
|
163
|
+
DELETE: 'delete';
|
|
88
164
|
};
|
|
89
|
-
|
|
165
|
+
|
|
90
166
|
export const ENCODINGS: {
|
|
91
|
-
|
|
92
|
-
|
|
167
|
+
json: 'json';
|
|
168
|
+
urlencoded: 'urlencoded';
|
|
93
169
|
};
|
|
94
|
-
|
|
170
|
+
|
|
95
171
|
export const HTTP_OPTIONS: RedWebOptions;
|
|
96
172
|
export const SOCKET_OPTIONS: SocketServerOptions;
|
|
97
|
-
|
|
98
|
-
|
|
173
|
+
}
|
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,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
|
@@ -13,8 +13,9 @@ class SocketRoute {
|
|
|
13
13
|
* @param {string} options.path - The path of the WebSocket route (e.g., `/chat`, `/lobby`).
|
|
14
14
|
* @param {boolean} options.allowDuplicateConnections - Whether to allow multiple connections from the same client IP address.
|
|
15
15
|
* @param {import('./BaseHandler').BaseHandler[]} options.handlers - An array of handler instances that manage connections and messages for this route.
|
|
16
|
-
|
|
17
|
-
|
|
16
|
+
* @param {Array<new () => SocketService>} [options.services]
|
|
17
|
+
*/
|
|
18
|
+
constructor({ path, handlers, services = [], allowDuplicateConnections } = {}) {
|
|
18
19
|
if (!path) {
|
|
19
20
|
throw new Error('A `path` must be specified for the SocketRoute.');
|
|
20
21
|
}
|
|
@@ -37,6 +38,13 @@ class SocketRoute {
|
|
|
37
38
|
this.server = new WebSocketServer({ noServer: true, path });
|
|
38
39
|
this.server.on('connection', this.handleConnection.bind(this));
|
|
39
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
|
+
});
|
|
40
48
|
}
|
|
41
49
|
/**
|
|
42
50
|
* Adds a new handler to the WebSocket server.
|
|
@@ -99,7 +107,7 @@ class SocketRoute {
|
|
|
99
107
|
handleMessage(sock, data) {
|
|
100
108
|
const handler = this.handlers.find((handler) => handler.name == data.type);
|
|
101
109
|
if (!handler) {
|
|
102
|
-
sendJson(sock, {error: `No such handler ${data.type}`});
|
|
110
|
+
sendJson(sock, { error: `No such handler ${data.type}` });
|
|
103
111
|
sock.close();
|
|
104
112
|
} else {
|
|
105
113
|
try {
|
|
@@ -123,6 +131,11 @@ class SocketRoute {
|
|
|
123
131
|
if (this.connectionCloseCallback) this.connectionCloseCallback(socket);
|
|
124
132
|
}
|
|
125
133
|
|
|
134
|
+
shutdown() {
|
|
135
|
+
this.services.forEach(svc => svc.onShutdown && svc.onShutdown());
|
|
136
|
+
this.server.close();
|
|
137
|
+
}
|
|
138
|
+
|
|
126
139
|
/**
|
|
127
140
|
* Handles socket errors.
|
|
128
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
|
}
|