chore: unregister handlers when dispatcher goes (#15425)

This commit is contained in:
Pavel Feldman 2022-07-06 19:32:31 -08:00 committed by GitHub
parent b9e41df0a5
commit f71b620de7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 45 additions and 37 deletions

View file

@ -56,8 +56,8 @@ export class AndroidDeviceDispatcher extends Dispatcher<AndroidDevice, channels.
}, true); }, true);
for (const webView of device.webViews()) for (const webView of device.webViews())
this._dispatchEvent('webViewAdded', { webView }); this._dispatchEvent('webViewAdded', { webView });
device.on(AndroidDevice.Events.WebViewAdded, webView => this._dispatchEvent('webViewAdded', { webView })); this.addObjectListener(AndroidDevice.Events.WebViewAdded, webView => this._dispatchEvent('webViewAdded', { webView }));
device.on(AndroidDevice.Events.WebViewRemoved, socketName => this._dispatchEvent('webViewRemoved', { socketName })); this.addObjectListener(AndroidDevice.Events.WebViewRemoved, socketName => this._dispatchEvent('webViewRemoved', { socketName }));
} }
async wait(params: channels.AndroidDeviceWaitParams) { async wait(params: channels.AndroidDeviceWaitParams) {
@ -179,8 +179,8 @@ export class AndroidSocketDispatcher extends Dispatcher<SocketBackend, channels.
constructor(scope: DispatcherScope, socket: SocketBackend) { constructor(scope: DispatcherScope, socket: SocketBackend) {
super(scope, socket, 'AndroidSocket', {}, true); super(scope, socket, 'AndroidSocket', {}, true);
socket.on('data', (data: Buffer) => this._dispatchEvent('data', { data })); this.addObjectListener('data', (data: Buffer) => this._dispatchEvent('data', { data }));
socket.on('close', () => { this.addObjectListener('close', () => {
this._dispatchEvent('close'); this._dispatchEvent('close');
this._dispose(); this._dispose();
}); });

View file

@ -55,7 +55,7 @@ export class BrowserContextDispatcher extends Dispatcher<BrowserContext, channel
const artifactDispatcher = new ArtifactDispatcher(scope, artifact); const artifactDispatcher = new ArtifactDispatcher(scope, artifact);
this._dispatchEvent('video', { artifact: artifactDispatcher }); this._dispatchEvent('video', { artifact: artifactDispatcher });
}; };
context.on(BrowserContext.Events.VideoStarted, onVideo); this.addObjectListener(BrowserContext.Events.VideoStarted, onVideo);
for (const video of context._browser._idToVideo.values()) { for (const video of context._browser._idToVideo.values()) {
if (video.context === context) if (video.context === context)
onVideo(video.artifact); onVideo(video.artifact);
@ -63,8 +63,8 @@ export class BrowserContextDispatcher extends Dispatcher<BrowserContext, channel
for (const page of context.pages()) for (const page of context.pages())
this._dispatchEvent('page', { page: new PageDispatcher(this._scope, page) }); this._dispatchEvent('page', { page: new PageDispatcher(this._scope, page) });
context.on(BrowserContext.Events.Page, page => this._dispatchEvent('page', { page: new PageDispatcher(this._scope, page) })); this.addObjectListener(BrowserContext.Events.Page, page => this._dispatchEvent('page', { page: new PageDispatcher(this._scope, page) }));
context.on(BrowserContext.Events.Close, () => { this.addObjectListener(BrowserContext.Events.Close, () => {
this._dispatchEvent('close'); this._dispatchEvent('close');
this._dispose(); this._dispose();
}); });
@ -72,28 +72,28 @@ export class BrowserContextDispatcher extends Dispatcher<BrowserContext, channel
if (context._browser.options.name === 'chromium') { if (context._browser.options.name === 'chromium') {
for (const page of (context as CRBrowserContext).backgroundPages()) for (const page of (context as CRBrowserContext).backgroundPages())
this._dispatchEvent('backgroundPage', { page: new PageDispatcher(this._scope, page) }); this._dispatchEvent('backgroundPage', { page: new PageDispatcher(this._scope, page) });
context.on(CRBrowserContext.CREvents.BackgroundPage, page => this._dispatchEvent('backgroundPage', { page: new PageDispatcher(this._scope, page) })); this.addObjectListener(CRBrowserContext.CREvents.BackgroundPage, page => this._dispatchEvent('backgroundPage', { page: new PageDispatcher(this._scope, page) }));
for (const serviceWorker of (context as CRBrowserContext).serviceWorkers()) for (const serviceWorker of (context as CRBrowserContext).serviceWorkers())
this._dispatchEvent('serviceWorker', { worker: new WorkerDispatcher(this._scope, serviceWorker) }); this._dispatchEvent('serviceWorker', { worker: new WorkerDispatcher(this._scope, serviceWorker) });
context.on(CRBrowserContext.CREvents.ServiceWorker, serviceWorker => this._dispatchEvent('serviceWorker', { worker: new WorkerDispatcher(this._scope, serviceWorker) })); this.addObjectListener(CRBrowserContext.CREvents.ServiceWorker, serviceWorker => this._dispatchEvent('serviceWorker', { worker: new WorkerDispatcher(this._scope, serviceWorker) }));
} }
context.on(BrowserContext.Events.Request, (request: Request) => { this.addObjectListener(BrowserContext.Events.Request, (request: Request) => {
return this._dispatchEvent('request', { return this._dispatchEvent('request', {
request: RequestDispatcher.from(this._scope, request), request: RequestDispatcher.from(this._scope, request),
page: PageDispatcher.fromNullable(this._scope, request.frame()?._page.initializedOrUndefined()) page: PageDispatcher.fromNullable(this._scope, request.frame()?._page.initializedOrUndefined())
}); });
}); });
context.on(BrowserContext.Events.Response, (response: Response) => this._dispatchEvent('response', { this.addObjectListener(BrowserContext.Events.Response, (response: Response) => this._dispatchEvent('response', {
response: ResponseDispatcher.from(this._scope, response), response: ResponseDispatcher.from(this._scope, response),
page: PageDispatcher.fromNullable(this._scope, response.frame()?._page.initializedOrUndefined()) page: PageDispatcher.fromNullable(this._scope, response.frame()?._page.initializedOrUndefined())
})); }));
context.on(BrowserContext.Events.RequestFailed, (request: Request) => this._dispatchEvent('requestFailed', { this.addObjectListener(BrowserContext.Events.RequestFailed, (request: Request) => this._dispatchEvent('requestFailed', {
request: RequestDispatcher.from(this._scope, request), request: RequestDispatcher.from(this._scope, request),
failureText: request._failureText || undefined, failureText: request._failureText || undefined,
responseEndTiming: request._responseEndTiming, responseEndTiming: request._responseEndTiming,
page: PageDispatcher.fromNullable(this._scope, request.frame()?._page.initializedOrUndefined()) page: PageDispatcher.fromNullable(this._scope, request.frame()?._page.initializedOrUndefined())
})); }));
context.on(BrowserContext.Events.RequestFinished, ({ request, response }: { request: Request, response: Response | null }) => this._dispatchEvent('requestFinished', { this.addObjectListener(BrowserContext.Events.RequestFinished, ({ request, response }: { request: Request, response: Response | null }) => this._dispatchEvent('requestFinished', {
request: RequestDispatcher.from(scope, request), request: RequestDispatcher.from(scope, request),
response: ResponseDispatcher.fromNullable(scope, response), response: ResponseDispatcher.fromNullable(scope, response),
responseEndTiming: request._responseEndTiming, responseEndTiming: request._responseEndTiming,

View file

@ -31,7 +31,7 @@ export class BrowserDispatcher extends Dispatcher<Browser, channels.BrowserChann
_type_Browser = true; _type_Browser = true;
constructor(scope: DispatcherScope, browser: Browser) { constructor(scope: DispatcherScope, browser: Browser) {
super(scope, browser, 'Browser', { version: browser.version(), name: browser.options.name }, true); super(scope, browser, 'Browser', { version: browser.version(), name: browser.options.name }, true);
browser.on(Browser.Events.Disconnected, () => this._didClose()); this.addObjectListener(Browser.Events.Disconnected, () => this._didClose());
} }
_didClose() { _didClose() {

View file

@ -28,7 +28,7 @@ export class CDPSessionDispatcher extends Dispatcher<CRSession, channels.CDPSess
crSession._eventListener = (method, params) => { crSession._eventListener = (method, params) => {
this._dispatchEvent('event', { method, params }); this._dispatchEvent('event', { method, params });
}; };
crSession.on(CRSessionEvents.Disconnected, () => this._dispose()); this.addObjectListener(CRSessionEvents.Disconnected, () => this._dispose());
} }
async send(params: channels.CDPSessionSendParams): Promise<channels.CDPSessionSendResult> { async send(params: channels.CDPSessionSendParams): Promise<channels.CDPSessionSendResult> {

View file

@ -24,6 +24,8 @@ import type { CallMetadata } from '../instrumentation';
import { SdkObject } from '../instrumentation'; import { SdkObject } from '../instrumentation';
import { rewriteErrorMessage } from '../../utils/stackTrace'; import { rewriteErrorMessage } from '../../utils/stackTrace';
import type { PlaywrightDispatcher } from './playwrightDispatcher'; import type { PlaywrightDispatcher } from './playwrightDispatcher';
import { eventsHelper } from '../..//utils/eventsHelper';
import type { RegisteredListener } from '../..//utils/eventsHelper';
export const dispatcherSymbol = Symbol('dispatcher'); export const dispatcherSymbol = Symbol('dispatcher');
const metadataValidator = createMetadataValidator(); const metadataValidator = createMetadataValidator();
@ -50,6 +52,7 @@ export class Dispatcher<Type extends { guid: string }, ChannelType> extends Even
// Only "isScope" channel owners have registered dispatchers inside. // Only "isScope" channel owners have registered dispatchers inside.
private _dispatchers = new Map<string, Dispatcher<any, any>>(); private _dispatchers = new Map<string, Dispatcher<any, any>>();
protected _disposed = false; protected _disposed = false;
protected _eventListeners: RegisteredListener[] = [];
readonly _guid: string; readonly _guid: string;
readonly _type: string; readonly _type: string;
@ -81,6 +84,10 @@ export class Dispatcher<Type extends { guid: string }, ChannelType> extends Even
this._connection.sendCreate(this._parent, type, guid, initializer, this._parent._object); this._connection.sendCreate(this._parent, type, guid, initializer, this._parent._object);
} }
addObjectListener(eventName: (string | symbol), handler: (...args: any[]) => void) {
this._eventListeners.push(eventsHelper.addEventListener(this._object as unknown as EventEmitter, eventName, handler));
}
_dispatchEvent<T extends keyof channels.EventsTraits<ChannelType>>(method: T, params?: channels.EventsTraits<ChannelType>[T]) { _dispatchEvent<T extends keyof channels.EventsTraits<ChannelType>>(method: T, params?: channels.EventsTraits<ChannelType>[T]) {
if (this._disposed) { if (this._disposed) {
if (isUnderTest()) if (isUnderTest())
@ -95,6 +102,7 @@ export class Dispatcher<Type extends { guid: string }, ChannelType> extends Even
protected _dispose() { protected _dispose() {
assert(!this._disposed); assert(!this._disposed);
this._disposed = true; this._disposed = true;
eventsHelper.removeEventListeners(this._eventListeners);
// Clean up from parent and connection. // Clean up from parent and connection.
if (this._parent) if (this._parent)

View file

@ -44,7 +44,7 @@ export class ElectronApplicationDispatcher extends Dispatcher<ElectronApplicatio
super(scope, electronApplication, 'ElectronApplication', { super(scope, electronApplication, 'ElectronApplication', {
context: new BrowserContextDispatcher(scope, electronApplication.context()) context: new BrowserContextDispatcher(scope, electronApplication.context())
}, true); }, true);
electronApplication.on(ElectronApplication.Events.Close, () => { this.addObjectListener(ElectronApplication.Events.Close, () => {
this._dispatchEvent('close'); this._dispatchEvent('close');
this._dispose(); this._dispose();
}); });

View file

@ -51,13 +51,13 @@ export class FrameDispatcher extends Dispatcher<Frame, channels.FrameChannel> im
loadStates: Array.from(frame._subtreeLifecycleEvents), loadStates: Array.from(frame._subtreeLifecycleEvents),
}); });
this._frame = frame; this._frame = frame;
frame.on(Frame.Events.AddLifecycle, lifecycleEvent => { this.addObjectListener(Frame.Events.AddLifecycle, lifecycleEvent => {
this._dispatchEvent('loadstate', { add: lifecycleEvent }); this._dispatchEvent('loadstate', { add: lifecycleEvent });
}); });
frame.on(Frame.Events.RemoveLifecycle, lifecycleEvent => { this.addObjectListener(Frame.Events.RemoveLifecycle, lifecycleEvent => {
this._dispatchEvent('loadstate', { remove: lifecycleEvent }); this._dispatchEvent('loadstate', { remove: lifecycleEvent });
}); });
frame.on(Frame.Events.InternalNavigation, (event: NavigationEvent) => { this.addObjectListener(Frame.Events.InternalNavigation, (event: NavigationEvent) => {
if (!event.isPublic) if (!event.isPublic)
return; return;
const params = { url: event.url, name: event.name, error: event.error ? event.error.message : undefined }; const params = { url: event.url, name: event.name, error: event.error ? event.error.message : undefined };

View file

@ -153,10 +153,10 @@ export class WebSocketDispatcher extends Dispatcher<WebSocket, channels.WebSocke
super(scope, webSocket, 'WebSocket', { super(scope, webSocket, 'WebSocket', {
url: webSocket.url(), url: webSocket.url(),
}); });
webSocket.on(WebSocket.Events.FrameSent, (event: { opcode: number, data: string }) => this._dispatchEvent('frameSent', event)); this.addObjectListener(WebSocket.Events.FrameSent, (event: { opcode: number, data: string }) => this._dispatchEvent('frameSent', event));
webSocket.on(WebSocket.Events.FrameReceived, (event: { opcode: number, data: string }) => this._dispatchEvent('frameReceived', event)); this.addObjectListener(WebSocket.Events.FrameReceived, (event: { opcode: number, data: string }) => this._dispatchEvent('frameReceived', event));
webSocket.on(WebSocket.Events.SocketError, (error: string) => this._dispatchEvent('socketError', { error })); this.addObjectListener(WebSocket.Events.SocketError, (error: string) => this._dispatchEvent('socketError', { error }));
webSocket.on(WebSocket.Events.Close, () => this._dispatchEvent('close', {})); this.addObjectListener(WebSocket.Events.Close, () => this._dispatchEvent('close', {}));
} }
} }

View file

@ -59,26 +59,26 @@ export class PageDispatcher extends Dispatcher<Page, channels.PageChannel> imple
opener: PageDispatcher.fromNullable(scope, page.opener()) opener: PageDispatcher.fromNullable(scope, page.opener())
}, true); }, true);
this._page = page; this._page = page;
page.on(Page.Events.Close, () => { this.addObjectListener(Page.Events.Close, () => {
this._dispatchEvent('close'); this._dispatchEvent('close');
this._dispose(); this._dispose();
}); });
page.on(Page.Events.Console, message => this._dispatchEvent('console', { message: new ConsoleMessageDispatcher(this._scope, message) })); this.addObjectListener(Page.Events.Console, message => this._dispatchEvent('console', { message: new ConsoleMessageDispatcher(this._scope, message) }));
page.on(Page.Events.Crash, () => this._dispatchEvent('crash')); this.addObjectListener(Page.Events.Crash, () => this._dispatchEvent('crash'));
page.on(Page.Events.Dialog, dialog => this._dispatchEvent('dialog', { dialog: new DialogDispatcher(this._scope, dialog) })); this.addObjectListener(Page.Events.Dialog, dialog => this._dispatchEvent('dialog', { dialog: new DialogDispatcher(this._scope, dialog) }));
page.on(Page.Events.Download, (download: Download) => { this.addObjectListener(Page.Events.Download, (download: Download) => {
this._dispatchEvent('download', { url: download.url, suggestedFilename: download.suggestedFilename(), artifact: new ArtifactDispatcher(scope, download.artifact) }); this._dispatchEvent('download', { url: download.url, suggestedFilename: download.suggestedFilename(), artifact: new ArtifactDispatcher(scope, download.artifact) });
}); });
this._page.on(Page.Events.FileChooser, (fileChooser: FileChooser) => this._dispatchEvent('fileChooser', { this.addObjectListener(Page.Events.FileChooser, (fileChooser: FileChooser) => this._dispatchEvent('fileChooser', {
element: ElementHandleDispatcher.from(this._scope, fileChooser.element()), element: ElementHandleDispatcher.from(this._scope, fileChooser.element()),
isMultiple: fileChooser.isMultiple() isMultiple: fileChooser.isMultiple()
})); }));
page.on(Page.Events.FrameAttached, frame => this._onFrameAttached(frame)); this.addObjectListener(Page.Events.FrameAttached, frame => this._onFrameAttached(frame));
page.on(Page.Events.FrameDetached, frame => this._onFrameDetached(frame)); this.addObjectListener(Page.Events.FrameDetached, frame => this._onFrameDetached(frame));
page.on(Page.Events.PageError, error => this._dispatchEvent('pageError', { error: serializeError(error) })); this.addObjectListener(Page.Events.PageError, error => this._dispatchEvent('pageError', { error: serializeError(error) }));
page.on(Page.Events.WebSocket, webSocket => this._dispatchEvent('webSocket', { webSocket: new WebSocketDispatcher(this._scope, webSocket) })); this.addObjectListener(Page.Events.WebSocket, webSocket => this._dispatchEvent('webSocket', { webSocket: new WebSocketDispatcher(this._scope, webSocket) }));
page.on(Page.Events.Worker, worker => this._dispatchEvent('worker', { worker: new WorkerDispatcher(this._scope, worker) })); this.addObjectListener(Page.Events.Worker, worker => this._dispatchEvent('worker', { worker: new WorkerDispatcher(this._scope, worker) }));
page.on(Page.Events.Video, (artifact: Artifact) => this._dispatchEvent('video', { artifact: existingDispatcher<ArtifactDispatcher>(artifact) })); this.addObjectListener(Page.Events.Video, (artifact: Artifact) => this._dispatchEvent('video', { artifact: existingDispatcher<ArtifactDispatcher>(artifact) }));
if (page._video) if (page._video)
this._dispatchEvent('video', { artifact: existingDispatcher<ArtifactDispatcher>(page._video) }); this._dispatchEvent('video', { artifact: existingDispatcher<ArtifactDispatcher>(page._video) });
// Ensure client knows about all frames. // Ensure client knows about all frames.
@ -299,7 +299,7 @@ export class WorkerDispatcher extends Dispatcher<Worker, channels.WorkerChannel>
super(scope, worker, 'Worker', { super(scope, worker, 'Worker', {
url: worker.url() url: worker.url()
}); });
worker.on(Worker.Events.Close, () => this._dispatchEvent('close')); this.addObjectListener(Worker.Events.Close, () => this._dispatchEvent('close'));
} }
async evaluateExpression(params: channels.WorkerEvaluateExpressionParams, metadata: CallMetadata): Promise<channels.WorkerEvaluateExpressionResult> { async evaluateExpression(params: channels.WorkerEvaluateExpressionParams, metadata: CallMetadata): Promise<channels.WorkerEvaluateExpressionResult> {

View file

@ -30,7 +30,7 @@ export class TracingDispatcher extends Dispatcher<Tracing, channels.TracingChann
constructor(scope: DispatcherScope, tracing: Tracing) { constructor(scope: DispatcherScope, tracing: Tracing) {
super(scope, tracing, 'Tracing', {}, true); super(scope, tracing, 'Tracing', {}, true);
tracing.on(Tracing.Events.Dispose, () => this._dispose()); this.addObjectListener(Tracing.Events.Dispose, () => this._dispose());
} }
async tracingStart(params: channels.TracingTracingStartParams): Promise<channels.TracingTracingStartResult> { async tracingStart(params: channels.TracingTracingStartParams): Promise<channels.TracingTracingStartResult> {