room-kit 1.0.0 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +25 -10
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +83 -16
- package/dist/client.js.map +1 -1
- package/dist/server.d.ts +4 -4
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +143 -14
- package/dist/server.js.map +1 -1
- package/dist/types.d.ts +24 -4
- package/dist/types.d.ts.map +1 -1
- package/example/public/app.ts +184 -187
- package/example/server.ts +173 -146
- package/jsr.json +1 -1
- package/package.json +1 -1
- package/src/client.ts +105 -16
- package/src/server.ts +189 -25
- package/src/types.ts +28 -4
- package/test/room.spec.ts +241 -78
package/example/public/app.ts
CHANGED
|
@@ -26,271 +26,268 @@ const keyInput = document.getElementById("key-input") as HTMLInputElement;
|
|
|
26
26
|
const messageInput = document.getElementById("message-input") as HTMLInputElement;
|
|
27
27
|
|
|
28
28
|
let joinedRoom: JoinedRoom<typeof chatRoomType> | null = null;
|
|
29
|
-
let
|
|
30
|
-
let stopSystemListener: (() => void) | null = null;
|
|
31
|
-
let stopPresenceListener: (() => void) | null = null;
|
|
29
|
+
let stopRoomListeners: (() => void) | null = null;
|
|
32
30
|
let presenceOffset = 0;
|
|
33
31
|
const presencePageSize = 4;
|
|
34
32
|
|
|
35
33
|
function setStatus(message: string): void {
|
|
36
|
-
|
|
34
|
+
status.textContent = message;
|
|
37
35
|
}
|
|
38
36
|
|
|
39
37
|
function setWorkspaceVisible(visible: boolean): void {
|
|
40
|
-
|
|
41
|
-
|
|
38
|
+
workspace.classList.toggle("hidden", !visible);
|
|
39
|
+
joinPanel.classList.toggle("hidden", visible);
|
|
42
40
|
}
|
|
43
41
|
|
|
44
42
|
function clearMessages(): void {
|
|
45
|
-
|
|
43
|
+
messages.innerHTML = "";
|
|
46
44
|
}
|
|
47
45
|
|
|
48
46
|
function renderMessage(message: ChatMessage): void {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
47
|
+
const item = document.createElement("article");
|
|
48
|
+
item.className = "message";
|
|
49
|
+
item.innerHTML = `
|
|
52
50
|
<div class="message-header">
|
|
53
51
|
<span class="message-name">${escapeHtml(message.name)}</span>
|
|
54
52
|
<span class="message-time">${formatTime(message.sentAt)}</span>
|
|
55
53
|
</div>
|
|
56
54
|
<div class="message-text">${escapeHtml(message.text)}</div>
|
|
57
55
|
`;
|
|
58
|
-
|
|
59
|
-
|
|
56
|
+
messages.appendChild(item);
|
|
57
|
+
messages.scrollTop = messages.scrollHeight;
|
|
60
58
|
}
|
|
61
59
|
|
|
62
60
|
function renderSystemNotice(text: string, sentAt: string): void {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
61
|
+
const item = document.createElement("article");
|
|
62
|
+
item.className = "message";
|
|
63
|
+
item.innerHTML = `
|
|
66
64
|
<div class="message-header">
|
|
67
65
|
<span class="message-name">System</span>
|
|
68
66
|
<span class="message-time">${formatTime(sentAt)}</span>
|
|
69
67
|
</div>
|
|
70
68
|
<div class="message-text">${escapeHtml(text)}</div>
|
|
71
69
|
`;
|
|
72
|
-
|
|
73
|
-
|
|
70
|
+
messages.appendChild(item);
|
|
71
|
+
messages.scrollTop = messages.scrollHeight;
|
|
74
72
|
}
|
|
75
73
|
|
|
76
74
|
function renderHistory(history: ChatMessage[]): void {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
75
|
+
clearMessages();
|
|
76
|
+
for (const message of history) {
|
|
77
|
+
renderMessage(message);
|
|
78
|
+
}
|
|
81
79
|
}
|
|
82
80
|
|
|
83
81
|
function renderPresence(): void {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
82
|
+
if (!joinedRoom) {
|
|
83
|
+
presenceCount.textContent = "0 online";
|
|
84
|
+
presencePage.textContent = "Showing 0 members.";
|
|
85
|
+
presenceList.innerHTML = "";
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const presence = joinedRoom.presence.current;
|
|
90
|
+
presenceCount.textContent = `${presence.count} online`;
|
|
91
|
+
presencePage.textContent = `Showing ${Math.min(presenceOffset + 1, presence.count)}-${Math.min(
|
|
92
|
+
presenceOffset + presence.members.length,
|
|
93
|
+
presence.count,
|
|
94
|
+
)} of ${presence.count} members.`;
|
|
95
|
+
presenceList.innerHTML = "";
|
|
96
|
+
|
|
97
|
+
if (presence.members.length === 0) {
|
|
98
|
+
const empty = document.createElement("li");
|
|
99
|
+
empty.className = "presence-empty";
|
|
100
|
+
empty.textContent = "Nobody is here yet.";
|
|
101
|
+
presenceList.appendChild(empty);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
for (const entry of presence.members) {
|
|
106
|
+
const item = document.createElement("li");
|
|
107
|
+
item.className = "presence-item";
|
|
108
|
+
item.innerHTML = `
|
|
111
109
|
<span>${escapeHtml(entry.memberProfile.userName)}</span>
|
|
112
110
|
<span class="presence-pill" aria-hidden="true"></span>
|
|
113
111
|
`;
|
|
114
|
-
|
|
115
|
-
|
|
112
|
+
presenceList.appendChild(item);
|
|
113
|
+
}
|
|
116
114
|
}
|
|
117
115
|
|
|
118
116
|
async function refreshPresence(): Promise<void> {
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
117
|
+
if (!joinedRoom) {
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const [count, page] = await Promise.all([
|
|
122
|
+
joinedRoom.presence.count(),
|
|
123
|
+
joinedRoom.presence.list({ offset: presenceOffset, limit: presencePageSize }),
|
|
124
|
+
]);
|
|
125
|
+
|
|
126
|
+
presenceCount.textContent = `${count} online`;
|
|
127
|
+
presencePage.textContent = page.members.length === 0
|
|
128
|
+
? `Showing 0 of ${count} members.`
|
|
129
|
+
: `Showing ${page.offset + 1}-${page.offset + page.members.length} of ${count} members.`;
|
|
130
|
+
|
|
131
|
+
presenceList.innerHTML = "";
|
|
132
|
+
if (page.members.length === 0) {
|
|
133
|
+
const empty = document.createElement("li");
|
|
134
|
+
empty.className = "presence-empty";
|
|
135
|
+
empty.textContent = "Nobody is here yet.";
|
|
136
|
+
presenceList.appendChild(empty);
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
for (const entry of page.members) {
|
|
141
|
+
const item = document.createElement("li");
|
|
142
|
+
item.className = "presence-item";
|
|
143
|
+
item.innerHTML = `
|
|
146
144
|
<span>${escapeHtml(entry.memberProfile.userName)}</span>
|
|
147
145
|
<span class="presence-pill" aria-hidden="true"></span>
|
|
148
146
|
`;
|
|
149
|
-
|
|
150
|
-
|
|
147
|
+
presenceList.appendChild(item);
|
|
148
|
+
}
|
|
151
149
|
}
|
|
152
150
|
|
|
153
151
|
function formatTime(value: string): string {
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
152
|
+
const date = new Date(value);
|
|
153
|
+
return Number.isNaN(date.getTime())
|
|
154
|
+
? ""
|
|
155
|
+
: new Intl.DateTimeFormat(undefined, {
|
|
156
|
+
hour: "2-digit",
|
|
157
|
+
minute: "2-digit",
|
|
158
|
+
}).format(date);
|
|
161
159
|
}
|
|
162
160
|
|
|
163
161
|
function escapeHtml(value: string): string {
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
162
|
+
return String(value)
|
|
163
|
+
.replaceAll("&", "&")
|
|
164
|
+
.replaceAll("<", "<")
|
|
165
|
+
.replaceAll(">", ">")
|
|
166
|
+
.replaceAll('"', """)
|
|
167
|
+
.replaceAll("'", "'");
|
|
170
168
|
}
|
|
171
169
|
|
|
172
170
|
async function connectRoom(): Promise<void> {
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
171
|
+
const payload = {
|
|
172
|
+
roomId: roomInput.value.trim().toLowerCase(),
|
|
173
|
+
roomKey: keyInput.value,
|
|
174
|
+
userName: nameInput.value,
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
setStatus("Joining room...");
|
|
178
|
+
|
|
179
|
+
try {
|
|
180
|
+
presenceOffset = 0;
|
|
181
|
+
joinedRoom = await chatClient.join(payload);
|
|
182
|
+
renderHistory(joinedRoom.roomProfile.history);
|
|
183
|
+
stopRoomListeners = joinedRoom.listen({
|
|
184
|
+
events: {
|
|
185
|
+
message: (message) => {
|
|
186
|
+
renderMessage(message);
|
|
187
|
+
},
|
|
188
|
+
systemNotice: (notice) => {
|
|
189
|
+
renderSystemNotice(notice.text, notice.sentAt);
|
|
190
|
+
},
|
|
191
|
+
},
|
|
192
|
+
presence: {
|
|
193
|
+
onChange: () => {
|
|
194
|
+
void refreshPresence();
|
|
195
|
+
},
|
|
196
|
+
},
|
|
197
|
+
});
|
|
198
|
+
await refreshPresence();
|
|
199
|
+
|
|
200
|
+
roomTitle.textContent = joinedRoom.roomId;
|
|
201
|
+
roomSubtitle.textContent = `Signed in as ${payload.userName}. The room stays private behind the shared key.`;
|
|
202
|
+
setWorkspaceVisible(true);
|
|
203
|
+
setStatus(`Connected to ${joinedRoom.roomId}.`);
|
|
204
|
+
messageInput.focus();
|
|
205
|
+
} catch (error) {
|
|
206
|
+
joinedRoom = null;
|
|
207
|
+
setStatus(error instanceof Error ? error.message : String(error));
|
|
208
|
+
}
|
|
208
209
|
}
|
|
209
210
|
|
|
210
211
|
async function leaveRoom(): Promise<void> {
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
roomTitle.textContent = "-";
|
|
235
|
-
roomSubtitle.textContent = "-";
|
|
236
|
-
setStatus(`Left ${roomId}.`);
|
|
237
|
-
}
|
|
212
|
+
if (!joinedRoom) {
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const roomId = joinedRoom.roomId;
|
|
217
|
+
|
|
218
|
+
try {
|
|
219
|
+
await joinedRoom.leave();
|
|
220
|
+
} finally {
|
|
221
|
+
stopRoomListeners?.();
|
|
222
|
+
stopRoomListeners = null;
|
|
223
|
+
joinedRoom = null;
|
|
224
|
+
|
|
225
|
+
setWorkspaceVisible(false);
|
|
226
|
+
joinForm.reset();
|
|
227
|
+
clearMessages();
|
|
228
|
+
presenceList.innerHTML = "";
|
|
229
|
+
presenceCount.textContent = "0 online";
|
|
230
|
+
presencePage.textContent = "Showing 0 members.";
|
|
231
|
+
roomTitle.textContent = "-";
|
|
232
|
+
roomSubtitle.textContent = "-";
|
|
233
|
+
setStatus(`Left ${roomId}.`);
|
|
234
|
+
}
|
|
238
235
|
}
|
|
239
236
|
|
|
240
237
|
joinForm.addEventListener("submit", async (event) => {
|
|
241
|
-
|
|
242
|
-
|
|
238
|
+
event.preventDefault();
|
|
239
|
+
await connectRoom();
|
|
243
240
|
});
|
|
244
241
|
|
|
245
242
|
messageForm.addEventListener("submit", async (event) => {
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
243
|
+
event.preventDefault();
|
|
244
|
+
|
|
245
|
+
if (!joinedRoom) {
|
|
246
|
+
setStatus("Join a room before sending messages.");
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const text = messageInput.value.trim();
|
|
251
|
+
if (!text) {
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
try {
|
|
256
|
+
await joinedRoom.rpc.sendMessage({ text });
|
|
257
|
+
messageInput.value = "";
|
|
258
|
+
} catch (error) {
|
|
259
|
+
setStatus(error instanceof Error ? error.message : String(error));
|
|
260
|
+
}
|
|
264
261
|
});
|
|
265
262
|
|
|
266
263
|
leaveButton.addEventListener("click", async () => {
|
|
267
|
-
|
|
264
|
+
await leaveRoom();
|
|
268
265
|
});
|
|
269
266
|
|
|
270
267
|
presencePrevButton.addEventListener("click", async () => {
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
268
|
+
if (!joinedRoom || presenceOffset === 0) {
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
274
271
|
|
|
275
|
-
|
|
276
|
-
|
|
272
|
+
presenceOffset = Math.max(0, presenceOffset - presencePageSize);
|
|
273
|
+
await refreshPresence();
|
|
277
274
|
});
|
|
278
275
|
|
|
279
276
|
presenceNextButton.addEventListener("click", async () => {
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
277
|
+
if (!joinedRoom) {
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
283
280
|
|
|
284
|
-
|
|
285
|
-
|
|
281
|
+
presenceOffset += presencePageSize;
|
|
282
|
+
await refreshPresence();
|
|
286
283
|
});
|
|
287
284
|
|
|
288
285
|
socket.on("connect", () => {
|
|
289
|
-
|
|
286
|
+
setStatus("Connected. Join a room to start chatting.");
|
|
290
287
|
});
|
|
291
288
|
|
|
292
289
|
socket.on("disconnect", () => {
|
|
293
|
-
|
|
290
|
+
setStatus("Disconnected from the server.");
|
|
294
291
|
});
|
|
295
292
|
|
|
296
293
|
setWorkspaceVisible(false);
|