diff --git a/src/rpc/client/browser.ts b/src/rpc/client/browser.ts index e0ff406646619..0da524c2525fe 100644 --- a/src/rpc/client/browser.ts +++ b/src/rpc/client/browser.ts @@ -19,7 +19,6 @@ import { BrowserChannel, BrowserInitializer } from '../channels'; import { BrowserContext } from './browserContext'; import { Page } from './page'; import { ChannelOwner } from './channelOwner'; -import { ConnectionScope } from './connection'; import { Events } from '../../events'; import { CDPSession } from './cdpSession'; @@ -37,13 +36,13 @@ export class Browser extends ChannelOwner { return browser ? Browser.from(browser) : null; } - constructor(scope: ConnectionScope, guid: string, initializer: BrowserInitializer) { - super(scope, guid, initializer, true); + constructor(parent: ChannelOwner, guid: string, initializer: BrowserInitializer) { + super(parent, guid, initializer, true); this._channel.on('close', () => { this._isConnected = false; this.emit(Events.Browser.Disconnected); this._isClosedOrClosing = true; - this._scope.dispose(); + this._dispose(); }); this._closedPromise = new Promise(f => this.once(Events.Browser.Disconnected, f)); } diff --git a/src/rpc/client/browserContext.ts b/src/rpc/client/browserContext.ts index 13c3adfe1f63d..7b1b28ba61f32 100644 --- a/src/rpc/client/browserContext.ts +++ b/src/rpc/client/browserContext.ts @@ -23,7 +23,6 @@ import { BrowserContextChannel, BrowserContextInitializer } from '../channels'; import { ChannelOwner } from './channelOwner'; import { helper } from '../../helper'; import { Browser } from './browser'; -import { ConnectionScope } from './connection'; import { Events } from '../../events'; import { TimeoutSettings } from '../../timeoutSettings'; import { CDPSession } from './cdpSession'; @@ -51,8 +50,8 @@ export class BrowserContext extends ChannelOwner { const page = Page.from(p); this._pages.add(page); @@ -225,7 +224,7 @@ export class BrowserContext extends ChannelOwner { diff --git a/src/rpc/client/browserServer.ts b/src/rpc/client/browserServer.ts index 4c77f7ba43bcb..eacbbe852abd4 100644 --- a/src/rpc/client/browserServer.ts +++ b/src/rpc/client/browserServer.ts @@ -16,7 +16,6 @@ import { ChildProcess } from 'child_process'; import { BrowserServerChannel, BrowserServerInitializer } from '../channels'; -import { ConnectionScope } from './connection'; import { ChannelOwner } from './channelOwner'; import { Events } from '../../events'; @@ -25,8 +24,8 @@ export class BrowserServer extends ChannelOwner this.emit(Events.BrowserServer.Close)); } diff --git a/src/rpc/client/browserType.ts b/src/rpc/client/browserType.ts index 3405e86130ce3..07c5a16270036 100644 --- a/src/rpc/client/browserType.ts +++ b/src/rpc/client/browserType.ts @@ -19,7 +19,6 @@ import { BrowserTypeChannel, BrowserTypeInitializer } from '../channels'; import { Browser } from './browser'; import { BrowserContext } from './browserContext'; import { ChannelOwner } from './channelOwner'; -import { ConnectionScope } from './connection'; import { BrowserServer } from './browserServer'; export class BrowserType extends ChannelOwner { @@ -28,8 +27,8 @@ export class BrowserType extends ChannelOwner(event: T, listener: (payload: T extends symbol ? any : Protocol.Events[T extends keyof Protocol.Events ? T : never]) => void) => this; once: (event: T, listener: (payload: T extends symbol ? any : Protocol.Events[T extends keyof Protocol.Events ? T : never]) => void) => this; - constructor(scope: ConnectionScope, guid: string, initializer: CDPSessionInitializer) { - super(scope, guid, initializer, true); + constructor(parent: ChannelOwner, guid: string, initializer: CDPSessionInitializer) { + super(parent, guid, initializer, true); this._channel.on('event', ({ method, params }) => this.emit(method, params)); - this._channel.on('disconnected', () => this._scope.dispose()); + this._channel.on('disconnected', () => this._dispose()); this.on = super.on; this.addListener = super.addListener; diff --git a/src/rpc/client/channelOwner.ts b/src/rpc/client/channelOwner.ts index 5c5f3e151cc44..aba5155d55d37 100644 --- a/src/rpc/client/channelOwner.ts +++ b/src/rpc/client/channelOwner.ts @@ -16,18 +16,33 @@ import { EventEmitter } from 'events'; import { Channel } from '../channels'; -import { ConnectionScope } from './connection'; +import { Connection } from './connection'; +import { assert } from '../../helper'; -export abstract class ChannelOwner extends EventEmitter { +export abstract class ChannelOwner extends EventEmitter { + private _connection: Connection; + private _isScope: boolean; + // Parent is always "isScope". + private _parent: ChannelOwner | undefined; + // Only "isScope" channel owners have registered objects inside. + private _objects = new Map(); + + readonly _guid: string; readonly _channel: T; readonly _initializer: Initializer; - readonly _scope: ConnectionScope; - readonly guid: string; - constructor(scope: ConnectionScope, guid: string, initializer: Initializer, isScope?: boolean) { + constructor(parent: ChannelOwner | Connection, guid: string, initializer: Initializer, isScope?: boolean) { super(); - this.guid = guid; - this._scope = isScope ? scope.createChild(guid) : scope; + + this._connection = parent instanceof Connection ? parent : parent._connection; + this._guid = guid; + this._isScope = !!isScope; + this._parent = parent instanceof Connection ? undefined : parent; + + this._connection._objects.set(guid, this); + if (this._parent) + this._parent._objects.set(guid, this); + const base = new EventEmitter(); this._channel = new Proxy(base, { get: (obj: any, prop) => { @@ -45,10 +60,35 @@ export abstract class ChannelOwner extends Event return obj.addListener; if (prop === 'removeEventListener') return obj.removeListener; - return (params: any) => scope.sendMessageToServer({ guid, method: String(prop), params }); + return (params: any) => this._connection.sendMessageToServer({ guid, method: String(prop), params }); }, }); (this._channel as any)._object = this; this._initializer = initializer; } + + _dispose() { + assert(this._isScope); + + // Clean up from parent and connection. + if (this._parent) + this._parent._objects.delete(this._guid); + this._connection._objects.delete(this._guid); + + // Dispose all children. + for (const [guid, object] of [...this._objects]) { + if (object._isScope) + object._dispose(); + else + this._connection._objects.delete(guid); + } + this._objects.clear(); + } + + _debugScopeState(): any { + return { + _guid: this._guid, + objects: this._isScope ? Array.from(this._objects.values()).map(o => o._debugScopeState()) : undefined, + }; + } } diff --git a/src/rpc/client/connection.ts b/src/rpc/client/connection.ts index 2cd0e08bff8c3..578bc2b661431 100644 --- a/src/rpc/client/connection.ts +++ b/src/rpc/client/connection.ts @@ -32,18 +32,24 @@ import { parseError } from '../serializers'; import { BrowserServer } from './browserServer'; import { CDPSession } from './cdpSession'; import { Playwright } from './playwright'; +import { Channel } from '../channels'; + +class Root extends ChannelOwner { + constructor(connection: Connection) { + super(connection, '', {}, true); + } +} export class Connection { - readonly _objects = new Map>(); - readonly _waitingForObject = new Map(); + readonly _objects = new Map(); + private _waitingForObject = new Map(); onmessage = (message: string): void => {}; private _lastId = 0; private _callbacks = new Map void, reject: (a: Error) => void }>(); - readonly _scopes = new Map(); - private _rootScript: ConnectionScope; + private _rootObject: ChannelOwner; constructor() { - this._rootScript = this.createScope(''); + this._rootObject = new Root(this); } async waitForObjectWithKnownName(guid: string): Promise { @@ -61,13 +67,7 @@ export class Connection { } _debugScopeState(): any { - const scopeState: any = {}; - scopeState.objects = [...this._objects.keys()]; - scopeState.scopes = [...this._scopes.values()].map(scope => ({ - _guid: scope._guid, - objects: [...scope._objects.keys()] - })); - return scopeState; + return this._rootObject._debugScopeState(); } dispatch(message: string) { @@ -86,22 +86,20 @@ export class Connection { debug('pw:channel:event')(parsedMessage); if (method === '__create__') { - const scope = this._scopes.get(guid)!; - scope.createRemoteObject(params.type, params.guid, params.initializer); + this._createRemoteObject(guid, params.type, params.guid, params.initializer); return; } const object = this._objects.get(guid)!; object._channel.emit(method, this._replaceGuidsWithChannels(params)); } - private _replaceChannelsWithGuids(payload: any): any { if (!payload) return payload; if (Array.isArray(payload)) return payload.map(p => this._replaceChannelsWithGuids(p)); if (payload._object instanceof ChannelOwner) - return { guid: payload._object.guid }; + return { guid: payload._object._guid }; if (typeof payload === 'object') { const result: any = {}; for (const key of Object.keys(payload)) @@ -111,7 +109,7 @@ export class Connection { return payload; } - _replaceGuidsWithChannels(payload: any): any { + private _replaceGuidsWithChannels(payload: any): any { if (!payload) return payload; if (Array.isArray(payload)) @@ -127,125 +125,73 @@ export class Connection { return payload; } - createScope(guid: string): ConnectionScope { - const scope = new ConnectionScope(this, guid); - this._scopes.set(guid, scope); - return scope; - } -} - -export class ConnectionScope { - private _connection: Connection; - readonly _objects = new Map>(); - private _children = new Set(); - private _parent: ConnectionScope | undefined; - readonly _guid: string; - - constructor(connection: Connection, guid: string) { - this._connection = connection; - this._guid = guid; - } - - createChild(guid: string): ConnectionScope { - const scope = this._connection.createScope(guid); - this._children.add(scope); - scope._parent = this; - return scope; - } - - dispose() { - // Take care of hierarchy. - for (const child of [...this._children]) - child.dispose(); - this._children.clear(); - - // Delete self from scopes and objects. - this._connection._scopes.delete(this._guid); - this._connection._objects.delete(this._guid); - - // Delete all of the objects from connection. - for (const guid of this._objects.keys()) - this._connection._objects.delete(guid); - - // Clean up from parent. - if (this._parent) { - this._parent._objects.delete(this._guid); - this._parent._children.delete(this); - } - } - - async sendMessageToServer(message: { guid: string, method: string, params: any }): Promise { - return this._connection.sendMessageToServer(message); - } - - createRemoteObject(type: string, guid: string, initializer: any): any { + private _createRemoteObject(parentGuid: string, type: string, guid: string, initializer: any): any { + const parent = this._objects.get(parentGuid)!; let result: ChannelOwner; - initializer = this._connection._replaceGuidsWithChannels(initializer); + initializer = this._replaceGuidsWithChannels(initializer); switch (type) { case 'bindingCall': - result = new BindingCall(this, guid, initializer); + result = new BindingCall(parent, guid, initializer); break; case 'browser': - result = new Browser(this, guid, initializer); + result = new Browser(parent, guid, initializer); break; case 'browserServer': - result = new BrowserServer(this, guid, initializer); + result = new BrowserServer(parent, guid, initializer); break; case 'browserType': - result = new BrowserType(this, guid, initializer); + result = new BrowserType(parent, guid, initializer); break; case 'cdpSession': // Chromium-specific. - result = new CDPSession(this, guid, initializer); + result = new CDPSession(parent, guid, initializer); break; case 'context': - result = new BrowserContext(this, guid, initializer); + result = new BrowserContext(parent, guid, initializer); break; case 'consoleMessage': - result = new ConsoleMessage(this, guid, initializer); + result = new ConsoleMessage(parent, guid, initializer); break; case 'dialog': - result = new Dialog(this, guid, initializer); + result = new Dialog(parent, guid, initializer); break; case 'download': - result = new Download(this, guid, initializer); + result = new Download(parent, guid, initializer); break; case 'elementHandle': - result = new ElementHandle(this, guid, initializer); + result = new ElementHandle(parent, guid, initializer); break; case 'frame': - result = new Frame(this, guid, initializer); + result = new Frame(parent, guid, initializer); break; case 'jsHandle': - result = new JSHandle(this, guid, initializer); + result = new JSHandle(parent, guid, initializer); break; case 'page': - result = new Page(this, guid, initializer); + result = new Page(parent, guid, initializer); break; case 'playwright': - result = new Playwright(this, guid, initializer); + result = new Playwright(parent, guid, initializer); break; case 'request': - result = new Request(this, guid, initializer); + result = new Request(parent, guid, initializer); break; case 'response': - result = new Response(this, guid, initializer); + result = new Response(parent, guid, initializer); break; case 'route': - result = new Route(this, guid, initializer); + result = new Route(parent, guid, initializer); break; case 'worker': - result = new Worker(this, guid, initializer); + result = new Worker(parent, guid, initializer); break; default: throw new Error('Missing type ' + type); } - this._connection._objects.set(guid, result); - this._objects.set(guid, result); - const callback = this._connection._waitingForObject.get(guid); + const callback = this._waitingForObject.get(guid); if (callback) { callback(result); - this._connection._waitingForObject.delete(guid); + this._waitingForObject.delete(guid); } return result; } diff --git a/src/rpc/client/consoleMessage.ts b/src/rpc/client/consoleMessage.ts index 2397e00a7fc0d..82fa8433e3ce3 100644 --- a/src/rpc/client/consoleMessage.ts +++ b/src/rpc/client/consoleMessage.ts @@ -19,15 +19,14 @@ import { ConsoleMessageLocation } from '../../types'; import { JSHandle } from './jsHandle'; import { ConsoleMessageChannel, ConsoleMessageInitializer } from '../channels'; import { ChannelOwner } from './channelOwner'; -import { ConnectionScope } from './connection'; export class ConsoleMessage extends ChannelOwner { static from(message: ConsoleMessageChannel): ConsoleMessage { return (message as any)._object; } - constructor(scope: ConnectionScope, guid: string, initializer: ConsoleMessageInitializer) { - super(scope, guid, initializer); + constructor(parent: ChannelOwner, guid: string, initializer: ConsoleMessageInitializer) { + super(parent, guid, initializer); } type(): string { diff --git a/src/rpc/client/dialog.ts b/src/rpc/client/dialog.ts index 3bf39f6e2ca9c..3f6d9cc5c63da 100644 --- a/src/rpc/client/dialog.ts +++ b/src/rpc/client/dialog.ts @@ -15,7 +15,6 @@ */ import { DialogChannel, DialogInitializer } from '../channels'; -import { ConnectionScope } from './connection'; import { ChannelOwner } from './channelOwner'; export class Dialog extends ChannelOwner { @@ -23,8 +22,8 @@ export class Dialog extends ChannelOwner { return (dialog as any)._object; } - constructor(scope: ConnectionScope, guid: string, initializer: DialogInitializer) { - super(scope, guid, initializer); + constructor(parent: ChannelOwner, guid: string, initializer: DialogInitializer) { + super(parent, guid, initializer); } type(): string { diff --git a/src/rpc/client/download.ts b/src/rpc/client/download.ts index b5043f7ef4165..5c4434c59719a 100644 --- a/src/rpc/client/download.ts +++ b/src/rpc/client/download.ts @@ -16,7 +16,6 @@ import * as fs from 'fs'; import { DownloadChannel, DownloadInitializer } from '../channels'; -import { ConnectionScope } from './connection'; import { ChannelOwner } from './channelOwner'; import { Readable } from 'stream'; @@ -25,8 +24,8 @@ export class Download extends ChannelOwner return (download as any)._object; } - constructor(scope: ConnectionScope, guid: string, initializer: DownloadInitializer) { - super(scope, guid, initializer); + constructor(parent: ChannelOwner, guid: string, initializer: DownloadInitializer) { + super(parent, guid, initializer); } url(): string { diff --git a/src/rpc/client/elementHandle.ts b/src/rpc/client/elementHandle.ts index ad614f7e0d361..7b864c04c502f 100644 --- a/src/rpc/client/elementHandle.ts +++ b/src/rpc/client/elementHandle.ts @@ -18,7 +18,7 @@ import * as types from '../../types'; import { ElementHandleChannel, JSHandleInitializer } from '../channels'; import { Frame } from './frame'; import { FuncOn, JSHandle, serializeArgument, parseResult } from './jsHandle'; -import { ConnectionScope } from './connection'; +import { ChannelOwner } from './channelOwner'; export class ElementHandle extends JSHandle { readonly _elementChannel: ElementHandleChannel; @@ -31,8 +31,8 @@ export class ElementHandle extends JSHandle { return handle ? ElementHandle.from(handle) : null; } - constructor(scope: ConnectionScope, guid: string, initializer: JSHandleInitializer) { - super(scope, guid, initializer); + constructor(parent: ChannelOwner, guid: string, initializer: JSHandleInitializer) { + super(parent, guid, initializer); this._elementChannel = this._channel as ElementHandleChannel; } diff --git a/src/rpc/client/frame.ts b/src/rpc/client/frame.ts index 940fada31e092..e78cb0b09772a 100644 --- a/src/rpc/client/frame.ts +++ b/src/rpc/client/frame.ts @@ -25,7 +25,6 @@ import { JSHandle, Func1, FuncOn, SmartHandle, serializeArgument, parseResult } import * as network from './network'; import { Response } from './network'; import { Page } from './page'; -import { ConnectionScope } from './connection'; import { normalizeFilePayloads } from '../serializers'; export type GotoOptions = types.NavigateOptions & { @@ -50,8 +49,8 @@ export class Frame extends ChannelOwner { return frame ? Frame.from(frame) : null; } - constructor(scope: ConnectionScope, guid: string, initializer: FrameInitializer) { - super(scope, guid, initializer); + constructor(parent: ChannelOwner, guid: string, initializer: FrameInitializer) { + super(parent, guid, initializer); this._parentFrame = Frame.fromNullable(initializer.parentFrame); if (this._parentFrame) this._parentFrame._childFrames.add(this); diff --git a/src/rpc/client/jsHandle.ts b/src/rpc/client/jsHandle.ts index 1fb3d0c571127..7f75a607a8aad 100644 --- a/src/rpc/client/jsHandle.ts +++ b/src/rpc/client/jsHandle.ts @@ -17,7 +17,6 @@ import { JSHandleChannel, JSHandleInitializer } from '../channels'; import { ElementHandle } from './elementHandle'; import { ChannelOwner } from './channelOwner'; -import { ConnectionScope } from './connection'; import { serializeAsCallArgument, parseEvaluationResultValue } from '../../common/utilityScriptSerializers'; type NoHandles = Arg extends JSHandle ? never : (Arg extends object ? { [Key in keyof Arg]: NoHandles } : Arg); @@ -47,8 +46,8 @@ export class JSHandle extends ChannelOwner this._preview = preview); } @@ -103,7 +102,7 @@ export function serializeArgument(arg: any): any { }; const value = serializeAsCallArgument(arg, value => { if (value instanceof ChannelOwner) - return { h: pushHandle(value.guid) }; + return { h: pushHandle(value._guid) }; return { fallThrough: value }; }); return { value, guids }; diff --git a/src/rpc/client/network.ts b/src/rpc/client/network.ts index 99f44d2fa5a8d..e5d92c504da0f 100644 --- a/src/rpc/client/network.ts +++ b/src/rpc/client/network.ts @@ -19,7 +19,6 @@ import * as types from '../../types'; import { RequestChannel, ResponseChannel, RouteChannel, RequestInitializer, ResponseInitializer, RouteInitializer } from '../channels'; import { ChannelOwner } from './channelOwner'; import { Frame } from './frame'; -import { ConnectionScope } from './connection'; import { normalizeFulfillParameters } from '../serializers'; export type NetworkCookie = { @@ -58,8 +57,8 @@ export class Request extends ChannelOwner { return request ? Request.from(request) : null; } - constructor(scope: ConnectionScope, guid: string, initializer: RequestInitializer) { - super(scope, guid, initializer); + constructor(parent: ChannelOwner, guid: string, initializer: RequestInitializer) { + super(parent, guid, initializer); this._redirectedFrom = Request.fromNullable(initializer.redirectedFrom); if (this._redirectedFrom) this._redirectedFrom._redirectedTo = this; @@ -138,8 +137,8 @@ export class Route extends ChannelOwner { return (route as any)._object; } - constructor(scope: ConnectionScope, guid: string, initializer: RouteInitializer) { - super(scope, guid, initializer); + constructor(parent: ChannelOwner, guid: string, initializer: RouteInitializer) { + super(parent, guid, initializer); } request(): Request { @@ -171,8 +170,8 @@ export class Response extends ChannelOwner return response ? Response.from(response) : null; } - constructor(scope: ConnectionScope, guid: string, initializer: ResponseInitializer) { - super(scope, guid, initializer); + constructor(parent: ChannelOwner, guid: string, initializer: ResponseInitializer) { + super(parent, guid, initializer); } url(): string { diff --git a/src/rpc/client/page.ts b/src/rpc/client/page.ts index 6e9ea6fb71677..81dcc08588b9b 100644 --- a/src/rpc/client/page.ts +++ b/src/rpc/client/page.ts @@ -22,7 +22,6 @@ import { assert, assertMaxArguments, helper, Listener } from '../../helper'; import { TimeoutSettings } from '../../timeoutSettings'; import * as types from '../../types'; import { BindingCallChannel, BindingCallInitializer, PageChannel, PageInitializer } from '../channels'; -import { ConnectionScope } from './connection'; import { parseError, serializeError } from '../serializers'; import { Accessibility } from './accessibility'; import { BrowserContext } from './browserContext'; @@ -69,8 +68,8 @@ export class Page extends ChannelOwner { return page ? Page.from(page) : null; } - constructor(scope: ConnectionScope, guid: string, initializer: PageInitializer) { - super(scope, guid, initializer); + constructor(parent: ChannelOwner, guid: string, initializer: PageInitializer) { + super(parent, guid, initializer); this.accessibility = new Accessibility(this._channel); this.keyboard = new Keyboard(this._channel); this.mouse = new Mouse(this._channel); @@ -511,8 +510,8 @@ export class BindingCall extends ChannelOwner { chromium: BrowserType; @@ -26,8 +25,8 @@ export class Playwright extends ChannelOwner { return (worker as any)._object; } - constructor(scope: ConnectionScope, guid: string, initializer: WorkerInitializer) { - super(scope, guid, initializer); + constructor(parent: ChannelOwner, guid: string, initializer: WorkerInitializer) { + super(parent, guid, initializer); this._channel.on('close', () => { if (this._page) this._page._workers.delete(this); diff --git a/test/channels.spec.js b/test/channels.spec.js index 347df572d8557..23b8374cfedea 100644 --- a/test/channels.spec.js +++ b/test/channels.spec.js @@ -19,7 +19,7 @@ const { FFOX, CHROMIUM, WEBKIT, WIN, CHANNEL } = require('./utils').testOptions( describe.skip(!CHANNEL)('Channels', function() { it('should work', async({browser}) => { - expect(!!browser._channel).toBeTruthy(); + expect(!!browser._connection).toBeTruthy(); }); it('should scope context handles', async({browser, server}) => { @@ -101,8 +101,8 @@ describe.skip(!CHANNEL)('Channels', function() { async function expectScopeState(object, golden) { const remoteState = trimGuids(await object._channel.debugScopeState()); - const localState = trimGuids(object._scope._connection._debugScopeState()); - expect(localState).toEqual(golden); + const localState = trimGuids(object._connection._debugScopeState()); + expect(processLocalState(localState)).toEqual(golden); expect(remoteState).toEqual(golden); } @@ -119,3 +119,21 @@ function trimGuids(object) { return object ? object.match(/[^@]+/)[0] : ''; return object; } + +function processLocalState(root) { + const objects = []; + const scopes = []; + const visit = (object, scope) => { + if (object._guid !== '') + objects.push(object._guid); + scope.push(object._guid); + if (object.objects) { + scope = []; + scopes.push({ _guid: object._guid, objects: scope }); + for (const child of object.objects) + visit(child, scope); + } + }; + visit(root, []); + return { objects, scopes }; +}