CDPSession.ts (3336B)
1 /** 2 * @license 3 * Copyright 2024 Google Inc. 4 * SPDX-License-Identifier: Apache-2.0 5 */ 6 import type ProtocolMapping from 'devtools-protocol/types/protocol-mapping.js'; 7 8 import type {CommandOptions} from '../api/CDPSession.js'; 9 import {CDPSession} from '../api/CDPSession.js'; 10 import type {Connection as CdpConnection} from '../cdp/Connection.js'; 11 import {TargetCloseError, UnsupportedOperation} from '../common/Errors.js'; 12 import {Deferred} from '../util/Deferred.js'; 13 14 import type {BidiConnection} from './Connection.js'; 15 import type {BidiFrame} from './Frame.js'; 16 17 /** 18 * @internal 19 */ 20 export class BidiCdpSession extends CDPSession { 21 static sessions = new Map<string, BidiCdpSession>(); 22 23 #detached = false; 24 readonly #connection?: BidiConnection; 25 readonly #sessionId = Deferred.create<string>(); 26 readonly frame: BidiFrame; 27 28 constructor(frame: BidiFrame, sessionId?: string) { 29 super(); 30 this.frame = frame; 31 if (!this.frame.page().browser().cdpSupported) { 32 return; 33 } 34 35 const connection = this.frame.page().browser().connection; 36 this.#connection = connection; 37 38 if (sessionId) { 39 this.#sessionId.resolve(sessionId); 40 BidiCdpSession.sessions.set(sessionId, this); 41 } else { 42 (async () => { 43 try { 44 const {result} = await connection.send('goog:cdp.getSession', { 45 context: frame._id, 46 }); 47 this.#sessionId.resolve(result.session!); 48 BidiCdpSession.sessions.set(result.session!, this); 49 } catch (error) { 50 this.#sessionId.reject(error as Error); 51 } 52 })(); 53 } 54 55 // SAFETY: We never throw #sessionId. 56 BidiCdpSession.sessions.set(this.#sessionId.value() as string, this); 57 } 58 59 override connection(): CdpConnection | undefined { 60 return undefined; 61 } 62 63 override get detached(): boolean { 64 return this.#detached; 65 } 66 67 override async send<T extends keyof ProtocolMapping.Commands>( 68 method: T, 69 params?: ProtocolMapping.Commands[T]['paramsType'][0], 70 options?: CommandOptions, 71 ): Promise<ProtocolMapping.Commands[T]['returnType']> { 72 if (this.#connection === undefined) { 73 throw new UnsupportedOperation( 74 'CDP support is required for this feature. The current browser does not support CDP.', 75 ); 76 } 77 if (this.#detached) { 78 throw new TargetCloseError( 79 `Protocol error (${method}): Session closed. Most likely the page has been closed.`, 80 ); 81 } 82 const session = await this.#sessionId.valueOrThrow(); 83 const {result} = await this.#connection.send( 84 'goog:cdp.sendCommand', 85 { 86 method: method, 87 params: params, 88 session, 89 }, 90 options?.timeout, 91 ); 92 return result.result; 93 } 94 95 override async detach(): Promise<void> { 96 if ( 97 this.#connection === undefined || 98 this.#connection.closed || 99 this.#detached 100 ) { 101 return; 102 } 103 try { 104 await this.frame.client.send('Target.detachFromTarget', { 105 sessionId: this.id(), 106 }); 107 } finally { 108 this.onClose(); 109 } 110 } 111 112 /** 113 * @internal 114 */ 115 onClose = (): void => { 116 BidiCdpSession.sessions.delete(this.id()); 117 this.#detached = true; 118 }; 119 120 override id(): string { 121 const value = this.#sessionId.value(); 122 return typeof value === 'string' ? value : ''; 123 } 124 }