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.
- package/CHANGELOG.md +35 -29
- package/README.md +293 -291
- package/contract.d.ts +11 -11
- package/docs/API_EXAMPLES_VERIFICATION.md +22 -22
- package/docs/APPLICATION.md +96 -94
- package/docs/CLI.md +122 -122
- package/docs/CLIENT_DEVELOPMENT.md +9 -9
- package/docs/CONNECTED_CLIENTS_VERIFICATION.md +65 -65
- package/docs/DEVELOPMENT.md +81 -81
- package/docs/GETTING_STARTED.md +78 -78
- package/docs/LIVE_HTML.md +555 -478
- package/docs/MIGRATION.md +28 -28
- package/docs/RELEASE_TRUST.md +88 -88
- package/docs/RUNTIME_DIAGNOSTICS.md +78 -78
- package/docs/SOCKET_CONTRACTS.md +42 -42
- package/docs/SOCKET_PAGES.md +172 -172
- package/docs/SOCKET_PAGE_RELEASE_PREPARATION.md +120 -120
- package/docs/SOCKET_PAGE_VERIFICATION.md +85 -85
- package/docs/generated.json +2286 -2286
- package/docs/guides/chatroom.md +1 -1
- package/docs/guides/jsx-without-react.md +14 -14
- package/docs/reference.json +1329 -1329
- package/docs/releases/0.15.0.json +2217 -2217
- package/docs/releases/0.16.0.json +2217 -2217
- package/docs/releases/0.16.1.json +2286 -2286
- package/docs/releases/0.16.2.json +2286 -2286
- package/docs/releases/0.16.3.json +2286 -0
- package/docs/snippets/components.tsx +24 -24
- package/docs/snippets/counter.tsx +16 -16
- package/docs/snippets/room-access.tsx +11 -11
- package/docs/snippets/site.css +2 -2
- package/docs/snippets/site.tsx +22 -22
- package/docs/topics.json +3 -3
- package/index.d.ts +92 -57
- package/index.js +13 -8
- package/package.json +8 -8
- package/recipes/foundation/README.md +7 -7
- package/recipes/foundation/app.test.cjs +15 -15
- package/recipes/foundation/app.tsx +12 -12
- package/recipes/shared/README.md +7 -7
- package/src/Application.js +4 -4
- package/src/access/failure-codes.json +4 -0
- package/src/cli/ProjectInitializer.js +1 -1
- package/src/cli/arguments.js +21 -21
- package/src/cli/run.js +12 -12
- package/src/cli/templates.js +40 -40
- package/src/docs/Documentation.js +29 -29
- package/src/htmx/Jsx.js +2 -2
- package/src/htmx/LiveHtmlServer.js +5 -1
- package/src/htmx/LivePage.js +5 -1
- package/src/htmx/LiveResource.js +96 -0
- package/src/htmx/PageManager.js +126 -13
- package/src/htmx/PageSocketRoute.js +132 -132
- package/src/htmx/PageTaskLane.js +39 -0
- package/src/htmx/ReactiveRenderer.js +8 -8
- package/src/htmx/SocketAction.js +19 -19
- package/src/htmx/TemplateRenderer.js +1 -1
- package/src/htmx/index.js +3 -2
- package/src/htmx/metadata.js +117 -6
- package/src/ws/BaseHandler.js +6 -6
- package/src/ws/ConnectedClients.js +207 -207
- package/src/ws/HandlerGuard.js +4 -4
- package/src/ws/RoomRegistry.js +4 -4
- package/src/ws/RouteRuntime.js +11 -11
- package/src/ws/SocketAction.js +16 -16
- package/src/ws/SocketContract.js +3 -3
- 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
|
+
}
|
package/docs/snippets/site.css
CHANGED
|
@@ -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; }
|
package/docs/snippets/site.tsx
CHANGED
|
@@ -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.
|
|
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
|
|
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.
|
|
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();
|
package/recipes/shared/README.md
CHANGED
|
@@ -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.
|