redweb 0.11.0 → 0.13.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 (167) hide show
  1. package/CHANGELOG.md +163 -0
  2. package/README.md +176 -508
  3. package/bin/redweb.js +11 -0
  4. package/client.d.ts +7 -2
  5. package/config/tsconfig.json +14 -0
  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 +763 -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 -8
  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 +11 -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 +58 -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/snippets/room-access.tsx +51 -0
  66. package/docs/topics.json +21 -0
  67. package/examples/live-html/chatroom.js +207 -268
  68. package/examples/live-html/chatroom.tsx +167 -0
  69. package/examples/live-html/jsx-page.js +1 -1
  70. package/examples/live-html/jsx-page.tsx +1 -1
  71. package/examples/live-html/tsconfig.json +3 -8
  72. package/index.d.ts +170 -45
  73. package/index.js +2 -0
  74. package/jsx-dev-runtime.js +2 -2
  75. package/jsx-runtime.d.ts +7 -2
  76. package/package.json +94 -7
  77. package/recipes/add/artifact.test.cjs +57 -0
  78. package/recipes/add/live.tsx +18 -0
  79. package/recipes/add/socket-route.ts +24 -0
  80. package/recipes/chat/README.md +22 -0
  81. package/recipes/chat/app.test.cjs +105 -0
  82. package/recipes/chat/app.tsx +9 -0
  83. package/recipes/dashboard/README.md +43 -0
  84. package/recipes/dashboard/admin.ts +21 -0
  85. package/recipes/dashboard/app.css +16 -0
  86. package/recipes/dashboard/app.test.cjs +450 -0
  87. package/recipes/dashboard/app.tsx +86 -0
  88. package/recipes/dashboard/auth.ts +80 -0
  89. package/recipes/dashboard/cards.tsx +102 -0
  90. package/recipes/dashboard/rate-window.test.cjs +17 -0
  91. package/recipes/dashboard/store.ts +120 -0
  92. package/recipes/http-ws/README.md +11 -0
  93. package/recipes/http-ws/app.test.cjs +92 -0
  94. package/recipes/http-ws/app.tsx +36 -0
  95. package/recipes/realtime/README.md +8 -0
  96. package/recipes/realtime/app.test.cjs +15 -0
  97. package/recipes/realtime/app.tsx +28 -0
  98. package/recipes/shared/README.md +40 -0
  99. package/recipes/shared/app.css +8 -0
  100. package/recipes/shared/copy-assets.cjs +8 -0
  101. package/recipes/shared/network.cjs +59 -0
  102. package/recipes/shared/run-app.test.cjs +158 -0
  103. package/recipes/shared/run-app.ts +50 -0
  104. package/recipes/site/README.md +4 -0
  105. package/recipes/site/app.test.cjs +19 -0
  106. package/recipes/site/app.tsx +25 -0
  107. package/recipes/socket/README.md +39 -0
  108. package/recipes/socket/app.test.cjs +85 -0
  109. package/recipes/socket/app.tsx +30 -0
  110. package/recipes/socket/contract.ts +12 -0
  111. package/recipes/socket/handlers.ts +40 -0
  112. package/src/OwnedServerLifecycle.js +66 -0
  113. package/src/access/AccessPolicy.js +37 -0
  114. package/src/access/AuthenticationFailure.js +13 -0
  115. package/src/access/RequestFailure.js +33 -0
  116. package/src/access/failure-codes.json +25 -0
  117. package/src/async/BoundedOperation.js +62 -0
  118. package/src/cli/ActionReferences.js +193 -0
  119. package/src/cli/AdditionLayout.js +140 -0
  120. package/src/cli/FilePlan.js +94 -0
  121. package/src/cli/ProjectAddition.js +60 -0
  122. package/src/cli/ProjectConfig.js +26 -0
  123. package/src/cli/ProjectDoctor.js +112 -0
  124. package/src/cli/ProjectInitializer.js +20 -0
  125. package/src/cli/SourceInspector.js +207 -0
  126. package/src/cli/StaticSource.js +192 -0
  127. package/src/cli/arguments.js +62 -0
  128. package/src/cli/formatCommand.js +10 -0
  129. package/src/cli/run.js +57 -0
  130. package/src/cli/templates.js +86 -0
  131. package/src/context/RequestSnapshot.js +41 -0
  132. package/src/dataProperty.js +11 -0
  133. package/src/development/DevelopmentPageManager.js +48 -0
  134. package/src/development/Inspection.js +104 -0
  135. package/src/development/ObservedRenderer.js +42 -0
  136. package/src/development/description.js +35 -0
  137. package/src/development/loopbackRequest.js +27 -0
  138. package/src/development/refreshBrowser.js +96 -0
  139. package/src/development/refreshStyles.js +9 -0
  140. package/src/development/settings.js +17 -0
  141. package/src/docs/Documentation.js +182 -0
  142. package/src/htmx/ActionDefinition.js +44 -0
  143. package/src/htmx/Jsx.js +24 -8
  144. package/src/htmx/LiveHtmlServer.js +41 -19
  145. package/src/htmx/LivePage.js +63 -13
  146. package/src/htmx/PageIdentity.js +32 -0
  147. package/src/htmx/PageLifetime.js +37 -0
  148. package/src/htmx/PageManager.js +203 -74
  149. package/src/htmx/ReactiveRenderer.js +241 -0
  150. package/src/htmx/StaticExporter.js +1 -1
  151. package/src/htmx/TemplateRenderer.js +13 -7
  152. package/src/htmx/browserRuntime.js +2 -93
  153. package/src/htmx/metadata.js +19 -7
  154. package/src/validation/ActionInputError.js +12 -0
  155. package/src/validation/SchemaValidator.js +38 -0
  156. package/src/ws/AdmissionPolicy.js +24 -23
  157. package/src/ws/BaseSocketServer.js +53 -38
  158. package/src/ws/ContractValidationError.js +12 -0
  159. package/src/ws/HeartbeatMonitor.js +19 -7
  160. package/src/ws/ProtocolPolicy.js +1 -1
  161. package/src/ws/RoomAccess.js +82 -0
  162. package/src/ws/RoomRegistry.js +56 -6
  163. package/src/ws/RouteRuntime.js +56 -10
  164. package/src/ws/SocketContract.js +112 -0
  165. package/src/ws/SocketRoute.js +18 -0
  166. package/src/ws/protocol-schema.json +6 -1
  167. package/examples/live-html/chatroom.ts +0 -217
package/docs/LIVE_HTML.md CHANGED
@@ -4,7 +4,20 @@ Live HTML is Redweb's decorator-first server-rendering layer. It uses the existi
4
4
 
5
5
  ## TSX rendering
6
6
 
7
- New pages can return TSX directly. Configure TypeScript with `"jsx": "react-jsx"` and `"jsxImportSource": "redweb"`; Redweb supplies its own dependency-free JSX runtimes and renders immediately to `HtmlFragment` values:
7
+ New pages can return TSX directly. Run `npx redweb init` for a starter project, or extend `redweb/tsconfig.json` from an existing project's root `tsconfig.json`. The preset makes builds and editors use Redweb's dependency-free JSX runtime consistently:
8
+
9
+ ```json
10
+ {
11
+ "extends": "redweb/tsconfig.json",
12
+ "compilerOptions": {
13
+ "rootDir": "src",
14
+ "outDir": "dist"
15
+ },
16
+ "include": ["src/**/*.ts", "src/**/*.tsx"]
17
+ }
18
+ ```
19
+
20
+ Redweb renders TSX immediately to `HtmlFragment` values:
8
21
 
9
22
  ```tsx
10
23
  import { LivePage, action, component, page, state } from 'redweb';
@@ -28,7 +41,7 @@ class CounterPage extends LivePage {
28
41
  return (
29
42
  <Panel title="Server counter">
30
43
  <button rw-click="increment">
31
- Count <output data-rw-state="count">{this.count}</output>
44
+ Count <output>{this.count}</output>
32
45
  </button>
33
46
  </Panel>
34
47
  );
@@ -38,7 +51,29 @@ class CounterPage extends LivePage {
38
51
 
39
52
  Intrinsic elements, fragments (`<>...</>`), nested readonly arrays, and synchronous function components are supported. Strings, numbers, and attributes are escaped once; null, undefined, and boolean children render nothing. Safe existing `html` fragments compose in either direction.
40
53
 
41
- JSX intentionally remains a server serializer rather than a React compatibility layer. It retains no tree and provides no hooks, refs, hydration, client event functions, or object-style API. Use `rw-click`, `rw-submit`, `rw-bind`, and the other Redweb directives for server actions, and use `@page({ css })` or external assets for styling and scripts. Unsafe URL protocols, `on*`, dynamic `style`, `srcdoc`, `srcset`, children on void elements, and executable `<script>` or `<style>` children are rejected.
54
+ JSX remains a server renderer rather than a React compatibility layer: no React hooks, refs, hydration, client event functions, or object-style API. Live sessions retain owner-level HTML snapshots and state dependencies for automatic updates; static pages retain no reactive tree and ship no runtime. Use `rw-click`, `rw-submit`, `rw-bind`, and the other Redweb directives for server actions, and use `@page({ css })` or external assets for styling and scripts. Unsafe URL protocols, `on*`, dynamic `style`, `srcdoc`, `srcset`, children on void elements, and executable `<script>` or `<style>` children are rejected.
55
+
56
+ ## Automatic reactive TSX
57
+
58
+ A decorated state read during `render()` subscribes that page or class component to the state. Changing the property rerenders the affected owners, batches synchronous assignments, and sends changed HTML only. Ordinary expressions such as `{this.count * 2}`, conditional branches, and `.map()` need no state-binding attributes. Function components participate in their enclosing owner's render; use a class `@component()` for an independently stateful boundary.
59
+
60
+ ```tsx
61
+ render() {
62
+ return <ul>{this.cards.map(card => (
63
+ <li key={card.id}><input name="title" value={card.title} /></li>
64
+ ))}</ul>;
65
+ }
66
+ ```
67
+
68
+ Keys must be stable strings or numbers (at most 256 characters), unique among siblings. Keyed elements and fragments preserve their DOM nodes during moves. Unchanged server values preserve unsent input; a changed server `value` or `checked` attribute intentionally updates the control. Focus and text selection are preserved when their node survives. Removing a keyed item removes its local input state. Unkeyed repeated items do not promise identity across reordering.
69
+
70
+ Select controls preserve surviving selected options whose values are unchanged, even when several options have the same value. Replaced options fall back to available matching values without selecting every duplicate. Changed server-authored `selected` defaults intentionally update the selection; reordering the same keyed defaults does not discard a different unsent choice. Use stable option keys when option identity matters.
71
+
72
+ Updates use `redweb:patch` with owner patches and any explicit state bindings in one frame. Existing non-TSX pages continue using `redweb:state`. Explicit `data-rw-state` and `rw-bind` directives can coexist with TSX; do not combine a direct binding with a different derived expression on the same element. The runtime reconciles HTML rather than executing browser components. Internal HTML comments delimit components/keys without introducing layout wrappers, including inside table bodies and selects.
73
+
74
+ State changes remain assignment-driven. Mutating an array or object in place is not observed; assign a new value. `render()` must be side-effect-free with respect to decorated state (writes during rendering throw). Loading, connections, timers, and persistence belong in lifecycle hooks or actions, which are not rerun for UI patches. Hiding an element is not an authorization boundary for its actions.
75
+
76
+ Each HTTP/page session retains its own request context and snapshots, even when the underlying page state is shared. Reconnect sends a current root snapshot. Disconnect discards unfinished update results; session disposal aborts its render signal and releases snapshots. Async rendering has a five-second limit, and a snapshot tree is bounded to 1 MiB of retained HTML and 1,024 owners. These bounds include nested snapshots, not just visible document size. As with ordinary JavaScript, synchronous application code cannot be preempted; async work should honor cancellation and avoid unbounded operations. A failed update is logged and closes the affected connection instead of emitting partial HTML.
42
77
 
43
78
  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.
44
79
 
@@ -235,11 +270,11 @@ save(form: { displayName: string }) {
235
270
  </form>
236
271
  ```
237
272
 
238
- `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 })`.
273
+ `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 an unchanged, still-connected form after the server acknowledges success. `rw-bind="property"` sends text values or checkbox state only when that property was declared with `@state({ writable: true })`.
239
274
 
240
275
  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.
241
276
 
242
- 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.
277
+ The document emits `redweb:connection` events as transport state changes and `redweb:error` events when an interaction fails. Interactions require an open connection; they are not queued during initial connection or reconnect. Actions use request/response operations and are never automatically replayed.
243
278
 
244
279
  Names such as `constructor`, `prototype`, and `__proto__` are rejected. Arbitrary methods and undeclared state cannot be reached through the Live HTML protocol.
245
280
 
@@ -256,10 +291,134 @@ Timers and subscriptions created by a page should be owned by that page and stop
256
291
 
257
292
  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.
258
293
 
294
+ Live HTML shuts down sockets, page resources, and its owned HTTP listener in successive phases. `shutdownTimeoutMs` bounds phases rather than imposing one total wall-clock deadline. The final HTTP phase also waits up to this duration before destroying remaining TCP peers, including incomplete HTTP requests and unfinished TLS handshakes. This applies to both static and live pages, even when native listener close has already started. Successful forced transport closure does not prove that application work completed, data was persisted, or a response reached its client. Cleanup failures remain reported after the other phases are attempted. Applications must separately close their database handles, workers, and other resources; arbitrary synchronous work cannot be preempted by a JavaScript timer.
295
+
259
296
  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.
260
297
 
261
298
  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.
262
299
 
300
+ ## Validated action inputs
301
+
302
+ Use the same Standard Schema v1 validators supported by socket contracts to validate a form once, at the server boundary. Redweb adds no runtime schema-library dependency; install your chosen validator in the application (`npm install zod` for this example).
303
+
304
+ ```tsx
305
+ import { action, page, start, state, type ActionInput } from 'redweb';
306
+ import { z } from 'zod';
307
+
308
+ const input = z.object({
309
+ amount: z.string().regex(/^\d+$/).transform(Number).pipe(z.number().int().min(1).max(1000)),
310
+ }).strict();
311
+
312
+ @page('/')
313
+ class AmountPage {
314
+ @state() total = 0;
315
+
316
+ @action({ input })
317
+ save(value: ActionInput<typeof input>) {
318
+ this.total += value.amount;
319
+ }
320
+
321
+ render() {
322
+ return <form rw-submit="save">
323
+ <label>Amount <input name="amount" /></label>
324
+ <button type="submit">Add</button>
325
+ <output>{this.total}</output>
326
+ </form>;
327
+ }
328
+ }
329
+
330
+ start(AmountPage);
331
+ ```
332
+
333
+ The browser sends form values as one object (repeated names become arrays). The schema converts `amount` from its submitted string to an integer between 1 and 1,000, rejecting overflow and out-of-range values after conversion. `ActionInput<typeof input>` describes that transformed result; TypeScript cannot infer a method parameter annotation from its decorator. An optional second `LivePageConnectionContext` parameter receives trusted server context, never a caller-supplied replacement. Both standard and legacy TypeScript decorators are supported, including scoped component actions. A validated action accepts exactly one submitted argument; ordinary `@action()` retains its existing argument behavior.
334
+
335
+ Invalid input produces `ACTION_INVALID_INPUT`, does not invoke the method, and leaves the socket open for correction. The browser reports it through `redweb:error` and does not reset the failed form. Validator exception details, submitted values, and raw schema issues are not returned to the browser. A throwing validator or malformed validator result remains a sanitized `HANDLER_FAILED` server error, not a recoverable user mistake.
336
+
337
+ Validation has a five-second default deadline; override it with `@action({ input, validationTimeoutMs: 500 })`. A validation deadline produces `ACTION_VALIDATION_TIMEOUT`; an interrupted validation produces `ACTION_CANCELLED` if the connection can still receive a response. Neither invokes the action. Disconnects and disposal prevent an outstanding validation result from starting application code later. These limits apply to validation, not to an action that has already started: Redweb cannot undo its side effects, preempt synchronous JavaScript, or stop external work inside a validator. It does not automatically retry actions. Prefer pure validators, and implement application-specific cancellation/idempotency where needed.
338
+
339
+ The same bounded validation implementation is shared with socket contracts. Their existing `INVALID_PAYLOAD` contract error behavior is unchanged. Automatic action feedback reports safe form-level messages, not raw validator issues or field-level messages.
340
+
341
+ ## Action authorization
342
+
343
+ Identity and permission are separate: the server's existing `authenticate(request)` hook establishes `context.principal`; an action policy decides whether that identity may perform this operation. On this unreleased branch, add `authorize` to the action decorator instead of repeating permission checks inside each method:
344
+
345
+ ```tsx
346
+ // Inside a page/component; `input` is the amount schema from the example above.
347
+ @action({
348
+ input,
349
+ authorize: (context, value) => context.principal === 'owner' && value.amount <= 10,
350
+ })
351
+ save(value: ActionInput<typeof input>) {
352
+ this.total += value.amount;
353
+ }
354
+ ```
355
+
356
+ The policy receives **trusted context first, transformed input second**. Only `true` permits invocation. The check runs after validation on every invocation, so a permission change during asynchronous validation is visible to the policy. Both standard and legacy decorators and component-scoped actions follow the same path. The literal owner check above illustrates the API, not an authentication system: applications must verify real credentials in `authenticate`, query their own current permissions, and enforce database ownership/transaction rules.
357
+
358
+ For a button without a schema, use `@action({ authorize: context => context.principal === 'owner' })`. Such methods use the fixed signature `run(input: unknown, context: LivePageConnectionContext)`; with no submitted payload, `input` is `undefined`. A caller can supply at most one untrusted input, never replace the second context argument. Use a schema whenever you inspect submitted values. Ordinary `@action()` keeps its existing variadic behavior.
359
+
360
+ Policies may be asynchronous. `authorizationTimeoutMs` defaults to 5,000 ms and requires an `authorize` callback. This deadline is separate from `validationTimeoutMs`; neither bounds a method that has started. The policy's `context.signal` aborts when the connection closes or the permission deadline expires. Pass it to cancellable application operations. Redweb cannot preempt synchronous code, cancel work that ignores the signal, undo policy side effects, or make a policy check and later database write atomic; keep policies read-only and enforce transactional authorization in storage where required. An overdue or cancelled result cannot invoke the action later. Page disposal also prevents invocation, but does not itself stop ongoing external policy work.
361
+
362
+ Denial returns recoverable `ACCESS_DENIED`; timeout returns `ACCESS_TIMEOUT`; connection cancellation returns `ACCESS_CANCELLED` when a response can still be delivered. None invokes the action. Built-in feedback shows safe text and retains the draft. A thrown/rejected policy is a sanitized `HANDLER_FAILED` application failure, not a permission denial, and must be investigated rather than blindly retried. Authentication/permission secrets and submitted values are never included in these protocol errors.
363
+
364
+ **An action policy protects action invocation only.** It does not protect HTTP rendering, loading hooks, writable state, room publication, or passive subscriptions. Use the page policy and explicit session revocation below for page access. Shared page state is shared across identities, not private per user. Durable dashboard and room-policy recipes remain separate acceptance items.
365
+
366
+ ## Protected pages and shared request identity
367
+
368
+ On this unreleased branch, a page can declare `authorize(context)` alongside its route. This is an API pattern for an application that already supplies the server's `authenticate(request)` hook, not a standalone login system:
369
+
370
+ ```tsx
371
+ @page('/account/:id', {
372
+ authorize: ({ principal, params }) => principal === params.id,
373
+ authorizationTimeoutMs: 500,
374
+ })
375
+ class AccountPage {
376
+ render(context: LivePageRequestContext) {
377
+ return <h1>Account {context.params.id}</h1>;
378
+ }
379
+ }
380
+ ```
381
+
382
+ Redweb reserves render capacity, captures the request, resolves identity, and checks permission **before constructing this page or running its loading hooks**. Only `true` allows access. Protected pages require connection scope; `shared: true`/`scope: 'shared'` are rejected because a shared mutable instance is not private per identity. Keep public shared counters/chat state separate from private account state. Page policies are checked again on socket admission/reconnect, immediately before actions (after input validation and action authorization), and before browser-writable state changes. Returning false denies that operation; it does not automatically disconnect idle viewers.
383
+
384
+ `authenticate(request)` still receives the real HTTP/upgrade request, so it can use the application's existing cookie/session/token implementation. It must verify credentials and return a primitive identity: string, finite number, bigint, or `true`. False/null/undefined and objects/functions/symbols/non-finite numbers are rejected. An upgrade must authenticate as the same identity that rendered its page token. `authenticationTimeoutMs` defaults to 5,000 ms and requires an authentication hook. A timeout prevents later admission, but cannot stop external work inside that hook. Redweb does not provide credential storage, login endpoints, or distributed session invalidation.
385
+
386
+ Loading/rendering, connected/disconnected hooks, and actions share the original HTTP page's `request`, `params`, `query`, `body`, and `principal`. `LivePageConnectionContext` extends `LivePageRequestContext` and adds `socket`; its signal belongs to the current connection. The request does not become the upgrade URL when reconnecting. It is a deep-frozen copy of supported fields, not an Express request: path, URL, method, headers, params, query, JSON-compatible body, and a case-insensitive header `get()`. It never retains an Express response/socket graph or freezes application-owned objects. Nested data has a depth limit of 16 and a conservative 64 KiB aggregate budget, including per-value overhead; arrays are additionally limited to 8,192 entries. Dates, functions, and other unsupported body values must be normalized by application middleware. Header values, including credentials, remain private server-side data; do not render or log them unnecessarily.
387
+
388
+ Denied HTTP authentication returns `AUTHENTICATION_REQUIRED` (401); authentication timeout/cancellation return `AUTHENTICATION_TIMEOUT`/`AUTHENTICATION_CANCELLED` (503); authentication hook failures return sanitized `AUTHENTICATION_FAILED` (500). Page permission denial is `ACCESS_DENIED` (403), with bounded policy timeout/cancellation at 503. Broken policies or protected-page application errors return sanitized `PAGE_FAILED` (500). Protected responses, including errors and non-live pages, are `private, no-store` and never use conditional 304 responses. `exportStatic()` and `defineSite().export()` reject authorized pages before construction or final output writes.
389
+
390
+ ## Explicit session revocation
391
+
392
+ After invalidating a credential or changing permissions in your own authority, call `await server.revoke(principal)` before publishing further private updates. `server` is the object returned by `start()`. This revokes matching rendered page tokens, live connections, and unfinished renders in this process. It is not a permanent identity denylist: a later HTTP request may establish a new session only if your authentication and page policy still allow it. Coordinate revocation across every application instance yourself.
393
+
394
+ All affected lifetimes are marked unavailable and their transports stopped synchronously, before application-visible abort listeners or cleanup hooks run. Therefore an abort listener cannot publish a final framework state update to another affected connection. Old page tokens cannot reconnect, and late authentication, policy, loading, connection-hook, validation, or render completions cannot restore them. In-flight identity lookups whose principal is not yet known are conservatively cancelled too; an unrelated in-progress login may need retrying. The returned number counts affected page sessions/render operations, including those unresolved lookups, not unique people or sockets.
395
+
396
+ Application disconnect/disposal cleanup is awaited up to `shutdownTimeoutMs`. `REVOCATION_CLEANUP_FAILED` means access has already been revoked but cleanup rejected or exceeded the deadline; it never restores access. Revocation cannot retract data already sent/buffered, roll back application side effects that already started, or cancel external work that ignores its signal. Ordinary network disconnect cancels connection work but preserves eligible session state for reconnect; explicit revocation permanently invalidates that page token. Abandoned HTTP requests cancel their render lifetime and release framework capacity even if a loading hook ignores cancellation.
397
+
398
+ Use `LiveHtmlStartOptions` for wrappers around `start()`; it preserves the authentication/timeout constraints without writing `Omit<LiveHtmlServerOptions, 'pages'>`. The starter recipes use this shorter public type.
399
+
400
+ ## Automatic action feedback
401
+
402
+ Existing `rw-click` buttons and `rw-submit` forms show **Working…**, **Done.**, or a safe error message without custom browser JavaScript. Redweb inserts a plain-text status span at the end of a form or immediately after a click control, with `role="status"` and `aria-live="polite"`. The control and its status have `data-rw-status="pending"`, `"success"`, or `"error"` for application CSS. Native form constraints still run before submission.
403
+
404
+ For deliberate placement, supply a slot in the same component (or page scope):
405
+
406
+ ```tsx
407
+ <form rw-submit="save">
408
+ <label>Amount <input name="amount" /></label>
409
+ <button type="submit">Save</button>
410
+ <output rw-status="save" />
411
+ </form>
412
+ ```
413
+
414
+ This replaces the automatic span for that action. Slots receive text, never HTML. Redweb preserves authored accessibility attributes; use an `output`, or add an appropriate live-region role to another element. Slots are component-scoped, including wrapper-free/nested components. If several controls in one scope share a slot, the most recently started invocation owns that slot; an older completion cannot overwrite it. Do not combine `rw-status` with a server-rendered state binding on the same node.
415
+
416
+ Each control allows one pending invocation; repeated clicks/submits from that same DOM node are ignored until it settles. Other controls remain independent, with a fixed page-wide maximum of 32 outstanding actions. This is UI duplicate suppression, not authorization, server rate limiting, or an exactly-once guarantee. Inputs stay editable and controls keep their authored accessibility/disabled attributes. A successful form resets only if its node, action binding, submitted values, and input/change revision are unchanged. New drafts, failed forms, and replacement forms are never cleared by an old response. Use stable JSX keys to preserve the intended node identity during reordering.
417
+
418
+ Feedback follows surviving nodes through server patches, including replacement status slots; removed controls release their generated status nodes and clear slots they still own. A replacement control does not inherit an old invocation's outcome. The most recently started invocation keeps ownership when controls share a slot, so late results cannot overwrite its feedback.
419
+
420
+ Disconnected actions are not queued, and actions are never automatically retried. The browser reports a known-unsent action separately from an unconfirmed result. A lost connection, response timeout, or application failure can occur after side effects; the message asks the user to check before trying again. Successful completion confirms the response, not durable persistence. Applications remain responsible for transactions, idempotency, and durable storage. Browser state writes are also not queued while disconnected. `redweb:error` remains available for application-level reporting, and `data-rw-connection` on the document element reflects the current client connection state.
421
+
263
422
  ## Browser transport
264
423
 
265
424
  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.
@@ -274,8 +433,8 @@ The injected module uses the published `redweb-client` package served by the sam
274
433
  - `sessionTtlMs`: pending/reconnect session lifetime; defaults to 30 seconds.
275
434
  - `maxSessions`: maximum pending plus active page sessions; defaults to 1,000.
276
435
  - `maxConcurrentRenders`: maximum simultaneous HTTP page renders, independent of live session occupancy; defaults to `maxSessions`.
277
- - `shutdownTimeoutMs`: maximum render/route drain time before forced cleanup; defaults to one second.
278
- - `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.
436
+ - `shutdownTimeoutMs`: phase-local render/route drain and final owned-HTTP cleanup timeout, not a total application shutdown deadline; defaults to one second.
437
+ - `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. An expired pong check is deferred to the event-loop check phase before termination, allowing already-dispatched pong handling to win after a server stall. A silent peer is still terminated by the deferred check. Scheduler latency means `timeoutMs` is a liveness threshold, not a hard wall-clock deadline; use connection and queue limits as the resource bounds.
279
438
  - `authenticate`: optional HTTP/WebSocket identity function for binding page sessions to an authenticated principal.
280
439
  - `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).
281
440
  - `livePaths`: optional `{ socket, client, runtime }` internal path overrides.
@@ -285,13 +444,15 @@ The internal paths and application page paths must be unique.
285
444
  ## Verification examples
286
445
 
287
446
  - `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.
288
- - `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.
447
+ - `examples/live-html/chatroom.tsx` 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. Join/send use `@action({ input })` with shared Zod text schemas for normalization, bounds and inferred `ActionInput` types; invalid input gets automatic form feedback before the method runs. The chat starter includes Zod as an application dependency, not a new Redweb runtime dependency.
289
448
  - `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.
290
449
  - `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.
291
450
  - `examples/live-html/jsx-page.tsx` uses Redweb's automatic JSX runtime, a function component, decorated state, and a server action without HTML template strings.
292
451
 
293
452
  Run the examples immediately with `npm run example:counter`, `npm run example:chatroom`, `npm run example:cards`, `npm run example:components`, and `npm run example:jsx`. Their checked-in JavaScript artifacts are generated from the decorated TypeScript or TSX 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`.
294
453
 
454
+ These commands assume the cloned repository's development dependencies are installed. For the packed chat example used directly in another application, install `zod` there first; the generated chat starter already declares it. Core Redweb and the counter example remain usable without a validator library.
455
+
295
456
  ## Static pages and documentation export
296
457
 
297
458
  Set `live: false` when a page needs server rendering but no realtime session:
@@ -0,0 +1,98 @@
1
+ # Live HTML load-verifier correction
2
+
3
+ This unreleased increment changes verification tooling, not Redweb rendering or
4
+ client runtime behavior. It does not resolve the separate disabled-feature
5
+ throughput discrepancy documented in `BENCHMARK_VERIFICATION.md`.
6
+
7
+ ## Defects and correction
8
+
9
+ An unchanged `getPage` helper was executed against an actual local HTTP server
10
+ returning malformed bootstrap JSON. It emitted an uncaught `SyntaxError` while
11
+ its promise remained unsettled. The probe used the exact original helper source
12
+ without replacing HTTP, promises or event behavior. The old helper also lacked
13
+ response deadlines, body bounds and aborted-response handling.
14
+
15
+ `readLiveHtmlPage` now bounds the response at10 seconds/1MiB, catches parsing and
16
+ configuration failures, and independently closes the request and response. Native
17
+ request closure has a five-second observation bound; failures retain their causes.
18
+ Requests use `agent:false` so this verifier owns non-pooled HTTP transports. That
19
+ is a harness change: heap deltas are not byte-identical repeats of historical
20
+ pooled-request measurements, nor evidence of a rendering optimization.
21
+
22
+ `LiveHtmlLoadClient` keeps every actual WebSocket created by RedwebClient until
23
+ closure is confirmed. Opening has a native five-second handshake timeout and a
24
+ ten-second connection bound. Cleanup reuses the existing socket helper's bounded
25
+ graceful-close/forced-termination path. Unexpected disconnects, protocol errors
26
+ and malformed patches latch failures instead of disappearing in listener error
27
+ handling. Concurrent request and previously latched client failures remain visible
28
+ alongside cleanup failures; the critic's combined-error finding has a regression.
29
+
30
+ The coordinator settles parallel acquisition before proceeding, retains clients
31
+ until successful closure, attempts every cleanup independently, requires explicit
32
+ GC, and emits success only after server shutdown. A single broadcast establishes
33
+ delivery, not ordering; the verification label now says that precisely.
34
+
35
+ ## Preserved acceptance
36
+
37
+ - 200 initial renders; pending-session count and expiry.
38
+ - 110 live clients; initial patches, joins, all-client presence, capped visible
39
+ membership, and one broadcast delivered to every client.
40
+ - Session TTL1,000ms, maxSessions500, the same three GC calls and50ms settling
41
+ pause, disconnected-session expiry and a 24 MiB heap budget sampled after
42
+ client closure/session expiry, before server shutdown.
43
+
44
+ No workload, production option or acceptance threshold was relaxed. Native tests
45
+ separately establish ownership mechanics; they do not replace clean measurements.
46
+
47
+ ## Verification evidence
48
+
49
+ Windows / Node22.21.0:54 tests across three suites pass in25.128 seconds, with
50
+ all-four100% coverage across the coordinator and two helpers:195 statements,
51
+ 49 branches,52 functions and139 lines. The maintained command is
52
+ `npm run verify:live-html:load:coverage`. CI runs it with20-minute supervision and
53
+ retains reports on failure or success; the separate default gate has four minutes.
54
+
55
+ Native tests use actual HTTP/WebSockets/processes for valid/malformed/missing/
56
+ oversized/aborted/silent responses, malformed patches/protocol messages, server
57
+ errors, unexpected disconnects, unanswered upgrades, paused close handshakes,
58
+ missing-GC rejection and the unchanged full CLI workload. Explicit boundary
59
+ units cover synchronous throws, cleanup failures, timeout policy, stage failures,
60
+ heap-budget rejection and suppression of success after failed shutdown.
61
+
62
+ The first unanswered-upgrade fixture incorrectly expected an upgraded raw HTTP
63
+ peer to fully close when the client closed. A native probe confirmed local
64
+ WebSocket CLOSED(3), server readable-ended=true and writable-ended=false. The
65
+ fixture now consumes the FIN and ends its own half-open writable side; it does
66
+ not answer the upgrade or weaken the assertion about client ownership. HTTP and
67
+ WebSocket test budgets include acquisition, operation and all cleanup phases.
68
+
69
+ The critic approved both corrections and the final scope, then verified all 14
70
+ remote blobs on actual PR head `d15b1a3`. Source SHA-256:
71
+
72
+ | Source | SHA-256 |
73
+ | --- | --- |
74
+ | `scripts/verify-live-html-load.js` | `345610cc75f6cf0f14ed5c3bb203fa68228dabb6d7b088ca208c8c6ce934e72b` |
75
+ | `scripts/lib/readLiveHtmlPage.js` | `bc28f4e2319e1a50517937f1c61e92406ffa4bf1f84294cc595fc420bb03fe25` |
76
+ | `scripts/lib/LiveHtmlLoadClient.js` | `d8df0f083d531d0f1e2cbad2ca83f80eb19f5238147ca18ff505ec057134df61` |
77
+
78
+ Report `coverage/live-html-load-tools/coverage-final.json` SHA-256:
79
+ `7d3ca3f938dc7dca75919add49808f538cebbb53ecef2539ed1cf24d9985b049`.
80
+ After the focused suites exited, one clean default run passed200 renders/110
81
+ clients with 6,824,576-byte heap growth after client closure/session expiry and
82
+ before server shutdown, against 24 MiB. This is a scoped
83
+ increment, not final release approval or a replacement for remaining hosted/package
84
+ gates. No publication, deployment or merge occurred.
85
+
86
+ The full regression selected at `d15b1a3` subsequently passed 1,152 tests across
87
+ 113 suites in 631.578 seconds, with two POSIX-only skips on Windows. All 91 library
88
+ files retain 100% coverage: 5,449 statements, 4,046 branches, 978 functions and
89
+ 4,468 lines. Library and HTML-verifier sources remained unchanged throughout.
90
+ The 14 later JSX-verifier tests were added after inventory selection and passed
91
+ separately; they are not included in 1,152. Generated-content/type checks pass.
92
+
93
+ Full report `coverage/coverage-final.json` SHA-256:
94
+ `ae7eca89c22dbb9cc9b7adacf8789921a37f2f835b6b537374a40830f52119dd`.
95
+ Inventory `coverage/html-load-full-results.json` SHA-256:
96
+ `cb36a54b43a27d2e458e4c65da0ae077f9880369f97e836094a48544740daff6`.
97
+ Hosted runs PR33367978260 and push33367973847 were still running when this
98
+ evidence was recorded; partial job success is not a full workflow pass.
@@ -0,0 +1,28 @@
1
+ # Upgrade an existing Redweb application
2
+
3
+ Match the installed package to its versioned documentation. This checkout contains unreleased work even while its package metadata still matches an older npm version. See [release verification](RELEASE_TRUST.md) and the changelog shipped with your selected package. Keep your lockfile and rollback artifact, and run your own real HTTP/WebSocket/browser tests after upgrading.
4
+
5
+ ## 0.8 migration notes
6
+
7
+ - Unmatched WebSocket paths are rejected unless `fallbackToRoot: true` is configured.
8
+ - Handler exception details are hidden unless `exposeErrors: true` is configured. Do not expose private exception messages in production.
9
+ - Shutting down a WebSocket server no longer closes a caller-supplied HTTP/HTTPS server by default. Explicitly set `closeServerOnShutdown: true` only when handing cleanup responsibility to that socket server.
10
+ - `bind` is honored by HTTP, HTTPS, WebSocket, and secure WebSocket listeners.
11
+ - `shutdown()` is asynchronous; await it when deterministic cleanup matters. Awaiting a shutdown is not a delivery or persistence guarantee.
12
+
13
+ ## 0.9 migration notes
14
+
15
+ - No migration is required when the new multiplayer options are disabled.
16
+ - Production controls are route-local and opt-in; size them from measured capacity rather than copying example limits.
17
+ - `ProtocolClient` is available from `redweb/client` for negotiated protocol routes without adding runtime dependencies. It wraps a transport; your application creates and reconnects that transport.
18
+ - Node.js 18 is the installation/legacy-compatibility floor, not a recommendation to deploy an end-of-life runtime. Use a maintained LTS release with current security patches; check [runtime compatibility](RELEASE_TRUST.md).
19
+
20
+ ## Live HTML migration
21
+
22
+ The executable `.htmx` sandbox and `enableHtmxRendering` option were replaced. Templates are ordinary `.html` files registered through decorated plain classes. Move calculations and imports into the page class, mark reactive fields with `@state()`, expose browser-callable methods with `@action()`, and start the page with `start(PageClass)`.
23
+
24
+ For server-rendered TSX, extend `redweb/tsconfig.json`; do not configure React's JSX runtime. `redweb init --existing` creates a missing root configuration without overwriting one you already have. Check the effective configuration with your installed CLI: `npx --no-install redweb doctor --json`. Review warnings and fix errors before compiling; preservation does not imply correctness.
25
+
26
+ In the reactive-rendering candidate, ordinary TSX expressions reading decorated state update after assignment. Replace arrays/objects instead of mutating them in place. Use stable JSX keys for lists. Existing explicit HTML bindings remain supported. See [rendering and lifecycle](LIVE_HTML.md) for owner isolation, component lifetimes and reconnect behavior, and [runtime diagnostics](RUNTIME_DIAGNOSTICS.md) for failure categories and retry limits.
27
+
28
+ Shared page state is process-local, not durable or automatically private. Add explicit identity, authorization and persistence for your application. The [private dashboard recipe](../recipes/dashboard/README.md) demonstrates one single-process implementation; it is not a distributed session store.
@@ -37,14 +37,36 @@ Redweb limits the number and lifetime of session records, but it deliberately do
37
37
 
38
38
  ## Verification
39
39
 
40
- Run `npm test` for unit, real HTTP/WebSocket/WSS integration, fuzz, type-generation, and 100% coverage gates. The additional production gates are:
40
+ Run these commands from the matching Redweb source checkout. `npm test` includes unit, real HTTP/WebSocket/WSS integration, fuzz, type-generation, and enforced 100% coverage of the declared library scope; browser, package and verifier-source coverage have separate commands. The additional production gates are:
41
41
 
42
42
  ```bash
43
43
  npm run verify:load
44
44
  npm run verify:memory
45
- npm run verify:recovery
45
+ npm run verify:recovery:server
46
46
  npm run verify:soak
47
- npm run verify:overhead -- /path/to/redweb-0.8-baseline
47
+ npm run verify:overhead -- /path/to/prepared-release-baseline
48
48
  ```
49
49
 
50
- The soak defaults to 60 minutes. Shorter durations are useful for CI smoke checks but are not release evidence.
50
+ The soak defaults to 60 minutes. Shorter durations are useful for CI smoke checks but are not hour-soak acceptance. Its 99% delivery allowance is not a lossless guarantee; inspect actual sent, received and missing counts as well as all resource trends.
51
+
52
+ Prepare and identify the intended comparison release separately, using the same machine, Node runtime and controlled environment as the candidate. The overhead command does not choose a baseline version for you. The 3% throughput and 5% p99 regression limits remain unchanged; historical 0.8 comparisons do not certify a newer candidate against its previous release.
53
+
54
+ ### Blocking server recovery
55
+
56
+ `npm run verify:recovery:server` uses the approved `server-steady-v1` contract with separate coordinator, server and native load-generator processes. It preconditions with 1,200 connections, warms with 200, then runs five storms of 1,200 in batches of 50: 7,400 exact exchanges. Phase samples settle for 400 ms and collect twice. Every storm must retain at most 110% of the **same** warmed server heap; client heap is diagnostic, not subject to that server budget.
57
+
58
+ Exact client sends/replies and server receives must reconcile. Measured registries must be empty, input fingerprints unchanged, logs complete, and workers must exit normally with closed output pipes. Forced cleanup cannot produce a pass. A bad middle storm still fails even if the final storm recovers. This finite workload is not proof of an indefinite memory plateau or a resolution of historical shared-process failures.
59
+
60
+ The server gate rejects workload overrides, Node flags, nonempty `NODE_OPTIONS` and `NODE_V8_COVERAGE`, including `REDWEB_RECOVERY_*` variables. It creates an exclusive report directory under `coverage/`; an optional absolute, nonexistent destination can follow `--`. Do not use instrumented or snapshot runs as clean memory evidence. CI bounds this command at two minutes and retains available evidence after success or failure.
61
+
62
+ ## Original recovery diagnostic
63
+
64
+ `npm run verify:recovery` remains a visible **non-blocking** CI diagnostic. It measures server and load-generator work together, so its heap ratio is not the server-focused measurement above. It retains its own exit status and logs and runs in CI only after server acceptance confirms worker cleanup. A diagnostic failure or skip is not reported as a pass.
65
+
66
+ The original command defaults to the versioned `steady-v2` protocol: one fixed 1,200-connection preconditioning workload, 200 warm connections, then five 1,200-connection storms, in batches of 50. After each phase it waits 400 ms for expiry, collects twice, and requires empty client/room/session registries. Every storm must retain at most 110% of the **same** shared-process warm baseline. It never moves the baseline, subtracts compiled-code bytes, or repeats a failed run until one passes.
67
+
68
+ For this original diagnostic only, `REDWEB_RECOVERY_WARM_CONNECTIONS`, `REDWEB_RECOVERY_STORM_CONNECTIONS`, and `REDWEB_RECOVERY_BATCH_SIZE` select positive safe-integer workload sizes; preconditioning always uses the selected storm size. `REDWEB_RECOVERY_STORM_ROUNDS` can increase the five-round minimum. Reports include the selected protocol, phase heaps, counts and every storm's ratio. Smaller custom traffic is useful for functional checks but is not the fixed server acceptance workload.
69
+
70
+ Set `REDWEB_RECOVERY_PROTOCOL=cold-v1` to reproduce the earlier unpreconditioned protocol (200 warm connections and one storm by default). Its recorded Node 20 failures remain failures; later steady-protocol results do not rewrite them. The revised warm-up is supported by native heap diagnostics showing substantial compiled-code growth after the earlier baseline and by fixed repeated-storm experiments. See the [acceptance work log](AGENT_READY_ACCEPTANCE.md) for exact environments, measurements and outstanding release gates.
71
+
72
+ For investigation only, `REDWEB_RECOVERY_DIAGNOSTICS=1` adds native V8 space/code statistics. Combining it with an absolute `REDWEB_RECOVERY_HEAP_DIRECTORY` creates exclusive private warm/recovered snapshot files in an existing directory. Snapshots may contain secrets and introduce additional GC/work: use an isolated process/environment, never upload the raw files, and do not treat snapshot runs as acceptance. `scripts/diagnostics/recovery-heap-summary.cjs` accepts the two files and emits only fixed-label numeric aggregates; delete private snapshots after investigation.
@@ -0,0 +1,100 @@
1
+ # Original recovery verifier: exact boundary and maintained coverage
2
+
3
+ The shared-process command remains a visible, non-blocking diagnostic. The
4
+ separate server recovery contract remains blocking. Neither historical failed
5
+ measurements nor the unresolved performance/cleanup observations are relabelled
6
+ by this correction.
7
+
8
+ ## Correctness
9
+
10
+ The previous comparison rejected `(1100 / 1000) * 100`, which JavaScript rounds
11
+ to `110.00000000000001`, although the byte counts are exactly at the 110% limit.
12
+ Expanded boundary tests reproduced two false rejections: 1,000/1,100 bytes and
13
+ 1,000,000,000,000,030/1,100,000,000,000,033 bytes. The 28-case baseline had
14
+ 26 passes and two failures despite 100% authored coverage; coverage alone did
15
+ not detect the missing equality oracle.
16
+
17
+ The verifier now compares `BigInt(heap) * 100n > BigInt(warmedHeap) * 110n`.
18
+ Equality passes, one byte over fails, and an intermediate over-budget cycle
19
+ still fails even if the final cycle returns to its warm value. This is exact
20
+ for the safe-integer byte observations tested; it cannot restore precision
21
+ already lost before a Number value is supplied. Displayed percentage fields,
22
+ workload, settling, snapshots and the strict any-cycle 110% limit are unchanged.
23
+ This narrow rounding bug does not explain the larger historical failures.
24
+
25
+ ## Maintained scope
26
+
27
+ `npm run verify:recovery:original:coverage` runs 28 explicit boundary units and
28
+ the existing 17-test verifier suite, including actual HTTP/WebSocket traffic,
29
+ native CLI configuration refusal and private V8 snapshot checks. The units
30
+ provide the authored coverage map; the native child processes are not
31
+ instrumented by this gate. Synthetic unit transports, clocks and heaps are not
32
+ network delivery or memory evidence. Native fixtures use small connection
33
+ counts, not the default 7,400-connection acceptance workload.
34
+
35
+ On Windows / Node 22.21.0, the combined selection passed all 45 tests in
36
+ 35.606 seconds: 112 statements, 71 branch outcomes, 20 functions and 93 lines,
37
+ all 100%. Source and statement/function/branch maps are checked before merging
38
+ the unit measurements. CI runs this separate bounded gate and retains available
39
+ coverage artifacts on success or failure; raw private heap snapshots are never
40
+ included in that artifact path.
41
+
42
+ Evidence (SHA-256):
43
+
44
+ - Corrected LF-normalized source: `fec32203599936362243fbd8fbf33d310852dccd727668f68da9ecfe9bee3de7`.
45
+ - Previous LF-normalized source: `95d56f52d5f9668b6dbfa8e8158f7ae86e128d65103e127abaed85fb3bab37c1`.
46
+ - Combined map, `coverage/original-recovery/coverage-final.json`: `bc0b50cb38278fb96dce51cacc1676ec9ffb58e5c27088145617a1f6bef54405`.
47
+ - Failed expanded baseline map remains separately at `coverage/original-recovery-boundary-baseline/coverage-final.json`.
48
+
49
+ ## Full-suite failure and session observation correction
50
+
51
+ The preceding full Windows run at `0e3e257` failed: 1,893 passed, three failed,
52
+ five skipped across 172 suites in 1,386.598 seconds. Library coverage was 100%,
53
+ but that does not make the run successful. Two isolated-package tests timed out
54
+ during npm installation; actual npm logs show repeated
55
+ `UNABLE_TO_VERIFY_LEAF_SIGNATURE`. A read-only registry control succeeded with
56
+ Node's system certificate store and TLS verification retained. The retained
57
+ second log is `coverage/release-0e3e257/2026-08-31T19_08_04_026Z-debug-0.log`,
58
+ SHA-256 `6668414f7a4e4ee8eb456ff26879af4e950207d103ad3bdbc6a3515b51a8ee7f`.
59
+ The first log was inspected but rotated away before copying; no retained hash
60
+ is claimed for it. Focused retries must preserve certificate validation.
61
+
62
+ The focused package retests subsequently passed using scoped
63
+ `NODE_OPTIONS=--use-system-ca`, restored afterward, with no global npm config
64
+ change: 71 package-coordinator tests in 307.617s and 40 packaged-example tests
65
+ in 28.365s, both at all-four 100% in their existing scopes. The isolated consumer
66
+ installed published `redweb-client@0.2.0`; real Chromium acceptance, runtime and
67
+ refresh checks passed, alongside source-free generated applications. The
68
+ production dependency audit found zero vulnerabilities. This verified correction
69
+ of local certificate trust does not relabel the failed full-suite run.
70
+
71
+ - Package-coordinator map SHA-256: `199e25f0d8f67c35119dbcd86e227ba6dace836aea14fe29e8d4f20d99791faa`.
72
+ - Packaged-example map SHA-256: `51729c6f8604ad0a0b1f4e5c3d71688b75f833d3cb6b78155c7dc769cdc14c17`.
73
+ - Packed browser report: `coverage/packed-browser/1ffb552b-0168-4220-8555-4f74537cb6ec/report.json`, SHA-256 `41a229941b45105d107ad91e8cacb9527f8acaf0e17dba97fe0e1d6a2432d9c9`.
74
+ - Tested archive SHA-256: `6bf8f30b8cf2f719f202ff192b05bbd1865909cfeebd3a3d8974897e6d4cb2b9`. It predates this final verification-note update, not the tested runtime/recipe code.
75
+
76
+ The maintained recovery command was then run verbatim: all 45 tests passed in
77
+ 32.615s and produced the identical authored map hash. Generated documentation,
78
+ all three type configurations, four recovery-CI and 15 documentation/CI unit
79
+ checks passed. The independent critic approved the increment's scope and claims;
80
+ this is not whole-release approval.
81
+
82
+ The third failure asserted that a disconnected session still existed after
83
+ waiting for room cleanup, despite its 20 ms TTL. Adding an actual 50 ms observer
84
+ delay reproduced that assertion failure while the zero-delay case passed.
85
+ This demonstrates a fragile test assumption, not the exact timing of the
86
+ original failure, which retained no session timestamps.
87
+
88
+ The corrected real-network test observes session release in the route's close
89
+ lifecycle callback, after detachment and before a later expiry timer turn.
90
+ Both immediate and delayed observers assert takeover/data preservation and
91
+ then await actual expiry. TTL (20 ms), sweep (5 ms) and production socket
92
+ behavior are unchanged; no timer or transport mocks are used. All 31 tests in
93
+ the socket integration file passed in 6.849 seconds. Those focused passes do
94
+ not supersede the failed full run.
95
+
96
+ Both hosted runs for the preceding `0e3e257` commit passed every Node
97
+ 18/20/22/24 and lifecycle job: PR run 33427313619 and push run 33427307579.
98
+ They predate this correction and do not certify it. Remaining split-worker
99
+ authored coverage and final release gates are recorded in
100
+ [the scope audit](COVERAGE_SCOPE_AUDIT.md).