Skip to content

Commit fc68614

Browse files
authored
feat(rpc): merge DispatcherScope and Dispatcher (#2918)
1 parent ebb4c33 commit fc68614

File tree

8 files changed

+135
-121
lines changed

8 files changed

+135
-121
lines changed

src/rpc/server.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,4 +25,4 @@ transport.onmessage = message => dispatcherConnection.dispatch(message);
2525
dispatcherConnection.onmessage = message => transport.send(message);
2626

2727
const playwright = new Playwright(__dirname, require('../../browsers.json')['browsers']);
28-
new PlaywrightDispatcher(dispatcherConnection.rootScope(), playwright);
28+
new PlaywrightDispatcher(dispatcherConnection.rootDispatcher(), playwright);

src/rpc/server/browserContextDispatcher.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ export class BrowserContextDispatcher extends Dispatcher<BrowserContext, Browser
4848
context.on(Events.BrowserContext.Page, page => this._dispatchEvent('page', new PageDispatcher(this._scope, page)));
4949
context.on(Events.BrowserContext.Close, () => {
5050
this._dispatchEvent('close');
51-
this._scope.dispose();
51+
this._dispose();
5252
});
5353
}
5454

src/rpc/server/browserDispatcher.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ export class BrowserDispatcher extends Dispatcher<Browser, BrowserInitializer> i
3030
super(scope, browser, 'browser', {}, true);
3131
browser.on(Events.Browser.Disconnected, () => {
3232
this._dispatchEvent('close');
33-
this._scope.dispose();
33+
this._dispose();
3434
});
3535
}
3636

src/rpc/server/cdpSessionDispatcher.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ export class CDPSessionDispatcher extends Dispatcher<CRSession, CDPSessionInitia
2424
crSession._eventListener = (method, params) => this._dispatchEvent('event', { method, params });
2525
crSession.on(CRSessionEvents.Disconnected, () => {
2626
this._dispatchEvent('disconnected');
27-
this._scope.dispose();
27+
this._dispose();
2828
});
2929
}
3030

src/rpc/server/dispatcher.ts

Lines changed: 57 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -36,99 +36,95 @@ export function lookupNullableDispatcher<DispatcherType>(object: any | null): Di
3636
}
3737

3838
export class Dispatcher<Type, Initializer> extends EventEmitter implements Channel {
39+
private _connection: DispatcherConnection;
40+
private _isScope: boolean;
41+
// Parent is always "isScope".
42+
private _parent: Dispatcher<any, any> | undefined;
43+
// Only "isScope" channel owners have registered dispatchers inside.
44+
private _dispatchers = new Map<string, Dispatcher<any, any>>();
45+
3946
readonly _guid: string;
4047
readonly _type: string;
41-
protected _scope: DispatcherScope;
48+
readonly _scope: Dispatcher<any, any>;
4249
_object: Type;
4350

44-
constructor(scope: DispatcherScope, object: Type, type: string, initializer: Initializer, isScope?: boolean, guid = type + '@' + helper.guid()) {
51+
constructor(parent: Dispatcher<any, any> | DispatcherConnection, object: Type, type: string, initializer: Initializer, isScope?: boolean, guid = type + '@' + helper.guid()) {
4552
super();
53+
54+
this._connection = parent instanceof DispatcherConnection ? parent : parent._connection;
55+
this._isScope = !!isScope;
56+
this._parent = parent instanceof DispatcherConnection ? undefined : parent;
57+
this._scope = isScope ? this : this._parent!;
58+
59+
assert(!this._connection._dispatchers.has(guid));
60+
this._connection._dispatchers.set(guid, this);
61+
if (this._parent) {
62+
assert(!this._parent._dispatchers.has(guid));
63+
this._parent._dispatchers.set(guid, this);
64+
}
65+
4666
this._type = type;
4767
this._guid = guid;
4868
this._object = object;
49-
this._scope = isScope ? scope.createChild(guid) : scope;
50-
scope.bind(this._guid, this);
69+
5170
(object as any)[dispatcherSymbol] = this;
52-
this._scope.sendMessageToClient(scope.guid, '__create__', { type, initializer, guid });
71+
if (this._parent)
72+
this._connection.sendMessageToClient(this._parent._guid, '__create__', { type, initializer, guid });
5373
}
5474

5575
_dispatchEvent(method: string, params: Dispatcher<any, any> | any = {}) {
56-
this._scope.sendMessageToClient(this._guid, method, params);
57-
}
58-
}
59-
60-
export class DispatcherScope {
61-
private _connection: DispatcherConnection;
62-
private _dispatchers = new Map<string, Dispatcher<any, any>>();
63-
private _parent: DispatcherScope | undefined;
64-
readonly _children = new Set<DispatcherScope>();
65-
readonly guid: string;
66-
67-
constructor(connection: DispatcherConnection, guid: string, parent?: DispatcherScope) {
68-
this._connection = connection;
69-
this._parent = parent;
70-
this.guid = guid;
71-
if (parent)
72-
parent._children.add(this);
73-
}
74-
75-
createChild(guid: string): DispatcherScope {
76-
return new DispatcherScope(this._connection, guid, this);
77-
}
78-
79-
bind(guid: string, arg: Dispatcher<any, any>) {
80-
assert(!this._dispatchers.has(guid));
81-
this._dispatchers.set(guid, arg);
82-
this._connection._dispatchers.set(guid, arg);
76+
this._connection.sendMessageToClient(this._guid, method, params);
8377
}
8478

85-
dispose() {
86-
// Take care of hierarchy.
87-
for (const child of [...this._children])
88-
child.dispose();
89-
this._children.clear();
79+
_dispose() {
80+
assert(this._isScope);
9081

91-
// Delete self from scopes and objects.
92-
this._connection._dispatchers.delete(this.guid);
82+
// Clean up from parent and connection.
83+
if (this._parent)
84+
this._parent._dispatchers.delete(this._guid);
85+
this._connection._dispatchers.delete(this._guid);
9386

94-
// Delete all of the objects from connection.
95-
for (const guid of this._dispatchers.keys())
96-
this._connection._dispatchers.delete(guid);
97-
98-
if (this._parent) {
99-
this._parent._children.delete(this);
100-
this._parent._dispatchers.delete(this.guid);
87+
// Dispose all children.
88+
for (const [guid, dispatcher] of [...this._dispatchers]) {
89+
if (dispatcher._isScope)
90+
dispatcher._dispose();
91+
else
92+
this._connection._dispatchers.delete(guid);
10193
}
94+
this._dispatchers.clear();
10295
}
10396

104-
async sendMessageToClient(guid: string, method: string, params: any): Promise<any> {
105-
this._connection._sendMessageToClient(guid, method, params);
97+
_debugScopeState(): any {
98+
return {
99+
_guid: this._guid,
100+
objects: this._isScope ? Array.from(this._dispatchers.values()).map(o => o._debugScopeState()) : undefined,
101+
};
106102
}
103+
}
104+
105+
export type DispatcherScope = Dispatcher<any, any>;
107106

108-
_dumpScopeState(scopes: any[]): any {
109-
const scopeState: any = { _guid: this.guid };
110-
scopeState.objects = [...this._dispatchers.keys()];
111-
scopes.push(scopeState);
112-
[...this._children].map(c => c._dumpScopeState(scopes));
113-
return scopeState;
107+
class Root extends Dispatcher<{}, {}> {
108+
constructor(connection: DispatcherConnection) {
109+
super(connection, {}, '', {}, true, '');
114110
}
115111
}
116112

117113
export class DispatcherConnection {
118114
readonly _dispatchers = new Map<string, Dispatcher<any, any>>();
119-
private _rootScope: DispatcherScope;
115+
private _rootDispatcher: Root;
120116
onmessage = (message: string) => {};
121117

122-
async _sendMessageToClient(guid: string, method: string, params: any): Promise<any> {
118+
async sendMessageToClient(guid: string, method: string, params: any): Promise<any> {
123119
this.onmessage(JSON.stringify({ guid, method, params: this._replaceDispatchersWithGuids(params) }));
124120
}
125121

126122
constructor() {
127-
this._rootScope = new DispatcherScope(this, '');
123+
this._rootDispatcher = new Root(this);
128124
}
129125

130-
rootScope(): DispatcherScope {
131-
return this._rootScope;
126+
rootDispatcher(): Dispatcher<any, any> {
127+
return this._rootDispatcher;
132128
}
133129

134130
async dispatch(message: string) {
@@ -140,11 +136,7 @@ export class DispatcherConnection {
140136
return;
141137
}
142138
if (method === 'debugScopeState') {
143-
const dispatcherState: any = {};
144-
dispatcherState.objects = [...this._dispatchers.keys()];
145-
dispatcherState.scopes = [];
146-
this._rootScope._dumpScopeState(dispatcherState.scopes);
147-
this.onmessage(JSON.stringify({ id, result: dispatcherState }));
139+
this.onmessage(JSON.stringify({ id, result: this._rootDispatcher._debugScopeState() }));
148140
return;
149141
}
150142
try {
@@ -155,7 +147,7 @@ export class DispatcherConnection {
155147
}
156148
}
157149

158-
_replaceDispatchersWithGuids(payload: any): any {
150+
private _replaceDispatchersWithGuids(payload: any): any {
159151
if (!payload)
160152
return payload;
161153
if (payload instanceof Dispatcher)

test/channels.spec.js

Lines changed: 68 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,13 @@ describe.skip(!CHANNEL)('Channels', function() {
2424

2525
it('should scope context handles', async({browser, server}) => {
2626
const GOLDEN_PRECONDITION = {
27-
objects: [ 'chromium', 'firefox', 'webkit', 'playwright', 'browser' ],
28-
scopes: [
29-
{ _guid: '', objects: [ 'chromium', 'firefox', 'webkit', 'playwright', 'browser' ] },
30-
{ _guid: 'browser', objects: [] }
27+
_guid: '',
28+
objects: [
29+
{ _guid: 'chromium' },
30+
{ _guid: 'firefox' },
31+
{ _guid: 'webkit' },
32+
{ _guid: 'playwright' },
33+
{ _guid: 'browser', objects: [] },
3134
]
3235
};
3336
await expectScopeState(browser, GOLDEN_PRECONDITION);
@@ -36,11 +39,20 @@ describe.skip(!CHANNEL)('Channels', function() {
3639
const page = await context.newPage();
3740
await page.goto(server.EMPTY_PAGE);
3841
await expectScopeState(browser, {
39-
objects: [ 'chromium', 'firefox', 'webkit', 'playwright', 'browser', 'context', 'frame', 'page', 'request', 'response' ],
40-
scopes: [
41-
{ _guid: '', objects: [ 'chromium', 'firefox', 'webkit', 'playwright', 'browser' ] },
42-
{ _guid: 'browser', objects: ['context'] },
43-
{ _guid: 'context', objects: ['frame', 'page', 'request', 'response'] }
42+
_guid: '',
43+
objects: [
44+
{ _guid: 'chromium' },
45+
{ _guid: 'firefox' },
46+
{ _guid: 'webkit' },
47+
{ _guid: 'playwright' },
48+
{ _guid: 'browser', objects: [
49+
{ _guid: 'context', objects: [
50+
{ _guid: 'frame' },
51+
{ _guid: 'page' },
52+
{ _guid: 'request' },
53+
{ _guid: 'response' },
54+
]},
55+
] },
4456
]
4557
});
4658

@@ -50,21 +62,28 @@ describe.skip(!CHANNEL)('Channels', function() {
5062

5163
it.skip(!CHROMIUM)('should scope CDPSession handles', async({browserType, browser, server}) => {
5264
const GOLDEN_PRECONDITION = {
53-
objects: [ 'chromium', 'firefox', 'webkit', 'playwright', 'browser' ],
54-
scopes: [
55-
{ _guid: '', objects: [ 'chromium', 'firefox', 'webkit', 'playwright', 'browser' ] },
56-
{ _guid: 'browser', objects: [] }
65+
_guid: '',
66+
objects: [
67+
{ _guid: 'chromium' },
68+
{ _guid: 'firefox' },
69+
{ _guid: 'webkit' },
70+
{ _guid: 'playwright' },
71+
{ _guid: 'browser', objects: [] },
5772
]
5873
};
5974
await expectScopeState(browserType, GOLDEN_PRECONDITION);
6075

6176
const session = await browser.newBrowserCDPSession();
6277
await expectScopeState(browserType, {
63-
objects: [ 'chromium', 'firefox', 'webkit', 'playwright', 'browser', 'cdpSession' ],
64-
scopes: [
65-
{ _guid: '', objects: [ 'chromium', 'firefox', 'webkit', 'playwright', 'browser' ] },
66-
{ _guid: 'browser', objects: ['cdpSession'] },
67-
{ _guid: 'cdpSession', objects: [] },
78+
_guid: '',
79+
objects: [
80+
{ _guid: 'chromium' },
81+
{ _guid: 'firefox' },
82+
{ _guid: 'webkit' },
83+
{ _guid: 'playwright' },
84+
{ _guid: 'browser', objects: [
85+
{ _guid: 'cdpSession', objects: [] },
86+
] },
6887
]
6988
});
7089

@@ -74,23 +93,30 @@ describe.skip(!CHANNEL)('Channels', function() {
7493

7594
it('should scope browser handles', async({browserType, defaultBrowserOptions}) => {
7695
const GOLDEN_PRECONDITION = {
77-
objects: [ 'chromium', 'firefox', 'webkit', 'playwright', 'browser' ],
78-
scopes: [
79-
{ _guid: '', objects: [ 'chromium', 'firefox', 'webkit', 'playwright', 'browser' ] },
80-
{ _guid: 'browser', objects: [] }
96+
_guid: '',
97+
objects: [
98+
{ _guid: 'chromium' },
99+
{ _guid: 'firefox' },
100+
{ _guid: 'webkit' },
101+
{ _guid: 'playwright' },
102+
{ _guid: 'browser', objects: [] },
81103
]
82104
};
83105
await expectScopeState(browserType, GOLDEN_PRECONDITION);
84106

85107
const browser = await browserType.launch(defaultBrowserOptions);
86108
await browser.newContext();
87109
await expectScopeState(browserType, {
88-
objects: [ 'chromium', 'firefox', 'webkit', 'playwright', 'browser', 'browser', 'context' ],
89-
scopes: [
90-
{ _guid: '', objects: [ 'chromium', 'firefox', 'webkit', 'playwright', 'browser', 'browser' ] },
110+
_guid: '',
111+
objects: [
112+
{ _guid: 'chromium' },
113+
{ _guid: 'firefox' },
114+
{ _guid: 'webkit' },
115+
{ _guid: 'playwright' },
116+
{ _guid: 'browser', objects: [
117+
{ _guid: 'context', objects: [] },
118+
] },
91119
{ _guid: 'browser', objects: [] },
92-
{ _guid: 'browser', objects: ['context'] },
93-
{ _guid: 'context', objects: [] },
94120
]
95121
});
96122

@@ -100,15 +126,28 @@ describe.skip(!CHANNEL)('Channels', function() {
100126
});
101127

102128
async function expectScopeState(object, golden) {
129+
golden = trimGuids(golden);
103130
const remoteState = trimGuids(await object._channel.debugScopeState());
104131
const localState = trimGuids(object._connection._debugScopeState());
105-
expect(processLocalState(localState)).toEqual(golden);
132+
expect(localState).toEqual(golden);
106133
expect(remoteState).toEqual(golden);
107134
}
108135

136+
function compareObjects(a, b) {
137+
if (a._guid !== b._guid)
138+
return a._guid.localeCompare(b._guid);
139+
if (a.objects && !b.objects)
140+
return -1;
141+
if (!a.objects && b.objects)
142+
return 1;
143+
if (!a.objects && !b.objects)
144+
return 0;
145+
return a.objects.length - b.objects.length;
146+
}
147+
109148
function trimGuids(object) {
110149
if (Array.isArray(object))
111-
return object.map(trimGuids);
150+
return object.map(trimGuids).sort(compareObjects);
112151
if (typeof object === 'object') {
113152
const result = {};
114153
for (const key in object)
@@ -119,21 +158,3 @@ function trimGuids(object) {
119158
return object ? object.match(/[^@]+/)[0] : '';
120159
return object;
121160
}
122-
123-
function processLocalState(root) {
124-
const objects = [];
125-
const scopes = [];
126-
const visit = (object, scope) => {
127-
if (object._guid !== '')
128-
objects.push(object._guid);
129-
scope.push(object._guid);
130-
if (object.objects) {
131-
scope = [];
132-
scopes.push({ _guid: object._guid, objects: scope });
133-
for (const child of object.objects)
134-
visit(child, scope);
135-
}
136-
};
137-
visit(root, []);
138-
return { objects, scopes };
139-
}

0 commit comments

Comments
 (0)