redweb 0.13.2 → 0.13.5

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.
@@ -1,207 +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 = 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 });
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 });
@@ -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", { children: this.count })] })] })] }));
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 ", 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>{this.count}</output>
33
+ Count {this.count}
34
34
  </button>
35
35
  </Card>
36
36
  </main>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "redweb",
3
- "version": "0.13.2",
3
+ "version": "0.13.5",
4
4
  "description": "A small Node.js foundation for HTTP, WebSockets, multiplayer services, and server-rendered HTML",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",
@@ -83,6 +83,7 @@
83
83
  "verify:live-html:browser": "node scripts/build-live-html-examples.js --check && node scripts/verify-live-html-browser.js",
84
84
  "verify:browser:supplements": "node node_modules/jest/bin/jest.js tests/unit/browser-supplements.unit.test.js tests/integration/browser-supplements.integration.test.js --runInBand --coverage --collectCoverageFrom=scripts/lib/verify-live-page-ownership.js --collectCoverageFrom=scripts/lib/verify-runtime-browser.js --coverageDirectory=coverage/browser-supplements-authored",
85
85
  "verify:dashboard:coverage": "node node_modules/jest/bin/jest.js tests/unit/dashboard-verifier.unit.test.js tests/integration/dashboard-verifier.integration.test.js --runInBand --coverage --collectCoverageFrom=scripts/lib/verify-dashboard-browser.js --coverageDirectory=coverage/dashboard-verifier",
86
+ "verify:starter:browser:coverage": "node node_modules/jest/bin/jest.js tests/unit/browser-acceptance.unit.test.js tests/unit/browser-handle-release.unit.test.js tests/unit/starter-browser.unit.test.js tests/integration/starter-browser.integration.test.js --runInBand --coverage --collectCoverageFrom=scripts/lib/BrowserAcceptance.js --collectCoverageFrom=scripts/lib/releaseBrowserHandles.js --collectCoverageFrom=scripts/lib/verify-starter-browser.js --coverageDirectory=coverage/starter-browser",
86
87
  "verify:refresh:coverage": "node node_modules/jest/bin/jest.js tests/unit/refresh-controls-failures.unit.test.js tests/unit/refresh-observation.unit.test.js tests/unit/heading-readiness.unit.test.js tests/integration/refresh-observation.integration.test.js tests/integration/refresh-coverage.integration.test.js --runInBand --coverage --collectCoverageFrom=scripts/lib/verify-refresh-controls.js --collectCoverageFrom=scripts/lib/verify-refresh-coverage.js --coverageDirectory=coverage/refresh-verifier",
87
88
  "verify:browser:coverage": "node node_modules/jest/bin/jest.js tests/unit/browser-coverage.unit.test.js tests/unit/application-coverage.unit.test.js tests/unit/packed-browser-harness.unit.test.js --runInBand --coverage --collectCoverageFrom=scripts/lib/BrowserCoverage.js --collectCoverageFrom=scripts/lib/ApplicationCoverage.js --collectCoverageFrom=scripts/lib/assertCoverageFile.js --coverageDirectory=coverage/browser-collector && npm run verify:browser:coordinator:coverage",
88
89
  "verify:browser:coordinator:coverage": "node node_modules/jest/bin/jest.js tests/unit/browser-coordinator-report.unit.test.js tests/unit/browser-coordinator-lifecycle.unit.test.js tests/unit/browser-coverage-cleanup.unit.test.js tests/unit/browser-supplements.unit.test.js tests/unit/refresh-controls-failures.unit.test.js tests/unit/refresh-observation.unit.test.js tests/unit/heading-readiness.unit.test.js tests/integration/browser-coordinator.integration.test.js tests/integration/browser-supplements.integration.test.js tests/integration/refresh-observation.integration.test.js tests/integration/refresh-coverage.integration.test.js --runInBand --coverage --collectCoverageFrom=scripts/verify-browser-coverage.js --collectCoverageFrom=scripts/lib/verify-live-page-ownership.js --collectCoverageFrom=scripts/lib/verify-runtime-browser.js --collectCoverageFrom=scripts/lib/verify-refresh-controls.js --collectCoverageFrom=scripts/lib/verify-refresh-coverage.js --coverageDirectory=coverage/browser-coordinator",
@@ -107,7 +108,7 @@
107
108
  "verify:package:examples:coverage": "node node_modules/jest/bin/jest.js tests/unit/packed-action.unit.test.js tests/unit/example-dependencies.unit.test.js tests/unit/example-probe.unit.test.js tests/integration/packed-action.integration.test.js tests/integration/example-dependencies.integration.test.js --runInBand --coverage --collectCoverageFrom=scripts/lib/performProbeAction.js --collectCoverageFrom=scripts/lib/verify-example-dependencies.js --collectCoverageFrom=scripts/lib/example-dependency-probe.cjs --coverageDirectory=coverage/example-dependency-tools",
108
109
  "verify:package:browser:coverage": "node node_modules/jest/bin/jest.js tests/unit/browser-pages.unit.test.js tests/unit/packed-browser-verifier.unit.test.js tests/unit/browser-coverage-cleanup.unit.test.js tests/integration/packed-browser-verifier.integration.test.js --runInBand --silent --coverage --collectCoverageFrom=scripts/lib/verify-packed-browser.js --collectCoverageFrom=scripts/lib/BrowserPages.js --coverageDirectory=coverage/packed-browser-verifier",
109
110
  "verify:client:link": "node --test tests/integration/npm-link.integration.test.cjs",
110
- "test": "npx jest"
111
+ "test": "npx jest --testPathIgnorePatterns=soak-.*[.]test[.]js$ --testPathIgnorePatterns=benchmark-measurement[.]integration[.]test[.]js$"
111
112
  },
112
113
  "files": [
113
114
  "bin",
@@ -32,7 +32,7 @@ test('__NAME__ generated __KIND__ works over the network', { timeout: 10000 }, a
32
32
  const response = await fetch(`${origin}${'__KIND__' === 'page' ? '/__NAME__' : '/'}`);
33
33
  assert.equal(response.status, 200);
34
34
  const document = await response.text();
35
- assert.match(document, /<output>0<\/output>/);
35
+ assert.match(document, /Count 0/);
36
36
  config = JSON.parse(document.match(/id="__redweb_page">([^<]+)</)[1]);
37
37
  }
38
38
  const endpoint = config
@@ -49,7 +49,7 @@ test('__NAME__ generated __KIND__ works over the network', { timeout: 10000 }, a
49
49
  } }));
50
50
  } else await artifact.contract.client(socket).send('ping', { text: 'Hello' }, { requestId: 'probe' });
51
51
  const success = message => config
52
- ? message.type === 'redweb:patch' && message.payload.patches.some(patch => patch.html?.includes('<output>1</output>'))
52
+ ? message.type === 'redweb:patch' && message.payload.patches.some(patch => patch.html?.includes('Count 1'))
53
53
  : message.type === 'pong' && message.payload.text === 'Hello' && message.requestId === 'probe';
54
54
  const deadline = Date.now() + 3000;
55
55
  while (!received.some(success) && Date.now() < deadline) await new Promise(resolve => setTimeout(resolve, 10));
@@ -11,7 +11,7 @@ export class __CLASS__ {
11
11
  return (
12
12
  <section>
13
13
  <h1>__TITLE__</h1>
14
- <button rw-click="increment">Count <output>{this.count}</output></button>
14
+ <button rw-click="increment">Count {this.count}</button>
15
15
  </section>
16
16
  );
17
17
  }
@@ -14,11 +14,11 @@ npm test
14
14
  npm run dev
15
15
  ```
16
16
 
17
- The provisioning command displays a randomly generated password once. Save it securely; there are no default accounts or passwords. Open **http://127.0.0.1:8181/login**, sign in, and add a card. A second signed-in tab updates immediately. Restart the app: your cards and unexpired credentials remain valid. Sign out all sessions to close every connected tab for that account and invalidate all its cookies.
17
+ The provisioning command displays a randomly generated password once. Save it securely; there are no default accounts or passwords. Open **http://127.0.0.1:8181/login** or **http://localhost:8181/login**, sign in, and add a card. A second signed-in tab updates immediately. Restart the app: your cards and unexpired credentials remain valid. Sign out all sessions to close every connected tab for that account and invalidate all its cookies.
18
18
 
19
- `npm test` provisions temporary test accounts and a real temporary database, then exercises HTTP, WebSockets, isolation, restart, and session expiry. It never modifies your application database. Integration tests use no mocks. A separately labelled unit test injects a cleanup error after closing a real SQLite database to verify rejection handling; it does not simulate a real operating-system failure.
19
+ `npm test` provisions temporary test accounts and a real temporary database, then exercises HTTP, WebSockets, isolation, restart, and session expiry. It never modifies your application database. Integration tests use no mocks, including hostile cross-loopback form and socket origins. Redweb's release gate also signs in with both valid and invalid credentials through a visible Chromium window, using `localhost` so synthetic request headers cannot hide browser-origin bugs. CI supplies a virtual display for that same headed test. A separately labelled unit test injects a cleanup error after closing a real SQLite database to verify rejection handling; it does not simulate a real operating-system failure.
20
20
 
21
- `npm run test:coverage` measures the TypeScript application through source maps, separately from Redweb's own instrumented-library coverage. It also waits through the actual one-minute login admission window without mocking the clock. The report includes TypeScript-generated decorator accessor functions; inspect that distinction rather than assuming a library coverage figure applies to this recipe. The generated npm configuration enforces this recipe's Node engine requirement before installation.
21
+ `npm run test:coverage` measures the TypeScript application through source maps, separately from Redweb's own instrumented-library coverage. Login-window expiry uses the same real limiter with an explicitly short test configuration; it never waits through the production one-minute default. The report includes TypeScript-generated decorator accessor functions; inspect that distinction rather than assuming a library coverage figure applies to this recipe. The generated npm configuration enforces this recipe's Node engine requirement before installation.
22
22
 
23
23
  ## Where the behavior lives
24
24
 
@@ -8,7 +8,7 @@ const { DatabaseSync } = require('node:sqlite');
8
8
  const { spawn, spawnSync } = require('node:child_process');
9
9
  const net = require('node:net');
10
10
  const { WebSocketServer, WebSocket } = require('ws');
11
- const { createApp, databasePath } = require('../dist/app');
11
+ const { allowsDashboardOrigin, createApp, databasePath } = require('../dist/app');
12
12
  const { DashboardStore } = require('../dist/store');
13
13
  const { DashboardAuth, credentials, sessionToken } = require('../dist/auth');
14
14
  const { PrivateCards } = require('../dist/cards');
@@ -70,9 +70,12 @@ async function cardClient(t, origin, cookie) {
70
70
  test('private live cards: real HTTP, sockets, isolation, reconnect, sign-out and durable restart', async t => {
71
71
  const fixtureApp = await fixture(t);
72
72
  let { origin } = fixtureApp;
73
+ const localhost = origin.replace('127.0.0.1', 'localhost');
73
74
  assert.equal((await fetch(`${origin}/login`)).status, 200);
74
75
  assert.equal((await fetch(origin)).status, 401);
75
76
  assert.equal((await post(origin, '/login', { account: 'alice', password }, '', 'https://foreign.example')).status, 403);
77
+ assert.equal((await post(origin, '/login', { account: 'alice', password }, '', localhost)).status, 403);
78
+ assert.equal((await post(localhost, '/login', { account: 'alice', password: 'wrong-password-at-least-16' })).status, 401);
76
79
  assert.equal((await post(origin, '/login', { account: 'alice', password: 'wrong-password-at-least-16' })).status, 401);
77
80
  const alice = await login(origin);
78
81
  const alice2 = await login(origin);
@@ -80,6 +83,14 @@ test('private live cards: real HTTP, sockets, isolation, reconnect, sign-out and
80
83
  const page = await fetch(origin, { headers: { Cookie: alice } });
81
84
  assert.match(page.headers.get('cache-control'), /private.*no-store/);
82
85
  assert.equal(page.headers.get('etag'), null);
86
+ const document = await page.text();
87
+ const pageConfig = JSON.parse(document.match(/id="__redweb_page">([^<]+)/)[1]);
88
+ const hostile = new WebSocket(`${origin.replace('http:', 'ws:')}${pageConfig.socketPath}?pageId=${pageConfig.pageId}&redwebVersion=${encodeURIComponent(pageConfig.version)}`,
89
+ { headers: { Cookie: alice, Origin: localhost } });
90
+ hostile.on('error', () => {});
91
+ const [, rejection] = await once(hostile, 'unexpected-response');
92
+ assert.equal(rejection.statusCode, 403);
93
+ rejection.resume();
83
94
  const first = await cardClient(t, origin, alice);
84
95
  const second = await cardClient(t, origin, alice2);
85
96
  const other = await cardClient(t, origin, bob);
@@ -124,6 +135,23 @@ test('private live cards: real HTTP, sockets, isolation, reconnect, sign-out and
124
135
  await client.items(value => value.length === 1);
125
136
  });
126
137
 
138
+ test('unit: development accepts only equivalent loopback browser origins on the listening port', () => {
139
+ const expected = 'http://127.0.0.1:8181';
140
+ assert.equal(allowsDashboardOrigin('http://localhost:8181', expected, false, 'localhost:8181'), true);
141
+ assert.equal(allowsDashboardOrigin('http://127.0.0.1:8181', expected, false, '127.0.0.1:8181'), true);
142
+ assert.equal(allowsDashboardOrigin('http://[::1]:8181', expected, false, '[::1]:8181'), true);
143
+ assert.equal(allowsDashboardOrigin('http://localhost:8181', expected, false, '127.0.0.1:8181'), false);
144
+ assert.equal(allowsDashboardOrigin('https://localhost:8181', expected, false, 'localhost:8181'), false);
145
+ assert.equal(allowsDashboardOrigin('http://localhost:8182', expected, false, 'localhost:8182'), false);
146
+ assert.equal(allowsDashboardOrigin('http://localhost:8181/path', expected, false, 'localhost:8181'), false);
147
+ assert.equal(allowsDashboardOrigin('not an origin', expected, false, 'localhost:8181'), false);
148
+ assert.equal(allowsDashboardOrigin(undefined, expected, false, 'localhost:8181'), false);
149
+ assert.equal(allowsDashboardOrigin('http://localhost:8181', expected, false), false);
150
+ assert.equal(allowsDashboardOrigin('http://localhost:8181', expected, false, 'localhost:8181/path'), false);
151
+ assert.equal(allowsDashboardOrigin('https://dashboard.example', 'https://dashboard.example', true), true);
152
+ assert.equal(allowsDashboardOrigin('http://localhost:8181', 'https://dashboard.example', true), false);
153
+ });
154
+
127
155
  test('session expiry closes idle sockets and rejects later HTTP access', async t => {
128
156
  const { origin } = await fixture(t, { sessionLifetimeMs: 1200 });
129
157
  const cookie = await login(origin);
@@ -200,6 +228,8 @@ test('logout fences password checks in flight; close and admission bounds stop n
200
228
  assert.equal(await closing, undefined);
201
229
  assert.equal(await auth.login('peer', 'alice', password), undefined);
202
230
  assert.throws(() => new DashboardAuth(store, 0));
231
+ assert.throws(() => new DashboardAuth(store, 3600000, 19));
232
+ assert.throws(() => new DashboardAuth(store, 3600000, 60001));
203
233
  const limited = new DashboardAuth(store);
204
234
  const concurrent = Array.from({ length: 5 }, (_, index) => limited.login(`peer-${index}`, 'alice', password));
205
235
  assert.equal(await concurrent[4], undefined);