redweb 0.8.0 → 0.10.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 (72) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/README.md +573 -307
  3. package/client.d.ts +42 -0
  4. package/client.js +55 -0
  5. package/docs/LIVE_HTML.md +313 -0
  6. package/docs/MULTIPLAYER_OPERATIONS.md +50 -0
  7. package/docs/PRODUCTION_READINESS.md +68 -0
  8. package/docs/VERIFICATION_EVIDENCE.md +20 -0
  9. package/examples/live-html/cards.css +36 -0
  10. package/examples/live-html/cards.html +11 -0
  11. package/examples/live-html/cards.js +91 -0
  12. package/examples/live-html/cards.ts +35 -0
  13. package/examples/live-html/chatroom.css +156 -0
  14. package/examples/live-html/chatroom.js +268 -0
  15. package/examples/live-html/chatroom.ts +217 -0
  16. package/examples/live-html/components.css +7 -0
  17. package/examples/live-html/components.js +113 -0
  18. package/examples/live-html/components.ts +41 -0
  19. package/examples/live-html/counter.css +24 -0
  20. package/examples/live-html/counter.html +10 -0
  21. package/examples/live-html/counter.js +73 -0
  22. package/examples/live-html/counter.ts +21 -0
  23. package/examples/live-html/tsconfig.json +16 -0
  24. package/index.d.ts +538 -114
  25. package/index.js +44 -12
  26. package/package.json +39 -15
  27. package/src/htmx/Html.js +133 -0
  28. package/src/htmx/HtmlRenderer.js +88 -0
  29. package/src/htmx/HtmlSyntax.js +168 -0
  30. package/src/htmx/LiveHtmlServer.js +91 -0
  31. package/src/htmx/LivePage.js +232 -0
  32. package/src/htmx/PageAssetLoader.js +34 -0
  33. package/src/htmx/PageManager.js +435 -0
  34. package/src/htmx/StaticExporter.js +78 -0
  35. package/src/htmx/StaticSite.js +182 -0
  36. package/src/htmx/TemplateRenderer.js +231 -0
  37. package/src/htmx/browserRuntime.js +97 -0
  38. package/src/htmx/index.js +10 -0
  39. package/src/htmx/metadata.js +349 -0
  40. package/src/htmx/sourceRoot.js +28 -0
  41. package/src/htmx/start.js +17 -0
  42. package/src/htmx/synchronous.js +9 -0
  43. package/src/http/BaseHttpServer.js +82 -117
  44. package/src/http/HttpServer.js +18 -18
  45. package/src/http/HttpsServer.js +20 -20
  46. package/src/serverLifecycle.js +46 -46
  47. package/src/ws/AdmissionPolicy.js +145 -0
  48. package/src/ws/BaseHandler.js +40 -40
  49. package/src/ws/BaseSocketServer.js +199 -100
  50. package/src/ws/DefaultHandler.js +5 -5
  51. package/src/ws/DefaultRoute.js +8 -8
  52. package/src/ws/DistributionBridge.js +271 -0
  53. package/src/ws/FixedStepService.js +74 -0
  54. package/src/ws/HeartbeatMonitor.js +75 -0
  55. package/src/ws/Metrics.js +34 -0
  56. package/src/ws/ProtocolPolicy.js +130 -0
  57. package/src/ws/RoomRegistry.js +117 -0
  58. package/src/ws/RouteRuntime.js +146 -0
  59. package/src/ws/SecureSocketServer.js +9 -9
  60. package/src/ws/SessionRegistry.js +135 -0
  61. package/src/ws/SocketRoute.js +523 -254
  62. package/src/ws/SocketServer.js +8 -8
  63. package/src/ws/TaskQueue.js +64 -0
  64. package/src/ws/TokenBucket.js +31 -0
  65. package/src/ws/TransportPolicy.js +68 -0
  66. package/src/ws/index.js +7 -2
  67. package/src/ws/protocol-schema.json +13 -0
  68. package/src/ws/protocol-validation.js +21 -0
  69. package/src/ws/shutdown.js +33 -33
  70. package/src/ws/util.js +38 -30
  71. package/src/htmx/HtmxRenderer.js +0 -73
  72. package/src/htmx/RedWebHtmxComponent.js +0 -11
package/client.d.ts ADDED
@@ -0,0 +1,42 @@
1
+ // Generated from src/ws/protocol-schema.json by scripts/generate-protocol-types.js.
2
+ export type RedWebProtocolErrorCode =
3
+ | 'INVALID_MESSAGE'
4
+ | 'UNKNOWN_HANDLER'
5
+ | 'HANDLER_FAILED'
6
+ | 'BINARY_UNSUPPORTED'
7
+ | 'RATE_LIMITED'
8
+ | 'QUEUE_FULL'
9
+ | 'CAPACITY_REACHED'
10
+ | 'INITIALIZATION_FAILED';
11
+
12
+ export interface ProtocolMetadata {
13
+ requestId?: string;
14
+ sequence?: number;
15
+ }
16
+
17
+ export interface ProtocolEnvelope<T = unknown> extends ProtocolMetadata {
18
+ v: string;
19
+ type: string;
20
+ payload: T;
21
+ }
22
+
23
+ export interface ProtocolErrorEnvelope extends ProtocolMetadata {
24
+ v: string;
25
+ type: 'error';
26
+ error: { code: RedWebProtocolErrorCode | string; message: string };
27
+ }
28
+
29
+ export interface SendableSocket {
30
+ send(data: string): unknown;
31
+ }
32
+
33
+ export class ProtocolClient {
34
+ constructor(socket: SendableSocket, version: string);
35
+ readonly socket: SendableSocket;
36
+ readonly version: string;
37
+ envelope<T>(type: string, payload: T, metadata?: ProtocolMetadata): ProtocolEnvelope<T>;
38
+ send<T>(type: string, payload: T, metadata?: ProtocolMetadata): void;
39
+ parse<T = unknown>(input: string | Uint8Array | ArrayBuffer | { data: string | Uint8Array | ArrayBuffer }): ProtocolEnvelope<T> | ProtocolErrorEnvelope;
40
+ }
41
+
42
+ export const ERROR_CODES: Readonly<Record<RedWebProtocolErrorCode, RedWebProtocolErrorCode>>;
package/client.js ADDED
@@ -0,0 +1,55 @@
1
+ const schema = require('./src/ws/protocol-schema.json');
2
+ const { validateEnvelope } = require('./src/ws/protocol-validation');
3
+
4
+ const ERROR_CODES = Object.freeze(Object.fromEntries(schema.errorCodes.map(code => [code, code])));
5
+
6
+ function boundedString(value, name, maxLength) {
7
+ if (typeof value !== 'string' || !value || value.length > maxLength) {
8
+ throw new TypeError(`${name} must be a non-empty string of at most ${maxLength} characters.`);
9
+ }
10
+ return value;
11
+ }
12
+
13
+ class ProtocolClient {
14
+ constructor(socket, version) {
15
+ if (!socket || typeof socket.send !== 'function') throw new TypeError('socket must provide send(data).');
16
+ this.socket = socket;
17
+ this.version = boundedString(version, 'version', 64);
18
+ }
19
+
20
+ envelope(type, payload, metadata = {}) {
21
+ const message = { v: this.version, type: boundedString(type, 'type', 256), payload };
22
+ if (metadata.requestId !== undefined) {
23
+ message.requestId = boundedString(metadata.requestId, 'requestId', 256);
24
+ }
25
+ if (metadata.sequence !== undefined) {
26
+ if (!Number.isSafeInteger(metadata.sequence) || metadata.sequence < 0) {
27
+ throw new TypeError('sequence must be a non-negative safe integer.');
28
+ }
29
+ message.sequence = metadata.sequence;
30
+ }
31
+ return message;
32
+ }
33
+
34
+ send(type, payload, metadata) {
35
+ this.socket.send(JSON.stringify(this.envelope(type, payload, metadata)));
36
+ }
37
+
38
+ parse(input) {
39
+ const raw = input && typeof input === 'object' && 'data' in input ? input.data : input;
40
+ let serialized;
41
+ if (typeof raw === 'string') serialized = raw;
42
+ else if (raw instanceof ArrayBuffer) serialized = new TextDecoder().decode(new Uint8Array(raw));
43
+ else if (ArrayBuffer.isView(raw)) serialized = new TextDecoder().decode(
44
+ new Uint8Array(raw.buffer, raw.byteOffset, raw.byteLength)
45
+ );
46
+ else serialized = raw.toString();
47
+ const message = JSON.parse(serialized);
48
+ if (!validateEnvelope(message, this.version)) {
49
+ throw new TypeError('Received an invalid Redweb protocol envelope.');
50
+ }
51
+ return message;
52
+ }
53
+ }
54
+
55
+ module.exports = { ProtocolClient, ERROR_CODES };
@@ -0,0 +1,313 @@
1
+ # Redweb Live HTML
2
+
3
+ Live HTML is Redweb's decorator-first server-rendering layer. It uses the existing `HttpServer`, `SocketRoute`, admission, protocol, ordering, backpressure, and shutdown implementations rather than maintaining a second network stack.
4
+
5
+ This layer deliberately owns page concerns only: `@page`, `@state`, `@view`, and `@action`. It does not clone jax.on's `@get`/`@post` controller API. Continue using Redweb's `services` option for ordinary HTTP APIs; a unified controller decorator surface is a separate compatibility decision rather than hidden behavior in the rendering layer.
6
+
7
+ ## Page model
8
+
9
+ Every page is a plain class registered with `@page(path, options)`. Extending `LivePage` remains compatible but is not required:
10
+
11
+ ```ts
12
+ @page('/profile', { template: 'profile.html', css: 'profile.css' })
13
+ class ProfilePage {
14
+ @state()
15
+ displayName = 'Guest';
16
+ }
17
+ ```
18
+
19
+ The decorators support both TypeScript's current standard decorator emit and the legacy `experimentalDecorators` ABI.
20
+
21
+ Pages use connection scope by default: each rendered browser page receives its own instance. `shared: true` creates one instance shared by every visitor to that page class and is appropriate for intentionally shared state such as a bounded chatroom history. `scope: 'shared'` remains available as the explicit equivalent.
22
+
23
+ `start(PageClass)` creates the Live HTML server. `@page()` captures its source directory when the module is evaluated, so colocated templates and styles work for unexported classes, CommonJS, ESM, and barrel exports without module scanning. Pass `templateRoot` explicitly only when page assets live in a different directory. Template and stylesheet traversal outside that root is rejected.
24
+
25
+ ## Colocated CSS
26
+
27
+ Declare a stylesheet on the same decorator—no Express static middleware or manual `<link>` is required:
28
+
29
+ ```ts
30
+ @page('/profile', { template: 'profile.html', css: 'profile.css' })
31
+ class ProfilePage {}
32
+ ```
33
+
34
+ For composed styles, use `css: ['base.css', 'profile.css']`. Paths resolve from the same captured source directory as the template and cannot traverse outside it. Redweb reads each file once at startup, injects stylesheet links into the server-rendered document, and serves the CSS from a content-addressed URL with the correct content type and immutable caching. Remote URLs and static asset hosting remain under the application's control.
35
+
36
+ ## Declarative HTML templates
37
+
38
+ Template files use the ordinary `.html` extension and contain no executable server code:
39
+
40
+ ```html
41
+ <h1>{{ displayName }}</h1>
42
+ <input rw-bind="displayName">
43
+ ```
44
+
45
+ `{{ property }}` creates an inline text binding. For context-safe container updates, bind an existing element; this is especially useful when a value contains several list or table children:
46
+
47
+ ```html
48
+ <ul data-rw-state="messages"></ul>
49
+ ```
50
+
51
+ During SSR Redweb fills the bound element with the current property value, and subsequent assignments to a decorated `@state()` property update the same element.
52
+
53
+ Ordinary values are escaped during SSR and applied with `textContent` in the browser. The `html` tagged template returns an explicit `HtmlFragment`; its interpolations are escaped, while the resulting fragment may be applied as HTML.
54
+
55
+ ## Rendering collections
56
+
57
+ Keep collection data as an ordinary array and decorate the method that renders one item:
58
+
59
+ ```ts
60
+ @state()
61
+ cards = [{ title: 'Sword' }, { title: 'Shield' }];
62
+
63
+ @view('cards')
64
+ card(item: { title: string }) {
65
+ return html`<article class="card"><h2>${item.title}</h2></article>`;
66
+ }
67
+ ```
68
+
69
+ Place the collection in the template with `<section rw-each="cards"></section>`. Redweb server-renders every item, escapes interpolated values, and replaces the collection contents when the array is reassigned. View methods are synchronous and must return an `HtmlFragment`. Arrays of fragments also compose naturally inside `html`, such as ``html`<div>${items.map(renderItem)}</div>` ``.
70
+
71
+ For a small, auditable safety model, primitive values may be interpolated into quoted attributes. URL-bearing attributes additionally pass through Redweb's safe-URL policy. The explicit `attribute()` and `url()` wrappers remain supported when they improve intent. Interpolation in event handlers, inline styles, `srcdoc`, `srcset`, `<script>`, and `<style>` remains prohibited.
72
+
73
+ ## Reusable components
74
+
75
+ For stateless snippets, pass a render function directly to `component()`:
76
+
77
+ ```ts
78
+ const Badge = component((properties: { label: string }) =>
79
+ html`<strong class="badge">${properties.label}</strong>`
80
+ );
81
+ ```
82
+
83
+ Function components are synchronous and must return `html`. Use a decorated class when a component needs state, actions, or lifecycle hooks.
84
+
85
+ Decorate a plain class with `@component()` to give a reusable HTML snippet its own server state, actions, and lifecycle. Store component instances in page fields and interpolate them like any other safe HTML fragment:
86
+
87
+ ```ts
88
+ import { action, component, html, page, start, state } from 'redweb';
89
+
90
+ @component()
91
+ class Counter {
92
+ @state()
93
+ count = 0;
94
+
95
+ constructor(private readonly label: string) {}
96
+
97
+ @action()
98
+ increment() {
99
+ this.count += 1;
100
+ }
101
+
102
+ render() {
103
+ return html`
104
+ <article>
105
+ <h2>${this.label}</h2>
106
+ <output data-rw-state="count">${this.count}</output>
107
+ <button rw-click="increment">Increment</button>
108
+ </article>
109
+ `;
110
+ }
111
+ }
112
+
113
+ @page('/')
114
+ class Dashboard {
115
+ primary = new Counter('Primary');
116
+ secondary = new Counter('Independent');
117
+
118
+ render() {
119
+ return html`<main>${this.primary}${this.secondary}</main>`;
120
+ }
121
+ }
122
+
123
+ start(Dashboard);
124
+ ```
125
+
126
+ The field path is the component's public protocol namespace, so both counters can expose `count` and `increment` without collisions. Browser events carry that visible namespace and the server resolves it through its component registry; client-supplied object paths are never evaluated. It is routing metadata, not an authorization boundary—component actions must enforce the same application authorization as page actions. Components may contain other decorated components, and state updates retain the complete nested namespace.
127
+
128
+ Component instances are owned by exactly one construction-time page or component field. Their synchronous `render(context)` method may return a safe `HtmlFragment` or a declarative template string; request context is propagated per render, including on concurrent shared pages. Components receive the same `loading`, `connected`, `disconnected`, and `disposed` hooks as their page, including the authenticated principal and cancellation signal where applicable. Disposal starts every child and owner cleanup together and preserves every settled failure, so one broken sibling cannot starve later hooks.
129
+
130
+ Redweb scopes only elements that carry a state, binding, or action directive; it does not add layout wrappers or inline styles. Components therefore remain valid in restricted contexts such as tables and selects and work with strict `style-src` policies.
131
+
132
+ ### Safe attributes and links
133
+
134
+ Dynamic document navigation remains explicit:
135
+
136
+ ```ts
137
+ import { attribute, html, url } from 'redweb';
138
+
139
+ const section = { id: 'socket-server', name: 'SocketServer' };
140
+ const markup = html`
141
+ <article id="${attribute(section.id)}">
142
+ <a href="${url(`#${section.id}`)}">${section.name}</a>
143
+ </article>
144
+ `;
145
+ ```
146
+
147
+ `attribute()` accepts primitive values and is valid only inside a quoted non-URL attribute. `url()` explicitly brands URL-bearing attributes such as `href`, `src`, and `action`; direct string values receive the same validation. Redweb permits relative URLs plus HTTP, HTTPS, mail, and telephone URLs, while rejecting control characters, protocol-relative URLs, and executable schemes. Both wrappers are escaped when rendered and are rejected in element text.
148
+
149
+ ### Nested components and code
150
+
151
+ Plain functions returning `html` fragments are reusable server components. `each()` validates and joins arrays of those fragments, including nested lists:
152
+
153
+ ```ts
154
+ import { codeBlock, each, html } from 'redweb';
155
+
156
+ const method = (entry: Method) => html`
157
+ <section>
158
+ <h3>${entry.name}</h3>
159
+ <p>${entry.description}</p>
160
+ ${codeBlock(entry.usage, { language: 'ts', label: 'TypeScript' })}
161
+ </section>
162
+ `;
163
+
164
+ const reference = each(apiSections, section => html`
165
+ <article>
166
+ <h2>${section.name}</h2>
167
+ ${each(section.methods, method)}
168
+ </article>
169
+ `);
170
+ ```
171
+
172
+ `codeBlock()` escapes strings by default. It may also receive an explicit `HtmlFragment`, or a `highlight(source, language)` callback that returns one, allowing a server-side highlighter to compose safe token spans without accepting arbitrary HTML strings.
173
+
174
+ An `HtmlFragment` returned from `render()` is already fully composed and is never reparsed for `{{ bindings }}` or directives. This keeps code samples literal and prevents escaped documentation text from becoming executable template syntax. Return a string or use `template` when Redweb should process declarative bindings.
175
+
176
+ State observation is deliberately shallow. Assigning a new value publishes an update; mutating a nested object or array in place does not. Reassign after nested changes:
177
+
178
+ ```ts
179
+ this.players = [...this.players, player];
180
+ ```
181
+
182
+ ## Browser actions and input
183
+
184
+ Only methods decorated with `@action()` may be invoked by the browser:
185
+
186
+ ```ts
187
+ @action()
188
+ save(form: { displayName: string }) {
189
+ this.displayName = form.displayName;
190
+ }
191
+ ```
192
+
193
+ ```html
194
+ <form rw-submit="save">
195
+ <input name="displayName">
196
+ <button>Save</button>
197
+ </form>
198
+ ```
199
+
200
+ `rw-click="action"` prevents default navigation and invokes an action without arguments. `rw-submit="action"` prevents submission, passes form fields as the first argument, preserves duplicate field names as arrays, and resets only after the server acknowledges success. `rw-bind="property"` sends text values or checkbox state only when that property was declared with `@state({ writable: true })`.
201
+
202
+ When an HTML-valued component state renders new actions or bindings, Redweb automatically scopes those directives back to that component. A component can therefore replace a join form with a composer—or swap any other interactive view—without manual component IDs or browser glue.
203
+
204
+ The document emits `redweb:connection` events as transport state changes and `redweb:error` events when an interaction fails. A bounded queue covers interaction during initial connection; actions are request/response operations and are not replayed during reconnect.
205
+
206
+ Names such as `constructor`, `prototype`, and `__proto__` are rejected. Arbitrary methods and undeclared state cannot be reached through the Live HTML protocol.
207
+
208
+ ## Lifecycle
209
+
210
+ Pages can implement these optional hooks:
211
+
212
+ - `loading(context)` runs before SSR and receives the portable page request, params, query, body, and shutdown `signal`.
213
+ - `connected(context)` runs after the page's authenticated socket connects and receives the socket and cancellation signal.
214
+ - `disconnected(context)` runs when that socket closes and may be asynchronous.
215
+ - `disposed()` runs once when a connection-scoped page expires or the server shuts down and may be asynchronous.
216
+
217
+ Timers and subscriptions created by a page should be owned by that page and stopped in `disconnected()` or `disposed()`. `dispose()` is idempotent.
218
+
219
+ Shutdown aborts the render signal and waits up to `shutdownTimeoutMs` (one second by default) for active `loading()` and `render()` hooks. If a hook ignores cancellation, Redweb disposes its page, force-closes the affected HTTP connection, completes the remaining cleanup phases, and then reports the timeout.
220
+
221
+ HTTP rendering produces an unpredictable page ID. The browser presents it during a same-origin, versioned WebSocket upgrade. Pending and disconnected sessions expire, the registry is bounded by `maxSessions`, and a page ID cannot own two active sockets simultaneously.
222
+
223
+ For authenticated pages, provide `authenticate(request)`. It runs for both the HTTP render and WebSocket upgrade and must return the same stable primitive identity (commonly a user ID) for both requests. A missing, rejected, changed, or object identity is denied, preventing a copied page token from crossing authentication boundaries. The identity is available as `context.principal` in page hooks and actions.
224
+
225
+ ## Browser transport
226
+
227
+ The injected module uses the published `redweb-client` package served by the same Redweb listener. It derives `ws:` or `wss:` from the current page, negotiates protocol version `1`, uses one socket per page, delegates DOM events at the document level, and opts into bounded reconnection attempts. Every initial connection and reconnect receives an authoritative state snapshot. Supplying the normal `ssl` option runs both the page and socket over HTTPS/WSS.
228
+
229
+ ## Options
230
+
231
+ `start(PageClass, options)` accepts normal HTTP options plus the following Live HTML controls. `new LiveHtmlServer({ pages, ...options })` remains available for explicit composition:
232
+
233
+ - `pages`: non-empty array of decorated class constructors when using `LiveHtmlServer` directly.
234
+ - `templateRoot`: optional root for all `.html` templates and CSS files; when omitted, each page uses the source directory captured by its `@page()` decorator.
235
+ - `livePaths.css`: optional internal URL prefix for generated stylesheet routes; defaults to `/__redweb/css`.
236
+ - `sessionTtlMs`: pending/reconnect session lifetime; defaults to 30 seconds.
237
+ - `maxSessions`: maximum pending plus active page sessions; defaults to 1,000.
238
+ - `maxConcurrentRenders`: maximum simultaneous HTTP page renders, independent of live session occupancy; defaults to `maxSessions`.
239
+ - `shutdownTimeoutMs`: maximum render/route drain time before forced cleanup; defaults to one second.
240
+ - `heartbeat`: optional `{ intervalMs, timeoutMs }` WebSocket liveness policy. Live HTML defaults to a 15-second ping interval and 10-second pong timeout so half-open browsers are disconnected and component `disconnected()` hooks update presence promptly.
241
+ - `authenticate`: optional HTTP/WebSocket identity function for binding page sessions to an authenticated principal.
242
+ - `origins`: optional exact origin list or predicate for deployments behind a trusted proxy. Without it, Redweb requires a scheme-and-host match (`http`/WS or `https`/WSS).
243
+ - `livePaths`: optional `{ socket, client, runtime }` internal path overrides.
244
+
245
+ The internal paths and application page paths must be unique.
246
+
247
+ ## Verification examples
248
+
249
+ - `examples/live-html/counter.ts` uses `@page()`, colocated CSS, and `@state()` to prove a connection-owned server timer can update browser state and is stopped on disconnect.
250
+ - `examples/live-html/chatroom.ts` uses a connection-scoped `@component()` backed by a room service created by `createChatroomPage()`, so separate server instances cannot leak history or names. Visitors join once, receive a stable dedicated composer, see a capped presence list with the total online count, share bounded history, and recover their identity and missed messages after reconnect.
251
+ - `examples/live-html/cards.ts` uses a shared decorated page, `@view()`, and `rw-each` to prove server-rendered collection SSR, realtime replacement, and persistence across reloads and reconnects while the server is running.
252
+ - `examples/live-html/components.ts` uses two instances of one `@component()` class to prove reusable markup, isolated server state, scoped actions, and component CSS composition.
253
+
254
+ Run the examples immediately with `npm run example:counter`, `npm run example:chatroom`, `npm run example:cards`, and `npm run example:components`. Their checked-in JavaScript artifacts are generated from the decorated TypeScript sources, and every test and package build rejects stale output. The artifacts are launched unchanged by `tests/integration/live-html.integration.test.js` over real loopback HTTP and WebSocket connections. Run the focused gate with `npm run verify:live-html`, or the complete 100% coverage suite with `npm test`.
255
+
256
+ ## Static pages and documentation export
257
+
258
+ Set `live: false` when a page needs server rendering but no realtime session:
259
+
260
+ ```ts
261
+ import { exportStatic, page } from 'redweb';
262
+
263
+ @page('/docs', {
264
+ template: 'docs.html',
265
+ css: ['base.css', 'docs.css'],
266
+ live: false,
267
+ head: {
268
+ title: 'Redweb API reference',
269
+ description: 'HTTP, WebSocket, multiplayer, and Live HTML APIs.',
270
+ canonical: 'https://example.com/docs',
271
+ image: 'https://example.com/og.png',
272
+ robots: 'index,follow',
273
+ },
274
+ cache: { maxAge: 300, staleWhileRevalidate: 3600 },
275
+ })
276
+ class DocsPage {}
277
+
278
+ await exportStatic(DocsPage, { outDir: 'dist' });
279
+ ```
280
+
281
+ Non-live pages contain no page token or browser runtime. When served by `start()`, Redweb skips its WebSocket route, emits an ETag, honors `If-None-Match`, and applies the declared public cache policy. Interactive pages are always sent with `private, no-store`.
282
+
283
+ `exportStatic()` accepts one decorated class or an array. It requires `live: false`, maps `/` to `index.html` and `/docs` to `docs/index.html`, emits content-addressed CSS beside the pages, and returns frozen lists of written files. It never deletes or cleans the output directory.
284
+
285
+ For several pages, define shared defaults once:
286
+
287
+ ```ts
288
+ import { defineSite, html } from 'redweb';
289
+
290
+ const docs = defineSite({
291
+ origin: 'https://example.com',
292
+ css: 'site.css',
293
+ head: { description: 'Redweb documentation' },
294
+ cache: { maxAge: 300 },
295
+ layout: (content, context) => html`
296
+ <body data-path="${context.request.path}">
297
+ <nav>Redweb</nav>
298
+ <main>${content}</main>
299
+ </body>
300
+ `,
301
+ });
302
+
303
+ @docs.page('/docs', { head: { title: 'Documentation' } })
304
+ class DocsPage {
305
+ render() { return html`<h1>Documentation</h1>`; }
306
+ }
307
+
308
+ await docs.export(DocsPage, { outDir: 'dist', publicDir: 'public' });
309
+ ```
310
+
311
+ `defineSite()` creates runtime-free page decorators, merges and deduplicates shared CSS, inherits head/cache/layout defaults, and derives canonical URLs from `origin`. Shared and page-local styles resolve from the modules that declare them. Layouts receive a trusted page fragment plus the normal render context, run synchronously, and must return `html`. `site.export()` stages a validated, link-free `publicDir` with the rendered pages before touching the destination, rejects case-insensitive public/generated path collisions, never cleans existing output, and includes copied files in its returned `assets` list.
312
+
313
+ The request exposed to `loading()` and `render()` is deliberately the portable `LivePageRequest` surface: `path`, `url`, `method`, `headers`, `params`, `query`, `body`, and `get(name)`. HTTP rendering supplies these from Express; static export supplies deterministic empty headers, parameters, query, and body values. Framework-specific Express request methods are not part of the page contract.
@@ -0,0 +1,50 @@
1
+ # Multiplayer operations
2
+
3
+ Redweb exposes small composition points and leaves deployment policy to the game. These examples are deliberately infrastructure-neutral.
4
+
5
+ ## Readiness and shutdown
6
+
7
+ Expose `socketServer.isReady()` from the HTTP stack used by the orchestrator. On termination, call `beginDrain()` first, stop external placement to the node, then call and await `shutdown()`. New upgrades receive `503` after draining begins.
8
+
9
+ ```js
10
+ app.get('/ready', (_request, response) => {
11
+ response.sendStatus(socketServer.isReady() ? 200 : 503)
12
+ })
13
+
14
+ process.once('SIGTERM', async () => {
15
+ socketServer.beginDrain()
16
+ await socketServer.shutdown()
17
+ })
18
+ ```
19
+
20
+ If `drainHandlers` is enabled, handlers should observe `socket.context.signal` and return promptly. Set the platform termination grace period above the application's maximum cooperative handler time plus `shutdownTimeoutMs`.
21
+
22
+ ## Placement and partitions
23
+
24
+ The admission `place(principal, request, context)` hook can return another node's `ws`/`wss` URL before upgrade. Keep placement decisions short-lived and retryable. A redirect is not a reservation: the destination must still authenticate, enforce capacity, and reject stale placement.
25
+
26
+ Treat the distribution adapter as ephemeral fan-out. During broker or network partitions, pause affected matches, continue from one authoritative owner, or reconcile from durable application state. Do not treat adapter delivery or its bounded deduplication window as persistence.
27
+
28
+ Adapter lifecycle and publish methods receive an optional final `AbortSignal`. Observe it in broker clients that support cancellation. Redweb compensates late startup and subscription completion, but a publish that the broker has already accepted cannot be recalled.
29
+
30
+ ## Capacity signals
31
+
32
+ Alert on rejected connections, rate-limited messages, full queues, handler failures, active connections, and readiness. Metrics intentionally contain only the route label. Join high-cardinality player, room, and match diagnostics in application logs or traces under the studio's own privacy and retention policy.
33
+
34
+ Size `maxConnections`, `maxBufferedBytes`, `maxPendingMessages`, room limits, session limits, adapter event limits, and codec byte limits from measured budgets. Run the included verification scripts on the deployment's Node version and instance class before changing those ceilings.
35
+
36
+ Redweb limits the number and lifetime of session records, but it deliberately does not inspect application session data. Keep that data small, schema-validated, and free of authoritative state that belongs in durable storage.
37
+
38
+ ## Verification
39
+
40
+ Run `npm test` for unit, real HTTP/WebSocket/WSS integration, fuzz, type-generation, and 100% coverage gates. The additional production gates are:
41
+
42
+ ```bash
43
+ npm run verify:load
44
+ npm run verify:memory
45
+ npm run verify:recovery
46
+ npm run verify:soak
47
+ npm run verify:overhead -- /path/to/redweb-0.8-baseline
48
+ ```
49
+
50
+ The soak defaults to 60 minutes. Shorter durations are useful for CI smoke checks but are not release evidence.
@@ -0,0 +1,68 @@
1
+ # Multiplayer production-readiness contract
2
+
3
+ Redweb remains a small transport foundation. Applications own game rules, authoritative state, matchmaking, databases, and identity providers. Redweb owns bounded connection admission, delivery, grouping, lifecycle, and optional composition points.
4
+
5
+ ## Compatibility invariants
6
+
7
+ - Every production feature is opt-in.
8
+ - Existing route, handler, and service subclasses require no source changes.
9
+ - The default route, strict routing, IP collision policy, handler dispatch, error hiding, listener ownership, and shutdown behavior remain compatible with 0.8.
10
+ - Disabled multiplayer features create no timers or per-connection queues.
11
+ - No global mutable registry or mandatory infrastructure dependency is permitted.
12
+ - Every timer, listener, queued task, membership, session lease, and adapter subscription has one deterministic cleanup owner.
13
+ - User hooks may be synchronous or asynchronous and may not escape as process-level failures.
14
+ - Cleanup is bounded, idempotent, best-effort, and continues after individual failures. Owned listeners also terminate incomplete HTTP peers at the route deadline; borrowed listeners remain application-owned.
15
+
16
+ ## Delivery claims
17
+
18
+ WebSocket provides an ordered byte stream while a connection remains healthy. Redweb does not claim exactly-once delivery. Reconnection, distributed adapters, and application retries can introduce loss or duplication; protocol users must use explicitly scoped sequence identifiers when those cases matter.
19
+
20
+ ## Roadmap gates
21
+
22
+ 1. **Bounded transport:** pre-upgrade admission, origin policy, rate limits, slow-consumer enforcement, bounded ordered processing, payload limits, and route-level heartbeat.
23
+ 2. **Multiplayer grouping:** route-scoped rooms, atomic membership cleanup, bounded session resumption, fixed-step services, and vendor-neutral metrics.
24
+ 3. **Horizontal composition:** draining/readiness, adapter lifecycle, loop prevention, bounded validation, placement hooks, and documented partition behavior.
25
+ 4. **Protocol and clients:** version negotiation, stable envelopes and error codes, generated client-facing types, binary replication hooks, and operational examples.
26
+
27
+ ## Release gates
28
+
29
+ - Existing tests and documented examples run unchanged.
30
+ - New behavior has unit tests and mock-free HTTP/WS/WSS integration tests.
31
+ - Coverage remains 100% for statements, branches, functions, and lines.
32
+ - Disabled-feature throughput regression is at most 3%; p99 latency regression is at most 5% on the same machine and Node version.
33
+ - Heartbeat uses one scheduler per route, never one interval per connection.
34
+ - Every queue, retained session, room, adapter backlog, and deduplication window is finite.
35
+ - Broadcast serializes once and remains O(n) in selected recipients.
36
+ - Slow clients cannot grow framework-owned memory without bound.
37
+ - A 60-minute soak shows no monotonic growth in timers, listeners, rooms, sessions, or queues.
38
+ - Reconnect storms return retained heap to within 10% of the warmed baseline after expiry and forced collection.
39
+ - Readiness becomes false before draining and shutdown completes within its documented bound.
40
+
41
+ The independent senior-review gate rejects releases that weaken any invariant, hide ambiguous delivery semantics, add mandatory brokers or identity libraries, or substitute coverage percentages for race, load, soak, and failure evidence.
42
+
43
+ ## Horizontal composition contract
44
+
45
+ - Placement runs before upgrade within the admission timeout. Redirects must use `wss`, contain no credentials or fragment, and may be restricted with `allowedPlacementOrigins`. Plain `ws` placement requires the explicit `allowInsecurePlacement` escape hatch for private development networks.
46
+ - Readiness becomes false before shutdown work begins. New upgrades receive `503`; existing connections stop accepting messages.
47
+ - `drainHandlers` is opt-in. When enabled, every connection context shares the route drain signal and shutdown awaits tracked work. Application handlers remain responsible for observing the signal; non-cooperating promises cannot be forcibly cancelled.
48
+ - Distribution adapters have no framework backlog. Publish and inbound concurrency are finite; publish failure returns `false`; startup, subscription, unsubscription, draining, and close are bounded. Adapter operations receive an optional `AbortSignal`, and late startup/subscription settlement is compensated. Adapters must observe the signal when their external side effects are not otherwise reversible.
49
+ - A failed publish marks a `required` adapter unhealthy, makes the route unready, and causes new upgrades to receive `503`; a later successful publish restores health. Best-effort adapters do not affect route readiness.
50
+ - Event IDs are deduplicated only inside a finite TTL/size window. Source-node events are ignored to prevent reflection loops.
51
+ - Broker partitions and process failure can lose events. Redweb makes no exactly-once or durable-delivery claim; applications own authoritative persistence, reconciliation, tick/sequence semantics, and partition policy.
52
+
53
+ ## Protocol contract
54
+
55
+ - Negotiation is opt-in and happens before upgrade. Unsupported clients receive `426` plus the finite supported-version list.
56
+ - JSON events use `{ v, type, payload, requestId?, sequence? }`. Error events use `{ v, type: "error", error: { code, message }, requestId? }`.
57
+ - `requestId` correlates a request and response; `sequence` expresses application ordering. Neither implies acknowledgement, durability, or exactly-once delivery.
58
+ - Stable framework codes are generated from `src/ws/protocol-schema.json`; the client declarations and runtime constants share that source.
59
+ - Binary replication is a codec hook, not a codec dependency. Size is checked before decode and after encode, and outbound data uses the normal backpressure ceiling.
60
+ - Protocol-disabled routes retain their 0.8 wire shapes and allocate no protocol context.
61
+
62
+ ## Resource ownership
63
+
64
+ - `maxPendingUpgrades` bounds authorization work before a socket is accepted.
65
+ - Timed-out admission hooks that ignore cancellation retain their reservation until they actually settle, preventing repeated timeout waves from accumulating unbounded application work.
66
+ - Fixed-step services clamp retained lag with `maxRetainedLagMs`; dropped time is observable rather than replayed forever.
67
+ - Session count, ID length, and lifetime are bounded by Redweb. Session `data` is application-owned, so applications must validate and cap its shape and byte size before storing it.
68
+ - Fully enabled idle routes have a 2 KiB framework-metadata budget per connection. Disabled features retain the legacy path and are compared against 0.8 by the performance gate.
@@ -0,0 +1,20 @@
1
+ # 0.9.0 verification evidence
2
+
3
+ Release-candidate measurements were taken on Windows x64, Node 22.21.0, and an AMD Ryzen 7 7800X3D. Performance numbers are machine-specific; the scripts and thresholds are the durable contract.
4
+
5
+ ## Automated correctness
6
+
7
+ - 290 unit and mock-free integration/fuzz tests pass on Node 18, 20, and 22.
8
+ - Statements, branches, functions, and lines are each 100% covered.
9
+ - Type declarations compile and generated protocol declarations match their schema.
10
+
11
+ ## Resource and failure gates
12
+
13
+ - Real-socket load: 32 concurrent clients, 3,200 request/response messages, 6,874 messages/second, 5.87 ms p99, with a paused slow consumer disconnected by the outbound-buffer policy.
14
+ - Reconnect recovery: 200 warm connections followed by 1,200 storm connections; retained heap recovered to 104.33% of warm baseline and connection, room, and session registries returned to zero.
15
+ - Fully enabled idle-route metadata: 1,756.93 bytes per connection across the median of three 500-connection trials, below the 2,048-byte gate.
16
+ - Disabled-feature comparison with Redweb 0.8: throughput improved 1.09% and p99 regressed 2.00% (limits: 3% and 5%), using five alternating 20,000-message trials at concurrency 128.
17
+ - `npm audit` reports zero vulnerabilities after upgrading Express to 4.22.2, `ws` to 8.21.3, and patched transitive dependencies.
18
+ - The corrected 60-minute soak sent 2,099,717 messages across 64 rotating clients and received 2,099,565 responses (99.993%). Steady-state clients, rooms, sessions, in-flight work, queued work, listeners, and Redweb-owned timers showed no sustained growth; late-window heap was 3.24% above the early window, within the 10% gate. Final heap was 100.04% of the warmed baseline; all registries returned to zero; active handles stayed within the one-handle allowance.
19
+
20
+ All measurements above were rerun on the final release candidate. Shortened soak smoke runs are not counted as release evidence.
@@ -0,0 +1,36 @@
1
+ :root {
2
+ color-scheme: dark;
3
+ font-family: system-ui, sans-serif;
4
+ background: #111827;
5
+ color: #f9fafb;
6
+ }
7
+
8
+ main {
9
+ width: min(64rem, calc(100% - 2rem));
10
+ margin: 3rem auto;
11
+ }
12
+
13
+ .card-grid {
14
+ display: grid;
15
+ grid-template-columns: repeat(auto-fit, minmax(14rem, 1fr));
16
+ gap: 1rem;
17
+ }
18
+
19
+ .card {
20
+ padding: 1rem;
21
+ border: 1px solid #374151;
22
+ border-radius: .75rem;
23
+ background: #1f2937;
24
+ }
25
+
26
+ button {
27
+ margin-top: 1rem;
28
+ padding: .7rem 1rem;
29
+ border: 0;
30
+ border-radius: .4rem;
31
+ background: #22d3ee;
32
+ color: #083344;
33
+ font: inherit;
34
+ font-weight: 700;
35
+ cursor: pointer;
36
+ }
@@ -0,0 +1,11 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head><meta charset="utf-8"><title>Redweb cards</title></head>
4
+ <body>
5
+ <main>
6
+ <h1>Server-rendered cards</h1>
7
+ <section class="card-grid" aria-live="polite" rw-each="cards"></section>
8
+ <button type="button" rw-click="add">Add a card</button>
9
+ </main>
10
+ </body>
11
+ </html>