redweb 0.16.2 → 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 -29
  2. package/README.md +293 -291
  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 -122
  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 -78
  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 -2286
  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 -7
  38. package/recipes/foundation/app.test.cjs +15 -15
  39. package/recipes/foundation/app.tsx +12 -12
  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 +21 -21
  45. package/src/cli/run.js +12 -12
  46. package/src/cli/templates.js +40 -40
  47. package/src/docs/Documentation.js +29 -29
  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
@@ -1,24 +1,24 @@
1
- import { action, component, defineApp, page, state } from 'redweb';
2
-
3
- @component()
4
- class Counter {
5
- @state() count = 0;
6
-
7
- @action()
8
- increment() { this.count += 1; }
9
-
10
- render() {
11
- return <button rw-click="increment">Count {this.count}</button>;
12
- }
13
- }
14
-
15
- @page('/')
16
- class CountersPage {
17
- first = new Counter();
18
- second = new Counter();
19
-
20
- render() { return <main>{this.first}{this.second}</main>; }
21
- }
22
-
23
- const app = defineApp({ pages: [CountersPage] });
24
- app.run();
1
+ import { action, component, defineApp, page, state } from 'redweb';
2
+
3
+ @component()
4
+ class Counter {
5
+ @state() count = 0;
6
+
7
+ @action()
8
+ increment() { this.count += 1; }
9
+
10
+ render() {
11
+ return <button rw-click="increment">Count {this.count}</button>;
12
+ }
13
+ }
14
+
15
+ @page('/')
16
+ class CountersPage {
17
+ first = new Counter();
18
+ second = new Counter();
19
+
20
+ render() { return <main>{this.first}{this.second}</main>; }
21
+ }
22
+
23
+ const app = defineApp({ pages: [CountersPage] });
24
+ app.run();
@@ -1,16 +1,16 @@
1
- import { action, defineApp, page, state } from 'redweb';
2
-
3
- @page('/', { shared: true })
4
- class CounterPage {
5
- @state() count = 0;
6
-
7
- @action()
8
- increment() { this.count += 1; }
9
-
10
- render() {
11
- return <button rw-click="increment">Count {this.count}</button>;
12
- }
13
- }
14
-
15
- const app = defineApp({ pages: [CounterPage] });
16
- app.run();
1
+ import { action, defineApp, page, state } from 'redweb';
2
+
3
+ @page('/', { shared: true })
4
+ class CounterPage {
5
+ @state() count = 0;
6
+
7
+ @action()
8
+ increment() { this.count += 1; }
9
+
10
+ render() {
11
+ return <button rw-click="increment">Count {this.count}</button>;
12
+ }
13
+ }
14
+
15
+ const app = defineApp({ pages: [CounterPage] });
16
+ app.run();
@@ -1,8 +1,8 @@
1
1
  import { randomBytes } from 'node:crypto';
2
- import { page, defineApp, BaseHandler, SocketRoute, RedWebSocket, RedWebRequest, LivePageRequestContext } from 'redweb';
2
+ import { page, defineApp, BaseHandler, SocketRoute, RedWebSocket, RedWebRequest, LivePageRequestContext } from 'redweb';
3
3
 
4
4
  // A runnable local demonstration, not a production credential store.
5
- export async function createApp(port = 8181) {
5
+ export async function createApp(port = 8181) {
6
6
  const token = randomBytes(32).toString('base64url');
7
7
  let enabled = true;
8
8
  const authenticate = (request: Pick<RedWebRequest, 'headers'>) =>
@@ -28,9 +28,9 @@ export async function createApp(port = 8181) {
28
28
  }
29
29
  }
30
30
 
31
- const app = defineApp({ pages: [Home], sockets: [Team], authenticate, port, bind: '127.0.0.1', logger: null });
32
- await app.run();
33
- const team = app.sockets!.routes.find(route => route instanceof Team)!;
31
+ const app = defineApp({ pages: [Home], sockets: [Team], authenticate, port, bind: '127.0.0.1', logger: null });
32
+ await app.run();
33
+ const team = app.sockets!.routes.find(route => route instanceof Team)!;
34
34
  return {
35
35
  app, team, token,
36
36
  async revoke() {
@@ -42,9 +42,9 @@ export async function createApp(port = 8181) {
42
42
  };
43
43
  }
44
44
 
45
- if (require.main === module) {
46
- createApp().then(demo => {
47
- console.log('Local demo: http://127.0.0.1:8181/ and ws://127.0.0.1:8181/team');
48
- console.log(`Authorization: Bearer ${demo.token}`); // One fresh local-demo credential per run.
49
- });
50
- }
45
+ if (require.main === module) {
46
+ createApp().then(demo => {
47
+ console.log('Local demo: http://127.0.0.1:8181/ and ws://127.0.0.1:8181/team');
48
+ console.log(`Authorization: Bearer ${demo.token}`); // One fresh local-demo credential per run.
49
+ });
50
+ }
@@ -1,2 +1,2 @@
1
- body { max-width: 50rem; margin: 3rem auto; padding: 0 1rem; font-family: system-ui, sans-serif; }
2
- nav { display: flex; gap: 1rem; margin-bottom: 2rem; }
1
+ body { max-width: 50rem; margin: 3rem auto; padding: 0 1rem; font-family: system-ui, sans-serif; }
2
+ nav { display: flex; gap: 1rem; margin-bottom: 2rem; }
@@ -1,22 +1,22 @@
1
- import { defineApp, defineSite } from 'redweb';
2
-
3
- const site = defineSite({
4
- css: 'site.css',
5
- layout: content => <body>
6
- <nav><a href="/">Home</a> · <a href="/about">About</a></nav>
7
- <main>{content}</main>
8
- </body>,
9
- });
10
-
11
- @site.page('/', { head: { title: 'Home' } })
12
- class HomePage {
13
- render() { return <h1>Welcome to Redweb</h1>; }
14
- }
15
-
16
- @site.page('/about', { head: { title: 'About' } })
17
- class AboutPage {
18
- render() { return <p>Two pages, one layout, no browser framework.</p>; }
19
- }
20
-
21
- const app = defineApp({ pages: [HomePage, AboutPage] });
22
- app.run();
1
+ import { defineApp, defineSite } from 'redweb';
2
+
3
+ const site = defineSite({
4
+ css: 'site.css',
5
+ layout: content => <body>
6
+ <nav><a href="/">Home</a> · <a href="/about">About</a></nav>
7
+ <main>{content}</main>
8
+ </body>,
9
+ });
10
+
11
+ @site.page('/', { head: { title: 'Home' } })
12
+ class HomePage {
13
+ render() { return <h1>Welcome to Redweb</h1>; }
14
+ }
15
+
16
+ @site.page('/about', { head: { title: 'About' } })
17
+ class AboutPage {
18
+ render() { return <p>Two pages, one layout, no browser framework.</p>; }
19
+ }
20
+
21
+ const app = defineApp({ pages: [HomePage, AboutPage] });
22
+ app.run();
package/docs/topics.json CHANGED
@@ -2,7 +2,7 @@
2
2
  { "id": "getting-started", "title": "Choose a starter and build a working app", "summary": "Requirements, fit, development, tests, and production boundaries.", "source": "docs/GETTING_STARTED.md" },
3
3
  { "id": "application", "title": "One application, one listener", "summary": "Define pages, socket routes and application services together; run and shut down one owned HTTP/WebSocket listener.", "source": "docs/APPLICATION.md" },
4
4
  { "id": "guides/realtime-dashboard", "title": "Build a private realtime dashboard", "summary": "Persistent SQLite cards, account-private updates and sign-out across tabs, with explicit single-process limits.", "source": "docs/guides/realtime-dashboard.md", "recipe": { "template": "dashboard", "file": "src/cards.tsx" } },
5
- { "id": "guides/jsx-without-react", "title": "Render JSX without React", "summary": "TypeScript pages, a shared layout and external CSS, rendered on the server without browser framework code.", "source": "docs/guides/jsx-without-react.md", "codeSource": "docs/snippets/site.tsx", "recipe": { "template": "site", "file": "src/app.tsx" } },
5
+ { "id": "guides/jsx-without-react", "title": "Render JSX without React", "summary": "TypeScript pages, a shared layout and external CSS, rendered on the server without browser framework code.", "source": "docs/guides/jsx-without-react.md", "codeSource": "docs/snippets/site.tsx", "recipe": { "template": "site", "file": "src/app.tsx" } },
6
6
  { "id": "guides/chatroom", "title": "Build a chatroom with live presence", "summary": "Reusable server-side components, validated forms and disconnect-aware presence, without custom browser socket glue.", "source": "docs/guides/chatroom.md", "recipe": { "template": "chat", "file": "src/chatroom.tsx" } },
7
7
  { "id": "guides/typed-websockets", "title": "Share typed WebSocket contracts", "summary": "One match route, separate join/move/resume handlers and validated client/server payloads from the same schema.", "source": "docs/guides/typed-websockets.md", "recipe": { "template": "socket", "file": "src/handlers.ts" } },
8
8
  { "id": "guides/http-websocket", "title": "Serve HTTP and WebSockets on one port", "summary": "An Express endpoint and raw socket route share one listener with one explicit shutdown owner.", "source": "docs/guides/http-websocket.md", "recipe": { "template": "http-ws", "file": "src/app.tsx" } },
@@ -12,8 +12,8 @@
12
12
  { "id": "runtime-diagnostics", "title": "Understand runtime failures before retrying", "summary": "Safe authentication, authorization, validation and application errors, with actual retry and cancellation guarantees.", "source": "docs/RUNTIME_DIAGNOSTICS.md" },
13
13
  { "id": "agent-access", "title": "Optional read-only agent documentation access", "summary": "Configure local MCP search and exact recipe retrieval without adding dependencies to Redweb servers.", "source": "docs/AGENT_ACCESS.md" },
14
14
  { "id": "live-html", "title": "Pages, components, state, actions, and CSS", "summary": "Server-side TSX, automatic updates, keyed lists, lifecycle, and static export.", "source": "docs/LIVE_HTML.md" },
15
- { "id": "socket-contracts", "title": "Typed socket routes and handlers", "summary": "Shared validation, client/server types, protocol errors, and join/move/resume.", "source": "docs/SOCKET_CONTRACTS.md" },
16
- { "id": "socket-pages", "title": "Server-side TSX for custom socket routes", "summary": "Typed handler bindings, private page state and one generated client connection without handwritten browser glue.", "source": "docs/SOCKET_PAGES.md" },
15
+ { "id": "socket-contracts", "title": "Typed socket routes and handlers", "summary": "Shared validation, client/server types, protocol errors, and join/move/resume.", "source": "docs/SOCKET_CONTRACTS.md" },
16
+ { "id": "socket-pages", "title": "Server-side TSX for custom socket routes", "summary": "Typed handler bindings, private page state and one generated client connection without handwritten browser glue.", "source": "docs/SOCKET_PAGES.md" },
17
17
  { "id": "room-authorization", "title": "Private rooms and shared request identity", "summary": "Explicit entry policies, bounded authorization, trusted context, publication and revocation.", "source": "docs/ROOM_AUTHORIZATION.md" },
18
18
  { "id": "operations", "title": "Deploy and operate socket services", "summary": "Readiness, shutdown, capacity, reconnection, and distributed boundaries.", "source": "docs/MULTIPLAYER_OPERATIONS.md" },
19
19
  { "id": "production-contract", "title": "Production guarantees and limits", "summary": "Resource ownership, delivery semantics, compatibility, and release gates.", "source": "docs/PRODUCTION_READINESS.md" },
package/index.d.ts CHANGED
@@ -35,9 +35,9 @@ declare module 'redweb' {
35
35
  isAssigned: boolean;
36
36
  sendJson(data: unknown): boolean;
37
37
  broadcast(data: unknown): number;
38
- context?: RedWebConnectionContext;
39
- /** Present on an attached page connection; checks the actual page class and connection lifetime. */
40
- page?<Page extends object>(PageClass: new () => Page): Page;
38
+ context?: RedWebConnectionContext;
39
+ /** Present on an attached page connection; checks the actual page class and connection lifetime. */
40
+ page?<Page extends object>(PageClass: new () => Page): Page;
41
41
  joinRoom?(roomId: string): boolean;
42
42
  /** Bounded permission check followed by atomic membership insertion. */
43
43
  enterRoom?(roomId: string): Promise<boolean>;
@@ -51,57 +51,57 @@ declare module 'redweb' {
51
51
  sendBinaryEvent?(value: unknown): Promise<boolean>;
52
52
  };
53
53
 
54
- export interface RedWebConnectionContext extends RequestContext {
54
+ export interface RedWebConnectionContext extends RequestContext {
55
55
  readonly connectionId: string;
56
56
  readonly principal: unknown;
57
57
  session: unknown | null;
58
58
  metadata: Record<string, unknown>;
59
59
  readonly protocol?: Readonly<{ version: string }>;
60
- }
61
-
62
- /** A deliberate, safe command rejection, unlike an unexpected application error. */
63
- export class ClientError extends Error { constructor(message: string); }
64
- export interface ClientEvent { type: string; payload: unknown; }
65
- export interface ConnectedClientsOptions<Page extends object, Identity> {
66
- /** Must still equal the admitted principal. Returning another identity never transfers a connection. */
67
- identity(context: RedWebConnectionContext): Identity | undefined | false | Promise<Identity | undefined | false>;
68
- page(): new () => Page;
69
- project(client: ConnectedClient<Page, Identity>, room: string, online: readonly Identity[]): Partial<Page> | Promise<Partial<Page>>;
70
- authorizationTimeoutMs?: number;
71
- projectionTimeoutMs?: number;
72
- /** Explicitly classify domain errors safe to display; all other failures remain private. */
73
- reject?(error: unknown): string | undefined;
74
- errorState?(message: string): Partial<Page>;
75
- /** Optional non-HTML client support. Redweb performs the final checked send. */
76
- raw?: {
77
- update(state: Partial<Page>): ClientEvent | Promise<ClientEvent>;
78
- reject?(message: string): ClientEvent | Promise<ClientEvent>;
79
- };
80
- }
81
- export class ConnectedClient<Page extends object, Identity = unknown> {
82
- private constructor();
83
- readonly socket: RedWebSocket;
84
- readonly identity: Identity;
85
- readonly page: Page;
86
- readonly rooms: readonly string[];
87
- /** Requires exactly one current room; use rooms for multi-room applications. */
88
- readonly room: string;
89
- /** Reserve room capacity, run a synchronous domain commit, and roll back new membership if it rejects. */
90
- join(room: string, commit?: () => unknown): Promise<void>;
91
- leave(room: string): boolean;
92
- }
93
- export class ConnectedClients<Page extends object, Identity = unknown> {
94
- constructor(options: ConnectedClientsOptions<Page, Identity>);
95
- get(socket: RedWebSocket): ConnectedClient<Page, Identity>;
96
- refresh(room: string): Promise<unknown>;
97
- bind<Schemas extends import('redweb/contract').SocketSchemas>(contract: import('redweb/contract').SocketContract<Schemas>): {
98
- readonly protocol: { readonly versions: readonly string[] };
99
- handler<Type extends keyof Schemas & string>(type: Type, callback: (
100
- client: ConnectedClient<Page, Identity>, payload: import('redweb/contract').ContractOutput<Schemas[Type]>,
101
- ) => unknown): import('redweb/contract').SocketHandler<import('redweb/contract').ContractInput<Schemas[Type]>>;
102
- };
103
- }
104
- export function connectedClients<Page extends object, Identity>(options: ConnectedClientsOptions<Page, Identity>): ConnectedClients<Page, Identity>;
60
+ }
61
+
62
+ /** A deliberate, safe command rejection, unlike an unexpected application error. */
63
+ export class ClientError extends Error { constructor(message: string); }
64
+ export interface ClientEvent { type: string; payload: unknown; }
65
+ export interface ConnectedClientsOptions<Page extends object, Identity> {
66
+ /** Must still equal the admitted principal. Returning another identity never transfers a connection. */
67
+ identity(context: RedWebConnectionContext): Identity | undefined | false | Promise<Identity | undefined | false>;
68
+ page(): new () => Page;
69
+ project(client: ConnectedClient<Page, Identity>, room: string, online: readonly Identity[]): Partial<Page> | Promise<Partial<Page>>;
70
+ authorizationTimeoutMs?: number;
71
+ projectionTimeoutMs?: number;
72
+ /** Explicitly classify domain errors safe to display; all other failures remain private. */
73
+ reject?(error: unknown): string | undefined;
74
+ errorState?(message: string): Partial<Page>;
75
+ /** Optional non-HTML client support. Redweb performs the final checked send. */
76
+ raw?: {
77
+ update(state: Partial<Page>): ClientEvent | Promise<ClientEvent>;
78
+ reject?(message: string): ClientEvent | Promise<ClientEvent>;
79
+ };
80
+ }
81
+ export class ConnectedClient<Page extends object, Identity = unknown> {
82
+ private constructor();
83
+ readonly socket: RedWebSocket;
84
+ readonly identity: Identity;
85
+ readonly page: Page;
86
+ readonly rooms: readonly string[];
87
+ /** Requires exactly one current room; use rooms for multi-room applications. */
88
+ readonly room: string;
89
+ /** Reserve room capacity, run a synchronous domain commit, and roll back new membership if it rejects. */
90
+ join(room: string, commit?: () => unknown): Promise<void>;
91
+ leave(room: string): boolean;
92
+ }
93
+ export class ConnectedClients<Page extends object, Identity = unknown> {
94
+ constructor(options: ConnectedClientsOptions<Page, Identity>);
95
+ get(socket: RedWebSocket): ConnectedClient<Page, Identity>;
96
+ refresh(room: string): Promise<unknown>;
97
+ bind<Schemas extends import('redweb/contract').SocketSchemas>(contract: import('redweb/contract').SocketContract<Schemas>): {
98
+ readonly protocol: { readonly versions: readonly string[] };
99
+ handler<Type extends keyof Schemas & string>(type: Type, callback: (
100
+ client: ConnectedClient<Page, Identity>, payload: import('redweb/contract').ContractOutput<Schemas[Type]>,
101
+ ) => unknown): import('redweb/contract').SocketHandler<import('redweb/contract').ContractInput<Schemas[Type]>>;
102
+ };
103
+ }
104
+ export function connectedClients<Page extends object, Identity>(options: ConnectedClientsOptions<Page, Identity>): ConnectedClients<Page, Identity>;
105
105
 
106
106
  export interface AdmissionContext {
107
107
  signal: AbortSignal;
@@ -328,9 +328,9 @@ declare module 'redweb' {
328
328
 
329
329
  /** ─────────────────── ROUTES & HANDLERS ─────────────────── */
330
330
 
331
- export interface SocketRouteConfig {
332
- /** Opt-in server-side clients, with RoomRegistry membership and private page projection. */
333
- connections?: ConnectedClients<any, any>;
331
+ export interface SocketRouteConfig {
332
+ /** Opt-in server-side clients, with RoomRegistry membership and private page projection. */
333
+ connections?: ConnectedClients<any, any>;
334
334
  path: string;
335
335
  handlers: Array<new () => BaseHandler>;
336
336
  services?: Array<new () => SocketService>;
@@ -460,8 +460,8 @@ declare module 'redweb' {
460
460
  enter(roomId: string, socket: RedWebSocket): Promise<boolean>;
461
461
  leave(roomId: string, socket: RedWebSocket): boolean;
462
462
  leaveAll(socket: RedWebSocket): number;
463
- members(roomId: string): RedWebSocket[];
464
- roomsFor(socket: RedWebSocket): string[];
463
+ members(roomId: string): RedWebSocket[];
464
+ roomsFor(socket: RedWebSocket): string[];
465
465
  has(roomId: string, socket: RedWebSocket): boolean;
466
466
  broadcast(roomId: string, data: unknown, options?: { except?: RedWebSocket }): number;
467
467
  broadcastFrom(socket: RedWebSocket, roomId: string, data: unknown, options?: { except?: RedWebSocket }): number;
@@ -603,9 +603,9 @@ declare module 'redweb' {
603
603
  dispose(): Promise<boolean>;
604
604
  }
605
605
 
606
- export type PageOptions = {
607
- /** Use a registered custom route for this private live page's typed TSX commands and rendering. */
608
- socket?: new () => SocketRoute;
606
+ export type PageOptions = {
607
+ /** Use a registered custom route for this private live page's typed TSX commands and rendering. */
608
+ socket?: new () => SocketRoute;
609
609
  template?: string;
610
610
  css?: string | readonly string[];
611
611
  live?: boolean;
@@ -648,6 +648,21 @@ declare module 'redweb' {
648
648
  (this: This, initialValue: Value) => Value;
649
649
  }
650
650
 
651
+ export interface LiveResourceDecorator extends LiveStateDecorator {}
652
+
653
+ export interface LiveInjectDecorator {
654
+ (target: object, propertyKey: string): void;
655
+ <This, Value>(value: undefined, context: ClassFieldDecoratorContext<This, Value>):
656
+ (this: This, initialValue: Value) => Value;
657
+ }
658
+
659
+ /** A server-owned keyed publisher for connection-scoped page state. */
660
+ export class LiveResource<Value = unknown, Key extends string | number | bigint | boolean = string> {
661
+ publish(key: Key, value: Value): number;
662
+ }
663
+
664
+ export function liveResource<Value = unknown, Key extends string | number | bigint | boolean = string>(): LiveResource<Value, Key>;
665
+
651
666
  export interface LiveActionDecorator {
652
667
  (target: object, propertyKey: string, descriptor: PropertyDescriptor): void | PropertyDescriptor;
653
668
  <This, Value extends (this: This, ...args: any[]) => any>(
@@ -680,6 +695,18 @@ declare module 'redweb' {
680
695
  export function component<Props = void>(render: (properties: Props) => HtmlFragment):
681
696
  (properties: Props) => HtmlFragment;
682
697
  export function state(options?: StateOptions): LiveStateDecorator;
698
+ /** Keeps this state field synchronized with values published for its server-derived key. */
699
+ export function resource<Value, Key extends string | number | bigint | boolean>(
700
+ source: LiveResource<Value, Key>, select: (page: any) => Key | null | undefined
701
+ ): LiveResourceDecorator;
702
+ /** Receives an explicit instance from defineApp({ providers }). */
703
+ export function inject(provider: string): LiveInjectDecorator;
704
+ export interface UploadedFile {
705
+ readonly stream: import('stream').Readable;
706
+ readonly type: string;
707
+ readonly name: string | null;
708
+ }
709
+ export function upload(options?: { maxBytes?: number; accept?: string | readonly string[] }): LiveActionDecorator;
683
710
  export function action(): LiveActionDecorator;
684
711
  export interface ActionAuthorization<Input> {
685
712
  authorize: (context: LivePageConnectionContext, input: Input) => boolean | Promise<boolean>;
@@ -717,14 +744,20 @@ declare module 'redweb' {
717
744
  client?: string;
718
745
  runtime?: string;
719
746
  css?: string;
747
+ /** Same-origin bounded streaming endpoint used by rw-upload and rw-paste. */
748
+ upload?: string;
720
749
  };
721
750
  sessionTtlMs?: number;
722
751
  maxSessions?: number;
723
752
  maxConcurrentRenders?: number;
724
753
  /** Phase-local render/route and final owned-HTTP cleanup bound; defaults to 1000ms, not a total application deadline. */
725
754
  shutdownTimeoutMs?: number;
755
+ /** Maximum time Redweb will receive a live page upload stream before cancelling it; defaults to 30000ms. */
756
+ uploadTimeoutMs?: number;
726
757
  heartbeat?: HeartbeatOptions;
727
758
  origins?: string[] | ((origin: string | undefined, request: import('http').IncomingMessage) => boolean | Promise<boolean>);
759
+ /** Explicit application-owned instances available through @inject fields on pages. */
760
+ providers?: Record<string, unknown>;
728
761
  }
729
762
 
730
763
  export type LiveHtmlAuthentication = {
@@ -773,6 +806,8 @@ declare module 'redweb' {
773
806
  pages?: readonly LivePageClass[];
774
807
  sockets?: ReadonlyArray<new () => SocketRoute>;
775
808
  services?: ReadonlyArray<new () => ApplicationService>;
809
+ /** Application-owned instances available to decorated page fields. Redweb never constructs them. */
810
+ providers?: Record<string, unknown>;
776
811
  httpServices?: RedWebOptions['services'];
777
812
  startupTimeoutMs?: number;
778
813
  /** Install process signal handlers only when run() is called; defaults to true. */
package/index.js CHANGED
@@ -15,18 +15,18 @@ const {
15
15
  } = require('./src/ws');
16
16
  const { BaseHandler } = require('./src/ws/BaseHandler');
17
17
  const { defineSocketContract } = require('./contract');
18
- const { Application, defineApp } = require('./src/Application');
19
- const { connectedClients, ConnectedClients, ConnectedClient, ClientError } = require('./src/ws/ConnectedClients');
18
+ const { Application, defineApp } = require('./src/Application');
19
+ const { connectedClients, ConnectedClients, ConnectedClient, ClientError } = require('./src/ws/ConnectedClients');
20
20
  const HttpServer = require('./src/http/HttpServer');
21
21
  const HttpsServer = require('./src/http/HttpsServer');
22
- const { action, attribute, codeBlock, component, defineSite, each, exportStatic, html, HtmlRenderer, LiveHtmlServer, LivePage, page, start, state, url, view } = require('./src/htmx');
22
+ const { action, attribute, codeBlock, component, defineSite, each, exportStatic, html, HtmlRenderer, inject, LiveHtmlServer, LivePage, LiveResource, liveResource, page, resource, start, state, upload, url, view } = require('./src/htmx');
23
23
  module.exports = {
24
24
  Application,
25
- defineApp,
26
- connectedClients,
27
- ConnectedClients,
28
- ConnectedClient,
29
- ClientError,
25
+ defineApp,
26
+ connectedClients,
27
+ ConnectedClients,
28
+ ConnectedClient,
29
+ ClientError,
30
30
  HttpServer,
31
31
  HttpsServer,
32
32
  BaseHttpServer,
@@ -55,11 +55,16 @@ module.exports = {
55
55
  exportStatic,
56
56
  html,
57
57
  HtmlRenderer,
58
+ inject,
58
59
  LiveHtmlServer,
59
60
  LivePage,
61
+ LiveResource,
62
+ liveResource,
60
63
  page,
64
+ resource,
61
65
  start,
62
66
  state,
67
+ upload,
63
68
  url,
64
69
  view
65
70
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "redweb",
3
- "version": "0.16.2",
3
+ "version": "0.16.3",
4
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",
@@ -44,7 +44,7 @@
44
44
  "scripts": {
45
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
46
  "prepack": "node scripts/build-live-html-examples.js --check && node scripts/generate-docs.js --check",
47
- "prepublishOnly": "node scripts/generate-docs.js --release-check",
47
+ "prepublishOnly": "node scripts/generate-docs.js --release-check && node node_modules/jest/bin/jest.js tests/unit/documentation.unit.test.js --runInBand --coverage=false",
48
48
  "generate:docs": "node scripts/generate-docs.js",
49
49
  "verify:docs:mcp": "npm --prefix integrations/docs-mcp test",
50
50
  "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",
@@ -108,7 +108,7 @@
108
108
  "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",
109
109
  "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",
110
110
  "verify:client:link": "node --test tests/integration/npm-link.integration.test.cjs",
111
- "test": "npx jest --testPathIgnorePatterns=soak-.*[.]test[.]js$ --testPathIgnorePatterns=benchmark-measurement[.]integration[.]test[.]js$"
111
+ "test": "npx jest --testPathIgnorePatterns=soak-tools[.]integration[.]test[.]js$ --testPathIgnorePatterns=benchmark-measurement[.]integration[.]test[.]js$"
112
112
  },
113
113
  "files": [
114
114
  "bin",
@@ -159,13 +159,13 @@
159
159
  "@types/ws": "^8.18.1",
160
160
  "cors": "^2.8.5",
161
161
  "express": "^4.22.2",
162
- "redweb-client": "^0.3.0",
162
+ "redweb-client": "^0.3.1",
163
163
  "ws": "^8.21.3"
164
164
  },
165
- "overrides": {
166
- "express": { "qs": "6.16.0" }
167
- },
168
- "devDependencies": {
165
+ "overrides": {
166
+ "express": { "qs": "6.16.0" }
167
+ },
168
+ "devDependencies": {
169
169
  "@types/jest": "^29.5.12",
170
170
  "c8": "^10.1.3",
171
171
  "expect": "29.7.0",
@@ -1,7 +1,7 @@
1
- # Neutral application foundation
2
-
3
- The default initializer creates a working TypeScript/TSX application without choosing a product domain. Replace `HomePage` with your pages, then add socket routes and application services as needed.
4
-
5
- Use `--with auth,multiplayer` when the project needs those dependency sets. `auth` adds Express, Zod, their TypeScript declarations, and the Node version required by Redweb's native-SQLite authentication path. `multiplayer` adds Redweb Client and Zod. Capabilities adjust the manifest without copying dashboard, chat, counter, or match-example source. Use an explicit `--template` only when you want a complete example walkthrough.
6
-
7
- Run `npm test` for the real HTTP and lifecycle checks. Pass `--bare` only when you intentionally do not want the test directory, test scripts, or test-only coverage dependency; the runnable source, assets, build scripts, and development setup stay the same.
1
+ # Neutral application foundation
2
+
3
+ The default initializer creates a working TypeScript/TSX application without choosing a product domain. Replace `HomePage` with your pages, then add socket routes and application services as needed.
4
+
5
+ Use `--with auth,multiplayer` when the project needs those dependency sets. `auth` adds Express, Zod, their TypeScript declarations, and the Node version required by Redweb's native-SQLite authentication path. `multiplayer` adds Redweb Client and Zod. Capabilities adjust the manifest without copying dashboard, chat, counter, or match-example source. Use an explicit `--template` only when you want a complete example walkthrough.
6
+
7
+ Run `npm test` for the real HTTP and lifecycle checks. Pass `--bare` only when you intentionally do not want the test directory, test scripts, or test-only coverage dependency; the runnable source, assets, build scripts, and development setup stay the same.
@@ -1,15 +1,15 @@
1
- const test = require('node:test');
2
- const assert = require('node:assert/strict');
3
- const { listen } = require('./network.cjs');
4
-
5
- test('neutral application foundation serves real HTML and CSS', { timeout: 10000 }, async t => {
6
- const origin = await listen(t);
7
- const response = await fetch(origin);
8
- assert.equal(response.status, 200);
9
- const document = await response.text();
10
- assert.match(document, /<h1>Redweb is ready\.<\/h1>/);
11
- const css = document.match(/<link rel="stylesheet" href="([^"]+)"/)[1];
12
- const stylesheet = await fetch(`${origin}${css}`);
13
- assert.equal(stylesheet.status, 200);
14
- assert.match(await stylesheet.text(), /\.home/);
15
- });
1
+ const test = require('node:test');
2
+ const assert = require('node:assert/strict');
3
+ const { listen } = require('./network.cjs');
4
+
5
+ test('neutral application foundation serves real HTML and CSS', { timeout: 10000 }, async t => {
6
+ const origin = await listen(t);
7
+ const response = await fetch(origin);
8
+ assert.equal(response.status, 200);
9
+ const document = await response.text();
10
+ assert.match(document, /<h1>Redweb is ready\.<\/h1>/);
11
+ const css = document.match(/<link rel="stylesheet" href="([^"]+)"/)[1];
12
+ const stylesheet = await fetch(`${origin}${css}`);
13
+ assert.equal(stylesheet.status, 200);
14
+ assert.match(await stylesheet.text(), /\.home/);
15
+ });
@@ -1,12 +1,12 @@
1
- import { defineApp, page } from 'redweb';
2
-
3
- @page('/', { live: false, css: 'app.css' })
4
- export class HomePage {
5
- render() {
6
- return <main class="home"><h1>Redweb is ready.</h1><p>Replace this page with your application.</p></main>;
7
- }
8
- }
9
-
10
- export const app = defineApp({ pages: [HomePage], port: Number(process.env.PORT ?? 8181), templateRoot: __dirname });
11
-
12
- if (require.main === module) app.run();
1
+ import { defineApp, page } from 'redweb';
2
+
3
+ @page('/', { live: false, css: 'app.css' })
4
+ export class HomePage {
5
+ render() {
6
+ return <main class="home"><h1>Redweb is ready.</h1><p>Replace this page with your application.</p></main>;
7
+ }
8
+ }
9
+
10
+ export const app = defineApp({ pages: [HomePage], port: Number(process.env.PORT ?? 8181), templateRoot: __dirname });
11
+
12
+ if (require.main === module) app.run();
@@ -40,10 +40,10 @@ and application-specific rate limits. These starters are demonstrations, not a h
40
40
  Never commit secrets; `.env` is ignored but is not loaded automatically.
41
41
 
42
42
  `npx --no-install redweb doctor --json` reports configuration problems without changing your files.
43
- ## Dependency security
44
-
45
- This starter includes an application-root npm override for Express 4's `qs`
46
- dependency, selecting patched `qs@6.16.0`. Keep the override when merging this
47
- starter into an existing application, refresh its lockfile and run `npm audit`.
48
- Overrides in Redweb's own package do not apply to installed consumers. Recheck
49
- upstream Express/body-parser releases before removing this temporary mitigation.
43
+ ## Dependency security
44
+
45
+ This starter includes an application-root npm override for Express 4's `qs`
46
+ dependency, selecting patched `qs@6.16.0`. Keep the override when merging this
47
+ starter into an existing application, refresh its lockfile and run `npm audit`.
48
+ Overrides in Redweb's own package do not apply to installed consumers. Recheck
49
+ upstream Express/body-parser releases before removing this temporary mitigation.