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.
- package/CHANGELOG.md +166 -9
- package/README.md +177 -523
- package/bin/redweb.js +11 -20
- package/client.d.ts +7 -2
- package/config/tsconfig.json +14 -14
- package/contract.d.ts +45 -0
- package/contract.js +5 -0
- package/docs/ACTION_INPUT_VERIFICATION.md +96 -0
- package/docs/ADMISSION_TIMEOUT_VERIFICATION.md +69 -0
- package/docs/AGENT_ACCESS.md +35 -0
- package/docs/AGENT_EVALUATION.md +58 -0
- package/docs/AGENT_READY_ACCEPTANCE.md +763 -0
- package/docs/APPLICATION_RECORDER_VERIFICATION.md +50 -0
- package/docs/BENCHMARK_VERIFICATION.md +307 -0
- package/docs/BROWSER_OWNER_VERIFICATION.md +191 -0
- package/docs/CLI.md +116 -0
- package/docs/CLIENT_DEVELOPMENT.md +152 -0
- package/docs/CLIENT_POLISH_VERIFICATION.md +282 -0
- package/docs/COVERAGE_COUNTER_VALIDATION.md +109 -0
- package/docs/COVERAGE_SCOPE_AUDIT.md +1183 -0
- package/docs/DEVELOPMENT.md +79 -0
- package/docs/DIAGNOSTIC_COMPATIBILITY.md +76 -0
- package/docs/DOCUMENTATION.md +37 -0
- package/docs/FEEDBACK_COMMAND_VERIFICATION.md +228 -0
- package/docs/GETTING_STARTED.md +58 -0
- package/docs/JSX_PERFORMANCE_VERIFICATION.md +59 -0
- package/docs/LIVE_HTML.md +169 -21
- package/docs/LIVE_HTML_LOAD_VERIFICATION.md +98 -0
- package/docs/MIGRATION.md +28 -0
- package/docs/MULTIPLAYER_OPERATIONS.md +26 -4
- package/docs/ORIGINAL_RECOVERY_VERIFICATION.md +100 -0
- package/docs/PACKAGED_EXAMPLE_VERIFICATION.md +126 -0
- package/docs/POLISH_RELEASE_CHECKPOINT.md +91 -0
- package/docs/PROCESS_CLEANUP_OBSERVATION.md +61 -0
- package/docs/PROCESS_REAPING_VERIFICATION.md +30 -0
- package/docs/PRODUCTION_READINESS.md +11 -2
- package/docs/RECOVERY_CLIENT_HEAP.md +201 -0
- package/docs/RECOVERY_CODE_ATTRIBUTION.md +174 -0
- package/docs/RECOVERY_CODE_CENSUS.md +158 -0
- package/docs/RECOVERY_COMPARISON.md +103 -0
- package/docs/RECOVERY_DEOPTIMIZATION.md +169 -0
- package/docs/RECOVERY_FOLLOWUP_SPIKE.md +147 -0
- package/docs/RECOVERY_INVESTIGATION.md +229 -0
- package/docs/RECOVERY_RUNTIME_CONTROLS.md +181 -0
- package/docs/RELEASE_TRUST.md +58 -0
- package/docs/ROOM_AUTHORIZATION.md +49 -0
- package/docs/RUNTIME_DIAGNOSTICS.md +78 -0
- package/docs/SERVER_RECOVERY_CANDIDATE.md +185 -0
- package/docs/SOAK_ROTATION_OBSERVATION.md +160 -0
- package/docs/SOAK_VERIFICATION.md +154 -0
- package/docs/SOCKET_CONTRACTS.md +39 -0
- package/docs/SPLIT_RECOVERY_COVERAGE.md +83 -0
- package/docs/SPLIT_RECOVERY_ERROR_HANDLING.md +67 -0
- package/docs/STARTER_COORDINATOR_VERIFICATION.md +112 -0
- package/docs/STARTER_LIFECYCLE_VERIFICATION.md +75 -0
- package/docs/STARTER_REPORT_RETENTION.md +73 -0
- package/docs/VERIFICATION_EVIDENCE.md +2 -0
- package/docs/generated.json +2154 -0
- package/docs/guides/chatroom.md +27 -0
- package/docs/guides/http-websocket.md +28 -0
- package/docs/guides/jsx-without-react.md +26 -0
- package/docs/guides/realtime-dashboard.md +29 -0
- package/docs/guides/typed-websockets.md +26 -0
- package/docs/reference.json +1207 -0
- package/docs/snippets/room-access.tsx +51 -0
- package/docs/topics.json +21 -0
- package/examples/live-html/chatroom.js +207 -268
- package/examples/live-html/chatroom.tsx +167 -0
- package/examples/live-html/jsx-page.js +1 -1
- package/examples/live-html/jsx-page.tsx +1 -1
- package/examples/live-html/tsconfig.json +8 -7
- package/index.d.ts +170 -45
- package/index.js +2 -0
- package/jsx-dev-runtime.js +2 -2
- package/jsx-runtime.d.ts +7 -2
- package/package.json +88 -7
- package/recipes/add/artifact.test.cjs +57 -0
- package/recipes/add/live.tsx +18 -0
- package/recipes/add/socket-route.ts +24 -0
- package/recipes/chat/README.md +22 -0
- package/recipes/chat/app.test.cjs +105 -0
- package/recipes/chat/app.tsx +9 -0
- package/recipes/dashboard/README.md +43 -0
- package/recipes/dashboard/admin.ts +21 -0
- package/recipes/dashboard/app.css +16 -0
- package/recipes/dashboard/app.test.cjs +450 -0
- package/recipes/dashboard/app.tsx +86 -0
- package/recipes/dashboard/auth.ts +80 -0
- package/recipes/dashboard/cards.tsx +102 -0
- package/recipes/dashboard/rate-window.test.cjs +17 -0
- package/recipes/dashboard/store.ts +120 -0
- package/recipes/http-ws/README.md +11 -0
- package/recipes/http-ws/app.test.cjs +92 -0
- package/recipes/http-ws/app.tsx +36 -0
- package/recipes/realtime/README.md +8 -0
- package/recipes/realtime/app.test.cjs +15 -0
- package/recipes/realtime/app.tsx +28 -0
- package/recipes/shared/README.md +40 -0
- package/recipes/shared/app.css +8 -0
- package/recipes/shared/copy-assets.cjs +8 -0
- package/recipes/shared/network.cjs +59 -0
- package/recipes/shared/run-app.test.cjs +158 -0
- package/recipes/shared/run-app.ts +50 -0
- package/recipes/site/README.md +4 -0
- package/recipes/site/app.test.cjs +19 -0
- package/recipes/site/app.tsx +25 -0
- package/recipes/socket/README.md +39 -0
- package/recipes/socket/app.test.cjs +85 -0
- package/recipes/socket/app.tsx +30 -0
- package/recipes/socket/contract.ts +12 -0
- package/recipes/socket/handlers.ts +40 -0
- package/src/OwnedServerLifecycle.js +66 -0
- package/src/access/AccessPolicy.js +37 -0
- package/src/access/AuthenticationFailure.js +13 -0
- package/src/access/RequestFailure.js +33 -0
- package/src/access/failure-codes.json +25 -0
- package/src/async/BoundedOperation.js +62 -0
- package/src/cli/ActionReferences.js +193 -0
- package/src/cli/AdditionLayout.js +140 -0
- package/src/cli/FilePlan.js +94 -0
- package/src/cli/ProjectAddition.js +60 -0
- package/src/cli/ProjectConfig.js +26 -0
- package/src/cli/ProjectDoctor.js +112 -0
- package/src/cli/ProjectInitializer.js +20 -30
- package/src/cli/SourceInspector.js +207 -0
- package/src/cli/StaticSource.js +192 -0
- package/src/cli/arguments.js +62 -0
- package/src/cli/formatCommand.js +10 -0
- package/src/cli/run.js +57 -0
- package/src/cli/templates.js +86 -87
- package/src/context/RequestSnapshot.js +41 -0
- package/src/dataProperty.js +11 -0
- package/src/development/DevelopmentPageManager.js +48 -0
- package/src/development/Inspection.js +104 -0
- package/src/development/ObservedRenderer.js +42 -0
- package/src/development/description.js +35 -0
- package/src/development/loopbackRequest.js +27 -0
- package/src/development/refreshBrowser.js +96 -0
- package/src/development/refreshStyles.js +9 -0
- package/src/development/settings.js +17 -0
- package/src/docs/Documentation.js +182 -0
- package/src/htmx/ActionDefinition.js +44 -0
- package/src/htmx/Jsx.js +24 -8
- package/src/htmx/LiveHtmlServer.js +41 -19
- package/src/htmx/LivePage.js +63 -13
- package/src/htmx/PageIdentity.js +32 -0
- package/src/htmx/PageLifetime.js +37 -0
- package/src/htmx/PageManager.js +203 -74
- package/src/htmx/ReactiveRenderer.js +241 -0
- package/src/htmx/StaticExporter.js +1 -1
- package/src/htmx/TemplateRenderer.js +13 -7
- package/src/htmx/browserRuntime.js +2 -93
- package/src/htmx/metadata.js +19 -7
- package/src/validation/ActionInputError.js +12 -0
- package/src/validation/SchemaValidator.js +38 -0
- package/src/ws/AdmissionPolicy.js +24 -23
- package/src/ws/BaseSocketServer.js +53 -38
- package/src/ws/ContractValidationError.js +12 -0
- package/src/ws/HeartbeatMonitor.js +19 -7
- package/src/ws/ProtocolPolicy.js +1 -1
- package/src/ws/RoomAccess.js +82 -0
- package/src/ws/RoomRegistry.js +56 -6
- package/src/ws/RouteRuntime.js +56 -10
- package/src/ws/SocketContract.js +112 -0
- package/src/ws/SocketRoute.js +18 -0
- package/src/ws/protocol-schema.json +6 -1
- 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.
|