feat(browser): use persistent Playwright connections for remote profile tab operations

For remote CDP profiles (e.g., Browserless), tab operations now use Playwright's
persistent connection instead of stateless HTTP requests. This ensures tabs
persist across operations rather than being terminated after each request.

Changes:
- pw-session.ts: Add listPagesViaPlaywright, createPageViaPlaywright, and
  closePageByTargetIdViaPlaywright functions using the cached Playwright connection
- pw-ai.ts: Export new functions for dynamic import
- server-context.ts: For remote profiles (!cdpIsLoopback), use Playwright-based
  tab operations; local profiles continue using HTTP endpoints
- server-context.ts: Fix ensureTabAvailable to not require wsUrl for remote
  profiles since Playwright accesses pages directly

This is a follow-up to #895 which added authentication support for remote CDP
profiles. The original PR description mentioned switching to persistent Playwright
connections for tab operations, but only the auth changes were merged.
This commit is contained in:
Muhammed Mukhthar CM
2026-01-17 00:22:27 +00:00
committed by Peter Steinberger
parent bcfc9bead5
commit 02a4de0029
3 changed files with 166 additions and 1 deletions

View File

@@ -100,6 +100,22 @@ function createProfileContext(
};
const listTabs = async (): Promise<BrowserTab[]> => {
// For remote profiles, use Playwright's persistent connection to avoid ephemeral sessions
if (!profile.cdpIsLoopback) {
try {
const mod = await import("./pw-ai.js");
const pages = await mod.listPagesViaPlaywright({ cdpUrl: profile.cdpUrl });
return pages.map((p) => ({
targetId: p.targetId,
title: p.title,
url: p.url,
type: p.type,
}));
} catch {
// Fall back to HTTP-based listing if Playwright is not available
}
}
const raw = await fetchJson<
Array<{
id?: string;
@@ -121,6 +137,30 @@ function createProfileContext(
};
const openTab = async (url: string): Promise<BrowserTab> => {
// For remote profiles, use Playwright's persistent connection to create tabs
// This ensures the tab persists beyond a single request
if (!profile.cdpIsLoopback) {
try {
const mod = await import("./pw-ai.js");
const page = await mod.createPageViaPlaywright({ cdpUrl: profile.cdpUrl, url });
const profileState = getProfileState();
profileState.lastTargetId = page.targetId;
return {
targetId: page.targetId,
title: page.title,
url: page.url,
type: page.type,
};
} catch (err) {
// Fall back to HTTP-based tab creation if Playwright is not available
// (though it will likely be ephemeral for remote profiles)
if (String(err).includes("No browser context")) {
// This is a real error, not a missing Playwright issue
throw err;
}
}
}
const createdViaCdp = await createTargetViaCdp({
cdpUrl: profile.cdpUrl,
url,
@@ -321,7 +361,11 @@ function createProfileContext(
}
const tabs = await listTabs();
const candidates = profile.driver === "extension" ? tabs : tabs.filter((t) => Boolean(t.wsUrl));
// For remote profiles using Playwright's persistent connection, we don't need wsUrl
// because we access pages directly through Playwright, not via individual WebSocket URLs.
const candidates = profile.driver === "extension" || !profile.cdpIsLoopback
? tabs
: tabs.filter((t) => Boolean(t.wsUrl));
const resolveById = (raw: string) => {
const resolved = resolveTargetIdFromTabs(raw, candidates);
@@ -379,6 +423,21 @@ function createProfileContext(
}
throw new Error("tab not found");
}
// For remote profiles, use Playwright's persistent connection to close tabs
if (!profile.cdpIsLoopback) {
try {
const mod = await import("./pw-ai.js");
await mod.closePageByTargetIdViaPlaywright({
cdpUrl: profile.cdpUrl,
targetId: resolved.targetId,
});
return;
} catch {
// Fall back to HTTP-based close if Playwright is not available
}
}
await fetchOk(appendCdpPath(profile.cdpUrl, `/json/close/${resolved.targetId}`));
};