chore: extract BiDi context to allow emitting only to it (#9742)

This commit is contained in:
Nikolay Vitkov 2023-02-28 11:10:14 +01:00 committed by GitHub
parent ed1bb7cbe0
commit 4a365a42b4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 234 additions and 117 deletions

View File

@ -103,13 +103,11 @@ export class ChromeTargetManager extends EventEmitter implements TargetManager {
this.#connection.on('sessiondetached', this.#onSessionDetached); this.#connection.on('sessiondetached', this.#onSessionDetached);
this.#setupAttachmentListeners(this.#connection); this.#setupAttachmentListeners(this.#connection);
// TODO: remove `as any` once the protocol definitions are updated with the
// next Chromium roll.
this.#connection this.#connection
.send('Target.setDiscoverTargets', { .send('Target.setDiscoverTargets', {
discover: true, discover: true,
filter: [{type: 'tab', exclude: true}, {}], filter: [{type: 'tab', exclude: true}, {}],
} as any) })
.then(this.#storeExistingTargetsForInit) .then(this.#storeExistingTargetsForInit)
.catch(debugError); .catch(debugError);
} }

View File

@ -1,3 +1,19 @@
/**
* Copyright 2023 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import * as BidiMapper from 'chromium-bidi/lib/cjs/bidiMapper/bidiMapper.js'; import * as BidiMapper from 'chromium-bidi/lib/cjs/bidiMapper/bidiMapper.js';
import * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js'; import * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js';
import type {ProtocolMapping} from 'devtools-protocol/types/protocol-mapping.js'; import type {ProtocolMapping} from 'devtools-protocol/types/protocol-mapping.js';

View File

@ -18,6 +18,7 @@ import {BrowserContext as BrowserContextBase} from '../../api/BrowserContext.js'
import {Page as PageBase} from '../../api/Page.js'; import {Page as PageBase} from '../../api/Page.js';
import {Connection} from './Connection.js'; import {Connection} from './Connection.js';
import {Context} from './Context.js';
import {Page} from './Page.js'; import {Page} from './Page.js';
/** /**
@ -32,10 +33,11 @@ export class BrowserContext extends BrowserContextBase {
} }
override async newPage(): Promise<PageBase> { override async newPage(): Promise<PageBase> {
const response = await this.#connection.send('browsingContext.create', { const {result} = await this.#connection.send('browsingContext.create', {
type: 'tab', type: 'tab',
}); });
return new Page(this.#connection, response.result.context); const context = this.#connection.context(result.context) as Context;
return new Page(context);
} }
override async close(): Promise<void> {} override async close(): Promise<void> {}

View File

@ -22,6 +22,8 @@ import {debug} from '../Debug.js';
import {ProtocolError} from '../Errors.js'; import {ProtocolError} from '../Errors.js';
import {EventEmitter} from '../EventEmitter.js'; import {EventEmitter} from '../EventEmitter.js';
import {Context} from './Context.js';
const debugProtocolSend = debug('puppeteer:webDriverBiDi:SEND ►'); const debugProtocolSend = debug('puppeteer:webDriverBiDi:SEND ►');
const debugProtocolReceive = debug('puppeteer:webDriverBiDi:RECV ◀'); const debugProtocolReceive = debug('puppeteer:webDriverBiDi:RECV ◀');
@ -78,6 +80,7 @@ export class Connection extends EventEmitter {
#lastId = 0; #lastId = 0;
#closed = false; #closed = false;
#callbacks: Map<number, ConnectionCallback> = new Map(); #callbacks: Map<number, ConnectionCallback> = new Map();
#contexts: Map<string, Context> = new Map();
constructor(transport: ConnectionTransport, delay = 0) { constructor(transport: ConnectionTransport, delay = 0) {
super(); super();
@ -92,6 +95,10 @@ export class Connection extends EventEmitter {
return this.#closed; return this.#closed;
} }
context(contextId: string): Context | null {
return this.#contexts.get(contextId) || null;
}
send<T extends keyof Commands>( send<T extends keyof Commands>(
method: T, method: T,
params: Commands[T]['params'] params: Commands[T]['params']
@ -126,7 +133,8 @@ export class Connection extends EventEmitter {
debugProtocolReceive(message); debugProtocolReceive(message);
const object = JSON.parse(message) as const object = JSON.parse(message) as
| Bidi.Message.CommandResponse | Bidi.Message.CommandResponse
| Bidi.EventResponse<string, unknown>; | Bidi.Message.EventMessage;
if ('id' in object) { if ('id' in object) {
const callback = this.#callbacks.get(object.id); const callback = this.#callbacks.get(object.id);
// Callbacks could be all rejected if someone has called `.dispose()`. // Callbacks could be all rejected if someone has called `.dispose()`.
@ -137,10 +145,21 @@ export class Connection extends EventEmitter {
createProtocolError(callback.error, callback.method, object) createProtocolError(callback.error, callback.method, object)
); );
} else { } else {
if (callback.method === 'browsingContext.create') {
this.#contexts.set(
object.result.context,
new Context(this, object.result.context)
);
}
callback.resolve(object); callback.resolve(object);
} }
} }
} else { } else {
if ('source' in object.params && !!object.params.source.context) {
const context = this.#contexts.get(object.params.source.context);
context?.emit(object.method, object.params);
}
this.emit(object.method, object.params); this.emit(object.method, object.params);
} }
} }

View File

@ -0,0 +1,140 @@
/**
* Copyright 2023 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js';
import {stringifyFunction} from '../../util/Function.js';
import {EventEmitter} from '../EventEmitter.js';
import {EvaluateFunc, HandleFor} from '../types.js';
import {isString} from '../util.js';
import {Connection} from './Connection.js';
import {JSHandle} from './JSHandle.js';
import {BidiSerializer} from './Serializer.js';
/**
* @internal
*/
export class Context extends EventEmitter {
#connection: Connection;
_contextId: string;
constructor(connection: Connection, contextId: string) {
super();
this.#connection = connection;
this._contextId = contextId;
}
get connection(): Connection {
return this.#connection;
}
get id(): string {
return this._contextId;
}
async evaluateHandle<
Params extends unknown[],
Func extends EvaluateFunc<Params> = EvaluateFunc<Params>
>(
pageFunction: Func | string,
...args: Params
): Promise<HandleFor<Awaited<ReturnType<Func>>>> {
return this.#evaluate(false, pageFunction, ...args);
}
async evaluate<
Params extends unknown[],
Func extends EvaluateFunc<Params> = EvaluateFunc<Params>
>(
pageFunction: Func | string,
...args: Params
): Promise<Awaited<ReturnType<Func>>> {
return this.#evaluate(true, pageFunction, ...args);
}
async #evaluate<
Params extends unknown[],
Func extends EvaluateFunc<Params> = EvaluateFunc<Params>
>(
returnByValue: true,
pageFunction: Func | string,
...args: Params
): Promise<Awaited<ReturnType<Func>>>;
async #evaluate<
Params extends unknown[],
Func extends EvaluateFunc<Params> = EvaluateFunc<Params>
>(
returnByValue: false,
pageFunction: Func | string,
...args: Params
): Promise<HandleFor<Awaited<ReturnType<Func>>>>;
async #evaluate<
Params extends unknown[],
Func extends EvaluateFunc<Params> = EvaluateFunc<Params>
>(
returnByValue: boolean,
pageFunction: Func | string,
...args: Params
): Promise<HandleFor<Awaited<ReturnType<Func>>> | Awaited<ReturnType<Func>>> {
let responsePromise;
const resultOwnership = returnByValue ? 'none' : 'root';
if (isString(pageFunction)) {
responsePromise = this.#connection.send('script.evaluate', {
expression: pageFunction,
target: {context: this._contextId},
resultOwnership,
awaitPromise: true,
});
} else {
responsePromise = this.#connection.send('script.callFunction', {
functionDeclaration: stringifyFunction(pageFunction),
arguments: await Promise.all(
args.map(arg => {
return BidiSerializer.serialize(arg, this);
})
),
target: {context: this._contextId},
resultOwnership,
awaitPromise: true,
});
}
const {result} = await responsePromise;
if ('type' in result && result.type === 'exception') {
throw new Error(result.exceptionDetails.text);
}
return returnByValue
? BidiSerializer.deserialize(result.result)
: getBidiHandle(this, result.result);
}
}
/**
* @internal
*/
export function getBidiHandle(
context: Context,
result: Bidi.CommonDataTypes.RemoteValue
): JSHandle {
if ((result.type === 'node' || result.type === 'window') && context) {
// TODO: Implement ElementHandle
return new JSHandle(context, result);
}
return new JSHandle(context, result);
}

View File

@ -21,7 +21,7 @@ import {JSHandle as BaseJSHandle} from '../../api/JSHandle.js';
import {EvaluateFuncWith, HandleFor, HandleOr} from '../../common/types.js'; import {EvaluateFuncWith, HandleFor, HandleOr} from '../../common/types.js';
import {Connection} from './Connection.js'; import {Connection} from './Connection.js';
import {Page} from './Page.js'; import {Context} from './Context.js';
import {BidiSerializer} from './Serializer.js'; import {BidiSerializer} from './Serializer.js';
import {releaseReference} from './utils.js'; import {releaseReference} from './utils.js';
@ -30,17 +30,17 @@ export class JSHandle<T = unknown> extends BaseJSHandle<T> {
#context; #context;
#remoteValue; #remoteValue;
constructor(context: Page, remoteValue: Bidi.CommonDataTypes.RemoteValue) { constructor(context: Context, remoteValue: Bidi.CommonDataTypes.RemoteValue) {
super(); super();
this.#context = context; this.#context = context;
this.#remoteValue = remoteValue; this.#remoteValue = remoteValue;
} }
context(): Page { context(): Context {
return this.#context; return this.#context;
} }
get connecton(): Connection { get connection(): Connection {
return this.#context.connection; return this.#context.connection;
} }
@ -122,7 +122,7 @@ export class JSHandle<T = unknown> extends BaseJSHandle<T> {
} }
this.#disposed = true; this.#disposed = true;
if ('handle' in this.#remoteValue) { if ('handle' in this.#remoteValue) {
await releaseReference(this.connecton, this.#remoteValue); await releaseReference(this.#context, this.#remoteValue);
} }
} }
@ -153,7 +153,7 @@ export class JSHandle<T = unknown> extends BaseJSHandle<T> {
return 'handle' in this.#remoteValue ? this.#remoteValue.handle : undefined; return 'handle' in this.#remoteValue ? this.#remoteValue.handle : undefined;
} }
bidiObject(): Bidi.CommonDataTypes.RemoteValue { remoteValue(): Bidi.CommonDataTypes.RemoteValue {
return this.#remoteValue; return this.#remoteValue;
} }
} }

View File

@ -17,49 +17,46 @@
import * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js'; import * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js';
import {Page as PageBase, PageEmittedEvents} from '../../api/Page.js'; import {Page as PageBase, PageEmittedEvents} from '../../api/Page.js';
import {stringifyFunction} from '../../util/Function.js';
import {ConsoleMessage, ConsoleMessageLocation} from '../ConsoleMessage.js'; import {ConsoleMessage, ConsoleMessageLocation} from '../ConsoleMessage.js';
import type {EvaluateFunc, HandleFor} from '../types.js'; import {EvaluateFunc, HandleFor} from '../types.js';
import {isString} from '../util.js';
import {Connection} from './Connection.js'; import {Connection} from './Connection.js';
import {JSHandle} from './JSHandle.js'; import {Context, getBidiHandle} from './Context.js';
import {BidiSerializer} from './Serializer.js'; import {BidiSerializer} from './Serializer.js';
/** /**
* @internal * @internal
*/ */
export class Page extends PageBase { export class Page extends PageBase {
#connection: Connection; #context: Context;
#subscribedEvents = [ #subscribedEvents = [
'log.entryAdded', 'log.entryAdded',
] as Bidi.Session.SubscribeParameters['events']; ] as Bidi.Session.SubscribeParameters['events'];
_contextId: string; #boundOnLogEntryAdded = this.#onLogEntryAdded.bind(this);
constructor(connection: Connection, contextId: string) { constructor(context: Context) {
super(); super();
this.#connection = connection; this.#context = context;
this._contextId = contextId;
// TODO: Investigate an implementation similar to CDPSession // TODO: Investigate an implementation similar to CDPSession
this.connection.send('session.subscribe', { this.connection.send('session.subscribe', {
events: this.#subscribedEvents, events: this.#subscribedEvents,
contexts: [this._contextId], contexts: [this.contextId],
}); });
this.connection.on('log.entryAdded', this.#onLogEntryAdded.bind(this)); this.#context.on('log.entryAdded', this.#boundOnLogEntryAdded);
} }
#onLogEntryAdded(event: Bidi.Log.LogEntry): void { #onLogEntryAdded(event: Bidi.Log.LogEntry): void {
if (isConsoleLogEntry(event)) { if (isConsoleLogEntry(event)) {
const args = event.args.map(arg => { const args = event.args.map(arg => {
return getBidiHandle(this, arg); return getBidiHandle(this.#context, arg);
}); });
const text = args const text = args
.reduce((value, arg) => { .reduce((value, arg) => {
const parsedValue = arg.isPrimitiveValue const parsedValue = arg.isPrimitiveValue
? BidiSerializer.deserialize(arg.bidiObject()) ? BidiSerializer.deserialize(arg.remoteValue())
: arg.toString(); : arg.toString();
return `${value} ${parsedValue}`; return `${value} ${parsedValue}`;
}, '') }, '')
@ -88,20 +85,24 @@ export class Page extends PageBase {
} }
override async close(): Promise<void> { override async close(): Promise<void> {
await this.#connection.send('browsingContext.close', { await this.connection.send('session.unsubscribe', {
context: this._contextId,
});
this.connection.send('session.unsubscribe', {
events: this.#subscribedEvents, events: this.#subscribedEvents,
contexts: [this._contextId], contexts: [this.contextId],
}); });
this.connection.off('log.entryAdded', this.#onLogEntryAdded.bind(this)); await this.connection.send('browsingContext.close', {
context: this.contextId,
});
this.#context.off('log.entryAdded', this.#boundOnLogEntryAdded);
} }
get connection(): Connection { get connection(): Connection {
return this.#connection; return this.#context.connection;
}
get contextId(): string {
return this.#context.id;
} }
override async evaluateHandle< override async evaluateHandle<
@ -111,7 +112,7 @@ export class Page extends PageBase {
pageFunction: Func | string, pageFunction: Func | string,
...args: Params ...args: Params
): Promise<HandleFor<Awaited<ReturnType<Func>>>> { ): Promise<HandleFor<Awaited<ReturnType<Func>>>> {
return this.#evaluate(false, pageFunction, ...args); return this.#context.evaluateHandle(pageFunction, ...args);
} }
override async evaluate< override async evaluate<
@ -121,83 +122,8 @@ export class Page extends PageBase {
pageFunction: Func | string, pageFunction: Func | string,
...args: Params ...args: Params
): Promise<Awaited<ReturnType<Func>>> { ): Promise<Awaited<ReturnType<Func>>> {
return this.#evaluate(true, pageFunction, ...args); return this.#context.evaluate(pageFunction, ...args);
} }
async #evaluate<
Params extends unknown[],
Func extends EvaluateFunc<Params> = EvaluateFunc<Params>
>(
returnByValue: true,
pageFunction: Func | string,
...args: Params
): Promise<Awaited<ReturnType<Func>>>;
async #evaluate<
Params extends unknown[],
Func extends EvaluateFunc<Params> = EvaluateFunc<Params>
>(
returnByValue: false,
pageFunction: Func | string,
...args: Params
): Promise<HandleFor<Awaited<ReturnType<Func>>>>;
async #evaluate<
Params extends unknown[],
Func extends EvaluateFunc<Params> = EvaluateFunc<Params>
>(
returnByValue: boolean,
pageFunction: Func | string,
...args: Params
): Promise<HandleFor<Awaited<ReturnType<Func>>> | Awaited<ReturnType<Func>>> {
let responsePromise;
const resultOwnership = returnByValue ? 'none' : 'root';
if (isString(pageFunction)) {
responsePromise = this.#connection.send('script.evaluate', {
expression: pageFunction,
target: {context: this._contextId},
resultOwnership,
awaitPromise: true,
});
} else {
responsePromise = this.#connection.send('script.callFunction', {
functionDeclaration: stringifyFunction(pageFunction),
arguments: await Promise.all(
args.map(arg => {
return BidiSerializer.serialize(arg, this);
})
),
target: {context: this._contextId},
resultOwnership,
awaitPromise: true,
});
}
const {result} = await responsePromise;
if ('type' in result && result.type === 'exception') {
throw new Error(result.exceptionDetails.text);
}
return returnByValue
? BidiSerializer.deserialize(result.result)
: getBidiHandle(this, result.result);
}
}
/**
* @internal
*/
export function getBidiHandle(
context: Page,
result: Bidi.CommonDataTypes.RemoteValue
): JSHandle {
if (
(result.type === 'node' || result.type === 'window') &&
context._contextId
) {
// TODO: Implement ElementHandle
return new JSHandle(context, result);
}
return new JSHandle(context, result);
} }
function isConsoleLogEntry( function isConsoleLogEntry(

View File

@ -1,9 +1,25 @@
/**
* Copyright 2023 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js'; import * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js';
import {debugError, isDate, isPlainObject, isRegExp} from '../util.js'; import {debugError, isDate, isPlainObject, isRegExp} from '../util.js';
import {Context} from './Context.js';
import {JSHandle} from './JSHandle.js'; import {JSHandle} from './JSHandle.js';
import {Page} from './Page.js';
/** /**
* @internal * @internal
@ -130,7 +146,7 @@ export class BidiSerializer {
static serialize( static serialize(
arg: unknown, arg: unknown,
context: Page context: Context
): Bidi.CommonDataTypes.LocalOrRemoteValue { ): Bidi.CommonDataTypes.LocalOrRemoteValue {
// TODO: See use case of LazyArgs // TODO: See use case of LazyArgs
const objectHandle = arg && arg instanceof JSHandle ? arg : null; const objectHandle = arg && arg instanceof JSHandle ? arg : null;
@ -143,7 +159,7 @@ export class BidiSerializer {
if (objectHandle.disposed) { if (objectHandle.disposed) {
throw new Error('JSHandle is disposed!'); throw new Error('JSHandle is disposed!');
} }
return objectHandle.bidiObject(); return objectHandle.remoteValue();
} }
return BidiSerializer.serializeRemoveValue(arg); return BidiSerializer.serializeRemoveValue(arg);

View File

@ -18,7 +18,7 @@ import * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js';
import {debug} from '../Debug.js'; import {debug} from '../Debug.js';
import {Connection} from './Connection.js'; import {Context} from './Context.js';
/** /**
* @internal * @internal
@ -28,15 +28,15 @@ export const debugError = debug('puppeteer:error');
* @internal * @internal
*/ */
export async function releaseReference( export async function releaseReference(
client: Connection, client: Context,
remoteReference: Bidi.CommonDataTypes.RemoteReference remoteReference: Bidi.CommonDataTypes.RemoteReference
): Promise<void> { ): Promise<void> {
if (!remoteReference.handle) { if (!remoteReference.handle) {
return; return;
} }
await client await client.connection
.send('script.disown', { .send('script.disown', {
target: {realm: '', context: ''}, // TODO: Populate target: {context: client._contextId},
handles: [remoteReference.handle], handles: [remoteReference.handle],
}) })
.catch((error: any) => { .catch((error: any) => {