mirror of
https://github.com/puppeteer/puppeteer
synced 2024-06-14 14:02:48 +00:00
chore: use internal event emitter for trusted events (#11898)
This commit is contained in:
parent
5b6456a7ca
commit
928d14ac84
@ -8,6 +8,7 @@ import type {ChildProcess} from 'child_process';
|
||||
|
||||
import type * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js';
|
||||
|
||||
import type {BrowserEvents} from '../api/Browser.js';
|
||||
import {
|
||||
Browser,
|
||||
BrowserEvent,
|
||||
@ -19,8 +20,10 @@ import {BrowserContextEvent} from '../api/BrowserContext.js';
|
||||
import type {Page} from '../api/Page.js';
|
||||
import type {Target} from '../api/Target.js';
|
||||
import {UnsupportedOperation} from '../common/Errors.js';
|
||||
import {EventEmitter} from '../common/EventEmitter.js';
|
||||
import {debugError} from '../common/util.js';
|
||||
import type {Viewport} from '../common/Viewport.js';
|
||||
import {bubble} from '../util/decorators.js';
|
||||
|
||||
import {BidiBrowserContext} from './BrowserContext.js';
|
||||
import type {BidiConnection} from './Connection.js';
|
||||
@ -85,6 +88,9 @@ export class BidiBrowser extends Browser {
|
||||
return browser;
|
||||
}
|
||||
|
||||
@bubble()
|
||||
accessor #trustedEmitter = new EventEmitter<BrowserEvents>();
|
||||
|
||||
#process?: ChildProcess;
|
||||
#closeCallback?: BrowserCloseCallback;
|
||||
#browserCore: BrowserCore;
|
||||
@ -107,7 +113,8 @@ export class BidiBrowser extends Browser {
|
||||
}
|
||||
|
||||
this.#browserCore.once('disconnected', () => {
|
||||
this.emit(BrowserEvent.Disconnected, undefined);
|
||||
this.#trustedEmitter.emit(BrowserEvent.Disconnected, undefined);
|
||||
this.#trustedEmitter.removeAllListeners();
|
||||
});
|
||||
this.#process?.once('close', () => {
|
||||
this.#browserCore.dispose('Browser process exited.', true);
|
||||
@ -136,15 +143,24 @@ export class BidiBrowser extends Browser {
|
||||
});
|
||||
this.#browserContexts.set(userContext, browserContext);
|
||||
|
||||
browserContext.on(BrowserContextEvent.TargetCreated, target => {
|
||||
this.emit(BrowserEvent.TargetCreated, target);
|
||||
});
|
||||
browserContext.on(BrowserContextEvent.TargetChanged, target => {
|
||||
this.emit(BrowserEvent.TargetChanged, target);
|
||||
});
|
||||
browserContext.on(BrowserContextEvent.TargetDestroyed, target => {
|
||||
this.emit(BrowserEvent.TargetDestroyed, target);
|
||||
});
|
||||
browserContext.trustedEmitter.on(
|
||||
BrowserContextEvent.TargetCreated,
|
||||
target => {
|
||||
this.#trustedEmitter.emit(BrowserEvent.TargetCreated, target);
|
||||
}
|
||||
);
|
||||
browserContext.trustedEmitter.on(
|
||||
BrowserContextEvent.TargetChanged,
|
||||
target => {
|
||||
this.#trustedEmitter.emit(BrowserEvent.TargetChanged, target);
|
||||
}
|
||||
);
|
||||
browserContext.trustedEmitter.on(
|
||||
BrowserContextEvent.TargetDestroyed,
|
||||
target => {
|
||||
this.#trustedEmitter.emit(BrowserEvent.TargetDestroyed, target);
|
||||
}
|
||||
);
|
||||
|
||||
return browserContext;
|
||||
}
|
||||
|
@ -6,20 +6,22 @@
|
||||
|
||||
import * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js';
|
||||
|
||||
import type {BrowserContextEvents} from '../api/BrowserContext.js';
|
||||
import {BrowserContext, BrowserContextEvent} from '../api/BrowserContext.js';
|
||||
import {PageEvent, type Page} from '../api/Page.js';
|
||||
import type {Target} from '../api/Target.js';
|
||||
import {UnsupportedOperation} from '../common/Errors.js';
|
||||
import {EventEmitter} from '../common/EventEmitter.js';
|
||||
import {debugError} from '../common/util.js';
|
||||
import type {Viewport} from '../common/Viewport.js';
|
||||
import {bubble} from '../util/decorators.js';
|
||||
|
||||
import type {BidiBrowser} from './Browser.js';
|
||||
import type {BrowsingContext} from './core/BrowsingContext.js';
|
||||
import {UserContext} from './core/UserContext.js';
|
||||
import type {BidiFrame} from './Frame.js';
|
||||
import {BidiPage} from './Page.js';
|
||||
import {BidiPageTarget} from './Target.js';
|
||||
import {BidiFrameTarget} from './Target.js';
|
||||
import {BidiFrameTarget, BidiPageTarget} from './Target.js';
|
||||
|
||||
/**
|
||||
* @internal
|
||||
@ -42,6 +44,9 @@ export class BidiBrowserContext extends BrowserContext {
|
||||
return context;
|
||||
}
|
||||
|
||||
@bubble()
|
||||
accessor trustedEmitter = new EventEmitter<BrowserContextEvents>();
|
||||
|
||||
readonly #browser: BidiBrowser;
|
||||
readonly #defaultViewport: Viewport | null;
|
||||
// This is public because of cookies.
|
||||
@ -52,7 +57,7 @@ export class BidiBrowserContext extends BrowserContext {
|
||||
[BidiPageTarget, Map<BidiFrame, BidiFrameTarget>]
|
||||
>();
|
||||
|
||||
constructor(
|
||||
private constructor(
|
||||
browser: BidiBrowser,
|
||||
userContext: UserContext,
|
||||
options: BidiBrowserContextOptions
|
||||
@ -72,12 +77,15 @@ export class BidiBrowserContext extends BrowserContext {
|
||||
this.userContext.on('browsingcontext', ({browsingContext}) => {
|
||||
this.#createPage(browsingContext);
|
||||
});
|
||||
this.userContext.on('closed', () => {
|
||||
this.trustedEmitter.removeAllListeners();
|
||||
});
|
||||
}
|
||||
|
||||
#createPage(browsingContext: BrowsingContext): BidiPage {
|
||||
const page = BidiPage.from(this, browsingContext);
|
||||
this.#pages.set(browsingContext, page);
|
||||
page.on(PageEvent.Close, () => {
|
||||
page.trustedEmitter.on(PageEvent.Close, () => {
|
||||
this.#pages.delete(browsingContext);
|
||||
});
|
||||
|
||||
@ -85,36 +93,36 @@ export class BidiBrowserContext extends BrowserContext {
|
||||
const pageTarget = new BidiPageTarget(page);
|
||||
const frameTargets = new Map();
|
||||
this.#targets.set(page, [pageTarget, frameTargets]);
|
||||
page.on(PageEvent.FrameAttached, frame => {
|
||||
page.trustedEmitter.on(PageEvent.FrameAttached, frame => {
|
||||
const bidiFrame = frame as BidiFrame;
|
||||
const target = new BidiFrameTarget(bidiFrame);
|
||||
frameTargets.set(bidiFrame, target);
|
||||
this.emit(BrowserContextEvent.TargetCreated, target);
|
||||
this.trustedEmitter.emit(BrowserContextEvent.TargetCreated, target);
|
||||
});
|
||||
page.on(PageEvent.FrameNavigated, frame => {
|
||||
page.trustedEmitter.on(PageEvent.FrameNavigated, frame => {
|
||||
const bidiFrame = frame as BidiFrame;
|
||||
const target = frameTargets.get(bidiFrame);
|
||||
// If there is no target, then this is the page's frame.
|
||||
if (target === undefined) {
|
||||
this.emit(BrowserContextEvent.TargetChanged, pageTarget);
|
||||
this.trustedEmitter.emit(BrowserContextEvent.TargetChanged, pageTarget);
|
||||
} else {
|
||||
this.emit(BrowserContextEvent.TargetChanged, target);
|
||||
this.trustedEmitter.emit(BrowserContextEvent.TargetChanged, target);
|
||||
}
|
||||
});
|
||||
page.on(PageEvent.FrameDetached, frame => {
|
||||
page.trustedEmitter.on(PageEvent.FrameDetached, frame => {
|
||||
const bidiFrame = frame as BidiFrame;
|
||||
const target = frameTargets.get(bidiFrame);
|
||||
if (target === undefined) {
|
||||
return;
|
||||
}
|
||||
frameTargets.delete(bidiFrame);
|
||||
this.emit(BrowserContextEvent.TargetDestroyed, target);
|
||||
this.trustedEmitter.emit(BrowserContextEvent.TargetDestroyed, target);
|
||||
});
|
||||
page.on(PageEvent.Close, () => {
|
||||
page.trustedEmitter.on(PageEvent.Close, () => {
|
||||
this.#targets.delete(page);
|
||||
this.emit(BrowserContextEvent.TargetDestroyed, pageTarget);
|
||||
this.trustedEmitter.emit(BrowserContextEvent.TargetDestroyed, pageTarget);
|
||||
});
|
||||
this.emit(BrowserContextEvent.TargetCreated, pageTarget);
|
||||
this.trustedEmitter.emit(BrowserContextEvent.TargetCreated, pageTarget);
|
||||
// -- Target stuff ends here --
|
||||
|
||||
return page;
|
||||
|
@ -101,38 +101,40 @@ export class BidiFrame extends Frame {
|
||||
void session.detach().catch(debugError);
|
||||
}
|
||||
}
|
||||
this.page().emit(PageEvent.FrameDetached, this);
|
||||
this.removeAllListeners();
|
||||
this.page().trustedEmitter.emit(PageEvent.FrameDetached, this);
|
||||
});
|
||||
|
||||
this.browsingContext.on('request', ({request}) => {
|
||||
const httpRequest = BidiHTTPRequest.from(request, this);
|
||||
request.once('success', () => {
|
||||
// SAFETY: BidiHTTPRequest will create this before here.
|
||||
this.page().emit(PageEvent.RequestFinished, httpRequest);
|
||||
this.page().trustedEmitter.emit(PageEvent.RequestFinished, httpRequest);
|
||||
});
|
||||
|
||||
request.once('error', () => {
|
||||
this.page().emit(PageEvent.RequestFailed, httpRequest);
|
||||
this.page().trustedEmitter.emit(PageEvent.RequestFailed, httpRequest);
|
||||
});
|
||||
});
|
||||
|
||||
this.browsingContext.on('navigation', ({navigation}) => {
|
||||
navigation.once('fragment', () => {
|
||||
this.page().emit(PageEvent.FrameNavigated, this);
|
||||
this.page().trustedEmitter.emit(PageEvent.FrameNavigated, this);
|
||||
});
|
||||
});
|
||||
this.browsingContext.on('load', () => {
|
||||
this.page().emit(PageEvent.Load, undefined);
|
||||
this.page().trustedEmitter.emit(PageEvent.Load, undefined);
|
||||
});
|
||||
this.browsingContext.on('DOMContentLoaded', () => {
|
||||
this._hasStartedLoading = true;
|
||||
this.page().emit(PageEvent.DOMContentLoaded, undefined);
|
||||
this.page().emit(PageEvent.FrameNavigated, this);
|
||||
this.page().trustedEmitter.emit(PageEvent.DOMContentLoaded, undefined);
|
||||
this.page().trustedEmitter.emit(PageEvent.FrameNavigated, this);
|
||||
});
|
||||
|
||||
this.browsingContext.on('userprompt', ({userPrompt}) => {
|
||||
this.page().emit(PageEvent.Dialog, BidiDialog.from(userPrompt));
|
||||
this.page().trustedEmitter.emit(
|
||||
PageEvent.Dialog,
|
||||
BidiDialog.from(userPrompt)
|
||||
);
|
||||
});
|
||||
|
||||
this.browsingContext.on('log', ({entry}) => {
|
||||
@ -154,7 +156,7 @@ export class BidiFrame extends Frame {
|
||||
}, '')
|
||||
.slice(1);
|
||||
|
||||
this.page().emit(
|
||||
this.page().trustedEmitter.emit(
|
||||
PageEvent.Console,
|
||||
new ConsoleMessage(
|
||||
entry.method as any,
|
||||
@ -185,7 +187,7 @@ export class BidiFrame extends Frame {
|
||||
}
|
||||
|
||||
error.stack = [...messageLines, ...stackLines].join('\n');
|
||||
this.page().emit(PageEvent.PageError, error);
|
||||
this.page().trustedEmitter.emit(PageEvent.PageError, error);
|
||||
} else {
|
||||
debugError(
|
||||
`Unhandled LogEntry with type "${entry.type}", text "${entry.text}" and level "${entry.level}"`
|
||||
@ -197,7 +199,7 @@ export class BidiFrame extends Frame {
|
||||
#createFrameTarget(browsingContext: BrowsingContext) {
|
||||
const frame = BidiFrame.from(this, browsingContext);
|
||||
this.#frames.set(browsingContext, frame);
|
||||
this.page().emit(PageEvent.FrameAttached, frame);
|
||||
this.page().trustedEmitter.emit(PageEvent.FrameAttached, frame);
|
||||
|
||||
browsingContext.on('closed', () => {
|
||||
this.#frames.delete(browsingContext);
|
||||
|
@ -60,7 +60,7 @@ export class BidiHTTPRequest extends HTTPRequest {
|
||||
this.#response = BidiHTTPResponse.from(data, this);
|
||||
});
|
||||
|
||||
this.#frame?.page().emit(PageEvent.Request, this);
|
||||
this.#frame?.page().trustedEmitter.emit(PageEvent.Request, this);
|
||||
}
|
||||
|
||||
override url(): string {
|
||||
|
@ -40,7 +40,7 @@ export class BidiHTTPResponse extends HTTPResponse {
|
||||
}
|
||||
|
||||
#initialize() {
|
||||
this.#request.frame()?.page().emit(PageEvent.Response, this);
|
||||
this.#request.frame()?.page().trustedEmitter.emit(PageEvent.Response, this);
|
||||
}
|
||||
|
||||
@invokeAtMostOnceForArguments
|
||||
|
@ -12,7 +12,11 @@ import type {CDPSession} from '../api/CDPSession.js';
|
||||
import type {BoundingBox} from '../api/ElementHandle.js';
|
||||
import type {WaitForOptions} from '../api/Frame.js';
|
||||
import type {HTTPResponse} from '../api/HTTPResponse.js';
|
||||
import type {MediaFeature, GeolocationOptions} from '../api/Page.js';
|
||||
import type {
|
||||
MediaFeature,
|
||||
GeolocationOptions,
|
||||
PageEvents,
|
||||
} from '../api/Page.js';
|
||||
import {
|
||||
Page,
|
||||
PageEvent,
|
||||
@ -25,11 +29,13 @@ import {EmulationManager} from '../cdp/EmulationManager.js';
|
||||
import {Tracing} from '../cdp/Tracing.js';
|
||||
import type {Cookie, CookieParam, CookieSameSite} from '../common/Cookie.js';
|
||||
import {UnsupportedOperation} from '../common/Errors.js';
|
||||
import {EventEmitter} from '../common/EventEmitter.js';
|
||||
import type {PDFOptions} from '../common/PDFOptions.js';
|
||||
import type {Awaitable} from '../common/types.js';
|
||||
import {evaluationString, parsePDFOptions, timeout} from '../common/util.js';
|
||||
import type {Viewport} from '../common/Viewport.js';
|
||||
import {assert} from '../util/assert.js';
|
||||
import {bubble} from '../util/decorators.js';
|
||||
import {isErrorLike} from '../util/ErrorLike.js';
|
||||
|
||||
import type {BidiBrowser} from './Browser.js';
|
||||
@ -56,6 +62,9 @@ export class BidiPage extends Page {
|
||||
return page;
|
||||
}
|
||||
|
||||
@bubble()
|
||||
accessor trustedEmitter = new EventEmitter<PageEvents>();
|
||||
|
||||
readonly #browserContext: BidiBrowserContext;
|
||||
readonly #frame: BidiFrame;
|
||||
#viewport: Viewport | null = null;
|
||||
@ -91,8 +100,8 @@ export class BidiPage extends Page {
|
||||
|
||||
#initialize() {
|
||||
this.#frame.browsingContext.on('closed', () => {
|
||||
this.emit(PageEvent.Close, undefined);
|
||||
this.removeAllListeners();
|
||||
this.trustedEmitter.emit(PageEvent.Close, undefined);
|
||||
this.trustedEmitter.removeAllListeners();
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -157,7 +157,4 @@ export interface Connection<Events extends BidiEvents = BidiEvents>
|
||||
method: T,
|
||||
params: Commands[T]['params']
|
||||
): Promise<{result: Commands[T]['returnType']}>;
|
||||
|
||||
// This will pipe events into the provided emitter.
|
||||
pipeTo<Events extends BidiEvents>(emitter: EventEmitter<Events>): void;
|
||||
}
|
||||
|
@ -8,7 +8,11 @@ import type * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js';
|
||||
|
||||
import {EventEmitter} from '../../common/EventEmitter.js';
|
||||
import {debugError} from '../../common/util.js';
|
||||
import {inertIfDisposed, throwIfDisposed} from '../../util/decorators.js';
|
||||
import {
|
||||
bubble,
|
||||
inertIfDisposed,
|
||||
throwIfDisposed,
|
||||
} from '../../util/decorators.js';
|
||||
import {DisposableStack, disposeSymbol} from '../../util/disposable.js';
|
||||
|
||||
import {Browser} from './Browser.js';
|
||||
@ -81,7 +85,8 @@ export class Session
|
||||
readonly #disposables = new DisposableStack();
|
||||
readonly #info: Bidi.Session.NewResult;
|
||||
readonly browser!: Browser;
|
||||
readonly connection: Connection;
|
||||
@bubble()
|
||||
accessor connection: Connection;
|
||||
// keep-sorted end
|
||||
|
||||
private constructor(connection: Connection, info: Bidi.Session.NewResult) {
|
||||
@ -93,8 +98,6 @@ export class Session
|
||||
}
|
||||
|
||||
async #initialize(): Promise<void> {
|
||||
this.connection.pipeTo(this);
|
||||
|
||||
// SAFETY: We use `any` to allow assignment of the readonly property.
|
||||
(this as any).browser = await Browser.from(this);
|
||||
|
||||
@ -125,10 +128,6 @@ export class Session
|
||||
this[disposeSymbol]();
|
||||
}
|
||||
|
||||
pipeTo<Events extends BidiEvents>(emitter: EventEmitter<Events>): void {
|
||||
this.connection.pipeTo(emitter);
|
||||
}
|
||||
|
||||
/**
|
||||
* Currently, there is a 1:1 relationship between the session and the
|
||||
* session. In the future, we might support multiple sessions and in that
|
||||
|
@ -9,7 +9,9 @@ import {describe, it} from 'node:test';
|
||||
import expect from 'expect';
|
||||
import sinon from 'sinon';
|
||||
|
||||
import {invokeAtMostOnceForArguments} from './decorators.js';
|
||||
import {EventEmitter} from '../common/EventEmitter.js';
|
||||
|
||||
import {bubble, invokeAtMostOnceForArguments} from './decorators.js';
|
||||
|
||||
describe('decorators', function () {
|
||||
describe('invokeAtMostOnceForArguments', () => {
|
||||
@ -76,4 +78,48 @@ describe('decorators', function () {
|
||||
}).toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('bubble', () => {
|
||||
it('should work', () => {
|
||||
class Test extends EventEmitter<any> {
|
||||
@bubble()
|
||||
accessor field = new EventEmitter();
|
||||
}
|
||||
|
||||
const t = new Test();
|
||||
let a = false;
|
||||
t.on('a', (value: boolean) => {
|
||||
a = value;
|
||||
});
|
||||
|
||||
t.field.emit('a', true);
|
||||
expect(a).toBeTruthy();
|
||||
|
||||
// Set a new emitter.
|
||||
t.field = new EventEmitter();
|
||||
a = false;
|
||||
|
||||
t.field.emit('a', true);
|
||||
expect(a).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should not bubble down', () => {
|
||||
class Test extends EventEmitter<any> {
|
||||
@bubble()
|
||||
accessor field = new EventEmitter<any>();
|
||||
}
|
||||
|
||||
const t = new Test();
|
||||
let a = false;
|
||||
t.field.on('a', (value: boolean) => {
|
||||
a = value;
|
||||
});
|
||||
|
||||
t.emit('a', true);
|
||||
expect(a).toBeFalsy();
|
||||
|
||||
t.field.emit('a', true);
|
||||
expect(a).toBeTruthy();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -4,6 +4,8 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type {EventType} from '../common/EventEmitter.js';
|
||||
import type {EventEmitter} from '../common/EventEmitter.js';
|
||||
import type {Disposed, Moveable} from '../common/types.js';
|
||||
|
||||
import {asyncDisposeSymbol, disposeSymbol} from './disposable.js';
|
||||
@ -138,3 +140,67 @@ export function guarded<T extends object>(
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
const bubbleHandlers = new WeakMap<object, Map<any, any>>();
|
||||
|
||||
/**
|
||||
* Event emitter fields marked with `bubble` will have their events bubble up
|
||||
* the field owner.
|
||||
*/
|
||||
// The type is too complicated to type.
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
||||
export function bubble<T extends EventType[]>(events?: T) {
|
||||
return <This extends EventEmitter<any>, Value extends EventEmitter<any>>(
|
||||
{set, get}: ClassAccessorDecoratorTarget<This, Value>,
|
||||
context: ClassAccessorDecoratorContext<This, Value>
|
||||
): ClassAccessorDecoratorResult<This, Value> => {
|
||||
context.addInitializer(function () {
|
||||
const handlers = bubbleHandlers.get(this) ?? new Map();
|
||||
if (handlers.has(events)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const handler =
|
||||
events !== undefined
|
||||
? (type: EventType, event: unknown) => {
|
||||
if (events.includes(type)) {
|
||||
this.emit(type, event);
|
||||
}
|
||||
}
|
||||
: (type: EventType, event: unknown) => {
|
||||
this.emit(type, event);
|
||||
};
|
||||
|
||||
handlers.set(events, handler);
|
||||
bubbleHandlers.set(this, handlers);
|
||||
});
|
||||
return {
|
||||
set(emitter) {
|
||||
const handler = bubbleHandlers.get(this)!.get(events)!;
|
||||
|
||||
// In case we are re-setting.
|
||||
const oldEmitter = get.call(this);
|
||||
if (oldEmitter !== undefined) {
|
||||
oldEmitter.off('*', handler);
|
||||
}
|
||||
|
||||
if (emitter === undefined) {
|
||||
return;
|
||||
}
|
||||
emitter.on('*', handler);
|
||||
set.call(this, emitter);
|
||||
},
|
||||
// @ts-expect-error -- TypeScript incorrectly types init to require a
|
||||
// return.
|
||||
init(emitter) {
|
||||
if (emitter === undefined) {
|
||||
return;
|
||||
}
|
||||
const handler = bubbleHandlers.get(this)!.get(events)!;
|
||||
|
||||
emitter.on('*', handler);
|
||||
return emitter;
|
||||
},
|
||||
};
|
||||
};
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user