@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
|
@@ -0,0 +1,491 @@
|
|
|
1
|
+
import { RoomClientMessageType, RoomServerMessageType, SUPPORTED_ROOM_PROTOCOL_VERSION, } from "./types.js";
|
|
2
|
+
import { VERSION } from "./version.js";
|
|
3
|
+
export class RoomProtocolError extends Error {
|
|
4
|
+
name = "RoomProtocolError";
|
|
5
|
+
code;
|
|
6
|
+
details;
|
|
7
|
+
requestId;
|
|
8
|
+
constructor(payload) {
|
|
9
|
+
super(payload.message);
|
|
10
|
+
this.code = payload.code;
|
|
11
|
+
if (payload.details !== undefined)
|
|
12
|
+
this.details = payload.details;
|
|
13
|
+
if (payload.requestId)
|
|
14
|
+
this.requestId = payload.requestId;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
export class RoomTransportError extends Error {
|
|
18
|
+
name = "RoomTransportError";
|
|
19
|
+
}
|
|
20
|
+
export class RoomWsClient {
|
|
21
|
+
url;
|
|
22
|
+
// Mutable: a guest session re-mints its key via refreshAuthToken.
|
|
23
|
+
apiKey;
|
|
24
|
+
agentIdentity;
|
|
25
|
+
webSocketCtor;
|
|
26
|
+
timeoutMs;
|
|
27
|
+
ws = null;
|
|
28
|
+
auth = null;
|
|
29
|
+
serverProtocolVersionValue = null;
|
|
30
|
+
listeners = new Set();
|
|
31
|
+
connectionListeners = new Set();
|
|
32
|
+
pending = [];
|
|
33
|
+
connected = false;
|
|
34
|
+
reconnectEnabled;
|
|
35
|
+
reconnectBaseMs;
|
|
36
|
+
reconnectMaxMs;
|
|
37
|
+
reconnectHerdJitterMs;
|
|
38
|
+
reconnectMaxAttempts;
|
|
39
|
+
refreshAuthToken;
|
|
40
|
+
onProtocolMismatch;
|
|
41
|
+
reconnectAttempts = 0;
|
|
42
|
+
reconnectTimer = null;
|
|
43
|
+
closedByClient = false;
|
|
44
|
+
authRefreshRequested = false;
|
|
45
|
+
authRefreshInFlight = false;
|
|
46
|
+
/** True after a successful re-mint whose refreshed attempt hasn't authed yet.
|
|
47
|
+
* Caps the mint-succeeds-but-server-rejects cycle (config skew) at ONE
|
|
48
|
+
* refreshed attempt per episode — without it, openSocket → fail → re-mint →
|
|
49
|
+
* openSocket recurses in a hot loop with no backoff. Reset on AUTH_SUCCESS.
|
|
50
|
+
* A FAILED re-mint does not set it: that path already backs off through
|
|
51
|
+
* maybeReconnect, so transient auth-service outages keep retrying. */
|
|
52
|
+
authRefreshAttempted = false;
|
|
53
|
+
constructor(options) {
|
|
54
|
+
this.url = options.url;
|
|
55
|
+
this.apiKey = options.apiKey;
|
|
56
|
+
this.agentIdentity = options.agentIdentity;
|
|
57
|
+
this.webSocketCtor = options.webSocketCtor ?? globalThis.WebSocket;
|
|
58
|
+
this.timeoutMs = options.timeoutMs ?? 15_000;
|
|
59
|
+
this.reconnectEnabled = options.reconnect ?? false;
|
|
60
|
+
this.reconnectBaseMs = options.reconnectBaseMs ?? 500;
|
|
61
|
+
this.reconnectMaxMs = options.reconnectMaxMs ?? 15_000;
|
|
62
|
+
this.reconnectHerdJitterMs = options.reconnectHerdJitterMs ?? 5_000;
|
|
63
|
+
this.reconnectMaxAttempts = options.reconnectMaxAttempts ?? Number.POSITIVE_INFINITY;
|
|
64
|
+
this.refreshAuthToken = options.refreshAuthToken;
|
|
65
|
+
this.onProtocolMismatch = options.onProtocolMismatch;
|
|
66
|
+
}
|
|
67
|
+
/** Subscribe to connection-state transitions (reconnecting/reconnected/failed). */
|
|
68
|
+
onConnectionChange(listener) {
|
|
69
|
+
this.connectionListeners.add(listener);
|
|
70
|
+
return () => {
|
|
71
|
+
this.connectionListeners.delete(listener);
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
emitConnectionState(state, detail) {
|
|
75
|
+
for (const listener of this.connectionListeners) {
|
|
76
|
+
try {
|
|
77
|
+
listener(state, detail);
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
// A listener error must not break reconnection.
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
connect() {
|
|
85
|
+
if (this.ws) {
|
|
86
|
+
if (this.auth)
|
|
87
|
+
return Promise.resolve(this.auth);
|
|
88
|
+
return this.waitFor(RoomServerMessageType.AUTH_SUCCESS).then((msg) => msg.payload);
|
|
89
|
+
}
|
|
90
|
+
this.closedByClient = false;
|
|
91
|
+
return this.openSocket();
|
|
92
|
+
}
|
|
93
|
+
openSocket() {
|
|
94
|
+
// Fence any prior socket so a slow-to-die predecessor (e.g. one whose auth
|
|
95
|
+
// timed out while still connecting) can't feed record() or null out the
|
|
96
|
+
// fresh socket. Handlers below no-op unless they belong to the current ws.
|
|
97
|
+
const previous = this.ws;
|
|
98
|
+
if (previous) {
|
|
99
|
+
this.ws = null;
|
|
100
|
+
try {
|
|
101
|
+
previous.close(1000, "superseded");
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
// best-effort
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
let ws;
|
|
108
|
+
try {
|
|
109
|
+
ws = new this.webSocketCtor(this.url);
|
|
110
|
+
}
|
|
111
|
+
catch (err) {
|
|
112
|
+
throw new RoomTransportError(errorMessage(err));
|
|
113
|
+
}
|
|
114
|
+
this.ws = ws;
|
|
115
|
+
ws.addEventListener("open", () => {
|
|
116
|
+
if (this.ws !== ws)
|
|
117
|
+
return;
|
|
118
|
+
this.connected = true;
|
|
119
|
+
this.send({
|
|
120
|
+
type: RoomClientMessageType.AUTHENTICATE,
|
|
121
|
+
payload: {
|
|
122
|
+
token: this.apiKey,
|
|
123
|
+
protocolVersion: SUPPORTED_ROOM_PROTOCOL_VERSION,
|
|
124
|
+
// Sidekick sessions assert their agenthood and display name;
|
|
125
|
+
// ghost and user sessions send the bare frame unchanged.
|
|
126
|
+
...(this.agentIdentity ? { isAgent: true, username: this.agentIdentity.name } : {}),
|
|
127
|
+
},
|
|
128
|
+
timestamp: Date.now(),
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
ws.addEventListener("message", (event) => {
|
|
132
|
+
if (this.ws !== ws)
|
|
133
|
+
return;
|
|
134
|
+
const data = event.data;
|
|
135
|
+
if (typeof data !== "string" || data.length === 0)
|
|
136
|
+
return;
|
|
137
|
+
let parsed;
|
|
138
|
+
try {
|
|
139
|
+
parsed = JSON.parse(data);
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
this.rejectAll(new RoomTransportError("rooms ws: dropped non-json frame"));
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
if (!isRoomShapedFrame(parsed)) {
|
|
146
|
+
this.rejectAll(new RoomTransportError("rooms ws: dropped malformed frame"));
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
if (!isRoomServerMessage(parsed)) {
|
|
150
|
+
// Additive-only protocol: a newer relay may send frame types this
|
|
151
|
+
// client does not know yet. Ignore them; never fail the session.
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
this.record(parsed);
|
|
155
|
+
});
|
|
156
|
+
ws.addEventListener("close", (event) => {
|
|
157
|
+
// A superseded socket's close must not touch current state or trigger
|
|
158
|
+
// another reconnect.
|
|
159
|
+
if (this.ws !== ws)
|
|
160
|
+
return;
|
|
161
|
+
const e = event;
|
|
162
|
+
const hadAuth = this.auth !== null;
|
|
163
|
+
this.connected = false;
|
|
164
|
+
this.auth = null;
|
|
165
|
+
this.ws = null;
|
|
166
|
+
if (!this.closedByClient && !hadAuth && e.code === 1008 && this.refreshAuthToken) {
|
|
167
|
+
this.authRefreshRequested = true;
|
|
168
|
+
}
|
|
169
|
+
this.rejectAll(new RoomTransportError(`rooms ws closed${e.code ? ` (${e.code})` : ""}${e.reason ? `: ${e.reason}` : ""}`));
|
|
170
|
+
this.maybeReconnect(e.reason || (e.code ? `code ${e.code}` : undefined));
|
|
171
|
+
});
|
|
172
|
+
ws.addEventListener("error", () => {
|
|
173
|
+
if (this.ws !== ws)
|
|
174
|
+
return;
|
|
175
|
+
this.rejectAll(new RoomTransportError("rooms ws error"));
|
|
176
|
+
});
|
|
177
|
+
return this.waitFor(RoomServerMessageType.AUTH_SUCCESS)
|
|
178
|
+
.then((msg) => {
|
|
179
|
+
this.reconnectAttempts = 0;
|
|
180
|
+
this.authRefreshAttempted = false;
|
|
181
|
+
return msg.payload;
|
|
182
|
+
})
|
|
183
|
+
.catch(async (error) => this.retryGuestAuthentication(error));
|
|
184
|
+
}
|
|
185
|
+
maybeReconnect(reason) {
|
|
186
|
+
if (!this.reconnectEnabled ||
|
|
187
|
+
this.closedByClient ||
|
|
188
|
+
this.authRefreshRequested ||
|
|
189
|
+
this.authRefreshInFlight) {
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
if (this.reconnectTimer)
|
|
193
|
+
return;
|
|
194
|
+
if (this.reconnectAttempts >= this.reconnectMaxAttempts) {
|
|
195
|
+
this.emitConnectionState("failed", {
|
|
196
|
+
attempt: this.reconnectAttempts,
|
|
197
|
+
...(reason ? { error: reason } : {}),
|
|
198
|
+
});
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
const attempt = this.reconnectAttempts + 1;
|
|
202
|
+
this.reconnectAttempts = attempt;
|
|
203
|
+
// Exponential backoff with full jitter, capped.
|
|
204
|
+
const ceiling = Math.min(this.reconnectMaxMs, this.reconnectBaseMs * 2 ** (attempt - 1));
|
|
205
|
+
const delay = Math.floor(ceiling * (0.5 + Math.random() * 0.5) + Math.random() * this.reconnectHerdJitterMs);
|
|
206
|
+
this.emitConnectionState("reconnecting", {
|
|
207
|
+
attempt,
|
|
208
|
+
...(reason ? { error: reason } : {}),
|
|
209
|
+
});
|
|
210
|
+
this.reconnectTimer = setTimeout(() => {
|
|
211
|
+
this.reconnectTimer = null;
|
|
212
|
+
if (this.closedByClient)
|
|
213
|
+
return;
|
|
214
|
+
try {
|
|
215
|
+
this.openSocket()
|
|
216
|
+
.then(() => {
|
|
217
|
+
this.emitConnectionState("reconnected", { attempt });
|
|
218
|
+
})
|
|
219
|
+
.catch((err) => {
|
|
220
|
+
// Re-auth failed; drive the next backoff attempt.
|
|
221
|
+
this.maybeReconnect(errorMessage(err));
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
catch (err) {
|
|
225
|
+
// openSocket can throw synchronously (constructor failure) before it
|
|
226
|
+
// returns a promise — that must not escape the timer as an uncaught
|
|
227
|
+
// exception. Fold it back into the backoff loop.
|
|
228
|
+
this.maybeReconnect(errorMessage(err));
|
|
229
|
+
}
|
|
230
|
+
}, delay);
|
|
231
|
+
if (typeof this.reconnectTimer.unref === "function") {
|
|
232
|
+
this.reconnectTimer.unref();
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
send(msg) {
|
|
236
|
+
if (!this.ws || !this.connected) {
|
|
237
|
+
throw new RoomTransportError("rooms ws: not open");
|
|
238
|
+
}
|
|
239
|
+
this.ws.send(JSON.stringify(msg));
|
|
240
|
+
}
|
|
241
|
+
waitFor(type, predicate, timeoutMs = this.timeoutMs, requestId) {
|
|
242
|
+
return new Promise((resolve, reject) => {
|
|
243
|
+
let pending;
|
|
244
|
+
const timer = setTimeout(() => {
|
|
245
|
+
this.pending = this.pending.filter((wait) => wait !== pending);
|
|
246
|
+
reject(new RoomTransportError(`timed out waiting for ${type}`));
|
|
247
|
+
}, timeoutMs);
|
|
248
|
+
pending = {
|
|
249
|
+
type,
|
|
250
|
+
...(predicate
|
|
251
|
+
? { predicate: (msg) => predicate(msg) }
|
|
252
|
+
: {}),
|
|
253
|
+
...(requestId ? { requestId } : {}),
|
|
254
|
+
resolve: (msg) => resolve(msg),
|
|
255
|
+
reject,
|
|
256
|
+
timer,
|
|
257
|
+
};
|
|
258
|
+
this.pending.push(pending);
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
waitForAny(types, predicate, timeoutMs = this.timeoutMs, requestId) {
|
|
262
|
+
return new Promise((resolve, reject) => {
|
|
263
|
+
let pending;
|
|
264
|
+
const timer = setTimeout(() => {
|
|
265
|
+
this.pending = this.pending.filter((wait) => wait !== pending);
|
|
266
|
+
reject(new RoomTransportError(`timed out waiting for ${types.join(" or ")}`));
|
|
267
|
+
}, timeoutMs);
|
|
268
|
+
const typeSet = new Set(types);
|
|
269
|
+
pending = {
|
|
270
|
+
type: types[0],
|
|
271
|
+
types: typeSet,
|
|
272
|
+
predicate: (msg) => typeSet.has(msg.type) &&
|
|
273
|
+
(!predicate || predicate(msg)),
|
|
274
|
+
...(requestId ? { requestId } : {}),
|
|
275
|
+
resolve: (msg) => resolve(msg),
|
|
276
|
+
reject,
|
|
277
|
+
timer,
|
|
278
|
+
};
|
|
279
|
+
this.pending.push(pending);
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
onMessage(listener) {
|
|
283
|
+
this.listeners.add(listener);
|
|
284
|
+
return () => {
|
|
285
|
+
this.listeners.delete(listener);
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
close() {
|
|
289
|
+
this.closedByClient = true;
|
|
290
|
+
if (this.reconnectTimer) {
|
|
291
|
+
clearTimeout(this.reconnectTimer);
|
|
292
|
+
this.reconnectTimer = null;
|
|
293
|
+
}
|
|
294
|
+
const ws = this.ws;
|
|
295
|
+
this.ws = null;
|
|
296
|
+
this.auth = null;
|
|
297
|
+
this.connected = false;
|
|
298
|
+
this.rejectAll(new RoomTransportError("rooms ws closed by client"));
|
|
299
|
+
ws?.close(1000, "client closed");
|
|
300
|
+
}
|
|
301
|
+
waiterHandledErrors = new WeakSet();
|
|
302
|
+
serverProtocolVersion() {
|
|
303
|
+
return this.serverProtocolVersionValue;
|
|
304
|
+
}
|
|
305
|
+
isGuest() {
|
|
306
|
+
return this.auth?.isGuest === true;
|
|
307
|
+
}
|
|
308
|
+
record(msg) {
|
|
309
|
+
if (msg.type === RoomServerMessageType.AUTH_SUCCESS) {
|
|
310
|
+
if (!this.handleAuthSuccess(msg.payload))
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
if (msg.type === RoomServerMessageType.ERROR) {
|
|
314
|
+
if (!this.auth &&
|
|
315
|
+
this.refreshAuthToken &&
|
|
316
|
+
isAuthenticationError(msg.payload.code) &&
|
|
317
|
+
!this.closedByClient) {
|
|
318
|
+
this.authRefreshRequested = true;
|
|
319
|
+
}
|
|
320
|
+
const err = new RoomProtocolError(msg.payload);
|
|
321
|
+
if (msg.payload.requestId) {
|
|
322
|
+
if (this.rejectByRequestId(msg.payload.requestId, err)) {
|
|
323
|
+
// Precisely correlated: the request's caller owns this failure. Mark
|
|
324
|
+
// the frame so passive listeners (the tape pump) don't ALSO surface
|
|
325
|
+
// it — background requests against older relays degrade silently.
|
|
326
|
+
this.waiterHandledErrors.add(msg);
|
|
327
|
+
}
|
|
328
|
+
// A requestId we don't recognize is someone else's request (another
|
|
329
|
+
// session, a stale retry): never misattribute it to an unrelated
|
|
330
|
+
// waiter. Passive surfaces still see the frame.
|
|
331
|
+
}
|
|
332
|
+
else if (this.rejectOldest(err)) {
|
|
333
|
+
// Heuristic old-relay fallback: the oldest waiter got the rejection,
|
|
334
|
+
// but the correlation is a guess. Only hide pure protocol noise;
|
|
335
|
+
// user-actionable errors (MUTED, READ_ONLY, …) must stay visible.
|
|
336
|
+
if (msg.payload.code === "BAD_FRAME")
|
|
337
|
+
this.waiterHandledErrors.add(msg);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
else {
|
|
341
|
+
this.resolvePending(msg);
|
|
342
|
+
}
|
|
343
|
+
for (const listener of this.listeners)
|
|
344
|
+
listener(msg);
|
|
345
|
+
}
|
|
346
|
+
handleAuthSuccess(payload) {
|
|
347
|
+
this.auth = payload;
|
|
348
|
+
this.serverProtocolVersionValue = payload.protocolVersion;
|
|
349
|
+
if (payload.protocolVersion !== SUPPORTED_ROOM_PROTOCOL_VERSION) {
|
|
350
|
+
this.onProtocolMismatch?.({
|
|
351
|
+
supported: SUPPORTED_ROOM_PROTOCOL_VERSION,
|
|
352
|
+
server: payload.protocolVersion,
|
|
353
|
+
});
|
|
354
|
+
const err = new RoomTransportError(`rooms protocol mismatch: server v${payload.protocolVersion}, client supports v${SUPPORTED_ROOM_PROTOCOL_VERSION}`);
|
|
355
|
+
this.closedByClient = true;
|
|
356
|
+
this.rejectAll(err);
|
|
357
|
+
this.ws?.close(1002, "protocol mismatch");
|
|
358
|
+
return false;
|
|
359
|
+
}
|
|
360
|
+
if (!payload.minClientBuild ||
|
|
361
|
+
!isRoomClientBuildBelowMinimum(VERSION, payload.minClientBuild)) {
|
|
362
|
+
return true;
|
|
363
|
+
}
|
|
364
|
+
const err = new RoomTransportError(`rooms client upgrade required: server requires om v${payload.minClientBuild} or newer (this build is v${VERSION})`);
|
|
365
|
+
this.closedByClient = true;
|
|
366
|
+
this.rejectAll(err);
|
|
367
|
+
this.ws?.close(1008, "client upgrade required");
|
|
368
|
+
return false;
|
|
369
|
+
}
|
|
370
|
+
async retryGuestAuthentication(error) {
|
|
371
|
+
if (!this.authRefreshRequested || !this.refreshAuthToken || this.closedByClient) {
|
|
372
|
+
throw error;
|
|
373
|
+
}
|
|
374
|
+
if (this.authRefreshAttempted) {
|
|
375
|
+
// The previous re-mint SUCCEEDED and the server still refused the fresh
|
|
376
|
+
// key — retrying would mint in a hot loop. Fail the episode instead.
|
|
377
|
+
throw error;
|
|
378
|
+
}
|
|
379
|
+
this.authRefreshRequested = false;
|
|
380
|
+
this.authRefreshInFlight = true;
|
|
381
|
+
try {
|
|
382
|
+
this.apiKey = await this.refreshAuthToken();
|
|
383
|
+
this.authRefreshAttempted = true;
|
|
384
|
+
return await this.openSocket();
|
|
385
|
+
}
|
|
386
|
+
catch (refreshError) {
|
|
387
|
+
this.authRefreshInFlight = false;
|
|
388
|
+
this.maybeReconnect(errorMessage(refreshError));
|
|
389
|
+
throw refreshError;
|
|
390
|
+
}
|
|
391
|
+
finally {
|
|
392
|
+
this.authRefreshInFlight = false;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
resolvePending(msg) {
|
|
396
|
+
const remaining = [];
|
|
397
|
+
for (const wait of this.pending) {
|
|
398
|
+
if (wait.types ? !wait.types.has(msg.type) : wait.type !== msg.type) {
|
|
399
|
+
remaining.push(wait);
|
|
400
|
+
continue;
|
|
401
|
+
}
|
|
402
|
+
if (wait.predicate && !wait.predicate(msg)) {
|
|
403
|
+
remaining.push(wait);
|
|
404
|
+
continue;
|
|
405
|
+
}
|
|
406
|
+
clearTimeout(wait.timer);
|
|
407
|
+
wait.resolve(msg);
|
|
408
|
+
}
|
|
409
|
+
this.pending = remaining;
|
|
410
|
+
}
|
|
411
|
+
rejectAll(err) {
|
|
412
|
+
for (const wait of this.pending) {
|
|
413
|
+
clearTimeout(wait.timer);
|
|
414
|
+
wait.reject(err);
|
|
415
|
+
}
|
|
416
|
+
this.pending = [];
|
|
417
|
+
}
|
|
418
|
+
rejectByRequestId(requestId, err) {
|
|
419
|
+
if (!requestId)
|
|
420
|
+
return false;
|
|
421
|
+
const index = this.pending.findIndex((wait) => wait.requestId === requestId);
|
|
422
|
+
if (index < 0)
|
|
423
|
+
return false;
|
|
424
|
+
const [wait] = this.pending.splice(index, 1);
|
|
425
|
+
if (!wait)
|
|
426
|
+
return false;
|
|
427
|
+
clearTimeout(wait.timer);
|
|
428
|
+
wait.reject(err);
|
|
429
|
+
return true;
|
|
430
|
+
}
|
|
431
|
+
// Fallback for old relays that do not include requestId on ERROR frames.
|
|
432
|
+
rejectOldest(err) {
|
|
433
|
+
const wait = this.pending.shift();
|
|
434
|
+
if (!wait)
|
|
435
|
+
return false;
|
|
436
|
+
clearTimeout(wait.timer);
|
|
437
|
+
wait.reject(err);
|
|
438
|
+
return true;
|
|
439
|
+
}
|
|
440
|
+
/** Was this ERROR frame already delivered to a waiting request's caller? */
|
|
441
|
+
errorHandledByWaiter(msg) {
|
|
442
|
+
return this.waiterHandledErrors.has(msg);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
/** Returns false for an invalid server value so a typo cannot lock out every client. */
|
|
446
|
+
export function isRoomClientBuildBelowMinimum(clientBuild, minimumBuild) {
|
|
447
|
+
const client = parseBuild(clientBuild);
|
|
448
|
+
const minimum = parseBuild(minimumBuild);
|
|
449
|
+
if (!client || !minimum)
|
|
450
|
+
return false;
|
|
451
|
+
for (const index of [0, 1, 2]) {
|
|
452
|
+
const clientPart = client[index];
|
|
453
|
+
const minimumPart = minimum[index];
|
|
454
|
+
if (clientPart !== minimumPart)
|
|
455
|
+
return clientPart < minimumPart;
|
|
456
|
+
}
|
|
457
|
+
return false;
|
|
458
|
+
}
|
|
459
|
+
function parseBuild(value) {
|
|
460
|
+
const match = /^v?(\d+)(?:\.(\d+))?(?:\.(\d+))?$/.exec(value.trim());
|
|
461
|
+
if (!match)
|
|
462
|
+
return null;
|
|
463
|
+
const parts = [match[1], match[2] ?? "0", match[3] ?? "0"].map(Number);
|
|
464
|
+
if (parts.some((part) => !Number.isSafeInteger(part)))
|
|
465
|
+
return null;
|
|
466
|
+
return parts;
|
|
467
|
+
}
|
|
468
|
+
const KNOWN_SERVER_MESSAGE_TYPES = new Set(Object.values(RoomServerMessageType));
|
|
469
|
+
function isRoomShapedFrame(value) {
|
|
470
|
+
if (!value || typeof value !== "object")
|
|
471
|
+
return false;
|
|
472
|
+
const candidate = value;
|
|
473
|
+
if (typeof candidate.type !== "string")
|
|
474
|
+
return false;
|
|
475
|
+
if (typeof candidate.timestamp !== "number")
|
|
476
|
+
return false;
|
|
477
|
+
if (!candidate.payload || typeof candidate.payload !== "object")
|
|
478
|
+
return false;
|
|
479
|
+
return true;
|
|
480
|
+
}
|
|
481
|
+
function isRoomServerMessage(value) {
|
|
482
|
+
// A hostile or buggy relay cannot smuggle an unexpected shape past the
|
|
483
|
+
// type switch that consumers rely on: only known frame types pass.
|
|
484
|
+
return isRoomShapedFrame(value) && KNOWN_SERVER_MESSAGE_TYPES.has(value.type);
|
|
485
|
+
}
|
|
486
|
+
function errorMessage(err) {
|
|
487
|
+
return err instanceof Error ? err.message : String(err);
|
|
488
|
+
}
|
|
489
|
+
function isAuthenticationError(code) {
|
|
490
|
+
return code === "AUTH_FAILED" || code === "AUTH_TIMEOUT" || code === "TOKEN_REQUIRED";
|
|
491
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@openmarket/rooms-client",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "OM Rooms protocol client: wire types, WebSocket + REST clients, and chat view-models. Browser-safe (no node:/bun: imports).",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "Apache-2.0",
|
|
7
|
+
"author": "OpenMarket",
|
|
8
|
+
"homepage": "https://github.com/openmarket-xyz/openmarket-releases#readme",
|
|
9
|
+
"bugs": {
|
|
10
|
+
"url": "https://github.com/openmarket-xyz/openmarket-releases/issues"
|
|
11
|
+
},
|
|
12
|
+
"repository": {
|
|
13
|
+
"type": "git",
|
|
14
|
+
"url": "git+https://github.com/openmarket-xyz/openmarket-releases.git"
|
|
15
|
+
},
|
|
16
|
+
"engines": {
|
|
17
|
+
"bun": ">=1.3.0",
|
|
18
|
+
"node": ">=22"
|
|
19
|
+
},
|
|
20
|
+
"files": ["src", "dist", "README.md", "LICENSE"],
|
|
21
|
+
"publishConfig": {
|
|
22
|
+
"access": "public"
|
|
23
|
+
},
|
|
24
|
+
"exports": {
|
|
25
|
+
"./client": {
|
|
26
|
+
"bun": "./src/client.ts",
|
|
27
|
+
"types": "./dist/client.d.ts",
|
|
28
|
+
"default": "./dist/client.js"
|
|
29
|
+
},
|
|
30
|
+
"./ws-client": {
|
|
31
|
+
"bun": "./src/ws-client.ts",
|
|
32
|
+
"types": "./dist/ws-client.d.ts",
|
|
33
|
+
"default": "./dist/ws-client.js"
|
|
34
|
+
},
|
|
35
|
+
"./social-client": {
|
|
36
|
+
"bun": "./src/social-client.ts",
|
|
37
|
+
"types": "./dist/social-client.d.ts",
|
|
38
|
+
"default": "./dist/social-client.js"
|
|
39
|
+
},
|
|
40
|
+
"./social-types": {
|
|
41
|
+
"bun": "./src/social-types.ts",
|
|
42
|
+
"types": "./dist/social-types.d.ts",
|
|
43
|
+
"default": "./dist/social-types.js"
|
|
44
|
+
},
|
|
45
|
+
"./library-publisher": {
|
|
46
|
+
"bun": "./src/library-publisher.ts",
|
|
47
|
+
"types": "./dist/library-publisher.d.ts",
|
|
48
|
+
"default": "./dist/library-publisher.js"
|
|
49
|
+
},
|
|
50
|
+
"./doc-reconcile": {
|
|
51
|
+
"bun": "./src/doc-reconcile.ts",
|
|
52
|
+
"types": "./dist/doc-reconcile.d.ts",
|
|
53
|
+
"default": "./dist/doc-reconcile.js"
|
|
54
|
+
},
|
|
55
|
+
"./topic-uri": {
|
|
56
|
+
"bun": "./src/topic-uri.ts",
|
|
57
|
+
"types": "./dist/topic-uri.d.ts",
|
|
58
|
+
"default": "./dist/topic-uri.js"
|
|
59
|
+
},
|
|
60
|
+
"./doc-uri": {
|
|
61
|
+
"bun": "./src/doc-uri.ts",
|
|
62
|
+
"types": "./dist/doc-uri.d.ts",
|
|
63
|
+
"default": "./dist/doc-uri.js"
|
|
64
|
+
},
|
|
65
|
+
"./endpoints": {
|
|
66
|
+
"bun": "./src/endpoints.ts",
|
|
67
|
+
"types": "./dist/endpoints.d.ts",
|
|
68
|
+
"default": "./dist/endpoints.js"
|
|
69
|
+
},
|
|
70
|
+
"./jwt": {
|
|
71
|
+
"bun": "./src/jwt.ts",
|
|
72
|
+
"types": "./dist/jwt.d.ts",
|
|
73
|
+
"default": "./dist/jwt.js"
|
|
74
|
+
},
|
|
75
|
+
"./names": {
|
|
76
|
+
"bun": "./src/names.ts",
|
|
77
|
+
"types": "./dist/names.d.ts",
|
|
78
|
+
"default": "./dist/names.js"
|
|
79
|
+
},
|
|
80
|
+
"./suggestion-attribution": {
|
|
81
|
+
"bun": "./src/suggestion-attribution.ts",
|
|
82
|
+
"types": "./dist/suggestion-attribution.d.ts",
|
|
83
|
+
"default": "./dist/suggestion-attribution.js"
|
|
84
|
+
},
|
|
85
|
+
"./merge3": {
|
|
86
|
+
"bun": "./src/merge3.ts",
|
|
87
|
+
"types": "./dist/merge3.d.ts",
|
|
88
|
+
"default": "./dist/merge3.js"
|
|
89
|
+
},
|
|
90
|
+
"./types": {
|
|
91
|
+
"bun": "./src/types.ts",
|
|
92
|
+
"types": "./dist/types.d.ts",
|
|
93
|
+
"default": "./dist/types.js"
|
|
94
|
+
},
|
|
95
|
+
"./agent/armed-mode": {
|
|
96
|
+
"bun": "./src/agent/armed-mode.ts",
|
|
97
|
+
"types": "./dist/agent/armed-mode.d.ts",
|
|
98
|
+
"default": "./dist/agent/armed-mode.js"
|
|
99
|
+
},
|
|
100
|
+
"./agent/lane-draft": {
|
|
101
|
+
"bun": "./src/agent/lane-draft.ts",
|
|
102
|
+
"types": "./dist/agent/lane-draft.d.ts",
|
|
103
|
+
"default": "./dist/agent/lane-draft.js"
|
|
104
|
+
},
|
|
105
|
+
"./agent/lane-prompts": {
|
|
106
|
+
"bun": "./src/agent/lane-prompts.ts",
|
|
107
|
+
"types": "./dist/agent/lane-prompts.d.ts",
|
|
108
|
+
"default": "./dist/agent/lane-prompts.js"
|
|
109
|
+
},
|
|
110
|
+
"./agent/library-context": {
|
|
111
|
+
"bun": "./src/agent/library-context.ts",
|
|
112
|
+
"types": "./dist/agent/library-context.d.ts",
|
|
113
|
+
"default": "./dist/agent/library-context.js"
|
|
114
|
+
},
|
|
115
|
+
"./agent/library-model": {
|
|
116
|
+
"bun": "./src/agent/library-model.ts",
|
|
117
|
+
"types": "./dist/agent/library-model.d.ts",
|
|
118
|
+
"default": "./dist/agent/library-model.js"
|
|
119
|
+
},
|
|
120
|
+
"./agent/room-context": {
|
|
121
|
+
"bun": "./src/agent/room-context.ts",
|
|
122
|
+
"types": "./dist/agent/room-context.d.ts",
|
|
123
|
+
"default": "./dist/agent/room-context.js"
|
|
124
|
+
},
|
|
125
|
+
"./agent/sidebar-model": {
|
|
126
|
+
"bun": "./src/agent/sidebar-model.ts",
|
|
127
|
+
"types": "./dist/agent/sidebar-model.d.ts",
|
|
128
|
+
"default": "./dist/agent/sidebar-model.js"
|
|
129
|
+
},
|
|
130
|
+
"./agent/topic-inbox": {
|
|
131
|
+
"bun": "./src/agent/topic-inbox.ts",
|
|
132
|
+
"types": "./dist/agent/topic-inbox.d.ts",
|
|
133
|
+
"default": "./dist/agent/topic-inbox.js"
|
|
134
|
+
},
|
|
135
|
+
"./agent/topic-view-batcher": {
|
|
136
|
+
"bun": "./src/agent/topic-view-batcher.ts",
|
|
137
|
+
"types": "./dist/agent/topic-view-batcher.d.ts",
|
|
138
|
+
"default": "./dist/agent/topic-view-batcher.js"
|
|
139
|
+
},
|
|
140
|
+
"./agent/clients/common": {
|
|
141
|
+
"bun": "./src/agent/clients/common.ts",
|
|
142
|
+
"types": "./dist/agent/clients/common.d.ts",
|
|
143
|
+
"default": "./dist/agent/clients/common.js"
|
|
144
|
+
},
|
|
145
|
+
"./shared/rooms-protocol": {
|
|
146
|
+
"bun": "./src/shared/rooms-protocol.ts",
|
|
147
|
+
"types": "./dist/shared/rooms-protocol.d.ts",
|
|
148
|
+
"default": "./dist/shared/rooms-protocol.js"
|
|
149
|
+
},
|
|
150
|
+
"./shared/mentions": {
|
|
151
|
+
"bun": "./src/shared/mentions.ts",
|
|
152
|
+
"types": "./dist/shared/mentions.d.ts",
|
|
153
|
+
"default": "./dist/shared/mentions.js"
|
|
154
|
+
},
|
|
155
|
+
"./shared/emoji-data": {
|
|
156
|
+
"bun": "./src/shared/emoji-data.ts",
|
|
157
|
+
"types": "./dist/shared/emoji-data.d.ts",
|
|
158
|
+
"default": "./dist/shared/emoji-data.js"
|
|
159
|
+
},
|
|
160
|
+
"./version": {
|
|
161
|
+
"bun": "./src/version.ts",
|
|
162
|
+
"types": "./dist/version.d.ts",
|
|
163
|
+
"default": "./dist/version.js"
|
|
164
|
+
},
|
|
165
|
+
"./package.json": "./package.json"
|
|
166
|
+
},
|
|
167
|
+
"scripts": {
|
|
168
|
+
"build": "rm -rf dist && tsc -p tsconfig.build.json",
|
|
169
|
+
"prepublishOnly": "bun run build",
|
|
170
|
+
"test": "bun test",
|
|
171
|
+
"typecheck": "tsc --noEmit -p tsconfig.json"
|
|
172
|
+
},
|
|
173
|
+
"dependencies": {
|
|
174
|
+
"@earendil-works/pi-ai": "0.78.1",
|
|
175
|
+
"picocolors": "1.1.1"
|
|
176
|
+
},
|
|
177
|
+
"devDependencies": {
|
|
178
|
+
"@types/node": "25.9.2"
|
|
179
|
+
}
|
|
180
|
+
}
|