export const runtime = "nodejs"; import { type Browser, type BrowserContext } from "playwright"; import { chromium } from "playwright-extra"; // A simple singleton manager for a persistent Chromium context with ref-counting. // Prevents concurrent tasks from closing the shared window prematurely. import stealth from "puppeteer-extra-plugin-stealth"; chromium.use(stealth()); let contextPromise: Promise | null = null; let context: BrowserContext | null = null; let refCount = 0; let idleCloseTimer: NodeJS.Timeout | null = null; const USER_DATA_DIR = process.env.USER_DATA_DIR ?? "chrome-profile/douyin"; async function launchContext(): Promise { const ctx = await chromium.launchPersistentContext(USER_DATA_DIR, { headless: process.env.CHROMIUM_HEADLESS === "true", viewport: { width: Number(process.env.CHROMIUM_VIEWPORT_WIDTH ?? 1280), height: Number(process.env.CHROMIUM_VIEWPORT_HEIGHT ?? 1080), }, }); // When the context is closed externally, reset manager state ctx.on("close", () => { context = null; contextPromise = null; refCount = 0; if (idleCloseTimer) { clearTimeout(idleCloseTimer); idleCloseTimer = null; } }); return ctx; } export async function acquireBrowserContext(): Promise { // Cancel any pending idle close if a new consumer arrives if (idleCloseTimer) { clearTimeout(idleCloseTimer); idleCloseTimer = null; } if (context) { refCount += 1; return context; } if (!contextPromise) { contextPromise = launchContext(); } context = await contextPromise; refCount += 1; return context; } export async function releaseBrowserContext(options?: { idleMillis?: number; }): Promise { const idleMillis = options?.idleMillis ?? 15_000; refCount = Math.max(0, refCount - 1); if (refCount > 0 || !context) return; // Delay the close to allow bursty workloads to reuse the context if (idleCloseTimer) { clearTimeout(idleCloseTimer); idleCloseTimer = null; } idleCloseTimer = setTimeout(async () => { try { if (context && refCount === 0) { await context.close(); } } finally { context = null; contextPromise = null; idleCloseTimer = null; } }, idleMillis); } // --- Isolated context support for per-scrape independence --- const isolatedMap = new WeakMap(); async function getSharedStorageState(): Promise { try { const shared = await acquireBrowserContext(); const state = await shared.storageState(); // Do not force-close immediately; keep ref-counting behavior await releaseBrowserContext(); return state; } catch { // If shared context not available, proceed without storageState return undefined; } } /** * Create a brand-new, independent Browser + Context for a single scrape. * Attempts to import cookies/localStorage from the shared persistent profile * so you remain logged-in, but isolates network events, cache and listeners. */ export async function acquireIsolatedContext(): Promise { const storageState = await getSharedStorageState(); const browser = await chromium.launch({ headless: process.env.CHROMIUM_HEADLESS === "true", }); const ctx = await browser.newContext({ storageState, viewport: { width: Number(process.env.CHROMIUM_VIEWPORT_WIDTH ?? 1280), height: Number(process.env.CHROMIUM_VIEWPORT_HEIGHT ?? 1080), }, }); isolatedMap.set(ctx, browser); ctx.on("close", () => { const b = isolatedMap.get(ctx); if (b) { b.close().catch(() => {}); isolatedMap.delete(ctx); } }); return ctx; } export async function releaseIsolatedContext( ctx: BrowserContext | null | undefined, ): Promise { if (!ctx) return; try { await ctx.close(); } finally { const b = isolatedMap.get(ctx); if (b) { await b.close().catch(() => {}); isolatedMap.delete(ctx); } } }