Skip to main content
Glama
browser.manager.ts10.6 kB
/** * Browser Manager for XHS Operations */ import puppeteer, { Browser, Page } from 'puppeteer'; import { Config, Cookie } from '../../shared/types'; import { BrowserLaunchError, BrowserNavigationError, XHSError } from '../../shared/errors'; import { getConfig } from '../../shared/config'; import { loadCookies, saveCookies } from '../../shared/cookies'; import { logger } from '../../shared/logger'; import { sleep } from '../../shared/utils'; import { BrowserPoolService, ManagedBrowser } from './browser-pool.service'; export class BrowserManager { private config: Config; private browser: Browser | null = null; private browserPool: BrowserPoolService | null = null; private usePool: boolean = false; constructor(config?: Config, usePool: boolean = false) { this.config = config || getConfig(); this.usePool = usePool; if (this.usePool) { this.browserPool = new BrowserPoolService(this.config); } } async createPage( headless?: boolean, executablePath?: string, shouldLoadCookies: boolean = true ): Promise<Page> { try { // Use browser pool if enabled if (this.usePool && this.browserPool) { return await this.createPageFromPool(shouldLoadCookies); } // Fallback to traditional browser management // Launch browser if not already launched if (!this.browser) { this.browser = await this.launchBrowser(headless, executablePath); } // Create new page const page = await this.browser.newPage(); // Configure page timeouts page.setDefaultTimeout(this.config.browser.defaultTimeout); page.setDefaultNavigationTimeout(this.config.browser.navigationTimeout); // Load cookies if requested if (shouldLoadCookies) { await this.loadCookiesIntoPage(page); } return page; } catch (error) { logger.error(`Browser page creation error: ${error}`); throw this.handlePuppeteerError(error as Error, 'create_page'); } } /** * Create a page using the browser pool */ private async createPageFromPool(shouldLoadCookies: boolean = true): Promise<Page> { if (!this.browserPool) { throw new XHSError('Browser pool not initialized', 'BrowserPoolError'); } const managedBrowser = await this.browserPool.acquireBrowser(); try { // Create new page from the managed browser context const page = await managedBrowser.context.newPage(); // Configure page timeouts page.setDefaultTimeout(this.config.browser.defaultTimeout); page.setDefaultNavigationTimeout(this.config.browser.navigationTimeout); // Load cookies if requested if (shouldLoadCookies) { await this.loadCookiesIntoPage(page); } // Store reference to managed browser for cleanup (page as Page & { _managedBrowser?: ManagedBrowser })._managedBrowser = managedBrowser; // Set up page close handler to release browser back to pool page.once('close', async () => { try { await this.browserPool!.releaseBrowser(managedBrowser); } catch (error) { logger.warn(`Error releasing browser back to pool: ${error}`); } }); return page; } catch (error) { // Release browser back to pool on error try { await this.browserPool.releaseBrowser(managedBrowser); } catch (releaseError) { logger.warn(`Error releasing browser after page creation failure: ${releaseError}`); } throw error; } } private async launchBrowser(headless?: boolean, executablePath?: string): Promise<Browser> { const isHeadless = headless !== undefined ? headless : this.config.browser.headlessDefault; try { const launchOptions: any = { headless: isHeadless, slowMo: this.config.browser.slowmo, args: [ '--no-sandbox', '--disable-setuid-sandbox', '--disable-dev-shm-usage', '--disable-accelerated-2d-canvas', '--no-first-run', '--no-zygote', '--disable-gpu', ], }; if (executablePath) { launchOptions.executablePath = executablePath; } const browser = await puppeteer.launch(launchOptions); return browser; } catch (error) { logger.error(`Failed to launch browser: ${error}`); throw new BrowserLaunchError( `Failed to launch browser: ${error}`, { headless: isHeadless, executablePath }, error as Error ); } } private async loadCookiesIntoPage(page: Page): Promise<boolean> { try { const cookies = loadCookies(); if (!cookies) { return false; } // Convert our cookie format to Puppeteer format const puppeteerCookies = cookies.map((cookie) => ({ name: cookie.name, value: cookie.value, domain: cookie.domain, path: cookie.path, expires: cookie.expires, httpOnly: cookie.httpOnly, secure: cookie.secure, sameSite: cookie.sameSite as 'Strict' | 'Lax' | 'None' | undefined, })); await page.setCookie(...puppeteerCookies); return true; } catch (error) { logger.warn(`Failed to load cookies: ${error}`); return false; } } async saveCookiesFromPage(page: Page): Promise<void> { try { const cookies = await page.cookies(); // Convert Puppeteer cookie format to app format const appCookies: Cookie[] = cookies.map((cookie) => ({ name: cookie.name, value: cookie.value, domain: cookie.domain, path: cookie.path, expires: cookie.expires, httpOnly: cookie.httpOnly, secure: cookie.secure, sameSite: cookie.sameSite as 'Strict' | 'Lax' | 'None', })); saveCookies(appCookies); } catch (error) { logger.error(`Failed to save cookies: ${error}`); throw this.handlePuppeteerError(error as Error, 'save_cookies'); } } async navigateWithRetry( page: Page, url: string, waitUntil: 'load' | 'domcontentloaded' | 'networkidle0' | 'networkidle2' = 'load', maxRetries?: number ): Promise<void> { const retries = maxRetries || this.config.xhs.maxRetries; for (let attempt = 0; attempt <= retries; attempt++) { try { await page.goto(url, { waitUntil, timeout: this.config.browser.navigationTimeout, }); return; } catch (error) { if (error instanceof Error && error.name === 'TimeoutError') { if (attempt === retries) { throw new BrowserNavigationError( `Failed to navigate to ${url} after ${retries + 1} attempts`, { url, attempts: attempt + 1 }, error ); } await sleep(this.config.xhs.retryDelay * 1000); } else { throw error; } } } } async tryWaitForSelector( page: Page, selector: string, timeout?: number, visible: boolean = true ): Promise<boolean> { try { await page.waitForSelector(selector, { timeout: timeout || this.config.browser.defaultTimeout, visible, }); return true; } catch (error) { if (error instanceof Error && error.name === 'TimeoutError') { return false; } throw error; } } async waitForSelectorVisible(page: Page, selector: string, timeout?: number): Promise<boolean> { return this.tryWaitForSelector(page, selector, timeout, true); } async waitForSelectorHidden(page: Page, selector: string, timeout?: number): Promise<boolean> { return this.tryWaitForSelector(page, selector, timeout, false); } async cleanup(): Promise<void> { // Cleanup browser pool if using it if (this.usePool && this.browserPool) { try { await this.browserPool.cleanup(); } catch (error) { logger.warn(`Error cleaning up browser pool: ${error}`); } finally { this.browserPool = null; } } // Cleanup traditional browser instance if (this.browser) { try { await this.browser.close(); } catch (error) { logger.warn(`Error closing browser: ${error}`); } finally { this.browser = null; } } } /** * Get browser pool statistics (if using pool) */ getBrowserPoolStats() { if (this.usePool && this.browserPool) { return this.browserPool.getPoolStats(); } return null; } /** * Enable browser pooling */ enableBrowserPool(): void { if (!this.usePool) { this.usePool = true; this.browserPool = new BrowserPoolService(this.config); } } /** * Disable browser pooling */ async disableBrowserPool(): Promise<void> { if (this.usePool && this.browserPool) { await this.browserPool.cleanup(); this.browserPool = null; this.usePool = false; } } private handlePuppeteerError(error: Error, operationName: string): XHSError { const context = { operationName }; if (error.name === 'TimeoutError') { if (operationName.toLowerCase().includes('login')) { return new XHSError( `Login operation timed out during ${operationName}`, 'LoginTimeoutError', context, error ); } else { return new XHSError( `Browser operation timed out: ${operationName}`, 'BrowserError', context, error ); } } else { if (error.message.toLowerCase().includes('navigation')) { return new BrowserNavigationError( `Navigation failed during ${operationName}: ${error.message}`, context, error ); } else { return new XHSError( `Browser error during ${operationName}: ${error.message}`, 'BrowserError', context, error ); } } } } // Global browser manager instance let globalBrowserManager: BrowserManager | null = null; export function getBrowserManager(usePool: boolean = false): BrowserManager { if (!globalBrowserManager) { globalBrowserManager = new BrowserManager(undefined, usePool); } return globalBrowserManager; } export function getPooledBrowserManager(): BrowserManager { return getBrowserManager(true); } export async function cleanupGlobalBrowserManager(): Promise<void> { if (globalBrowserManager) { await globalBrowserManager.cleanup(); globalBrowserManager = null; } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/Algovate/xhs-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server