| 1 | import { previewBridge } from "~/components/preview/previewBridge"; |
| 2 | |
| 3 | import { browserDefaultTabState, resolveBrowserDefaults } from "./browserDefaults"; |
| 4 | import { stopBrowserRecording } from "./browserRecording"; |
| 5 | |
| 6 | interface DesktopTabLease { |
| 7 | references: number; |
| 8 | closeTimer: number | null; |
| 9 | ready: Promise<void>; |
| 10 | } |
| 11 | |
| 12 | const leases = new Map<string, DesktopTabLease>(); |
| 13 | const pendingTabOperations = new Map<string, Promise<void>>(); |
| 14 | |
| 15 | const 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 | */ |
| 38 | export function withDesktopTab(tabId: string, operation: () => Promise<void>): void { |
| 39 | void enqueueDesktopTabOperation(tabId, operation).catch(() => undefined); |
| 40 | } |
| 41 | |
| 42 | export 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. */ |
| 48 | export 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 | } |