fix: per-profile browser caching to allow parallel connections

Changes global singleton cached/connecting variables to per-URL Maps.
This allows different browser profiles (e.g., linkedin on port 18805,
clawd on port 18800) to connect in parallel instead of blocking.

Fixes #4289, #3605
This commit is contained in:
Tarun Sukhani 2026-01-30 13:38:43 +00:00
parent da71eaebd2
commit 7f5e686742

View File

@ -98,8 +98,9 @@ const MAX_CONSOLE_MESSAGES = 500;
const MAX_PAGE_ERRORS = 200; const MAX_PAGE_ERRORS = 200;
const MAX_NETWORK_REQUESTS = 500; const MAX_NETWORK_REQUESTS = 500;
let cached: ConnectedBrowser | null = null; // Per-profile caching to allow parallel connections to different Chrome instances
let connecting: Promise<ConnectedBrowser> | null = null; const cachedByUrl = new Map<string, ConnectedBrowser>();
const connectingByUrl = new Map<string, Promise<ConnectedBrowser>>();
function normalizeCdpUrl(raw: string) { function normalizeCdpUrl(raw: string) {
return raw.replace(/\/$/, ""); return raw.replace(/\/$/, "");
@ -281,8 +282,14 @@ function observeBrowser(browser: Browser) {
async function connectBrowser(cdpUrl: string): Promise<ConnectedBrowser> { async function connectBrowser(cdpUrl: string): Promise<ConnectedBrowser> {
const normalized = normalizeCdpUrl(cdpUrl); const normalized = normalizeCdpUrl(cdpUrl);
if (cached?.cdpUrl === normalized) return cached;
if (connecting) return await connecting; // Check if we already have a cached connection for this specific URL
const cached = cachedByUrl.get(normalized);
if (cached) return cached;
// Check if there's already a connection in progress for this specific URL
const existingConnecting = connectingByUrl.get(normalized);
if (existingConnecting) return await existingConnecting;
const connectWithRetry = async (): Promise<ConnectedBrowser> => { const connectWithRetry = async (): Promise<ConnectedBrowser> => {
let lastErr: unknown; let lastErr: unknown;
@ -294,10 +301,12 @@ async function connectBrowser(cdpUrl: string): Promise<ConnectedBrowser> {
const headers = getHeadersWithAuth(endpoint); const headers = getHeadersWithAuth(endpoint);
const browser = await chromium.connectOverCDP(endpoint, { timeout, headers }); const browser = await chromium.connectOverCDP(endpoint, { timeout, headers });
const connected: ConnectedBrowser = { browser, cdpUrl: normalized }; const connected: ConnectedBrowser = { browser, cdpUrl: normalized };
cached = connected; cachedByUrl.set(normalized, connected);
observeBrowser(browser); observeBrowser(browser);
browser.on("disconnected", () => { browser.on("disconnected", () => {
if (cached?.browser === browser) cached = null; if (cachedByUrl.get(normalized)?.browser === browser) {
cachedByUrl.delete(normalized);
}
}); });
return connected; return connected;
} catch (err) { } catch (err) {
@ -313,11 +322,12 @@ async function connectBrowser(cdpUrl: string): Promise<ConnectedBrowser> {
throw new Error(message); throw new Error(message);
}; };
connecting = connectWithRetry().finally(() => { const connectingPromise = connectWithRetry().finally(() => {
connecting = null; connectingByUrl.delete(normalized);
}); });
connectingByUrl.set(normalized, connectingPromise);
return await connecting; return await connectingPromise;
} }
async function getAllPages(browser: Browser): Promise<Page[]> { async function getAllPages(browser: Browser): Promise<Page[]> {
@ -450,10 +460,11 @@ export function refLocator(page: Page, ref: string) {
} }
export async function closePlaywrightBrowserConnection(): Promise<void> { export async function closePlaywrightBrowserConnection(): Promise<void> {
const cur = cached; // Close all cached browser connections
cached = null; const connections = Array.from(cachedByUrl.values());
if (!cur) return; cachedByUrl.clear();
await cur.browser.close().catch(() => {}); connectingByUrl.clear();
await Promise.all(connections.map((c) => c.browser.close().catch(() => {})));
} }
/** /**