redweb 0.12.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 +166 -9
  2. package/README.md +177 -523
  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 +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 -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 +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 +8 -7
  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 +88 -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 -30
  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 -87
  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
@@ -0,0 +1,49 @@
1
+ # Private rooms without custom socket plumbing
2
+
3
+ A room is a list of subscriptions, not an identity provider. Authentication answers **who is this connection?** A room policy answers **may that identity enter this room?** Only a literal `true` grants entry.
4
+
5
+ Configure `rooms.authorize(context, roomId)` on a `SocketRoute`, then use `await socket.enterRoom(roomId)` in the corresponding handler. URL paths still choose routes and message `type` still chooses handlers; there is no second action dispatcher or socket decorator system.
6
+
7
+ ## A complete shared-identity example
8
+
9
+ The [runnable page and room example](snippets/room-access.tsx) uses one authentication function for the rendered page and `/team` socket route. It generates a fresh local-demo bearer credential on each run, has a protected page, exposes a `join` handler, and revokes page sessions and room memberships together. Its exact source is also shown on the generated examples page. The library's package gate compiles it with both standard and legacy decorators, removes its TypeScript source, and verifies real HTTP/WebSocket access, publication, and revocation against the extracted package.
10
+
11
+ To run locally, save that file as `src/app.tsx` in an initialized realtime starter, then run `npm run build` and `npm start`. Send the printed `Authorization: Bearer …` header to `http://127.0.0.1:8181/` or `ws://127.0.0.1:8181/team`; the socket accepts `{"type":"join"}`. The existing starter's counter tests describe a different application, so do not treat them as acceptance tests for your modified app. Never publish the printed token or use this demonstration as a production credential service. A normal browser WebSocket cannot set an Authorization header; use your application's secure cookie/session integration for a browser product. The [dashboard recipe](../recipes/dashboard/README.md) demonstrates real cookies, persistent accounts and sign-out.
12
+
13
+ ## Entry, publication, and revocation
14
+
15
+ - `await socket.enterRoom(id)` and `await route.rooms.enter(id, socket)` perform the bounded policy check and then commit membership synchronously. They resolve to `false` when the connection is no longer eligible or a membership limit prevents entry.
16
+ - Existing `socket.joinRoom(id)` and `route.rooms.join(id, socket)` remain synchronous for unprotected rooms. They throw for protected rooms, with guidance to use asynchronous entry; they cannot bypass the guard. Use `enterRoom` for new guarded application code.
17
+ - Denial, timeout, cancellation, and authorization capacity exhaustion reject entry with safe diagnostic codes. The normal handler boundary sends these to the client without disconnecting it. Broken policies remain sanitized application failures, not disguised permission denials.
18
+ - On protected rooms, `socket.roomBroadcast` and `rooms.broadcastFrom(socket, …)` require the sender to be a current, live member. That is a membership check, **not** an application-specific write-role policy. Validate and authorize actions such as moderator announcements separately.
19
+ - `route.rooms.broadcast` is privileged server publication. Do not expose a client-selected room through it without your own authorization. It publishes only to current eligible members and rechecks membership after serialization.
20
+ - A grant lasts until leave, disconnect, replacement, clear, or shutdown. Changing a policy does not automatically unsubscribe existing readers. Invalidate credentials/permissions first, then call `leaveRoom`, `rooms.leave`, or `rooms.leaveAll` on affected connections. Those operations also cancel pending entry. A late policy completion cannot silently rejoin the connection.
21
+ - `leaveAll` removes all of that connection's memberships before firing policy cancellation callbacks. `clear` removes every membership before cancellation. Nested cancellation/clear cannot reopen entry while the outer operation is still cancelling work.
22
+
23
+ Redweb's `LiveHtmlServer.revoke(principal)` manages its own page lifetimes; it does not automatically revoke custom raw socket routes. The complete example explicitly invalidates its shared credential, removes the raw route's memberships, then revokes page sessions. Application storage and cross-process invalidation remain application-owned.
24
+
25
+ ## One request-context shape
26
+
27
+ `RequestContext` and `RedWebRequest` are shared public types. Page callbacks receive `LivePageRequestContext`; enabled raw socket features expose `socket.context` as `RedWebConnectionContext`. Both provide a selected request snapshot, `principal`, and cancellation `signal`.
28
+
29
+ The request snapshot is captured before raw-route admission code runs. It contains path, URL, method, headers, params, query, body and a case-insensitive `get(name)` helper—no HTTP response, transport, or framework object graph. Its data is deeply frozen and bounded to 64 KiB/16 nesting levels. Raw upgrade paths and repeated query parameters are parsed from the URL. It is not an Express request, and forwarded headers do not become trusted identities automatically.
30
+
31
+ Socket identity/request/protocol references cannot be replaced; application `metadata` and resumable `session` fields remain mutable. Existing raw admission object identities remain supported and application-owned, not deeply frozen by Redweb. Page identities retain their existing primitive identity contract. Never derive a trusted identity from a client message's `principal` field or mutable application metadata.
32
+
33
+ Each relevant raw connection has its own signal, cancelled on disconnect, replacement, or route draining. An operation policy receives a separate bounded signal that also cancels on leave or its deadline. Socket context remains optional when all features requiring it are disabled. In Live HTML callbacks, use the supplied callback context for the application identity: the underlying transport's internal page-session principal is not that callback identity.
34
+
35
+ ## Resource limits and failure meanings
36
+
37
+ Protected rooms retain the existing room/member/name limits and add:
38
+
39
+ | Option | Default | Meaning |
40
+ | --- | --- | --- |
41
+ | `authorizationTimeoutMs` | 5000 | Maximum time to wait for one policy. |
42
+ | `maxPendingAuthorizations` | 128 | Underlying policy work across the registry. |
43
+ | `maxPendingPerConnection` | 4 | Underlying policy work for one connection. |
44
+
45
+ These options require `authorize`. Concurrent pending requests for the same connection/room share one check. No room or membership is reserved while permission is pending; final insertion rechecks all capacities. Timed-out or cancelled policy work stays charged until the **actual application promise settles**, so an uncooperative policy cannot spawn unlimited background work. Honor the signal and use bounded downstream I/O; policies that never settle can exhaust capacity until corrected/restarted. JavaScript's synchronous execution cannot be preempted.
46
+
47
+ `ACCESS_DENIED`, `ACCESS_TIMEOUT`, `ACCESS_CANCELLED`, and `ACCESS_CAPACITY` indicate no membership was committed by that failed entry. They do not assert that an external policy had no side effects. Protocol routes return the standard error envelope and request ID; unversioned routes return `{ code, error }`. A policy exception becomes sanitized `HANDLER_FAILED` and follows the normal application-error close behavior. Nothing automatically retries entry or promises exactly-once application delivery.
48
+
49
+ Use HTTPS/WSS, trusted origins for browser credentials, real session expiry, input limits, and persistent application authorization before public deployment. Process-local rooms and grants do not become distributed merely because a socket route has a distribution adapter.
@@ -0,0 +1,78 @@
1
+ # Understand failures before retrying
2
+
3
+ Status: unreleased on `codex/agent-ready`; these handshake diagnostics are not in npm 0.12.0.
4
+
5
+ Authentication identifies a visitor. Authorization decides what that visitor may do. Validation checks an input's shape. An application failure means server code or a dependency failed; it is not evidence that the visitor supplied bad credentials.
6
+
7
+ Redweb keeps these boundaries separate. It does not automatically retry failed actions, undo application writes, or guarantee exactly-once delivery.
8
+
9
+ ## Reading a failed connection
10
+
11
+ Before a WebSocket opens, Redweb sends an HTTP rejection with a fixed `Redweb-Error` header and `Cache-Control: no-store`. There is no error body and no callback exception text. Node's `ws` client can inspect it:
12
+
13
+ ```typescript
14
+ import WebSocket from 'ws';
15
+
16
+ const socket = new WebSocket('ws://127.0.0.1:8181/match?redwebVersion=1');
17
+ socket.on('unexpected-response', (_request, response) => {
18
+ console.error(response.statusCode, response.headers['redweb-error']);
19
+ response.resume();
20
+ socket.terminate();
21
+ });
22
+ socket.on('error', () => console.error('Connection did not open.'));
23
+ ```
24
+
25
+ This is a Node diagnostic example, not browser code. Native browser WebSocket JavaScript cannot inspect handshake status or response headers. Use the browser network inspector during development and your application's normal HTTP sign-in/status flow for user-facing guidance. A generic browser socket error alone cannot distinguish rejected credentials from networking, origin, protocol, or server failures. Proxies may replace or strip responses.
26
+
27
+ | Code | HTTP status | Meaning and next step |
28
+ | --- | --- | --- |
29
+ | `REQUEST_INVALID` | 400 | The upgrade request cannot be represented safely. Correct the request or server middleware. |
30
+ | `AUTHENTICATION_REQUIRED` | 401 | Identity was rejected, or the page session is missing, expired, already attached, or mismatched. Obtain valid credentials/a fresh page; do not retry the same rejected credentials in a loop. |
31
+ | `ORIGIN_DENIED` | 403 | Browser origin was missing or not allowed. Correct the trusted-origin configuration; do not disable origin checks to hide the failure. |
32
+ | `ACCESS_DENIED` | 403 | The page permission policy denied access. Obtain permission before retrying. |
33
+ | `PLACEMENT_DENIED` | 403 | Placement explicitly rejected the connection. Follow application placement rules. |
34
+ | `PROTOCOL_UNSUPPORTED` | 426 | Negotiation requires a supported version. `Redweb-Versions` lists supported versions; use a compatible client and contract. |
35
+ | `AUTHENTICATION_FAILED` | 500 | The rendered-page identity callback failed. Investigate the application or identity provider. |
36
+ | `ADMISSION_FAILED` | 500 | Admission/origin/placement code, a page upgrade policy, or the upgrade pipeline failed. This is not a bad-password response. |
37
+ | `PLACEMENT_INVALID` | 500 | Placement returned an unsafe or disallowed redirect. Repair the server-side placement result/allowlist. |
38
+ | `AUTHENTICATION_TIMEOUT`, `ACCESS_TIMEOUT`, `ADMISSION_TIMEOUT` | 503 | The relevant stage exceeded its deadline. Check the dependency and use bounded reconnect backoff only when appropriate. |
39
+ | `AUTHENTICATION_CANCELLED`, `ACCESS_CANCELLED`, `ADMISSION_CANCELLED` | 503 | The relevant lifetime ended. Start a new permitted attempt rather than reusing a revoked page/session. A disconnected peer may receive no response. |
40
+ | `ACCESS_CAPACITY`, `ADMISSION_CAPACITY` | 503 | Bounded authorization/admission work or connection capacity is exhausted. Wait and back off; do not retry in a tight loop. |
41
+ | `SERVER_DRAINING`, `ROUTE_UNAVAILABLE` | 503 | The service is draining or the route is not ready. Reconnect to a ready instance according to the application's routing policy. |
42
+
43
+ Accepted placement redirects remain HTTP 307 with the validated `Location`, no error code, and no-store caching. A redirect is not proof that the destination will admit the same credentials. Never forward credentials to arbitrary redirect destinations.
44
+
45
+ Raw route authentication preserves its existing contract: only literal `false` rejects the identity; application-owned principal objects remain supported. Page authentication requires its documented primitive identity. Do not rely on a raw callback returning `undefined` to deny access.
46
+
47
+ ## Page requests
48
+
49
+ Page HTTP failures return `{ "error": { "code": "...", "message": "..." } }` with `Cache-Control: private, no-store`. Authentication and authorization use the categories above. `PAGE_CAPACITY` is 503; `PAGE_FAILED` is a sanitized 500 for construction, loading, or rendering failures, including public pages. Unknown application error text and Express development stacks are not returned. Typed errors are reconstructed from the fixed catalogue rather than trusting mutable status/message fields.
50
+
51
+ If the response is already closed, Redweb does not write another response. If headers were already sent by application middleware, the connection is closed instead of appending a misleading JSON error. Redweb cannot retract content your middleware already sent or sanitize arbitrary HTTP routes you mount yourself.
52
+
53
+ ## Actions and established sockets
54
+
55
+ Once connected, failures use the existing protocol error envelope and request ID when available. Unversioned room-entry failures use `{ code, error }`; other legacy unversioned failures retain `{ error }` without a structured code. The client may still disconnect before receiving the response. Typed permission/input errors are normalized again at the final send boundary, so application catch/rethrow code cannot accidentally expose appended private exception text.
56
+
57
+ | Boundary | Diagnostic | What Redweb guarantees |
58
+ | --- | --- | --- |
59
+ | Action input | `ACTION_INVALID_INPUT` | The action method was not invoked. Correct the form values. |
60
+ | Action input lifetime | `ACTION_VALIDATION_TIMEOUT`, `ACTION_CANCELLED` | Validation did not complete within its lifetime; the action method was not invoked. |
61
+ | Action/room permission | `ACCESS_DENIED`, `ACCESS_TIMEOUT`, `ACCESS_CANCELLED`, `ACCESS_CAPACITY` | That failed permission check did not commit room entry or invoke the guarded action. Existing memberships and prior actions are separate. These responses do not inherently close the socket. |
62
+ | Browser send | `ACTION_OFFLINE`, `ACTION_CAPACITY` | This browser action was not sent. Reconnect or wait before deliberately trying again. |
63
+ | Socket envelope/contract | `INVALID_MESSAGE`, `INVALID_PAYLOAD`, `UNKNOWN_HANDLER` | The requested handler callback was not invoked. Correct the message/contract. These paths generally close the connection; they are not automatic retry signals. |
64
+ | Application/validator/output bug | `HANDLER_FAILED` | The operation failed; application effects may already have happened. Inspect authoritative state before resubmitting. |
65
+
66
+ An input validator, identity lookup, or permission callback can itself perform external work. A “method was not invoked” result does not promise those callbacks had no side effects. Keep validators and policies free of writes where practical; use explicit idempotency keys and durable transactions for application operations that may be retried.
67
+
68
+ Those non-invocation guarantees describe failures produced by Redweb's validation and permission gates. They do not apply to application code deliberately throwing an internal typed error or sending the same diagnostic after its own work has begun.
69
+
70
+ ## Deadlines, cancellation, and disclosure limits
71
+
72
+ Raw admission shares the bounded-operation implementation used by other policy/validation paths. It checks the deadline between origin, identity, and placement stages, so a timed-out or cancelled stage cannot start the next stage after eventually returning. It retains its actual evaluation promise in admission accounting until that evaluation settles. Synchronous JavaScript cannot be interrupted; a callback that blocks the event loop delays timeout observation, but an overdue result cannot admit a connection.
73
+
74
+ Page identity and permission evaluation also have their own deadlines and session/revocation signals. The outer raw-admission deadline does not forcibly stop those nested callbacks or their external I/O. Cancellation of observation is not cancellation of database/network side effects. Honor available signals, set downstream timeouts, and never treat this as a sandbox for untrusted callback code.
75
+
76
+ Default handler responses are sanitized. Raw routes deliberately configured with `exposeErrors: true` opt into disclosing handler exception text; do not enable that in production. Existing application/logger hooks may receive original errors and client metadata, so logs require access controls and redaction. The new upgrade pipeline logs only fixed admission failure details, and a throwing logger cannot prevent upgrade rejection or reservation cleanup.
77
+
78
+ See [private rooms](ROOM_AUTHORIZATION.md), [socket contracts](SOCKET_CONTRACTS.md), and [operating socket services](MULTIPLAYER_OPERATIONS.md) for their complete limits. A successful `send` means accepted by the local transport, not acknowledged application delivery.
@@ -0,0 +1,185 @@
1
+ # Server-focused recovery candidate
2
+
3
+ Status: **adopted as the blocking CI recovery check** after independent review,
4
+ five clean cross-runtime results and explicit maintainer authorization to continue.
5
+ Original `npm run verify:recovery` keeps its command and runs in CI as an explicitly
6
+ non-blocking diagnostic. A later [exact-byte boundary correction](ORIGINAL_RECOVERY_VERIFICATION.md)
7
+ fixes false rejection at precisely 110%; its workload and limit are unchanged.
8
+ Historical failures are not erased by a server pass or that rounding correction.
9
+
10
+ ## Contract: server-steady-v1
11
+
12
+ The candidate reuses the existing split coordinator and workers in baseline mode,
13
+ with separate coordinator, Redweb server and native `ws` load-generator processes.
14
+ It preconditions with 1,200 connections, warms with 200, then executes five storms
15
+ of 1,200 each, in batches of 50: 7,400 total exact request/reply exchanges.
16
+ Each batch reconciles client sends/replies and server receives and waits for
17
+ server connection cleanup. Rooms and sessions are checked at phase boundaries,
18
+ not claimed empty after every batch. Sampling settles for 400 ms and performs the
19
+ existing two explicit garbage collections before reading retained heap.
20
+
21
+ Every storm's server heap must be at most 110% of the **same** warmed server heap.
22
+ A failed middle storm fails even if the final storm recovers. Client heap ratios
23
+ are reported separately without a client heap acceptance budget. Exact delivery,
24
+ empty registries, stable runtime/process identities, normal worker exits, closed
25
+ output pipes, complete logs and unchanged input hashes remain mandatory for both
26
+ roles. A forced cleanup cannot produce a passing candidate.
27
+
28
+ This is a different measurement from the shared-process gate, not proof that its
29
+ failure was harmless. No code-memory subtraction, moving baseline, retries until
30
+ green, instrumentation, snapshots or tuned workload overrides are permitted.
31
+ The reused worker reads V8 statistics after its heap sample; that existing
32
+ observation and split-process scheduling are part of this candidate protocol.
33
+
34
+ Reports retain the reviewed `candidateOnly: true` / `candidatePassed` field names
35
+ for format compatibility with the collected evidence. CI now adopts the command's
36
+ exit status as this scoped recovery gate; these legacy field names do not mean
37
+ whole-release approval. No workload, threshold, worker or report-schema change
38
+ was needed for adoption. The original measurement keeps its own exit status;
39
+ CI records the raw step outcome, emits a warning on non-success, and retains logs
40
+ alongside server samples, process exits and input/output hashes for 30 days.
41
+ Both commands have two-minute CI deadlines and run sequentially, without retries.
42
+ The diagnostic only starts after a passing server gate confirms worker cleanup;
43
+ after a failed/timed-out server run it is explicitly skipped, not called passing.
44
+
45
+ ## Run the candidate
46
+
47
+ From the matching Redweb source checkout (not an installed application), run
48
+ `npm run verify:recovery:server` in a
49
+ clean environment. It creates an exclusive directory under `coverage/` and
50
+ prints its location. An optional absolute, nonexistent directory can be supplied
51
+ after `--`. Existing evidence is never overwritten.
52
+
53
+ The command rejects Node flags, nonempty `NODE_OPTIONS`, `NODE_V8_COVERAGE` and
54
+ `REDWEB_RECOVERY_*` overrides (including differently cased environment names).
55
+ Reports include source hashes, all phase samples, observed worker exits and log
56
+ digests. Failures retain partial evidence; primary errors are not replaced by
57
+ secondary finalization errors. A heap-budget failure exits nonzero just like an
58
+ invalid-evidence failure, but is identified separately in its saved report.
59
+
60
+ Policy units use explicitly synthetic reports for malformed evidence and exact
61
+ budget boundaries. Real integration uses a managed coordinator process and real
62
+ workers/sockets; its parent bounds the coordinator and separately reaps registered
63
+ workers, whose POSIX process groups are independent. Missing registration or
64
+ uncertain cleanup fails and retains the workspace rather than claiming complete
65
+ tree cleanup. Coordinator-only
66
+ instrumentation is used for behavioral coverage, never claimed as a clean
67
+ candidate measurement. Shared worker lifecycle tests remain separate. No mocked
68
+ transport, garbage collector, process or filesystem replaces these integration
69
+ checks.
70
+
71
+ ## Maintained acceptance-tool coverage
72
+
73
+ The current gate also includes the full authored split coordinator/worker:
74
+ 161 tests, all-four 100% over five source files. See
75
+ [authored coverage](SPLIT_RECOVERY_COVERAGE.md) for current maps, boundaries and
76
+ the separate full-regression/hosted outcomes. The original three-file checkpoint
77
+ below is historical and does not describe the current command's complete scope.
78
+
79
+ ### Original policy/coordinator/CLI checkpoint
80
+
81
+ `npm run verify:recovery:coverage` now runs the existing coordinator/policy units,
82
+ real worker integrations and three added CLI exit-boundary units together. It
83
+ enforces all four 100% thresholds over `ServerRecoveryPolicy.js`,
84
+ `ServerRecoveryCandidate.js` and `verify-server-recovery.js`, and is required in
85
+ the lifecycle CI job. The command passed 76 tests across five suites in 28.656
86
+ seconds on Windows/Node 22.21.0. The CLI units explicitly substitute the coordinator
87
+ result and stderr to cover pass, budget-failure and rejected-promise exits; they
88
+ are not described as mock-free integration. Actual CLI/worker/socket behavior is
89
+ also exercised by integration tests and the ordinary CI acceptance invocation.
90
+ This closes the earlier thin-CLI coverage gap without claiming coverage of the
91
+ shared diagnostic workers or every private verification tool.
92
+ The report contains 132 statements, 25 branches, 25 functions and 120 lines,
93
+ all covered. `coverage/server-recovery-acceptance/coverage-final.json` SHA-256:
94
+ `0d6ebe45fe2e1f87f2ddcb6afb4fe1960473c40a09c877f4697cf90db2a1ff3c`.
95
+
96
+ ## Implementation checkpoint
97
+
98
+ The senior critic approved measurement after environment-case and test-process
99
+ ownership findings were fixed. The scoped regression passed 73 tests in four
100
+ suites, including existing split-worker regressions. New policy/coordinator
101
+ coverage is all-four 100%: 128 statements, 23 branches, 23 functions and 116 lines.
102
+ The report is `coverage/server-recovery-candidate/coverage-final.json`, SHA-256
103
+ `17906b03fae19ed3bef54d8d82743ad831a6124fa1bd88147cbb4e2da64f87d7`.
104
+ This does not claim whole-repository, test-helper or thin-CLI-wrapper coverage.
105
+ Generated-content freshness and all TypeScript pretest configurations also pass.
106
+
107
+ Coverage uses original coordinator source in a managed Jest child and the parent
108
+ unit tests. It checks identical source mappings before merging only the parent's
109
+ selected files. Child collection is non-gating; the combined scoped gate retains
110
+ all four 100% thresholds. This is instrumentation for behavioral verification,
111
+ not a clean candidate heap measurement.
112
+
113
+ The temporary `server-recovery-candidate.yml` workflow collects one clean result
114
+ each on Ubuntu 24.04 with Node 18.20.8, 20.20.2, 22.23.2 and 24.19.0. Its trigger
115
+ is restricted to changes to that workflow on the implementation branch; repeated
116
+ attempts are rejected. Each isolated job installs the locked dependencies before
117
+ measurement, runs no competing tests, and uploads candidate evidence on failure
118
+ as well as success. Remove the workflow after collection. This new candidate
119
+ protocol is not a rerun of the historical original/split comparison and does not
120
+ modify the ordinary acceptance workflow.
121
+
122
+ ## Collected candidate results at 15c5a4e
123
+
124
+ The [single Ubuntu matrix](https://github.com/lakam99/redweb/actions/runs/33352534392)
125
+ completed successfully on all four pinned runtimes. A separate single Windows
126
+ Node 22.21.0 run also passed. These were clean candidate invocations, not the
127
+ instrumented behavioral runs described above. Each verified all 7,400 exact
128
+ replies, phase registries, unchanged inputs, complete logs and normal worker
129
+ exits. Downloaded sample streams, worker inventories and every log's bytes/hash
130
+ were independently reconciled with the saved reports.
131
+
132
+ | Environment | Server peak % of warm | Server final % of warm | Client peak % of warm |
133
+ | --- | ---: | ---: | ---: |
134
+ | Ubuntu / Node 18.20.8 | 102.738879 | 101.936032 | 107.220473 |
135
+ | Ubuntu / Node 20.20.2 | 106.724670 | 104.968229 | 111.411540 |
136
+ | Ubuntu / Node 22.23.2 | 108.491002 | 97.235712 | 112.873352 |
137
+ | Ubuntu / Node 24.19.0 | 100.817681 | 100.817681 | 101.139821 |
138
+ | Windows / Node 22.21.0 | 109.253735 | 95.525155 | 113.270161 |
139
+
140
+ Only the server column has the candidate's 110% budget. The client measurements
141
+ above 110% are explicit, not subtracted or represented as passing that budget.
142
+ One run per environment demonstrates these executions, not repeatability or
143
+ production capacity. The original shared-process CI failures remain unchanged.
144
+
145
+ Ubuntu artifacts are retained locally under
146
+ `coverage/server-recovery-matrix-33352534392/`; hosted retention is 30 days.
147
+ Each artifact is named `server-recovery-<node>-15c5a4eef60a7a544745a9382bf9f517c02a30f8`.
148
+ SHA-256 for each `report.json`:
149
+
150
+ - Node 18.20.8: `9899ea2201724957fc1aba88d0eb901cea0a0e7e17f43ebc3e21a7593cdca9c4`.
151
+ - Node 20.20.2: `8b0949c154e905122091ba6a0e37019d404c1a21266406ce00b23b6c73464860`.
152
+ - Node 22.23.2: `41f55fb4fbb1b90079d624f4602a3a4d5441391dd112c992fabdef5b720f523b`.
153
+ - Node 24.19.0: `e7ec1c25a3c94aa6b48d30382cf044f8425fab01a09dd548fd0285e06544733e`.
154
+ - Windows `coverage/server-recovery-local-15c5a4e/report.json`:
155
+ `26e90708d03537f16a1b73b5ba16c57cbee9743e4fff2c770933118ffdcf032d`.
156
+
157
+ The temporary collection workflow was removed after completion; no measurement
158
+ rerun occurred. Adoption was pending at this historical checkpoint and is now
159
+ authorized as described above.
160
+
161
+ The senior critic independently verified the actual PR commit, all five reports,
162
+ the Ubuntu manifests against 104 committed inputs, delivery/sample/inventory
163
+ reconciliation and log hashes. The reviewer recommends presenting adoption for
164
+ the maintainer's decision without another experiment. The justification is
165
+ isolating server retention from the load generator, not proving the original
166
+ failures harmless or claiming their cause was fixed.
167
+
168
+ ## Full regression and unchanged CI at 15c5a4e
169
+
170
+ Windows `npm test -- --runInBand --silent` completed successfully: 918 tests in
171
+ 86 suites, 447.46 seconds, including the normal generated-content/type pretest.
172
+ The configured library scope remains all-four 100%: 5,445 statements, 4,044
173
+ branches, 978 functions and 4,464 lines. The coverage report
174
+ `coverage/coverage-final.json` has SHA-256
175
+ `961dd4e6bc2e90df001df71281622140f153024ae71e4295e7b2ed92d49cb045`.
176
+ The candidate tooling coverage is the separate scoped result above.
177
+
178
+ Ordinary [push CI](https://github.com/lakam99/redweb/actions/runs/33352534344)
179
+ passed. Ordinary [PR CI](https://github.com/lakam99/redweb/actions/runs/33352536964)
180
+ failed only the unchanged Node 22 shared-process recovery check: storm 4 reached
181
+ 110.530483% of warm against the 110% limit; final heap was 97.045378%, with empty
182
+ registries. Its tests and load check passed; its subsequent audit was skipped.
183
+ Node 18/20/24 and the lifecycle/browser/package job passed. No retry occurred.
184
+ These results preserve the distinction between a passing candidate and the
185
+ still-failing existing acceptance contract; this is not merge approval.
@@ -0,0 +1,160 @@
1
+ # Soak rotation: retained failure and controlled observation
2
+
3
+ ## Actual CI failure, still unresolved
4
+
5
+ At `df58f94`, PR run 33432429300, Node 24 job 99620736610, failed the native
6
+ ten-second mechanics test's delivery assertion: **98.47715736040608%**, below
7
+ the unchanged 99% limit. The job recorded 1,939 passes, one failure, eight skips,
8
+ 175 suites and 1,212.024 seconds, despite all-four 100% library coverage.
9
+ The matching push and later Windows run passed; they do not erase this failure.
10
+
11
+ The previous test saved its measurement only after all assertions. Its temporary
12
+ workspace was then deleted on failure, so the original raw JSON and exact
13
+ sent/received counts are unavailable. Do not infer those counts from the ratio.
14
+ The complete failed-job log was retained with ANSI removed and LF newlines at
15
+ `coverage/ci-df58f94-node24-99620736610.log`, SHA-256
16
+ `170eec5766098b6d9b27cfbebaf56ace15984d964fe5e2327ddb7b6daea4df8a`.
17
+
18
+ ## Evidence preservation correction
19
+
20
+ The mechanics fixture now saves raw report text, command output, observed normal
21
+ exit status and available errors before parsing or policy assertions. Invalid
22
+ JSON and a sub-limit outcome remain inspectable. Unknown launch/timeout/cleanup
23
+ failures remain failures, not fabricated exit statuses. If reading or writing
24
+ evidence fails, the original workspace is retained and primary errors survive.
25
+ The critic caught the outer-workspace deletion case; real-filesystem regressions
26
+ now verify the original raw file survives after `VerificationWorkspace.run()`
27
+ rejects. Test cleanup explicitly removes its own retained fixtures afterwards.
28
+
29
+ The GitHub Node matrix now uploads available `coverage/soak-tools/smoke-reports/`
30
+ artifacts on success or failure, independently of the lifecycle job's coverage
31
+ artifact. Missing evidence warns because a pretest/launch failure can occur
32
+ before the measurement exists; that warning does not turn failed tests green.
33
+ Retention is 30 days. Each observation gets a new exclusive filename.
34
+
35
+ ## Controlled real-socket comparison
36
+
37
+ Two native WebSocket cases hold one actual reply after the peer receives its
38
+ tick. They use no mocked transport, timers or process APIs:
39
+
40
+ - Reply before rotation: the original reply arrives, rotation closes the old
41
+ socket and opens its replacement, and both later replies arrive: **4 sent,
42
+ 4 received**.
43
+ - Rotation before releasing the held reply: the old socket is closed before
44
+ replacement; releasing that reply fails on the closed peer. Both later
45
+ replies arrive: **4 sent, 3 received**, with exactly one still missing.
46
+
47
+ Assertions verify the pending tick before rotation, closed original socket,
48
+ event order, distinct replacement, generation increment and exact counters.
49
+ The targeted comparison passed both cases in 0.582 seconds.
50
+
51
+ This proves a possible loss mechanism during intentional rotation, **not the
52
+ cause of the historical CI run**. The existing 100 ms send and 1,000 ms rotation
53
+ timers can overlap with pending work. No drain, timing, workload or delivery
54
+ limit has been changed, and missing replies remain in the denominator. The
55
+ next ordinary CI observation must be retained before making a stronger claim.
56
+ A future drain policy would change rotation semantics and needs its own tested,
57
+ explicitly documented methodology; it is not a silent fix for this result.
58
+
59
+ See [the original soak-verifier correction](SOAK_VERIFICATION.md) for policy
60
+ details and the distinction between short mechanics tests and hour acceptance.
61
+
62
+ ## Final verification of this correction
63
+
64
+ The maintained `npm run verify:soak:coverage` passed 91 tests across five suites
65
+ in 16.992 seconds after the retention review fix. All 280 statements / 116 branch
66
+ outcomes / 68 functions / 200 lines are covered across the three unchanged
67
+ soak modules and the new test-only retention helper. The latter contributes
68
+ 39 statements / 16 branches / three functions / 27 lines; it is not shipped
69
+ runtime coverage. Map: `coverage/soak-tools/coverage-final.json`, SHA-256
70
+ `19d9f505a2d2156e09177a86e5a922fd848b5e1fcdb14c0033f6fab889a98377`.
71
+
72
+ The reviewed nine-test retention selection also passed independently in 2.266
73
+ seconds with all-four 100% helper coverage. It includes actual child exits and
74
+ filesystem failures, plus explicit application callback faults.
75
+
76
+ A follow-up bounds each of the three real child-command cases at ten seconds,
77
+ with a 40-second outer test budget allowing independent process/pipe/workspace
78
+ cleanup to finish first. The unchanged nine cases passed again in 0.745 seconds
79
+ at all-four 100% helper coverage; no soak workload was rerun for this test-budget
80
+ correction.
81
+
82
+ Two ordinary mechanics observations were retained during implementation:
83
+
84
+ - Before the outer-retention fix: 163 sent / 162 received, 99.38650306748467%,
85
+ command exit 1 for the known room-phase trend (early 1, late 2), not delivery.
86
+ Observation `2bed814f-ca66-438e-b3a4-1ab338848976.json`, SHA-256
87
+ `6b2ff5305c42c103a878a304aaadd353d9225710539674a749d8fa636a4c2af8`.
88
+ - Final wiring verification: 173 sent / 172 received, 99.42196531791907%,
89
+ command exit 0. Observation `8aacb5b0-9512-48ac-a201-7b64255af574.json`, SHA-256
90
+ `bee152a47e3047ce9d9bd20570d1b3e253359a315e7cc259847c0acd76421760`.
91
+
92
+ Both are under `coverage/soak-tools/smoke-reports/`. The prior fixture's explicit
93
+ room-phase allowance is unchanged: a mechanics-test pass can retain command
94
+ exit 1 and is not soak acceptance. Neither observation is lossless, neither
95
+ supersedes the hosted sub-99% failure, and neither is a new hour soak.
96
+
97
+ ## Current-head long-run failure and heartbeat correction
98
+
99
+ At `1bb61c3`, a fresh default hour began at 21:07:04Z under a one-shot Windows
100
+ Scheduled Task with independently reviewed child ownership. The workload exited1
101
+ after about nine minutes with `Soak client disconnected unexpectedly.` No report
102
+ was created, so it provides no partial delivery, resource-trend or acceptance
103
+ measurement. The owner exited normally, did not force termination, retained its
104
+ 1,520-byte stderr and terminal outcome, and did not retry. The exact task was
105
+ removed only after terminal validation. Outcome SHA-256:
106
+ `53083685f019d82d5f62421faaa80dd15f16d06a3d85ff6b3b08e3546982e258`;
107
+ stderr SHA-256:
108
+ `951a06f05779be74031fb2f7736645557593b1cc6f1b750736b77a8348241131`.
109
+
110
+ The old soak client discarded the native close code and reason. Unexpected close
111
+ events now preserve both, with reasons escaped for unambiguous line-oriented logs;
112
+ unit and real-WebSocket checks cover framework policy and transport closes,
113
+ including absent codes, without changing delivery accounting. An earlier error
114
+ remains the primary failure if its event arrives before close. The maintained
115
+ soak gate passes94 tests at all-four100% across its existing four-file scope.
116
+
117
+ A separate no-mock regression deterministically reproduced one possible false
118
+ disconnect: a responsive same-process client automatically pongs, while one
119
+ server-side ping callback stalls the event loop beyond the heartbeat deadline.
120
+ Previously the next timer terminated that healthy client before its already
121
+ dispatched pong handling could win. Heartbeat expiry now owns one deduplicated,
122
+ unreferenced `Immediate` per expired socket. The deferred check terminates a peer
123
+ that is still silent; pong handling, detach/reattach, or monitor shutdown makes
124
+ the stale check harmless. No deferred-check allocation occurs on healthy ticks and no timeout is
125
+ reset. Connection/queue limits remain the resource bounds.
126
+
127
+ The focused heartbeat scope passes71 unit and real-socket tests at100% statements,
128
+ branches, functions and lines. The senior critic required and approved silent-peer,
129
+ deduplication, detach/reattach and shutdown ownership cases. This establishes a
130
+ real possible mechanism and its correction, **not the cause of the failed hour**:
131
+ that run's missing close code cannot be recovered.
132
+
133
+ ## Corrected-hour outcome
134
+
135
+ An immutable `11d1dcc` candidate then completed a fresh default hour from
136
+ 2026-08-31T21:39:31.146Z to 22:39:35.639Z on Windows and Node22.21.0. A one-shot,
137
+ no-trigger Scheduled Task owned the hidden process. No competing local verification
138
+ workload ran. The task finished ready with result0, the child exited0 without
139
+ forced termination or cleanup error, stderr was empty, and the exact task was
140
+ removed only after independent validation.
141
+
142
+ - 3,600 seconds,64 clients and720 samples.
143
+ - 2,106,821 messages sent;2,106,745 received;76 missing:99.9963926693%
144
+ delivery against the unchanged99% requirement. This is not lossless delivery.
145
+ - Warm heap11,570,488 bytes; peak12,129,384; final11,293,216:97.6036274356%
146
+ of warm against the unchanged110% limit.
147
+ - All eight resource trends passed; final client, room, session and in-flight
148
+ registries were empty. Handles moved0→1, within the allowed one.
149
+ - The report and stdout match byte-for-byte at SHA-256
150
+ `0ab7b67adfa23469cbccd546782102650206a2052c2952b3bf21556cecb802b8`;
151
+ stderr contains zero bytes.
152
+ - All112 raw runtime/dependency/soak inputs matched their recorded hashes after
153
+ execution. An independent recomputation also matched the582-file tracked-tree
154
+ digest `39d0b28ae6554467f6b7737c1ada246a51e1cf85dc672e8406796b7ca86983b3`.
155
+ The launcher itself did not recompute that aggregate digest.
156
+
157
+ Report: `coverage/soak-11d1dcc-scheduled-20260831.json`. The subsequent
158
+ `136bbaf` review increment strengthens proof and diagnostics without changing the
159
+ production heartbeat implementation. Because it changes one soak-owned diagnostic
160
+ input, this hour remains evidence for `11d1dcc`, not a final-head soak claim.
@@ -0,0 +1,154 @@
1
+ # Soak-verifier correction
2
+
3
+ This increment changes verification tooling, not Redweb's production runtime.
4
+ It closes a private-tool coverage gap without waiving the separate throughput
5
+ benchmark or claiming a new one-hour soak.
6
+
7
+ ## Reproduced weakness and correction
8
+
9
+ The original command ran against real sockets for ten seconds with two clients
10
+ and a 20-second sample interval. It exited successfully with only two samples.
11
+ Its final heap became its own warm baseline (exactly 100% reported), and all
12
+ eight trends compared the initial sample with itself. The original script SHA-256
13
+ was `ce30f8abcda0e018b8c586bd53dba775aebdaaaaf5275aa8bdf2b2f8d9b1d22b`.
14
+ The retained report `coverage/soak-original-sparse.json` has SHA-256
15
+ `fc8cf5d93237e27f91f7c3ac8838682ac4b8ec629d1c0ed65ef3652ea8ad4eaa`.
16
+
17
+ The policy now requires safe native timer ranges, array/derived exchange-count
18
+ capacities and enough actual active-phase samples. The socket owner records every
19
+ transport immediately, settles parallel acquisition, and matches the existing
20
+ `{tick}` replies against each connection's pending ticks exactly once. Arbitrary
21
+ frames, duplicate/unsent ticks and unexpected disconnects fail instead of inflating
22
+ delivery or silently reducing the workload. Intentional rotation/cleanup remains
23
+ distinct, and lost replies remain counted as missing.
24
+
25
+ Traffic/sample failures and rotation errors signal the coordinator immediately.
26
+ Timers stop, pending rotation settles, drain/client closure/expiry sampling/server
27
+ shutdown are attempted independently, and primary plus cleanup failures survive.
28
+ Output follows cleanup; an existing output file cannot be overwritten, and a
29
+ failed file write cannot be preceded by misleading success output.
30
+
31
+ ## Preserved measurement boundaries
32
+
33
+ - Defaults: 3,600 seconds, 64 clients, five-second samples; 100 ms traffic and
34
+ one-second rotation cadence; original route limits and unchanged wire payloads.
35
+ - Original GC sampling sequence, upper medians, trend windows and eight growth
36
+ budgets; at least two active-phase periodic samples are now required.
37
+ - Final heap is sampled after client closure/expiry but before server shutdown.
38
+ The later two GC calls do not update the reported final heap.
39
+ - The existing 99% delivery, 110% final heap and baseline-plus-one handle limits
40
+ remain. Integer count/byte comparisons avoid floating-point boundary errors;
41
+ the displayed ratios are retained. A genuine 1,000→1,100-byte fixture previously
42
+ calculated `110.00000000000001` and falsely failed the exact 110% boundary.
43
+
44
+ Explicit pending sets/listeners/ownership change the harness's allocations and
45
+ parsing work. These results are not byte-identical historical measurements or a
46
+ claimed runtime optimization. Short integration workloads establish mechanics,
47
+ not production capacity or long-run stability.
48
+
49
+ ## Verification
50
+
51
+ Windows / Node 22.21.0: `npm run verify:soak:coverage` passes 80 tests/four suites
52
+ in 14.494 seconds, requiring all-four 100% across three files: 241 statements,
53
+ 100 branches, 65 functions and 173 lines. Native WebSocket/process tests cover
54
+ valid traffic, rotation, malformed/duplicate/unsent replies, server-error frames,
55
+ unexpected closes, partial acquisition, failed replacement, missing GC, sparse
56
+ sampling and actual short-run reports. Explicit boundary units cover policy,
57
+ timers, cleanup faults, exact limit acceptance and output suppression. No API mocks
58
+ are used in the native integration tests.
59
+
60
+ The first client-unit draft used cross-realm `instanceof Error` against Node's
61
+ AssertionError. It was corrected to Node's native-error check; no production
62
+ change was made for that fixture issue. The critic's exact-boundary, derived
63
+ counter capacity and supervision findings were corrected. The native rotation
64
+ test allows 80 seconds for all operation/cleanup phases. CI allows 20 minutes for
65
+ the complete scoped suite and retains reports for 30 days on failure or success;
66
+ the separate 30-second/16-client CI soak has two-minute external supervision.
67
+
68
+ | Source | SHA-256 |
69
+ | --- | --- |
70
+ | `scripts/verify-soak.js` | `1a2b1a7bebc81b0b95e078774bc0c9b534d70b58f852caad1b564f508151c91c` |
71
+ | `scripts/lib/SoakClients.js` | `b00458a3dd1970124107e3c6990848af738f080e6ea3f2c7b56e9e952a4c22a5` |
72
+ | `scripts/lib/SoakMeasurement.js` | `0883d6d5ef6c92b8a2161274b1dbe86187795a629de0705f6e6928f812d7424a` |
73
+
74
+ Report `coverage/soak-tools/coverage-final.json` SHA-256:
75
+ `a32bc0a90a69aba60c727e060df20e5fe90181335b618e96a963cb69d028a4c8`.
76
+
77
+ An initial clean 30-second/16-client run before the review follow-ups passed:
78
+ 4,365 sent, 4,364 received, one missing (99.97709049%), seven samples, all eight
79
+ trends stable, zero registries, final heap 100.09062623% and handles 1→2.
80
+ Report `coverage/soak-corrected-20260831-0346.json` SHA-256:
81
+ `860351a6038c9a43c5a0190ff98400c88cdf3de0201a20eaacfda721d8b8a73f`.
82
+
83
+ After final scoped tests exited, the corrected 30-second/16-client run passed:
84
+ 4,368 sent, 4,365 received, **three missing** (99.93131868%), seven samples, all
85
+ eight trends stable, zero registries, final heap 99.78205308% and handles 1→2.
86
+ Raw evidence `coverage/soak-corrected-final-20260831.json` has SHA-256
87
+ `9816802b52c68a774b232e23871fed16609baa88cd9c9a7c7e4809ac357f0113`. Neither run is
88
+ lossless. Hosted checks for this increment remain pending.
89
+ No npm publication, deployment, merge or new 60-minute soak occurred.
90
+
91
+ The full regression selected at `31fa9b2` passed 1,246 tests/119 suites in
92
+ 653.569 seconds, with two POSIX-only skips on Windows. All 91 library files retain
93
+ 100% coverage: 5,449 statements, 4,046 branches, 978 functions and 4,468 lines.
94
+ Library and soak-verifier sources remained unchanged throughout. Five later
95
+ application-recorder units passed separately and are not included in 1,246.
96
+ Generated-content/type checks pass, and the critic approved all 15 actual remote
97
+ blobs on PR16. Hosted PR33370390186 and push33370386741 were still running when
98
+ this evidence was recorded; completed lifecycle/package jobs are not a complete
99
+ workflow pass.
100
+
101
+ Full map `coverage/coverage-final.json` SHA-256:
102
+ `78ffc666b8181154cf5a6c2b9673e82b6f65b50d6c5fbb29d02941c96d598d0e`.
103
+ Inventory `coverage/soak-full-results.json` SHA-256:
104
+ `2ac4d0bc14ec7fee441a38f710ac1234b68ceb7c94d1280523df3acaa9b2304e`.
105
+
106
+ ## Subsequent hosted room-phase failure
107
+
108
+ Both workflows at `31fa9b2` eventually completed successfully. At `d29be8c`,
109
+ push run 33371370687 passed, but [PR run 33371374433](https://github.com/lakam99/redweb/actions/runs/33371374433)
110
+ failed its Node 20 ten-second/two-client mechanics fixture. That failed result
111
+ is not replaced by the passing sibling: 198 sent/198 received, no missing replies,
112
+ 11 samples, zero final registries and final heap 103.71149824% of warm. The sole
113
+ failed trend was rooms: early 1, late 2, delta 1, peak 2, allowance 0, monotonically
114
+ increasing. The saved summary lacks phase samples, so the exact CI timing cannot
115
+ be reconstructed.
116
+
117
+ A new deterministic native test uses real Redweb room handling and `SoakClients`:
118
+ two clients join separate rooms; rotating one removes its sole-member room; the
119
+ replacement connects while the room count remains one; its next tick recreates
120
+ the room. All four replies are exact and final clients/rooms are zero. This proves
121
+ the observable 2→1→2 phase exists, not that the historical CI run followed that
122
+ precise sequence. Its first local fixture omitted room configuration and failed;
123
+ after supplying the actual soak's room settings, the deterministic test passed.
124
+
125
+ The ten-second test now checks measurement mechanics and the unchanged policy's
126
+ actual exit code. It permits only that exact 1→2 room-trend failure; all other
127
+ trends, delivery, final heap, handles and empty-registry assertions remain strict.
128
+ Launch, timeout, malformed output, stderr and cleanup failures still fail the test.
129
+ Its report and actual exit code are retained in
130
+ `coverage/soak-tools/smoke-reports/` as **mechanics-only**, including in CI artifacts.
131
+ A passing mechanics test does not relabel its nonzero measurement as acceptance.
132
+
133
+ No runtime, traffic, rotation, sampling, trend budget or policy source changed.
134
+ The separate blocking CI soak remains 30 seconds / 16 clients, and the default
135
+ one-hour workload and all acceptance thresholds are unchanged. Neither the
136
+ historical short failure nor the open throughput benchmark is waived.
137
+
138
+ The corrected mechanics fixture passed within the maintained 80-test/four-suite
139
+ soak coverage run (14.414 seconds), retaining all-four 100% of the unchanged three
140
+ soak modules. The separate deterministic room-phase test passed in 0.751 seconds.
141
+ The senior critic approved both the bounded reproduction and narrowly preserved
142
+ failure outcome. A complete regression and fresh clean blocking-soak check follow
143
+ this checkpoint; neither is claimed here before it finishes.
144
+
145
+ That follow-up completed at the `551a905` implementation checkpoint: full local
146
+ regression passed 1,292 tests/126 suites with unchanged library scope/100% coverage,
147
+ and both PR33373452034 and push33373448945 passed every matrix/lifecycle job.
148
+ The single clean 30-second / 16-client gate recorded 4,368 sent, 4,363 received,
149
+ five missing (99.88553114%), seven samples, all eight trends passing, empty final
150
+ registries, final heap 99.909556% and handles 1→2. Raw report
151
+ `coverage/soak-package-final-20260831.json` SHA-256:
152
+ `4bd8bd5e95054da8c27f1236ca42335663fde6e303c3a850db3e4a684079b371`.
153
+ This remains an unchanged-threshold short acceptance result, not lossless delivery,
154
+ a new hour soak or an explanation of the exact earlier CI scheduling sequence.