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.
- package/CHANGELOG.md +11 -0
- package/README.md +458 -307
- package/client.d.ts +42 -0
- package/client.js +55 -0
- package/docs/MULTIPLAYER_OPERATIONS.md +50 -0
- package/docs/PRODUCTION_READINESS.md +68 -0
- package/docs/VERIFICATION_EVIDENCE.md +20 -0
- package/index.d.ts +320 -114
- package/index.js +27 -12
- package/package.json +28 -15
- package/src/htmx/HtmxRenderer.js +13 -13
- package/src/http/BaseHttpServer.js +112 -112
- package/src/http/HttpServer.js +18 -18
- package/src/http/HttpsServer.js +20 -20
- package/src/serverLifecycle.js +46 -46
- package/src/ws/AdmissionPolicy.js +145 -0
- package/src/ws/BaseHandler.js +40 -40
- package/src/ws/BaseSocketServer.js +195 -100
- package/src/ws/DefaultHandler.js +5 -5
- package/src/ws/DefaultRoute.js +8 -8
- package/src/ws/DistributionBridge.js +271 -0
- package/src/ws/FixedStepService.js +74 -0
- package/src/ws/HeartbeatMonitor.js +75 -0
- package/src/ws/Metrics.js +34 -0
- package/src/ws/ProtocolPolicy.js +130 -0
- package/src/ws/RoomRegistry.js +117 -0
- package/src/ws/RouteRuntime.js +146 -0
- package/src/ws/SecureSocketServer.js +9 -9
- package/src/ws/SessionRegistry.js +135 -0
- package/src/ws/SocketRoute.js +523 -254
- package/src/ws/SocketServer.js +8 -8
- package/src/ws/TaskQueue.js +64 -0
- package/src/ws/TokenBucket.js +31 -0
- package/src/ws/TransportPolicy.js +68 -0
- package/src/ws/index.js +7 -2
- package/src/ws/protocol-schema.json +13 -0
- package/src/ws/protocol-validation.js +21 -0
- package/src/ws/shutdown.js +33 -33
- 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
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
```
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
-
|
|
55
|
-
|
|
56
|
-
new HttpServer({
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
```
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
class
|
|
260
|
-
constructor() {
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
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.
|