refactor: replace deferred with rxjs (#12406)

This commit is contained in:
Alex Rudenko 2024-05-07 12:30:47 +02:00 committed by GitHub
parent 93bf52bdfc
commit bc17e339bc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

View File

@ -6,18 +6,22 @@
import type {Protocol} from 'devtools-protocol'; 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 {CDPSession} from '../api/CDPSession.js';
import type {ElementHandle} from '../api/ElementHandle.js'; import type {ElementHandle} from '../api/ElementHandle.js';
import type {JSHandle} from '../api/JSHandle.js'; import type {JSHandle} from '../api/JSHandle.js';
import {Realm} from '../api/Realm.js'; import {Realm} from '../api/Realm.js';
import {EventEmitter} from '../common/EventEmitter.js';
import type {TimeoutSettings} from '../common/TimeoutSettings.js'; import type {TimeoutSettings} from '../common/TimeoutSettings.js';
import type {EvaluateFunc, HandleFor} from '../common/types.js'; import type {EvaluateFunc, HandleFor} from '../common/types.js';
import {withSourcePuppeteerURLIfNone} from '../common/util.js'; import {
import {Deferred} from '../util/Deferred.js'; fromEmitterEvent,
withSourcePuppeteerURLIfNone,
} from '../common/util.js';
import {disposeSymbol} from '../util/disposable.js'; import {disposeSymbol} from '../util/disposable.js';
import {CdpElementHandle} from './ElementHandle.js'; import {CdpElementHandle} from './ElementHandle.js';
import {ExecutionContext} from './ExecutionContext.js'; import type {ExecutionContext} from './ExecutionContext.js';
import type {CdpFrame} from './Frame.js'; import type {CdpFrame} from './Frame.js';
import type {MAIN_WORLD, PUPPETEER_WORLD} from './IsolatedWorlds.js'; import type {MAIN_WORLD, PUPPETEER_WORLD} from './IsolatedWorlds.js';
import {CdpJSHandle} from './JSHandle.js'; import {CdpJSHandle} from './JSHandle.js';
@ -44,7 +48,13 @@ export interface IsolatedWorldChart {
* @internal * @internal
*/ */
export class IsolatedWorld extends Realm { export class IsolatedWorld extends Realm {
#context = Deferred.create<ExecutionContext>(); #context?: ExecutionContext;
#emitter = new EventEmitter<{
// Emitted when the isolated world gets a new execution context.
context: ExecutionContext;
// Emitted when the isolated world is disposed.
disposed: undefined;
}>();
readonly #frameOrWorker: CdpFrame | CdpWebWorker; readonly #frameOrWorker: CdpFrame | CdpWebWorker;
@ -65,36 +75,48 @@ export class IsolatedWorld extends Realm {
} }
setContext(context: ExecutionContext): void { setContext(context: ExecutionContext): void {
const existingContext = this.#context.value(); this.#context?.[disposeSymbol]();
if (existingContext instanceof ExecutionContext) {
existingContext[disposeSymbol]();
}
context.once('disposed', () => { context.once('disposed', () => {
// The message has to match the CDP message expected by the WaitTask class. this.#context = undefined;
this.#context?.reject(new Error('Execution context was destroyed'));
this.#context = Deferred.create();
if ('clearDocumentHandle' in this.#frameOrWorker) { if ('clearDocumentHandle' in this.#frameOrWorker) {
this.#frameOrWorker.clearDocumentHandle(); this.#frameOrWorker.clearDocumentHandle();
} }
}); });
this.#context.resolve(context); this.#context = context;
this.#emitter.emit('context', context);
void this.taskManager.rerunAll(); void this.taskManager.rerunAll();
} }
hasContext(): boolean { hasContext(): boolean {
return this.#context.resolved(); return !!this.#context;
} }
#executionContext(): Promise<ExecutionContext> { #executionContext(): ExecutionContext | undefined {
if (this.disposed) { if (this.disposed) {
throw new Error( throw new Error(
`Execution context is not available in detached frame "${this.environment.url()}" (are you trying to evaluate?)` `Execution context is not available in detached frame "${this.environment.url()}" (are you trying to evaluate?)`
); );
} }
if (this.#context === null) { return this.#context;
throw new Error(`Execution content promise is missing`); }
}
return this.#context.valueOrThrow(); /**
* 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');
})
)
)
)
);
return result;
} }
async evaluateHandle< async evaluateHandle<
@ -108,7 +130,13 @@ export class IsolatedWorld extends Realm {
this.evaluateHandle.name, this.evaluateHandle.name,
pageFunction pageFunction
); );
const context = await this.#executionContext(); // 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); return await context.evaluateHandle(pageFunction, ...args);
} }
@ -123,9 +151,12 @@ export class IsolatedWorld extends Realm {
this.evaluate.name, this.evaluate.name,
pageFunction pageFunction
); );
let context = this.#context.value(); // This code needs to schedule evaluate call synchroniously (at
if (!context || !(context instanceof ExecutionContext)) { // least when the context is there) so we cannot unconditionally
context = await this.#executionContext(); // await.
let context = this.#executionContext();
if (!context) {
context = await this.#waitForExecutionContext();
} }
return await context.evaluate(pageFunction, ...args); return await context.evaluate(pageFunction, ...args);
} }
@ -133,10 +164,16 @@ export class IsolatedWorld extends Realm {
override async adoptBackendNode( override async adoptBackendNode(
backendNodeId?: Protocol.DOM.BackendNodeId backendNodeId?: Protocol.DOM.BackendNodeId
): Promise<JSHandle<Node>> { ): Promise<JSHandle<Node>> {
const executionContext = await this.#executionContext(); // 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', { const {object} = await this.client.send('DOM.resolveNode', {
backendNodeId: backendNodeId, backendNodeId: backendNodeId,
executionContextId: executionContext._contextId, executionContextId: context._contextId,
}); });
return this.createCdpHandle(object) as JSHandle<Node>; return this.createCdpHandle(object) as JSHandle<Node>;
} }
@ -186,10 +223,8 @@ export class IsolatedWorld extends Realm {
} }
[disposeSymbol](): void { [disposeSymbol](): void {
const existingContext = this.#context.value(); this.#context?.[disposeSymbol]();
if (existingContext instanceof ExecutionContext) { this.#emitter.emit('disposed', undefined);
existingContext[disposeSymbol]();
}
super[disposeSymbol](); super[disposeSymbol]();
} }
} }