summaryrefslogtreecommitdiffstats
path: root/remote/test/puppeteer/packages/puppeteer-core/src/bidi/ExposedFunction.ts
diff options
context:
space:
mode:
Diffstat (limited to 'remote/test/puppeteer/packages/puppeteer-core/src/bidi/ExposedFunction.ts')
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/bidi/ExposedFunction.ts363
1 files changed, 161 insertions, 202 deletions
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/bidi/ExposedFunction.ts b/remote/test/puppeteer/packages/puppeteer-core/src/bidi/ExposedFunction.ts
index 62c6b5e37e..f6e1304a55 100644
--- a/remote/test/puppeteer/packages/puppeteer-core/src/bidi/ExposedFunction.ts
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/bidi/ExposedFunction.ts
@@ -6,97 +6,91 @@
import * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js';
+import {EventEmitter} from '../common/EventEmitter.js';
import type {Awaitable, FlattenHandle} from '../common/types.js';
import {debugError} from '../common/util.js';
-import {assert} from '../util/assert.js';
-import {Deferred} from '../util/Deferred.js';
+import {DisposableStack} from '../util/disposable.js';
import {interpolateFunction, stringifyFunction} from '../util/Function.js';
-import type {BidiConnection} from './Connection.js';
-import {BidiDeserializer} from './Deserializer.js';
+import type {Connection} from './core/Connection.js';
+import {BidiElementHandle} from './ElementHandle.js';
import type {BidiFrame} from './Frame.js';
-import {BidiSerializer} from './Serializer.js';
-
-type SendArgsChannel<Args> = (value: [id: number, args: Args]) => void;
-type SendResolveChannel<Ret> = (
- value: [id: number, resolve: (ret: FlattenHandle<Awaited<Ret>>) => void]
-) => void;
-type SendRejectChannel = (
- value: [id: number, reject: (error: unknown) => void]
+import {BidiJSHandle} from './JSHandle.js';
+
+type CallbackChannel<Args, Ret> = (
+ value: [
+ resolve: (ret: FlattenHandle<Awaited<Ret>>) => void,
+ reject: (error: unknown) => void,
+ args: Args,
+ ]
) => void;
-interface RemotePromiseCallbacks {
- resolve: Deferred<Bidi.Script.RemoteValue>;
- reject: Deferred<Bidi.Script.RemoteValue>;
-}
-
/**
* @internal
*/
export class ExposeableFunction<Args extends unknown[], Ret> {
+ static async from<Args extends unknown[], Ret>(
+ frame: BidiFrame,
+ name: string,
+ apply: (...args: Args) => Awaitable<Ret>,
+ isolate = false
+ ): Promise<ExposeableFunction<Args, Ret>> {
+ const func = new ExposeableFunction(frame, name, apply, isolate);
+ await func.#initialize();
+ return func;
+ }
+
readonly #frame;
readonly name;
readonly #apply;
+ readonly #isolate;
- readonly #channels;
- readonly #callerInfos = new Map<
- string,
- Map<number, RemotePromiseCallbacks>
- >();
+ readonly #channel;
- #preloadScriptId?: Bidi.Script.PreloadScript;
+ #scripts: Array<[BidiFrame, Bidi.Script.PreloadScript]> = [];
+ #disposables = new DisposableStack();
constructor(
frame: BidiFrame,
name: string,
- apply: (...args: Args) => Awaitable<Ret>
+ apply: (...args: Args) => Awaitable<Ret>,
+ isolate = false
) {
this.#frame = frame;
this.name = name;
this.#apply = apply;
+ this.#isolate = isolate;
- this.#channels = {
- args: `__puppeteer__${this.#frame._id}_page_exposeFunction_${this.name}_args`,
- resolve: `__puppeteer__${this.#frame._id}_page_exposeFunction_${this.name}_resolve`,
- reject: `__puppeteer__${this.#frame._id}_page_exposeFunction_${this.name}_reject`,
- };
+ this.#channel = `__puppeteer__${this.#frame._id}_page_exposeFunction_${this.name}`;
}
- async expose(): Promise<void> {
+ async #initialize() {
const connection = this.#connection;
- const channelArguments = this.#channelArguments;
+ const channel = {
+ type: 'channel' as const,
+ value: {
+ channel: this.#channel,
+ ownership: Bidi.Script.ResultOwnership.Root,
+ },
+ };
- // TODO(jrandolf): Implement cleanup with removePreloadScript.
- connection.on(
- Bidi.ChromiumBidi.Script.EventNames.Message,
- this.#handleArgumentsMessage
+ const connectionEmitter = this.#disposables.use(
+ new EventEmitter(connection)
);
- connection.on(
+ connectionEmitter.on(
Bidi.ChromiumBidi.Script.EventNames.Message,
- this.#handleResolveMessage
- );
- connection.on(
- Bidi.ChromiumBidi.Script.EventNames.Message,
- this.#handleRejectMessage
+ this.#handleMessage
);
const functionDeclaration = stringifyFunction(
interpolateFunction(
- (
- sendArgs: SendArgsChannel<Args>,
- sendResolve: SendResolveChannel<Ret>,
- sendReject: SendRejectChannel
- ) => {
- let id = 0;
+ (callback: CallbackChannel<Args, Ret>) => {
Object.assign(globalThis, {
[PLACEHOLDER('name') as string]: function (...args: Args) {
return new Promise<FlattenHandle<Awaited<Ret>>>(
(resolve, reject) => {
- sendArgs([id, args]);
- sendResolve([id, resolve]);
- sendReject([id, reject]);
- ++id;
+ callback([resolve, reject, args]);
}
);
},
@@ -106,179 +100,133 @@ export class ExposeableFunction<Args extends unknown[], Ret> {
)
);
- const {result} = await connection.send('script.addPreloadScript', {
- functionDeclaration,
- arguments: channelArguments,
- contexts: [this.#frame.page().mainFrame()._id],
- });
- this.#preloadScriptId = result.script;
+ const frames = [this.#frame];
+ for (const frame of frames) {
+ frames.push(...frame.childFrames());
+ }
await Promise.all(
- this.#frame
- .page()
- .frames()
- .map(async frame => {
- return await connection.send('script.callFunction', {
- functionDeclaration,
- arguments: channelArguments,
- awaitPromise: false,
- target: frame.mainRealm().realm.target,
- });
- })
+ frames.map(async frame => {
+ const realm = this.#isolate ? frame.isolatedRealm() : frame.mainRealm();
+ try {
+ const [script] = await Promise.all([
+ frame.browsingContext.addPreloadScript(functionDeclaration, {
+ arguments: [channel],
+ sandbox: realm.sandbox,
+ }),
+ realm.realm.callFunction(functionDeclaration, false, {
+ arguments: [channel],
+ }),
+ ]);
+ this.#scripts.push([frame, script]);
+ } catch (error) {
+ // If it errors, the frame probably doesn't support call function. We
+ // fail gracefully.
+ debugError(error);
+ }
+ })
);
}
- #handleArgumentsMessage = async (params: Bidi.Script.MessageParameters) => {
- if (params.channel !== this.#channels.args) {
+ get #connection(): Connection {
+ return this.#frame.page().browser().connection;
+ }
+
+ #handleMessage = async (params: Bidi.Script.MessageParameters) => {
+ if (params.channel !== this.#channel) {
return;
}
- const connection = this.#connection;
- const {callbacks, remoteValue} = this.#getCallbacksAndRemoteValue(params);
- const args = remoteValue.value?.[1];
- assert(args);
+ const realm = this.#getRealm(params.source);
+ if (!realm) {
+ // Unrelated message.
+ return;
+ }
+
+ using dataHandle = BidiJSHandle.from<
+ [
+ resolve: (ret: FlattenHandle<Awaited<Ret>>) => void,
+ reject: (error: unknown) => void,
+ args: Args,
+ ]
+ >(params.data, realm);
+
+ using argsHandle = await dataHandle.evaluateHandle(([, , args]) => {
+ return args;
+ });
+
+ using stack = new DisposableStack();
+ const args = [];
+ for (const [index, handle] of await argsHandle.getProperties()) {
+ stack.use(handle);
+
+ // Element handles are passed as is.
+ if (handle instanceof BidiElementHandle) {
+ args[+index] = handle;
+ stack.use(handle);
+ continue;
+ }
+
+ // Everything else is passed as the JS value.
+ args[+index] = handle.jsonValue();
+ }
+
+ let result;
try {
- const result = await this.#apply(...BidiDeserializer.deserialize(args));
- await connection.send('script.callFunction', {
- functionDeclaration: stringifyFunction(([_, resolve]: any, result) => {
- resolve(result);
- }),
- arguments: [
- (await callbacks.resolve.valueOrThrow()) as Bidi.Script.LocalValue,
- BidiSerializer.serializeRemoteValue(result),
- ],
- awaitPromise: false,
- target: {
- realm: params.source.realm,
- },
- });
+ result = await this.#apply(...((await Promise.all(args)) as Args));
} catch (error) {
try {
if (error instanceof Error) {
- await connection.send('script.callFunction', {
- functionDeclaration: stringifyFunction(
- (
- [_, reject]: [unknown, (error: Error) => void],
- name: string,
- message: string,
- stack?: string
- ) => {
- const error = new Error(message);
- error.name = name;
- if (stack) {
- error.stack = stack;
- }
- reject(error);
+ await dataHandle.evaluate(
+ ([, reject], name, message, stack) => {
+ const error = new Error(message);
+ error.name = name;
+ if (stack) {
+ error.stack = stack;
}
- ),
- arguments: [
- (await callbacks.reject.valueOrThrow()) as Bidi.Script.LocalValue,
- BidiSerializer.serializeRemoteValue(error.name),
- BidiSerializer.serializeRemoteValue(error.message),
- BidiSerializer.serializeRemoteValue(error.stack),
- ],
- awaitPromise: false,
- target: {
- realm: params.source.realm,
+ reject(error);
},
- });
+ error.name,
+ error.message,
+ error.stack
+ );
} else {
- await connection.send('script.callFunction', {
- functionDeclaration: stringifyFunction(
- (
- [_, reject]: [unknown, (error: unknown) => void],
- error: unknown
- ) => {
- reject(error);
- }
- ),
- arguments: [
- (await callbacks.reject.valueOrThrow()) as Bidi.Script.LocalValue,
- BidiSerializer.serializeRemoteValue(error),
- ],
- awaitPromise: false,
- target: {
- realm: params.source.realm,
- },
- });
+ await dataHandle.evaluate(([, reject], error) => {
+ reject(error);
+ }, error);
}
} catch (error) {
debugError(error);
}
- }
- };
-
- get #connection(): BidiConnection {
- return this.#frame.context().connection;
- }
-
- get #channelArguments() {
- return [
- {
- type: 'channel' as const,
- value: {
- channel: this.#channels.args,
- ownership: Bidi.Script.ResultOwnership.Root,
- },
- },
- {
- type: 'channel' as const,
- value: {
- channel: this.#channels.resolve,
- ownership: Bidi.Script.ResultOwnership.Root,
- },
- },
- {
- type: 'channel' as const,
- value: {
- channel: this.#channels.reject,
- ownership: Bidi.Script.ResultOwnership.Root,
- },
- },
- ];
- }
-
- #handleResolveMessage = (params: Bidi.Script.MessageParameters) => {
- if (params.channel !== this.#channels.resolve) {
return;
}
- const {callbacks, remoteValue} = this.#getCallbacksAndRemoteValue(params);
- callbacks.resolve.resolve(remoteValue);
- };
- #handleRejectMessage = (params: Bidi.Script.MessageParameters) => {
- if (params.channel !== this.#channels.reject) {
- return;
+ try {
+ await dataHandle.evaluate(([resolve], result) => {
+ resolve(result);
+ }, result);
+ } catch (error) {
+ debugError(error);
}
- const {callbacks, remoteValue} = this.#getCallbacksAndRemoteValue(params);
- callbacks.reject.resolve(remoteValue);
};
- #getCallbacksAndRemoteValue(params: Bidi.Script.MessageParameters) {
- const {data, source} = params;
- assert(data.type === 'array');
- assert(data.value);
-
- const callerIdRemote = data.value[0];
- assert(callerIdRemote);
- assert(callerIdRemote.type === 'number');
- assert(typeof callerIdRemote.value === 'number');
-
- let bindingMap = this.#callerInfos.get(source.realm);
- if (!bindingMap) {
- bindingMap = new Map();
- this.#callerInfos.set(source.realm, bindingMap);
+ #getRealm(source: Bidi.Script.Source) {
+ const frame = this.#findFrame(source.context as string);
+ if (!frame) {
+ // Unrelated message.
+ return;
}
+ return frame.realm(source.realm);
+ }
- const callerId = callerIdRemote.value;
- let callbacks = bindingMap.get(callerId);
- if (!callbacks) {
- callbacks = {
- resolve: new Deferred(),
- reject: new Deferred(),
- };
- bindingMap.set(callerId, callbacks);
+ #findFrame(id: string) {
+ const frames = [this.#frame];
+ for (const frame of frames) {
+ if (frame._id === id) {
+ return frame;
+ }
+ frames.push(...frame.childFrames());
}
- return {callbacks, remoteValue: data};
+ return;
}
[Symbol.dispose](): void {
@@ -286,10 +234,21 @@ export class ExposeableFunction<Args extends unknown[], Ret> {
}
async [Symbol.asyncDispose](): Promise<void> {
- if (this.#preloadScriptId) {
- await this.#connection.send('script.removePreloadScript', {
- script: this.#preloadScriptId,
- });
- }
+ this.#disposables.dispose();
+ await Promise.all(
+ this.#scripts.map(async ([frame, script]) => {
+ const realm = this.#isolate ? frame.isolatedRealm() : frame.mainRealm();
+ try {
+ await Promise.all([
+ realm.evaluate(name => {
+ delete (globalThis as any)[name];
+ }, this.name),
+ frame.browsingContext.removePreloadScript(script),
+ ]);
+ } catch (error) {
+ debugError(error);
+ }
+ })
+ );
}
}