1import { previewBridge } from "~/components/preview/previewBridge";
2
3import { browserDefaultTabState, resolveBrowserDefaults } from "./browserDefaults";
4import { stopBrowserRecording } from "./browserRecording";
5
6interface DesktopTabLease {
7 references: number;
8 closeTimer: number | null;
9 ready: Promise<void>;
10}
11
12const leases = new Map<string, DesktopTabLease>();
13const pendingTabOperations = new Map<string, Promise<void>>();
14
15const enqueueDesktopTabOperation = (
16 tabId: string,
17 operation: () => Promise<void> | void,
18): Promise<void> => {
19 const previous = pendingTabOperations.get(tabId);
20 const pending = previous
21 ? previous.catch(() => undefined).then(operation)
22 : Promise.resolve(operation());
23 pendingTabOperations.set(tabId, pending);
24 void pending
25 .finally(() => {
26 if (pendingTabOperations.get(tabId) === pending) {
27 pendingTabOperations.delete(tabId);
28 }
29 })
30 .catch(() => undefined);
31 return pending;
32};
33
34/**
35 * Runs a call against a desktop tab after the tab's create and earlier calls,
36 * so a setting sent while the tab is being created still lands on it.
37 */
38export function withDesktopTab(tabId: string, operation: () => Promise<void>): void {
39 void enqueueDesktopTabOperation(tabId, operation).catch(() => undefined);
40}
41
42export interface AcquiredDesktopTab {
43 readonly ready: Promise<void>;
44 readonly release: () => void;
45}
46
47/** `serverTab` names the server tab this desktop tab renders, so the server can drive it. */
48export function acquireDesktopTab(
49 tabId: string,
50 serverTab?: { readonly threadId: string; readonly tabId: string },
51): AcquiredDesktopTab {
52 const current =
53 leases.get(tabId) ??
54 ({
55 references: 0,
56 closeTimer: null,
57 // Zoom/appearance defaults travel with creation so the guest never
58 // paints a frame at 100%/system before the preference is applied.
59 ready: enqueueDesktopTabOperation(tabId, async () =>
60 previewBridge?.createTab(tabId, {
61 ...browserDefaultTabState(await resolveBrowserDefaults()),
62 ...(serverTab === undefined ? {} : { serverTab }),
63 }),
64 ),
65 } satisfies DesktopTabLease);
66 if (current.closeTimer !== null) window.clearTimeout(current.closeTimer);
67 current.references += 1;
68 current.closeTimer = null;
69 leases.set(tabId, current);
70
71 return {
72 ready: current.ready,
73 release: () => {
74 const lease = leases.get(tabId);
75 if (!lease) return;
76 lease.references = Math.max(0, lease.references - 1);
77 if (lease.references > 0) return;
78 lease.closeTimer = window.setTimeout(() => {
79 const latest = leases.get(tabId);
80 if (!latest || latest.references > 0) return;
81 leases.delete(tabId);
82 void enqueueDesktopTabOperation(tabId, async () => {
83 await stopBrowserRecording(tabId).catch(() => null);
84 await previewBridge?.closeTab(tabId);
85 }).catch(() => undefined);
86 }, 0);
87 },
88 };
89}