redweb 0.16.1 → 0.16.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/CHANGELOG.md +35 -22
  2. package/README.md +293 -289
  3. package/contract.d.ts +11 -11
  4. package/docs/API_EXAMPLES_VERIFICATION.md +22 -22
  5. package/docs/APPLICATION.md +96 -94
  6. package/docs/CLI.md +122 -116
  7. package/docs/CLIENT_DEVELOPMENT.md +9 -9
  8. package/docs/CONNECTED_CLIENTS_VERIFICATION.md +65 -65
  9. package/docs/DEVELOPMENT.md +81 -81
  10. package/docs/GETTING_STARTED.md +78 -58
  11. package/docs/LIVE_HTML.md +555 -478
  12. package/docs/MIGRATION.md +28 -28
  13. package/docs/RELEASE_TRUST.md +88 -88
  14. package/docs/RUNTIME_DIAGNOSTICS.md +78 -78
  15. package/docs/SOCKET_CONTRACTS.md +42 -42
  16. package/docs/SOCKET_PAGES.md +172 -172
  17. package/docs/SOCKET_PAGE_RELEASE_PREPARATION.md +120 -120
  18. package/docs/SOCKET_PAGE_VERIFICATION.md +85 -85
  19. package/docs/generated.json +2286 -2286
  20. package/docs/guides/chatroom.md +1 -1
  21. package/docs/guides/jsx-without-react.md +14 -14
  22. package/docs/reference.json +1329 -1329
  23. package/docs/releases/0.15.0.json +2217 -2217
  24. package/docs/releases/0.16.0.json +2217 -2217
  25. package/docs/releases/0.16.1.json +2286 -2286
  26. package/docs/releases/0.16.2.json +2286 -0
  27. package/docs/releases/0.16.3.json +2286 -0
  28. package/docs/snippets/components.tsx +24 -24
  29. package/docs/snippets/counter.tsx +16 -16
  30. package/docs/snippets/room-access.tsx +11 -11
  31. package/docs/snippets/site.css +2 -2
  32. package/docs/snippets/site.tsx +22 -22
  33. package/docs/topics.json +3 -3
  34. package/index.d.ts +92 -57
  35. package/index.js +13 -8
  36. package/package.json +8 -8
  37. package/recipes/foundation/README.md +7 -0
  38. package/recipes/foundation/app.test.cjs +15 -0
  39. package/recipes/foundation/app.tsx +12 -0
  40. package/recipes/shared/README.md +7 -7
  41. package/src/Application.js +4 -4
  42. package/src/access/failure-codes.json +4 -0
  43. package/src/cli/ProjectInitializer.js +1 -1
  44. package/src/cli/arguments.js +15 -3
  45. package/src/cli/run.js +10 -1
  46. package/src/cli/templates.js +34 -21
  47. package/src/docs/Documentation.js +25 -15
  48. package/src/htmx/Jsx.js +2 -2
  49. package/src/htmx/LiveHtmlServer.js +5 -1
  50. package/src/htmx/LivePage.js +5 -1
  51. package/src/htmx/LiveResource.js +96 -0
  52. package/src/htmx/PageManager.js +126 -13
  53. package/src/htmx/PageSocketRoute.js +132 -132
  54. package/src/htmx/PageTaskLane.js +39 -0
  55. package/src/htmx/ReactiveRenderer.js +8 -8
  56. package/src/htmx/SocketAction.js +19 -19
  57. package/src/htmx/TemplateRenderer.js +1 -1
  58. package/src/htmx/index.js +3 -2
  59. package/src/htmx/metadata.js +117 -6
  60. package/src/ws/BaseHandler.js +6 -6
  61. package/src/ws/ConnectedClients.js +207 -207
  62. package/src/ws/HandlerGuard.js +4 -4
  63. package/src/ws/RoomRegistry.js +4 -4
  64. package/src/ws/RouteRuntime.js +11 -11
  65. package/src/ws/SocketAction.js +16 -16
  66. package/src/ws/SocketContract.js +3 -3
  67. package/src/ws/SocketRoute.js +7 -7
package/contract.d.ts CHANGED
@@ -15,17 +15,17 @@ export interface SocketSchema<Input = unknown, Output = Input> {
15
15
  export type SocketSchemas = Readonly<Record<string, SocketSchema>>;
16
16
  export type ContractInput<Schema extends SocketSchema> = NonNullable<Schema['~standard']['types']>['input'];
17
17
  export type ContractOutput<Schema extends SocketSchema> = Awaited<NonNullable<Schema['~standard']['types']>['output']>;
18
- export type ContractMessage<Schemas extends SocketSchemas> = {
18
+ export type ContractMessage<Schemas extends SocketSchemas> = {
19
19
  [Type in keyof Schemas & string]: ProtocolEnvelope<ContractOutput<Schemas[Type]>> & { type: Type };
20
- }[keyof Schemas & string];
21
-
22
- declare const socketActionBrand: unique symbol;
23
- export interface SocketAction { readonly [socketActionBrand]: true; }
24
- export interface SocketHandler<Input> {
25
- new (): BaseHandler;
26
- /** Bind a JSON payload to a server-rendered rw-click/rw-submit control. */
27
- with(payload: Input): SocketAction;
28
- }
20
+ }[keyof Schemas & string];
21
+
22
+ declare const socketActionBrand: unique symbol;
23
+ export interface SocketAction { readonly [socketActionBrand]: true; }
24
+ export interface SocketHandler<Input> {
25
+ new (): BaseHandler;
26
+ /** Bind a JSON payload to a server-rendered rw-click/rw-submit control. */
27
+ with(payload: Input): SocketAction;
28
+ }
29
29
 
30
30
  export interface ContractClient<Schemas extends SocketSchemas> {
31
31
  envelope<Type extends keyof Schemas & string>(type: Type, payload: ContractInput<Schemas[Type]>, metadata?: ProtocolMetadata):
@@ -44,7 +44,7 @@ export interface SocketContract<Schemas extends SocketSchemas> {
44
44
  handler<Type extends keyof Schemas & string>(type: Type, callback: (
45
45
  socket: RedWebSocket, payload: ContractOutput<Schemas[Type]>,
46
46
  message: ProtocolEnvelope<ContractOutput<Schemas[Type]>> & { type: Type },
47
- ) => unknown): SocketHandler<ContractInput<Schemas[Type]>>;
47
+ ) => unknown): SocketHandler<ContractInput<Schemas[Type]>>;
48
48
  client(socket: SendableSocket): ContractClient<Schemas>;
49
49
  send<Type extends keyof Schemas & string>(socket: RedWebSocket, type: Type, payload: ContractInput<Schemas[Type]>, metadata?: ProtocolMetadata): Promise<boolean>;
50
50
  }
@@ -1,22 +1,22 @@
1
- # API example refresh verification
2
-
3
- Verified locally on Windows, 2026-09-02, for the 0.16.1 documentation patch.
4
-
5
- ## Scope
6
-
7
- - Reviewed the current 31 API sections and 11 showcase examples. Prefer concise `defineApp`, plain decorated classes and TSX for application examples; retain explicitly labeled lower-level APIs.
8
- - Source reuse keeps the counter, component and multi-page/CSS examples identical between generated documentation and acceptance fixtures.
9
- - No runtime API change. Documentation generation and the executable room example were changed.
10
- - Prior release snapshots, including 0.16.0, remain untouched. The website remains pinned to published 0.16.0 until manual npm publication and subsequent site synchronization.
11
-
12
- ## Passed checks
13
-
14
- - `tests/unit/documentation.unit.test.js` and `tests/unit/api-examples.unit.test.js`: 13 tests; 100% statements, branches, functions and lines for `src/docs/Documentation.js`. All current API/showcase examples parse as TSX; partial API patterns are not claimed to be independently runnable or fully type-checked applications.
15
- - `tests/integration/api-examples.integration.test.js`: six real headed-Chromium scenarios, counter/components/two-page site under both standard and legacy decorators. Exact source is compiled; only a fixture ownership export is appended. Verifies server updates, a second counter tab, component isolation, CSS and navigation without mocks.
16
- - Focused `tests/integration/documentation.integration.test.js` and `tests/unit/room-verifier.unit.test.js` gate (`shared page/room|actual generator|room`): 10 tests passed, two unrelated tests not selected. Includes actual HTTP/WebSocket room authorization and cleanup under both decorator modes.
17
- - `npm run pretest`: generated examples, protocol declarations, documentation freshness and all three consumer type configurations pass.
18
- - `npm run prepublishOnly`: release channel and immutable 0.16.1 snapshot match the package version.
19
- - `npm pack --dry-run --json`: packaging/prepack checks pass.
20
- - Senior critic reviewed modular source reuse, tests and API semantics. Corrected all findings, including both lifecycle prose and option-table distinctions between application and lower-level shutdown budgets.
21
-
22
- No soak, fixed-duration observation gate, full runtime suite or deployment was run for this documentation-only patch. Coverage above is scoped to the changed documentation generator, not a new whole-project coverage claim.
1
+ # API example refresh verification
2
+
3
+ Verified locally on Windows, 2026-09-02, for the 0.16.1 documentation patch.
4
+
5
+ ## Scope
6
+
7
+ - Reviewed the current 31 API sections and 11 showcase examples. Prefer concise `defineApp`, plain decorated classes and TSX for application examples; retain explicitly labeled lower-level APIs.
8
+ - Source reuse keeps the counter, component and multi-page/CSS examples identical between generated documentation and acceptance fixtures.
9
+ - No runtime API change. Documentation generation and the executable room example were changed.
10
+ - Prior release snapshots, including 0.16.0, remain untouched. The website remains pinned to published 0.16.0 until manual npm publication and subsequent site synchronization.
11
+
12
+ ## Passed checks
13
+
14
+ - `tests/unit/documentation.unit.test.js` and `tests/unit/api-examples.unit.test.js`: 13 tests; 100% statements, branches, functions and lines for `src/docs/Documentation.js`. All current API/showcase examples parse as TSX; partial API patterns are not claimed to be independently runnable or fully type-checked applications.
15
+ - `tests/integration/api-examples.integration.test.js`: six real headed-Chromium scenarios, counter/components/two-page site under both standard and legacy decorators. Exact source is compiled; only a fixture ownership export is appended. Verifies server updates, a second counter tab, component isolation, CSS and navigation without mocks.
16
+ - Focused `tests/integration/documentation.integration.test.js` and `tests/unit/room-verifier.unit.test.js` gate (`shared page/room|actual generator|room`): 10 tests passed, two unrelated tests not selected. Includes actual HTTP/WebSocket room authorization and cleanup under both decorator modes.
17
+ - `npm run pretest`: generated examples, protocol declarations, documentation freshness and all three consumer type configurations pass.
18
+ - `npm run prepublishOnly`: release channel and immutable 0.16.1 snapshot match the package version.
19
+ - `npm pack --dry-run --json`: packaging/prepack checks pass.
20
+ - Senior critic reviewed modular source reuse, tests and API semantics. Corrected all findings, including both lifecycle prose and option-table distinctions between application and lower-level shutdown budgets.
21
+
22
+ No soak, fixed-duration observation gate, full runtime suite or deployment was run for this documentation-only patch. Coverage above is scoped to the changed documentation generator, not a new whole-project coverage claim.
@@ -1,94 +1,96 @@
1
- # One application, one listener
2
-
3
- `defineApp()` describes the application. `await app.run()` initializes it and opens one port for HTTP pages and WebSocket routes. You do not create an HTTP server and then pass it to a second socket server.
4
-
5
- Added in Redweb 0.14.0. Earlier releases do not export `defineApp`.
6
-
7
- ## The entry point
8
-
9
- In a Redweb TypeScript project, the application can be as small as:
10
-
11
- ```tsx
12
- import { defineApp, page, action, state } from 'redweb';
13
-
14
- @page('/', { shared: true })
15
- class HomePage {
16
- @state() count = 0;
17
- @action() increment() { this.count += 1; }
18
- render() {
19
- return <main><h1>Hello</h1><button rw-click="increment">Count {this.count}</button></main>;
20
- }
21
- }
22
-
23
- @page('/about', { live: false })
24
- class AboutPage {
25
- render() { return <main><h1>About this app</h1></main>; }
26
- }
27
-
28
- const app = defineApp({ pages: [HomePage, AboutPage] });
29
- app.run();
30
- ```
31
-
32
- `run()` returns a Promise. Await it when startup completion matters; an ESM entry point can use top-level `await app.run()`. In CommonJS, the direct call above also works; add rejection handling when the application needs a custom failure policy. A module imported by tests should export its definition separately or guard startup with `require.main === module`. Constructing a definition never opens a port or installs signal handlers.
33
-
34
- ## Add socket routes and services
35
-
36
- The same definition accepts all three registration arrays:
37
-
38
- ```ts
39
- const app = defineApp({
40
- pages: [HomePage, AboutPage],
41
- sockets: [MatchRoute, ChatRoute],
42
- services: [GameSimulation],
43
- port: 8181,
44
- });
45
-
46
- await app.run();
47
- ```
48
-
49
- These names refer to your application's classes. A page's decorator chooses its HTTP path. A `SocketRoute` chooses its WebSocket path, such as `/match`; its handlers dispatch `join`, `move`, and `resume` by message `type`. Live-page connections and custom socket routes share one upgrade listener. Do not register a custom socket route at the live-page socket path.
50
-
51
- In five-year-old terms: the app has one front door. Pages are ordinary visits, sockets are ongoing conversations, and services are the staff who prepare the building before the door opens and clean up after it closes.
52
-
53
- `services` are application-wide lifecycle classes, not HTTP endpoint descriptors or route-specific `SocketService` classes:
54
-
55
- ```ts
56
- import type { ApplicationContext, ApplicationService } from 'redweb';
57
-
58
- class GameSimulation implements ApplicationService {
59
- private timer?: ReturnType<typeof setInterval>;
60
- private ticks = 0;
61
-
62
- onInit(app: ApplicationContext, signal: AbortSignal) {
63
- signal.throwIfAborted();
64
- app.app.get('/health', (_request, response) => response.json({ ticks: this.ticks }));
65
- this.timer = setInterval(() => { this.ticks++; }, 1000);
66
- }
67
-
68
- onShutdown() { clearInterval(this.timer); }
69
- }
70
- ```
71
-
72
- Keep constructors inert. Acquire resources in `onInit(app, signal)` and release them in `onShutdown()`. Initializers run in registration order before the port opens; cleanup runs in reverse order, including the service whose initialization failed. Asynchronous methods are supported. Pass the abort signal to cancelable work and make cleanup safe after partial initialization. A deadline cannot stop arbitrary code that ignores cancellation or blocks the event loop.
73
-
74
- Use `httpServices` for the existing HTTP endpoint descriptor array. Keep route-specific `SocketService` registrations on their `SocketRoute`. For an existing Express application, pass it as `server`; Redweb still creates and owns the underlying Node listener. The low-level server classes remain available for applications that explicitly manage their own listener ownership.
75
-
76
- ## Lifecycle contract
77
-
78
- - Definitions are deferred. `app.server`, `app.app`, `app.http`, and `app.sockets` are initially `null`. Await `run()` before accessing their runtime values; its returned value has non-null HTTP members in TypeScript.
79
- - `pages`, `sockets`, and `services` are optional. HTTP-only, socket-only, and non-live-page applications use the same entry point. Non-live pages need no WebSocket server unless custom socket routes were supplied.
80
- - Repeated `run()` calls share the pending or successful startup promise while the application is running. `shutdown()` is idempotent. A stopped application cannot restart; define a new application instead.
81
- - Startup failure rolls back resources before rejection, subject to the shutdown deadline. Cleanup failures are retained alongside the original startup error, not reported as success.
82
- - `startupTimeoutMs` and `shutdownTimeoutMs` default to 5000. Each is one total application budget, not a fresh full timeout for every service. Shutdown cancels pending startup, closes admission, attempts page/socket cleanup, closes owned connections, and releases services.
83
- - `signals: true` is the default. Signal handlers are installed when `run()` begins, so SIGINT/SIGTERM also cancel pending initialization. Unexpected listener closure and listener errors trigger owned shutdown. Repeated signals do not bypass active cleanup. Failed process-owned cleanup sets a failure exit status and retains a deadline for leaked handles. Explicit `shutdown()` rejects on cleanup failure but never forcibly exits its caller.
84
- - Tests and embedded applications should set `signals: false` and call `shutdown()` in their own cleanup. Use `port: 0` for an OS-assigned test port. TLS uses the existing `ssl` key/certificate options and shares one HTTPS/WSS listener.
85
-
86
- ## Boundaries
87
-
88
- This is application composition, not dependency injection, a distributed worker manager, or durable storage. It does not automatically inject services into page constructors. `shared: true` shares in-process state across visitors, not across server processes or restarts. Static file export remains the separate `exportStatic()` API; a non-live page served over HTTP is not a static export.
89
-
90
- `app.revoke(principal)` revokes matching live-page sessions and returns their count; it returns zero when no live-page server has been created. `app.inspect()` exposes opt-in development metadata and otherwise returns `null`. These preserve the same live-page policies as `start()`.
91
-
92
- `app.options` is the copied definition. An independent test instance can use `defineApp({ ...app.options, port: 0, signals: false })`, followed by `await run()` and owned cleanup. This creates new page/service instances, but does not clone an Express application or objects deliberately captured by class closures. The chat module exports a default `ChatroomPage`; its optional `createChatroomPage()` factory creates isolated rooms for separate apps or tests.
93
-
94
- For rendering and CSS see [Live HTML](LIVE_HTML.md). For message validation and handler classes see [socket contracts](SOCKET_CONTRACTS.md). For deployment and persistence boundaries see [operations](MULTIPLAYER_OPERATIONS.md).
1
+ # One application, one listener
2
+
3
+ `defineApp()` describes the application. `await app.run()` initializes it and opens one port for HTTP pages and WebSocket routes. You do not create an HTTP server and then pass it to a second socket server.
4
+
5
+ Added in Redweb 0.14.0. Earlier releases do not export `defineApp`.
6
+
7
+ ## The entry point
8
+
9
+ In a Redweb TypeScript project, the application can be as small as:
10
+
11
+ ```tsx
12
+ import { defineApp, page, action, state } from 'redweb';
13
+
14
+ @page('/', { shared: true })
15
+ class HomePage {
16
+ @state() count = 0;
17
+ @action() increment() { this.count += 1; }
18
+ render() {
19
+ return <main><h1>Hello</h1><button rw-click="increment">Count {this.count}</button></main>;
20
+ }
21
+ }
22
+
23
+ @page('/about', { live: false })
24
+ class AboutPage {
25
+ render() { return <main><h1>About this app</h1></main>; }
26
+ }
27
+
28
+ const app = defineApp({ pages: [HomePage, AboutPage] });
29
+ app.run();
30
+ ```
31
+
32
+ `run()` returns a Promise. Await it when startup completion matters; an ESM entry point can use top-level `await app.run()`. In CommonJS, the direct call above also works; add rejection handling when the application needs a custom failure policy. A module imported by tests should export its definition separately or guard startup with `require.main === module`. Constructing a definition never opens a port or installs signal handlers.
33
+
34
+ ## Add socket routes and services
35
+
36
+ The same definition accepts all three registration arrays:
37
+
38
+ ```ts
39
+ const app = defineApp({
40
+ pages: [HomePage, AboutPage],
41
+ sockets: [MatchRoute, ChatRoute],
42
+ services: [GameSimulation],
43
+ port: 8181,
44
+ });
45
+
46
+ await app.run();
47
+ ```
48
+
49
+ These names refer to your application's classes. A page's decorator chooses its HTTP path. A `SocketRoute` chooses its WebSocket path, such as `/match`; its handlers dispatch `join`, `move`, and `resume` by message `type`. Live-page connections and custom socket routes share one upgrade listener. Do not register a custom socket route at the live-page socket path.
50
+
51
+ `providers` is different from `services`: it is a named map of already-created application objects that a page explicitly requests through `@inject('name')`. Use it for a store or other dependency shared across page instances. Redweb does not construct, persist, or authorize those objects for you.
52
+
53
+ In five-year-old terms: the app has one front door. Pages are ordinary visits, sockets are ongoing conversations, and services are the staff who prepare the building before the door opens and clean up after it closes.
54
+
55
+ `services` are application-wide lifecycle classes, not HTTP endpoint descriptors or route-specific `SocketService` classes:
56
+
57
+ ```ts
58
+ import type { ApplicationContext, ApplicationService } from 'redweb';
59
+
60
+ class GameSimulation implements ApplicationService {
61
+ private timer?: ReturnType<typeof setInterval>;
62
+ private ticks = 0;
63
+
64
+ onInit(app: ApplicationContext, signal: AbortSignal) {
65
+ signal.throwIfAborted();
66
+ app.app.get('/health', (_request, response) => response.json({ ticks: this.ticks }));
67
+ this.timer = setInterval(() => { this.ticks++; }, 1000);
68
+ }
69
+
70
+ onShutdown() { clearInterval(this.timer); }
71
+ }
72
+ ```
73
+
74
+ Keep constructors inert. Acquire resources in `onInit(app, signal)` and release them in `onShutdown()`. Initializers run in registration order before the port opens; cleanup runs in reverse order, including the service whose initialization failed. Asynchronous methods are supported. Pass the abort signal to cancelable work and make cleanup safe after partial initialization. A deadline cannot stop arbitrary code that ignores cancellation or blocks the event loop.
75
+
76
+ Use `httpServices` for the existing HTTP endpoint descriptor array. Keep route-specific `SocketService` registrations on their `SocketRoute`. For an existing Express application, pass it as `server`; Redweb still creates and owns the underlying Node listener. The low-level server classes remain available for applications that explicitly manage their own listener ownership.
77
+
78
+ ## Lifecycle contract
79
+
80
+ - Definitions are deferred. `app.server`, `app.app`, `app.http`, and `app.sockets` are initially `null`. Await `run()` before accessing their runtime values; its returned value has non-null HTTP members in TypeScript.
81
+ - `pages`, `sockets`, and `services` are optional. HTTP-only, socket-only, and non-live-page applications use the same entry point. Non-live pages need no WebSocket server unless custom socket routes were supplied.
82
+ - Repeated `run()` calls share the pending or successful startup promise while the application is running. `shutdown()` is idempotent. A stopped application cannot restart; define a new application instead.
83
+ - Startup failure rolls back resources before rejection, subject to the shutdown deadline. Cleanup failures are retained alongside the original startup error, not reported as success.
84
+ - `startupTimeoutMs` and `shutdownTimeoutMs` default to 5000. Each is one total application budget, not a fresh full timeout for every service. Shutdown cancels pending startup, closes admission, attempts page/socket cleanup, closes owned connections, and releases services.
85
+ - `signals: true` is the default. Signal handlers are installed when `run()` begins, so SIGINT/SIGTERM also cancel pending initialization. Unexpected listener closure and listener errors trigger owned shutdown. Repeated signals do not bypass active cleanup. Failed process-owned cleanup sets a failure exit status and retains a deadline for leaked handles. Explicit `shutdown()` rejects on cleanup failure but never forcibly exits its caller.
86
+ - Tests and embedded applications should set `signals: false` and call `shutdown()` in their own cleanup. Use `port: 0` for an OS-assigned test port. TLS uses the existing `ssl` key/certificate options and shares one HTTPS/WSS listener.
87
+
88
+ ## Boundaries
89
+
90
+ This is application composition, not a general-purpose dependency-injection container, distributed worker manager, or durable storage. `services` are lifecycle classes and are not injected into page constructors. For an explicit application-owned dependency, register an instance in `providers` and declare a matching `@inject('name')` field on the page; see the [complete Live HTML example](LIVE_HTML.md#putting-uploads-providers-and-resources-together). `shared: true` shares in-process state across visitors, not across server processes or restarts. Static file export remains the separate `exportStatic()` API; a non-live page served over HTTP is not a static export.
91
+
92
+ `app.revoke(principal)` revokes matching live-page sessions and returns their count; it returns zero when no live-page server has been created. `app.inspect()` exposes opt-in development metadata and otherwise returns `null`. These preserve the same live-page policies as `start()`.
93
+
94
+ `app.options` is the copied definition. An independent test instance can use `defineApp({ ...app.options, port: 0, signals: false })`, followed by `await run()` and owned cleanup. This creates new page/service instances, but does not clone an Express application or objects deliberately captured by class closures. The chat module exports a default `ChatroomPage`; its optional `createChatroomPage()` factory creates isolated rooms for separate apps or tests.
95
+
96
+ For rendering and CSS see [Live HTML](LIVE_HTML.md). For message validation and handler classes see [socket contracts](SOCKET_CONTRACTS.md). For deployment and persistence boundaries see [operations](MULTIPLAYER_OPERATIONS.md).