@openmarket/rooms-client 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/README.md +25 -0
- package/dist/agent/armed-mode.d.ts +187 -0
- package/dist/agent/armed-mode.js +306 -0
- package/dist/agent/clients/common.d.ts +70 -0
- package/dist/agent/clients/common.js +46 -0
- package/dist/agent/lane-draft.d.ts +15 -0
- package/dist/agent/lane-draft.js +43 -0
- package/dist/agent/lane-prompts.d.ts +102 -0
- package/dist/agent/lane-prompts.js +120 -0
- package/dist/agent/library-context.d.ts +38 -0
- package/dist/agent/library-context.js +57 -0
- package/dist/agent/library-model.d.ts +41 -0
- package/dist/agent/library-model.js +173 -0
- package/dist/agent/room-context.d.ts +151 -0
- package/dist/agent/room-context.js +251 -0
- package/dist/agent/sidebar-model.d.ts +86 -0
- package/dist/agent/sidebar-model.js +162 -0
- package/dist/agent/topic-inbox.d.ts +119 -0
- package/dist/agent/topic-inbox.js +266 -0
- package/dist/agent/topic-view-batcher.d.ts +54 -0
- package/dist/agent/topic-view-batcher.js +115 -0
- package/dist/client.d.ts +421 -0
- package/dist/client.js +1428 -0
- package/dist/doc-reconcile.d.ts +100 -0
- package/dist/doc-reconcile.js +110 -0
- package/dist/doc-uri.d.ts +29 -0
- package/dist/doc-uri.js +55 -0
- package/dist/endpoints.d.ts +9 -0
- package/dist/endpoints.js +13 -0
- package/dist/jwt.d.ts +10 -0
- package/dist/jwt.js +51 -0
- package/dist/library-publisher.d.ts +52 -0
- package/dist/library-publisher.js +49 -0
- package/dist/merge3.d.ts +50 -0
- package/dist/merge3.js +280 -0
- package/dist/names.d.ts +6 -0
- package/dist/names.js +35 -0
- package/dist/shared/emoji-data.d.ts +22 -0
- package/dist/shared/emoji-data.js +8834 -0
- package/dist/shared/mentions.d.ts +6 -0
- package/dist/shared/mentions.js +32 -0
- package/dist/shared/rooms-protocol.d.ts +1183 -0
- package/dist/shared/rooms-protocol.js +160 -0
- package/dist/social-client.d.ts +361 -0
- package/dist/social-client.js +686 -0
- package/dist/social-types.d.ts +338 -0
- package/dist/social-types.js +1 -0
- package/dist/suggestion-attribution.d.ts +10 -0
- package/dist/suggestion-attribution.js +56 -0
- package/dist/topic-uri.d.ts +26 -0
- package/dist/topic-uri.js +40 -0
- package/dist/types.d.ts +2 -0
- package/dist/types.js +1 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.js +2 -0
- package/dist/ws-client.d.ts +115 -0
- package/dist/ws-client.js +491 -0
- package/package.json +180 -0
- package/src/agent/armed-mode.ts +368 -0
- package/src/agent/clients/common.ts +91 -0
- package/src/agent/lane-draft.ts +47 -0
- package/src/agent/lane-prompts.ts +267 -0
- package/src/agent/library-context.ts +97 -0
- package/src/agent/library-model.ts +210 -0
- package/src/agent/room-context.ts +351 -0
- package/src/agent/sidebar-model.ts +235 -0
- package/src/agent/topic-inbox.ts +297 -0
- package/src/agent/topic-view-batcher.ts +134 -0
- package/src/client.ts +2331 -0
- package/src/doc-reconcile.ts +160 -0
- package/src/doc-uri.ts +83 -0
- package/src/endpoints.ts +14 -0
- package/src/jwt.ts +59 -0
- package/src/library-publisher.ts +93 -0
- package/src/merge3.ts +326 -0
- package/src/names.ts +44 -0
- package/src/shared/emoji-data.ts +8868 -0
- package/src/shared/mentions.ts +32 -0
- package/src/shared/rooms-protocol.ts +1339 -0
- package/src/social-client.ts +1287 -0
- package/src/social-types.ts +376 -0
- package/src/suggestion-attribution.ts +83 -0
- package/src/topic-uri.ts +64 -0
- package/src/types.ts +83 -0
- package/src/version.ts +2 -0
- package/src/ws-client.ts +611 -0
package/dist/client.js
ADDED
|
@@ -0,0 +1,1428 @@
|
|
|
1
|
+
// Browser + Bun + Node 18: the global crypto implements randomUUID, so the
|
|
2
|
+
// shared client stays importable by the web GUI (no node: specifiers).
|
|
3
|
+
const randomUUID = () => globalThis.crypto.randomUUID();
|
|
4
|
+
import { envString, ROOM_CHAT_API_URL, ROOMS_WS_URL } from "./endpoints.js";
|
|
5
|
+
import { authFromApiKey } from "./jwt.js";
|
|
6
|
+
import { consumeRoomSuggestionAttribution } from "./suggestion-attribution.js";
|
|
7
|
+
import { RoomClientMessageType, RoomLedgerEntryType, RoomServerMessageType, } from "./types.js";
|
|
8
|
+
import { RoomWsClient } from "./ws-client.js";
|
|
9
|
+
function toRoomAttachmentPayloads(attachments) {
|
|
10
|
+
return attachments.map(({ key, name, size, mime }) => ({ key, name, size, mime }));
|
|
11
|
+
}
|
|
12
|
+
export function resolveRoomsWsUrl() {
|
|
13
|
+
const env = envString("OM_ROOMS_WS_URL")?.trim();
|
|
14
|
+
return env && env.length > 0 ? env : ROOMS_WS_URL;
|
|
15
|
+
}
|
|
16
|
+
export { workspaceRoomName } from "./names.js";
|
|
17
|
+
export async function listRooms(config, request = {}) {
|
|
18
|
+
if (!config.webSocketCtor) {
|
|
19
|
+
try {
|
|
20
|
+
return await listRoomsViaHttp(config, request);
|
|
21
|
+
}
|
|
22
|
+
catch (error) {
|
|
23
|
+
if (!shouldFallbackToRoomsWebSocket(error))
|
|
24
|
+
throw error;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return listRoomsViaWebSocket(config, request);
|
|
28
|
+
}
|
|
29
|
+
async function listRoomsViaWebSocket(config, request = {}) {
|
|
30
|
+
const client = new RoomWsClient(config);
|
|
31
|
+
try {
|
|
32
|
+
const auth = await client.connect();
|
|
33
|
+
const requestId = randomUUID();
|
|
34
|
+
client.send({
|
|
35
|
+
type: RoomClientMessageType.SEARCH,
|
|
36
|
+
payload: {
|
|
37
|
+
requestId,
|
|
38
|
+
...(request.query ? { query: request.query } : {}),
|
|
39
|
+
...(request.scope ? { scope: request.scope } : {}),
|
|
40
|
+
...(request.category?.length ? { category: request.category } : {}),
|
|
41
|
+
...(request.tag?.length ? { tag: request.tag } : {}),
|
|
42
|
+
...(request.kind?.length ? { kind: request.kind } : {}),
|
|
43
|
+
...(request.entity?.length ? { entity: request.entity } : {}),
|
|
44
|
+
...(request.symbol?.length ? { symbol: request.symbol } : {}),
|
|
45
|
+
...(request.spaceId?.length ? { spaceId: request.spaceId } : {}),
|
|
46
|
+
...(request.hot ? { hot: true } : {}),
|
|
47
|
+
...(request.createdByMe ? { createdByMe: true } : {}),
|
|
48
|
+
...(request.includeArchived ? { includeArchived: true } : {}),
|
|
49
|
+
...(request.limit !== undefined ? { limit: request.limit } : {}),
|
|
50
|
+
},
|
|
51
|
+
timestamp: Date.now(),
|
|
52
|
+
});
|
|
53
|
+
const rooms = await client.waitFor(RoomServerMessageType.ROOMS, (msg) => msg.payload.requestId === requestId, undefined, requestId);
|
|
54
|
+
return { auth, rooms: rooms.payload.rooms };
|
|
55
|
+
}
|
|
56
|
+
finally {
|
|
57
|
+
client.close();
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
export async function createRoom(config, request) {
|
|
61
|
+
if (!config.webSocketCtor) {
|
|
62
|
+
try {
|
|
63
|
+
return await createRoomViaHttp(config, request);
|
|
64
|
+
}
|
|
65
|
+
catch (error) {
|
|
66
|
+
if (!shouldFallbackToRoomsWebSocket(error))
|
|
67
|
+
throw error;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return createRoomViaWebSocket(config, request);
|
|
71
|
+
}
|
|
72
|
+
async function createRoomViaWebSocket(config, request) {
|
|
73
|
+
const client = new RoomWsClient(config);
|
|
74
|
+
try {
|
|
75
|
+
const auth = await client.connect();
|
|
76
|
+
const requestId = randomUUID();
|
|
77
|
+
client.send({
|
|
78
|
+
type: RoomClientMessageType.CREATE_ROOM,
|
|
79
|
+
payload: {
|
|
80
|
+
requestId,
|
|
81
|
+
name: request.name,
|
|
82
|
+
...(request.title ? { title: request.title } : {}),
|
|
83
|
+
...(request.description ? { description: request.description } : {}),
|
|
84
|
+
...(request.access ? { access: request.access } : {}),
|
|
85
|
+
...(request.maxMembers !== undefined ? { maxMembers: request.maxMembers } : {}),
|
|
86
|
+
...(request.category ? { category: request.category } : {}),
|
|
87
|
+
...(request.sortOrder !== undefined ? { sortOrder: request.sortOrder } : {}),
|
|
88
|
+
...(request.spaceId ? { spaceId: request.spaceId } : {}),
|
|
89
|
+
...(request.postingRoles?.length ? { postingRoles: request.postingRoles } : {}),
|
|
90
|
+
...(request.tags?.length ? { tags: request.tags } : {}),
|
|
91
|
+
},
|
|
92
|
+
timestamp: Date.now(),
|
|
93
|
+
});
|
|
94
|
+
const created = await client.waitFor(RoomServerMessageType.ROOM_CREATED, (msg) => msg.payload.requestId === requestId, undefined, requestId);
|
|
95
|
+
return { auth, room: created.payload.room, joinCode: created.payload.joinCode };
|
|
96
|
+
}
|
|
97
|
+
finally {
|
|
98
|
+
client.close();
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
export async function inviteRoom(config, request) {
|
|
102
|
+
// `@user` and `user` are the same person: strip the typing syntax once,
|
|
103
|
+
// before either transport (the relay resolves the bare name).
|
|
104
|
+
const normalized = request.username
|
|
105
|
+
? { ...request, username: request.username.trim().replace(/^@+/, "") }
|
|
106
|
+
: request;
|
|
107
|
+
if (!config.webSocketCtor) {
|
|
108
|
+
try {
|
|
109
|
+
return await inviteRoomViaHttp(config, normalized);
|
|
110
|
+
}
|
|
111
|
+
catch (error) {
|
|
112
|
+
if (!shouldFallbackToRoomsWebSocket(error))
|
|
113
|
+
throw error;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return inviteRoomViaWebSocket(config, normalized);
|
|
117
|
+
}
|
|
118
|
+
async function inviteRoomViaWebSocket(config, request) {
|
|
119
|
+
const client = new RoomWsClient(config);
|
|
120
|
+
try {
|
|
121
|
+
const auth = await client.connect();
|
|
122
|
+
const requestId = randomUUID();
|
|
123
|
+
client.send({
|
|
124
|
+
type: RoomClientMessageType.INVITE,
|
|
125
|
+
payload: {
|
|
126
|
+
requestId,
|
|
127
|
+
room: request.room,
|
|
128
|
+
...(request.username ? { username: request.username } : {}),
|
|
129
|
+
...(request.userId ? { userId: request.userId } : {}),
|
|
130
|
+
},
|
|
131
|
+
timestamp: Date.now(),
|
|
132
|
+
});
|
|
133
|
+
const invited = await client.waitFor(RoomServerMessageType.ROOM_INVITED, (msg) => msg.payload.requestId === requestId, undefined, requestId);
|
|
134
|
+
return { auth, invite: invited.payload };
|
|
135
|
+
}
|
|
136
|
+
finally {
|
|
137
|
+
client.close();
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
export async function removeRoomMember(config, request) {
|
|
141
|
+
if (!config.webSocketCtor) {
|
|
142
|
+
try {
|
|
143
|
+
return await removeRoomMemberViaHttp(config, request);
|
|
144
|
+
}
|
|
145
|
+
catch (error) {
|
|
146
|
+
if (!shouldFallbackToRoomsWebSocket(error))
|
|
147
|
+
throw error;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return removeRoomMemberViaWebSocket(config, request);
|
|
151
|
+
}
|
|
152
|
+
async function removeRoomMemberViaWebSocket(config, request) {
|
|
153
|
+
const client = new RoomWsClient(config);
|
|
154
|
+
try {
|
|
155
|
+
const auth = await client.connect();
|
|
156
|
+
const requestId = randomUUID();
|
|
157
|
+
client.send({
|
|
158
|
+
type: RoomClientMessageType.REMOVE_MEMBER,
|
|
159
|
+
payload: {
|
|
160
|
+
requestId,
|
|
161
|
+
room: request.room,
|
|
162
|
+
...(request.username ? { username: request.username } : {}),
|
|
163
|
+
...(request.userId ? { userId: request.userId } : {}),
|
|
164
|
+
...(request.reason ? { reason: request.reason } : {}),
|
|
165
|
+
},
|
|
166
|
+
timestamp: Date.now(),
|
|
167
|
+
});
|
|
168
|
+
const removal = await client.waitFor(RoomServerMessageType.ROOM_MEMBER_REMOVED, (msg) => msg.payload.requestId === requestId, undefined, requestId);
|
|
169
|
+
return { auth, removal: removal.payload };
|
|
170
|
+
}
|
|
171
|
+
finally {
|
|
172
|
+
client.close();
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
export async function reportRoomTarget(config, request) {
|
|
176
|
+
const client = new RoomWsClient(config);
|
|
177
|
+
try {
|
|
178
|
+
const auth = await client.connect();
|
|
179
|
+
const requestId = randomUUID();
|
|
180
|
+
client.send({
|
|
181
|
+
type: RoomClientMessageType.REPORT,
|
|
182
|
+
payload: {
|
|
183
|
+
requestId,
|
|
184
|
+
room: request.room,
|
|
185
|
+
...(request.seq !== undefined ? { seq: request.seq } : {}),
|
|
186
|
+
...(request.targetUserId ? { targetUserId: request.targetUserId } : {}),
|
|
187
|
+
reason: request.reason,
|
|
188
|
+
},
|
|
189
|
+
timestamp: Date.now(),
|
|
190
|
+
});
|
|
191
|
+
const report = await client.waitFor(RoomServerMessageType.ROOM_REPORTED, (msg) => msg.payload.requestId === requestId, undefined, requestId);
|
|
192
|
+
return { auth, report: report.payload };
|
|
193
|
+
}
|
|
194
|
+
finally {
|
|
195
|
+
client.close();
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
export async function muteRoomMember(config, request) {
|
|
199
|
+
const client = new RoomWsClient(config);
|
|
200
|
+
try {
|
|
201
|
+
const auth = await client.connect();
|
|
202
|
+
const requestId = randomUUID();
|
|
203
|
+
client.send({
|
|
204
|
+
type: RoomClientMessageType.MUTE,
|
|
205
|
+
payload: {
|
|
206
|
+
requestId,
|
|
207
|
+
room: request.room,
|
|
208
|
+
until: request.until,
|
|
209
|
+
...(request.username ? { username: request.username } : {}),
|
|
210
|
+
...(request.userId ? { userId: request.userId } : {}),
|
|
211
|
+
...(request.reason ? { reason: request.reason } : {}),
|
|
212
|
+
},
|
|
213
|
+
timestamp: Date.now(),
|
|
214
|
+
});
|
|
215
|
+
const mute = await client.waitFor(RoomServerMessageType.ROOM_MUTED, (msg) => msg.payload.requestId === requestId, undefined, requestId);
|
|
216
|
+
return { auth, mute: mute.payload };
|
|
217
|
+
}
|
|
218
|
+
finally {
|
|
219
|
+
client.close();
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
export async function kickRoomMember(config, request) {
|
|
223
|
+
return moderateMember(config, RoomClientMessageType.KICK, RoomServerMessageType.ROOM_MEMBER_REMOVED, request);
|
|
224
|
+
}
|
|
225
|
+
export async function banRoomMember(config, request) {
|
|
226
|
+
const client = new RoomWsClient(config);
|
|
227
|
+
try {
|
|
228
|
+
const auth = await client.connect();
|
|
229
|
+
const requestId = randomUUID();
|
|
230
|
+
client.send({
|
|
231
|
+
type: RoomClientMessageType.BAN,
|
|
232
|
+
payload: {
|
|
233
|
+
requestId,
|
|
234
|
+
room: request.room,
|
|
235
|
+
...(request.username ? { username: request.username } : {}),
|
|
236
|
+
...(request.userId ? { userId: request.userId } : {}),
|
|
237
|
+
...(request.reason ? { reason: request.reason } : {}),
|
|
238
|
+
},
|
|
239
|
+
timestamp: Date.now(),
|
|
240
|
+
});
|
|
241
|
+
const ban = await client.waitFor(RoomServerMessageType.ROOM_BANNED, (msg) => msg.payload.requestId === requestId, undefined, requestId);
|
|
242
|
+
return { auth, ban: ban.payload };
|
|
243
|
+
}
|
|
244
|
+
finally {
|
|
245
|
+
client.close();
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
export async function changeRoomMemberRole(config, request) {
|
|
249
|
+
const client = new RoomWsClient(config);
|
|
250
|
+
try {
|
|
251
|
+
const auth = await client.connect();
|
|
252
|
+
const requestId = randomUUID();
|
|
253
|
+
client.send({
|
|
254
|
+
type: RoomClientMessageType.ROLE_CHANGE,
|
|
255
|
+
payload: {
|
|
256
|
+
requestId,
|
|
257
|
+
room: request.room,
|
|
258
|
+
role: request.role,
|
|
259
|
+
...(request.username ? { username: request.username } : {}),
|
|
260
|
+
...(request.userId ? { userId: request.userId } : {}),
|
|
261
|
+
...(request.reason ? { reason: request.reason } : {}),
|
|
262
|
+
},
|
|
263
|
+
timestamp: Date.now(),
|
|
264
|
+
});
|
|
265
|
+
const roleChange = await client.waitFor(RoomServerMessageType.ROOM_MEMBER_ROLE_CHANGED, (msg) => msg.payload.requestId === requestId, undefined, requestId);
|
|
266
|
+
return { auth, roleChange: roleChange.payload };
|
|
267
|
+
}
|
|
268
|
+
finally {
|
|
269
|
+
client.close();
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
export async function setRoomReadOnly(config, request) {
|
|
273
|
+
const client = new RoomWsClient(config);
|
|
274
|
+
try {
|
|
275
|
+
const auth = await client.connect();
|
|
276
|
+
const requestId = randomUUID();
|
|
277
|
+
client.send({
|
|
278
|
+
type: RoomClientMessageType.SET_READ_ONLY,
|
|
279
|
+
payload: {
|
|
280
|
+
requestId,
|
|
281
|
+
room: request.room,
|
|
282
|
+
postingRoles: request.postingRoles,
|
|
283
|
+
...(request.reason ? { reason: request.reason } : {}),
|
|
284
|
+
},
|
|
285
|
+
timestamp: Date.now(),
|
|
286
|
+
});
|
|
287
|
+
const updated = await client.waitFor(RoomServerMessageType.ROOM_READ_ONLY_SET, (msg) => msg.payload.requestId === requestId, undefined, requestId);
|
|
288
|
+
return {
|
|
289
|
+
auth,
|
|
290
|
+
room: updated.payload.room,
|
|
291
|
+
postingRoles: updated.payload.postingRoles,
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
finally {
|
|
295
|
+
client.close();
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
export async function requestRoomAuditLog(config, request) {
|
|
299
|
+
const client = new RoomWsClient(config);
|
|
300
|
+
try {
|
|
301
|
+
const auth = await client.connect();
|
|
302
|
+
const requestId = randomUUID();
|
|
303
|
+
client.send({
|
|
304
|
+
type: RoomClientMessageType.AUDIT_LOG,
|
|
305
|
+
payload: {
|
|
306
|
+
requestId,
|
|
307
|
+
room: request.room,
|
|
308
|
+
...(request.limit !== undefined ? { limit: request.limit } : {}),
|
|
309
|
+
},
|
|
310
|
+
timestamp: Date.now(),
|
|
311
|
+
});
|
|
312
|
+
const audit = await client.waitFor(RoomServerMessageType.ROOM_AUDIT_LOG, (msg) => msg.payload.requestId === requestId, undefined, requestId);
|
|
313
|
+
return { auth, audit: audit.payload };
|
|
314
|
+
}
|
|
315
|
+
finally {
|
|
316
|
+
client.close();
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
export async function listRoomSpaces(config) {
|
|
320
|
+
const client = new RoomWsClient(config);
|
|
321
|
+
try {
|
|
322
|
+
const auth = await client.connect();
|
|
323
|
+
const requestId = randomUUID();
|
|
324
|
+
client.send({
|
|
325
|
+
type: RoomClientMessageType.SPACES,
|
|
326
|
+
payload: { requestId },
|
|
327
|
+
timestamp: Date.now(),
|
|
328
|
+
});
|
|
329
|
+
const spaces = await client.waitFor(RoomServerMessageType.SPACES, (msg) => msg.payload.requestId === requestId, undefined, requestId);
|
|
330
|
+
return { auth, spaces: spaces.payload.spaces };
|
|
331
|
+
}
|
|
332
|
+
finally {
|
|
333
|
+
client.close();
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
export async function joinRoomSpace(config, request) {
|
|
337
|
+
const client = new RoomWsClient(config);
|
|
338
|
+
try {
|
|
339
|
+
const auth = await client.connect();
|
|
340
|
+
const requestId = randomUUID();
|
|
341
|
+
client.send({
|
|
342
|
+
type: RoomClientMessageType.JOIN_SPACE,
|
|
343
|
+
payload: { requestId, spaceId: request.spaceId },
|
|
344
|
+
timestamp: Date.now(),
|
|
345
|
+
});
|
|
346
|
+
const joined = await client.waitFor(RoomServerMessageType.SPACE_JOINED, (msg) => msg.payload.requestId === requestId, undefined, requestId);
|
|
347
|
+
return { auth, space: joined.payload.space };
|
|
348
|
+
}
|
|
349
|
+
finally {
|
|
350
|
+
client.close();
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
async function moderateMember(config, clientType, serverType, request) {
|
|
354
|
+
const client = new RoomWsClient(config);
|
|
355
|
+
try {
|
|
356
|
+
const auth = await client.connect();
|
|
357
|
+
const requestId = randomUUID();
|
|
358
|
+
client.send({
|
|
359
|
+
type: clientType,
|
|
360
|
+
payload: {
|
|
361
|
+
requestId,
|
|
362
|
+
room: request.room,
|
|
363
|
+
...(request.username ? { username: request.username } : {}),
|
|
364
|
+
...(request.userId ? { userId: request.userId } : {}),
|
|
365
|
+
...(request.reason ? { reason: request.reason } : {}),
|
|
366
|
+
},
|
|
367
|
+
timestamp: Date.now(),
|
|
368
|
+
});
|
|
369
|
+
const removal = await client.waitFor(serverType, (msg) => msg.payload.requestId === requestId, undefined, requestId);
|
|
370
|
+
return { auth, removal: removal.payload };
|
|
371
|
+
}
|
|
372
|
+
finally {
|
|
373
|
+
client.close();
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
/** Paged WHO on an already-connected room client (member panel). */
|
|
377
|
+
export async function requestRoomWho(client, room, offset = 0, limit = 100) {
|
|
378
|
+
const requestId = randomUUID();
|
|
379
|
+
client.send({
|
|
380
|
+
type: RoomClientMessageType.WHO,
|
|
381
|
+
payload: { room: normalizeRoomName(room), requestId, offset, limit },
|
|
382
|
+
timestamp: Date.now(),
|
|
383
|
+
});
|
|
384
|
+
const response = await client.waitFor(RoomServerMessageType.WHO, (msg) => msg.payload.room === normalizeRoomName(room) && msg.payload.requestId === requestId, undefined, requestId);
|
|
385
|
+
return response.payload;
|
|
386
|
+
}
|
|
387
|
+
export async function whoRoom(config, room) {
|
|
388
|
+
const client = new RoomWsClient(config);
|
|
389
|
+
try {
|
|
390
|
+
await client.connect();
|
|
391
|
+
const requestId = randomUUID();
|
|
392
|
+
client.send({
|
|
393
|
+
type: RoomClientMessageType.WHO,
|
|
394
|
+
payload: { room, requestId, offset: 0, limit: 50 },
|
|
395
|
+
timestamp: Date.now(),
|
|
396
|
+
});
|
|
397
|
+
const response = await client.waitForAny([RoomServerMessageType.WHO, RoomServerMessageType.PRESENCE], (msg) => msg.payload.room === normalizeRoomName(room) &&
|
|
398
|
+
("requestId" in msg.payload ? msg.payload.requestId === requestId : true), undefined, requestId);
|
|
399
|
+
if (response.type === RoomServerMessageType.PRESENCE)
|
|
400
|
+
return normalizePresence(response.payload);
|
|
401
|
+
return presenceFromWho(response.payload);
|
|
402
|
+
}
|
|
403
|
+
finally {
|
|
404
|
+
client.close();
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
/** The signed-in user's own room identity (userId / handle / username), learned
|
|
408
|
+
* by opening and immediately closing a room connection. Used to attribute "my"
|
|
409
|
+
* past messages when inferring a writing persona. */
|
|
410
|
+
export async function whoamiRoom(config) {
|
|
411
|
+
const client = new RoomWsClient(config);
|
|
412
|
+
try {
|
|
413
|
+
return await client.connect();
|
|
414
|
+
}
|
|
415
|
+
finally {
|
|
416
|
+
client.close();
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
const FRIENDS_FETCH_TIMEOUT_MS = 2000;
|
|
420
|
+
export async function requestRoomFriends(client) {
|
|
421
|
+
client.send({ type: RoomClientMessageType.FRIENDS, payload: {}, timestamp: Date.now() });
|
|
422
|
+
const msg = await client.waitFor(RoomServerMessageType.FRIENDS, () => true, FRIENDS_FETCH_TIMEOUT_MS);
|
|
423
|
+
return msg.payload.friends;
|
|
424
|
+
}
|
|
425
|
+
export async function openRoomDmOnClient(client, request, timeoutMs) {
|
|
426
|
+
const requestId = randomUUID();
|
|
427
|
+
client.send({
|
|
428
|
+
type: RoomClientMessageType.DM_OPEN,
|
|
429
|
+
payload: {
|
|
430
|
+
requestId,
|
|
431
|
+
username: request.username,
|
|
432
|
+
...(request.limit !== undefined ? { limit: request.limit } : {}),
|
|
433
|
+
...(request.markRead !== undefined ? { markRead: request.markRead } : {}),
|
|
434
|
+
},
|
|
435
|
+
timestamp: Date.now(),
|
|
436
|
+
});
|
|
437
|
+
const opened = await client.waitFor(RoomServerMessageType.DM_OPENED, (msg) => msg.payload.requestId === requestId, timeoutMs, requestId);
|
|
438
|
+
return opened.payload;
|
|
439
|
+
}
|
|
440
|
+
export async function requestRoomDmHistoryOnClient(client, request, timeoutMs) {
|
|
441
|
+
const requestId = randomUUID();
|
|
442
|
+
client.send({
|
|
443
|
+
type: RoomClientMessageType.DM_HISTORY,
|
|
444
|
+
payload: {
|
|
445
|
+
requestId,
|
|
446
|
+
...(request.username ? { username: request.username } : {}),
|
|
447
|
+
...(request.conversationId ? { conversationId: request.conversationId } : {}),
|
|
448
|
+
...(request.before !== undefined ? { before: request.before } : {}),
|
|
449
|
+
...(request.limit !== undefined ? { limit: request.limit } : {}),
|
|
450
|
+
...(request.markRead !== undefined ? { markRead: request.markRead } : {}),
|
|
451
|
+
},
|
|
452
|
+
timestamp: Date.now(),
|
|
453
|
+
});
|
|
454
|
+
const history = await client.waitFor(RoomServerMessageType.DM_HISTORY, (msg) => msg.payload.requestId === requestId, timeoutMs, requestId);
|
|
455
|
+
return history.payload;
|
|
456
|
+
}
|
|
457
|
+
export async function sendRoomDmOnClient(client, request, timeoutMs) {
|
|
458
|
+
const requestId = randomUUID();
|
|
459
|
+
const clientMsgId = request.clientMsgId ?? randomUUID();
|
|
460
|
+
client.send({
|
|
461
|
+
type: RoomClientMessageType.DM_SEND,
|
|
462
|
+
payload: {
|
|
463
|
+
requestId,
|
|
464
|
+
content: request.content,
|
|
465
|
+
clientMsgId,
|
|
466
|
+
...(request.username ? { username: request.username } : {}),
|
|
467
|
+
...(request.conversationId ? { conversationId: request.conversationId } : {}),
|
|
468
|
+
},
|
|
469
|
+
timestamp: Date.now(),
|
|
470
|
+
});
|
|
471
|
+
const sent = await client.waitFor(RoomServerMessageType.DM_MESSAGE, (msg) => msg.payload.requestId === requestId ||
|
|
472
|
+
msg.payload.clientMsgId === clientMsgId ||
|
|
473
|
+
msg.payload.message.clientMsgId === clientMsgId, timeoutMs, requestId);
|
|
474
|
+
return sent.payload;
|
|
475
|
+
}
|
|
476
|
+
export async function peekRoom(config, options) {
|
|
477
|
+
if (!config.webSocketCtor) {
|
|
478
|
+
try {
|
|
479
|
+
return await peekRoomViaHttp(config, options);
|
|
480
|
+
}
|
|
481
|
+
catch (error) {
|
|
482
|
+
if (!shouldFallbackToRoomsWebSocket(error))
|
|
483
|
+
throw error;
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
return peekRoomViaWebSocket(config, options);
|
|
487
|
+
}
|
|
488
|
+
async function peekRoomViaWebSocket(config, options) {
|
|
489
|
+
const client = new RoomWsClient(config);
|
|
490
|
+
try {
|
|
491
|
+
await client.connect();
|
|
492
|
+
const requestId = randomUUID();
|
|
493
|
+
client.send({
|
|
494
|
+
type: RoomClientMessageType.PEEK,
|
|
495
|
+
payload: {
|
|
496
|
+
requestId,
|
|
497
|
+
room: options.room,
|
|
498
|
+
...(options.beforeSeq !== undefined ? { beforeSeq: options.beforeSeq } : {}),
|
|
499
|
+
...(options.sinceSeq !== undefined ? { sinceSeq: options.sinceSeq } : {}),
|
|
500
|
+
...(options.atMs !== undefined ? { atMs: options.atMs } : {}),
|
|
501
|
+
...(options.limit !== undefined ? { limit: options.limit } : {}),
|
|
502
|
+
},
|
|
503
|
+
timestamp: Date.now(),
|
|
504
|
+
});
|
|
505
|
+
const result = await client.waitFor(RoomServerMessageType.PEEK_RESULT, (msg) => msg.payload.room === normalizeRoomName(options.room), undefined, requestId);
|
|
506
|
+
return result.payload;
|
|
507
|
+
}
|
|
508
|
+
finally {
|
|
509
|
+
client.close();
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
export async function sayRoom(config, options) {
|
|
513
|
+
const clientMsgId = options.clientMsgId ?? randomUUID();
|
|
514
|
+
const client = new RoomWsClient(config);
|
|
515
|
+
try {
|
|
516
|
+
const auth = await client.connect();
|
|
517
|
+
await joinRoomOnClient(client, options);
|
|
518
|
+
client.send({
|
|
519
|
+
type: RoomClientMessageType.POST,
|
|
520
|
+
payload: {
|
|
521
|
+
room: options.room,
|
|
522
|
+
...(options.text ? { text: options.text } : {}),
|
|
523
|
+
...(options.attachments?.length
|
|
524
|
+
? { attachments: toRoomAttachmentPayloads(options.attachments) }
|
|
525
|
+
: {}),
|
|
526
|
+
clientMsgId,
|
|
527
|
+
...(options.inReplyTo !== undefined ? { inReplyTo: options.inReplyTo } : {}),
|
|
528
|
+
...(options.topicId ? { topicId: options.topicId } : {}),
|
|
529
|
+
},
|
|
530
|
+
timestamp: Date.now(),
|
|
531
|
+
});
|
|
532
|
+
const echo = await client.waitFor(RoomServerMessageType.MESSAGE, (msg) => {
|
|
533
|
+
const entry = msg.payload.entry;
|
|
534
|
+
if (entry.clientMsgId)
|
|
535
|
+
return entry.clientMsgId === clientMsgId;
|
|
536
|
+
return (msg.payload.room === normalizeRoomName(options.room) &&
|
|
537
|
+
entry.userId === auth.userId &&
|
|
538
|
+
entry.type === RoomLedgerEntryType.POST &&
|
|
539
|
+
(options.attachments?.length
|
|
540
|
+
? Boolean(entry.attachments?.some((attachment) => options.attachments?.some((expected) => expected.key === attachment.key)))
|
|
541
|
+
: entry.text === options.text));
|
|
542
|
+
});
|
|
543
|
+
ackRoom(client, options.room, echo.payload.seq);
|
|
544
|
+
return { auth, entry: echo.payload.entry, clientMsgId };
|
|
545
|
+
}
|
|
546
|
+
finally {
|
|
547
|
+
client.close();
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
export async function joinRoom(config, options) {
|
|
551
|
+
// The live session is long-lived and interactive — enable transport-level
|
|
552
|
+
// reconnection so a dropped socket recovers instead of silently going dead.
|
|
553
|
+
// The session re-joins and resyncs from its last seq on the "reconnected"
|
|
554
|
+
// event (see live-session onConnectionChange).
|
|
555
|
+
const client = new RoomWsClient({ ...config, reconnect: true });
|
|
556
|
+
const auth = await client.connect();
|
|
557
|
+
const backfill = await joinRoomOnClient(client, options);
|
|
558
|
+
if (options.ackOnJoin !== false && backfill.upToSeq > 0) {
|
|
559
|
+
ackRoom(client, options.room, backfill.upToSeq);
|
|
560
|
+
}
|
|
561
|
+
return { auth, backfill, client };
|
|
562
|
+
}
|
|
563
|
+
/** Re-join a room on an already-connected client (used after a reconnect). */
|
|
564
|
+
export async function rejoinRoomOnClient(client, options) {
|
|
565
|
+
return joinRoomOnClient(client, { ...options, isRejoin: true });
|
|
566
|
+
}
|
|
567
|
+
export function attachRejoinOnReconnect(client, options, latestSeq, onRejoined, onError) {
|
|
568
|
+
return client.onConnectionChange((state) => {
|
|
569
|
+
if (state !== "reconnected")
|
|
570
|
+
return;
|
|
571
|
+
const sinceSeq = latestSeq();
|
|
572
|
+
void rejoinRoomOnClient(client, {
|
|
573
|
+
...options,
|
|
574
|
+
...(sinceSeq !== undefined ? { sinceSeq } : {}),
|
|
575
|
+
})
|
|
576
|
+
.then((backfill) => onRejoined?.(backfill))
|
|
577
|
+
.catch((err) => onError?.(err));
|
|
578
|
+
});
|
|
579
|
+
}
|
|
580
|
+
export function postRoomMessage(client, room, text, inReplyTo, attachments, topicId) {
|
|
581
|
+
const clientMsgId = randomUUID();
|
|
582
|
+
client.send({
|
|
583
|
+
type: RoomClientMessageType.POST,
|
|
584
|
+
payload: {
|
|
585
|
+
room,
|
|
586
|
+
...(text ? { text } : {}),
|
|
587
|
+
...(attachments?.length ? { attachments: toRoomAttachmentPayloads(attachments) } : {}),
|
|
588
|
+
clientMsgId,
|
|
589
|
+
...(inReplyTo !== undefined ? { inReplyTo } : {}),
|
|
590
|
+
...(topicId ? { topicId } : {}),
|
|
591
|
+
},
|
|
592
|
+
timestamp: Date.now(),
|
|
593
|
+
});
|
|
594
|
+
return clientMsgId;
|
|
595
|
+
}
|
|
596
|
+
export function sendRoomTyping(client, room, state) {
|
|
597
|
+
if (client.isGuest())
|
|
598
|
+
return;
|
|
599
|
+
client.send({
|
|
600
|
+
type: RoomClientMessageType.TYPING,
|
|
601
|
+
payload: { room, state },
|
|
602
|
+
timestamp: Date.now(),
|
|
603
|
+
});
|
|
604
|
+
}
|
|
605
|
+
export function sendRoomStatus(client, status, statusText) {
|
|
606
|
+
client.send({
|
|
607
|
+
type: RoomClientMessageType.STATUS,
|
|
608
|
+
payload: { status, ...(statusText !== undefined ? { statusText } : {}) },
|
|
609
|
+
timestamp: Date.now(),
|
|
610
|
+
});
|
|
611
|
+
}
|
|
612
|
+
/**
|
|
613
|
+
* Fetch a joined room's meta (title, access, postingRoles, viewerRole) over
|
|
614
|
+
* the live socket via a scoped SEARCH. Returns null on timeout/no match so
|
|
615
|
+
* callers can degrade to role hints from presence.
|
|
616
|
+
*/
|
|
617
|
+
export async function requestRoomMeta(client, room, timeoutMs = 1500) {
|
|
618
|
+
const requestId = randomUUID();
|
|
619
|
+
client.send({
|
|
620
|
+
type: RoomClientMessageType.SEARCH,
|
|
621
|
+
payload: { requestId, query: room, limit: 20 },
|
|
622
|
+
timestamp: Date.now(),
|
|
623
|
+
});
|
|
624
|
+
try {
|
|
625
|
+
const result = await client.waitFor(RoomServerMessageType.ROOMS, (msg) => msg.payload.requestId === requestId, timeoutMs, requestId);
|
|
626
|
+
return (result.payload.rooms.find((candidate) => normalizeRoomName(candidate.name) === normalizeRoomName(room)) ?? null);
|
|
627
|
+
}
|
|
628
|
+
catch {
|
|
629
|
+
return null;
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
export function reactRoomEntry(client, room, seq, emoji, op) {
|
|
633
|
+
client.send({
|
|
634
|
+
type: RoomClientMessageType.REACT,
|
|
635
|
+
payload: { room, seq, emoji, op },
|
|
636
|
+
timestamp: Date.now(),
|
|
637
|
+
});
|
|
638
|
+
}
|
|
639
|
+
export function editRoomEntry(client, room, seq, text) {
|
|
640
|
+
client.send({
|
|
641
|
+
type: RoomClientMessageType.EDIT,
|
|
642
|
+
payload: { room, seq, text },
|
|
643
|
+
timestamp: Date.now(),
|
|
644
|
+
});
|
|
645
|
+
}
|
|
646
|
+
export async function deleteRoomEntry(client, room, seq) {
|
|
647
|
+
const requestId = randomUUID();
|
|
648
|
+
client.send({
|
|
649
|
+
type: RoomClientMessageType.DELETE,
|
|
650
|
+
payload: { requestId, room, seq },
|
|
651
|
+
timestamp: Date.now(),
|
|
652
|
+
});
|
|
653
|
+
return waitForEntryUpdate(client, room, seq, requestId);
|
|
654
|
+
}
|
|
655
|
+
export async function requestRoomThread(client, request) {
|
|
656
|
+
const requestId = randomUUID();
|
|
657
|
+
client.send({
|
|
658
|
+
type: RoomClientMessageType.THREAD,
|
|
659
|
+
payload: {
|
|
660
|
+
requestId,
|
|
661
|
+
room: request.room,
|
|
662
|
+
rootSeq: request.rootSeq,
|
|
663
|
+
...(request.beforeSeq !== undefined ? { beforeSeq: request.beforeSeq } : {}),
|
|
664
|
+
...(request.sinceSeq !== undefined ? { sinceSeq: request.sinceSeq } : {}),
|
|
665
|
+
...(request.limit !== undefined ? { limit: request.limit } : {}),
|
|
666
|
+
},
|
|
667
|
+
timestamp: Date.now(),
|
|
668
|
+
});
|
|
669
|
+
const result = await client.waitFor(RoomServerMessageType.THREAD, (msg) => msg.payload.requestId === requestId &&
|
|
670
|
+
msg.payload.room === normalizeRoomName(request.room) &&
|
|
671
|
+
msg.payload.rootSeq === request.rootSeq, undefined, requestId);
|
|
672
|
+
return result.payload;
|
|
673
|
+
}
|
|
674
|
+
export async function requestRoomPins(client, room, limit = 50) {
|
|
675
|
+
const requestId = randomUUID();
|
|
676
|
+
client.send({
|
|
677
|
+
type: RoomClientMessageType.PINS,
|
|
678
|
+
payload: { requestId, room, limit },
|
|
679
|
+
timestamp: Date.now(),
|
|
680
|
+
});
|
|
681
|
+
const result = await client.waitFor(RoomServerMessageType.PINS, (msg) => msg.payload.requestId === requestId && msg.payload.room === normalizeRoomName(room), undefined, requestId);
|
|
682
|
+
return result.payload;
|
|
683
|
+
}
|
|
684
|
+
export async function pinRoomEntry(client, room, seq) {
|
|
685
|
+
const requestId = randomUUID();
|
|
686
|
+
client.send({
|
|
687
|
+
type: RoomClientMessageType.PIN,
|
|
688
|
+
payload: { requestId, room, seq },
|
|
689
|
+
timestamp: Date.now(),
|
|
690
|
+
});
|
|
691
|
+
return waitForEntryUpdate(client, room, seq, requestId);
|
|
692
|
+
}
|
|
693
|
+
export async function unpinRoomEntry(client, room, seq) {
|
|
694
|
+
const requestId = randomUUID();
|
|
695
|
+
client.send({
|
|
696
|
+
type: RoomClientMessageType.UNPIN,
|
|
697
|
+
payload: { requestId, room, seq },
|
|
698
|
+
timestamp: Date.now(),
|
|
699
|
+
});
|
|
700
|
+
return waitForEntryUpdate(client, room, seq, requestId);
|
|
701
|
+
}
|
|
702
|
+
async function waitForEntryUpdate(client, room, seq, requestId) {
|
|
703
|
+
const result = await client.waitFor(RoomServerMessageType.ENTRY_UPDATED, (msg) => msg.payload.requestId === requestId &&
|
|
704
|
+
msg.payload.room === normalizeRoomName(room) &&
|
|
705
|
+
msg.payload.seq === seq, undefined, requestId);
|
|
706
|
+
return result.payload.entry;
|
|
707
|
+
}
|
|
708
|
+
/** Who reacted (live socket): lazy read for reaction-chip tooltips. */
|
|
709
|
+
export async function requestRoomReactors(client, request) {
|
|
710
|
+
const requestId = randomUUID();
|
|
711
|
+
client.send({
|
|
712
|
+
type: RoomClientMessageType.REACTORS,
|
|
713
|
+
payload: {
|
|
714
|
+
requestId,
|
|
715
|
+
room: request.room,
|
|
716
|
+
seq: request.seq,
|
|
717
|
+
...(request.emoji ? { emoji: request.emoji } : {}),
|
|
718
|
+
...(request.limit !== undefined ? { limit: request.limit } : {}),
|
|
719
|
+
},
|
|
720
|
+
timestamp: Date.now(),
|
|
721
|
+
});
|
|
722
|
+
const result = await client.waitFor(RoomServerMessageType.REACTORS, (msg) => msg.payload.requestId === requestId &&
|
|
723
|
+
msg.payload.room === normalizeRoomName(request.room) &&
|
|
724
|
+
msg.payload.seq === request.seq, undefined, requestId);
|
|
725
|
+
return result.payload;
|
|
726
|
+
}
|
|
727
|
+
/** Set my presence status (one-shot, agent-verb transport). The relay
|
|
728
|
+
* persists it durably; a one-shot socket has no joined rooms, so live
|
|
729
|
+
* rosters pick the change up from the store (the TUI/GUI send STATUS on
|
|
730
|
+
* their live connection instead, which also broadcasts PRESENCE_DELTA). */
|
|
731
|
+
export async function setRoomStatusOnce(config, status, statusText) {
|
|
732
|
+
const client = new RoomWsClient(config);
|
|
733
|
+
try {
|
|
734
|
+
await client.connect();
|
|
735
|
+
sendRoomStatus(client, status, statusText);
|
|
736
|
+
// STATUS has no ack frame: give the socket a beat to flush before close.
|
|
737
|
+
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
738
|
+
}
|
|
739
|
+
finally {
|
|
740
|
+
client.close();
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
/** One-shot session helper for the config-based mutation verbs below (the
|
|
744
|
+
* agent tools' transport: connect, join, act, wait for the echo, close). */
|
|
745
|
+
async function withRoomEcho(config, room, seq, send) {
|
|
746
|
+
const client = new RoomWsClient(config);
|
|
747
|
+
try {
|
|
748
|
+
await client.connect();
|
|
749
|
+
await joinRoomOnClient(client, { room, limit: 1 });
|
|
750
|
+
send(client);
|
|
751
|
+
const updated = await client.waitFor(RoomServerMessageType.ENTRY_UPDATED, (msg) => msg.payload.room === normalizeRoomName(room) && msg.payload.seq === seq);
|
|
752
|
+
return updated.payload.entry;
|
|
753
|
+
}
|
|
754
|
+
finally {
|
|
755
|
+
client.close();
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
/** Add/remove one reaction (one-shot). Returns the updated entry. */
|
|
759
|
+
export async function reactRoom(config, options) {
|
|
760
|
+
return withRoomEcho(config, options.room, options.seq, (client) => reactRoomEntry(client, options.room, options.seq, options.emoji, options.op));
|
|
761
|
+
}
|
|
762
|
+
/** Edit own message (one-shot). Returns the updated entry. */
|
|
763
|
+
export async function editRoomMessage(config, options) {
|
|
764
|
+
return withRoomEcho(config, options.room, options.seq, (client) => editRoomEntry(client, options.room, options.seq, options.text));
|
|
765
|
+
}
|
|
766
|
+
/** Delete a message (own, or moderator delete-any; one-shot). */
|
|
767
|
+
export async function deleteRoomMessage(config, options) {
|
|
768
|
+
return withRoomEcho(config, options.room, options.seq, (client) => deleteRoomEntry(client, options.room, options.seq));
|
|
769
|
+
}
|
|
770
|
+
/** Pin or unpin a message (one-shot). Returns the updated entry. */
|
|
771
|
+
export async function pinRoomMessage(config, options) {
|
|
772
|
+
return withRoomEcho(config, options.room, options.seq, (client) => options.op === "pin"
|
|
773
|
+
? pinRoomEntry(client, options.room, options.seq)
|
|
774
|
+
: unpinRoomEntry(client, options.room, options.seq));
|
|
775
|
+
}
|
|
776
|
+
/** List pinned messages (one-shot). */
|
|
777
|
+
export async function listRoomPinsOnce(config, options) {
|
|
778
|
+
const client = new RoomWsClient(config);
|
|
779
|
+
try {
|
|
780
|
+
await client.connect();
|
|
781
|
+
return await requestRoomPins(client, options.room, options.limit ?? 50);
|
|
782
|
+
}
|
|
783
|
+
finally {
|
|
784
|
+
client.close();
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
/** Fetch one reply thread (one-shot). */
|
|
788
|
+
export async function getRoomThreadOnce(config, options) {
|
|
789
|
+
const client = new RoomWsClient(config);
|
|
790
|
+
try {
|
|
791
|
+
await client.connect();
|
|
792
|
+
return await requestRoomThread(client, {
|
|
793
|
+
room: options.room,
|
|
794
|
+
rootSeq: options.rootSeq,
|
|
795
|
+
...(options.limit !== undefined ? { limit: options.limit } : {}),
|
|
796
|
+
});
|
|
797
|
+
}
|
|
798
|
+
finally {
|
|
799
|
+
client.close();
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
/** Who reacted (one-shot). */
|
|
803
|
+
export async function getRoomReactorsOnce(config, options) {
|
|
804
|
+
const client = new RoomWsClient(config);
|
|
805
|
+
try {
|
|
806
|
+
await client.connect();
|
|
807
|
+
return await requestRoomReactors(client, options);
|
|
808
|
+
}
|
|
809
|
+
finally {
|
|
810
|
+
client.close();
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
/** Fetch one entry by seq (one-shot; HISTORY page of one). */
|
|
814
|
+
export async function getRoomMessageOnce(config, options) {
|
|
815
|
+
const client = new RoomWsClient(config);
|
|
816
|
+
try {
|
|
817
|
+
await client.connect();
|
|
818
|
+
const backfill = await joinRoomOnClient(client, {
|
|
819
|
+
room: options.room,
|
|
820
|
+
limit: 1,
|
|
821
|
+
});
|
|
822
|
+
if (backfill.entries.some((entry) => entry.seq === options.seq)) {
|
|
823
|
+
return backfill.entries.find((entry) => entry.seq === options.seq) ?? null;
|
|
824
|
+
}
|
|
825
|
+
const page = await requestRoomHistoryPage(client, options.room, {
|
|
826
|
+
beforeSeq: options.seq + 1,
|
|
827
|
+
limit: 1,
|
|
828
|
+
});
|
|
829
|
+
return page.entries.find((entry) => entry.seq === options.seq) ?? null;
|
|
830
|
+
}
|
|
831
|
+
finally {
|
|
832
|
+
client.close();
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
export async function requestRoomReadState(client, rooms, timeoutMs) {
|
|
836
|
+
if (client.isGuest())
|
|
837
|
+
return { readState: {} };
|
|
838
|
+
const requestId = randomUUID();
|
|
839
|
+
client.send({
|
|
840
|
+
type: RoomClientMessageType.READ_STATE,
|
|
841
|
+
payload: { requestId, rooms },
|
|
842
|
+
timestamp: Date.now(),
|
|
843
|
+
});
|
|
844
|
+
const result = await client.waitFor(RoomServerMessageType.READ_STATE, (msg) => msg.payload.requestId === requestId, timeoutMs, requestId);
|
|
845
|
+
return result.payload;
|
|
846
|
+
}
|
|
847
|
+
export async function searchRoomEntries(client, request) {
|
|
848
|
+
const requestId = randomUUID();
|
|
849
|
+
client.send({
|
|
850
|
+
type: RoomClientMessageType.SEARCH_ENTRIES,
|
|
851
|
+
payload: {
|
|
852
|
+
requestId,
|
|
853
|
+
room: request.room,
|
|
854
|
+
query: request.query,
|
|
855
|
+
...(request.beforeSeq !== undefined ? { beforeSeq: request.beforeSeq } : {}),
|
|
856
|
+
...(request.sinceSeq !== undefined ? { sinceSeq: request.sinceSeq } : {}),
|
|
857
|
+
...(request.fromMs !== undefined ? { fromMs: request.fromMs } : {}),
|
|
858
|
+
...(request.toMs !== undefined ? { toMs: request.toMs } : {}),
|
|
859
|
+
...(request.limit !== undefined ? { limit: request.limit } : {}),
|
|
860
|
+
},
|
|
861
|
+
timestamp: Date.now(),
|
|
862
|
+
});
|
|
863
|
+
const result = await client.waitFor(RoomServerMessageType.SEARCH_RESULTS, (msg) => msg.payload.requestId === requestId && msg.payload.room === normalizeRoomName(request.room), undefined, requestId);
|
|
864
|
+
return result.payload;
|
|
865
|
+
}
|
|
866
|
+
/** One-shot variant for headless surfaces (actions): connect, join, search,
|
|
867
|
+
* close. The relay gates SEARCH_ENTRIES on per-socket room participation
|
|
868
|
+
* (isInRoom checks this very WebSocket), so a minimal JOIN is required
|
|
869
|
+
* first; the brief presence blip matches getRoomMessageOnce. */
|
|
870
|
+
export async function searchRoomEntriesOnce(config, request) {
|
|
871
|
+
const client = new RoomWsClient(config);
|
|
872
|
+
try {
|
|
873
|
+
await client.connect();
|
|
874
|
+
await joinRoomOnClient(client, { room: request.room, limit: 1 });
|
|
875
|
+
return await searchRoomEntries(client, request);
|
|
876
|
+
}
|
|
877
|
+
finally {
|
|
878
|
+
client.close();
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
export async function requestRoomHistory(client, room, beforeSeq, limit) {
|
|
882
|
+
return requestRoomHistoryPage(client, room, { beforeSeq, limit });
|
|
883
|
+
}
|
|
884
|
+
export async function requestRoomHistoryPage(client, room, request) {
|
|
885
|
+
const requestId = randomUUID();
|
|
886
|
+
client.send({
|
|
887
|
+
type: RoomClientMessageType.HISTORY,
|
|
888
|
+
payload: {
|
|
889
|
+
requestId,
|
|
890
|
+
room,
|
|
891
|
+
...(request.beforeSeq !== undefined ? { beforeSeq: request.beforeSeq } : {}),
|
|
892
|
+
...(request.sinceSeq !== undefined ? { sinceSeq: request.sinceSeq } : {}),
|
|
893
|
+
...(request.atMs !== undefined ? { atMs: request.atMs } : {}),
|
|
894
|
+
limit: request.limit,
|
|
895
|
+
},
|
|
896
|
+
timestamp: Date.now(),
|
|
897
|
+
});
|
|
898
|
+
const history = await client.waitFor(RoomServerMessageType.HISTORY, (msg) => msg.payload.requestId === requestId && msg.payload.room === normalizeRoomName(room), undefined, requestId);
|
|
899
|
+
return history.payload;
|
|
900
|
+
}
|
|
901
|
+
/** `topicId` acks a per-topic cursor; the room-wide cursor is separate. */
|
|
902
|
+
export function ackRoom(client, room, seq, topicId) {
|
|
903
|
+
client.send({
|
|
904
|
+
type: RoomClientMessageType.ACK,
|
|
905
|
+
payload: { room, seq, ...(topicId ? { topicId } : {}) },
|
|
906
|
+
timestamp: Date.now(),
|
|
907
|
+
});
|
|
908
|
+
}
|
|
909
|
+
export class RoomAckDebouncer {
|
|
910
|
+
client;
|
|
911
|
+
delayMs;
|
|
912
|
+
pending = new Map();
|
|
913
|
+
timer;
|
|
914
|
+
constructor(client, delayMs = 1_000) {
|
|
915
|
+
this.client = client;
|
|
916
|
+
this.delayMs = delayMs;
|
|
917
|
+
}
|
|
918
|
+
/** `topicId` batches a per-topic view-time cursor; omit for the room-wide one. */
|
|
919
|
+
schedule(room, seq, topicId) {
|
|
920
|
+
if (!Number.isFinite(seq) || seq <= 0)
|
|
921
|
+
return;
|
|
922
|
+
const normalizedRoom = normalizeRoomName(room);
|
|
923
|
+
const key = ackDebounceKey(normalizedRoom, topicId);
|
|
924
|
+
const previous = this.pending.get(key);
|
|
925
|
+
if (previous && seq <= previous.seq)
|
|
926
|
+
return;
|
|
927
|
+
this.pending.set(key, { room: normalizedRoom, topicId, seq });
|
|
928
|
+
if (this.timer)
|
|
929
|
+
return;
|
|
930
|
+
this.timer = setTimeout(() => {
|
|
931
|
+
this.timer = undefined;
|
|
932
|
+
this.flush();
|
|
933
|
+
}, this.delayMs);
|
|
934
|
+
this.timer.unref?.();
|
|
935
|
+
}
|
|
936
|
+
flush() {
|
|
937
|
+
if (this.timer) {
|
|
938
|
+
clearTimeout(this.timer);
|
|
939
|
+
this.timer = undefined;
|
|
940
|
+
}
|
|
941
|
+
const pending = [...this.pending.entries()];
|
|
942
|
+
this.pending.clear();
|
|
943
|
+
for (const [key, entry] of pending) {
|
|
944
|
+
// ACKs are fire-and-forget and seq is monotonic, so a send that fails
|
|
945
|
+
// because the socket just dropped is harmless — the next ACK (or the
|
|
946
|
+
// post-reconnect resync) supersedes it. Never let a debounced flush that
|
|
947
|
+
// fires inside a timer throw an uncaught error and take down the process.
|
|
948
|
+
try {
|
|
949
|
+
ackRoom(this.client, entry.room, entry.seq, entry.topicId);
|
|
950
|
+
}
|
|
951
|
+
catch {
|
|
952
|
+
// Re-queue the highest seq so a later flush (post-reconnect) retries it.
|
|
953
|
+
const current = this.pending.get(key);
|
|
954
|
+
if (!current || entry.seq > current.seq)
|
|
955
|
+
this.pending.set(key, entry);
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
close() {
|
|
960
|
+
this.flush();
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
function ackDebounceKey(room, topicId) {
|
|
964
|
+
return `${encodeURIComponent(room)}:${encodeURIComponent(topicId ?? "")}`;
|
|
965
|
+
}
|
|
966
|
+
export async function createRoomTopic(config, request) {
|
|
967
|
+
// Always the relay websocket, never the store REST write: the relay's
|
|
968
|
+
// handleTopicCreate is what fans TOPIC_UPDATED out to room members (plus
|
|
969
|
+
// the cross-pod bus), while a direct store POST broadcasts nothing, so
|
|
970
|
+
// topics created that way stay invisible to open GUIs until their next
|
|
971
|
+
// hydrate. Every other topic mutation (topicResultViaWebSocket below)
|
|
972
|
+
// already rides the socket; create is no different.
|
|
973
|
+
//
|
|
974
|
+
// Room-scoped (unlike the topicId-only verbs sharing
|
|
975
|
+
// topicResultViaWebSocket below): the relay's handleTopicCreate checks
|
|
976
|
+
// the caller's connection is in the room, the same NOT_IN_ROOM gate
|
|
977
|
+
// sayRoom satisfies via joinRoomOnClient.
|
|
978
|
+
const client = new RoomWsClient(config);
|
|
979
|
+
try {
|
|
980
|
+
const auth = await client.connect();
|
|
981
|
+
await joinRoomOnClient(client, { room: request.room });
|
|
982
|
+
const requestId = randomUUID();
|
|
983
|
+
client.send({
|
|
984
|
+
type: RoomClientMessageType.TOPIC_CREATE,
|
|
985
|
+
payload: { requestId, room: request.room, name: request.name },
|
|
986
|
+
timestamp: Date.now(),
|
|
987
|
+
});
|
|
988
|
+
const result = await client.waitFor(RoomServerMessageType.TOPIC, (msg) => msg.payload.requestId === requestId, undefined, requestId);
|
|
989
|
+
return { auth, topic: result.payload.topic };
|
|
990
|
+
}
|
|
991
|
+
finally {
|
|
992
|
+
client.close();
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
export async function listRoomTopics(config, request) {
|
|
996
|
+
if (!config.webSocketCtor) {
|
|
997
|
+
try {
|
|
998
|
+
return await listRoomTopicsViaHttp(config, request);
|
|
999
|
+
}
|
|
1000
|
+
catch (error) {
|
|
1001
|
+
if (!shouldFallbackToRoomsWebSocket(error))
|
|
1002
|
+
throw error;
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
// Room-scoped: the relay's handleTopicList uses the same read-target
|
|
1006
|
+
// room-membership check as other room reads (assertRoomReadTarget), so
|
|
1007
|
+
// the caller must join first, same as createRoomTopic above.
|
|
1008
|
+
const client = new RoomWsClient(config);
|
|
1009
|
+
try {
|
|
1010
|
+
const auth = await client.connect();
|
|
1011
|
+
await joinRoomOnClient(client, { room: request.room });
|
|
1012
|
+
const requestId = randomUUID();
|
|
1013
|
+
client.send({
|
|
1014
|
+
type: RoomClientMessageType.TOPIC_LIST,
|
|
1015
|
+
payload: {
|
|
1016
|
+
requestId,
|
|
1017
|
+
room: request.room,
|
|
1018
|
+
...(request.includeResolved !== undefined
|
|
1019
|
+
? { includeResolved: request.includeResolved }
|
|
1020
|
+
: {}),
|
|
1021
|
+
},
|
|
1022
|
+
timestamp: Date.now(),
|
|
1023
|
+
});
|
|
1024
|
+
const result = await client.waitFor(RoomServerMessageType.TOPICS, (msg) => msg.payload.requestId === requestId, undefined, requestId);
|
|
1025
|
+
return { auth, topics: result.payload.topics };
|
|
1026
|
+
}
|
|
1027
|
+
finally {
|
|
1028
|
+
client.close();
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
export async function renameRoomTopic(config, request) {
|
|
1032
|
+
return topicResultViaWebSocket(config, (requestId) => ({
|
|
1033
|
+
type: RoomClientMessageType.TOPIC_RENAME,
|
|
1034
|
+
payload: { requestId, topicId: request.topicId, name: request.name },
|
|
1035
|
+
timestamp: Date.now(),
|
|
1036
|
+
}));
|
|
1037
|
+
}
|
|
1038
|
+
export async function moveRoomTopicMessages(config, request) {
|
|
1039
|
+
return topicResultViaWebSocket(config, (requestId) => ({
|
|
1040
|
+
type: RoomClientMessageType.TOPIC_MOVE,
|
|
1041
|
+
payload: {
|
|
1042
|
+
requestId,
|
|
1043
|
+
topicId: request.topicId,
|
|
1044
|
+
seqs: request.seqs,
|
|
1045
|
+
toTopicId: request.toTopicId,
|
|
1046
|
+
},
|
|
1047
|
+
timestamp: Date.now(),
|
|
1048
|
+
}));
|
|
1049
|
+
}
|
|
1050
|
+
export async function mergeRoomTopics(config, request) {
|
|
1051
|
+
return topicResultViaWebSocket(config, (requestId) => ({
|
|
1052
|
+
type: RoomClientMessageType.TOPIC_MERGE,
|
|
1053
|
+
payload: { requestId, topicId: request.topicId, intoTopicId: request.intoTopicId },
|
|
1054
|
+
timestamp: Date.now(),
|
|
1055
|
+
}));
|
|
1056
|
+
}
|
|
1057
|
+
export async function resolveRoomTopic(config, request) {
|
|
1058
|
+
return topicResultViaWebSocket(config, (requestId) => ({
|
|
1059
|
+
type: RoomClientMessageType.TOPIC_RESOLVE,
|
|
1060
|
+
payload: {
|
|
1061
|
+
requestId,
|
|
1062
|
+
topicId: request.topicId,
|
|
1063
|
+
...(request.resolved !== undefined ? { resolved: request.resolved } : {}),
|
|
1064
|
+
},
|
|
1065
|
+
timestamp: Date.now(),
|
|
1066
|
+
}));
|
|
1067
|
+
}
|
|
1068
|
+
export async function setRoomTopicAttention(config, request) {
|
|
1069
|
+
return topicResultViaWebSocket(config, (requestId) => ({
|
|
1070
|
+
type: RoomClientMessageType.TOPIC_SET_ATTENTION,
|
|
1071
|
+
payload: { requestId, topicId: request.topicId, attention: request.attention },
|
|
1072
|
+
timestamp: Date.now(),
|
|
1073
|
+
}));
|
|
1074
|
+
}
|
|
1075
|
+
/** Send one topic verb frame and resolve on the TOPIC result that echoes
|
|
1076
|
+
* the requestId (every mutation verb replies TOPIC; only list replies
|
|
1077
|
+
* TOPICS). The requestId is also passed to waitFor so a coded ERROR frame
|
|
1078
|
+
* for this request rejects instead of timing out. */
|
|
1079
|
+
async function topicResultViaWebSocket(config, frame) {
|
|
1080
|
+
const client = new RoomWsClient(config);
|
|
1081
|
+
try {
|
|
1082
|
+
const auth = await client.connect();
|
|
1083
|
+
const requestId = randomUUID();
|
|
1084
|
+
client.send(frame(requestId));
|
|
1085
|
+
const result = await client.waitFor(RoomServerMessageType.TOPIC, (msg) => msg.payload.requestId === requestId, undefined, requestId);
|
|
1086
|
+
return { auth, topic: result.payload.topic };
|
|
1087
|
+
}
|
|
1088
|
+
finally {
|
|
1089
|
+
client.close();
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
async function listRoomTopicsViaHttp(config, request) {
|
|
1093
|
+
const room = normalizeRoomName(request.room);
|
|
1094
|
+
const params = new URLSearchParams();
|
|
1095
|
+
if (request.includeResolved)
|
|
1096
|
+
params.set("includeResolved", "true");
|
|
1097
|
+
const result = await roomHttpRequest(config, `/rooms/${encodeURIComponent(room)}/topics${params.size ? `?${params.toString()}` : ""}`);
|
|
1098
|
+
return {
|
|
1099
|
+
auth: authFromApiKey(config.apiKey),
|
|
1100
|
+
topics: Array.isArray(result.topics) ? result.topics : [],
|
|
1101
|
+
};
|
|
1102
|
+
}
|
|
1103
|
+
export async function requestRoomTopicsOnClient(client, request) {
|
|
1104
|
+
const requestId = randomUUID();
|
|
1105
|
+
client.send({
|
|
1106
|
+
type: RoomClientMessageType.TOPIC_LIST,
|
|
1107
|
+
payload: {
|
|
1108
|
+
requestId,
|
|
1109
|
+
room: normalizeRoomName(request.room),
|
|
1110
|
+
...(request.includeResolved !== undefined
|
|
1111
|
+
? { includeResolved: request.includeResolved }
|
|
1112
|
+
: {}),
|
|
1113
|
+
...(request.withState ? { withState: true } : {}),
|
|
1114
|
+
},
|
|
1115
|
+
timestamp: Date.now(),
|
|
1116
|
+
});
|
|
1117
|
+
const result = await client.waitFor(RoomServerMessageType.TOPICS, (msg) => msg.payload.requestId === requestId, undefined, requestId);
|
|
1118
|
+
return result.payload;
|
|
1119
|
+
}
|
|
1120
|
+
export async function createRoomTopicOnClient(client, request) {
|
|
1121
|
+
return topicResultOnClient(client, (requestId) => ({
|
|
1122
|
+
type: RoomClientMessageType.TOPIC_CREATE,
|
|
1123
|
+
payload: {
|
|
1124
|
+
requestId,
|
|
1125
|
+
room: normalizeRoomName(request.room),
|
|
1126
|
+
name: request.name,
|
|
1127
|
+
},
|
|
1128
|
+
timestamp: Date.now(),
|
|
1129
|
+
}));
|
|
1130
|
+
}
|
|
1131
|
+
export async function renameRoomTopicOnClient(client, request) {
|
|
1132
|
+
return topicResultOnClient(client, (requestId) => ({
|
|
1133
|
+
type: RoomClientMessageType.TOPIC_RENAME,
|
|
1134
|
+
payload: {
|
|
1135
|
+
requestId,
|
|
1136
|
+
topicId: request.topicId,
|
|
1137
|
+
name: request.name,
|
|
1138
|
+
},
|
|
1139
|
+
timestamp: Date.now(),
|
|
1140
|
+
}));
|
|
1141
|
+
}
|
|
1142
|
+
export async function setRoomTopicAttentionOnClient(client, request) {
|
|
1143
|
+
return topicResultOnClient(client, (requestId) => ({
|
|
1144
|
+
type: RoomClientMessageType.TOPIC_SET_ATTENTION,
|
|
1145
|
+
payload: {
|
|
1146
|
+
requestId,
|
|
1147
|
+
topicId: request.topicId,
|
|
1148
|
+
attention: request.attention,
|
|
1149
|
+
},
|
|
1150
|
+
timestamp: Date.now(),
|
|
1151
|
+
}));
|
|
1152
|
+
}
|
|
1153
|
+
export async function resolveRoomTopicOnClient(client, request) {
|
|
1154
|
+
return topicResultOnClient(client, (requestId) => ({
|
|
1155
|
+
type: RoomClientMessageType.TOPIC_RESOLVE,
|
|
1156
|
+
payload: {
|
|
1157
|
+
requestId,
|
|
1158
|
+
topicId: request.topicId,
|
|
1159
|
+
...(request.resolved !== undefined ? { resolved: request.resolved } : {}),
|
|
1160
|
+
},
|
|
1161
|
+
timestamp: Date.now(),
|
|
1162
|
+
}));
|
|
1163
|
+
}
|
|
1164
|
+
async function topicResultOnClient(client, frame) {
|
|
1165
|
+
const requestId = randomUUID();
|
|
1166
|
+
client.send(frame(requestId));
|
|
1167
|
+
const result = await client.waitFor(RoomServerMessageType.TOPIC, (msg) => msg.payload.requestId === requestId, undefined, requestId);
|
|
1168
|
+
return result.payload;
|
|
1169
|
+
}
|
|
1170
|
+
async function joinRoomOnClient(client, options) {
|
|
1171
|
+
const requestId = randomUUID();
|
|
1172
|
+
const renderedSuggestionAttribution = options.sinceSeq === undefined && !options.isRejoin && !options.hook && !options.suggestionId
|
|
1173
|
+
? consumeRoomSuggestionAttribution(options.room)
|
|
1174
|
+
: undefined;
|
|
1175
|
+
const hook = options.hook ?? renderedSuggestionAttribution?.hook;
|
|
1176
|
+
const suggestionId = options.suggestionId ?? renderedSuggestionAttribution?.suggestionId;
|
|
1177
|
+
client.send({
|
|
1178
|
+
type: RoomClientMessageType.JOIN,
|
|
1179
|
+
payload: {
|
|
1180
|
+
requestId,
|
|
1181
|
+
room: options.room,
|
|
1182
|
+
...(options.workspaceId ? { workspaceId: options.workspaceId } : {}),
|
|
1183
|
+
...(options.code ? { code: options.code } : {}),
|
|
1184
|
+
...(options.sinceSeq !== undefined ? { sinceSeq: options.sinceSeq } : {}),
|
|
1185
|
+
...(options.limit !== undefined ? { limit: options.limit } : {}),
|
|
1186
|
+
...(options.reason ? { reason: options.reason } : {}),
|
|
1187
|
+
...(options.source ? { source: options.source } : {}),
|
|
1188
|
+
...(hook ? { hook } : {}),
|
|
1189
|
+
...(suggestionId ? { suggestionId } : {}),
|
|
1190
|
+
},
|
|
1191
|
+
timestamp: Date.now(),
|
|
1192
|
+
});
|
|
1193
|
+
const backfill = await client.waitFor(RoomServerMessageType.BACKFILL, (msg) => msg.payload.requestId === requestId ||
|
|
1194
|
+
(!msg.payload.requestId && msg.payload.room === normalizeRoomName(options.room)), undefined, requestId);
|
|
1195
|
+
return backfill.payload;
|
|
1196
|
+
}
|
|
1197
|
+
function normalizeRoomName(room) {
|
|
1198
|
+
return room.trim().replace(/^#/, "").toLowerCase();
|
|
1199
|
+
}
|
|
1200
|
+
async function listRoomsViaHttp(config, request) {
|
|
1201
|
+
const params = new URLSearchParams();
|
|
1202
|
+
if (request.query)
|
|
1203
|
+
params.set("query", request.query);
|
|
1204
|
+
if (request.scope)
|
|
1205
|
+
params.set("scope", request.scope);
|
|
1206
|
+
appendAll(params, "category", request.category);
|
|
1207
|
+
appendAll(params, "tag", request.tag);
|
|
1208
|
+
appendAll(params, "kind", request.kind);
|
|
1209
|
+
appendAll(params, "entity", request.entity);
|
|
1210
|
+
appendAll(params, "symbol", request.symbol);
|
|
1211
|
+
appendAll(params, "spaceId", request.spaceId);
|
|
1212
|
+
if (request.hot)
|
|
1213
|
+
params.set("hot", "true");
|
|
1214
|
+
if (request.includeHeat)
|
|
1215
|
+
params.set("includeHeat", "true");
|
|
1216
|
+
if (request.createdByMe)
|
|
1217
|
+
params.set("createdByMe", "true");
|
|
1218
|
+
if (request.includeArchived)
|
|
1219
|
+
params.set("includeArchived", "true");
|
|
1220
|
+
if (request.limit !== undefined)
|
|
1221
|
+
params.set("limit", String(request.limit));
|
|
1222
|
+
const result = await roomHttpRequest(config, `/rooms${params.size ? `?${params.toString()}` : ""}`);
|
|
1223
|
+
return {
|
|
1224
|
+
auth: authFromApiKey(config.apiKey),
|
|
1225
|
+
rooms: Array.isArray(result.rooms) ? result.rooms : [],
|
|
1226
|
+
};
|
|
1227
|
+
}
|
|
1228
|
+
async function peekRoomViaHttp(config, options) {
|
|
1229
|
+
const params = new URLSearchParams();
|
|
1230
|
+
if (options.beforeSeq !== undefined)
|
|
1231
|
+
params.set("beforeSeq", String(options.beforeSeq));
|
|
1232
|
+
if (options.sinceSeq !== undefined)
|
|
1233
|
+
params.set("sinceSeq", String(options.sinceSeq));
|
|
1234
|
+
if (options.atMs !== undefined)
|
|
1235
|
+
params.set("atMs", String(options.atMs));
|
|
1236
|
+
if (options.limit !== undefined)
|
|
1237
|
+
params.set("limit", String(options.limit));
|
|
1238
|
+
const result = await roomHttpRequest(config, `/rooms/${encodeURIComponent(normalizeRoomName(options.room))}/events${params.size ? `?${params.toString()}` : ""}`);
|
|
1239
|
+
return {
|
|
1240
|
+
room: result.room || normalizeRoomName(options.room),
|
|
1241
|
+
entries: Array.isArray(result.entries) ? result.entries : [],
|
|
1242
|
+
upToSeq: typeof result.upToSeq === "number" ? result.upToSeq : options.sinceSeq || 0,
|
|
1243
|
+
hasMore: Boolean(result.hasMore),
|
|
1244
|
+
...(result.readOnly !== undefined ? { readOnly: result.readOnly } : {}),
|
|
1245
|
+
...(result.beforeSeq !== undefined ? { beforeSeq: result.beforeSeq } : {}),
|
|
1246
|
+
...(result.sinceSeq !== undefined ? { sinceSeq: result.sinceSeq } : {}),
|
|
1247
|
+
...(result.atMs !== undefined ? { atMs: result.atMs } : {}),
|
|
1248
|
+
};
|
|
1249
|
+
}
|
|
1250
|
+
async function createRoomViaHttp(config, request) {
|
|
1251
|
+
const result = await roomHttpRequest(config, "/rooms", {
|
|
1252
|
+
method: "POST",
|
|
1253
|
+
body: request,
|
|
1254
|
+
});
|
|
1255
|
+
if (!result.room)
|
|
1256
|
+
throw new RoomHttpError("chat service returned no room", 502);
|
|
1257
|
+
return {
|
|
1258
|
+
auth: authFromApiKey(config.apiKey),
|
|
1259
|
+
room: result.room,
|
|
1260
|
+
...(result.joinCode ? { joinCode: result.joinCode } : {}),
|
|
1261
|
+
};
|
|
1262
|
+
}
|
|
1263
|
+
async function inviteRoomViaHttp(config, request) {
|
|
1264
|
+
const room = normalizeRoomName(request.room);
|
|
1265
|
+
const result = await roomHttpRequest(config, `/rooms/${encodeURIComponent(room)}/invites`, {
|
|
1266
|
+
method: "POST",
|
|
1267
|
+
body: {
|
|
1268
|
+
...(request.username ? { username: request.username } : {}),
|
|
1269
|
+
...(request.userId ? { userId: request.userId } : {}),
|
|
1270
|
+
},
|
|
1271
|
+
});
|
|
1272
|
+
const invitedUserId = result.invitedUserId;
|
|
1273
|
+
const invitedHandle = result.invitedHandle;
|
|
1274
|
+
if (!invitedUserId || !invitedHandle) {
|
|
1275
|
+
throw new RoomHttpError("chat service returned no invited member", 502);
|
|
1276
|
+
}
|
|
1277
|
+
return {
|
|
1278
|
+
auth: authFromApiKey(config.apiKey),
|
|
1279
|
+
invite: {
|
|
1280
|
+
room: result.room || room,
|
|
1281
|
+
invitedUserId,
|
|
1282
|
+
invitedHandle,
|
|
1283
|
+
},
|
|
1284
|
+
};
|
|
1285
|
+
}
|
|
1286
|
+
async function removeRoomMemberViaHttp(config, request) {
|
|
1287
|
+
const room = normalizeRoomName(request.room);
|
|
1288
|
+
const result = await roomHttpRequest(config, `/rooms/${encodeURIComponent(room)}/members/remove`, {
|
|
1289
|
+
method: "POST",
|
|
1290
|
+
body: {
|
|
1291
|
+
...(request.username ? { username: request.username } : {}),
|
|
1292
|
+
...(request.userId ? { userId: request.userId } : {}),
|
|
1293
|
+
},
|
|
1294
|
+
});
|
|
1295
|
+
const removedUserId = result.removedUserId;
|
|
1296
|
+
const removedHandle = result.removedHandle;
|
|
1297
|
+
if (!removedUserId || !removedHandle) {
|
|
1298
|
+
throw new RoomHttpError("chat service returned no removed member", 502);
|
|
1299
|
+
}
|
|
1300
|
+
return {
|
|
1301
|
+
auth: authFromApiKey(config.apiKey),
|
|
1302
|
+
removal: {
|
|
1303
|
+
room: result.room || room,
|
|
1304
|
+
removedUserId,
|
|
1305
|
+
removedHandle,
|
|
1306
|
+
},
|
|
1307
|
+
};
|
|
1308
|
+
}
|
|
1309
|
+
async function roomHttpRequest(config, path, init = {}) {
|
|
1310
|
+
const url = `${resolveRoomChatApiUrl(config)}${path}`;
|
|
1311
|
+
const signal = init.signal ?? roomHttpTimeoutSignal(config.timeoutMs);
|
|
1312
|
+
let res;
|
|
1313
|
+
try {
|
|
1314
|
+
res = await fetch(url, {
|
|
1315
|
+
method: init.method ?? "GET",
|
|
1316
|
+
...(signal ? { signal } : {}),
|
|
1317
|
+
headers: {
|
|
1318
|
+
Authorization: `Bearer ${config.apiKey}`,
|
|
1319
|
+
Accept: "application/json",
|
|
1320
|
+
...(init.body !== undefined ? { "Content-Type": "application/json" } : {}),
|
|
1321
|
+
},
|
|
1322
|
+
...(init.body !== undefined ? { body: JSON.stringify(init.body) } : {}),
|
|
1323
|
+
});
|
|
1324
|
+
}
|
|
1325
|
+
catch (error) {
|
|
1326
|
+
throw new RoomHttpError(error instanceof Error ? error.message : String(error), 0);
|
|
1327
|
+
}
|
|
1328
|
+
const text = await res.text();
|
|
1329
|
+
let body = {};
|
|
1330
|
+
if (text) {
|
|
1331
|
+
try {
|
|
1332
|
+
body = JSON.parse(text);
|
|
1333
|
+
}
|
|
1334
|
+
catch {
|
|
1335
|
+
body = {
|
|
1336
|
+
message: text
|
|
1337
|
+
.replace(/<[^>]*>/g, " ")
|
|
1338
|
+
.replace(/\s+/g, " ")
|
|
1339
|
+
.trim(),
|
|
1340
|
+
};
|
|
1341
|
+
}
|
|
1342
|
+
}
|
|
1343
|
+
if (!res.ok) {
|
|
1344
|
+
throw new RoomHttpError(errorMessageFromBody(body) || `chat service returned ${res.status}`, res.status);
|
|
1345
|
+
}
|
|
1346
|
+
return (body ?? {});
|
|
1347
|
+
}
|
|
1348
|
+
function roomHttpTimeoutSignal(timeoutMs) {
|
|
1349
|
+
if (!timeoutMs || !Number.isFinite(timeoutMs) || timeoutMs <= 0)
|
|
1350
|
+
return undefined;
|
|
1351
|
+
return AbortSignal.timeout(timeoutMs);
|
|
1352
|
+
}
|
|
1353
|
+
class RoomHttpError extends Error {
|
|
1354
|
+
status;
|
|
1355
|
+
name = "RoomHttpError";
|
|
1356
|
+
constructor(message, status) {
|
|
1357
|
+
super(message);
|
|
1358
|
+
this.status = status;
|
|
1359
|
+
}
|
|
1360
|
+
}
|
|
1361
|
+
function shouldFallbackToRoomsWebSocket(error) {
|
|
1362
|
+
return error instanceof RoomHttpError && (error.status === 0 || error.status === 404);
|
|
1363
|
+
}
|
|
1364
|
+
export function resolveRoomChatApiUrl(config = {}) {
|
|
1365
|
+
if (config.apiUrl?.trim())
|
|
1366
|
+
return trimTrailingSlash(config.apiUrl.trim());
|
|
1367
|
+
const explicit = envString("OM_ROOM_CHAT_API_URL")?.trim() || envString("OM_CHAT_API_URL")?.trim();
|
|
1368
|
+
if (explicit)
|
|
1369
|
+
return trimTrailingSlash(explicit);
|
|
1370
|
+
try {
|
|
1371
|
+
const url = new URL(config.url || resolveRoomsWsUrl());
|
|
1372
|
+
url.protocol = url.protocol === "wss:" ? "https:" : "http:";
|
|
1373
|
+
url.pathname = "/chat";
|
|
1374
|
+
url.search = "";
|
|
1375
|
+
url.hash = "";
|
|
1376
|
+
return trimTrailingSlash(url.toString());
|
|
1377
|
+
}
|
|
1378
|
+
catch {
|
|
1379
|
+
return trimTrailingSlash(ROOM_CHAT_API_URL);
|
|
1380
|
+
}
|
|
1381
|
+
}
|
|
1382
|
+
function appendAll(params, key, values) {
|
|
1383
|
+
for (const value of values || []) {
|
|
1384
|
+
if (value)
|
|
1385
|
+
params.append(key, value);
|
|
1386
|
+
}
|
|
1387
|
+
}
|
|
1388
|
+
function errorMessageFromBody(body) {
|
|
1389
|
+
if (!body || typeof body !== "object")
|
|
1390
|
+
return null;
|
|
1391
|
+
const record = body;
|
|
1392
|
+
return typeof record.message === "string"
|
|
1393
|
+
? record.message
|
|
1394
|
+
: typeof record.error === "string"
|
|
1395
|
+
? record.error
|
|
1396
|
+
: null;
|
|
1397
|
+
}
|
|
1398
|
+
function trimTrailingSlash(value) {
|
|
1399
|
+
return value.replace(/\/+$/, "");
|
|
1400
|
+
}
|
|
1401
|
+
export function normalizePresence(payload) {
|
|
1402
|
+
const sample = payload.sample ?? payload.members ?? [];
|
|
1403
|
+
const total = payload.total ?? payload.humans + payload.agents;
|
|
1404
|
+
return {
|
|
1405
|
+
...payload,
|
|
1406
|
+
total,
|
|
1407
|
+
sample,
|
|
1408
|
+
members: payload.members ?? sample,
|
|
1409
|
+
};
|
|
1410
|
+
}
|
|
1411
|
+
export function presenceFromWho(payload) {
|
|
1412
|
+
const sampleAgents = payload.members.filter((member) => member.isAgent).length;
|
|
1413
|
+
const agents = payload.agents ?? sampleAgents;
|
|
1414
|
+
const humans = payload.humans ??
|
|
1415
|
+
(payload.total > payload.members.length
|
|
1416
|
+
? Math.max(0, payload.total - agents)
|
|
1417
|
+
: payload.members.filter((member) => !member.isAgent).length);
|
|
1418
|
+
return {
|
|
1419
|
+
room: payload.room,
|
|
1420
|
+
members: payload.members,
|
|
1421
|
+
sample: payload.members,
|
|
1422
|
+
humans,
|
|
1423
|
+
agents,
|
|
1424
|
+
total: payload.total,
|
|
1425
|
+
...(payload.guestCount !== undefined ? { guestCount: payload.guestCount } : {}),
|
|
1426
|
+
...(payload.requestId ? { requestId: payload.requestId } : {}),
|
|
1427
|
+
};
|
|
1428
|
+
}
|