Realm.ts (11950B)
1 /** 2 * @license 3 * Copyright 2024 Google Inc. 4 * SPDX-License-Identifier: Apache-2.0 5 */ 6 import * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js'; 7 8 import type {JSHandle} from '../api/JSHandle.js'; 9 import {Realm} from '../api/Realm.js'; 10 import {ARIAQueryHandler} from '../cdp/AriaQueryHandler.js'; 11 import {LazyArg} from '../common/LazyArg.js'; 12 import {scriptInjector} from '../common/ScriptInjector.js'; 13 import type {TimeoutSettings} from '../common/TimeoutSettings.js'; 14 import type {EvaluateFunc, HandleFor} from '../common/types.js'; 15 import { 16 debugError, 17 getSourcePuppeteerURLIfAvailable, 18 getSourceUrlComment, 19 isString, 20 PuppeteerURL, 21 SOURCE_URL_REGEX, 22 } from '../common/util.js'; 23 import type PuppeteerUtil from '../injected/injected.js'; 24 import {AsyncIterableUtil} from '../util/AsyncIterableUtil.js'; 25 import {stringifyFunction} from '../util/Function.js'; 26 27 import type { 28 Realm as BidiRealmCore, 29 DedicatedWorkerRealm, 30 SharedWorkerRealm, 31 } from './core/Realm.js'; 32 import type {WindowRealm} from './core/Realm.js'; 33 import {BidiDeserializer} from './Deserializer.js'; 34 import {BidiElementHandle} from './ElementHandle.js'; 35 import {ExposableFunction} from './ExposedFunction.js'; 36 import type {BidiFrame} from './Frame.js'; 37 import {BidiJSHandle} from './JSHandle.js'; 38 import {BidiSerializer} from './Serializer.js'; 39 import {createEvaluationError} from './util.js'; 40 import type {BidiWebWorker} from './WebWorker.js'; 41 42 /** 43 * @internal 44 */ 45 export abstract class BidiRealm extends Realm { 46 readonly realm: BidiRealmCore; 47 48 constructor(realm: BidiRealmCore, timeoutSettings: TimeoutSettings) { 49 super(timeoutSettings); 50 this.realm = realm; 51 } 52 53 protected initialize(): void { 54 this.realm.on('destroyed', ({reason}) => { 55 this.taskManager.terminateAll(new Error(reason)); 56 this.dispose(); 57 }); 58 this.realm.on('updated', () => { 59 this.internalPuppeteerUtil = undefined; 60 void this.taskManager.rerunAll(); 61 }); 62 } 63 64 protected internalPuppeteerUtil?: Promise<BidiJSHandle<PuppeteerUtil>>; 65 get puppeteerUtil(): Promise<BidiJSHandle<PuppeteerUtil>> { 66 const promise = Promise.resolve() as Promise<unknown>; 67 scriptInjector.inject(script => { 68 if (this.internalPuppeteerUtil) { 69 void this.internalPuppeteerUtil.then(handle => { 70 void handle.dispose(); 71 }); 72 } 73 this.internalPuppeteerUtil = promise.then(() => { 74 return this.evaluateHandle(script) as Promise< 75 BidiJSHandle<PuppeteerUtil> 76 >; 77 }); 78 }, !this.internalPuppeteerUtil); 79 return this.internalPuppeteerUtil as Promise<BidiJSHandle<PuppeteerUtil>>; 80 } 81 82 override async evaluateHandle< 83 Params extends unknown[], 84 Func extends EvaluateFunc<Params> = EvaluateFunc<Params>, 85 >( 86 pageFunction: Func | string, 87 ...args: Params 88 ): Promise<HandleFor<Awaited<ReturnType<Func>>>> { 89 return await this.#evaluate(false, pageFunction, ...args); 90 } 91 92 override async evaluate< 93 Params extends unknown[], 94 Func extends EvaluateFunc<Params> = EvaluateFunc<Params>, 95 >( 96 pageFunction: Func | string, 97 ...args: Params 98 ): Promise<Awaited<ReturnType<Func>>> { 99 return await this.#evaluate(true, pageFunction, ...args); 100 } 101 102 async #evaluate< 103 Params extends unknown[], 104 Func extends EvaluateFunc<Params> = EvaluateFunc<Params>, 105 >( 106 returnByValue: true, 107 pageFunction: Func | string, 108 ...args: Params 109 ): Promise<Awaited<ReturnType<Func>>>; 110 async #evaluate< 111 Params extends unknown[], 112 Func extends EvaluateFunc<Params> = EvaluateFunc<Params>, 113 >( 114 returnByValue: false, 115 pageFunction: Func | string, 116 ...args: Params 117 ): Promise<HandleFor<Awaited<ReturnType<Func>>>>; 118 async #evaluate< 119 Params extends unknown[], 120 Func extends EvaluateFunc<Params> = EvaluateFunc<Params>, 121 >( 122 returnByValue: boolean, 123 pageFunction: Func | string, 124 ...args: Params 125 ): Promise<HandleFor<Awaited<ReturnType<Func>>> | Awaited<ReturnType<Func>>> { 126 const sourceUrlComment = getSourceUrlComment( 127 getSourcePuppeteerURLIfAvailable(pageFunction)?.toString() ?? 128 PuppeteerURL.INTERNAL_URL, 129 ); 130 131 let responsePromise; 132 const resultOwnership = returnByValue 133 ? Bidi.Script.ResultOwnership.None 134 : Bidi.Script.ResultOwnership.Root; 135 const serializationOptions: Bidi.Script.SerializationOptions = returnByValue 136 ? {} 137 : { 138 maxObjectDepth: 0, 139 maxDomDepth: 0, 140 }; 141 if (isString(pageFunction)) { 142 const expression = SOURCE_URL_REGEX.test(pageFunction) 143 ? pageFunction 144 : `${pageFunction}\n${sourceUrlComment}\n`; 145 146 responsePromise = this.realm.evaluate(expression, true, { 147 resultOwnership, 148 userActivation: true, 149 serializationOptions, 150 }); 151 } else { 152 let functionDeclaration = stringifyFunction(pageFunction); 153 functionDeclaration = SOURCE_URL_REGEX.test(functionDeclaration) 154 ? functionDeclaration 155 : `${functionDeclaration}\n${sourceUrlComment}\n`; 156 responsePromise = this.realm.callFunction( 157 functionDeclaration, 158 /* awaitPromise= */ true, 159 { 160 // LazyArgs are used only internally and should not affect the order 161 // evaluate calls for the public APIs. 162 arguments: args.some(arg => { 163 return arg instanceof LazyArg; 164 }) 165 ? await Promise.all( 166 args.map(arg => { 167 return this.serializeAsync(arg); 168 }), 169 ) 170 : args.map(arg => { 171 return this.serialize(arg); 172 }), 173 resultOwnership, 174 userActivation: true, 175 serializationOptions, 176 }, 177 ); 178 } 179 180 const result = await responsePromise; 181 182 if ('type' in result && result.type === 'exception') { 183 throw createEvaluationError(result.exceptionDetails); 184 } 185 186 if (returnByValue) { 187 return BidiDeserializer.deserialize(result.result); 188 } 189 190 return this.createHandle(result.result) as unknown as HandleFor< 191 Awaited<ReturnType<Func>> 192 >; 193 } 194 195 createHandle( 196 result: Bidi.Script.RemoteValue, 197 ): BidiJSHandle<unknown> | BidiElementHandle<Node> { 198 if ( 199 (result.type === 'node' || result.type === 'window') && 200 this instanceof BidiFrameRealm 201 ) { 202 return BidiElementHandle.from(result, this); 203 } 204 return BidiJSHandle.from(result, this); 205 } 206 207 async serializeAsync(arg: unknown): Promise<Bidi.Script.LocalValue> { 208 if (arg instanceof LazyArg) { 209 arg = await arg.get(this); 210 } 211 return this.serialize(arg); 212 } 213 214 serialize(arg: unknown): Bidi.Script.LocalValue { 215 if (arg instanceof BidiJSHandle || arg instanceof BidiElementHandle) { 216 if (arg.realm !== this) { 217 if ( 218 !(arg.realm instanceof BidiFrameRealm) || 219 !(this instanceof BidiFrameRealm) 220 ) { 221 throw new Error( 222 "Trying to evaluate JSHandle from different global types. Usually this means you're using a handle from a worker in a page or vice versa.", 223 ); 224 } 225 if (arg.realm.environment !== this.environment) { 226 throw new Error( 227 "Trying to evaluate JSHandle from different frames. Usually this means you're using a handle from a page on a different page.", 228 ); 229 } 230 } 231 if (arg.disposed) { 232 throw new Error('JSHandle is disposed!'); 233 } 234 return arg.remoteValue() as Bidi.Script.RemoteReference; 235 } 236 237 return BidiSerializer.serialize(arg); 238 } 239 240 async destroyHandles(handles: Array<BidiJSHandle<unknown>>): Promise<void> { 241 if (this.disposed) { 242 return; 243 } 244 245 const handleIds = handles 246 .map(({id}) => { 247 return id; 248 }) 249 .filter((id): id is string => { 250 return id !== undefined; 251 }); 252 253 if (handleIds.length === 0) { 254 return; 255 } 256 257 await this.realm.disown(handleIds).catch(error => { 258 // Exceptions might happen in case of a page been navigated or closed. 259 // Swallow these since they are harmless and we don't leak anything in this case. 260 debugError(error); 261 }); 262 } 263 264 override async adoptHandle<T extends JSHandle<Node>>(handle: T): Promise<T> { 265 return (await this.evaluateHandle(node => { 266 return node; 267 }, handle)) as unknown as T; 268 } 269 270 override async transferHandle<T extends JSHandle<Node>>( 271 handle: T, 272 ): Promise<T> { 273 if (handle.realm === this) { 274 return handle; 275 } 276 const transferredHandle = this.adoptHandle(handle); 277 await handle.dispose(); 278 return await transferredHandle; 279 } 280 } 281 282 /** 283 * @internal 284 */ 285 export class BidiFrameRealm extends BidiRealm { 286 static from(realm: WindowRealm, frame: BidiFrame): BidiFrameRealm { 287 const frameRealm = new BidiFrameRealm(realm, frame); 288 frameRealm.#initialize(); 289 return frameRealm; 290 } 291 declare readonly realm: WindowRealm; 292 293 readonly #frame: BidiFrame; 294 295 private constructor(realm: WindowRealm, frame: BidiFrame) { 296 super(realm, frame.timeoutSettings); 297 this.#frame = frame; 298 } 299 300 #initialize() { 301 super.initialize(); 302 303 // This should run first. 304 this.realm.on('updated', () => { 305 this.environment.clearDocumentHandle(); 306 this.#bindingsInstalled = false; 307 }); 308 } 309 310 #bindingsInstalled = false; 311 override get puppeteerUtil(): Promise<BidiJSHandle<PuppeteerUtil>> { 312 let promise = Promise.resolve() as Promise<unknown>; 313 if (!this.#bindingsInstalled) { 314 promise = Promise.all([ 315 ExposableFunction.from( 316 this.environment as BidiFrame, 317 '__ariaQuerySelector', 318 ARIAQueryHandler.queryOne, 319 !!this.sandbox, 320 ), 321 ExposableFunction.from( 322 this.environment as BidiFrame, 323 '__ariaQuerySelectorAll', 324 async ( 325 element: BidiElementHandle<Node>, 326 selector: string, 327 ): Promise<JSHandle<Node[]>> => { 328 const results = ARIAQueryHandler.queryAll(element, selector); 329 return await element.realm.evaluateHandle( 330 (...elements) => { 331 return elements; 332 }, 333 ...(await AsyncIterableUtil.collect(results)), 334 ); 335 }, 336 !!this.sandbox, 337 ), 338 ]); 339 this.#bindingsInstalled = true; 340 } 341 return promise.then(() => { 342 return super.puppeteerUtil; 343 }); 344 } 345 346 get sandbox(): string | undefined { 347 return this.realm.sandbox; 348 } 349 350 override get environment(): BidiFrame { 351 return this.#frame; 352 } 353 354 override async adoptBackendNode( 355 backendNodeId?: number | undefined, 356 ): Promise<JSHandle<Node>> { 357 const {object} = await this.#frame.client.send('DOM.resolveNode', { 358 backendNodeId, 359 executionContextId: await this.realm.resolveExecutionContextId(), 360 }); 361 using handle = BidiElementHandle.from( 362 { 363 handle: object.objectId, 364 type: 'node', 365 }, 366 this, 367 ); 368 // We need the sharedId, so we perform the following to obtain it. 369 return await handle.evaluateHandle(element => { 370 return element; 371 }); 372 } 373 } 374 375 /** 376 * @internal 377 */ 378 export class BidiWorkerRealm extends BidiRealm { 379 static from( 380 realm: DedicatedWorkerRealm | SharedWorkerRealm, 381 worker: BidiWebWorker, 382 ): BidiWorkerRealm { 383 const workerRealm = new BidiWorkerRealm(realm, worker); 384 workerRealm.initialize(); 385 return workerRealm; 386 } 387 declare readonly realm: DedicatedWorkerRealm | SharedWorkerRealm; 388 389 readonly #worker: BidiWebWorker; 390 391 private constructor( 392 realm: DedicatedWorkerRealm | SharedWorkerRealm, 393 frame: BidiWebWorker, 394 ) { 395 super(realm, frame.timeoutSettings); 396 this.#worker = frame; 397 } 398 399 override get environment(): BidiWebWorker { 400 return this.#worker; 401 } 402 403 override async adoptBackendNode(): Promise<JSHandle<Node>> { 404 throw new Error('Cannot adopt DOM nodes into a worker.'); 405 } 406 }