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:
committed by
Peter Steinberger
parent
bcfc9bead5
commit
02a4de0029
@@ -1,8 +1,11 @@
|
||||
export {
|
||||
type BrowserConsoleMessage,
|
||||
closePageByTargetIdViaPlaywright,
|
||||
closePlaywrightBrowserConnection,
|
||||
createPageViaPlaywright,
|
||||
ensurePageState,
|
||||
getPageForTargetId,
|
||||
listPagesViaPlaywright,
|
||||
refLocator,
|
||||
type WithSnapshotForAI,
|
||||
} from "./pw-session.js";
|
||||
|
||||
@@ -389,3 +389,106 @@ export async function closePlaywrightBrowserConnection(): Promise<void> {
|
||||
if (!cur) return;
|
||||
await cur.browser.close().catch(() => {});
|
||||
}
|
||||
|
||||
/**
|
||||
* List all pages/tabs from the persistent Playwright connection.
|
||||
* Used for remote profiles where HTTP-based /json/list is ephemeral.
|
||||
*/
|
||||
export async function listPagesViaPlaywright(opts: {
|
||||
cdpUrl: string;
|
||||
}): Promise<
|
||||
Array<{
|
||||
targetId: string;
|
||||
title: string;
|
||||
url: string;
|
||||
type: string;
|
||||
}>
|
||||
> {
|
||||
const { browser } = await connectBrowser(opts.cdpUrl);
|
||||
const pages = await getAllPages(browser);
|
||||
const results: Array<{
|
||||
targetId: string;
|
||||
title: string;
|
||||
url: string;
|
||||
type: string;
|
||||
}> = [];
|
||||
|
||||
for (const page of pages) {
|
||||
const tid = await pageTargetId(page).catch(() => null);
|
||||
if (tid) {
|
||||
results.push({
|
||||
targetId: tid,
|
||||
title: await page.title().catch(() => ""),
|
||||
url: page.url(),
|
||||
type: "page",
|
||||
});
|
||||
}
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new page/tab using the persistent Playwright connection.
|
||||
* Used for remote profiles where HTTP-based /json/new is ephemeral.
|
||||
* Returns the new page's targetId and metadata.
|
||||
*/
|
||||
export async function createPageViaPlaywright(opts: {
|
||||
cdpUrl: string;
|
||||
url: string;
|
||||
}): Promise<{
|
||||
targetId: string;
|
||||
title: string;
|
||||
url: string;
|
||||
type: string;
|
||||
}> {
|
||||
const { browser } = await connectBrowser(opts.cdpUrl);
|
||||
const contexts = browser.contexts();
|
||||
// Use the first context if available, otherwise this is a fresh connection
|
||||
// and we need to use the default context that Browserless provides
|
||||
let context = contexts[0];
|
||||
if (!context) {
|
||||
// For Browserless over CDP, there should be at least one context
|
||||
// If not, we can try accessing pages directly from contexts
|
||||
throw new Error("No browser context available for creating a new page");
|
||||
}
|
||||
|
||||
const page = await context.newPage();
|
||||
ensurePageState(page);
|
||||
|
||||
// Navigate to the URL
|
||||
const targetUrl = opts.url.trim() || "about:blank";
|
||||
if (targetUrl !== "about:blank") {
|
||||
await page.goto(targetUrl, { timeout: 30_000 }).catch(() => {
|
||||
// Navigation might fail for some URLs, but page is still created
|
||||
});
|
||||
}
|
||||
|
||||
// Get the targetId for this page
|
||||
const tid = await pageTargetId(page).catch(() => null);
|
||||
if (!tid) {
|
||||
throw new Error("Failed to get targetId for new page");
|
||||
}
|
||||
|
||||
return {
|
||||
targetId: tid,
|
||||
title: await page.title().catch(() => ""),
|
||||
url: page.url(),
|
||||
type: "page",
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Close a page/tab by targetId using the persistent Playwright connection.
|
||||
* Used for remote profiles where HTTP-based /json/close is ephemeral.
|
||||
*/
|
||||
export async function closePageByTargetIdViaPlaywright(opts: {
|
||||
cdpUrl: string;
|
||||
targetId: string;
|
||||
}): Promise<void> {
|
||||
const { browser } = await connectBrowser(opts.cdpUrl);
|
||||
const page = await findPageByTargetId(browser, opts.targetId);
|
||||
if (!page) {
|
||||
throw new Error("tab not found");
|
||||
}
|
||||
await page.close();
|
||||
}
|
||||
|
||||
@@ -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}`));
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user