oysterun 1.5.7-beta.7 → 1.5.7-beta.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. package/dev/client/connectivity/internal-connectivity-status.d.mts +14 -0
  2. package/dev/client/connectivity/internal-connectivity-status.mjs +59 -0
  3. package/dev/client/connectivity/logical-host-connection.mjs +21 -14
  4. package/dev/client/connectivity/managed-host-browser.d.mts +20 -0
  5. package/dev/client/connectivity/managed-host-browser.mjs +128 -34
  6. package/dev/client/connectivity/webrtc-logical-transport.mjs +336 -22
  7. package/dev/client/web/index.html +487 -63
  8. package/dev/client/web-chat/dist/assets/{ReactPrism-DZ1Ev_vc.js → ReactPrism-BVwTeJ2J.js} +1 -1
  9. package/dev/client/web-chat/dist/assets/{ReactPrism-DZ1Ev_vc.js.map → ReactPrism-BVwTeJ2J.js.map} +1 -1
  10. package/dev/client/web-chat/dist/assets/{index-D-3SLM-g.js → index-B0-fm02U.js} +80 -80
  11. package/dev/client/web-chat/dist/assets/index-B0-fm02U.js.map +1 -0
  12. package/dev/client/web-chat/dist/assets/{index-BxiONAdX.js → index-BIJqUapL.js} +2 -2
  13. package/dev/client/web-chat/dist/assets/{index-BxiONAdX.js.map → index-BIJqUapL.js.map} +1 -1
  14. package/dev/client/web-chat/dist/assets/{index-f13huqVE.js → index-Bcsmmpmp.js} +1 -1
  15. package/dev/client/web-chat/dist/assets/{index-f13huqVE.js.map → index-Bcsmmpmp.js.map} +1 -1
  16. package/dev/client/web-chat/dist/assets/{pdf-DqKSOCNK.js → pdf-Dx9dlfRj.js} +3 -3
  17. package/dev/client/web-chat/dist/assets/pdf-Dx9dlfRj.js.map +1 -0
  18. package/dev/client/web-chat/dist/assets/{web-LmiNr0Ov.js → web-CNSm9WCL.js} +1 -1
  19. package/dev/client/web-chat/dist/assets/{web-LmiNr0Ov.js.map → web-CNSm9WCL.js.map} +1 -1
  20. package/dev/client/web-chat/dist/assets/{web-BgssiYmu.js → web-Ql5RPS3a.js} +1 -1
  21. package/dev/client/web-chat/dist/assets/{web-BgssiYmu.js.map → web-Ql5RPS3a.js.map} +1 -1
  22. package/dev/client/web-chat/dist/index.html +1 -1
  23. package/host-service/config.mjs +7 -0
  24. package/host-service/connectivity/webrtc-host-dispatcher.mjs +18 -10
  25. package/host-service/connectivity/webrtc-signaling-service.mjs +67 -16
  26. package/host-service/matrix-facade.mjs +16 -0
  27. package/host-service/package.json +1 -1
  28. package/package.json +1 -1
  29. package/dev/client/web-chat/dist/assets/index-D-3SLM-g.js.map +0 -1
  30. package/dev/client/web-chat/dist/assets/pdf-DqKSOCNK.js.map +0 -1
@@ -0,0 +1,14 @@
1
+ export type InternalConnectivityStatus = {
2
+ matrix: "Connected" | "Reconnecting" | "Error" | "Offline";
3
+ activePath: "WebRTC" | "Bootstrap" | "Direct" | "None";
4
+ promotion: "Idle" | `Attempt ${number}/4` | "Quiesced" | "Not applicable";
5
+ };
6
+
7
+ export const INTERNAL_CONNECTIVITY_PROMOTION_ATTEMPT_LIMIT: 4;
8
+ export function deriveInternalConnectivityStatus(input?: {
9
+ syncState?: string | null;
10
+ transportStatus?: Record<string, unknown>;
11
+ }): InternalConnectivityStatus;
12
+ export function internalConnectivityStatusText(
13
+ status: InternalConnectivityStatus,
14
+ ): string;
@@ -0,0 +1,59 @@
1
+ const MATRIX_STATUS = Object.freeze({
2
+ ERROR: "Error",
3
+ CATCHUP: "Reconnecting",
4
+ RECONNECTING: "Reconnecting",
5
+ PREPARED: "Connected",
6
+ SYNCING: "Connected",
7
+ STOPPED: "Offline",
8
+ });
9
+
10
+ const ACTIVE_PATH_STATUS = Object.freeze({
11
+ webrtc: "WebRTC",
12
+ bootstrap: "Bootstrap",
13
+ direct_host: "Direct",
14
+ });
15
+
16
+ export const INTERNAL_CONNECTIVITY_PROMOTION_ATTEMPT_LIMIT = 4;
17
+
18
+ function finiteNonNegativeInteger(value) {
19
+ return Number.isSafeInteger(value) && value >= 0 ? value : 0;
20
+ }
21
+
22
+ export function deriveInternalConnectivityStatus({
23
+ syncState = null,
24
+ transportStatus = {},
25
+ } = {}) {
26
+ const browserOffline = transportStatus.browser_online === false;
27
+ const transportConnectionState = transportStatus.connection_state;
28
+ const matrix = browserOffline
29
+ ? "Offline"
30
+ : MATRIX_STATUS[syncState] ||
31
+ (transportConnectionState === "recovering" ||
32
+ transportConnectionState === "disconnected"
33
+ ? "Reconnecting"
34
+ : transportConnectionState === "connected"
35
+ ? "Connected"
36
+ : "Offline");
37
+ const activePath =
38
+ ACTIVE_PATH_STATUS[transportStatus.active_path_kind] || "None";
39
+
40
+ let promotion = "Not applicable";
41
+ if (transportStatus.managed_transport_enabled === true) {
42
+ const attempt = Math.min(
43
+ finiteNonNegativeInteger(transportStatus.promotion_attempt_count),
44
+ INTERNAL_CONNECTIVITY_PROMOTION_ATTEMPT_LIMIT,
45
+ );
46
+ promotion =
47
+ transportStatus.retry_quiesced === true
48
+ ? "Quiesced"
49
+ : attempt > 0
50
+ ? `Attempt ${attempt}/${INTERNAL_CONNECTIVITY_PROMOTION_ATTEMPT_LIMIT}`
51
+ : "Idle";
52
+ }
53
+
54
+ return Object.freeze({ matrix, activePath, promotion });
55
+ }
56
+
57
+ export function internalConnectivityStatusText(status) {
58
+ return `Debug Network Connection Status · Matrix: ${status.matrix} · Active path: ${status.activePath} · WebRTC promotion: ${status.promotion}`;
59
+ }
@@ -16,7 +16,9 @@ async function withTimeout(promise, timeoutMs, message) {
16
16
  const timeout = new Promise((_, reject) => {
17
17
  timeoutId = setTimeout(() => reject(new Error(message)), timeoutMs);
18
18
  });
19
- return Promise.race([promise, timeout]).finally(() => clearTimeout(timeoutId));
19
+ return Promise.race([promise, timeout]).finally(() =>
20
+ clearTimeout(timeoutId)
21
+ );
20
22
  }
21
23
 
22
24
  function headersToObject(headers) {
@@ -84,13 +86,22 @@ async function fetchWithDeadline(fetchImpl, url, options, timeoutMs) {
84
86
  }
85
87
 
86
88
  export class BootstrapFetchTransport {
87
- constructor({ fetchImpl = globalThis.fetch, origin = "", diagnostic } = {}) {
89
+ constructor({
90
+ fetchImpl = globalThis.fetch,
91
+ origin = "",
92
+ diagnostic,
93
+ responseGuard = null,
94
+ } = {}) {
88
95
  if (typeof fetchImpl !== "function") {
89
96
  throw new Error("Bootstrap fetch implementation is required");
90
97
  }
98
+ if (responseGuard !== null && typeof responseGuard !== "function") {
99
+ throw new Error("Bootstrap response guard must be a function");
100
+ }
91
101
  this.fetchImpl = fetchImpl;
92
102
  this.origin = origin.replace(/\/+$/, "");
93
103
  this.diagnostic = diagnostic;
104
+ this.responseGuard = responseGuard;
94
105
  this.pathKind = "bootstrap";
95
106
  }
96
107
 
@@ -109,6 +120,7 @@ export class BootstrapFetchTransport {
109
120
  },
110
121
  request.timeoutMs
111
122
  );
123
+ await this.responseGuard?.(response);
112
124
  const body = new Uint8Array(await response.arrayBuffer());
113
125
  this.diagnostic?.({
114
126
  event_type: "bootstrap_request_completed",
@@ -279,12 +291,7 @@ export class OysterunLogicalHostConnection {
279
291
  return retirePromise;
280
292
  }
281
293
 
282
- changeActiveTransport({
283
- transport,
284
- pathKind,
285
- connectionId,
286
- reason,
287
- }) {
294
+ changeActiveTransport({ transport, pathKind, connectionId, reason }) {
288
295
  const previousTransport = this.activeTransport;
289
296
  const previousPathKind = this.activePathKind;
290
297
  if (
@@ -310,10 +317,7 @@ export class OysterunLogicalHostConnection {
310
317
  reason,
311
318
  });
312
319
 
313
- if (
314
- previousPathKind === "webrtc" &&
315
- previousTransport !== transport
316
- ) {
320
+ if (previousPathKind === "webrtc" && previousTransport !== transport) {
317
321
  return this.retireTransport(previousTransport, {
318
322
  transitionGeneration,
319
323
  reason,
@@ -329,6 +333,7 @@ export class OysterunLogicalHostConnection {
329
333
  requestId = createConnectivityRandomId(),
330
334
  syncOwnerId = null,
331
335
  timeoutMs = undefined,
336
+ onUploadProgress = null,
332
337
  } = {}
333
338
  ) {
334
339
  if (this.closed) throw new Error("Logical Host connection is closed");
@@ -356,6 +361,7 @@ export class OysterunLogicalHostConnection {
356
361
  const result = await transport.request(request, {
357
362
  requestId,
358
363
  signal: request.signal || null,
364
+ onUploadProgress,
359
365
  timeoutMs,
360
366
  });
361
367
  this.emitDiagnostic({
@@ -417,8 +423,9 @@ export class OysterunLogicalHostConnection {
417
423
  active_connection_id: this.activeConnectionId,
418
424
  transition_generation: this.transitionGeneration,
419
425
  transition_state: this.transitionState,
420
- active_in_flight_request_count:
421
- this.ensureTransportState(this.activeTransport).inFlight,
426
+ active_in_flight_request_count: this.ensureTransportState(
427
+ this.activeTransport
428
+ ).inFlight,
422
429
  retiring_transport_count: this.retiringTransports.size,
423
430
  matrix_sync_owner_id: this.syncOwnerId,
424
431
  closed: this.closed,
@@ -43,6 +43,26 @@ export function managedHostFetch(
43
43
  input: RequestInfo | URL,
44
44
  init?: RequestInit
45
45
  ): Promise<Response>;
46
+ export function managedRequest(
47
+ input: RequestInfo | URL,
48
+ init?: RequestInit
49
+ ): Promise<Response>;
50
+ export type ManagedBlobUploadInit = Omit<RequestInit, "body"> & {
51
+ body: Blob | ArrayBuffer | Uint8Array | FormData;
52
+ onProgress?: ((progress: { loaded: number; total: number }) => void) | null;
53
+ };
54
+ export function managedBlobUpload(
55
+ input: RequestInfo | URL,
56
+ init: ManagedBlobUploadInit
57
+ ): Promise<Response>;
58
+ export function managedBlobDownload(
59
+ input: RequestInfo | URL,
60
+ init?: RequestInit
61
+ ): Promise<Response>;
62
+ export function managedMediaObjectUrl(
63
+ input: RequestInfo | URL,
64
+ init?: RequestInit
65
+ ): Promise<{ blob: Blob; url: string; revoke(): void }>;
46
66
  export function waitForManagedHostAvailability(): Promise<boolean>;
47
67
  export function getManagedHostConnectionStatus(): Record<string, unknown>;
48
68
  export function subscribeManagedHostConnectionStatus(
@@ -16,12 +16,18 @@ const BOOTSTRAP_MATRIX_SYNC_TIMEOUT_MS = 45_000;
16
16
  const PRESENCE_WAIT_SECONDS = 55;
17
17
  const PRESENCE_REQUEST_TIMEOUT_MS = 60_000;
18
18
  const PRESENCE_FALLBACK_PROBE_MS = 30_000;
19
+ const PRESENCE_HEALTH_RECOVERY_DELAYS_MS = Object.freeze([
20
+ 250, 500, 1_000, 2_000,
21
+ ]);
22
+ const PRESENCE_HEALTH_RECOVERY_MAX_TOTAL_MS = 10_000;
19
23
  const BACKGROUND_PROMOTION_ATTEMPT_LIMIT = 4;
20
24
  const BACKGROUND_PROMOTION_WINDOW_MS = 30 * 60 * 1000;
21
25
  const HOST_UNAVAILABLE_BODY_MAX_BYTES = 16 * 1024;
22
26
  const HOST_UNAVAILABLE_MARKER =
23
27
  '<meta name="oysterun-route-state" content="host-unavailable">';
24
- const MAX_CONTROL_REQUEST_BODY_BYTES = 512 * 1024;
28
+ const HOST_UNAVAILABLE_RESPONSE_ERROR =
29
+ "managed_host_route_unavailable_response";
30
+ const BULK_REQUEST_TIMEOUT_MS = 120_000;
25
31
  const MATRIX_SYNC_PATH = /^\/_matrix\/client\/[^/]+\/sync(?:\?|$)/;
26
32
  const SIGNALING_PATH_PREFIX = "/connectivity/webrtc/";
27
33
  const MANAGED_ENDPOINT_HOSTNAME = /^h-([a-z2-7]{16})\.tunnel\.oysterun\.com$/i;
@@ -370,6 +376,14 @@ function managedHostUnavailableError() {
370
376
  return error;
371
377
  }
372
378
 
379
+ function managedHostRouteUnavailableResponseError() {
380
+ const error = new Error(
381
+ "Managed Oysterun Relay reported that the Host route is unavailable"
382
+ );
383
+ error.code = HOST_UNAVAILABLE_RESPONSE_ERROR;
384
+ return error;
385
+ }
386
+
373
387
  function managedHostConnectionClosedError() {
374
388
  const error = new Error("Managed Oysterun Host browser connection is closed");
375
389
  error.code = "managed_host_connection_closed";
@@ -460,17 +474,6 @@ function withTimeout(promise, timeoutMs, message) {
460
474
  return Promise.race([promise, timeout]).finally(() => clearTimeout(timer));
461
475
  }
462
476
 
463
- function bytesToBase64(bytes) {
464
- let binary = "";
465
- const chunkBytes = 0x8000;
466
- for (let offset = 0; offset < bytes.byteLength; offset += chunkBytes) {
467
- binary += String.fromCharCode(
468
- ...bytes.subarray(offset, Math.min(bytes.byteLength, offset + chunkBytes))
469
- );
470
- }
471
- return btoa(binary);
472
- }
473
-
474
477
  function base64ToBytes(value) {
475
478
  const binary = atob(value);
476
479
  const bytes = new Uint8Array(binary.length);
@@ -811,7 +814,7 @@ export class DirectHostBrowserConnection {
811
814
  this.notifyStatusListeners();
812
815
  }
813
816
 
814
- async fetch(input, init = {}) {
817
+ async fetch(input, init = {}, _options = {}) {
815
818
  const path = normalizeHostPath(input);
816
819
  if (path) recordTransportRequest(this.accounting, "direct", path);
817
820
  try {
@@ -885,6 +888,7 @@ export class ManagedHostBrowserConnection {
885
888
  ticketProvider = null,
886
889
  bootstrapSafeRequestTimeoutMs = BOOTSTRAP_SAFE_REQUEST_TIMEOUT_MS,
887
890
  bootstrapMatrixSyncTimeoutMs = BOOTSTRAP_MATRIX_SYNC_TIMEOUT_MS,
891
+ presenceHealthRecoveryDelaysMs = PRESENCE_HEALTH_RECOVERY_DELAYS_MS,
888
892
  } = {}) {
889
893
  this.origin = origin.replace(/\/+$/, "");
890
894
  this.fetchImpl = fetchImpl;
@@ -898,13 +902,36 @@ export class ManagedHostBrowserConnection {
898
902
  ) {
899
903
  throw new Error("Managed Bootstrap request timeouts must be positive");
900
904
  }
905
+ if (
906
+ !Array.isArray(presenceHealthRecoveryDelaysMs) ||
907
+ presenceHealthRecoveryDelaysMs.length === 0 ||
908
+ presenceHealthRecoveryDelaysMs.some(
909
+ (delayMs) => !Number.isInteger(delayMs) || delayMs <= 0
910
+ ) ||
911
+ presenceHealthRecoveryDelaysMs.reduce(
912
+ (totalMs, delayMs) => totalMs + delayMs,
913
+ 0
914
+ ) > PRESENCE_HEALTH_RECOVERY_MAX_TOTAL_MS
915
+ ) {
916
+ throw new Error(
917
+ "Managed Host presence health recovery delays must be positive integers totaling at most 10000ms"
918
+ );
919
+ }
901
920
  this.bootstrapSafeRequestTimeoutMs = bootstrapSafeRequestTimeoutMs;
902
921
  this.bootstrapMatrixSyncTimeoutMs = bootstrapMatrixSyncTimeoutMs;
922
+ this.presenceHealthRecoveryDelaysMs = Object.freeze([
923
+ ...presenceHealthRecoveryDelaysMs,
924
+ ]);
903
925
  this.clientInstanceId = getClientInstanceId();
904
926
  this.bootstrapTransport = new BootstrapFetchTransport({
905
927
  origin: this.origin,
906
928
  fetchImpl,
907
929
  diagnostic: (event) => this.emitDiagnostic(event),
930
+ responseGuard: async (response) => {
931
+ if (await isRelayHostUnavailableResponse(response)) {
932
+ throw managedHostRouteUnavailableResponseError();
933
+ }
934
+ },
908
935
  });
909
936
  this.logicalConnection = new OysterunLogicalHostConnection({
910
937
  bootstrapTransport: this.bootstrapTransport,
@@ -1918,13 +1945,36 @@ export class ManagedHostBrowserConnection {
1918
1945
  return this.immediateRecoveryPromise;
1919
1946
  }
1920
1947
 
1948
+ async attemptBoundedDataPlaneRecovery() {
1949
+ for (const delayMs of this.presenceHealthRecoveryDelaysMs) {
1950
+ await sleep(delayMs);
1951
+ if (
1952
+ !this.routeUnavailable ||
1953
+ this.closed ||
1954
+ globalThis.navigator?.onLine === false
1955
+ ) {
1956
+ return false;
1957
+ }
1958
+ if (await this.probeManagedHealth()) {
1959
+ this.completePresenceRecovery();
1960
+ return true;
1961
+ }
1962
+ }
1963
+ return false;
1964
+ }
1965
+
1921
1966
  async monitorPresenceRecovery() {
1922
1967
  while (this.routeUnavailable && !this.closed) {
1923
1968
  try {
1924
1969
  const presence = await this.fetchPresence();
1925
- if (presence.route_available && (await this.probeManagedHealth())) {
1926
- this.completePresenceRecovery();
1927
- return;
1970
+ if (presence.route_available) {
1971
+ if (await this.probeManagedHealth()) {
1972
+ this.completePresenceRecovery();
1973
+ return;
1974
+ }
1975
+ if (await this.attemptBoundedDataPlaneRecovery()) {
1976
+ return;
1977
+ }
1928
1978
  }
1929
1979
  const waited = await this.fetchPresence("/wait", {
1930
1980
  after_revision: presence.presence_revision,
@@ -2049,6 +2099,16 @@ export class ManagedHostBrowserConnection {
2049
2099
  : null,
2050
2100
  });
2051
2101
  } catch (error) {
2102
+ if (error?.code === HOST_UNAVAILABLE_RESPONSE_ERROR) {
2103
+ const activated = await this.activateHostUnavailable();
2104
+ if (activated) {
2105
+ if (!isSafeRecoveryMethod(method)) {
2106
+ throw managedHostUnavailableError();
2107
+ }
2108
+ await this.waitForPresenceRecovery(requestSignal(input, init));
2109
+ return this.performFetch(input, init, recovery);
2110
+ }
2111
+ }
2052
2112
  const currentPathKind =
2053
2113
  this.logicalConnection.status().active_path_kind;
2054
2114
  if (
@@ -2081,16 +2141,6 @@ export class ManagedHostBrowserConnection {
2081
2141
  logicalResultBodyByteLength(result),
2082
2142
  path
2083
2143
  );
2084
- if (await isRelayHostUnavailableResponse(response)) {
2085
- const activated = await this.activateHostUnavailable();
2086
- if (activated) {
2087
- if (!isSafeRecoveryMethod(method)) {
2088
- throw managedHostUnavailableError();
2089
- }
2090
- await this.waitForPresenceRecovery(requestSignal(input, init));
2091
- return this.performFetch(input, init, recovery);
2092
- }
2093
- }
2094
2144
  return response;
2095
2145
  }
2096
2146
 
@@ -2099,13 +2149,6 @@ export class ManagedHostBrowserConnection {
2099
2149
  request.method === "GET" || request.method === "HEAD"
2100
2150
  ? null
2101
2151
  : new Uint8Array(await request.arrayBuffer());
2102
- if (body && body.byteLength > MAX_CONTROL_REQUEST_BODY_BYTES) {
2103
- recordFallbackReason(this.accounting, "request_body_requires_bootstrap");
2104
- recordTransportRequest(this.accounting, "bootstrap", path);
2105
- const response = await this.fetchImpl(input, init);
2106
- recordNativeResponseBytes(this.accounting, "bootstrap", path, response);
2107
- return response;
2108
- }
2109
2152
  if (request.signal.aborted) {
2110
2153
  throw new DOMException("The operation was aborted.", "AbortError");
2111
2154
  }
@@ -2113,14 +2156,18 @@ export class ManagedHostBrowserConnection {
2113
2156
  method: request.method,
2114
2157
  path,
2115
2158
  headers: Object.fromEntries(request.headers.entries()),
2116
- ...(body ? { body_base64: bytesToBase64(body) } : {}),
2159
+ signal: request.signal,
2160
+ ...(body ? { body } : {}),
2117
2161
  };
2118
2162
  const result = await this.logicalConnection.request(requestPayload, {
2119
2163
  requestId: createConnectivityRandomId(),
2120
2164
  syncOwnerId: MATRIX_SYNC_PATH.test(path) ? this.ensureSyncOwner() : null,
2121
2165
  timeoutMs: MATRIX_SYNC_PATH.test(path)
2122
2166
  ? this.bootstrapMatrixSyncTimeoutMs
2167
+ : body?.byteLength > 0
2168
+ ? BULK_REQUEST_TIMEOUT_MS
2123
2169
  : undefined,
2170
+ onUploadProgress: recovery.onUploadProgress || null,
2124
2171
  });
2125
2172
  recordTransportRequest(this.accounting, "datachannel", path);
2126
2173
  recordTransportResponseBytes(
@@ -2224,6 +2271,53 @@ export function managedHostFetch(input, init) {
2224
2271
  return getManagedHostBrowserConnection().fetch(input, init);
2225
2272
  }
2226
2273
 
2274
+ export function managedRequest(input, init) {
2275
+ return managedHostFetch(input, init);
2276
+ }
2277
+
2278
+ export function managedBlobUpload(
2279
+ input,
2280
+ { body, onProgress = null, ...init } = {}
2281
+ ) {
2282
+ if (
2283
+ !(body instanceof Blob) &&
2284
+ !(body instanceof ArrayBuffer) &&
2285
+ !(body instanceof Uint8Array) &&
2286
+ !(body instanceof FormData)
2287
+ ) {
2288
+ throw new TypeError(
2289
+ "Managed blob upload requires Blob, bytes, or FormData"
2290
+ );
2291
+ }
2292
+ return getManagedHostBrowserConnection().fetch(
2293
+ input,
2294
+ { ...init, body },
2295
+ { onUploadProgress: onProgress }
2296
+ );
2297
+ }
2298
+
2299
+ export function managedBlobDownload(input, init = {}) {
2300
+ return managedHostFetch(input, { ...init, method: init.method || "GET" });
2301
+ }
2302
+
2303
+ export async function managedMediaObjectUrl(input, init = {}) {
2304
+ const response = await managedBlobDownload(input, init);
2305
+ if (!response.ok) {
2306
+ throw new Error(
2307
+ `Managed media download failed with HTTP ${response.status}`
2308
+ );
2309
+ }
2310
+ const blob = await response.blob();
2311
+ const url = URL.createObjectURL(blob);
2312
+ return {
2313
+ blob,
2314
+ url,
2315
+ revoke() {
2316
+ URL.revokeObjectURL(url);
2317
+ },
2318
+ };
2319
+ }
2320
+
2227
2321
  export function waitForManagedHostAvailability() {
2228
2322
  const connection = getManagedHostBrowserConnection();
2229
2323
  if (