runtime.ts
1 import { BrowserBridge, CDPBridge } from './browser/index.js'; 2 import type { IPage } from './types.js'; 3 import { TimeoutError } from './errors.js'; 4 import { isElectronApp } from './electron-apps.js'; 5 import { log } from './logger.js'; 6 7 /** 8 * Returns the appropriate browser factory based on site type. 9 * Uses CDPBridge for registered Electron apps, otherwise BrowserBridge. 10 */ 11 export function getBrowserFactory(site?: string): new () => IBrowserFactory { 12 if (site && isElectronApp(site)) return CDPBridge; 13 return BrowserBridge; 14 } 15 16 function parseEnvTimeout(envVar: string, fallback: number): number { 17 const raw = process.env[envVar]; 18 if (raw === undefined) return fallback; 19 const parsed = parseInt(raw, 10); 20 if (Number.isNaN(parsed) || parsed <= 0) { 21 log.warn(`[runtime] Invalid ${envVar}="${raw}", using default ${fallback}s`); 22 return fallback; 23 } 24 return parsed; 25 } 26 27 export const DEFAULT_BROWSER_CONNECT_TIMEOUT = parseEnvTimeout('OPENCLI_BROWSER_CONNECT_TIMEOUT', 30); 28 export const DEFAULT_BROWSER_COMMAND_TIMEOUT = parseEnvTimeout('OPENCLI_BROWSER_COMMAND_TIMEOUT', 60); 29 export const DEFAULT_BROWSER_EXPLORE_TIMEOUT = parseEnvTimeout('OPENCLI_BROWSER_EXPLORE_TIMEOUT', 120); 30 31 /** 32 * Timeout with seconds unit. Used for high-level command timeouts. 33 */ 34 export async function runWithTimeout<T>( 35 promise: Promise<T>, 36 opts: { timeout: number; label?: string; hint?: string }, 37 ): Promise<T> { 38 const label = opts.label ?? 'Operation'; 39 return withTimeoutMs(promise, opts.timeout * 1000, 40 () => new TimeoutError(label, opts.timeout, opts.hint)); 41 } 42 43 /** 44 * Timeout with milliseconds unit. Used for low-level internal timeouts. 45 * Accepts a factory function to create the rejection error, keeping this 46 * utility decoupled from specific error types. 47 */ 48 export function withTimeoutMs<T>( 49 promise: Promise<T>, 50 timeoutMs: number, 51 makeError: string | (() => Error) = 'Operation timed out', 52 ): Promise<T> { 53 const reject_ = typeof makeError === 'string' 54 ? () => new Error(makeError) 55 : makeError; 56 return new Promise<T>((resolve, reject) => { 57 const timer = setTimeout(() => reject(reject_()), timeoutMs); 58 promise.then( 59 (value) => { clearTimeout(timer); resolve(value); }, 60 (error) => { clearTimeout(timer); reject(error); }, 61 ); 62 }); 63 } 64 65 /** Interface for browser factory (BrowserBridge or test mocks) */ 66 export interface IBrowserFactory { 67 connect(opts?: { timeout?: number; workspace?: string; cdpEndpoint?: string }): Promise<IPage>; 68 close(): Promise<void>; 69 } 70 71 export async function browserSession<T>( 72 BrowserFactory: new () => IBrowserFactory, 73 fn: (page: IPage) => Promise<T>, 74 opts: { workspace?: string; cdpEndpoint?: string } = {}, 75 ): Promise<T> { 76 const browser = new BrowserFactory(); 77 try { 78 const page = await browser.connect({ 79 timeout: DEFAULT_BROWSER_CONNECT_TIMEOUT, 80 workspace: opts.workspace, 81 cdpEndpoint: opts.cdpEndpoint, 82 }); 83 return await fn(page); 84 } finally { 85 await browser.close().catch(() => {}); 86 } 87 }