@neta-art/cohub 10.0.0 → 11.0.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/README.md +11 -2
- package/dist/board/editor/animation.d.ts +3 -0
- package/dist/board/editor/animation.js +16 -1
- package/dist/board/editor/clipboard.js +7 -0
- package/dist/board/editor/copy.js +23 -18
- package/dist/board/editor/creation.d.ts +5 -1
- package/dist/board/editor/creation.js +2 -1
- package/dist/board/editor/deletion.js +3 -3
- package/dist/board/editor/document.d.ts +8 -2
- package/dist/board/editor/document.js +54 -7
- package/dist/board/editor/editor.d.ts +18 -3
- package/dist/board/editor/editor.js +11 -1
- package/dist/board/editor/query.d.ts +5 -5
- package/dist/board/editor/query.js +14 -12
- package/dist/board/editor/selection.js +2 -1
- package/dist/board/export/index.d.ts +25 -4
- package/dist/board/export/index.js +45 -12
- package/dist/board/export/node/index.d.ts +5 -4
- package/dist/board/export/node/index.js +16 -11
- package/dist/board/export/node/scripts.d.ts +16 -0
- package/dist/board/export/node/scripts.js +258 -0
- package/dist/board/export/scene.d.ts +2 -1
- package/dist/board/export/scene.js +20 -6
- package/dist/board/model/animation.d.ts +4 -1
- package/dist/board/model/animation.js +34 -7
- package/dist/board/model/authoring.d.ts +20 -0
- package/dist/board/model/authoring.js +185 -0
- package/dist/board/model/export-plan.js +1 -0
- package/dist/board/model/file-preview.js +1 -1
- package/dist/board/model/file-snapshot.js +3 -2
- package/dist/board/model/image-key.js +6 -2
- package/dist/board/model/index.d.ts +13 -6
- package/dist/board/model/index.js +14 -8
- package/dist/board/model/items/builtin.js +2 -2
- package/dist/board/model/items/create.d.ts +4 -2
- package/dist/board/model/items/create.js +6 -5
- package/dist/board/model/items/index.d.ts +2 -2
- package/dist/board/model/items/index.js +2 -2
- package/dist/board/model/media-playback.d.ts +2 -1
- package/dist/board/model/media-playback.js +14 -4
- package/dist/board/model/media.js +7 -1
- package/dist/board/model/sampling.d.ts +19 -0
- package/dist/board/model/sampling.js +96 -0
- package/dist/board/model/script-example.d.ts +4 -0
- package/dist/board/model/script-example.js +21 -0
- package/dist/board/model/script-hit.d.ts +10 -0
- package/dist/board/model/script-hit.js +52 -0
- package/dist/board/model/script-index.d.ts +7 -0
- package/dist/board/model/script-index.js +15 -0
- package/dist/board/model/script-scene.d.ts +24 -0
- package/dist/board/model/script-scene.js +267 -0
- package/dist/board/model/script.d.ts +105 -0
- package/dist/board/model/script.js +1 -0
- package/dist/board/render/css-color.d.ts +6 -1
- package/dist/board/render/css-color.js +34 -20
- package/dist/board/render/diagnostics.d.ts +20 -0
- package/dist/board/render/diagnostics.js +117 -0
- package/dist/board/render/index.d.ts +9 -6
- package/dist/board/render/index.js +9 -6
- package/dist/board/render/palette.d.ts +2 -1
- package/dist/board/render/palette.js +6 -2
- package/dist/board/render/renderers/arrow-card-renderer.js +3 -2
- package/dist/board/render/renderers/audio-card-renderer.js +2 -1
- package/dist/board/render/renderers/board-renderer-registry.d.ts +10 -5
- package/dist/board/render/renderers/board-renderer-registry.js +3 -3
- package/dist/board/render/renderers/custom-card-renderer.d.ts +5 -0
- package/dist/board/render/renderers/{sketch-card-renderer.js → custom-card-renderer.js} +16 -15
- package/dist/board/render/renderers/draw-card-renderer.js +3 -2
- package/dist/board/render/renderers/effect-card-renderer.js +22 -5
- package/dist/board/render/renderers/frame-card-renderer.js +30 -14
- package/dist/board/render/renderers/image-card-renderer.js +43 -4
- package/dist/board/render/renderers/shape-card-renderer.js +10 -27
- package/dist/board/render/renderers/text-card-renderer.js +5 -3
- package/dist/board/render/renderers/unknown-card-renderer.js +1 -0
- package/dist/board/render/renderers/video-card-renderer.js +2 -1
- package/dist/board/render/sampling.d.ts +22 -0
- package/dist/board/render/sampling.js +21 -0
- package/dist/board/render/script-export.d.ts +11 -0
- package/dist/board/render/script-export.js +86 -0
- package/dist/board/render/script-host.d.ts +31 -0
- package/dist/board/render/script-host.js +421 -0
- package/dist/board/render/script-modules.d.ts +2 -0
- package/dist/board/render/script-modules.js +2 -0
- package/dist/board/render/script-runtime.d.ts +29 -0
- package/dist/board/render/script-runtime.js +213 -0
- package/dist/board/render/script-sandbox.d.ts +4 -0
- package/dist/board/render/script-sandbox.js +173 -0
- package/dist/board/render/stroke.d.ts +2 -1
- package/dist/board/render/stroke.js +22 -1
- package/dist/board/render/text-measurement.js +1 -1
- package/dist/board/stage/assets.js +6 -1
- package/dist/board/stage/maintenance.d.ts +5 -0
- package/dist/board/stage/maintenance.js +11 -0
- package/dist/board/stage/scene.js +5 -2
- package/dist/board/stage/script-pointer.d.ts +19 -0
- package/dist/board/stage/script-pointer.js +35 -0
- package/dist/board/stage/stage.d.ts +10 -0
- package/dist/board/stage/stage.js +249 -43
- package/dist/board/stage/texture-pool.js +2 -3
- package/dist/chunks/environment.d.ts +266 -23
- package/dist/chunks/host-bridge.d.ts +2 -2
- package/dist/chunks/http.d.ts +88 -7
- package/dist/chunks/http.js +155 -63
- package/dist/chunks/transport.js +3 -1
- package/dist/chunks/websocket.d.ts +9 -1
- package/dist/chunks/websocket.js +16 -2
- package/dist/host-bridge.js +2 -1
- package/dist/http.d.ts +2 -2
- package/dist/index.d.ts +182 -4
- package/dist/index.js +1374 -126
- package/dist/protocol/dist/board-assets.d.ts +1 -0
- package/dist/protocol/dist/board-constants.js +2 -2
- package/dist/protocol/dist/board-model.d.ts +201 -11
- package/dist/protocol/dist/board-model.js +33 -14
- package/dist/protocol/dist/board-patch.js +2 -1
- package/dist/protocol/dist/board-schema.d.ts +4 -2
- package/dist/protocol/dist/board-schema.js +18 -4
- package/dist/protocol/dist/board-script-modules.d.ts +25 -0
- package/dist/protocol/dist/board-script-modules.js +127 -0
- package/dist/protocol/dist/display/index.d.ts +1 -0
- package/dist/protocol/dist/index.d.ts +4 -2
- package/dist/protocol/dist/realtime/types.d.ts +1 -0
- package/dist/protocol/dist/rtc/index.d.ts +1 -0
- package/package.json +2 -1
- package/dist/board/export/node/sketch.d.ts +0 -19
- package/dist/board/export/node/sketch.js +0 -146
- package/dist/board/render/renderers/sketch-card-renderer.d.ts +0 -5
- package/dist/board/render/sketch-host.d.ts +0 -14
- package/dist/board/render/sketch-host.js +0 -216
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { $ as
|
|
2
|
-
import { A as
|
|
1
|
+
import { $ as isTerminalDesktopCommandStatus, A as BoardClient, B as CronJobsApi, C as SessionPatchReducer, D as connectDisplay, E as DisplayConnection, F as PublicAssetsApi, G as DESKTOP_COMMAND_OPEN_TIMEOUT_MS, H as DESKTOP_COMMAND_ACCEPT_TIMEOUT_MS, I as SkillsApi, J as DESKTOP_COMMAND_SETTLEMENT_GRACE_SECONDS, K as DESKTOP_COMMAND_PAYLOAD_MAX_BYTES, L as PromptsApi, M as SessionAccessApi, N as ReferencesApi, O as readRtcTransportStats, P as SearchApi, Q as isDesktopCallMethod, R as ModelsApi, S as parseAssistantMessageCommit, T as SpaceDisplaysApi, U as DESKTOP_COMMAND_DEFAULT_TIMEOUT_MS, V as ChannelsApi, W as DESKTOP_COMMAND_MAX_TIMEOUT_MS, X as DESKTOP_COMMAND_VERSION, Y as DESKTOP_COMMAND_TERMINAL_TTL_SECONDS, Z as defaultDesktopCommandTimeoutMs, _ as PublicInviteApi, a as WorkCommerceApi, b as SessionGenerationStreamClient, c as AppsApi, ct as normalizeDisplayPoint, d as UserApi, et as parseDesktopCommand, f as TasksApi, g as SpacesApi, h as SpaceWebhooksApi, i as AppCommerceApi, it as DISPLAY_ELEMENT_REF_PATTERN, j as ensureRealtimeConnected, k as waitForIceGathering, l as ReferralsApi, lt as splitDisplayText, m as SpacePublicFilesApi, n as createHttpClient, nt as resolveOpenSurface, o as DesktopCommandsApi, ot as DISPLAY_INPUT_MAX_TEXT, p as SpaceClient, q as DESKTOP_COMMAND_PENDING_TTL_SECONDS, r as RoomsApi, rt as NAVIGATION_ERROR_MESSAGE_MAX_LENGTH, s as UiCommandsApi, st as DISPLAY_SYSTEM_ACTIONS, t as CohubHttpClient, tt as parseDesktopCommandId, u as UsersApi, v as buildSpaceInvitePath, w as createSessionPatchReducer, x as createSessionGenerationStreamClient, y as buildSpacePath, z as GenerationsApi } from "./chunks/http.js";
|
|
2
|
+
import { A as requestSourceToHeaders, C as isRequestSourceClientId, D as normalizeRequestSource, E as mergeRequestSourceIntoMeta, M as isUuid, N as isUuidLike, O as parseRequestSourceFromHeaders, P as isUuidOrShortUuid, S as hasRequestSourceIdentity, T as isRequestSourceUuid, a as sanitizeAccessToken, b as COHUB_SOURCE_HEADER_NAMES, i as matchesUnauthorizedErrorToken, j as resolveRequestSourceChannel, k as readRequestSourceFromEnv, n as HttpTransport, o as REALTIME_ROOM_EVENT_NAME_PATTERN, r as joinApiUrl, t as HttpError, w as isRequestSourceEmpty, x as REQUEST_SOURCE_VIA_MAX_LENGTH, y as COHUB_SOURCE_HEADER } from "./chunks/transport.js";
|
|
3
3
|
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
4
|
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
5
|
import { VoiceApi, VoiceInputClient, createVoiceInputClient } from "./voice-input.js";
|
|
@@ -78,8 +78,95 @@ const UPLOAD_MAX_FILE_BYTES = 1 * GiB;
|
|
|
78
78
|
const UPLOAD_MAX_BATCH_BYTES = 2 * GiB;
|
|
79
79
|
/** Maximum file count of one upload batch. */
|
|
80
80
|
const UPLOAD_MAX_BATCH_FILES = 2e3;
|
|
81
|
+
//#endregion
|
|
82
|
+
//#region ../protocol/dist/board-url.js
|
|
83
|
+
const BOARD_REMOTE_URL_MAX_LENGTH = 4096;
|
|
84
|
+
function parseIpv4(host) {
|
|
85
|
+
const parts = host.split(".").map(Number);
|
|
86
|
+
return parts.length === 4 && parts.every((part) => Number.isInteger(part) && part >= 0 && part <= 255) ? parts : null;
|
|
87
|
+
}
|
|
88
|
+
function isBlockedIpv4(host) {
|
|
89
|
+
const parts = parseIpv4(host);
|
|
90
|
+
if (!parts) return false;
|
|
91
|
+
const [first, second, third] = parts;
|
|
92
|
+
if (first === 0 || first === 10 || first === 127) return true;
|
|
93
|
+
if (first === 100 && second >= 64 && second <= 127) return true;
|
|
94
|
+
if (first === 169 && second === 254) return true;
|
|
95
|
+
if (first === 172 && second >= 16 && second <= 31) return true;
|
|
96
|
+
if (first === 192 && second === 168) return true;
|
|
97
|
+
if (first === 192 && second === 0 && (third === 0 || third === 2)) return true;
|
|
98
|
+
if (first === 192 && second === 88 && third === 99) return true;
|
|
99
|
+
if (first === 198 && (second === 18 || second === 19)) return true;
|
|
100
|
+
if (first === 198 && second === 51 && third === 100) return true;
|
|
101
|
+
if (first === 203 && second === 0 && third === 113) return true;
|
|
102
|
+
return first >= 224;
|
|
103
|
+
}
|
|
104
|
+
function expandIpv6(host) {
|
|
105
|
+
const [head, tail, extra] = host.toLowerCase().split("::");
|
|
106
|
+
if (extra !== void 0) return null;
|
|
107
|
+
const headParts = head ? head.split(":").filter(Boolean) : [];
|
|
108
|
+
const tailParts = tail ? tail.split(":").filter(Boolean) : [];
|
|
109
|
+
const missing = 8 - headParts.length - tailParts.length;
|
|
110
|
+
if (missing < 0 || tail === void 0 && missing !== 0) return null;
|
|
111
|
+
const parts = [
|
|
112
|
+
...headParts,
|
|
113
|
+
...Array.from({ length: missing }, () => "0"),
|
|
114
|
+
...tailParts
|
|
115
|
+
];
|
|
116
|
+
if (parts.length !== 8 || parts.some((part) => !/^[0-9a-f]{1,4}$/.test(part))) return null;
|
|
117
|
+
return parts.map((part) => part.padStart(4, "0"));
|
|
118
|
+
}
|
|
119
|
+
function isBlockedIpv6(host) {
|
|
120
|
+
const parts = expandIpv6(host);
|
|
121
|
+
if (!parts) return true;
|
|
122
|
+
if (parts.every((part) => part === "0000")) return true;
|
|
123
|
+
if (parts.slice(0, 7).every((part) => part === "0000") && parts[7] === "0001") return true;
|
|
124
|
+
if (parts.slice(0, 5).every((part) => part === "0000") && parts[5] === "ffff") {
|
|
125
|
+
const high = Number.parseInt(parts[6] ?? "0", 16);
|
|
126
|
+
const low = Number.parseInt(parts[7] ?? "0", 16);
|
|
127
|
+
return isBlockedIpv4(`${high >> 8}.${high & 255}.${low >> 8}.${low & 255}`);
|
|
128
|
+
}
|
|
129
|
+
if (parts.slice(0, 6).every((part) => part === "0000")) return true;
|
|
130
|
+
const first = Number.parseInt(parts[0] ?? "0", 16);
|
|
131
|
+
if ((first & 65024) === 64512) return true;
|
|
132
|
+
if ((first & 65472) === 65152 || (first & 65472) === 65216) return true;
|
|
133
|
+
if ((first & 65280) === 65280) return true;
|
|
134
|
+
return parts[0] === "2001" && parts[1] === "0db8";
|
|
135
|
+
}
|
|
136
|
+
function isPublicBoardRemoteAddress(value) {
|
|
137
|
+
const address = value.toLowerCase().replace(/^\[|\]$/g, "").replace(/\.$/, "");
|
|
138
|
+
if (parseIpv4(address)) return !isBlockedIpv4(address);
|
|
139
|
+
return address.includes(":") && !isBlockedIpv6(address);
|
|
140
|
+
}
|
|
141
|
+
function isBlockedHost(hostname) {
|
|
142
|
+
const host = hostname.toLowerCase().replace(/^\[|\]$/g, "").replace(/\.$/, "");
|
|
143
|
+
if (host === "localhost" || host.endsWith(".localhost") || host.endsWith(".local") || host.endsWith(".internal")) return true;
|
|
144
|
+
if (parseIpv4(host) || host.includes(":")) return !isPublicBoardRemoteAddress(host);
|
|
145
|
+
return false;
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Normalize a browser-loadable public HTTP(S) URL. This blocks explicit local
|
|
149
|
+
* addresses; any future server-side fetcher must additionally validate DNS
|
|
150
|
+
* resolution to defend against rebinding.
|
|
151
|
+
*/
|
|
152
|
+
function normalizeBoardRemoteUrl(value) {
|
|
153
|
+
if (typeof value !== "string") return void 0;
|
|
154
|
+
const input = value.trim();
|
|
155
|
+
if (!input || input.length > 4096) return void 0;
|
|
156
|
+
try {
|
|
157
|
+
const url = new URL(input);
|
|
158
|
+
if (url.protocol !== "https:" && url.protocol !== "http:") return void 0;
|
|
159
|
+
if (url.username || url.password || isBlockedHost(url.hostname)) return;
|
|
160
|
+
const normalized = url.toString();
|
|
161
|
+
return normalized.length <= 4096 ? normalized : void 0;
|
|
162
|
+
} catch {
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
z.string().max(BOARD_REMOTE_URL_MAX_LENGTH).refine((value) => normalizeBoardRemoteUrl(value) !== void 0, { message: "URL must be a public HTTP(S) URL without credentials" });
|
|
81
167
|
const BOARD_CAMERA_TARGET = "camera";
|
|
82
|
-
const
|
|
168
|
+
const BoardKeySchema = z.string().min(1).max(160).regex(/^[^\s/]+$/, "ids must not contain whitespace or '/'");
|
|
169
|
+
const BoardIdSchema = BoardKeySchema.refine((value) => value !== BOARD_CAMERA_TARGET, "\"camera\" is reserved");
|
|
83
170
|
const finite$1 = z.number().finite();
|
|
84
171
|
const nonNegative = finite$1.nonnegative();
|
|
85
172
|
const ratio = finite$1.min(0).max(1);
|
|
@@ -126,10 +213,29 @@ const BoardStyleSchema = z.object({
|
|
|
126
213
|
trim: ratio.optional()
|
|
127
214
|
}).strict();
|
|
128
215
|
function isSafeBoardSourcePath(value) {
|
|
129
|
-
return value.length > 0 && value.length <= 4096 && !value.startsWith("/") && !value.includes("\\") && value.split("/").every((part) => part.length > 0 && part !== "." && part !== "..");
|
|
216
|
+
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 !== "..");
|
|
130
217
|
}
|
|
131
218
|
const srcSchema = z.string().refine(isSafeBoardSourcePath, "src must be a relative Space file path");
|
|
219
|
+
function parseBoardMediaSource(src) {
|
|
220
|
+
if (isSafeBoardSourcePath(src)) return {
|
|
221
|
+
kind: "file",
|
|
222
|
+
value: src
|
|
223
|
+
};
|
|
224
|
+
return normalizeBoardRemoteUrl(src) ? {
|
|
225
|
+
kind: "url",
|
|
226
|
+
value: src
|
|
227
|
+
} : null;
|
|
228
|
+
}
|
|
229
|
+
const BoardMediaSourceSchema = z.string().max(4096).refine((src) => parseBoardMediaSource(src) !== null, "src must be a public HTTP(S) URL or relative Space file path").describe("Prefer a stable public HTTP(S) URL; relative Space file paths remain supported.");
|
|
230
|
+
const 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(),
|
|
235
|
+
enabled: z.boolean().default(true)
|
|
236
|
+
}).strict();
|
|
132
237
|
const itemEnvelope = {
|
|
238
|
+
script: BoardScriptSchema.optional(),
|
|
133
239
|
parent: BoardIdSchema.optional(),
|
|
134
240
|
z: finite$1.optional(),
|
|
135
241
|
position: BoardVec2Schema.default({
|
|
@@ -174,6 +280,7 @@ const BoardFrameItemSchema = z.object({
|
|
|
174
280
|
type: z.literal("frame"),
|
|
175
281
|
props: z.object({
|
|
176
282
|
label: z.string().max(280).default(""),
|
|
283
|
+
decoration: z.enum(["frame", "none"]).optional(),
|
|
177
284
|
clip: z.boolean().default(true)
|
|
178
285
|
}).strict().prefault({})
|
|
179
286
|
}).strict();
|
|
@@ -260,7 +367,7 @@ const BoardArrowItemSchema = z.object({
|
|
|
260
367
|
}).strict()
|
|
261
368
|
}).strict();
|
|
262
369
|
const fileProps = {
|
|
263
|
-
src:
|
|
370
|
+
src: BoardMediaSourceSchema,
|
|
264
371
|
snapshot: z.object({
|
|
265
372
|
title: z.string().optional(),
|
|
266
373
|
mimeType: z.string().optional(),
|
|
@@ -389,14 +496,11 @@ const BoardEffectItemSchema = z.object({
|
|
|
389
496
|
seed: z.string().min(1).max(160).optional()
|
|
390
497
|
}).strict()
|
|
391
498
|
}).strict();
|
|
392
|
-
const
|
|
499
|
+
const BoardCustomItemSchema = z.object({
|
|
393
500
|
...boxed(480, 320),
|
|
394
|
-
type: z.literal("
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
params: z.record(z.string(), z.unknown()).default({}),
|
|
398
|
-
seed: z.string().min(1).max(160).optional()
|
|
399
|
-
}).strict()
|
|
501
|
+
type: z.literal("custom"),
|
|
502
|
+
script: BoardScriptSchema,
|
|
503
|
+
props: z.object({}).strict().default({})
|
|
400
504
|
}).strict();
|
|
401
505
|
const BOARD_EXTENSION_TYPE_PATTERN = /^[a-z][a-z0-9]*(?:\.[a-z0-9-]+)+$/;
|
|
402
506
|
const BoardExtensionItemSchema = z.object({
|
|
@@ -420,7 +524,7 @@ const BOARD_ITEM_SCHEMAS = {
|
|
|
420
524
|
file: BoardFileItemSchema,
|
|
421
525
|
task: BoardTaskItemSchema,
|
|
422
526
|
effect: BoardEffectItemSchema,
|
|
423
|
-
|
|
527
|
+
custom: BoardCustomItemSchema
|
|
424
528
|
};
|
|
425
529
|
const BOARD_ITEM_TYPES = Object.keys(BOARD_ITEM_SCHEMAS);
|
|
426
530
|
function isBuiltinItemType(type) {
|
|
@@ -535,7 +639,7 @@ const BoardAnimationHeaderSchema = z.object({
|
|
|
535
639
|
end: z.enum(["hold", "reset"]).default("hold"),
|
|
536
640
|
markers: z.array(BoardMarkerSchema).max(1e4).default([])
|
|
537
641
|
}).strict();
|
|
538
|
-
const BoardAnimationSchema = BoardAnimationHeaderSchema.extend({ tracks: z.record(
|
|
642
|
+
const BoardAnimationSchema = BoardAnimationHeaderSchema.extend({ tracks: z.record(BoardKeySchema, BoardTrackSchema).default({}) }).strict();
|
|
539
643
|
const BOARD_ENTER_PRESETS = [
|
|
540
644
|
"fade-in",
|
|
541
645
|
"rise",
|
|
@@ -575,7 +679,7 @@ const patchObject = z.record(z.string(), z.unknown());
|
|
|
575
679
|
const BoardPatchSchema = z.object({
|
|
576
680
|
board: patchObject.nullable().optional(),
|
|
577
681
|
items: z.record(BoardIdSchema, patchObject.nullable()).optional(),
|
|
578
|
-
animations: z.record(BoardIdSchema, patchObject.and(z.object({ tracks: z.record(
|
|
682
|
+
animations: z.record(BoardIdSchema, patchObject.and(z.object({ tracks: z.record(BoardKeySchema, patchObject.nullable()).optional() })).nullable()).optional()
|
|
579
683
|
}).strict();
|
|
580
684
|
//#endregion
|
|
581
685
|
//#region ../protocol/dist/board-json.js
|
|
@@ -1119,7 +1223,8 @@ const NOT_ANIMATABLE = /* @__PURE__ */ new Set([
|
|
|
1119
1223
|
"parent",
|
|
1120
1224
|
"z",
|
|
1121
1225
|
"locked",
|
|
1122
|
-
"metadata"
|
|
1226
|
+
"metadata",
|
|
1227
|
+
"script"
|
|
1123
1228
|
]);
|
|
1124
1229
|
const CAMERA_PROPERTIES = {
|
|
1125
1230
|
focus: {
|
|
@@ -1646,92 +1751,6 @@ function parseBoardDocument(input) {
|
|
|
1646
1751
|
} : result;
|
|
1647
1752
|
}
|
|
1648
1753
|
//#endregion
|
|
1649
|
-
//#region ../protocol/dist/board-url.js
|
|
1650
|
-
const BOARD_REMOTE_URL_MAX_LENGTH = 4096;
|
|
1651
|
-
function parseIpv4(host) {
|
|
1652
|
-
const parts = host.split(".").map(Number);
|
|
1653
|
-
return parts.length === 4 && parts.every((part) => Number.isInteger(part) && part >= 0 && part <= 255) ? parts : null;
|
|
1654
|
-
}
|
|
1655
|
-
function isBlockedIpv4(host) {
|
|
1656
|
-
const parts = parseIpv4(host);
|
|
1657
|
-
if (!parts) return false;
|
|
1658
|
-
const [first, second, third] = parts;
|
|
1659
|
-
if (first === 0 || first === 10 || first === 127) return true;
|
|
1660
|
-
if (first === 100 && second >= 64 && second <= 127) return true;
|
|
1661
|
-
if (first === 169 && second === 254) return true;
|
|
1662
|
-
if (first === 172 && second >= 16 && second <= 31) return true;
|
|
1663
|
-
if (first === 192 && second === 168) return true;
|
|
1664
|
-
if (first === 192 && second === 0 && (third === 0 || third === 2)) return true;
|
|
1665
|
-
if (first === 192 && second === 88 && third === 99) return true;
|
|
1666
|
-
if (first === 198 && (second === 18 || second === 19)) return true;
|
|
1667
|
-
if (first === 198 && second === 51 && third === 100) return true;
|
|
1668
|
-
if (first === 203 && second === 0 && third === 113) return true;
|
|
1669
|
-
return first >= 224;
|
|
1670
|
-
}
|
|
1671
|
-
function expandIpv6(host) {
|
|
1672
|
-
const [head, tail, extra] = host.toLowerCase().split("::");
|
|
1673
|
-
if (extra !== void 0) return null;
|
|
1674
|
-
const headParts = head ? head.split(":").filter(Boolean) : [];
|
|
1675
|
-
const tailParts = tail ? tail.split(":").filter(Boolean) : [];
|
|
1676
|
-
const missing = 8 - headParts.length - tailParts.length;
|
|
1677
|
-
if (missing < 0 || tail === void 0 && missing !== 0) return null;
|
|
1678
|
-
const parts = [
|
|
1679
|
-
...headParts,
|
|
1680
|
-
...Array.from({ length: missing }, () => "0"),
|
|
1681
|
-
...tailParts
|
|
1682
|
-
];
|
|
1683
|
-
if (parts.length !== 8 || parts.some((part) => !/^[0-9a-f]{1,4}$/.test(part))) return null;
|
|
1684
|
-
return parts.map((part) => part.padStart(4, "0"));
|
|
1685
|
-
}
|
|
1686
|
-
function isBlockedIpv6(host) {
|
|
1687
|
-
const parts = expandIpv6(host);
|
|
1688
|
-
if (!parts) return true;
|
|
1689
|
-
if (parts.every((part) => part === "0000")) return true;
|
|
1690
|
-
if (parts.slice(0, 7).every((part) => part === "0000") && parts[7] === "0001") return true;
|
|
1691
|
-
if (parts.slice(0, 5).every((part) => part === "0000") && parts[5] === "ffff") {
|
|
1692
|
-
const high = Number.parseInt(parts[6] ?? "0", 16);
|
|
1693
|
-
const low = Number.parseInt(parts[7] ?? "0", 16);
|
|
1694
|
-
return isBlockedIpv4(`${high >> 8}.${high & 255}.${low >> 8}.${low & 255}`);
|
|
1695
|
-
}
|
|
1696
|
-
if (parts.slice(0, 6).every((part) => part === "0000")) return true;
|
|
1697
|
-
const first = Number.parseInt(parts[0] ?? "0", 16);
|
|
1698
|
-
if ((first & 65024) === 64512) return true;
|
|
1699
|
-
if ((first & 65472) === 65152 || (first & 65472) === 65216) return true;
|
|
1700
|
-
if ((first & 65280) === 65280) return true;
|
|
1701
|
-
return parts[0] === "2001" && parts[1] === "0db8";
|
|
1702
|
-
}
|
|
1703
|
-
function isPublicBoardRemoteAddress(value) {
|
|
1704
|
-
const address = value.toLowerCase().replace(/^\[|\]$/g, "").replace(/\.$/, "");
|
|
1705
|
-
if (parseIpv4(address)) return !isBlockedIpv4(address);
|
|
1706
|
-
return address.includes(":") && !isBlockedIpv6(address);
|
|
1707
|
-
}
|
|
1708
|
-
function isBlockedHost(hostname) {
|
|
1709
|
-
const host = hostname.toLowerCase().replace(/^\[|\]$/g, "").replace(/\.$/, "");
|
|
1710
|
-
if (host === "localhost" || host.endsWith(".localhost") || host.endsWith(".local") || host.endsWith(".internal")) return true;
|
|
1711
|
-
if (parseIpv4(host) || host.includes(":")) return !isPublicBoardRemoteAddress(host);
|
|
1712
|
-
return false;
|
|
1713
|
-
}
|
|
1714
|
-
/**
|
|
1715
|
-
* Normalize a browser-loadable public HTTP(S) URL. This blocks explicit local
|
|
1716
|
-
* addresses; any future server-side fetcher must additionally validate DNS
|
|
1717
|
-
* resolution to defend against rebinding.
|
|
1718
|
-
*/
|
|
1719
|
-
function normalizeBoardRemoteUrl(value) {
|
|
1720
|
-
if (typeof value !== "string") return void 0;
|
|
1721
|
-
const input = value.trim();
|
|
1722
|
-
if (!input || input.length > 4096) return void 0;
|
|
1723
|
-
try {
|
|
1724
|
-
const url = new URL(input);
|
|
1725
|
-
if (url.protocol !== "https:" && url.protocol !== "http:") return void 0;
|
|
1726
|
-
if (url.username || url.password || isBlockedHost(url.hostname)) return;
|
|
1727
|
-
const normalized = url.toString();
|
|
1728
|
-
return normalized.length <= 4096 ? normalized : void 0;
|
|
1729
|
-
} catch {
|
|
1730
|
-
return;
|
|
1731
|
-
}
|
|
1732
|
-
}
|
|
1733
|
-
z.string().max(BOARD_REMOTE_URL_MAX_LENGTH).refine((value) => normalizeBoardRemoteUrl(value) !== void 0, { message: "URL must be a public HTTP(S) URL without credentials" });
|
|
1734
|
-
//#endregion
|
|
1735
1754
|
//#region ../protocol/dist/turn-origin.js
|
|
1736
1755
|
const record$2 = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
1737
1756
|
const normalizeSessionTurnOrigin = (value) => {
|
|
@@ -2030,6 +2049,102 @@ function compileDisplayActions(actions, width, height) {
|
|
|
2030
2049
|
if (events.length === 0) throw new DisplayActionError("no action touches the display");
|
|
2031
2050
|
return events;
|
|
2032
2051
|
}
|
|
2052
|
+
z.object({
|
|
2053
|
+
urls: z.union([z.string().min(1).max(512), z.array(z.string().min(1).max(512)).min(1).max(16)]),
|
|
2054
|
+
username: z.string().max(512).optional(),
|
|
2055
|
+
credential: z.string().max(512).optional()
|
|
2056
|
+
});
|
|
2057
|
+
const sessionDescriptionSchema = z.object({
|
|
2058
|
+
type: z.enum(["offer", "answer"]),
|
|
2059
|
+
sdp: z.string().min(1).max(65536)
|
|
2060
|
+
});
|
|
2061
|
+
const APP_RTC_DELIVERIES = ["reliable", "latest"];
|
|
2062
|
+
const APP_RTC_PUBLICATION_KINDS = [
|
|
2063
|
+
"data",
|
|
2064
|
+
"audio",
|
|
2065
|
+
"video"
|
|
2066
|
+
];
|
|
2067
|
+
const APP_RTC_NAME_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,63}$/;
|
|
2068
|
+
const isAppRtcName = (value) => typeof value === "string" && APP_RTC_NAME_PATTERN.test(value) && !value.toLowerCase().startsWith("cohub.");
|
|
2069
|
+
const APP_RTC_LIMITS = {
|
|
2070
|
+
channelsPerMember: 16,
|
|
2071
|
+
tracksPerMember: 4,
|
|
2072
|
+
dataSubscriptions: 512,
|
|
2073
|
+
mediaSubscriptions: 16,
|
|
2074
|
+
messageBytes: 65536,
|
|
2075
|
+
operationsPerSecond: 10,
|
|
2076
|
+
opensPerSecond: 2,
|
|
2077
|
+
batch: 64
|
|
2078
|
+
};
|
|
2079
|
+
const APP_RTC_QUALITIES = [
|
|
2080
|
+
"high",
|
|
2081
|
+
"medium",
|
|
2082
|
+
"low"
|
|
2083
|
+
];
|
|
2084
|
+
const APP_RTC_SIMULCAST_LAYERS = [
|
|
2085
|
+
{
|
|
2086
|
+
rid: "f",
|
|
2087
|
+
quality: "high",
|
|
2088
|
+
scaleResolutionDownBy: 1
|
|
2089
|
+
},
|
|
2090
|
+
{
|
|
2091
|
+
rid: "h",
|
|
2092
|
+
quality: "medium",
|
|
2093
|
+
scaleResolutionDownBy: 2
|
|
2094
|
+
},
|
|
2095
|
+
{
|
|
2096
|
+
rid: "q",
|
|
2097
|
+
quality: "low",
|
|
2098
|
+
scaleResolutionDownBy: 4
|
|
2099
|
+
}
|
|
2100
|
+
];
|
|
2101
|
+
z.object({
|
|
2102
|
+
id: z.string().min(1).max(32),
|
|
2103
|
+
name: z.string().regex(APP_RTC_NAME_PATTERN),
|
|
2104
|
+
kind: z.enum(APP_RTC_PUBLICATION_KINDS),
|
|
2105
|
+
delivery: z.enum(APP_RTC_DELIVERIES).optional(),
|
|
2106
|
+
simulcast: z.boolean().optional()
|
|
2107
|
+
});
|
|
2108
|
+
const nameSchema = z.string().refine(isAppRtcName, "invalid name");
|
|
2109
|
+
const participantIdSchema = z.string().uuid();
|
|
2110
|
+
z.object({
|
|
2111
|
+
ticket: z.string().min(1).max(4096),
|
|
2112
|
+
participantId: participantIdSchema
|
|
2113
|
+
});
|
|
2114
|
+
z.object({ answer: sessionDescriptionSchema.refine((value) => value.type === "answer", "expected an answer") });
|
|
2115
|
+
z.union([z.object({ channels: z.array(z.object({
|
|
2116
|
+
name: nameSchema,
|
|
2117
|
+
delivery: z.enum(APP_RTC_DELIVERIES)
|
|
2118
|
+
})).min(1).max(APP_RTC_LIMITS.channelsPerMember) }), z.object({
|
|
2119
|
+
offer: sessionDescriptionSchema.refine((value) => value.type === "offer", "expected an offer"),
|
|
2120
|
+
tracks: z.array(z.object({
|
|
2121
|
+
name: nameSchema,
|
|
2122
|
+
kind: z.enum(["audio", "video"]),
|
|
2123
|
+
mid: z.string().min(1).max(32),
|
|
2124
|
+
simulcast: z.boolean().optional()
|
|
2125
|
+
})).min(1).max(APP_RTC_LIMITS.tracksPerMember)
|
|
2126
|
+
})]);
|
|
2127
|
+
z.object({
|
|
2128
|
+
ids: z.array(z.string().min(1).max(32)).min(1).max(APP_RTC_LIMITS.channelsPerMember + APP_RTC_LIMITS.tracksPerMember),
|
|
2129
|
+
offer: sessionDescriptionSchema.refine((value) => value.type === "offer", "expected an offer").optional()
|
|
2130
|
+
});
|
|
2131
|
+
const subscriptionRefSchema = z.object({
|
|
2132
|
+
participantId: participantIdSchema,
|
|
2133
|
+
publicationId: z.string().min(1).max(32)
|
|
2134
|
+
});
|
|
2135
|
+
const qualitySchema = z.enum(APP_RTC_QUALITIES);
|
|
2136
|
+
z.object({
|
|
2137
|
+
kind: z.enum(["data", "media"]),
|
|
2138
|
+
items: z.array(subscriptionRefSchema.extend({ quality: qualitySchema.optional() })).min(1).max(APP_RTC_LIMITS.batch)
|
|
2139
|
+
});
|
|
2140
|
+
z.object({ items: z.array(subscriptionRefSchema.extend({
|
|
2141
|
+
mid: z.string().min(1).max(32),
|
|
2142
|
+
quality: qualitySchema.optional()
|
|
2143
|
+
})).min(1).max(APP_RTC_LIMITS.mediaSubscriptions) });
|
|
2144
|
+
z.object({
|
|
2145
|
+
items: z.array(subscriptionRefSchema).min(1).max(APP_RTC_LIMITS.batch),
|
|
2146
|
+
offer: sessionDescriptionSchema.refine((value) => value.type === "offer", "expected an offer").optional()
|
|
2147
|
+
});
|
|
2033
2148
|
//#endregion
|
|
2034
2149
|
//#region ../protocol/dist/generation/catalog.js
|
|
2035
2150
|
function isGenerationModelHidden(model) {
|
|
@@ -3727,6 +3842,11 @@ const APP_APPEARANCE_TOKENS = [
|
|
|
3727
3842
|
"brand-ring",
|
|
3728
3843
|
"brand-contrast-fg",
|
|
3729
3844
|
"error-fg",
|
|
3845
|
+
"success",
|
|
3846
|
+
"warning",
|
|
3847
|
+
"error",
|
|
3848
|
+
"info",
|
|
3849
|
+
"status-running",
|
|
3730
3850
|
"selection-bg",
|
|
3731
3851
|
"overlay-scrim",
|
|
3732
3852
|
"shadow-subtle",
|
|
@@ -3821,6 +3941,33 @@ const buildAppEmbedShellChanged = (input) => ({
|
|
|
3821
3941
|
...input
|
|
3822
3942
|
});
|
|
3823
3943
|
//#endregion
|
|
3944
|
+
//#region ../protocol/dist/app-devices.js
|
|
3945
|
+
const APP_DEVICES = [
|
|
3946
|
+
"camera",
|
|
3947
|
+
"microphone",
|
|
3948
|
+
"screen"
|
|
3949
|
+
];
|
|
3950
|
+
const isAppDevice = (value) => typeof value === "string" && APP_DEVICES.includes(value);
|
|
3951
|
+
function parseAppDevices(value) {
|
|
3952
|
+
const items = typeof value === "string" ? value.split(/[\s,]+/) : Array.isArray(value) ? value : [];
|
|
3953
|
+
const wanted = new Set(items.map((item) => typeof item === "string" ? item.trim().toLowerCase() : item).filter(isAppDevice));
|
|
3954
|
+
return APP_DEVICES.filter((device) => wanted.has(device));
|
|
3955
|
+
}
|
|
3956
|
+
const APP_DEVICES_REQUEST_TYPE = "cohub.app.devices.request";
|
|
3957
|
+
const APP_DEVICES_RESULT_TYPE = "cohub.app.devices.result";
|
|
3958
|
+
function parseAppDevicesResult(value) {
|
|
3959
|
+
if (!value || typeof value !== "object") return null;
|
|
3960
|
+
const record = value;
|
|
3961
|
+
if (record.type !== "cohub.app.devices.result" || typeof record.requestId !== "string") return null;
|
|
3962
|
+
const status = record.status;
|
|
3963
|
+
if (status !== "granted" && status !== "denied" && status !== "unavailable") return null;
|
|
3964
|
+
return {
|
|
3965
|
+
type: APP_DEVICES_RESULT_TYPE,
|
|
3966
|
+
requestId: record.requestId,
|
|
3967
|
+
status
|
|
3968
|
+
};
|
|
3969
|
+
}
|
|
3970
|
+
//#endregion
|
|
3824
3971
|
//#region src/apis/billing.ts
|
|
3825
3972
|
var BillingApi = class {
|
|
3826
3973
|
transport;
|
|
@@ -3903,6 +4050,960 @@ var BillingApi = class {
|
|
|
3903
4050
|
}
|
|
3904
4051
|
};
|
|
3905
4052
|
//#endregion
|
|
4053
|
+
//#region src/apis/app-room-rtc.ts
|
|
4054
|
+
var AppRtcError = class extends Error {
|
|
4055
|
+
code;
|
|
4056
|
+
constructor(code, message) {
|
|
4057
|
+
super(message);
|
|
4058
|
+
this.code = code;
|
|
4059
|
+
this.name = "AppRtcError";
|
|
4060
|
+
}
|
|
4061
|
+
};
|
|
4062
|
+
const SERVER_CODES = /* @__PURE__ */ new Set([
|
|
4063
|
+
"RTC_UNAVAILABLE",
|
|
4064
|
+
"RTC_FORBIDDEN",
|
|
4065
|
+
"RTC_LIMIT_EXCEEDED",
|
|
4066
|
+
"RTC_RATE_LIMITED",
|
|
4067
|
+
"ROOM_NOT_FOUND",
|
|
4068
|
+
"ROOM_EXPIRED"
|
|
4069
|
+
]);
|
|
4070
|
+
const toAppRtcError = (error) => {
|
|
4071
|
+
if (error instanceof AppRtcError) return error;
|
|
4072
|
+
if (error instanceof HttpError && SERVER_CODES.has(error.code)) return new AppRtcError(error.code, error.message);
|
|
4073
|
+
return new AppRtcError("RTC_FAILED", error instanceof Error ? error.message : String(error));
|
|
4074
|
+
};
|
|
4075
|
+
const HEADER_BYTES = 5;
|
|
4076
|
+
const FLAG_BINARY = 1;
|
|
4077
|
+
const encoder = new TextEncoder();
|
|
4078
|
+
const decoder = new TextDecoder();
|
|
4079
|
+
function encodeAppRtcFrame(data, sequence) {
|
|
4080
|
+
let payload;
|
|
4081
|
+
let flags = 0;
|
|
4082
|
+
if (data instanceof ArrayBuffer) {
|
|
4083
|
+
payload = new Uint8Array(data);
|
|
4084
|
+
flags = FLAG_BINARY;
|
|
4085
|
+
} else if (ArrayBuffer.isView(data)) {
|
|
4086
|
+
payload = new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
|
|
4087
|
+
flags = FLAG_BINARY;
|
|
4088
|
+
} else {
|
|
4089
|
+
const json = JSON.stringify(data);
|
|
4090
|
+
if (json === void 0) throw new TypeError("RTC message is not serializable");
|
|
4091
|
+
payload = encoder.encode(json);
|
|
4092
|
+
}
|
|
4093
|
+
if (payload.byteLength > APP_RTC_LIMITS.messageBytes) throw new RangeError(`RTC message exceeds ${APP_RTC_LIMITS.messageBytes} bytes`);
|
|
4094
|
+
const frame = new Uint8Array(HEADER_BYTES + payload.byteLength);
|
|
4095
|
+
frame[0] = flags;
|
|
4096
|
+
new DataView(frame.buffer).setUint32(1, sequence >>> 0);
|
|
4097
|
+
frame.set(payload, HEADER_BYTES);
|
|
4098
|
+
return frame;
|
|
4099
|
+
}
|
|
4100
|
+
function decodeAppRtcFrame(frame) {
|
|
4101
|
+
if (frame.byteLength < HEADER_BYTES) return null;
|
|
4102
|
+
const view = new DataView(frame);
|
|
4103
|
+
const flags = view.getUint8(0);
|
|
4104
|
+
const sequence = view.getUint32(1);
|
|
4105
|
+
const payload = new Uint8Array(frame, HEADER_BYTES);
|
|
4106
|
+
if (flags & FLAG_BINARY) return {
|
|
4107
|
+
data: payload.slice().buffer,
|
|
4108
|
+
sequence
|
|
4109
|
+
};
|
|
4110
|
+
try {
|
|
4111
|
+
return {
|
|
4112
|
+
data: JSON.parse(decoder.decode(payload)),
|
|
4113
|
+
sequence
|
|
4114
|
+
};
|
|
4115
|
+
} catch {
|
|
4116
|
+
return null;
|
|
4117
|
+
}
|
|
4118
|
+
}
|
|
4119
|
+
const isNewerSequence = (next, last) => last === null || next !== last && next - last >>> 0 < 2147483648;
|
|
4120
|
+
const publicationsKey = (members) => members.map((member) => `${member.participantId}:${member.publications?.map((item) => item.id).join(",") ?? ""}`).join(";");
|
|
4121
|
+
const GATHER_TIMEOUT_MS = 2e3;
|
|
4122
|
+
const CONNECT_TIMEOUT_MS = 15e3;
|
|
4123
|
+
const PENDING_RELIABLE_FRAMES = 256;
|
|
4124
|
+
const LATEST_BACKLOG_BYTES = 65536;
|
|
4125
|
+
const RECONNECT_DELAYS_MS = [
|
|
4126
|
+
500,
|
|
4127
|
+
1e3,
|
|
4128
|
+
2e3,
|
|
4129
|
+
5e3,
|
|
4130
|
+
1e4,
|
|
4131
|
+
3e4
|
|
4132
|
+
];
|
|
4133
|
+
const RATE_LIMIT_RETRY_MS = 1e3;
|
|
4134
|
+
var ConnectionReplacedError = class extends Error {
|
|
4135
|
+
constructor() {
|
|
4136
|
+
super("RTC connection was replaced");
|
|
4137
|
+
this.name = "ConnectionReplacedError";
|
|
4138
|
+
}
|
|
4139
|
+
};
|
|
4140
|
+
const policyOf = (delivery) => delivery === "latest" ? {
|
|
4141
|
+
ordered: false,
|
|
4142
|
+
maxRetransmits: 0
|
|
4143
|
+
} : { ordered: true };
|
|
4144
|
+
const isSessionLost = (error) => error instanceof HttpError && (error.code === "RTC_STALE" || error.code === "RTC_SESSION_NOT_FOUND");
|
|
4145
|
+
const isRoomGone = (error) => error instanceof HttpError && (error.code === "ROOM_NOT_FOUND" || error.code === "ROOM_EXPIRED");
|
|
4146
|
+
const chunk = (items, size) => {
|
|
4147
|
+
const chunks = [];
|
|
4148
|
+
for (let index = 0; index < items.length; index += size) chunks.push(items.slice(index, index + size));
|
|
4149
|
+
return chunks;
|
|
4150
|
+
};
|
|
4151
|
+
const mediaKey = (participantId, publicationId) => `${participantId}:${publicationId}`;
|
|
4152
|
+
var AppRtcChannel = class {
|
|
4153
|
+
rtc;
|
|
4154
|
+
name;
|
|
4155
|
+
delivery;
|
|
4156
|
+
handlers = /* @__PURE__ */ new Set();
|
|
4157
|
+
peerHandlers = /* @__PURE__ */ new Set();
|
|
4158
|
+
openPeers = /* @__PURE__ */ new Set();
|
|
4159
|
+
publication = null;
|
|
4160
|
+
pending = [];
|
|
4161
|
+
sequence = 0;
|
|
4162
|
+
closed = false;
|
|
4163
|
+
constructor(rtc, name, delivery) {
|
|
4164
|
+
this.rtc = rtc;
|
|
4165
|
+
this.name = name;
|
|
4166
|
+
this.delivery = delivery;
|
|
4167
|
+
}
|
|
4168
|
+
get peers() {
|
|
4169
|
+
return [...this.openPeers];
|
|
4170
|
+
}
|
|
4171
|
+
send(data) {
|
|
4172
|
+
if (this.closed || this.rtc.state === "closed") return false;
|
|
4173
|
+
this.sequence = this.sequence + 1 >>> 0;
|
|
4174
|
+
const frame = encodeAppRtcFrame(data, this.sequence);
|
|
4175
|
+
const channel = this.publication?.channel;
|
|
4176
|
+
if (channel?.readyState === "open") return this.write(channel, frame);
|
|
4177
|
+
if (this.delivery === "latest") this.pending = [frame];
|
|
4178
|
+
else if (this.pending.length >= PENDING_RELIABLE_FRAMES) return false;
|
|
4179
|
+
else this.pending.push(frame);
|
|
4180
|
+
return true;
|
|
4181
|
+
}
|
|
4182
|
+
onMessage(handler) {
|
|
4183
|
+
this.handlers.add(handler);
|
|
4184
|
+
return () => this.handlers.delete(handler);
|
|
4185
|
+
}
|
|
4186
|
+
/** Reports when this client starts or stops receiving a peer's channel. */
|
|
4187
|
+
onPeer(handler) {
|
|
4188
|
+
this.peerHandlers.add(handler);
|
|
4189
|
+
return () => this.peerHandlers.delete(handler);
|
|
4190
|
+
}
|
|
4191
|
+
close() {
|
|
4192
|
+
if (this.closed) return;
|
|
4193
|
+
this.closed = true;
|
|
4194
|
+
this.handlers.clear();
|
|
4195
|
+
this.peerHandlers.clear();
|
|
4196
|
+
this.openPeers.clear();
|
|
4197
|
+
this.pending = [];
|
|
4198
|
+
this.rtc.closeChannel(this);
|
|
4199
|
+
}
|
|
4200
|
+
/** @internal */
|
|
4201
|
+
get isOpen() {
|
|
4202
|
+
return !this.closed;
|
|
4203
|
+
}
|
|
4204
|
+
/** @internal */
|
|
4205
|
+
get publicationId() {
|
|
4206
|
+
return this.publication?.id ?? null;
|
|
4207
|
+
}
|
|
4208
|
+
/** @internal */
|
|
4209
|
+
attach(id, channel) {
|
|
4210
|
+
this.publication = {
|
|
4211
|
+
id,
|
|
4212
|
+
channel
|
|
4213
|
+
};
|
|
4214
|
+
channel.binaryType = "arraybuffer";
|
|
4215
|
+
const flush = () => {
|
|
4216
|
+
const frames = this.pending;
|
|
4217
|
+
this.pending = [];
|
|
4218
|
+
for (const frame of frames) this.write(channel, frame);
|
|
4219
|
+
};
|
|
4220
|
+
if (channel.readyState === "open") flush();
|
|
4221
|
+
else channel.addEventListener("open", flush, { once: true });
|
|
4222
|
+
}
|
|
4223
|
+
/** @internal */
|
|
4224
|
+
detach() {
|
|
4225
|
+
this.publication = null;
|
|
4226
|
+
}
|
|
4227
|
+
/** @internal */
|
|
4228
|
+
fail() {
|
|
4229
|
+
this.close();
|
|
4230
|
+
}
|
|
4231
|
+
/** @internal */
|
|
4232
|
+
deliver(from, data) {
|
|
4233
|
+
for (const handler of this.handlers) handler({
|
|
4234
|
+
from,
|
|
4235
|
+
data
|
|
4236
|
+
});
|
|
4237
|
+
}
|
|
4238
|
+
/** @internal */
|
|
4239
|
+
setPeer(participantId, open) {
|
|
4240
|
+
if (open ? this.openPeers.has(participantId) : !this.openPeers.delete(participantId)) return;
|
|
4241
|
+
if (open) this.openPeers.add(participantId);
|
|
4242
|
+
for (const handler of this.peerHandlers) handler({
|
|
4243
|
+
participantId,
|
|
4244
|
+
open
|
|
4245
|
+
});
|
|
4246
|
+
}
|
|
4247
|
+
write(channel, frame) {
|
|
4248
|
+
if (this.delivery === "latest" && channel.bufferedAmount > LATEST_BACKLOG_BYTES) return false;
|
|
4249
|
+
try {
|
|
4250
|
+
channel.send(frame);
|
|
4251
|
+
return true;
|
|
4252
|
+
} catch {
|
|
4253
|
+
return false;
|
|
4254
|
+
}
|
|
4255
|
+
}
|
|
4256
|
+
};
|
|
4257
|
+
var AppRoomRtc = class {
|
|
4258
|
+
host;
|
|
4259
|
+
_state = "connecting";
|
|
4260
|
+
pc = null;
|
|
4261
|
+
sessionId = null;
|
|
4262
|
+
queue = Promise.resolve();
|
|
4263
|
+
reconcileQueued = false;
|
|
4264
|
+
reconcileFailures = 0;
|
|
4265
|
+
reconnectAttempt = 0;
|
|
4266
|
+
reconnectTimer = null;
|
|
4267
|
+
generation = 0;
|
|
4268
|
+
filter = null;
|
|
4269
|
+
channels = /* @__PURE__ */ new Map();
|
|
4270
|
+
channelsToPublish = /* @__PURE__ */ new Set();
|
|
4271
|
+
remoteData = /* @__PURE__ */ new Map();
|
|
4272
|
+
localTracks = /* @__PURE__ */ new Map();
|
|
4273
|
+
remoteMedia = /* @__PURE__ */ new Map();
|
|
4274
|
+
mediaByMid = /* @__PURE__ */ new Map();
|
|
4275
|
+
stateHandlers = /* @__PURE__ */ new Set();
|
|
4276
|
+
trackHandlers = /* @__PURE__ */ new Set();
|
|
4277
|
+
offMembers;
|
|
4278
|
+
constructor(host) {
|
|
4279
|
+
this.host = host;
|
|
4280
|
+
let published = "";
|
|
4281
|
+
this.offMembers = host.onMembersChanged((members) => {
|
|
4282
|
+
const next = publicationsKey(members);
|
|
4283
|
+
if (next === published) return;
|
|
4284
|
+
published = next;
|
|
4285
|
+
this.scheduleReconcile();
|
|
4286
|
+
});
|
|
4287
|
+
}
|
|
4288
|
+
get state() {
|
|
4289
|
+
return this._state;
|
|
4290
|
+
}
|
|
4291
|
+
get tracks() {
|
|
4292
|
+
const tracks = [];
|
|
4293
|
+
for (const [key, item] of this.remoteMedia) {
|
|
4294
|
+
if (!item.track) continue;
|
|
4295
|
+
tracks.push({
|
|
4296
|
+
participantId: item.participantId,
|
|
4297
|
+
name: item.name,
|
|
4298
|
+
kind: item.kind,
|
|
4299
|
+
track: item.track,
|
|
4300
|
+
quality: item.quality ?? null,
|
|
4301
|
+
setQuality: (quality) => this.setQuality(key, quality)
|
|
4302
|
+
});
|
|
4303
|
+
}
|
|
4304
|
+
return tracks;
|
|
4305
|
+
}
|
|
4306
|
+
get candidates() {
|
|
4307
|
+
return this.mediaCandidates().map(({ candidate }) => candidate);
|
|
4308
|
+
}
|
|
4309
|
+
onStateChange(handler) {
|
|
4310
|
+
this.stateHandlers.add(handler);
|
|
4311
|
+
return () => this.stateHandlers.delete(handler);
|
|
4312
|
+
}
|
|
4313
|
+
onTracksChanged(handler) {
|
|
4314
|
+
this.trackHandlers.add(handler);
|
|
4315
|
+
return () => this.trackHandlers.delete(handler);
|
|
4316
|
+
}
|
|
4317
|
+
select(filter) {
|
|
4318
|
+
this.filter = filter;
|
|
4319
|
+
this.scheduleReconcile();
|
|
4320
|
+
}
|
|
4321
|
+
channel(name, options = {}) {
|
|
4322
|
+
if (!isAppRtcName(name)) throw new AppRtcError("RTC_FAILED", `invalid RTC channel name: ${name}`);
|
|
4323
|
+
const delivery = options.delivery ?? "reliable";
|
|
4324
|
+
const existing = this.channels.get(name);
|
|
4325
|
+
if (existing?.isOpen) {
|
|
4326
|
+
if (existing.delivery !== delivery) throw new AppRtcError("RTC_FAILED", `RTC channel ${name} is already open with ${existing.delivery} delivery`);
|
|
4327
|
+
return existing;
|
|
4328
|
+
}
|
|
4329
|
+
if (this._state === "closed") throw new AppRtcError("RTC_CLOSED", "RTC connection is closed");
|
|
4330
|
+
const channel = new AppRtcChannel(this, name, delivery);
|
|
4331
|
+
this.channels.set(name, channel);
|
|
4332
|
+
this.publishChannel(channel);
|
|
4333
|
+
this.scheduleReconcile();
|
|
4334
|
+
return channel;
|
|
4335
|
+
}
|
|
4336
|
+
async publish(tracks) {
|
|
4337
|
+
if (this._state === "closed") throw new AppRtcError("RTC_CLOSED", "RTC connection is closed");
|
|
4338
|
+
const inputs = Object.entries(tracks);
|
|
4339
|
+
for (const [name] of inputs) {
|
|
4340
|
+
if (!isAppRtcName(name)) throw new AppRtcError("RTC_FAILED", `invalid RTC track name: ${name}`);
|
|
4341
|
+
if (this.localTracks.has(name)) throw new AppRtcError("RTC_FAILED", `RTC track ${name} is already published`);
|
|
4342
|
+
}
|
|
4343
|
+
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`);
|
|
4344
|
+
const entries = inputs.map(([name, input]) => {
|
|
4345
|
+
const { track, simulcast } = "track" in input ? input : {
|
|
4346
|
+
track: input,
|
|
4347
|
+
simulcast: false
|
|
4348
|
+
};
|
|
4349
|
+
const kind = track.kind === "audio" ? "audio" : "video";
|
|
4350
|
+
return this.localEntry(name, kind, track, simulcast === true && kind === "video");
|
|
4351
|
+
});
|
|
4352
|
+
for (const entry of entries) this.localTracks.set(entry.name, entry);
|
|
4353
|
+
try {
|
|
4354
|
+
await this.enqueue(() => this.publishTracks(entries.map((entry) => entry.name)));
|
|
4355
|
+
} catch (error) {
|
|
4356
|
+
if (!(error instanceof ConnectionReplacedError)) {
|
|
4357
|
+
for (const entry of entries) this.dropLocal(entry);
|
|
4358
|
+
throw toAppRtcError(error);
|
|
4359
|
+
}
|
|
4360
|
+
}
|
|
4361
|
+
return Object.fromEntries(entries.map((entry) => [entry.name, this.localHandle(entry)]));
|
|
4362
|
+
}
|
|
4363
|
+
async stats() {
|
|
4364
|
+
return this.pc ? readRtcTransportStats(await this.pc.getStats()) : {
|
|
4365
|
+
path: null,
|
|
4366
|
+
relayProtocol: null,
|
|
4367
|
+
rttMs: null
|
|
4368
|
+
};
|
|
4369
|
+
}
|
|
4370
|
+
close() {
|
|
4371
|
+
if (this._state === "closed") return;
|
|
4372
|
+
this.setState("closed");
|
|
4373
|
+
this.offMembers();
|
|
4374
|
+
if (this.reconnectTimer) clearTimeout(this.reconnectTimer);
|
|
4375
|
+
for (const channel of this.channels.values()) channel.close();
|
|
4376
|
+
for (const entry of this.localTracks.values()) entry.track.removeEventListener("ended", entry.onEnded);
|
|
4377
|
+
this.teardown(true);
|
|
4378
|
+
}
|
|
4379
|
+
/** @internal */
|
|
4380
|
+
async start() {
|
|
4381
|
+
try {
|
|
4382
|
+
await this.connect();
|
|
4383
|
+
} catch (error) {
|
|
4384
|
+
throw toAppRtcError(error);
|
|
4385
|
+
}
|
|
4386
|
+
}
|
|
4387
|
+
/** @internal */
|
|
4388
|
+
async verify() {
|
|
4389
|
+
if (this._state === "closed" || !this.sessionId) return;
|
|
4390
|
+
try {
|
|
4391
|
+
await this.request("GET", `/${this.sessionId}`);
|
|
4392
|
+
} catch (error) {
|
|
4393
|
+
if (isSessionLost(error)) this.reconnect();
|
|
4394
|
+
}
|
|
4395
|
+
}
|
|
4396
|
+
/** @internal */
|
|
4397
|
+
closeChannel(channel) {
|
|
4398
|
+
if (this.channels.get(channel.name) === channel) this.channels.delete(channel.name);
|
|
4399
|
+
const id = channel.publicationId;
|
|
4400
|
+
channel.detach();
|
|
4401
|
+
if (id) this.enqueue(() => this.unpublishData([id])).catch(() => void 0);
|
|
4402
|
+
this.scheduleReconcile();
|
|
4403
|
+
}
|
|
4404
|
+
localEntry(name, kind, track, simulcast) {
|
|
4405
|
+
const entry = {
|
|
4406
|
+
name,
|
|
4407
|
+
kind,
|
|
4408
|
+
track,
|
|
4409
|
+
simulcast,
|
|
4410
|
+
transceiver: null,
|
|
4411
|
+
id: null,
|
|
4412
|
+
ended: /* @__PURE__ */ new Set(),
|
|
4413
|
+
onEnded: () => void 0
|
|
4414
|
+
};
|
|
4415
|
+
entry.onEnded = () => {
|
|
4416
|
+
if (this.localTracks.get(name) !== entry) return;
|
|
4417
|
+
for (const handler of entry.ended) handler();
|
|
4418
|
+
this.stopLocal(entry).catch(() => void 0);
|
|
4419
|
+
};
|
|
4420
|
+
track.addEventListener("ended", entry.onEnded);
|
|
4421
|
+
return entry;
|
|
4422
|
+
}
|
|
4423
|
+
localHandle(entry) {
|
|
4424
|
+
return {
|
|
4425
|
+
name: entry.name,
|
|
4426
|
+
kind: entry.kind,
|
|
4427
|
+
simulcast: entry.simulcast,
|
|
4428
|
+
get track() {
|
|
4429
|
+
return entry.track;
|
|
4430
|
+
},
|
|
4431
|
+
replace: async (track) => {
|
|
4432
|
+
if (track.kind !== entry.kind) throw new AppRtcError("RTC_FAILED", `replace ${entry.name} with another ${entry.kind} track`);
|
|
4433
|
+
if (this.localTracks.get(entry.name) !== entry) throw new AppRtcError("RTC_CLOSED", `RTC track ${entry.name} has stopped`);
|
|
4434
|
+
entry.track.removeEventListener("ended", entry.onEnded);
|
|
4435
|
+
entry.track = track;
|
|
4436
|
+
track.addEventListener("ended", entry.onEnded);
|
|
4437
|
+
await entry.transceiver?.sender.replaceTrack(track);
|
|
4438
|
+
},
|
|
4439
|
+
onEnded: (handler) => {
|
|
4440
|
+
entry.ended.add(handler);
|
|
4441
|
+
return () => entry.ended.delete(handler);
|
|
4442
|
+
},
|
|
4443
|
+
stop: () => this.stopLocal(entry)
|
|
4444
|
+
};
|
|
4445
|
+
}
|
|
4446
|
+
dropLocal(entry) {
|
|
4447
|
+
entry.track.removeEventListener("ended", entry.onEnded);
|
|
4448
|
+
if (this.localTracks.get(entry.name) === entry) this.localTracks.delete(entry.name);
|
|
4449
|
+
}
|
|
4450
|
+
stopLocal(entry) {
|
|
4451
|
+
if (this.localTracks.get(entry.name) !== entry) return Promise.resolve();
|
|
4452
|
+
this.dropLocal(entry);
|
|
4453
|
+
return this.enqueue(() => this.unpublishTrack(entry)).catch((error) => {
|
|
4454
|
+
if (error instanceof ConnectionReplacedError) return;
|
|
4455
|
+
throw toAppRtcError(error);
|
|
4456
|
+
});
|
|
4457
|
+
}
|
|
4458
|
+
async publishTracks(names) {
|
|
4459
|
+
const pc = this.requirePc();
|
|
4460
|
+
const entries = names.flatMap((name) => {
|
|
4461
|
+
const entry = this.localTracks.get(name);
|
|
4462
|
+
return entry && !entry.id ? [entry] : [];
|
|
4463
|
+
});
|
|
4464
|
+
if (entries.length === 0) return;
|
|
4465
|
+
for (const entry of entries) entry.transceiver = pc.addTransceiver(entry.track, {
|
|
4466
|
+
direction: "sendonly",
|
|
4467
|
+
...entry.simulcast ? { sendEncodings: APP_RTC_SIMULCAST_LAYERS.map(({ rid, scaleResolutionDownBy }) => ({
|
|
4468
|
+
rid,
|
|
4469
|
+
scaleResolutionDownBy
|
|
4470
|
+
})) } : {}
|
|
4471
|
+
});
|
|
4472
|
+
const offer = await this.localOffer(pc);
|
|
4473
|
+
let result;
|
|
4474
|
+
try {
|
|
4475
|
+
result = await this.request("POST", `/${this.sessionId}/publish`, {
|
|
4476
|
+
offer,
|
|
4477
|
+
tracks: entries.map((entry) => ({
|
|
4478
|
+
name: entry.name,
|
|
4479
|
+
kind: entry.kind,
|
|
4480
|
+
mid: entry.transceiver?.mid ?? "",
|
|
4481
|
+
...entry.simulcast ? { simulcast: true } : {}
|
|
4482
|
+
}))
|
|
4483
|
+
});
|
|
4484
|
+
} catch (error) {
|
|
4485
|
+
if (error instanceof ConnectionReplacedError) throw error;
|
|
4486
|
+
await this.settleOffer(pc, void 0).catch(() => void 0);
|
|
4487
|
+
for (const entry of entries) {
|
|
4488
|
+
entry.transceiver?.stop();
|
|
4489
|
+
entry.transceiver = null;
|
|
4490
|
+
}
|
|
4491
|
+
throw error;
|
|
4492
|
+
}
|
|
4493
|
+
await this.settleOffer(pc, result.answer);
|
|
4494
|
+
for (const publication of result.publications) {
|
|
4495
|
+
const entry = entries.find((candidate) => candidate.name === publication.name);
|
|
4496
|
+
if (entry) entry.id = publication.id;
|
|
4497
|
+
}
|
|
4498
|
+
}
|
|
4499
|
+
async unpublishTrack(entry) {
|
|
4500
|
+
if (!entry.id || !entry.transceiver || !this.pc) return;
|
|
4501
|
+
const id = entry.id;
|
|
4502
|
+
entry.transceiver.stop();
|
|
4503
|
+
await this.offerExchange(this.pc, (offer) => this.request("POST", `/${this.sessionId}/unpublish`, {
|
|
4504
|
+
ids: [id],
|
|
4505
|
+
offer
|
|
4506
|
+
}));
|
|
4507
|
+
entry.id = null;
|
|
4508
|
+
}
|
|
4509
|
+
publishChannel(channel) {
|
|
4510
|
+
this.channelsToPublish.add(channel);
|
|
4511
|
+
if (this.channelsToPublish.size > 1 || !this.pc) return;
|
|
4512
|
+
this.enqueue(async () => {
|
|
4513
|
+
const batch = [...this.channelsToPublish];
|
|
4514
|
+
this.channelsToPublish.clear();
|
|
4515
|
+
try {
|
|
4516
|
+
await this.publishChannels(batch);
|
|
4517
|
+
} catch (error) {
|
|
4518
|
+
if (isSessionLost(error) || error instanceof ConnectionReplacedError) throw error;
|
|
4519
|
+
if (error instanceof HttpError && error.code === "RTC_RATE_LIMITED") {
|
|
4520
|
+
setTimeout(() => {
|
|
4521
|
+
for (const item of batch) if (item.isOpen) this.publishChannel(item);
|
|
4522
|
+
}, RATE_LIMIT_RETRY_MS);
|
|
4523
|
+
return;
|
|
4524
|
+
}
|
|
4525
|
+
for (const item of batch) item.fail();
|
|
4526
|
+
console.warn("[Cohub AppRtc] failed to publish channels", error);
|
|
4527
|
+
}
|
|
4528
|
+
}).catch(() => void 0);
|
|
4529
|
+
}
|
|
4530
|
+
async connect() {
|
|
4531
|
+
const generation = ++this.generation;
|
|
4532
|
+
const opened = await this.send("POST", "", {
|
|
4533
|
+
ticket: this.host.ticket,
|
|
4534
|
+
participantId: this.host.participantId()
|
|
4535
|
+
});
|
|
4536
|
+
if (generation !== this.generation || this._state === "closed") {
|
|
4537
|
+
this.send("DELETE", `/${opened.sessionId}`).catch(() => void 0);
|
|
4538
|
+
return;
|
|
4539
|
+
}
|
|
4540
|
+
const pc = new RTCPeerConnection({
|
|
4541
|
+
iceServers: opened.iceServers,
|
|
4542
|
+
bundlePolicy: "max-bundle"
|
|
4543
|
+
});
|
|
4544
|
+
this.pc = pc;
|
|
4545
|
+
this.sessionId = opened.sessionId;
|
|
4546
|
+
pc.addEventListener("connectionstatechange", () => {
|
|
4547
|
+
if (this.pc !== pc) return;
|
|
4548
|
+
if (pc.connectionState === "connected") {
|
|
4549
|
+
this.reconnectAttempt = 0;
|
|
4550
|
+
this.setState("connected");
|
|
4551
|
+
} else if (pc.connectionState === "failed") this.reconnect();
|
|
4552
|
+
});
|
|
4553
|
+
pc.addEventListener("track", (event) => this.onTrack(event));
|
|
4554
|
+
const ready = (async () => {
|
|
4555
|
+
await this.answerOffer(opened.offer);
|
|
4556
|
+
await this.connected(pc);
|
|
4557
|
+
})();
|
|
4558
|
+
this.queue = ready.catch(() => void 0);
|
|
4559
|
+
try {
|
|
4560
|
+
await ready;
|
|
4561
|
+
} catch (error) {
|
|
4562
|
+
if (generation === this.generation) this.reconnect();
|
|
4563
|
+
throw error;
|
|
4564
|
+
}
|
|
4565
|
+
this.channelsToPublish.clear();
|
|
4566
|
+
const republish = [...this.channels.values()].filter((channel) => channel.isOpen);
|
|
4567
|
+
const tracks = [...this.localTracks.keys()];
|
|
4568
|
+
await Promise.all([republish.length ? this.enqueue(() => this.publishChannels(republish)).catch((error) => {
|
|
4569
|
+
if (error instanceof ConnectionReplacedError || isSessionLost(error)) return;
|
|
4570
|
+
for (const channel of republish) channel.fail();
|
|
4571
|
+
console.warn("[Cohub AppRtc] failed to publish channels on the new connection", error);
|
|
4572
|
+
}) : null, tracks.length ? this.enqueue(() => this.publishTracks(tracks)).catch((error) => {
|
|
4573
|
+
if (error instanceof ConnectionReplacedError || isSessionLost(error)) return;
|
|
4574
|
+
console.warn("[Cohub AppRtc] failed to publish tracks on the new connection", error);
|
|
4575
|
+
}) : null]);
|
|
4576
|
+
this.scheduleReconcile();
|
|
4577
|
+
}
|
|
4578
|
+
connected(pc) {
|
|
4579
|
+
if (pc.connectionState === "connected") return Promise.resolve();
|
|
4580
|
+
return new Promise((resolve, reject) => {
|
|
4581
|
+
const timer = setTimeout(() => done(/* @__PURE__ */ new Error("RTC connection timed out")), CONNECT_TIMEOUT_MS);
|
|
4582
|
+
const done = (error) => {
|
|
4583
|
+
clearTimeout(timer);
|
|
4584
|
+
pc.removeEventListener("connectionstatechange", check);
|
|
4585
|
+
if (error) reject(error);
|
|
4586
|
+
else resolve();
|
|
4587
|
+
};
|
|
4588
|
+
const check = () => {
|
|
4589
|
+
if (pc.connectionState === "connected") done();
|
|
4590
|
+
else if (pc.connectionState === "failed" || pc.connectionState === "closed") done(/* @__PURE__ */ new Error("RTC connection failed"));
|
|
4591
|
+
};
|
|
4592
|
+
pc.addEventListener("connectionstatechange", check);
|
|
4593
|
+
});
|
|
4594
|
+
}
|
|
4595
|
+
reconnect() {
|
|
4596
|
+
if (this._state === "closed" || this.reconnectTimer) return;
|
|
4597
|
+
this.setState(this._state === "connecting" ? "connecting" : "reconnecting");
|
|
4598
|
+
this.teardown(true);
|
|
4599
|
+
const delay = RECONNECT_DELAYS_MS[Math.min(this.reconnectAttempt, RECONNECT_DELAYS_MS.length - 1)] ?? 3e4;
|
|
4600
|
+
this.reconnectAttempt += 1;
|
|
4601
|
+
this.reconnectTimer = setTimeout(() => {
|
|
4602
|
+
this.reconnectTimer = null;
|
|
4603
|
+
this.connect().catch((error) => {
|
|
4604
|
+
if (isRoomGone(error) || error instanceof HttpError && (error.status === 403 || error.status === 503)) this.close();
|
|
4605
|
+
else this.reconnect();
|
|
4606
|
+
});
|
|
4607
|
+
}, delay);
|
|
4608
|
+
}
|
|
4609
|
+
teardown(release) {
|
|
4610
|
+
this.generation += 1;
|
|
4611
|
+
const sessionId = this.sessionId;
|
|
4612
|
+
this.sessionId = null;
|
|
4613
|
+
this.queue = Promise.resolve();
|
|
4614
|
+
for (const channel of this.channels.values()) channel.detach();
|
|
4615
|
+
for (const item of this.remoteData.values()) this.dropRemoteData(item);
|
|
4616
|
+
this.remoteData.clear();
|
|
4617
|
+
for (const item of this.localTracks.values()) {
|
|
4618
|
+
item.transceiver = null;
|
|
4619
|
+
item.id = null;
|
|
4620
|
+
}
|
|
4621
|
+
const hadTracks = this.remoteMedia.size > 0;
|
|
4622
|
+
this.remoteMedia.clear();
|
|
4623
|
+
this.mediaByMid.clear();
|
|
4624
|
+
this.pc?.close();
|
|
4625
|
+
this.pc = null;
|
|
4626
|
+
if (hadTracks) this.notifyTracks();
|
|
4627
|
+
if (release && sessionId) this.send("DELETE", `/${sessionId}`).catch(() => void 0);
|
|
4628
|
+
}
|
|
4629
|
+
enqueue(operation) {
|
|
4630
|
+
const generation = this.generation;
|
|
4631
|
+
const run = async () => {
|
|
4632
|
+
if (generation !== this.generation || !this.pc || !this.sessionId) throw new ConnectionReplacedError();
|
|
4633
|
+
try {
|
|
4634
|
+
return await operation();
|
|
4635
|
+
} catch (error) {
|
|
4636
|
+
if (isSessionLost(error)) this.reconnect();
|
|
4637
|
+
throw error;
|
|
4638
|
+
}
|
|
4639
|
+
};
|
|
4640
|
+
const next = this.queue.then(run, run);
|
|
4641
|
+
this.queue = next.catch(() => void 0);
|
|
4642
|
+
return next;
|
|
4643
|
+
}
|
|
4644
|
+
async publishChannels(channels) {
|
|
4645
|
+
const pc = this.requirePc();
|
|
4646
|
+
const open = channels.filter((channel) => channel.isOpen && !channel.publicationId);
|
|
4647
|
+
if (open.length === 0) return;
|
|
4648
|
+
const result = await this.request("POST", `/${this.sessionId}/publish`, { channels: open.map((channel) => ({
|
|
4649
|
+
name: channel.name,
|
|
4650
|
+
delivery: channel.delivery
|
|
4651
|
+
})) });
|
|
4652
|
+
for (const publication of result.publications) {
|
|
4653
|
+
const channel = open.find((item) => item.name === publication.name);
|
|
4654
|
+
const id = result.channelIds?.[publication.id];
|
|
4655
|
+
if (!channel || id === void 0) continue;
|
|
4656
|
+
const dataChannel = pc.createDataChannel(publication.name, {
|
|
4657
|
+
negotiated: true,
|
|
4658
|
+
id,
|
|
4659
|
+
...policyOf(channel.delivery)
|
|
4660
|
+
});
|
|
4661
|
+
channel.attach(publication.id, dataChannel);
|
|
4662
|
+
if (!channel.isOpen) await this.unpublishData([publication.id]);
|
|
4663
|
+
}
|
|
4664
|
+
}
|
|
4665
|
+
async unpublishData(ids) {
|
|
4666
|
+
await this.request("POST", `/${this.sessionId}/unpublish`, { ids });
|
|
4667
|
+
}
|
|
4668
|
+
async settleOffer(pc, answer) {
|
|
4669
|
+
if (answer) await pc.setRemoteDescription(answer);
|
|
4670
|
+
else if (pc.signalingState === "have-local-offer") await pc.setLocalDescription({ type: "rollback" });
|
|
4671
|
+
}
|
|
4672
|
+
async offerExchange(pc, send) {
|
|
4673
|
+
const offer = await this.localOffer(pc);
|
|
4674
|
+
let answer;
|
|
4675
|
+
try {
|
|
4676
|
+
({answer} = await send(offer));
|
|
4677
|
+
} catch (error) {
|
|
4678
|
+
if (!(error instanceof ConnectionReplacedError)) await this.settleOffer(pc, void 0).catch(() => void 0);
|
|
4679
|
+
throw error;
|
|
4680
|
+
}
|
|
4681
|
+
await this.settleOffer(pc, answer);
|
|
4682
|
+
}
|
|
4683
|
+
scheduleReconcile() {
|
|
4684
|
+
if (this.reconcileQueued || !this.pc || this._state === "closed") return;
|
|
4685
|
+
this.reconcileQueued = true;
|
|
4686
|
+
this.enqueue(async () => {
|
|
4687
|
+
this.reconcileQueued = false;
|
|
4688
|
+
await this.reconcile();
|
|
4689
|
+
this.reconcileFailures = 0;
|
|
4690
|
+
}).catch((error) => {
|
|
4691
|
+
this.reconcileQueued = false;
|
|
4692
|
+
if (isSessionLost(error) || error instanceof ConnectionReplacedError) return;
|
|
4693
|
+
console.warn("[Cohub AppRtc] failed to update subscriptions", error);
|
|
4694
|
+
if (error instanceof HttpError && error.code === "RTC_LIMIT_EXCEEDED") return;
|
|
4695
|
+
const delay = RECONNECT_DELAYS_MS[Math.min(this.reconcileFailures, RECONNECT_DELAYS_MS.length - 1)] ?? 3e4;
|
|
4696
|
+
this.reconcileFailures += 1;
|
|
4697
|
+
const generation = this.generation;
|
|
4698
|
+
setTimeout(() => {
|
|
4699
|
+
if (generation === this.generation) this.scheduleReconcile();
|
|
4700
|
+
}, delay);
|
|
4701
|
+
});
|
|
4702
|
+
}
|
|
4703
|
+
mediaCandidates() {
|
|
4704
|
+
const self = this.host.participantId();
|
|
4705
|
+
const candidates = [];
|
|
4706
|
+
for (const member of this.host.members()) {
|
|
4707
|
+
if (member.participantId === self) continue;
|
|
4708
|
+
for (const publication of member.publications ?? []) {
|
|
4709
|
+
if (publication.kind === "data") continue;
|
|
4710
|
+
candidates.push({
|
|
4711
|
+
publication,
|
|
4712
|
+
candidate: {
|
|
4713
|
+
participantId: member.participantId,
|
|
4714
|
+
name: publication.name,
|
|
4715
|
+
kind: publication.kind,
|
|
4716
|
+
simulcast: publication.simulcast === true,
|
|
4717
|
+
subscribed: this.remoteMedia.has(mediaKey(member.participantId, publication.id))
|
|
4718
|
+
}
|
|
4719
|
+
});
|
|
4720
|
+
}
|
|
4721
|
+
}
|
|
4722
|
+
return candidates;
|
|
4723
|
+
}
|
|
4724
|
+
async reconcile() {
|
|
4725
|
+
const self = this.host.participantId();
|
|
4726
|
+
const wantedData = /* @__PURE__ */ new Map();
|
|
4727
|
+
for (const member of this.host.members()) {
|
|
4728
|
+
if (member.participantId === self) continue;
|
|
4729
|
+
for (const publication of member.publications ?? []) {
|
|
4730
|
+
if (publication.kind !== "data") continue;
|
|
4731
|
+
const channel = this.channels.get(publication.name);
|
|
4732
|
+
if (channel?.isOpen && channel.delivery === (publication.delivery ?? "reliable")) wantedData.set(mediaKey(member.participantId, publication.id), {
|
|
4733
|
+
participantId: member.participantId,
|
|
4734
|
+
publicationId: publication.id,
|
|
4735
|
+
name: publication.name,
|
|
4736
|
+
delivery: channel.delivery
|
|
4737
|
+
});
|
|
4738
|
+
}
|
|
4739
|
+
}
|
|
4740
|
+
const wantedMedia = /* @__PURE__ */ new Map();
|
|
4741
|
+
for (const { candidate, publication } of this.mediaCandidates()) {
|
|
4742
|
+
if (wantedMedia.size >= APP_RTC_LIMITS.mediaSubscriptions) break;
|
|
4743
|
+
if (this.filter && !this.filter(candidate)) continue;
|
|
4744
|
+
const key = mediaKey(candidate.participantId, publication.id);
|
|
4745
|
+
const quality = this.remoteMedia.get(key)?.quality ?? (candidate.simulcast ? "high" : void 0);
|
|
4746
|
+
wantedMedia.set(key, {
|
|
4747
|
+
participantId: candidate.participantId,
|
|
4748
|
+
publicationId: publication.id,
|
|
4749
|
+
name: candidate.name,
|
|
4750
|
+
kind: candidate.kind,
|
|
4751
|
+
...quality ? { quality } : {}
|
|
4752
|
+
});
|
|
4753
|
+
}
|
|
4754
|
+
await this.reconcileData(wantedData);
|
|
4755
|
+
await this.reconcileMedia(wantedMedia);
|
|
4756
|
+
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");
|
|
4757
|
+
}
|
|
4758
|
+
dropRemoteData(item) {
|
|
4759
|
+
item.channel.close();
|
|
4760
|
+
this.channels.get(item.name)?.setPeer(item.participantId, false);
|
|
4761
|
+
}
|
|
4762
|
+
async reconcileData(wanted) {
|
|
4763
|
+
const pc = this.requirePc();
|
|
4764
|
+
const stale = [...this.remoteData].filter(([key]) => !wanted.has(key));
|
|
4765
|
+
for (const batch of chunk(stale, APP_RTC_LIMITS.batch)) {
|
|
4766
|
+
await this.request("POST", `/${this.sessionId}/unsubscribe`, { items: batch.map(([, item]) => ({
|
|
4767
|
+
participantId: item.participantId,
|
|
4768
|
+
publicationId: item.publicationId
|
|
4769
|
+
})) });
|
|
4770
|
+
for (const [key, item] of batch) {
|
|
4771
|
+
this.dropRemoteData(item);
|
|
4772
|
+
this.remoteData.delete(key);
|
|
4773
|
+
}
|
|
4774
|
+
}
|
|
4775
|
+
const missing = [...wanted].filter(([key]) => !this.remoteData.has(key)).map(([, item]) => item);
|
|
4776
|
+
for (const batch of chunk(missing, APP_RTC_LIMITS.batch)) {
|
|
4777
|
+
const result = await this.request("POST", `/${this.sessionId}/subscribe`, {
|
|
4778
|
+
kind: "data",
|
|
4779
|
+
items: batch.map(({ participantId, publicationId }) => ({
|
|
4780
|
+
participantId,
|
|
4781
|
+
publicationId
|
|
4782
|
+
}))
|
|
4783
|
+
});
|
|
4784
|
+
for (const subscription of result.subscriptions) {
|
|
4785
|
+
const item = batch.find((entry) => entry.participantId === subscription.participantId && entry.publicationId === subscription.publicationId);
|
|
4786
|
+
if (!item || subscription.channelId === void 0) continue;
|
|
4787
|
+
const channel = pc.createDataChannel(item.name, {
|
|
4788
|
+
negotiated: true,
|
|
4789
|
+
id: subscription.channelId,
|
|
4790
|
+
...policyOf(item.delivery)
|
|
4791
|
+
});
|
|
4792
|
+
channel.binaryType = "arraybuffer";
|
|
4793
|
+
const remote = {
|
|
4794
|
+
name: item.name,
|
|
4795
|
+
participantId: item.participantId,
|
|
4796
|
+
publicationId: item.publicationId,
|
|
4797
|
+
channel,
|
|
4798
|
+
last: null
|
|
4799
|
+
};
|
|
4800
|
+
channel.addEventListener("message", (event) => this.onData(remote, event.data));
|
|
4801
|
+
channel.addEventListener("open", () => {
|
|
4802
|
+
if (this.remoteData.get(mediaKey(remote.participantId, remote.publicationId)) === remote) this.channels.get(remote.name)?.setPeer(remote.participantId, true);
|
|
4803
|
+
});
|
|
4804
|
+
this.remoteData.set(mediaKey(item.participantId, item.publicationId), remote);
|
|
4805
|
+
}
|
|
4806
|
+
}
|
|
4807
|
+
}
|
|
4808
|
+
async reconcileMedia(wanted) {
|
|
4809
|
+
const stale = [...this.remoteMedia].filter(([key]) => !wanted.has(key));
|
|
4810
|
+
const missing = [...wanted].filter(([key]) => !this.remoteMedia.has(key)).map(([, item]) => item);
|
|
4811
|
+
const moves = [];
|
|
4812
|
+
for (const to of [...missing]) {
|
|
4813
|
+
const index = stale.findIndex(([, receiver]) => receiver.kind === to.kind);
|
|
4814
|
+
const found = index >= 0 ? stale.splice(index, 1)[0] : void 0;
|
|
4815
|
+
if (!found) continue;
|
|
4816
|
+
moves.push({
|
|
4817
|
+
from: found[0],
|
|
4818
|
+
receiver: found[1],
|
|
4819
|
+
to
|
|
4820
|
+
});
|
|
4821
|
+
missing.splice(missing.indexOf(to), 1);
|
|
4822
|
+
}
|
|
4823
|
+
if (moves.length) {
|
|
4824
|
+
const moved = await this.retarget(moves.map(({ receiver, to }) => ({
|
|
4825
|
+
...to,
|
|
4826
|
+
mid: receiver.mid
|
|
4827
|
+
})));
|
|
4828
|
+
for (const { from, receiver, to } of moves) if (moved.has(receiver.mid)) {
|
|
4829
|
+
this.remoteMedia.delete(from);
|
|
4830
|
+
this.remoteMedia.set(mediaKey(to.participantId, to.publicationId), {
|
|
4831
|
+
...to,
|
|
4832
|
+
mid: receiver.mid,
|
|
4833
|
+
track: receiver.track
|
|
4834
|
+
});
|
|
4835
|
+
this.mediaByMid.set(receiver.mid, mediaKey(to.participantId, to.publicationId));
|
|
4836
|
+
} else {
|
|
4837
|
+
stale.push([from, receiver]);
|
|
4838
|
+
missing.push(to);
|
|
4839
|
+
}
|
|
4840
|
+
if (moved.size) this.notifyTracks();
|
|
4841
|
+
}
|
|
4842
|
+
await this.unsubscribeMedia(stale);
|
|
4843
|
+
await this.subscribeMedia(missing);
|
|
4844
|
+
}
|
|
4845
|
+
async retarget(items) {
|
|
4846
|
+
try {
|
|
4847
|
+
const result = await this.request("PATCH", `/${this.sessionId}/media`, { items: items.map(({ participantId, publicationId, mid, quality }) => ({
|
|
4848
|
+
participantId,
|
|
4849
|
+
publicationId,
|
|
4850
|
+
mid,
|
|
4851
|
+
...quality ? { quality } : {}
|
|
4852
|
+
})) });
|
|
4853
|
+
if (result.offer) await this.answerOffer(result.offer);
|
|
4854
|
+
return new Set(result.updated.map((item) => item.mid));
|
|
4855
|
+
} catch (error) {
|
|
4856
|
+
if (error instanceof ConnectionReplacedError || isSessionLost(error)) throw error;
|
|
4857
|
+
return /* @__PURE__ */ new Set();
|
|
4858
|
+
}
|
|
4859
|
+
}
|
|
4860
|
+
async setQuality(key, quality) {
|
|
4861
|
+
await this.enqueue(async () => {
|
|
4862
|
+
const item = this.remoteMedia.get(key);
|
|
4863
|
+
if (!item || item.quality === void 0 || item.quality === quality) return;
|
|
4864
|
+
if ((await this.retarget([{
|
|
4865
|
+
...item,
|
|
4866
|
+
quality
|
|
4867
|
+
}])).has(item.mid)) {
|
|
4868
|
+
item.quality = quality;
|
|
4869
|
+
this.notifyTracks();
|
|
4870
|
+
}
|
|
4871
|
+
}).catch((error) => {
|
|
4872
|
+
if (error instanceof ConnectionReplacedError) return;
|
|
4873
|
+
throw toAppRtcError(error);
|
|
4874
|
+
});
|
|
4875
|
+
}
|
|
4876
|
+
async unsubscribeMedia(stale) {
|
|
4877
|
+
if (stale.length === 0) return;
|
|
4878
|
+
const pc = this.requirePc();
|
|
4879
|
+
for (const [, item] of stale) {
|
|
4880
|
+
pc.getTransceivers().find((transceiver) => transceiver.mid === item.mid)?.stop();
|
|
4881
|
+
item.track = null;
|
|
4882
|
+
}
|
|
4883
|
+
this.notifyTracks();
|
|
4884
|
+
await this.offerExchange(pc, (offer) => this.request("POST", `/${this.sessionId}/unsubscribe`, {
|
|
4885
|
+
items: stale.map(([, item]) => ({
|
|
4886
|
+
participantId: item.participantId,
|
|
4887
|
+
publicationId: item.publicationId
|
|
4888
|
+
})),
|
|
4889
|
+
offer
|
|
4890
|
+
}));
|
|
4891
|
+
for (const [key, item] of stale) {
|
|
4892
|
+
this.remoteMedia.delete(key);
|
|
4893
|
+
this.mediaByMid.delete(item.mid);
|
|
4894
|
+
}
|
|
4895
|
+
}
|
|
4896
|
+
async subscribeMedia(missing) {
|
|
4897
|
+
if (missing.length === 0) return;
|
|
4898
|
+
const result = await this.request("POST", `/${this.sessionId}/subscribe`, {
|
|
4899
|
+
kind: "media",
|
|
4900
|
+
items: missing.map(({ participantId, publicationId, quality }) => ({
|
|
4901
|
+
participantId,
|
|
4902
|
+
publicationId,
|
|
4903
|
+
...quality ? { quality } : {}
|
|
4904
|
+
}))
|
|
4905
|
+
});
|
|
4906
|
+
for (const subscription of result.subscriptions) {
|
|
4907
|
+
const item = missing.find((entry) => entry.participantId === subscription.participantId && entry.publicationId === subscription.publicationId);
|
|
4908
|
+
if (!item || !subscription.mid) continue;
|
|
4909
|
+
const key = mediaKey(item.participantId, item.publicationId);
|
|
4910
|
+
this.remoteMedia.set(key, {
|
|
4911
|
+
...item,
|
|
4912
|
+
mid: subscription.mid,
|
|
4913
|
+
track: null
|
|
4914
|
+
});
|
|
4915
|
+
this.mediaByMid.set(subscription.mid, key);
|
|
4916
|
+
}
|
|
4917
|
+
if (!result.offer) return;
|
|
4918
|
+
try {
|
|
4919
|
+
await this.answerOffer(result.offer);
|
|
4920
|
+
} catch (error) {
|
|
4921
|
+
if (!(error instanceof ConnectionReplacedError)) this.reconnect();
|
|
4922
|
+
throw error;
|
|
4923
|
+
}
|
|
4924
|
+
}
|
|
4925
|
+
async answerOffer(offer) {
|
|
4926
|
+
const pc = this.requirePc();
|
|
4927
|
+
await pc.setRemoteDescription(offer);
|
|
4928
|
+
await pc.setLocalDescription(await pc.createAnswer());
|
|
4929
|
+
await waitForIceGathering(pc, GATHER_TIMEOUT_MS);
|
|
4930
|
+
const answer = pc.localDescription;
|
|
4931
|
+
if (!answer) throw new Error("the browser produced no answer");
|
|
4932
|
+
await this.request("PUT", `/${this.sessionId}`, { answer: {
|
|
4933
|
+
type: "answer",
|
|
4934
|
+
sdp: answer.sdp
|
|
4935
|
+
} });
|
|
4936
|
+
}
|
|
4937
|
+
async localOffer(pc) {
|
|
4938
|
+
await pc.setLocalDescription(await pc.createOffer());
|
|
4939
|
+
await waitForIceGathering(pc, GATHER_TIMEOUT_MS);
|
|
4940
|
+
const offer = pc.localDescription;
|
|
4941
|
+
if (!offer) throw new Error("the browser produced no offer");
|
|
4942
|
+
return {
|
|
4943
|
+
type: "offer",
|
|
4944
|
+
sdp: offer.sdp
|
|
4945
|
+
};
|
|
4946
|
+
}
|
|
4947
|
+
async request(method, path, body) {
|
|
4948
|
+
const generation = this.generation;
|
|
4949
|
+
const settled = await this.send(method, path, body).then((value) => ({ value }), (error) => ({ error }));
|
|
4950
|
+
if (generation !== this.generation) throw new ConnectionReplacedError();
|
|
4951
|
+
if ("error" in settled) throw settled.error;
|
|
4952
|
+
return settled.value;
|
|
4953
|
+
}
|
|
4954
|
+
send(method, path, body) {
|
|
4955
|
+
const url = `/api/apps/${encodeURIComponent(this.host.appId)}/realtime/rooms/${encodeURIComponent(this.host.roomId)}/rtc${path}`;
|
|
4956
|
+
return this.host.transport.request(url, {
|
|
4957
|
+
method,
|
|
4958
|
+
...body === void 0 ? {} : {
|
|
4959
|
+
headers: { "Content-Type": "application/json" },
|
|
4960
|
+
body: JSON.stringify(body)
|
|
4961
|
+
}
|
|
4962
|
+
});
|
|
4963
|
+
}
|
|
4964
|
+
requirePc() {
|
|
4965
|
+
if (!this.pc || !this.sessionId) throw new ConnectionReplacedError();
|
|
4966
|
+
return this.pc;
|
|
4967
|
+
}
|
|
4968
|
+
onData(remote, raw) {
|
|
4969
|
+
if (!(raw instanceof ArrayBuffer)) return;
|
|
4970
|
+
const frame = decodeAppRtcFrame(raw);
|
|
4971
|
+
if (!frame) return;
|
|
4972
|
+
const channel = this.channels.get(remote.name);
|
|
4973
|
+
if (!channel?.isOpen) return;
|
|
4974
|
+
if (channel.delivery === "latest") {
|
|
4975
|
+
if (!isNewerSequence(frame.sequence, remote.last)) return;
|
|
4976
|
+
remote.last = frame.sequence;
|
|
4977
|
+
}
|
|
4978
|
+
channel.deliver(remote.participantId, frame.data);
|
|
4979
|
+
}
|
|
4980
|
+
onTrack(event) {
|
|
4981
|
+
const mid = event.transceiver.mid;
|
|
4982
|
+
const key = mid ? this.mediaByMid.get(mid) : void 0;
|
|
4983
|
+
const item = key ? this.remoteMedia.get(key) : void 0;
|
|
4984
|
+
if (!item || !mid) return;
|
|
4985
|
+
item.track = event.track;
|
|
4986
|
+
event.track.addEventListener("ended", () => {
|
|
4987
|
+
const current = this.mediaByMid.get(mid);
|
|
4988
|
+
const owner = current ? this.remoteMedia.get(current) : void 0;
|
|
4989
|
+
if (owner?.track === event.track) {
|
|
4990
|
+
owner.track = null;
|
|
4991
|
+
this.notifyTracks();
|
|
4992
|
+
}
|
|
4993
|
+
});
|
|
4994
|
+
this.notifyTracks();
|
|
4995
|
+
}
|
|
4996
|
+
notifyTracks() {
|
|
4997
|
+
const tracks = this.tracks;
|
|
4998
|
+
for (const handler of this.trackHandlers) handler(tracks);
|
|
4999
|
+
}
|
|
5000
|
+
setState(state) {
|
|
5001
|
+
if (this._state === state) return;
|
|
5002
|
+
this._state = state;
|
|
5003
|
+
for (const handler of this.stateHandlers) handler(state);
|
|
5004
|
+
}
|
|
5005
|
+
};
|
|
5006
|
+
//#endregion
|
|
3906
5007
|
//#region src/apis/app-realtime.ts
|
|
3907
5008
|
const randomId = () => globalThis.crypto?.randomUUID?.() ?? `${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
3908
5009
|
const textEncoder = new TextEncoder();
|
|
@@ -3911,13 +5012,20 @@ const byteLength = (text) => textEncoder.encode(text).byteLength;
|
|
|
3911
5012
|
* Ceiling on deltas held during a join, so a stalled handshake cannot buffer without
|
|
3912
5013
|
* limit. Dropping the tail needs no extra signal: the gap detection below reports it.
|
|
3913
5014
|
*/
|
|
3914
|
-
const
|
|
5015
|
+
const APP_ROOM_MAX_JOIN_BUFFER = 512;
|
|
3915
5016
|
/** Room events that mutate state, so they must not be applied before the join snapshot. */
|
|
3916
|
-
const
|
|
5017
|
+
const APP_ROOM_DELTA_EVENTS = /* @__PURE__ */ new Set([
|
|
3917
5018
|
"realtime.room.event",
|
|
3918
5019
|
"realtime.room.member.joined",
|
|
3919
5020
|
"realtime.room.member.left",
|
|
3920
|
-
"realtime.room.presence.updated"
|
|
5021
|
+
"realtime.room.presence.updated",
|
|
5022
|
+
"realtime.room.publications.updated"
|
|
5023
|
+
]);
|
|
5024
|
+
const APP_ROOM_MEMBER_EVENTS = /* @__PURE__ */ new Set([
|
|
5025
|
+
"realtime.room.member.joined",
|
|
5026
|
+
"realtime.room.member.left",
|
|
5027
|
+
"realtime.room.presence.updated",
|
|
5028
|
+
"realtime.room.publications.updated"
|
|
3921
5029
|
]);
|
|
3922
5030
|
const requestError = (event) => {
|
|
3923
5031
|
const payload = event.payload;
|
|
@@ -3930,6 +5038,7 @@ const isRoomEvent = (event, roomId) => {
|
|
|
3930
5038
|
var AppRoom = class {
|
|
3931
5039
|
websocket;
|
|
3932
5040
|
admission;
|
|
5041
|
+
appContext;
|
|
3933
5042
|
id;
|
|
3934
5043
|
code;
|
|
3935
5044
|
createdAt;
|
|
@@ -3957,9 +5066,12 @@ var AppRoom = class {
|
|
|
3957
5066
|
offOpen;
|
|
3958
5067
|
offClose;
|
|
3959
5068
|
offReconnecting;
|
|
3960
|
-
|
|
5069
|
+
instanceId = randomId().replace(/[^A-Za-z0-9_-]/g, "");
|
|
5070
|
+
rtcConnection = null;
|
|
5071
|
+
constructor(websocket, admission, appContext) {
|
|
3961
5072
|
this.websocket = websocket;
|
|
3962
5073
|
this.admission = admission;
|
|
5074
|
+
this.appContext = appContext;
|
|
3963
5075
|
this.id = admission.room.id;
|
|
3964
5076
|
this.code = admission.room.code;
|
|
3965
5077
|
this.createdAt = admission.room.createdAt;
|
|
@@ -4085,6 +5197,66 @@ var AppRoom = class {
|
|
|
4085
5197
|
clientEventId: typeof payload.clientEventId === "string" ? payload.clientEventId : null
|
|
4086
5198
|
};
|
|
4087
5199
|
}
|
|
5200
|
+
/** Published events after `after`, oldest first; without it, the newest ones. */
|
|
5201
|
+
async history(input = {}) {
|
|
5202
|
+
this.assertJoined();
|
|
5203
|
+
if (!this.websocket.supportsCapability("realtime.room.history.v1")) throw new Error("Room history is not supported by the Gateway");
|
|
5204
|
+
const requestId = randomId();
|
|
5205
|
+
const response = await this.request(requestId, /* @__PURE__ */ new Set(["realtime.room.history"]), () => this.websocket.readRealtimeRoomHistory({
|
|
5206
|
+
roomId: this.id,
|
|
5207
|
+
...input,
|
|
5208
|
+
requestId
|
|
5209
|
+
}));
|
|
5210
|
+
return this.toHistory(response.payload);
|
|
5211
|
+
}
|
|
5212
|
+
/** An invite for someone outside the App, e.g. an Agent: `cohub rooms join <invite>`. */
|
|
5213
|
+
async invite(input = {}) {
|
|
5214
|
+
this.assertJoined();
|
|
5215
|
+
const context = this.appContext;
|
|
5216
|
+
if (!context) throw new Error("Invites are only available inside a published App");
|
|
5217
|
+
return context.transport.request(`/api/apps/${encodeURIComponent(context.appId)}/realtime/rooms/${encodeURIComponent(this.id)}/invites`, {
|
|
5218
|
+
method: "POST",
|
|
5219
|
+
headers: { "Content-Type": "application/json" },
|
|
5220
|
+
body: JSON.stringify({
|
|
5221
|
+
ticket: this.admission.ticket,
|
|
5222
|
+
...input
|
|
5223
|
+
})
|
|
5224
|
+
});
|
|
5225
|
+
}
|
|
5226
|
+
rtc() {
|
|
5227
|
+
this.assertJoined();
|
|
5228
|
+
if (this.rtcConnection) {
|
|
5229
|
+
const current = this.rtcConnection;
|
|
5230
|
+
return current.then((rtc) => {
|
|
5231
|
+
if (rtc.state !== "closed") return rtc;
|
|
5232
|
+
return (this.rtcConnection === current ? this.openRtc() : this.rtcConnection) ?? this.openRtc();
|
|
5233
|
+
});
|
|
5234
|
+
}
|
|
5235
|
+
return this.openRtc();
|
|
5236
|
+
}
|
|
5237
|
+
openRtc() {
|
|
5238
|
+
this.assertJoined();
|
|
5239
|
+
const context = this.appContext;
|
|
5240
|
+
if (!context) return Promise.reject(new AppRtcError("RTC_UNSUPPORTED", "RTC is only available inside a published App"));
|
|
5241
|
+
if (typeof RTCPeerConnection === "undefined") return Promise.reject(new AppRtcError("RTC_UNSUPPORTED", "RTC needs a browser with WebRTC"));
|
|
5242
|
+
if (!this.websocket.supportsCapability("realtime.room.rtc.v1")) return Promise.reject(new AppRtcError("RTC_UNSUPPORTED", "RTC is not supported by the Gateway"));
|
|
5243
|
+
const rtc = new AppRoomRtc({
|
|
5244
|
+
roomId: this.id,
|
|
5245
|
+
appId: context.appId,
|
|
5246
|
+
ticket: this.admission.ticket,
|
|
5247
|
+
transport: context.transport,
|
|
5248
|
+
participantId: () => this._participantId,
|
|
5249
|
+
members: () => this._members,
|
|
5250
|
+
onMembersChanged: (handler) => this.onMembersChanged(handler)
|
|
5251
|
+
});
|
|
5252
|
+
const opening = rtc.start().then(() => rtc, (error) => {
|
|
5253
|
+
rtc.close();
|
|
5254
|
+
if (this.rtcConnection === opening) this.rtcConnection = null;
|
|
5255
|
+
throw error;
|
|
5256
|
+
});
|
|
5257
|
+
this.rtcConnection = opening;
|
|
5258
|
+
return opening;
|
|
5259
|
+
}
|
|
4088
5260
|
async setPresence(presence) {
|
|
4089
5261
|
this.assertJoined();
|
|
4090
5262
|
const requestId = randomId();
|
|
@@ -4126,17 +5298,23 @@ var AppRoom = class {
|
|
|
4126
5298
|
if (!this.websocket.supportsCapability("realtime.room.v1") && this.websocket.state === "open") throw new Error("Realtime rooms are not supported by the Gateway");
|
|
4127
5299
|
this.setState(isReconnect ? "reconnecting" : "connecting");
|
|
4128
5300
|
const requestId = randomId();
|
|
5301
|
+
const after = isReconnect && this.lastSequence > 0 && this.websocket.supportsCapability("realtime.room.history.v1") ? this.lastSequence : void 0;
|
|
5302
|
+
let missed = null;
|
|
4129
5303
|
this.joinBuffer = [];
|
|
4130
5304
|
try {
|
|
4131
5305
|
const payload = (await this.request(requestId, /* @__PURE__ */ new Set(["realtime.room.joined"]), () => this.websocket.joinRealtimeRoom({
|
|
4132
5306
|
roomId: this.id,
|
|
4133
5307
|
ticket: this.admission.ticket,
|
|
5308
|
+
instanceId: this.instanceId,
|
|
5309
|
+
after,
|
|
4134
5310
|
requestId
|
|
4135
5311
|
}))).payload;
|
|
4136
5312
|
if (typeof payload.participantId !== "string" || !Array.isArray(payload.members)) throw new Error("invalid room join response");
|
|
4137
5313
|
this._participantId = payload.participantId;
|
|
4138
5314
|
this._members = payload.members;
|
|
4139
|
-
|
|
5315
|
+
const sequence = typeof payload.sequence === "number" ? payload.sequence : 0;
|
|
5316
|
+
if (after !== void 0 && sequence > after) missed = await this.catchUp(payload.missed, after, sequence);
|
|
5317
|
+
this.lastSequence = sequence;
|
|
4140
5318
|
} catch (error) {
|
|
4141
5319
|
this.joinBuffer = null;
|
|
4142
5320
|
throw error;
|
|
@@ -4144,6 +5322,11 @@ var AppRoom = class {
|
|
|
4144
5322
|
this.hasJoined = true;
|
|
4145
5323
|
this.setState("joined");
|
|
4146
5324
|
this.notifyMembers();
|
|
5325
|
+
if (missed) {
|
|
5326
|
+
const { lost } = missed;
|
|
5327
|
+
if (lost) for (const handler of this.outOfSyncHandlers) handler(lost[0], lost[1]);
|
|
5328
|
+
for (const event of missed.events) this.emit(event);
|
|
5329
|
+
}
|
|
4147
5330
|
const buffered = this.joinBuffer ?? [];
|
|
4148
5331
|
this.joinBuffer = null;
|
|
4149
5332
|
for (const item of buffered) {
|
|
@@ -4151,6 +5334,7 @@ var AppRoom = class {
|
|
|
4151
5334
|
if (typeof sequence === "number" && sequence <= this.lastSequence) continue;
|
|
4152
5335
|
this.handleEvent(item);
|
|
4153
5336
|
}
|
|
5337
|
+
if (isReconnect) this.rtcConnection?.then((rtc) => rtc.verify()).catch(() => void 0);
|
|
4154
5338
|
}
|
|
4155
5339
|
request(requestId, expected, send) {
|
|
4156
5340
|
return new Promise((resolve, reject) => {
|
|
@@ -4198,14 +5382,13 @@ var AppRoom = class {
|
|
|
4198
5382
|
this.takePending(event.requestId);
|
|
4199
5383
|
awaiting.resolve(event);
|
|
4200
5384
|
}
|
|
4201
|
-
if (this.joinBuffer &&
|
|
4202
|
-
if (this.joinBuffer.length <
|
|
5385
|
+
if (this.joinBuffer && APP_ROOM_DELTA_EVENTS.has(event.type)) {
|
|
5386
|
+
if (this.joinBuffer.length < APP_ROOM_MAX_JOIN_BUFFER) this.joinBuffer.push(event);
|
|
4203
5387
|
return;
|
|
4204
5388
|
}
|
|
4205
5389
|
const payload = event.payload;
|
|
4206
|
-
|
|
4207
|
-
if (
|
|
4208
|
-
if (event.type === "realtime.room.member.joined" || event.type === "realtime.room.member.left" || event.type === "realtime.room.presence.updated") {
|
|
5390
|
+
if (APP_ROOM_DELTA_EVENTS.has(event.type) && typeof payload.sequence === "number") this.observeSequence(payload.sequence);
|
|
5391
|
+
if (APP_ROOM_MEMBER_EVENTS.has(event.type)) {
|
|
4209
5392
|
const member = payload.member;
|
|
4210
5393
|
if (member?.participantId) {
|
|
4211
5394
|
const members = new Map(this._members.map((item) => [item.participantId, item]));
|
|
@@ -4225,11 +5408,10 @@ var AppRoom = class {
|
|
|
4225
5408
|
return;
|
|
4226
5409
|
}
|
|
4227
5410
|
if (event.type !== "realtime.room.event") return;
|
|
4228
|
-
const
|
|
4229
|
-
|
|
4230
|
-
|
|
4231
|
-
|
|
4232
|
-
timestamp: roomEvent.timestamp,
|
|
5411
|
+
const { id, timestamp, payload: roomPayload } = event;
|
|
5412
|
+
this.emit({
|
|
5413
|
+
id,
|
|
5414
|
+
timestamp,
|
|
4233
5415
|
roomId: roomPayload.roomId,
|
|
4234
5416
|
sequence: roomPayload.sequence,
|
|
4235
5417
|
type: roomPayload.event,
|
|
@@ -4237,7 +5419,35 @@ var AppRoom = class {
|
|
|
4237
5419
|
clientEventId: roomPayload.clientEventId,
|
|
4238
5420
|
sender: roomPayload.sender,
|
|
4239
5421
|
self: roomPayload.sender.participantId === this.participantId
|
|
5422
|
+
});
|
|
5423
|
+
}
|
|
5424
|
+
async catchUp(first, after, sequence) {
|
|
5425
|
+
if (!first) return {
|
|
5426
|
+
events: [],
|
|
5427
|
+
lost: [after + 1, sequence]
|
|
5428
|
+
};
|
|
5429
|
+
const events = this.toHistory(first).events;
|
|
5430
|
+
const { truncated } = first;
|
|
5431
|
+
let { cursor } = first;
|
|
5432
|
+
while (cursor < sequence) {
|
|
5433
|
+
const requestId = randomId();
|
|
5434
|
+
const page = (await this.request(requestId, /* @__PURE__ */ new Set(["realtime.room.history"]), () => this.websocket.readRealtimeRoomHistory({
|
|
5435
|
+
roomId: this.id,
|
|
5436
|
+
after: cursor,
|
|
5437
|
+
requestId
|
|
5438
|
+
})).catch(() => null))?.payload;
|
|
5439
|
+
if (!page || page.cursor <= cursor) break;
|
|
5440
|
+
events.push(...this.toHistory(page).events);
|
|
5441
|
+
cursor = page.cursor;
|
|
5442
|
+
}
|
|
5443
|
+
const kept = events.filter((event) => event.sequence <= sequence);
|
|
5444
|
+
const lost = truncated ? [after + 1, kept[0]?.sequence ?? sequence] : cursor < sequence ? [cursor + 1, sequence] : null;
|
|
5445
|
+
return {
|
|
5446
|
+
events: kept,
|
|
5447
|
+
lost: lost && lost[1] > lost[0] ? lost : null
|
|
4240
5448
|
};
|
|
5449
|
+
}
|
|
5450
|
+
emit(item) {
|
|
4241
5451
|
this.handlers.get(item.type)?.forEach((handler) => {
|
|
4242
5452
|
handler(item);
|
|
4243
5453
|
});
|
|
@@ -4245,6 +5455,18 @@ var AppRoom = class {
|
|
|
4245
5455
|
handler(item);
|
|
4246
5456
|
});
|
|
4247
5457
|
}
|
|
5458
|
+
toHistory(history) {
|
|
5459
|
+
return {
|
|
5460
|
+
events: history.events.map((event) => ({
|
|
5461
|
+
...event,
|
|
5462
|
+
data: event.data,
|
|
5463
|
+
roomId: this.id,
|
|
5464
|
+
self: event.sender.participantId === this.participantId
|
|
5465
|
+
})),
|
|
5466
|
+
cursor: history.cursor,
|
|
5467
|
+
truncated: history.truncated
|
|
5468
|
+
};
|
|
5469
|
+
}
|
|
4248
5470
|
observeSequence(sequence) {
|
|
4249
5471
|
if (this.lastSequence > 0 && sequence > this.lastSequence + 1) for (const handler of this.outOfSyncHandlers) handler(this.lastSequence + 1, sequence);
|
|
4250
5472
|
if (sequence > this.lastSequence) this.lastSequence = sequence;
|
|
@@ -4276,6 +5498,9 @@ var AppRoom = class {
|
|
|
4276
5498
|
this.dispose();
|
|
4277
5499
|
}
|
|
4278
5500
|
dispose() {
|
|
5501
|
+
const rtc = this.rtcConnection;
|
|
5502
|
+
this.rtcConnection = null;
|
|
5503
|
+
rtc?.then((connection) => connection.close()).catch(() => void 0);
|
|
4279
5504
|
this.offEvent();
|
|
4280
5505
|
this.offOpen();
|
|
4281
5506
|
this.offClose();
|
|
@@ -4294,28 +5519,33 @@ var AppRealtimeApi = class {
|
|
|
4294
5519
|
this.getContext = getContext;
|
|
4295
5520
|
}
|
|
4296
5521
|
async createRoom(input = {}) {
|
|
4297
|
-
const
|
|
5522
|
+
const appId = await this.requireAppId();
|
|
5523
|
+
const admission = await this.transport.request(`/api/apps/${encodeURIComponent(appId)}/realtime/rooms`, {
|
|
4298
5524
|
method: "POST",
|
|
4299
5525
|
headers: { "Content-Type": "application/json" },
|
|
4300
5526
|
body: JSON.stringify(input)
|
|
4301
5527
|
});
|
|
4302
|
-
return this.openRoom(admission);
|
|
5528
|
+
return this.openRoom(appId, admission);
|
|
4303
5529
|
}
|
|
4304
5530
|
async joinRoom(input) {
|
|
4305
|
-
const
|
|
5531
|
+
const appId = await this.requireAppId();
|
|
5532
|
+
const admission = await this.transport.request(`/api/apps/${encodeURIComponent(appId)}/realtime/rooms/join`, {
|
|
4306
5533
|
method: "POST",
|
|
4307
5534
|
headers: { "Content-Type": "application/json" },
|
|
4308
5535
|
body: JSON.stringify(input)
|
|
4309
5536
|
});
|
|
4310
|
-
return this.openRoom(admission);
|
|
5537
|
+
return this.openRoom(appId, admission);
|
|
4311
5538
|
}
|
|
4312
5539
|
async requireAppId() {
|
|
4313
5540
|
const context = await this.getContext();
|
|
4314
5541
|
if (!context?.app?.id) throw new Error("App context is unavailable — not running inside a published app runtime.");
|
|
4315
5542
|
return context.app.id;
|
|
4316
5543
|
}
|
|
4317
|
-
async openRoom(admission) {
|
|
4318
|
-
const room = new AppRoom(this.websocket, admission
|
|
5544
|
+
async openRoom(appId, admission) {
|
|
5545
|
+
const room = new AppRoom(this.websocket, admission, {
|
|
5546
|
+
transport: this.transport,
|
|
5547
|
+
appId
|
|
5548
|
+
});
|
|
4319
5549
|
try {
|
|
4320
5550
|
await room.connect();
|
|
4321
5551
|
return room;
|
|
@@ -4582,10 +5812,12 @@ var AppWindowApi = class {
|
|
|
4582
5812
|
}
|
|
4583
5813
|
/** Report the tab title, save status, and whether work is still unsaved. */
|
|
4584
5814
|
setState(patch) {
|
|
4585
|
-
|
|
5815
|
+
const next = {
|
|
4586
5816
|
...this.state,
|
|
4587
5817
|
...patch
|
|
4588
5818
|
};
|
|
5819
|
+
if (this.stateReported && next.title === this.state.title && next.status === this.state.status && next.dirty === this.state.dirty) return;
|
|
5820
|
+
this.state = next;
|
|
4589
5821
|
this.stateReported = true;
|
|
4590
5822
|
this.listen();
|
|
4591
5823
|
this.transport.notify?.(buildAppRuntimeWindowState(this.state));
|
|
@@ -5392,6 +6624,18 @@ var AppRuntimeApi = class {
|
|
|
5392
6624
|
reason: response === null ? "timeout" : "unsupported"
|
|
5393
6625
|
};
|
|
5394
6626
|
}
|
|
6627
|
+
/** Asks for devices declared with `<meta name="cohub:devices">`; allowing the camera or microphone reloads the App once. */
|
|
6628
|
+
async requestDevices(devices) {
|
|
6629
|
+
const wanted = parseAppDevices(devices);
|
|
6630
|
+
if (wanted.length === 0) throw new AppRuntimeError("invalid_request", `Request at least one of: ${APP_DEVICES.join(", ")}.`);
|
|
6631
|
+
const context = await this.context().catch(() => null);
|
|
6632
|
+
if (!context || context.mode === "broker") return "granted";
|
|
6633
|
+
if (!context.capabilities?.devices) return "unavailable";
|
|
6634
|
+
return parseAppDevicesResult(await this.transport.request({
|
|
6635
|
+
type: APP_DEVICES_REQUEST_TYPE,
|
|
6636
|
+
devices: wanted
|
|
6637
|
+
}, { timeoutMs: 6e5 }))?.status ?? "unavailable";
|
|
6638
|
+
}
|
|
5395
6639
|
/**
|
|
5396
6640
|
* Asks the host to close this App: a workspace tab closes, an embedded page
|
|
5397
6641
|
* forwards the request to its embedder, a standalone page closes the tab.
|
|
@@ -5756,6 +7000,7 @@ var CohubClient = class {
|
|
|
5756
7000
|
voice;
|
|
5757
7001
|
apps;
|
|
5758
7002
|
appCommerce;
|
|
7003
|
+
rooms;
|
|
5759
7004
|
navigation;
|
|
5760
7005
|
/** @deprecated Use `client.desktop`. */
|
|
5761
7006
|
get ui() {
|
|
@@ -5835,6 +7080,7 @@ var CohubClient = class {
|
|
|
5835
7080
|
this.referrals = new ReferralsApi(this.transport);
|
|
5836
7081
|
this.apps = new AppsApi(this.transport);
|
|
5837
7082
|
this.appCommerce = new AppCommerceApi(this.transport);
|
|
7083
|
+
this.rooms = new RoomsApi(this.transport);
|
|
5838
7084
|
this.navigation = { open: (target, options) => this.appRuntime.navigationOpen(typeof target === "string" ? {
|
|
5839
7085
|
kind: "app",
|
|
5840
7086
|
ref: target
|
|
@@ -5891,6 +7137,7 @@ sync: (root) => {
|
|
|
5891
7137
|
} },
|
|
5892
7138
|
/** Ask the host to close this App's surface. */
|
|
5893
7139
|
requestClose: () => this.appRuntime.requestClose(),
|
|
7140
|
+
devices: { request: (devices) => this.appRuntime.requestDevices(devices) },
|
|
5894
7141
|
/**
|
|
5895
7142
|
* Request that the host update this overlay's geometry or pointer hit
|
|
5896
7143
|
* regions. Only meaningful for `overlay` surfaces; ignored otherwise.
|
|
@@ -6564,7 +7811,8 @@ function createAppBridgeCore(config) {
|
|
|
6564
7811
|
return {
|
|
6565
7812
|
capabilities: {
|
|
6566
7813
|
authorization: 2,
|
|
6567
|
-
serverGrants: true
|
|
7814
|
+
serverGrants: true,
|
|
7815
|
+
...config.devices ? { devices: true } : {}
|
|
6568
7816
|
},
|
|
6569
7817
|
mode: authorizationContext.surface === "broker" ? "broker" : "bridge",
|
|
6570
7818
|
app: {
|
|
@@ -7830,4 +9078,4 @@ function generationOutputSource(result, outputIndex) {
|
|
|
7830
9078
|
return `data:${declared && MEDIA_TYPE_PATTERN.test(declared) ? declared : DEFAULT_MEDIA_TYPES[String(block.type)] ?? "application/octet-stream"};base64,${data}`;
|
|
7831
9079
|
}
|
|
7832
9080
|
//#endregion
|
|
7833
|
-
export { APP_APPEARANCE_TOKENS, APP_COMPOSER_CHIP_CONTENT_MAX_BYTES, APP_COMPOSER_CHIP_KEY_MAX_LENGTH, APP_COMPOSER_CHIP_LABEL_MAX_LENGTH, APP_SURFACE_READY_TIMEOUT_MS, APP_SURFACE_REQUEST_TIMEOUT_MS, AppCommerceApi, AppRealtimeApi, AppRefParseError, AppRoom, AppRuntimeApi, AppRuntimeApi as WorkRuntimeApi, AppRuntimeError, AppSurfaceApi, AppWindowApi, AppsApi, BILLING_ACCESS_BLOCKED_ERROR_CODE, BOARD_COLOR_TOKENS, BOARD_ITEM_TYPES, BillingApi, BoardAnimationSchema, BoardClient, BoardPatchSchema, BoardTrackSchema, COHUB_ENVIRONMENTS, COHUB_SOURCE_HEADER, COHUB_SOURCE_HEADER_NAMES, CohubClient, CohubHttpClient, DESKTOP_COMMAND_ACCEPT_TIMEOUT_MS, DESKTOP_COMMAND_DEFAULT_TIMEOUT_MS, DESKTOP_COMMAND_MAX_TIMEOUT_MS, DESKTOP_COMMAND_OPEN_TIMEOUT_MS, DESKTOP_COMMAND_PAYLOAD_MAX_BYTES, DESKTOP_COMMAND_PENDING_TTL_SECONDS, DESKTOP_COMMAND_SETTLEMENT_GRACE_SECONDS, DESKTOP_COMMAND_TERMINAL_TTL_SECONDS, DESKTOP_COMMAND_VERSION, DISPLAY_ACTION_TYPES, DISPLAY_ELEMENT_REF_PATTERN, DISPLAY_SYSTEM_ACTIONS, DesktopCommandsApi, DisplayActionError, DisplayConnection, FEATURE_NOT_ENTITLED_ERROR_CODE, GenerationPolicyError, HOME_SPACE_SLUG, HttpError, NATIVE_INGEST_MAX_TURNS, NATIVE_SYNC_SOURCE, OriginBrokerTransport, PERMISSIONS, ParentBridgeTransport, PopupBrokerTransport, REQUEST_SOURCE_VIA_MAX_LENGTH, RUNTIME_ARCHIVE_SEGMENT_BYTES, RUNTIME_MAX_FRAME_BYTES, RUNTIME_PROTOCOL_VERSION, RUNTIME_RECOVERY_BATCH_SIZE, ReferencesApi, ReferralsApi, SETTLED_TURN_STATUSES, SessionGenerationStreamClient, SessionPatchReducer, SpaceDisplaysApi, SpacePublicFilesApi, SpaceWebhooksApi, UPLOAD_MAX_BATCH_BYTES, UPLOAD_MAX_BATCH_FILES, UPLOAD_MAX_FILE_BYTES, USER_LEVEL_PERMISSIONS, UiCommandsApi, UsersApi, VoiceApi, VoiceInputClient, WebsocketClient, WorkCommerceApi, WorkRealtimeApi, WorkRefParseError, WorkRoom, WorkSurfaceApi, appAppearanceVar, applyAppAppearance, assertGenerationRequestAllowedByPolicy, attachAppEmbed, buildAppSurfaceRequest, buildSpaceInvitePath, buildSpacePath, clearGrantedAppScopes, compileDisplayActions, connectDisplay, contextToPiMessages, createAppBridgeCore, createAppBridgeCore as createWorkBridgeCore, createAppRuntime, createAppRuntime as createWorkRuntime, createCohubClient, createHttpClient, createOriginAppResolver, createSessionGenerationStreamClient, createSessionPatchReducer, createSlugAppIdResolver, createSlugAppIdResolver as createSlugWorkIdResolver, createVoiceInputClient, createWebsocketClient, decodeGenerationPolicy, defaultDesktopCommandTimeoutMs, encodeGenerationPolicy, extractBillingPayload, filterDiscoverableGenerationModels, filterGenerationDeclarationsByPolicy, filterSpacePickerItems, findGenerationModelPolicy, fingerprintProjectionTurns, formatAppRef, formatWorkRef, generationOutputSource, getAllowedGenerationModelIds, getCohubContext, harnessArchiveIndexSchema, hasGrantedAppScopes, hasRequestSourceIdentity, isActiveGenerationTask, isAppId, isBillingAccessBlockedCode, isBillingAccessBlockedError, isDesktopCallMethod, isFeatureNotEntitledError, isGenerationModelHidden, isHttpErrorCode, isLocalHarness, isNativeClientTurn, isProjectionCompaction, isRequestSourceClientId, isRequestSourceEmpty, isRequestSourceUuid, isTerminalDesktopCommandStatus, isUserLevelPermission, isUuid, isWorkId, joinApiUrl, matchesUnauthorizedErrorToken, mergeRequestSourceIntoMeta, nativeIngestTurnSchema, nativeRuntimeEventSchema, nativeTurnCompleteSchema, nativeTurnProgressSchema, normalizeBaseUrl, normalizeDisplayPoint, normalizeGenerationPolicy, normalizeRequestSource, normalizeSessionTurnOrigin, normalizeSpacePickerQuery, normalizeVoiceInputWebsocketUrl, normalizeWebsocketUrl, orderSpacePickerItems, parseAppRef, parseAppSurfaceReady, parseAppSurfaceResponse, parseAssistantMessageCommit, parseBoardDocument, parseBoardItem, parseDesktopCommand, parseGenerationPolicyFromEnv, parseRequestSourceFromHeaders, parseSpaceRef, parseWorkRef, projectNativeMessageMeta, projectNativeSession, readRequestSourceFromEnv, readSentTurns, readSessionTurnOrigin, requestSourceToHeaders, resolveApiBaseUrl, resolveAppTransport, resolveAppTransport as resolveWorkTransport, resolveCohubEnvironment, resolveExecutionAppId, resolveExecutionToken, resolveHarness, resolveOpenSurface, resolveRequestSourceChannel, resolveVoiceInputWebsocketUrl, resolveWebBaseUrl, resolveWebsocketUrl, runtimeCommandSchema, runtimeEventSchema, runtimeNativeStopSchema, runtimeReadySchema, sanitizeAccessToken, scopeListHasPermission, selectRuntimeContextMessages, selectSpacePickerItems, serializeProjection, serializeProjectionRecords, setGrantedAppScopes, toGenerationTaskView, trimProjectionTurnsToCompaction, validateArchiveBoundary };
|
|
9081
|
+
export { APP_APPEARANCE_TOKENS, APP_COMPOSER_CHIP_CONTENT_MAX_BYTES, APP_COMPOSER_CHIP_KEY_MAX_LENGTH, APP_COMPOSER_CHIP_LABEL_MAX_LENGTH, APP_DEVICES, APP_SURFACE_READY_TIMEOUT_MS, APP_SURFACE_REQUEST_TIMEOUT_MS, AppCommerceApi, AppRealtimeApi, AppRefParseError, AppRoom, AppRoomRtc, AppRtcChannel, AppRtcError, AppRuntimeApi, AppRuntimeApi as WorkRuntimeApi, AppRuntimeError, AppSurfaceApi, AppWindowApi, AppsApi, BILLING_ACCESS_BLOCKED_ERROR_CODE, BOARD_COLOR_TOKENS, BOARD_ITEM_TYPES, BillingApi, BoardAnimationSchema, BoardClient, BoardPatchSchema, BoardTrackSchema, COHUB_ENVIRONMENTS, COHUB_SOURCE_HEADER, COHUB_SOURCE_HEADER_NAMES, CohubClient, CohubHttpClient, DESKTOP_COMMAND_ACCEPT_TIMEOUT_MS, DESKTOP_COMMAND_DEFAULT_TIMEOUT_MS, DESKTOP_COMMAND_MAX_TIMEOUT_MS, DESKTOP_COMMAND_OPEN_TIMEOUT_MS, DESKTOP_COMMAND_PAYLOAD_MAX_BYTES, DESKTOP_COMMAND_PENDING_TTL_SECONDS, DESKTOP_COMMAND_SETTLEMENT_GRACE_SECONDS, DESKTOP_COMMAND_TERMINAL_TTL_SECONDS, DESKTOP_COMMAND_VERSION, DISPLAY_ACTION_TYPES, DISPLAY_ELEMENT_REF_PATTERN, DISPLAY_SYSTEM_ACTIONS, DesktopCommandsApi, DisplayActionError, DisplayConnection, FEATURE_NOT_ENTITLED_ERROR_CODE, GenerationPolicyError, HOME_SPACE_SLUG, HttpError, NATIVE_INGEST_MAX_TURNS, NATIVE_SYNC_SOURCE, OriginBrokerTransport, PERMISSIONS, ParentBridgeTransport, PopupBrokerTransport, REQUEST_SOURCE_VIA_MAX_LENGTH, RUNTIME_ARCHIVE_SEGMENT_BYTES, RUNTIME_MAX_FRAME_BYTES, RUNTIME_PROTOCOL_VERSION, RUNTIME_RECOVERY_BATCH_SIZE, ReferencesApi, ReferralsApi, RoomsApi, SETTLED_TURN_STATUSES, SessionGenerationStreamClient, SessionPatchReducer, SpaceDisplaysApi, SpacePublicFilesApi, SpaceWebhooksApi, UPLOAD_MAX_BATCH_BYTES, UPLOAD_MAX_BATCH_FILES, UPLOAD_MAX_FILE_BYTES, USER_LEVEL_PERMISSIONS, UiCommandsApi, UsersApi, VoiceApi, VoiceInputClient, WebsocketClient, WorkCommerceApi, WorkRealtimeApi, WorkRefParseError, WorkRoom, WorkSurfaceApi, appAppearanceVar, applyAppAppearance, assertGenerationRequestAllowedByPolicy, attachAppEmbed, buildAppSurfaceRequest, buildSpaceInvitePath, buildSpacePath, clearGrantedAppScopes, compileDisplayActions, connectDisplay, contextToPiMessages, createAppBridgeCore, createAppBridgeCore as createWorkBridgeCore, createAppRuntime, createAppRuntime as createWorkRuntime, createCohubClient, createHttpClient, createOriginAppResolver, createSessionGenerationStreamClient, createSessionPatchReducer, createSlugAppIdResolver, createSlugAppIdResolver as createSlugWorkIdResolver, createVoiceInputClient, createWebsocketClient, decodeGenerationPolicy, defaultDesktopCommandTimeoutMs, encodeGenerationPolicy, extractBillingPayload, filterDiscoverableGenerationModels, filterGenerationDeclarationsByPolicy, filterSpacePickerItems, findGenerationModelPolicy, fingerprintProjectionTurns, formatAppRef, formatWorkRef, generationOutputSource, getAllowedGenerationModelIds, getCohubContext, harnessArchiveIndexSchema, hasGrantedAppScopes, hasRequestSourceIdentity, isActiveGenerationTask, isAppId, isBillingAccessBlockedCode, isBillingAccessBlockedError, isDesktopCallMethod, isFeatureNotEntitledError, isGenerationModelHidden, isHttpErrorCode, isLocalHarness, isNativeClientTurn, isProjectionCompaction, isRequestSourceClientId, isRequestSourceEmpty, isRequestSourceUuid, isTerminalDesktopCommandStatus, isUserLevelPermission, isUuid, isWorkId, joinApiUrl, matchesUnauthorizedErrorToken, mergeRequestSourceIntoMeta, nativeIngestTurnSchema, nativeRuntimeEventSchema, nativeTurnCompleteSchema, nativeTurnProgressSchema, normalizeBaseUrl, normalizeDisplayPoint, normalizeGenerationPolicy, normalizeRequestSource, normalizeSessionTurnOrigin, normalizeSpacePickerQuery, normalizeVoiceInputWebsocketUrl, normalizeWebsocketUrl, orderSpacePickerItems, parseAppRef, parseAppSurfaceReady, parseAppSurfaceResponse, parseAssistantMessageCommit, parseBoardDocument, parseBoardItem, parseDesktopCommand, parseGenerationPolicyFromEnv, parseRequestSourceFromHeaders, parseSpaceRef, parseWorkRef, projectNativeMessageMeta, projectNativeSession, readRequestSourceFromEnv, readSentTurns, readSessionTurnOrigin, requestSourceToHeaders, resolveApiBaseUrl, resolveAppTransport, resolveAppTransport as resolveWorkTransport, resolveCohubEnvironment, resolveExecutionAppId, resolveExecutionToken, resolveHarness, resolveOpenSurface, resolveRequestSourceChannel, resolveVoiceInputWebsocketUrl, resolveWebBaseUrl, resolveWebsocketUrl, runtimeCommandSchema, runtimeEventSchema, runtimeNativeStopSchema, runtimeReadySchema, sanitizeAccessToken, scopeListHasPermission, selectRuntimeContextMessages, selectSpacePickerItems, serializeProjection, serializeProjectionRecords, setGrantedAppScopes, toGenerationTaskView, trimProjectionTurnsToCompaction, validateArchiveBoundary };
|