redweb 0.12.0 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +166 -9
- package/README.md +177 -523
- package/bin/redweb.js +11 -20
- package/client.d.ts +7 -2
- package/config/tsconfig.json +14 -14
- package/contract.d.ts +45 -0
- package/contract.js +5 -0
- package/docs/ACTION_INPUT_VERIFICATION.md +96 -0
- package/docs/ADMISSION_TIMEOUT_VERIFICATION.md +69 -0
- package/docs/AGENT_ACCESS.md +35 -0
- package/docs/AGENT_EVALUATION.md +58 -0
- package/docs/AGENT_READY_ACCEPTANCE.md +763 -0
- package/docs/APPLICATION_RECORDER_VERIFICATION.md +50 -0
- package/docs/BENCHMARK_VERIFICATION.md +307 -0
- package/docs/BROWSER_OWNER_VERIFICATION.md +191 -0
- package/docs/CLI.md +116 -0
- package/docs/CLIENT_DEVELOPMENT.md +152 -0
- package/docs/CLIENT_POLISH_VERIFICATION.md +282 -0
- package/docs/COVERAGE_COUNTER_VALIDATION.md +109 -0
- package/docs/COVERAGE_SCOPE_AUDIT.md +1183 -0
- package/docs/DEVELOPMENT.md +79 -0
- package/docs/DIAGNOSTIC_COMPATIBILITY.md +76 -0
- package/docs/DOCUMENTATION.md +37 -0
- package/docs/FEEDBACK_COMMAND_VERIFICATION.md +228 -0
- package/docs/GETTING_STARTED.md +58 -0
- package/docs/JSX_PERFORMANCE_VERIFICATION.md +59 -0
- package/docs/LIVE_HTML.md +169 -21
- package/docs/LIVE_HTML_LOAD_VERIFICATION.md +98 -0
- package/docs/MIGRATION.md +28 -0
- package/docs/MULTIPLAYER_OPERATIONS.md +26 -4
- package/docs/ORIGINAL_RECOVERY_VERIFICATION.md +100 -0
- package/docs/PACKAGED_EXAMPLE_VERIFICATION.md +126 -0
- package/docs/POLISH_RELEASE_CHECKPOINT.md +91 -0
- package/docs/PROCESS_CLEANUP_OBSERVATION.md +61 -0
- package/docs/PROCESS_REAPING_VERIFICATION.md +30 -0
- package/docs/PRODUCTION_READINESS.md +11 -2
- package/docs/RECOVERY_CLIENT_HEAP.md +201 -0
- package/docs/RECOVERY_CODE_ATTRIBUTION.md +174 -0
- package/docs/RECOVERY_CODE_CENSUS.md +158 -0
- package/docs/RECOVERY_COMPARISON.md +103 -0
- package/docs/RECOVERY_DEOPTIMIZATION.md +169 -0
- package/docs/RECOVERY_FOLLOWUP_SPIKE.md +147 -0
- package/docs/RECOVERY_INVESTIGATION.md +229 -0
- package/docs/RECOVERY_RUNTIME_CONTROLS.md +181 -0
- package/docs/RELEASE_TRUST.md +58 -0
- package/docs/ROOM_AUTHORIZATION.md +49 -0
- package/docs/RUNTIME_DIAGNOSTICS.md +78 -0
- package/docs/SERVER_RECOVERY_CANDIDATE.md +185 -0
- package/docs/SOAK_ROTATION_OBSERVATION.md +160 -0
- package/docs/SOAK_VERIFICATION.md +154 -0
- package/docs/SOCKET_CONTRACTS.md +39 -0
- package/docs/SPLIT_RECOVERY_COVERAGE.md +83 -0
- package/docs/SPLIT_RECOVERY_ERROR_HANDLING.md +67 -0
- package/docs/STARTER_COORDINATOR_VERIFICATION.md +112 -0
- package/docs/STARTER_LIFECYCLE_VERIFICATION.md +75 -0
- package/docs/STARTER_REPORT_RETENTION.md +73 -0
- package/docs/VERIFICATION_EVIDENCE.md +2 -0
- package/docs/generated.json +2154 -0
- package/docs/guides/chatroom.md +27 -0
- package/docs/guides/http-websocket.md +28 -0
- package/docs/guides/jsx-without-react.md +26 -0
- package/docs/guides/realtime-dashboard.md +29 -0
- package/docs/guides/typed-websockets.md +26 -0
- package/docs/reference.json +1207 -0
- package/docs/snippets/room-access.tsx +51 -0
- package/docs/topics.json +21 -0
- package/examples/live-html/chatroom.js +207 -268
- package/examples/live-html/chatroom.tsx +167 -0
- package/examples/live-html/jsx-page.js +1 -1
- package/examples/live-html/jsx-page.tsx +1 -1
- package/examples/live-html/tsconfig.json +8 -7
- package/index.d.ts +170 -45
- package/index.js +2 -0
- package/jsx-dev-runtime.js +2 -2
- package/jsx-runtime.d.ts +7 -2
- package/package.json +88 -7
- package/recipes/add/artifact.test.cjs +57 -0
- package/recipes/add/live.tsx +18 -0
- package/recipes/add/socket-route.ts +24 -0
- package/recipes/chat/README.md +22 -0
- package/recipes/chat/app.test.cjs +105 -0
- package/recipes/chat/app.tsx +9 -0
- package/recipes/dashboard/README.md +43 -0
- package/recipes/dashboard/admin.ts +21 -0
- package/recipes/dashboard/app.css +16 -0
- package/recipes/dashboard/app.test.cjs +450 -0
- package/recipes/dashboard/app.tsx +86 -0
- package/recipes/dashboard/auth.ts +80 -0
- package/recipes/dashboard/cards.tsx +102 -0
- package/recipes/dashboard/rate-window.test.cjs +17 -0
- package/recipes/dashboard/store.ts +120 -0
- package/recipes/http-ws/README.md +11 -0
- package/recipes/http-ws/app.test.cjs +92 -0
- package/recipes/http-ws/app.tsx +36 -0
- package/recipes/realtime/README.md +8 -0
- package/recipes/realtime/app.test.cjs +15 -0
- package/recipes/realtime/app.tsx +28 -0
- package/recipes/shared/README.md +40 -0
- package/recipes/shared/app.css +8 -0
- package/recipes/shared/copy-assets.cjs +8 -0
- package/recipes/shared/network.cjs +59 -0
- package/recipes/shared/run-app.test.cjs +158 -0
- package/recipes/shared/run-app.ts +50 -0
- package/recipes/site/README.md +4 -0
- package/recipes/site/app.test.cjs +19 -0
- package/recipes/site/app.tsx +25 -0
- package/recipes/socket/README.md +39 -0
- package/recipes/socket/app.test.cjs +85 -0
- package/recipes/socket/app.tsx +30 -0
- package/recipes/socket/contract.ts +12 -0
- package/recipes/socket/handlers.ts +40 -0
- package/src/OwnedServerLifecycle.js +66 -0
- package/src/access/AccessPolicy.js +37 -0
- package/src/access/AuthenticationFailure.js +13 -0
- package/src/access/RequestFailure.js +33 -0
- package/src/access/failure-codes.json +25 -0
- package/src/async/BoundedOperation.js +62 -0
- package/src/cli/ActionReferences.js +193 -0
- package/src/cli/AdditionLayout.js +140 -0
- package/src/cli/FilePlan.js +94 -0
- package/src/cli/ProjectAddition.js +60 -0
- package/src/cli/ProjectConfig.js +26 -0
- package/src/cli/ProjectDoctor.js +112 -0
- package/src/cli/ProjectInitializer.js +20 -30
- package/src/cli/SourceInspector.js +207 -0
- package/src/cli/StaticSource.js +192 -0
- package/src/cli/arguments.js +62 -0
- package/src/cli/formatCommand.js +10 -0
- package/src/cli/run.js +57 -0
- package/src/cli/templates.js +86 -87
- package/src/context/RequestSnapshot.js +41 -0
- package/src/dataProperty.js +11 -0
- package/src/development/DevelopmentPageManager.js +48 -0
- package/src/development/Inspection.js +104 -0
- package/src/development/ObservedRenderer.js +42 -0
- package/src/development/description.js +35 -0
- package/src/development/loopbackRequest.js +27 -0
- package/src/development/refreshBrowser.js +96 -0
- package/src/development/refreshStyles.js +9 -0
- package/src/development/settings.js +17 -0
- package/src/docs/Documentation.js +182 -0
- package/src/htmx/ActionDefinition.js +44 -0
- package/src/htmx/Jsx.js +24 -8
- package/src/htmx/LiveHtmlServer.js +41 -19
- package/src/htmx/LivePage.js +63 -13
- package/src/htmx/PageIdentity.js +32 -0
- package/src/htmx/PageLifetime.js +37 -0
- package/src/htmx/PageManager.js +203 -74
- package/src/htmx/ReactiveRenderer.js +241 -0
- package/src/htmx/StaticExporter.js +1 -1
- package/src/htmx/TemplateRenderer.js +13 -7
- package/src/htmx/browserRuntime.js +2 -93
- package/src/htmx/metadata.js +19 -7
- package/src/validation/ActionInputError.js +12 -0
- package/src/validation/SchemaValidator.js +38 -0
- package/src/ws/AdmissionPolicy.js +24 -23
- package/src/ws/BaseSocketServer.js +53 -38
- package/src/ws/ContractValidationError.js +12 -0
- package/src/ws/HeartbeatMonitor.js +19 -7
- package/src/ws/ProtocolPolicy.js +1 -1
- package/src/ws/RoomAccess.js +82 -0
- package/src/ws/RoomRegistry.js +56 -6
- package/src/ws/RouteRuntime.js +56 -10
- package/src/ws/SocketContract.js +112 -0
- package/src/ws/SocketRoute.js +18 -0
- package/src/ws/protocol-schema.json +6 -1
- package/examples/live-html/chatroom.ts +0 -217
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { action, component, page, start, state, type ActionInput } from 'redweb';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
|
|
4
|
+
const MAX_VISIBLE_MEMBERS = 100;
|
|
5
|
+
const visibleText = (maximum: number) => z.string()
|
|
6
|
+
.transform(value => value.normalize('NFKC').trim())
|
|
7
|
+
.pipe(z.string().min(1).max(maximum).regex(/^[^\p{Cc}\p{Cf}]+$/u));
|
|
8
|
+
export const chatInputs = {
|
|
9
|
+
join: z.object({ name: visibleText(40) }).strict(),
|
|
10
|
+
send: z.object({ message: visibleText(500) }).strict(),
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
interface StoredMessage { id: number; sender: string; text: string; }
|
|
14
|
+
interface RoomParticipant {
|
|
15
|
+
readonly displayName: string;
|
|
16
|
+
updateMessages(messages: readonly StoredMessage[]): void;
|
|
17
|
+
updatePresence(members: readonly string[]): void;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
class ChatRoom {
|
|
21
|
+
private history: readonly StoredMessage[] = [];
|
|
22
|
+
private nextMessageId = 0;
|
|
23
|
+
private readonly participants = new Set<RoomParticipant>();
|
|
24
|
+
private readonly online = new Set<RoomParticipant>();
|
|
25
|
+
|
|
26
|
+
join(participant: RoomParticipant) {
|
|
27
|
+
const name = participant.displayName.toLocaleLowerCase();
|
|
28
|
+
if ([...this.participants].some(member => member !== participant && member.displayName.toLocaleLowerCase() === name)) return false;
|
|
29
|
+
this.participants.add(participant);
|
|
30
|
+
this.online.add(participant);
|
|
31
|
+
participant.updateMessages(this.history);
|
|
32
|
+
this.publishPresence();
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
disconnect(participant: RoomParticipant) {
|
|
37
|
+
if (this.online.delete(participant)) this.publishPresence();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
leave(participant: RoomParticipant) {
|
|
41
|
+
this.online.delete(participant);
|
|
42
|
+
if (this.participants.delete(participant)) this.publishPresence();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
send(participant: RoomParticipant, text: string) {
|
|
46
|
+
if (!this.online.has(participant)) return false;
|
|
47
|
+
this.history = [...this.history, { id: ++this.nextMessageId, sender: participant.displayName, text }].slice(-100);
|
|
48
|
+
for (const member of this.participants) member.updateMessages(this.history);
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
private publishPresence() {
|
|
53
|
+
const members = [...this.online].map(participant => participant.displayName);
|
|
54
|
+
for (const participant of this.participants) participant.updatePresence(members);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
@component()
|
|
59
|
+
export class ChatroomComponent implements RoomParticipant {
|
|
60
|
+
@state() displayName = '';
|
|
61
|
+
@state() feedback = '';
|
|
62
|
+
@state() messages: readonly StoredMessage[] = [];
|
|
63
|
+
@state() members: readonly string[] = [];
|
|
64
|
+
|
|
65
|
+
constructor(private readonly room: ChatRoom) {}
|
|
66
|
+
|
|
67
|
+
connected() { if (this.displayName) this.room.join(this); }
|
|
68
|
+
disconnected() { this.room.disconnect(this); }
|
|
69
|
+
disposed() { this.room.leave(this); }
|
|
70
|
+
|
|
71
|
+
@action({ input: chatInputs.join })
|
|
72
|
+
join({ name }: ActionInput<typeof chatInputs.join>) {
|
|
73
|
+
if (this.displayName) return false;
|
|
74
|
+
this.displayName = name;
|
|
75
|
+
if (!this.room.join(this)) {
|
|
76
|
+
this.displayName = '';
|
|
77
|
+
this.feedback = 'That display name is already in use.';
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
this.feedback = '';
|
|
81
|
+
return true;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
@action({ input: chatInputs.send })
|
|
85
|
+
send({ message }: ActionInput<typeof chatInputs.send>) {
|
|
86
|
+
return this.room.send(this, message);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
@action()
|
|
90
|
+
leave() {
|
|
91
|
+
this.room.leave(this);
|
|
92
|
+
this.displayName = '';
|
|
93
|
+
this.feedback = '';
|
|
94
|
+
this.messages = [];
|
|
95
|
+
this.members = [];
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
updateMessages(messages: readonly StoredMessage[]) { this.messages = messages; }
|
|
99
|
+
updatePresence(members: readonly string[]) { this.members = members; }
|
|
100
|
+
|
|
101
|
+
render() {
|
|
102
|
+
return <section class="chatroom">{this.displayName ? this.roomScreen() : this.joinScreen()}</section>;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
private joinScreen() {
|
|
106
|
+
return (
|
|
107
|
+
<section class="join-panel">
|
|
108
|
+
<p class="eyebrow">Live room</p>
|
|
109
|
+
<h1>Join the chatroom</h1>
|
|
110
|
+
<p>Choose a name once, then chat in realtime with everyone currently in the room.</p>
|
|
111
|
+
{this.feedback && <p class="form-error" role="alert">{this.feedback}</p>}
|
|
112
|
+
<form rw-submit="join" class="join-form">
|
|
113
|
+
<label for="display-name">Display name</label>
|
|
114
|
+
<div class="input-row">
|
|
115
|
+
<input id="display-name" name="name" maxlength="40" autocomplete="nickname" required autofocus />
|
|
116
|
+
<button type="submit">Join room</button>
|
|
117
|
+
</div>
|
|
118
|
+
</form>
|
|
119
|
+
</section>
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
private roomScreen() {
|
|
124
|
+
const remaining = this.members.length - MAX_VISIBLE_MEMBERS;
|
|
125
|
+
return (
|
|
126
|
+
<div class="room-layout">
|
|
127
|
+
<section class="conversation">
|
|
128
|
+
<header class="room-header">
|
|
129
|
+
<div><p class="eyebrow">Connected as</p><h1>{this.displayName}</h1></div>
|
|
130
|
+
<button type="button" class="quiet-button" rw-click="leave">Leave</button>
|
|
131
|
+
</header>
|
|
132
|
+
<ol class="message-list" aria-live="polite">
|
|
133
|
+
{this.messages.length ? this.messages.map(entry => (
|
|
134
|
+
<li key={entry.id}><strong>{entry.sender}</strong><p>{entry.text}</p></li>
|
|
135
|
+
)) : <li class="empty-message">No messages yet. Say hello.</li>}
|
|
136
|
+
</ol>
|
|
137
|
+
<form rw-submit="send" class="composer">
|
|
138
|
+
<label class="sr-only" for="chat-message">Message</label>
|
|
139
|
+
<input id="chat-message" name="message" maxlength="500" autocomplete="off" placeholder="Message the room…" required autofocus />
|
|
140
|
+
<button type="submit">Send</button>
|
|
141
|
+
</form>
|
|
142
|
+
</section>
|
|
143
|
+
<aside class="presence" aria-label="People in the room">
|
|
144
|
+
<p class="eyebrow">Online · {this.members.length}</p>
|
|
145
|
+
<ul>
|
|
146
|
+
{this.members.slice(0, MAX_VISIBLE_MEMBERS).map(member => <li key={member}>{member}</li>)}
|
|
147
|
+
{remaining > 0 && <li class="more-members">+{remaining} more</li>}
|
|
148
|
+
</ul>
|
|
149
|
+
</aside>
|
|
150
|
+
</div>
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export function createChatroomPage() {
|
|
156
|
+
const room = new ChatRoom();
|
|
157
|
+
|
|
158
|
+
@page('/', { css: 'chatroom.css' })
|
|
159
|
+
class ChatroomPage {
|
|
160
|
+
chat = new ChatroomComponent(room);
|
|
161
|
+
render() { return <main>{this.chat}</main>; }
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return ChatroomPage;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (require.main === module) start(createChatroomPage(), { port: 8080 });
|
|
@@ -67,7 +67,7 @@ let JsxPage = (() => {
|
|
|
67
67
|
this.count += 1;
|
|
68
68
|
}
|
|
69
69
|
render() {
|
|
70
|
-
return ((0, jsx_runtime_1.jsxs)("main", { class: "page-shell", children: [(0, jsx_runtime_1.jsx)("h1", { children: "Redweb JSX" }), (0, jsx_runtime_1.jsxs)(Card, { title: "Server rendered", children: [(0, jsx_runtime_1.jsx)("p", { children: "Plain TSX, escaped by default, with no browser framework." }), (0, jsx_runtime_1.jsxs)("button", { type: "button", "rw-click": "increment", children: ["Count ", (0, jsx_runtime_1.jsx)("output", {
|
|
70
|
+
return ((0, jsx_runtime_1.jsxs)("main", { class: "page-shell", children: [(0, jsx_runtime_1.jsx)("h1", { children: "Redweb JSX" }), (0, jsx_runtime_1.jsxs)(Card, { title: "Server rendered", children: [(0, jsx_runtime_1.jsx)("p", { children: "Plain TSX, escaped by default, with no browser framework." }), (0, jsx_runtime_1.jsxs)("button", { type: "button", "rw-click": "increment", children: ["Count ", (0, jsx_runtime_1.jsx)("output", { children: this.count })] })] })] }));
|
|
71
71
|
}
|
|
72
72
|
constructor() {
|
|
73
73
|
super(...arguments);
|
|
@@ -30,7 +30,7 @@ export class JsxPage extends LivePage {
|
|
|
30
30
|
<Card title="Server rendered">
|
|
31
31
|
<p>Plain TSX, escaped by default, with no browser framework.</p>
|
|
32
32
|
<button type="button" rw-click="increment">
|
|
33
|
-
Count <output
|
|
33
|
+
Count <output>{this.count}</output>
|
|
34
34
|
</button>
|
|
35
35
|
</Card>
|
|
36
36
|
</main>
|
|
@@ -1,13 +1,14 @@
|
|
|
1
|
-
{
|
|
2
|
-
"extends": "../../config/tsconfig.json",
|
|
3
|
-
"compilerOptions": {
|
|
4
|
-
"module": "CommonJS",
|
|
5
|
-
"moduleResolution": "Node",
|
|
6
|
-
"
|
|
1
|
+
{
|
|
2
|
+
"extends": "../../config/tsconfig.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"module": "CommonJS",
|
|
5
|
+
"moduleResolution": "Node",
|
|
6
|
+
"esModuleInterop": true,
|
|
7
|
+
"baseUrl": "../..",
|
|
7
8
|
"paths": {
|
|
8
9
|
"redweb": ["."],
|
|
9
10
|
"redweb/*": ["*"]
|
|
10
11
|
}
|
|
11
12
|
},
|
|
12
|
-
"files": ["counter.ts", "chatroom.
|
|
13
|
+
"files": ["counter.ts", "chatroom.tsx", "cards.ts", "components.ts", "jsx-page.tsx"]
|
|
13
14
|
}
|
package/index.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
declare module 'redweb' {
|
|
2
|
-
|
|
2
|
+
export { defineSocketContract } from 'redweb/contract';
|
|
3
|
+
export type { SocketContract, ContractClient, SocketSchema, ContractInput, ContractOutput, ContractMessage } from 'redweb/contract';
|
|
4
|
+
import { Application, RequestHandler } from 'express';
|
|
3
5
|
import { CorsOptions } from 'cors';
|
|
4
6
|
import { Server as NodeHttpServer } from 'http';
|
|
5
7
|
import { Server as NodeHttpsServer } from 'https';
|
|
@@ -15,7 +17,7 @@ declare module 'redweb' {
|
|
|
15
17
|
port?: number;
|
|
16
18
|
bind?: string;
|
|
17
19
|
publicPaths?: string[];
|
|
18
|
-
services?: Array<{ serviceName: string; method: string; function:
|
|
20
|
+
services?: Array<{ serviceName: string; method: string; function: RequestHandler }>;
|
|
19
21
|
listen?: boolean;
|
|
20
22
|
listenCallback?: () => void;
|
|
21
23
|
encoding?: RedWebEncoding;
|
|
@@ -35,6 +37,8 @@ declare module 'redweb' {
|
|
|
35
37
|
broadcast(data: unknown): number;
|
|
36
38
|
context?: RedWebConnectionContext;
|
|
37
39
|
joinRoom?(roomId: string): boolean;
|
|
40
|
+
/** Bounded permission check followed by atomic membership insertion. */
|
|
41
|
+
enterRoom?(roomId: string): Promise<boolean>;
|
|
38
42
|
leaveRoom?(roomId: string): boolean;
|
|
39
43
|
roomBroadcast?(roomId: string, data: unknown, options?: { except?: RedWebSocket }): number;
|
|
40
44
|
createSession?(sessionId: string, data: unknown): boolean;
|
|
@@ -45,13 +49,12 @@ declare module 'redweb' {
|
|
|
45
49
|
sendBinaryEvent?(value: unknown): Promise<boolean>;
|
|
46
50
|
};
|
|
47
51
|
|
|
48
|
-
export interface RedWebConnectionContext {
|
|
49
|
-
connectionId: string;
|
|
50
|
-
principal: unknown;
|
|
52
|
+
export interface RedWebConnectionContext extends RequestContext {
|
|
53
|
+
readonly connectionId: string;
|
|
54
|
+
readonly principal: unknown;
|
|
51
55
|
session: unknown | null;
|
|
52
56
|
metadata: Record<string, unknown>;
|
|
53
|
-
|
|
54
|
-
protocol?: Readonly<{ version: string }>;
|
|
57
|
+
readonly protocol?: Readonly<{ version: string }>;
|
|
55
58
|
}
|
|
56
59
|
|
|
57
60
|
export interface AdmissionContext {
|
|
@@ -98,12 +101,18 @@ declare module 'redweb' {
|
|
|
98
101
|
timeoutMs: number;
|
|
99
102
|
}
|
|
100
103
|
|
|
101
|
-
export
|
|
104
|
+
export type RoomOptions = {
|
|
102
105
|
maxRooms?: number;
|
|
103
106
|
maxMembersPerRoom?: number;
|
|
104
107
|
maxRoomsPerConnection?: number;
|
|
105
108
|
maxRoomIdLength?: number;
|
|
106
|
-
}
|
|
109
|
+
} & ({ authorize?: undefined; authorizationTimeoutMs?: never; maxPendingAuthorizations?: never; maxPendingPerConnection?: never } | {
|
|
110
|
+
/** Grants subscription until explicit leave/disconnect; not per-message receive authorization. */
|
|
111
|
+
authorize: (context: Readonly<RedWebConnectionContext>, roomId: string) => boolean | Promise<boolean>;
|
|
112
|
+
authorizationTimeoutMs?: number;
|
|
113
|
+
maxPendingAuthorizations?: number;
|
|
114
|
+
maxPendingPerConnection?: number;
|
|
115
|
+
});
|
|
107
116
|
|
|
108
117
|
export interface SessionOptions {
|
|
109
118
|
ttlMs?: number;
|
|
@@ -164,7 +173,7 @@ declare module 'redweb' {
|
|
|
164
173
|
}
|
|
165
174
|
|
|
166
175
|
export interface ProtocolOptions {
|
|
167
|
-
versions: string[];
|
|
176
|
+
versions: readonly string[];
|
|
168
177
|
required?: boolean;
|
|
169
178
|
queryParameter?: string;
|
|
170
179
|
header?: string;
|
|
@@ -173,7 +182,86 @@ declare module 'redweb' {
|
|
|
173
182
|
|
|
174
183
|
/** ─────────────────── SOCKET SERVER ─────────────────── */
|
|
175
184
|
|
|
185
|
+
export interface DevelopmentOptions {
|
|
186
|
+
/** Explicit local-only inspection; rejected when NODE_ENV is production. */
|
|
187
|
+
inspect?: boolean;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export interface LiveDevelopmentOptions extends DevelopmentOptions {
|
|
191
|
+
/** Loopback-only browser refresh. False overrides REDWEB_DEV_REFRESH=1. */
|
|
192
|
+
refresh?: boolean;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export interface InspectionList<T> {
|
|
196
|
+
readonly items: readonly T[];
|
|
197
|
+
readonly total: number;
|
|
198
|
+
readonly truncated: boolean;
|
|
199
|
+
}
|
|
200
|
+
export type InspectionSection<T> = { readonly available: false } | ({ readonly available: true } & T);
|
|
201
|
+
export interface InspectionMembers {
|
|
202
|
+
readonly className: string;
|
|
203
|
+
readonly actions: InspectionList<string>;
|
|
204
|
+
readonly states: InspectionList<string>;
|
|
205
|
+
}
|
|
206
|
+
export interface InspectionPage extends InspectionMembers {
|
|
207
|
+
readonly path: string;
|
|
208
|
+
readonly live: boolean;
|
|
209
|
+
readonly shared: boolean;
|
|
210
|
+
readonly instanceMetadata: 'observed' | 'unobserved';
|
|
211
|
+
readonly instances: InspectionList<{
|
|
212
|
+
readonly id: number;
|
|
213
|
+
readonly disposed: boolean;
|
|
214
|
+
readonly components: InspectionList<InspectionMembers & { readonly id: string }>;
|
|
215
|
+
}>;
|
|
216
|
+
}
|
|
217
|
+
export interface InspectionSession {
|
|
218
|
+
/** Inspector-local IDs; never page tokens, credentials or socket IDs. */
|
|
219
|
+
readonly render: number;
|
|
220
|
+
readonly instance: number;
|
|
221
|
+
readonly route: string;
|
|
222
|
+
readonly status: 'connected' | 'detaching' | 'pending' | 'retained';
|
|
223
|
+
readonly reactive: boolean;
|
|
224
|
+
}
|
|
225
|
+
export interface InspectionEvent {
|
|
226
|
+
readonly sequence: number;
|
|
227
|
+
readonly render: number;
|
|
228
|
+
readonly route: string;
|
|
229
|
+
readonly kind: 'state-invalidated' | 'flush-started' | 'flush-completed' | 'flush-superseded' | 'flush-failed';
|
|
230
|
+
readonly state?: string;
|
|
231
|
+
readonly component?: string;
|
|
232
|
+
readonly affectedOwners?: InspectionList<string>;
|
|
233
|
+
readonly snapshot?: boolean;
|
|
234
|
+
readonly dirtyOwners?: InspectionList<string>;
|
|
235
|
+
readonly durationMs?: number;
|
|
236
|
+
}
|
|
237
|
+
export interface DevelopmentSnapshot {
|
|
238
|
+
readonly schemaVersion: 1;
|
|
239
|
+
readonly mode: 'development';
|
|
240
|
+
readonly pages: InspectionSection<{
|
|
241
|
+
readonly registrations: InspectionList<InspectionPage>;
|
|
242
|
+
readonly sessions: InspectionList<InspectionSession>;
|
|
243
|
+
readonly closing?: boolean;
|
|
244
|
+
readonly rendering?: number;
|
|
245
|
+
readonly connections?: Readonly<Record<InspectionSession['status'], number>>;
|
|
246
|
+
}>;
|
|
247
|
+
readonly sockets: InspectionSection<{
|
|
248
|
+
readonly routes: InspectionList<{
|
|
249
|
+
readonly path: string;
|
|
250
|
+
readonly handlers: InspectionList<string>;
|
|
251
|
+
readonly registeredConnections: number;
|
|
252
|
+
readonly draining: boolean;
|
|
253
|
+
readonly rooms: number;
|
|
254
|
+
readonly sessions: number;
|
|
255
|
+
}>;
|
|
256
|
+
readonly pendingUpgrades: number;
|
|
257
|
+
readonly draining: boolean;
|
|
258
|
+
}>;
|
|
259
|
+
/** Flush completion is not a network delivery guarantee. */
|
|
260
|
+
readonly history: InspectionList<InspectionEvent> & { readonly limit: number };
|
|
261
|
+
}
|
|
262
|
+
|
|
176
263
|
export interface SocketServerOptions {
|
|
264
|
+
development?: DevelopmentOptions;
|
|
177
265
|
server?: NodeHttpServer;
|
|
178
266
|
port?: number;
|
|
179
267
|
bind?: string;
|
|
@@ -306,6 +394,7 @@ declare module 'redweb' {
|
|
|
306
394
|
|
|
307
395
|
addRoute(route: new () => SocketRoute): SocketRoute;
|
|
308
396
|
isReady(): boolean;
|
|
397
|
+
inspect(): DevelopmentSnapshot | null;
|
|
309
398
|
beginDrain(): boolean;
|
|
310
399
|
shutdown(): Promise<void>;
|
|
311
400
|
}
|
|
@@ -317,11 +406,13 @@ declare module 'redweb' {
|
|
|
317
406
|
export class RoomRegistry {
|
|
318
407
|
constructor(options?: RoomOptions);
|
|
319
408
|
join(roomId: string, socket: RedWebSocket): boolean;
|
|
409
|
+
enter(roomId: string, socket: RedWebSocket): Promise<boolean>;
|
|
320
410
|
leave(roomId: string, socket: RedWebSocket): boolean;
|
|
321
411
|
leaveAll(socket: RedWebSocket): number;
|
|
322
412
|
members(roomId: string): RedWebSocket[];
|
|
323
413
|
has(roomId: string, socket: RedWebSocket): boolean;
|
|
324
414
|
broadcast(roomId: string, data: unknown, options?: { except?: RedWebSocket }): number;
|
|
415
|
+
broadcastFrom(socket: RedWebSocket, roomId: string, data: unknown, options?: { except?: RedWebSocket }): number;
|
|
325
416
|
clear(): void;
|
|
326
417
|
close(): boolean;
|
|
327
418
|
readonly size: number;
|
|
@@ -421,30 +512,33 @@ declare module 'redweb' {
|
|
|
421
512
|
readonly [htmlUrlBrand]: true;
|
|
422
513
|
}
|
|
423
514
|
|
|
424
|
-
export interface
|
|
515
|
+
export interface RedWebRequest {
|
|
425
516
|
readonly path: string;
|
|
426
517
|
readonly url: string;
|
|
427
518
|
readonly method: string;
|
|
428
|
-
readonly headers:
|
|
519
|
+
readonly headers: Readonly<Record<string, string | readonly string[] | undefined>>;
|
|
429
520
|
readonly params: Readonly<Record<string, string>>;
|
|
430
521
|
readonly query: Readonly<Record<string, unknown>>;
|
|
431
522
|
readonly body: unknown;
|
|
432
523
|
get(name: string): string | undefined;
|
|
433
524
|
}
|
|
434
525
|
|
|
435
|
-
export interface
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
526
|
+
export interface LivePageRequest extends RedWebRequest {}
|
|
527
|
+
|
|
528
|
+
export interface RequestContext<Principal = unknown> {
|
|
529
|
+
readonly request: RedWebRequest;
|
|
530
|
+
readonly params: Readonly<Record<string, string>>;
|
|
531
|
+
readonly query: Readonly<Record<string, unknown>>;
|
|
532
|
+
readonly body: unknown;
|
|
533
|
+
readonly principal?: Principal;
|
|
534
|
+
readonly signal: AbortSignal;
|
|
442
535
|
}
|
|
443
536
|
|
|
444
|
-
export interface
|
|
537
|
+
export interface LivePageRequestContext extends RequestContext<string | number | bigint | boolean> {}
|
|
538
|
+
|
|
539
|
+
/** The original page request/identity is retained across normal reconnects. */
|
|
540
|
+
export interface LivePageConnectionContext extends LivePageRequestContext {
|
|
445
541
|
socket: RedWebSocket;
|
|
446
|
-
signal?: AbortSignal;
|
|
447
|
-
principal?: string | number | bigint | boolean;
|
|
448
542
|
}
|
|
449
543
|
|
|
450
544
|
export abstract class LivePage {
|
|
@@ -452,21 +546,27 @@ declare module 'redweb' {
|
|
|
452
546
|
loading?(context: LivePageRequestContext): void | Promise<void>;
|
|
453
547
|
render?(context: LivePageRequestContext): string | HtmlFragment | Promise<string | HtmlFragment>;
|
|
454
548
|
connected?(context: LivePageConnectionContext): void | Promise<void>;
|
|
455
|
-
disconnected?(context:
|
|
549
|
+
disconnected?(context: LivePageConnectionContext): void | Promise<void>;
|
|
456
550
|
disposed?(): void | Promise<void>;
|
|
457
551
|
dispose(): Promise<boolean>;
|
|
458
552
|
}
|
|
459
553
|
|
|
460
|
-
export
|
|
554
|
+
export type PageOptions = {
|
|
461
555
|
template?: string;
|
|
462
556
|
css?: string | readonly string[];
|
|
463
|
-
scope?: 'connection' | 'shared';
|
|
464
|
-
shared?: boolean;
|
|
465
557
|
live?: boolean;
|
|
466
558
|
head?: PageHead;
|
|
467
559
|
cache?: PageCache;
|
|
468
560
|
layout?: PageLayout;
|
|
469
|
-
}
|
|
561
|
+
} & ({
|
|
562
|
+
scope?: 'connection' | 'shared'; shared?: boolean;
|
|
563
|
+
authorize?: undefined; authorizationTimeoutMs?: never;
|
|
564
|
+
} | {
|
|
565
|
+
scope?: 'connection'; shared?: false;
|
|
566
|
+
/** Checked before construction/loading, on connection, and before actions/state writes. */
|
|
567
|
+
authorize: (context: LivePageRequestContext) => boolean | Promise<boolean>;
|
|
568
|
+
authorizationTimeoutMs?: number;
|
|
569
|
+
});
|
|
470
570
|
|
|
471
571
|
export type PageLayout = (content: HtmlFragment, context: LivePageRequestContext) => HtmlFragment;
|
|
472
572
|
|
|
@@ -502,6 +602,17 @@ declare module 'redweb' {
|
|
|
502
602
|
): Value;
|
|
503
603
|
}
|
|
504
604
|
|
|
605
|
+
/** The validated (possibly transformed) value passed as an action's first argument. */
|
|
606
|
+
export type ActionInput<Schema extends import('redweb/contract').SocketSchema> = import('redweb/contract').ContractOutput<Schema>;
|
|
607
|
+
|
|
608
|
+
export interface ValidatedActionDecorator<Input> {
|
|
609
|
+
<Value extends (input: Input, context: LivePageConnectionContext) => any>(target: object, propertyKey: string,
|
|
610
|
+
descriptor: TypedPropertyDescriptor<Value>): void;
|
|
611
|
+
<This, Value extends (this: This, input: Input, context: LivePageConnectionContext) => any>(
|
|
612
|
+
value: Value, context: ClassMethodDecoratorContext<This, Value>
|
|
613
|
+
): Value;
|
|
614
|
+
}
|
|
615
|
+
|
|
505
616
|
export interface LiveViewDecorator {
|
|
506
617
|
(target: object, propertyKey: string, descriptor: PropertyDescriptor): void | PropertyDescriptor;
|
|
507
618
|
<This, Value extends (this: This, item: any, index: number) => HtmlFragment>(
|
|
@@ -516,6 +627,18 @@ declare module 'redweb' {
|
|
|
516
627
|
(properties: Props) => HtmlFragment;
|
|
517
628
|
export function state(options?: StateOptions): LiveStateDecorator;
|
|
518
629
|
export function action(): LiveActionDecorator;
|
|
630
|
+
export interface ActionAuthorization<Input> {
|
|
631
|
+
authorize: (context: LivePageConnectionContext, input: Input) => boolean | Promise<boolean>;
|
|
632
|
+
/** Bounds permission checks, not application execution; defaults to 5000ms. */
|
|
633
|
+
authorizationTimeoutMs?: number;
|
|
634
|
+
}
|
|
635
|
+
export function action<Schema extends import('redweb/contract').SocketSchema>(options: {
|
|
636
|
+
input: Schema;
|
|
637
|
+
/** Bounds input validation, not application execution; defaults to 5000ms. */
|
|
638
|
+
validationTimeoutMs?: number;
|
|
639
|
+
} & (ActionAuthorization<ActionInput<Schema>> | { authorize?: undefined; authorizationTimeoutMs?: never })): ValidatedActionDecorator<ActionInput<Schema>>;
|
|
640
|
+
/** Authorized actions use a fixed (input, context) shape, including buttons without a payload. */
|
|
641
|
+
export function action(options: ActionAuthorization<unknown>): ValidatedActionDecorator<unknown>;
|
|
519
642
|
export function view(stateName: string): LiveViewDecorator;
|
|
520
643
|
export function html(strings: TemplateStringsArray, ...values: unknown[]): HtmlFragment;
|
|
521
644
|
export function attribute(value: string | number | bigint | boolean): HtmlAttribute;
|
|
@@ -529,7 +652,8 @@ declare module 'redweb' {
|
|
|
529
652
|
|
|
530
653
|
export type LivePageClass = new () => object;
|
|
531
654
|
|
|
532
|
-
export interface
|
|
655
|
+
export interface LiveHtmlServerBaseOptions extends Omit<RedWebOptions, 'enableHtmxRendering'> {
|
|
656
|
+
development?: LiveDevelopmentOptions;
|
|
533
657
|
pages: readonly LivePageClass[];
|
|
534
658
|
templateRoot?: string;
|
|
535
659
|
livePaths?: {
|
|
@@ -541,26 +665,38 @@ declare module 'redweb' {
|
|
|
541
665
|
sessionTtlMs?: number;
|
|
542
666
|
maxSessions?: number;
|
|
543
667
|
maxConcurrentRenders?: number;
|
|
668
|
+
/** Phase-local render/route and final owned-HTTP cleanup bound; defaults to 1000ms, not a total application deadline. */
|
|
544
669
|
shutdownTimeoutMs?: number;
|
|
545
670
|
heartbeat?: HeartbeatOptions;
|
|
546
|
-
authenticate?(request: import('http').IncomingMessage | import('express').Request):
|
|
547
|
-
string | number | bigint | boolean | false | null | undefined |
|
|
548
|
-
Promise<string | number | bigint | boolean | false | null | undefined>;
|
|
549
671
|
origins?: string[] | ((origin: string | undefined, request: import('http').IncomingMessage) => boolean | Promise<boolean>);
|
|
550
672
|
}
|
|
551
673
|
|
|
674
|
+
export type LiveHtmlAuthentication = {
|
|
675
|
+
authenticate(request: import('http').IncomingMessage | import('express').Request):
|
|
676
|
+
string | number | bigint | boolean | null | undefined |
|
|
677
|
+
Promise<string | number | bigint | boolean | null | undefined>;
|
|
678
|
+
/** Bounds identity lookup, not external application work; defaults to 5000ms. */
|
|
679
|
+
authenticationTimeoutMs?: number;
|
|
680
|
+
} | { authenticate?: undefined; authenticationTimeoutMs?: never };
|
|
681
|
+
export type LiveHtmlServerOptions = LiveHtmlServerBaseOptions & LiveHtmlAuthentication;
|
|
682
|
+
export type LiveHtmlStartOptions = Omit<LiveHtmlServerBaseOptions, 'pages'> & LiveHtmlAuthentication;
|
|
683
|
+
|
|
552
684
|
export class LiveHtmlServer {
|
|
553
685
|
app: Application;
|
|
554
686
|
server: NodeHttpServer | NodeHttpsServer;
|
|
555
687
|
http: HttpServer | HttpsServer;
|
|
556
688
|
sockets: SocketServer | null;
|
|
557
689
|
constructor(options: LiveHtmlServerOptions);
|
|
690
|
+
/** Revoke matching in-process sessions/renders; credential invalidation remains application-owned. */
|
|
691
|
+
revoke(principal: string | number | bigint | true): Promise<number>;
|
|
692
|
+
/** Local metadata only; null unless development inspection was explicitly enabled. */
|
|
693
|
+
inspect(): DevelopmentSnapshot | null;
|
|
558
694
|
shutdown(): Promise<void>;
|
|
559
695
|
}
|
|
560
696
|
|
|
561
697
|
export function start(
|
|
562
698
|
pageOrPages: LivePageClass | readonly LivePageClass[],
|
|
563
|
-
options?:
|
|
699
|
+
options?: LiveHtmlStartOptions
|
|
564
700
|
): LiveHtmlServer;
|
|
565
701
|
|
|
566
702
|
export interface StaticExportOptions {
|
|
@@ -587,9 +723,7 @@ declare module 'redweb' {
|
|
|
587
723
|
layout?: PageLayout;
|
|
588
724
|
}
|
|
589
725
|
|
|
590
|
-
export
|
|
591
|
-
live?: false;
|
|
592
|
-
}
|
|
726
|
+
export type SitePageOptions = PageOptions & { live?: false };
|
|
593
727
|
|
|
594
728
|
export interface SiteExportOptions extends StaticExportOptions {
|
|
595
729
|
publicDir?: string;
|
|
@@ -641,14 +775,5 @@ declare module 'redweb' {
|
|
|
641
775
|
export const HTTP_OPTIONS: RedWebOptions;
|
|
642
776
|
export const SOCKET_OPTIONS: SocketServerOptions;
|
|
643
777
|
|
|
644
|
-
export const ERROR_CODES:
|
|
645
|
-
INVALID_MESSAGE: 'INVALID_MESSAGE';
|
|
646
|
-
UNKNOWN_HANDLER: 'UNKNOWN_HANDLER';
|
|
647
|
-
HANDLER_FAILED: 'HANDLER_FAILED';
|
|
648
|
-
BINARY_UNSUPPORTED: 'BINARY_UNSUPPORTED';
|
|
649
|
-
RATE_LIMITED: 'RATE_LIMITED';
|
|
650
|
-
QUEUE_FULL: 'QUEUE_FULL';
|
|
651
|
-
CAPACITY_REACHED: 'CAPACITY_REACHED';
|
|
652
|
-
INITIALIZATION_FAILED: 'INITIALIZATION_FAILED';
|
|
653
|
-
}>;
|
|
778
|
+
export const ERROR_CODES: typeof import('redweb/client').ERROR_CODES;
|
|
654
779
|
}
|
package/index.js
CHANGED
|
@@ -14,6 +14,7 @@ const {
|
|
|
14
14
|
ERROR_CODES,
|
|
15
15
|
} = require('./src/ws');
|
|
16
16
|
const { BaseHandler } = require('./src/ws/BaseHandler');
|
|
17
|
+
const { defineSocketContract } = require('./contract');
|
|
17
18
|
const HttpServer = require('./src/http/HttpServer');
|
|
18
19
|
const HttpsServer = require('./src/http/HttpsServer');
|
|
19
20
|
const { action, attribute, codeBlock, component, defineSite, each, exportStatic, html, HtmlRenderer, LiveHtmlServer, LivePage, page, start, state, url, view } = require('./src/htmx');
|
|
@@ -24,6 +25,7 @@ module.exports = {
|
|
|
24
25
|
SocketServer,
|
|
25
26
|
SecureSocketServer,
|
|
26
27
|
BaseHandler,
|
|
28
|
+
defineSocketContract,
|
|
27
29
|
SocketRoute,
|
|
28
30
|
SocketService,
|
|
29
31
|
FixedStepService,
|
package/jsx-dev-runtime.js
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
const { Fragment, createElement } = require('./src/htmx/Jsx');
|
|
4
4
|
|
|
5
|
-
function jsxDEV(type, properties) {
|
|
6
|
-
return createElement(type, properties);
|
|
5
|
+
function jsxDEV(type, properties, key) {
|
|
6
|
+
return createElement(type, properties, key);
|
|
7
7
|
}
|
|
8
8
|
|
|
9
9
|
module.exports = { Fragment, jsxDEV };
|
package/jsx-runtime.d.ts
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
|
-
import type { HtmlFragment } from 'redweb';
|
|
1
|
+
import type { HtmlFragment, LivePageRequestContext } from 'redweb';
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
/** A synchronous @component() instance owned by a page/component field. Ownership is checked at runtime. */
|
|
4
|
+
export interface ServerComponent {
|
|
5
|
+
render(context: LivePageRequestContext): string | HtmlFragment | readonly HtmlFragment[];
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export type Child = HtmlFragment | ServerComponent | string | number | bigint | boolean | null | undefined | readonly Child[];
|
|
4
9
|
|
|
5
10
|
export interface IntrinsicAttributes {
|
|
6
11
|
key?: string | number;
|