redweb 0.7.0 β†’ 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,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
- const httpServer = new HttpServer();
25
- const socketServer = new SocketServer();
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 // key for local testing!
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
- ## πŸ”§ Options
269
+ ### πŸ“£ Built-in Events
157
270
 
158
- ### HTTP / HTTPS Server Options
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 | Type | Default | Description |
174
- | --------------------------- | ------------------- | ------- | ----------------------------------- |
175
- | `port` | number | `3000` | WebSocket port |
176
- | `routes` | `SocketRoute[]` | `[]` | List of custom route classes |
177
- | `allowDuplicateConnections` | boolean (per route) | `false` | Allow multiple clients from same IP |
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:
178
326
 
179
327
  ---
180
328
 
181
- ## πŸ†• 0.7.0 Update Highlights
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
- * βœ… `allowDuplicateConnections` for multi-tab testing
184
- * βœ… Robust message validation
185
- * βœ… `socket.broadcast()` now excludes sender
186
- * βœ… Better error handling
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,170 @@ 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
- port?: number;
11
- bind?: string;
12
- publicPaths?: string[];
13
- services?: Array<{ serviceName: string; method: string; function: Function }>;
14
- listenCallback?: () => void;
15
- encoding?: RedWebEncoding;
16
- ssl?: { key: string; cert: string };
17
- server?: Application;
18
- corsOptions?: CorsOptions;
19
- enableHtmxRendering?: boolean;
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
- server?: HttpServer;
24
- port?: number;
25
- routes?: Array<new () => SocketRoute>;
26
- ssl?: { key: string; cert: string };
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
- path: string;
31
- handlers: Array<new () => BaseHandler>;
32
- allowDuplicateConnections?: boolean;
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
- name: string;
37
- constructor(name: string);
38
- handleMessage(
39
- socket: WebSocket & {
40
- sendJson: (message: object) => void;
41
- broadcast: (message: object) => void;
42
- },
43
- message: any
44
- ): void;
45
- onMessage(socket: WebSocket, message: any): void;
46
- onInitialContact(socket: WebSocket): void;
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
- path: string;
51
- handlers: BaseHandler[];
52
- clients: Map<string, WebSocket>;
53
- allowDuplicateConnections?: boolean;
54
- constructor(config: SocketRouteConfig);
55
- addHandler(handler: new () => BaseHandler): void;
56
- handleMessage(sock: WebSocket, data: any): void;
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
- clients: Map<string, WebSocket>;
61
- server: HttpServer;
62
- routes: SocketRoute[];
63
- constructor(server: HttpServer, options?: SocketServerOptions);
64
- addRoute(route: new () => SocketRoute): void;
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
+ 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>[];
65
136
  }
66
-
137
+
138
+ /** ─────────────────── CONCRETE SERVERS ─────────────────── */
139
+
67
140
  export class SocketServer extends BaseSocketServer {
68
- constructor(options?: SocketServerOptions);
141
+ constructor(options?: SocketServerOptions);
69
142
  }
70
-
143
+
71
144
  export class SecureSocketServer extends BaseSocketServer {
72
- constructor(options?: SocketServerOptions);
145
+ constructor(options?: SocketServerOptions);
73
146
  }
74
-
147
+
75
148
  export class HttpServer {
76
- constructor(options?: RedWebOptions);
149
+ constructor(options?: RedWebOptions);
77
150
  }
78
-
151
+
79
152
  export class HttpsServer {
80
- constructor(options?: RedWebOptions);
153
+ constructor(options?: RedWebOptions);
81
154
  }
82
-
155
+
156
+ /** ─────────────────── CONSTANTS ─────────────────── */
157
+
83
158
  export const METHODS: {
84
- GET: 'get';
85
- POST: 'post';
86
- PUT: 'put';
87
- DELETE: 'delete';
159
+ GET: 'get';
160
+ POST: 'post';
161
+ PUT: 'put';
162
+ DELETE: 'delete';
88
163
  };
89
-
164
+
90
165
  export const ENCODINGS: {
91
- json: 'json';
92
- urlencoded: 'urlencoded';
166
+ json: 'json';
167
+ urlencoded: 'urlencoded';
93
168
  };
94
-
169
+
95
170
  export const HTTP_OPTIONS: RedWebOptions;
96
171
  export const SOCKET_OPTIONS: SocketServerOptions;
97
- }
98
-
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.7.0",
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": {
@@ -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;
@@ -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
- constructor({path, handlers, allowDuplicateConnections } = {}) {
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
  }