redweb 0.11.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.
Files changed (167) hide show
  1. package/CHANGELOG.md +163 -0
  2. package/README.md +176 -508
  3. package/bin/redweb.js +11 -0
  4. package/client.d.ts +7 -2
  5. package/config/tsconfig.json +14 -0
  6. package/contract.d.ts +45 -0
  7. package/contract.js +5 -0
  8. package/docs/ACTION_INPUT_VERIFICATION.md +96 -0
  9. package/docs/ADMISSION_TIMEOUT_VERIFICATION.md +69 -0
  10. package/docs/AGENT_ACCESS.md +35 -0
  11. package/docs/AGENT_EVALUATION.md +58 -0
  12. package/docs/AGENT_READY_ACCEPTANCE.md +763 -0
  13. package/docs/APPLICATION_RECORDER_VERIFICATION.md +50 -0
  14. package/docs/BENCHMARK_VERIFICATION.md +307 -0
  15. package/docs/BROWSER_OWNER_VERIFICATION.md +191 -0
  16. package/docs/CLI.md +116 -0
  17. package/docs/CLIENT_DEVELOPMENT.md +152 -0
  18. package/docs/CLIENT_POLISH_VERIFICATION.md +282 -0
  19. package/docs/COVERAGE_COUNTER_VALIDATION.md +109 -0
  20. package/docs/COVERAGE_SCOPE_AUDIT.md +1183 -0
  21. package/docs/DEVELOPMENT.md +79 -0
  22. package/docs/DIAGNOSTIC_COMPATIBILITY.md +76 -0
  23. package/docs/DOCUMENTATION.md +37 -0
  24. package/docs/FEEDBACK_COMMAND_VERIFICATION.md +228 -0
  25. package/docs/GETTING_STARTED.md +58 -0
  26. package/docs/JSX_PERFORMANCE_VERIFICATION.md +59 -0
  27. package/docs/LIVE_HTML.md +169 -8
  28. package/docs/LIVE_HTML_LOAD_VERIFICATION.md +98 -0
  29. package/docs/MIGRATION.md +28 -0
  30. package/docs/MULTIPLAYER_OPERATIONS.md +26 -4
  31. package/docs/ORIGINAL_RECOVERY_VERIFICATION.md +100 -0
  32. package/docs/PACKAGED_EXAMPLE_VERIFICATION.md +126 -0
  33. package/docs/POLISH_RELEASE_CHECKPOINT.md +91 -0
  34. package/docs/PROCESS_CLEANUP_OBSERVATION.md +61 -0
  35. package/docs/PROCESS_REAPING_VERIFICATION.md +30 -0
  36. package/docs/PRODUCTION_READINESS.md +11 -2
  37. package/docs/RECOVERY_CLIENT_HEAP.md +201 -0
  38. package/docs/RECOVERY_CODE_ATTRIBUTION.md +174 -0
  39. package/docs/RECOVERY_CODE_CENSUS.md +158 -0
  40. package/docs/RECOVERY_COMPARISON.md +103 -0
  41. package/docs/RECOVERY_DEOPTIMIZATION.md +169 -0
  42. package/docs/RECOVERY_FOLLOWUP_SPIKE.md +147 -0
  43. package/docs/RECOVERY_INVESTIGATION.md +229 -0
  44. package/docs/RECOVERY_RUNTIME_CONTROLS.md +181 -0
  45. package/docs/RELEASE_TRUST.md +58 -0
  46. package/docs/ROOM_AUTHORIZATION.md +49 -0
  47. package/docs/RUNTIME_DIAGNOSTICS.md +78 -0
  48. package/docs/SERVER_RECOVERY_CANDIDATE.md +185 -0
  49. package/docs/SOAK_ROTATION_OBSERVATION.md +160 -0
  50. package/docs/SOAK_VERIFICATION.md +154 -0
  51. package/docs/SOCKET_CONTRACTS.md +39 -0
  52. package/docs/SPLIT_RECOVERY_COVERAGE.md +83 -0
  53. package/docs/SPLIT_RECOVERY_ERROR_HANDLING.md +67 -0
  54. package/docs/STARTER_COORDINATOR_VERIFICATION.md +112 -0
  55. package/docs/STARTER_LIFECYCLE_VERIFICATION.md +75 -0
  56. package/docs/STARTER_REPORT_RETENTION.md +73 -0
  57. package/docs/VERIFICATION_EVIDENCE.md +2 -0
  58. package/docs/generated.json +2154 -0
  59. package/docs/guides/chatroom.md +27 -0
  60. package/docs/guides/http-websocket.md +28 -0
  61. package/docs/guides/jsx-without-react.md +26 -0
  62. package/docs/guides/realtime-dashboard.md +29 -0
  63. package/docs/guides/typed-websockets.md +26 -0
  64. package/docs/reference.json +1207 -0
  65. package/docs/snippets/room-access.tsx +51 -0
  66. package/docs/topics.json +21 -0
  67. package/examples/live-html/chatroom.js +207 -268
  68. package/examples/live-html/chatroom.tsx +167 -0
  69. package/examples/live-html/jsx-page.js +1 -1
  70. package/examples/live-html/jsx-page.tsx +1 -1
  71. package/examples/live-html/tsconfig.json +3 -8
  72. package/index.d.ts +170 -45
  73. package/index.js +2 -0
  74. package/jsx-dev-runtime.js +2 -2
  75. package/jsx-runtime.d.ts +7 -2
  76. package/package.json +94 -7
  77. package/recipes/add/artifact.test.cjs +57 -0
  78. package/recipes/add/live.tsx +18 -0
  79. package/recipes/add/socket-route.ts +24 -0
  80. package/recipes/chat/README.md +22 -0
  81. package/recipes/chat/app.test.cjs +105 -0
  82. package/recipes/chat/app.tsx +9 -0
  83. package/recipes/dashboard/README.md +43 -0
  84. package/recipes/dashboard/admin.ts +21 -0
  85. package/recipes/dashboard/app.css +16 -0
  86. package/recipes/dashboard/app.test.cjs +450 -0
  87. package/recipes/dashboard/app.tsx +86 -0
  88. package/recipes/dashboard/auth.ts +80 -0
  89. package/recipes/dashboard/cards.tsx +102 -0
  90. package/recipes/dashboard/rate-window.test.cjs +17 -0
  91. package/recipes/dashboard/store.ts +120 -0
  92. package/recipes/http-ws/README.md +11 -0
  93. package/recipes/http-ws/app.test.cjs +92 -0
  94. package/recipes/http-ws/app.tsx +36 -0
  95. package/recipes/realtime/README.md +8 -0
  96. package/recipes/realtime/app.test.cjs +15 -0
  97. package/recipes/realtime/app.tsx +28 -0
  98. package/recipes/shared/README.md +40 -0
  99. package/recipes/shared/app.css +8 -0
  100. package/recipes/shared/copy-assets.cjs +8 -0
  101. package/recipes/shared/network.cjs +59 -0
  102. package/recipes/shared/run-app.test.cjs +158 -0
  103. package/recipes/shared/run-app.ts +50 -0
  104. package/recipes/site/README.md +4 -0
  105. package/recipes/site/app.test.cjs +19 -0
  106. package/recipes/site/app.tsx +25 -0
  107. package/recipes/socket/README.md +39 -0
  108. package/recipes/socket/app.test.cjs +85 -0
  109. package/recipes/socket/app.tsx +30 -0
  110. package/recipes/socket/contract.ts +12 -0
  111. package/recipes/socket/handlers.ts +40 -0
  112. package/src/OwnedServerLifecycle.js +66 -0
  113. package/src/access/AccessPolicy.js +37 -0
  114. package/src/access/AuthenticationFailure.js +13 -0
  115. package/src/access/RequestFailure.js +33 -0
  116. package/src/access/failure-codes.json +25 -0
  117. package/src/async/BoundedOperation.js +62 -0
  118. package/src/cli/ActionReferences.js +193 -0
  119. package/src/cli/AdditionLayout.js +140 -0
  120. package/src/cli/FilePlan.js +94 -0
  121. package/src/cli/ProjectAddition.js +60 -0
  122. package/src/cli/ProjectConfig.js +26 -0
  123. package/src/cli/ProjectDoctor.js +112 -0
  124. package/src/cli/ProjectInitializer.js +20 -0
  125. package/src/cli/SourceInspector.js +207 -0
  126. package/src/cli/StaticSource.js +192 -0
  127. package/src/cli/arguments.js +62 -0
  128. package/src/cli/formatCommand.js +10 -0
  129. package/src/cli/run.js +57 -0
  130. package/src/cli/templates.js +86 -0
  131. package/src/context/RequestSnapshot.js +41 -0
  132. package/src/dataProperty.js +11 -0
  133. package/src/development/DevelopmentPageManager.js +48 -0
  134. package/src/development/Inspection.js +104 -0
  135. package/src/development/ObservedRenderer.js +42 -0
  136. package/src/development/description.js +35 -0
  137. package/src/development/loopbackRequest.js +27 -0
  138. package/src/development/refreshBrowser.js +96 -0
  139. package/src/development/refreshStyles.js +9 -0
  140. package/src/development/settings.js +17 -0
  141. package/src/docs/Documentation.js +182 -0
  142. package/src/htmx/ActionDefinition.js +44 -0
  143. package/src/htmx/Jsx.js +24 -8
  144. package/src/htmx/LiveHtmlServer.js +41 -19
  145. package/src/htmx/LivePage.js +63 -13
  146. package/src/htmx/PageIdentity.js +32 -0
  147. package/src/htmx/PageLifetime.js +37 -0
  148. package/src/htmx/PageManager.js +203 -74
  149. package/src/htmx/ReactiveRenderer.js +241 -0
  150. package/src/htmx/StaticExporter.js +1 -1
  151. package/src/htmx/TemplateRenderer.js +13 -7
  152. package/src/htmx/browserRuntime.js +2 -93
  153. package/src/htmx/metadata.js +19 -7
  154. package/src/validation/ActionInputError.js +12 -0
  155. package/src/validation/SchemaValidator.js +38 -0
  156. package/src/ws/AdmissionPolicy.js +24 -23
  157. package/src/ws/BaseSocketServer.js +53 -38
  158. package/src/ws/ContractValidationError.js +12 -0
  159. package/src/ws/HeartbeatMonitor.js +19 -7
  160. package/src/ws/ProtocolPolicy.js +1 -1
  161. package/src/ws/RoomAccess.js +82 -0
  162. package/src/ws/RoomRegistry.js +56 -6
  163. package/src/ws/RouteRuntime.js +56 -10
  164. package/src/ws/SocketContract.js +112 -0
  165. package/src/ws/SocketRoute.js +18 -0
  166. package/src/ws/protocol-schema.json +6 -1
  167. package/examples/live-html/chatroom.ts +0 -217
@@ -0,0 +1,51 @@
1
+ import { randomBytes } from 'node:crypto';
2
+ import { page, start, BaseHandler, SocketRoute, RedWebSocket, RedWebRequest, LivePageRequestContext } from 'redweb';
3
+
4
+ // A runnable local demonstration, not a production credential store.
5
+ export function createApp(port = 8181) {
6
+ const token = randomBytes(32).toString('base64url');
7
+ let enabled = true;
8
+ const authenticate = (request: Pick<RedWebRequest, 'headers'>) =>
9
+ enabled && request.headers.authorization === `Bearer ${token}` ? 'alice' : false;
10
+
11
+ @page('/', { authorize: context => context.principal === 'alice' })
12
+ class Home {
13
+ render({ principal }: LivePageRequestContext) { return <main><h1>Private workspace</h1><p>{principal}</p></main>; }
14
+ }
15
+
16
+ class Join extends BaseHandler {
17
+ constructor() { super('join'); }
18
+ async onMessage(socket: RedWebSocket) {
19
+ socket.sendJson({ joined: await socket.enterRoom!('team'), principal: socket.context!.principal });
20
+ }
21
+ }
22
+ class Team extends SocketRoute {
23
+ constructor() {
24
+ super({ path: '/team', handlers: [Join], allowDuplicateConnections: true, logger: null,
25
+ admission: { authenticate },
26
+ rooms: { authorize: (context, roomId) => enabled && context.principal === 'alice' && roomId === 'team' },
27
+ });
28
+ }
29
+ }
30
+
31
+ const app = start(Home, { listen: false, authenticate, logger: null });
32
+ const team = app.sockets!.addRoute(Team);
33
+ app.server.listen(port, '127.0.0.1');
34
+ return {
35
+ app, team, token,
36
+ async revoke() {
37
+ enabled = false; // Invalidate credentials and future permissions first.
38
+ team.clients.forEach(socket => team.rooms!.leaveAll(socket));
39
+ await app.revoke('alice');
40
+ },
41
+ shutdown: () => app.shutdown(),
42
+ };
43
+ }
44
+
45
+ if (require.main === module) {
46
+ const demo = createApp();
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
+ process.once('SIGTERM', () => void demo.shutdown().catch(console.error));
50
+ process.once('SIGINT', () => void demo.shutdown().catch(console.error));
51
+ }
@@ -0,0 +1,21 @@
1
+ [
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
+ { "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" } },
4
+ { "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", "recipe": { "template": "site", "file": "src/app.tsx" } },
5
+ { "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" } },
6
+ { "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" } },
7
+ { "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" } },
8
+ { "id": "migration", "title": "Upgrade an existing Redweb application", "summary": "Historical socket defaults, asynchronous ownership, HTML migration and version-matched verification.", "source": "docs/MIGRATION.md" },
9
+ { "id": "cli", "title": "Initialization and diagnostics", "summary": "Noninteractive setup, safe existing-project adoption, and structured repair guidance.", "source": "docs/CLI.md" },
10
+ { "id": "development", "title": "Develop with browser refresh and safe inspection", "summary": "Loopback-only refresh with an explicit edit guard, plus bounded metadata and reactive update inspection without application values.", "source": "docs/DEVELOPMENT.md" },
11
+ { "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" },
12
+ { "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" },
13
+ { "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" },
14
+ { "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" },
15
+ { "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" },
16
+ { "id": "operations", "title": "Deploy and operate socket services", "summary": "Readiness, shutdown, capacity, reconnection, and distributed boundaries.", "source": "docs/MULTIPLAYER_OPERATIONS.md" },
17
+ { "id": "production-contract", "title": "Production guarantees and limits", "summary": "Resource ownership, delivery semantics, compatibility, and release gates.", "source": "docs/PRODUCTION_READINESS.md" },
18
+ { "id": "verification", "title": "Recorded verification evidence", "summary": "Historical measurements and their exact scope; not proof of a newer release.", "source": "docs/VERIFICATION_EVIDENCE.md" },
19
+ { "id": "release-status", "title": "Development release status", "summary": "Acceptance checklist, completed increments, and unfinished release gates.", "source": "docs/AGENT_READY_ACCEPTANCE.md" },
20
+ { "id": "release-trust", "title": "Choose and verify a release", "summary": "Maintained runtimes, compiler/browser verification boundaries, pinned packages, registry signatures, provenance and support limits.", "source": "docs/RELEASE_TRUST.md" }
21
+ ]
@@ -1,268 +1,207 @@
1
- "use strict";
2
- var __runInitializers = (this && this.__runInitializers) || function (thisArg, initializers, value) {
3
- var useValue = arguments.length > 2;
4
- for (var i = 0; i < initializers.length; i++) {
5
- value = useValue ? initializers[i].call(thisArg, value) : initializers[i].call(thisArg);
6
- }
7
- return useValue ? value : void 0;
8
- };
9
- var __esDecorate = (this && this.__esDecorate) || function (ctor, descriptorIn, decorators, contextIn, initializers, extraInitializers) {
10
- function accept(f) { if (f !== void 0 && typeof f !== "function") throw new TypeError("Function expected"); return f; }
11
- var kind = contextIn.kind, key = kind === "getter" ? "get" : kind === "setter" ? "set" : "value";
12
- var target = !descriptorIn && ctor ? contextIn["static"] ? ctor : ctor.prototype : null;
13
- var descriptor = descriptorIn || (target ? Object.getOwnPropertyDescriptor(target, contextIn.name) : {});
14
- var _, done = false;
15
- for (var i = decorators.length - 1; i >= 0; i--) {
16
- var context = {};
17
- for (var p in contextIn) context[p] = p === "access" ? {} : contextIn[p];
18
- for (var p in contextIn.access) context.access[p] = contextIn.access[p];
19
- context.addInitializer = function (f) { if (done) throw new TypeError("Cannot add initializers after decoration has completed"); extraInitializers.push(accept(f || null)); };
20
- var result = (0, decorators[i])(kind === "accessor" ? { get: descriptor.get, set: descriptor.set } : descriptor[key], context);
21
- if (kind === "accessor") {
22
- if (result === void 0) continue;
23
- if (result === null || typeof result !== "object") throw new TypeError("Object expected");
24
- if (_ = accept(result.get)) descriptor.get = _;
25
- if (_ = accept(result.set)) descriptor.set = _;
26
- if (_ = accept(result.init)) initializers.unshift(_);
27
- }
28
- else if (_ = accept(result)) {
29
- if (kind === "field") initializers.unshift(_);
30
- else descriptor[key] = _;
31
- }
32
- }
33
- if (target) Object.defineProperty(target, contextIn.name, descriptor);
34
- done = true;
35
- };
36
- Object.defineProperty(exports, "__esModule", { value: true });
37
- exports.ChatroomComponent = void 0;
38
- exports.createChatroomPage = createChatroomPage;
39
- const redweb_1 = require('../..');
40
- const MAX_VISIBLE_MEMBERS = 100;
41
- const UNSAFE_TEXT = /[\p{Cc}\p{Cf}]/u;
42
- function messageView(messages) {
43
- return messages.length
44
- ? (0, redweb_1.each)([...messages], entry => (0, redweb_1.html) `<li><strong>${entry.sender}</strong><p>${entry.text}</p></li>`)
45
- : (0, redweb_1.html) `<li class="empty-message">No messages yet. Say hello.</li>`;
46
- }
47
- function presenceView(members) {
48
- const visible = members.slice(0, MAX_VISIBLE_MEMBERS);
49
- const remaining = members.length - visible.length;
50
- return (0, redweb_1.html) `
51
- <p class="eyebrow">Online · ${members.length}</p>
52
- <ul>
53
- ${(0, redweb_1.each)([...visible], member => (0, redweb_1.html) `<li>${member}</li>`)}
54
- ${remaining ? (0, redweb_1.html) `<li class="more-members">+${remaining} more</li>` : (0, redweb_1.html) ``}
55
- </ul>
56
- `;
57
- }
58
- class ChatRoom {
59
- history = [];
60
- participants = new Set();
61
- online = new Set();
62
- join(participant) {
63
- const name = participant.displayName.toLocaleLowerCase();
64
- if ([...this.participants].some(member => member !== participant && member.displayName.toLocaleLowerCase() === name)) {
65
- return false;
66
- }
67
- this.participants.add(participant);
68
- this.online.add(participant);
69
- participant.updateMessages(messageView(this.history));
70
- this.publishPresence();
71
- return true;
72
- }
73
- disconnect(participant) {
74
- if (!this.online.delete(participant))
75
- return;
76
- this.publishPresence();
77
- }
78
- leave(participant) {
79
- this.online.delete(participant);
80
- if (!this.participants.delete(participant))
81
- return;
82
- this.publishPresence();
83
- }
84
- send(participant, text) {
85
- if (!this.online.has(participant))
86
- return false;
87
- this.history = [...this.history, { sender: participant.displayName, text }].slice(-100);
88
- const messages = messageView(this.history);
89
- for (const member of this.participants)
90
- member.updateMessages(messages);
91
- return true;
92
- }
93
- publishPresence() {
94
- const members = [...this.online].map(participant => participant.displayName);
95
- const presence = presenceView(members);
96
- for (const participant of this.participants)
97
- participant.updatePresence(presence);
98
- }
99
- }
100
- let ChatroomComponent = (() => {
101
- let _classDecorators = [(0, redweb_1.component)()];
102
- let _classDescriptor;
103
- let _classExtraInitializers = [];
104
- let _classThis;
105
- let _instanceExtraInitializers = [];
106
- let _screen_decorators;
107
- let _screen_initializers = [];
108
- let _screen_extraInitializers = [];
109
- let _messages_decorators;
110
- let _messages_initializers = [];
111
- let _messages_extraInitializers = [];
112
- let _presence_decorators;
113
- let _presence_initializers = [];
114
- let _presence_extraInitializers = [];
115
- let _join_decorators;
116
- let _send_decorators;
117
- let _leave_decorators;
118
- var ChatroomComponent = class {
119
- static { _classThis = this; }
120
- static {
121
- const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(null) : void 0;
122
- _screen_decorators = [(0, redweb_1.state)()];
123
- _messages_decorators = [(0, redweb_1.state)()];
124
- _presence_decorators = [(0, redweb_1.state)()];
125
- _join_decorators = [(0, redweb_1.action)()];
126
- _send_decorators = [(0, redweb_1.action)()];
127
- _leave_decorators = [(0, redweb_1.action)()];
128
- __esDecorate(this, null, _join_decorators, { kind: "method", name: "join", static: false, private: false, access: { has: obj => "join" in obj, get: obj => obj.join }, metadata: _metadata }, null, _instanceExtraInitializers);
129
- __esDecorate(this, null, _send_decorators, { kind: "method", name: "send", static: false, private: false, access: { has: obj => "send" in obj, get: obj => obj.send }, metadata: _metadata }, null, _instanceExtraInitializers);
130
- __esDecorate(this, null, _leave_decorators, { kind: "method", name: "leave", static: false, private: false, access: { has: obj => "leave" in obj, get: obj => obj.leave }, metadata: _metadata }, null, _instanceExtraInitializers);
131
- __esDecorate(null, null, _screen_decorators, { kind: "field", name: "screen", static: false, private: false, access: { has: obj => "screen" in obj, get: obj => obj.screen, set: (obj, value) => { obj.screen = value; } }, metadata: _metadata }, _screen_initializers, _screen_extraInitializers);
132
- __esDecorate(null, null, _messages_decorators, { kind: "field", name: "messages", static: false, private: false, access: { has: obj => "messages" in obj, get: obj => obj.messages, set: (obj, value) => { obj.messages = value; } }, metadata: _metadata }, _messages_initializers, _messages_extraInitializers);
133
- __esDecorate(null, null, _presence_decorators, { kind: "field", name: "presence", static: false, private: false, access: { has: obj => "presence" in obj, get: obj => obj.presence, set: (obj, value) => { obj.presence = value; } }, metadata: _metadata }, _presence_initializers, _presence_extraInitializers);
134
- __esDecorate(null, _classDescriptor = { value: _classThis }, _classDecorators, { kind: "class", name: _classThis.name, metadata: _metadata }, null, _classExtraInitializers);
135
- ChatroomComponent = _classThis = _classDescriptor.value;
136
- if (_metadata) Object.defineProperty(_classThis, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
137
- __runInitializers(_classThis, _classExtraInitializers);
138
- }
139
- room = __runInitializers(this, _instanceExtraInitializers);
140
- displayName = '';
141
- screen = __runInitializers(this, _screen_initializers, this.joinScreen());
142
- messages = (__runInitializers(this, _screen_extraInitializers), __runInitializers(this, _messages_initializers, messageView([])));
143
- presence = (__runInitializers(this, _messages_extraInitializers), __runInitializers(this, _presence_initializers, presenceView([])));
144
- constructor(room) {
145
- __runInitializers(this, _presence_extraInitializers);
146
- this.room = room;
147
- }
148
- connected() {
149
- if (this.displayName)
150
- this.room.join(this);
151
- }
152
- disconnected() {
153
- this.room.disconnect(this);
154
- }
155
- disposed() {
156
- this.room.leave(this);
157
- }
158
- join({ name }) {
159
- if (this.displayName)
160
- return false;
161
- if (typeof name !== 'string') {
162
- this.screen = this.joinScreen('Display name must be text.');
163
- return false;
164
- }
165
- const displayName = name.normalize('NFKC').trim();
166
- if (!displayName || displayName.length > 40 || UNSAFE_TEXT.test(displayName)) {
167
- this.screen = this.joinScreen('Choose a visible display name of at most 40 characters.');
168
- return false;
169
- }
170
- this.displayName = displayName;
171
- if (this.room.join(this)) {
172
- this.screen = this.roomScreen();
173
- return true;
174
- }
175
- this.displayName = '';
176
- this.screen = this.joinScreen('That display name is already in use.');
177
- return false;
178
- }
179
- send({ message }) {
180
- if (typeof message !== 'string')
181
- return false;
182
- const text = message.normalize('NFKC').trim();
183
- if (!text || text.length > 500 || UNSAFE_TEXT.test(text))
184
- return false;
185
- return this.room.send(this, text);
186
- }
187
- leave() {
188
- this.room.leave(this);
189
- this.displayName = '';
190
- this.screen = this.joinScreen();
191
- }
192
- updateMessages(messages) {
193
- this.messages = messages;
194
- }
195
- updatePresence(presence) {
196
- this.presence = presence;
197
- }
198
- render() {
199
- return (0, redweb_1.html) `<section class="chatroom" data-rw-state="screen">${this.screen}</section>`;
200
- }
201
- joinScreen(error = '') {
202
- const feedback = error ? (0, redweb_1.html) `<p class="form-error" role="alert">${error}</p>` : (0, redweb_1.html) ``;
203
- return (0, redweb_1.html) `
204
- <section class="join-panel">
205
- <p class="eyebrow">Live room</p>
206
- <h1>Join the chatroom</h1>
207
- <p>Choose a name once, then chat in realtime with everyone currently in the room.</p>
208
- ${feedback}
209
- <form rw-submit="join" class="join-form">
210
- <label for="display-name">Display name</label>
211
- <div class="input-row">
212
- <input id="display-name" name="name" maxlength="40" autocomplete="nickname" required autofocus>
213
- <button type="submit">Join room</button>
214
- </div>
215
- </form>
216
- </section>
217
- `;
218
- }
219
- roomScreen() {
220
- return (0, redweb_1.html) `
221
- <div class="room-layout">
222
- <section class="conversation">
223
- <header class="room-header">
224
- <div><p class="eyebrow">Connected as</p><h1>${this.displayName}</h1></div>
225
- <button type="button" class="quiet-button" rw-click="leave">Leave</button>
226
- </header>
227
- <ol class="message-list" aria-live="polite" data-rw-state="messages">${this.messages}</ol>
228
- <form rw-submit="send" class="composer">
229
- <label class="sr-only" for="chat-message">Message</label>
230
- <input id="chat-message" name="message" maxlength="500" autocomplete="off" placeholder="Message the room…" required autofocus>
231
- <button type="submit">Send</button>
232
- </form>
233
- </section>
234
- <aside class="presence" aria-label="People in the room" data-rw-state="presence">${this.presence}</aside>
235
- </div>
236
- `;
237
- }
238
- };
239
- return ChatroomComponent = _classThis;
240
- })();
241
- exports.ChatroomComponent = ChatroomComponent;
242
- function createChatroomPage() {
243
- const room = new ChatRoom();
244
- let ChatroomPage = (() => {
245
- let _classDecorators = [(0, redweb_1.page)('/', { css: 'chatroom.css' })];
246
- let _classDescriptor;
247
- let _classExtraInitializers = [];
248
- let _classThis;
249
- var ChatroomPage = class {
250
- static { _classThis = this; }
251
- static {
252
- const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(null) : void 0;
253
- __esDecorate(null, _classDescriptor = { value: _classThis }, _classDecorators, { kind: "class", name: _classThis.name, metadata: _metadata }, null, _classExtraInitializers);
254
- ChatroomPage = _classThis = _classDescriptor.value;
255
- if (_metadata) Object.defineProperty(_classThis, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
256
- __runInitializers(_classThis, _classExtraInitializers);
257
- }
258
- chat = new ChatroomComponent(room);
259
- render() {
260
- return (0, redweb_1.html) `<main>${this.chat}</main>`;
261
- }
262
- };
263
- return ChatroomPage = _classThis;
264
- })();
265
- return ChatroomPage;
266
- }
267
- if (require.main === module)
268
- (0, redweb_1.start)(createChatroomPage(), { port: 8080 });
1
+ "use strict";
2
+ var __runInitializers = (this && this.__runInitializers) || function (thisArg, initializers, value) {
3
+ var useValue = arguments.length > 2;
4
+ for (var i = 0; i < initializers.length; i++) {
5
+ value = useValue ? initializers[i].call(thisArg, value) : initializers[i].call(thisArg);
6
+ }
7
+ return useValue ? value : void 0;
8
+ };
9
+ var __esDecorate = (this && this.__esDecorate) || function (ctor, descriptorIn, decorators, contextIn, initializers, extraInitializers) {
10
+ function accept(f) { if (f !== void 0 && typeof f !== "function") throw new TypeError("Function expected"); return f; }
11
+ var kind = contextIn.kind, key = kind === "getter" ? "get" : kind === "setter" ? "set" : "value";
12
+ var target = !descriptorIn && ctor ? contextIn["static"] ? ctor : ctor.prototype : null;
13
+ var descriptor = descriptorIn || (target ? Object.getOwnPropertyDescriptor(target, contextIn.name) : {});
14
+ var _, done = false;
15
+ for (var i = decorators.length - 1; i >= 0; i--) {
16
+ var context = {};
17
+ for (var p in contextIn) context[p] = p === "access" ? {} : contextIn[p];
18
+ for (var p in contextIn.access) context.access[p] = contextIn.access[p];
19
+ context.addInitializer = function (f) { if (done) throw new TypeError("Cannot add initializers after decoration has completed"); extraInitializers.push(accept(f || null)); };
20
+ var result = (0, decorators[i])(kind === "accessor" ? { get: descriptor.get, set: descriptor.set } : descriptor[key], context);
21
+ if (kind === "accessor") {
22
+ if (result === void 0) continue;
23
+ if (result === null || typeof result !== "object") throw new TypeError("Object expected");
24
+ if (_ = accept(result.get)) descriptor.get = _;
25
+ if (_ = accept(result.set)) descriptor.set = _;
26
+ if (_ = accept(result.init)) initializers.unshift(_);
27
+ }
28
+ else if (_ = accept(result)) {
29
+ if (kind === "field") initializers.unshift(_);
30
+ else descriptor[key] = _;
31
+ }
32
+ }
33
+ if (target) Object.defineProperty(target, contextIn.name, descriptor);
34
+ done = true;
35
+ };
36
+ Object.defineProperty(exports, "__esModule", { value: true });
37
+ exports.ChatroomComponent = exports.chatInputs = void 0;
38
+ exports.createChatroomPage = createChatroomPage;
39
+ const jsx_runtime_1 = require('../../jsx-runtime');
40
+ const redweb_1 = require('../..');
41
+ const zod_1 = require("zod");
42
+ const MAX_VISIBLE_MEMBERS = 100;
43
+ const visibleText = (maximum) => zod_1.z.string()
44
+ .transform(value => value.normalize('NFKC').trim())
45
+ .pipe(zod_1.z.string().min(1).max(maximum).regex(/^[^\p{Cc}\p{Cf}]+$/u));
46
+ exports.chatInputs = {
47
+ join: zod_1.z.object({ name: visibleText(40) }).strict(),
48
+ send: zod_1.z.object({ message: visibleText(500) }).strict(),
49
+ };
50
+ class ChatRoom {
51
+ history = [];
52
+ nextMessageId = 0;
53
+ participants = new Set();
54
+ online = new Set();
55
+ join(participant) {
56
+ const name = participant.displayName.toLocaleLowerCase();
57
+ if ([...this.participants].some(member => member !== participant && member.displayName.toLocaleLowerCase() === name))
58
+ return false;
59
+ this.participants.add(participant);
60
+ this.online.add(participant);
61
+ participant.updateMessages(this.history);
62
+ this.publishPresence();
63
+ return true;
64
+ }
65
+ disconnect(participant) {
66
+ if (this.online.delete(participant))
67
+ this.publishPresence();
68
+ }
69
+ leave(participant) {
70
+ this.online.delete(participant);
71
+ if (this.participants.delete(participant))
72
+ this.publishPresence();
73
+ }
74
+ send(participant, text) {
75
+ if (!this.online.has(participant))
76
+ return false;
77
+ this.history = [...this.history, { id: ++this.nextMessageId, sender: participant.displayName, text }].slice(-100);
78
+ for (const member of this.participants)
79
+ member.updateMessages(this.history);
80
+ return true;
81
+ }
82
+ publishPresence() {
83
+ const members = [...this.online].map(participant => participant.displayName);
84
+ for (const participant of this.participants)
85
+ participant.updatePresence(members);
86
+ }
87
+ }
88
+ let ChatroomComponent = (() => {
89
+ let _classDecorators = [(0, redweb_1.component)()];
90
+ let _classDescriptor;
91
+ let _classExtraInitializers = [];
92
+ let _classThis;
93
+ let _instanceExtraInitializers = [];
94
+ let _displayName_decorators;
95
+ let _displayName_initializers = [];
96
+ let _displayName_extraInitializers = [];
97
+ let _feedback_decorators;
98
+ let _feedback_initializers = [];
99
+ let _feedback_extraInitializers = [];
100
+ let _messages_decorators;
101
+ let _messages_initializers = [];
102
+ let _messages_extraInitializers = [];
103
+ let _members_decorators;
104
+ let _members_initializers = [];
105
+ let _members_extraInitializers = [];
106
+ let _join_decorators;
107
+ let _send_decorators;
108
+ let _leave_decorators;
109
+ var ChatroomComponent = class {
110
+ static { _classThis = this; }
111
+ static {
112
+ const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(null) : void 0;
113
+ _displayName_decorators = [(0, redweb_1.state)()];
114
+ _feedback_decorators = [(0, redweb_1.state)()];
115
+ _messages_decorators = [(0, redweb_1.state)()];
116
+ _members_decorators = [(0, redweb_1.state)()];
117
+ _join_decorators = [(0, redweb_1.action)({ input: exports.chatInputs.join })];
118
+ _send_decorators = [(0, redweb_1.action)({ input: exports.chatInputs.send })];
119
+ _leave_decorators = [(0, redweb_1.action)()];
120
+ __esDecorate(this, null, _join_decorators, { kind: "method", name: "join", static: false, private: false, access: { has: obj => "join" in obj, get: obj => obj.join }, metadata: _metadata }, null, _instanceExtraInitializers);
121
+ __esDecorate(this, null, _send_decorators, { kind: "method", name: "send", static: false, private: false, access: { has: obj => "send" in obj, get: obj => obj.send }, metadata: _metadata }, null, _instanceExtraInitializers);
122
+ __esDecorate(this, null, _leave_decorators, { kind: "method", name: "leave", static: false, private: false, access: { has: obj => "leave" in obj, get: obj => obj.leave }, metadata: _metadata }, null, _instanceExtraInitializers);
123
+ __esDecorate(null, null, _displayName_decorators, { kind: "field", name: "displayName", static: false, private: false, access: { has: obj => "displayName" in obj, get: obj => obj.displayName, set: (obj, value) => { obj.displayName = value; } }, metadata: _metadata }, _displayName_initializers, _displayName_extraInitializers);
124
+ __esDecorate(null, null, _feedback_decorators, { kind: "field", name: "feedback", static: false, private: false, access: { has: obj => "feedback" in obj, get: obj => obj.feedback, set: (obj, value) => { obj.feedback = value; } }, metadata: _metadata }, _feedback_initializers, _feedback_extraInitializers);
125
+ __esDecorate(null, null, _messages_decorators, { kind: "field", name: "messages", static: false, private: false, access: { has: obj => "messages" in obj, get: obj => obj.messages, set: (obj, value) => { obj.messages = value; } }, metadata: _metadata }, _messages_initializers, _messages_extraInitializers);
126
+ __esDecorate(null, null, _members_decorators, { kind: "field", name: "members", static: false, private: false, access: { has: obj => "members" in obj, get: obj => obj.members, set: (obj, value) => { obj.members = value; } }, metadata: _metadata }, _members_initializers, _members_extraInitializers);
127
+ __esDecorate(null, _classDescriptor = { value: _classThis }, _classDecorators, { kind: "class", name: _classThis.name, metadata: _metadata }, null, _classExtraInitializers);
128
+ ChatroomComponent = _classThis = _classDescriptor.value;
129
+ if (_metadata) Object.defineProperty(_classThis, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
130
+ __runInitializers(_classThis, _classExtraInitializers);
131
+ }
132
+ room = __runInitializers(this, _instanceExtraInitializers);
133
+ displayName = __runInitializers(this, _displayName_initializers, '');
134
+ feedback = (__runInitializers(this, _displayName_extraInitializers), __runInitializers(this, _feedback_initializers, ''));
135
+ messages = (__runInitializers(this, _feedback_extraInitializers), __runInitializers(this, _messages_initializers, []));
136
+ members = (__runInitializers(this, _messages_extraInitializers), __runInitializers(this, _members_initializers, []));
137
+ constructor(room) {
138
+ __runInitializers(this, _members_extraInitializers);
139
+ this.room = room;
140
+ }
141
+ connected() { if (this.displayName)
142
+ this.room.join(this); }
143
+ disconnected() { this.room.disconnect(this); }
144
+ disposed() { this.room.leave(this); }
145
+ join({ name }) {
146
+ if (this.displayName)
147
+ return false;
148
+ this.displayName = name;
149
+ if (!this.room.join(this)) {
150
+ this.displayName = '';
151
+ this.feedback = 'That display name is already in use.';
152
+ return false;
153
+ }
154
+ this.feedback = '';
155
+ return true;
156
+ }
157
+ send({ message }) {
158
+ return this.room.send(this, message);
159
+ }
160
+ leave() {
161
+ this.room.leave(this);
162
+ this.displayName = '';
163
+ this.feedback = '';
164
+ this.messages = [];
165
+ this.members = [];
166
+ }
167
+ updateMessages(messages) { this.messages = messages; }
168
+ updatePresence(members) { this.members = members; }
169
+ render() {
170
+ return (0, jsx_runtime_1.jsx)("section", { class: "chatroom", children: this.displayName ? this.roomScreen() : this.joinScreen() });
171
+ }
172
+ joinScreen() {
173
+ return ((0, jsx_runtime_1.jsxs)("section", { class: "join-panel", children: [(0, jsx_runtime_1.jsx)("p", { class: "eyebrow", children: "Live room" }), (0, jsx_runtime_1.jsx)("h1", { children: "Join the chatroom" }), (0, jsx_runtime_1.jsx)("p", { children: "Choose a name once, then chat in realtime with everyone currently in the room." }), this.feedback && (0, jsx_runtime_1.jsx)("p", { class: "form-error", role: "alert", children: this.feedback }), (0, jsx_runtime_1.jsxs)("form", { "rw-submit": "join", class: "join-form", children: [(0, jsx_runtime_1.jsx)("label", { for: "display-name", children: "Display name" }), (0, jsx_runtime_1.jsxs)("div", { class: "input-row", children: [(0, jsx_runtime_1.jsx)("input", { id: "display-name", name: "name", maxlength: "40", autocomplete: "nickname", required: true, autofocus: true }), (0, jsx_runtime_1.jsx)("button", { type: "submit", children: "Join room" })] })] })] }));
174
+ }
175
+ roomScreen() {
176
+ const remaining = this.members.length - MAX_VISIBLE_MEMBERS;
177
+ return ((0, jsx_runtime_1.jsxs)("div", { class: "room-layout", children: [(0, jsx_runtime_1.jsxs)("section", { class: "conversation", children: [(0, jsx_runtime_1.jsxs)("header", { class: "room-header", children: [(0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("p", { class: "eyebrow", children: "Connected as" }), (0, jsx_runtime_1.jsx)("h1", { children: this.displayName })] }), (0, jsx_runtime_1.jsx)("button", { type: "button", class: "quiet-button", "rw-click": "leave", children: "Leave" })] }), (0, jsx_runtime_1.jsx)("ol", { class: "message-list", "aria-live": "polite", children: this.messages.length ? this.messages.map(entry => ((0, jsx_runtime_1.jsxs)("li", { children: [(0, jsx_runtime_1.jsx)("strong", { children: entry.sender }), (0, jsx_runtime_1.jsx)("p", { children: entry.text })] }, entry.id))) : (0, jsx_runtime_1.jsx)("li", { class: "empty-message", children: "No messages yet. Say hello." }) }), (0, jsx_runtime_1.jsxs)("form", { "rw-submit": "send", class: "composer", children: [(0, jsx_runtime_1.jsx)("label", { class: "sr-only", for: "chat-message", children: "Message" }), (0, jsx_runtime_1.jsx)("input", { id: "chat-message", name: "message", maxlength: "500", autocomplete: "off", placeholder: "Message the room\u2026", required: true, autofocus: true }), (0, jsx_runtime_1.jsx)("button", { type: "submit", children: "Send" })] })] }), (0, jsx_runtime_1.jsxs)("aside", { class: "presence", "aria-label": "People in the room", children: [(0, jsx_runtime_1.jsxs)("p", { class: "eyebrow", children: ["Online \u00B7 ", this.members.length] }), (0, jsx_runtime_1.jsxs)("ul", { children: [this.members.slice(0, MAX_VISIBLE_MEMBERS).map(member => (0, jsx_runtime_1.jsx)("li", { children: member }, member)), remaining > 0 && (0, jsx_runtime_1.jsxs)("li", { class: "more-members", children: ["+", remaining, " more"] })] })] })] }));
178
+ }
179
+ };
180
+ return ChatroomComponent = _classThis;
181
+ })();
182
+ exports.ChatroomComponent = ChatroomComponent;
183
+ function createChatroomPage() {
184
+ const room = new ChatRoom();
185
+ let ChatroomPage = (() => {
186
+ let _classDecorators = [(0, redweb_1.page)('/', { css: 'chatroom.css' })];
187
+ let _classDescriptor;
188
+ let _classExtraInitializers = [];
189
+ let _classThis;
190
+ var ChatroomPage = class {
191
+ static { _classThis = this; }
192
+ static {
193
+ const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(null) : void 0;
194
+ __esDecorate(null, _classDescriptor = { value: _classThis }, _classDecorators, { kind: "class", name: _classThis.name, metadata: _metadata }, null, _classExtraInitializers);
195
+ ChatroomPage = _classThis = _classDescriptor.value;
196
+ if (_metadata) Object.defineProperty(_classThis, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
197
+ __runInitializers(_classThis, _classExtraInitializers);
198
+ }
199
+ chat = new ChatroomComponent(room);
200
+ render() { return (0, jsx_runtime_1.jsx)("main", { children: this.chat }); }
201
+ };
202
+ return ChatroomPage = _classThis;
203
+ })();
204
+ return ChatroomPage;
205
+ }
206
+ if (require.main === module)
207
+ (0, redweb_1.start)(createChatroomPage(), { port: 8080 });