/** * @license * Copyright 2019 Google Inc. * SPDX-License-Identifier: Apache-2.0 */ import type {Protocol} from 'devtools-protocol'; import {firstValueFrom, map, raceWith} from '../../third_party/rxjs/rxjs.js'; import type {CDPSession} from '../api/CDPSession.js'; import type {ElementHandle} from '../api/ElementHandle.js'; import type {JSHandle} from '../api/JSHandle.js'; import {Realm} from '../api/Realm.js'; import {EventEmitter} from '../common/EventEmitter.js'; import type {TimeoutSettings} from '../common/TimeoutSettings.js'; import type {EvaluateFunc, HandleFor} from '../common/types.js'; import { fromEmitterEvent, timeout, withSourcePuppeteerURLIfNone, } from '../common/util.js'; import {disposeSymbol} from '../util/disposable.js'; import {CdpElementHandle} from './ElementHandle.js'; import type {ExecutionContext} from './ExecutionContext.js'; import type {CdpFrame} from './Frame.js'; import type {MAIN_WORLD, PUPPETEER_WORLD} from './IsolatedWorlds.js'; import {CdpJSHandle} from './JSHandle.js'; import type {CdpWebWorker} from './WebWorker.js'; /** * @internal */ export interface PageBinding { name: string; pptrFunction: Function; } /** * @internal */ export interface IsolatedWorldChart { [key: string]: IsolatedWorld; [MAIN_WORLD]: IsolatedWorld; [PUPPETEER_WORLD]: IsolatedWorld; } /** * @internal */ type IsolatedWorldEmitter = EventEmitter<{ // Emitted when the isolated world gets a new execution context. context: ExecutionContext; // Emitted when the isolated world is disposed. disposed: undefined; // Emitted when a new console message is logged. consoleapicalled: Protocol.Runtime.ConsoleAPICalledEvent; /** Emitted when a binding that is not installed by the ExecutionContext is called. */ bindingcalled: Protocol.Runtime.BindingCalledEvent; }>; /** * @internal */ export class IsolatedWorld extends Realm { #context?: ExecutionContext; #emitter: IsolatedWorldEmitter = new EventEmitter(); readonly #frameOrWorker: CdpFrame | CdpWebWorker; constructor( frameOrWorker: CdpFrame | CdpWebWorker, timeoutSettings: TimeoutSettings ) { super(timeoutSettings); this.#frameOrWorker = frameOrWorker; } get environment(): CdpFrame | CdpWebWorker { return this.#frameOrWorker; } get client(): CDPSession { return this.#frameOrWorker.client; } get emitter(): IsolatedWorldEmitter { return this.#emitter; } setContext(context: ExecutionContext): void { this.#context?.[disposeSymbol](); context.once('disposed', this.#onContextDisposed.bind(this)); context.on('consoleapicalled', this.#onContextConsoleApiCalled.bind(this)); context.on('bindingcalled', this.#onContextBindingCalled.bind(this)); this.#context = context; this.#emitter.emit('context', context); void this.taskManager.rerunAll(); } #onContextDisposed(): void { this.#context = undefined; if ('clearDocumentHandle' in this.#frameOrWorker) { this.#frameOrWorker.clearDocumentHandle(); } } #onContextConsoleApiCalled( event: Protocol.Runtime.ConsoleAPICalledEvent ): void { this.#emitter.emit('consoleapicalled', event); } #onContextBindingCalled(event: Protocol.Runtime.BindingCalledEvent): void { this.#emitter.emit('bindingcalled', event); } hasContext(): boolean { return !!this.#context; } get context(): ExecutionContext | undefined { return this.#context; } #executionContext(): ExecutionContext | undefined { if (this.disposed) { throw new Error( `Execution context is not available in detached frame or worker "${this.environment.url()}" (are you trying to evaluate?)` ); } return this.#context; } /** * Waits for the next context to be set on the isolated world. */ async #waitForExecutionContext(): Promise<ExecutionContext> { const result = await firstValueFrom( fromEmitterEvent(this.#emitter, 'context').pipe( raceWith( fromEmitterEvent(this.#emitter, 'disposed').pipe( map(() => { // The message has to match the CDP message expected by the WaitTask class. throw new Error('Execution context was destroyed'); }) ), timeout(this.timeoutSettings.timeout()) ) ) ); return result; } async evaluateHandle< Params extends unknown[], Func extends EvaluateFunc<Params> = EvaluateFunc<Params>, >( pageFunction: Func | string, ...args: Params ): Promise<HandleFor<Awaited<ReturnType<Func>>>> { pageFunction = withSourcePuppeteerURLIfNone( this.evaluateHandle.name, pageFunction ); // This code needs to schedule evaluateHandle call synchroniously (at // least when the context is there) so we cannot unconditionally // await. let context = this.#executionContext(); if (!context) { context = await this.#waitForExecutionContext(); } return await context.evaluateHandle(pageFunction, ...args); } async evaluate< Params extends unknown[], Func extends EvaluateFunc<Params> = EvaluateFunc<Params>, >( pageFunction: Func | string, ...args: Params ): Promise<Awaited<ReturnType<Func>>> { pageFunction = withSourcePuppeteerURLIfNone( this.evaluate.name, pageFunction ); // This code needs to schedule evaluate call synchroniously (at // least when the context is there) so we cannot unconditionally // await. let context = this.#executionContext(); if (!context) { context = await this.#waitForExecutionContext(); } return await context.evaluate(pageFunction, ...args); } override async adoptBackendNode( backendNodeId?: Protocol.DOM.BackendNodeId ): Promise<JSHandle<Node>> { // This code needs to schedule resolveNode call synchroniously (at // least when the context is there) so we cannot unconditionally // await. let context = this.#executionContext(); if (!context) { context = await this.#waitForExecutionContext(); } const {object} = await this.client.send('DOM.resolveNode', { backendNodeId: backendNodeId, executionContextId: context.id, }); return this.createCdpHandle(object) as JSHandle<Node>; } async adoptHandle<T extends JSHandle<Node>>(handle: T): Promise<T> { if (handle.realm === this) { // If the context has already adopted this handle, clone it so downstream // disposal doesn't become an issue. return (await handle.evaluateHandle(value => { return value; })) as unknown as T; } const nodeInfo = await this.client.send('DOM.describeNode', { objectId: handle.id, }); return (await this.adoptBackendNode(nodeInfo.node.backendNodeId)) as T; } async transferHandle<T extends JSHandle<Node>>(handle: T): Promise<T> { if (handle.realm === this) { return handle; } // Implies it's a primitive value, probably. if (handle.remoteObject().objectId === undefined) { return handle; } const info = await this.client.send('DOM.describeNode', { objectId: handle.remoteObject().objectId, }); const newHandle = (await this.adoptBackendNode( info.node.backendNodeId )) as T; await handle.dispose(); return newHandle; } /** * @internal */ createCdpHandle( remoteObject: Protocol.Runtime.RemoteObject ): JSHandle | ElementHandle<Node> { if (remoteObject.subtype === 'node') { return new CdpElementHandle(this, remoteObject); } return new CdpJSHandle(this, remoteObject); } [disposeSymbol](): void { this.#context?.[disposeSymbol](); this.#emitter.emit('disposed', undefined); super[disposeSymbol](); this.#emitter.removeAllListeners(); } }