import puppeteer, { Browser } from 'puppeteer';
import { Result, ok, err, browserNotLaunched, browserCrashed } from './errors.js';
/**
* Browser state - module-level singleton
*/
interface BrowserState {
browser: Browser | null;
isLaunching: boolean;
}
const state: BrowserState = {
browser: null,
isLaunching: false,
};
/**
* Get browser configuration from environment
*/
function getConfig() {
return {
headless: process.env.HEADLESS !== 'false',
timeout: parseInt(process.env.TIMEOUT ?? '30000', 10),
};
}
/**
* Launch the browser if not already running
* @returns Result with the browser instance
*/
export async function launchBrowser(): Promise<Result<Browser>> {
if (state.browser && state.browser.connected) {
return ok(state.browser);
}
if (state.isLaunching) {
// Wait for existing launch to complete
await waitForBrowser();
if (state.browser && state.browser.connected) {
return ok(state.browser);
}
return err(browserNotLaunched());
}
state.isLaunching = true;
try {
const config = getConfig();
state.browser = await puppeteer.launch({
headless: config.headless,
args: [
'--no-sandbox',
'--disable-setuid-sandbox',
'--disable-dev-shm-usage',
'--disable-gpu',
],
});
// Handle browser disconnect
state.browser.on('disconnected', () => {
state.browser = null;
});
state.isLaunching = false;
return ok(state.browser);
} catch (error) {
state.isLaunching = false;
const message = error instanceof Error ? error.message : String(error);
return err(browserCrashed(`Failed to launch browser: ${message}`));
}
}
/**
* Wait for browser to finish launching
*/
async function waitForBrowser(): Promise<void> {
const maxWait = 30000;
const startTime = Date.now();
while (state.isLaunching && Date.now() - startTime < maxWait) {
await sleep(100);
}
}
/**
* Sleep for specified milliseconds
*/
function sleep(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
/**
* Get the browser instance, launching if needed
* @returns Result with the browser instance
*/
export async function getBrowser(): Promise<Result<Browser>> {
if (state.browser && state.browser.connected) {
return ok(state.browser);
}
return launchBrowser();
}
/**
* Check if browser is connected
*/
export function isBrowserConnected(): boolean {
return state.browser !== null && state.browser.connected;
}
/**
* Close the browser
*/
export async function closeBrowser(): Promise<void> {
if (state.browser) {
try {
await state.browser.close();
} catch {
// Ignore close errors - browser may already be gone
}
state.browser = null;
}
}
/**
* Get the default timeout from config
*/
export function getDefaultTimeout(): number {
return getConfig().timeout;
}