redweb 0.8.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/README.md +458 -307
  3. package/client.d.ts +42 -0
  4. package/client.js +55 -0
  5. package/docs/MULTIPLAYER_OPERATIONS.md +50 -0
  6. package/docs/PRODUCTION_READINESS.md +68 -0
  7. package/docs/VERIFICATION_EVIDENCE.md +20 -0
  8. package/index.d.ts +320 -114
  9. package/index.js +27 -12
  10. package/package.json +28 -15
  11. package/src/htmx/HtmxRenderer.js +13 -13
  12. package/src/http/BaseHttpServer.js +112 -112
  13. package/src/http/HttpServer.js +18 -18
  14. package/src/http/HttpsServer.js +20 -20
  15. package/src/serverLifecycle.js +46 -46
  16. package/src/ws/AdmissionPolicy.js +145 -0
  17. package/src/ws/BaseHandler.js +40 -40
  18. package/src/ws/BaseSocketServer.js +195 -100
  19. package/src/ws/DefaultHandler.js +5 -5
  20. package/src/ws/DefaultRoute.js +8 -8
  21. package/src/ws/DistributionBridge.js +271 -0
  22. package/src/ws/FixedStepService.js +74 -0
  23. package/src/ws/HeartbeatMonitor.js +75 -0
  24. package/src/ws/Metrics.js +34 -0
  25. package/src/ws/ProtocolPolicy.js +130 -0
  26. package/src/ws/RoomRegistry.js +117 -0
  27. package/src/ws/RouteRuntime.js +146 -0
  28. package/src/ws/SecureSocketServer.js +9 -9
  29. package/src/ws/SessionRegistry.js +135 -0
  30. package/src/ws/SocketRoute.js +523 -254
  31. package/src/ws/SocketServer.js +8 -8
  32. package/src/ws/TaskQueue.js +64 -0
  33. package/src/ws/TokenBucket.js +31 -0
  34. package/src/ws/TransportPolicy.js +68 -0
  35. package/src/ws/index.js +7 -2
  36. package/src/ws/protocol-schema.json +13 -0
  37. package/src/ws/protocol-validation.js +21 -0
  38. package/src/ws/shutdown.js +33 -33
  39. package/src/ws/util.js +38 -30
package/README.md CHANGED
@@ -1,307 +1,458 @@
1
- # RedWeb
2
-
3
- RedWeb is a small Node.js helper that wires together Express HTTP/HTTPS servers and `ws` WebSocket servers with simple defaults. Use it to serve static files plus JSON APIs and to route WebSocket traffic to handler classes.
4
-
5
- ## Install
6
-
7
- ```bash
8
- npm install redweb
9
- ```
10
-
11
- ## Exports
12
-
13
- ```js
14
- const {
15
- HttpServer, // HTTP over Express
16
- HttpsServer, // HTTP with TLS (key/cert required)
17
- SocketServer, // WebSocket over HTTP
18
- SecureSocketServer, // WebSocket over HTTPS
19
- SocketRoute, // Per-path WebSocket routing
20
- SocketService, // Route-scoped background/tick logic
21
- SocketRegistry, // Evented in-memory store
22
- BaseHttpServer, // Express app builder for advanced composition
23
- BaseHandler, // WebSocket message handler base
24
- sendJson, // Utility to stringify+send
25
- HTTP_OPTIONS, // Defaults for HTTP servers
26
- ENCODINGS, // json/urlencoded encoding names
27
- SOCKET_OPTIONS, // Defaults for socket servers
28
- METHODS // Express method helpers
29
- } = require('redweb');
30
- ```
31
-
32
- ## HTTP servers (Express)
33
-
34
- `new HttpServer(options)` creates a Node HTTP server and starts listening immediately by default (default port `80`). `new HttpsServer({ ssl: { key, cert }, ... })` does the same over TLS.
35
-
36
- Options:
37
-
38
- - `port` (number): defaults to `80`.
39
- - `bind` (string): defaults to `0.0.0.0`.
40
- - `publicPaths` (string[]): folders served as static assets.
41
- - `services` (array): `{ serviceName, method, function }` for REST endpoints.
42
- - `listen` (boolean): defaults to `true`; set `false` to build `.app` and `.server` without binding a port.
43
- - `listenCallback` (function): invoked after `.listen`.
44
- - `encoding` (`'json' | 'urlencoded'`): body parser selection.
45
- - `corsOptions`: passed to `cors`.
46
- - `corsOptions: false`: disables the CORS middleware entirely.
47
- - `enableHtmxRendering` (boolean): render `.htmx` files with the built-in renderer.
48
- - `exposeErrors` (boolean): include HTMX rendering details in responses; defaults to `false`.
49
- - `logger`: an object with optional `log`, `warn`, and `error` methods. Pass `null` to disable library logging.
50
-
51
- Example:
52
-
53
- ```js
54
- const { HttpServer, METHODS } = require('redweb');
55
-
56
- new HttpServer({
57
- port: 3000,
58
- publicPaths: ['./public'],
59
- services: [
60
- {
61
- serviceName: '/api/hello',
62
- method: METHODS.GET,
63
- function: (req, res) => res.json({ hello: 'world' })
64
- }
65
- ]
66
- });
67
- ```
68
-
69
- HTMX rendering example (`enableHtmxRendering: true`):
70
-
71
- ```js
72
- new HttpServer({ publicPaths: ['./public'], enableHtmxRendering: true });
73
- ```
74
-
75
- `public/example.htmx`:
76
-
77
- ```js
78
- const name = 'RedWeb';
79
-
80
- <@>
81
- <h1>Hello, {{name}}!</h1>
82
- <@/>
83
- ```
84
-
85
- Requesting `/example.htmx` returns rendered HTML.
86
-
87
- Templates are trusted server-side code. They may load relative modules within their configured public directory, execute for at most one second by default, and interpolate raw HTML. Never render user-supplied template files.
88
-
89
- CORS remains permissive by default for backward compatibility. CORS is not authorization; configure `corsOptions`, add authentication middleware to `server.app`, or disable the middleware as appropriate.
90
-
91
- ## WebSocket servers
92
-
93
- `SocketServer` uses `ws` and routes connections to `SocketRoute` instances. Clients must send JSON containing a `type` that matches a handler name.
94
-
95
- Handler:
96
-
97
- ```js
98
- const { BaseHandler } = require('redweb');
99
-
100
- class ChatHandler extends BaseHandler {
101
- constructor() { super('chat'); }
102
-
103
- onMessage(socket, message) {
104
- socket.broadcast({ type: 'chat', text: message.text });
105
- }
106
- }
107
- ```
108
-
109
- Route:
110
-
111
- ```js
112
- const { SocketRoute } = require('redweb');
113
-
114
- class ChatRoute extends SocketRoute {
115
- constructor() {
116
- super({
117
- path: '/chat',
118
- handlers: [ChatHandler],
119
- allowDuplicateConnections: true // otherwise one connection per IP
120
- });
121
- }
122
- }
123
- ```
124
-
125
- Server:
126
-
127
- ```js
128
- const { SocketServer } = require('redweb');
129
-
130
- new SocketServer({
131
- port: 3000, // default
132
- routes: [ChatRoute], // defaults to a route at "/" with DefaultHandler if omitted
133
- });
134
- ```
135
-
136
- Each connected socket gets:
137
-
138
- - `socket.sendJson(data)` to send JSON.
139
- - `socket.broadcast(data)` to send JSON to all other clients on the same route.
140
-
141
- Invalid JSON triggers an error response and closes the socket.
142
-
143
- ### Binary WebSocket messages
144
-
145
- Text frames are still parsed as JSON and routed by `message.type`. Binary frames are dispatched separately, so handlers can receive raw `Buffer` payloads without triggering JSON parse errors.
146
-
147
- ```js
148
- const { BaseHandler, SocketRoute } = require('redweb');
149
-
150
- class UploadHandler extends BaseHandler {
151
- constructor() { super('upload'); }
152
-
153
- onMessage(socket, message) {
154
- socket.sendJson({ type: 'upload:control', action: message.action });
155
- }
156
-
157
- onBinaryMessage(socket, buffer) {
158
- socket.sendJson({ type: 'upload:chunk', bytes: buffer.length });
159
- }
160
- }
161
-
162
- class UploadRoute extends SocketRoute {
163
- constructor() {
164
- super({
165
- path: '/upload',
166
- handlers: [UploadHandler],
167
- allowDuplicateConnections: true,
168
- websocketOptions: {
169
- maxPayload: 2 * 1024 * 1024
170
- }
171
- });
172
- }
173
- }
174
- ```
175
-
176
- `BaseHandler` provides `handleBinaryMessage(socket, buffer)` and `onBinaryMessage(socket, buffer)`. Override `onBinaryMessage` for normal use. If a handler does not override it, RedWeb sends:
177
-
178
- ```json
179
- { "error": "Binary messages are not supported by this handler" }
180
- ```
181
-
182
- Routes may also select a binary-capable handler with `acceptsBinary(socket, buffer)`:
183
-
184
- ```js
185
- class ImageHandler extends BaseHandler {
186
- constructor() { super('image'); }
187
-
188
- acceptsBinary(socket, buffer) {
189
- return buffer.length > 0;
190
- }
191
-
192
- onMessage(socket, message) {}
193
- onBinaryMessage(socket, buffer) {}
194
- }
195
- ```
196
-
197
- ### WebSocket route options
198
-
199
- `SocketRoute` accepts `websocketOptions`, which are passed to `new WebSocketServer(...)`. Use this for `ws` server settings such as `maxPayload` or `perMessageDeflate`.
200
- Redweb controls `noServer`, `path`, `server`, and `port`; do not include them in `websocketOptions`. Route selection is performed once by Redweb so strict matching and optional root fallback behave consistently. Handshake authentication can use the `ws` `verifyClient` option, although authenticating in the surrounding HTTP upgrade flow is preferable for complex applications.
201
-
202
- ```js
203
- class ClipboardRoute extends SocketRoute {
204
- constructor() {
205
- super({
206
- path: '/clipboard',
207
- handlers: [ClipboardHandler],
208
- websocketOptions: {
209
- maxPayload: 1024 * 1024,
210
- perMessageDeflate: false
211
- }
212
- });
213
- }
214
- }
215
- ```
216
-
217
- Other route options:
218
-
219
- - `trustProxy`: use the first `X-Forwarded-For` value as the connection identity. Enable this only behind a trusted proxy.
220
- - `getClientKey(req)`: provide application-specific connection identity logic instead of IP-based identity.
221
- - `exposeErrors`: return handler exception messages to clients; defaults to `false`.
222
- - `logger`: route logger with optional `log`, `warn`, and `error` methods; pass `null` to disable it.
223
- - `shutdownTimeoutMs`: grace period before non-cooperating peers are terminated during shutdown; defaults to `1000`.
224
-
225
- `BaseHandler.validateMessage(message, socket)` may return `false` or a promise resolving to `false` to reject a message. Text and binary handlers may be asynchronous; rejected promises are caught and converted to safe error responses.
226
-
227
- ### Sharing an HTTP/HTTPS server
228
-
229
- Use `listen: false` on `HttpServer` to build the Express app and Node server without binding a port. Then pass `httpServer.server` to `SocketServer`. When `SocketServer` receives a prebuilt `server`, it attaches upgrade handling but does not call `.listen()` unless you explicitly set `listen: true`.
230
-
231
- ```js
232
- const { HttpServer, METHODS, SocketServer } = require('redweb');
233
-
234
- const httpServer = new HttpServer({
235
- port: 3030,
236
- listen: false,
237
- publicPaths: ['./public'],
238
- services: [
239
- { serviceName: '/health', method: METHODS.GET, function: (req, res) => res.json({ ok: true }) },
240
- { serviceName: '/session', method: METHODS.POST, function: createSession }
241
- ]
242
- });
243
-
244
- new SocketServer({
245
- server: httpServer.server,
246
- routes: [ClipboardRoute]
247
- });
248
-
249
- httpServer.server.listen(3030, () => console.log('HTTP and WebSocket server listening on 3030'));
250
- ```
251
-
252
- ### Socket services
253
-
254
- Route-scoped background logic:
255
-
256
- ```js
257
- const { SocketService } = require('redweb');
258
-
259
- class ClockService extends SocketService {
260
- constructor() { super('clock', 1000); } // tick every 1s
261
- onTick() {
262
- this.route.clients.forEach((socket) => socket.sendJson({ type: 'time', now: Date.now() }));
263
- }
264
- }
265
- ```
266
-
267
- Add with `services: [ClockService]` when constructing a `SocketRoute`.
268
-
269
- ### Socket registries
270
-
271
- `SocketRegistry` is a small evented list for socket-bound objects.
272
-
273
- ```js
274
- const { SocketRegistry } = require('redweb');
275
-
276
- class PlayerRegistry extends SocketRegistry {
277
- addPlayer(player) {
278
- this.add(player);
279
- this.emit('playerJoined', player);
280
- }
281
- }
282
- ```
283
-
284
- Helpers: `add`, `remove(itemOrId, byKey = 'id')`, `all()`, `count()`.
285
-
286
- ## Defaults and lifecycle
287
-
288
- - HTTP defaults: port `80`, bind `0.0.0.0`, `listen: true`.
289
- - WebSocket defaults: port `3000`, single connection per IP unless `allowDuplicateConnections` is set.
290
- - `SocketServer` owns and listens on its own server by default; if you pass `server`, you own calling `.listen()` unless you also pass `listen: true`.
291
- - Upgrade paths are matched strictly by default. Set `fallbackToRoot: true` for legacy behavior that sends unmatched paths to `/`.
292
- - If you do not supply `routes`, `SocketServer` registers a default route at `/` with `DefaultHandler` (it expects messages with `type: 'DefaultHandler'`).
293
- - `shutdown()` closes routes and services. It closes an owned listener, but leaves a supplied listener running unless `closeServerOnShutdown: true` is set.
294
- - Shutdown is best-effort: all hooks, clients, routes, and owned listeners are processed before collected cleanup errors are reported.
295
- - `HttpServer` and `HttpsServer` expose an idempotent async `shutdown()` helper.
296
-
297
- ## 0.8 migration notes
298
-
299
- - Unmatched WebSocket paths are rejected unless `fallbackToRoot: true` is configured.
300
- - Handler exception details are hidden unless `exposeErrors: true` is configured.
301
- - Shutting down a WebSocket server no longer closes a caller-supplied HTTP/HTTPS server by default.
302
- - `bind` is now honored by HTTP, HTTPS, WebSocket, and secure WebSocket listeners.
303
- - `shutdown()` is asynchronous; await it when deterministic cleanup matters.
304
-
305
- ## Developing
306
-
307
- - Run tests with `npm test` (Jest). The suite includes mock-free HTTP, HTTPS, WebSocket, and secure WebSocket integration tests plus unit tests, with 100% coverage enforced for statements, branches, functions, and lines.
1
+ # RedWeb
2
+
3
+ RedWeb is a small Node.js transport foundation that wires together Express HTTP/HTTPS servers and `ws` WebSocket servers with simple defaults. Use it for ordinary web apps or opt into bounded multiplayer controls without adopting a broker, identity system, or game-state framework.
4
+
5
+ Version 0.9 adds production-minded multiplayer building blocks while preserving the 0.8 API and wire behavior when they are disabled. Redweb owns transport boundaries and lifecycle; your game remains responsible for authoritative state, rules, matchmaking, persistence, and identity.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ npm install redweb
11
+ ```
12
+
13
+ ## Exports
14
+
15
+ ```js
16
+ const {
17
+ HttpServer, // HTTP over Express
18
+ HttpsServer, // HTTP with TLS (key/cert required)
19
+ SocketServer, // WebSocket over HTTP
20
+ SecureSocketServer, // WebSocket over HTTPS
21
+ SocketRoute, // Per-path WebSocket routing
22
+ SocketService, // Route-scoped background/tick logic
23
+ FixedStepService, // Drift-aware, non-overlapping simulation ticks
24
+ SocketRegistry, // Evented in-memory store
25
+ RoomRegistry, // Bounded route-local connection groups
26
+ SessionRegistry, // Bounded, expiring application-issued sessions
27
+ BaseHttpServer, // Express app builder for advanced composition
28
+ BaseHandler, // WebSocket message handler base
29
+ sendJson, // Utility to stringify+send
30
+ HTTP_OPTIONS, // Defaults for HTTP servers
31
+ ENCODINGS, // json/urlencoded encoding names
32
+ SOCKET_OPTIONS, // Defaults for socket servers
33
+ METHODS // Express method helpers
34
+ } = require('redweb');
35
+ ```
36
+
37
+ ## Multiplayer in 0.9
38
+
39
+ Redweb keeps each production feature independent and opt-in:
40
+
41
+ | Need | Redweb primitive |
42
+ | --- | --- |
43
+ | Authenticate and place players before upgrade | Bounded `admission` hooks with origin and redirect policy |
44
+ | Contain abusive or slow peers | Connection, rate, queue, payload, and outbound-buffer limits |
45
+ | Detect dead connections cheaply | One heartbeat scheduler per route |
46
+ | Group players and resume ownership | Bounded rooms and expiring application-issued sessions |
47
+ | Run simulation work predictably | Drift-aware, non-overlapping `FixedStepService` ticks |
48
+ | Scale across nodes | Optional broker adapter with bounded fan-out and explicit best-effort semantics |
49
+ | Roll deployments safely | Readiness, draining, cooperative cancellation, and bounded shutdown |
50
+ | Evolve clients | Opt-in version negotiation, stable envelopes/error codes, generated types, and codec hooks |
51
+
52
+ The framework does not claim exactly-once delivery or durable state. See the [production-readiness contract](docs/PRODUCTION_READINESS.md), [multiplayer operations guide](docs/MULTIPLAYER_OPERATIONS.md), and [release evidence](docs/VERIFICATION_EVIDENCE.md) before running authoritative sessions.
53
+
54
+ ## HTTP servers (Express)
55
+
56
+ `new HttpServer(options)` creates a Node HTTP server and starts listening immediately by default (default port `80`). `new HttpsServer({ ssl: { key, cert }, ... })` does the same over TLS.
57
+
58
+ Options:
59
+
60
+ - `port` (number): defaults to `80`.
61
+ - `bind` (string): defaults to `0.0.0.0`.
62
+ - `publicPaths` (string[]): folders served as static assets.
63
+ - `services` (array): `{ serviceName, method, function }` for REST endpoints.
64
+ - `listen` (boolean): defaults to `true`; set `false` to build `.app` and `.server` without binding a port.
65
+ - `listenCallback` (function): invoked after `.listen`.
66
+ - `encoding` (`'json' | 'urlencoded'`): body parser selection.
67
+ - `corsOptions`: passed to `cors`.
68
+ - `corsOptions: false`: disables the CORS middleware entirely.
69
+ - `enableHtmxRendering` (boolean): render `.htmx` files with the built-in renderer.
70
+ - `exposeErrors` (boolean): include HTMX rendering details in responses; defaults to `false`.
71
+ - `logger`: an object with optional `log`, `warn`, and `error` methods. Pass `null` to disable library logging.
72
+
73
+ Example:
74
+
75
+ ```js
76
+ const { HttpServer, METHODS } = require('redweb');
77
+
78
+ new HttpServer({
79
+ port: 3000,
80
+ publicPaths: ['./public'],
81
+ services: [
82
+ {
83
+ serviceName: '/api/hello',
84
+ method: METHODS.GET,
85
+ function: (req, res) => res.json({ hello: 'world' })
86
+ }
87
+ ]
88
+ });
89
+ ```
90
+
91
+ HTMX rendering example (`enableHtmxRendering: true`):
92
+
93
+ ```js
94
+ new HttpServer({ publicPaths: ['./public'], enableHtmxRendering: true });
95
+ ```
96
+
97
+ `public/example.htmx`:
98
+
99
+ ```js
100
+ const name = 'RedWeb';
101
+
102
+ <@>
103
+ <h1>Hello, {{name}}!</h1>
104
+ <@/>
105
+ ```
106
+
107
+ Requesting `/example.htmx` returns rendered HTML.
108
+
109
+ Templates are trusted server-side code. They may load relative modules within their configured public directory, execute for at most one second by default, and interpolate raw HTML. Never render user-supplied template files.
110
+
111
+ CORS remains permissive by default for backward compatibility. CORS is not authorization; configure `corsOptions`, add authentication middleware to `server.app`, or disable the middleware as appropriate.
112
+
113
+ ## WebSocket servers
114
+
115
+ `SocketServer` uses `ws` and routes connections to `SocketRoute` instances. Clients must send JSON containing a `type` that matches a handler name.
116
+
117
+ Handler:
118
+
119
+ ```js
120
+ const { BaseHandler } = require('redweb');
121
+
122
+ class ChatHandler extends BaseHandler {
123
+ constructor() { super('chat'); }
124
+
125
+ onMessage(socket, message) {
126
+ socket.broadcast({ type: 'chat', text: message.text });
127
+ }
128
+ }
129
+ ```
130
+
131
+ Route:
132
+
133
+ ```js
134
+ const { SocketRoute } = require('redweb');
135
+
136
+ class ChatRoute extends SocketRoute {
137
+ constructor() {
138
+ super({
139
+ path: '/chat',
140
+ handlers: [ChatHandler],
141
+ allowDuplicateConnections: true // otherwise one connection per IP
142
+ });
143
+ }
144
+ }
145
+ ```
146
+
147
+ Server:
148
+
149
+ ```js
150
+ const { SocketServer } = require('redweb');
151
+
152
+ new SocketServer({
153
+ port: 3000, // default
154
+ routes: [ChatRoute], // defaults to a route at "/" with DefaultHandler if omitted
155
+ });
156
+ ```
157
+
158
+ Each connected socket gets:
159
+
160
+ - `socket.sendJson(data)` to send JSON.
161
+ - `socket.broadcast(data)` to send JSON to all other clients on the same route.
162
+
163
+ Invalid JSON triggers an error response and closes the socket.
164
+
165
+ ### Binary WebSocket messages
166
+
167
+ Text frames are still parsed as JSON and routed by `message.type`. Binary frames are dispatched separately, so handlers can receive raw `Buffer` payloads without triggering JSON parse errors.
168
+
169
+ ```js
170
+ const { BaseHandler, SocketRoute } = require('redweb');
171
+
172
+ class UploadHandler extends BaseHandler {
173
+ constructor() { super('upload'); }
174
+
175
+ onMessage(socket, message) {
176
+ socket.sendJson({ type: 'upload:control', action: message.action });
177
+ }
178
+
179
+ onBinaryMessage(socket, buffer) {
180
+ socket.sendJson({ type: 'upload:chunk', bytes: buffer.length });
181
+ }
182
+ }
183
+
184
+ class UploadRoute extends SocketRoute {
185
+ constructor() {
186
+ super({
187
+ path: '/upload',
188
+ handlers: [UploadHandler],
189
+ allowDuplicateConnections: true,
190
+ websocketOptions: {
191
+ maxPayload: 2 * 1024 * 1024
192
+ }
193
+ });
194
+ }
195
+ }
196
+ ```
197
+
198
+ `BaseHandler` provides `handleBinaryMessage(socket, buffer)` and `onBinaryMessage(socket, buffer)`. Override `onBinaryMessage` for normal use. If a handler does not override it, RedWeb sends:
199
+
200
+ ```json
201
+ { "error": "Binary messages are not supported by this handler" }
202
+ ```
203
+
204
+ Routes may also select a binary-capable handler with `acceptsBinary(socket, buffer)`:
205
+
206
+ ```js
207
+ class ImageHandler extends BaseHandler {
208
+ constructor() { super('image'); }
209
+
210
+ acceptsBinary(socket, buffer) {
211
+ return buffer.length > 0;
212
+ }
213
+
214
+ onMessage(socket, message) {}
215
+ onBinaryMessage(socket, buffer) {}
216
+ }
217
+ ```
218
+
219
+ ### WebSocket route options
220
+
221
+ `SocketRoute` accepts `websocketOptions`, which are passed to `new WebSocketServer(...)`. Use this for `ws` server settings such as `maxPayload` or `perMessageDeflate`.
222
+ Redweb controls `noServer`, `path`, `server`, and `port`; do not include them in `websocketOptions`. Route selection is performed once by Redweb so strict matching and optional root fallback behave consistently. Handshake authentication can use the `ws` `verifyClient` option, although authenticating in the surrounding HTTP upgrade flow is preferable for complex applications.
223
+
224
+ ```js
225
+ class ClipboardRoute extends SocketRoute {
226
+ constructor() {
227
+ super({
228
+ path: '/clipboard',
229
+ handlers: [ClipboardHandler],
230
+ websocketOptions: {
231
+ maxPayload: 1024 * 1024,
232
+ perMessageDeflate: false
233
+ }
234
+ });
235
+ }
236
+ }
237
+ ```
238
+
239
+ Other route options:
240
+
241
+ - `trustProxy`: use the first `X-Forwarded-For` value as the connection identity. Enable this only behind a trusted proxy.
242
+ - `getClientKey(req)`: provide application-specific connection identity logic instead of IP-based identity.
243
+ - `exposeErrors`: return handler exception messages to clients; defaults to `false`.
244
+ - `logger`: route logger with optional `log`, `warn`, and `error` methods; pass `null` to disable it.
245
+ - `shutdownTimeoutMs`: grace period before non-cooperating peers are terminated during shutdown; defaults to `1000`.
246
+ - `admission`: optional pre-upgrade authentication/origin/placement policy. It may be a function or `{ authenticate, origins, place, allowedPlacementOrigins, allowInsecurePlacement, timeoutMs }`. Secure `wss` placement is the default; returned destinations can be origin-allowlisted.
247
+ - `maxPendingUpgrades`: maximum concurrent pre-upgrade authorization/negotiation operations; defaults to `64`.
248
+ - `limits`: opt-in connection, message-rate, pending-message, and outbound-buffer limits.
249
+ - `orderedMessages`: process each connection's messages serially through a bounded queue; defaults to `false` for compatibility.
250
+ - `heartbeat`: optional `{ intervalMs, timeoutMs }` half-open detection using one scheduler per route.
251
+ - `rooms` and `sessions`: optional bounded route-local grouping and resumable session registries. Session payload shape and byte size remain the application's responsibility.
252
+ - `distribution`: optional bounded fan-out adapter. Mark it `required` to fail readiness and reject new upgrades after startup or publish failure; adapter operations receive cancellation signals.
253
+ - `drainHandlers`: expose a route shutdown signal to handlers and track their work within `shutdownTimeoutMs`.
254
+ - `protocol`: optional version negotiation, stable envelopes, and binary codec hooks.
255
+
256
+ Production protections are deliberately opt-in, so existing applications retain their behavior and disabled features add no timers or per-connection queues. A protected route can stay compact:
257
+
258
+ ```js
259
+ class GameRoute extends SocketRoute {
260
+ constructor() {
261
+ super({
262
+ path: '/game',
263
+ handlers: [InputHandler],
264
+ admission: {
265
+ origins: ['https://game.example'],
266
+ timeoutMs: 3000,
267
+ authenticate: (request, { signal }) => verifySession(request, signal)
268
+ },
269
+ limits: {
270
+ maxConnections: 5000,
271
+ maxBufferedBytes: 1024 * 1024,
272
+ maxPendingMessages: 64,
273
+ messageRate: { capacity: 60, refillPerSecond: 30 }
274
+ },
275
+ orderedMessages: true,
276
+ heartbeat: { intervalMs: 30000, timeoutMs: 10000 },
277
+ websocketOptions: { maxPayload: 64 * 1024 }
278
+ });
279
+ }
280
+ }
281
+ ```
282
+
283
+ Admission completes before the WebSocket upgrade and before any handler hook runs. Its return value becomes `socket.context.principal`; the random `connectionId`, authenticated principal, future resumable session, and legacy IP-based `clientKey` remain separate concepts. Authentication errors are never returned to clients.
284
+
285
+ Rate and backpressure actions are `"drop"` or `"disconnect"`. Slow-consumer checks apply equally to `sendJson` and `broadcast`, and broadcasts still serialize a message once. Ordered processing never keeps more than `maxPendingMessages` waiting behind the active task.
286
+
287
+ ### Rooms, resumable sessions, and metrics
288
+
289
+ Set `rooms: true` to add bounded route-local rooms, or pass limits such as `{ maxRooms, maxMembersPerRoom, maxRoomsPerConnection, maxRoomIdLength }`. Connected sockets receive `joinRoom`, `leaveRoom`, and `roomBroadcast`. Joins and leaves are idempotent, disconnect removes every membership, and empty rooms are reclaimed.
290
+
291
+ Set `sessions: true` or provide `{ ttlMs, maxSessions, maxSessionIdLength, sweepIntervalMs }`. Applications supply opaque session IDs; Redweb does not create credentials. Sockets receive `createSession` and `resumeSession`. A successful takeover closes the former owner, and a stale close cannot release the replacement. Disconnected sessions expire through one route scheduler.
292
+
293
+ The optional `metrics` sink is vendor-neutral and supports `increment`, `gauge`, and `observe`. Framework attributes contain only the static route path—never player IDs, room IDs, tokens, payloads, or exception text.
294
+
295
+ ```js
296
+ class MatchRoute extends SocketRoute {
297
+ constructor() {
298
+ super({
299
+ path: '/match',
300
+ handlers: [MatchHandler],
301
+ rooms: { maxRooms: 1000, maxMembersPerRoom: 32 },
302
+ sessions: { ttlMs: 30000, maxSessions: 10000 },
303
+ metrics: myMetricsSink
304
+ });
305
+ }
306
+ }
307
+ ```
308
+
309
+ ### Horizontal composition and draining
310
+
311
+ Distribution is an opt-in adapter seam, not a bundled broker. Provide `distribution: { adapter, channel, nodeId, onEvent }`; the adapter only needs `publish(channel, serializedEvent)` and `subscribe(channel, listener)`. Optional `start`, `unsubscribe`, and `close` hooks have bounded lifecycles. Redweb validates event size, ignores events published by the same node, and retains a bounded, expiring deduplication window. Delivery remains at-most-effort: partitions can lose events and reconnects can duplicate them, so authoritative games should include their own tick or sequence in payloads.
312
+
313
+ Sockets on distributed routes receive `publishEvent(type, payload)`. The application decides how a received event affects rooms or state:
314
+
315
+ ```js
316
+ super({
317
+ path: '/match',
318
+ handlers: [MatchHandler],
319
+ rooms: true,
320
+ distribution: {
321
+ adapter: brokerAdapter,
322
+ channel: 'matches',
323
+ nodeId: process.env.INSTANCE_ID,
324
+ onEvent(event, route) {
325
+ route.rooms.broadcast('match-42', event.payload)
326
+ }
327
+ }
328
+ })
329
+ ```
330
+
331
+ `server.beginDrain()` flips readiness before rejecting new upgrades with `503`; `server.isReady()` exposes the state. Set `drainHandlers: true` to give connection contexts an `AbortSignal` and make shutdown wait for active handlers. Handlers must cooperate with that signal—JavaScript cannot forcibly cancel arbitrary application promises. This option is off by default, adding no per-message tracking to existing routes.
332
+
333
+ ### Versioned game protocol
334
+
335
+ Set `protocol: { versions: ['1'] }` to require version negotiation before upgrade. Browser clients use `?redwebVersion=1`; non-browser clients may send `x-redweb-version: 1`. Missing or unsupported versions receive `426 Upgrade Required` with a `Redweb-Versions` response header. The selected value is available as `socket.context.protocol.version`.
336
+
337
+ Protocol messages use `{ v, type, payload, requestId?, sequence? }`. Protocol routes add `socket.sendEvent(...)` and `socket.sendProtocolError(...)`; framework failures use stable codes exported as `ERROR_CODES`. This affects only opted-in routes. Existing routes retain their existing message and error shapes.
338
+
339
+ ```js
340
+ super({
341
+ path: '/match',
342
+ handlers: [MoveHandler],
343
+ protocol: {
344
+ versions: ['2', '1'],
345
+ binary: {
346
+ maxBytes: 64 * 1024,
347
+ encode: state => myCodec.encode(state),
348
+ decode: bytes => myCodec.decode(bytes)
349
+ }
350
+ }
351
+ })
352
+ ```
353
+
354
+ The optional binary hooks add no codec dependency. Decoded values pass through the same version/envelope validation and handler dispatch as JSON; `socket.sendBinaryEvent(value)` applies the same slow-consumer policy as other outbound traffic. Without binary hooks, binary frames on a protocol route receive `BINARY_UNSUPPORTED`.
355
+
356
+ For clients, `require('redweb/client')` exports the dependency-free `ProtocolClient` and the same error codes. Its TypeScript declarations are generated from Redweb's checked-in protocol schema and checked for drift before every test run.
357
+
358
+ `BaseHandler.validateMessage(message, socket)` may return `false` or a promise resolving to `false` to reject a message. Text and binary handlers may be asynchronous; rejected promises are caught and converted to safe error responses.
359
+
360
+ ### Sharing an HTTP/HTTPS server
361
+
362
+ Use `listen: false` on `HttpServer` to build the Express app and Node server without binding a port. Then pass `httpServer.server` to `SocketServer`. When `SocketServer` receives a prebuilt `server`, it attaches upgrade handling but does not call `.listen()` unless you explicitly set `listen: true`.
363
+
364
+ ```js
365
+ const { HttpServer, METHODS, SocketServer } = require('redweb');
366
+
367
+ const httpServer = new HttpServer({
368
+ port: 3030,
369
+ listen: false,
370
+ publicPaths: ['./public'],
371
+ services: [
372
+ { serviceName: '/health', method: METHODS.GET, function: (req, res) => res.json({ ok: true }) },
373
+ { serviceName: '/session', method: METHODS.POST, function: createSession }
374
+ ]
375
+ });
376
+
377
+ new SocketServer({
378
+ server: httpServer.server,
379
+ routes: [ClipboardRoute]
380
+ });
381
+
382
+ httpServer.server.listen(3030, () => console.log('HTTP and WebSocket server listening on 3030'));
383
+ ```
384
+
385
+ ### Socket services
386
+
387
+ Route-scoped background logic:
388
+
389
+ ```js
390
+ const { SocketService } = require('redweb');
391
+
392
+ class ClockService extends SocketService {
393
+ constructor() { super('clock', 1000); } // tick every 1s
394
+ onTick() {
395
+ this.route.clients.forEach((socket) => socket.sendJson({ type: 'time', now: Date.now() }));
396
+ }
397
+ }
398
+ ```
399
+
400
+ Add with `services: [ClockService]` when constructing a `SocketRoute`.
401
+
402
+ For authoritative simulation timing, extend `FixedStepService`. It compensates for timer drift, caps catch-up work, contains tick failures, and never overlaps an asynchronous tick with itself:
403
+
404
+ ```js
405
+ class Simulation extends FixedStepService {
406
+ constructor() { super('simulation', 50, 3); }
407
+ async onTick(stepMs, tick) {
408
+ await game.update(stepMs, tick);
409
+ }
410
+ }
411
+ ```
412
+
413
+ ### Socket registries
414
+
415
+ `SocketRegistry` is a small evented list for socket-bound objects.
416
+
417
+ ```js
418
+ const { SocketRegistry } = require('redweb');
419
+
420
+ class PlayerRegistry extends SocketRegistry {
421
+ addPlayer(player) {
422
+ this.add(player);
423
+ this.emit('playerJoined', player);
424
+ }
425
+ }
426
+ ```
427
+
428
+ Helpers: `add`, `remove(itemOrId, byKey = 'id')`, `all()`, `count()`.
429
+
430
+ ## Defaults and lifecycle
431
+
432
+ - HTTP defaults: port `80`, bind `0.0.0.0`, `listen: true`.
433
+ - WebSocket defaults: port `3000`, single connection per IP unless `allowDuplicateConnections` is set.
434
+ - `SocketServer` owns and listens on its own server by default; if you pass `server`, you own calling `.listen()` unless you also pass `listen: true`.
435
+ - Upgrade paths are matched strictly by default. Set `fallbackToRoot: true` for legacy behavior that sends unmatched paths to `/`.
436
+ - If you do not supply `routes`, `SocketServer` registers a default route at `/` with `DefaultHandler` (it expects messages with `type: 'DefaultHandler'`).
437
+ - `shutdown()` closes routes and services. It closes an owned listener, but leaves a supplied listener running unless `closeServerOnShutdown: true` is set.
438
+ - Shutdown is best-effort: all hooks, clients, routes, and owned listeners are processed before collected cleanup errors are reported.
439
+ - `HttpServer` and `HttpsServer` expose an idempotent async `shutdown()` helper.
440
+
441
+ ## 0.8 migration notes
442
+
443
+ - Unmatched WebSocket paths are rejected unless `fallbackToRoot: true` is configured.
444
+ - Handler exception details are hidden unless `exposeErrors: true` is configured.
445
+ - Shutting down a WebSocket server no longer closes a caller-supplied HTTP/HTTPS server by default.
446
+ - `bind` is now honored by HTTP, HTTPS, WebSocket, and secure WebSocket listeners.
447
+ - `shutdown()` is asynchronous; await it when deterministic cleanup matters.
448
+
449
+ ## 0.9 migration notes
450
+
451
+ - No migration is required when the new multiplayer options are disabled.
452
+ - Production controls are route-local and opt-in; enable and size them from measured capacity rather than copying example limits.
453
+ - `ProtocolClient` is available from `redweb/client` for negotiated protocol routes without adding runtime dependencies.
454
+ - The minimum supported Node.js version is 18.
455
+
456
+ ## Developing
457
+
458
+ - Run tests with `npm test` (Jest). The suite includes mock-free HTTP, HTTPS, WebSocket, and secure WebSocket integration tests plus unit tests, with 100% coverage enforced for statements, branches, functions, and lines.