feat(rpc): make SerializedValue format pdl-friendly (#3007)

This avoids sum types and instead uses different fields
for different types.
This commit is contained in:
Dmitry Gozman 2020-07-19 19:46:19 -07:00 committed by GitHub
parent 79d5991a27
commit 29504c0824
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 34 additions and 33 deletions

View file

@ -18,9 +18,9 @@ export type SerializedValue =
undefined | boolean | number | string | undefined | boolean | number | string |
{ v: 'null' | 'undefined' | 'NaN' | 'Infinity' | '-Infinity' | '-0' } | { v: 'null' | 'undefined' | 'NaN' | 'Infinity' | '-Infinity' | '-0' } |
{ d: string } | { d: string } |
{ r: [string, string] } | { r: { p: string, f: string} } |
{ a: SerializedValue[] } | { a: SerializedValue[] } |
{ o: { [key: string]: SerializedValue } } | { o: { k: string, v: SerializedValue }[] } |
{ h: number }; { h: number };
function isRegExp(obj: any): obj is RegExp { function isRegExp(obj: any): obj is RegExp {
@ -36,9 +36,9 @@ function isError(obj: any): obj is Error {
} }
export function parseEvaluationResultValue(value: SerializedValue, handles: any[] = []): any { export function parseEvaluationResultValue(value: SerializedValue, handles: any[] = []): any {
if (value === undefined) if (Object.is(value, undefined))
return undefined; return undefined;
if (typeof value === 'object') { if (typeof value === 'object' && value) {
if ('v' in value) { if ('v' in value) {
if (value.v === 'undefined') if (value.v === 'undefined')
return undefined; return undefined;
@ -52,17 +52,18 @@ export function parseEvaluationResultValue(value: SerializedValue, handles: any[
return -Infinity; return -Infinity;
if (value.v === '-0') if (value.v === '-0')
return -0; return -0;
return undefined;
} }
if ('d' in value) if ('d' in value)
return new Date(value.d); return new Date(value.d);
if ('r' in value) if ('r' in value)
return new RegExp(value.r[0], value.r[1]); return new RegExp(value.r.p, value.r.f);
if ('a' in value) if ('a' in value)
return value.a.map((a: any) => parseEvaluationResultValue(a, handles)); return value.a.map((a: any) => parseEvaluationResultValue(a, handles));
if ('o' in value) { if ('o' in value) {
const result: any = {}; const result: any = {};
for (const name of Object.keys(value.o)) for (const { k, v } of value.o)
result[name] = parseEvaluationResultValue(value.o[name], handles); result[k] = parseEvaluationResultValue(v, handles);
return result; return result;
} }
if ('h' in value) if ('h' in value)
@ -72,12 +73,12 @@ export function parseEvaluationResultValue(value: SerializedValue, handles: any[
} }
export type HandleOrValue = { h: number } | { fallThrough: any }; export type HandleOrValue = { h: number } | { fallThrough: any };
export function serializeAsCallArgument(value: any, jsHandleSerializer: (value: any) => HandleOrValue): SerializedValue { export function serializeAsCallArgument(value: any, handleSerializer: (value: any) => HandleOrValue): SerializedValue {
return serialize(value, jsHandleSerializer, new Set()); return serialize(value, handleSerializer, new Set());
} }
function serialize(value: any, jsHandleSerializer: (value: any) => HandleOrValue, visited: Set<any>): SerializedValue { function serialize(value: any, handleSerializer: (value: any) => HandleOrValue, visited: Set<any>): SerializedValue {
const result = jsHandleSerializer(value); const result = handleSerializer(value);
if ('fallThrough' in result) if ('fallThrough' in result)
value = result.fallThrough; value = result.fallThrough;
else else
@ -111,26 +112,26 @@ function serialize(value: any, jsHandleSerializer: (value: any) => HandleOrValue
const error = value; const error = value;
if ('captureStackTrace' in global.Error) { if ('captureStackTrace' in global.Error) {
// v8 // v8
return error.stack; return error.stack || '';
} }
return `${error.name}: ${error.message}\n${error.stack}`; return `${error.name}: ${error.message}\n${error.stack}`;
} }
if (isDate(value)) if (isDate(value))
return { d: value.toJSON() }; return { d: value.toJSON() };
if (isRegExp(value)) if (isRegExp(value))
return { r: [ value.source, value.flags ] }; return { r: { p: value.source, f: value.flags } };
if (Array.isArray(value)) { if (Array.isArray(value)) {
const result = []; const a = [];
visited.add(value); visited.add(value);
for (let i = 0; i < value.length; ++i) for (let i = 0; i < value.length; ++i)
result.push(serialize(value[i], jsHandleSerializer, visited)); a.push(serialize(value[i], handleSerializer, visited));
visited.delete(value); visited.delete(value);
return { a: result }; return { a };
} }
if (typeof value === 'object') { if (typeof value === 'object') {
const result: any = {}; const o: { k: string, v: SerializedValue }[] = [];
visited.add(value); visited.add(value);
for (const name of Object.keys(value)) { for (const name of Object.keys(value)) {
let item; let item;
@ -140,11 +141,11 @@ function serialize(value: any, jsHandleSerializer: (value: any) => HandleOrValue
continue; // native bindings will throw sometimes continue; // native bindings will throw sometimes
} }
if (name === 'toJSON' && typeof item === 'function') if (name === 'toJSON' && typeof item === 'function')
result[name] = {}; o.push({ k: name, v: { o: [] } });
else else
result[name] = serialize(item, jsHandleSerializer, visited); o.push({ k: name, v: serialize(item, handleSerializer, visited) });
} }
visited.delete(value); visited.delete(value);
return { o: result }; return { o };
} }
} }

View file

@ -22,10 +22,10 @@ export default class UtilityScript {
return returnByValue ? this._promiseAwareJsonValueNoThrow(result) : result; return returnByValue ? this._promiseAwareJsonValueNoThrow(result) : result;
} }
callFunction(returnByValue: boolean, functionText: string, ...args: any[]) { callFunction(returnByValue: boolean, functionText: string, argCount: number, ...argsAndHandles: any[]) {
const argCount = args[0] as number; const args = argsAndHandles.slice(0, argCount);
const handles = args.slice(argCount + 1); const handles = argsAndHandles.slice(argCount);
const parameters = args.slice(1, argCount + 1).map(a => parseEvaluationResultValue(a, handles)); const parameters = args.map(a => parseEvaluationResultValue(a, handles));
const func = global.eval('(' + functionText + ')'); const func = global.eval('(' + functionText + ')');
const result = func(...parameters); const result = func(...parameters);
return returnByValue ? this._promiseAwareJsonValueNoThrow(result) : result; return returnByValue ? this._promiseAwareJsonValueNoThrow(result) : result;

View file

@ -367,8 +367,7 @@ export interface BindingCallChannel extends Channel {
export type BindingCallInitializer = { export type BindingCallInitializer = {
frame: FrameChannel, frame: FrameChannel,
name: string, name: string,
// TODO: migrate this to SerializedArgument. args: SerializedValue[],
args: any[]
}; };

View file

@ -32,7 +32,7 @@ import { ElementHandle } from './elementHandle';
import { Worker } from './worker'; import { Worker } from './worker';
import { Frame, FunctionWithSource, GotoOptions } from './frame'; import { Frame, FunctionWithSource, GotoOptions } from './frame';
import { Keyboard, Mouse } from './input'; import { Keyboard, Mouse } from './input';
import { Func1, FuncOn, SmartHandle, serializeArgument } from './jsHandle'; import { Func1, FuncOn, SmartHandle, serializeArgument, parseResult } from './jsHandle';
import { Request, Response, Route, RouteHandler } from './network'; import { Request, Response, Route, RouteHandler } from './network';
import { FileChooser } from './fileChooser'; import { FileChooser } from './fileChooser';
import { Buffer } from 'buffer'; import { Buffer } from 'buffer';
@ -557,7 +557,7 @@ export class BindingCall extends ChannelOwner<BindingCallChannel, BindingCallIni
page: frame._page!, page: frame._page!,
frame frame
}; };
const result = await func(source, ...this._initializer.args); const result = await func(source, ...this._initializer.args.map(parseResult));
this._channel.resolve({ result: serializeArgument(result) }); this._channel.resolve({ result: serializeArgument(result) });
} catch (e) { } catch (e) {
this._channel.reject({ error: serializeError(e) }); this._channel.reject({ error: serializeError(e) });

View file

@ -20,7 +20,7 @@ import { ElectronApplicationChannel, ElectronApplicationInitializer, PageChannel
import { BrowserContextDispatcher } from './browserContextDispatcher'; import { BrowserContextDispatcher } from './browserContextDispatcher';
import { BrowserContextBase } from '../../browserContext'; import { BrowserContextBase } from '../../browserContext';
import { PageDispatcher } from './pageDispatcher'; import { PageDispatcher } from './pageDispatcher';
import { parseArgument } from './jsHandleDispatcher'; import { parseArgument, serializeResult } from './jsHandleDispatcher';
import { createHandle } from './elementHandlerDispatcher'; import { createHandle } from './elementHandlerDispatcher';
import { SerializedValue } from '../../common/utilityScriptSerializers'; import { SerializedValue } from '../../common/utilityScriptSerializers';
@ -57,7 +57,7 @@ export class ElectronApplicationDispatcher extends Dispatcher<ElectronApplicatio
async evaluateExpression(params: { expression: string, isFunction: boolean, arg: SerializedArgument }): Promise<{ value: SerializedValue }> { async evaluateExpression(params: { expression: string, isFunction: boolean, arg: SerializedArgument }): Promise<{ value: SerializedValue }> {
const handle = this._object._nodeElectronHandle!; const handle = this._object._nodeElectronHandle!;
return { value: await handle._evaluateExpression(params.expression, params.isFunction, true /* returnByValue */, parseArgument(params.arg)) }; return { value: serializeResult(await handle._evaluateExpression(params.expression, params.isFunction, true /* returnByValue */, parseArgument(params.arg))) };
} }
async evaluateExpressionHandle(params: { expression: string, isFunction: boolean, arg: SerializedArgument }): Promise<{ handle: JSHandleChannel }> { async evaluateExpressionHandle(params: { expression: string, isFunction: boolean, arg: SerializedArgument }): Promise<{ handle: JSHandleChannel }> {

View file

@ -251,7 +251,7 @@ export class BindingCallDispatcher extends Dispatcher<{}, BindingCallInitializer
super(scope, {}, 'bindingCall', { super(scope, {}, 'bindingCall', {
frame: lookupDispatcher<FrameDispatcher>(source.frame), frame: lookupDispatcher<FrameDispatcher>(source.frame),
name, name,
args args: args.map(serializeResult),
}); });
this._promise = new Promise((resolve, reject) => { this._promise = new Promise((resolve, reject) => {
this._resolve = resolve; this._resolve = resolve;

View file

@ -17,7 +17,7 @@
const utils = require('./utils'); const utils = require('./utils');
const path = require('path'); const path = require('path');
const {FFOX, CHROMIUM, WEBKIT, USES_HOOKS, CHANNEL} = testOptions; const {FFOX, CHROMIUM, WEBKIT, USES_HOOKS} = testOptions;
describe('Page.evaluate', function() { describe('Page.evaluate', function() {
it('should work', async({page, server}) => { it('should work', async({page, server}) => {
@ -373,7 +373,8 @@ describe('Page.evaluate', function() {
}); });
expect(result).toBe(undefined); expect(result).toBe(undefined);
}); });
it.skip(CHANNEL)('should transfer 100Mb of data from page to node.js', async({page}) => { it.fail(USES_HOOKS)('should transfer 100Mb of data from page to node.js', async({page}) => {
// This does not use hooks, but is slow in wire channel.
const a = await page.evaluate(() => Array(100 * 1024 * 1024 + 1).join('a')); const a = await page.evaluate(() => Array(100 * 1024 * 1024 + 1).join('a'));
expect(a.length).toBe(100 * 1024 * 1024); expect(a.length).toBe(100 * 1024 * 1024);
}); });