import { BrowserWindow, nativeTheme } from "electron/main"; import type { BrowserWindow as ElectronBrowserWindow, Session } from "electron/main"; import type { Browser as PlaywrightBrowser, Page as PlaywrightPage } from "playwright-core"; import { STANDARD_USER_AGENT } from "./user-agent"; const hiddenPlaywrightIdleTTLms = 5 * 60_000; const hiddenPlaywrightReaperIntervalMs = 60_000; const defaultConnectTimeoutMs = 10_000; const defaultPageTimeoutMs = 45_000; const cdpPort = Number.parseInt(process.env.GEO_ELECTRON_CDP_PORT ?? "9339", 10); const cdpEndpointURL = `http://127.0.0.1:${cdpPort}`; interface HiddenPlaywrightRecord { key: string; accountId: string; title: string; targetURL: string; window: ElectronBrowserWindow; browser: PlaywrightBrowser; page: PlaywrightPage; retainCount: number; activatedAt: number; lastReleasedAt: number | null; } export interface HiddenPlaywrightLease { accountId: string; browser: PlaywrightBrowser; page: PlaywrightPage; release(): Promise; } const records = new Map(); let reaperHandle: ReturnType | null = null; let connectPromise: Promise | null = null; let connectedBrowser: PlaywrightBrowser | null = null; export function getPlaywrightCDPPort(): number { return cdpPort; } export function startHiddenPlaywrightReaper(): void { if (reaperHandle) { return; } reaperHandle = setInterval(() => { reapIdleHiddenPlaywrightPages(); }, hiddenPlaywrightReaperIntervalMs); reaperHandle.unref?.(); } export async function retainHiddenPlaywrightPage(options: { accountId: string; session: Session; targetURL: string; title: string; }): Promise { startHiddenPlaywrightReaper(); const key = hiddenPlaywrightKey(options.accountId, options.targetURL); const existing = records.get(key); if (existing && !existing.window.isDestroyed() && !existing.page.isClosed()) { await ensureRecordTarget(existing, options.targetURL); existing.retainCount += 1; existing.activatedAt = Date.now(); existing.lastReleasedAt = null; return createLease(existing); } const browser = await connectBrowserOverCDP(); const knownPages = new Set(defaultContext(browser).pages()); const window = createHiddenWindow(options.title, options.session); const page = await waitForNewPage(browser, knownPages); page.setDefaultTimeout(defaultPageTimeoutMs); page.setDefaultNavigationTimeout(defaultPageTimeoutMs); await page.goto(options.targetURL, { timeout: defaultPageTimeoutMs, waitUntil: "domcontentloaded", }); const record: HiddenPlaywrightRecord = { key, accountId: options.accountId, title: options.title, targetURL: options.targetURL, window, browser, page, retainCount: 1, activatedAt: Date.now(), lastReleasedAt: null, }; window.once("closed", () => { records.delete(key); }); page.once("close", () => { records.delete(key); }); records.set(key, record); return createLease(record); } export async function releaseHiddenPlaywrightPage(accountId: string, targetURL: string): Promise { const key = hiddenPlaywrightKey(accountId, targetURL); const record = records.get(key); if (!record) { return; } if (record.retainCount > 0) { record.retainCount -= 1; } if (record.retainCount === 0) { record.lastReleasedAt = Date.now(); } } export function reapIdleHiddenPlaywrightPages( now = Date.now(), idleTTLms = hiddenPlaywrightIdleTTLms, ): string[] { const reclaimed: string[] = []; for (const [key, record] of records.entries()) { if (record.retainCount > 0) { continue; } const idleSince = record.lastReleasedAt ?? record.activatedAt; if (now - idleSince < idleTTLms) { continue; } closeRecord(record); records.delete(key); reclaimed.push(key); } return reclaimed; } export function getHiddenPlaywrightSnapshot() { return { cdpPort, cdpEndpointURL, connected: Boolean(connectedBrowser?.isConnected()), pageCount: records.size, idleTTLms: hiddenPlaywrightIdleTTLms, records: [...records.values()].map((record) => ({ accountId: record.accountId, title: record.title, targetURL: record.targetURL, retainCount: record.retainCount, activatedAt: record.activatedAt, lastReleasedAt: record.lastReleasedAt, windowDestroyed: record.window.isDestroyed(), pageClosed: record.page.isClosed(), })), }; } function createLease(record: HiddenPlaywrightRecord): HiddenPlaywrightLease { return { accountId: record.accountId, browser: record.browser, page: record.page, release: () => releaseHiddenPlaywrightPage(record.accountId, record.targetURL), }; } function createHiddenWindow(title: string, session: Session): ElectronBrowserWindow { const window = new BrowserWindow({ show: false, width: 1320, height: 900, autoHideMenuBar: true, backgroundColor: nativeTheme.shouldUseDarkColors ? "#15191a" : "#f4f1ea", title, webPreferences: { session, sandbox: true, contextIsolation: true, nodeIntegration: false, backgroundThrottling: false, }, }); window.webContents.setUserAgent(STANDARD_USER_AGENT); void window.loadURL("about:blank"); return window; } async function ensureRecordTarget(record: HiddenPlaywrightRecord, targetURL: string): Promise { record.targetURL = targetURL; const currentURL = safePageURL(record.page); if (currentURL === targetURL) { return; } await record.page.goto(targetURL, { timeout: defaultPageTimeoutMs, waitUntil: "domcontentloaded", }); } async function connectBrowserOverCDP(): Promise { if (connectedBrowser?.isConnected()) { return connectedBrowser; } if (connectPromise) { return connectPromise; } connectPromise = (async () => { await waitForCDPEndpoint(); const { chromium } = await import("playwright-core"); const browser = await chromium.connectOverCDP(cdpEndpointURL, { timeout: defaultConnectTimeoutMs, }); browser.on("disconnected", () => { connectedBrowser = null; connectPromise = null; }); connectedBrowser = browser; return browser; })(); try { return await connectPromise; } finally { if (!connectedBrowser?.isConnected()) { connectPromise = null; } } } async function waitForCDPEndpoint(timeoutMs = defaultConnectTimeoutMs): Promise { const startedAt = Date.now(); while (Date.now() - startedAt < timeoutMs) { try { const response = await fetch(`${cdpEndpointURL}/json/version`); if (response.ok) { return; } } catch { // Ignore and continue polling. } await wait(200); } throw new Error("playwright_cdp_endpoint_unavailable"); } async function waitForNewPage( browser: PlaywrightBrowser, knownPages: Set, ): Promise { const context = defaultContext(browser); const startedAt = Date.now(); while (Date.now() - startedAt < defaultPageTimeoutMs) { const nextPage = context.pages().find((page) => !knownPages.has(page)); if (nextPage) { return nextPage; } await wait(100); } throw new Error("playwright_hidden_page_unavailable"); } function defaultContext(browser: PlaywrightBrowser) { const context = browser.contexts()[0]; if (!context) { throw new Error("playwright_default_context_unavailable"); } return context; } function hiddenPlaywrightKey(accountId: string, targetURL: string): string { return `${accountId}::${targetURL}`; } function safePageURL(page: PlaywrightPage): string { try { return page.url(); } catch { return ""; } } function closeRecord(record: HiddenPlaywrightRecord): void { try { record.page.close().catch(() => undefined); } catch { // Ignore and fall through to window close. } if (!record.window.isDestroyed()) { record.window.close(); } } function wait(timeMs: number): Promise { return new Promise((resolve) => { setTimeout(resolve, timeMs); }); }