11import { WebSocket } from 'ws' ;
22
33/**
4- * Configuration options for the CDP client.
4+ * Configuration options for the Chrome Developer Protocol ( CDP) client.
55 */
66export interface CDPClientOptions {
77 /**
@@ -78,20 +78,20 @@ type EventHandler = (params: unknown) => void;
7878 * ```
7979 */
8080export class CDPClient {
81- private _ws : WebSocket | null ;
82- private _messageId : number ;
83- private _pendingRequests : Map < number , PendingRequest > ;
84- private _eventHandlers : Map < string , Set < EventHandler > > ;
85- private _connected : boolean ;
86- private readonly _options : Required < CDPClientOptions > ;
81+ #ws : WebSocket | null ;
82+ #messageId : number ;
83+ #pendingRequests : Map < number , PendingRequest > ;
84+ #eventHandlers : Map < string , Set < EventHandler > > ;
85+ #connected : boolean ;
86+ readonly #options : Required < CDPClientOptions > ;
8787
8888 public constructor ( options : CDPClientOptions ) {
89- this . _ws = null ;
90- this . _messageId = 0 ;
91- this . _pendingRequests = new Map ( ) ;
92- this . _eventHandlers = new Map ( ) ;
93- this . _connected = false ;
94- this . _options = {
89+ this . #ws = null ;
90+ this . #messageId = 0 ;
91+ this . #pendingRequests = new Map ( ) ;
92+ this . #eventHandlers = new Map ( ) ;
93+ this . #connected = false ;
94+ this . #options = {
9595 retries : 5 ,
9696 retryDelayMs : 1000 ,
9797 connectionTimeoutMs : 10000 ,
@@ -106,14 +106,14 @@ export class CDPClient {
106106 * Will retry according to the configured retry settings.
107107 */
108108 public async connect ( ) : Promise < void > {
109- const { retries, retryDelayMs } = this . _options ;
109+ const { retries, retryDelayMs } = this . #options ;
110110
111111 for ( let attempt = 1 ; attempt <= retries ; attempt ++ ) {
112112 try {
113- await this . _tryConnect ( ) ;
113+ await this . #tryConnect ( ) ;
114114 return ;
115115 } catch ( err ) {
116- this . _log ( `Connection attempt ${ attempt } /${ retries } failed:` , ( err as Error ) . message ) ;
116+ this . #log ( `Connection attempt ${ attempt } /${ retries } failed:` , ( err as Error ) . message ) ;
117117 if ( attempt < retries ) {
118118 await new Promise ( resolve => setTimeout ( resolve , retryDelayMs ) ) ;
119119 } else {
@@ -132,27 +132,26 @@ export class CDPClient {
132132 * @returns The result from the CDP method
133133 */
134134 public async send < T = unknown > ( method : string , params ?: Record < string , unknown > , timeoutMs ?: number ) : Promise < T > {
135- if ( ! this . _ws || this . _ws . readyState !== WebSocket . OPEN ) {
135+ if ( ! this . #ws || this . #ws . readyState !== WebSocket . OPEN ) {
136136 throw new Error ( 'WebSocket not connected' ) ;
137137 }
138138
139- const timeout = timeoutMs ?? this . _options . defaultTimeoutMs ;
140- const id = ++ this . _messageId ;
139+ const timeout = timeoutMs ?? this . #options . defaultTimeoutMs ;
140+ const id = ++ this . #messageId ;
141141 const message = JSON . stringify ( { id, method, params } ) ;
142142
143- this . _log ( 'Sending:' , method , params || '' ) ;
143+ this . #log ( 'Sending:' , method , params || '' ) ;
144144
145145 return new Promise ( ( resolve , reject ) => {
146- this . _pendingRequests . set ( id , {
146+ this . #pendingRequests . set ( id , {
147147 resolve : value => resolve ( value as T ) ,
148148 reject,
149149 } ) ;
150- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
151- this . _ws ! . send ( message ) ;
150+ this . #ws! . send ( message ) ;
152151
153152 setTimeout ( ( ) => {
154- if ( this . _pendingRequests . has ( id ) ) {
155- this . _pendingRequests . delete ( id ) ;
153+ if ( this . #pendingRequests . has ( id ) ) {
154+ this . #pendingRequests . delete ( id ) ;
156155 reject ( new Error ( `CDP request ${ method } timed out after ${ timeout } ms` ) ) ;
157156 }
158157 } , timeout ) ;
@@ -168,16 +167,16 @@ export class CDPClient {
168167 * @param settleDelayMs - Time to wait after sending (default: 100ms)
169168 */
170169 public async sendFireAndForget ( method : string , params ?: Record < string , unknown > , settleDelayMs = 100 ) : Promise < void > {
171- if ( ! this . _ws || this . _ws . readyState !== WebSocket . OPEN ) {
170+ if ( ! this . #ws || this . #ws . readyState !== WebSocket . OPEN ) {
172171 throw new Error ( 'WebSocket not connected' ) ;
173172 }
174173
175- const id = ++ this . _messageId ;
174+ const id = ++ this . #messageId ;
176175 const message = JSON . stringify ( { id, method, params } ) ;
177176
178- this . _log ( 'Sending (fire-and-forget):' , method , params || '' ) ;
177+ this . #log ( 'Sending (fire-and-forget):' , method , params || '' ) ;
179178
180- this . _ws . send ( message ) ;
179+ this . #ws . send ( message ) ;
181180
182181 // Give the command time to execute
183182 await new Promise ( resolve => setTimeout ( resolve , settleDelayMs ) ) ;
@@ -188,17 +187,17 @@ export class CDPClient {
188187 * Returns a function that, when called, removes the handler.
189188 */
190189 public on ( method : string , handler : EventHandler ) : ( ) => void {
191- let handlers = this . _eventHandlers . get ( method ) ;
190+ let handlers = this . #eventHandlers . get ( method ) ;
192191 if ( ! handlers ) {
193192 handlers = new Set ( ) ;
194- this . _eventHandlers . set ( method , handlers ) ;
193+ this . #eventHandlers . set ( method , handlers ) ;
195194 }
196195 handlers . add ( handler ) ;
197196
198197 return ( ) => {
199198 handlers . delete ( handler ) ;
200199 if ( handlers . size === 0 ) {
201- this . _eventHandlers . delete ( method ) ;
200+ this . #eventHandlers . delete ( method ) ;
202201 }
203202 } ;
204203 }
@@ -207,101 +206,113 @@ export class CDPClient {
207206 * Check if the client is currently connected.
208207 */
209208 public isConnected ( ) : boolean {
210- return this . _connected && this . _ws ?. readyState === WebSocket . OPEN ;
209+ return this . #connected && this . #ws ?. readyState === WebSocket . OPEN ;
211210 }
212211
213212 /**
214213 * Close the WebSocket connection.
215214 */
216215 public async close ( ) : Promise < void > {
217- if ( this . _ws ) {
218- this . _ws . close ( ) ;
219- this . _ws = null ;
220- this . _connected = false ;
216+ if ( this . #ws ) {
217+ this . #ws . close ( ) ;
218+ this . #ws = null ;
219+ this . #connected = false ;
221220 }
222221 }
223222
224- private _log ( ...args : unknown [ ] ) : void {
225- if ( this . _options . debug ) {
223+ #log ( ...args : unknown [ ] ) : void {
224+ if ( this . #options . debug ) {
226225 // eslint-disable-next-line no-console
227226 console . log ( '[CDPClient]' , ...args ) ;
228227 }
229228 }
230229
231- private async _tryConnect ( ) : Promise < void > {
232- const { url, connectionTimeoutMs } = this . _options ;
230+ async #tryConnect ( ) : Promise < void > {
231+ const { url, connectionTimeoutMs } = this . #options ;
233232
234233 return new Promise ( ( resolve , reject ) => {
234+ this . #ws = new WebSocket ( url ) ;
235+
235236 const timeoutId = setTimeout ( ( ) => {
237+ // Close the WebSocket to prevent state corruption from orphaned sockets on retry
238+ this . #ws?. close ( ) ;
236239 reject ( new Error ( `Connection to ${ url } timed out after ${ connectionTimeoutMs } ms` ) ) ;
237240 } , connectionTimeoutMs ) ;
238241
239- this . _ws = new WebSocket ( url ) ;
240-
241- this . _ws . on ( 'open' , ( ) => {
242+ this . #ws. on ( 'open' , ( ) => {
242243 clearTimeout ( timeoutId ) ;
243- this . _connected = true ;
244- this . _log ( 'WebSocket connected to' , url ) ;
244+ this . #connected = true ;
245+ this . #log ( 'WebSocket connected to' , url ) ;
245246 resolve ( ) ;
246247 } ) ;
247248
248- this . _ws . on ( 'error' , ( err : Error ) => {
249+ this . #ws . on ( 'error' , ( err : Error ) => {
249250 clearTimeout ( timeoutId ) ;
251+ this . #ws?. close ( ) ;
250252 reject ( new Error ( `Failed to connect to inspector at ${ url } : ${ err . message } ` ) ) ;
251253 } ) ;
252254
253- this . _ws . on ( 'close' , ( ) => {
254- this . _connected = false ;
255+ this . #ws . on ( 'close' , ( ) => {
256+ this . #connected = false ;
255257 } ) ;
256258
257- this . _ws . on ( 'message' , ( data : Buffer ) => {
258- try {
259- const rawMessage = data . toString ( ) ;
260- this . _log ( 'Received raw message:' , rawMessage . slice ( 0 , 500 ) ) ;
261-
262- const message = JSON . parse ( rawMessage ) as CDPResponse ;
263-
264- // CDP event (not a response to our request)
265- if ( message . method ) {
266- this . _log ( 'CDP event:' , message . method ) ;
267- const handlers = this . _eventHandlers . get ( message . method ) ;
268- if ( handlers ) {
269- for ( const handler of handlers ) {
270- try {
271- handler ( message . params ) ;
272- } catch ( err ) {
273- this . _log ( 'Event handler threw:' , err ) ;
274- }
275- }
276- }
277- return ;
278- }
279-
280- if ( message . id !== undefined ) {
281- this . _log (
282- 'CDP response for id:' ,
283- message . id ,
284- 'error:' ,
285- message . error ,
286- 'has result:' ,
287- message . result !== undefined ,
288- ) ;
289- const pending = this . _pendingRequests . get ( message . id ) ;
290- if ( pending ) {
291- this . _pendingRequests . delete ( message . id ) ;
292- if ( message . error ) {
293- pending . reject ( new Error ( `CDP error: ${ message . error . message } ` ) ) ;
294- } else {
295- pending . resolve ( message . result ) ;
296- }
297- } else {
298- this . _log ( 'No pending request found for id:' , message . id ) ;
299- }
300- }
301- } catch ( e ) {
302- this . _log ( 'Failed to parse CDP message:' , e ) ;
259+ this . #setupMessageHandler( ) ;
260+ } ) ;
261+ }
262+
263+ #setupMessageHandler( ) : void {
264+ this . #ws?. on ( 'message' , ( data : Buffer ) => {
265+ try {
266+ const rawMessage = data . toString ( ) ;
267+ this . #log( 'Received raw message:' , rawMessage . slice ( 0 , 500 ) ) ;
268+
269+ const message = JSON . parse ( rawMessage ) as CDPResponse ;
270+
271+ if ( message . method ) {
272+ this . #handleCdpEvent( message ) ;
273+ return ;
303274 }
304- } ) ;
275+
276+ if ( message . id !== undefined ) {
277+ this . #handleCdpResponse( message ) ;
278+ }
279+ } catch ( e ) {
280+ this . #log( 'Failed to parse CDP message:' , e ) ;
281+ }
305282 } ) ;
306283 }
284+
285+ #handleCdpEvent( message : CDPResponse ) : void {
286+ this . #log( 'CDP event:' , message . method ) ;
287+
288+ const handlers = this . #eventHandlers. get ( message . method ! ) ;
289+
290+ if ( handlers ) {
291+ for ( const handler of handlers ) {
292+ try {
293+ handler ( message . params ) ;
294+ } catch ( err ) {
295+ this . #log( 'Event handler threw:' , err ) ;
296+ }
297+ }
298+ }
299+ }
300+
301+ #handleCdpResponse( message : CDPResponse ) : void {
302+ this . #log( 'CDP response for id:' , message . id , 'error:' , message . error , 'has result:' , message . result !== undefined ) ;
303+
304+ const pending = this . #pendingRequests. get ( message . id ! ) ;
305+
306+ if ( pending ) {
307+ this . #pendingRequests. delete ( message . id ! ) ;
308+
309+ if ( message . error ) {
310+ pending . reject ( new Error ( `CDP error: ${ message . error . message } ` ) ) ;
311+ } else {
312+ pending . resolve ( message . result ) ;
313+ }
314+ } else {
315+ this . #log( 'No pending request found for id:' , message . id ) ;
316+ }
317+ }
307318}
0 commit comments