chrome-cdp-cli 1.5.0 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,430 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.ConnectionPool = void 0;
7
+ const ws_1 = __importDefault(require("ws"));
8
+ const ConnectionManager_1 = require("../../connection/ConnectionManager");
9
+ const logger_1 = require("../../utils/logger");
10
+ class ConnectionPool {
11
+ constructor(eventMonitor, messageStore, config) {
12
+ this.connections = new Map();
13
+ this.connectionsByKey = new Map();
14
+ this.reconnectionAttempts = new Map();
15
+ this.reconnectionTimers = new Map();
16
+ this.connectionManager = new ConnectionManager_1.ConnectionManager();
17
+ this.logger = (0, logger_1.createLogger)({ component: 'ConnectionPool' });
18
+ this.eventMonitor = eventMonitor;
19
+ this.messageStore = messageStore;
20
+ this.config = {
21
+ reconnectMaxAttempts: 5,
22
+ reconnectBackoffMs: 1000,
23
+ ...config
24
+ };
25
+ }
26
+ async getOrCreateConnection(host, port, targetId) {
27
+ try {
28
+ const targets = await this.connectionManager.discoverTargets(host, port);
29
+ if (targets.length === 0) {
30
+ throw new Error(`No Chrome targets found at ${host}:${port}`);
31
+ }
32
+ let target;
33
+ if (targetId) {
34
+ const foundTarget = targets.find(t => t.id === targetId);
35
+ if (!foundTarget) {
36
+ throw new Error(`Target ${targetId} not found`);
37
+ }
38
+ target = foundTarget;
39
+ }
40
+ else {
41
+ const pageTarget = targets.find(t => t.type === 'page');
42
+ if (!pageTarget) {
43
+ throw new Error('No page targets available');
44
+ }
45
+ target = pageTarget;
46
+ }
47
+ const connectionKey = this.createConnectionKey(host, port, target.id);
48
+ const existingConnectionId = this.connectionsByKey.get(connectionKey);
49
+ if (existingConnectionId) {
50
+ const existingConnection = this.connections.get(existingConnectionId);
51
+ if (existingConnection && existingConnection.isHealthy) {
52
+ existingConnection.lastUsed = Date.now();
53
+ existingConnection.clientCount++;
54
+ this.logger.logConnectionEvent('established', existingConnectionId, `Reusing existing connection for ${connectionKey}`, {
55
+ connectionKey,
56
+ clientCount: existingConnection.clientCount,
57
+ target: {
58
+ id: target.id,
59
+ title: target.title,
60
+ url: target.url
61
+ }
62
+ });
63
+ return existingConnection;
64
+ }
65
+ else {
66
+ if (existingConnection) {
67
+ await this.closeConnection(existingConnectionId);
68
+ }
69
+ }
70
+ }
71
+ const connectionId = this.generateConnectionId();
72
+ const wsUrl = target.webSocketDebuggerUrl;
73
+ if (!wsUrl) {
74
+ throw new Error(`No WebSocket URL available for target ${target.id}`);
75
+ }
76
+ this.logger.logConnectionEvent('established', connectionId, `Creating new CDP connection to ${wsUrl}`, {
77
+ connectionKey,
78
+ wsUrl,
79
+ target: {
80
+ id: target.id,
81
+ title: target.title,
82
+ url: target.url,
83
+ type: target.type
84
+ }
85
+ });
86
+ const ws = new ws_1.default(wsUrl);
87
+ await new Promise((resolve, reject) => {
88
+ const timeout = setTimeout(() => {
89
+ reject(new Error('Connection timeout'));
90
+ }, 10000);
91
+ ws.on('open', () => {
92
+ clearTimeout(timeout);
93
+ resolve();
94
+ });
95
+ ws.on('error', (error) => {
96
+ clearTimeout(timeout);
97
+ reject(error);
98
+ });
99
+ });
100
+ const connectionInfo = {
101
+ id: connectionId,
102
+ host,
103
+ port,
104
+ targetId: target.id,
105
+ wsUrl,
106
+ connection: ws,
107
+ createdAt: Date.now(),
108
+ lastUsed: Date.now(),
109
+ isHealthy: true,
110
+ clientCount: 1
111
+ };
112
+ this.connections.set(connectionId, connectionInfo);
113
+ this.connectionsByKey.set(connectionKey, connectionId);
114
+ this.setupConnectionHandlers(connectionInfo);
115
+ if (this.eventMonitor) {
116
+ try {
117
+ await this.eventMonitor.startMonitoring(connectionInfo);
118
+ }
119
+ catch (error) {
120
+ this.logger.logConnectionEvent('established', connectionId, 'Failed to start event monitoring', { connectionKey }, error);
121
+ }
122
+ }
123
+ this.logger.logConnectionEvent('established', connectionId, 'CDP connection established successfully', {
124
+ connectionKey,
125
+ clientCount: connectionInfo.clientCount,
126
+ monitoringEnabled: !!this.eventMonitor
127
+ });
128
+ return connectionInfo;
129
+ }
130
+ catch (error) {
131
+ const errorConnectionId = 'unknown';
132
+ const errorConnectionKey = `${host}:${port}:${targetId}`;
133
+ this.logger.logConnectionEvent('established', errorConnectionId, `Failed to create connection for ${host}:${port}:${targetId}`, { connectionKey: errorConnectionKey, host, port, targetId }, error);
134
+ throw error;
135
+ }
136
+ }
137
+ async closeConnection(connectionId) {
138
+ const connection = this.connections.get(connectionId);
139
+ if (!connection) {
140
+ this.logger.warn(`Connection ${connectionId} not found for closing`);
141
+ return;
142
+ }
143
+ try {
144
+ this.logger.info(`Closing CDP connection ${connectionId}`);
145
+ this.reconnectionAttempts.delete(connectionId);
146
+ this.clearReconnectionTimer(connectionId);
147
+ if (this.eventMonitor) {
148
+ await this.eventMonitor.stopMonitoring(connectionId);
149
+ }
150
+ if (this.messageStore) {
151
+ this.messageStore.cleanupConnection(connectionId);
152
+ }
153
+ if (connection.connection.readyState === ws_1.default.OPEN) {
154
+ connection.connection.close();
155
+ }
156
+ const connectionKey = this.createConnectionKey(connection.host, connection.port, connection.targetId);
157
+ this.connections.delete(connectionId);
158
+ this.connectionsByKey.delete(connectionKey);
159
+ this.logger.info(`CDP connection ${connectionId} closed successfully`);
160
+ }
161
+ catch (error) {
162
+ this.logger.error(`Error closing connection ${connectionId}:`, error);
163
+ }
164
+ }
165
+ async healthCheck(connectionId) {
166
+ const connection = this.connections.get(connectionId);
167
+ if (!connection) {
168
+ return false;
169
+ }
170
+ try {
171
+ if (connection.connection.readyState !== ws_1.default.OPEN) {
172
+ connection.isHealthy = false;
173
+ return false;
174
+ }
175
+ const messageId = Math.floor(Date.now() % 1000000) + Math.floor(Math.random() * 1000);
176
+ const testMessage = {
177
+ id: messageId,
178
+ method: 'Runtime.evaluate',
179
+ params: { expression: '1+1' }
180
+ };
181
+ const healthCheckPromise = new Promise((resolve) => {
182
+ const timeout = setTimeout(() => {
183
+ resolve(false);
184
+ }, 5000);
185
+ const messageHandler = (data) => {
186
+ try {
187
+ const response = JSON.parse(data.toString());
188
+ if (response.id === testMessage.id) {
189
+ clearTimeout(timeout);
190
+ connection.connection.off('message', messageHandler);
191
+ resolve(response.result && !response.error);
192
+ }
193
+ }
194
+ catch (error) {
195
+ }
196
+ };
197
+ connection.connection.on('message', messageHandler);
198
+ connection.connection.send(JSON.stringify(testMessage));
199
+ });
200
+ const isHealthy = await healthCheckPromise;
201
+ connection.isHealthy = isHealthy;
202
+ if (!isHealthy) {
203
+ this.logger.warn(`Health check failed for connection ${connectionId}`);
204
+ }
205
+ return isHealthy;
206
+ }
207
+ catch (error) {
208
+ this.logger.error(`Health check error for connection ${connectionId}:`, error);
209
+ connection.isHealthy = false;
210
+ return false;
211
+ }
212
+ }
213
+ async reconnect(connectionId) {
214
+ const connection = this.connections.get(connectionId);
215
+ if (!connection) {
216
+ this.logger.warn(`Cannot reconnect: connection ${connectionId} not found`);
217
+ return false;
218
+ }
219
+ const currentAttempts = this.reconnectionAttempts.get(connectionId) || 0;
220
+ const maxAttempts = this.config.reconnectMaxAttempts || 5;
221
+ if (currentAttempts >= maxAttempts) {
222
+ this.logger.error(`Maximum reconnection attempts (${maxAttempts}) exceeded for connection ${connectionId}`);
223
+ await this.handleReconnectionFailure(connectionId);
224
+ return false;
225
+ }
226
+ this.reconnectionAttempts.set(connectionId, currentAttempts + 1);
227
+ try {
228
+ this.logger.info(`Attempting reconnection ${currentAttempts + 1}/${maxAttempts} for connection ${connectionId}`);
229
+ const baseDelay = this.config.reconnectBackoffMs || 1000;
230
+ const delay = baseDelay * Math.pow(2, currentAttempts);
231
+ const jitter = Math.random() * 0.1 * delay;
232
+ const totalDelay = delay + jitter;
233
+ this.logger.debug(`Waiting ${Math.round(totalDelay)}ms before reconnection attempt`);
234
+ await new Promise(resolve => setTimeout(resolve, totalDelay));
235
+ const preservedData = this.preserveConnectionData(connectionId);
236
+ await this.closeExistingConnection(connection);
237
+ const success = await this.establishNewConnection(connection);
238
+ if (success) {
239
+ await this.restoreConnectionData(connectionId, preservedData);
240
+ this.reconnectionAttempts.delete(connectionId);
241
+ this.clearReconnectionTimer(connectionId);
242
+ this.logger.info(`Connection ${connectionId} reconnected successfully after ${currentAttempts + 1} attempts`);
243
+ return true;
244
+ }
245
+ else {
246
+ this.scheduleReconnectionAttempt(connectionId);
247
+ return false;
248
+ }
249
+ }
250
+ catch (error) {
251
+ this.logger.error(`Reconnection attempt ${currentAttempts + 1} failed for connection ${connectionId}:`, error);
252
+ connection.isHealthy = false;
253
+ this.scheduleReconnectionAttempt(connectionId);
254
+ return false;
255
+ }
256
+ }
257
+ scheduleReconnectionAttempt(connectionId) {
258
+ this.clearReconnectionTimer(connectionId);
259
+ const currentAttempts = this.reconnectionAttempts.get(connectionId) || 0;
260
+ const maxAttempts = this.config.reconnectMaxAttempts || 5;
261
+ if (currentAttempts < maxAttempts) {
262
+ const baseDelay = this.config.reconnectBackoffMs || 1000;
263
+ const delay = baseDelay * Math.pow(2, currentAttempts);
264
+ this.logger.debug(`Scheduling next reconnection attempt for connection ${connectionId} in ${delay}ms`);
265
+ const timer = setTimeout(async () => {
266
+ await this.reconnect(connectionId);
267
+ }, delay);
268
+ this.reconnectionTimers.set(connectionId, timer);
269
+ }
270
+ else {
271
+ this.logger.error(`All reconnection attempts exhausted for connection ${connectionId}`);
272
+ this.handleReconnectionFailure(connectionId);
273
+ }
274
+ }
275
+ preserveConnectionData(connectionId) {
276
+ const preservedData = {};
277
+ if (this.messageStore) {
278
+ try {
279
+ preservedData.consoleMessages = this.messageStore.getConsoleMessages(connectionId);
280
+ preservedData.networkRequests = this.messageStore.getNetworkRequests(connectionId);
281
+ this.logger.debug(`Preserved ${preservedData.consoleMessages.length} console messages and ${preservedData.networkRequests.length} network requests for connection ${connectionId}`);
282
+ }
283
+ catch (error) {
284
+ this.logger.warn(`Failed to preserve data for connection ${connectionId}:`, error);
285
+ }
286
+ }
287
+ return preservedData;
288
+ }
289
+ async restoreConnectionData(connectionId, preservedData) {
290
+ if (!this.messageStore || !preservedData) {
291
+ return;
292
+ }
293
+ try {
294
+ this.logger.debug(`Data preservation verified for connection ${connectionId}`);
295
+ }
296
+ catch (error) {
297
+ this.logger.warn(`Failed to restore data for connection ${connectionId}:`, error);
298
+ }
299
+ }
300
+ async closeExistingConnection(connection) {
301
+ try {
302
+ if (connection.connection.readyState === ws_1.default.OPEN) {
303
+ connection.connection.close(1000, 'Reconnecting');
304
+ }
305
+ await new Promise(resolve => setTimeout(resolve, 100));
306
+ }
307
+ catch (error) {
308
+ this.logger.warn(`Error during graceful connection closure:`, error);
309
+ }
310
+ }
311
+ async establishNewConnection(connection) {
312
+ try {
313
+ const ws = new ws_1.default(connection.wsUrl);
314
+ await new Promise((resolve, reject) => {
315
+ const timeout = setTimeout(() => {
316
+ reject(new Error('Reconnection timeout after 10 seconds'));
317
+ }, 10000);
318
+ ws.on('open', () => {
319
+ clearTimeout(timeout);
320
+ resolve();
321
+ });
322
+ ws.on('error', (error) => {
323
+ clearTimeout(timeout);
324
+ reject(error);
325
+ });
326
+ });
327
+ connection.connection = ws;
328
+ connection.isHealthy = true;
329
+ connection.lastUsed = Date.now();
330
+ this.setupConnectionHandlers(connection);
331
+ if (this.eventMonitor) {
332
+ try {
333
+ await this.eventMonitor.startMonitoring(connection);
334
+ }
335
+ catch (error) {
336
+ this.logger.warn(`Failed to restart event monitoring for connection ${connection.id}:`, error);
337
+ }
338
+ }
339
+ return true;
340
+ }
341
+ catch (error) {
342
+ this.logger.error(`Failed to establish new connection:`, error);
343
+ connection.isHealthy = false;
344
+ return false;
345
+ }
346
+ }
347
+ async handleReconnectionFailure(connectionId) {
348
+ this.logger.error(`Reconnection failed permanently for connection ${connectionId}, cleaning up`);
349
+ this.reconnectionAttempts.delete(connectionId);
350
+ this.clearReconnectionTimer(connectionId);
351
+ const connection = this.connections.get(connectionId);
352
+ if (connection) {
353
+ connection.isHealthy = false;
354
+ }
355
+ }
356
+ clearReconnectionTimer(connectionId) {
357
+ const timer = this.reconnectionTimers.get(connectionId);
358
+ if (timer) {
359
+ clearTimeout(timer);
360
+ this.reconnectionTimers.delete(connectionId);
361
+ }
362
+ }
363
+ getReconnectionStatus(connectionId) {
364
+ return {
365
+ isReconnecting: this.reconnectionTimers.has(connectionId),
366
+ attempts: this.reconnectionAttempts.get(connectionId) || 0,
367
+ maxAttempts: this.config.reconnectMaxAttempts || 5
368
+ };
369
+ }
370
+ getConnectionInfo(connectionId) {
371
+ return this.connections.get(connectionId) || null;
372
+ }
373
+ getAllConnections() {
374
+ return Array.from(this.connections.values());
375
+ }
376
+ async cleanupUnusedConnections(maxIdleTime) {
377
+ const now = Date.now();
378
+ const connectionsToClose = [];
379
+ for (const [connectionId, connection] of Array.from(this.connections.entries())) {
380
+ if (connection.clientCount === 0 && (now - connection.lastUsed) > maxIdleTime) {
381
+ connectionsToClose.push(connectionId);
382
+ }
383
+ }
384
+ for (const connectionId of connectionsToClose) {
385
+ this.logger.info(`Cleaning up unused connection ${connectionId}`);
386
+ await this.closeConnection(connectionId);
387
+ }
388
+ }
389
+ decrementClientCount(connectionId) {
390
+ const connection = this.connections.get(connectionId);
391
+ if (connection && connection.clientCount > 0) {
392
+ connection.clientCount--;
393
+ connection.lastUsed = Date.now();
394
+ }
395
+ }
396
+ setupConnectionHandlers(connectionInfo) {
397
+ const { connection, id } = connectionInfo;
398
+ connection.on('close', () => {
399
+ this.logger.warn(`CDP connection ${id} closed unexpectedly`);
400
+ connectionInfo.isHealthy = false;
401
+ });
402
+ connection.on('error', (error) => {
403
+ this.logger.error(`CDP connection ${id} error:`, error);
404
+ connectionInfo.isHealthy = false;
405
+ });
406
+ }
407
+ createConnectionKey(host, port, targetId) {
408
+ return `${host}:${port}:${targetId}`;
409
+ }
410
+ generateConnectionId() {
411
+ return `conn_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
412
+ }
413
+ cleanup() {
414
+ this.logger.info('Cleaning up ConnectionPool resources');
415
+ for (const timer of this.reconnectionTimers.values()) {
416
+ clearTimeout(timer);
417
+ }
418
+ this.reconnectionTimers.clear();
419
+ this.reconnectionAttempts.clear();
420
+ this.logger.debug('ConnectionPool cleanup completed');
421
+ }
422
+ getAllReconnectionStatuses() {
423
+ const statuses = new Map();
424
+ for (const connectionId of this.connections.keys()) {
425
+ statuses.set(connectionId, this.getReconnectionStatus(connectionId));
426
+ }
427
+ return statuses;
428
+ }
429
+ }
430
+ exports.ConnectionPool = ConnectionPool;