From defade432452150ccc7d421bd818a4b4a6f43043 Mon Sep 17 00:00:00 2001 From: germondai Date: Fri, 24 Jul 2026 12:08:55 +0200 Subject: [PATCH] refactor(browser): simplify optional pool state --- packages/browser/src/pool.ts | 30 ++++++++++++++--------------- packages/browser/src/session.ts | 6 +++--- packages/browser/tests/basic.ts | 4 ++-- packages/browser/tests/camoufox.ts | 6 +++--- packages/browser/tests/cf.ts | 8 ++++---- packages/browser/tests/pool.test.ts | 22 ++++++++++++++++----- 6 files changed, 43 insertions(+), 33 deletions(-) diff --git a/packages/browser/src/pool.ts b/packages/browser/src/pool.ts index 6cc3afd..349f6e8 100644 --- a/packages/browser/src/pool.ts +++ b/packages/browser/src/pool.ts @@ -23,8 +23,8 @@ export class PoolExhaustedError extends Error { export type { BrowserHandle } from "@trawl/types" interface PoolEntry extends PoolBrowser { - browser: Browser | null - context: BrowserContext | null + browser?: Browser + context?: BrowserContext temporaryContextUses: number restartReason?: string restarting?: boolean @@ -41,7 +41,7 @@ export class BrowserPool { private recycleAfterTemporaryContexts: number private contentProcesses!: number private browserFactory?: BrowserFactory - private healthInterval: ReturnType | null = null + private healthInterval?: ReturnType constructor({ poolSize, @@ -207,8 +207,7 @@ export class BrowserPool { const _attachShadow = Element.prototype.attachShadow Element.prototype.attachShadow = function (init: ShadowRootInit) { const shadowRoot = _attachShadow.call(this, init) - // biome-ignore lint/suspicious/noExplicitAny: extending DOM element with custom property - ;(this as any).shadowRootUnl = shadowRoot + Object.defineProperty(this, "shadowRootUnl", { configurable: true, value: shadowRoot }) return shadowRoot } }) @@ -253,9 +252,9 @@ export class BrowserPool { }) } - private pickEntry(domain?: string): PoolEntry | null { + private pickEntry(domain?: string): PoolEntry | undefined { const available = this.entries.filter((e) => !e.busy && !e.restarting && e.healthy && e.context) - if (available.length === 0) return null + if (available.length === 0) return if (domain) { const sticky = available.find((e) => e.lastDomain === domain) if (sticky) return sticky @@ -321,8 +320,8 @@ export class BrowserPool { try { await entry.browser?.close() } catch {} - entry.browser = null - entry.context = null + delete entry.browser + delete entry.context try { // On restart, keep the entry's original fingerprint so this browser instance // keeps its identity across restart cycles (otherwise cross-session correlation @@ -355,7 +354,10 @@ export class BrowserPool { } async shutdown(): Promise { - if (this.healthInterval) clearInterval(this.healthInterval) + if (this.healthInterval) { + clearInterval(this.healthInterval) + delete this.healthInterval + } for (const entry of this.entries) { await entry.context?.close().catch(() => {}) await entry.browser?.close().catch(() => {}) @@ -364,10 +366,7 @@ export class BrowserPool { } } -// Creates a fresh context from any browser with TRAWL init scripts applied. -// A fresh context (no prior cookies/localStorage/service workers) gets CF managed-mode -// treatment — challenge resolves in 3-4s vs ~40s for warm/reused contexts. -// biome-ignore lint/suspicious/noExplicitAny: camoufox-js doesn't export BrowserContext type +// biome-ignore lint/suspicious/noExplicitAny: preserves the caller's Playwright or Patchright context type export const newFreshContext = async (browser: any, options?: { proxy?: string }): Promise => { const context = await browser.newContext({ viewport: null, @@ -385,8 +384,7 @@ export const newFreshContext = async (browser: any, options?: { proxy?: string } const _orig = Element.prototype.attachShadow Element.prototype.attachShadow = function (init: ShadowRootInit) { const r = _orig.call(this, init) - // biome-ignore lint/suspicious/noExplicitAny: extending DOM element with custom property - ;(this as any).shadowRootUnl = r + Object.defineProperty(this, "shadowRootUnl", { configurable: true, value: r }) return r } }) diff --git a/packages/browser/src/session.ts b/packages/browser/src/session.ts index af33b5f..038239f 100644 --- a/packages/browser/src/session.ts +++ b/packages/browser/src/session.ts @@ -18,13 +18,13 @@ export class SessionCache { await this.redis.set(this.key(domain), JSON.stringify(data), "EX", this.ttl) } - async load(domain: string): Promise { + async load(domain: string) { const raw = await this.redis.get(this.key(domain)) - if (!raw) return null + if (!raw) return try { return JSON.parse(raw) as SessionData } catch { - return null + return } } diff --git a/packages/browser/tests/basic.ts b/packages/browser/tests/basic.ts index 027c7dd..0976c0b 100644 --- a/packages/browser/tests/basic.ts +++ b/packages/browser/tests/basic.ts @@ -8,7 +8,7 @@ process.on("uncaughtException", (err) => { }) async function main() { - const browser: any = await Camoufox({ + const browser = await Camoufox({ headless: true, geoip: true, humanize: true, @@ -59,7 +59,7 @@ async function main() { const cookies = await ctx.cookies() console.log( "[3] cf_clearance:", - cookies.some((c: any) => c.name === "cf_clearance"), + cookies.some((c) => c.name === "cf_clearance"), ) await browser.close() diff --git a/packages/browser/tests/camoufox.ts b/packages/browser/tests/camoufox.ts index 411811c..d21581c 100644 --- a/packages/browser/tests/camoufox.ts +++ b/packages/browser/tests/camoufox.ts @@ -2,7 +2,7 @@ import { Camoufox } from "camoufox-js" async function main() { console.log("[test] launching Camoufox...") - const browser: any = await Camoufox({ + const browser = await Camoufox({ headless: true, geoip: true, humanize: true, @@ -22,8 +22,8 @@ async function main() { const title = await page.title() console.log("[test] UA:", ua) console.log("[test] Title:", title) - console.log("[test] navigator.webdriver:", await page.evaluate(() => (navigator as any).webdriver)) - console.log("[test] window.chrome exists:", await page.evaluate(() => !!(window as any).chrome)) + console.log("[test] navigator.webdriver:", await page.evaluate(() => navigator.webdriver)) + console.log("[test] window.chrome exists:", await page.evaluate(() => "chrome" in window)) await browser.close() console.log("[test] PASS") diff --git a/packages/browser/tests/cf.ts b/packages/browser/tests/cf.ts index aaa681d..e666230 100644 --- a/packages/browser/tests/cf.ts +++ b/packages/browser/tests/cf.ts @@ -17,7 +17,7 @@ async function main() { i_know_what_im_doing: true, }) - const browser = await (firefox as any).launch(opts) + const browser = await firefox.launch(opts) const ctx = await browser.newContext({ viewport: null }) const page = await ctx.newPage() @@ -27,8 +27,8 @@ async function main() { waitUntil: "domcontentloaded", timeout: 30000, }) - .catch((e: any) => { - console.log("[test] goto error:", e.message.slice(0, 80)) + .catch((error: unknown) => { + console.log("[test] goto error:", error instanceof Error ? error.message.slice(0, 80) : String(error)) }) await new Promise((r) => setTimeout(r, 8000)) @@ -48,7 +48,7 @@ async function main() { } const cookies = await ctx.cookies() - console.log("[test] cookies:", cookies.map((c: any) => c.name).join(", ")) + console.log("[test] cookies:", cookies.map((c) => c.name).join(", ")) await browser.close() } diff --git a/packages/browser/tests/pool.test.ts b/packages/browser/tests/pool.test.ts index fa503a6..3bfa201 100644 --- a/packages/browser/tests/pool.test.ts +++ b/packages/browser/tests/pool.test.ts @@ -1,6 +1,18 @@ -import { describe, expect, test } from "bun:test" +import { afterEach, describe, expect, test } from "bun:test" import { BrowserPool } from "../src/pool" +const pools: BrowserPool[] = [] + +const createPool = (opts: ConstructorParameters[0]): BrowserPool => { + const pool = new BrowserPool(opts) + pools.push(pool) + return pool +} + +afterEach(async () => { + await Promise.all(pools.splice(0).map((pool) => pool.shutdown())) +}) + const waitFor = async (predicate: () => boolean) => { const deadline = Date.now() + 1000 while (Date.now() < deadline) { @@ -52,7 +64,7 @@ describe("BrowserPool recycling", () => { test("restarts the browser after the temporary context threshold", async () => { const { factory, browsers, contexts } = makeFactory() - const pool = new BrowserPool({ + const pool = createPool({ poolSize: 1, recycleAfterTemporaryContexts: 2, browserFactory: factory, @@ -82,7 +94,7 @@ describe("BrowserPool recycling", () => { test("noteTemporaryContext is no-op when recycleAfterTemporaryContexts=0", async () => { const { factory, browsers } = makeFactory() - const pool = new BrowserPool({ + const pool = createPool({ poolSize: 1, recycleAfterTemporaryContexts: 0, // disabled browserFactory: factory, @@ -109,7 +121,7 @@ describe("BrowserPool recycling", () => { // that the pool, given N successful acquires, never recycles on its own. const { factory, browsers } = makeFactory() - const pool = new BrowserPool({ + const pool = createPool({ poolSize: 1, recycleAfterTemporaryContexts: 2, browserFactory: factory, @@ -134,7 +146,7 @@ describe("BrowserPool recycling", () => { // option round-trips through the constructor without error. const { factory } = makeFactory() - const pool = new BrowserPool({ + const pool = createPool({ poolSize: 1, contentProcesses: 4, browserFactory: factory,