redweb 0.12.0 → 0.13.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (169) hide show
  1. package/CHANGELOG.md +170 -9
  2. package/README.md +283 -629
  3. package/bin/redweb.js +11 -20
  4. package/client.d.ts +7 -2
  5. package/config/tsconfig.json +14 -14
  6. package/contract.d.ts +45 -0
  7. package/contract.js +5 -0
  8. package/docs/ACTION_INPUT_VERIFICATION.md +96 -0
  9. package/docs/ADMISSION_TIMEOUT_VERIFICATION.md +69 -0
  10. package/docs/AGENT_ACCESS.md +35 -0
  11. package/docs/AGENT_EVALUATION.md +58 -0
  12. package/docs/AGENT_READY_ACCEPTANCE.md +778 -0
  13. package/docs/APPLICATION_RECORDER_VERIFICATION.md +50 -0
  14. package/docs/BENCHMARK_VERIFICATION.md +307 -0
  15. package/docs/BROWSER_OWNER_VERIFICATION.md +191 -0
  16. package/docs/CLI.md +116 -0
  17. package/docs/CLIENT_DEVELOPMENT.md +152 -0
  18. package/docs/CLIENT_POLISH_VERIFICATION.md +282 -0
  19. package/docs/COVERAGE_COUNTER_VALIDATION.md +109 -0
  20. package/docs/COVERAGE_SCOPE_AUDIT.md +1183 -0
  21. package/docs/DEVELOPMENT.md +79 -0
  22. package/docs/DIAGNOSTIC_COMPATIBILITY.md +76 -0
  23. package/docs/DOCUMENTATION.md +37 -0
  24. package/docs/FEEDBACK_COMMAND_VERIFICATION.md +228 -0
  25. package/docs/GETTING_STARTED.md +58 -0
  26. package/docs/JSX_PERFORMANCE_VERIFICATION.md +59 -0
  27. package/docs/LIVE_HTML.md +169 -21
  28. package/docs/LIVE_HTML_LOAD_VERIFICATION.md +98 -0
  29. package/docs/MIGRATION.md +28 -0
  30. package/docs/MULTIPLAYER_OPERATIONS.md +26 -4
  31. package/docs/ORIGINAL_RECOVERY_VERIFICATION.md +100 -0
  32. package/docs/PACKAGED_EXAMPLE_VERIFICATION.md +126 -0
  33. package/docs/POLISH_RELEASE_CHECKPOINT.md +91 -0
  34. package/docs/PROCESS_CLEANUP_OBSERVATION.md +61 -0
  35. package/docs/PROCESS_REAPING_VERIFICATION.md +30 -0
  36. package/docs/PRODUCTION_READINESS.md +12 -2
  37. package/docs/RECOVERY_CLIENT_HEAP.md +201 -0
  38. package/docs/RECOVERY_CODE_ATTRIBUTION.md +174 -0
  39. package/docs/RECOVERY_CODE_CENSUS.md +158 -0
  40. package/docs/RECOVERY_COMPARISON.md +103 -0
  41. package/docs/RECOVERY_DEOPTIMIZATION.md +169 -0
  42. package/docs/RECOVERY_FOLLOWUP_SPIKE.md +147 -0
  43. package/docs/RECOVERY_INVESTIGATION.md +229 -0
  44. package/docs/RECOVERY_RUNTIME_CONTROLS.md +181 -0
  45. package/docs/RELEASE_TRUST.md +61 -0
  46. package/docs/ROOM_AUTHORIZATION.md +49 -0
  47. package/docs/RUNTIME_DIAGNOSTICS.md +78 -0
  48. package/docs/SERVER_RECOVERY_CANDIDATE.md +185 -0
  49. package/docs/SOAK_ROTATION_OBSERVATION.md +160 -0
  50. package/docs/SOAK_VERIFICATION.md +154 -0
  51. package/docs/SOCKET_CONTRACTS.md +39 -0
  52. package/docs/SPLIT_RECOVERY_COVERAGE.md +83 -0
  53. package/docs/SPLIT_RECOVERY_ERROR_HANDLING.md +67 -0
  54. package/docs/STARTER_COORDINATOR_VERIFICATION.md +112 -0
  55. package/docs/STARTER_LIFECYCLE_VERIFICATION.md +75 -0
  56. package/docs/STARTER_REPORT_RETENTION.md +73 -0
  57. package/docs/VERIFICATION_EVIDENCE.md +2 -0
  58. package/docs/generated.json +2154 -0
  59. package/docs/guides/chatroom.md +27 -0
  60. package/docs/guides/http-websocket.md +28 -0
  61. package/docs/guides/jsx-without-react.md +26 -0
  62. package/docs/guides/realtime-dashboard.md +29 -0
  63. package/docs/guides/typed-websockets.md +26 -0
  64. package/docs/reference.json +1207 -0
  65. package/docs/releases/0.13.0.json +2154 -0
  66. package/docs/releases/audit-0.13.0.json +1549 -0
  67. package/docs/snippets/room-access.tsx +51 -0
  68. package/docs/topics.json +21 -0
  69. package/examples/live-html/chatroom.js +207 -268
  70. package/examples/live-html/chatroom.tsx +167 -0
  71. package/examples/live-html/jsx-page.js +1 -1
  72. package/examples/live-html/jsx-page.tsx +1 -1
  73. package/examples/live-html/tsconfig.json +8 -7
  74. package/index.d.ts +170 -45
  75. package/index.js +2 -0
  76. package/jsx-dev-runtime.js +2 -2
  77. package/jsx-runtime.d.ts +7 -2
  78. package/package.json +88 -7
  79. package/recipes/add/artifact.test.cjs +57 -0
  80. package/recipes/add/live.tsx +18 -0
  81. package/recipes/add/socket-route.ts +24 -0
  82. package/recipes/chat/README.md +22 -0
  83. package/recipes/chat/app.test.cjs +105 -0
  84. package/recipes/chat/app.tsx +9 -0
  85. package/recipes/dashboard/README.md +43 -0
  86. package/recipes/dashboard/admin.ts +21 -0
  87. package/recipes/dashboard/app.css +16 -0
  88. package/recipes/dashboard/app.test.cjs +450 -0
  89. package/recipes/dashboard/app.tsx +86 -0
  90. package/recipes/dashboard/auth.ts +80 -0
  91. package/recipes/dashboard/cards.tsx +102 -0
  92. package/recipes/dashboard/rate-window.test.cjs +17 -0
  93. package/recipes/dashboard/store.ts +120 -0
  94. package/recipes/http-ws/README.md +11 -0
  95. package/recipes/http-ws/app.test.cjs +92 -0
  96. package/recipes/http-ws/app.tsx +36 -0
  97. package/recipes/realtime/README.md +8 -0
  98. package/recipes/realtime/app.test.cjs +15 -0
  99. package/recipes/realtime/app.tsx +28 -0
  100. package/recipes/shared/README.md +40 -0
  101. package/recipes/shared/app.css +8 -0
  102. package/recipes/shared/copy-assets.cjs +8 -0
  103. package/recipes/shared/network.cjs +59 -0
  104. package/recipes/shared/run-app.test.cjs +158 -0
  105. package/recipes/shared/run-app.ts +50 -0
  106. package/recipes/site/README.md +4 -0
  107. package/recipes/site/app.test.cjs +19 -0
  108. package/recipes/site/app.tsx +25 -0
  109. package/recipes/socket/README.md +39 -0
  110. package/recipes/socket/app.test.cjs +85 -0
  111. package/recipes/socket/app.tsx +30 -0
  112. package/recipes/socket/contract.ts +12 -0
  113. package/recipes/socket/handlers.ts +40 -0
  114. package/src/OwnedServerLifecycle.js +66 -0
  115. package/src/access/AccessPolicy.js +37 -0
  116. package/src/access/AuthenticationFailure.js +13 -0
  117. package/src/access/RequestFailure.js +33 -0
  118. package/src/access/failure-codes.json +25 -0
  119. package/src/async/BoundedOperation.js +62 -0
  120. package/src/cli/ActionReferences.js +193 -0
  121. package/src/cli/AdditionLayout.js +140 -0
  122. package/src/cli/FilePlan.js +94 -0
  123. package/src/cli/ProjectAddition.js +60 -0
  124. package/src/cli/ProjectConfig.js +26 -0
  125. package/src/cli/ProjectDoctor.js +112 -0
  126. package/src/cli/ProjectInitializer.js +20 -30
  127. package/src/cli/SourceInspector.js +207 -0
  128. package/src/cli/StaticSource.js +192 -0
  129. package/src/cli/arguments.js +62 -0
  130. package/src/cli/formatCommand.js +10 -0
  131. package/src/cli/run.js +57 -0
  132. package/src/cli/templates.js +86 -87
  133. package/src/context/RequestSnapshot.js +41 -0
  134. package/src/dataProperty.js +11 -0
  135. package/src/development/DevelopmentPageManager.js +48 -0
  136. package/src/development/Inspection.js +104 -0
  137. package/src/development/ObservedRenderer.js +42 -0
  138. package/src/development/description.js +35 -0
  139. package/src/development/loopbackRequest.js +27 -0
  140. package/src/development/refreshBrowser.js +96 -0
  141. package/src/development/refreshStyles.js +9 -0
  142. package/src/development/settings.js +17 -0
  143. package/src/docs/Documentation.js +182 -0
  144. package/src/htmx/ActionDefinition.js +44 -0
  145. package/src/htmx/Jsx.js +24 -8
  146. package/src/htmx/LiveHtmlServer.js +41 -19
  147. package/src/htmx/LivePage.js +63 -13
  148. package/src/htmx/PageIdentity.js +32 -0
  149. package/src/htmx/PageLifetime.js +37 -0
  150. package/src/htmx/PageManager.js +203 -74
  151. package/src/htmx/ReactiveRenderer.js +241 -0
  152. package/src/htmx/StaticExporter.js +1 -1
  153. package/src/htmx/TemplateRenderer.js +13 -7
  154. package/src/htmx/browserRuntime.js +2 -93
  155. package/src/htmx/metadata.js +19 -7
  156. package/src/validation/ActionInputError.js +12 -0
  157. package/src/validation/SchemaValidator.js +38 -0
  158. package/src/ws/AdmissionPolicy.js +24 -23
  159. package/src/ws/BaseSocketServer.js +53 -38
  160. package/src/ws/ContractValidationError.js +12 -0
  161. package/src/ws/HeartbeatMonitor.js +26 -8
  162. package/src/ws/ProtocolPolicy.js +1 -1
  163. package/src/ws/RoomAccess.js +82 -0
  164. package/src/ws/RoomRegistry.js +56 -6
  165. package/src/ws/RouteRuntime.js +56 -10
  166. package/src/ws/SocketContract.js +112 -0
  167. package/src/ws/SocketRoute.js +18 -0
  168. package/src/ws/protocol-schema.json +6 -1
  169. package/examples/live-html/chatroom.ts +0 -217
package/README.md CHANGED
@@ -1,636 +1,290 @@
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
- ```
1
+ # Redweb
2
+
3
+ Build a TypeScript website and its realtime backend together. Decorated classes own state and actions; server-rendered JSX updates the browser through WebSockets. No React, frontend bundler, or separate socket glue is required.
4
+
5
+ Use the same package for a live site, static HTML, Express HTTP endpoints, or routed WebSocket services.
6
+
7
+ ## Install
12
8
 
13
- Start a TypeScript + TSX project with Redweb's compiler preset and a small server-rendered page:
9
+ Start with a complete, tested counter application:
14
10
 
15
- ```bash
16
- npx redweb init
17
- npm install
11
+ <!-- redweb:setup:start -->
12
+ > Unreleased development documentation. Package metadata is 0.13.1, but these features are not claimed to be published in that npm version. Use the matching Redweb tarball described in the recipe setup; its published client dependency installs automatically. Do not install latest and assume compatibility.
13
+
14
+ Replace `TARBALL` with the absolute path to the matching Redweb tarball produced by `npm pack` (quoted if it contains spaces). This is an explicit prerequisite, not an npm package name. Both commands must use the same tarball. The published redweb-client dependency installs automatically; no separate client checkout or linking is required:
15
+
16
+ ```sh
17
+ npx --yes --package TARBALL redweb init my-realtime --template realtime
18
+ cd my-realtime
19
+ npm install --save-exact TARBALL
20
+ npm test
18
21
  npm run dev
19
22
  ```
20
23
 
21
- Pass a directory to create a new project there: `npx redweb init my-app`. Existing files are never overwritten, so rerunning the command is safe.
22
-
23
- ## Exports
24
-
25
- ```js
26
- const {
27
- HttpServer, // HTTP over Express
28
- HttpsServer, // HTTP with TLS (key/cert required)
29
- SocketServer, // WebSocket over HTTP
30
- SecureSocketServer, // WebSocket over HTTPS
31
- SocketRoute, // Per-path WebSocket routing
32
- SocketService, // Route-scoped background/tick logic
33
- FixedStepService, // Drift-aware, non-overlapping simulation ticks
34
- SocketRegistry, // Evented in-memory store
35
- RoomRegistry, // Bounded route-local connection groups
36
- SessionRegistry, // Bounded, expiring application-issued sessions
37
- BaseHttpServer, // Express app builder for advanced composition
38
- BaseHandler, // WebSocket message handler base
39
- sendJson, // Utility to stringify+send
40
- HTTP_OPTIONS, // Defaults for HTTP servers
41
- ENCODINGS, // json/urlencoded encoding names
42
- SOCKET_OPTIONS, // Defaults for socket servers
43
- METHODS, // Express method helpers
44
- LiveHtmlServer, // SSR plus lifecycle-safe realtime HTML
45
- HtmlRenderer, // Safe HTML templates, collections, and state payloads
46
- LivePage, // Optional base for advanced page internals
47
- page, state, action, view, // Live HTML decorators
48
- html, start // Safe HTML plus one-call page startup
49
- } = require('redweb');
50
- ```
51
-
52
- ## Live HTML
53
-
54
- `start(PageClass)` combines server-rendered TSX or `.html` templates and Redweb WebSockets on one listener. Decorated plain classes hold the behavior. Redweb injects a small browser runtime backed by [`redweb-client`](https://www.npmjs.com/package/redweb-client), binds the HTTP render to an expiring page token, and disposes connection-owned state after disconnect.
55
-
56
- TSX is the concise default for new pages. It renders straight to Redweb's existing `HtmlFragment`; there is no React dependency, virtual DOM, hydration pass, or client component runtime:
57
-
58
- Extend Redweb's TypeScript preset so builds and editors use the dependency-free JSX runtime consistently:
59
-
60
- ```json
61
- {
62
- "extends": "redweb/tsconfig.json",
63
- "compilerOptions": {
64
- "rootDir": "src",
65
- "outDir": "dist"
66
- },
67
- "include": ["src/**/*.ts", "src/**/*.tsx"]
24
+ This prerelease Redweb artifact is development-only until its release checks finish. For released applications, use an available versioned release guide.
25
+ <!-- redweb:setup:end -->
26
+
27
+ Open two tabs at `http://localhost:8181`. Clicking either button changes the counter on the server and updates both tabs.
28
+
29
+ This is the starter's exact `src/app.tsx`. The initializer also supplies its stylesheet, compiler configuration, shutdown helper, and real-network tests; the file is not a standalone copy-and-run program.
30
+
31
+ <!-- redweb:realtime:start -->
32
+ ```tsx
33
+ import { action, page, start, state, type LiveHtmlStartOptions } from 'redweb';
34
+ import { runApp } from './run-app';
35
+
36
+ @page('/', { css: 'app.css', shared: true })
37
+ export class CounterPage {
38
+ @state() count = 0;
39
+
40
+ @action()
41
+ increment() { this.count += 1; }
42
+
43
+ render() {
44
+ return (
45
+ <main class="home">
46
+ <h1>A counter owned by the server</h1>
47
+ <p>Open this page in two tabs. Either button updates both.</p>
48
+ <button rw-click="increment">
49
+ Count <output>{this.count}</output>
50
+ </button>
51
+ </main>
52
+ );
53
+ }
68
54
  }
55
+
56
+ export function createApp(options: LiveHtmlStartOptions = {}) {
57
+ return start(CounterPage, { port: Number(process.env.PORT ?? 8181), templateRoot: __dirname, ...options });
58
+ }
59
+
60
+ if (require.main === module) runApp(createApp);
69
61
  ```
70
-
71
- ```tsx
72
- import { LivePage, action, component, page, start, state } from 'redweb';
73
- import type { Child } from 'redweb/jsx-runtime';
74
-
75
- const Card = component((props: { title: string; children?: Child }) => (
76
- <article class="card">
77
- <h2>{props.title}</h2>
78
- {props.children}
79
- </article>
80
- ));
81
-
82
- @page('/', { css: 'counter.css' })
83
- class CounterPage extends LivePage {
84
- @state() count = 0;
85
-
86
- @action()
87
- increment() { this.count += 1; }
88
-
89
- render() {
90
- return (
91
- <main>
92
- <Card title="Server counter">
93
- <button rw-click="increment">
94
- Count <output data-rw-state="count">{this.count}</output>
95
- </button>
96
- </Card>
97
- </main>
98
- );
99
- }
100
- }
101
-
102
- start(CounterPage, { port: 8181 });
103
- ```
104
-
105
- Text and attribute values are escaped automatically. URL attributes use Redweb's existing safe-protocol policy. `on*`, inline `style`, `srcdoc`, `srcset`, and executable `<script>` or `<style>` children are rejected; use `rw-*` server directives and external CSS or JavaScript assets. Existing `html` fragments can be nested in TSX, and TSX fragments can be nested in `html`, so migration can be incremental.
106
-
107
- Ordinary declarative `.html` templates remain available when separating markup into a standalone file is preferable:
108
-
109
- ```ts
110
- import { page, start, state } from 'redweb';
111
-
112
- @page('/', { template: 'counter.html', css: 'counter.css' })
113
- class CounterPage {
114
- @state()
115
- count = 0;
116
-
117
- private ticker?: NodeJS.Timeout;
118
-
119
- connected() {
120
- this.ticker = setInterval(() => this.count++, 1000);
121
- }
122
-
123
- disconnected() {
124
- clearInterval(this.ticker);
125
- }
126
- }
127
-
128
- start(CounterPage, { port: 8080 });
129
- ```
130
-
131
- `counter.html` contains no executable server code:
132
-
133
- ```html
134
- <h1>Server-side counter</h1>
135
- <output aria-live="polite" data-rw-state="count"></output>
136
- ```
137
-
138
- Changing a `@state()` property sends only that binding's new value. State updates are shallow and assignment-driven; Redweb does not install deep proxies or rerender the document for scalar changes.
139
-
140
- CSS is colocated with the page and needs no static-server setup. Pass one file with `css: 'counter.css'` or compose several with `css: ['base.css', 'counter.css']`. Redweb resolves the files beside the decorated class, injects `<link>` elements during SSR, and serves content-addressed stylesheets with immutable browser caching.
141
-
142
- Browser events can call only explicitly exposed actions:
143
-
144
- ```ts
145
- @component()
146
- class Chatroom {
147
- @state()
148
- screen = html`<form rw-submit="join"><input name="name"><button>Join</button></form>`;
149
-
150
- @action()
151
- join({ name }: { name: string }) {
152
- this.screen = html`<p>Connected as ${name}</p><form rw-submit="send"><input name="message"><button>Send</button></form>`;
153
- }
154
- }
155
- ```
156
-
157
- ```ts
158
- @page('/chat', { css: 'chatroom.css' })
159
- class ChatroomPage {
160
- chat = new Chatroom();
161
- render() { return html`<main>${this.chat}</main>`; }
162
- }
163
- ```
164
-
165
- Interpolations created with `html` are escaped by default and are restricted to element text—not attributes, URLs, scripts, or styles. Only `HtmlFragment` values may produce HTML patches; ordinary state uses `textContent`. Use `@state({ writable: true })` to opt a property into `rw-bind="property"` browser updates. A page is connection-scoped by default; `shared: true` deliberately shares one instance across its connected visitors. The older `scope: 'shared'` spelling remains supported.
166
-
167
- Collections use the same model without manual concatenation. Keep the array in `@state()`, render one item with `@view('cards')`, and place it with `<section rw-each="cards"></section>`. Item views must return `html` fragments, so values remain escaped. The current protocol replaces the collection contents atomically; keyed incremental patches can be added later without changing the page API.
168
-
169
- Documentation and content-heavy pages can compose nested fragments without a client framework:
170
-
171
- ```ts
172
- import { attribute, codeBlock, each, html, url } from 'redweb';
173
-
174
- const sections = each(apiSections, section => html`
175
- <article id="${attribute(section.id)}">
176
- <h2>${section.name}</h2>
177
- <a href="${url(`#${section.id}`)}">Permalink</a>
178
- ${each(section.methods, method => html`<section><h3>${method.name}</h3></section>`)}
179
- ${codeBlock(section.usage, { language: 'ts', label: 'TypeScript' })}
180
- </article>
181
- `);
182
- ```
183
-
184
- Primitive values may be interpolated directly into quoted attributes and safe URL attributes. Redweb escapes attributes and rejects unsafe or protocol-relative URL schemes; `attribute()` and `url()` remain available when explicit intent helps readability. Event handlers, inline styles, `srcdoc`, and `srcset` remain prohibited. `codeBlock()` escapes ordinary code and can call a server-side `highlight` function that returns an `HtmlFragment`.
185
-
186
- For React-free documentation or marketing pages, set `live: false`. Redweb omits page tokens, browser JavaScript, and WebSockets; adds document metadata; and serves the result with an ETag:
187
-
188
- ```ts
189
- @page('/docs', {
190
- template: 'docs.html',
191
- css: 'docs.css',
192
- live: false,
193
- head: {
194
- title: 'Redweb API',
195
- description: 'Complete Redweb API reference.',
196
- canonical: 'https://example.com/docs',
197
- image: 'https://example.com/og.png',
198
- },
199
- cache: { maxAge: 300, staleWhileRevalidate: 3600 },
200
- })
201
- class DocsPage {}
202
- ```
203
-
204
- Export the same decorated page to CDN-ready files with `await exportStatic(DocsPage, { outDir: 'dist' })`. Route paths become `index.html` files, colocated stylesheets are emitted under their content-addressed URLs, and no Live HTML runtime is included. Static export requires `live: false`.
205
-
206
- For a multi-page site, `defineSite()` removes repeated static-page configuration. It shares CSS, metadata, caching, and a safe layout; generates canonical URLs; and can copy a public asset directory during export:
207
-
208
- ```ts
209
- const docs = defineSite({
210
- origin: 'https://redweb.example',
211
- css: 'site.css',
212
- head: { description: 'Redweb documentation' },
213
- layout: content => html`<body><nav>Redweb</nav><main>${content}</main></body>`,
214
- });
215
-
216
- @docs.page('/docs', { head: { title: 'Documentation' } })
217
- class DocsPage {
218
- render() { return html`<h1>Documentation</h1>`; }
219
- }
220
-
221
- await docs.export(DocsPage, { outDir: 'dist', publicDir: 'public' });
222
- ```
223
-
224
- An `html` fragment returned by `render()` is final safe markup, so documentation examples containing literal `{{ bindings }}` are never parsed a second time. Return a string or use a template file when Redweb should resolve template bindings and directives.
225
-
226
- The same API serves HTTPS/WSS when `ssl` is provided. For private pages, an optional `authenticate(request)` callback binds the page token to the same stable user identity across the HTTP render and WebSocket upgrade. Initial connections and reconnects always receive a complete authoritative state snapshot.
227
-
228
- See the [Live HTML guide](docs/LIVE_HTML.md), runnable [TSX page](examples/live-html/jsx-page.tsx), TypeScript [server counter](examples/live-html/counter.ts), component-based [chatroom](examples/live-html/chatroom.ts), and [persistent card collection](examples/live-html/cards.ts). The chatroom separates joining from its stable message composer, tracks online members, preserves bounded history, restores identity and missed messages after reconnect, and creates an isolated room for every server. The cards page uses `shared: true`, so additions survive reloads, reconnects, and new visitors while its server is running. Run the examples with `npm run example:jsx`, `npm run example:counter`, `npm run example:chatroom`, and `npm run example:cards`. The decorated sources are compiled and exercised unchanged by mock-free HTTP/WebSocket integration tests and a real-Chromium DOM gate.
229
-
230
- Reusable snippets can own server behavior without page-level forwarding methods. Decorate a class with `@component()`, put instances in page fields, and interpolate them directly: `` html`<main>${this.primary}${this.secondary}</main>` ``. Each instance gets isolated `@state()`, scoped `@action()` methods, nested-component support, and page-owned lifecycle cleanup. See the runnable [component counters](examples/live-html/components.ts) or run `npm run example:components`.
231
-
232
- ## Multiplayer in 0.9
233
-
234
- Redweb keeps each production feature independent and opt-in:
235
-
236
- | Need | Redweb primitive |
237
- | --- | --- |
238
- | Authenticate and place players before upgrade | Bounded `admission` hooks with origin and redirect policy |
239
- | Contain abusive or slow peers | Connection, rate, queue, payload, and outbound-buffer limits |
240
- | Detect dead connections cheaply | One heartbeat scheduler per route |
241
- | Group players and resume ownership | Bounded rooms and expiring application-issued sessions |
242
- | Run simulation work predictably | Drift-aware, non-overlapping `FixedStepService` ticks |
243
- | Scale across nodes | Optional broker adapter with bounded fan-out and explicit best-effort semantics |
244
- | Roll deployments safely | Readiness, draining, cooperative cancellation, and bounded shutdown |
245
- | Evolve clients | Opt-in version negotiation, stable envelopes/error codes, generated types, and codec hooks |
246
-
247
- 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.
248
-
249
- ## HTTP servers (Express)
250
-
251
- `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.
252
-
253
- Options:
254
-
255
- - `port` (number): defaults to `80`.
256
- - `bind` (string): defaults to `0.0.0.0`.
257
- - `publicPaths` (string[]): folders served as static assets.
258
- - `services` (array): `{ serviceName, method, function }` for REST endpoints.
259
- - `listen` (boolean): defaults to `true`; set `false` to build `.app` and `.server` without binding a port.
260
- - `listenCallback` (function): invoked after `.listen`.
261
- - `encoding` (`'json' | 'urlencoded'`): body parser selection.
262
- - `corsOptions`: passed to `cors`.
263
- - `corsOptions: false`: disables the CORS middleware entirely.
264
- - `exposeErrors` (boolean): include WebSocket handler details in responses; defaults to `false`.
265
- - `logger`: an object with optional `log`, `warn`, and `error` methods. Pass `null` to disable library logging.
266
-
267
- Example:
268
-
269
- ```js
270
- const { HttpServer, METHODS } = require('redweb');
271
-
272
- new HttpServer({
273
- port: 3000,
274
- publicPaths: ['./public'],
275
- services: [
276
- {
277
- serviceName: '/api/hello',
278
- method: METHODS.GET,
279
- function: (req, res) => res.json({ hello: 'world' })
280
- }
281
- ]
282
- });
283
- ```
284
-
285
- 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.
286
-
287
- ## WebSocket servers
288
-
289
- `SocketServer` uses `ws` and routes connections to `SocketRoute` instances. Clients must send JSON containing a `type` that matches a handler name.
290
-
291
- Handler:
292
-
293
- ```js
294
- const { BaseHandler } = require('redweb');
295
-
296
- class ChatHandler extends BaseHandler {
297
- constructor() { super('chat'); }
298
-
299
- onMessage(socket, message) {
300
- socket.broadcast({ type: 'chat', text: message.text });
301
- }
302
- }
303
- ```
304
-
305
- Route:
306
-
307
- ```js
308
- const { SocketRoute } = require('redweb');
309
-
310
- class ChatRoute extends SocketRoute {
311
- constructor() {
312
- super({
313
- path: '/chat',
314
- handlers: [ChatHandler],
315
- allowDuplicateConnections: true // otherwise one connection per IP
316
- });
317
- }
318
- }
319
- ```
320
-
321
- Server:
322
-
323
- ```js
324
- const { SocketServer } = require('redweb');
325
-
326
- new SocketServer({
327
- port: 3000, // default
328
- routes: [ChatRoute], // defaults to a route at "/" with DefaultHandler if omitted
329
- });
330
- ```
331
-
332
- Each connected socket gets:
333
-
334
- - `socket.sendJson(data)` to send JSON.
335
- - `socket.broadcast(data)` to send JSON to all other clients on the same route.
336
-
337
- Invalid JSON triggers an error response and closes the socket.
338
-
339
- ### Binary WebSocket messages
340
-
341
- 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.
342
-
343
- ```js
344
- const { BaseHandler, SocketRoute } = require('redweb');
345
-
346
- class UploadHandler extends BaseHandler {
347
- constructor() { super('upload'); }
348
-
349
- onMessage(socket, message) {
350
- socket.sendJson({ type: 'upload:control', action: message.action });
351
- }
352
-
353
- onBinaryMessage(socket, buffer) {
354
- socket.sendJson({ type: 'upload:chunk', bytes: buffer.length });
355
- }
356
- }
357
-
358
- class UploadRoute extends SocketRoute {
359
- constructor() {
360
- super({
361
- path: '/upload',
362
- handlers: [UploadHandler],
363
- allowDuplicateConnections: true,
364
- websocketOptions: {
365
- maxPayload: 2 * 1024 * 1024
366
- }
367
- });
368
- }
369
- }
370
- ```
371
-
372
- `BaseHandler` provides `handleBinaryMessage(socket, buffer)` and `onBinaryMessage(socket, buffer)`. Override `onBinaryMessage` for normal use. If a handler does not override it, RedWeb sends:
373
-
374
- ```json
375
- { "error": "Binary messages are not supported by this handler" }
376
- ```
377
-
378
- Routes may also select a binary-capable handler with `acceptsBinary(socket, buffer)`:
379
-
380
- ```js
381
- class ImageHandler extends BaseHandler {
382
- constructor() { super('image'); }
383
-
384
- acceptsBinary(socket, buffer) {
385
- return buffer.length > 0;
386
- }
387
-
388
- onMessage(socket, message) {}
389
- onBinaryMessage(socket, buffer) {}
390
- }
391
- ```
392
-
393
- ### WebSocket route options
394
-
395
- `SocketRoute` accepts `websocketOptions`, which are passed to `new WebSocketServer(...)`. Use this for `ws` server settings such as `maxPayload` or `perMessageDeflate`.
396
- 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.
397
-
398
- ```js
399
- class ClipboardRoute extends SocketRoute {
400
- constructor() {
401
- super({
402
- path: '/clipboard',
403
- handlers: [ClipboardHandler],
404
- websocketOptions: {
405
- maxPayload: 1024 * 1024,
406
- perMessageDeflate: false
407
- }
408
- });
409
- }
410
- }
411
- ```
412
-
413
- Other route options:
414
-
415
- - `trustProxy`: use the first `X-Forwarded-For` value as the connection identity. Enable this only behind a trusted proxy.
416
- - `getClientKey(req)`: provide application-specific connection identity logic instead of IP-based identity.
417
- - `exposeErrors`: return handler exception messages to clients; defaults to `false`.
418
- - `logger`: route logger with optional `log`, `warn`, and `error` methods; pass `null` to disable it.
419
- - `shutdownTimeoutMs`: grace period before non-cooperating peers are terminated during shutdown; defaults to `1000`.
420
- - `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.
421
- - `maxPendingUpgrades`: maximum concurrent pre-upgrade authorization/negotiation operations; defaults to `64`.
422
- - `limits`: opt-in connection, message-rate, pending-message, and outbound-buffer limits.
423
- - `orderedMessages`: process each connection's messages serially through a bounded queue; defaults to `false` for compatibility.
424
- - `heartbeat`: optional `{ intervalMs, timeoutMs }` half-open detection using one scheduler per route.
425
- - `rooms` and `sessions`: optional bounded route-local grouping and resumable session registries. Session payload shape and byte size remain the application's responsibility.
426
- - `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.
427
- - `drainHandlers`: expose a route shutdown signal to handlers and track their work within `shutdownTimeoutMs`.
428
- - `protocol`: optional version negotiation, stable envelopes, and binary codec hooks.
429
-
430
- 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:
431
-
432
- ```js
433
- class GameRoute extends SocketRoute {
434
- constructor() {
435
- super({
436
- path: '/game',
437
- handlers: [InputHandler],
438
- admission: {
439
- origins: ['https://game.example'],
440
- timeoutMs: 3000,
441
- authenticate: (request, { signal }) => verifySession(request, signal)
442
- },
443
- limits: {
444
- maxConnections: 5000,
445
- maxBufferedBytes: 1024 * 1024,
446
- maxPendingMessages: 64,
447
- messageRate: { capacity: 60, refillPerSecond: 30 }
448
- },
449
- orderedMessages: true,
450
- heartbeat: { intervalMs: 30000, timeoutMs: 10000 },
451
- websocketOptions: { maxPayload: 64 * 1024 }
452
- });
453
- }
454
- }
455
- ```
456
-
457
- 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.
458
-
459
- 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.
460
-
461
- ### Rooms, resumable sessions, and metrics
462
-
463
- 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.
464
-
465
- 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.
466
-
467
- 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.
468
-
469
- ```js
470
- class MatchRoute extends SocketRoute {
471
- constructor() {
472
- super({
473
- path: '/match',
474
- handlers: [MatchHandler],
475
- rooms: { maxRooms: 1000, maxMembersPerRoom: 32 },
476
- sessions: { ttlMs: 30000, maxSessions: 10000 },
477
- metrics: myMetricsSink
478
- });
479
- }
480
- }
481
- ```
482
-
483
- ### Horizontal composition and draining
484
-
485
- 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.
486
-
487
- Sockets on distributed routes receive `publishEvent(type, payload)`. The application decides how a received event affects rooms or state:
488
-
489
- ```js
490
- super({
491
- path: '/match',
492
- handlers: [MatchHandler],
493
- rooms: true,
494
- distribution: {
495
- adapter: brokerAdapter,
496
- channel: 'matches',
497
- nodeId: process.env.INSTANCE_ID,
498
- onEvent(event, route) {
499
- route.rooms.broadcast('match-42', event.payload)
500
- }
501
- }
502
- })
503
- ```
504
-
505
- `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.
506
-
507
- ### Versioned game protocol
508
-
509
- 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`.
510
-
511
- 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.
512
-
513
- ```js
514
- super({
515
- path: '/match',
516
- handlers: [MoveHandler],
517
- protocol: {
518
- versions: ['2', '1'],
519
- binary: {
520
- maxBytes: 64 * 1024,
521
- encode: state => myCodec.encode(state),
522
- decode: bytes => myCodec.decode(bytes)
523
- }
524
- }
525
- })
526
- ```
527
-
528
- 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`.
529
-
530
- 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.
531
-
532
- `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.
533
-
534
- ### Sharing an HTTP/HTTPS server
535
-
536
- 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`.
537
-
538
- ```js
539
- const { HttpServer, METHODS, SocketServer } = require('redweb');
540
-
541
- const httpServer = new HttpServer({
542
- port: 3030,
543
- listen: false,
544
- publicPaths: ['./public'],
545
- services: [
546
- { serviceName: '/health', method: METHODS.GET, function: (req, res) => res.json({ ok: true }) },
547
- { serviceName: '/session', method: METHODS.POST, function: createSession }
548
- ]
549
- });
550
-
551
- new SocketServer({
552
- server: httpServer.server,
553
- routes: [ClipboardRoute]
554
- });
555
-
556
- httpServer.server.listen(3030, () => console.log('HTTP and WebSocket server listening on 3030'));
557
- ```
558
-
559
- ### Socket services
560
-
561
- Route-scoped background logic:
562
-
563
- ```js
564
- const { SocketService } = require('redweb');
565
-
566
- class ClockService extends SocketService {
567
- constructor() { super('clock', 1000); } // tick every 1s
568
- onTick() {
569
- this.route.clients.forEach((socket) => socket.sendJson({ type: 'time', now: Date.now() }));
570
- }
571
- }
572
- ```
573
-
574
- Add with `services: [ClockService]` when constructing a `SocketRoute`.
575
-
576
- 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:
577
-
578
- ```js
579
- class Simulation extends FixedStepService {
580
- constructor() { super('simulation', 50, 3); }
581
- async onTick(stepMs, tick) {
582
- await game.update(stepMs, tick);
583
- }
584
- }
585
- ```
586
-
587
- ### Socket registries
588
-
589
- `SocketRegistry` is a small evented list for socket-bound objects.
590
-
591
- ```js
592
- const { SocketRegistry } = require('redweb');
593
-
594
- class PlayerRegistry extends SocketRegistry {
595
- addPlayer(player) {
596
- this.add(player);
597
- this.emit('playerJoined', player);
598
- }
599
- }
600
- ```
601
-
602
- Helpers: `add`, `remove(itemOrId, byKey = 'id')`, `all()`, `count()`.
603
-
604
- ## Defaults and lifecycle
605
-
606
- - HTTP defaults: port `80`, bind `0.0.0.0`, `listen: true`.
607
- - WebSocket defaults: port `3000`, single connection per IP unless `allowDuplicateConnections` is set.
608
- - `SocketServer` owns and listens on its own server by default; if you pass `server`, you own calling `.listen()` unless you also pass `listen: true`.
609
- - Upgrade paths are matched strictly by default. Set `fallbackToRoot: true` for legacy behavior that sends unmatched paths to `/`.
610
- - If you do not supply `routes`, `SocketServer` registers a default route at `/` with `DefaultHandler` (it expects messages with `type: 'DefaultHandler'`).
611
- - `shutdown()` closes routes and services. It closes an owned listener, but leaves a supplied listener running unless `closeServerOnShutdown: true` is set.
612
- - Shutdown is best-effort: all hooks, clients, routes, and owned listeners are processed before collected cleanup errors are reported.
613
- - `HttpServer` and `HttpsServer` expose an idempotent async `shutdown()` helper.
614
-
615
- ## 0.8 migration notes
616
-
617
- - Unmatched WebSocket paths are rejected unless `fallbackToRoot: true` is configured.
618
- - Handler exception details are hidden unless `exposeErrors: true` is configured.
619
- - Shutting down a WebSocket server no longer closes a caller-supplied HTTP/HTTPS server by default.
620
- - `bind` is now honored by HTTP, HTTPS, WebSocket, and secure WebSocket listeners.
621
- - `shutdown()` is asynchronous; await it when deterministic cleanup matters.
622
-
623
- ## 0.9 migration notes
624
-
625
- - No migration is required when the new multiplayer options are disabled.
626
- - Production controls are route-local and opt-in; enable and size them from measured capacity rather than copying example limits.
627
- - `ProtocolClient` is available from `redweb/client` for negotiated protocol routes without adding runtime dependencies.
628
- - The minimum supported Node.js version is 18.
629
-
630
- ## Live HTML migration
631
-
632
- The earlier executable `.htmx` sandbox and `enableHtmxRendering` option have been replaced. Templates are now ordinary `.html` files registered through decorated plain classes. Move template calculations and imports into the page class, mark reactive fields with `@state()`, expose browser-callable methods with `@action()`, and launch the page with `start(PageClass)`.
633
-
634
- ## Developing
635
-
636
- - 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.
62
+ <!-- redweb:realtime:end -->
63
+
64
+ ## Choose what to build
65
+
66
+ The links below describe each starter and its boundaries. Reuse the version-correct setup above, changing both the directory name and `--template realtime` to your chosen template. Every initialized project includes all application files and real tests; complete generated recipe pages and file contents are also available in the [documentation catalogue](docs/generated.json).
67
+
68
+ | Build | Starter | Recipe notes |
69
+ | --- | --- | --- |
70
+ | Live site with server-owned state | `realtime` | [Counter](recipes/realtime/README.md) |
71
+ | Chatroom with reusable components and presence | `chat` | [Chat](recipes/chat/README.md) |
72
+ | Non-live pages with shared layout and CSS | `site` | [Site](recipes/site/README.md) |
73
+ | Typed `/match` route with join/move/resume handlers | `socket` | [Socket service](recipes/socket/README.md) |
74
+ | Account-private cards with persistent SQLite data | `dashboard` | [Dashboard](recipes/dashboard/README.md), Node 22.13+ |
75
+ | HTTP and raw WebSockets on one port | `http-ws` | [Shared listener](recipes/http-ws/README.md) |
76
+
77
+ Choose the recipe's `--template` option when initializing. Shared memory survives visitors, not server restarts. The dashboard demonstrates application-owned persistence and identity; it is single-process, not a managed database or authentication service.
78
+
79
+ ## Live HTML
80
+
81
+ - A page is a decorated class whose `render()` returns server-side TSX.
82
+ - Ordinary expressions over `@state()` update after assignment. Replace arrays/objects rather than mutating them in place.
83
+ - `@action()` explicitly exposes a method to the browser. Validate inputs and authorize the operation on the server.
84
+ - Function components reuse presentation; decorated class components reuse state, actions, and lifecycle.
85
+ - Stable JSX keys preserve DOM identity for lists. CSS lives in ordinary external files.
86
+ - Pages are connection-scoped by default. `shared: true` deliberately shares one instance: do not put private visitor data there.
87
+
88
+ TSX and `html` templates escape text and attribute values and restrict URL protocols. Use external assets instead of inline executable markup. Ordinary `.html` templates remain available; the old executable `.htmx` sandbox does not.
89
+
90
+ See [pages, components, forms, CSS and rendering](docs/LIVE_HTML.md), [private rooms and request identity](docs/ROOM_AUTHORIZATION.md), and [runtime failures and retry limits](docs/RUNTIME_DIAGNOSTICS.md).
91
+
92
+ ## HTTP servers (Express)
93
+
94
+ Use `HttpServer` for Express services and `HttpsServer` when Node terminates TLS. HTTP and WebSockets can run independently; the example below combines them on one listener.
95
+
96
+ ## WebSocket servers
97
+
98
+ The `http-ws` starter answers `GET /health` and accepts `{"type":"hello"}` at `ws://127.0.0.1:8181/chat`, using the same port. A URL selects a route; a message's `type` selects its handler. No secondary `message.action` dispatcher is needed.
99
+
100
+ <!-- redweb:http-ws:start -->
101
+ ```tsx
102
+ import { BaseHandler, HttpServer, METHODS, SocketRoute, SocketServer, type RedWebSocket, type SocketServerOptions } from 'redweb';
103
+ import { runApp } from './run-app';
104
+
105
+ export class Hello extends BaseHandler {
106
+ constructor() { super('hello'); }
107
+
108
+ onMessage(socket: RedWebSocket) {
109
+ socket.sendJson({ type: 'hello', message: 'Hello from the server!' });
110
+ }
111
+ }
112
+
113
+ export class ChatRoute extends SocketRoute {
114
+ constructor() {
115
+ super({ path: '/chat', handlers: [Hello], allowDuplicateConnections: true });
116
+ }
117
+ }
118
+
119
+ export function createApp(options: Pick<SocketServerOptions, 'port' | 'bind' | 'logger'> = {}) {
120
+ const http = new HttpServer({
121
+ listen: false,
122
+ publicPaths: [],
123
+ services: [{ serviceName: '/health', method: METHODS.GET, function: (_req, res) => res.json({ ok: true }) }],
124
+ });
125
+
126
+ return new SocketServer({
127
+ port: options.port ?? Number(process.env.PORT ?? 8181),
128
+ bind: options.bind ?? '127.0.0.1',
129
+ logger: options.logger,
130
+ server: http.server,
131
+ routes: [ChatRoute],
132
+ listen: true,
133
+ closeServerOnShutdown: true, // One owner closes routes and the shared HTTP listener.
134
+ });
135
+ }
136
+
137
+ if (require.main === module) runApp(createApp);
138
+ ```
139
+ <!-- redweb:http-ws:end -->
140
+
141
+ Follow the [shared-listener notes](recipes/http-ws/README.md) and initialize with `--template http-ws` using the matching artifact above. The socket service explicitly owns cleanup of the supplied HTTP listener. `/health` reports liveness, not readiness. This demonstrates raw JSON messages, not a chatroom UI.
142
+
143
+ For validated, inferred client/server payloads, use [shared socket contracts](docs/SOCKET_CONTRACTS.md). The client wraps your transport; it does not create or reconnect one for you.
144
+
145
+ ## One development loop
146
+
147
+ After installing the matching package:
148
+
149
+ ```sh
150
+ npm test
151
+ npm run dev
152
+ ```
153
+
154
+ Tests compile the application and use real HTTP/WebSocket listeners. Development watches source, CSS, HTML, and root TypeScript configuration, then rebuilds/restarts. Local HTML pages refresh; detected edits require confirmation before reload. This is not autosave or state-preserving hot-module replacement. See [development refresh and inspection](docs/DEVELOPMENT.md).
155
+
156
+ Build with `npm run build`, then run compiled output with `npm start`. Production ships `dist/`, the manifest, and lockfile, with runtime dependencies installed through `npm ci --omit=dev`; it does not need `src/` or TypeScript.
157
+
158
+ ## Add to an existing project
159
+
160
+ Use the installed CLI so the tool and application agree:
161
+
162
+ ```sh
163
+ npx --no-install redweb init --existing --dry-run --json
164
+ npx --no-install redweb doctor --json
165
+ npx --no-install redweb add page dashboard --dry-run --json
166
+ ```
167
+
168
+ Remove `--dry-run` to create missing files. Existing configuration/source is never overwritten. Incremental generation reports imports, registration steps and isolated tests; it does not rewrite startup or silently repair your project. See [CLI prerequisites, commands and limitations](docs/CLI.md).
169
+
170
+ ## Fit and production boundaries
171
+
172
+ Good fit: Node-hosted live dashboards, chat, collaboration, server-rendered sites and multiplayer socket endpoints. Static HTML export is a separate deployment mode.
173
+
174
+ Choose something else when you need React compatibility, browser-side components, an edge-only runtime without Node listeners, or managed authentication/database/matchmaking infrastructure.
175
+
176
+ Before public deployment, configure HTTPS/WSS, trusted origins, identity, authorization, resource limits and application persistence. Reconnect is not exactly-once delivery; multiple processes do not automatically share state. See [operations](docs/MULTIPLAYER_OPERATIONS.md), [guarantees and limits](docs/PRODUCTION_READINESS.md), and [runtime compatibility and release verification](docs/RELEASE_TRUST.md).
177
+
178
+ ## Exports
179
+
180
+ See the [public TypeScript API](index.d.ts), [complete documentation catalogue](docs/generated.json), and [getting-started guide](docs/GETTING_STARTED.md). The catalogue includes version-labelled Markdown and executable recipes. An [optional read-only MCP adapter](docs/AGENT_ACCESS.md) serves the same source without adding SDK dependencies to your application; it is currently private/unpublished.
181
+
182
+ ## Defaults and lifecycle
183
+
184
+ HTTP defaults to port 80; sockets default to 3000. Generated applications explicitly select 8181. Supplied socket listeners are neither started nor closed unless the corresponding options explicitly transfer that responsibility. Await `shutdown()`; forced transport closure does not guarantee completed application work.
185
+
186
+ See [production ownership and lifecycle](docs/PRODUCTION_READINESS.md).
187
+
188
+ ## 0.8 migration notes
189
+
190
+ See [strict paths, sanitized errors and borrowed-listener ownership](docs/MIGRATION.md#08-migration-notes).
191
+
192
+ ## 0.9 migration notes
193
+
194
+ See [opt-in multiplayer controls and protocol clients](docs/MIGRATION.md#09-migration-notes).
195
+
196
+ ## Live HTML migration
197
+
198
+ See [replacing the executable HTMX sandbox and configuring TSX](docs/MIGRATION.md#live-html-migration).
199
+
200
+ ## Developing
201
+
202
+ Run `npm test` for unit tests, actual HTTP/HTTPS/WS/WSS integration tests, type checks and enforced 100% instrumented-library statement/branch/function/line coverage. Browser, package, performance and tool verification have separate gates; this is not a claim of exhaustive repository or application coverage.
203
+
204
+ `npm run verify:load` checks the default 32-client/3,200-message workload, p99 latency, throughput and slow-client containment. Its separate `npm run verify:load:coverage` gate combines unit failure tests with real malformed-message, disconnect and timeout integration tests and enforces all-four 100% coverage of the load policy, coordinator, traffic driver and shared socket helper. Coverage runs do not replace clean performance measurements. See the [scope audit](docs/COVERAGE_SCOPE_AUDIT.md) for exact evidence and remaining gaps.
205
+
206
+ Run `npm run verify:cli` to test the actual initializer, doctor and incremental-add commands and enforce 100% coverage of the shipped CLI entrypoint across subprocesses. This complements, rather than replaces, the library's CLI implementation coverage.
207
+
208
+ `npm run verify:package:examples:coverage` checks the packed counter without optional development dependencies, chat with explicit Zod, and generated TypeScript additions in a real installed consumer. Unit and real-socket failure tests require all-four 100% coverage of the three verifier modules. See [packaged-example evidence](docs/PACKAGED_EXAMPLE_VERIFICATION.md); this complements the full browser/package gate.
209
+
210
+ `npm run verify:action:coverage` checks the source-free typed action consumer in both decorator modes, plus real failed upgrades/responses and unit cleanup failures. It requires all-four 100% of the action-input verifier without enlarging the library coverage scope.
211
+
212
+ `npm run verify:reports:coverage` checks that failed starter commands retain available raw reports without overwriting or merging prior evidence. It combines unit faults with real child processes/filesystem checks and requires all-four 100% of the shared retention helper.
213
+
214
+ `npm run verify:starter-coordinators:coverage` checks both starter coverage runners and their shared final-report handling at all-four 100%. Real compiler/test runs prove changed inputs are rejected; filesystem failures cannot turn a failed command into success. See [starter verification](docs/STARTER_COORDINATOR_VERIFICATION.md) for the exact scope and unit/integration boundaries.
215
+
216
+ `npm run verify:starters:lifecycle` requires a nonempty, complete report for the deployed lifecycle helper. Its separate `npm run verify:starters:lifecycle:coverage` command covers the verifier itself. See [lifecycle evidence](docs/STARTER_LIFECYCLE_VERIFICATION.md) for the emitted-JavaScript scope and temporary source-map metadata removal.
217
+
218
+ `npm run verify:package:browser:coverage` covers the shared browser page owner and packed-browser verifier at all-four 100%, combining explicit failure units with actual Chromium counter/chat integration. Late page openings and cleanup failures retain uncertain workspaces. See [browser ownership evidence](docs/BROWSER_OWNER_VERIFICATION.md) for the checkout/package distinction and exact scope.
219
+
220
+ Browser and authored-source coverage share strict source-map and execution-counter validation. Malformed reports are rejected before merging; see [coverage validation evidence](docs/COVERAGE_COUNTER_VALIDATION.md) for the unit and real-browser checks.
221
+
222
+ Feedback and development-refresh verification share bounded browser commands so a disconnected debugging socket reaches cleanup. [Native failure evidence](docs/FEEDBACK_COMMAND_VERIFICATION.md) records the fixes, exact coverage scopes using actual Chromium/server cases, and the remaining acquisition boundary.
223
+
224
+ Development-refresh checks retain uncertain browser-launch cleanup and preserve shutdown failures. [Verification boundaries](docs/BROWSER_OWNER_VERIFICATION.md#development-refresh-launch-cleanup-follow-up) distinguish these fault tests from real generated-app/browser acceptance.
225
+
226
+ `npm run verify:live-html:load` checks 200 expired renders, 110 connected clients, presence/broadcast delivery and heap growth after client closure/session expiry, before server shutdown. Its separate `verify:live-html:load:coverage` command tests the verifier's HTTP/socket ownership, malformed responses, real timeouts and failure handling with unit and native integration tests. It requires all-four 100% coverage of the three verifier modules; instrumented tests do not replace clean memory measurements.
227
+
228
+ `npm run verify:jsx:performance` renders 10,000 component rows and validates their complete markup outside the timed render. CI supervises the command externally; the five-second performance limit cannot itself interrupt synchronous code. `npm run verify:jsx:coverage` separately checks malformed output, measurement limits and the actual CLI, requiring all-four 100% coverage of this verifier.
229
+
230
+ `npm run verify:soak` checks exact per-connection replies, rotation, disconnects and resource trends. It rejects undersampled runs and reports missing replies explicitly; the existing 99% delivery allowance is not a lossless guarantee. Final heap is sampled after client closure/expiry, before server shutdown. `npm run verify:soak:coverage` separately requires all-four 100% coverage of the verifier, policy and socket owner using unit and real-network/process tests. A short test run does not certify the default one-hour workload.
231
+
232
+ `npm run verify:overhead -- <baseline-directory>` compares disabled-feature socket throughput and p99 latency against a separately prepared baseline. Both sides must complete every warm-up and measured exchange with valid, unique reply IDs; malformed output, timeouts and cleanup failures fail the check. The limits remain 3% throughput regression and 5% p99 regression. `npm run verify:overhead:coverage` separately enforces all-four 100% coverage of the six benchmark modules through unit and real-socket/process tests. See [benchmark evidence and limitations](docs/BENCHMARK_VERIFICATION.md); a coverage pass is not a performance pass.
233
+
234
+ Run `npm run verify:recovery:server` from this source checkout for the blocking CI recovery contract: 7,400 exact exchanges with a separately measured server, empty registries, normal worker exits and every storm within 110% of warmed server heap. The original `npm run verify:recovery` remains a visible non-blocking CI diagnostic; its historical failures are not resolved by this measurement change. CI preserves both results and logs. See the [reviewed recovery contract and evidence](docs/SERVER_RECOVERY_CANDIDATE.md).
235
+
236
+ `npm run verify:recovery:coverage` separately enforces all-four 100% coverage of that gate's policy, CLI, and full authored coordinator/worker source. It combines unit boundary tests with actual worker/socket integration without repeating the native workload. The instrumented behavioral run is not used as a clean heap measurement; the normal server recovery command remains separate. See [authored recovery coverage](docs/SPLIT_RECOVERY_COVERAGE.md).
237
+
238
+ `npm run verify:recovery:original:coverage` separately tests the original shared-process verifier with authored-source coverage units and its existing real CLI/socket/snapshot checks. Its 110% limit now compares integer bytes exactly: equality passes, a one-byte excess fails, and any over-budget storm still fails even if the final heap recovers. Displayed ratios and workloads are unchanged. Synthetic unit heap values are not memory evidence, and this rounding correction does not explain larger historical failures. See the [coverage and regression evidence](docs/ORIGINAL_RECOVERY_VERIFICATION.md).
239
+
240
+ Recovery workers also retain non-Error failures and clean up rejected IPC requests immediately; empty error replies cannot masquerade as success. [Real-process regressions and server recovery evidence](docs/SPLIT_RECOVERY_ERROR_HANDLING.md) distinguish correctness fixes from coverage. Short soak checks retain raw results before assertions and preserve original reports if artifact writing fails. A [hosted delivery failure and real rotation controls](docs/SOAK_ROTATION_OBSERVATION.md) remain visible; no delivery threshold was relaxed.
241
+
242
+ Run `npm run verify:recovery:diagnostics` for native-source 100% coverage of the two private heap-analysis tools, including their real command-line entrypoints. It reuses the graph unit cases under Node's test runner to avoid merging Jest-transformed and original source ranges. Parser fixtures and real file/subprocess checks cover graph bounds, shared references, redaction, and malformed input; the separate recovery integration suite also checks actual V8 snapshots and a real server-held object. This gate does not cover the recovery workload verifier itself or waive its failed memory budget.
243
+
244
+ Run `npm run verify:package:tools` for a separate 100% coverage gate over the managed subprocess owner, failure normalizer, and starter/Markdown application verifiers. Tests use actual npm/native commands, descendants, files and generated applications. Use Node 22.13+ to exercise all six recipes, including the SQLite dashboard. Package packing/extraction and consumer checks use the same bounded owner; uncertain cleanup fails verification and retains its workspace. This is a scoped tool gate, verified on Windows, not coverage of every verification script or proof of cross-platform execution; Windows file-lock cases are skipped elsewhere.
245
+
246
+ Every generated starter also has `npm run test:coverage`: real application tests with coverage mapped to its TypeScript, separate from library coverage. In this repository, `npm run measure:starters:coverage` runs all six starter commands and records source/report hashes and run-specific results. It checks that every application module is measured, but does not present incomplete coverage as a passing 100% gate; compiler-generated decorator accessors appear in function counts. The chat and socket recipes include domain and real-network tests for reconnecting, identity conflicts, bounded history and session capacity.
247
+
248
+ `npm run verify:starters:source-coverage` separately instruments original TypeScript before compilation, so compiler-created decorator helpers do not inflate authored function counts. It runs the same application tests plain and instrumented, checks unchanged inputs, retains V8 reports, and enforces 100% of Istanbul's tracked statements/branches/functions/lines. All six starters pass with 104 tests per mode and all 600 statements, 299 branches, 160 functions and 472 lines covered. Integration tests use real networking and persistence; explicitly labelled unit cases exercise defensive failure paths. Istanbul does not independently count optional-chaining short circuits, so this is not an exhaustive semantic-branch claim or a replacement for V8 evidence. Reports identify received process reports, not every spawned child: hard termination can prevent an exit report, while every source module still starts in the denominator at zero. Node 22.13+ is required for all six recipes. Instrumentation and reports are test-only and are not shipped.
249
+
250
+ Run `npm run verify:browser:coverage` for native Chromium tests of the complete emitted Live HTML runtime and development-refresh script. The gate enforces 100% statement/branch/function/line coverage and runs the same cases without instrumentation. Actual HTTP/WebSocket checks cover actions, forms, state updates, reconnection and selection preservation. Refresh checks cover real reloads, draft guards, failed HTTP peers, history restoration and explicit discard under a self-only script policy; instrumentation requires no dynamic code evaluation.
251
+
252
+ The refresh report (`coverage/browser-refresh/report.json`) also retains `historyRestoration.plain.bfcacheRestored` and `historyRestoration.instrumented.bfcacheRestored` for each successfully completed mode. These are actual browser observations, not requirements: history navigation and resumed polling must pass, but the browser may choose to reload instead of restoring from its back/forward cache. A mode that fails before completion may only log its observation.
253
+
254
+ `npm run verify:refresh:coverage` separately requires 100% authored coverage of both refresh verification helpers. It combines explicit failure-boundary units with actual Chromium, HTTP uploads and socket cleanup, and is included in the browser coverage gate. Collection, page-close and socket-release failures remain visible together; a rejected non-Error value cannot become a passing result. The host-side helper map does not measure execution inside browser-expression strings; the separate generated-refresh map and native checks remain required.
255
+
256
+ `npm run verify:development:coverage` additionally covers the generated-app refresh verifier: real TypeScript/CSS rebuilds, browser draft preservation, adverse HTTP peers and process cleanup, plus explicit startup/cleanup failure units. Page openings remain owned if they time out or settle late; uncertain cleanup retains the workspace. CI runs this gate instead of repeating the standalone development browser command. Its 100% scope is the authored coordinator, not embedded browser programs or every possible platform failure.
257
+
258
+ `npm run verify:package:coordinator:coverage` runs the complete isolated-package check alongside explicit failure-boundary units and real listener-cleanup tests. It requires 100% authored coverage of the package coordinator and report helper. Every acquired example server gets its own cleanup attempt; a missing error value cannot become success, and success is printed only after workspace cleanup. CI uses this instead of repeating the standalone package command. The full consumer check requires the dashboard starter's Node version (22.13 or newer); older supported library versions run the native cleanup checks but skip that consumer case.
259
+
260
+ The isolated browser harness copies its verification helpers explicitly and checks their literal relative imports against that copied set. This catches missing test dependencies without falling back to checkout runtime code; the real packed-consumer gate still verifies installation and execution.
261
+
262
+ `npm run verify:evaluation:process:coverage` measures the unchanged evaluation process and evidence-sealing tools at 100% authored coverage. It combines explicit OS-boundary units with actual subprocess, archive, file-lock, CLI and listener checks. A test-only preload instruments selected code in memory; frozen source and sealed evaluation records are not rewritten. Native interface inspection is Windows-only; unsupported platforms are tested for explicit rejection. This coverage gate does not rerun an agent trial or resolve historical cleanup failures.
263
+
264
+ `npm run verify:evaluation:prepare:coverage` separately checks candidate preparation against actual npm archives, catalogue bytes and Git identity, comparing plain and instrumented CLI execution. Real launch-failure checks supplement explicit subprocess-boundary units. It uses owned temporary directories and does not publish packages or rerun sealed agent trials.
265
+
266
+ `npm run verify:evaluation:trial:coverage` checks the unchanged trial runner's input hashes, build outcomes and evidence retention. It combines real archive/CLI checks, explicit failure units and the evaluator's actual HTTP/WebSocket browser control on Windows. Synthetic checker fixtures are not new agent trials or substitutes for packed Redweb acceptance. Uncertain cleanup preserves the outer test workspace and its report, including leftover browser profiles even when no report was saved.
267
+
268
+ `npm run verify:evaluation:controls:coverage` measures the unchanged control validator and browser evaluator together: four working protocol controls and seven deliberately broken variants run in actual Chromium on Windows. Real CLI tests also cover failed builds, early exits, invalid startup URLs and HTTP rejection. Elsewhere, interface inspection must explicitly refuse support, not imply browser success. Separate browser/process/result boundary units cover reporting and cleanup faults; unexpected native outcomes retain their original errors and workspace. These evaluator controls are not new Redweb agent submissions or release acceptance.
269
+
270
+ `npm run verify:live-html:browser:coverage` combines the existing full browser workload (counter, chat, CSS, JSX, components, forms and dashboard) with explicit failure-path unit tests. Its 100% authored-tool coverage is separate from frontend coverage and release acceptance. The native workload requires the dashboard's supported Node version. Known limitations of the unchanged legacy browser tool— including uncertain descendant cleanup—are characterized, not silently fixed or counted as verified cleanup; see the [coverage audit](docs/COVERAGE_SCOPE_AUDIT.md).
271
+
272
+ The frontend is maintained in `redweb-client/live-html`; Redweb emits only a two-line mounting bootstrap. Published `redweb@0.13.0` depends on published `redweb-client@^0.2.0`, so ordinary application installation needs no client checkout or link. Contributors editing the client can still use the [linked development workflow](docs/CLIENT_DEVELOPMENT.md).
273
+
274
+ `npm run measure:browser:client` separately serves the exact installed socket-only module with and without instrumentation through the same real HTTP/WebSocket/browser cases and retains its source hash and counters. It exits unsuccessfully until all four coverage metrics reach 100%; incomplete results are not a passing dependency-coverage claim. Reports are local under `coverage/browser-client` and do not alter the installed dependency or published package.
275
+
276
+ `npm run verify:browser:coordinator:coverage` checks the browser coordinator and four runtime/refresh verification helpers together at 100% authored coverage. The umbrella browser gate uses this combined run to avoid repeating the native workloads. Failure units supplement actual Chromium/HTTP/WebSocket checks; the installed-client diagnostic must still report incomplete coverage as failure, not a release pass. CI retains the combined map and separate runtime/refresh/client reports. An additional source-build integration requires `REDWEB_VERIFY_CLIENT_SOURCE=1` and the linked client checkout with its development dependencies; ordinary registry-only CI skips that case. The standalone `verify:client:source-coverage` remains the original-source acceptance gate.
277
+
278
+ Ownership and stopped-poll edge cases also use native-browser unit-style tests; browser and transport APIs are not replaced. Runtime coverage now covers all Live HTML modules inside the linked client bundle, excluding its transport prefix; the development-refresh script is measured separately. Whole-application/tool coverage and cross-browser certification remain separate gates.
279
+
280
+ `npm run verify:client:source-coverage` measures the linked client's original TypeScript/JavaScript using one instrumentation map across its Node tests and the native browser tests. Every executable source module starts at zero; erased declarations and static export linkage are separately audited. Both test passes use identical source/test inputs, every Vitest test realm must report, and plain browser bundles must match the linked build byte-for-byte. Reports retain separate Node/browser contributions under `coverage/client-source/<run-id>`. The gate passes all 791 statements, 521 branches, 125 functions and 659 lines, with 77 Node tests per mode plus native-browser acceptance. The client's default `npm test` uses this same complete gate after linkage, build and type checks. Its original Node-only V8 diagnostic remains separately available as `npm run test:v8`, with unchanged thresholds and known missing-browser coverage. Original-source instrumentation does not count every optional-chain short circuit, replace V8 evidence, or mean all tests are mock-free: isolated unit transports remain, while integration/browser tests use actual networking.
281
+
282
+ Client verification also retains raw worker files before parsing or cleanup, including failed runs. Its private coordinator has a separate 100% coverage gate; real Vitest failure fixtures exercise retention without replacing filesystem, compiler or process APIs. See [client development](docs/CLIENT_DEVELOPMENT.md) for commands and scope.
283
+
284
+ `npm run verify:browser:supplements` combines focused units with the existing real-browser runtime cases to require 100% authored-source coverage of the page-ownership and runtime-frame verification helpers, including anonymous callbacks. It is included in the browser coverage gate; see the [coverage scope audit](docs/COVERAGE_SCOPE_AUDIT.md) for exact boundaries and remaining gaps.
285
+
286
+ `npm run verify:dashboard:coverage` measures the dashboard browser verifier separately: failure-boundary units plus actual Chromium, SQLite, sign-in, private card updates, draft preservation and logout checks. Native dashboard tests require the starter's supported Node version; file-lock retention is Windows-specific. The scope is the authored verifier, not internal coverage of its browser-expression strings.
287
+
288
+ An unresolved Linux CI process-cleanup assertion and the diagnostics added to investigate it are tracked in [process cleanup observations](docs/PROCESS_CLEANUP_OBSERVATION.md). Passing runs do not establish its cause or waive the original failure.
289
+
290
+ Edit canonical recipes/guides, then run `npm run generate:docs`; do not maintain independent copies of the examples. See [documentation maintenance](docs/DOCUMENTATION.md) and the [full acceptance checklist](docs/AGENT_READY_ACCEPTANCE.md) for verification evidence and remaining release work.