@neta-art/cohub 10.0.0 → 11.1.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 +42 -7
- 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 +7 -3
- package/dist/board/editor/creation.js +5 -4
- 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 +22 -7
- package/dist/board/editor/editor.js +12 -2
- package/dist/board/editor/index.d.ts +2 -2
- package/dist/board/editor/interaction.d.ts +8 -2
- package/dist/board/editor/interaction.js +34 -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/editor/snapshots.d.ts +0 -2
- package/dist/board/editor/snapshots.js +4 -20
- 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 +24 -7
- 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 +7 -3
- package/dist/board/model/index.d.ts +15 -7
- package/dist/board/model/index.js +16 -9
- package/dist/board/model/items/builtin.js +2 -2
- package/dist/board/model/items/create.d.ts +19 -4
- package/dist/board/model/items/create.js +130 -9
- 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 +15 -5
- package/dist/board/model/media.js +7 -1
- package/dist/board/model/overlay-placement.d.ts +25 -0
- package/dist/board/model/overlay-placement.js +35 -0
- package/dist/board/model/replay.d.ts +1 -1
- package/dist/board/model/replay.js +1 -0
- 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 +27 -0
- package/dist/board/model/script-hit.js +109 -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-observers.d.ts +18 -0
- package/dist/board/model/script-observers.js +92 -0
- package/dist/board/model/script-scene.d.ts +24 -0
- package/dist/board/model/script-scene.js +294 -0
- package/dist/board/model/script.d.ts +149 -0
- package/dist/board/model/script.js +1 -0
- package/dist/board/render/board-background.js +5 -4
- 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 +10 -7
- package/dist/board/render/media-interaction.js +1 -1
- 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 +25 -5
- package/dist/board/render/renderers/board-renderer-registry.js +11 -4
- 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} +18 -16
- 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/task-card-renderer.d.ts +2 -1
- package/dist/board/render/renderers/task-card-renderer.js +220 -71
- 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 +446 -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 +223 -0
- package/dist/board/render/script-sandbox.d.ts +4 -0
- package/dist/board/render/script-sandbox.js +173 -0
- package/dist/board/render/script-texture.d.ts +11 -0
- package/dist/board/render/script-texture.js +33 -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 +7 -2
- package/dist/board/stage/maintenance.d.ts +5 -0
- package/dist/board/stage/maintenance.js +11 -0
- package/dist/board/stage/scene.d.ts +2 -0
- package/dist/board/stage/scene.js +21 -2
- package/dist/board/stage/script-pointer.d.ts +20 -0
- package/dist/board/stage/script-pointer.js +41 -0
- package/dist/board/stage/stage.d.ts +21 -3
- package/dist/board/stage/stage.js +350 -47
- package/dist/board/stage/texture-pool.js +2 -3
- package/dist/chunks/environment.d.ts +305 -27
- package/dist/chunks/environment.js +12 -4
- package/dist/chunks/host-bridge.d.ts +2 -2
- package/dist/chunks/http.d.ts +90 -7
- package/dist/chunks/http.js +157 -64
- package/dist/chunks/transport.js +4 -2
- 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 +3 -3
- package/dist/index.d.ts +182 -4
- package/dist/index.js +1559 -299
- 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 +236 -11
- package/dist/protocol/dist/board-model.js +46 -17
- 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/board-task.d.ts +22 -0
- package/dist/{board/model/task.js → protocol/dist/board-task.js} +34 -7
- package/dist/protocol/dist/display/index.d.ts +1 -0
- package/dist/protocol/dist/generation/blocks.d.ts +8 -0
- package/dist/{generation-blocks.js → protocol/dist/generation/blocks.js} +14 -15
- package/dist/protocol/dist/index.d.ts +7 -2
- package/dist/protocol/dist/realtime/types.d.ts +1 -0
- package/dist/protocol/dist/rtc/index.d.ts +1 -0
- package/dist/protocol/dist/task/index.d.ts +4 -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/model/task.d.ts +0 -11
- 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/types.d.ts +0 -36
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(),
|
|
@@ -274,8 +381,16 @@ const fileProps = {
|
|
|
274
381
|
coverUrl: z.string().optional()
|
|
275
382
|
}).strict().optional()
|
|
276
383
|
};
|
|
277
|
-
const
|
|
384
|
+
const BoardMediaOriginSchema = z.object({
|
|
385
|
+
taskRunId: z.string().min(1).max(160),
|
|
386
|
+
artifactId: z.string().min(1).max(240)
|
|
387
|
+
}).strict();
|
|
388
|
+
const mediaProps = {
|
|
278
389
|
...fileProps,
|
|
390
|
+
origin: BoardMediaOriginSchema.optional()
|
|
391
|
+
};
|
|
392
|
+
const timedFileProps = {
|
|
393
|
+
...mediaProps,
|
|
279
394
|
time: nonNegative.optional()
|
|
280
395
|
};
|
|
281
396
|
const BoardCropSchema = z.object({
|
|
@@ -288,7 +403,7 @@ const BoardImageItemSchema = z.object({
|
|
|
288
403
|
...boxed(640, 360),
|
|
289
404
|
type: z.literal("image"),
|
|
290
405
|
props: z.object({
|
|
291
|
-
...
|
|
406
|
+
...mediaProps,
|
|
292
407
|
crop: BoardCropSchema.optional()
|
|
293
408
|
}).strict()
|
|
294
409
|
}).strict();
|
|
@@ -353,8 +468,10 @@ const BoardTaskSnapshotSchema = z.object({
|
|
|
353
468
|
title: z.string().min(1).max(240),
|
|
354
469
|
model: z.string().max(160).optional(),
|
|
355
470
|
promptExcerpt: z.string().max(480).optional(),
|
|
471
|
+
errorExcerpt: z.string().max(240).optional(),
|
|
356
472
|
artifactCount: z.number().int().nonnegative(),
|
|
357
473
|
artifacts: z.array(BoardTaskArtifactSchema).max(6).default([]),
|
|
474
|
+
startedAt: z.string().max(64).optional(),
|
|
358
475
|
updatedAt: z.string().optional()
|
|
359
476
|
}).strict();
|
|
360
477
|
const BoardTaskItemSchema = z.object({
|
|
@@ -362,7 +479,7 @@ const BoardTaskItemSchema = z.object({
|
|
|
362
479
|
type: z.literal("task"),
|
|
363
480
|
props: z.object({
|
|
364
481
|
taskRunId: z.string().min(1).max(160),
|
|
365
|
-
snapshot: BoardTaskSnapshotSchema
|
|
482
|
+
snapshot: BoardTaskSnapshotSchema.describe("Maintained by the server for tasks in the Board's Space; writers may omit it.")
|
|
366
483
|
}).strict()
|
|
367
484
|
}).strict();
|
|
368
485
|
const BOARD_EFFECT_KINDS = [
|
|
@@ -389,14 +506,11 @@ const BoardEffectItemSchema = z.object({
|
|
|
389
506
|
seed: z.string().min(1).max(160).optional()
|
|
390
507
|
}).strict()
|
|
391
508
|
}).strict();
|
|
392
|
-
const
|
|
509
|
+
const BoardCustomItemSchema = z.object({
|
|
393
510
|
...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()
|
|
511
|
+
type: z.literal("custom"),
|
|
512
|
+
script: BoardScriptSchema,
|
|
513
|
+
props: z.object({}).strict().default({})
|
|
400
514
|
}).strict();
|
|
401
515
|
const BOARD_EXTENSION_TYPE_PATTERN = /^[a-z][a-z0-9]*(?:\.[a-z0-9-]+)+$/;
|
|
402
516
|
const BoardExtensionItemSchema = z.object({
|
|
@@ -420,7 +534,7 @@ const BOARD_ITEM_SCHEMAS = {
|
|
|
420
534
|
file: BoardFileItemSchema,
|
|
421
535
|
task: BoardTaskItemSchema,
|
|
422
536
|
effect: BoardEffectItemSchema,
|
|
423
|
-
|
|
537
|
+
custom: BoardCustomItemSchema
|
|
424
538
|
};
|
|
425
539
|
const BOARD_ITEM_TYPES = Object.keys(BOARD_ITEM_SCHEMAS);
|
|
426
540
|
function isBuiltinItemType(type) {
|
|
@@ -535,7 +649,7 @@ const BoardAnimationHeaderSchema = z.object({
|
|
|
535
649
|
end: z.enum(["hold", "reset"]).default("hold"),
|
|
536
650
|
markers: z.array(BoardMarkerSchema).max(1e4).default([])
|
|
537
651
|
}).strict();
|
|
538
|
-
const BoardAnimationSchema = BoardAnimationHeaderSchema.extend({ tracks: z.record(
|
|
652
|
+
const BoardAnimationSchema = BoardAnimationHeaderSchema.extend({ tracks: z.record(BoardKeySchema, BoardTrackSchema).default({}) }).strict();
|
|
539
653
|
const BOARD_ENTER_PRESETS = [
|
|
540
654
|
"fade-in",
|
|
541
655
|
"rise",
|
|
@@ -575,7 +689,7 @@ const patchObject = z.record(z.string(), z.unknown());
|
|
|
575
689
|
const BoardPatchSchema = z.object({
|
|
576
690
|
board: patchObject.nullable().optional(),
|
|
577
691
|
items: z.record(BoardIdSchema, patchObject.nullable()).optional(),
|
|
578
|
-
animations: z.record(BoardIdSchema, patchObject.and(z.object({ tracks: z.record(
|
|
692
|
+
animations: z.record(BoardIdSchema, patchObject.and(z.object({ tracks: z.record(BoardKeySchema, patchObject.nullable()).optional() })).nullable()).optional()
|
|
579
693
|
}).strict();
|
|
580
694
|
//#endregion
|
|
581
695
|
//#region ../protocol/dist/board-json.js
|
|
@@ -1119,7 +1233,8 @@ const NOT_ANIMATABLE = /* @__PURE__ */ new Set([
|
|
|
1119
1233
|
"parent",
|
|
1120
1234
|
"z",
|
|
1121
1235
|
"locked",
|
|
1122
|
-
"metadata"
|
|
1236
|
+
"metadata",
|
|
1237
|
+
"script"
|
|
1123
1238
|
]);
|
|
1124
1239
|
const CAMERA_PROPERTIES = {
|
|
1125
1240
|
focus: {
|
|
@@ -1646,96 +1761,132 @@ function parseBoardDocument(input) {
|
|
|
1646
1761
|
} : result;
|
|
1647
1762
|
}
|
|
1648
1763
|
//#endregion
|
|
1649
|
-
//#region ../protocol/dist/
|
|
1650
|
-
const
|
|
1651
|
-
function
|
|
1652
|
-
|
|
1653
|
-
return parts.length === 4 && parts.every((part) => Number.isInteger(part) && part >= 0 && part <= 255) ? parts : null;
|
|
1764
|
+
//#region ../protocol/dist/generation/blocks.js
|
|
1765
|
+
const EXCERPT_LIMIT = 240;
|
|
1766
|
+
function asRecord(value) {
|
|
1767
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
1654
1768
|
}
|
|
1655
|
-
function
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
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;
|
|
1769
|
+
function cleanExcerpt(value, limit = EXCERPT_LIMIT) {
|
|
1770
|
+
if (typeof value !== "string") return void 0;
|
|
1771
|
+
const clean = value.replace(/\s+/g, " ").trim();
|
|
1772
|
+
if (!clean) return void 0;
|
|
1773
|
+
return clean.length > limit ? `${clean.slice(0, limit - 3).trimEnd()}...` : clean;
|
|
1670
1774
|
}
|
|
1671
|
-
function
|
|
1672
|
-
|
|
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"));
|
|
1775
|
+
function blockText(block) {
|
|
1776
|
+
return cleanExcerpt(block.text ?? block.content ?? block.value);
|
|
1685
1777
|
}
|
|
1686
|
-
function
|
|
1687
|
-
const
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1778
|
+
function blockUrl(block) {
|
|
1779
|
+
const source = asRecord(block.source);
|
|
1780
|
+
return normalizeBoardRemoteUrl(source?.url ?? source?.src ?? block.url ?? block.src);
|
|
1781
|
+
}
|
|
1782
|
+
function blockMimeType(block) {
|
|
1783
|
+
const source = asRecord(block.source);
|
|
1784
|
+
const value = source?.mediaType ?? source?.media_type ?? source?.mimeType ?? block.mediaType ?? block.media_type ?? block.mimeType;
|
|
1785
|
+
return typeof value === "string" ? value : void 0;
|
|
1786
|
+
}
|
|
1787
|
+
function positiveNumber(value) {
|
|
1788
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : void 0;
|
|
1789
|
+
}
|
|
1790
|
+
function blockNaturalSize(block) {
|
|
1791
|
+
const source = asRecord(block.source);
|
|
1792
|
+
const naturalWidth = positiveNumber(source?.width ?? source?.naturalWidth ?? block.width ?? block.naturalWidth);
|
|
1793
|
+
const naturalHeight = positiveNumber(source?.height ?? source?.naturalHeight ?? block.height ?? block.naturalHeight);
|
|
1794
|
+
return {
|
|
1795
|
+
...naturalWidth ? { naturalWidth } : {},
|
|
1796
|
+
...naturalHeight ? { naturalHeight } : {}
|
|
1797
|
+
};
|
|
1798
|
+
}
|
|
1799
|
+
function contentBlocks(value) {
|
|
1800
|
+
return Array.isArray(value) ? value.filter((item) => asRecord(item)) : [];
|
|
1801
|
+
}
|
|
1802
|
+
function taskData(run) {
|
|
1803
|
+
const payload = asRecord(run.payload);
|
|
1804
|
+
return asRecord(payload?.data) ?? payload;
|
|
1805
|
+
}
|
|
1806
|
+
function generationOutput(run) {
|
|
1807
|
+
return contentBlocks(asRecord(run.result)?.output);
|
|
1808
|
+
}
|
|
1809
|
+
function generationPrompt(run) {
|
|
1810
|
+
for (const block of contentBlocks(taskData(run)?.content)) {
|
|
1811
|
+
const text = blockText(block);
|
|
1812
|
+
if (text) return text;
|
|
1695
1813
|
}
|
|
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
1814
|
}
|
|
1703
|
-
function
|
|
1704
|
-
|
|
1705
|
-
if (parseIpv4(address)) return !isBlockedIpv4(address);
|
|
1706
|
-
return address.includes(":") && !isBlockedIpv6(address);
|
|
1815
|
+
function blockMeta(block) {
|
|
1816
|
+
return asRecord(block.meta);
|
|
1707
1817
|
}
|
|
1708
|
-
function
|
|
1709
|
-
const
|
|
1710
|
-
|
|
1711
|
-
if (
|
|
1712
|
-
return
|
|
1818
|
+
function blockIdentity(block) {
|
|
1819
|
+
const meta = blockMeta(block);
|
|
1820
|
+
const value = meta?.id ?? meta?.clip_id ?? meta?.clipId ?? block.id ?? block.clip_id ?? block.clipId;
|
|
1821
|
+
if (typeof value !== "string" && typeof value !== "number") return void 0;
|
|
1822
|
+
return cleanExcerpt(String(value), 220);
|
|
1823
|
+
}
|
|
1824
|
+
function blockTitle(block) {
|
|
1825
|
+
return cleanExcerpt(blockMeta(block)?.title ?? block.title ?? block.name, 240);
|
|
1826
|
+
}
|
|
1827
|
+
function blockDurationMs(block) {
|
|
1828
|
+
const source = asRecord(block.source);
|
|
1829
|
+
const meta = blockMeta(block);
|
|
1830
|
+
const milliseconds = positiveNumber(source?.durationMs ?? meta?.durationMs ?? block.durationMs);
|
|
1831
|
+
if (milliseconds) return Math.round(milliseconds);
|
|
1832
|
+
const seconds = positiveNumber(source?.duration ?? meta?.duration ?? block.duration);
|
|
1833
|
+
return seconds ? Math.round(seconds * 1e3) : void 0;
|
|
1834
|
+
}
|
|
1835
|
+
function blockPreviewUrl(block) {
|
|
1836
|
+
const source = asRecord(block.source);
|
|
1837
|
+
return normalizeBoardRemoteUrl(source?.poster ?? source?.thumbnail ?? source?.previewUrl ?? block.poster ?? block.thumbnail ?? block.previewUrl);
|
|
1713
1838
|
}
|
|
1714
1839
|
/**
|
|
1715
|
-
*
|
|
1716
|
-
*
|
|
1717
|
-
* resolution to defend against rebinding.
|
|
1840
|
+
* Image roles that depict playable media, by the media types they may cover;
|
|
1841
|
+
* `last_frame` stays a result of its own.
|
|
1718
1842
|
*/
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1843
|
+
const COVER_ROLES = /* @__PURE__ */ new Map([
|
|
1844
|
+
["first_frame", /* @__PURE__ */ new Set(["video"])],
|
|
1845
|
+
["cover", /* @__PURE__ */ new Set(["video", "audio"])],
|
|
1846
|
+
["poster", /* @__PURE__ */ new Set(["video", "audio"])],
|
|
1847
|
+
["thumbnail", /* @__PURE__ */ new Set(["video", "audio"])]
|
|
1848
|
+
]);
|
|
1849
|
+
function isPlayable(block) {
|
|
1850
|
+
return block.type === "video" || block.type === "audio";
|
|
1851
|
+
}
|
|
1852
|
+
/**
|
|
1853
|
+
* Pair playable media with its cover image, returning media index → cover
|
|
1854
|
+
* index. An image sharing the provider id wins (music tracks and their art,
|
|
1855
|
+
* shared by every media of that id); otherwise cover-role images pair in
|
|
1856
|
+
* order with remaining media their role fits (a video and its `first_frame`).
|
|
1857
|
+
* Covers need a remote URL, so an inline image is never folded away.
|
|
1858
|
+
*/
|
|
1859
|
+
function generationCovers(blocks) {
|
|
1860
|
+
const covers = /* @__PURE__ */ new Map();
|
|
1861
|
+
const isCover = (block) => block.type === "image" && blockUrl(block) !== void 0 && blockMeta(block)?.role !== "last_frame";
|
|
1862
|
+
blocks.forEach((block, index) => {
|
|
1863
|
+
const id = blockIdentity(block);
|
|
1864
|
+
if (!isPlayable(block) || id === void 0) return;
|
|
1865
|
+
const cover = blocks.findIndex((candidate) => isCover(candidate) && blockIdentity(candidate) === id);
|
|
1866
|
+
if (cover >= 0) covers.set(index, cover);
|
|
1867
|
+
});
|
|
1868
|
+
const paired = new Set(covers.values());
|
|
1869
|
+
const roleCovers = blocks.flatMap((block, index) => {
|
|
1870
|
+
const role = blockMeta(block)?.role;
|
|
1871
|
+
const types = typeof role === "string" ? COVER_ROLES.get(role) : void 0;
|
|
1872
|
+
return isCover(block) && !paired.has(index) && types ? [{
|
|
1873
|
+
index,
|
|
1874
|
+
types
|
|
1875
|
+
}] : [];
|
|
1876
|
+
});
|
|
1877
|
+
blocks.forEach((block, index) => {
|
|
1878
|
+
if (!isPlayable(block) || covers.has(index)) return;
|
|
1879
|
+
const at = roleCovers.findIndex(({ types }) => types.has(String(block.type)));
|
|
1880
|
+
const [cover] = at < 0 ? [] : roleCovers.splice(at, 1);
|
|
1881
|
+
if (cover) covers.set(index, cover.index);
|
|
1882
|
+
});
|
|
1883
|
+
return covers;
|
|
1732
1884
|
}
|
|
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
1885
|
//#endregion
|
|
1735
1886
|
//#region ../protocol/dist/turn-origin.js
|
|
1736
|
-
const record$
|
|
1887
|
+
const record$1 = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
1737
1888
|
const normalizeSessionTurnOrigin = (value) => {
|
|
1738
|
-
const input = record$
|
|
1889
|
+
const input = record$1(value);
|
|
1739
1890
|
if (!input || ![
|
|
1740
1891
|
"prompt",
|
|
1741
1892
|
"scheduled_prompt",
|
|
@@ -1755,12 +1906,12 @@ const normalizeSessionTurnOrigin = (value) => {
|
|
|
1755
1906
|
};
|
|
1756
1907
|
/** Reads durable provenance; legacy background tasks only knew their caller's Session/Turn. */
|
|
1757
1908
|
const readSessionTurnOrigin = (meta, spaceId) => {
|
|
1758
|
-
const input = record$
|
|
1909
|
+
const input = record$1(meta);
|
|
1759
1910
|
const origin = normalizeSessionTurnOrigin(input?.origin);
|
|
1760
1911
|
if (origin) return origin;
|
|
1761
|
-
const context = record$
|
|
1912
|
+
const context = record$1(input?.context);
|
|
1762
1913
|
if (context?.kind !== "background_bash_task" || !isUuidLike(spaceId)) return null;
|
|
1763
|
-
const legacy = record$
|
|
1914
|
+
const legacy = record$1(context.origin);
|
|
1764
1915
|
return normalizeSessionTurnOrigin({
|
|
1765
1916
|
...legacy,
|
|
1766
1917
|
spaceId,
|
|
@@ -1775,9 +1926,9 @@ const KINDS = [
|
|
|
1775
1926
|
"background_task",
|
|
1776
1927
|
"hook"
|
|
1777
1928
|
];
|
|
1778
|
-
const record
|
|
1929
|
+
const record = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
1779
1930
|
const normalizeSentTurnRef = (value) => {
|
|
1780
|
-
const input = record
|
|
1931
|
+
const input = record(value);
|
|
1781
1932
|
if (!input) return null;
|
|
1782
1933
|
if (!isUuidLike(input.sessionId) || !isUuidLike(input.turnId)) return null;
|
|
1783
1934
|
const kind = String(input.kind);
|
|
@@ -1796,7 +1947,7 @@ const normalizeSentTurnRef = (value) => {
|
|
|
1796
1947
|
};
|
|
1797
1948
|
/** Deduplicates by child Turn, so a retried request cannot render one twice. */
|
|
1798
1949
|
const readSentTurns = (meta) => {
|
|
1799
|
-
const raw = record
|
|
1950
|
+
const raw = record(meta)?.messagesSent;
|
|
1800
1951
|
if (!Array.isArray(raw)) return [];
|
|
1801
1952
|
const seen = /* @__PURE__ */ new Set();
|
|
1802
1953
|
const refs = [];
|
|
@@ -2030,6 +2181,102 @@ function compileDisplayActions(actions, width, height) {
|
|
|
2030
2181
|
if (events.length === 0) throw new DisplayActionError("no action touches the display");
|
|
2031
2182
|
return events;
|
|
2032
2183
|
}
|
|
2184
|
+
z.object({
|
|
2185
|
+
urls: z.union([z.string().min(1).max(512), z.array(z.string().min(1).max(512)).min(1).max(16)]),
|
|
2186
|
+
username: z.string().max(512).optional(),
|
|
2187
|
+
credential: z.string().max(512).optional()
|
|
2188
|
+
});
|
|
2189
|
+
const sessionDescriptionSchema = z.object({
|
|
2190
|
+
type: z.enum(["offer", "answer"]),
|
|
2191
|
+
sdp: z.string().min(1).max(65536)
|
|
2192
|
+
});
|
|
2193
|
+
const APP_RTC_DELIVERIES = ["reliable", "latest"];
|
|
2194
|
+
const APP_RTC_PUBLICATION_KINDS = [
|
|
2195
|
+
"data",
|
|
2196
|
+
"audio",
|
|
2197
|
+
"video"
|
|
2198
|
+
];
|
|
2199
|
+
const APP_RTC_NAME_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,63}$/;
|
|
2200
|
+
const isAppRtcName = (value) => typeof value === "string" && APP_RTC_NAME_PATTERN.test(value) && !value.toLowerCase().startsWith("cohub.");
|
|
2201
|
+
const APP_RTC_LIMITS = {
|
|
2202
|
+
channelsPerMember: 16,
|
|
2203
|
+
tracksPerMember: 4,
|
|
2204
|
+
dataSubscriptions: 512,
|
|
2205
|
+
mediaSubscriptions: 16,
|
|
2206
|
+
messageBytes: 65536,
|
|
2207
|
+
operationsPerSecond: 10,
|
|
2208
|
+
opensPerSecond: 2,
|
|
2209
|
+
batch: 64
|
|
2210
|
+
};
|
|
2211
|
+
const APP_RTC_QUALITIES = [
|
|
2212
|
+
"high",
|
|
2213
|
+
"medium",
|
|
2214
|
+
"low"
|
|
2215
|
+
];
|
|
2216
|
+
const APP_RTC_SIMULCAST_LAYERS = [
|
|
2217
|
+
{
|
|
2218
|
+
rid: "f",
|
|
2219
|
+
quality: "high",
|
|
2220
|
+
scaleResolutionDownBy: 1
|
|
2221
|
+
},
|
|
2222
|
+
{
|
|
2223
|
+
rid: "h",
|
|
2224
|
+
quality: "medium",
|
|
2225
|
+
scaleResolutionDownBy: 2
|
|
2226
|
+
},
|
|
2227
|
+
{
|
|
2228
|
+
rid: "q",
|
|
2229
|
+
quality: "low",
|
|
2230
|
+
scaleResolutionDownBy: 4
|
|
2231
|
+
}
|
|
2232
|
+
];
|
|
2233
|
+
z.object({
|
|
2234
|
+
id: z.string().min(1).max(32),
|
|
2235
|
+
name: z.string().regex(APP_RTC_NAME_PATTERN),
|
|
2236
|
+
kind: z.enum(APP_RTC_PUBLICATION_KINDS),
|
|
2237
|
+
delivery: z.enum(APP_RTC_DELIVERIES).optional(),
|
|
2238
|
+
simulcast: z.boolean().optional()
|
|
2239
|
+
});
|
|
2240
|
+
const nameSchema = z.string().refine(isAppRtcName, "invalid name");
|
|
2241
|
+
const participantIdSchema = z.string().uuid();
|
|
2242
|
+
z.object({
|
|
2243
|
+
ticket: z.string().min(1).max(4096),
|
|
2244
|
+
participantId: participantIdSchema
|
|
2245
|
+
});
|
|
2246
|
+
z.object({ answer: sessionDescriptionSchema.refine((value) => value.type === "answer", "expected an answer") });
|
|
2247
|
+
z.union([z.object({ channels: z.array(z.object({
|
|
2248
|
+
name: nameSchema,
|
|
2249
|
+
delivery: z.enum(APP_RTC_DELIVERIES)
|
|
2250
|
+
})).min(1).max(APP_RTC_LIMITS.channelsPerMember) }), z.object({
|
|
2251
|
+
offer: sessionDescriptionSchema.refine((value) => value.type === "offer", "expected an offer"),
|
|
2252
|
+
tracks: z.array(z.object({
|
|
2253
|
+
name: nameSchema,
|
|
2254
|
+
kind: z.enum(["audio", "video"]),
|
|
2255
|
+
mid: z.string().min(1).max(32),
|
|
2256
|
+
simulcast: z.boolean().optional()
|
|
2257
|
+
})).min(1).max(APP_RTC_LIMITS.tracksPerMember)
|
|
2258
|
+
})]);
|
|
2259
|
+
z.object({
|
|
2260
|
+
ids: z.array(z.string().min(1).max(32)).min(1).max(APP_RTC_LIMITS.channelsPerMember + APP_RTC_LIMITS.tracksPerMember),
|
|
2261
|
+
offer: sessionDescriptionSchema.refine((value) => value.type === "offer", "expected an offer").optional()
|
|
2262
|
+
});
|
|
2263
|
+
const subscriptionRefSchema = z.object({
|
|
2264
|
+
participantId: participantIdSchema,
|
|
2265
|
+
publicationId: z.string().min(1).max(32)
|
|
2266
|
+
});
|
|
2267
|
+
const qualitySchema = z.enum(APP_RTC_QUALITIES);
|
|
2268
|
+
z.object({
|
|
2269
|
+
kind: z.enum(["data", "media"]),
|
|
2270
|
+
items: z.array(subscriptionRefSchema.extend({ quality: qualitySchema.optional() })).min(1).max(APP_RTC_LIMITS.batch)
|
|
2271
|
+
});
|
|
2272
|
+
z.object({ items: z.array(subscriptionRefSchema.extend({
|
|
2273
|
+
mid: z.string().min(1).max(32),
|
|
2274
|
+
quality: qualitySchema.optional()
|
|
2275
|
+
})).min(1).max(APP_RTC_LIMITS.mediaSubscriptions) });
|
|
2276
|
+
z.object({
|
|
2277
|
+
items: z.array(subscriptionRefSchema).min(1).max(APP_RTC_LIMITS.batch),
|
|
2278
|
+
offer: sessionDescriptionSchema.refine((value) => value.type === "offer", "expected an offer").optional()
|
|
2279
|
+
});
|
|
2033
2280
|
//#endregion
|
|
2034
2281
|
//#region ../protocol/dist/generation/catalog.js
|
|
2035
2282
|
function isGenerationModelHidden(model) {
|
|
@@ -3727,6 +3974,11 @@ const APP_APPEARANCE_TOKENS = [
|
|
|
3727
3974
|
"brand-ring",
|
|
3728
3975
|
"brand-contrast-fg",
|
|
3729
3976
|
"error-fg",
|
|
3977
|
+
"success",
|
|
3978
|
+
"warning",
|
|
3979
|
+
"error",
|
|
3980
|
+
"info",
|
|
3981
|
+
"status-running",
|
|
3730
3982
|
"selection-bg",
|
|
3731
3983
|
"overlay-scrim",
|
|
3732
3984
|
"shadow-subtle",
|
|
@@ -3821,6 +4073,33 @@ const buildAppEmbedShellChanged = (input) => ({
|
|
|
3821
4073
|
...input
|
|
3822
4074
|
});
|
|
3823
4075
|
//#endregion
|
|
4076
|
+
//#region ../protocol/dist/app-devices.js
|
|
4077
|
+
const APP_DEVICES = [
|
|
4078
|
+
"camera",
|
|
4079
|
+
"microphone",
|
|
4080
|
+
"screen"
|
|
4081
|
+
];
|
|
4082
|
+
const isAppDevice = (value) => typeof value === "string" && APP_DEVICES.includes(value);
|
|
4083
|
+
function parseAppDevices(value) {
|
|
4084
|
+
const items = typeof value === "string" ? value.split(/[\s,]+/) : Array.isArray(value) ? value : [];
|
|
4085
|
+
const wanted = new Set(items.map((item) => typeof item === "string" ? item.trim().toLowerCase() : item).filter(isAppDevice));
|
|
4086
|
+
return APP_DEVICES.filter((device) => wanted.has(device));
|
|
4087
|
+
}
|
|
4088
|
+
const APP_DEVICES_REQUEST_TYPE = "cohub.app.devices.request";
|
|
4089
|
+
const APP_DEVICES_RESULT_TYPE = "cohub.app.devices.result";
|
|
4090
|
+
function parseAppDevicesResult(value) {
|
|
4091
|
+
if (!value || typeof value !== "object") return null;
|
|
4092
|
+
const record = value;
|
|
4093
|
+
if (record.type !== "cohub.app.devices.result" || typeof record.requestId !== "string") return null;
|
|
4094
|
+
const status = record.status;
|
|
4095
|
+
if (status !== "granted" && status !== "denied" && status !== "unavailable") return null;
|
|
4096
|
+
return {
|
|
4097
|
+
type: APP_DEVICES_RESULT_TYPE,
|
|
4098
|
+
requestId: record.requestId,
|
|
4099
|
+
status
|
|
4100
|
+
};
|
|
4101
|
+
}
|
|
4102
|
+
//#endregion
|
|
3824
4103
|
//#region src/apis/billing.ts
|
|
3825
4104
|
var BillingApi = class {
|
|
3826
4105
|
transport;
|
|
@@ -3842,64 +4121,1018 @@ var BillingApi = class {
|
|
|
3842
4121
|
async getCatalog() {
|
|
3843
4122
|
return this.transport.request("/api/billing/catalog");
|
|
3844
4123
|
}
|
|
3845
|
-
async getFeatureEntitlement(featureKey) {
|
|
3846
|
-
return this.transport.request(`/api/billing/features/${encodeURIComponent(featureKey)}`);
|
|
4124
|
+
async getFeatureEntitlement(featureKey) {
|
|
4125
|
+
return this.transport.request(`/api/billing/features/${encodeURIComponent(featureKey)}`);
|
|
4126
|
+
}
|
|
4127
|
+
async getSubscriptions(input) {
|
|
4128
|
+
const params = new URLSearchParams();
|
|
4129
|
+
if (input?.page) params.set("page", String(input.page));
|
|
4130
|
+
if (input?.limit) params.set("limit", String(input.limit));
|
|
4131
|
+
const query = params.toString();
|
|
4132
|
+
return this.transport.request(`/api/billing/subscriptions${query ? `?${query}` : ""}`);
|
|
4133
|
+
}
|
|
4134
|
+
/** Confirms a returned checkout against the provider's order/subscription state. */
|
|
4135
|
+
async confirmCheckout(productKey, checkoutId) {
|
|
4136
|
+
return this.transport.request("/api/billing/checkout-confirmation", {
|
|
4137
|
+
method: "POST",
|
|
4138
|
+
body: JSON.stringify({
|
|
4139
|
+
productKey,
|
|
4140
|
+
checkoutId
|
|
4141
|
+
})
|
|
4142
|
+
});
|
|
4143
|
+
}
|
|
4144
|
+
async previewPromotionCode(input) {
|
|
4145
|
+
return this.transport.request("/api/billing/promotion-code-preview", {
|
|
4146
|
+
method: "POST",
|
|
4147
|
+
body: JSON.stringify(input)
|
|
4148
|
+
});
|
|
4149
|
+
}
|
|
4150
|
+
async createOrder(productKey, input) {
|
|
4151
|
+
return this.transport.request("/api/billing/orders", {
|
|
4152
|
+
method: "POST",
|
|
4153
|
+
body: JSON.stringify({
|
|
4154
|
+
...input ?? {},
|
|
4155
|
+
productKey
|
|
4156
|
+
})
|
|
4157
|
+
});
|
|
4158
|
+
}
|
|
4159
|
+
async createSubscription(productKey, input) {
|
|
4160
|
+
return this.transport.request("/api/billing/subscriptions", {
|
|
4161
|
+
method: "POST",
|
|
4162
|
+
body: JSON.stringify({
|
|
4163
|
+
...input ?? {},
|
|
4164
|
+
productKey
|
|
4165
|
+
})
|
|
4166
|
+
});
|
|
4167
|
+
}
|
|
4168
|
+
async cancelSubscriptionCheckout(subscriptionId) {
|
|
4169
|
+
return this.transport.request(`/api/billing/subscriptions/${encodeURIComponent(subscriptionId)}/checkout`, { method: "DELETE" });
|
|
4170
|
+
}
|
|
4171
|
+
async cancelSubscriptionAutoRenew(subscriptionId) {
|
|
4172
|
+
return this.transport.request(`/api/billing/subscriptions/${encodeURIComponent(subscriptionId)}`, {
|
|
4173
|
+
method: "PATCH",
|
|
4174
|
+
body: JSON.stringify({ cancelAtPeriodEnd: true })
|
|
4175
|
+
});
|
|
4176
|
+
}
|
|
4177
|
+
async createRedemption(input) {
|
|
4178
|
+
return this.transport.request("/api/billing/redemptions", {
|
|
4179
|
+
method: "POST",
|
|
4180
|
+
body: JSON.stringify(input)
|
|
4181
|
+
});
|
|
4182
|
+
}
|
|
4183
|
+
};
|
|
4184
|
+
//#endregion
|
|
4185
|
+
//#region src/apis/app-room-rtc.ts
|
|
4186
|
+
var AppRtcError = class extends Error {
|
|
4187
|
+
code;
|
|
4188
|
+
constructor(code, message) {
|
|
4189
|
+
super(message);
|
|
4190
|
+
this.code = code;
|
|
4191
|
+
this.name = "AppRtcError";
|
|
4192
|
+
}
|
|
4193
|
+
};
|
|
4194
|
+
const SERVER_CODES = /* @__PURE__ */ new Set([
|
|
4195
|
+
"RTC_UNAVAILABLE",
|
|
4196
|
+
"RTC_FORBIDDEN",
|
|
4197
|
+
"RTC_LIMIT_EXCEEDED",
|
|
4198
|
+
"RTC_RATE_LIMITED",
|
|
4199
|
+
"ROOM_NOT_FOUND",
|
|
4200
|
+
"ROOM_EXPIRED"
|
|
4201
|
+
]);
|
|
4202
|
+
const toAppRtcError = (error) => {
|
|
4203
|
+
if (error instanceof AppRtcError) return error;
|
|
4204
|
+
if (error instanceof HttpError && SERVER_CODES.has(error.code)) return new AppRtcError(error.code, error.message);
|
|
4205
|
+
return new AppRtcError("RTC_FAILED", error instanceof Error ? error.message : String(error));
|
|
4206
|
+
};
|
|
4207
|
+
const HEADER_BYTES = 5;
|
|
4208
|
+
const FLAG_BINARY = 1;
|
|
4209
|
+
const encoder = new TextEncoder();
|
|
4210
|
+
const decoder = new TextDecoder();
|
|
4211
|
+
function encodeAppRtcFrame(data, sequence) {
|
|
4212
|
+
let payload;
|
|
4213
|
+
let flags = 0;
|
|
4214
|
+
if (data instanceof ArrayBuffer) {
|
|
4215
|
+
payload = new Uint8Array(data);
|
|
4216
|
+
flags = FLAG_BINARY;
|
|
4217
|
+
} else if (ArrayBuffer.isView(data)) {
|
|
4218
|
+
payload = new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
|
|
4219
|
+
flags = FLAG_BINARY;
|
|
4220
|
+
} else {
|
|
4221
|
+
const json = JSON.stringify(data);
|
|
4222
|
+
if (json === void 0) throw new TypeError("RTC message is not serializable");
|
|
4223
|
+
payload = encoder.encode(json);
|
|
4224
|
+
}
|
|
4225
|
+
if (payload.byteLength > APP_RTC_LIMITS.messageBytes) throw new RangeError(`RTC message exceeds ${APP_RTC_LIMITS.messageBytes} bytes`);
|
|
4226
|
+
const frame = new Uint8Array(HEADER_BYTES + payload.byteLength);
|
|
4227
|
+
frame[0] = flags;
|
|
4228
|
+
new DataView(frame.buffer).setUint32(1, sequence >>> 0);
|
|
4229
|
+
frame.set(payload, HEADER_BYTES);
|
|
4230
|
+
return frame;
|
|
4231
|
+
}
|
|
4232
|
+
function decodeAppRtcFrame(frame) {
|
|
4233
|
+
if (frame.byteLength < HEADER_BYTES) return null;
|
|
4234
|
+
const view = new DataView(frame);
|
|
4235
|
+
const flags = view.getUint8(0);
|
|
4236
|
+
const sequence = view.getUint32(1);
|
|
4237
|
+
const payload = new Uint8Array(frame, HEADER_BYTES);
|
|
4238
|
+
if (flags & FLAG_BINARY) return {
|
|
4239
|
+
data: payload.slice().buffer,
|
|
4240
|
+
sequence
|
|
4241
|
+
};
|
|
4242
|
+
try {
|
|
4243
|
+
return {
|
|
4244
|
+
data: JSON.parse(decoder.decode(payload)),
|
|
4245
|
+
sequence
|
|
4246
|
+
};
|
|
4247
|
+
} catch {
|
|
4248
|
+
return null;
|
|
4249
|
+
}
|
|
4250
|
+
}
|
|
4251
|
+
const isNewerSequence = (next, last) => last === null || next !== last && next - last >>> 0 < 2147483648;
|
|
4252
|
+
const publicationsKey = (members) => members.map((member) => `${member.participantId}:${member.publications?.map((item) => item.id).join(",") ?? ""}`).join(";");
|
|
4253
|
+
const GATHER_TIMEOUT_MS = 2e3;
|
|
4254
|
+
const CONNECT_TIMEOUT_MS = 15e3;
|
|
4255
|
+
const PENDING_RELIABLE_FRAMES = 256;
|
|
4256
|
+
const LATEST_BACKLOG_BYTES = 65536;
|
|
4257
|
+
const RECONNECT_DELAYS_MS = [
|
|
4258
|
+
500,
|
|
4259
|
+
1e3,
|
|
4260
|
+
2e3,
|
|
4261
|
+
5e3,
|
|
4262
|
+
1e4,
|
|
4263
|
+
3e4
|
|
4264
|
+
];
|
|
4265
|
+
const RATE_LIMIT_RETRY_MS = 1e3;
|
|
4266
|
+
var ConnectionReplacedError = class extends Error {
|
|
4267
|
+
constructor() {
|
|
4268
|
+
super("RTC connection was replaced");
|
|
4269
|
+
this.name = "ConnectionReplacedError";
|
|
4270
|
+
}
|
|
4271
|
+
};
|
|
4272
|
+
const policyOf = (delivery) => delivery === "latest" ? {
|
|
4273
|
+
ordered: false,
|
|
4274
|
+
maxRetransmits: 0
|
|
4275
|
+
} : { ordered: true };
|
|
4276
|
+
const isSessionLost = (error) => error instanceof HttpError && (error.code === "RTC_STALE" || error.code === "RTC_SESSION_NOT_FOUND");
|
|
4277
|
+
const isRoomGone = (error) => error instanceof HttpError && (error.code === "ROOM_NOT_FOUND" || error.code === "ROOM_EXPIRED");
|
|
4278
|
+
const chunk = (items, size) => {
|
|
4279
|
+
const chunks = [];
|
|
4280
|
+
for (let index = 0; index < items.length; index += size) chunks.push(items.slice(index, index + size));
|
|
4281
|
+
return chunks;
|
|
4282
|
+
};
|
|
4283
|
+
const mediaKey = (participantId, publicationId) => `${participantId}:${publicationId}`;
|
|
4284
|
+
var AppRtcChannel = class {
|
|
4285
|
+
rtc;
|
|
4286
|
+
name;
|
|
4287
|
+
delivery;
|
|
4288
|
+
handlers = /* @__PURE__ */ new Set();
|
|
4289
|
+
peerHandlers = /* @__PURE__ */ new Set();
|
|
4290
|
+
openPeers = /* @__PURE__ */ new Set();
|
|
4291
|
+
publication = null;
|
|
4292
|
+
pending = [];
|
|
4293
|
+
sequence = 0;
|
|
4294
|
+
closed = false;
|
|
4295
|
+
constructor(rtc, name, delivery) {
|
|
4296
|
+
this.rtc = rtc;
|
|
4297
|
+
this.name = name;
|
|
4298
|
+
this.delivery = delivery;
|
|
4299
|
+
}
|
|
4300
|
+
get peers() {
|
|
4301
|
+
return [...this.openPeers];
|
|
4302
|
+
}
|
|
4303
|
+
send(data) {
|
|
4304
|
+
if (this.closed || this.rtc.state === "closed") return false;
|
|
4305
|
+
this.sequence = this.sequence + 1 >>> 0;
|
|
4306
|
+
const frame = encodeAppRtcFrame(data, this.sequence);
|
|
4307
|
+
const channel = this.publication?.channel;
|
|
4308
|
+
if (channel?.readyState === "open") return this.write(channel, frame);
|
|
4309
|
+
if (this.delivery === "latest") this.pending = [frame];
|
|
4310
|
+
else if (this.pending.length >= PENDING_RELIABLE_FRAMES) return false;
|
|
4311
|
+
else this.pending.push(frame);
|
|
4312
|
+
return true;
|
|
4313
|
+
}
|
|
4314
|
+
onMessage(handler) {
|
|
4315
|
+
this.handlers.add(handler);
|
|
4316
|
+
return () => this.handlers.delete(handler);
|
|
4317
|
+
}
|
|
4318
|
+
/** Reports when this client starts or stops receiving a peer's channel. */
|
|
4319
|
+
onPeer(handler) {
|
|
4320
|
+
this.peerHandlers.add(handler);
|
|
4321
|
+
return () => this.peerHandlers.delete(handler);
|
|
4322
|
+
}
|
|
4323
|
+
close() {
|
|
4324
|
+
if (this.closed) return;
|
|
4325
|
+
this.closed = true;
|
|
4326
|
+
this.handlers.clear();
|
|
4327
|
+
this.peerHandlers.clear();
|
|
4328
|
+
this.openPeers.clear();
|
|
4329
|
+
this.pending = [];
|
|
4330
|
+
this.rtc.closeChannel(this);
|
|
4331
|
+
}
|
|
4332
|
+
/** @internal */
|
|
4333
|
+
get isOpen() {
|
|
4334
|
+
return !this.closed;
|
|
4335
|
+
}
|
|
4336
|
+
/** @internal */
|
|
4337
|
+
get publicationId() {
|
|
4338
|
+
return this.publication?.id ?? null;
|
|
4339
|
+
}
|
|
4340
|
+
/** @internal */
|
|
4341
|
+
attach(id, channel) {
|
|
4342
|
+
this.publication = {
|
|
4343
|
+
id,
|
|
4344
|
+
channel
|
|
4345
|
+
};
|
|
4346
|
+
channel.binaryType = "arraybuffer";
|
|
4347
|
+
const flush = () => {
|
|
4348
|
+
const frames = this.pending;
|
|
4349
|
+
this.pending = [];
|
|
4350
|
+
for (const frame of frames) this.write(channel, frame);
|
|
4351
|
+
};
|
|
4352
|
+
if (channel.readyState === "open") flush();
|
|
4353
|
+
else channel.addEventListener("open", flush, { once: true });
|
|
4354
|
+
}
|
|
4355
|
+
/** @internal */
|
|
4356
|
+
detach() {
|
|
4357
|
+
this.publication = null;
|
|
4358
|
+
}
|
|
4359
|
+
/** @internal */
|
|
4360
|
+
fail() {
|
|
4361
|
+
this.close();
|
|
4362
|
+
}
|
|
4363
|
+
/** @internal */
|
|
4364
|
+
deliver(from, data) {
|
|
4365
|
+
for (const handler of this.handlers) handler({
|
|
4366
|
+
from,
|
|
4367
|
+
data
|
|
4368
|
+
});
|
|
4369
|
+
}
|
|
4370
|
+
/** @internal */
|
|
4371
|
+
setPeer(participantId, open) {
|
|
4372
|
+
if (open ? this.openPeers.has(participantId) : !this.openPeers.delete(participantId)) return;
|
|
4373
|
+
if (open) this.openPeers.add(participantId);
|
|
4374
|
+
for (const handler of this.peerHandlers) handler({
|
|
4375
|
+
participantId,
|
|
4376
|
+
open
|
|
4377
|
+
});
|
|
4378
|
+
}
|
|
4379
|
+
write(channel, frame) {
|
|
4380
|
+
if (this.delivery === "latest" && channel.bufferedAmount > LATEST_BACKLOG_BYTES) return false;
|
|
4381
|
+
try {
|
|
4382
|
+
channel.send(frame);
|
|
4383
|
+
return true;
|
|
4384
|
+
} catch {
|
|
4385
|
+
return false;
|
|
4386
|
+
}
|
|
4387
|
+
}
|
|
4388
|
+
};
|
|
4389
|
+
var AppRoomRtc = class {
|
|
4390
|
+
host;
|
|
4391
|
+
_state = "connecting";
|
|
4392
|
+
pc = null;
|
|
4393
|
+
sessionId = null;
|
|
4394
|
+
queue = Promise.resolve();
|
|
4395
|
+
reconcileQueued = false;
|
|
4396
|
+
reconcileFailures = 0;
|
|
4397
|
+
reconnectAttempt = 0;
|
|
4398
|
+
reconnectTimer = null;
|
|
4399
|
+
generation = 0;
|
|
4400
|
+
filter = null;
|
|
4401
|
+
channels = /* @__PURE__ */ new Map();
|
|
4402
|
+
channelsToPublish = /* @__PURE__ */ new Set();
|
|
4403
|
+
remoteData = /* @__PURE__ */ new Map();
|
|
4404
|
+
localTracks = /* @__PURE__ */ new Map();
|
|
4405
|
+
remoteMedia = /* @__PURE__ */ new Map();
|
|
4406
|
+
mediaByMid = /* @__PURE__ */ new Map();
|
|
4407
|
+
stateHandlers = /* @__PURE__ */ new Set();
|
|
4408
|
+
trackHandlers = /* @__PURE__ */ new Set();
|
|
4409
|
+
offMembers;
|
|
4410
|
+
constructor(host) {
|
|
4411
|
+
this.host = host;
|
|
4412
|
+
let published = "";
|
|
4413
|
+
this.offMembers = host.onMembersChanged((members) => {
|
|
4414
|
+
const next = publicationsKey(members);
|
|
4415
|
+
if (next === published) return;
|
|
4416
|
+
published = next;
|
|
4417
|
+
this.scheduleReconcile();
|
|
4418
|
+
});
|
|
4419
|
+
}
|
|
4420
|
+
get state() {
|
|
4421
|
+
return this._state;
|
|
4422
|
+
}
|
|
4423
|
+
get tracks() {
|
|
4424
|
+
const tracks = [];
|
|
4425
|
+
for (const [key, item] of this.remoteMedia) {
|
|
4426
|
+
if (!item.track) continue;
|
|
4427
|
+
tracks.push({
|
|
4428
|
+
participantId: item.participantId,
|
|
4429
|
+
name: item.name,
|
|
4430
|
+
kind: item.kind,
|
|
4431
|
+
track: item.track,
|
|
4432
|
+
quality: item.quality ?? null,
|
|
4433
|
+
setQuality: (quality) => this.setQuality(key, quality)
|
|
4434
|
+
});
|
|
4435
|
+
}
|
|
4436
|
+
return tracks;
|
|
4437
|
+
}
|
|
4438
|
+
get candidates() {
|
|
4439
|
+
return this.mediaCandidates().map(({ candidate }) => candidate);
|
|
4440
|
+
}
|
|
4441
|
+
onStateChange(handler) {
|
|
4442
|
+
this.stateHandlers.add(handler);
|
|
4443
|
+
return () => this.stateHandlers.delete(handler);
|
|
4444
|
+
}
|
|
4445
|
+
onTracksChanged(handler) {
|
|
4446
|
+
this.trackHandlers.add(handler);
|
|
4447
|
+
return () => this.trackHandlers.delete(handler);
|
|
4448
|
+
}
|
|
4449
|
+
select(filter) {
|
|
4450
|
+
this.filter = filter;
|
|
4451
|
+
this.scheduleReconcile();
|
|
4452
|
+
}
|
|
4453
|
+
channel(name, options = {}) {
|
|
4454
|
+
if (!isAppRtcName(name)) throw new AppRtcError("RTC_FAILED", `invalid RTC channel name: ${name}`);
|
|
4455
|
+
const delivery = options.delivery ?? "reliable";
|
|
4456
|
+
const existing = this.channels.get(name);
|
|
4457
|
+
if (existing?.isOpen) {
|
|
4458
|
+
if (existing.delivery !== delivery) throw new AppRtcError("RTC_FAILED", `RTC channel ${name} is already open with ${existing.delivery} delivery`);
|
|
4459
|
+
return existing;
|
|
4460
|
+
}
|
|
4461
|
+
if (this._state === "closed") throw new AppRtcError("RTC_CLOSED", "RTC connection is closed");
|
|
4462
|
+
const channel = new AppRtcChannel(this, name, delivery);
|
|
4463
|
+
this.channels.set(name, channel);
|
|
4464
|
+
this.publishChannel(channel);
|
|
4465
|
+
this.scheduleReconcile();
|
|
4466
|
+
return channel;
|
|
4467
|
+
}
|
|
4468
|
+
async publish(tracks) {
|
|
4469
|
+
if (this._state === "closed") throw new AppRtcError("RTC_CLOSED", "RTC connection is closed");
|
|
4470
|
+
const inputs = Object.entries(tracks);
|
|
4471
|
+
for (const [name] of inputs) {
|
|
4472
|
+
if (!isAppRtcName(name)) throw new AppRtcError("RTC_FAILED", `invalid RTC track name: ${name}`);
|
|
4473
|
+
if (this.localTracks.has(name)) throw new AppRtcError("RTC_FAILED", `RTC track ${name} is already published`);
|
|
4474
|
+
}
|
|
4475
|
+
if (this.localTracks.size + inputs.length > APP_RTC_LIMITS.tracksPerMember) throw new AppRtcError("RTC_LIMIT_EXCEEDED", `at most ${APP_RTC_LIMITS.tracksPerMember} tracks per member`);
|
|
4476
|
+
const entries = inputs.map(([name, input]) => {
|
|
4477
|
+
const { track, simulcast } = "track" in input ? input : {
|
|
4478
|
+
track: input,
|
|
4479
|
+
simulcast: false
|
|
4480
|
+
};
|
|
4481
|
+
const kind = track.kind === "audio" ? "audio" : "video";
|
|
4482
|
+
return this.localEntry(name, kind, track, simulcast === true && kind === "video");
|
|
4483
|
+
});
|
|
4484
|
+
for (const entry of entries) this.localTracks.set(entry.name, entry);
|
|
4485
|
+
try {
|
|
4486
|
+
await this.enqueue(() => this.publishTracks(entries.map((entry) => entry.name)));
|
|
4487
|
+
} catch (error) {
|
|
4488
|
+
if (!(error instanceof ConnectionReplacedError)) {
|
|
4489
|
+
for (const entry of entries) this.dropLocal(entry);
|
|
4490
|
+
throw toAppRtcError(error);
|
|
4491
|
+
}
|
|
4492
|
+
}
|
|
4493
|
+
return Object.fromEntries(entries.map((entry) => [entry.name, this.localHandle(entry)]));
|
|
4494
|
+
}
|
|
4495
|
+
async stats() {
|
|
4496
|
+
return this.pc ? readRtcTransportStats(await this.pc.getStats()) : {
|
|
4497
|
+
path: null,
|
|
4498
|
+
relayProtocol: null,
|
|
4499
|
+
rttMs: null
|
|
4500
|
+
};
|
|
4501
|
+
}
|
|
4502
|
+
close() {
|
|
4503
|
+
if (this._state === "closed") return;
|
|
4504
|
+
this.setState("closed");
|
|
4505
|
+
this.offMembers();
|
|
4506
|
+
if (this.reconnectTimer) clearTimeout(this.reconnectTimer);
|
|
4507
|
+
for (const channel of this.channels.values()) channel.close();
|
|
4508
|
+
for (const entry of this.localTracks.values()) entry.track.removeEventListener("ended", entry.onEnded);
|
|
4509
|
+
this.teardown(true);
|
|
4510
|
+
}
|
|
4511
|
+
/** @internal */
|
|
4512
|
+
async start() {
|
|
4513
|
+
try {
|
|
4514
|
+
await this.connect();
|
|
4515
|
+
} catch (error) {
|
|
4516
|
+
throw toAppRtcError(error);
|
|
4517
|
+
}
|
|
4518
|
+
}
|
|
4519
|
+
/** @internal */
|
|
4520
|
+
async verify() {
|
|
4521
|
+
if (this._state === "closed" || !this.sessionId) return;
|
|
4522
|
+
try {
|
|
4523
|
+
await this.request("GET", `/${this.sessionId}`);
|
|
4524
|
+
} catch (error) {
|
|
4525
|
+
if (isSessionLost(error)) this.reconnect();
|
|
4526
|
+
}
|
|
4527
|
+
}
|
|
4528
|
+
/** @internal */
|
|
4529
|
+
closeChannel(channel) {
|
|
4530
|
+
if (this.channels.get(channel.name) === channel) this.channels.delete(channel.name);
|
|
4531
|
+
const id = channel.publicationId;
|
|
4532
|
+
channel.detach();
|
|
4533
|
+
if (id) this.enqueue(() => this.unpublishData([id])).catch(() => void 0);
|
|
4534
|
+
this.scheduleReconcile();
|
|
4535
|
+
}
|
|
4536
|
+
localEntry(name, kind, track, simulcast) {
|
|
4537
|
+
const entry = {
|
|
4538
|
+
name,
|
|
4539
|
+
kind,
|
|
4540
|
+
track,
|
|
4541
|
+
simulcast,
|
|
4542
|
+
transceiver: null,
|
|
4543
|
+
id: null,
|
|
4544
|
+
ended: /* @__PURE__ */ new Set(),
|
|
4545
|
+
onEnded: () => void 0
|
|
4546
|
+
};
|
|
4547
|
+
entry.onEnded = () => {
|
|
4548
|
+
if (this.localTracks.get(name) !== entry) return;
|
|
4549
|
+
for (const handler of entry.ended) handler();
|
|
4550
|
+
this.stopLocal(entry).catch(() => void 0);
|
|
4551
|
+
};
|
|
4552
|
+
track.addEventListener("ended", entry.onEnded);
|
|
4553
|
+
return entry;
|
|
4554
|
+
}
|
|
4555
|
+
localHandle(entry) {
|
|
4556
|
+
return {
|
|
4557
|
+
name: entry.name,
|
|
4558
|
+
kind: entry.kind,
|
|
4559
|
+
simulcast: entry.simulcast,
|
|
4560
|
+
get track() {
|
|
4561
|
+
return entry.track;
|
|
4562
|
+
},
|
|
4563
|
+
replace: async (track) => {
|
|
4564
|
+
if (track.kind !== entry.kind) throw new AppRtcError("RTC_FAILED", `replace ${entry.name} with another ${entry.kind} track`);
|
|
4565
|
+
if (this.localTracks.get(entry.name) !== entry) throw new AppRtcError("RTC_CLOSED", `RTC track ${entry.name} has stopped`);
|
|
4566
|
+
entry.track.removeEventListener("ended", entry.onEnded);
|
|
4567
|
+
entry.track = track;
|
|
4568
|
+
track.addEventListener("ended", entry.onEnded);
|
|
4569
|
+
await entry.transceiver?.sender.replaceTrack(track);
|
|
4570
|
+
},
|
|
4571
|
+
onEnded: (handler) => {
|
|
4572
|
+
entry.ended.add(handler);
|
|
4573
|
+
return () => entry.ended.delete(handler);
|
|
4574
|
+
},
|
|
4575
|
+
stop: () => this.stopLocal(entry)
|
|
4576
|
+
};
|
|
4577
|
+
}
|
|
4578
|
+
dropLocal(entry) {
|
|
4579
|
+
entry.track.removeEventListener("ended", entry.onEnded);
|
|
4580
|
+
if (this.localTracks.get(entry.name) === entry) this.localTracks.delete(entry.name);
|
|
4581
|
+
}
|
|
4582
|
+
stopLocal(entry) {
|
|
4583
|
+
if (this.localTracks.get(entry.name) !== entry) return Promise.resolve();
|
|
4584
|
+
this.dropLocal(entry);
|
|
4585
|
+
return this.enqueue(() => this.unpublishTrack(entry)).catch((error) => {
|
|
4586
|
+
if (error instanceof ConnectionReplacedError) return;
|
|
4587
|
+
throw toAppRtcError(error);
|
|
4588
|
+
});
|
|
4589
|
+
}
|
|
4590
|
+
async publishTracks(names) {
|
|
4591
|
+
const pc = this.requirePc();
|
|
4592
|
+
const entries = names.flatMap((name) => {
|
|
4593
|
+
const entry = this.localTracks.get(name);
|
|
4594
|
+
return entry && !entry.id ? [entry] : [];
|
|
4595
|
+
});
|
|
4596
|
+
if (entries.length === 0) return;
|
|
4597
|
+
for (const entry of entries) entry.transceiver = pc.addTransceiver(entry.track, {
|
|
4598
|
+
direction: "sendonly",
|
|
4599
|
+
...entry.simulcast ? { sendEncodings: APP_RTC_SIMULCAST_LAYERS.map(({ rid, scaleResolutionDownBy }) => ({
|
|
4600
|
+
rid,
|
|
4601
|
+
scaleResolutionDownBy
|
|
4602
|
+
})) } : {}
|
|
4603
|
+
});
|
|
4604
|
+
const offer = await this.localOffer(pc);
|
|
4605
|
+
let result;
|
|
4606
|
+
try {
|
|
4607
|
+
result = await this.request("POST", `/${this.sessionId}/publish`, {
|
|
4608
|
+
offer,
|
|
4609
|
+
tracks: entries.map((entry) => ({
|
|
4610
|
+
name: entry.name,
|
|
4611
|
+
kind: entry.kind,
|
|
4612
|
+
mid: entry.transceiver?.mid ?? "",
|
|
4613
|
+
...entry.simulcast ? { simulcast: true } : {}
|
|
4614
|
+
}))
|
|
4615
|
+
});
|
|
4616
|
+
} catch (error) {
|
|
4617
|
+
if (error instanceof ConnectionReplacedError) throw error;
|
|
4618
|
+
await this.settleOffer(pc, void 0).catch(() => void 0);
|
|
4619
|
+
for (const entry of entries) {
|
|
4620
|
+
entry.transceiver?.stop();
|
|
4621
|
+
entry.transceiver = null;
|
|
4622
|
+
}
|
|
4623
|
+
throw error;
|
|
4624
|
+
}
|
|
4625
|
+
await this.settleOffer(pc, result.answer);
|
|
4626
|
+
for (const publication of result.publications) {
|
|
4627
|
+
const entry = entries.find((candidate) => candidate.name === publication.name);
|
|
4628
|
+
if (entry) entry.id = publication.id;
|
|
4629
|
+
}
|
|
4630
|
+
}
|
|
4631
|
+
async unpublishTrack(entry) {
|
|
4632
|
+
if (!entry.id || !entry.transceiver || !this.pc) return;
|
|
4633
|
+
const id = entry.id;
|
|
4634
|
+
entry.transceiver.stop();
|
|
4635
|
+
await this.offerExchange(this.pc, (offer) => this.request("POST", `/${this.sessionId}/unpublish`, {
|
|
4636
|
+
ids: [id],
|
|
4637
|
+
offer
|
|
4638
|
+
}));
|
|
4639
|
+
entry.id = null;
|
|
4640
|
+
}
|
|
4641
|
+
publishChannel(channel) {
|
|
4642
|
+
this.channelsToPublish.add(channel);
|
|
4643
|
+
if (this.channelsToPublish.size > 1 || !this.pc) return;
|
|
4644
|
+
this.enqueue(async () => {
|
|
4645
|
+
const batch = [...this.channelsToPublish];
|
|
4646
|
+
this.channelsToPublish.clear();
|
|
4647
|
+
try {
|
|
4648
|
+
await this.publishChannels(batch);
|
|
4649
|
+
} catch (error) {
|
|
4650
|
+
if (isSessionLost(error) || error instanceof ConnectionReplacedError) throw error;
|
|
4651
|
+
if (error instanceof HttpError && error.code === "RTC_RATE_LIMITED") {
|
|
4652
|
+
setTimeout(() => {
|
|
4653
|
+
for (const item of batch) if (item.isOpen) this.publishChannel(item);
|
|
4654
|
+
}, RATE_LIMIT_RETRY_MS);
|
|
4655
|
+
return;
|
|
4656
|
+
}
|
|
4657
|
+
for (const item of batch) item.fail();
|
|
4658
|
+
console.warn("[Cohub AppRtc] failed to publish channels", error);
|
|
4659
|
+
}
|
|
4660
|
+
}).catch(() => void 0);
|
|
4661
|
+
}
|
|
4662
|
+
async connect() {
|
|
4663
|
+
const generation = ++this.generation;
|
|
4664
|
+
const opened = await this.send("POST", "", {
|
|
4665
|
+
ticket: this.host.ticket,
|
|
4666
|
+
participantId: this.host.participantId()
|
|
4667
|
+
});
|
|
4668
|
+
if (generation !== this.generation || this._state === "closed") {
|
|
4669
|
+
this.send("DELETE", `/${opened.sessionId}`).catch(() => void 0);
|
|
4670
|
+
return;
|
|
4671
|
+
}
|
|
4672
|
+
const pc = new RTCPeerConnection({
|
|
4673
|
+
iceServers: opened.iceServers,
|
|
4674
|
+
bundlePolicy: "max-bundle"
|
|
4675
|
+
});
|
|
4676
|
+
this.pc = pc;
|
|
4677
|
+
this.sessionId = opened.sessionId;
|
|
4678
|
+
pc.addEventListener("connectionstatechange", () => {
|
|
4679
|
+
if (this.pc !== pc) return;
|
|
4680
|
+
if (pc.connectionState === "connected") {
|
|
4681
|
+
this.reconnectAttempt = 0;
|
|
4682
|
+
this.setState("connected");
|
|
4683
|
+
} else if (pc.connectionState === "failed") this.reconnect();
|
|
4684
|
+
});
|
|
4685
|
+
pc.addEventListener("track", (event) => this.onTrack(event));
|
|
4686
|
+
const ready = (async () => {
|
|
4687
|
+
await this.answerOffer(opened.offer);
|
|
4688
|
+
await this.connected(pc);
|
|
4689
|
+
})();
|
|
4690
|
+
this.queue = ready.catch(() => void 0);
|
|
4691
|
+
try {
|
|
4692
|
+
await ready;
|
|
4693
|
+
} catch (error) {
|
|
4694
|
+
if (generation === this.generation) this.reconnect();
|
|
4695
|
+
throw error;
|
|
4696
|
+
}
|
|
4697
|
+
this.channelsToPublish.clear();
|
|
4698
|
+
const republish = [...this.channels.values()].filter((channel) => channel.isOpen);
|
|
4699
|
+
const tracks = [...this.localTracks.keys()];
|
|
4700
|
+
await Promise.all([republish.length ? this.enqueue(() => this.publishChannels(republish)).catch((error) => {
|
|
4701
|
+
if (error instanceof ConnectionReplacedError || isSessionLost(error)) return;
|
|
4702
|
+
for (const channel of republish) channel.fail();
|
|
4703
|
+
console.warn("[Cohub AppRtc] failed to publish channels on the new connection", error);
|
|
4704
|
+
}) : null, tracks.length ? this.enqueue(() => this.publishTracks(tracks)).catch((error) => {
|
|
4705
|
+
if (error instanceof ConnectionReplacedError || isSessionLost(error)) return;
|
|
4706
|
+
console.warn("[Cohub AppRtc] failed to publish tracks on the new connection", error);
|
|
4707
|
+
}) : null]);
|
|
4708
|
+
this.scheduleReconcile();
|
|
4709
|
+
}
|
|
4710
|
+
connected(pc) {
|
|
4711
|
+
if (pc.connectionState === "connected") return Promise.resolve();
|
|
4712
|
+
return new Promise((resolve, reject) => {
|
|
4713
|
+
const timer = setTimeout(() => done(/* @__PURE__ */ new Error("RTC connection timed out")), CONNECT_TIMEOUT_MS);
|
|
4714
|
+
const done = (error) => {
|
|
4715
|
+
clearTimeout(timer);
|
|
4716
|
+
pc.removeEventListener("connectionstatechange", check);
|
|
4717
|
+
if (error) reject(error);
|
|
4718
|
+
else resolve();
|
|
4719
|
+
};
|
|
4720
|
+
const check = () => {
|
|
4721
|
+
if (pc.connectionState === "connected") done();
|
|
4722
|
+
else if (pc.connectionState === "failed" || pc.connectionState === "closed") done(/* @__PURE__ */ new Error("RTC connection failed"));
|
|
4723
|
+
};
|
|
4724
|
+
pc.addEventListener("connectionstatechange", check);
|
|
4725
|
+
});
|
|
4726
|
+
}
|
|
4727
|
+
reconnect() {
|
|
4728
|
+
if (this._state === "closed" || this.reconnectTimer) return;
|
|
4729
|
+
this.setState(this._state === "connecting" ? "connecting" : "reconnecting");
|
|
4730
|
+
this.teardown(true);
|
|
4731
|
+
const delay = RECONNECT_DELAYS_MS[Math.min(this.reconnectAttempt, RECONNECT_DELAYS_MS.length - 1)] ?? 3e4;
|
|
4732
|
+
this.reconnectAttempt += 1;
|
|
4733
|
+
this.reconnectTimer = setTimeout(() => {
|
|
4734
|
+
this.reconnectTimer = null;
|
|
4735
|
+
this.connect().catch((error) => {
|
|
4736
|
+
if (isRoomGone(error) || error instanceof HttpError && (error.status === 403 || error.status === 503)) this.close();
|
|
4737
|
+
else this.reconnect();
|
|
4738
|
+
});
|
|
4739
|
+
}, delay);
|
|
4740
|
+
}
|
|
4741
|
+
teardown(release) {
|
|
4742
|
+
this.generation += 1;
|
|
4743
|
+
const sessionId = this.sessionId;
|
|
4744
|
+
this.sessionId = null;
|
|
4745
|
+
this.queue = Promise.resolve();
|
|
4746
|
+
for (const channel of this.channels.values()) channel.detach();
|
|
4747
|
+
for (const item of this.remoteData.values()) this.dropRemoteData(item);
|
|
4748
|
+
this.remoteData.clear();
|
|
4749
|
+
for (const item of this.localTracks.values()) {
|
|
4750
|
+
item.transceiver = null;
|
|
4751
|
+
item.id = null;
|
|
4752
|
+
}
|
|
4753
|
+
const hadTracks = this.remoteMedia.size > 0;
|
|
4754
|
+
this.remoteMedia.clear();
|
|
4755
|
+
this.mediaByMid.clear();
|
|
4756
|
+
this.pc?.close();
|
|
4757
|
+
this.pc = null;
|
|
4758
|
+
if (hadTracks) this.notifyTracks();
|
|
4759
|
+
if (release && sessionId) this.send("DELETE", `/${sessionId}`).catch(() => void 0);
|
|
4760
|
+
}
|
|
4761
|
+
enqueue(operation) {
|
|
4762
|
+
const generation = this.generation;
|
|
4763
|
+
const run = async () => {
|
|
4764
|
+
if (generation !== this.generation || !this.pc || !this.sessionId) throw new ConnectionReplacedError();
|
|
4765
|
+
try {
|
|
4766
|
+
return await operation();
|
|
4767
|
+
} catch (error) {
|
|
4768
|
+
if (isSessionLost(error)) this.reconnect();
|
|
4769
|
+
throw error;
|
|
4770
|
+
}
|
|
4771
|
+
};
|
|
4772
|
+
const next = this.queue.then(run, run);
|
|
4773
|
+
this.queue = next.catch(() => void 0);
|
|
4774
|
+
return next;
|
|
4775
|
+
}
|
|
4776
|
+
async publishChannels(channels) {
|
|
4777
|
+
const pc = this.requirePc();
|
|
4778
|
+
const open = channels.filter((channel) => channel.isOpen && !channel.publicationId);
|
|
4779
|
+
if (open.length === 0) return;
|
|
4780
|
+
const result = await this.request("POST", `/${this.sessionId}/publish`, { channels: open.map((channel) => ({
|
|
4781
|
+
name: channel.name,
|
|
4782
|
+
delivery: channel.delivery
|
|
4783
|
+
})) });
|
|
4784
|
+
for (const publication of result.publications) {
|
|
4785
|
+
const channel = open.find((item) => item.name === publication.name);
|
|
4786
|
+
const id = result.channelIds?.[publication.id];
|
|
4787
|
+
if (!channel || id === void 0) continue;
|
|
4788
|
+
const dataChannel = pc.createDataChannel(publication.name, {
|
|
4789
|
+
negotiated: true,
|
|
4790
|
+
id,
|
|
4791
|
+
...policyOf(channel.delivery)
|
|
4792
|
+
});
|
|
4793
|
+
channel.attach(publication.id, dataChannel);
|
|
4794
|
+
if (!channel.isOpen) await this.unpublishData([publication.id]);
|
|
4795
|
+
}
|
|
4796
|
+
}
|
|
4797
|
+
async unpublishData(ids) {
|
|
4798
|
+
await this.request("POST", `/${this.sessionId}/unpublish`, { ids });
|
|
3847
4799
|
}
|
|
3848
|
-
async
|
|
3849
|
-
|
|
3850
|
-
if (
|
|
3851
|
-
if (input?.limit) params.set("limit", String(input.limit));
|
|
3852
|
-
const query = params.toString();
|
|
3853
|
-
return this.transport.request(`/api/billing/subscriptions${query ? `?${query}` : ""}`);
|
|
4800
|
+
async settleOffer(pc, answer) {
|
|
4801
|
+
if (answer) await pc.setRemoteDescription(answer);
|
|
4802
|
+
else if (pc.signalingState === "have-local-offer") await pc.setLocalDescription({ type: "rollback" });
|
|
3854
4803
|
}
|
|
3855
|
-
|
|
3856
|
-
|
|
3857
|
-
|
|
3858
|
-
|
|
3859
|
-
|
|
3860
|
-
|
|
3861
|
-
|
|
3862
|
-
|
|
4804
|
+
async offerExchange(pc, send) {
|
|
4805
|
+
const offer = await this.localOffer(pc);
|
|
4806
|
+
let answer;
|
|
4807
|
+
try {
|
|
4808
|
+
({answer} = await send(offer));
|
|
4809
|
+
} catch (error) {
|
|
4810
|
+
if (!(error instanceof ConnectionReplacedError)) await this.settleOffer(pc, void 0).catch(() => void 0);
|
|
4811
|
+
throw error;
|
|
4812
|
+
}
|
|
4813
|
+
await this.settleOffer(pc, answer);
|
|
4814
|
+
}
|
|
4815
|
+
scheduleReconcile() {
|
|
4816
|
+
if (this.reconcileQueued || !this.pc || this._state === "closed") return;
|
|
4817
|
+
this.reconcileQueued = true;
|
|
4818
|
+
this.enqueue(async () => {
|
|
4819
|
+
this.reconcileQueued = false;
|
|
4820
|
+
await this.reconcile();
|
|
4821
|
+
this.reconcileFailures = 0;
|
|
4822
|
+
}).catch((error) => {
|
|
4823
|
+
this.reconcileQueued = false;
|
|
4824
|
+
if (isSessionLost(error) || error instanceof ConnectionReplacedError) return;
|
|
4825
|
+
console.warn("[Cohub AppRtc] failed to update subscriptions", error);
|
|
4826
|
+
if (error instanceof HttpError && error.code === "RTC_LIMIT_EXCEEDED") return;
|
|
4827
|
+
const delay = RECONNECT_DELAYS_MS[Math.min(this.reconcileFailures, RECONNECT_DELAYS_MS.length - 1)] ?? 3e4;
|
|
4828
|
+
this.reconcileFailures += 1;
|
|
4829
|
+
const generation = this.generation;
|
|
4830
|
+
setTimeout(() => {
|
|
4831
|
+
if (generation === this.generation) this.scheduleReconcile();
|
|
4832
|
+
}, delay);
|
|
3863
4833
|
});
|
|
3864
4834
|
}
|
|
3865
|
-
|
|
3866
|
-
|
|
3867
|
-
|
|
3868
|
-
|
|
3869
|
-
|
|
4835
|
+
mediaCandidates() {
|
|
4836
|
+
const self = this.host.participantId();
|
|
4837
|
+
const candidates = [];
|
|
4838
|
+
for (const member of this.host.members()) {
|
|
4839
|
+
if (member.participantId === self) continue;
|
|
4840
|
+
for (const publication of member.publications ?? []) {
|
|
4841
|
+
if (publication.kind === "data") continue;
|
|
4842
|
+
candidates.push({
|
|
4843
|
+
publication,
|
|
4844
|
+
candidate: {
|
|
4845
|
+
participantId: member.participantId,
|
|
4846
|
+
name: publication.name,
|
|
4847
|
+
kind: publication.kind,
|
|
4848
|
+
simulcast: publication.simulcast === true,
|
|
4849
|
+
subscribed: this.remoteMedia.has(mediaKey(member.participantId, publication.id))
|
|
4850
|
+
}
|
|
4851
|
+
});
|
|
4852
|
+
}
|
|
4853
|
+
}
|
|
4854
|
+
return candidates;
|
|
4855
|
+
}
|
|
4856
|
+
async reconcile() {
|
|
4857
|
+
const self = this.host.participantId();
|
|
4858
|
+
const wantedData = /* @__PURE__ */ new Map();
|
|
4859
|
+
for (const member of this.host.members()) {
|
|
4860
|
+
if (member.participantId === self) continue;
|
|
4861
|
+
for (const publication of member.publications ?? []) {
|
|
4862
|
+
if (publication.kind !== "data") continue;
|
|
4863
|
+
const channel = this.channels.get(publication.name);
|
|
4864
|
+
if (channel?.isOpen && channel.delivery === (publication.delivery ?? "reliable")) wantedData.set(mediaKey(member.participantId, publication.id), {
|
|
4865
|
+
participantId: member.participantId,
|
|
4866
|
+
publicationId: publication.id,
|
|
4867
|
+
name: publication.name,
|
|
4868
|
+
delivery: channel.delivery
|
|
4869
|
+
});
|
|
4870
|
+
}
|
|
4871
|
+
}
|
|
4872
|
+
const wantedMedia = /* @__PURE__ */ new Map();
|
|
4873
|
+
for (const { candidate, publication } of this.mediaCandidates()) {
|
|
4874
|
+
if (wantedMedia.size >= APP_RTC_LIMITS.mediaSubscriptions) break;
|
|
4875
|
+
if (this.filter && !this.filter(candidate)) continue;
|
|
4876
|
+
const key = mediaKey(candidate.participantId, publication.id);
|
|
4877
|
+
const quality = this.remoteMedia.get(key)?.quality ?? (candidate.simulcast ? "high" : void 0);
|
|
4878
|
+
wantedMedia.set(key, {
|
|
4879
|
+
participantId: candidate.participantId,
|
|
4880
|
+
publicationId: publication.id,
|
|
4881
|
+
name: candidate.name,
|
|
4882
|
+
kind: candidate.kind,
|
|
4883
|
+
...quality ? { quality } : {}
|
|
4884
|
+
});
|
|
4885
|
+
}
|
|
4886
|
+
await this.reconcileData(wantedData);
|
|
4887
|
+
await this.reconcileMedia(wantedMedia);
|
|
4888
|
+
if ([...wantedData.keys()].some((key) => !this.remoteData.has(key)) || [...wantedMedia.keys()].some((key) => !this.remoteMedia.has(key))) throw new AppRtcError("RTC_FAILED", "Some RTC subscriptions could not be established");
|
|
4889
|
+
}
|
|
4890
|
+
dropRemoteData(item) {
|
|
4891
|
+
item.channel.close();
|
|
4892
|
+
this.channels.get(item.name)?.setPeer(item.participantId, false);
|
|
4893
|
+
}
|
|
4894
|
+
async reconcileData(wanted) {
|
|
4895
|
+
const pc = this.requirePc();
|
|
4896
|
+
const stale = [...this.remoteData].filter(([key]) => !wanted.has(key));
|
|
4897
|
+
for (const batch of chunk(stale, APP_RTC_LIMITS.batch)) {
|
|
4898
|
+
await this.request("POST", `/${this.sessionId}/unsubscribe`, { items: batch.map(([, item]) => ({
|
|
4899
|
+
participantId: item.participantId,
|
|
4900
|
+
publicationId: item.publicationId
|
|
4901
|
+
})) });
|
|
4902
|
+
for (const [key, item] of batch) {
|
|
4903
|
+
this.dropRemoteData(item);
|
|
4904
|
+
this.remoteData.delete(key);
|
|
4905
|
+
}
|
|
4906
|
+
}
|
|
4907
|
+
const missing = [...wanted].filter(([key]) => !this.remoteData.has(key)).map(([, item]) => item);
|
|
4908
|
+
for (const batch of chunk(missing, APP_RTC_LIMITS.batch)) {
|
|
4909
|
+
const result = await this.request("POST", `/${this.sessionId}/subscribe`, {
|
|
4910
|
+
kind: "data",
|
|
4911
|
+
items: batch.map(({ participantId, publicationId }) => ({
|
|
4912
|
+
participantId,
|
|
4913
|
+
publicationId
|
|
4914
|
+
}))
|
|
4915
|
+
});
|
|
4916
|
+
for (const subscription of result.subscriptions) {
|
|
4917
|
+
const item = batch.find((entry) => entry.participantId === subscription.participantId && entry.publicationId === subscription.publicationId);
|
|
4918
|
+
if (!item || subscription.channelId === void 0) continue;
|
|
4919
|
+
const channel = pc.createDataChannel(item.name, {
|
|
4920
|
+
negotiated: true,
|
|
4921
|
+
id: subscription.channelId,
|
|
4922
|
+
...policyOf(item.delivery)
|
|
4923
|
+
});
|
|
4924
|
+
channel.binaryType = "arraybuffer";
|
|
4925
|
+
const remote = {
|
|
4926
|
+
name: item.name,
|
|
4927
|
+
participantId: item.participantId,
|
|
4928
|
+
publicationId: item.publicationId,
|
|
4929
|
+
channel,
|
|
4930
|
+
last: null
|
|
4931
|
+
};
|
|
4932
|
+
channel.addEventListener("message", (event) => this.onData(remote, event.data));
|
|
4933
|
+
channel.addEventListener("open", () => {
|
|
4934
|
+
if (this.remoteData.get(mediaKey(remote.participantId, remote.publicationId)) === remote) this.channels.get(remote.name)?.setPeer(remote.participantId, true);
|
|
4935
|
+
});
|
|
4936
|
+
this.remoteData.set(mediaKey(item.participantId, item.publicationId), remote);
|
|
4937
|
+
}
|
|
4938
|
+
}
|
|
3870
4939
|
}
|
|
3871
|
-
async
|
|
3872
|
-
|
|
3873
|
-
|
|
3874
|
-
|
|
3875
|
-
|
|
3876
|
-
|
|
3877
|
-
|
|
4940
|
+
async reconcileMedia(wanted) {
|
|
4941
|
+
const stale = [...this.remoteMedia].filter(([key]) => !wanted.has(key));
|
|
4942
|
+
const missing = [...wanted].filter(([key]) => !this.remoteMedia.has(key)).map(([, item]) => item);
|
|
4943
|
+
const moves = [];
|
|
4944
|
+
for (const to of [...missing]) {
|
|
4945
|
+
const index = stale.findIndex(([, receiver]) => receiver.kind === to.kind);
|
|
4946
|
+
const found = index >= 0 ? stale.splice(index, 1)[0] : void 0;
|
|
4947
|
+
if (!found) continue;
|
|
4948
|
+
moves.push({
|
|
4949
|
+
from: found[0],
|
|
4950
|
+
receiver: found[1],
|
|
4951
|
+
to
|
|
4952
|
+
});
|
|
4953
|
+
missing.splice(missing.indexOf(to), 1);
|
|
4954
|
+
}
|
|
4955
|
+
if (moves.length) {
|
|
4956
|
+
const moved = await this.retarget(moves.map(({ receiver, to }) => ({
|
|
4957
|
+
...to,
|
|
4958
|
+
mid: receiver.mid
|
|
4959
|
+
})));
|
|
4960
|
+
for (const { from, receiver, to } of moves) if (moved.has(receiver.mid)) {
|
|
4961
|
+
this.remoteMedia.delete(from);
|
|
4962
|
+
this.remoteMedia.set(mediaKey(to.participantId, to.publicationId), {
|
|
4963
|
+
...to,
|
|
4964
|
+
mid: receiver.mid,
|
|
4965
|
+
track: receiver.track
|
|
4966
|
+
});
|
|
4967
|
+
this.mediaByMid.set(receiver.mid, mediaKey(to.participantId, to.publicationId));
|
|
4968
|
+
} else {
|
|
4969
|
+
stale.push([from, receiver]);
|
|
4970
|
+
missing.push(to);
|
|
4971
|
+
}
|
|
4972
|
+
if (moved.size) this.notifyTracks();
|
|
4973
|
+
}
|
|
4974
|
+
await this.unsubscribeMedia(stale);
|
|
4975
|
+
await this.subscribeMedia(missing);
|
|
4976
|
+
}
|
|
4977
|
+
async retarget(items) {
|
|
4978
|
+
try {
|
|
4979
|
+
const result = await this.request("PATCH", `/${this.sessionId}/media`, { items: items.map(({ participantId, publicationId, mid, quality }) => ({
|
|
4980
|
+
participantId,
|
|
4981
|
+
publicationId,
|
|
4982
|
+
mid,
|
|
4983
|
+
...quality ? { quality } : {}
|
|
4984
|
+
})) });
|
|
4985
|
+
if (result.offer) await this.answerOffer(result.offer);
|
|
4986
|
+
return new Set(result.updated.map((item) => item.mid));
|
|
4987
|
+
} catch (error) {
|
|
4988
|
+
if (error instanceof ConnectionReplacedError || isSessionLost(error)) throw error;
|
|
4989
|
+
return /* @__PURE__ */ new Set();
|
|
4990
|
+
}
|
|
4991
|
+
}
|
|
4992
|
+
async setQuality(key, quality) {
|
|
4993
|
+
await this.enqueue(async () => {
|
|
4994
|
+
const item = this.remoteMedia.get(key);
|
|
4995
|
+
if (!item || item.quality === void 0 || item.quality === quality) return;
|
|
4996
|
+
if ((await this.retarget([{
|
|
4997
|
+
...item,
|
|
4998
|
+
quality
|
|
4999
|
+
}])).has(item.mid)) {
|
|
5000
|
+
item.quality = quality;
|
|
5001
|
+
this.notifyTracks();
|
|
5002
|
+
}
|
|
5003
|
+
}).catch((error) => {
|
|
5004
|
+
if (error instanceof ConnectionReplacedError) return;
|
|
5005
|
+
throw toAppRtcError(error);
|
|
3878
5006
|
});
|
|
3879
5007
|
}
|
|
3880
|
-
async
|
|
3881
|
-
|
|
3882
|
-
|
|
3883
|
-
|
|
3884
|
-
|
|
3885
|
-
|
|
3886
|
-
|
|
5008
|
+
async unsubscribeMedia(stale) {
|
|
5009
|
+
if (stale.length === 0) return;
|
|
5010
|
+
const pc = this.requirePc();
|
|
5011
|
+
for (const [, item] of stale) {
|
|
5012
|
+
pc.getTransceivers().find((transceiver) => transceiver.mid === item.mid)?.stop();
|
|
5013
|
+
item.track = null;
|
|
5014
|
+
}
|
|
5015
|
+
this.notifyTracks();
|
|
5016
|
+
await this.offerExchange(pc, (offer) => this.request("POST", `/${this.sessionId}/unsubscribe`, {
|
|
5017
|
+
items: stale.map(([, item]) => ({
|
|
5018
|
+
participantId: item.participantId,
|
|
5019
|
+
publicationId: item.publicationId
|
|
5020
|
+
})),
|
|
5021
|
+
offer
|
|
5022
|
+
}));
|
|
5023
|
+
for (const [key, item] of stale) {
|
|
5024
|
+
this.remoteMedia.delete(key);
|
|
5025
|
+
this.mediaByMid.delete(item.mid);
|
|
5026
|
+
}
|
|
5027
|
+
}
|
|
5028
|
+
async subscribeMedia(missing) {
|
|
5029
|
+
if (missing.length === 0) return;
|
|
5030
|
+
const result = await this.request("POST", `/${this.sessionId}/subscribe`, {
|
|
5031
|
+
kind: "media",
|
|
5032
|
+
items: missing.map(({ participantId, publicationId, quality }) => ({
|
|
5033
|
+
participantId,
|
|
5034
|
+
publicationId,
|
|
5035
|
+
...quality ? { quality } : {}
|
|
5036
|
+
}))
|
|
3887
5037
|
});
|
|
5038
|
+
for (const subscription of result.subscriptions) {
|
|
5039
|
+
const item = missing.find((entry) => entry.participantId === subscription.participantId && entry.publicationId === subscription.publicationId);
|
|
5040
|
+
if (!item || !subscription.mid) continue;
|
|
5041
|
+
const key = mediaKey(item.participantId, item.publicationId);
|
|
5042
|
+
this.remoteMedia.set(key, {
|
|
5043
|
+
...item,
|
|
5044
|
+
mid: subscription.mid,
|
|
5045
|
+
track: null
|
|
5046
|
+
});
|
|
5047
|
+
this.mediaByMid.set(subscription.mid, key);
|
|
5048
|
+
}
|
|
5049
|
+
if (!result.offer) return;
|
|
5050
|
+
try {
|
|
5051
|
+
await this.answerOffer(result.offer);
|
|
5052
|
+
} catch (error) {
|
|
5053
|
+
if (!(error instanceof ConnectionReplacedError)) this.reconnect();
|
|
5054
|
+
throw error;
|
|
5055
|
+
}
|
|
3888
5056
|
}
|
|
3889
|
-
async
|
|
3890
|
-
|
|
5057
|
+
async answerOffer(offer) {
|
|
5058
|
+
const pc = this.requirePc();
|
|
5059
|
+
await pc.setRemoteDescription(offer);
|
|
5060
|
+
await pc.setLocalDescription(await pc.createAnswer());
|
|
5061
|
+
await waitForIceGathering(pc, GATHER_TIMEOUT_MS);
|
|
5062
|
+
const answer = pc.localDescription;
|
|
5063
|
+
if (!answer) throw new Error("the browser produced no answer");
|
|
5064
|
+
await this.request("PUT", `/${this.sessionId}`, { answer: {
|
|
5065
|
+
type: "answer",
|
|
5066
|
+
sdp: answer.sdp
|
|
5067
|
+
} });
|
|
5068
|
+
}
|
|
5069
|
+
async localOffer(pc) {
|
|
5070
|
+
await pc.setLocalDescription(await pc.createOffer());
|
|
5071
|
+
await waitForIceGathering(pc, GATHER_TIMEOUT_MS);
|
|
5072
|
+
const offer = pc.localDescription;
|
|
5073
|
+
if (!offer) throw new Error("the browser produced no offer");
|
|
5074
|
+
return {
|
|
5075
|
+
type: "offer",
|
|
5076
|
+
sdp: offer.sdp
|
|
5077
|
+
};
|
|
3891
5078
|
}
|
|
3892
|
-
async
|
|
3893
|
-
|
|
3894
|
-
|
|
3895
|
-
|
|
5079
|
+
async request(method, path, body) {
|
|
5080
|
+
const generation = this.generation;
|
|
5081
|
+
const settled = await this.send(method, path, body).then((value) => ({ value }), (error) => ({ error }));
|
|
5082
|
+
if (generation !== this.generation) throw new ConnectionReplacedError();
|
|
5083
|
+
if ("error" in settled) throw settled.error;
|
|
5084
|
+
return settled.value;
|
|
5085
|
+
}
|
|
5086
|
+
send(method, path, body) {
|
|
5087
|
+
const url = `/api/apps/${encodeURIComponent(this.host.appId)}/realtime/rooms/${encodeURIComponent(this.host.roomId)}/rtc${path}`;
|
|
5088
|
+
return this.host.transport.request(url, {
|
|
5089
|
+
method,
|
|
5090
|
+
...body === void 0 ? {} : {
|
|
5091
|
+
headers: { "Content-Type": "application/json" },
|
|
5092
|
+
body: JSON.stringify(body)
|
|
5093
|
+
}
|
|
3896
5094
|
});
|
|
3897
5095
|
}
|
|
3898
|
-
|
|
3899
|
-
|
|
3900
|
-
|
|
3901
|
-
|
|
5096
|
+
requirePc() {
|
|
5097
|
+
if (!this.pc || !this.sessionId) throw new ConnectionReplacedError();
|
|
5098
|
+
return this.pc;
|
|
5099
|
+
}
|
|
5100
|
+
onData(remote, raw) {
|
|
5101
|
+
if (!(raw instanceof ArrayBuffer)) return;
|
|
5102
|
+
const frame = decodeAppRtcFrame(raw);
|
|
5103
|
+
if (!frame) return;
|
|
5104
|
+
const channel = this.channels.get(remote.name);
|
|
5105
|
+
if (!channel?.isOpen) return;
|
|
5106
|
+
if (channel.delivery === "latest") {
|
|
5107
|
+
if (!isNewerSequence(frame.sequence, remote.last)) return;
|
|
5108
|
+
remote.last = frame.sequence;
|
|
5109
|
+
}
|
|
5110
|
+
channel.deliver(remote.participantId, frame.data);
|
|
5111
|
+
}
|
|
5112
|
+
onTrack(event) {
|
|
5113
|
+
const mid = event.transceiver.mid;
|
|
5114
|
+
const key = mid ? this.mediaByMid.get(mid) : void 0;
|
|
5115
|
+
const item = key ? this.remoteMedia.get(key) : void 0;
|
|
5116
|
+
if (!item || !mid) return;
|
|
5117
|
+
item.track = event.track;
|
|
5118
|
+
event.track.addEventListener("ended", () => {
|
|
5119
|
+
const current = this.mediaByMid.get(mid);
|
|
5120
|
+
const owner = current ? this.remoteMedia.get(current) : void 0;
|
|
5121
|
+
if (owner?.track === event.track) {
|
|
5122
|
+
owner.track = null;
|
|
5123
|
+
this.notifyTracks();
|
|
5124
|
+
}
|
|
3902
5125
|
});
|
|
5126
|
+
this.notifyTracks();
|
|
5127
|
+
}
|
|
5128
|
+
notifyTracks() {
|
|
5129
|
+
const tracks = this.tracks;
|
|
5130
|
+
for (const handler of this.trackHandlers) handler(tracks);
|
|
5131
|
+
}
|
|
5132
|
+
setState(state) {
|
|
5133
|
+
if (this._state === state) return;
|
|
5134
|
+
this._state = state;
|
|
5135
|
+
for (const handler of this.stateHandlers) handler(state);
|
|
3903
5136
|
}
|
|
3904
5137
|
};
|
|
3905
5138
|
//#endregion
|
|
@@ -3911,13 +5144,20 @@ const byteLength = (text) => textEncoder.encode(text).byteLength;
|
|
|
3911
5144
|
* Ceiling on deltas held during a join, so a stalled handshake cannot buffer without
|
|
3912
5145
|
* limit. Dropping the tail needs no extra signal: the gap detection below reports it.
|
|
3913
5146
|
*/
|
|
3914
|
-
const
|
|
5147
|
+
const APP_ROOM_MAX_JOIN_BUFFER = 512;
|
|
3915
5148
|
/** Room events that mutate state, so they must not be applied before the join snapshot. */
|
|
3916
|
-
const
|
|
5149
|
+
const APP_ROOM_DELTA_EVENTS = /* @__PURE__ */ new Set([
|
|
3917
5150
|
"realtime.room.event",
|
|
3918
5151
|
"realtime.room.member.joined",
|
|
3919
5152
|
"realtime.room.member.left",
|
|
3920
|
-
"realtime.room.presence.updated"
|
|
5153
|
+
"realtime.room.presence.updated",
|
|
5154
|
+
"realtime.room.publications.updated"
|
|
5155
|
+
]);
|
|
5156
|
+
const APP_ROOM_MEMBER_EVENTS = /* @__PURE__ */ new Set([
|
|
5157
|
+
"realtime.room.member.joined",
|
|
5158
|
+
"realtime.room.member.left",
|
|
5159
|
+
"realtime.room.presence.updated",
|
|
5160
|
+
"realtime.room.publications.updated"
|
|
3921
5161
|
]);
|
|
3922
5162
|
const requestError = (event) => {
|
|
3923
5163
|
const payload = event.payload;
|
|
@@ -3930,6 +5170,7 @@ const isRoomEvent = (event, roomId) => {
|
|
|
3930
5170
|
var AppRoom = class {
|
|
3931
5171
|
websocket;
|
|
3932
5172
|
admission;
|
|
5173
|
+
appContext;
|
|
3933
5174
|
id;
|
|
3934
5175
|
code;
|
|
3935
5176
|
createdAt;
|
|
@@ -3957,9 +5198,12 @@ var AppRoom = class {
|
|
|
3957
5198
|
offOpen;
|
|
3958
5199
|
offClose;
|
|
3959
5200
|
offReconnecting;
|
|
3960
|
-
|
|
5201
|
+
instanceId = randomId().replace(/[^A-Za-z0-9_-]/g, "");
|
|
5202
|
+
rtcConnection = null;
|
|
5203
|
+
constructor(websocket, admission, appContext) {
|
|
3961
5204
|
this.websocket = websocket;
|
|
3962
5205
|
this.admission = admission;
|
|
5206
|
+
this.appContext = appContext;
|
|
3963
5207
|
this.id = admission.room.id;
|
|
3964
5208
|
this.code = admission.room.code;
|
|
3965
5209
|
this.createdAt = admission.room.createdAt;
|
|
@@ -4085,6 +5329,66 @@ var AppRoom = class {
|
|
|
4085
5329
|
clientEventId: typeof payload.clientEventId === "string" ? payload.clientEventId : null
|
|
4086
5330
|
};
|
|
4087
5331
|
}
|
|
5332
|
+
/** Published events after `after`, oldest first; without it, the newest ones. */
|
|
5333
|
+
async history(input = {}) {
|
|
5334
|
+
this.assertJoined();
|
|
5335
|
+
if (!this.websocket.supportsCapability("realtime.room.history.v1")) throw new Error("Room history is not supported by the Gateway");
|
|
5336
|
+
const requestId = randomId();
|
|
5337
|
+
const response = await this.request(requestId, /* @__PURE__ */ new Set(["realtime.room.history"]), () => this.websocket.readRealtimeRoomHistory({
|
|
5338
|
+
roomId: this.id,
|
|
5339
|
+
...input,
|
|
5340
|
+
requestId
|
|
5341
|
+
}));
|
|
5342
|
+
return this.toHistory(response.payload);
|
|
5343
|
+
}
|
|
5344
|
+
/** An invite for someone outside the App, e.g. an Agent: `cohub rooms join <invite>`. */
|
|
5345
|
+
async invite(input = {}) {
|
|
5346
|
+
this.assertJoined();
|
|
5347
|
+
const context = this.appContext;
|
|
5348
|
+
if (!context) throw new Error("Invites are only available inside a published App");
|
|
5349
|
+
return context.transport.request(`/api/apps/${encodeURIComponent(context.appId)}/realtime/rooms/${encodeURIComponent(this.id)}/invites`, {
|
|
5350
|
+
method: "POST",
|
|
5351
|
+
headers: { "Content-Type": "application/json" },
|
|
5352
|
+
body: JSON.stringify({
|
|
5353
|
+
ticket: this.admission.ticket,
|
|
5354
|
+
...input
|
|
5355
|
+
})
|
|
5356
|
+
});
|
|
5357
|
+
}
|
|
5358
|
+
rtc() {
|
|
5359
|
+
this.assertJoined();
|
|
5360
|
+
if (this.rtcConnection) {
|
|
5361
|
+
const current = this.rtcConnection;
|
|
5362
|
+
return current.then((rtc) => {
|
|
5363
|
+
if (rtc.state !== "closed") return rtc;
|
|
5364
|
+
return (this.rtcConnection === current ? this.openRtc() : this.rtcConnection) ?? this.openRtc();
|
|
5365
|
+
});
|
|
5366
|
+
}
|
|
5367
|
+
return this.openRtc();
|
|
5368
|
+
}
|
|
5369
|
+
openRtc() {
|
|
5370
|
+
this.assertJoined();
|
|
5371
|
+
const context = this.appContext;
|
|
5372
|
+
if (!context) return Promise.reject(new AppRtcError("RTC_UNSUPPORTED", "RTC is only available inside a published App"));
|
|
5373
|
+
if (typeof RTCPeerConnection === "undefined") return Promise.reject(new AppRtcError("RTC_UNSUPPORTED", "RTC needs a browser with WebRTC"));
|
|
5374
|
+
if (!this.websocket.supportsCapability("realtime.room.rtc.v1")) return Promise.reject(new AppRtcError("RTC_UNSUPPORTED", "RTC is not supported by the Gateway"));
|
|
5375
|
+
const rtc = new AppRoomRtc({
|
|
5376
|
+
roomId: this.id,
|
|
5377
|
+
appId: context.appId,
|
|
5378
|
+
ticket: this.admission.ticket,
|
|
5379
|
+
transport: context.transport,
|
|
5380
|
+
participantId: () => this._participantId,
|
|
5381
|
+
members: () => this._members,
|
|
5382
|
+
onMembersChanged: (handler) => this.onMembersChanged(handler)
|
|
5383
|
+
});
|
|
5384
|
+
const opening = rtc.start().then(() => rtc, (error) => {
|
|
5385
|
+
rtc.close();
|
|
5386
|
+
if (this.rtcConnection === opening) this.rtcConnection = null;
|
|
5387
|
+
throw error;
|
|
5388
|
+
});
|
|
5389
|
+
this.rtcConnection = opening;
|
|
5390
|
+
return opening;
|
|
5391
|
+
}
|
|
4088
5392
|
async setPresence(presence) {
|
|
4089
5393
|
this.assertJoined();
|
|
4090
5394
|
const requestId = randomId();
|
|
@@ -4126,17 +5430,23 @@ var AppRoom = class {
|
|
|
4126
5430
|
if (!this.websocket.supportsCapability("realtime.room.v1") && this.websocket.state === "open") throw new Error("Realtime rooms are not supported by the Gateway");
|
|
4127
5431
|
this.setState(isReconnect ? "reconnecting" : "connecting");
|
|
4128
5432
|
const requestId = randomId();
|
|
5433
|
+
const after = isReconnect && this.lastSequence > 0 && this.websocket.supportsCapability("realtime.room.history.v1") ? this.lastSequence : void 0;
|
|
5434
|
+
let missed = null;
|
|
4129
5435
|
this.joinBuffer = [];
|
|
4130
5436
|
try {
|
|
4131
5437
|
const payload = (await this.request(requestId, /* @__PURE__ */ new Set(["realtime.room.joined"]), () => this.websocket.joinRealtimeRoom({
|
|
4132
5438
|
roomId: this.id,
|
|
4133
5439
|
ticket: this.admission.ticket,
|
|
5440
|
+
instanceId: this.instanceId,
|
|
5441
|
+
after,
|
|
4134
5442
|
requestId
|
|
4135
5443
|
}))).payload;
|
|
4136
5444
|
if (typeof payload.participantId !== "string" || !Array.isArray(payload.members)) throw new Error("invalid room join response");
|
|
4137
5445
|
this._participantId = payload.participantId;
|
|
4138
5446
|
this._members = payload.members;
|
|
4139
|
-
|
|
5447
|
+
const sequence = typeof payload.sequence === "number" ? payload.sequence : 0;
|
|
5448
|
+
if (after !== void 0 && sequence > after) missed = await this.catchUp(payload.missed, after, sequence);
|
|
5449
|
+
this.lastSequence = sequence;
|
|
4140
5450
|
} catch (error) {
|
|
4141
5451
|
this.joinBuffer = null;
|
|
4142
5452
|
throw error;
|
|
@@ -4144,6 +5454,11 @@ var AppRoom = class {
|
|
|
4144
5454
|
this.hasJoined = true;
|
|
4145
5455
|
this.setState("joined");
|
|
4146
5456
|
this.notifyMembers();
|
|
5457
|
+
if (missed) {
|
|
5458
|
+
const { lost } = missed;
|
|
5459
|
+
if (lost) for (const handler of this.outOfSyncHandlers) handler(lost[0], lost[1]);
|
|
5460
|
+
for (const event of missed.events) this.emit(event);
|
|
5461
|
+
}
|
|
4147
5462
|
const buffered = this.joinBuffer ?? [];
|
|
4148
5463
|
this.joinBuffer = null;
|
|
4149
5464
|
for (const item of buffered) {
|
|
@@ -4151,6 +5466,7 @@ var AppRoom = class {
|
|
|
4151
5466
|
if (typeof sequence === "number" && sequence <= this.lastSequence) continue;
|
|
4152
5467
|
this.handleEvent(item);
|
|
4153
5468
|
}
|
|
5469
|
+
if (isReconnect) this.rtcConnection?.then((rtc) => rtc.verify()).catch(() => void 0);
|
|
4154
5470
|
}
|
|
4155
5471
|
request(requestId, expected, send) {
|
|
4156
5472
|
return new Promise((resolve, reject) => {
|
|
@@ -4198,14 +5514,13 @@ var AppRoom = class {
|
|
|
4198
5514
|
this.takePending(event.requestId);
|
|
4199
5515
|
awaiting.resolve(event);
|
|
4200
5516
|
}
|
|
4201
|
-
if (this.joinBuffer &&
|
|
4202
|
-
if (this.joinBuffer.length <
|
|
5517
|
+
if (this.joinBuffer && APP_ROOM_DELTA_EVENTS.has(event.type)) {
|
|
5518
|
+
if (this.joinBuffer.length < APP_ROOM_MAX_JOIN_BUFFER) this.joinBuffer.push(event);
|
|
4203
5519
|
return;
|
|
4204
5520
|
}
|
|
4205
5521
|
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") {
|
|
5522
|
+
if (APP_ROOM_DELTA_EVENTS.has(event.type) && typeof payload.sequence === "number") this.observeSequence(payload.sequence);
|
|
5523
|
+
if (APP_ROOM_MEMBER_EVENTS.has(event.type)) {
|
|
4209
5524
|
const member = payload.member;
|
|
4210
5525
|
if (member?.participantId) {
|
|
4211
5526
|
const members = new Map(this._members.map((item) => [item.participantId, item]));
|
|
@@ -4225,11 +5540,10 @@ var AppRoom = class {
|
|
|
4225
5540
|
return;
|
|
4226
5541
|
}
|
|
4227
5542
|
if (event.type !== "realtime.room.event") return;
|
|
4228
|
-
const
|
|
4229
|
-
|
|
4230
|
-
|
|
4231
|
-
|
|
4232
|
-
timestamp: roomEvent.timestamp,
|
|
5543
|
+
const { id, timestamp, payload: roomPayload } = event;
|
|
5544
|
+
this.emit({
|
|
5545
|
+
id,
|
|
5546
|
+
timestamp,
|
|
4233
5547
|
roomId: roomPayload.roomId,
|
|
4234
5548
|
sequence: roomPayload.sequence,
|
|
4235
5549
|
type: roomPayload.event,
|
|
@@ -4237,7 +5551,35 @@ var AppRoom = class {
|
|
|
4237
5551
|
clientEventId: roomPayload.clientEventId,
|
|
4238
5552
|
sender: roomPayload.sender,
|
|
4239
5553
|
self: roomPayload.sender.participantId === this.participantId
|
|
5554
|
+
});
|
|
5555
|
+
}
|
|
5556
|
+
async catchUp(first, after, sequence) {
|
|
5557
|
+
if (!first) return {
|
|
5558
|
+
events: [],
|
|
5559
|
+
lost: [after + 1, sequence]
|
|
5560
|
+
};
|
|
5561
|
+
const events = this.toHistory(first).events;
|
|
5562
|
+
const { truncated } = first;
|
|
5563
|
+
let { cursor } = first;
|
|
5564
|
+
while (cursor < sequence) {
|
|
5565
|
+
const requestId = randomId();
|
|
5566
|
+
const page = (await this.request(requestId, /* @__PURE__ */ new Set(["realtime.room.history"]), () => this.websocket.readRealtimeRoomHistory({
|
|
5567
|
+
roomId: this.id,
|
|
5568
|
+
after: cursor,
|
|
5569
|
+
requestId
|
|
5570
|
+
})).catch(() => null))?.payload;
|
|
5571
|
+
if (!page || page.cursor <= cursor) break;
|
|
5572
|
+
events.push(...this.toHistory(page).events);
|
|
5573
|
+
cursor = page.cursor;
|
|
5574
|
+
}
|
|
5575
|
+
const kept = events.filter((event) => event.sequence <= sequence);
|
|
5576
|
+
const lost = truncated ? [after + 1, kept[0]?.sequence ?? sequence] : cursor < sequence ? [cursor + 1, sequence] : null;
|
|
5577
|
+
return {
|
|
5578
|
+
events: kept,
|
|
5579
|
+
lost: lost && lost[1] > lost[0] ? lost : null
|
|
4240
5580
|
};
|
|
5581
|
+
}
|
|
5582
|
+
emit(item) {
|
|
4241
5583
|
this.handlers.get(item.type)?.forEach((handler) => {
|
|
4242
5584
|
handler(item);
|
|
4243
5585
|
});
|
|
@@ -4245,6 +5587,18 @@ var AppRoom = class {
|
|
|
4245
5587
|
handler(item);
|
|
4246
5588
|
});
|
|
4247
5589
|
}
|
|
5590
|
+
toHistory(history) {
|
|
5591
|
+
return {
|
|
5592
|
+
events: history.events.map((event) => ({
|
|
5593
|
+
...event,
|
|
5594
|
+
data: event.data,
|
|
5595
|
+
roomId: this.id,
|
|
5596
|
+
self: event.sender.participantId === this.participantId
|
|
5597
|
+
})),
|
|
5598
|
+
cursor: history.cursor,
|
|
5599
|
+
truncated: history.truncated
|
|
5600
|
+
};
|
|
5601
|
+
}
|
|
4248
5602
|
observeSequence(sequence) {
|
|
4249
5603
|
if (this.lastSequence > 0 && sequence > this.lastSequence + 1) for (const handler of this.outOfSyncHandlers) handler(this.lastSequence + 1, sequence);
|
|
4250
5604
|
if (sequence > this.lastSequence) this.lastSequence = sequence;
|
|
@@ -4276,6 +5630,9 @@ var AppRoom = class {
|
|
|
4276
5630
|
this.dispose();
|
|
4277
5631
|
}
|
|
4278
5632
|
dispose() {
|
|
5633
|
+
const rtc = this.rtcConnection;
|
|
5634
|
+
this.rtcConnection = null;
|
|
5635
|
+
rtc?.then((connection) => connection.close()).catch(() => void 0);
|
|
4279
5636
|
this.offEvent();
|
|
4280
5637
|
this.offOpen();
|
|
4281
5638
|
this.offClose();
|
|
@@ -4294,28 +5651,33 @@ var AppRealtimeApi = class {
|
|
|
4294
5651
|
this.getContext = getContext;
|
|
4295
5652
|
}
|
|
4296
5653
|
async createRoom(input = {}) {
|
|
4297
|
-
const
|
|
5654
|
+
const appId = await this.requireAppId();
|
|
5655
|
+
const admission = await this.transport.request(`/api/apps/${encodeURIComponent(appId)}/realtime/rooms`, {
|
|
4298
5656
|
method: "POST",
|
|
4299
5657
|
headers: { "Content-Type": "application/json" },
|
|
4300
5658
|
body: JSON.stringify(input)
|
|
4301
5659
|
});
|
|
4302
|
-
return this.openRoom(admission);
|
|
5660
|
+
return this.openRoom(appId, admission);
|
|
4303
5661
|
}
|
|
4304
5662
|
async joinRoom(input) {
|
|
4305
|
-
const
|
|
5663
|
+
const appId = await this.requireAppId();
|
|
5664
|
+
const admission = await this.transport.request(`/api/apps/${encodeURIComponent(appId)}/realtime/rooms/join`, {
|
|
4306
5665
|
method: "POST",
|
|
4307
5666
|
headers: { "Content-Type": "application/json" },
|
|
4308
5667
|
body: JSON.stringify(input)
|
|
4309
5668
|
});
|
|
4310
|
-
return this.openRoom(admission);
|
|
5669
|
+
return this.openRoom(appId, admission);
|
|
4311
5670
|
}
|
|
4312
5671
|
async requireAppId() {
|
|
4313
5672
|
const context = await this.getContext();
|
|
4314
5673
|
if (!context?.app?.id) throw new Error("App context is unavailable — not running inside a published app runtime.");
|
|
4315
5674
|
return context.app.id;
|
|
4316
5675
|
}
|
|
4317
|
-
async openRoom(admission) {
|
|
4318
|
-
const room = new AppRoom(this.websocket, admission
|
|
5676
|
+
async openRoom(appId, admission) {
|
|
5677
|
+
const room = new AppRoom(this.websocket, admission, {
|
|
5678
|
+
transport: this.transport,
|
|
5679
|
+
appId
|
|
5680
|
+
});
|
|
4319
5681
|
try {
|
|
4320
5682
|
await room.connect();
|
|
4321
5683
|
return room;
|
|
@@ -4582,10 +5944,12 @@ var AppWindowApi = class {
|
|
|
4582
5944
|
}
|
|
4583
5945
|
/** Report the tab title, save status, and whether work is still unsaved. */
|
|
4584
5946
|
setState(patch) {
|
|
4585
|
-
|
|
5947
|
+
const next = {
|
|
4586
5948
|
...this.state,
|
|
4587
5949
|
...patch
|
|
4588
5950
|
};
|
|
5951
|
+
if (this.stateReported && next.title === this.state.title && next.status === this.state.status && next.dirty === this.state.dirty) return;
|
|
5952
|
+
this.state = next;
|
|
4589
5953
|
this.stateReported = true;
|
|
4590
5954
|
this.listen();
|
|
4591
5955
|
this.transport.notify?.(buildAppRuntimeWindowState(this.state));
|
|
@@ -5392,6 +6756,18 @@ var AppRuntimeApi = class {
|
|
|
5392
6756
|
reason: response === null ? "timeout" : "unsupported"
|
|
5393
6757
|
};
|
|
5394
6758
|
}
|
|
6759
|
+
/** Asks for devices declared with `<meta name="cohub:devices">`; allowing the camera or microphone reloads the App once. */
|
|
6760
|
+
async requestDevices(devices) {
|
|
6761
|
+
const wanted = parseAppDevices(devices);
|
|
6762
|
+
if (wanted.length === 0) throw new AppRuntimeError("invalid_request", `Request at least one of: ${APP_DEVICES.join(", ")}.`);
|
|
6763
|
+
const context = await this.context().catch(() => null);
|
|
6764
|
+
if (!context || context.mode === "broker") return "granted";
|
|
6765
|
+
if (!context.capabilities?.devices) return "unavailable";
|
|
6766
|
+
return parseAppDevicesResult(await this.transport.request({
|
|
6767
|
+
type: APP_DEVICES_REQUEST_TYPE,
|
|
6768
|
+
devices: wanted
|
|
6769
|
+
}, { timeoutMs: 6e5 }))?.status ?? "unavailable";
|
|
6770
|
+
}
|
|
5395
6771
|
/**
|
|
5396
6772
|
* Asks the host to close this App: a workspace tab closes, an embedded page
|
|
5397
6773
|
* forwards the request to its embedder, a standalone page closes the tab.
|
|
@@ -5756,6 +7132,7 @@ var CohubClient = class {
|
|
|
5756
7132
|
voice;
|
|
5757
7133
|
apps;
|
|
5758
7134
|
appCommerce;
|
|
7135
|
+
rooms;
|
|
5759
7136
|
navigation;
|
|
5760
7137
|
/** @deprecated Use `client.desktop`. */
|
|
5761
7138
|
get ui() {
|
|
@@ -5786,7 +7163,10 @@ var CohubClient = class {
|
|
|
5786
7163
|
const appTransport = resolveAppTransport(appRuntime, appIdResolver, createOriginAppResolver({
|
|
5787
7164
|
apiBaseUrl,
|
|
5788
7165
|
fetch: options.fetch
|
|
5789
|
-
}), resolveWebBaseUrl({
|
|
7166
|
+
}), resolveWebBaseUrl({
|
|
7167
|
+
env: options.env,
|
|
7168
|
+
url: options.webBaseUrl
|
|
7169
|
+
}));
|
|
5790
7170
|
this.appRuntime = createAppRuntime(appTransport, appRuntime?.appId, appIdResolver);
|
|
5791
7171
|
this.appWindow = new AppWindowApi(appTransport, {
|
|
5792
7172
|
context: () => this.appRuntime.context(),
|
|
@@ -5835,6 +7215,7 @@ var CohubClient = class {
|
|
|
5835
7215
|
this.referrals = new ReferralsApi(this.transport);
|
|
5836
7216
|
this.apps = new AppsApi(this.transport);
|
|
5837
7217
|
this.appCommerce = new AppCommerceApi(this.transport);
|
|
7218
|
+
this.rooms = new RoomsApi(this.transport);
|
|
5838
7219
|
this.navigation = { open: (target, options) => this.appRuntime.navigationOpen(typeof target === "string" ? {
|
|
5839
7220
|
kind: "app",
|
|
5840
7221
|
ref: target
|
|
@@ -5891,6 +7272,7 @@ sync: (root) => {
|
|
|
5891
7272
|
} },
|
|
5892
7273
|
/** Ask the host to close this App's surface. */
|
|
5893
7274
|
requestClose: () => this.appRuntime.requestClose(),
|
|
7275
|
+
devices: { request: (devices) => this.appRuntime.requestDevices(devices) },
|
|
5894
7276
|
/**
|
|
5895
7277
|
* Request that the host update this overlay's geometry or pointer hit
|
|
5896
7278
|
* regions. Only meaningful for `overlay` surfaces; ignored otherwise.
|
|
@@ -6564,7 +7946,8 @@ function createAppBridgeCore(config) {
|
|
|
6564
7946
|
return {
|
|
6565
7947
|
capabilities: {
|
|
6566
7948
|
authorization: 2,
|
|
6567
|
-
serverGrants: true
|
|
7949
|
+
serverGrants: true,
|
|
7950
|
+
...config.devices ? { devices: true } : {}
|
|
6568
7951
|
},
|
|
6569
7952
|
mode: authorizationContext.surface === "broker" ? "broker" : "bridge",
|
|
6570
7953
|
app: {
|
|
@@ -7623,129 +9006,6 @@ function createAppBridgeCore(config) {
|
|
|
7623
9006
|
};
|
|
7624
9007
|
}
|
|
7625
9008
|
//#endregion
|
|
7626
|
-
//#region src/generation-blocks.ts
|
|
7627
|
-
/** Tolerant readers for generation payloads, shared by every task projection. */
|
|
7628
|
-
const EXCERPT_LIMIT = 240;
|
|
7629
|
-
function record(value) {
|
|
7630
|
-
return value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
7631
|
-
}
|
|
7632
|
-
function cleanExcerpt(value, limit = EXCERPT_LIMIT) {
|
|
7633
|
-
if (typeof value !== "string") return void 0;
|
|
7634
|
-
const clean = value.replace(/\s+/g, " ").trim();
|
|
7635
|
-
if (!clean) return void 0;
|
|
7636
|
-
return clean.length > limit ? `${clean.slice(0, limit - 3).trimEnd()}...` : clean;
|
|
7637
|
-
}
|
|
7638
|
-
function blockText(block) {
|
|
7639
|
-
return cleanExcerpt(block.text ?? block.content ?? block.value);
|
|
7640
|
-
}
|
|
7641
|
-
function blockUrl(block) {
|
|
7642
|
-
const source = record(block.source);
|
|
7643
|
-
return normalizeBoardRemoteUrl(source?.url ?? source?.src ?? block.url ?? block.src);
|
|
7644
|
-
}
|
|
7645
|
-
function blockMimeType(block) {
|
|
7646
|
-
const source = record(block.source);
|
|
7647
|
-
const value = source?.mediaType ?? source?.media_type ?? source?.mimeType ?? block.mediaType ?? block.media_type ?? block.mimeType;
|
|
7648
|
-
return typeof value === "string" ? value : void 0;
|
|
7649
|
-
}
|
|
7650
|
-
function positiveNumber(value) {
|
|
7651
|
-
return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : void 0;
|
|
7652
|
-
}
|
|
7653
|
-
function blockNaturalSize(block) {
|
|
7654
|
-
const source = record(block.source);
|
|
7655
|
-
const naturalWidth = positiveNumber(source?.width ?? source?.naturalWidth ?? block.width ?? block.naturalWidth);
|
|
7656
|
-
const naturalHeight = positiveNumber(source?.height ?? source?.naturalHeight ?? block.height ?? block.naturalHeight);
|
|
7657
|
-
return {
|
|
7658
|
-
...naturalWidth ? { naturalWidth } : {},
|
|
7659
|
-
...naturalHeight ? { naturalHeight } : {}
|
|
7660
|
-
};
|
|
7661
|
-
}
|
|
7662
|
-
function contentBlocks(value) {
|
|
7663
|
-
return Array.isArray(value) ? value.filter((item) => record(item)) : [];
|
|
7664
|
-
}
|
|
7665
|
-
function taskData(run) {
|
|
7666
|
-
const payload = record(run.payload);
|
|
7667
|
-
return record(payload?.data) ?? payload;
|
|
7668
|
-
}
|
|
7669
|
-
function generationOutput(run) {
|
|
7670
|
-
return contentBlocks(record(run.result)?.output);
|
|
7671
|
-
}
|
|
7672
|
-
function generationPrompt(run) {
|
|
7673
|
-
for (const block of contentBlocks(taskData(run)?.content)) {
|
|
7674
|
-
const text = blockText(block);
|
|
7675
|
-
if (text) return text;
|
|
7676
|
-
}
|
|
7677
|
-
}
|
|
7678
|
-
function blockMeta(block) {
|
|
7679
|
-
return record(block.meta);
|
|
7680
|
-
}
|
|
7681
|
-
function blockIdentity(block) {
|
|
7682
|
-
const meta = blockMeta(block);
|
|
7683
|
-
const value = meta?.id ?? meta?.clip_id ?? meta?.clipId ?? block.id ?? block.clip_id ?? block.clipId;
|
|
7684
|
-
if (typeof value !== "string" && typeof value !== "number") return void 0;
|
|
7685
|
-
return cleanExcerpt(String(value), 220);
|
|
7686
|
-
}
|
|
7687
|
-
function blockTitle(block) {
|
|
7688
|
-
return cleanExcerpt(blockMeta(block)?.title ?? block.title ?? block.name, 240);
|
|
7689
|
-
}
|
|
7690
|
-
function blockDurationMs(block) {
|
|
7691
|
-
const source = record(block.source);
|
|
7692
|
-
const meta = blockMeta(block);
|
|
7693
|
-
const milliseconds = positiveNumber(source?.durationMs ?? meta?.durationMs ?? block.durationMs);
|
|
7694
|
-
if (milliseconds) return Math.round(milliseconds);
|
|
7695
|
-
const seconds = positiveNumber(source?.duration ?? meta?.duration ?? block.duration);
|
|
7696
|
-
return seconds ? Math.round(seconds * 1e3) : void 0;
|
|
7697
|
-
}
|
|
7698
|
-
function blockPreviewUrl(block) {
|
|
7699
|
-
const source = record(block.source);
|
|
7700
|
-
return normalizeBoardRemoteUrl(source?.poster ?? source?.thumbnail ?? source?.previewUrl ?? block.poster ?? block.thumbnail ?? block.previewUrl);
|
|
7701
|
-
}
|
|
7702
|
-
/**
|
|
7703
|
-
* Image roles that depict playable media, by the media types they may cover;
|
|
7704
|
-
* `last_frame` stays a result of its own.
|
|
7705
|
-
*/
|
|
7706
|
-
const COVER_ROLES = /* @__PURE__ */ new Map([
|
|
7707
|
-
["first_frame", /* @__PURE__ */ new Set(["video"])],
|
|
7708
|
-
["cover", /* @__PURE__ */ new Set(["video", "audio"])],
|
|
7709
|
-
["poster", /* @__PURE__ */ new Set(["video", "audio"])],
|
|
7710
|
-
["thumbnail", /* @__PURE__ */ new Set(["video", "audio"])]
|
|
7711
|
-
]);
|
|
7712
|
-
function isPlayable(block) {
|
|
7713
|
-
return block.type === "video" || block.type === "audio";
|
|
7714
|
-
}
|
|
7715
|
-
/**
|
|
7716
|
-
* Pair playable media with its cover image, returning media index → cover
|
|
7717
|
-
* index. An image sharing the provider id wins (music tracks and their art,
|
|
7718
|
-
* shared by every media of that id); otherwise cover-role images pair in
|
|
7719
|
-
* order with remaining media their role fits (a video and its `first_frame`).
|
|
7720
|
-
* Covers need a remote URL, so an inline image is never folded away.
|
|
7721
|
-
*/
|
|
7722
|
-
function generationCovers(blocks) {
|
|
7723
|
-
const covers = /* @__PURE__ */ new Map();
|
|
7724
|
-
const isCover = (block) => block.type === "image" && blockUrl(block) !== void 0 && blockMeta(block)?.role !== "last_frame";
|
|
7725
|
-
blocks.forEach((block, index) => {
|
|
7726
|
-
const id = blockIdentity(block);
|
|
7727
|
-
if (!isPlayable(block) || id === void 0) return;
|
|
7728
|
-
const cover = blocks.findIndex((candidate) => isCover(candidate) && blockIdentity(candidate) === id);
|
|
7729
|
-
if (cover >= 0) covers.set(index, cover);
|
|
7730
|
-
});
|
|
7731
|
-
const paired = new Set(covers.values());
|
|
7732
|
-
const roleCovers = blocks.flatMap((block, index) => {
|
|
7733
|
-
const role = blockMeta(block)?.role;
|
|
7734
|
-
const types = typeof role === "string" ? COVER_ROLES.get(role) : void 0;
|
|
7735
|
-
return isCover(block) && !paired.has(index) && types ? [{
|
|
7736
|
-
index,
|
|
7737
|
-
types
|
|
7738
|
-
}] : [];
|
|
7739
|
-
});
|
|
7740
|
-
blocks.forEach((block, index) => {
|
|
7741
|
-
if (!isPlayable(block) || covers.has(index)) return;
|
|
7742
|
-
const at = roleCovers.findIndex(({ types }) => types.has(String(block.type)));
|
|
7743
|
-
const [cover] = at < 0 ? [] : roleCovers.splice(at, 1);
|
|
7744
|
-
if (cover) covers.set(index, cover.index);
|
|
7745
|
-
});
|
|
7746
|
-
return covers;
|
|
7747
|
-
}
|
|
7748
|
-
//#endregion
|
|
7749
9009
|
//#region src/generation-task.ts
|
|
7750
9010
|
const INLINE_KEYS = [
|
|
7751
9011
|
"data",
|
|
@@ -7753,7 +9013,7 @@ const INLINE_KEYS = [
|
|
|
7753
9013
|
"contentBase64"
|
|
7754
9014
|
];
|
|
7755
9015
|
function isDeferred(block) {
|
|
7756
|
-
return block.deferredBase64 === true ||
|
|
9016
|
+
return block.deferredBase64 === true || asRecord(block.source)?.deferredBase64 === true;
|
|
7757
9017
|
}
|
|
7758
9018
|
const MEDIA_TYPE_PATTERN = /^(image|video|audio)\/[\w.+-]+$/;
|
|
7759
9019
|
const DEFAULT_MEDIA_TYPES = {
|
|
@@ -7796,7 +9056,7 @@ function projectOutputs(run) {
|
|
|
7796
9056
|
}
|
|
7797
9057
|
function toGenerationTaskView(run) {
|
|
7798
9058
|
const data = taskData(run);
|
|
7799
|
-
const result =
|
|
9059
|
+
const result = asRecord(run.result);
|
|
7800
9060
|
const model = data?.model ?? result?.model;
|
|
7801
9061
|
return {
|
|
7802
9062
|
id: run.id,
|
|
@@ -7819,15 +9079,15 @@ function isActiveGenerationTask(task) {
|
|
|
7819
9079
|
}
|
|
7820
9080
|
/** Playable source of an output: its URL, or a data URL for inline payloads. */
|
|
7821
9081
|
function generationOutputSource(result, outputIndex) {
|
|
7822
|
-
const block = contentBlocks(
|
|
9082
|
+
const block = contentBlocks(asRecord(result)?.output)[outputIndex];
|
|
7823
9083
|
if (!block) return null;
|
|
7824
9084
|
const url = blockUrl(block);
|
|
7825
9085
|
if (url) return url;
|
|
7826
|
-
const source =
|
|
9086
|
+
const source = asRecord(block.source);
|
|
7827
9087
|
const data = INLINE_KEYS.map((key) => source?.[key] ?? block[key]).find((value) => typeof value === "string" && value !== "");
|
|
7828
9088
|
if (!data) return null;
|
|
7829
9089
|
const declared = blockMimeType(block);
|
|
7830
9090
|
return `data:${declared && MEDIA_TYPE_PATTERN.test(declared) ? declared : DEFAULT_MEDIA_TYPES[String(block.type)] ?? "application/octet-stream"};base64,${data}`;
|
|
7831
9091
|
}
|
|
7832
9092
|
//#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 };
|
|
9093
|
+
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 };
|