v1.1.0: pooled runtime, 959 tests, production hardening (0 squash)
This commit is contained in:
@@ -0,0 +1,149 @@
|
||||
/**
|
||||
* CDP Session Management
|
||||
*
|
||||
* Wraps Playwright's CDP session access to provide a typed, injectable
|
||||
* interface for sending Chrome DevTools Protocol commands.
|
||||
*
|
||||
* Design: dependency injection over optional imports. The extractor receives
|
||||
* a session factory rather than importing Playwright directly.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Minimal CDP session interface. Any object that can send CDP commands
|
||||
* and return JSON-like responses satisfies this contract.
|
||||
*/
|
||||
export interface CDPSession {
|
||||
send<T = unknown>(method: string, params?: Record<string, unknown>): Promise<T>
|
||||
on(event: string, handler: (payload: unknown) => void): void
|
||||
off(event: string, handler: (payload: unknown) => void): void
|
||||
detach(): Promise<void>
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory that creates a CDP session for a given Playwright page.
|
||||
* This is injected so the package does not hard-depend on Playwright.
|
||||
*/
|
||||
export type CDPSessionFactory = (pageRef: unknown) => Promise<CDPSession>
|
||||
|
||||
/**
|
||||
* Playwright-backed session factory implementation.
|
||||
*
|
||||
* Usage:
|
||||
* const session = await createPlaywrightCDPSession(page)
|
||||
*
|
||||
* Where `page` is a Playwright Page object.
|
||||
*/
|
||||
export async function createPlaywrightCDPSession(
|
||||
page: unknown
|
||||
): Promise<CDPSession> {
|
||||
// Playwright pages expose context() and a CDP session method.
|
||||
// We access it dynamically to avoid a hard import dependency.
|
||||
const pwPage = page as {
|
||||
context: () => {
|
||||
newCDPSession: (p: unknown) => Promise<CDPSession>
|
||||
}
|
||||
}
|
||||
|
||||
const context = pwPage.context()
|
||||
const session = await context.newCDPSession(page)
|
||||
return session
|
||||
}
|
||||
|
||||
/**
|
||||
* Session manager that owns the lifecycle of a CDP session.
|
||||
*
|
||||
* Handles attach, detach, and basic error recovery.
|
||||
* Caches document root nodeId and enabled domains to minimize round-trips.
|
||||
*/
|
||||
export class CDPSessionManager {
|
||||
private session: CDPSession | null = null
|
||||
private attachPromise: Promise<CDPSession> | null = null
|
||||
private readonly factory: CDPSessionFactory
|
||||
private readonly pageRef: unknown
|
||||
private documentRootNodeId: number | null = null
|
||||
private enabledDomains = new Set<string>()
|
||||
|
||||
constructor(factory: CDPSessionFactory, pageRef: unknown) {
|
||||
this.factory = factory
|
||||
this.pageRef = pageRef
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure a session is attached. Idempotent and concurrency-safe.
|
||||
*/
|
||||
async attach(): Promise<CDPSession> {
|
||||
if (this.session) return this.session
|
||||
if (this.attachPromise) return this.attachPromise
|
||||
this.attachPromise = this.factory(this.pageRef).then((s) => {
|
||||
this.session = s
|
||||
this.documentRootNodeId = null
|
||||
this.enabledDomains.clear()
|
||||
return s
|
||||
})
|
||||
return this.attachPromise
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cached document root nodeId, or fetch and cache it.
|
||||
*/
|
||||
async getDocumentRootNodeId(): Promise<number> {
|
||||
if (this.documentRootNodeId !== null) {
|
||||
return this.documentRootNodeId
|
||||
}
|
||||
const s = await this.attach()
|
||||
const result = await s.send<{ root: { nodeId: number } }>('DOM.getDocument')
|
||||
this.documentRootNodeId = result.root.nodeId
|
||||
return this.documentRootNodeId
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable a CDP domain if not already enabled.
|
||||
*/
|
||||
async enableDomain(domain: string): Promise<void> {
|
||||
if (this.enabledDomains.has(domain)) {
|
||||
return
|
||||
}
|
||||
const s = await this.attach()
|
||||
await s.send(`${domain}.enable`)
|
||||
this.enabledDomains.add(domain)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current session, throwing if not attached.
|
||||
*/
|
||||
getSession(): CDPSession {
|
||||
if (!this.session) {
|
||||
throw new Error('CDP session not attached. Call attach() first.')
|
||||
}
|
||||
return this.session
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a CDP command, automatically attaching if needed.
|
||||
*/
|
||||
async send<T = unknown>(
|
||||
method: string,
|
||||
params?: Record<string, unknown>
|
||||
): Promise<T> {
|
||||
const s = await this.attach()
|
||||
return s.send<T>(method, params)
|
||||
}
|
||||
|
||||
/**
|
||||
* Detach and clean up the session.
|
||||
*/
|
||||
async detach(): Promise<void> {
|
||||
if (this.session) {
|
||||
await this.session.detach()
|
||||
this.session = null
|
||||
}
|
||||
this.attachPromise = null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenient builder for a manager using the Playwright factory.
|
||||
*/
|
||||
export function createSessionManager(page: unknown): CDPSessionManager {
|
||||
return new CDPSessionManager(createPlaywrightCDPSession, page)
|
||||
}
|
||||
Reference in New Issue
Block a user