/** * @license * Copyright 2024 Google Inc. * 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 {debugError} from '../../common/util.js'; import {inertIfDisposed, throwIfDisposed} from '../../util/decorators.js'; import {DisposableStack, disposeSymbol} from '../../util/disposable.js'; import {Browser} from './Browser.js'; import type {BidiEvents, Commands, Connection} from './Connection.js'; // TODO: Once Chrome supports session.status properly, uncomment this block. // const MAX_RETRIES = 5; /** * @internal */ export class Session extends EventEmitter implements Connection { static async from( connection: Connection, capabilities: Bidi.Session.CapabilitiesRequest ): Promise { // Wait until the session is ready. // // TODO: Once Chrome supports session.status properly, uncomment this block // and remove `getBiDiConnection` in BrowserConnector. // let status = {message: '', ready: false}; // for (let i = 0; i < MAX_RETRIES; ++i) { // status = (await connection.send('session.status', {})).result; // if (status.ready) { // break; // } // // Backoff a little bit each time. // await new Promise(resolve => { // return setTimeout(resolve, (1 << i) * 100); // }); // } // if (!status.ready) { // throw new Error(status.message); // } let result; try { result = ( await connection.send('session.new', { capabilities, }) ).result; } catch (err) { // Chrome does not support session.new. debugError(err); result = { sessionId: '', capabilities: { acceptInsecureCerts: false, browserName: '', browserVersion: '', platformName: '', setWindowRect: false, webSocketUrl: '', }, }; } const session = new Session(connection, result); await session.#initialize(); return session; } // keep-sorted start #reason: string | undefined; readonly #disposables = new DisposableStack(); readonly #info: Bidi.Session.NewResult; readonly browser!: Browser; readonly connection: Connection; // keep-sorted end private constructor(connection: Connection, info: Bidi.Session.NewResult) { super(); // keep-sorted start this.#info = info; this.connection = connection; // keep-sorted end } async #initialize(): Promise { this.connection.pipeTo(this); // SAFETY: We use `any` to allow assignment of the readonly property. (this as any).browser = await Browser.from(this); const browserEmitter = this.#disposables.use(this.browser); browserEmitter.once('closed', ({reason}) => { this.dispose(reason); }); } // keep-sorted start block=yes get capabilities(): Bidi.Session.NewResult['capabilities'] { return this.#info.capabilities; } get disposed(): boolean { return this.ended; } get ended(): boolean { return this.#reason !== undefined; } get id(): string { return this.#info.sessionId; } // keep-sorted end @inertIfDisposed private dispose(reason?: string): void { this.#reason = reason; this[disposeSymbol](); } pipeTo(emitter: EventEmitter): 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 * case we always needs to make sure that the session for the right session * object is used, so we implement this method here, although it's not defined * in the spec. */ @throwIfDisposed(session => { // SAFETY: By definition of `disposed`, `#reason` is defined. return session.#reason!; }) async send( method: T, params: Commands[T]['params'] ): Promise<{result: Commands[T]['returnType']}> { return await this.connection.send(method, params); } @throwIfDisposed(session => { // SAFETY: By definition of `disposed`, `#reason` is defined. return session.#reason!; }) async subscribe(events: string[]): Promise { await this.send('session.subscribe', { events, }); } @throwIfDisposed(session => { // SAFETY: By definition of `disposed`, `#reason` is defined. return session.#reason!; }) async end(): Promise { try { await this.send('session.end', {}); } finally { 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](); } }