/ src / runtime.ts
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  }