import { chromium, Browser, BrowserContext } from 'playwright'; import { config } from '../config'; import { logger } from '../utils/logger'; let browser: Browser | null = null; let browserLaunchPromise: Promise | null = null; /** * Get or launch the shared browser instance. * Multiple callers waiting concurrently will share the same launch promise. */ export async function getBrowser(): Promise { if (browser && browser.isConnected()) { return browser; } if (browserLaunchPromise) { return browserLaunchPromise; } browserLaunchPromise = launchBrowser(); try { browser = await browserLaunchPromise; return browser; } finally { browserLaunchPromise = null; } } async function launchBrowser(): Promise { logger.info('Launching Chromium browser', { headless: config.headless }); const b = await chromium.launch({ headless: config.headless, args: [ '--no-sandbox', '--disable-setuid-sandbox', '--disable-dev-shm-usage', '--disable-gpu', ], }); b.on('disconnected', () => { logger.warn('Browser disconnected'); browser = null; }); logger.info('Browser launched successfully'); return b; } /** * Create a new isolated browser context for a job. * Optionally restore storage state from a previous session. */ export async function createContext( storageState?: string ): Promise { const b = await getBrowser(); const contextOptions: Parameters[0] = { viewport: { width: 1280, height: 720 }, userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36', acceptDownloads: true, }; if (storageState) { try { contextOptions.storageState = JSON.parse(storageState); } catch { logger.warn('Failed to parse storage state, starting fresh'); } } const context = await b.newContext(contextOptions); // Set reasonable timeouts context.setDefaultTimeout(30000); context.setDefaultNavigationTimeout(60000); return context; } /** * Gracefully close the shared browser. */ export async function closeBrowser(): Promise { if (browser) { try { await browser.close(); } catch { // Ignore close errors } browser = null; logger.info('Browser closed'); } } /** * Get count of active contexts (approximation of active jobs). */ export async function getActiveContextCount(): Promise { if (!browser || !browser.isConnected()) return 0; return browser.contexts().length; }