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,112 @@
1
+ 'use strict';
2
+
3
+ const { BaseHandler } = require('./BaseHandler');
4
+ const { ProtocolClient } = require('../../client');
5
+ const { ContractValidationError, InboundContractValidationError } = require('./ContractValidationError');
6
+ const { SchemaValidator } = require('../validation/SchemaValidator');
7
+
8
+ function boundedName(value, label) {
9
+ if (typeof value !== 'string' || !value || value.length > 256) throw new TypeError(`${label} must be a non-empty string of at most 256 characters.`);
10
+ return value;
11
+ }
12
+
13
+ async function validatedWirePayload(contract, type, payload) {
14
+ let serialized, input;
15
+ try {
16
+ serialized = JSON.stringify(payload);
17
+ input = JSON.parse(serialized);
18
+ }
19
+ catch { throw new ContractValidationError(); }
20
+ await contract.parse(type, input);
21
+ // A validator may mutate its argument. Always send the original input snapshot.
22
+ return JSON.parse(serialized);
23
+ }
24
+
25
+ class ContractClient {
26
+ constructor(contract, socket) {
27
+ this.contract = contract;
28
+ this.protocol = new ProtocolClient(socket, contract.version);
29
+ }
30
+
31
+ async envelope(type, payload, metadata) {
32
+ const input = await validatedWirePayload(this.contract, type, payload);
33
+ // Validate the input, but keep its wire representation. Receiver-side transforms run exactly once.
34
+ return this.protocol.envelope(type, input, metadata);
35
+ }
36
+
37
+ async send(type, payload, metadata) {
38
+ this.protocol.socket.send(JSON.stringify(await this.envelope(type, payload, metadata)));
39
+ }
40
+
41
+ async parse(input) {
42
+ const message = this.protocol.parse(input);
43
+ if (message.type === 'error') return message;
44
+ return { ...message, payload: await this.contract.parse(message.type, message.payload) };
45
+ }
46
+ }
47
+
48
+ class SocketContract {
49
+ #validators;
50
+
51
+ constructor(version, schemas, options) {
52
+ boundedName(version, 'Contract version');
53
+ if (version.length > 64) throw new TypeError('Contract version must be at most 64 characters.');
54
+ if (!schemas || typeof schemas !== 'object' || Array.isArray(schemas)) throw new TypeError('Contract schemas must be an object.');
55
+ if (!options || typeof options !== 'object' || Array.isArray(options)) throw new TypeError('Contract options must be an object.');
56
+ const timeout = options.validationTimeoutMs ?? 5000;
57
+ const entries = Object.entries(schemas);
58
+ if (!entries.length || entries.length > 256) throw new TypeError('A socket contract must define between 1 and 256 message types.');
59
+ this.#validators = new Map(entries.map(([type, schema]) => {
60
+ boundedName(type, 'Contract message type');
61
+ if (type === 'error') throw new TypeError('The error message type is reserved by the protocol.');
62
+ return [type, new SchemaValidator(schema, timeout)];
63
+ }));
64
+ this.version = version;
65
+ this.validationTimeoutMs = timeout;
66
+ this.types = Object.freeze(entries.map(([type]) => type));
67
+ this.protocol = Object.freeze({ versions: Object.freeze([version]) });
68
+ Object.freeze(this);
69
+ }
70
+
71
+ async parse(type, payload) {
72
+ const validate = this.#validators.get(type);
73
+ if (!validate) throw new ContractValidationError('UNKNOWN_HANDLER');
74
+ try {
75
+ return await validate.parse(payload);
76
+ } catch {
77
+ // Validator details can contain application data. Never expose them as protocol errors.
78
+ throw new ContractValidationError();
79
+ }
80
+ }
81
+
82
+ handler(type, callback) {
83
+ if (!this.#validators.has(type)) throw new TypeError('Handler message type is not defined in the contract.');
84
+ if (typeof callback !== 'function') throw new TypeError('A contract handler requires a callback.');
85
+ const contract = this;
86
+ return class ContractHandler extends BaseHandler {
87
+ constructor() { super(type); }
88
+ async handleMessage(socket, message) {
89
+ if (socket.context?.protocol?.version !== contract.version) throw new TypeError('The route must negotiate this contract version before handling messages.');
90
+ let payload;
91
+ try { payload = await contract.parse(type, message.payload); }
92
+ catch { throw new InboundContractValidationError(); }
93
+ return super.handleMessage(socket, { ...message, payload });
94
+ }
95
+ onMessage(socket, message) { return callback(socket, message.payload, message); }
96
+ };
97
+ }
98
+
99
+ client(socket) { return new ContractClient(this, socket); }
100
+
101
+ async send(socket, type, payload, metadata) {
102
+ if (socket.context?.protocol?.version !== this.version) throw new TypeError('The socket must negotiate this contract version before sending events.');
103
+ const input = await validatedWirePayload(this, type, payload);
104
+ return socket.sendEvent(type, input, metadata);
105
+ }
106
+ }
107
+
108
+ function defineSocketContract(version, schemas, options = {}) {
109
+ return new SocketContract(version, schemas, options);
110
+ }
111
+
112
+ module.exports = { defineSocketContract };
@@ -8,6 +8,8 @@ const TransportPolicy = require('./TransportPolicy');
8
8
  const Metrics = require('./Metrics');
9
9
  const RouteRuntime = require('./RouteRuntime');
10
10
  const { ProtocolPolicy, ERROR_CODES } = require('./ProtocolPolicy');
11
+ const { InboundContractValidationError } = require('./ContractValidationError');
12
+ const { RequestFailure } = require('../access/RequestFailure');
11
13
 
12
14
  function errorMessage(error) {
13
15
  return error instanceof Error ? error.message : String(error);
@@ -272,6 +274,7 @@ class SocketRoute {
272
274
  if (!this.allowDuplicateConnections) {
273
275
  const existing = this.clients.get(clientKey);
274
276
  if (existing) {
277
+ this.runtime.detach(existing);
275
278
  this.logger.warn?.(`Client ${ip} already connected, disconnecting existing connection.`);
276
279
  if (existing.sendEvent) {
277
280
  existing.sendEvent('system.disconnect', { reason: 'replaced' });
@@ -344,6 +347,14 @@ class SocketRoute {
344
347
  );
345
348
  }
346
349
 
350
+ sendAccessFailure(socket, error, metadata) {
351
+ const failure = RequestFailure.from(error);
352
+ if (!failure.code.startsWith('ACCESS_')) return false;
353
+ if (this.protocolPolicy && socket.context?.protocol) this.sendFailure(socket, failure.code, failure.message, metadata);
354
+ else this.send(socket, { code: failure.code, error: failure.message });
355
+ return true;
356
+ }
357
+
347
358
  async sendBinary(socket, value) {
348
359
  try {
349
360
  const encoded = await this.protocolPolicy.encodeBinary(value, socket.context);
@@ -440,6 +451,12 @@ class SocketRoute {
440
451
  await handler.handleMessage(sock, data);
441
452
  return true;
442
453
  } catch (error) {
454
+ if (this.sendAccessFailure(sock, error, { requestId: data.requestId })) return false;
455
+ if (error instanceof InboundContractValidationError) {
456
+ this.sendFailure(sock, error.code, error.message, { requestId: data.requestId });
457
+ sock.close?.(1008, 'Invalid contract payload');
458
+ return false;
459
+ }
443
460
  this.logger.error?.(`Error handling message in handler ${handler.name}:`, error);
444
461
  this.metrics?.increment('redweb.handlers.failed');
445
462
  this.sendFailure(sock, ERROR_CODES.HANDLER_FAILED, this.exposeErrors ? errorMessage(error) : 'Handler failed', { requestId: data.requestId });
@@ -476,6 +493,7 @@ class SocketRoute {
476
493
  await handler.handleBinaryMessage(socket, buffer);
477
494
  return true;
478
495
  } catch (error) {
496
+ if (this.sendAccessFailure(socket, error)) return false;
479
497
  this.logger.error?.('Error handling binary message:', error);
480
498
  this.metrics?.increment('redweb.handlers.failed');
481
499
  this.sendFailure(socket, ERROR_CODES.HANDLER_FAILED, this.exposeErrors ? errorMessage(error) : 'Binary handler failed');
@@ -2,12 +2,17 @@
2
2
  "schemaVersion": 1,
3
3
  "errorCodes": [
4
4
  "INVALID_MESSAGE",
5
+ "INVALID_PAYLOAD",
5
6
  "UNKNOWN_HANDLER",
6
7
  "HANDLER_FAILED",
7
8
  "BINARY_UNSUPPORTED",
8
9
  "RATE_LIMITED",
9
10
  "QUEUE_FULL",
10
11
  "CAPACITY_REACHED",
11
- "INITIALIZATION_FAILED"
12
+ "INITIALIZATION_FAILED",
13
+ "ACCESS_DENIED",
14
+ "ACCESS_TIMEOUT",
15
+ "ACCESS_CANCELLED",
16
+ "ACCESS_CAPACITY"
12
17
  ]
13
18
  }
@@ -1,217 +0,0 @@
1
- import { action, component, each, html, page, start, state, type HtmlFragment } from 'redweb';
2
-
3
- const MAX_VISIBLE_MEMBERS = 100;
4
- const UNSAFE_TEXT = /[\p{Cc}\p{Cf}]/u;
5
-
6
- interface JoinForm {
7
- name?: string;
8
- }
9
-
10
- interface MessageForm {
11
- message?: string;
12
- }
13
-
14
- interface StoredMessage {
15
- sender: string;
16
- text: string;
17
- }
18
-
19
- interface RoomParticipant {
20
- readonly displayName: string;
21
- updateMessages(messages: HtmlFragment): void;
22
- updatePresence(presence: HtmlFragment): void;
23
- }
24
-
25
- function messageView(messages: readonly StoredMessage[]) {
26
- return messages.length
27
- ? each([...messages], entry => html`<li><strong>${entry.sender}</strong><p>${entry.text}</p></li>`)
28
- : html`<li class="empty-message">No messages yet. Say hello.</li>`;
29
- }
30
-
31
- function presenceView(members: readonly string[]) {
32
- const visible = members.slice(0, MAX_VISIBLE_MEMBERS);
33
- const remaining = members.length - visible.length;
34
- return html`
35
- <p class="eyebrow">Online · ${members.length}</p>
36
- <ul>
37
- ${each([...visible], member => html`<li>${member}</li>`)}
38
- ${remaining ? html`<li class="more-members">+${remaining} more</li>` : html``}
39
- </ul>
40
- `;
41
- }
42
-
43
- class ChatRoom {
44
- private history: StoredMessage[] = [];
45
- private readonly participants = new Set<RoomParticipant>();
46
- private readonly online = new Set<RoomParticipant>();
47
-
48
- join(participant: RoomParticipant) {
49
- const name = participant.displayName.toLocaleLowerCase();
50
- if ([...this.participants].some(member => member !== participant && member.displayName.toLocaleLowerCase() === name)) {
51
- return false;
52
- }
53
- this.participants.add(participant);
54
- this.online.add(participant);
55
- participant.updateMessages(messageView(this.history));
56
- this.publishPresence();
57
- return true;
58
- }
59
-
60
- disconnect(participant: RoomParticipant) {
61
- if (!this.online.delete(participant)) return;
62
- this.publishPresence();
63
- }
64
-
65
- leave(participant: RoomParticipant) {
66
- this.online.delete(participant);
67
- if (!this.participants.delete(participant)) return;
68
- this.publishPresence();
69
- }
70
-
71
- send(participant: RoomParticipant, text: string) {
72
- if (!this.online.has(participant)) return false;
73
- this.history = [...this.history, { sender: participant.displayName, text }].slice(-100);
74
- const messages = messageView(this.history);
75
- for (const member of this.participants) member.updateMessages(messages);
76
- return true;
77
- }
78
-
79
- private publishPresence() {
80
- const members = [...this.online].map(participant => participant.displayName);
81
- const presence = presenceView(members);
82
- for (const participant of this.participants) participant.updatePresence(presence);
83
- }
84
- }
85
-
86
- @component()
87
- export class ChatroomComponent implements RoomParticipant {
88
- displayName = '';
89
-
90
- @state()
91
- screen = this.joinScreen();
92
-
93
- @state()
94
- messages = messageView([]);
95
-
96
- @state()
97
- presence = presenceView([]);
98
-
99
- constructor(private readonly room: ChatRoom) {}
100
-
101
- connected() {
102
- if (this.displayName) this.room.join(this);
103
- }
104
-
105
- disconnected() {
106
- this.room.disconnect(this);
107
- }
108
-
109
- disposed() {
110
- this.room.leave(this);
111
- }
112
-
113
- @action()
114
- join({ name }: JoinForm) {
115
- if (this.displayName) return false;
116
- if (typeof name !== 'string') {
117
- this.screen = this.joinScreen('Display name must be text.');
118
- return false;
119
- }
120
- const displayName = name.normalize('NFKC').trim();
121
- if (!displayName || displayName.length > 40 || UNSAFE_TEXT.test(displayName)) {
122
- this.screen = this.joinScreen('Choose a visible display name of at most 40 characters.');
123
- return false;
124
- }
125
- this.displayName = displayName;
126
- if (this.room.join(this)) {
127
- this.screen = this.roomScreen();
128
- return true;
129
- }
130
- this.displayName = '';
131
- this.screen = this.joinScreen('That display name is already in use.');
132
- return false;
133
- }
134
-
135
- @action()
136
- send({ message }: MessageForm) {
137
- if (typeof message !== 'string') return false;
138
- const text = message.normalize('NFKC').trim();
139
- if (!text || text.length > 500 || UNSAFE_TEXT.test(text)) return false;
140
- return this.room.send(this, text);
141
- }
142
-
143
- @action()
144
- leave() {
145
- this.room.leave(this);
146
- this.displayName = '';
147
- this.screen = this.joinScreen();
148
- }
149
-
150
- updateMessages(messages: HtmlFragment) {
151
- this.messages = messages;
152
- }
153
-
154
- updatePresence(presence: HtmlFragment) {
155
- this.presence = presence;
156
- }
157
-
158
- render() {
159
- return html`<section class="chatroom" data-rw-state="screen">${this.screen}</section>`;
160
- }
161
-
162
- private joinScreen(error = '') {
163
- const feedback = error ? html`<p class="form-error" role="alert">${error}</p>` : html``;
164
- return html`
165
- <section class="join-panel">
166
- <p class="eyebrow">Live room</p>
167
- <h1>Join the chatroom</h1>
168
- <p>Choose a name once, then chat in realtime with everyone currently in the room.</p>
169
- ${feedback}
170
- <form rw-submit="join" class="join-form">
171
- <label for="display-name">Display name</label>
172
- <div class="input-row">
173
- <input id="display-name" name="name" maxlength="40" autocomplete="nickname" required autofocus>
174
- <button type="submit">Join room</button>
175
- </div>
176
- </form>
177
- </section>
178
- `;
179
- }
180
-
181
- private roomScreen() {
182
- return html`
183
- <div class="room-layout">
184
- <section class="conversation">
185
- <header class="room-header">
186
- <div><p class="eyebrow">Connected as</p><h1>${this.displayName}</h1></div>
187
- <button type="button" class="quiet-button" rw-click="leave">Leave</button>
188
- </header>
189
- <ol class="message-list" aria-live="polite" data-rw-state="messages">${this.messages}</ol>
190
- <form rw-submit="send" class="composer">
191
- <label class="sr-only" for="chat-message">Message</label>
192
- <input id="chat-message" name="message" maxlength="500" autocomplete="off" placeholder="Message the room…" required autofocus>
193
- <button type="submit">Send</button>
194
- </form>
195
- </section>
196
- <aside class="presence" aria-label="People in the room" data-rw-state="presence">${this.presence}</aside>
197
- </div>
198
- `;
199
- }
200
- }
201
-
202
- export function createChatroomPage() {
203
- const room = new ChatRoom();
204
-
205
- @page('/', { css: 'chatroom.css' })
206
- class ChatroomPage {
207
- chat = new ChatroomComponent(room);
208
-
209
- render() {
210
- return html`<main>${this.chat}</main>`;
211
- }
212
- }
213
-
214
- return ChatroomPage;
215
- }
216
-
217
- if (require.main === module) start(createChatroomPage(), { port: 8080 });