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
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "redweb",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "A small Node.js foundation for HTTP, WebSockets, multiplayer services, and server-rendered HTML",
|
|
3
|
+
"version": "0.13.0",
|
|
4
|
+
"description": "A small Node.js foundation for HTTP, WebSockets, multiplayer services, and server-rendered HTML",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"types": "index.d.ts",
|
|
7
7
|
"bin": {
|
|
@@ -20,6 +20,12 @@
|
|
|
20
20
|
"require": "./client.js",
|
|
21
21
|
"default": "./client.js"
|
|
22
22
|
},
|
|
23
|
+
"./contract": {
|
|
24
|
+
"types": "./contract.d.ts",
|
|
25
|
+
"import": "./contract.js",
|
|
26
|
+
"require": "./contract.js",
|
|
27
|
+
"default": "./contract.js"
|
|
28
|
+
},
|
|
23
29
|
"./jsx-runtime": {
|
|
24
30
|
"types": "./jsx-runtime.d.ts",
|
|
25
31
|
"import": "./jsx-runtime.js",
|
|
@@ -36,8 +42,17 @@
|
|
|
36
42
|
"./package.json": "./package.json"
|
|
37
43
|
},
|
|
38
44
|
"scripts": {
|
|
39
|
-
"pretest": "node scripts/build-live-html-examples.js --check && node scripts/generate-protocol-types.js --check && tsc -p tests/types/tsconfig.json && tsc -p tests/types/tsconfig.jsxdev.json && tsc -p tests/types/tsconfig.standard.json",
|
|
40
|
-
"prepack": "node scripts/build-live-html-examples.js --check",
|
|
45
|
+
"pretest": "node scripts/build-live-html-examples.js --check && node scripts/generate-protocol-types.js --check && node scripts/generate-docs.js --check && tsc -p tests/types/tsconfig.json && tsc -p tests/types/tsconfig.jsxdev.json && tsc -p tests/types/tsconfig.standard.json",
|
|
46
|
+
"prepack": "node scripts/build-live-html-examples.js --check && node scripts/generate-docs.js --check",
|
|
47
|
+
"generate:docs": "node scripts/generate-docs.js",
|
|
48
|
+
"verify:docs:mcp": "npm --prefix integrations/docs-mcp test",
|
|
49
|
+
"verify:cli": "c8 --all --src=bin --include=bin/redweb.js --reporter=text --reporter=json --reports-dir=coverage/cli-entrypoint --check-coverage --lines=100 --branches=100 --functions=100 --statements=100 node node_modules/jest/bin/jest.js tests/integration/init-cli.integration.test.js tests/integration/add-cli.integration.test.js --testNamePattern=\"redweb init CLI integration|human and subprocess CLI\" --runInBand --coverage=false",
|
|
50
|
+
"verify:agents:controls": "node scripts/evaluation/validate.js",
|
|
51
|
+
"verify:live-html:browser:coverage": "node node_modules/jest/bin/jest.js tests/integration/frozen-live-browser.integration.test.js tests/unit/frozen-live-browser.unit.test.js --runInBand --coverage --collectCoverageFrom=scripts/verify-live-html-browser.js --coverageDirectory=coverage/frozen-live-browser",
|
|
52
|
+
"verify:evaluation:controls:coverage": "node node_modules/jest/bin/jest.js tests/integration/evaluation-controls.integration.test.js tests/unit/evaluation-controls-boundaries.unit.test.js tests/integration/evaluation-verifier.integration.test.js tests/unit/evaluation-verifier-boundaries.unit.test.js --runInBand --coverage --collectCoverageFrom=scripts/evaluation/validate.js --collectCoverageFrom=scripts/evaluation/verify.js --coverageDirectory=coverage/frozen-controls",
|
|
53
|
+
"verify:evaluation:prepare:coverage": "node node_modules/jest/bin/jest.js tests/integration/evaluation-prepare.integration.test.js tests/unit/evaluation-prepare-boundaries.unit.test.js --runInBand --coverage --collectCoverageFrom=scripts/evaluation/prepare.js --coverageDirectory=coverage/frozen-prepare",
|
|
54
|
+
"verify:evaluation:trial:coverage": "node node_modules/jest/bin/jest.js tests/unit/evaluation-evidence.unit.test.js tests/integration/evaluation-trial.integration.test.js tests/unit/evaluation-trial-boundaries.unit.test.js --runInBand --coverage --collectCoverageFrom=scripts/evaluation/run-trial.js --coverageDirectory=coverage/frozen-trial",
|
|
55
|
+
"verify:evaluation:process:coverage": "node node_modules/jest/bin/jest.js tests/unit/evaluation-process-boundaries.unit.test.js tests/unit/evaluation-evidence.unit.test.js tests/unit/evaluation-process.unit.test.js tests/integration/evaluation-frozen.integration.test.js --runInBand --coverage --collectCoverageFrom=scripts/evaluation/seal.js --collectCoverageFrom=scripts/evaluation/process.js --coverageDirectory=coverage/frozen-process-seal",
|
|
41
56
|
"example:counter": "node examples/live-html/counter.js",
|
|
42
57
|
"example:chatroom": "node examples/live-html/chatroom.js",
|
|
43
58
|
"example:cards": "node examples/live-html/cards.js",
|
|
@@ -45,15 +60,52 @@
|
|
|
45
60
|
"example:jsx": "node examples/live-html/jsx-page.js",
|
|
46
61
|
"generate:protocol-types": "node scripts/generate-protocol-types.js",
|
|
47
62
|
"verify:overhead": "node scripts/verify-disabled-overhead.js",
|
|
63
|
+
"verify:overhead:coverage": "node node_modules/jest/bin/jest.js tests/unit/benchmark-command.unit.test.js tests/integration/benchmark-measurement.integration.test.js tests/unit/benchmark-traffic.unit.test.js tests/unit/benchmark-batch.unit.test.js tests/unit/benchmark-comparison.unit.test.js --runInBand --coverage --collectCoverageFrom=scripts/benchmark-worker.js --collectCoverageFrom=scripts/verify-disabled-overhead.js --collectCoverageFrom=scripts/lib/BenchmarkBatch.js --collectCoverageFrom=scripts/lib/BenchmarkComparison.js --collectCoverageFrom=scripts/lib/BenchmarkWorkload.js --collectCoverageFrom=scripts/lib/measureBenchmarkBatch.js --coverageDirectory=coverage/benchmark-tools",
|
|
48
64
|
"verify:soak": "node --expose-gc scripts/verify-soak.js",
|
|
65
|
+
"verify:soak:coverage": "node node_modules/jest/bin/jest.js tests/unit/soak-measurement.unit.test.js tests/unit/soak-clients.unit.test.js tests/unit/soak-command.unit.test.js tests/unit/soak-command-evidence.unit.test.js tests/integration/soak-tools.integration.test.js --runInBand --coverage --collectCoverageFrom=scripts/verify-soak.js --collectCoverageFrom=scripts/lib/SoakClients.js --collectCoverageFrom=scripts/lib/SoakMeasurement.js --collectCoverageFrom=tests/helpers/SoakCommandEvidence.js --coverageDirectory=coverage/soak-tools",
|
|
49
66
|
"verify:memory": "node scripts/verify-memory-overhead.js",
|
|
67
|
+
"verify:memory:coverage": "node node_modules/jest/bin/jest.js tests/unit/memory-command.unit.test.js tests/unit/memory-measurement.unit.test.js tests/integration/memory-measurement.integration.test.js tests/unit/verification-workspace.unit.test.js --runInBand --coverage --collectCoverageFrom=scripts/memory-worker.js --collectCoverageFrom=scripts/verify-memory-overhead.js --collectCoverageFrom=scripts/lib/MemoryMeasurement.js --collectCoverageFrom=scripts/lib/VerificationWorkspace.js --coverageDirectory=coverage/memory-tools",
|
|
50
68
|
"verify:load": "node scripts/verify-load.js",
|
|
69
|
+
"verify:load:coverage": "node node_modules/jest/bin/jest.js tests/unit/load-command.unit.test.js tests/unit/load-traffic.unit.test.js tests/unit/load-measurement.unit.test.js tests/integration/load-measurement.integration.test.js tests/unit/realtime-harness.unit.test.js tests/integration/realtime-harness.integration.test.js --runInBand --coverage --collectCoverageFrom=scripts/verify-load.js --collectCoverageFrom=scripts/lib/LoadMeasurement.js --collectCoverageFrom=scripts/lib/measureLoadTraffic.js --collectCoverageFrom=scripts/realtime-harness.js --coverageDirectory=coverage/load-tools",
|
|
51
70
|
"verify:recovery": "node --expose-gc scripts/verify-recovery.js",
|
|
71
|
+
"verify:recovery:original:coverage": "node node_modules/jest/bin/jest.js tests/unit/original-recovery.unit.test.js tests/integration/recovery-verifier.integration.test.js --runInBand --silent --coverage --collectCoverageFrom=scripts/verify-recovery.js --coverageDirectory=coverage/original-recovery",
|
|
72
|
+
"verify:recovery:server": "node scripts/verify-server-recovery.js",
|
|
73
|
+
"verify:recovery:coverage": "node node_modules/jest/bin/jest.js tests/unit/server-recovery-policy.unit.test.js tests/unit/server-recovery-candidate.unit.test.js tests/unit/server-recovery-cli.unit.test.js tests/unit/recovery-split.unit.test.js tests/unit/recovery-coordinator-boundaries.unit.test.js tests/unit/recovery-worker-boundaries.unit.test.js tests/unit/recovery-worker-error.unit.test.js tests/integration/server-recovery-candidate.integration.test.js tests/integration/recovery-split.integration.test.js tests/integration/recovery-channel.integration.test.js --runInBand --silent --coverage --collectCoverageFrom=scripts/lib/ServerRecoveryPolicy.js --collectCoverageFrom=scripts/lib/ServerRecoveryCandidate.js --collectCoverageFrom=scripts/verify-server-recovery.js --collectCoverageFrom=scripts/diagnostics/recovery-split.cjs --collectCoverageFrom=scripts/diagnostics/recovery-split-worker.cjs --coverageDirectory=coverage/server-recovery-acceptance",
|
|
74
|
+
"verify:assets:coverage": "c8 --all --src=recipes/shared --include=recipes/shared/copy-assets.cjs --reporter=text --reporter=json --reports-dir=coverage/recipe-assets --check-coverage --lines=100 --branches=100 --functions=100 --statements=100 node node_modules/jest/bin/jest.js tests/integration/copy-assets.integration.test.js --runInBand --coverage=false",
|
|
75
|
+
"verify:room:coverage": "node node_modules/jest/bin/jest.js tests/unit/room-verifier.unit.test.js tests/integration/documentation.integration.test.js --testNamePattern=\"room cleanup|shared page/room\" --runInBand --coverage --collectCoverageFrom=scripts/lib/verify-room-example.js --coverageDirectory=coverage/room-verifier",
|
|
76
|
+
"verify:action:coverage": "node node_modules/jest/bin/jest.js tests/unit/action-verifier.unit.test.js tests/integration/action-verifier.integration.test.js tests/integration/action-input.integration.test.js --runInBand --silent --coverage --collectCoverageFrom=scripts/lib/verify-action-input.js --coverageDirectory=coverage/action-verifier",
|
|
77
|
+
"verify:reports:coverage": "node node_modules/jest/bin/jest.js tests/unit/report-command.unit.test.js tests/integration/command-report.integration.test.js --runInBand --silent --coverage --collectCoverageFrom=scripts/lib/reportCommand.js --coverageDirectory=coverage/command-report",
|
|
78
|
+
"verify:build:coverage": "node node_modules/jest/bin/jest.js tests/integration/protocol-generator.integration.test.js tests/integration/example-builder.integration.test.js tests/integration/documentation.integration.test.js tests/integration/script-coverage.integration.test.js --testNamePattern=\"protocol generator|example builder|release snapshots|unit fault: script verifier|real command evidence\" --runInBand --coverage=false",
|
|
79
|
+
"verify:recorder:coverage": "node node_modules/jest/bin/jest.js tests/unit/application-recorder.unit.test.js tests/integration/script-coverage.integration.test.js --testNamePattern=\"native coverage recorder|real command evidence associates\" --runInBand --coverage --collectCoverageFrom=scripts/lib/record-application-coverage.cjs --coverageDirectory=coverage/application-recorder-authored",
|
|
80
|
+
"verify:recovery:diagnostics": "c8 --all --include=scripts/diagnostics/recovery-heap-graph.cjs --include=scripts/diagnostics/recovery-heap-summary.cjs --reporter=text --reporter=json --reports-dir=coverage/recovery-native-exact --check-coverage --lines=100 --branches=100 --functions=100 --statements=100 node --test tests/unit/recovery-heap-graph.unit.test.js",
|
|
52
81
|
"verify:live-html": "node scripts/build-live-html-examples.js --check && npx jest tests/integration/live-html.integration.test.js --runInBand --coverage=false",
|
|
53
82
|
"verify:live-html:browser": "node scripts/build-live-html-examples.js --check && node scripts/verify-live-html-browser.js",
|
|
83
|
+
"verify:browser:supplements": "node node_modules/jest/bin/jest.js tests/unit/browser-supplements.unit.test.js tests/integration/browser-supplements.integration.test.js --runInBand --coverage --collectCoverageFrom=scripts/lib/verify-live-page-ownership.js --collectCoverageFrom=scripts/lib/verify-runtime-browser.js --coverageDirectory=coverage/browser-supplements-authored",
|
|
84
|
+
"verify:dashboard:coverage": "node node_modules/jest/bin/jest.js tests/unit/dashboard-verifier.unit.test.js tests/integration/dashboard-verifier.integration.test.js --runInBand --coverage --collectCoverageFrom=scripts/lib/verify-dashboard-browser.js --coverageDirectory=coverage/dashboard-verifier",
|
|
85
|
+
"verify:refresh:coverage": "node node_modules/jest/bin/jest.js tests/unit/refresh-controls-failures.unit.test.js tests/unit/refresh-observation.unit.test.js tests/unit/heading-readiness.unit.test.js tests/integration/refresh-observation.integration.test.js tests/integration/refresh-coverage.integration.test.js --runInBand --coverage --collectCoverageFrom=scripts/lib/verify-refresh-controls.js --collectCoverageFrom=scripts/lib/verify-refresh-coverage.js --coverageDirectory=coverage/refresh-verifier",
|
|
86
|
+
"verify:browser:coverage": "node node_modules/jest/bin/jest.js tests/unit/browser-coverage.unit.test.js tests/unit/application-coverage.unit.test.js tests/unit/packed-browser-harness.unit.test.js --runInBand --coverage --collectCoverageFrom=scripts/lib/BrowserCoverage.js --collectCoverageFrom=scripts/lib/ApplicationCoverage.js --collectCoverageFrom=scripts/lib/assertCoverageFile.js --coverageDirectory=coverage/browser-collector && npm run verify:browser:coordinator:coverage",
|
|
87
|
+
"verify:browser:coordinator:coverage": "node node_modules/jest/bin/jest.js tests/unit/browser-coordinator-report.unit.test.js tests/unit/browser-coordinator-lifecycle.unit.test.js tests/unit/browser-coverage-cleanup.unit.test.js tests/unit/browser-supplements.unit.test.js tests/unit/refresh-controls-failures.unit.test.js tests/unit/refresh-observation.unit.test.js tests/unit/heading-readiness.unit.test.js tests/integration/browser-coordinator.integration.test.js tests/integration/browser-supplements.integration.test.js tests/integration/refresh-observation.integration.test.js tests/integration/refresh-coverage.integration.test.js --runInBand --coverage --collectCoverageFrom=scripts/verify-browser-coverage.js --collectCoverageFrom=scripts/lib/verify-live-page-ownership.js --collectCoverageFrom=scripts/lib/verify-runtime-browser.js --collectCoverageFrom=scripts/lib/verify-refresh-controls.js --collectCoverageFrom=scripts/lib/verify-refresh-coverage.js --coverageDirectory=coverage/browser-coordinator",
|
|
88
|
+
"verify:feedback:commands": "node node_modules/jest/bin/jest.js tests/unit/browser-commands.unit.test.js tests/integration/feedback-browser.integration.test.js tests/integration/feedback-command-timeout.integration.test.js tests/integration/feedback-verifier.integration.test.js --runInBand --coverage --collectCoverageFrom=scripts/lib/browserCommands.js --collectCoverageFrom=scripts/lib/verify-action-feedback.js --coverageDirectory=coverage/browser-commands",
|
|
89
|
+
"measure:browser:client": "node scripts/verify-browser-coverage.js client",
|
|
90
|
+
"verify:client:coordinator:coverage": "node node_modules/jest/bin/jest.js tests/unit/client-source-coordinator.unit.test.js tests/integration/client-source-preflight.integration.test.js --runInBand --coverage --collectCoverageFrom=scripts/verify-client-source-coverage.js --coverageDirectory=coverage/client-source-coordinator",
|
|
91
|
+
"verify:client:source-coverage": "node node_modules/jest/bin/jest.js tests/unit/client-source-coverage.unit.test.js tests/integration/client-source-preflight.integration.test.js tests/integration/command-report.integration.test.js --runInBand --coverage --collectCoverageFrom=scripts/lib/ClientSourceCoverage.js --collectCoverageFrom=scripts/lib/reportCommand.js --coverageDirectory=coverage/client-source-collector && node --test tests/integration/client-worker-retention.integration.test.cjs && node scripts/verify-client-source-coverage.js",
|
|
92
|
+
"verify:development:browser": "node scripts/verify-development-refresh-browser.js",
|
|
93
|
+
"verify:development:coverage": "node node_modules/jest/bin/jest.js tests/unit/development-refresh-cleanup.unit.test.js tests/unit/development-refresh-boundaries.unit.test.js tests/integration/development-refresh-verifier.integration.test.js --runInBand --coverage --collectCoverageFrom=scripts/verify-development-refresh-browser.js --coverageDirectory=coverage/development-coordinator",
|
|
94
|
+
"verify:starters:lifecycle": "node scripts/verify-starter-lifecycle.js",
|
|
95
|
+
"verify:starters:lifecycle:coverage": "node node_modules/jest/bin/jest.js tests/unit/starter-lifecycle-command.unit.test.js tests/integration/starter-lifecycle-coverage.integration.test.js --runInBand --silent --coverage --collectCoverageFrom=scripts/verify-starter-lifecycle.js --coverageDirectory=coverage/starter-lifecycle-coordinator",
|
|
96
|
+
"measure:starters:coverage": "node scripts/measure-starter-coverage.js",
|
|
97
|
+
"verify:starter-coordinators:coverage": "node node_modules/jest/bin/jest.js tests/unit/starter-coordinators.unit.test.js tests/integration/verification-summary.integration.test.js tests/integration/starter-coordinator-input.integration.test.js --runInBand --silent --coverage --collectCoverageFrom=scripts/measure-starter-coverage.js --collectCoverageFrom=scripts/verify-starter-source-coverage.js --collectCoverageFrom=scripts/lib/finishVerificationSummary.js --coverageDirectory=coverage/starter-coordinators",
|
|
98
|
+
"verify:starters:source-coverage": "node node_modules/jest/bin/jest.js tests/unit/application-coverage.unit.test.js --runInBand --coverage --collectCoverageFrom=scripts/lib/ApplicationCoverage.js --collectCoverageFrom=scripts/lib/assertCoverageFile.js --coverageDirectory=coverage/application-collector && node scripts/verify-starter-source-coverage.js",
|
|
99
|
+
"verify:package:tools": "node node_modules/jest/bin/jest.js tests/unit/verification-workspace.unit.test.js tests/unit/client-candidate.unit.test.js tests/unit/packed-browser-harness.unit.test.js tests/unit/packed-browser-report.unit.test.js tests/integration/starter-verifier.integration.test.js tests/integration/starters.integration.test.js tests/integration/documentation.integration.test.js tests/integration/action-input.integration.test.js tests/integration/consumer-compiler.integration.test.js --runInBand --coverage --collectCoverageFrom=scripts/lib/VerificationWorkspace.js --collectCoverageFrom=scripts/lib/ClientCandidate.js --collectCoverageFrom=scripts/lib/InstalledClient.js --collectCoverageFrom=scripts/lib/PackedBrowserHarness.js --collectCoverageFrom=scripts/lib/preservePackedBrowserReport.js --collectCoverageFrom=scripts/lib/verificationError.js --collectCoverageFrom=scripts/lib/verify-starter.js --collectCoverageFrom=scripts/lib/verify-documentation.js --collectCoverageFrom=scripts/lib/compile-consumer.js --coverageDirectory=coverage/package-tools",
|
|
54
100
|
"verify:live-html:load": "node scripts/build-live-html-examples.js --check && node --expose-gc scripts/verify-live-html-load.js",
|
|
101
|
+
"verify:live-html:load:coverage": "node node_modules/jest/bin/jest.js tests/unit/live-html-load-command.unit.test.js tests/unit/live-html-load-tools.unit.test.js tests/integration/live-html-load-tools.integration.test.js --runInBand --coverage --collectCoverageFrom=scripts/verify-live-html-load.js --collectCoverageFrom=scripts/lib/readLiveHtmlPage.js --collectCoverageFrom=scripts/lib/LiveHtmlLoadClient.js --coverageDirectory=coverage/live-html-load-tools",
|
|
55
102
|
"verify:jsx:performance": "node --expose-gc scripts/verify-jsx-performance.js",
|
|
103
|
+
"verify:jsx:coverage": "node node_modules/jest/bin/jest.js tests/unit/jsx-performance-command.unit.test.js tests/integration/jsx-performance.integration.test.js --runInBand --coverage --collectCoverageFrom=scripts/verify-jsx-performance.js --coverageDirectory=coverage/jsx-performance-tools",
|
|
56
104
|
"verify:live-html:package": "node scripts/verify-live-html-package.js",
|
|
105
|
+
"verify:package:coordinator:coverage": "node node_modules/jest/bin/jest.js tests/unit/package-coordinator.unit.test.js tests/unit/packed-browser-report.unit.test.js tests/unit/packed-browser-report-failures.unit.test.js tests/integration/package-coordinator.integration.test.js --runInBand --coverage --collectCoverageFrom=scripts/verify-live-html-package.js --collectCoverageFrom=scripts/lib/preservePackedBrowserReport.js --coverageDirectory=coverage/package-coordinator",
|
|
106
|
+
"verify:package:examples:coverage": "node node_modules/jest/bin/jest.js tests/unit/packed-action.unit.test.js tests/unit/example-dependencies.unit.test.js tests/unit/example-probe.unit.test.js tests/integration/packed-action.integration.test.js tests/integration/example-dependencies.integration.test.js --runInBand --coverage --collectCoverageFrom=scripts/lib/performProbeAction.js --collectCoverageFrom=scripts/lib/verify-example-dependencies.js --collectCoverageFrom=scripts/lib/example-dependency-probe.cjs --coverageDirectory=coverage/example-dependency-tools",
|
|
107
|
+
"verify:package:browser:coverage": "node node_modules/jest/bin/jest.js tests/unit/browser-pages.unit.test.js tests/unit/packed-browser-verifier.unit.test.js tests/unit/browser-coverage-cleanup.unit.test.js tests/integration/packed-browser-verifier.integration.test.js --runInBand --silent --coverage --collectCoverageFrom=scripts/lib/verify-packed-browser.js --collectCoverageFrom=scripts/lib/BrowserPages.js --coverageDirectory=coverage/packed-browser-verifier",
|
|
108
|
+
"verify:client:link": "node --test tests/integration/npm-link.integration.test.cjs",
|
|
57
109
|
"test": "npx jest"
|
|
58
110
|
},
|
|
59
111
|
"files": [
|
|
@@ -62,6 +114,8 @@
|
|
|
62
114
|
"src/*",
|
|
63
115
|
"client.js",
|
|
64
116
|
"client.d.ts",
|
|
117
|
+
"contract.js",
|
|
118
|
+
"contract.d.ts",
|
|
65
119
|
"jsx-runtime.js",
|
|
66
120
|
"jsx-runtime.d.ts",
|
|
67
121
|
"jsx-dev-runtime.js",
|
|
@@ -69,9 +123,28 @@
|
|
|
69
123
|
"CHANGELOG.md",
|
|
70
124
|
"index.d.ts",
|
|
71
125
|
"docs",
|
|
126
|
+
"recipes",
|
|
72
127
|
"examples/live-html"
|
|
73
128
|
],
|
|
74
|
-
"
|
|
129
|
+
"homepage": "https://redweb.magnisolution.com/",
|
|
130
|
+
"repository": {
|
|
131
|
+
"type": "git",
|
|
132
|
+
"url": "git+https://github.com/lakam99/redweb.git"
|
|
133
|
+
},
|
|
134
|
+
"bugs": {
|
|
135
|
+
"url": "https://github.com/lakam99/redweb/issues"
|
|
136
|
+
},
|
|
137
|
+
"keywords": [
|
|
138
|
+
"typescript",
|
|
139
|
+
"jsx",
|
|
140
|
+
"server-rendering",
|
|
141
|
+
"websocket",
|
|
142
|
+
"realtime",
|
|
143
|
+
"multiplayer",
|
|
144
|
+
"express",
|
|
145
|
+
"ssr",
|
|
146
|
+
"static-site"
|
|
147
|
+
],
|
|
75
148
|
"author": "",
|
|
76
149
|
"license": "ISC",
|
|
77
150
|
"engines": {
|
|
@@ -84,13 +157,21 @@
|
|
|
84
157
|
"@types/ws": "^8.18.1",
|
|
85
158
|
"cors": "^2.8.5",
|
|
86
159
|
"express": "^4.22.2",
|
|
87
|
-
"redweb-client": "^0.
|
|
160
|
+
"redweb-client": "^0.2.0",
|
|
88
161
|
"ws": "^8.21.3"
|
|
89
162
|
},
|
|
90
163
|
"devDependencies": {
|
|
91
164
|
"@types/jest": "^29.5.12",
|
|
165
|
+
"c8": "^10.1.3",
|
|
166
|
+
"expect": "29.7.0",
|
|
92
167
|
"jest": "^29.7.0",
|
|
168
|
+
"istanbul-lib-coverage": "3.2.2",
|
|
169
|
+
"istanbul-lib-instrument": "6.0.2",
|
|
170
|
+
"nodemon": "^3.1.11",
|
|
171
|
+
"redweb-dashboard-types": "npm:@types/node@^22.20.1",
|
|
172
|
+
"redweb-legacy-compiler-fixture": "file:tests/fixtures/legacy-compiler",
|
|
93
173
|
"supertest": "^7.0.0",
|
|
94
|
-
"typescript": "^5.9.3"
|
|
174
|
+
"typescript": "^5.9.3",
|
|
175
|
+
"zod": "^4.4.3"
|
|
95
176
|
}
|
|
96
177
|
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
const test = require('node:test');
|
|
2
|
+
const assert = require('node:assert/strict');
|
|
3
|
+
const { once } = require('node:events');
|
|
4
|
+
const { pathToFileURL } = require('node:url');
|
|
5
|
+
const path = require('node:path');
|
|
6
|
+
const WebSocket = require('ws');
|
|
7
|
+
const { start, page, SocketServer } = require('redweb');
|
|
8
|
+
const { jsx } = require('redweb/jsx-runtime');
|
|
9
|
+
|
|
10
|
+
test('__NAME__ generated __KIND__ works over the network', { timeout: 10000 }, async t => {
|
|
11
|
+
// Import only this artifact, never the application's entry point.
|
|
12
|
+
const artifact = await import(pathToFileURL(path.resolve(__dirname, __IMPORT__)).href);
|
|
13
|
+
let Page = artifact.__CLASS__;
|
|
14
|
+
if ('__KIND__' === 'component') {
|
|
15
|
+
Page = page('/') (class TestPage {
|
|
16
|
+
subject = new artifact.__CLASS__();
|
|
17
|
+
render() { return jsx('main', { children: this.subject }); }
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
const app = '__KIND__' === 'socket-route'
|
|
21
|
+
? new SocketServer({ port: 0, bind: '127.0.0.1', logger: null, routes: [artifact.__CLASS__] })
|
|
22
|
+
: start(Page, { port: 0, bind: '127.0.0.1', logger: null });
|
|
23
|
+
let socket;
|
|
24
|
+
t.after(async () => {
|
|
25
|
+
socket?.terminate();
|
|
26
|
+
await app.shutdown();
|
|
27
|
+
});
|
|
28
|
+
if (!app.server.listening) await once(app.server, 'listening');
|
|
29
|
+
const origin = `http://127.0.0.1:${app.server.address().port}`;
|
|
30
|
+
let config;
|
|
31
|
+
if ('__KIND__' !== 'socket-route') {
|
|
32
|
+
const response = await fetch(`${origin}${'__KIND__' === 'page' ? '/__NAME__' : '/'}`);
|
|
33
|
+
assert.equal(response.status, 200);
|
|
34
|
+
const document = await response.text();
|
|
35
|
+
assert.match(document, /<output>0<\/output>/);
|
|
36
|
+
config = JSON.parse(document.match(/id="__redweb_page">([^<]+)</)[1]);
|
|
37
|
+
}
|
|
38
|
+
const endpoint = config
|
|
39
|
+
? `${config.socketPath}?pageId=${config.pageId}&redwebVersion=${encodeURIComponent(config.version)}`
|
|
40
|
+
: `/__NAME__?redwebVersion=${artifact.contract.version}`;
|
|
41
|
+
socket = new WebSocket(`${origin.replace('http:', 'ws:')}${endpoint}`, { origin, handshakeTimeout: 3000 });
|
|
42
|
+
const received = [];
|
|
43
|
+
socket.on('message', raw => received.push(JSON.parse(raw.toString())));
|
|
44
|
+
await once(socket, 'open');
|
|
45
|
+
if (config) {
|
|
46
|
+
socket.send(JSON.stringify({ v: config.version, type: 'redweb:html', payload: {
|
|
47
|
+
kind: 'action', name: 'increment', args: [],
|
|
48
|
+
...('__KIND__' === 'component' ? { component: 'subject' } : {}),
|
|
49
|
+
} }));
|
|
50
|
+
} else await artifact.contract.client(socket).send('ping', { text: 'Hello' }, { requestId: 'probe' });
|
|
51
|
+
const success = message => config
|
|
52
|
+
? message.type === 'redweb:patch' && message.payload.patches.some(patch => patch.html?.includes('<output>1</output>'))
|
|
53
|
+
: message.type === 'pong' && message.payload.text === 'Hello' && message.requestId === 'probe';
|
|
54
|
+
const deadline = Date.now() + 3000;
|
|
55
|
+
while (!received.some(success) && Date.now() < deadline) await new Promise(resolve => setTimeout(resolve, 10));
|
|
56
|
+
assert.ok(received.some(success), `Expected action result; received ${JSON.stringify(received)}`);
|
|
57
|
+
});
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { action, __DECORATOR__, state } from 'redweb';
|
|
2
|
+
|
|
3
|
+
__DECORATION__
|
|
4
|
+
export class __CLASS__ {
|
|
5
|
+
@state() count = 0;
|
|
6
|
+
|
|
7
|
+
@action()
|
|
8
|
+
increment() { this.count += 1; }
|
|
9
|
+
|
|
10
|
+
render() {
|
|
11
|
+
return (
|
|
12
|
+
<section>
|
|
13
|
+
<h1>__TITLE__</h1>
|
|
14
|
+
<button rw-click="increment">Count <output>{this.count}</output></button>
|
|
15
|
+
</section>
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { SocketRoute } from 'redweb';
|
|
2
|
+
import { defineSocketContract } from 'redweb/contract';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
|
|
5
|
+
// This example is stateless. Add application-owned identity and persistence as needed.
|
|
6
|
+
export const contract = defineSocketContract('1', {
|
|
7
|
+
ping: z.object({ text: z.string().max(500) }).strict(),
|
|
8
|
+
pong: z.object({ text: z.string().max(500) }).strict(),
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
export const Ping = contract.handler('ping', (socket, payload, message) =>
|
|
12
|
+
contract.send(socket, 'pong', payload, { requestId: message.requestId }));
|
|
13
|
+
|
|
14
|
+
export class __CLASS__ extends SocketRoute {
|
|
15
|
+
constructor() {
|
|
16
|
+
super({
|
|
17
|
+
path: '/__NAME__', handlers: [Ping], protocol: contract.protocol,
|
|
18
|
+
orderedMessages: true,
|
|
19
|
+
allowDuplicateConnections: true,
|
|
20
|
+
websocketOptions: { maxPayload: 4096 },
|
|
21
|
+
limits: { maxConnections: 100, maxPendingMessages: 32, maxBufferedBytes: 65536 },
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
## Chat starter
|
|
2
|
+
|
|
3
|
+
`src/chatroom.tsx` is the canonical Redweb chat component example, included directly rather than a second implementation.
|
|
4
|
+
The component stores ordinary message/member data and renders it with reactive TSX and stable list keys; no HTML-valued state or explicit binding names are needed.
|
|
5
|
+
Visitors choose a name once, chat in a shared room, and see live presence. Disconnect removes online presence;
|
|
6
|
+
the page session retains its identity briefly for reconnect, then disposal releases it.
|
|
7
|
+
|
|
8
|
+
Display names are not authenticated identities. History is bounded to 100 messages in memory, not a persistent database.
|
|
9
|
+
Use an application-owned persistence service before promising history across restarts or multiple server processes.
|
|
10
|
+
|
|
11
|
+
`@action({ input: chatInputs.join })` validates and normalizes the form before `join` runs;
|
|
12
|
+
`ActionInput<typeof chatInputs.join>` supplies its TypeScript input type. The same pattern handles messages.
|
|
13
|
+
The starter installs Zod as an application dependency; Redweb itself remains validator-independent.
|
|
14
|
+
Invalid field values (including repeated names represented as arrays) receive `ACTION_INVALID_INPUT`, keep the draft,
|
|
15
|
+
and show Redweb's built-in form feedback. Name collisions remain a room rule with their own friendly message.
|
|
16
|
+
Calling a component method directly from server code bypasses transport validation: pass schema-parsed input.
|
|
17
|
+
The schemas reject ordinary unexpected fields; Zod may discard reserved object keys such as `__proto__`.
|
|
18
|
+
Only the parsed `name` or `message` reaches the corresponding action.
|
|
19
|
+
|
|
20
|
+
When using the packed `examples/live-html/chatroom.js` directly instead of the generated starter,
|
|
21
|
+
install its application validator with `npm install zod`. A cloned-repository development install already
|
|
22
|
+
includes it. Redweb's core and the counter example do not require Zod.
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
const test = require('node:test');
|
|
2
|
+
const { once } = require('node:events');
|
|
3
|
+
const assert = require('node:assert/strict');
|
|
4
|
+
const { listen, live, connect } = require('./network.cjs');
|
|
5
|
+
const { createChatroomPage, chatInputs } = require('../dist/chatroom.js');
|
|
6
|
+
|
|
7
|
+
test('the standalone canonical chat reports an occupied default port', { timeout: 10000 }, async t => {
|
|
8
|
+
const net = require('node:net');
|
|
9
|
+
const { spawnSync } = require('node:child_process');
|
|
10
|
+
const occupied = net.createServer(socket => socket.destroy());
|
|
11
|
+
t.after(() => new Promise(resolve => occupied.close(resolve)));
|
|
12
|
+
occupied.listen(8080, '0.0.0.0');
|
|
13
|
+
try { await once(occupied, 'listening'); }
|
|
14
|
+
catch (error) { assert.equal(error.code, 'EADDRINUSE'); } // An existing listener is left untouched.
|
|
15
|
+
const result = spawnSync(process.execPath, ['dist/chatroom.js'], {
|
|
16
|
+
encoding: 'utf8', timeout: 5000, windowsHide: true,
|
|
17
|
+
});
|
|
18
|
+
assert.equal(result.error, undefined);
|
|
19
|
+
assert.equal(result.status, 1);
|
|
20
|
+
assert.match(result.stderr, /EADDRINUSE/);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test('members join once, exchange messages, and leave presence on disconnect', { timeout: 10000 }, async t => {
|
|
24
|
+
const origin = await listen(t);
|
|
25
|
+
const alice = await live(t, origin);
|
|
26
|
+
const bob = await live(t, origin);
|
|
27
|
+
alice.action('join', [{ name: 'Alice' }], 'chat');
|
|
28
|
+
bob.action('join', [{ name: 'Bob' }], 'chat');
|
|
29
|
+
await alice.patch(patch => patch.html.includes('Online · 2'));
|
|
30
|
+
await bob.patch(patch => patch.html.includes('Online · 2'));
|
|
31
|
+
alice.action('send', [{ message: 'Hello <friends>' }], 'chat');
|
|
32
|
+
await bob.patch(patch => patch.html.includes('Hello <friends>'));
|
|
33
|
+
const closed = once(alice.socket, 'close');
|
|
34
|
+
alice.socket.close();
|
|
35
|
+
await closed;
|
|
36
|
+
await bob.patch(patch => patch.html.includes('Online · 1'));
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test('identities stay reserved across reconnects and are released by leaving', { timeout: 10000 }, async t => {
|
|
40
|
+
const origin = await listen(t);
|
|
41
|
+
const alice = await live(t, origin);
|
|
42
|
+
const visitor = await live(t, origin);
|
|
43
|
+
alice.action('join', [{ name: ' Alice ' }], 'chat');
|
|
44
|
+
await alice.patch(patch => patch.html.includes('Connected as') && patch.html.includes('Alice'));
|
|
45
|
+
visitor.action('join', [{ name: 'ALICE' }], 'chat');
|
|
46
|
+
await visitor.patch(patch => patch.html.includes('already in use'));
|
|
47
|
+
const closed = once(alice.socket, 'close');
|
|
48
|
+
alice.socket.close();
|
|
49
|
+
await closed;
|
|
50
|
+
visitor.send({ v: visitor.config.version, type: 'redweb:html', requestId: 'reserved-name', payload: { kind: 'action', name: 'join', args: [{ name: 'ALICE' }], component: 'chat' } });
|
|
51
|
+
assert.equal((await visitor.receive(message => message.requestId === 'reserved-name')).payload, false);
|
|
52
|
+
const { config } = alice;
|
|
53
|
+
const resumed = await connect(t, `${origin.replace('http:', 'ws:')}${config.socketPath}?pageId=${config.pageId}&redwebVersion=${encodeURIComponent(config.version)}`, origin);
|
|
54
|
+
await resumed.receive(message => message.type === 'redweb:patch' && message.payload.patches.some(patch => patch.html.includes('Online · 1')));
|
|
55
|
+
resumed.send({ v: config.version, type: 'redweb:html', payload: { kind: 'action', name: 'leave', args: [], component: 'chat' } });
|
|
56
|
+
await resumed.receive(message => message.type === 'redweb:patch' && message.payload.patches.some(patch => patch.html.includes('Join the chatroom')));
|
|
57
|
+
visitor.action('join', [{ name: 'Alice' }], 'chat');
|
|
58
|
+
await visitor.patch(patch => patch.html.includes('Connected as') && patch.html.includes('Online · 1'));
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test('room units bound history/presence, isolate rooms, and make repeated lifecycle calls harmless', () => {
|
|
62
|
+
const Page = createChatroomPage();
|
|
63
|
+
const alice = new Page().chat;
|
|
64
|
+
const bob = new Page().chat;
|
|
65
|
+
const isolated = new (createChatroomPage())().chat;
|
|
66
|
+
assert.equal(alice.send({ message: 'not joined' }), false);
|
|
67
|
+
alice.connected();
|
|
68
|
+
assert.equal(alice.join(chatInputs.join.parse({ name: ' Alice ' })), true);
|
|
69
|
+
assert.equal(alice.join({ name: 'Replacement' }), false);
|
|
70
|
+
assert.equal(bob.join({ name: 'ALICE' }), false);
|
|
71
|
+
assert.match(bob.render().toString(), /already in use/);
|
|
72
|
+
assert.equal(bob.join({ name: 'Bob' }), true);
|
|
73
|
+
assert.equal(isolated.join({ name: 'Alice' }), true);
|
|
74
|
+
assert.match(alice.render().toString(), /No messages yet/);
|
|
75
|
+
for (let index = 0; index < 101; index++) assert.equal(alice.send({ message: `message-${index}` }), true);
|
|
76
|
+
assert.equal(bob.messages.length, 100);
|
|
77
|
+
assert.deepEqual(bob.messages[0], { id: 2, sender: 'Alice', text: 'message-1' });
|
|
78
|
+
assert.equal(bob.messages.at(-1).id, 101);
|
|
79
|
+
assert.equal(isolated.messages.length, 0);
|
|
80
|
+
assert.match(bob.render().toString(), /message-100/);
|
|
81
|
+
alice.disconnected();
|
|
82
|
+
alice.disconnected();
|
|
83
|
+
assert.deepEqual(bob.members, ['Bob']);
|
|
84
|
+
assert.equal(alice.send({ message: 'offline' }), false);
|
|
85
|
+
alice.connected();
|
|
86
|
+
assert.deepEqual(bob.members, ['Bob', 'Alice']);
|
|
87
|
+
const visitors = Array.from({ length: 100 }, (_, index) => {
|
|
88
|
+
const member = new Page().chat;
|
|
89
|
+
assert.equal(member.join({ name: `visitor-${index}` }), true);
|
|
90
|
+
return member;
|
|
91
|
+
});
|
|
92
|
+
const rendered = alice.render().toString();
|
|
93
|
+
assert.match(rendered, /Online · 102/);
|
|
94
|
+
assert.match(rendered, /\+2 more/);
|
|
95
|
+
assert.doesNotMatch(rendered, /<li[^>]*>visitor-99<\/li>/);
|
|
96
|
+
visitors.forEach(member => member.disposed());
|
|
97
|
+
alice.leave();
|
|
98
|
+
alice.disposed();
|
|
99
|
+
alice.disposed();
|
|
100
|
+
assert.deepEqual([alice.displayName, alice.feedback, alice.messages, alice.members], ['', '', [], []]);
|
|
101
|
+
assert.deepEqual(bob.members, ['Bob']);
|
|
102
|
+
assert.match(alice.render().toString(), /Join the chatroom/);
|
|
103
|
+
bob.disposed();
|
|
104
|
+
isolated.disposed();
|
|
105
|
+
});
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { start, type LiveHtmlStartOptions } from 'redweb';
|
|
2
|
+
import { createChatroomPage } from './chatroom';
|
|
3
|
+
import { runApp } from './run-app';
|
|
4
|
+
|
|
5
|
+
export function createApp(options: LiveHtmlStartOptions = {}) {
|
|
6
|
+
return start(createChatroomPage(), { port: Number(process.env.PORT ?? 8181), templateRoot: __dirname, ...options });
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
if (require.main === module) runApp(createApp);
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# Persistent private dashboard
|
|
2
|
+
|
|
3
|
+
This recipe combines decorator-first pages, reusable live cards, validated actions, and real SQLite persistence. It is an application example, not an authentication framework or managed database.
|
|
4
|
+
|
|
5
|
+
## Run it
|
|
6
|
+
|
|
7
|
+
Requires **Node 22.13 or newer** (native `node:sqlite`, experimental in Node 22) and npm. Other Redweb starters retain their own Node requirements. Use a supported Node release in production.
|
|
8
|
+
|
|
9
|
+
After installing dependencies, create your account:
|
|
10
|
+
|
|
11
|
+
```sh
|
|
12
|
+
npm run add-user -- alice
|
|
13
|
+
npm test
|
|
14
|
+
npm run dev
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
The provisioning command displays a randomly generated password once. Save it securely; there are no default accounts or passwords. Open **http://127.0.0.1:8181/login**, sign in, and add a card. A second signed-in tab updates immediately. Restart the app: your cards and unexpired credentials remain valid. Sign out all sessions to close every connected tab for that account and invalidate all its cookies.
|
|
18
|
+
|
|
19
|
+
`npm test` provisions temporary test accounts and a real temporary database, then exercises HTTP, WebSockets, isolation, restart, and session expiry. It never modifies your application database. Integration tests use no mocks. A separately labelled unit test injects a cleanup error after closing a real SQLite database to verify rejection handling; it does not simulate a real operating-system failure.
|
|
20
|
+
|
|
21
|
+
`npm run test:coverage` measures the TypeScript application through source maps, separately from Redweb's own instrumented-library coverage. It also waits through the actual one-minute login admission window without mocking the clock. The report includes TypeScript-generated decorator accessor functions; inspect that distinction rather than assuming a library coverage figure applies to this recipe. The generated npm configuration enforces this recipe's Node engine requirement before installation.
|
|
22
|
+
|
|
23
|
+
## Where the behavior lives
|
|
24
|
+
|
|
25
|
+
- `app.tsx`: composition, login page, protected dashboard, listener and shutdown.
|
|
26
|
+
- `cards.tsx`: reusable `Cards` component and account-scoped live subscriptions. Normal TSX expressions update automatically. Forms call typed actions; feedback requires no browser glue.
|
|
27
|
+
- `store.ts`: prepared SQL, bounded cards/sessions, owner-filtered operations and synchronous transactions.
|
|
28
|
+
- `auth.ts`: asynchronous scrypt, bounded login attempts, hashed session tokens, cookies and sign-out.
|
|
29
|
+
- `admin.ts`: explicit local account provisioning.
|
|
30
|
+
|
|
31
|
+
## Production boundaries
|
|
32
|
+
|
|
33
|
+
Set `DASHBOARD_DATABASE` to a writable persistent file path (default `data/dashboard.sqlite`). Protect the directory with OS permissions: the database contains password hashes, private card text, and session metadata. It, its WAL/SHM files, and backups must never be served as public assets or committed. Stop the process cleanly before copying the database for a backup, or use a proper SQLite online backup facility; copying only the main file during live WAL writes is not a backup plan.
|
|
34
|
+
|
|
35
|
+
Set `NODE_ENV=production` and `DASHBOARD_ORIGIN=https://your-domain.example` behind an HTTPS reverse proxy. The origin must have no path or trailing slash. This enables Secure cookies; all session cookies are HttpOnly and SameSite=Strict. Both login/logout forms and socket upgrades require the exact trusted origin. The application does not trust Host or forwarded headers to establish origin or identity. The HTTP listener must not be publicly reachable around your TLS proxy.
|
|
36
|
+
|
|
37
|
+
Provision accounts on the same persistent volume before serving requests. Passwords use salted scrypt; only hashes of random session tokens are stored. Default sessions last one hour. Up to 32 unexpired sessions and 100 cards per account are supported. Login work is limited to four simultaneous checks and ten attempts per minute per direct peer IP, with at most 1,024 tracked IPs; clients behind one proxy share its bucket. Add appropriate proxy-level abuse controls for an Internet deployment. There is no registration, password reset, MFA, or account recovery; integrate a dedicated identity provider if your product needs those features.
|
|
38
|
+
|
|
39
|
+
SQL checks the current session and card owner inside each write transaction. Private subscriptions recheck session validity before publishing and close at expiry. Sign-out invalidates credentials before revoking Redweb sessions. An expired or disconnected page may need a reload/sign-in; the recipe does not silently retry actions with uncertain outcomes.
|
|
40
|
+
|
|
41
|
+
This is a **single-process live-update model**. SQLite transactions are synchronous and kept small; this is not a claim of unlimited concurrency. Do not put multiple app workers behind a load balancer and expect cross-worker notifications or revocation. Add a deliberate shared notification/session-revocation design before scaling horizontally. Static export cannot include protected dashboards.
|
|
42
|
+
|
|
43
|
+
Build and deploy using the shared instructions above, including the persistent data volume and the environment settings here. Neither `npm run dev` nor a process restart should erase durable cards. Redweb itself does not depend on SQLite.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { randomBytes } from 'node:crypto';
|
|
2
|
+
import { mkdirSync } from 'node:fs';
|
|
3
|
+
import { dirname } from 'node:path';
|
|
4
|
+
import { credentials } from './auth';
|
|
5
|
+
import { databasePath } from './app';
|
|
6
|
+
import { DashboardStore, USERNAME } from './store';
|
|
7
|
+
|
|
8
|
+
async function main() {
|
|
9
|
+
const account = process.argv[2];
|
|
10
|
+
if (!account || !USERNAME.test(account) || process.argv.length !== 3) throw new Error('Usage: npm run add-user -- alice (3–32 lowercase letters, digits, _ or -; starts with a letter).');
|
|
11
|
+
const filename = databasePath();
|
|
12
|
+
mkdirSync(dirname(filename), { recursive: true });
|
|
13
|
+
const store = new DashboardStore(filename);
|
|
14
|
+
try {
|
|
15
|
+
const password = randomBytes(24).toString('base64url');
|
|
16
|
+
store.provision(account, await credentials(password));
|
|
17
|
+
console.log(`Created ${account}. Store this password safely; it is displayed only once:\n${password}`);
|
|
18
|
+
} finally { store.close(); }
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
void main().catch(error => { console.error(error.message); process.exitCode = 1; });
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
:root { font-family: system-ui, sans-serif; color: #e8edf5; background: #111827; color-scheme: dark; }
|
|
2
|
+
* { box-sizing: border-box; }
|
|
3
|
+
body { margin: 0; }
|
|
4
|
+
.home { width: min(64rem, 100%); margin: 3rem auto; padding: 0 1.5rem; }
|
|
5
|
+
header { display: flex; align-items: center; justify-content: space-between; gap: 1rem; flex-wrap: wrap; }
|
|
6
|
+
form { display: flex; align-items: center; flex-wrap: wrap; gap: .75rem; }
|
|
7
|
+
input, button { font: inherit; border: 1px solid #526179; border-radius: .5rem; padding: .75rem; }
|
|
8
|
+
input { background: #1f2937; max-width: 100%; }
|
|
9
|
+
button { background: #a7f3d0; color: #102c23; cursor: pointer; }
|
|
10
|
+
button:disabled { opacity: .5; cursor: default; }
|
|
11
|
+
:focus-visible { outline: 3px solid #60a5fa; outline-offset: 3px; }
|
|
12
|
+
.cards { margin-top: 2rem; }
|
|
13
|
+
.card-grid { padding: 0; list-style: none; display: grid; grid-template-columns: repeat(auto-fit, minmax(min(15rem, 100%), 1fr)); gap: 1rem; }
|
|
14
|
+
.card-grid li { border: 1px solid #526179; border-radius: .75rem; padding: 1.25rem; overflow-wrap: anywhere; }
|
|
15
|
+
h2 { font-size: 1.25rem; }
|
|
16
|
+
[role="alert"] { color: #fca5a5; }
|