mirror of
https://github.com/puppeteer/puppeteer
synced 2024-06-14 14:02:48 +00:00
chore: handle disposal of core/bidi
resources (#11730)
This commit is contained in:
parent
bc7bd01d85
commit
69e44fc808
@ -126,11 +126,11 @@ export class BidiBrowser extends Browser {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#initialize() {
|
#initialize() {
|
||||||
this.#browserCore.once('disconnect', () => {
|
this.#browserCore.once('disconnected', () => {
|
||||||
this.emit(BrowserEvent.Disconnected, undefined);
|
this.emit(BrowserEvent.Disconnected, undefined);
|
||||||
});
|
});
|
||||||
this.#process?.once('close', () => {
|
this.#process?.once('close', async () => {
|
||||||
this.#browserCore.dispose('Browser process closed.', true);
|
this.#browserCore.dispose('Browser process exited.', true);
|
||||||
this.connection.dispose();
|
this.connection.dispose();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -7,7 +7,8 @@
|
|||||||
import type * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js';
|
import type * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js';
|
||||||
|
|
||||||
import {EventEmitter} from '../../common/EventEmitter.js';
|
import {EventEmitter} from '../../common/EventEmitter.js';
|
||||||
import {throwIfDisposed} from '../../util/decorators.js';
|
import {inertIfDisposed, throwIfDisposed} from '../../util/decorators.js';
|
||||||
|
import {DisposableStack, disposeSymbol} from '../../util/disposable.js';
|
||||||
|
|
||||||
import type {BrowsingContext} from './BrowsingContext.js';
|
import type {BrowsingContext} from './BrowsingContext.js';
|
||||||
import type {SharedWorkerRealm} from './Realm.js';
|
import type {SharedWorkerRealm} from './Realm.js';
|
||||||
@ -28,13 +29,13 @@ export type AddPreloadScriptOptions = Omit<
|
|||||||
* @internal
|
* @internal
|
||||||
*/
|
*/
|
||||||
export class Browser extends EventEmitter<{
|
export class Browser extends EventEmitter<{
|
||||||
/** Emitted after the browser closes. */
|
/** Emitted before the browser closes. */
|
||||||
closed: {
|
closed: {
|
||||||
/** The reason for closing the browser. */
|
/** The reason for closing the browser. */
|
||||||
reason: string;
|
reason: string;
|
||||||
};
|
};
|
||||||
/** Emitted after the browser disconnects. */
|
/** Emitted after the browser disconnects. */
|
||||||
disconnect: {
|
disconnected: {
|
||||||
/** The reason for disconnecting the browser. */
|
/** The reason for disconnecting the browser. */
|
||||||
reason: string;
|
reason: string;
|
||||||
};
|
};
|
||||||
@ -51,14 +52,15 @@ export class Browser extends EventEmitter<{
|
|||||||
}
|
}
|
||||||
|
|
||||||
// keep-sorted start
|
// keep-sorted start
|
||||||
|
#closed = false;
|
||||||
#reason: string | undefined;
|
#reason: string | undefined;
|
||||||
|
readonly #disposables = new DisposableStack();
|
||||||
readonly #userContexts = new Map();
|
readonly #userContexts = new Map();
|
||||||
readonly session: Session;
|
readonly session: Session;
|
||||||
// keep-sorted end
|
// keep-sorted end
|
||||||
|
|
||||||
private constructor(session: Session) {
|
private constructor(session: Session) {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
// keep-sorted start
|
// keep-sorted start
|
||||||
this.session = session;
|
this.session = session;
|
||||||
// keep-sorted end
|
// keep-sorted end
|
||||||
@ -67,49 +69,44 @@ export class Browser extends EventEmitter<{
|
|||||||
}
|
}
|
||||||
|
|
||||||
async #initialize() {
|
async #initialize() {
|
||||||
// ///////////////////////
|
const sessionEmitter = this.#disposables.use(
|
||||||
// Session listeners //
|
new EventEmitter(this.session)
|
||||||
// ///////////////////////
|
);
|
||||||
const session = this.#session;
|
sessionEmitter.once('ended', ({reason}) => {
|
||||||
session.on('script.realmCreated', info => {
|
this.dispose(reason);
|
||||||
|
});
|
||||||
|
|
||||||
|
sessionEmitter.on('script.realmCreated', info => {
|
||||||
if (info.type === 'shared-worker') {
|
if (info.type === 'shared-worker') {
|
||||||
// TODO: Create a SharedWorkerRealm.
|
// TODO: Create a SharedWorkerRealm.
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// ///////////////////
|
await this.#syncBrowsingContexts();
|
||||||
// Parent listeners //
|
}
|
||||||
// ///////////////////
|
|
||||||
this.session.once('ended', ({reason}) => {
|
|
||||||
this.dispose(reason);
|
|
||||||
});
|
|
||||||
|
|
||||||
// //////////////////////////////
|
async #syncBrowsingContexts() {
|
||||||
// Asynchronous initialization //
|
|
||||||
// //////////////////////////////
|
|
||||||
// In case contexts are created or destroyed during `getTree`, we use this
|
// In case contexts are created or destroyed during `getTree`, we use this
|
||||||
// set to detect them.
|
// set to detect them.
|
||||||
const contextIds = new Set<string>();
|
const contextIds = new Set<string>();
|
||||||
const created = (info: {context: string}) => {
|
let contexts: Bidi.BrowsingContext.Info[];
|
||||||
|
|
||||||
|
{
|
||||||
|
using sessionEmitter = new EventEmitter(this.session);
|
||||||
|
sessionEmitter.on('browsingContext.contextCreated', info => {
|
||||||
contextIds.add(info.context);
|
contextIds.add(info.context);
|
||||||
};
|
});
|
||||||
const destroyed = (info: {context: string}) => {
|
sessionEmitter.on('browsingContext.contextDestroyed', info => {
|
||||||
contextIds.delete(info.context);
|
contextIds.delete(info.context);
|
||||||
};
|
});
|
||||||
session.on('browsingContext.contextCreated', created);
|
const {result} = await this.session.send('browsingContext.getTree', {});
|
||||||
session.on('browsingContext.contextDestroyed', destroyed);
|
contexts = result.contexts;
|
||||||
|
}
|
||||||
const {
|
|
||||||
result: {contexts},
|
|
||||||
} = await session.send('browsingContext.getTree', {});
|
|
||||||
|
|
||||||
session.off('browsingContext.contextDestroyed', destroyed);
|
|
||||||
session.off('browsingContext.contextCreated', created);
|
|
||||||
|
|
||||||
// Simulating events so contexts are created naturally.
|
// Simulating events so contexts are created naturally.
|
||||||
for (const info of contexts) {
|
for (const info of contexts) {
|
||||||
if (contextIds.has(info.context)) {
|
if (contextIds.has(info.context)) {
|
||||||
session.emit('browsingContext.contextCreated', info);
|
this.session.emit('browsingContext.contextCreated', info);
|
||||||
}
|
}
|
||||||
if (info.children) {
|
if (info.children) {
|
||||||
contexts.push(...info.children);
|
contexts.push(...info.children);
|
||||||
@ -117,48 +114,45 @@ export class Browser extends EventEmitter<{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
get #session() {
|
// keep-sorted start block=yes
|
||||||
return this.session;
|
get closed(): boolean {
|
||||||
|
return this.#closed;
|
||||||
}
|
}
|
||||||
|
|
||||||
get disposed(): boolean {
|
|
||||||
return this.#reason !== undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
get defaultUserContext(): UserContext {
|
get defaultUserContext(): UserContext {
|
||||||
// SAFETY: A UserContext is always created for the default context.
|
// SAFETY: A UserContext is always created for the default context.
|
||||||
return this.#userContexts.get('')!;
|
return this.#userContexts.get('')!;
|
||||||
}
|
}
|
||||||
|
get disconnected(): boolean {
|
||||||
|
return this.#reason !== undefined;
|
||||||
|
}
|
||||||
|
get disposed(): boolean {
|
||||||
|
return this.disconnected;
|
||||||
|
}
|
||||||
get userContexts(): Iterable<UserContext> {
|
get userContexts(): Iterable<UserContext> {
|
||||||
return this.#userContexts.values();
|
return this.#userContexts.values();
|
||||||
}
|
}
|
||||||
|
// keep-sorted end
|
||||||
|
|
||||||
dispose(reason?: string, close?: boolean): void {
|
@inertIfDisposed
|
||||||
if (this.disposed) {
|
dispose(reason?: string, closed = false): void {
|
||||||
return;
|
this.#closed = closed;
|
||||||
}
|
this.#reason = reason;
|
||||||
this.#reason = reason ?? `Browser was disposed.`;
|
this[disposeSymbol]();
|
||||||
if (close) {
|
|
||||||
this.emit('closed', {reason: this.#reason});
|
|
||||||
}
|
|
||||||
this.emit('disconnect', {reason: this.#reason});
|
|
||||||
this.removeAllListeners();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@throwIfDisposed((browser: Browser) => {
|
@throwIfDisposed<Browser>(browser => {
|
||||||
// SAFETY: By definition of `disposed`, `#reason` is defined.
|
// SAFETY: By definition of `disposed`, `#reason` is defined.
|
||||||
return browser.#reason!;
|
return browser.#reason!;
|
||||||
})
|
})
|
||||||
async close(): Promise<void> {
|
async close(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
await this.#session.send('browser.close', {});
|
await this.session.send('browser.close', {});
|
||||||
} finally {
|
} finally {
|
||||||
this.dispose(`Browser was closed.`, true);
|
this.dispose('Browser already closed.', true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@throwIfDisposed((browser: Browser) => {
|
@throwIfDisposed<Browser>(browser => {
|
||||||
// SAFETY: By definition of `disposed`, `#reason` is defined.
|
// SAFETY: By definition of `disposed`, `#reason` is defined.
|
||||||
return browser.#reason!;
|
return browser.#reason!;
|
||||||
})
|
})
|
||||||
@ -168,7 +162,7 @@ export class Browser extends EventEmitter<{
|
|||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
const {
|
const {
|
||||||
result: {script},
|
result: {script},
|
||||||
} = await this.#session.send('script.addPreloadScript', {
|
} = await this.session.send('script.addPreloadScript', {
|
||||||
functionDeclaration,
|
functionDeclaration,
|
||||||
...options,
|
...options,
|
||||||
contexts: options.contexts?.map(context => {
|
contexts: options.contexts?.map(context => {
|
||||||
@ -178,13 +172,25 @@ export class Browser extends EventEmitter<{
|
|||||||
return script;
|
return script;
|
||||||
}
|
}
|
||||||
|
|
||||||
@throwIfDisposed((browser: Browser) => {
|
@throwIfDisposed<Browser>(browser => {
|
||||||
// SAFETY: By definition of `disposed`, `#reason` is defined.
|
// SAFETY: By definition of `disposed`, `#reason` is defined.
|
||||||
return browser.#reason!;
|
return browser.#reason!;
|
||||||
})
|
})
|
||||||
async removePreloadScript(script: string): Promise<void> {
|
async removePreloadScript(script: string): Promise<void> {
|
||||||
await this.#session.send('script.removePreloadScript', {
|
await this.session.send('script.removePreloadScript', {
|
||||||
script,
|
script,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[disposeSymbol](): void {
|
||||||
|
this.#reason ??=
|
||||||
|
'Browser was disconnected, probably because the session ended.';
|
||||||
|
if (this.closed) {
|
||||||
|
this.emit('closed', {reason: this.#reason});
|
||||||
|
}
|
||||||
|
this.emit('disconnected', {reason: this.#reason});
|
||||||
|
|
||||||
|
this.#disposables.dispose();
|
||||||
|
super[disposeSymbol]();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,7 +7,8 @@
|
|||||||
import type * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js';
|
import type * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js';
|
||||||
|
|
||||||
import {EventEmitter} from '../../common/EventEmitter.js';
|
import {EventEmitter} from '../../common/EventEmitter.js';
|
||||||
import {throwIfDisposed} from '../../util/decorators.js';
|
import {inertIfDisposed, throwIfDisposed} from '../../util/decorators.js';
|
||||||
|
import {DisposableStack, disposeSymbol} from '../../util/disposable.js';
|
||||||
|
|
||||||
import type {AddPreloadScriptOptions} from './Browser.js';
|
import type {AddPreloadScriptOptions} from './Browser.js';
|
||||||
import {Navigation} from './Navigation.js';
|
import {Navigation} from './Navigation.js';
|
||||||
@ -60,8 +61,11 @@ export type SetViewportOptions = Omit<
|
|||||||
* @internal
|
* @internal
|
||||||
*/
|
*/
|
||||||
export class BrowsingContext extends EventEmitter<{
|
export class BrowsingContext extends EventEmitter<{
|
||||||
/** Emitted when this context is destroyed. */
|
/** Emitted when this context is closed. */
|
||||||
destroyed: void;
|
closed: {
|
||||||
|
/** The reason the browsing context was closed */
|
||||||
|
reason: string;
|
||||||
|
};
|
||||||
/** Emitted when a child browsing context is created. */
|
/** Emitted when a child browsing context is created. */
|
||||||
browsingcontext: {
|
browsingcontext: {
|
||||||
/** The newly created child browsing context. */
|
/** The newly created child browsing context. */
|
||||||
@ -105,15 +109,16 @@ export class BrowsingContext extends EventEmitter<{
|
|||||||
|
|
||||||
// keep-sorted start
|
// keep-sorted start
|
||||||
#navigation: Navigation | undefined;
|
#navigation: Navigation | undefined;
|
||||||
|
#reason?: string;
|
||||||
#url: string;
|
#url: string;
|
||||||
readonly #children = new Map<string, BrowsingContext>();
|
readonly #children = new Map<string, BrowsingContext>();
|
||||||
|
readonly #disposables = new DisposableStack();
|
||||||
readonly #realms = new Map<string, WindowRealm>();
|
readonly #realms = new Map<string, WindowRealm>();
|
||||||
readonly #requests = new Map<string, Request>();
|
readonly #requests = new Map<string, Request>();
|
||||||
readonly defaultRealm: WindowRealm;
|
readonly defaultRealm: WindowRealm;
|
||||||
readonly id: string;
|
readonly id: string;
|
||||||
readonly parent: BrowsingContext | undefined;
|
readonly parent: BrowsingContext | undefined;
|
||||||
readonly userContext: UserContext;
|
readonly userContext: UserContext;
|
||||||
disposed = false;
|
|
||||||
// keep-sorted end
|
// keep-sorted end
|
||||||
|
|
||||||
private constructor(
|
private constructor(
|
||||||
@ -123,7 +128,6 @@ export class BrowsingContext extends EventEmitter<{
|
|||||||
url: string
|
url: string
|
||||||
) {
|
) {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
// keep-sorted start
|
// keep-sorted start
|
||||||
this.#url = url;
|
this.#url = url;
|
||||||
this.id = id;
|
this.id = id;
|
||||||
@ -135,11 +139,17 @@ export class BrowsingContext extends EventEmitter<{
|
|||||||
}
|
}
|
||||||
|
|
||||||
#initialize() {
|
#initialize() {
|
||||||
// ///////////////////////
|
const userContextEmitter = this.#disposables.use(
|
||||||
// Session listeners //
|
new EventEmitter(this.userContext)
|
||||||
// ///////////////////////
|
);
|
||||||
const session = this.#session;
|
userContextEmitter.once('closed', ({reason}) => {
|
||||||
session.on('browsingContext.contextCreated', info => {
|
this.dispose(`Browsing context already closed: ${reason}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
const sessionEmitter = this.#disposables.use(
|
||||||
|
new EventEmitter(this.#session)
|
||||||
|
);
|
||||||
|
sessionEmitter.on('browsingContext.contextCreated', info => {
|
||||||
if (info.parent !== this.id) {
|
if (info.parent !== this.id) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -150,24 +160,27 @@ export class BrowsingContext extends EventEmitter<{
|
|||||||
info.context,
|
info.context,
|
||||||
info.url
|
info.url
|
||||||
);
|
);
|
||||||
browsingContext.on('destroyed', () => {
|
this.#children.set(info.context, browsingContext);
|
||||||
|
|
||||||
|
const browsingContextEmitter = this.#disposables.use(
|
||||||
|
new EventEmitter(browsingContext)
|
||||||
|
);
|
||||||
|
browsingContextEmitter.once('closed', () => {
|
||||||
|
browsingContextEmitter.removeAllListeners();
|
||||||
|
|
||||||
this.#children.delete(browsingContext.id);
|
this.#children.delete(browsingContext.id);
|
||||||
});
|
});
|
||||||
|
|
||||||
this.#children.set(info.context, browsingContext);
|
|
||||||
|
|
||||||
this.emit('browsingcontext', {browsingContext});
|
this.emit('browsingcontext', {browsingContext});
|
||||||
});
|
});
|
||||||
session.on('browsingContext.contextDestroyed', info => {
|
sessionEmitter.on('browsingContext.contextDestroyed', info => {
|
||||||
if (info.context !== this.id) {
|
if (info.context !== this.id) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.disposed = true;
|
this.dispose('Browsing context already closed.');
|
||||||
this.emit('destroyed', undefined);
|
|
||||||
this.removeAllListeners();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
session.on('browsingContext.domContentLoaded', info => {
|
sessionEmitter.on('browsingContext.domContentLoaded', info => {
|
||||||
if (info.context !== this.id) {
|
if (info.context !== this.id) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -175,7 +188,7 @@ export class BrowsingContext extends EventEmitter<{
|
|||||||
this.emit('DOMContentLoaded', undefined);
|
this.emit('DOMContentLoaded', undefined);
|
||||||
});
|
});
|
||||||
|
|
||||||
session.on('browsingContext.load', info => {
|
sessionEmitter.on('browsingContext.load', info => {
|
||||||
if (info.context !== this.id) {
|
if (info.context !== this.id) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -183,22 +196,31 @@ export class BrowsingContext extends EventEmitter<{
|
|||||||
this.emit('load', undefined);
|
this.emit('load', undefined);
|
||||||
});
|
});
|
||||||
|
|
||||||
session.on('browsingContext.navigationStarted', info => {
|
sessionEmitter.on('browsingContext.navigationStarted', info => {
|
||||||
if (info.context !== this.id) {
|
if (info.context !== this.id) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
this.#url = info.url;
|
||||||
|
|
||||||
this.#requests.clear();
|
this.#requests.clear();
|
||||||
|
|
||||||
// Note the navigation ID is null for this event.
|
// Note the navigation ID is null for this event.
|
||||||
this.#navigation = Navigation.from(this, info.url);
|
this.#navigation = Navigation.from(this);
|
||||||
this.#navigation.on('fragment', ({url}) => {
|
|
||||||
|
const navigationEmitter = this.#disposables.use(
|
||||||
|
new EventEmitter(this.#navigation)
|
||||||
|
);
|
||||||
|
for (const eventName of ['fragment', 'failed', 'aborted'] as const) {
|
||||||
|
navigationEmitter.once(eventName, ({url}) => {
|
||||||
|
navigationEmitter[disposeSymbol]();
|
||||||
|
|
||||||
this.#url = url;
|
this.#url = url;
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
this.emit('navigation', {navigation: this.#navigation});
|
this.emit('navigation', {navigation: this.#navigation});
|
||||||
});
|
});
|
||||||
session.on('network.beforeRequestSent', event => {
|
sessionEmitter.on('network.beforeRequestSent', event => {
|
||||||
if (event.context !== this.id) {
|
if (event.context !== this.id) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -206,12 +228,12 @@ export class BrowsingContext extends EventEmitter<{
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const request = new Request(this, event);
|
const request = Request.from(this, event);
|
||||||
this.#requests.set(request.id, request);
|
this.#requests.set(request.id, request);
|
||||||
this.emit('request', {request});
|
this.emit('request', {request});
|
||||||
});
|
});
|
||||||
|
|
||||||
session.on('log.entryAdded', entry => {
|
sessionEmitter.on('log.entryAdded', entry => {
|
||||||
if (entry.source.context !== this.id) {
|
if (entry.source.context !== this.id) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -219,7 +241,7 @@ export class BrowsingContext extends EventEmitter<{
|
|||||||
this.emit('log', {entry});
|
this.emit('log', {entry});
|
||||||
});
|
});
|
||||||
|
|
||||||
session.on('browsingContext.userPromptOpened', info => {
|
sessionEmitter.on('browsingContext.userPromptOpened', info => {
|
||||||
if (info.context !== this.id) {
|
if (info.context !== this.id) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -236,6 +258,12 @@ export class BrowsingContext extends EventEmitter<{
|
|||||||
get children(): Iterable<BrowsingContext> {
|
get children(): Iterable<BrowsingContext> {
|
||||||
return this.#children.values();
|
return this.#children.values();
|
||||||
}
|
}
|
||||||
|
get closed(): boolean {
|
||||||
|
return this.#reason !== undefined;
|
||||||
|
}
|
||||||
|
get disposed(): boolean {
|
||||||
|
return this.closed;
|
||||||
|
}
|
||||||
get realms(): Iterable<WindowRealm> {
|
get realms(): Iterable<WindowRealm> {
|
||||||
return this.#realms.values();
|
return this.#realms.values();
|
||||||
}
|
}
|
||||||
@ -251,14 +279,26 @@ export class BrowsingContext extends EventEmitter<{
|
|||||||
}
|
}
|
||||||
// keep-sorted end
|
// keep-sorted end
|
||||||
|
|
||||||
@throwIfDisposed()
|
@inertIfDisposed
|
||||||
|
private dispose(reason?: string): void {
|
||||||
|
this.#reason = reason;
|
||||||
|
this[disposeSymbol]();
|
||||||
|
}
|
||||||
|
|
||||||
|
@throwIfDisposed<BrowsingContext>(context => {
|
||||||
|
// SAFETY: Disposal implies this exists.
|
||||||
|
return context.#reason!;
|
||||||
|
})
|
||||||
async activate(): Promise<void> {
|
async activate(): Promise<void> {
|
||||||
await this.#session.send('browsingContext.activate', {
|
await this.#session.send('browsingContext.activate', {
|
||||||
context: this.id,
|
context: this.id,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@throwIfDisposed()
|
@throwIfDisposed<BrowsingContext>(context => {
|
||||||
|
// SAFETY: Disposal implies this exists.
|
||||||
|
return context.#reason!;
|
||||||
|
})
|
||||||
async captureScreenshot(
|
async captureScreenshot(
|
||||||
options: CaptureScreenshotOptions = {}
|
options: CaptureScreenshotOptions = {}
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
@ -271,7 +311,10 @@ export class BrowsingContext extends EventEmitter<{
|
|||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
@throwIfDisposed()
|
@throwIfDisposed<BrowsingContext>(context => {
|
||||||
|
// SAFETY: Disposal implies this exists.
|
||||||
|
return context.#reason!;
|
||||||
|
})
|
||||||
async close(promptUnload?: boolean): Promise<void> {
|
async close(promptUnload?: boolean): Promise<void> {
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
[...this.#children.values()].map(async child => {
|
[...this.#children.values()].map(async child => {
|
||||||
@ -284,7 +327,10 @@ export class BrowsingContext extends EventEmitter<{
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@throwIfDisposed()
|
@throwIfDisposed<BrowsingContext>(context => {
|
||||||
|
// SAFETY: Disposal implies this exists.
|
||||||
|
return context.#reason!;
|
||||||
|
})
|
||||||
async traverseHistory(delta: number): Promise<void> {
|
async traverseHistory(delta: number): Promise<void> {
|
||||||
await this.#session.send('browsingContext.traverseHistory', {
|
await this.#session.send('browsingContext.traverseHistory', {
|
||||||
context: this.id,
|
context: this.id,
|
||||||
@ -292,7 +338,10 @@ export class BrowsingContext extends EventEmitter<{
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@throwIfDisposed()
|
@throwIfDisposed<BrowsingContext>(context => {
|
||||||
|
// SAFETY: Disposal implies this exists.
|
||||||
|
return context.#reason!;
|
||||||
|
})
|
||||||
async navigate(
|
async navigate(
|
||||||
url: string,
|
url: string,
|
||||||
wait?: Bidi.BrowsingContext.ReadinessState
|
wait?: Bidi.BrowsingContext.ReadinessState
|
||||||
@ -309,7 +358,10 @@ export class BrowsingContext extends EventEmitter<{
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@throwIfDisposed()
|
@throwIfDisposed<BrowsingContext>(context => {
|
||||||
|
// SAFETY: Disposal implies this exists.
|
||||||
|
return context.#reason!;
|
||||||
|
})
|
||||||
async reload(options: ReloadOptions = {}): Promise<Navigation> {
|
async reload(options: ReloadOptions = {}): Promise<Navigation> {
|
||||||
await this.#session.send('browsingContext.reload', {
|
await this.#session.send('browsingContext.reload', {
|
||||||
context: this.id,
|
context: this.id,
|
||||||
@ -322,7 +374,10 @@ export class BrowsingContext extends EventEmitter<{
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@throwIfDisposed()
|
@throwIfDisposed<BrowsingContext>(context => {
|
||||||
|
// SAFETY: Disposal implies this exists.
|
||||||
|
return context.#reason!;
|
||||||
|
})
|
||||||
async print(options: PrintOptions = {}): Promise<string> {
|
async print(options: PrintOptions = {}): Promise<string> {
|
||||||
const {
|
const {
|
||||||
result: {data},
|
result: {data},
|
||||||
@ -333,7 +388,10 @@ export class BrowsingContext extends EventEmitter<{
|
|||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
@throwIfDisposed()
|
@throwIfDisposed<BrowsingContext>(context => {
|
||||||
|
// SAFETY: Disposal implies this exists.
|
||||||
|
return context.#reason!;
|
||||||
|
})
|
||||||
async handleUserPrompt(options: HandleUserPromptOptions = {}): Promise<void> {
|
async handleUserPrompt(options: HandleUserPromptOptions = {}): Promise<void> {
|
||||||
await this.#session.send('browsingContext.handleUserPrompt', {
|
await this.#session.send('browsingContext.handleUserPrompt', {
|
||||||
context: this.id,
|
context: this.id,
|
||||||
@ -341,7 +399,10 @@ export class BrowsingContext extends EventEmitter<{
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@throwIfDisposed()
|
@throwIfDisposed<BrowsingContext>(context => {
|
||||||
|
// SAFETY: Disposal implies this exists.
|
||||||
|
return context.#reason!;
|
||||||
|
})
|
||||||
async setViewport(options: SetViewportOptions = {}): Promise<void> {
|
async setViewport(options: SetViewportOptions = {}): Promise<void> {
|
||||||
await this.#session.send('browsingContext.setViewport', {
|
await this.#session.send('browsingContext.setViewport', {
|
||||||
context: this.id,
|
context: this.id,
|
||||||
@ -349,7 +410,10 @@ export class BrowsingContext extends EventEmitter<{
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@throwIfDisposed()
|
@throwIfDisposed<BrowsingContext>(context => {
|
||||||
|
// SAFETY: Disposal implies this exists.
|
||||||
|
return context.#reason!;
|
||||||
|
})
|
||||||
async performActions(actions: Bidi.Input.SourceActions[]): Promise<void> {
|
async performActions(actions: Bidi.Input.SourceActions[]): Promise<void> {
|
||||||
await this.#session.send('input.performActions', {
|
await this.#session.send('input.performActions', {
|
||||||
context: this.id,
|
context: this.id,
|
||||||
@ -357,19 +421,28 @@ export class BrowsingContext extends EventEmitter<{
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@throwIfDisposed()
|
@throwIfDisposed<BrowsingContext>(context => {
|
||||||
|
// SAFETY: Disposal implies this exists.
|
||||||
|
return context.#reason!;
|
||||||
|
})
|
||||||
async releaseActions(): Promise<void> {
|
async releaseActions(): Promise<void> {
|
||||||
await this.#session.send('input.releaseActions', {
|
await this.#session.send('input.releaseActions', {
|
||||||
context: this.id,
|
context: this.id,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@throwIfDisposed()
|
@throwIfDisposed<BrowsingContext>(context => {
|
||||||
|
// SAFETY: Disposal implies this exists.
|
||||||
|
return context.#reason!;
|
||||||
|
})
|
||||||
createWindowRealm(sandbox: string): WindowRealm {
|
createWindowRealm(sandbox: string): WindowRealm {
|
||||||
return WindowRealm.from(this, sandbox);
|
return WindowRealm.from(this, sandbox);
|
||||||
}
|
}
|
||||||
|
|
||||||
@throwIfDisposed()
|
@throwIfDisposed<BrowsingContext>(context => {
|
||||||
|
// SAFETY: Disposal implies this exists.
|
||||||
|
return context.#reason!;
|
||||||
|
})
|
||||||
async addPreloadScript(
|
async addPreloadScript(
|
||||||
functionDeclaration: string,
|
functionDeclaration: string,
|
||||||
options: AddPreloadScriptOptions = {}
|
options: AddPreloadScriptOptions = {}
|
||||||
@ -383,8 +456,20 @@ export class BrowsingContext extends EventEmitter<{
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@throwIfDisposed()
|
@throwIfDisposed<BrowsingContext>(context => {
|
||||||
|
// SAFETY: Disposal implies this exists.
|
||||||
|
return context.#reason!;
|
||||||
|
})
|
||||||
async removePreloadScript(script: string): Promise<void> {
|
async removePreloadScript(script: string): Promise<void> {
|
||||||
await this.userContext.browser.removePreloadScript(script);
|
await this.userContext.browser.removePreloadScript(script);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[disposeSymbol](): void {
|
||||||
|
this.#reason ??=
|
||||||
|
'Browsing context already closed, probably because the user context closed.';
|
||||||
|
this.emit('closed', {reason: this.#reason});
|
||||||
|
|
||||||
|
this.#disposables.dispose();
|
||||||
|
super[disposeSymbol]();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,10 +4,10 @@
|
|||||||
* SPDX-License-Identifier: Apache-2.0
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js';
|
|
||||||
|
|
||||||
import {EventEmitter} from '../../common/EventEmitter.js';
|
import {EventEmitter} from '../../common/EventEmitter.js';
|
||||||
|
import {inertIfDisposed} from '../../util/decorators.js';
|
||||||
import {Deferred} from '../../util/Deferred.js';
|
import {Deferred} from '../../util/Deferred.js';
|
||||||
|
import {DisposableStack, disposeSymbol} from '../../util/disposable.js';
|
||||||
|
|
||||||
import type {BrowsingContext} from './BrowsingContext.js';
|
import type {BrowsingContext} from './BrowsingContext.js';
|
||||||
import type {Request} from './Request.js';
|
import type {Request} from './Request.js';
|
||||||
@ -24,44 +24,82 @@ export interface NavigationInfo {
|
|||||||
* @internal
|
* @internal
|
||||||
*/
|
*/
|
||||||
export class Navigation extends EventEmitter<{
|
export class Navigation extends EventEmitter<{
|
||||||
|
/** Emitted when navigation has a request associated with it. */
|
||||||
|
request: Request;
|
||||||
|
/** Emitted when fragment navigation occurred. */
|
||||||
fragment: NavigationInfo;
|
fragment: NavigationInfo;
|
||||||
|
/** Emitted when navigation failed. */
|
||||||
failed: NavigationInfo;
|
failed: NavigationInfo;
|
||||||
|
/** Emitted when navigation was aborted. */
|
||||||
aborted: NavigationInfo;
|
aborted: NavigationInfo;
|
||||||
}> {
|
}> {
|
||||||
static from(context: BrowsingContext, url: string): Navigation {
|
static from(context: BrowsingContext): Navigation {
|
||||||
const navigation = new Navigation(context, url);
|
const navigation = new Navigation(context);
|
||||||
navigation.#initialize();
|
navigation.#initialize();
|
||||||
return navigation;
|
return navigation;
|
||||||
}
|
}
|
||||||
|
|
||||||
// keep-sorted start
|
// keep-sorted start
|
||||||
#context: BrowsingContext;
|
|
||||||
#id = new Deferred<string>();
|
|
||||||
#request: Request | undefined;
|
#request: Request | undefined;
|
||||||
#url: string;
|
readonly #browsingContext: BrowsingContext;
|
||||||
|
readonly #disposables = new DisposableStack();
|
||||||
|
readonly #id = new Deferred<string>();
|
||||||
// keep-sorted end
|
// keep-sorted end
|
||||||
|
|
||||||
private constructor(context: BrowsingContext, url: string) {
|
private constructor(context: BrowsingContext) {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
// keep-sorted start
|
// keep-sorted start
|
||||||
this.#context = context;
|
this.#browsingContext = context;
|
||||||
this.#url = url;
|
|
||||||
// keep-sorted end
|
// keep-sorted end
|
||||||
}
|
}
|
||||||
|
|
||||||
#initialize() {
|
#initialize() {
|
||||||
// ///////////////////////
|
const browsingContextEmitter = this.#disposables.use(
|
||||||
// Session listeners //
|
new EventEmitter(this.#browsingContext)
|
||||||
// ///////////////////////
|
);
|
||||||
const session = this.#session;
|
browsingContextEmitter.once('closed', () => {
|
||||||
for (const [bidiEvent, event] of [
|
this.emit('failed', {
|
||||||
|
url: this.#browsingContext.url,
|
||||||
|
timestamp: new Date(),
|
||||||
|
});
|
||||||
|
this.dispose();
|
||||||
|
});
|
||||||
|
|
||||||
|
this.#browsingContext.on('request', ({request}) => {
|
||||||
|
if (request.navigation === this.#id.value()) {
|
||||||
|
this.#request = request;
|
||||||
|
this.emit('request', request);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const sessionEmitter = this.#disposables.use(
|
||||||
|
new EventEmitter(this.#session)
|
||||||
|
);
|
||||||
|
// To get the navigation ID if any.
|
||||||
|
for (const eventName of [
|
||||||
|
'browsingContext.domContentLoaded',
|
||||||
|
'browsingContext.load',
|
||||||
|
] as const) {
|
||||||
|
sessionEmitter.on(eventName, info => {
|
||||||
|
if (info.context !== this.#browsingContext.id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!info.navigation) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!this.#id.resolved()) {
|
||||||
|
this.#id.resolve(info.navigation);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [eventName, event] of [
|
||||||
['browsingContext.fragmentNavigated', 'fragment'],
|
['browsingContext.fragmentNavigated', 'fragment'],
|
||||||
['browsingContext.navigationFailed', 'failed'],
|
['browsingContext.navigationFailed', 'failed'],
|
||||||
['browsingContext.navigationAborted', 'aborted'],
|
['browsingContext.navigationAborted', 'aborted'],
|
||||||
] as const) {
|
] as const) {
|
||||||
session.on(bidiEvent, (info: Bidi.BrowsingContext.NavigationInfo) => {
|
sessionEmitter.on(eventName, info => {
|
||||||
if (info.context !== this.#context.id) {
|
if (info.context !== this.#browsingContext.id) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!info.navigation) {
|
if (!info.navigation) {
|
||||||
@ -73,33 +111,34 @@ export class Navigation extends EventEmitter<{
|
|||||||
if (this.#id.value() !== info.navigation) {
|
if (this.#id.value() !== info.navigation) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.#url = info.url;
|
|
||||||
this.emit(event, {
|
this.emit(event, {
|
||||||
url: this.#url,
|
url: info.url,
|
||||||
timestamp: new Date(info.timestamp),
|
timestamp: new Date(info.timestamp),
|
||||||
});
|
});
|
||||||
|
this.dispose();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// ///////////////////
|
|
||||||
// Parent listeners //
|
|
||||||
// ///////////////////
|
|
||||||
this.#context.on('request', ({request}) => {
|
|
||||||
if (request.navigation === this.#id.value()) {
|
|
||||||
this.#request = request;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// keep-sorted start block=yes
|
||||||
get #session() {
|
get #session() {
|
||||||
return this.#context.userContext.browser.session;
|
return this.#browsingContext.userContext.browser.session;
|
||||||
}
|
}
|
||||||
|
get disposed(): boolean {
|
||||||
get url(): string {
|
return this.#disposables.disposed;
|
||||||
return this.#url;
|
|
||||||
}
|
}
|
||||||
|
get request(): Request | undefined {
|
||||||
request(): Request | undefined {
|
|
||||||
return this.#request;
|
return this.#request;
|
||||||
}
|
}
|
||||||
|
// keep-sorted end
|
||||||
|
|
||||||
|
@inertIfDisposed
|
||||||
|
private dispose(): void {
|
||||||
|
this[disposeSymbol]();
|
||||||
|
}
|
||||||
|
|
||||||
|
[disposeSymbol](): void {
|
||||||
|
this.#disposables.dispose();
|
||||||
|
super[disposeSymbol]();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,6 +7,8 @@
|
|||||||
import type * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js';
|
import type * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js';
|
||||||
|
|
||||||
import {EventEmitter} from '../../common/EventEmitter.js';
|
import {EventEmitter} from '../../common/EventEmitter.js';
|
||||||
|
import {inertIfDisposed, throwIfDisposed} from '../../util/decorators.js';
|
||||||
|
import {DisposableStack, disposeSymbol} from '../../util/disposable.js';
|
||||||
|
|
||||||
import type {BrowsingContext} from './BrowsingContext.js';
|
import type {BrowsingContext} from './BrowsingContext.js';
|
||||||
import type {Session} from './Session.js';
|
import type {Session} from './Session.js';
|
||||||
@ -32,34 +34,57 @@ export type EvaluateOptions = Omit<
|
|||||||
*/
|
*/
|
||||||
export abstract class Realm extends EventEmitter<{
|
export abstract class Realm extends EventEmitter<{
|
||||||
/** Emitted when the realm is destroyed. */
|
/** Emitted when the realm is destroyed. */
|
||||||
destroyed: void;
|
destroyed: {reason: string};
|
||||||
/** Emitted when a dedicated worker is created in the realm. */
|
/** Emitted when a dedicated worker is created in the realm. */
|
||||||
worker: DedicatedWorkerRealm;
|
worker: DedicatedWorkerRealm;
|
||||||
/** Emitted when a shared worker is created in the realm. */
|
/** Emitted when a shared worker is created in the realm. */
|
||||||
sharedworker: SharedWorkerRealm;
|
sharedworker: SharedWorkerRealm;
|
||||||
}> {
|
}> {
|
||||||
|
// keep-sorted start
|
||||||
|
#reason?: string;
|
||||||
|
protected readonly disposables = new DisposableStack();
|
||||||
readonly id: string;
|
readonly id: string;
|
||||||
readonly origin: string;
|
readonly origin: string;
|
||||||
|
// keep-sorted end
|
||||||
|
|
||||||
protected constructor(id: string, origin: string) {
|
protected constructor(id: string, origin: string) {
|
||||||
super();
|
super();
|
||||||
|
// keep-sorted start
|
||||||
this.id = id;
|
this.id = id;
|
||||||
this.origin = origin;
|
this.origin = origin;
|
||||||
|
// keep-sorted end
|
||||||
}
|
}
|
||||||
|
|
||||||
protected initialize(): void {
|
protected initialize(): void {
|
||||||
this.session.on('script.realmDestroyed', info => {
|
const sessionEmitter = this.disposables.use(new EventEmitter(this.session));
|
||||||
if (info.realm === this.id) {
|
sessionEmitter.on('script.realmDestroyed', info => {
|
||||||
this.emit('destroyed', undefined);
|
if (info.realm !== this.id) {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
this.dispose('Realm already destroyed.');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// keep-sorted start block=yes
|
||||||
|
get disposed(): boolean {
|
||||||
|
return this.#reason !== undefined;
|
||||||
|
}
|
||||||
protected abstract get session(): Session;
|
protected abstract get session(): Session;
|
||||||
|
|
||||||
protected get target(): Bidi.Script.Target {
|
protected get target(): Bidi.Script.Target {
|
||||||
return {realm: this.id};
|
return {realm: this.id};
|
||||||
}
|
}
|
||||||
|
// keep-sorted end
|
||||||
|
|
||||||
|
@inertIfDisposed
|
||||||
|
protected dispose(reason?: string): void {
|
||||||
|
this.#reason = reason;
|
||||||
|
this[disposeSymbol]();
|
||||||
|
}
|
||||||
|
|
||||||
|
@throwIfDisposed<Realm>(realm => {
|
||||||
|
// SAFETY: Disposal implies this exists.
|
||||||
|
return realm.#reason!;
|
||||||
|
})
|
||||||
async disown(handles: string[]): Promise<void> {
|
async disown(handles: string[]): Promise<void> {
|
||||||
await this.session.send('script.disown', {
|
await this.session.send('script.disown', {
|
||||||
target: this.target,
|
target: this.target,
|
||||||
@ -67,6 +92,10 @@ export abstract class Realm extends EventEmitter<{
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@throwIfDisposed<Realm>(realm => {
|
||||||
|
// SAFETY: Disposal implies this exists.
|
||||||
|
return realm.#reason!;
|
||||||
|
})
|
||||||
async callFunction(
|
async callFunction(
|
||||||
functionDeclaration: string,
|
functionDeclaration: string,
|
||||||
awaitPromise: boolean,
|
awaitPromise: boolean,
|
||||||
@ -81,6 +110,10 @@ export abstract class Realm extends EventEmitter<{
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@throwIfDisposed<Realm>(realm => {
|
||||||
|
// SAFETY: Disposal implies this exists.
|
||||||
|
return realm.#reason!;
|
||||||
|
})
|
||||||
async evaluate(
|
async evaluate(
|
||||||
expression: string,
|
expression: string,
|
||||||
awaitPromise: boolean,
|
awaitPromise: boolean,
|
||||||
@ -94,6 +127,15 @@ export abstract class Realm extends EventEmitter<{
|
|||||||
});
|
});
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[disposeSymbol](): void {
|
||||||
|
this.#reason ??=
|
||||||
|
'Realm already destroyed, probably because all associated browsing contexts closed.';
|
||||||
|
this.emit('destroyed', {reason: this.#reason});
|
||||||
|
|
||||||
|
this.disposables.dispose();
|
||||||
|
super[disposeSymbol]();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -106,8 +148,10 @@ export class WindowRealm extends Realm {
|
|||||||
return realm;
|
return realm;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// keep-sorted start
|
||||||
readonly browsingContext: BrowsingContext;
|
readonly browsingContext: BrowsingContext;
|
||||||
readonly sandbox?: string;
|
readonly sandbox?: string;
|
||||||
|
// keep-sorted end
|
||||||
|
|
||||||
readonly #workers: {
|
readonly #workers: {
|
||||||
dedicated: Map<string, DedicatedWorkerRealm>;
|
dedicated: Map<string, DedicatedWorkerRealm>;
|
||||||
@ -117,53 +161,59 @@ export class WindowRealm extends Realm {
|
|||||||
shared: new Map(),
|
shared: new Map(),
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor(context: BrowsingContext, sandbox?: string) {
|
private constructor(context: BrowsingContext, sandbox?: string) {
|
||||||
super('', '');
|
super('', '');
|
||||||
|
// keep-sorted start
|
||||||
this.browsingContext = context;
|
this.browsingContext = context;
|
||||||
this.sandbox = sandbox;
|
this.sandbox = sandbox;
|
||||||
|
// keep-sorted end
|
||||||
}
|
}
|
||||||
|
|
||||||
override initialize(): void {
|
override initialize(): void {
|
||||||
super.initialize();
|
super.initialize();
|
||||||
|
|
||||||
// ///////////////////////
|
const sessionEmitter = this.disposables.use(new EventEmitter(this.session));
|
||||||
// Session listeners //
|
sessionEmitter.on('script.realmCreated', info => {
|
||||||
// ///////////////////////
|
if (info.type !== 'window') {
|
||||||
this.session.on('script.realmCreated', info => {
|
return;
|
||||||
if (info.type === 'window') {
|
}
|
||||||
// SAFETY: This is the only time we allow mutations.
|
(this as any).id = info.realm;
|
||||||
(this as any).id = info.realm;
|
(this as any).origin = info.origin;
|
||||||
|
});
|
||||||
|
sessionEmitter.on('script.realmCreated', info => {
|
||||||
|
if (info.type !== 'dedicated-worker') {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (info.type === 'dedicated-worker') {
|
|
||||||
if (!info.owners.includes(this.id)) {
|
if (!info.owners.includes(this.id)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const realm = DedicatedWorkerRealm.from(this, info.realm, info.origin);
|
const realm = DedicatedWorkerRealm.from(this, info.realm, info.origin);
|
||||||
realm.on('destroyed', () => {
|
this.#workers.dedicated.set(realm.id, realm);
|
||||||
|
|
||||||
|
const realmEmitter = this.disposables.use(new EventEmitter(realm));
|
||||||
|
realmEmitter.once('destroyed', () => {
|
||||||
|
realmEmitter.removeAllListeners();
|
||||||
this.#workers.dedicated.delete(realm.id);
|
this.#workers.dedicated.delete(realm.id);
|
||||||
});
|
});
|
||||||
|
|
||||||
this.#workers.dedicated.set(realm.id, realm);
|
|
||||||
|
|
||||||
this.emit('worker', realm);
|
this.emit('worker', realm);
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// ///////////////////
|
|
||||||
// Parent listeners //
|
|
||||||
// ///////////////////
|
|
||||||
this.browsingContext.userContext.browser.on('sharedworker', ({realm}) => {
|
this.browsingContext.userContext.browser.on('sharedworker', ({realm}) => {
|
||||||
if (realm.owners.has(this)) {
|
if (!realm.owners.has(this)) {
|
||||||
realm.on('destroyed', () => {
|
return;
|
||||||
this.#workers.shared.delete(realm.id);
|
}
|
||||||
});
|
|
||||||
|
|
||||||
this.#workers.shared.set(realm.id, realm);
|
this.#workers.shared.set(realm.id, realm);
|
||||||
|
|
||||||
|
const realmEmitter = this.disposables.use(new EventEmitter(realm));
|
||||||
|
realmEmitter.once('destroyed', () => {
|
||||||
|
realmEmitter.removeAllListeners();
|
||||||
|
this.#workers.shared.delete(realm.id);
|
||||||
|
});
|
||||||
|
|
||||||
this.emit('sharedworker', realm);
|
this.emit('sharedworker', realm);
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -198,11 +248,16 @@ export class DedicatedWorkerRealm extends Realm {
|
|||||||
return realm;
|
return realm;
|
||||||
}
|
}
|
||||||
|
|
||||||
readonly owners: Set<DedicatedWorkerOwnerRealm>;
|
// keep-sorted start
|
||||||
|
|
||||||
readonly #workers = new Map<string, DedicatedWorkerRealm>();
|
readonly #workers = new Map<string, DedicatedWorkerRealm>();
|
||||||
|
readonly owners: Set<DedicatedWorkerOwnerRealm>;
|
||||||
|
// keep-sorted end
|
||||||
|
|
||||||
constructor(owner: DedicatedWorkerOwnerRealm, id: string, origin: string) {
|
private constructor(
|
||||||
|
owner: DedicatedWorkerOwnerRealm,
|
||||||
|
id: string,
|
||||||
|
origin: string
|
||||||
|
) {
|
||||||
super(id, origin);
|
super(id, origin);
|
||||||
this.owners = new Set([owner]);
|
this.owners = new Set([owner]);
|
||||||
}
|
}
|
||||||
@ -210,24 +265,24 @@ export class DedicatedWorkerRealm extends Realm {
|
|||||||
override initialize(): void {
|
override initialize(): void {
|
||||||
super.initialize();
|
super.initialize();
|
||||||
|
|
||||||
// ///////////////////////
|
const sessionEmitter = this.disposables.use(new EventEmitter(this.session));
|
||||||
// Session listeners //
|
sessionEmitter.on('script.realmCreated', info => {
|
||||||
// ///////////////////////
|
if (info.type !== 'dedicated-worker') {
|
||||||
this.session.on('script.realmCreated', info => {
|
return;
|
||||||
if (info.type === 'dedicated-worker') {
|
}
|
||||||
if (!info.owners.includes(this.id)) {
|
if (!info.owners.includes(this.id)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const realm = DedicatedWorkerRealm.from(this, info.realm, info.origin);
|
const realm = DedicatedWorkerRealm.from(this, info.realm, info.origin);
|
||||||
realm.on('destroyed', () => {
|
this.#workers.set(realm.id, realm);
|
||||||
|
|
||||||
|
const realmEmitter = this.disposables.use(new EventEmitter(realm));
|
||||||
|
realmEmitter.once('destroyed', () => {
|
||||||
this.#workers.delete(realm.id);
|
this.#workers.delete(realm.id);
|
||||||
});
|
});
|
||||||
|
|
||||||
this.#workers.set(realm.id, realm);
|
|
||||||
|
|
||||||
this.emit('worker', realm);
|
this.emit('worker', realm);
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -251,11 +306,12 @@ export class SharedWorkerRealm extends Realm {
|
|||||||
return realm;
|
return realm;
|
||||||
}
|
}
|
||||||
|
|
||||||
readonly owners: Set<WindowRealm>;
|
// keep-sorted start
|
||||||
|
|
||||||
readonly #workers = new Map<string, DedicatedWorkerRealm>();
|
readonly #workers = new Map<string, DedicatedWorkerRealm>();
|
||||||
|
readonly owners: Set<WindowRealm>;
|
||||||
|
// keep-sorted end
|
||||||
|
|
||||||
constructor(
|
private constructor(
|
||||||
owners: [WindowRealm, ...WindowRealm[]],
|
owners: [WindowRealm, ...WindowRealm[]],
|
||||||
id: string,
|
id: string,
|
||||||
origin: string
|
origin: string
|
||||||
@ -267,24 +323,24 @@ export class SharedWorkerRealm extends Realm {
|
|||||||
override initialize(): void {
|
override initialize(): void {
|
||||||
super.initialize();
|
super.initialize();
|
||||||
|
|
||||||
// ///////////////////////
|
const sessionEmitter = this.disposables.use(new EventEmitter(this.session));
|
||||||
// Session listeners //
|
sessionEmitter.on('script.realmCreated', info => {
|
||||||
// ///////////////////////
|
if (info.type !== 'dedicated-worker') {
|
||||||
this.session.on('script.realmCreated', info => {
|
return;
|
||||||
if (info.type === 'dedicated-worker') {
|
}
|
||||||
if (!info.owners.includes(this.id)) {
|
if (!info.owners.includes(this.id)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const realm = DedicatedWorkerRealm.from(this, info.realm, info.origin);
|
const realm = DedicatedWorkerRealm.from(this, info.realm, info.origin);
|
||||||
realm.on('destroyed', () => {
|
this.#workers.set(realm.id, realm);
|
||||||
|
|
||||||
|
const realmEmitter = this.disposables.use(new EventEmitter(realm));
|
||||||
|
realmEmitter.once('destroyed', () => {
|
||||||
this.#workers.delete(realm.id);
|
this.#workers.delete(realm.id);
|
||||||
});
|
});
|
||||||
|
|
||||||
this.#workers.set(realm.id, realm);
|
|
||||||
|
|
||||||
this.emit('worker', realm);
|
this.emit('worker', realm);
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -7,6 +7,8 @@
|
|||||||
import type * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js';
|
import type * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js';
|
||||||
|
|
||||||
import {EventEmitter} from '../../common/EventEmitter.js';
|
import {EventEmitter} from '../../common/EventEmitter.js';
|
||||||
|
import {inertIfDisposed} from '../../util/decorators.js';
|
||||||
|
import {DisposableStack, disposeSymbol} from '../../util/disposable.js';
|
||||||
|
|
||||||
import type {BrowsingContext} from './BrowsingContext.js';
|
import type {BrowsingContext} from './BrowsingContext.js';
|
||||||
|
|
||||||
@ -14,44 +16,68 @@ import type {BrowsingContext} from './BrowsingContext.js';
|
|||||||
* @internal
|
* @internal
|
||||||
*/
|
*/
|
||||||
export class Request extends EventEmitter<{
|
export class Request extends EventEmitter<{
|
||||||
// Emitted whenever a redirect is received.
|
/** Emitted when the request is redirected. */
|
||||||
redirect: Request;
|
redirect: Request;
|
||||||
// Emitted when when the request succeeds.
|
/** Emitted when the request succeeds. */
|
||||||
success: Bidi.Network.ResponseData;
|
success: Bidi.Network.ResponseData;
|
||||||
// Emitted when when the request errors.
|
/** Emitted when the request fails. */
|
||||||
error: string;
|
error: string;
|
||||||
}> {
|
}> {
|
||||||
readonly #context: BrowsingContext;
|
static from(
|
||||||
readonly #event: Bidi.Network.BeforeRequestSentParameters;
|
browsingContext: BrowsingContext,
|
||||||
|
event: Bidi.Network.BeforeRequestSentParameters
|
||||||
|
): Request {
|
||||||
|
const request = new Request(browsingContext, event);
|
||||||
|
request.#initialize();
|
||||||
|
return request;
|
||||||
|
}
|
||||||
|
|
||||||
#response?: Bidi.Network.ResponseData;
|
// keep-sorted start
|
||||||
#redirect?: Request;
|
|
||||||
#error?: string;
|
#error?: string;
|
||||||
|
#redirect?: Request;
|
||||||
|
#response?: Bidi.Network.ResponseData;
|
||||||
|
readonly #browsingContext: BrowsingContext;
|
||||||
|
readonly #disposables = new DisposableStack();
|
||||||
|
readonly #event: Bidi.Network.BeforeRequestSentParameters;
|
||||||
|
// keep-sorted end
|
||||||
|
|
||||||
constructor(
|
private constructor(
|
||||||
context: BrowsingContext,
|
browsingContext: BrowsingContext,
|
||||||
event: Bidi.Network.BeforeRequestSentParameters
|
event: Bidi.Network.BeforeRequestSentParameters
|
||||||
) {
|
) {
|
||||||
super();
|
super();
|
||||||
this.#context = context;
|
// keep-sorted start
|
||||||
|
this.#browsingContext = browsingContext;
|
||||||
this.#event = event;
|
this.#event = event;
|
||||||
|
// keep-sorted end
|
||||||
|
}
|
||||||
|
|
||||||
const session = this.#session;
|
#initialize() {
|
||||||
session.on('network.beforeRequestSent', event => {
|
const browsingContextEmitter = this.#disposables.use(
|
||||||
if (event.context !== this.id) {
|
new EventEmitter(this.#browsingContext)
|
||||||
|
);
|
||||||
|
browsingContextEmitter.once('closed', ({reason}) => {
|
||||||
|
this.#error = reason;
|
||||||
|
this.emit('error', this.#error);
|
||||||
|
this.dispose();
|
||||||
|
});
|
||||||
|
|
||||||
|
const sessionEmitter = this.#disposables.use(
|
||||||
|
new EventEmitter(this.#session)
|
||||||
|
);
|
||||||
|
sessionEmitter.on('network.beforeRequestSent', event => {
|
||||||
|
if (event.context !== this.#browsingContext.id) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (event.request.request !== this.id) {
|
if (event.request.request !== this.id) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (this.#redirect) {
|
this.#redirect = Request.from(this.#browsingContext, event);
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.#redirect = new Request(this.#context, event);
|
|
||||||
this.emit('redirect', this.#redirect);
|
this.emit('redirect', this.#redirect);
|
||||||
|
this.dispose();
|
||||||
});
|
});
|
||||||
session.on('network.fetchError', event => {
|
sessionEmitter.on('network.fetchError', event => {
|
||||||
if (event.context !== this.#context.id) {
|
if (event.context !== this.#browsingContext.id) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (event.request.request !== this.id) {
|
if (event.request.request !== this.id) {
|
||||||
@ -59,9 +85,10 @@ export class Request extends EventEmitter<{
|
|||||||
}
|
}
|
||||||
this.#error = event.errorText;
|
this.#error = event.errorText;
|
||||||
this.emit('error', this.#error);
|
this.emit('error', this.#error);
|
||||||
|
this.dispose();
|
||||||
});
|
});
|
||||||
session.on('network.responseCompleted', event => {
|
sessionEmitter.on('network.responseCompleted', event => {
|
||||||
if (event.context !== this.#context.id) {
|
if (event.context !== this.#browsingContext.id) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (event.request.request !== this.id) {
|
if (event.request.request !== this.id) {
|
||||||
@ -69,46 +96,53 @@ export class Request extends EventEmitter<{
|
|||||||
}
|
}
|
||||||
this.#response = event.response;
|
this.#response = event.response;
|
||||||
this.emit('success', this.#response);
|
this.emit('success', this.#response);
|
||||||
|
this.dispose();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// keep-sorted start block=yes
|
||||||
get #session() {
|
get #session() {
|
||||||
return this.#context.userContext.browser.session;
|
return this.#browsingContext.userContext.browser.session;
|
||||||
}
|
}
|
||||||
|
get disposed(): boolean {
|
||||||
get id(): string {
|
return this.#disposables.disposed;
|
||||||
return this.#event.request.request;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
get url(): string {
|
|
||||||
return this.#event.request.url;
|
|
||||||
}
|
|
||||||
|
|
||||||
get initiator(): Bidi.Network.Initiator {
|
|
||||||
return this.#event.initiator;
|
|
||||||
}
|
|
||||||
|
|
||||||
get method(): string {
|
|
||||||
return this.#event.request.method;
|
|
||||||
}
|
|
||||||
|
|
||||||
get headers(): Bidi.Network.Header[] {
|
|
||||||
return this.#event.request.headers;
|
|
||||||
}
|
|
||||||
|
|
||||||
get navigation(): string | undefined {
|
|
||||||
return this.#event.navigation ?? undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
get redirect(): Request | undefined {
|
|
||||||
return this.redirect;
|
|
||||||
}
|
|
||||||
|
|
||||||
get response(): Bidi.Network.ResponseData | undefined {
|
|
||||||
return this.#response;
|
|
||||||
}
|
|
||||||
|
|
||||||
get error(): string | undefined {
|
get error(): string | undefined {
|
||||||
return this.#error;
|
return this.#error;
|
||||||
}
|
}
|
||||||
|
get headers(): Bidi.Network.Header[] {
|
||||||
|
return this.#event.request.headers;
|
||||||
|
}
|
||||||
|
get id(): string {
|
||||||
|
return this.#event.request.request;
|
||||||
|
}
|
||||||
|
get initiator(): Bidi.Network.Initiator {
|
||||||
|
return this.#event.initiator;
|
||||||
|
}
|
||||||
|
get method(): string {
|
||||||
|
return this.#event.request.method;
|
||||||
|
}
|
||||||
|
get navigation(): string | undefined {
|
||||||
|
return this.#event.navigation ?? undefined;
|
||||||
|
}
|
||||||
|
get redirect(): Request | undefined {
|
||||||
|
return this.redirect;
|
||||||
|
}
|
||||||
|
get response(): Bidi.Network.ResponseData | undefined {
|
||||||
|
return this.#response;
|
||||||
|
}
|
||||||
|
get url(): string {
|
||||||
|
return this.#event.request.url;
|
||||||
|
}
|
||||||
|
// keep-sorted end
|
||||||
|
|
||||||
|
@inertIfDisposed
|
||||||
|
private dispose(): void {
|
||||||
|
this[disposeSymbol]();
|
||||||
|
}
|
||||||
|
|
||||||
|
[disposeSymbol](): void {
|
||||||
|
this.#disposables.dispose();
|
||||||
|
super[disposeSymbol]();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -8,7 +8,8 @@ import type * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js';
|
|||||||
|
|
||||||
import {EventEmitter} from '../../common/EventEmitter.js';
|
import {EventEmitter} from '../../common/EventEmitter.js';
|
||||||
import {debugError} from '../../common/util.js';
|
import {debugError} from '../../common/util.js';
|
||||||
import {throwIfDisposed} from '../../util/decorators.js';
|
import {inertIfDisposed, throwIfDisposed} from '../../util/decorators.js';
|
||||||
|
import {DisposableStack, disposeSymbol} from '../../util/disposable.js';
|
||||||
|
|
||||||
import {Browser} from './Browser.js';
|
import {Browser} from './Browser.js';
|
||||||
import type {BidiEvents, Commands, Connection} from './Connection.js';
|
import type {BidiEvents, Commands, Connection} from './Connection.js';
|
||||||
@ -75,58 +76,53 @@ export class Session
|
|||||||
return session;
|
return session;
|
||||||
}
|
}
|
||||||
|
|
||||||
readonly connection: Connection;
|
// keep-sorted start
|
||||||
|
#reason: string | undefined;
|
||||||
|
readonly #disposables = new DisposableStack();
|
||||||
readonly #info: Bidi.Session.NewResult;
|
readonly #info: Bidi.Session.NewResult;
|
||||||
readonly browser!: Browser;
|
readonly browser!: Browser;
|
||||||
|
readonly connection: Connection;
|
||||||
#reason: string | undefined;
|
// keep-sorted end
|
||||||
|
|
||||||
private constructor(connection: Connection, info: Bidi.Session.NewResult) {
|
private constructor(connection: Connection, info: Bidi.Session.NewResult) {
|
||||||
super();
|
super();
|
||||||
this.connection = connection;
|
// keep-sorted start
|
||||||
this.#info = info;
|
this.#info = info;
|
||||||
|
this.connection = connection;
|
||||||
|
// keep-sorted end
|
||||||
}
|
}
|
||||||
|
|
||||||
async #initialize(): Promise<void> {
|
async #initialize(): Promise<void> {
|
||||||
// ///////////////////////
|
|
||||||
// Connection listeners //
|
|
||||||
// ///////////////////////
|
|
||||||
this.connection.pipeTo(this);
|
this.connection.pipeTo(this);
|
||||||
|
|
||||||
// //////////////////////////////
|
|
||||||
// Asynchronous initialization //
|
|
||||||
// //////////////////////////////
|
|
||||||
// SAFETY: We use `any` to allow assignment of the readonly property.
|
// SAFETY: We use `any` to allow assignment of the readonly property.
|
||||||
(this as any).browser = await Browser.from(this);
|
(this as any).browser = await Browser.from(this);
|
||||||
|
|
||||||
// //////////////////
|
const browserEmitter = this.#disposables.use(this.browser);
|
||||||
// Child listeners //
|
browserEmitter.once('closed', ({reason}) => {
|
||||||
// //////////////////
|
|
||||||
this.browser.once('closed', ({reason}) => {
|
|
||||||
this.dispose(reason);
|
this.dispose(reason);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
get disposed(): boolean {
|
// keep-sorted start block=yes
|
||||||
return this.#reason !== undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
get id(): string {
|
|
||||||
return this.#info.sessionId;
|
|
||||||
}
|
|
||||||
|
|
||||||
get capabilities(): Bidi.Session.NewResult['capabilities'] {
|
get capabilities(): Bidi.Session.NewResult['capabilities'] {
|
||||||
return this.#info.capabilities;
|
return this.#info.capabilities;
|
||||||
}
|
}
|
||||||
|
get disposed(): boolean {
|
||||||
dispose(reason?: string): void {
|
return this.ended;
|
||||||
if (this.disposed) {
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
this.#reason = reason ?? 'Session was disposed.';
|
get ended(): boolean {
|
||||||
this.emit('ended', {reason: this.#reason});
|
return this.#reason !== undefined;
|
||||||
this.removeAllListeners();
|
}
|
||||||
|
get id(): string {
|
||||||
|
return this.#info.sessionId;
|
||||||
|
}
|
||||||
|
// keep-sorted end
|
||||||
|
|
||||||
|
@inertIfDisposed
|
||||||
|
private dispose(reason?: string): void {
|
||||||
|
this.#reason = reason;
|
||||||
|
this[disposeSymbol]();
|
||||||
}
|
}
|
||||||
|
|
||||||
pipeTo<Events extends BidiEvents>(emitter: EventEmitter<Events>): void {
|
pipeTo<Events extends BidiEvents>(emitter: EventEmitter<Events>): void {
|
||||||
@ -140,7 +136,7 @@ export class Session
|
|||||||
* object is used, so we implement this method here, although it's not defined
|
* object is used, so we implement this method here, although it's not defined
|
||||||
* in the spec.
|
* in the spec.
|
||||||
*/
|
*/
|
||||||
@throwIfDisposed((session: Session) => {
|
@throwIfDisposed<Session>(session => {
|
||||||
// SAFETY: By definition of `disposed`, `#reason` is defined.
|
// SAFETY: By definition of `disposed`, `#reason` is defined.
|
||||||
return session.#reason!;
|
return session.#reason!;
|
||||||
})
|
})
|
||||||
@ -151,7 +147,7 @@ export class Session
|
|||||||
return await this.connection.send(method, params);
|
return await this.connection.send(method, params);
|
||||||
}
|
}
|
||||||
|
|
||||||
@throwIfDisposed((session: Session) => {
|
@throwIfDisposed<Session>(session => {
|
||||||
// SAFETY: By definition of `disposed`, `#reason` is defined.
|
// SAFETY: By definition of `disposed`, `#reason` is defined.
|
||||||
return session.#reason!;
|
return session.#reason!;
|
||||||
})
|
})
|
||||||
@ -161,7 +157,7 @@ export class Session
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@throwIfDisposed((session: Session) => {
|
@throwIfDisposed<Session>(session => {
|
||||||
// SAFETY: By definition of `disposed`, `#reason` is defined.
|
// SAFETY: By definition of `disposed`, `#reason` is defined.
|
||||||
return session.#reason!;
|
return session.#reason!;
|
||||||
})
|
})
|
||||||
@ -169,7 +165,16 @@ export class Session
|
|||||||
try {
|
try {
|
||||||
await this.send('session.end', {});
|
await this.send('session.end', {});
|
||||||
} finally {
|
} finally {
|
||||||
this.dispose(`Session (${this.id}) has already ended.`);
|
this.dispose(`Session already ended.`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[disposeSymbol](): void {
|
||||||
|
this.#reason ??=
|
||||||
|
'Session already destroyed, probably because the connection broke.';
|
||||||
|
this.emit('ended', {reason: this.#reason});
|
||||||
|
|
||||||
|
this.#disposables.dispose();
|
||||||
|
super[disposeSymbol]();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -8,7 +8,7 @@ import type * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js';
|
|||||||
|
|
||||||
import {EventEmitter} from '../../common/EventEmitter.js';
|
import {EventEmitter} from '../../common/EventEmitter.js';
|
||||||
import {assert} from '../../util/assert.js';
|
import {assert} from '../../util/assert.js';
|
||||||
import {throwIfDisposed} from '../../util/decorators.js';
|
import {inertIfDisposed, throwIfDisposed} from '../../util/decorators.js';
|
||||||
import {DisposableStack, disposeSymbol} from '../../util/disposable.js';
|
import {DisposableStack, disposeSymbol} from '../../util/disposable.js';
|
||||||
|
|
||||||
import type {Browser} from './Browser.js';
|
import type {Browser} from './Browser.js';
|
||||||
@ -35,6 +35,13 @@ export class UserContext extends EventEmitter<{
|
|||||||
/** The new browsing context. */
|
/** The new browsing context. */
|
||||||
browsingContext: BrowsingContext;
|
browsingContext: BrowsingContext;
|
||||||
};
|
};
|
||||||
|
/**
|
||||||
|
* Emitted when the user context is closed.
|
||||||
|
*/
|
||||||
|
closed: {
|
||||||
|
/** The reason the user context was closed. */
|
||||||
|
reason: string;
|
||||||
|
};
|
||||||
}> {
|
}> {
|
||||||
static create(browser: Browser, id: string): UserContext {
|
static create(browser: Browser, id: string): UserContext {
|
||||||
const context = new UserContext(browser, id);
|
const context = new UserContext(browser, id);
|
||||||
@ -46,16 +53,15 @@ export class UserContext extends EventEmitter<{
|
|||||||
#reason?: string;
|
#reason?: string;
|
||||||
// Note these are only top-level contexts.
|
// Note these are only top-level contexts.
|
||||||
readonly #browsingContexts = new Map<string, BrowsingContext>();
|
readonly #browsingContexts = new Map<string, BrowsingContext>();
|
||||||
|
readonly #disposables = new DisposableStack();
|
||||||
// @ts-expect-error -- TODO: This will be used once the WebDriver BiDi
|
// @ts-expect-error -- TODO: This will be used once the WebDriver BiDi
|
||||||
// protocol supports it.
|
// protocol supports it.
|
||||||
readonly #id: string;
|
readonly #id: string;
|
||||||
readonly #disposables = new DisposableStack();
|
|
||||||
readonly browser: Browser;
|
readonly browser: Browser;
|
||||||
// keep-sorted end
|
// keep-sorted end
|
||||||
|
|
||||||
private constructor(browser: Browser, id: string) {
|
private constructor(browser: Browser, id: string) {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
// keep-sorted start
|
// keep-sorted start
|
||||||
this.#id = id;
|
this.#id = id;
|
||||||
this.browser = browser;
|
this.browser = browser;
|
||||||
@ -63,9 +69,13 @@ export class UserContext extends EventEmitter<{
|
|||||||
}
|
}
|
||||||
|
|
||||||
#initialize() {
|
#initialize() {
|
||||||
// ////////////////////
|
const browserEmitter = this.#disposables.use(
|
||||||
// Session listeners //
|
new EventEmitter(this.browser)
|
||||||
// ////////////////////
|
);
|
||||||
|
browserEmitter.once('closed', ({reason}) => {
|
||||||
|
this.dispose(`User context already closed: ${reason}`);
|
||||||
|
});
|
||||||
|
|
||||||
const sessionEmitter = this.#disposables.use(
|
const sessionEmitter = this.#disposables.use(
|
||||||
new EventEmitter(this.#session)
|
new EventEmitter(this.#session)
|
||||||
);
|
);
|
||||||
@ -85,7 +95,9 @@ export class UserContext extends EventEmitter<{
|
|||||||
const browsingContextEmitter = this.#disposables.use(
|
const browsingContextEmitter = this.#disposables.use(
|
||||||
new EventEmitter(browsingContext)
|
new EventEmitter(browsingContext)
|
||||||
);
|
);
|
||||||
browsingContextEmitter.on('destroyed', () => {
|
browsingContextEmitter.on('closed', () => {
|
||||||
|
browsingContextEmitter.removeAllListeners();
|
||||||
|
|
||||||
this.#browsingContexts.delete(browsingContext.id);
|
this.#browsingContexts.delete(browsingContext.id);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -100,12 +112,16 @@ export class UserContext extends EventEmitter<{
|
|||||||
get browsingContexts(): Iterable<BrowsingContext> {
|
get browsingContexts(): Iterable<BrowsingContext> {
|
||||||
return this.#browsingContexts.values();
|
return this.#browsingContexts.values();
|
||||||
}
|
}
|
||||||
|
get closed(): boolean {
|
||||||
|
return this.#reason !== undefined;
|
||||||
|
}
|
||||||
get disposed(): boolean {
|
get disposed(): boolean {
|
||||||
return Boolean(this.#reason);
|
return this.closed;
|
||||||
}
|
}
|
||||||
// keep-sorted end
|
// keep-sorted end
|
||||||
|
|
||||||
dispose(reason?: string): void {
|
@inertIfDisposed
|
||||||
|
private dispose(reason?: string): void {
|
||||||
this.#reason = reason;
|
this.#reason = reason;
|
||||||
this[disposeSymbol]();
|
this[disposeSymbol]();
|
||||||
}
|
}
|
||||||
@ -136,23 +152,28 @@ export class UserContext extends EventEmitter<{
|
|||||||
return browsingContext;
|
return browsingContext;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@throwIfDisposed<UserContext>(context => {
|
||||||
|
// SAFETY: Disposal implies this exists.
|
||||||
|
return context.#reason!;
|
||||||
|
})
|
||||||
async close(): Promise<void> {
|
async close(): Promise<void> {
|
||||||
|
try {
|
||||||
const promises = [];
|
const promises = [];
|
||||||
for (const browsingContext of this.#browsingContexts.values()) {
|
for (const browsingContext of this.#browsingContexts.values()) {
|
||||||
promises.push(browsingContext.close());
|
promises.push(browsingContext.close());
|
||||||
}
|
}
|
||||||
await Promise.all(promises);
|
await Promise.all(promises);
|
||||||
this.dispose('User context was closed.');
|
} finally {
|
||||||
|
this.dispose('User context already closed.');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[disposeSymbol](): void {
|
[disposeSymbol](): void {
|
||||||
super[disposeSymbol]();
|
this.#reason ??=
|
||||||
|
'User context already closed, probably because the browser disconnected/closed.';
|
||||||
if (this.#reason === undefined) {
|
this.emit('closed', {reason: this.#reason});
|
||||||
this.#reason =
|
|
||||||
'User context was destroyed, probably because browser disconnected/closed.';
|
|
||||||
}
|
|
||||||
|
|
||||||
this.#disposables.dispose();
|
this.#disposables.dispose();
|
||||||
|
super[disposeSymbol]();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,7 +7,7 @@
|
|||||||
import type * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js';
|
import type * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js';
|
||||||
|
|
||||||
import {EventEmitter} from '../../common/EventEmitter.js';
|
import {EventEmitter} from '../../common/EventEmitter.js';
|
||||||
import {throwIfDisposed} from '../../util/decorators.js';
|
import {inertIfDisposed, throwIfDisposed} from '../../util/decorators.js';
|
||||||
import {DisposableStack, disposeSymbol} from '../../util/disposable.js';
|
import {DisposableStack, disposeSymbol} from '../../util/disposable.js';
|
||||||
|
|
||||||
import type {BrowsingContext} from './BrowsingContext.js';
|
import type {BrowsingContext} from './BrowsingContext.js';
|
||||||
@ -32,7 +32,13 @@ export type UserPromptResult = Omit<
|
|||||||
* @internal
|
* @internal
|
||||||
*/
|
*/
|
||||||
export class UserPrompt extends EventEmitter<{
|
export class UserPrompt extends EventEmitter<{
|
||||||
|
/** Emitted when the user prompt is handled. */
|
||||||
handled: UserPromptResult;
|
handled: UserPromptResult;
|
||||||
|
/** Emitted when the user prompt is closed. */
|
||||||
|
closed: {
|
||||||
|
/** The reason the user prompt was closed. */
|
||||||
|
reason: string;
|
||||||
|
};
|
||||||
}> {
|
}> {
|
||||||
static from(
|
static from(
|
||||||
browsingContext: BrowsingContext,
|
browsingContext: BrowsingContext,
|
||||||
@ -56,17 +62,20 @@ export class UserPrompt extends EventEmitter<{
|
|||||||
info: Bidi.BrowsingContext.UserPromptOpenedParameters
|
info: Bidi.BrowsingContext.UserPromptOpenedParameters
|
||||||
) {
|
) {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
// keep-sorted start
|
// keep-sorted start
|
||||||
this.info = info;
|
|
||||||
this.browsingContext = context;
|
this.browsingContext = context;
|
||||||
|
this.info = info;
|
||||||
// keep-sorted end
|
// keep-sorted end
|
||||||
}
|
}
|
||||||
|
|
||||||
#initialize() {
|
#initialize() {
|
||||||
// ////////////////////
|
const browserContextEmitter = this.#disposables.use(
|
||||||
// Session listeners //
|
new EventEmitter(this.browsingContext)
|
||||||
// ////////////////////
|
);
|
||||||
|
browserContextEmitter.once('closed', ({reason}) => {
|
||||||
|
this.dispose(`User prompt already closed: ${reason}`);
|
||||||
|
});
|
||||||
|
|
||||||
const sessionEmitter = this.#disposables.use(
|
const sessionEmitter = this.#disposables.use(
|
||||||
new EventEmitter(this.#session)
|
new EventEmitter(this.#session)
|
||||||
);
|
);
|
||||||
@ -76,7 +85,7 @@ export class UserPrompt extends EventEmitter<{
|
|||||||
}
|
}
|
||||||
this.#result = parameters;
|
this.#result = parameters;
|
||||||
this.emit('handled', parameters);
|
this.emit('handled', parameters);
|
||||||
this.dispose('User prompt was handled.');
|
this.dispose('User prompt already handled.');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -84,20 +93,27 @@ export class UserPrompt extends EventEmitter<{
|
|||||||
get #session() {
|
get #session() {
|
||||||
return this.browsingContext.userContext.browser.session;
|
return this.browsingContext.userContext.browser.session;
|
||||||
}
|
}
|
||||||
|
get closed(): boolean {
|
||||||
|
return this.#reason !== undefined;
|
||||||
|
}
|
||||||
get disposed(): boolean {
|
get disposed(): boolean {
|
||||||
return Boolean(this.#reason);
|
return this.closed;
|
||||||
|
}
|
||||||
|
get handled(): boolean {
|
||||||
|
return this.#result !== undefined;
|
||||||
}
|
}
|
||||||
get result(): UserPromptResult | undefined {
|
get result(): UserPromptResult | undefined {
|
||||||
return this.#result;
|
return this.#result;
|
||||||
}
|
}
|
||||||
// keep-sorted end
|
// keep-sorted end
|
||||||
|
|
||||||
dispose(reason?: string): void {
|
@inertIfDisposed
|
||||||
|
private dispose(reason?: string): void {
|
||||||
this.#reason = reason;
|
this.#reason = reason;
|
||||||
this[disposeSymbol]();
|
this[disposeSymbol]();
|
||||||
}
|
}
|
||||||
|
|
||||||
@throwIfDisposed((prompt: UserPrompt) => {
|
@throwIfDisposed<UserPrompt>(prompt => {
|
||||||
// SAFETY: Disposal implies this exists.
|
// SAFETY: Disposal implies this exists.
|
||||||
return prompt.#reason!;
|
return prompt.#reason!;
|
||||||
})
|
})
|
||||||
@ -111,13 +127,11 @@ export class UserPrompt extends EventEmitter<{
|
|||||||
}
|
}
|
||||||
|
|
||||||
[disposeSymbol](): void {
|
[disposeSymbol](): void {
|
||||||
super[disposeSymbol]();
|
this.#reason ??=
|
||||||
|
'User prompt already closed, probably because the associated browsing context was destroyed.';
|
||||||
if (this.#reason === undefined) {
|
this.emit('closed', {reason: this.#reason});
|
||||||
this.#reason =
|
|
||||||
'User prompt was destroyed, probably because the associated browsing context was destroyed.';
|
|
||||||
}
|
|
||||||
|
|
||||||
this.#disposables.dispose();
|
this.#disposables.dispose();
|
||||||
|
super[disposeSymbol]();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -63,6 +63,18 @@ export function throwIfDisposed<This extends Disposed>(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function inertIfDisposed<This extends Disposed>(
|
||||||
|
target: (this: This, ...args: any[]) => any,
|
||||||
|
_: unknown
|
||||||
|
) {
|
||||||
|
return function (this: This, ...args: any[]): any {
|
||||||
|
if (this.disposed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
return target.call(this, ...args);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The decorator only invokes the target if the target has not been invoked with
|
* The decorator only invokes the target if the target has not been invoked with
|
||||||
* the same arguments before. The decorated method throws an error if it's
|
* the same arguments before. The decorated method throws an error if it's
|
||||||
|
Loading…
Reference in New Issue
Block a user