@neta-art/cohub 1.32.0 → 1.33.1

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/dist/websocket.js CHANGED
@@ -1,2 +1,770 @@
1
- import { n as createWebsocketClient, t as WebsocketClient } from "./chunks/websocket.js";
1
+ import { a as normalizeRealtimeRooms, i as getSessionTurnPatchStreamKey, n as WS_ROOM_SUBSCRIPTION_CAPABILITY, r as getRealtimeSpaceRoom, t as WS_COMPACT_STREAM_CAPABILITY } from "./chunks/types.js";
2
+ import { c as resolveWebsocketUrl } from "./chunks/environment.js";
3
+ //#region src/websocket.ts
4
+ const createEventMap = () => ({
5
+ connecting: /* @__PURE__ */ new Set(),
6
+ reconnecting: /* @__PURE__ */ new Set(),
7
+ open: /* @__PURE__ */ new Set(),
8
+ close: /* @__PURE__ */ new Set(),
9
+ error: /* @__PURE__ */ new Set(),
10
+ event: /* @__PURE__ */ new Set(),
11
+ ready: /* @__PURE__ */ new Set(),
12
+ auth: /* @__PURE__ */ new Set(),
13
+ messageAccepted: /* @__PURE__ */ new Set(),
14
+ serverError: /* @__PURE__ */ new Set(),
15
+ subscribed: /* @__PURE__ */ new Set(),
16
+ subscribeError: /* @__PURE__ */ new Set(),
17
+ pong: /* @__PURE__ */ new Set()
18
+ });
19
+ const toWebSocketUrl = (input, env) => resolveWebsocketUrl({
20
+ url: input,
21
+ env
22
+ });
23
+ const normalizeOptions = (options = {}) => ({
24
+ url: toWebSocketUrl(options.url, options.env),
25
+ autoReconnect: options.autoReconnect !== false,
26
+ reconnectBaseDelayMs: options.reconnectBaseDelayMs ?? 1e3,
27
+ reconnectMaxDelayMs: options.reconnectMaxDelayMs ?? 15e3,
28
+ pingIntervalMs: options.pingIntervalMs ?? 2e4,
29
+ pongTimeoutMs: options.pongTimeoutMs ?? 15e3,
30
+ debug: options.debug === true
31
+ });
32
+ const formatCloseMessage = (code, reason) => `WebSocket closed: ${code ?? 0} ${reason || ""}`.trim();
33
+ const isRetryableCloseCode = (code) => {
34
+ if (code === 1e3) return false;
35
+ if (code === 4003) return false;
36
+ return true;
37
+ };
38
+ const AUTH_CLOSE_REASON = "authentication failed";
39
+ const PATCH_STREAM_BUFFER_MAX_PENDING = 128;
40
+ const isRecord = (value) => Boolean(value && typeof value === "object" && !Array.isArray(value));
41
+ const isRealtimeCompactFrame = (value) => {
42
+ if (!isRecord(value)) return false;
43
+ if (value.t !== "d" && value.t !== "p") return false;
44
+ if (typeof value.sid !== "string" || !value.sid) return false;
45
+ if (typeof value.s !== "number" || !Number.isInteger(value.s) || value.s < 0) return false;
46
+ if (typeof value.b !== "number" || !Number.isInteger(value.b) || value.b < 0) return false;
47
+ if (value.t === "d") return "v" in value;
48
+ return (value.o === "append" || value.o === "replace" || value.o === "add" || value.o === "merge" || value.o === "remove") && typeof value.p === "string" && value.p.length > 0;
49
+ };
50
+ const isRealtimeEnvelope = (value) => {
51
+ if (!isRecord(value)) return false;
52
+ if (typeof value.id !== "string") return false;
53
+ if (typeof value.timestamp !== "number") return false;
54
+ if (value.domain !== "system" && value.domain !== "session" && value.domain !== "space" && value.domain !== "label") return false;
55
+ if (typeof value.type !== "string") return false;
56
+ if (!isRecord(value.payload)) return false;
57
+ return true;
58
+ };
59
+ const compactFrameToPatchOperation = (frame) => {
60
+ if (frame.t === "d") return { v: frame.v };
61
+ if (frame.o === "remove") return {
62
+ o: "remove",
63
+ p: frame.p
64
+ };
65
+ if (frame.o === "merge") return isRecord(frame.v) ? {
66
+ o: "merge",
67
+ p: frame.p,
68
+ v: frame.v
69
+ } : null;
70
+ if (!("v" in frame)) return null;
71
+ switch (frame.o) {
72
+ case "append": return {
73
+ o: "append",
74
+ p: frame.p,
75
+ v: frame.v
76
+ };
77
+ case "replace": return {
78
+ o: "replace",
79
+ p: frame.p,
80
+ v: frame.v
81
+ };
82
+ case "add": return {
83
+ o: "add",
84
+ p: frame.p,
85
+ v: frame.v
86
+ };
87
+ default: return null;
88
+ }
89
+ };
90
+ var WebsocketAuthError = class extends Error {
91
+ constructor(message) {
92
+ super(message);
93
+ this.name = "WebsocketAuthError";
94
+ }
95
+ };
96
+ var WebsocketClient = class {
97
+ url;
98
+ autoReconnect;
99
+ reconnectBaseDelayMs;
100
+ reconnectMaxDelayMs;
101
+ pingIntervalMs;
102
+ pongTimeoutMs;
103
+ debug;
104
+ getAccessToken;
105
+ WebSocketImpl;
106
+ ws = null;
107
+ pingTimer = null;
108
+ reconnectTimer = null;
109
+ reconnectTimerResolver = null;
110
+ reconnectAttempt = 0;
111
+ manuallyClosed = false;
112
+ connectPromise = null;
113
+ authWaiter = null;
114
+ awaitingPong = false;
115
+ lastPingRequestId = null;
116
+ pongDeadlineAt = 0;
117
+ compactStreamContexts = /* @__PURE__ */ new Map();
118
+ patchStreamBuffers = /* @__PURE__ */ new Map();
119
+ roomSubscriptions = /* @__PURE__ */ new Map();
120
+ state = "idle";
121
+ connectionId = null;
122
+ listeners = createEventMap();
123
+ constructor(options = {}) {
124
+ const normalized = normalizeOptions(options);
125
+ this.url = normalized.url;
126
+ this.autoReconnect = normalized.autoReconnect;
127
+ this.reconnectBaseDelayMs = normalized.reconnectBaseDelayMs;
128
+ this.reconnectMaxDelayMs = normalized.reconnectMaxDelayMs;
129
+ this.pingIntervalMs = normalized.pingIntervalMs;
130
+ this.pongTimeoutMs = normalized.pongTimeoutMs;
131
+ this.debug = normalized.debug;
132
+ this.getAccessToken = options.getAccessToken;
133
+ this.WebSocketImpl = options.WebSocketImpl ?? WebSocket;
134
+ }
135
+ on(type, handler) {
136
+ this.listeners[type].add(handler);
137
+ return () => this.off(type, handler);
138
+ }
139
+ off(type, handler) {
140
+ this.listeners[type].delete(handler);
141
+ }
142
+ emit(type, payload) {
143
+ for (const handler of this.listeners[type]) handler(payload);
144
+ }
145
+ log(...args) {
146
+ if (this.debug) console.log("[WebsocketClient]", ...args);
147
+ }
148
+ async connect() {
149
+ if (this.connectPromise) return this.connectPromise;
150
+ if (this.state === "open" && this.ws?.readyState === WebSocket.OPEN) return;
151
+ const isReconnect = this.reconnectAttempt > 0 || this.state === "reconnecting";
152
+ this.manuallyClosed = false;
153
+ this.clearReconnectTimer();
154
+ this.state = isReconnect ? "reconnecting" : "connecting";
155
+ this.emit("connecting", {
156
+ isReconnect,
157
+ attempt: this.reconnectAttempt
158
+ });
159
+ this.connectPromise = new Promise((resolve, reject) => {
160
+ const ws = new this.WebSocketImpl(this.url);
161
+ this.ws = ws;
162
+ let settled = false;
163
+ const rejectOnce = (error) => {
164
+ if (settled) return;
165
+ settled = true;
166
+ this.connectPromise = null;
167
+ reject(error);
168
+ };
169
+ const resolveOnce = () => {
170
+ if (settled) return;
171
+ settled = true;
172
+ this.connectPromise = null;
173
+ resolve();
174
+ };
175
+ ws.onopen = async () => {
176
+ try {
177
+ this.log("connected", {
178
+ url: this.url,
179
+ isReconnect,
180
+ attempt: this.reconnectAttempt
181
+ });
182
+ this.startPingLoop();
183
+ await this.authenticate();
184
+ this.state = "open";
185
+ this.reconnectAttempt = 0;
186
+ this.emit("open", { connectionId: this.connectionId });
187
+ resolveOnce();
188
+ } catch (error) {
189
+ const authError = error instanceof Error ? error : /* @__PURE__ */ new Error("authentication failed");
190
+ this.emit("error", {
191
+ error: authError,
192
+ recoverable: false
193
+ });
194
+ rejectOnce(authError);
195
+ ws.close(4003, AUTH_CLOSE_REASON);
196
+ }
197
+ };
198
+ ws.onmessage = (event) => {
199
+ this.handleMessage(event.data);
200
+ };
201
+ ws.onerror = (error) => {
202
+ this.emit("error", {
203
+ error,
204
+ recoverable: !this.manuallyClosed
205
+ });
206
+ };
207
+ ws.onclose = (event) => {
208
+ this.stopPingLoop();
209
+ const wasConnecting = this.state === "connecting" || this.state === "reconnecting";
210
+ this.state = "closed";
211
+ this.ws = null;
212
+ this.compactStreamContexts.clear();
213
+ this.patchStreamBuffers.clear();
214
+ const closeError = new Error(formatCloseMessage(event.code, event.reason));
215
+ this.rejectAuthWaiter(closeError);
216
+ const willReconnect = !this.manuallyClosed && this.autoReconnect && isRetryableCloseCode(event.code);
217
+ this.log("closed", {
218
+ code: event.code,
219
+ reason: event.reason,
220
+ willReconnect,
221
+ wasConnecting
222
+ });
223
+ this.emit("close", {
224
+ code: event.code,
225
+ reason: event.reason,
226
+ willReconnect
227
+ });
228
+ if (wasConnecting) rejectOnce(closeError);
229
+ if (willReconnect) this.scheduleReconnect(event.code, event.reason);
230
+ };
231
+ });
232
+ return this.connectPromise;
233
+ }
234
+ async disconnect(code = 1e3, reason = "manual") {
235
+ this.manuallyClosed = true;
236
+ this.clearReconnectTimer();
237
+ this.stopPingLoop();
238
+ this.state = "closed";
239
+ this.rejectAuthWaiter(/* @__PURE__ */ new Error("disconnected"));
240
+ this.ws?.close(code, reason);
241
+ this.ws = null;
242
+ this.connectPromise = null;
243
+ this.compactStreamContexts.clear();
244
+ this.patchStreamBuffers.clear();
245
+ for (const state of this.roomSubscriptions.values()) {
246
+ state.subscribed = false;
247
+ state.pending = false;
248
+ }
249
+ }
250
+ async sendCanvasTransaction(input) {
251
+ await this.ensureOpen();
252
+ this.send({
253
+ type: "canvas.tx",
254
+ requestId: input.requestId,
255
+ payload: {
256
+ spaceId: input.spaceId,
257
+ documentId: input.documentId,
258
+ txId: input.txId,
259
+ baseVersion: input.baseVersion ?? null,
260
+ clientId: input.clientId ?? null,
261
+ undoGroupId: input.undoGroupId ?? null,
262
+ ops: input.ops
263
+ }
264
+ });
265
+ }
266
+ async sendMessage(input) {
267
+ await this.ensureOpen();
268
+ this.send({
269
+ type: "session.message.create",
270
+ requestId: input.requestId,
271
+ payload: {
272
+ spaceId: input.spaceId,
273
+ sessionId: input.sessionId,
274
+ content: input.content,
275
+ clientMessageId: input.clientMessageId,
276
+ model: input.model,
277
+ provider: input.provider
278
+ }
279
+ });
280
+ }
281
+ retainRooms(rooms) {
282
+ const normalized = normalizeRealtimeRooms(rooms);
283
+ if (normalized.length === 0) return () => void 0;
284
+ for (const room of normalized) {
285
+ const state = this.roomSubscriptions.get(room) ?? {
286
+ refCount: 0,
287
+ subscribed: false,
288
+ pending: false
289
+ };
290
+ state.refCount += 1;
291
+ this.roomSubscriptions.set(room, state);
292
+ }
293
+ this.flushRoomSubscriptions();
294
+ if (this.state !== "open" && this.state !== "connecting" && this.state !== "reconnecting") this.connect().then(() => this.flushRoomSubscriptions()).catch((error) => this.emit("error", {
295
+ error,
296
+ recoverable: true
297
+ }));
298
+ let released = false;
299
+ return () => {
300
+ if (released) return;
301
+ released = true;
302
+ const roomsToRelease = [];
303
+ for (const room of normalized) {
304
+ const state = this.roomSubscriptions.get(room);
305
+ if (!state) continue;
306
+ state.refCount -= 1;
307
+ if (state.refCount > 0) {
308
+ this.roomSubscriptions.set(room, state);
309
+ continue;
310
+ }
311
+ this.roomSubscriptions.delete(room);
312
+ if (state.subscribed) roomsToRelease.push(room);
313
+ }
314
+ if (roomsToRelease.length === 0) return;
315
+ if (this.ws?.readyState === WebSocket.OPEN) this.send({
316
+ type: "unsubscribe",
317
+ payload: { rooms: roomsToRelease }
318
+ });
319
+ };
320
+ }
321
+ subscribeRooms(rooms) {
322
+ return this.retainRooms(rooms);
323
+ }
324
+ subscribeSpace(spaceId) {
325
+ return this.retainRooms([getRealtimeSpaceRoom(spaceId)]);
326
+ }
327
+ ack(eventId, requestId) {
328
+ this.send({
329
+ type: "ack",
330
+ requestId,
331
+ payload: eventId ? { eventId } : void 0
332
+ });
333
+ }
334
+ ping(requestId) {
335
+ const effectiveRequestId = requestId ?? `ping-${Date.now()}`;
336
+ this.awaitingPong = true;
337
+ this.lastPingRequestId = effectiveRequestId;
338
+ this.pongDeadlineAt = Date.now() + this.pongTimeoutMs;
339
+ this.send({
340
+ type: "ping",
341
+ requestId: effectiveRequestId,
342
+ payload: {}
343
+ });
344
+ }
345
+ async ensureOpen() {
346
+ if (this.state === "open" && this.ws?.readyState === WebSocket.OPEN) return;
347
+ await this.connect();
348
+ }
349
+ send(event) {
350
+ const ws = this.ws;
351
+ if (!ws || ws.readyState !== WebSocket.OPEN) throw new Error("websocket is not open");
352
+ ws.send(JSON.stringify(event));
353
+ }
354
+ async authenticate() {
355
+ const token = this.getAccessToken ? await this.getAccessToken() : null;
356
+ if (!token) throw new WebsocketAuthError("missing access token");
357
+ const waiter = this.createAuthWaiter();
358
+ this.send({
359
+ type: "auth",
360
+ payload: {
361
+ token,
362
+ capabilities: [WS_COMPACT_STREAM_CAPABILITY, WS_ROOM_SUBSCRIPTION_CAPABILITY]
363
+ }
364
+ });
365
+ await waiter.promise;
366
+ await this.restoreRoomSubscriptions();
367
+ }
368
+ flushRoomSubscriptions() {
369
+ if (this.ws?.readyState !== WebSocket.OPEN) return;
370
+ const rooms = [...this.roomSubscriptions.entries()].filter(([, state]) => state.refCount > 0 && !state.subscribed && !state.pending).map(([room]) => room);
371
+ if (rooms.length === 0) return;
372
+ this.send({
373
+ type: "subscribe",
374
+ payload: { rooms }
375
+ });
376
+ for (const room of rooms) {
377
+ const state = this.roomSubscriptions.get(room);
378
+ if (state) state.pending = true;
379
+ }
380
+ }
381
+ async restoreRoomSubscriptions() {
382
+ for (const state of this.roomSubscriptions.values()) state.subscribed = false;
383
+ this.flushRoomSubscriptions();
384
+ }
385
+ createAuthWaiter() {
386
+ this.rejectAuthWaiter(/* @__PURE__ */ new Error("superseded auth waiter"));
387
+ let resolve;
388
+ let reject;
389
+ const promise = new Promise((res, rej) => {
390
+ resolve = res;
391
+ reject = rej;
392
+ });
393
+ this.authWaiter = {
394
+ promise,
395
+ resolve,
396
+ reject
397
+ };
398
+ return this.authWaiter;
399
+ }
400
+ resolveAuthWaiter() {
401
+ if (!this.authWaiter) return;
402
+ this.authWaiter.resolve();
403
+ this.authWaiter = null;
404
+ }
405
+ rejectAuthWaiter(error) {
406
+ if (!this.authWaiter) return;
407
+ this.authWaiter.reject(error);
408
+ this.authWaiter = null;
409
+ }
410
+ handleMessage(raw) {
411
+ let parsed;
412
+ try {
413
+ parsed = typeof raw === "string" ? JSON.parse(raw) : JSON.parse(String(raw));
414
+ } catch {
415
+ this.emit("error", {
416
+ error: /* @__PURE__ */ new Error("invalid websocket payload"),
417
+ recoverable: true
418
+ });
419
+ return;
420
+ }
421
+ if (isRealtimeCompactFrame(parsed)) {
422
+ this.handleCompactFrame(parsed);
423
+ return;
424
+ }
425
+ if (!isRealtimeEnvelope(parsed)) {
426
+ this.emit("error", {
427
+ error: /* @__PURE__ */ new Error("invalid realtime envelope"),
428
+ recoverable: true
429
+ });
430
+ return;
431
+ }
432
+ const envelope = parsed;
433
+ this.rememberCompactStreamContext(envelope);
434
+ if (envelope.type === "session.turn.patch") {
435
+ this.handlePatchEnvelope(envelope);
436
+ return;
437
+ }
438
+ switch (envelope.type) {
439
+ case "system.ready": {
440
+ const connectionId = typeof envelope.payload.connectionId === "string" ? envelope.payload.connectionId : null;
441
+ if (connectionId) {
442
+ this.connectionId = connectionId;
443
+ this.emit("ready", { connectionId });
444
+ }
445
+ this.emit("event", envelope);
446
+ return;
447
+ }
448
+ case "system.auth.ok": {
449
+ const connectionId = typeof envelope.payload.connectionId === "string" ? envelope.payload.connectionId : this.connectionId;
450
+ const user = envelope.payload.user && typeof envelope.payload.user === "object" ? envelope.payload.user : {};
451
+ if (connectionId) {
452
+ this.connectionId = connectionId;
453
+ this.emit("auth", {
454
+ connectionId,
455
+ user
456
+ });
457
+ }
458
+ this.resolveAuthWaiter();
459
+ this.emit("event", envelope);
460
+ return;
461
+ }
462
+ case "system.request.error": {
463
+ const message = typeof envelope.payload.message === "string" ? envelope.payload.message : "request failed";
464
+ const code = typeof envelope.payload.code === "string" ? envelope.payload.code : void 0;
465
+ const error = new WebsocketAuthError(message);
466
+ this.rejectAuthWaiter(error);
467
+ this.emit("serverError", {
468
+ code,
469
+ message,
470
+ requestId: envelope.requestId ?? null,
471
+ sessionId: envelope.sessionId ?? null,
472
+ spaceId: envelope.spaceId ?? null
473
+ });
474
+ this.emit("event", envelope);
475
+ return;
476
+ }
477
+ case "session.request.accepted": {
478
+ const payload = envelope.payload;
479
+ this.emit("messageAccepted", {
480
+ requestId: envelope.requestId ?? null,
481
+ sessionId: envelope.sessionId ?? null,
482
+ spaceId: envelope.spaceId ?? null,
483
+ clientMessageId: typeof payload.clientMessageId === "string" ? payload.clientMessageId : null,
484
+ turnId: typeof payload.turnId === "string" ? payload.turnId : null,
485
+ userMessageId: typeof payload.userMessageId === "string" ? payload.userMessageId : null,
486
+ traceId: typeof payload.traceId === "string" ? payload.traceId : null
487
+ });
488
+ this.emit("event", envelope);
489
+ return;
490
+ }
491
+ case "session.request.error": {
492
+ const payload = envelope.payload;
493
+ this.emit("serverError", {
494
+ code: typeof payload.code === "string" ? payload.code : void 0,
495
+ message: typeof payload.message === "string" ? payload.message : void 0,
496
+ requestId: envelope.requestId ?? null,
497
+ sessionId: envelope.sessionId ?? null,
498
+ spaceId: envelope.spaceId ?? null,
499
+ clientMessageId: typeof payload.clientMessageId === "string" ? payload.clientMessageId : null
500
+ });
501
+ this.emit("event", envelope);
502
+ return;
503
+ }
504
+ case "system.pong": {
505
+ const requestId = envelope.requestId ?? null;
506
+ if (!requestId || requestId === this.lastPingRequestId) {
507
+ this.awaitingPong = false;
508
+ this.lastPingRequestId = null;
509
+ this.pongDeadlineAt = 0;
510
+ }
511
+ this.emit("pong", { requestId });
512
+ return;
513
+ }
514
+ case "system.subscribe.ok": {
515
+ const payload = envelope.payload;
516
+ const rooms = normalizeRealtimeRooms(Array.isArray(payload.rooms) ? payload.rooms.filter((room) => typeof room === "string") : []);
517
+ for (const room of rooms) {
518
+ const state = this.roomSubscriptions.get(room);
519
+ if (state) {
520
+ state.subscribed = true;
521
+ state.pending = false;
522
+ }
523
+ }
524
+ this.emit("subscribed", {
525
+ rooms,
526
+ requestId: envelope.requestId ?? null
527
+ });
528
+ this.emit("event", envelope);
529
+ return;
530
+ }
531
+ case "system.subscribe.error": {
532
+ const payload = envelope.payload;
533
+ const rejected = Array.isArray(payload.rejected) ? payload.rejected.filter((entry) => Boolean(entry && typeof entry === "object")).map((entry) => ({
534
+ room: typeof entry.room === "string" ? entry.room : "",
535
+ code: typeof entry.code === "string" ? entry.code : "UNKNOWN",
536
+ message: typeof entry.message === "string" ? entry.message : "Subscription failed"
537
+ })).filter((entry) => entry.room) : [];
538
+ for (const item of rejected) {
539
+ const room = normalizeRealtimeRooms([item.room])[0];
540
+ if (!room) continue;
541
+ this.roomSubscriptions.delete(room);
542
+ }
543
+ this.emit("subscribeError", {
544
+ rejected,
545
+ requestId: envelope.requestId ?? null
546
+ });
547
+ this.emit("event", envelope);
548
+ return;
549
+ }
550
+ case "system.ack.ok": return;
551
+ default:
552
+ this.emit("event", envelope);
553
+ return;
554
+ }
555
+ }
556
+ rememberCompactStreamContext(envelope) {
557
+ if (envelope.type === "session.turn.patch") {
558
+ const payload = envelope.payload;
559
+ const turnId = typeof payload.turnId === "string" ? payload.turnId : null;
560
+ const messageId = typeof payload.messageId === "string" ? payload.messageId : null;
561
+ const realtimeMeta = payload._rt && typeof payload._rt === "object" ? payload._rt : null;
562
+ const sid = typeof realtimeMeta?.sid === "string" && realtimeMeta.sid.trim() ? realtimeMeta.sid : turnId ?? messageId;
563
+ if (!sid) return;
564
+ this.compactStreamContexts.set(sid, {
565
+ spaceId: envelope.spaceId ?? null,
566
+ sessionId: envelope.sessionId ?? null,
567
+ turnId,
568
+ messageId,
569
+ messageOrdinal: typeof payload.messageOrdinal === "number" ? payload.messageOrdinal : null,
570
+ anchorUserMessageId: typeof payload.anchorUserMessageId === "string" ? payload.anchorUserMessageId : null
571
+ });
572
+ return;
573
+ }
574
+ if (envelope.type !== "session.message.persisted") return;
575
+ const message = envelope.payload.message;
576
+ if (!message || typeof message !== "object") return;
577
+ const meta = message.meta;
578
+ const turnId = typeof meta?.turnId === "string" ? meta.turnId : null;
579
+ if (!turnId) return;
580
+ for (const [sid, context] of this.compactStreamContexts.entries()) if (context.turnId === turnId) {
581
+ this.compactStreamContexts.delete(sid);
582
+ this.patchStreamBuffers.delete(sid);
583
+ }
584
+ }
585
+ getPatchStreamBufferKey(envelope) {
586
+ if (envelope.type !== "session.turn.patch") return null;
587
+ const payload = envelope.payload;
588
+ const realtimeMeta = payload._rt && typeof payload._rt === "object" ? payload._rt : null;
589
+ if (typeof realtimeMeta?.sid === "string" && realtimeMeta.sid.trim()) return realtimeMeta.sid;
590
+ return getSessionTurnPatchStreamKey(payload, { includeSessionFallback: true });
591
+ }
592
+ handlePatchEnvelope(envelope) {
593
+ const payload = envelope.payload;
594
+ if (typeof payload.seq !== "number" || typeof payload.baseSeq !== "number" || !Number.isInteger(payload.seq) || !Number.isInteger(payload.baseSeq) || payload.seq < 0 || payload.baseSeq < 0) {
595
+ this.emit("event", envelope);
596
+ return;
597
+ }
598
+ const key = this.getPatchStreamBufferKey(envelope);
599
+ if (!key) {
600
+ this.emit("event", envelope);
601
+ return;
602
+ }
603
+ if (payload.baseSeq === 0) {
604
+ const buffer = {
605
+ nextSeq: payload.seq + 1,
606
+ pending: /* @__PURE__ */ new Map()
607
+ };
608
+ this.patchStreamBuffers.set(key, buffer);
609
+ this.emit("event", envelope);
610
+ this.flushPatchStreamBuffer(buffer);
611
+ return;
612
+ }
613
+ const buffer = this.patchStreamBuffers.get(key);
614
+ if (!buffer) {
615
+ const newBuffer = {
616
+ nextSeq: payload.baseSeq + 1,
617
+ pending: new Map([[payload.seq, envelope]])
618
+ };
619
+ this.patchStreamBuffers.set(key, newBuffer);
620
+ this.flushPatchStreamBuffer(newBuffer);
621
+ return;
622
+ }
623
+ if (payload.seq < buffer.nextSeq) return;
624
+ buffer.pending.set(payload.seq, envelope);
625
+ if (!this.enforcePatchStreamBufferLimit(key, buffer)) return;
626
+ this.flushPatchStreamBuffer(buffer);
627
+ }
628
+ enforcePatchStreamBufferLimit(key, buffer) {
629
+ if (buffer.pending.size <= PATCH_STREAM_BUFFER_MAX_PENDING) return true;
630
+ this.patchStreamBuffers.delete(key);
631
+ this.emit("error", {
632
+ error: /* @__PURE__ */ new Error(`patch stream buffer overflow: ${key}`),
633
+ recoverable: true
634
+ });
635
+ return false;
636
+ }
637
+ flushPatchStreamBuffer(buffer) {
638
+ while (true) {
639
+ const envelope = buffer.pending.get(buffer.nextSeq);
640
+ if (!envelope) return;
641
+ const seq = envelope.payload.seq;
642
+ if (typeof seq !== "number" || !Number.isInteger(seq)) return;
643
+ buffer.pending.delete(buffer.nextSeq);
644
+ buffer.nextSeq = seq + 1;
645
+ this.emit("event", envelope);
646
+ }
647
+ }
648
+ handleCompactFrame(frame) {
649
+ const context = this.compactStreamContexts.get(frame.sid);
650
+ if (!context?.sessionId) {
651
+ this.emit("error", {
652
+ error: /* @__PURE__ */ new Error(`unknown compact stream: ${frame.sid}`),
653
+ recoverable: true
654
+ });
655
+ return;
656
+ }
657
+ const op = compactFrameToPatchOperation(frame);
658
+ if (!op) {
659
+ this.emit("error", {
660
+ error: /* @__PURE__ */ new Error(`invalid compact stream frame: ${frame.sid}`),
661
+ recoverable: true
662
+ });
663
+ return;
664
+ }
665
+ const envelope = {
666
+ id: `compact:${frame.sid}:${frame.s}`,
667
+ timestamp: Date.now(),
668
+ domain: "session",
669
+ type: "session.turn.patch",
670
+ spaceId: context.spaceId,
671
+ sessionId: context.sessionId,
672
+ payload: {
673
+ turnId: context.turnId,
674
+ messageId: context.messageId,
675
+ messageOrdinal: context.messageOrdinal,
676
+ sourceMessageId: context.messageId,
677
+ anchorUserMessageId: context.anchorUserMessageId,
678
+ seq: frame.s,
679
+ baseSeq: frame.b,
680
+ ops: [op],
681
+ _rt: { sid: frame.sid }
682
+ }
683
+ };
684
+ this.handlePatchEnvelope(envelope);
685
+ }
686
+ startPingLoop() {
687
+ this.stopPingLoop();
688
+ this.pingTimer = setInterval(() => {
689
+ if (this.ws?.readyState !== WebSocket.OPEN) return;
690
+ if (this.awaitingPong && this.pongDeadlineAt > 0 && Date.now() > this.pongDeadlineAt) {
691
+ this.emit("error", {
692
+ error: /* @__PURE__ */ new Error("websocket pong timeout"),
693
+ recoverable: true
694
+ });
695
+ this.ws.close(4002, "pong timeout");
696
+ return;
697
+ }
698
+ this.ping();
699
+ }, this.pingIntervalMs);
700
+ }
701
+ stopPingLoop() {
702
+ if (this.pingTimer) {
703
+ clearInterval(this.pingTimer);
704
+ this.pingTimer = null;
705
+ }
706
+ this.awaitingPong = false;
707
+ this.lastPingRequestId = null;
708
+ this.pongDeadlineAt = 0;
709
+ }
710
+ clearReconnectTimer() {
711
+ if (this.reconnectTimer) {
712
+ clearTimeout(this.reconnectTimer);
713
+ this.reconnectTimer = null;
714
+ }
715
+ if (this.reconnectTimerResolver) {
716
+ const resolve = this.reconnectTimerResolver;
717
+ this.reconnectTimerResolver = null;
718
+ resolve();
719
+ }
720
+ }
721
+ async scheduleReconnect(code, reason) {
722
+ this.clearReconnectTimer();
723
+ const attempt = this.reconnectAttempt + 1;
724
+ const delay = Math.min(this.reconnectBaseDelayMs * 2 ** this.reconnectAttempt, this.reconnectMaxDelayMs);
725
+ this.reconnectAttempt = attempt;
726
+ this.state = "reconnecting";
727
+ this.log("schedule reconnect", {
728
+ attempt,
729
+ delay,
730
+ code,
731
+ reason
732
+ });
733
+ this.emit("reconnecting", {
734
+ attempt,
735
+ delayMs: delay,
736
+ code,
737
+ reason
738
+ });
739
+ await new Promise((resolve) => {
740
+ this.reconnectTimerResolver = resolve;
741
+ this.reconnectTimer = setTimeout(() => {
742
+ this.reconnectTimer = null;
743
+ this.reconnectTimerResolver = null;
744
+ resolve();
745
+ }, delay);
746
+ });
747
+ if (this.manuallyClosed) return;
748
+ if (typeof navigator !== "undefined" && navigator.onLine === false) {
749
+ await new Promise((resolve) => {
750
+ const fallbackTimer = setTimeout(resolve, this.reconnectMaxDelayMs);
751
+ const handleOnline = () => {
752
+ clearTimeout(fallbackTimer);
753
+ globalThis.removeEventListener?.("online", handleOnline);
754
+ resolve();
755
+ };
756
+ globalThis.addEventListener?.("online", handleOnline, { once: true });
757
+ });
758
+ if (this.manuallyClosed) return;
759
+ }
760
+ await this.connect().catch((error) => {
761
+ this.emit("error", {
762
+ error,
763
+ recoverable: true
764
+ });
765
+ });
766
+ }
767
+ };
768
+ const createWebsocketClient = (options) => new WebsocketClient(options);
769
+ //#endregion
2
770
  export { WebsocketClient, createWebsocketClient };