@neta-art/cohub 11.2.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.
- package/dist/board/model/index.d.ts +2 -1
- package/dist/board/model/index.js +2 -1
- package/dist/board/model/script-api.d.ts +69 -0
- package/dist/board/model/script-api.js +77 -0
- package/dist/board/stage/stage.js +1 -1
- package/dist/chunks/app-realtime.js +1697 -0
- package/dist/chunks/http.d.ts +346 -3
- package/dist/chunks/http.js +32 -1
- package/dist/chunks/websocket.d.ts +4 -0
- package/dist/chunks/websocket.js +11 -2
- package/dist/http.d.ts +1 -1
- package/dist/index.d.ts +3 -322
- package/dist/index.js +58 -1660
- package/dist/protocol/dist/board-model.js +51 -51
- package/dist/protocol/dist/board-schema.d.ts +1 -2
- package/dist/protocol/dist/board-schema.js +7 -22
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { A as
|
|
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
|
|
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
|
|
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();
|