@cloudflare/puppeteer
Version:
A high-level API to control headless Chrome over the DevTools Protocol
241 lines (227 loc) • 6.74 kB
text/typescript
/**
* @license
* Copyright 2025 Google Inc.
* SPDX-License-Identifier: Apache-2.0
*/
import './globalPatcher.js';
import type {Browser} from '../api/Browser.js';
import type {ConnectionTransport} from '../common/ConnectionTransport.js';
import type {ConnectOptions} from '../common/ConnectOptions.js';
import {Puppeteer} from '../common/Puppeteer.js';
import type {BrowserWorker} from './BrowserWorker.js';
import {connectToCDPBrowser, type Locations} from './utils.js';
import {WorkersWebSocketTransport} from './WorkersWebSocketTransport.js';
const FAKE_HOST = 'https://fake.host';
// We can't include both workers-types and dom because they conflict
declare global {
interface Response {
readonly webSocket: WebSocket | null;
}
interface WebSocket {
accept(): void;
}
}
/**
* @public
*/
export interface AcquireResponse {
sessionId: string;
}
/**
* @public
*/
export interface ActiveSession {
sessionId: string;
startTime: number; // timestamp
// connection info, if present means there's a connection established
// from a worker to that session
connectionId?: string;
connectionStartTime?: string;
}
/**
* @public
*/
export interface ClosedSession extends ActiveSession {
endTime: number; // timestamp
closeReason: number; // close reason code
closeReasonText: string; // close reason description
}
/**
* @public
*/
export interface SessionsResponse {
sessions: ActiveSession[];
}
/**
* @public
*/
export interface HistoryResponse {
history: ClosedSession[];
}
/**
* @public
*/
export interface LimitsResponse {
activeSessions: Array<{id: string}>;
maxConcurrentSessions: number;
allowedBrowserAcquisitions: number; // 1 if allowed, 0 otherwise
timeUntilNextAllowedBrowserAcquisition: number;
}
/**
* @public
*/
export interface WorkersLaunchOptions {
keep_alive?: number; // milliseconds to keep browser alive even if it has no activity (from 10_000ms to 600_000ms, default is 60_000)
location?: Locations;
}
/**
* @public
*/
export class PuppeteerWorkers extends Puppeteer {
public constructor() {
super({isPuppeteerCore: false});
this.acquire = this.acquire.bind(this);
this.connect = this.connect.bind(this);
this.launch = this.launch.bind(this);
this.sessions = this.sessions.bind(this);
this.history = this.history.bind(this);
this.limits = this.limits.bind(this);
}
/**
* Launch a browser session.
*
* @param endpoint - Cloudflare worker binding
* @returns a browser session or throws
*/
public async launch(
endpoint: BrowserWorker,
options?: WorkersLaunchOptions
): Promise<Browser> {
const response: AcquireResponse = await this.acquire(endpoint, options);
return await this.connect(endpoint, response.sessionId);
}
/**
* Returns active sessions
*
* @remarks
* Sessions with a connnectionId already have a worker connection established
*
* @param endpoint - Cloudflare worker binding
* @returns List of active sessions
*/
public async sessions(endpoint: BrowserWorker): Promise<ActiveSession[]> {
const res = await endpoint.fetch(`${FAKE_HOST}/v1/sessions`);
const status = res.status;
const text = await res.text();
if (status !== 200) {
throw new Error(
`Unable to fetch new sessions: code: ${status}: message: ${text}`
);
}
const data: SessionsResponse = JSON.parse(text);
return data.sessions;
}
/**
* Returns recent sessions (active and closed)
*
* @param endpoint - Cloudflare worker binding
* @returns List of recent sessions (active and closed)
*/
public async history(endpoint: BrowserWorker): Promise<ClosedSession[]> {
const res = await endpoint.fetch(`${FAKE_HOST}/v1/history`);
const status = res.status;
const text = await res.text();
if (status !== 200) {
throw new Error(
`Unable to fetch account history: code: ${status}: message: ${text}`
);
}
const data: HistoryResponse = JSON.parse(text);
return data.history;
}
/**
* Returns current limits
*
* @param endpoint - Cloudflare worker binding
* @returns current limits
*/
public async limits(endpoint: BrowserWorker): Promise<LimitsResponse> {
const res = await endpoint.fetch(`${FAKE_HOST}/v1/limits`);
const status = res.status;
const text = await res.text();
if (status !== 200) {
throw new Error(
`Unable to fetch account limits: code: ${status}: message: ${text}`
);
}
const data: LimitsResponse = JSON.parse(text);
return data;
}
/**
* Establish a devtools connection to an existing session
*
* @param endpoint - Cloudflare worker binding
* @param sessionId - sessionId obtained from a .sessions() call
* @returns a browser instance
*/
public override async connect(
endpoint: BrowserWorker | ConnectOptions,
sessionId?: string
): Promise<Browser>;
/**
* Establish a devtools connection to an existing session
*
* @param borwserWorker - BrowserWorker
* @returns a browser instance
*/
public override async connect(
endpoint: BrowserWorker | ConnectOptions,
sessionId?: string
): Promise<Browser> {
try {
if (!sessionId) {
return await super.connect(endpoint as ConnectOptions);
}
const connectionTransport: ConnectionTransport =
await WorkersWebSocketTransport.create(
endpoint as BrowserWorker,
sessionId
);
return await connectToCDPBrowser(connectionTransport, {sessionId});
} catch (e) {
throw new Error(
`Unable to connect to existing session ${sessionId} (it may still be in use or not ready yet) - retry or launch a new browser: ${e}`
);
}
}
/**
* Acquire a new browser session.
*
* @param borwserWorker - BrowserWorker
* @returns a new browser session
*/
public async acquire(
endpoint: BrowserWorker,
options?: WorkersLaunchOptions
): Promise<AcquireResponse> {
const searchParams = new URLSearchParams();
if (options?.keep_alive) {
searchParams.set('keep_alive', `${options.keep_alive}`);
}
if (options?.location) {
searchParams.set('location', options.location);
}
const acquireUrl = `${FAKE_HOST}/v1/acquire?${searchParams.toString()}`;
const res = await endpoint.fetch(acquireUrl);
const status = res.status;
const text = await res.text();
if (status !== 200) {
throw new Error(
`Unable to create new browser: code: ${status}: message: ${text}`
);
}
// Got a 200, so response text is actually an AcquireResponse
const response: AcquireResponse = JSON.parse(text);
return response;
}
}