@neta-art/cohub 11.1.0 → 11.3.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 (78) hide show
  1. package/dist/board/editor/context.d.ts +1 -0
  2. package/dist/board/editor/context.js +1 -0
  3. package/dist/board/editor/editor.d.ts +1 -0
  4. package/dist/board/editor/editor.js +7 -0
  5. package/dist/board/editor/interaction.d.ts +4 -0
  6. package/dist/board/editor/interaction.js +16 -7
  7. package/dist/board/editor/selection-transform.d.ts +1 -1
  8. package/dist/board/editor/selection-transform.js +13 -2
  9. package/dist/board/export/index.d.ts +2 -0
  10. package/dist/board/export/index.js +1 -0
  11. package/dist/board/export/scene.d.ts +2 -0
  12. package/dist/board/export/scene.js +4 -0
  13. package/dist/board/model/file-preview.d.ts +0 -1
  14. package/dist/board/model/file-preview.js +10 -1
  15. package/dist/board/model/index.d.ts +2 -1
  16. package/dist/board/model/index.js +2 -1
  17. package/dist/board/model/script-api.d.ts +69 -0
  18. package/dist/board/model/script-api.js +77 -0
  19. package/dist/board/render/audio-waveform.d.ts +1 -1
  20. package/dist/board/render/audio-waveform.js +8 -5
  21. package/dist/board/render/card-tag.d.ts +24 -0
  22. package/dist/board/render/card-tag.js +205 -0
  23. package/dist/board/render/index.d.ts +7 -4
  24. package/dist/board/render/index.js +7 -4
  25. package/dist/board/render/media-interaction.d.ts +2 -1
  26. package/dist/board/render/media-interaction.js +14 -6
  27. package/dist/board/render/palette.js +2 -2
  28. package/dist/board/render/renderers/arrow-card-renderer.js +20 -16
  29. package/dist/board/render/renderers/audio-card-renderer.d.ts +8 -1
  30. package/dist/board/render/renderers/audio-card-renderer.js +64 -60
  31. package/dist/board/render/renderers/base-card-renderer.d.ts +1 -26
  32. package/dist/board/render/renderers/base-card-renderer.js +6 -92
  33. package/dist/board/render/renderers/board-renderer-registry.d.ts +8 -0
  34. package/dist/board/render/renderers/board-renderer-registry.js +3 -3
  35. package/dist/board/render/renderers/custom-card-renderer.js +4 -7
  36. package/dist/board/render/renderers/effect-card-renderer.js +0 -5
  37. package/dist/board/render/renderers/file-card-renderer.d.ts +1 -11
  38. package/dist/board/render/renderers/file-card-renderer.js +43 -105
  39. package/dist/board/render/renderers/frame-card-renderer.js +14 -15
  40. package/dist/board/render/renderers/image-card-renderer.js +62 -67
  41. package/dist/board/render/renderers/media-key.d.ts +6 -0
  42. package/dist/board/render/renderers/media-key.js +39 -0
  43. package/dist/board/render/renderers/shape-card-renderer.js +6 -12
  44. package/dist/board/render/renderers/task-card-renderer.d.ts +1 -9
  45. package/dist/board/render/renderers/task-card-renderer.js +82 -56
  46. package/dist/board/render/renderers/unknown-card-renderer.js +17 -16
  47. package/dist/board/render/renderers/video-card-renderer.js +73 -80
  48. package/dist/board/render/stroke.d.ts +1 -1
  49. package/dist/board/render/stroke.js +5 -4
  50. package/dist/board/render/style.d.ts +29 -0
  51. package/dist/board/render/style.js +54 -0
  52. package/dist/board/render/surface.d.ts +34 -0
  53. package/dist/board/render/surface.js +132 -0
  54. package/dist/board/render/text-fit.d.ts +8 -0
  55. package/dist/board/render/text-fit.js +60 -0
  56. package/dist/board/render/text-resolution.d.ts +2 -1
  57. package/dist/board/render/text-resolution.js +4 -1
  58. package/dist/board/stage/index.d.ts +3 -2
  59. package/dist/board/stage/index.js +2 -1
  60. package/dist/board/stage/overlay.d.ts +41 -0
  61. package/dist/board/stage/overlay.js +182 -0
  62. package/dist/board/stage/scene.d.ts +2 -43
  63. package/dist/board/stage/scene.js +23 -131
  64. package/dist/board/stage/stage.d.ts +2 -0
  65. package/dist/board/stage/stage.js +99 -32
  66. package/dist/chunks/app-realtime.js +1697 -0
  67. package/dist/chunks/environment.d.ts +997 -3
  68. package/dist/chunks/http.d.ts +393 -3
  69. package/dist/chunks/http.js +40 -1
  70. package/dist/chunks/websocket.d.ts +4 -0
  71. package/dist/chunks/websocket.js +11 -2
  72. package/dist/http.d.ts +2 -2
  73. package/dist/index.d.ts +4 -323
  74. package/dist/index.js +58 -1660
  75. package/dist/protocol/dist/board-model.js +51 -51
  76. package/dist/protocol/dist/board-schema.d.ts +1 -2
  77. package/dist/protocol/dist/board-schema.js +7 -22
  78. package/package.json +2 -1
package/dist/index.js CHANGED
@@ -1,5 +1,6 @@
1
- import { $ as isTerminalDesktopCommandStatus, A as BoardClient, B as CronJobsApi, C as SessionPatchReducer, D as connectDisplay, E as DisplayConnection, F as PublicAssetsApi, G as DESKTOP_COMMAND_OPEN_TIMEOUT_MS, H as DESKTOP_COMMAND_ACCEPT_TIMEOUT_MS, I as SkillsApi, J as DESKTOP_COMMAND_SETTLEMENT_GRACE_SECONDS, K as DESKTOP_COMMAND_PAYLOAD_MAX_BYTES, L as PromptsApi, M as SessionAccessApi, N as ReferencesApi, O as readRtcTransportStats, P as SearchApi, Q as isDesktopCallMethod, R as ModelsApi, S as parseAssistantMessageCommit, T as SpaceDisplaysApi, U as DESKTOP_COMMAND_DEFAULT_TIMEOUT_MS, V as ChannelsApi, W as DESKTOP_COMMAND_MAX_TIMEOUT_MS, X as DESKTOP_COMMAND_VERSION, Y as DESKTOP_COMMAND_TERMINAL_TTL_SECONDS, Z as defaultDesktopCommandTimeoutMs, _ as PublicInviteApi, a as WorkCommerceApi, b as SessionGenerationStreamClient, c as AppsApi, ct as normalizeDisplayPoint, d as UserApi, et as parseDesktopCommand, f as TasksApi, g as SpacesApi, h as SpaceWebhooksApi, i as AppCommerceApi, it as DISPLAY_ELEMENT_REF_PATTERN, j as ensureRealtimeConnected, k as waitForIceGathering, l as ReferralsApi, lt as splitDisplayText, m as SpacePublicFilesApi, n as createHttpClient, nt as resolveOpenSurface, o as DesktopCommandsApi, ot as DISPLAY_INPUT_MAX_TEXT, p as SpaceClient, q as DESKTOP_COMMAND_PENDING_TTL_SECONDS, r as RoomsApi, rt as NAVIGATION_ERROR_MESSAGE_MAX_LENGTH, s as UiCommandsApi, st as DISPLAY_SYSTEM_ACTIONS, t as CohubHttpClient, tt as parseDesktopCommandId, u as UsersApi, v as buildSpaceInvitePath, w as createSessionPatchReducer, x as createSessionGenerationStreamClient, y as buildSpacePath, z as GenerationsApi } from "./chunks/http.js";
2
- import { A as requestSourceToHeaders, C as isRequestSourceClientId, D as normalizeRequestSource, E as mergeRequestSourceIntoMeta, M as isUuid, N as isUuidLike, O as parseRequestSourceFromHeaders, P as isUuidOrShortUuid, S as hasRequestSourceIdentity, T as isRequestSourceUuid, a as sanitizeAccessToken, b as COHUB_SOURCE_HEADER_NAMES, i as matchesUnauthorizedErrorToken, j as resolveRequestSourceChannel, k as readRequestSourceFromEnv, n as HttpTransport, o as REALTIME_ROOM_EVENT_NAME_PATTERN, r as joinApiUrl, t as HttpError, w as isRequestSourceEmpty, x as REQUEST_SOURCE_VIA_MAX_LENGTH, y as COHUB_SOURCE_HEADER } from "./chunks/transport.js";
1
+ import { c as AppRtcError, i as WorkRoom, n as AppRoom, o as AppRoomRtc, r as WorkRealtimeApi, s as AppRtcChannel, t as AppRealtimeApi } from "./chunks/app-realtime.js";
2
+ import { $ as isTerminalDesktopCommandStatus, A as BoardClient, B as CronJobsApi, C as SessionPatchReducer, D as connectDisplay, E as DisplayConnection, F as PublicAssetsApi, G as DESKTOP_COMMAND_OPEN_TIMEOUT_MS, H as DESKTOP_COMMAND_ACCEPT_TIMEOUT_MS, I as SkillsApi, J as DESKTOP_COMMAND_SETTLEMENT_GRACE_SECONDS, K as DESKTOP_COMMAND_PAYLOAD_MAX_BYTES, L as PromptsApi, M as SessionAccessApi, N as ReferencesApi, P as SearchApi, Q as isDesktopCallMethod, R as ModelsApi, S as parseAssistantMessageCommit, T as SpaceDisplaysApi, U as DESKTOP_COMMAND_DEFAULT_TIMEOUT_MS, V as ChannelsApi, W as DESKTOP_COMMAND_MAX_TIMEOUT_MS, X as DESKTOP_COMMAND_VERSION, Y as DESKTOP_COMMAND_TERMINAL_TTL_SECONDS, Z as defaultDesktopCommandTimeoutMs, _ as PublicInviteApi, a as WorkCommerceApi, b as SessionGenerationStreamClient, c as AppsApi, ct as normalizeDisplayPoint, d as UserApi, et as parseDesktopCommand, f as TasksApi, g as SpacesApi, h as SpaceWebhooksApi, i as AppCommerceApi, it as DISPLAY_ELEMENT_REF_PATTERN, j as ensureRealtimeConnected, l as ReferralsApi, lt as splitDisplayText, m as SpacePublicFilesApi, n as createHttpClient, nt as resolveOpenSurface, o as DesktopCommandsApi, ot as DISPLAY_INPUT_MAX_TEXT, p as SpaceClient, q as DESKTOP_COMMAND_PENDING_TTL_SECONDS, r as RoomsApi, rt as NAVIGATION_ERROR_MESSAGE_MAX_LENGTH, s as UiCommandsApi, st as DISPLAY_SYSTEM_ACTIONS, t as CohubHttpClient, tt as parseDesktopCommandId, u as UsersApi, v as buildSpaceInvitePath, w as createSessionPatchReducer, x as createSessionGenerationStreamClient, y as buildSpacePath, z as GenerationsApi } from "./chunks/http.js";
3
+ import { A as requestSourceToHeaders, C as isRequestSourceClientId, D as normalizeRequestSource, E as mergeRequestSourceIntoMeta, M as isUuid, N as isUuidLike, O as parseRequestSourceFromHeaders, P as isUuidOrShortUuid, S as hasRequestSourceIdentity, T as isRequestSourceUuid, a as sanitizeAccessToken, b as COHUB_SOURCE_HEADER_NAMES, i as matchesUnauthorizedErrorToken, j as resolveRequestSourceChannel, k as readRequestSourceFromEnv, n as HttpTransport, r as joinApiUrl, t as HttpError, w as isRequestSourceEmpty, x as REQUEST_SOURCE_VIA_MAX_LENGTH, y as COHUB_SOURCE_HEADER } from "./chunks/transport.js";
3
4
  import { a as normalizeWebsocketUrl, c as resolveExecutionAppId, d as resolveWebBaseUrl, f as resolveWebsocketUrl, g as scopeListHasPermission, h as isUserLevelPermission, i as normalizeVoiceInputWebsocketUrl, l as resolveExecutionToken, m as USER_LEVEL_PERMISSIONS, n as getCohubContext, o as resolveApiBaseUrl, p as PERMISSIONS, r as normalizeBaseUrl, s as resolveCohubEnvironment, t as COHUB_ENVIRONMENTS, u as resolveVoiceInputWebsocketUrl } from "./chunks/environment.js";
4
5
  import { a as extractBillingPayload, c as isFeatureNotEntitledError, i as FEATURE_NOT_ENTITLED_ERROR_CODE, l as isHttpErrorCode, n as createWebsocketClient, o as isBillingAccessBlockedCode, r as BILLING_ACCESS_BLOCKED_ERROR_CODE, s as isBillingAccessBlockedError, t as WebsocketClient } from "./chunks/websocket.js";
5
6
  import { VoiceApi, VoiceInputClient, createVoiceInputClient } from "./voice-input.js";
@@ -198,10 +199,10 @@ const colorString = z.string().max(120).refine(isBoardColorString, "expected a p
198
199
  const BoardColorSchema = z.union([colorString, z.object({
199
200
  light: colorString,
200
201
  dark: colorString
201
- }).strict()]);
202
+ }).strict()]).describe("Palette token (follows the theme), CSS color, or { light, dark }.");
202
203
  const BoardStyleSchema = z.object({
203
204
  fill: BoardColorSchema.optional(),
204
- fillOpacity: ratio.optional(),
205
+ fillOpacity: ratio.optional().describe("Fill opacity, independent of the item's opacity."),
205
206
  stroke: BoardColorSchema.optional(),
206
207
  strokeWidth: finite$1.min(0).max(64).optional(),
207
208
  dash: z.enum([
@@ -209,8 +210,8 @@ const BoardStyleSchema = z.object({
209
210
  "dashed",
210
211
  "dotted"
211
212
  ]).optional(),
212
- radius: nonNegative.optional(),
213
- trim: ratio.optional()
213
+ radius: nonNegative.optional().describe("Corner radius in board units."),
214
+ trim: ratio.optional().describe("Fraction of the outline drawn; animate 0 → 1 to draw it in.")
214
215
  }).strict();
215
216
  function isSafeBoardSourcePath(value) {
216
217
  return value.length > 0 && value.length <= 4096 && !value.startsWith("/") && !/^[a-z][a-z\d+.-]*:/i.test(value.trimStart()) && !value.includes("\\") && value.split("/").every((part) => part.length > 0 && part !== "." && part !== "..");
@@ -226,18 +227,18 @@ function parseBoardMediaSource(src) {
226
227
  value: src
227
228
  } : null;
228
229
  }
229
- const BoardMediaSourceSchema = z.string().max(4096).refine((src) => parseBoardMediaSource(src) !== null, "src must be a public HTTP(S) URL or relative Space file path").describe("Prefer a stable public HTTP(S) URL; relative Space file paths remain supported.");
230
+ const BoardMediaSourceSchema = z.string().max(4096).refine((src) => parseBoardMediaSource(src) !== null, "src must be a public HTTP(S) URL or relative Space file path").describe("Prefer a public HTTP(S) URL; a Space path relative to the workspace root also works.");
230
231
  const BoardScriptSchema = z.object({
231
- src: srcSchema.refine((path) => /\.m?js$/.test(path), "script src must be a .js or .mjs module"),
232
- params: z.record(z.string(), z.json()).default({}),
233
- targets: z.record(z.string().min(1).max(80), BoardIdSchema).default({}),
234
- seed: z.string().min(1).max(160).optional(),
232
+ src: srcSchema.refine((path) => /\.m?js$/.test(path), "script src must be a .js or .mjs module").describe("A .js or .mjs Space file."),
233
+ params: z.record(z.string(), z.json()).default({}).describe("Values the module reads as params."),
234
+ targets: z.record(z.string().min(1).max(80), BoardIdSchema).default({}).describe("Named items the module can reach, as ctx.targets."),
235
+ seed: z.string().min(1).max(160).optional().describe("Seed for ctx.random(); defaults to the item id."),
235
236
  enabled: z.boolean().default(true)
236
- }).strict();
237
+ }).strict().describe("A JavaScript module attached to the item; `cohub boards schema script` lists its API.");
237
238
  const itemEnvelope = {
238
239
  script: BoardScriptSchema.optional(),
239
- parent: BoardIdSchema.optional(),
240
- z: finite$1.optional(),
240
+ parent: BoardIdSchema.optional().describe("A frame id; position becomes relative to the frame, and the item moves with it."),
241
+ z: finite$1.optional().describe("Order among siblings; new items go on top."),
241
242
  position: BoardVec2Schema.default({
242
243
  x: 0,
243
244
  y: 0
@@ -247,7 +248,7 @@ const itemEnvelope = {
247
248
  origin: BoardVec2Schema.default({
248
249
  x: .5,
249
250
  y: .5
250
- }),
251
+ }).describe("Rotation and scale origin as fractions of the size."),
251
252
  opacity: ratio.default(1),
252
253
  style: BoardStyleSchema.default({}),
253
254
  locked: z.boolean().optional(),
@@ -280,8 +281,8 @@ const BoardFrameItemSchema = z.object({
280
281
  type: z.literal("frame"),
281
282
  props: z.object({
282
283
  label: z.string().max(280).default(""),
283
- decoration: z.enum(["frame", "none"]).optional(),
284
- clip: z.boolean().default(true)
284
+ decoration: z.enum(["frame", "none"]).optional().describe("none hides the title, border and background: an invisible group whose tracks move every child."),
285
+ clip: z.boolean().default(true).describe("Hide children outside the frame.")
285
286
  }).strict().prefault({})
286
287
  }).strict();
287
288
  const BoardTextItemSchema = z.object({
@@ -291,12 +292,12 @@ const BoardTextItemSchema = z.object({
291
292
  text: z.string().default(""),
292
293
  fontSize: fontSize.default(24),
293
294
  fontWeight: finite$1.min(100).max(900).default(500),
294
- font: z.string().min(1).max(120).default("sans"),
295
+ font: z.string().min(1).max(120).default("sans").describe("sans, serif, mono, or a font family name."),
295
296
  align: z.enum(BOARD_TEXT_ALIGNS).default("left"),
296
297
  lineHeight: finite$1.min(.5).max(4).default(4 / 3),
297
- width: finite$1.positive().optional(),
298
- reveal: ratio.default(1),
299
- caret: z.boolean().default(false)
298
+ width: finite$1.positive().optional().describe("Wrap width; the height follows the text."),
299
+ reveal: ratio.default(1).describe("Fraction of the text shown; animate 0 → 1 to type it."),
300
+ caret: z.boolean().default(false).describe("Blink a caret while the text is being revealed.")
300
301
  }).strict().prefault({})
301
302
  }).strict();
302
303
  const BoardShapeItemSchema = z.object({
@@ -304,7 +305,7 @@ const BoardShapeItemSchema = z.object({
304
305
  type: z.literal("shape"),
305
306
  props: z.object({
306
307
  geometry: z.enum(BOARD_SHAPE_GEOMETRIES).default("rectangle"),
307
- path: z.string().min(1).max(2e5).optional(),
308
+ path: z.string().min(1).max(2e5).optional().describe("SVG path data for path geometry, fitted to the item size."),
308
309
  text: z.string().default(""),
309
310
  fontSize: fontSize.default(20),
310
311
  align: z.enum(BOARD_TEXT_ALIGNS).default("center")
@@ -316,12 +317,12 @@ const BoardShapeItemSchema = z.object({
316
317
  const BoardDrawPointSchema = z.object({
317
318
  x: finite$1,
318
319
  y: finite$1,
319
- p: ratio.default(.5)
320
+ p: ratio.default(.5).describe("Pen pressure.")
320
321
  }).strict();
321
322
  const BoardDrawItemSchema = z.object({
322
323
  ...unsized,
323
324
  type: z.literal("draw"),
324
- props: z.object({ points: z.array(BoardDrawPointSchema).min(1).max(1e5) }).strict()
325
+ props: z.object({ points: z.array(BoardDrawPointSchema).min(1).max(1e5).describe("Stroke points relative to position; the size follows them.") }).strict()
325
326
  }).strict();
326
327
  const BoardArrowAnchorSchema = z.union([
327
328
  z.literal("auto"),
@@ -341,10 +342,10 @@ const BoardArrowAnchorSchema = z.union([
341
342
  ]);
342
343
  const BoardArrowBindingSchema = z.object({
343
344
  item: BoardIdSchema,
344
- anchor: BoardArrowAnchorSchema.default("auto"),
345
+ anchor: BoardArrowAnchorSchema.default("auto").describe("auto uses the side facing the other end; or a side with an offset, or a point as fractions of the item."),
345
346
  port: z.string().min(1).max(120).optional()
346
347
  }).strict();
347
- const BoardArrowEndSchema = z.union([BoardVec2Schema, BoardArrowBindingSchema]);
348
+ const BoardArrowEndSchema = z.union([BoardVec2Schema, BoardArrowBindingSchema]).describe("A point relative to position, or { item } to follow that item.");
348
349
  const BOARD_ARROW_ROUTES = [
349
350
  "straight",
350
351
  "curve",
@@ -357,13 +358,13 @@ const BoardArrowItemSchema = z.object({
357
358
  start: BoardArrowEndSchema,
358
359
  end: BoardArrowEndSchema,
359
360
  route: z.enum(BOARD_ARROW_ROUTES).default("straight"),
360
- bend: finite$1.min(-.85).max(.85).default(0),
361
- waypoints: z.array(BoardVec2Schema).max(64).default([]),
361
+ bend: finite$1.min(-.85).max(.85).default(0).describe("Sideways curve as a fraction of the length; the sign picks the side."),
362
+ waypoints: z.array(BoardVec2Schema).max(64).default([]).describe("Points the route passes through."),
362
363
  arrowStart: z.boolean().default(false),
363
364
  arrowEnd: z.boolean().default(true),
364
365
  label: z.string().max(280).default(""),
365
366
  fontSize: fontSize.default(14),
366
- relation: z.string().max(64).regex(/^[a-z0-9]+(?:[.-][a-z0-9]+)*$/, "relation must be a lowercase slug").optional()
367
+ relation: z.string().max(64).regex(/^[a-z0-9]+(?:[.-][a-z0-9]+)*$/, "relation must be a lowercase slug").optional().describe("What the arrow means, as a slug such as triggers or depends-on.")
367
368
  }).strict()
368
369
  }).strict();
369
370
  const fileProps = {
@@ -379,26 +380,26 @@ const fileProps = {
379
380
  excerpt: z.string().optional(),
380
381
  coverPath: z.string().optional(),
381
382
  coverUrl: z.string().optional()
382
- }).strict().optional()
383
+ }).strict().describe("Cached facts for an instant preview; filled in when the media loads.").optional()
383
384
  };
384
385
  const BoardMediaOriginSchema = z.object({
385
386
  taskRunId: z.string().min(1).max(160),
386
387
  artifactId: z.string().min(1).max(240)
387
- }).strict();
388
+ }).strict().describe("The generation run and result this media came from.");
388
389
  const mediaProps = {
389
390
  ...fileProps,
390
391
  origin: BoardMediaOriginSchema.optional()
391
392
  };
392
393
  const timedFileProps = {
393
394
  ...mediaProps,
394
- time: nonNegative.optional()
395
+ time: nonNegative.optional().describe("Playback position in milliseconds; animate props.time to play it on a timeline.")
395
396
  };
396
397
  const BoardCropSchema = z.object({
397
398
  x: ratio,
398
399
  y: ratio,
399
400
  w: ratio,
400
401
  h: ratio
401
- }).strict();
402
+ }).strict().describe("Region shown, as fractions of the image. Without it the whole image fits inside size.");
402
403
  const BoardImageItemSchema = z.object({
403
404
  ...boxed(640, 360),
404
405
  type: z.literal("image"),
@@ -478,8 +479,8 @@ const BoardTaskItemSchema = z.object({
478
479
  ...boxed(420, 240),
479
480
  type: z.literal("task"),
480
481
  props: z.object({
481
- taskRunId: z.string().min(1).max(160),
482
- snapshot: BoardTaskSnapshotSchema.describe("Maintained by the server for tasks in the Board's Space; writers may omit it.")
482
+ taskRunId: z.string().min(1).max(160).describe("The generation run shown; the server keeps the card's status and results up to date."),
483
+ snapshot: BoardTaskSnapshotSchema.describe("Maintained by the server; omit it.")
483
484
  }).strict()
484
485
  }).strict();
485
486
  const BOARD_EFFECT_KINDS = [
@@ -502,8 +503,8 @@ const BoardEffectItemSchema = z.object({
502
503
  spread: finite$1.min(0).max(360).default(360),
503
504
  gravity: finite$1.default(0),
504
505
  intensity: nonNegative.max(10).default(1),
505
- follow: BoardIdSchema.optional(),
506
- seed: z.string().min(1).max(160).optional()
506
+ follow: BoardIdSchema.optional().describe("trail: the item to follow."),
507
+ seed: z.string().min(1).max(160).optional().describe("Makes procedural motion repeatable.")
507
508
  }).strict()
508
509
  }).strict();
509
510
  const BoardCustomItemSchema = z.object({
@@ -605,21 +606,21 @@ function isBoardEase(value) {
605
606
  }
606
607
  const BoardEaseSchema = z.string().max(120).refine(isBoardEase, "expected a CSS easing: linear, ease-in-out, cubic-bezier(…) or steps(…)");
607
608
  const BoardKeyframeSchema = z.object({
608
- at: nonNegative,
609
+ at: nonNegative.describe("Time in milliseconds."),
609
610
  value: z.unknown(),
610
- ease: BoardEaseSchema.optional()
611
+ ease: BoardEaseSchema.optional().describe("Easing into this keyframe: a CSS keyword, cubic-bezier() or steps(); defaults to linear.")
611
612
  }).strict();
612
613
  const BoardTrackSchema = z.object({
613
- target: z.string().min(1).max(160),
614
- property: z.string().min(1).max(200),
615
- keyframes: z.array(BoardKeyframeSchema).min(1).max(1e5),
616
- composite: z.enum(["replace", "add"]).default("replace"),
614
+ target: z.string().min(1).max(160).describe("An item id, camera, or another animation id (its time property nests that timeline)."),
615
+ property: z.string().min(1).max(200).describe("JSON path in the target, e.g. position, opacity, style.fill, props.text."),
616
+ keyframes: z.array(BoardKeyframeSchema).min(1).max(1e5).describe("Ordered by at. The value holds before the first and after the last keyframe."),
617
+ composite: z.enum(["replace", "add"]).default("replace").describe("replace: the latest-started track wins. add: positions and rotations add; scale and opacity multiply."),
617
618
  interpolation: z.enum([
618
619
  "auto",
619
620
  "step",
620
621
  "spline"
621
- ]).default("auto"),
622
- orient: z.boolean().optional()
622
+ ]).default("auto").describe("auto blends numbers, vectors and colors and steps other values; spline smooths position paths."),
623
+ orient: z.boolean().optional().describe("Rotate the item along its path.")
623
624
  }).strict().superRefine((track, context) => {
624
625
  for (let index = 1; index < track.keyframes.length; index += 1) if ((track.keyframes[index]?.at ?? 0) < (track.keyframes[index - 1]?.at ?? 0)) context.addIssue({
625
626
  code: "custom",
@@ -634,20 +635,20 @@ const BoardTrackSchema = z.object({
634
635
  const BoardMarkerSchema = z.object({
635
636
  at: nonNegative,
636
637
  label: z.string().max(280).optional(),
637
- pause: z.boolean().optional()
638
+ pause: z.boolean().optional().describe("Hold shared playback here until next.")
638
639
  }).strict();
639
640
  const BoardAnimationHeaderSchema = z.object({
640
641
  name: z.string().max(280).optional(),
641
- duration: finite$1.positive().max(864e5),
642
+ duration: finite$1.positive().max(864e5).describe("Milliseconds; a new animation without it ends at its last keyframe."),
642
643
  play: z.enum([
643
644
  "manual",
644
645
  "auto",
645
646
  "always"
646
- ]).default("manual"),
647
+ ]).default("manual").describe("manual: only shared playback starts it. auto: plays when the Board opens. always: loops continuously."),
647
648
  delay: nonNegative.default(0),
648
649
  loop: z.boolean().default(false),
649
- end: z.enum(["hold", "reset"]).default("hold"),
650
- markers: z.array(BoardMarkerSchema).max(1e4).default([])
650
+ end: z.enum(["hold", "reset"]).default("hold").describe("hold keeps the last frame; reset returns to the start."),
651
+ markers: z.array(BoardMarkerSchema).max(1e4).default([]).describe("Labeled points on this timeline; a nested animation's markers do not carry over.")
651
652
  }).strict();
652
653
  const BoardAnimationSchema = BoardAnimationHeaderSchema.extend({ tracks: z.record(BoardKeySchema, BoardTrackSchema).default({}) }).strict();
653
654
  const BOARD_ENTER_PRESETS = [
@@ -665,7 +666,7 @@ const BoardBackgroundSchema = z.object({
665
666
  "image"
666
667
  ]).default("dots"),
667
668
  color: BoardColorSchema.optional(),
668
- imageUrl: z.string().url().max(4096).optional(),
669
+ imageUrl: z.string().url().max(4096).optional().describe("Background image for kind image."),
669
670
  fit: z.enum([
670
671
  "cover",
671
672
  "contain",
@@ -677,13 +678,13 @@ const BoardSettingsSchema = z.object({
677
678
  title: z.string().min(1).max(255).optional(),
678
679
  background: BoardBackgroundSchema.default({ kind: "dots" }),
679
680
  grid: z.object({
680
- visible: z.boolean().default(false),
681
+ visible: z.boolean().default(false).describe("Show the grid overlay."),
681
682
  size: finite$1.min(4).max(512).default(24)
682
683
  }).strict().optional(),
683
684
  enter: z.object({
684
685
  preset: z.enum(BOARD_ENTER_PRESETS),
685
686
  duration: finite$1.positive().max(1e4).optional()
686
- }).strict().optional()
687
+ }).strict().optional().describe("Motion for items as they are added.")
687
688
  }).strict();
688
689
  const patchObject = z.record(z.string(), z.unknown());
689
690
  const BoardPatchSchema = z.object({
@@ -2181,102 +2182,6 @@ function compileDisplayActions(actions, width, height) {
2181
2182
  if (events.length === 0) throw new DisplayActionError("no action touches the display");
2182
2183
  return events;
2183
2184
  }
2184
- z.object({
2185
- urls: z.union([z.string().min(1).max(512), z.array(z.string().min(1).max(512)).min(1).max(16)]),
2186
- username: z.string().max(512).optional(),
2187
- credential: z.string().max(512).optional()
2188
- });
2189
- const sessionDescriptionSchema = z.object({
2190
- type: z.enum(["offer", "answer"]),
2191
- sdp: z.string().min(1).max(65536)
2192
- });
2193
- const APP_RTC_DELIVERIES = ["reliable", "latest"];
2194
- const APP_RTC_PUBLICATION_KINDS = [
2195
- "data",
2196
- "audio",
2197
- "video"
2198
- ];
2199
- const APP_RTC_NAME_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,63}$/;
2200
- const isAppRtcName = (value) => typeof value === "string" && APP_RTC_NAME_PATTERN.test(value) && !value.toLowerCase().startsWith("cohub.");
2201
- const APP_RTC_LIMITS = {
2202
- channelsPerMember: 16,
2203
- tracksPerMember: 4,
2204
- dataSubscriptions: 512,
2205
- mediaSubscriptions: 16,
2206
- messageBytes: 65536,
2207
- operationsPerSecond: 10,
2208
- opensPerSecond: 2,
2209
- batch: 64
2210
- };
2211
- const APP_RTC_QUALITIES = [
2212
- "high",
2213
- "medium",
2214
- "low"
2215
- ];
2216
- const APP_RTC_SIMULCAST_LAYERS = [
2217
- {
2218
- rid: "f",
2219
- quality: "high",
2220
- scaleResolutionDownBy: 1
2221
- },
2222
- {
2223
- rid: "h",
2224
- quality: "medium",
2225
- scaleResolutionDownBy: 2
2226
- },
2227
- {
2228
- rid: "q",
2229
- quality: "low",
2230
- scaleResolutionDownBy: 4
2231
- }
2232
- ];
2233
- z.object({
2234
- id: z.string().min(1).max(32),
2235
- name: z.string().regex(APP_RTC_NAME_PATTERN),
2236
- kind: z.enum(APP_RTC_PUBLICATION_KINDS),
2237
- delivery: z.enum(APP_RTC_DELIVERIES).optional(),
2238
- simulcast: z.boolean().optional()
2239
- });
2240
- const nameSchema = z.string().refine(isAppRtcName, "invalid name");
2241
- const participantIdSchema = z.string().uuid();
2242
- z.object({
2243
- ticket: z.string().min(1).max(4096),
2244
- participantId: participantIdSchema
2245
- });
2246
- z.object({ answer: sessionDescriptionSchema.refine((value) => value.type === "answer", "expected an answer") });
2247
- z.union([z.object({ channels: z.array(z.object({
2248
- name: nameSchema,
2249
- delivery: z.enum(APP_RTC_DELIVERIES)
2250
- })).min(1).max(APP_RTC_LIMITS.channelsPerMember) }), z.object({
2251
- offer: sessionDescriptionSchema.refine((value) => value.type === "offer", "expected an offer"),
2252
- tracks: z.array(z.object({
2253
- name: nameSchema,
2254
- kind: z.enum(["audio", "video"]),
2255
- mid: z.string().min(1).max(32),
2256
- simulcast: z.boolean().optional()
2257
- })).min(1).max(APP_RTC_LIMITS.tracksPerMember)
2258
- })]);
2259
- z.object({
2260
- ids: z.array(z.string().min(1).max(32)).min(1).max(APP_RTC_LIMITS.channelsPerMember + APP_RTC_LIMITS.tracksPerMember),
2261
- offer: sessionDescriptionSchema.refine((value) => value.type === "offer", "expected an offer").optional()
2262
- });
2263
- const subscriptionRefSchema = z.object({
2264
- participantId: participantIdSchema,
2265
- publicationId: z.string().min(1).max(32)
2266
- });
2267
- const qualitySchema = z.enum(APP_RTC_QUALITIES);
2268
- z.object({
2269
- kind: z.enum(["data", "media"]),
2270
- items: z.array(subscriptionRefSchema.extend({ quality: qualitySchema.optional() })).min(1).max(APP_RTC_LIMITS.batch)
2271
- });
2272
- z.object({ items: z.array(subscriptionRefSchema.extend({
2273
- mid: z.string().min(1).max(32),
2274
- quality: qualitySchema.optional()
2275
- })).min(1).max(APP_RTC_LIMITS.mediaSubscriptions) });
2276
- z.object({
2277
- items: z.array(subscriptionRefSchema).min(1).max(APP_RTC_LIMITS.batch),
2278
- offer: sessionDescriptionSchema.refine((value) => value.type === "offer", "expected an offer").optional()
2279
- });
2280
2185
  //#endregion
2281
2186
  //#region ../protocol/dist/generation/catalog.js
2282
2187
  function isGenerationModelHidden(model) {
@@ -4182,1516 +4087,6 @@ var BillingApi = class {
4182
4087
  }
4183
4088
  };
4184
4089
  //#endregion
4185
- //#region src/apis/app-room-rtc.ts
4186
- var AppRtcError = class extends Error {
4187
- code;
4188
- constructor(code, message) {
4189
- super(message);
4190
- this.code = code;
4191
- this.name = "AppRtcError";
4192
- }
4193
- };
4194
- const SERVER_CODES = /* @__PURE__ */ new Set([
4195
- "RTC_UNAVAILABLE",
4196
- "RTC_FORBIDDEN",
4197
- "RTC_LIMIT_EXCEEDED",
4198
- "RTC_RATE_LIMITED",
4199
- "ROOM_NOT_FOUND",
4200
- "ROOM_EXPIRED"
4201
- ]);
4202
- const toAppRtcError = (error) => {
4203
- if (error instanceof AppRtcError) return error;
4204
- if (error instanceof HttpError && SERVER_CODES.has(error.code)) return new AppRtcError(error.code, error.message);
4205
- return new AppRtcError("RTC_FAILED", error instanceof Error ? error.message : String(error));
4206
- };
4207
- const HEADER_BYTES = 5;
4208
- const FLAG_BINARY = 1;
4209
- const encoder = new TextEncoder();
4210
- const decoder = new TextDecoder();
4211
- function encodeAppRtcFrame(data, sequence) {
4212
- let payload;
4213
- let flags = 0;
4214
- if (data instanceof ArrayBuffer) {
4215
- payload = new Uint8Array(data);
4216
- flags = FLAG_BINARY;
4217
- } else if (ArrayBuffer.isView(data)) {
4218
- payload = new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
4219
- flags = FLAG_BINARY;
4220
- } else {
4221
- const json = JSON.stringify(data);
4222
- if (json === void 0) throw new TypeError("RTC message is not serializable");
4223
- payload = encoder.encode(json);
4224
- }
4225
- if (payload.byteLength > APP_RTC_LIMITS.messageBytes) throw new RangeError(`RTC message exceeds ${APP_RTC_LIMITS.messageBytes} bytes`);
4226
- const frame = new Uint8Array(HEADER_BYTES + payload.byteLength);
4227
- frame[0] = flags;
4228
- new DataView(frame.buffer).setUint32(1, sequence >>> 0);
4229
- frame.set(payload, HEADER_BYTES);
4230
- return frame;
4231
- }
4232
- function decodeAppRtcFrame(frame) {
4233
- if (frame.byteLength < HEADER_BYTES) return null;
4234
- const view = new DataView(frame);
4235
- const flags = view.getUint8(0);
4236
- const sequence = view.getUint32(1);
4237
- const payload = new Uint8Array(frame, HEADER_BYTES);
4238
- if (flags & FLAG_BINARY) return {
4239
- data: payload.slice().buffer,
4240
- sequence
4241
- };
4242
- try {
4243
- return {
4244
- data: JSON.parse(decoder.decode(payload)),
4245
- sequence
4246
- };
4247
- } catch {
4248
- return null;
4249
- }
4250
- }
4251
- const isNewerSequence = (next, last) => last === null || next !== last && next - last >>> 0 < 2147483648;
4252
- const publicationsKey = (members) => members.map((member) => `${member.participantId}:${member.publications?.map((item) => item.id).join(",") ?? ""}`).join(";");
4253
- const GATHER_TIMEOUT_MS = 2e3;
4254
- const CONNECT_TIMEOUT_MS = 15e3;
4255
- const PENDING_RELIABLE_FRAMES = 256;
4256
- const LATEST_BACKLOG_BYTES = 65536;
4257
- const RECONNECT_DELAYS_MS = [
4258
- 500,
4259
- 1e3,
4260
- 2e3,
4261
- 5e3,
4262
- 1e4,
4263
- 3e4
4264
- ];
4265
- const RATE_LIMIT_RETRY_MS = 1e3;
4266
- var ConnectionReplacedError = class extends Error {
4267
- constructor() {
4268
- super("RTC connection was replaced");
4269
- this.name = "ConnectionReplacedError";
4270
- }
4271
- };
4272
- const policyOf = (delivery) => delivery === "latest" ? {
4273
- ordered: false,
4274
- maxRetransmits: 0
4275
- } : { ordered: true };
4276
- const isSessionLost = (error) => error instanceof HttpError && (error.code === "RTC_STALE" || error.code === "RTC_SESSION_NOT_FOUND");
4277
- const isRoomGone = (error) => error instanceof HttpError && (error.code === "ROOM_NOT_FOUND" || error.code === "ROOM_EXPIRED");
4278
- const chunk = (items, size) => {
4279
- const chunks = [];
4280
- for (let index = 0; index < items.length; index += size) chunks.push(items.slice(index, index + size));
4281
- return chunks;
4282
- };
4283
- const mediaKey = (participantId, publicationId) => `${participantId}:${publicationId}`;
4284
- var AppRtcChannel = class {
4285
- rtc;
4286
- name;
4287
- delivery;
4288
- handlers = /* @__PURE__ */ new Set();
4289
- peerHandlers = /* @__PURE__ */ new Set();
4290
- openPeers = /* @__PURE__ */ new Set();
4291
- publication = null;
4292
- pending = [];
4293
- sequence = 0;
4294
- closed = false;
4295
- constructor(rtc, name, delivery) {
4296
- this.rtc = rtc;
4297
- this.name = name;
4298
- this.delivery = delivery;
4299
- }
4300
- get peers() {
4301
- return [...this.openPeers];
4302
- }
4303
- send(data) {
4304
- if (this.closed || this.rtc.state === "closed") return false;
4305
- this.sequence = this.sequence + 1 >>> 0;
4306
- const frame = encodeAppRtcFrame(data, this.sequence);
4307
- const channel = this.publication?.channel;
4308
- if (channel?.readyState === "open") return this.write(channel, frame);
4309
- if (this.delivery === "latest") this.pending = [frame];
4310
- else if (this.pending.length >= PENDING_RELIABLE_FRAMES) return false;
4311
- else this.pending.push(frame);
4312
- return true;
4313
- }
4314
- onMessage(handler) {
4315
- this.handlers.add(handler);
4316
- return () => this.handlers.delete(handler);
4317
- }
4318
- /** Reports when this client starts or stops receiving a peer's channel. */
4319
- onPeer(handler) {
4320
- this.peerHandlers.add(handler);
4321
- return () => this.peerHandlers.delete(handler);
4322
- }
4323
- close() {
4324
- if (this.closed) return;
4325
- this.closed = true;
4326
- this.handlers.clear();
4327
- this.peerHandlers.clear();
4328
- this.openPeers.clear();
4329
- this.pending = [];
4330
- this.rtc.closeChannel(this);
4331
- }
4332
- /** @internal */
4333
- get isOpen() {
4334
- return !this.closed;
4335
- }
4336
- /** @internal */
4337
- get publicationId() {
4338
- return this.publication?.id ?? null;
4339
- }
4340
- /** @internal */
4341
- attach(id, channel) {
4342
- this.publication = {
4343
- id,
4344
- channel
4345
- };
4346
- channel.binaryType = "arraybuffer";
4347
- const flush = () => {
4348
- const frames = this.pending;
4349
- this.pending = [];
4350
- for (const frame of frames) this.write(channel, frame);
4351
- };
4352
- if (channel.readyState === "open") flush();
4353
- else channel.addEventListener("open", flush, { once: true });
4354
- }
4355
- /** @internal */
4356
- detach() {
4357
- this.publication = null;
4358
- }
4359
- /** @internal */
4360
- fail() {
4361
- this.close();
4362
- }
4363
- /** @internal */
4364
- deliver(from, data) {
4365
- for (const handler of this.handlers) handler({
4366
- from,
4367
- data
4368
- });
4369
- }
4370
- /** @internal */
4371
- setPeer(participantId, open) {
4372
- if (open ? this.openPeers.has(participantId) : !this.openPeers.delete(participantId)) return;
4373
- if (open) this.openPeers.add(participantId);
4374
- for (const handler of this.peerHandlers) handler({
4375
- participantId,
4376
- open
4377
- });
4378
- }
4379
- write(channel, frame) {
4380
- if (this.delivery === "latest" && channel.bufferedAmount > LATEST_BACKLOG_BYTES) return false;
4381
- try {
4382
- channel.send(frame);
4383
- return true;
4384
- } catch {
4385
- return false;
4386
- }
4387
- }
4388
- };
4389
- var AppRoomRtc = class {
4390
- host;
4391
- _state = "connecting";
4392
- pc = null;
4393
- sessionId = null;
4394
- queue = Promise.resolve();
4395
- reconcileQueued = false;
4396
- reconcileFailures = 0;
4397
- reconnectAttempt = 0;
4398
- reconnectTimer = null;
4399
- generation = 0;
4400
- filter = null;
4401
- channels = /* @__PURE__ */ new Map();
4402
- channelsToPublish = /* @__PURE__ */ new Set();
4403
- remoteData = /* @__PURE__ */ new Map();
4404
- localTracks = /* @__PURE__ */ new Map();
4405
- remoteMedia = /* @__PURE__ */ new Map();
4406
- mediaByMid = /* @__PURE__ */ new Map();
4407
- stateHandlers = /* @__PURE__ */ new Set();
4408
- trackHandlers = /* @__PURE__ */ new Set();
4409
- offMembers;
4410
- constructor(host) {
4411
- this.host = host;
4412
- let published = "";
4413
- this.offMembers = host.onMembersChanged((members) => {
4414
- const next = publicationsKey(members);
4415
- if (next === published) return;
4416
- published = next;
4417
- this.scheduleReconcile();
4418
- });
4419
- }
4420
- get state() {
4421
- return this._state;
4422
- }
4423
- get tracks() {
4424
- const tracks = [];
4425
- for (const [key, item] of this.remoteMedia) {
4426
- if (!item.track) continue;
4427
- tracks.push({
4428
- participantId: item.participantId,
4429
- name: item.name,
4430
- kind: item.kind,
4431
- track: item.track,
4432
- quality: item.quality ?? null,
4433
- setQuality: (quality) => this.setQuality(key, quality)
4434
- });
4435
- }
4436
- return tracks;
4437
- }
4438
- get candidates() {
4439
- return this.mediaCandidates().map(({ candidate }) => candidate);
4440
- }
4441
- onStateChange(handler) {
4442
- this.stateHandlers.add(handler);
4443
- return () => this.stateHandlers.delete(handler);
4444
- }
4445
- onTracksChanged(handler) {
4446
- this.trackHandlers.add(handler);
4447
- return () => this.trackHandlers.delete(handler);
4448
- }
4449
- select(filter) {
4450
- this.filter = filter;
4451
- this.scheduleReconcile();
4452
- }
4453
- channel(name, options = {}) {
4454
- if (!isAppRtcName(name)) throw new AppRtcError("RTC_FAILED", `invalid RTC channel name: ${name}`);
4455
- const delivery = options.delivery ?? "reliable";
4456
- const existing = this.channels.get(name);
4457
- if (existing?.isOpen) {
4458
- if (existing.delivery !== delivery) throw new AppRtcError("RTC_FAILED", `RTC channel ${name} is already open with ${existing.delivery} delivery`);
4459
- return existing;
4460
- }
4461
- if (this._state === "closed") throw new AppRtcError("RTC_CLOSED", "RTC connection is closed");
4462
- const channel = new AppRtcChannel(this, name, delivery);
4463
- this.channels.set(name, channel);
4464
- this.publishChannel(channel);
4465
- this.scheduleReconcile();
4466
- return channel;
4467
- }
4468
- async publish(tracks) {
4469
- if (this._state === "closed") throw new AppRtcError("RTC_CLOSED", "RTC connection is closed");
4470
- const inputs = Object.entries(tracks);
4471
- for (const [name] of inputs) {
4472
- if (!isAppRtcName(name)) throw new AppRtcError("RTC_FAILED", `invalid RTC track name: ${name}`);
4473
- if (this.localTracks.has(name)) throw new AppRtcError("RTC_FAILED", `RTC track ${name} is already published`);
4474
- }
4475
- if (this.localTracks.size + inputs.length > APP_RTC_LIMITS.tracksPerMember) throw new AppRtcError("RTC_LIMIT_EXCEEDED", `at most ${APP_RTC_LIMITS.tracksPerMember} tracks per member`);
4476
- const entries = inputs.map(([name, input]) => {
4477
- const { track, simulcast } = "track" in input ? input : {
4478
- track: input,
4479
- simulcast: false
4480
- };
4481
- const kind = track.kind === "audio" ? "audio" : "video";
4482
- return this.localEntry(name, kind, track, simulcast === true && kind === "video");
4483
- });
4484
- for (const entry of entries) this.localTracks.set(entry.name, entry);
4485
- try {
4486
- await this.enqueue(() => this.publishTracks(entries.map((entry) => entry.name)));
4487
- } catch (error) {
4488
- if (!(error instanceof ConnectionReplacedError)) {
4489
- for (const entry of entries) this.dropLocal(entry);
4490
- throw toAppRtcError(error);
4491
- }
4492
- }
4493
- return Object.fromEntries(entries.map((entry) => [entry.name, this.localHandle(entry)]));
4494
- }
4495
- async stats() {
4496
- return this.pc ? readRtcTransportStats(await this.pc.getStats()) : {
4497
- path: null,
4498
- relayProtocol: null,
4499
- rttMs: null
4500
- };
4501
- }
4502
- close() {
4503
- if (this._state === "closed") return;
4504
- this.setState("closed");
4505
- this.offMembers();
4506
- if (this.reconnectTimer) clearTimeout(this.reconnectTimer);
4507
- for (const channel of this.channels.values()) channel.close();
4508
- for (const entry of this.localTracks.values()) entry.track.removeEventListener("ended", entry.onEnded);
4509
- this.teardown(true);
4510
- }
4511
- /** @internal */
4512
- async start() {
4513
- try {
4514
- await this.connect();
4515
- } catch (error) {
4516
- throw toAppRtcError(error);
4517
- }
4518
- }
4519
- /** @internal */
4520
- async verify() {
4521
- if (this._state === "closed" || !this.sessionId) return;
4522
- try {
4523
- await this.request("GET", `/${this.sessionId}`);
4524
- } catch (error) {
4525
- if (isSessionLost(error)) this.reconnect();
4526
- }
4527
- }
4528
- /** @internal */
4529
- closeChannel(channel) {
4530
- if (this.channels.get(channel.name) === channel) this.channels.delete(channel.name);
4531
- const id = channel.publicationId;
4532
- channel.detach();
4533
- if (id) this.enqueue(() => this.unpublishData([id])).catch(() => void 0);
4534
- this.scheduleReconcile();
4535
- }
4536
- localEntry(name, kind, track, simulcast) {
4537
- const entry = {
4538
- name,
4539
- kind,
4540
- track,
4541
- simulcast,
4542
- transceiver: null,
4543
- id: null,
4544
- ended: /* @__PURE__ */ new Set(),
4545
- onEnded: () => void 0
4546
- };
4547
- entry.onEnded = () => {
4548
- if (this.localTracks.get(name) !== entry) return;
4549
- for (const handler of entry.ended) handler();
4550
- this.stopLocal(entry).catch(() => void 0);
4551
- };
4552
- track.addEventListener("ended", entry.onEnded);
4553
- return entry;
4554
- }
4555
- localHandle(entry) {
4556
- return {
4557
- name: entry.name,
4558
- kind: entry.kind,
4559
- simulcast: entry.simulcast,
4560
- get track() {
4561
- return entry.track;
4562
- },
4563
- replace: async (track) => {
4564
- if (track.kind !== entry.kind) throw new AppRtcError("RTC_FAILED", `replace ${entry.name} with another ${entry.kind} track`);
4565
- if (this.localTracks.get(entry.name) !== entry) throw new AppRtcError("RTC_CLOSED", `RTC track ${entry.name} has stopped`);
4566
- entry.track.removeEventListener("ended", entry.onEnded);
4567
- entry.track = track;
4568
- track.addEventListener("ended", entry.onEnded);
4569
- await entry.transceiver?.sender.replaceTrack(track);
4570
- },
4571
- onEnded: (handler) => {
4572
- entry.ended.add(handler);
4573
- return () => entry.ended.delete(handler);
4574
- },
4575
- stop: () => this.stopLocal(entry)
4576
- };
4577
- }
4578
- dropLocal(entry) {
4579
- entry.track.removeEventListener("ended", entry.onEnded);
4580
- if (this.localTracks.get(entry.name) === entry) this.localTracks.delete(entry.name);
4581
- }
4582
- stopLocal(entry) {
4583
- if (this.localTracks.get(entry.name) !== entry) return Promise.resolve();
4584
- this.dropLocal(entry);
4585
- return this.enqueue(() => this.unpublishTrack(entry)).catch((error) => {
4586
- if (error instanceof ConnectionReplacedError) return;
4587
- throw toAppRtcError(error);
4588
- });
4589
- }
4590
- async publishTracks(names) {
4591
- const pc = this.requirePc();
4592
- const entries = names.flatMap((name) => {
4593
- const entry = this.localTracks.get(name);
4594
- return entry && !entry.id ? [entry] : [];
4595
- });
4596
- if (entries.length === 0) return;
4597
- for (const entry of entries) entry.transceiver = pc.addTransceiver(entry.track, {
4598
- direction: "sendonly",
4599
- ...entry.simulcast ? { sendEncodings: APP_RTC_SIMULCAST_LAYERS.map(({ rid, scaleResolutionDownBy }) => ({
4600
- rid,
4601
- scaleResolutionDownBy
4602
- })) } : {}
4603
- });
4604
- const offer = await this.localOffer(pc);
4605
- let result;
4606
- try {
4607
- result = await this.request("POST", `/${this.sessionId}/publish`, {
4608
- offer,
4609
- tracks: entries.map((entry) => ({
4610
- name: entry.name,
4611
- kind: entry.kind,
4612
- mid: entry.transceiver?.mid ?? "",
4613
- ...entry.simulcast ? { simulcast: true } : {}
4614
- }))
4615
- });
4616
- } catch (error) {
4617
- if (error instanceof ConnectionReplacedError) throw error;
4618
- await this.settleOffer(pc, void 0).catch(() => void 0);
4619
- for (const entry of entries) {
4620
- entry.transceiver?.stop();
4621
- entry.transceiver = null;
4622
- }
4623
- throw error;
4624
- }
4625
- await this.settleOffer(pc, result.answer);
4626
- for (const publication of result.publications) {
4627
- const entry = entries.find((candidate) => candidate.name === publication.name);
4628
- if (entry) entry.id = publication.id;
4629
- }
4630
- }
4631
- async unpublishTrack(entry) {
4632
- if (!entry.id || !entry.transceiver || !this.pc) return;
4633
- const id = entry.id;
4634
- entry.transceiver.stop();
4635
- await this.offerExchange(this.pc, (offer) => this.request("POST", `/${this.sessionId}/unpublish`, {
4636
- ids: [id],
4637
- offer
4638
- }));
4639
- entry.id = null;
4640
- }
4641
- publishChannel(channel) {
4642
- this.channelsToPublish.add(channel);
4643
- if (this.channelsToPublish.size > 1 || !this.pc) return;
4644
- this.enqueue(async () => {
4645
- const batch = [...this.channelsToPublish];
4646
- this.channelsToPublish.clear();
4647
- try {
4648
- await this.publishChannels(batch);
4649
- } catch (error) {
4650
- if (isSessionLost(error) || error instanceof ConnectionReplacedError) throw error;
4651
- if (error instanceof HttpError && error.code === "RTC_RATE_LIMITED") {
4652
- setTimeout(() => {
4653
- for (const item of batch) if (item.isOpen) this.publishChannel(item);
4654
- }, RATE_LIMIT_RETRY_MS);
4655
- return;
4656
- }
4657
- for (const item of batch) item.fail();
4658
- console.warn("[Cohub AppRtc] failed to publish channels", error);
4659
- }
4660
- }).catch(() => void 0);
4661
- }
4662
- async connect() {
4663
- const generation = ++this.generation;
4664
- const opened = await this.send("POST", "", {
4665
- ticket: this.host.ticket,
4666
- participantId: this.host.participantId()
4667
- });
4668
- if (generation !== this.generation || this._state === "closed") {
4669
- this.send("DELETE", `/${opened.sessionId}`).catch(() => void 0);
4670
- return;
4671
- }
4672
- const pc = new RTCPeerConnection({
4673
- iceServers: opened.iceServers,
4674
- bundlePolicy: "max-bundle"
4675
- });
4676
- this.pc = pc;
4677
- this.sessionId = opened.sessionId;
4678
- pc.addEventListener("connectionstatechange", () => {
4679
- if (this.pc !== pc) return;
4680
- if (pc.connectionState === "connected") {
4681
- this.reconnectAttempt = 0;
4682
- this.setState("connected");
4683
- } else if (pc.connectionState === "failed") this.reconnect();
4684
- });
4685
- pc.addEventListener("track", (event) => this.onTrack(event));
4686
- const ready = (async () => {
4687
- await this.answerOffer(opened.offer);
4688
- await this.connected(pc);
4689
- })();
4690
- this.queue = ready.catch(() => void 0);
4691
- try {
4692
- await ready;
4693
- } catch (error) {
4694
- if (generation === this.generation) this.reconnect();
4695
- throw error;
4696
- }
4697
- this.channelsToPublish.clear();
4698
- const republish = [...this.channels.values()].filter((channel) => channel.isOpen);
4699
- const tracks = [...this.localTracks.keys()];
4700
- await Promise.all([republish.length ? this.enqueue(() => this.publishChannels(republish)).catch((error) => {
4701
- if (error instanceof ConnectionReplacedError || isSessionLost(error)) return;
4702
- for (const channel of republish) channel.fail();
4703
- console.warn("[Cohub AppRtc] failed to publish channels on the new connection", error);
4704
- }) : null, tracks.length ? this.enqueue(() => this.publishTracks(tracks)).catch((error) => {
4705
- if (error instanceof ConnectionReplacedError || isSessionLost(error)) return;
4706
- console.warn("[Cohub AppRtc] failed to publish tracks on the new connection", error);
4707
- }) : null]);
4708
- this.scheduleReconcile();
4709
- }
4710
- connected(pc) {
4711
- if (pc.connectionState === "connected") return Promise.resolve();
4712
- return new Promise((resolve, reject) => {
4713
- const timer = setTimeout(() => done(/* @__PURE__ */ new Error("RTC connection timed out")), CONNECT_TIMEOUT_MS);
4714
- const done = (error) => {
4715
- clearTimeout(timer);
4716
- pc.removeEventListener("connectionstatechange", check);
4717
- if (error) reject(error);
4718
- else resolve();
4719
- };
4720
- const check = () => {
4721
- if (pc.connectionState === "connected") done();
4722
- else if (pc.connectionState === "failed" || pc.connectionState === "closed") done(/* @__PURE__ */ new Error("RTC connection failed"));
4723
- };
4724
- pc.addEventListener("connectionstatechange", check);
4725
- });
4726
- }
4727
- reconnect() {
4728
- if (this._state === "closed" || this.reconnectTimer) return;
4729
- this.setState(this._state === "connecting" ? "connecting" : "reconnecting");
4730
- this.teardown(true);
4731
- const delay = RECONNECT_DELAYS_MS[Math.min(this.reconnectAttempt, RECONNECT_DELAYS_MS.length - 1)] ?? 3e4;
4732
- this.reconnectAttempt += 1;
4733
- this.reconnectTimer = setTimeout(() => {
4734
- this.reconnectTimer = null;
4735
- this.connect().catch((error) => {
4736
- if (isRoomGone(error) || error instanceof HttpError && (error.status === 403 || error.status === 503)) this.close();
4737
- else this.reconnect();
4738
- });
4739
- }, delay);
4740
- }
4741
- teardown(release) {
4742
- this.generation += 1;
4743
- const sessionId = this.sessionId;
4744
- this.sessionId = null;
4745
- this.queue = Promise.resolve();
4746
- for (const channel of this.channels.values()) channel.detach();
4747
- for (const item of this.remoteData.values()) this.dropRemoteData(item);
4748
- this.remoteData.clear();
4749
- for (const item of this.localTracks.values()) {
4750
- item.transceiver = null;
4751
- item.id = null;
4752
- }
4753
- const hadTracks = this.remoteMedia.size > 0;
4754
- this.remoteMedia.clear();
4755
- this.mediaByMid.clear();
4756
- this.pc?.close();
4757
- this.pc = null;
4758
- if (hadTracks) this.notifyTracks();
4759
- if (release && sessionId) this.send("DELETE", `/${sessionId}`).catch(() => void 0);
4760
- }
4761
- enqueue(operation) {
4762
- const generation = this.generation;
4763
- const run = async () => {
4764
- if (generation !== this.generation || !this.pc || !this.sessionId) throw new ConnectionReplacedError();
4765
- try {
4766
- return await operation();
4767
- } catch (error) {
4768
- if (isSessionLost(error)) this.reconnect();
4769
- throw error;
4770
- }
4771
- };
4772
- const next = this.queue.then(run, run);
4773
- this.queue = next.catch(() => void 0);
4774
- return next;
4775
- }
4776
- async publishChannels(channels) {
4777
- const pc = this.requirePc();
4778
- const open = channels.filter((channel) => channel.isOpen && !channel.publicationId);
4779
- if (open.length === 0) return;
4780
- const result = await this.request("POST", `/${this.sessionId}/publish`, { channels: open.map((channel) => ({
4781
- name: channel.name,
4782
- delivery: channel.delivery
4783
- })) });
4784
- for (const publication of result.publications) {
4785
- const channel = open.find((item) => item.name === publication.name);
4786
- const id = result.channelIds?.[publication.id];
4787
- if (!channel || id === void 0) continue;
4788
- const dataChannel = pc.createDataChannel(publication.name, {
4789
- negotiated: true,
4790
- id,
4791
- ...policyOf(channel.delivery)
4792
- });
4793
- channel.attach(publication.id, dataChannel);
4794
- if (!channel.isOpen) await this.unpublishData([publication.id]);
4795
- }
4796
- }
4797
- async unpublishData(ids) {
4798
- await this.request("POST", `/${this.sessionId}/unpublish`, { ids });
4799
- }
4800
- async settleOffer(pc, answer) {
4801
- if (answer) await pc.setRemoteDescription(answer);
4802
- else if (pc.signalingState === "have-local-offer") await pc.setLocalDescription({ type: "rollback" });
4803
- }
4804
- async offerExchange(pc, send) {
4805
- const offer = await this.localOffer(pc);
4806
- let answer;
4807
- try {
4808
- ({answer} = await send(offer));
4809
- } catch (error) {
4810
- if (!(error instanceof ConnectionReplacedError)) await this.settleOffer(pc, void 0).catch(() => void 0);
4811
- throw error;
4812
- }
4813
- await this.settleOffer(pc, answer);
4814
- }
4815
- scheduleReconcile() {
4816
- if (this.reconcileQueued || !this.pc || this._state === "closed") return;
4817
- this.reconcileQueued = true;
4818
- this.enqueue(async () => {
4819
- this.reconcileQueued = false;
4820
- await this.reconcile();
4821
- this.reconcileFailures = 0;
4822
- }).catch((error) => {
4823
- this.reconcileQueued = false;
4824
- if (isSessionLost(error) || error instanceof ConnectionReplacedError) return;
4825
- console.warn("[Cohub AppRtc] failed to update subscriptions", error);
4826
- if (error instanceof HttpError && error.code === "RTC_LIMIT_EXCEEDED") return;
4827
- const delay = RECONNECT_DELAYS_MS[Math.min(this.reconcileFailures, RECONNECT_DELAYS_MS.length - 1)] ?? 3e4;
4828
- this.reconcileFailures += 1;
4829
- const generation = this.generation;
4830
- setTimeout(() => {
4831
- if (generation === this.generation) this.scheduleReconcile();
4832
- }, delay);
4833
- });
4834
- }
4835
- mediaCandidates() {
4836
- const self = this.host.participantId();
4837
- const candidates = [];
4838
- for (const member of this.host.members()) {
4839
- if (member.participantId === self) continue;
4840
- for (const publication of member.publications ?? []) {
4841
- if (publication.kind === "data") continue;
4842
- candidates.push({
4843
- publication,
4844
- candidate: {
4845
- participantId: member.participantId,
4846
- name: publication.name,
4847
- kind: publication.kind,
4848
- simulcast: publication.simulcast === true,
4849
- subscribed: this.remoteMedia.has(mediaKey(member.participantId, publication.id))
4850
- }
4851
- });
4852
- }
4853
- }
4854
- return candidates;
4855
- }
4856
- async reconcile() {
4857
- const self = this.host.participantId();
4858
- const wantedData = /* @__PURE__ */ new Map();
4859
- for (const member of this.host.members()) {
4860
- if (member.participantId === self) continue;
4861
- for (const publication of member.publications ?? []) {
4862
- if (publication.kind !== "data") continue;
4863
- const channel = this.channels.get(publication.name);
4864
- if (channel?.isOpen && channel.delivery === (publication.delivery ?? "reliable")) wantedData.set(mediaKey(member.participantId, publication.id), {
4865
- participantId: member.participantId,
4866
- publicationId: publication.id,
4867
- name: publication.name,
4868
- delivery: channel.delivery
4869
- });
4870
- }
4871
- }
4872
- const wantedMedia = /* @__PURE__ */ new Map();
4873
- for (const { candidate, publication } of this.mediaCandidates()) {
4874
- if (wantedMedia.size >= APP_RTC_LIMITS.mediaSubscriptions) break;
4875
- if (this.filter && !this.filter(candidate)) continue;
4876
- const key = mediaKey(candidate.participantId, publication.id);
4877
- const quality = this.remoteMedia.get(key)?.quality ?? (candidate.simulcast ? "high" : void 0);
4878
- wantedMedia.set(key, {
4879
- participantId: candidate.participantId,
4880
- publicationId: publication.id,
4881
- name: candidate.name,
4882
- kind: candidate.kind,
4883
- ...quality ? { quality } : {}
4884
- });
4885
- }
4886
- await this.reconcileData(wantedData);
4887
- await this.reconcileMedia(wantedMedia);
4888
- if ([...wantedData.keys()].some((key) => !this.remoteData.has(key)) || [...wantedMedia.keys()].some((key) => !this.remoteMedia.has(key))) throw new AppRtcError("RTC_FAILED", "Some RTC subscriptions could not be established");
4889
- }
4890
- dropRemoteData(item) {
4891
- item.channel.close();
4892
- this.channels.get(item.name)?.setPeer(item.participantId, false);
4893
- }
4894
- async reconcileData(wanted) {
4895
- const pc = this.requirePc();
4896
- const stale = [...this.remoteData].filter(([key]) => !wanted.has(key));
4897
- for (const batch of chunk(stale, APP_RTC_LIMITS.batch)) {
4898
- await this.request("POST", `/${this.sessionId}/unsubscribe`, { items: batch.map(([, item]) => ({
4899
- participantId: item.participantId,
4900
- publicationId: item.publicationId
4901
- })) });
4902
- for (const [key, item] of batch) {
4903
- this.dropRemoteData(item);
4904
- this.remoteData.delete(key);
4905
- }
4906
- }
4907
- const missing = [...wanted].filter(([key]) => !this.remoteData.has(key)).map(([, item]) => item);
4908
- for (const batch of chunk(missing, APP_RTC_LIMITS.batch)) {
4909
- const result = await this.request("POST", `/${this.sessionId}/subscribe`, {
4910
- kind: "data",
4911
- items: batch.map(({ participantId, publicationId }) => ({
4912
- participantId,
4913
- publicationId
4914
- }))
4915
- });
4916
- for (const subscription of result.subscriptions) {
4917
- const item = batch.find((entry) => entry.participantId === subscription.participantId && entry.publicationId === subscription.publicationId);
4918
- if (!item || subscription.channelId === void 0) continue;
4919
- const channel = pc.createDataChannel(item.name, {
4920
- negotiated: true,
4921
- id: subscription.channelId,
4922
- ...policyOf(item.delivery)
4923
- });
4924
- channel.binaryType = "arraybuffer";
4925
- const remote = {
4926
- name: item.name,
4927
- participantId: item.participantId,
4928
- publicationId: item.publicationId,
4929
- channel,
4930
- last: null
4931
- };
4932
- channel.addEventListener("message", (event) => this.onData(remote, event.data));
4933
- channel.addEventListener("open", () => {
4934
- if (this.remoteData.get(mediaKey(remote.participantId, remote.publicationId)) === remote) this.channels.get(remote.name)?.setPeer(remote.participantId, true);
4935
- });
4936
- this.remoteData.set(mediaKey(item.participantId, item.publicationId), remote);
4937
- }
4938
- }
4939
- }
4940
- async reconcileMedia(wanted) {
4941
- const stale = [...this.remoteMedia].filter(([key]) => !wanted.has(key));
4942
- const missing = [...wanted].filter(([key]) => !this.remoteMedia.has(key)).map(([, item]) => item);
4943
- const moves = [];
4944
- for (const to of [...missing]) {
4945
- const index = stale.findIndex(([, receiver]) => receiver.kind === to.kind);
4946
- const found = index >= 0 ? stale.splice(index, 1)[0] : void 0;
4947
- if (!found) continue;
4948
- moves.push({
4949
- from: found[0],
4950
- receiver: found[1],
4951
- to
4952
- });
4953
- missing.splice(missing.indexOf(to), 1);
4954
- }
4955
- if (moves.length) {
4956
- const moved = await this.retarget(moves.map(({ receiver, to }) => ({
4957
- ...to,
4958
- mid: receiver.mid
4959
- })));
4960
- for (const { from, receiver, to } of moves) if (moved.has(receiver.mid)) {
4961
- this.remoteMedia.delete(from);
4962
- this.remoteMedia.set(mediaKey(to.participantId, to.publicationId), {
4963
- ...to,
4964
- mid: receiver.mid,
4965
- track: receiver.track
4966
- });
4967
- this.mediaByMid.set(receiver.mid, mediaKey(to.participantId, to.publicationId));
4968
- } else {
4969
- stale.push([from, receiver]);
4970
- missing.push(to);
4971
- }
4972
- if (moved.size) this.notifyTracks();
4973
- }
4974
- await this.unsubscribeMedia(stale);
4975
- await this.subscribeMedia(missing);
4976
- }
4977
- async retarget(items) {
4978
- try {
4979
- const result = await this.request("PATCH", `/${this.sessionId}/media`, { items: items.map(({ participantId, publicationId, mid, quality }) => ({
4980
- participantId,
4981
- publicationId,
4982
- mid,
4983
- ...quality ? { quality } : {}
4984
- })) });
4985
- if (result.offer) await this.answerOffer(result.offer);
4986
- return new Set(result.updated.map((item) => item.mid));
4987
- } catch (error) {
4988
- if (error instanceof ConnectionReplacedError || isSessionLost(error)) throw error;
4989
- return /* @__PURE__ */ new Set();
4990
- }
4991
- }
4992
- async setQuality(key, quality) {
4993
- await this.enqueue(async () => {
4994
- const item = this.remoteMedia.get(key);
4995
- if (!item || item.quality === void 0 || item.quality === quality) return;
4996
- if ((await this.retarget([{
4997
- ...item,
4998
- quality
4999
- }])).has(item.mid)) {
5000
- item.quality = quality;
5001
- this.notifyTracks();
5002
- }
5003
- }).catch((error) => {
5004
- if (error instanceof ConnectionReplacedError) return;
5005
- throw toAppRtcError(error);
5006
- });
5007
- }
5008
- async unsubscribeMedia(stale) {
5009
- if (stale.length === 0) return;
5010
- const pc = this.requirePc();
5011
- for (const [, item] of stale) {
5012
- pc.getTransceivers().find((transceiver) => transceiver.mid === item.mid)?.stop();
5013
- item.track = null;
5014
- }
5015
- this.notifyTracks();
5016
- await this.offerExchange(pc, (offer) => this.request("POST", `/${this.sessionId}/unsubscribe`, {
5017
- items: stale.map(([, item]) => ({
5018
- participantId: item.participantId,
5019
- publicationId: item.publicationId
5020
- })),
5021
- offer
5022
- }));
5023
- for (const [key, item] of stale) {
5024
- this.remoteMedia.delete(key);
5025
- this.mediaByMid.delete(item.mid);
5026
- }
5027
- }
5028
- async subscribeMedia(missing) {
5029
- if (missing.length === 0) return;
5030
- const result = await this.request("POST", `/${this.sessionId}/subscribe`, {
5031
- kind: "media",
5032
- items: missing.map(({ participantId, publicationId, quality }) => ({
5033
- participantId,
5034
- publicationId,
5035
- ...quality ? { quality } : {}
5036
- }))
5037
- });
5038
- for (const subscription of result.subscriptions) {
5039
- const item = missing.find((entry) => entry.participantId === subscription.participantId && entry.publicationId === subscription.publicationId);
5040
- if (!item || !subscription.mid) continue;
5041
- const key = mediaKey(item.participantId, item.publicationId);
5042
- this.remoteMedia.set(key, {
5043
- ...item,
5044
- mid: subscription.mid,
5045
- track: null
5046
- });
5047
- this.mediaByMid.set(subscription.mid, key);
5048
- }
5049
- if (!result.offer) return;
5050
- try {
5051
- await this.answerOffer(result.offer);
5052
- } catch (error) {
5053
- if (!(error instanceof ConnectionReplacedError)) this.reconnect();
5054
- throw error;
5055
- }
5056
- }
5057
- async answerOffer(offer) {
5058
- const pc = this.requirePc();
5059
- await pc.setRemoteDescription(offer);
5060
- await pc.setLocalDescription(await pc.createAnswer());
5061
- await waitForIceGathering(pc, GATHER_TIMEOUT_MS);
5062
- const answer = pc.localDescription;
5063
- if (!answer) throw new Error("the browser produced no answer");
5064
- await this.request("PUT", `/${this.sessionId}`, { answer: {
5065
- type: "answer",
5066
- sdp: answer.sdp
5067
- } });
5068
- }
5069
- async localOffer(pc) {
5070
- await pc.setLocalDescription(await pc.createOffer());
5071
- await waitForIceGathering(pc, GATHER_TIMEOUT_MS);
5072
- const offer = pc.localDescription;
5073
- if (!offer) throw new Error("the browser produced no offer");
5074
- return {
5075
- type: "offer",
5076
- sdp: offer.sdp
5077
- };
5078
- }
5079
- async request(method, path, body) {
5080
- const generation = this.generation;
5081
- const settled = await this.send(method, path, body).then((value) => ({ value }), (error) => ({ error }));
5082
- if (generation !== this.generation) throw new ConnectionReplacedError();
5083
- if ("error" in settled) throw settled.error;
5084
- return settled.value;
5085
- }
5086
- send(method, path, body) {
5087
- const url = `/api/apps/${encodeURIComponent(this.host.appId)}/realtime/rooms/${encodeURIComponent(this.host.roomId)}/rtc${path}`;
5088
- return this.host.transport.request(url, {
5089
- method,
5090
- ...body === void 0 ? {} : {
5091
- headers: { "Content-Type": "application/json" },
5092
- body: JSON.stringify(body)
5093
- }
5094
- });
5095
- }
5096
- requirePc() {
5097
- if (!this.pc || !this.sessionId) throw new ConnectionReplacedError();
5098
- return this.pc;
5099
- }
5100
- onData(remote, raw) {
5101
- if (!(raw instanceof ArrayBuffer)) return;
5102
- const frame = decodeAppRtcFrame(raw);
5103
- if (!frame) return;
5104
- const channel = this.channels.get(remote.name);
5105
- if (!channel?.isOpen) return;
5106
- if (channel.delivery === "latest") {
5107
- if (!isNewerSequence(frame.sequence, remote.last)) return;
5108
- remote.last = frame.sequence;
5109
- }
5110
- channel.deliver(remote.participantId, frame.data);
5111
- }
5112
- onTrack(event) {
5113
- const mid = event.transceiver.mid;
5114
- const key = mid ? this.mediaByMid.get(mid) : void 0;
5115
- const item = key ? this.remoteMedia.get(key) : void 0;
5116
- if (!item || !mid) return;
5117
- item.track = event.track;
5118
- event.track.addEventListener("ended", () => {
5119
- const current = this.mediaByMid.get(mid);
5120
- const owner = current ? this.remoteMedia.get(current) : void 0;
5121
- if (owner?.track === event.track) {
5122
- owner.track = null;
5123
- this.notifyTracks();
5124
- }
5125
- });
5126
- this.notifyTracks();
5127
- }
5128
- notifyTracks() {
5129
- const tracks = this.tracks;
5130
- for (const handler of this.trackHandlers) handler(tracks);
5131
- }
5132
- setState(state) {
5133
- if (this._state === state) return;
5134
- this._state = state;
5135
- for (const handler of this.stateHandlers) handler(state);
5136
- }
5137
- };
5138
- //#endregion
5139
- //#region src/apis/app-realtime.ts
5140
- const randomId = () => globalThis.crypto?.randomUUID?.() ?? `${Date.now()}-${Math.random().toString(36).slice(2)}`;
5141
- const textEncoder = new TextEncoder();
5142
- const byteLength = (text) => textEncoder.encode(text).byteLength;
5143
- /**
5144
- * Ceiling on deltas held during a join, so a stalled handshake cannot buffer without
5145
- * limit. Dropping the tail needs no extra signal: the gap detection below reports it.
5146
- */
5147
- const APP_ROOM_MAX_JOIN_BUFFER = 512;
5148
- /** Room events that mutate state, so they must not be applied before the join snapshot. */
5149
- const APP_ROOM_DELTA_EVENTS = /* @__PURE__ */ new Set([
5150
- "realtime.room.event",
5151
- "realtime.room.member.joined",
5152
- "realtime.room.member.left",
5153
- "realtime.room.presence.updated",
5154
- "realtime.room.publications.updated"
5155
- ]);
5156
- const APP_ROOM_MEMBER_EVENTS = /* @__PURE__ */ new Set([
5157
- "realtime.room.member.joined",
5158
- "realtime.room.member.left",
5159
- "realtime.room.presence.updated",
5160
- "realtime.room.publications.updated"
5161
- ]);
5162
- const requestError = (event) => {
5163
- const payload = event.payload;
5164
- return new Error(typeof payload.message === "string" ? payload.message : typeof payload.code === "string" ? payload.code : "room request failed");
5165
- };
5166
- const isRoomEvent = (event, roomId) => {
5167
- if (event.domain !== "room") return false;
5168
- return event.payload.roomId === roomId || event.rooms?.includes(`room:${roomId}`);
5169
- };
5170
- var AppRoom = class {
5171
- websocket;
5172
- admission;
5173
- appContext;
5174
- id;
5175
- code;
5176
- createdAt;
5177
- expiresAt;
5178
- maxParticipants;
5179
- seatPerUser;
5180
- /** Opaque, stable identity of this viewer inside the room. */
5181
- userKey;
5182
- _participantId;
5183
- _state = "connecting";
5184
- _members = [];
5185
- joinBuffer = null;
5186
- lastSequence = 0;
5187
- hasJoined = false;
5188
- explicitLeave = false;
5189
- expiryTimer = null;
5190
- pending = /* @__PURE__ */ new Map();
5191
- handlers = /* @__PURE__ */ new Map();
5192
- allHandlers = /* @__PURE__ */ new Set();
5193
- stateHandlers = /* @__PURE__ */ new Set();
5194
- membersHandlers = /* @__PURE__ */ new Set();
5195
- outOfSyncHandlers = /* @__PURE__ */ new Set();
5196
- sendErrorHandlers = /* @__PURE__ */ new Set();
5197
- offEvent;
5198
- offOpen;
5199
- offClose;
5200
- offReconnecting;
5201
- instanceId = randomId().replace(/[^A-Za-z0-9_-]/g, "");
5202
- rtcConnection = null;
5203
- constructor(websocket, admission, appContext) {
5204
- this.websocket = websocket;
5205
- this.admission = admission;
5206
- this.appContext = appContext;
5207
- this.id = admission.room.id;
5208
- this.code = admission.room.code;
5209
- this.createdAt = admission.room.createdAt;
5210
- this.expiresAt = admission.room.expiresAt;
5211
- this.maxParticipants = admission.room.maxParticipants;
5212
- this.seatPerUser = admission.room.seatPerUser === true;
5213
- this.userKey = admission.userKey;
5214
- this._participantId = admission.participantId;
5215
- this.offEvent = websocket.on("event", (event) => this.handleEvent(event));
5216
- this.offOpen = websocket.on("open", () => {
5217
- if (!this.hasJoined || this.explicitLeave || this._state === "expired" || this._state === "closed") return;
5218
- this.rejoin();
5219
- });
5220
- this.offReconnecting = websocket.on("reconnecting", () => {
5221
- if (this.hasJoined && !this.explicitLeave) this.setState("reconnecting");
5222
- });
5223
- this.offClose = websocket.on("close", ({ willReconnect }) => {
5224
- this.rejectPending(/* @__PURE__ */ new Error("room connection closed"));
5225
- if (this.explicitLeave || !willReconnect) {
5226
- if (this._state !== "expired") this.setState("closed");
5227
- this.hasJoined = false;
5228
- this.dispose();
5229
- } else if (this.hasJoined) this.setState("reconnecting");
5230
- });
5231
- const delay = Math.max(0, Date.parse(this.expiresAt) - Date.now());
5232
- this.expiryTimer = setTimeout(() => this.expire(), Math.min(delay, 2147e6));
5233
- }
5234
- get state() {
5235
- return this._state;
5236
- }
5237
- /**
5238
- * Identity of this connection inside the room. A `seatPerUser` join can take over a
5239
- * seat the viewer already holds, so this may differ from the id issued at admission.
5240
- */
5241
- get participantId() {
5242
- return this._participantId;
5243
- }
5244
- get members() {
5245
- return this._members.slice();
5246
- }
5247
- async connect() {
5248
- await this.joinOverWebsocket(false);
5249
- return this;
5250
- }
5251
- subscribe(type, handler) {
5252
- const handlers = this.handlers.get(type) ?? /* @__PURE__ */ new Set();
5253
- handlers.add(handler);
5254
- this.handlers.set(type, handlers);
5255
- return () => handlers.delete(handler);
5256
- }
5257
- subscribeAll(handler) {
5258
- this.allHandlers.add(handler);
5259
- return () => this.allHandlers.delete(handler);
5260
- }
5261
- onStateChange(handler) {
5262
- this.stateHandlers.add(handler);
5263
- return () => this.stateHandlers.delete(handler);
5264
- }
5265
- onMembersChanged(handler) {
5266
- this.membersHandlers.add(handler);
5267
- return () => this.membersHandlers.delete(handler);
5268
- }
5269
- onOutOfSync(handler) {
5270
- this.outOfSyncHandlers.add(handler);
5271
- return () => this.outOfSyncHandlers.delete(handler);
5272
- }
5273
- /** Reports failures of {@link send}, which has no ack to reject. */
5274
- onSendError(handler) {
5275
- this.sendErrorHandlers.add(handler);
5276
- return () => this.sendErrorHandlers.delete(handler);
5277
- }
5278
- /**
5279
- * Publishes without waiting for the server ack, for high-frequency traffic such as
5280
- * input frames. Awaiting {@link publish} instead caps a loop at `1000 / rtt` events
5281
- * per second. Ordering still holds, but failures surface through
5282
- * {@link onSendError} rather than a rejected promise, and calls are ignored while
5283
- * the room is not joined; watch {@link onStateChange} to know when it resumes.
5284
- */
5285
- send(type, data) {
5286
- if (this._state !== "joined" || !this.hasJoined) return;
5287
- const failure = this.validateSend(type, data);
5288
- if (failure) {
5289
- for (const handler of this.sendErrorHandlers) handler(failure);
5290
- return;
5291
- }
5292
- this.websocket.publishRealtimeRoom({
5293
- roomId: this.id,
5294
- event: type,
5295
- data
5296
- }).catch((error) => {
5297
- const reason = error instanceof Error ? error : new Error(String(error));
5298
- for (const handler of this.sendErrorHandlers) handler(reason);
5299
- });
5300
- }
5301
- validateSend(type, data) {
5302
- if (!REALTIME_ROOM_EVENT_NAME_PATTERN.test(type)) return /* @__PURE__ */ new Error(`invalid room event name: ${type}`);
5303
- let encoded;
5304
- try {
5305
- encoded = JSON.stringify(data);
5306
- } catch {
5307
- return /* @__PURE__ */ new Error("room event data is not serializable");
5308
- }
5309
- if (encoded === void 0) return /* @__PURE__ */ new Error("room event data is not serializable");
5310
- if (byteLength(encoded) > 16384) return /* @__PURE__ */ new Error("room event payload is too large");
5311
- return null;
5312
- }
5313
- async publish(type, data, options) {
5314
- this.assertJoined();
5315
- const failure = this.validateSend(type, data);
5316
- if (failure) throw failure;
5317
- const requestId = randomId();
5318
- const response = await this.request(requestId, /* @__PURE__ */ new Set(["realtime.room.request.ok"]), () => this.websocket.publishRealtimeRoom({
5319
- roomId: this.id,
5320
- event: type,
5321
- data,
5322
- clientEventId: options?.clientEventId,
5323
- requestId
5324
- }));
5325
- const payload = response.payload;
5326
- return {
5327
- eventId: typeof payload.eventId === "string" ? payload.eventId : response.id,
5328
- sequence: typeof payload.sequence === "number" ? payload.sequence : 0,
5329
- clientEventId: typeof payload.clientEventId === "string" ? payload.clientEventId : null
5330
- };
5331
- }
5332
- /** Published events after `after`, oldest first; without it, the newest ones. */
5333
- async history(input = {}) {
5334
- this.assertJoined();
5335
- if (!this.websocket.supportsCapability("realtime.room.history.v1")) throw new Error("Room history is not supported by the Gateway");
5336
- const requestId = randomId();
5337
- const response = await this.request(requestId, /* @__PURE__ */ new Set(["realtime.room.history"]), () => this.websocket.readRealtimeRoomHistory({
5338
- roomId: this.id,
5339
- ...input,
5340
- requestId
5341
- }));
5342
- return this.toHistory(response.payload);
5343
- }
5344
- /** An invite for someone outside the App, e.g. an Agent: `cohub rooms join <invite>`. */
5345
- async invite(input = {}) {
5346
- this.assertJoined();
5347
- const context = this.appContext;
5348
- if (!context) throw new Error("Invites are only available inside a published App");
5349
- return context.transport.request(`/api/apps/${encodeURIComponent(context.appId)}/realtime/rooms/${encodeURIComponent(this.id)}/invites`, {
5350
- method: "POST",
5351
- headers: { "Content-Type": "application/json" },
5352
- body: JSON.stringify({
5353
- ticket: this.admission.ticket,
5354
- ...input
5355
- })
5356
- });
5357
- }
5358
- rtc() {
5359
- this.assertJoined();
5360
- if (this.rtcConnection) {
5361
- const current = this.rtcConnection;
5362
- return current.then((rtc) => {
5363
- if (rtc.state !== "closed") return rtc;
5364
- return (this.rtcConnection === current ? this.openRtc() : this.rtcConnection) ?? this.openRtc();
5365
- });
5366
- }
5367
- return this.openRtc();
5368
- }
5369
- openRtc() {
5370
- this.assertJoined();
5371
- const context = this.appContext;
5372
- if (!context) return Promise.reject(new AppRtcError("RTC_UNSUPPORTED", "RTC is only available inside a published App"));
5373
- if (typeof RTCPeerConnection === "undefined") return Promise.reject(new AppRtcError("RTC_UNSUPPORTED", "RTC needs a browser with WebRTC"));
5374
- if (!this.websocket.supportsCapability("realtime.room.rtc.v1")) return Promise.reject(new AppRtcError("RTC_UNSUPPORTED", "RTC is not supported by the Gateway"));
5375
- const rtc = new AppRoomRtc({
5376
- roomId: this.id,
5377
- appId: context.appId,
5378
- ticket: this.admission.ticket,
5379
- transport: context.transport,
5380
- participantId: () => this._participantId,
5381
- members: () => this._members,
5382
- onMembersChanged: (handler) => this.onMembersChanged(handler)
5383
- });
5384
- const opening = rtc.start().then(() => rtc, (error) => {
5385
- rtc.close();
5386
- if (this.rtcConnection === opening) this.rtcConnection = null;
5387
- throw error;
5388
- });
5389
- this.rtcConnection = opening;
5390
- return opening;
5391
- }
5392
- async setPresence(presence) {
5393
- this.assertJoined();
5394
- const requestId = randomId();
5395
- await this.request(requestId, /* @__PURE__ */ new Set(["realtime.room.request.ok"]), () => this.websocket.updateRealtimeRoomPresence({
5396
- roomId: this.id,
5397
- presence,
5398
- requestId
5399
- }));
5400
- }
5401
- async leave() {
5402
- if (this.explicitLeave || this._state === "closed" || this._state === "expired") return;
5403
- this.explicitLeave = true;
5404
- try {
5405
- if (this.hasJoined && this.websocket.state === "open") {
5406
- const requestId = randomId();
5407
- await this.request(requestId, /* @__PURE__ */ new Set(["realtime.room.request.ok"]), () => this.websocket.leaveRealtimeRoom({
5408
- roomId: this.id,
5409
- requestId
5410
- }));
5411
- }
5412
- } finally {
5413
- this.hasJoined = false;
5414
- this.setState("closed");
5415
- this.dispose();
5416
- }
5417
- }
5418
- async rejoin() {
5419
- if (this.explicitLeave || this._state === "expired" || this._state === "closed") return;
5420
- try {
5421
- await this.joinOverWebsocket(true);
5422
- } catch (error) {
5423
- if (this.state === "expired") return;
5424
- this.setState("closed");
5425
- this.dispose();
5426
- console.warn("[Cohub AppRoom] failed to rejoin room", error);
5427
- }
5428
- }
5429
- async joinOverWebsocket(isReconnect) {
5430
- if (!this.websocket.supportsCapability("realtime.room.v1") && this.websocket.state === "open") throw new Error("Realtime rooms are not supported by the Gateway");
5431
- this.setState(isReconnect ? "reconnecting" : "connecting");
5432
- const requestId = randomId();
5433
- const after = isReconnect && this.lastSequence > 0 && this.websocket.supportsCapability("realtime.room.history.v1") ? this.lastSequence : void 0;
5434
- let missed = null;
5435
- this.joinBuffer = [];
5436
- try {
5437
- const payload = (await this.request(requestId, /* @__PURE__ */ new Set(["realtime.room.joined"]), () => this.websocket.joinRealtimeRoom({
5438
- roomId: this.id,
5439
- ticket: this.admission.ticket,
5440
- instanceId: this.instanceId,
5441
- after,
5442
- requestId
5443
- }))).payload;
5444
- if (typeof payload.participantId !== "string" || !Array.isArray(payload.members)) throw new Error("invalid room join response");
5445
- this._participantId = payload.participantId;
5446
- this._members = payload.members;
5447
- const sequence = typeof payload.sequence === "number" ? payload.sequence : 0;
5448
- if (after !== void 0 && sequence > after) missed = await this.catchUp(payload.missed, after, sequence);
5449
- this.lastSequence = sequence;
5450
- } catch (error) {
5451
- this.joinBuffer = null;
5452
- throw error;
5453
- }
5454
- this.hasJoined = true;
5455
- this.setState("joined");
5456
- this.notifyMembers();
5457
- if (missed) {
5458
- const { lost } = missed;
5459
- if (lost) for (const handler of this.outOfSyncHandlers) handler(lost[0], lost[1]);
5460
- for (const event of missed.events) this.emit(event);
5461
- }
5462
- const buffered = this.joinBuffer ?? [];
5463
- this.joinBuffer = null;
5464
- for (const item of buffered) {
5465
- const sequence = item.payload.sequence;
5466
- if (typeof sequence === "number" && sequence <= this.lastSequence) continue;
5467
- this.handleEvent(item);
5468
- }
5469
- if (isReconnect) this.rtcConnection?.then((rtc) => rtc.verify()).catch(() => void 0);
5470
- }
5471
- request(requestId, expected, send) {
5472
- return new Promise((resolve, reject) => {
5473
- const timer = setTimeout(() => {
5474
- this.pending.delete(requestId);
5475
- reject(/* @__PURE__ */ new Error("room request timed out"));
5476
- }, 2e4);
5477
- this.pending.set(requestId, {
5478
- expected,
5479
- resolve,
5480
- reject,
5481
- timer
5482
- });
5483
- send().catch((error) => {
5484
- this.pending.delete(requestId);
5485
- clearTimeout(timer);
5486
- reject(error instanceof Error ? error : new Error(String(error)));
5487
- });
5488
- });
5489
- }
5490
- /** Detaches the entry awaiting this requestId, so the caller can settle it. */
5491
- takePending(requestId) {
5492
- const pending = requestId ? this.pending.get(requestId) : void 0;
5493
- if (!pending || !requestId) return null;
5494
- this.pending.delete(requestId);
5495
- clearTimeout(pending.timer);
5496
- return pending;
5497
- }
5498
- handleEvent(event) {
5499
- if (!isRoomEvent(event, this.id)) {
5500
- if (event.type === "system.request.error") this.takePending(event.requestId)?.reject(requestError(event));
5501
- return;
5502
- }
5503
- if (event.type === "realtime.room.request.error") {
5504
- const failure = requestError(event);
5505
- if (event.requestId) {
5506
- this.takePending(event.requestId)?.reject(failure);
5507
- return;
5508
- }
5509
- for (const handler of this.sendErrorHandlers) handler(failure);
5510
- return;
5511
- }
5512
- const awaiting = event.requestId ? this.pending.get(event.requestId) : void 0;
5513
- if (awaiting?.expected.has(event.type)) {
5514
- this.takePending(event.requestId);
5515
- awaiting.resolve(event);
5516
- }
5517
- if (this.joinBuffer && APP_ROOM_DELTA_EVENTS.has(event.type)) {
5518
- if (this.joinBuffer.length < APP_ROOM_MAX_JOIN_BUFFER) this.joinBuffer.push(event);
5519
- return;
5520
- }
5521
- const payload = event.payload;
5522
- if (APP_ROOM_DELTA_EVENTS.has(event.type) && typeof payload.sequence === "number") this.observeSequence(payload.sequence);
5523
- if (APP_ROOM_MEMBER_EVENTS.has(event.type)) {
5524
- const member = payload.member;
5525
- if (member?.participantId) {
5526
- const members = new Map(this._members.map((item) => [item.participantId, item]));
5527
- if (event.type === "realtime.room.member.left") members.delete(member.participantId);
5528
- else members.set(member.participantId, member);
5529
- this._members = [...members.values()];
5530
- this.notifyMembers();
5531
- }
5532
- return;
5533
- }
5534
- if (event.type === "realtime.room.closed") {
5535
- this.hasJoined = false;
5536
- if (payload.reason === "expired") this.setState("expired");
5537
- else this.setState("closed");
5538
- this.rejectPending(/* @__PURE__ */ new Error(`room closed: ${typeof payload.reason === "string" ? payload.reason : "unknown"}`));
5539
- this.dispose();
5540
- return;
5541
- }
5542
- if (event.type !== "realtime.room.event") return;
5543
- const { id, timestamp, payload: roomPayload } = event;
5544
- this.emit({
5545
- id,
5546
- timestamp,
5547
- roomId: roomPayload.roomId,
5548
- sequence: roomPayload.sequence,
5549
- type: roomPayload.event,
5550
- data: roomPayload.data,
5551
- clientEventId: roomPayload.clientEventId,
5552
- sender: roomPayload.sender,
5553
- self: roomPayload.sender.participantId === this.participantId
5554
- });
5555
- }
5556
- async catchUp(first, after, sequence) {
5557
- if (!first) return {
5558
- events: [],
5559
- lost: [after + 1, sequence]
5560
- };
5561
- const events = this.toHistory(first).events;
5562
- const { truncated } = first;
5563
- let { cursor } = first;
5564
- while (cursor < sequence) {
5565
- const requestId = randomId();
5566
- const page = (await this.request(requestId, /* @__PURE__ */ new Set(["realtime.room.history"]), () => this.websocket.readRealtimeRoomHistory({
5567
- roomId: this.id,
5568
- after: cursor,
5569
- requestId
5570
- })).catch(() => null))?.payload;
5571
- if (!page || page.cursor <= cursor) break;
5572
- events.push(...this.toHistory(page).events);
5573
- cursor = page.cursor;
5574
- }
5575
- const kept = events.filter((event) => event.sequence <= sequence);
5576
- const lost = truncated ? [after + 1, kept[0]?.sequence ?? sequence] : cursor < sequence ? [cursor + 1, sequence] : null;
5577
- return {
5578
- events: kept,
5579
- lost: lost && lost[1] > lost[0] ? lost : null
5580
- };
5581
- }
5582
- emit(item) {
5583
- this.handlers.get(item.type)?.forEach((handler) => {
5584
- handler(item);
5585
- });
5586
- this.allHandlers.forEach((handler) => {
5587
- handler(item);
5588
- });
5589
- }
5590
- toHistory(history) {
5591
- return {
5592
- events: history.events.map((event) => ({
5593
- ...event,
5594
- data: event.data,
5595
- roomId: this.id,
5596
- self: event.sender.participantId === this.participantId
5597
- })),
5598
- cursor: history.cursor,
5599
- truncated: history.truncated
5600
- };
5601
- }
5602
- observeSequence(sequence) {
5603
- if (this.lastSequence > 0 && sequence > this.lastSequence + 1) for (const handler of this.outOfSyncHandlers) handler(this.lastSequence + 1, sequence);
5604
- if (sequence > this.lastSequence) this.lastSequence = sequence;
5605
- }
5606
- assertJoined() {
5607
- if (this._state !== "joined" || !this.hasJoined) throw new Error("room is not joined");
5608
- }
5609
- setState(state) {
5610
- if (this._state === state) return;
5611
- this._state = state;
5612
- for (const handler of this.stateHandlers) handler(state);
5613
- }
5614
- notifyMembers() {
5615
- const members = this.members;
5616
- for (const handler of this.membersHandlers) handler(members);
5617
- }
5618
- rejectPending(error) {
5619
- for (const [requestId, pending] of this.pending) {
5620
- this.pending.delete(requestId);
5621
- clearTimeout(pending.timer);
5622
- pending.reject(error);
5623
- }
5624
- }
5625
- expire() {
5626
- if (this.explicitLeave || this._state === "closed" || this._state === "expired") return;
5627
- this.hasJoined = false;
5628
- this.setState("expired");
5629
- this.rejectPending(/* @__PURE__ */ new Error("room expired"));
5630
- this.dispose();
5631
- }
5632
- dispose() {
5633
- const rtc = this.rtcConnection;
5634
- this.rtcConnection = null;
5635
- rtc?.then((connection) => connection.close()).catch(() => void 0);
5636
- this.offEvent();
5637
- this.offOpen();
5638
- this.offClose();
5639
- this.offReconnecting();
5640
- if (this.expiryTimer) clearTimeout(this.expiryTimer);
5641
- this.expiryTimer = null;
5642
- }
5643
- };
5644
- var AppRealtimeApi = class {
5645
- transport;
5646
- websocket;
5647
- getContext;
5648
- constructor(transport, websocket, getContext) {
5649
- this.transport = transport;
5650
- this.websocket = websocket;
5651
- this.getContext = getContext;
5652
- }
5653
- async createRoom(input = {}) {
5654
- const appId = await this.requireAppId();
5655
- const admission = await this.transport.request(`/api/apps/${encodeURIComponent(appId)}/realtime/rooms`, {
5656
- method: "POST",
5657
- headers: { "Content-Type": "application/json" },
5658
- body: JSON.stringify(input)
5659
- });
5660
- return this.openRoom(appId, admission);
5661
- }
5662
- async joinRoom(input) {
5663
- const appId = await this.requireAppId();
5664
- const admission = await this.transport.request(`/api/apps/${encodeURIComponent(appId)}/realtime/rooms/join`, {
5665
- method: "POST",
5666
- headers: { "Content-Type": "application/json" },
5667
- body: JSON.stringify(input)
5668
- });
5669
- return this.openRoom(appId, admission);
5670
- }
5671
- async requireAppId() {
5672
- const context = await this.getContext();
5673
- if (!context?.app?.id) throw new Error("App context is unavailable — not running inside a published app runtime.");
5674
- return context.app.id;
5675
- }
5676
- async openRoom(appId, admission) {
5677
- const room = new AppRoom(this.websocket, admission, {
5678
- transport: this.transport,
5679
- appId
5680
- });
5681
- try {
5682
- await room.connect();
5683
- return room;
5684
- } catch (error) {
5685
- await room.leave().catch(() => void 0);
5686
- throw error;
5687
- }
5688
- }
5689
- };
5690
- /** @deprecated Use `AppRealtimeApi`. */
5691
- var WorkRealtimeApi = class extends AppRealtimeApi {};
5692
- /** @deprecated Use `AppRoom`. */
5693
- var WorkRoom = class extends AppRoom {};
5694
- //#endregion
5695
4090
  //#region src/app-surface.ts
5696
4091
  /**
5697
4092
  * Explicit app origins, not a Cohub suffix match: apps themselves are
@@ -7215,7 +5610,7 @@ var CohubClient = class {
7215
5610
  this.referrals = new ReferralsApi(this.transport);
7216
5611
  this.apps = new AppsApi(this.transport);
7217
5612
  this.appCommerce = new AppCommerceApi(this.transport);
7218
- this.rooms = new RoomsApi(this.transport);
5613
+ this.rooms = new RoomsApi(this.transport, this.websocketClient);
7219
5614
  this.navigation = { open: (target, options) => this.appRuntime.navigationOpen(typeof target === "string" ? {
7220
5615
  kind: "app",
7221
5616
  ref: target
@@ -7225,6 +5620,9 @@ var CohubClient = class {
7225
5620
  context() {
7226
5621
  return this.appRuntime.context();
7227
5622
  }
5623
+ async disconnect() {
5624
+ await this.websocketClient.disconnect();
5625
+ }
7228
5626
  /** Releases listeners this client registered. Safe to call more than once. */
7229
5627
  dispose() {
7230
5628
  this.appRuntime.dispose();