Skip to content

Commit ba673f9

Browse files
committed
fixup! test(test-utils): Add MemoryProfiler for heap snapshot testing via CDP
1 parent 55510e9 commit ba673f9

2 files changed

Lines changed: 151 additions & 126 deletions

File tree

‎dev-packages/test-utils/src/cdp-client.ts‎

Lines changed: 107 additions & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { WebSocket } from 'ws';
22

33
/**
4-
* Configuration options for the CDP client.
4+
* Configuration options for the Chrome Developer Protocol (CDP) client.
55
*/
66
export interface CDPClientOptions {
77
/**
@@ -78,20 +78,20 @@ type EventHandler = (params: unknown) => void;
7878
* ```
7979
*/
8080
export 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

Comments
 (0)