@neta-art/cohub 12.1.0 → 12.2.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.
Files changed (85) hide show
  1. package/README.md +8 -5
  2. package/dist/board/editor/editor.d.ts +1 -0
  3. package/dist/board/editor/editor.js +4 -0
  4. package/dist/board/editor/snapshots.js +5 -57
  5. package/dist/board/export/index.d.ts +2 -2
  6. package/dist/board/export/node/fonts.js +4 -0
  7. package/dist/board/export/node/index.d.ts +3 -0
  8. package/dist/board/export/node/index.js +13 -1
  9. package/dist/board/export/scene.d.ts +2 -2
  10. package/dist/board/export/scene.js +2 -2
  11. package/dist/board/model/asset-key.d.ts +16 -0
  12. package/dist/board/model/asset-key.js +92 -0
  13. package/dist/board/model/export-assets.d.ts +7 -3
  14. package/dist/board/model/export-assets.js +34 -13
  15. package/dist/board/model/index.d.ts +7 -5
  16. package/dist/board/model/index.js +9 -7
  17. package/dist/board/model/items/create.js +1 -1
  18. package/dist/board/model/media-sizes.d.ts +28 -0
  19. package/dist/board/model/media-sizes.js +64 -0
  20. package/dist/board/model/palette.d.ts +1 -0
  21. package/dist/board/model/palette.js +20 -130
  22. package/dist/board/model/text-metrics.d.ts +8 -3
  23. package/dist/board/model/text-metrics.js +13 -2
  24. package/dist/board/model/themes.d.ts +488 -0
  25. package/dist/board/model/themes.js +495 -0
  26. package/dist/board/render/card-tag.js +2 -2
  27. package/dist/board/render/index.d.ts +6 -4
  28. package/dist/board/render/index.js +5 -3
  29. package/dist/board/render/lod.d.ts +22 -0
  30. package/dist/board/render/lod.js +27 -0
  31. package/dist/board/render/media-interaction.js +1 -1
  32. package/dist/board/render/palette.js +2 -25
  33. package/dist/board/render/renderers/audio-card-renderer.js +2 -1
  34. package/dist/board/render/renderers/board-renderer-registry.d.ts +10 -0
  35. package/dist/board/render/renderers/custom-card-renderer.js +5 -5
  36. package/dist/board/render/renderers/far-plate.d.ts +11 -1
  37. package/dist/board/render/renderers/far-plate.js +27 -8
  38. package/dist/board/render/renderers/file-card-renderer.js +3 -5
  39. package/dist/board/render/renderers/image-card-renderer.js +32 -6
  40. package/dist/board/render/renderers/model-card-renderer.js +1 -1
  41. package/dist/board/render/renderers/shape-card-renderer.js +25 -24
  42. package/dist/board/render/renderers/task-card-renderer.js +7 -8
  43. package/dist/board/render/renderers/text-card-renderer.js +86 -41
  44. package/dist/board/render/renderers/video-card-renderer.js +2 -1
  45. package/dist/board/render/style.d.ts +7 -2
  46. package/dist/board/render/style.js +5 -1
  47. package/dist/board/render/surface.d.ts +5 -0
  48. package/dist/board/render/surface.js +13 -3
  49. package/dist/board/render/text-fit.js +8 -12
  50. package/dist/board/render/text-measurement.d.ts +15 -1
  51. package/dist/board/render/text-measurement.js +113 -20
  52. package/dist/board/render/text-resolution.d.ts +1 -6
  53. package/dist/board/render/text-resolution.js +1 -10
  54. package/dist/board/render/theme.d.ts +15 -0
  55. package/dist/board/render/theme.js +15 -0
  56. package/dist/board/stage/assets.d.ts +15 -4
  57. package/dist/board/stage/assets.js +155 -89
  58. package/dist/board/stage/index.d.ts +5 -5
  59. package/dist/board/stage/index.js +4 -4
  60. package/dist/board/stage/lru.d.ts +13 -9
  61. package/dist/board/stage/lru.js +38 -20
  62. package/dist/board/stage/scene.d.ts +11 -1
  63. package/dist/board/stage/scene.js +213 -138
  64. package/dist/board/stage/stage.d.ts +12 -1
  65. package/dist/board/stage/stage.js +97 -24
  66. package/dist/board/stage/texture-pool.d.ts +8 -1
  67. package/dist/board/stage/texture-pool.js +37 -1
  68. package/dist/chunks/app-realtime.js +1 -1
  69. package/dist/chunks/{transport.js → http-error.js} +37 -1
  70. package/dist/chunks/http.js +30 -10
  71. package/dist/http.js +1 -1
  72. package/dist/index.js +139 -131
  73. package/dist/media.d.ts +3 -27
  74. package/dist/media.js +3 -82
  75. package/dist/protocol/dist/board-constants.js +3 -3
  76. package/dist/protocol/dist/board-layout.d.ts +17 -3
  77. package/dist/protocol/dist/board-layout.js +68 -11
  78. package/dist/protocol/dist/core/media.d.ts +35 -1
  79. package/dist/protocol/dist/core/media.js +123 -1
  80. package/dist/protocol/dist/index.d.ts +2 -2
  81. package/dist/websocket.js +951 -1
  82. package/package.json +3 -2
  83. package/dist/board/model/image-key.d.ts +0 -9
  84. package/dist/board/model/image-key.js +0 -33
  85. package/dist/chunks/websocket.js +0 -988
@@ -1,988 +0,0 @@
1
- import { _ as isRealtimeDomain, c as WS_BOARD_AWARENESS_CAPABILITY, d as WS_REALTIME_ROOM_HISTORY_CAPABILITY, g as getSessionTurnPatchStreamKey, h as getRealtimeSpaceRoom, l as WS_COMPACT_STREAM_CAPABILITY, p as WS_ROOM_SUBSCRIPTION_CAPABILITY, t as HttpError, u as WS_REALTIME_ROOM_CAPABILITY, v as normalizeRealtimeRooms } from "./transport.js";
2
- import { f as resolveWebsocketUrl } from "./environment.js";
3
- //#region src/http-error.ts
4
- /** Shared HTTP error code for every plan entitlement gate (402). */
5
- const FEATURE_NOT_ENTITLED_ERROR_CODE = "feature_not_entitled";
6
- /** Shared HTTP error code for a blocked usage gate (negative balance limit, 402). */
7
- const BILLING_ACCESS_BLOCKED_ERROR_CODE = "billing_credit_limit_exceeded";
8
- function isHttpErrorCode(error, code) {
9
- return error instanceof HttpError && error.code === code;
10
- }
11
- function isFeatureNotEntitledError(error) {
12
- return isHttpErrorCode(error, FEATURE_NOT_ENTITLED_ERROR_CODE);
13
- }
14
- function isBillingAccessBlockedError(error) {
15
- return isHttpErrorCode(error, BILLING_ACCESS_BLOCKED_ERROR_CODE);
16
- }
17
- function isBillingAccessBlockedCode(code) {
18
- return code === BILLING_ACCESS_BLOCKED_ERROR_CODE;
19
- }
20
- const isRecord$1 = (value) => !!value && typeof value === "object" && !Array.isArray(value);
21
- function isBillingConversionIntent(value) {
22
- if (!isRecord$1(value)) return false;
23
- return (value.level === "soft" || value.level === "hard") && typeof value.title === "string" && typeof value.message === "string" && isRecord$1(value.primaryAction) && value.primaryAction.action === "open_billing_conversion";
24
- }
25
- /**
26
- * Extracts the standard `billing` payload from a response body, an
27
- * `HttpError`, or a bare billing payload (e.g. a realtime error event's
28
- * `billing` field). Returns `null` unless it carries a valid conversion
29
- * intent.
30
- */
31
- function extractBillingPayload(source) {
32
- const root = source instanceof HttpError ? source.body : source;
33
- if (!isRecord$1(root)) return null;
34
- const billing = isRecord$1(root.billing) ? root.billing : root;
35
- if (!isBillingConversionIntent(billing.conversion)) return null;
36
- return billing;
37
- }
38
- //#endregion
39
- //#region src/websocket.ts
40
- const createEventMap = () => ({
41
- connecting: /* @__PURE__ */ new Set(),
42
- reconnecting: /* @__PURE__ */ new Set(),
43
- open: /* @__PURE__ */ new Set(),
44
- close: /* @__PURE__ */ new Set(),
45
- error: /* @__PURE__ */ new Set(),
46
- event: /* @__PURE__ */ new Set(),
47
- ready: /* @__PURE__ */ new Set(),
48
- auth: /* @__PURE__ */ new Set(),
49
- messageAccepted: /* @__PURE__ */ new Set(),
50
- serverError: /* @__PURE__ */ new Set(),
51
- subscribed: /* @__PURE__ */ new Set(),
52
- subscribeError: /* @__PURE__ */ new Set(),
53
- pong: /* @__PURE__ */ new Set()
54
- });
55
- const toWebSocketUrl = (input, env) => resolveWebsocketUrl({
56
- url: input,
57
- env
58
- });
59
- const normalizeOptions = (options = {}) => ({
60
- url: toWebSocketUrl(options.url, options.env),
61
- autoReconnect: options.autoReconnect !== false,
62
- reconnectBaseDelayMs: options.reconnectBaseDelayMs ?? 1e3,
63
- reconnectMaxDelayMs: options.reconnectMaxDelayMs ?? 15e3,
64
- connectTimeoutMs: normalizeConnectTimeoutMs(options.connectTimeoutMs ?? 15e3),
65
- pingIntervalMs: options.pingIntervalMs ?? 2e4,
66
- pongTimeoutMs: options.pongTimeoutMs ?? 15e3,
67
- debug: options.debug === true
68
- });
69
- const formatCloseMessage = (code, reason) => `WebSocket closed: ${code ?? 0} ${reason || ""}`.trim();
70
- const normalizeConnectTimeoutMs = (value) => {
71
- if (!Number.isInteger(value) || value < 1 || value > 2147483647) throw new Error("connectTimeoutMs must be an integer between 1 and 2147483647");
72
- return value;
73
- };
74
- const AUTH_CLOSE_CODE = 4003;
75
- const AUTH_CLOSE_REASON = "authentication failed";
76
- const isRetryableCloseCode = (code) => {
77
- if (code === 1e3) return false;
78
- if (code === AUTH_CLOSE_CODE) return false;
79
- return true;
80
- };
81
- const PATCH_STREAM_BUFFER_MAX_PENDING = 128;
82
- const isRecord = (value) => Boolean(value && typeof value === "object" && !Array.isArray(value));
83
- const isRealtimeCompactFrame = (value) => {
84
- if (!isRecord(value)) return false;
85
- if (value.t !== "d" && value.t !== "p") return false;
86
- if (typeof value.sid !== "string" || !value.sid) return false;
87
- if (typeof value.s !== "number" || !Number.isInteger(value.s) || value.s < 0) return false;
88
- if (typeof value.b !== "number" || !Number.isInteger(value.b) || value.b < 0) return false;
89
- if (value.t === "d") return "v" in value;
90
- return (value.o === "append" || value.o === "replace" || value.o === "add" || value.o === "merge" || value.o === "remove") && typeof value.p === "string" && value.p.length > 0;
91
- };
92
- const isRealtimeEnvelope = (value) => {
93
- if (!isRecord(value)) return false;
94
- if (typeof value.id !== "string") return false;
95
- if (typeof value.timestamp !== "number") return false;
96
- if (!isRealtimeDomain(value.domain)) return false;
97
- if (typeof value.type !== "string") return false;
98
- if (!isRecord(value.payload)) return false;
99
- return true;
100
- };
101
- const compactFrameToPatchOperation = (frame) => {
102
- if (frame.t === "d") return { v: frame.v };
103
- if (frame.o === "remove") return {
104
- o: "remove",
105
- p: frame.p
106
- };
107
- if (frame.o === "merge") return isRecord(frame.v) ? {
108
- o: "merge",
109
- p: frame.p,
110
- v: frame.v
111
- } : null;
112
- if (!("v" in frame)) return null;
113
- switch (frame.o) {
114
- case "append": return {
115
- o: "append",
116
- p: frame.p,
117
- v: frame.v
118
- };
119
- case "replace": return {
120
- o: "replace",
121
- p: frame.p,
122
- v: frame.v
123
- };
124
- case "add": return {
125
- o: "add",
126
- p: frame.p,
127
- v: frame.v
128
- };
129
- default: return null;
130
- }
131
- };
132
- var WebsocketAuthError = class extends Error {
133
- constructor(message) {
134
- super(message);
135
- this.name = "WebsocketAuthError";
136
- }
137
- };
138
- var WebsocketClient = class {
139
- url;
140
- autoReconnect;
141
- reconnectBaseDelayMs;
142
- reconnectMaxDelayMs;
143
- connectTimeoutMs;
144
- pingIntervalMs;
145
- pongTimeoutMs;
146
- debug;
147
- getAccessToken;
148
- WebSocketImpl;
149
- ws = null;
150
- pingTimer = null;
151
- reconnectTimer = null;
152
- reconnectTimerResolver = null;
153
- reconnectAttempt = 0;
154
- authReconnectAttempted = false;
155
- forceRefreshOnNextAuth = false;
156
- retryAuthClose = false;
157
- manuallyClosed = false;
158
- connectPromise = null;
159
- pendingConnectReject = null;
160
- authWaiter = null;
161
- awaitingPong = false;
162
- lastPingRequestId = null;
163
- pongDeadlineAt = 0;
164
- compactStreamContexts = /* @__PURE__ */ new Map();
165
- patchStreamBuffers = /* @__PURE__ */ new Map();
166
- roomSubscriptions = /* @__PURE__ */ new Map();
167
- serverCapabilities = /* @__PURE__ */ new Set();
168
- state = "idle";
169
- connectionId = null;
170
- supportsCapability(capability) {
171
- return this.serverCapabilities.has(capability);
172
- }
173
- listeners = createEventMap();
174
- constructor(options = {}) {
175
- const normalized = normalizeOptions(options);
176
- this.url = normalized.url;
177
- this.autoReconnect = normalized.autoReconnect;
178
- this.reconnectBaseDelayMs = normalized.reconnectBaseDelayMs;
179
- this.reconnectMaxDelayMs = normalized.reconnectMaxDelayMs;
180
- this.connectTimeoutMs = normalized.connectTimeoutMs;
181
- this.pingIntervalMs = normalized.pingIntervalMs;
182
- this.pongTimeoutMs = normalized.pongTimeoutMs;
183
- this.debug = normalized.debug;
184
- this.getAccessToken = options.getAccessToken;
185
- this.WebSocketImpl = options.WebSocketImpl ?? WebSocket;
186
- }
187
- on(type, handler) {
188
- this.listeners[type].add(handler);
189
- return () => this.off(type, handler);
190
- }
191
- off(type, handler) {
192
- this.listeners[type].delete(handler);
193
- }
194
- emit(type, payload) {
195
- for (const handler of this.listeners[type]) handler(payload);
196
- }
197
- log(...args) {
198
- if (this.debug) console.log("[WebsocketClient]", ...args);
199
- }
200
- async connect() {
201
- if (this.connectPromise) return this.connectPromise;
202
- if (this.state === "open" && this.ws?.readyState === WebSocket.OPEN) return;
203
- const isReconnect = this.reconnectAttempt > 0 || this.state === "reconnecting";
204
- this.manuallyClosed = false;
205
- this.clearReconnectTimer();
206
- this.state = isReconnect ? "reconnecting" : "connecting";
207
- this.emit("connecting", {
208
- isReconnect,
209
- attempt: this.reconnectAttempt
210
- });
211
- this.connectPromise = new Promise((resolve, reject) => {
212
- const ws = new this.WebSocketImpl(this.url);
213
- this.ws = ws;
214
- let settled = false;
215
- let closed = false;
216
- let connectTimer = null;
217
- const clearConnectTimer = () => {
218
- if (connectTimer === null) return;
219
- clearTimeout(connectTimer);
220
- connectTimer = null;
221
- };
222
- const rejectOnce = (error) => {
223
- if (settled) return;
224
- clearConnectTimer();
225
- settled = true;
226
- this.connectPromise = null;
227
- if (this.pendingConnectReject === rejectOnce) this.pendingConnectReject = null;
228
- reject(error);
229
- };
230
- const resolveOnce = () => {
231
- if (settled) return;
232
- clearConnectTimer();
233
- settled = true;
234
- this.connectPromise = null;
235
- if (this.pendingConnectReject === rejectOnce) this.pendingConnectReject = null;
236
- resolve();
237
- };
238
- const isCurrentConnection = () => this.ws === ws && !closed;
239
- const cleanupSocket = () => {
240
- ws.onopen = null;
241
- ws.onmessage = null;
242
- ws.onerror = null;
243
- ws.onclose = null;
244
- };
245
- const handleClose = (event) => {
246
- if (closed || this.ws !== ws) return;
247
- closed = true;
248
- clearConnectTimer();
249
- this.stopPingLoop();
250
- const wasConnecting = this.state === "connecting" || this.state === "reconnecting";
251
- this.state = "closed";
252
- this.ws = null;
253
- this.compactStreamContexts.clear();
254
- this.patchStreamBuffers.clear();
255
- this.resetRoomSubscriptions();
256
- const closeError = new Error(formatCloseMessage(event.code, event.reason));
257
- this.rejectAuthWaiter(closeError);
258
- const retryAuthClose = event.code === AUTH_CLOSE_CODE && this.retryAuthClose;
259
- this.retryAuthClose = false;
260
- const willReconnect = !this.manuallyClosed && this.autoReconnect && (retryAuthClose || isRetryableCloseCode(event.code));
261
- this.log("closed", {
262
- code: event.code,
263
- reason: event.reason,
264
- willReconnect,
265
- wasConnecting
266
- });
267
- this.emit("close", {
268
- code: event.code,
269
- reason: event.reason,
270
- willReconnect
271
- });
272
- cleanupSocket();
273
- if (wasConnecting) rejectOnce(closeError);
274
- if (willReconnect) this.scheduleReconnect(event.code, event.reason);
275
- };
276
- ws.onopen = async () => {
277
- if (!isCurrentConnection()) return;
278
- clearConnectTimer();
279
- try {
280
- this.log("connected", {
281
- url: this.url,
282
- isReconnect,
283
- attempt: this.reconnectAttempt
284
- });
285
- this.startPingLoop();
286
- await this.authenticate();
287
- if (!isCurrentConnection()) return;
288
- this.state = "open";
289
- this.restoreRoomSubscriptions();
290
- this.reconnectAttempt = 0;
291
- this.emit("open", { connectionId: this.connectionId });
292
- resolveOnce();
293
- } catch (error) {
294
- if (!isCurrentConnection()) return;
295
- const authError = error instanceof Error ? error : /* @__PURE__ */ new Error("authentication failed");
296
- const recoverable = Boolean(this.getAccessToken && this.autoReconnect && !this.manuallyClosed && !this.authReconnectAttempted);
297
- this.retryAuthClose = recoverable;
298
- if (recoverable) {
299
- this.authReconnectAttempted = true;
300
- this.forceRefreshOnNextAuth = true;
301
- }
302
- this.emit("error", {
303
- error: authError,
304
- recoverable
305
- });
306
- rejectOnce(authError);
307
- ws.close(AUTH_CLOSE_CODE, AUTH_CLOSE_REASON);
308
- }
309
- };
310
- ws.onmessage = (event) => {
311
- if (!isCurrentConnection()) return;
312
- this.handleMessage(event.data);
313
- };
314
- ws.onerror = (error) => {
315
- if (!isCurrentConnection()) return;
316
- this.emit("error", {
317
- error,
318
- recoverable: !this.manuallyClosed
319
- });
320
- };
321
- ws.onclose = (event) => {
322
- handleClose(event);
323
- };
324
- this.pendingConnectReject = rejectOnce;
325
- connectTimer = setTimeout(() => {
326
- if (!isCurrentConnection() || settled) return;
327
- const reason = "connect timeout";
328
- const error = /* @__PURE__ */ new Error(`WebSocket ${reason} after ${this.connectTimeoutMs}ms`);
329
- this.emit("error", {
330
- error,
331
- recoverable: !this.manuallyClosed && this.autoReconnect
332
- });
333
- try {
334
- ws.close(4002, reason);
335
- } finally {
336
- handleClose({
337
- code: 4002,
338
- reason
339
- });
340
- }
341
- }, this.connectTimeoutMs);
342
- });
343
- return this.connectPromise;
344
- }
345
- async disconnect(code = 1e3, reason = "manual") {
346
- this.manuallyClosed = true;
347
- this.clearReconnectTimer();
348
- this.stopPingLoop();
349
- this.state = "closed";
350
- this.rejectAuthWaiter(/* @__PURE__ */ new Error("disconnected"));
351
- const ws = this.ws;
352
- this.ws = null;
353
- ws?.close(code, reason);
354
- this.pendingConnectReject?.(/* @__PURE__ */ new Error(`WebSocket disconnected: ${reason}`));
355
- this.pendingConnectReject = null;
356
- this.connectPromise = null;
357
- if (ws) {
358
- ws.onopen = null;
359
- ws.onmessage = null;
360
- ws.onerror = null;
361
- ws.onclose = null;
362
- this.emit("close", {
363
- code,
364
- reason,
365
- willReconnect: false
366
- });
367
- }
368
- this.authReconnectAttempted = false;
369
- this.forceRefreshOnNextAuth = false;
370
- this.retryAuthClose = false;
371
- this.compactStreamContexts.clear();
372
- this.patchStreamBuffers.clear();
373
- this.resetRoomSubscriptions();
374
- }
375
- async updatePresence(input) {
376
- await this.ensureOpen();
377
- this.send({
378
- type: "presence.update",
379
- requestId: input.requestId,
380
- payload: {
381
- spaceId: input.spaceId,
382
- meta: input.meta ?? null
383
- }
384
- });
385
- }
386
- async updateBoardAwareness(input) {
387
- await this.ensureOpen();
388
- this.send({
389
- type: "board.awareness.update",
390
- requestId: input.requestId,
391
- payload: {
392
- spaceId: input.spaceId,
393
- boardId: input.boardId,
394
- seq: input.seq,
395
- update: input.update
396
- }
397
- });
398
- }
399
- async joinRealtimeRoom(input) {
400
- await this.ensureOpen();
401
- this.send({
402
- type: "realtime.room.join",
403
- requestId: input.requestId,
404
- payload: {
405
- roomId: input.roomId,
406
- ticket: input.ticket,
407
- ...input.instanceId ? { instanceId: input.instanceId } : {},
408
- ...input.after !== void 0 ? { after: input.after } : {}
409
- }
410
- });
411
- }
412
- async readRealtimeRoomHistory(input) {
413
- await this.ensureOpen();
414
- this.send({
415
- type: "realtime.room.history",
416
- requestId: input.requestId,
417
- payload: {
418
- roomId: input.roomId,
419
- ...input.after !== void 0 ? { after: input.after } : {},
420
- ...input.limit !== void 0 ? { limit: input.limit } : {}
421
- }
422
- });
423
- }
424
- async publishRealtimeRoom(input) {
425
- await this.ensureOpen();
426
- this.send({
427
- type: "realtime.room.publish",
428
- requestId: input.requestId,
429
- payload: {
430
- roomId: input.roomId,
431
- event: input.event,
432
- data: input.data,
433
- clientEventId: input.clientEventId
434
- }
435
- });
436
- }
437
- async detachRealtimeRoom(input) {
438
- await this.ensureOpen();
439
- this.send({
440
- type: "realtime.room.detach",
441
- requestId: input.requestId,
442
- payload: { roomId: input.roomId }
443
- });
444
- }
445
- async leaveRealtimeRoom(input) {
446
- await this.ensureOpen();
447
- this.send({
448
- type: "realtime.room.leave",
449
- requestId: input.requestId,
450
- payload: { roomId: input.roomId }
451
- });
452
- }
453
- async updateRealtimeRoomPresence(input) {
454
- await this.ensureOpen();
455
- this.send({
456
- type: "realtime.room.presence.update",
457
- requestId: input.requestId,
458
- payload: {
459
- roomId: input.roomId,
460
- presence: input.presence
461
- }
462
- });
463
- }
464
- async sendMessage(input) {
465
- await this.ensureOpen();
466
- this.send({
467
- type: "session.message.create",
468
- requestId: input.requestId,
469
- payload: {
470
- spaceId: input.spaceId,
471
- sessionId: input.sessionId,
472
- content: input.content,
473
- clientMessageId: input.clientMessageId,
474
- model: input.model,
475
- provider: input.provider,
476
- thinkingLevel: input.thinkingLevel
477
- }
478
- });
479
- }
480
- retainRooms(rooms) {
481
- const normalized = normalizeRealtimeRooms(rooms);
482
- if (normalized.length === 0) return () => void 0;
483
- for (const room of normalized) {
484
- const state = this.roomSubscriptions.get(room) ?? {
485
- refCount: 0,
486
- subscribed: false,
487
- pending: false
488
- };
489
- state.refCount += 1;
490
- this.roomSubscriptions.set(room, state);
491
- }
492
- this.flushRoomSubscriptions();
493
- if (this.state !== "open" && this.state !== "connecting" && this.state !== "reconnecting") this.connect().then(() => this.flushRoomSubscriptions()).catch((error) => this.emit("error", {
494
- error,
495
- recoverable: true
496
- }));
497
- let released = false;
498
- return () => {
499
- if (released) return;
500
- released = true;
501
- const roomsToRelease = [];
502
- for (const room of normalized) {
503
- const state = this.roomSubscriptions.get(room);
504
- if (!state) continue;
505
- state.refCount -= 1;
506
- if (state.refCount > 0) {
507
- this.roomSubscriptions.set(room, state);
508
- continue;
509
- }
510
- this.roomSubscriptions.delete(room);
511
- if (state.subscribed) roomsToRelease.push(room);
512
- }
513
- if (roomsToRelease.length === 0) return;
514
- if (this.ws?.readyState === WebSocket.OPEN) this.send({
515
- type: "unsubscribe",
516
- payload: { rooms: roomsToRelease }
517
- });
518
- };
519
- }
520
- subscribeRooms(rooms) {
521
- return this.retainRooms(rooms);
522
- }
523
- subscribeSpace(spaceId) {
524
- return this.retainRooms([getRealtimeSpaceRoom(spaceId)]);
525
- }
526
- ack(eventId, requestId) {
527
- this.send({
528
- type: "ack",
529
- requestId,
530
- payload: eventId ? { eventId } : void 0
531
- });
532
- }
533
- ping(requestId) {
534
- const effectiveRequestId = requestId ?? `ping-${Date.now()}`;
535
- this.awaitingPong = true;
536
- this.lastPingRequestId = effectiveRequestId;
537
- this.pongDeadlineAt = Date.now() + this.pongTimeoutMs;
538
- this.send({
539
- type: "ping",
540
- requestId: effectiveRequestId,
541
- payload: {}
542
- });
543
- }
544
- async ensureOpen() {
545
- if (this.state === "open" && this.ws?.readyState === WebSocket.OPEN) return;
546
- await this.connect();
547
- }
548
- send(event) {
549
- const ws = this.ws;
550
- if (!ws || ws.readyState !== WebSocket.OPEN) throw new Error("websocket is not open");
551
- ws.send(JSON.stringify(event));
552
- }
553
- async authenticate() {
554
- const forceRefresh = this.forceRefreshOnNextAuth;
555
- this.forceRefreshOnNextAuth = false;
556
- const token = this.getAccessToken ? await this.getAccessToken(forceRefresh ? { forceRefresh: true } : void 0) : null;
557
- if (!token) throw new WebsocketAuthError("missing access token");
558
- const waiter = this.createAuthWaiter();
559
- this.send({
560
- type: "auth",
561
- payload: {
562
- token,
563
- capabilities: [
564
- WS_COMPACT_STREAM_CAPABILITY,
565
- WS_ROOM_SUBSCRIPTION_CAPABILITY,
566
- WS_BOARD_AWARENESS_CAPABILITY,
567
- WS_REALTIME_ROOM_CAPABILITY,
568
- WS_REALTIME_ROOM_HISTORY_CAPABILITY
569
- ]
570
- }
571
- });
572
- await waiter.promise;
573
- this.authReconnectAttempted = false;
574
- }
575
- flushRoomSubscriptions() {
576
- if (this.state !== "open" || this.ws?.readyState !== WebSocket.OPEN) return;
577
- const rooms = [...this.roomSubscriptions.entries()].filter(([, state]) => state.refCount > 0 && !state.subscribed && !state.pending).map(([room]) => room);
578
- if (rooms.length === 0) return;
579
- this.send({
580
- type: "subscribe",
581
- payload: { rooms }
582
- });
583
- for (const room of rooms) {
584
- const state = this.roomSubscriptions.get(room);
585
- if (state) state.pending = true;
586
- }
587
- }
588
- restoreRoomSubscriptions() {
589
- this.resetRoomSubscriptions();
590
- this.flushRoomSubscriptions();
591
- }
592
- resetRoomSubscriptions() {
593
- for (const state of this.roomSubscriptions.values()) {
594
- state.subscribed = false;
595
- state.pending = false;
596
- }
597
- }
598
- createAuthWaiter() {
599
- this.rejectAuthWaiter(/* @__PURE__ */ new Error("superseded auth waiter"));
600
- let resolve;
601
- let reject;
602
- const promise = new Promise((res, rej) => {
603
- resolve = res;
604
- reject = rej;
605
- });
606
- this.authWaiter = {
607
- promise,
608
- resolve,
609
- reject
610
- };
611
- return this.authWaiter;
612
- }
613
- resolveAuthWaiter() {
614
- if (!this.authWaiter) return;
615
- this.authWaiter.resolve();
616
- this.authWaiter = null;
617
- }
618
- rejectAuthWaiter(error) {
619
- if (!this.authWaiter) return;
620
- this.authWaiter.reject(error);
621
- this.authWaiter = null;
622
- }
623
- handleMessage(raw) {
624
- let parsed;
625
- try {
626
- parsed = typeof raw === "string" ? JSON.parse(raw) : JSON.parse(String(raw));
627
- } catch {
628
- this.emit("error", {
629
- error: /* @__PURE__ */ new Error("invalid websocket payload"),
630
- recoverable: true
631
- });
632
- return;
633
- }
634
- if (isRealtimeCompactFrame(parsed)) {
635
- this.handleCompactFrame(parsed);
636
- return;
637
- }
638
- if (!isRealtimeEnvelope(parsed)) {
639
- this.emit("error", {
640
- error: /* @__PURE__ */ new Error("invalid realtime envelope"),
641
- recoverable: true
642
- });
643
- return;
644
- }
645
- const envelope = parsed;
646
- this.rememberCompactStreamContext(envelope);
647
- if (envelope.type === "session.turn.patch") {
648
- this.handlePatchEnvelope(envelope);
649
- return;
650
- }
651
- switch (envelope.type) {
652
- case "system.ready": {
653
- const connectionId = typeof envelope.payload.connectionId === "string" ? envelope.payload.connectionId : null;
654
- if (connectionId) {
655
- this.connectionId = connectionId;
656
- this.emit("ready", { connectionId });
657
- }
658
- this.emit("event", envelope);
659
- return;
660
- }
661
- case "system.auth.ok": {
662
- const connectionId = typeof envelope.payload.connectionId === "string" ? envelope.payload.connectionId : this.connectionId;
663
- const user = envelope.payload.user && typeof envelope.payload.user === "object" ? envelope.payload.user : {};
664
- const capabilities = Array.isArray(envelope.payload.capabilities) ? envelope.payload.capabilities.filter((value) => typeof value === "string") : [];
665
- this.serverCapabilities.clear();
666
- for (const capability of capabilities) this.serverCapabilities.add(capability);
667
- if (connectionId) {
668
- this.connectionId = connectionId;
669
- this.emit("auth", {
670
- connectionId,
671
- user,
672
- capabilities
673
- });
674
- }
675
- this.resolveAuthWaiter();
676
- this.emit("event", envelope);
677
- return;
678
- }
679
- case "system.request.error": {
680
- const message = typeof envelope.payload.message === "string" ? envelope.payload.message : "request failed";
681
- const code = typeof envelope.payload.code === "string" ? envelope.payload.code : void 0;
682
- const error = new WebsocketAuthError(message);
683
- this.rejectAuthWaiter(error);
684
- this.emit("serverError", {
685
- code,
686
- message,
687
- requestId: envelope.requestId ?? null,
688
- sessionId: envelope.sessionId ?? null,
689
- spaceId: envelope.spaceId ?? null
690
- });
691
- this.emit("event", envelope);
692
- return;
693
- }
694
- case "session.request.accepted": {
695
- const payload = envelope.payload;
696
- this.emit("messageAccepted", {
697
- requestId: envelope.requestId ?? null,
698
- sessionId: envelope.sessionId ?? null,
699
- spaceId: envelope.spaceId ?? null,
700
- clientMessageId: typeof payload.clientMessageId === "string" ? payload.clientMessageId : null,
701
- turnId: typeof payload.turnId === "string" ? payload.turnId : null,
702
- userMessageId: typeof payload.userMessageId === "string" ? payload.userMessageId : null,
703
- traceId: typeof payload.traceId === "string" ? payload.traceId : null
704
- });
705
- this.emit("event", envelope);
706
- return;
707
- }
708
- case "session.request.error": {
709
- const payload = envelope.payload;
710
- this.emit("serverError", {
711
- code: typeof payload.code === "string" ? payload.code : void 0,
712
- message: typeof payload.message === "string" ? payload.message : void 0,
713
- requestId: envelope.requestId ?? null,
714
- sessionId: envelope.sessionId ?? null,
715
- spaceId: envelope.spaceId ?? null,
716
- clientMessageId: typeof payload.clientMessageId === "string" ? payload.clientMessageId : null,
717
- billing: extractBillingPayload(payload.billing)
718
- });
719
- this.emit("event", envelope);
720
- return;
721
- }
722
- case "system.pong": {
723
- const requestId = envelope.requestId ?? null;
724
- if (!requestId || requestId === this.lastPingRequestId) {
725
- this.awaitingPong = false;
726
- this.lastPingRequestId = null;
727
- this.pongDeadlineAt = 0;
728
- }
729
- this.emit("pong", { requestId });
730
- return;
731
- }
732
- case "system.subscribe.ok": {
733
- const payload = envelope.payload;
734
- const rooms = normalizeRealtimeRooms(Array.isArray(payload.rooms) ? payload.rooms.filter((room) => typeof room === "string") : []);
735
- for (const room of rooms) {
736
- const state = this.roomSubscriptions.get(room);
737
- if (state) {
738
- state.subscribed = true;
739
- state.pending = false;
740
- }
741
- }
742
- this.emit("subscribed", {
743
- rooms,
744
- requestId: envelope.requestId ?? null
745
- });
746
- this.emit("event", envelope);
747
- return;
748
- }
749
- case "system.subscribe.error": {
750
- const payload = envelope.payload;
751
- const rejected = Array.isArray(payload.rejected) ? payload.rejected.filter((entry) => Boolean(entry && typeof entry === "object")).map((entry) => ({
752
- room: typeof entry.room === "string" ? entry.room : "",
753
- code: typeof entry.code === "string" ? entry.code : "UNKNOWN",
754
- message: typeof entry.message === "string" ? entry.message : "Subscription failed"
755
- })).filter((entry) => entry.room) : [];
756
- for (const item of rejected) {
757
- const room = normalizeRealtimeRooms([item.room])[0];
758
- if (!room) continue;
759
- this.roomSubscriptions.delete(room);
760
- }
761
- this.emit("subscribeError", {
762
- rejected,
763
- requestId: envelope.requestId ?? null
764
- });
765
- this.emit("event", envelope);
766
- return;
767
- }
768
- case "system.ack.ok": return;
769
- default:
770
- this.emit("event", envelope);
771
- return;
772
- }
773
- }
774
- rememberCompactStreamContext(envelope) {
775
- if (envelope.type === "session.turn.patch") {
776
- const payload = envelope.payload;
777
- const turnId = typeof payload.turnId === "string" ? payload.turnId : null;
778
- const messageId = typeof payload.messageId === "string" ? payload.messageId : null;
779
- const realtimeMeta = payload._rt && typeof payload._rt === "object" ? payload._rt : null;
780
- const sid = typeof realtimeMeta?.sid === "string" && realtimeMeta.sid.trim() ? realtimeMeta.sid : turnId ?? messageId;
781
- if (!sid) return;
782
- this.compactStreamContexts.set(sid, {
783
- spaceId: envelope.spaceId ?? null,
784
- sessionId: envelope.sessionId ?? null,
785
- turnId,
786
- messageId,
787
- messageOrdinal: typeof payload.messageOrdinal === "number" ? payload.messageOrdinal : null,
788
- anchorUserMessageId: typeof payload.anchorUserMessageId === "string" ? payload.anchorUserMessageId : null
789
- });
790
- return;
791
- }
792
- if (envelope.type !== "session.message.persisted") return;
793
- const message = envelope.payload.message;
794
- if (!message || typeof message !== "object") return;
795
- const meta = message.meta;
796
- const turnId = typeof meta?.turnId === "string" ? meta.turnId : null;
797
- if (!turnId) return;
798
- for (const [sid, context] of this.compactStreamContexts.entries()) if (context.turnId === turnId) {
799
- this.compactStreamContexts.delete(sid);
800
- this.patchStreamBuffers.delete(sid);
801
- }
802
- }
803
- getPatchStreamBufferKey(envelope) {
804
- if (envelope.type !== "session.turn.patch") return null;
805
- const payload = envelope.payload;
806
- const realtimeMeta = payload._rt && typeof payload._rt === "object" ? payload._rt : null;
807
- if (typeof realtimeMeta?.sid === "string" && realtimeMeta.sid.trim()) return realtimeMeta.sid;
808
- return getSessionTurnPatchStreamKey(payload, { includeSessionFallback: true });
809
- }
810
- handlePatchEnvelope(envelope) {
811
- const payload = envelope.payload;
812
- if (typeof payload.seq !== "number" || typeof payload.baseSeq !== "number" || !Number.isInteger(payload.seq) || !Number.isInteger(payload.baseSeq) || payload.seq < 0 || payload.baseSeq < 0) {
813
- this.emit("event", envelope);
814
- return;
815
- }
816
- const key = this.getPatchStreamBufferKey(envelope);
817
- if (!key) {
818
- this.emit("event", envelope);
819
- return;
820
- }
821
- if (payload.baseSeq === 0) {
822
- const buffer = {
823
- nextSeq: payload.seq + 1,
824
- pending: /* @__PURE__ */ new Map()
825
- };
826
- this.patchStreamBuffers.set(key, buffer);
827
- this.emit("event", envelope);
828
- this.flushPatchStreamBuffer(buffer);
829
- return;
830
- }
831
- const buffer = this.patchStreamBuffers.get(key);
832
- if (!buffer) {
833
- const newBuffer = {
834
- nextSeq: payload.baseSeq + 1,
835
- pending: /* @__PURE__ */ new Map([[payload.seq, envelope]])
836
- };
837
- this.patchStreamBuffers.set(key, newBuffer);
838
- this.flushPatchStreamBuffer(newBuffer);
839
- return;
840
- }
841
- if (payload.seq < buffer.nextSeq) return;
842
- buffer.pending.set(payload.seq, envelope);
843
- if (!this.enforcePatchStreamBufferLimit(key, buffer)) return;
844
- this.flushPatchStreamBuffer(buffer);
845
- }
846
- enforcePatchStreamBufferLimit(key, buffer) {
847
- if (buffer.pending.size <= PATCH_STREAM_BUFFER_MAX_PENDING) return true;
848
- this.patchStreamBuffers.delete(key);
849
- this.emit("error", {
850
- error: /* @__PURE__ */ new Error(`patch stream buffer overflow: ${key}`),
851
- recoverable: true
852
- });
853
- return false;
854
- }
855
- flushPatchStreamBuffer(buffer) {
856
- while (true) {
857
- const envelope = buffer.pending.get(buffer.nextSeq);
858
- if (!envelope) return;
859
- const seq = envelope.payload.seq;
860
- if (typeof seq !== "number" || !Number.isInteger(seq)) return;
861
- buffer.pending.delete(buffer.nextSeq);
862
- buffer.nextSeq = seq + 1;
863
- this.emit("event", envelope);
864
- }
865
- }
866
- handleCompactFrame(frame) {
867
- const context = this.compactStreamContexts.get(frame.sid);
868
- if (!context?.sessionId) {
869
- this.emit("error", {
870
- error: /* @__PURE__ */ new Error(`unknown compact stream: ${frame.sid}`),
871
- recoverable: true
872
- });
873
- return;
874
- }
875
- const op = compactFrameToPatchOperation(frame);
876
- if (!op) {
877
- this.emit("error", {
878
- error: /* @__PURE__ */ new Error(`invalid compact stream frame: ${frame.sid}`),
879
- recoverable: true
880
- });
881
- return;
882
- }
883
- const envelope = {
884
- id: `compact:${frame.sid}:${frame.s}`,
885
- timestamp: Date.now(),
886
- domain: "session",
887
- type: "session.turn.patch",
888
- spaceId: context.spaceId,
889
- sessionId: context.sessionId,
890
- payload: {
891
- turnId: context.turnId,
892
- messageId: context.messageId,
893
- messageOrdinal: context.messageOrdinal,
894
- sourceMessageId: context.messageId,
895
- anchorUserMessageId: context.anchorUserMessageId,
896
- seq: frame.s,
897
- baseSeq: frame.b,
898
- ops: [op],
899
- _rt: { sid: frame.sid }
900
- }
901
- };
902
- this.handlePatchEnvelope(envelope);
903
- }
904
- startPingLoop() {
905
- this.stopPingLoop();
906
- this.pingTimer = setInterval(() => {
907
- if (this.ws?.readyState !== WebSocket.OPEN) return;
908
- if (this.awaitingPong && this.pongDeadlineAt > 0 && Date.now() > this.pongDeadlineAt) {
909
- this.emit("error", {
910
- error: /* @__PURE__ */ new Error("websocket pong timeout"),
911
- recoverable: true
912
- });
913
- this.ws.close(4002, "pong timeout");
914
- return;
915
- }
916
- this.ping();
917
- }, this.pingIntervalMs);
918
- }
919
- stopPingLoop() {
920
- if (this.pingTimer) {
921
- clearInterval(this.pingTimer);
922
- this.pingTimer = null;
923
- }
924
- this.awaitingPong = false;
925
- this.lastPingRequestId = null;
926
- this.pongDeadlineAt = 0;
927
- }
928
- clearReconnectTimer() {
929
- if (this.reconnectTimer) {
930
- clearTimeout(this.reconnectTimer);
931
- this.reconnectTimer = null;
932
- }
933
- if (this.reconnectTimerResolver) {
934
- const resolve = this.reconnectTimerResolver;
935
- this.reconnectTimerResolver = null;
936
- resolve();
937
- }
938
- }
939
- async scheduleReconnect(code, reason) {
940
- this.clearReconnectTimer();
941
- const attempt = this.reconnectAttempt + 1;
942
- const delay = Math.min(this.reconnectBaseDelayMs * 2 ** this.reconnectAttempt, this.reconnectMaxDelayMs);
943
- this.reconnectAttempt = attempt;
944
- this.state = "reconnecting";
945
- this.log("schedule reconnect", {
946
- attempt,
947
- delay,
948
- code,
949
- reason
950
- });
951
- this.emit("reconnecting", {
952
- attempt,
953
- delayMs: delay,
954
- code,
955
- reason
956
- });
957
- await new Promise((resolve) => {
958
- this.reconnectTimerResolver = resolve;
959
- this.reconnectTimer = setTimeout(() => {
960
- this.reconnectTimer = null;
961
- this.reconnectTimerResolver = null;
962
- resolve();
963
- }, delay);
964
- });
965
- if (this.manuallyClosed) return;
966
- if (typeof navigator !== "undefined" && navigator.onLine === false) {
967
- await new Promise((resolve) => {
968
- const fallbackTimer = setTimeout(resolve, this.reconnectMaxDelayMs);
969
- const handleOnline = () => {
970
- clearTimeout(fallbackTimer);
971
- globalThis.removeEventListener?.("online", handleOnline);
972
- resolve();
973
- };
974
- globalThis.addEventListener?.("online", handleOnline, { once: true });
975
- });
976
- if (this.manuallyClosed) return;
977
- }
978
- await this.connect().catch((error) => {
979
- this.emit("error", {
980
- error,
981
- recoverable: this.state === "reconnecting"
982
- });
983
- });
984
- }
985
- };
986
- const createWebsocketClient = (options) => new WebsocketClient(options);
987
- //#endregion
988
- export { extractBillingPayload as a, isFeatureNotEntitledError as c, FEATURE_NOT_ENTITLED_ERROR_CODE as i, isHttpErrorCode as l, createWebsocketClient as n, isBillingAccessBlockedCode as o, BILLING_ACCESS_BLOCKED_ERROR_CODE as r, isBillingAccessBlockedError as s, WebsocketClient as t };