UNPKG

@dorothywebb/any-browser-mcp

Version:

Any Browser MCP - Launch Chrome with your actual data in debug mode for comprehensive browser automation

330 lines โ€ข 11.8 kB
/** * Connection Pool for Browser Connections * * @fileoverview Manages a pool of browser connections to improve performance * with multiple concurrent operations. Provides connection reuse, automatic * cleanup, and resource management. * * @example * ```typescript * import { ConnectionPool } from './ConnectionPool.js'; * * const pool = new ConnectionPool({ maxConnections: 5 }); * * // Get a connection from the pool * const connection = await pool.acquire(); * * try { * // Use the connection * await connection.sendCommand('Page.navigate', { url: 'https://example.com' }); * } finally { * // Return connection to pool * pool.release(connection); * } * ``` * * @category Utilities */ import { createBrowserConnection } from './BrowserConnection.js'; import { ConfigManager } from '../core/ConfigManager.js'; import { ErrorFactory } from '../types/errors.js'; import { handleError } from './ErrorHandler.js'; /** * Connection pool for managing browser connections efficiently. * * @description This class implements a connection pool pattern to reuse * browser connections across multiple operations, reducing the overhead * of creating new connections for each request. */ export class ConnectionPool { connections = new Map(); waitingQueue = []; options; cleanupTimer; stats; config; /** * Creates a new connection pool. * * @param options - Configuration options for the pool */ constructor(options = {}) { this.config = ConfigManager.getInstance(); this.options = { maxConnections: options.maxConnections ?? this.config.getServerConfig().maxConcurrentConnections, minConnections: options.minConnections ?? 1, maxIdleTime: options.maxIdleTime ?? 300000, // 5 minutes cleanupInterval: options.cleanupInterval ?? 60000, // 1 minute acquireTimeout: options.acquireTimeout ?? 30000, // 30 seconds verbose: options.verbose ?? this.config.isVerbose() }; this.stats = { totalConnections: 0, activeConnections: 0, availableConnections: 0, acquisitions: 0, releases: 0, connectionsCreated: 0, connectionsDestroyed: 0, averageConnectionAge: 0 }; this.startCleanupTimer(); if (this.options.verbose) { console.log(`๐ŸŠ Connection pool initialized with max ${this.options.maxConnections} connections`); } } /** * Acquires a connection from the pool. * * @returns Promise that resolves to a browser connection * @throws Error if unable to acquire connection within timeout * * @example * ```typescript * const connection = await pool.acquire(); * try { * await connection.sendCommand('Page.navigate', { url: 'https://example.com' }); * } finally { * pool.release(connection); * } * ``` */ async acquire() { this.stats.acquisitions++; // Try to find an available connection const availableConnection = this.findAvailableConnection(); if (availableConnection) { availableConnection.inUse = true; availableConnection.lastUsedAt = new Date(); this.updateStats(); if (this.options.verbose) { console.log(`๐Ÿ”— Acquired existing connection ${availableConnection.id}`); } return availableConnection.connection; } // Create new connection if under limit if (this.connections.size < this.options.maxConnections) { const connection = await this.createConnection(); this.updateStats(); return connection; } // Wait for a connection to become available return this.waitForConnection(); } /** * Releases a connection back to the pool. * * @param connection - The connection to release * * @example * ```typescript * pool.release(connection); * ``` */ release(connection) { this.stats.releases++; const pooledConnection = this.findPooledConnection(connection); if (pooledConnection) { pooledConnection.inUse = false; pooledConnection.lastUsedAt = new Date(); if (this.options.verbose) { console.log(`๐Ÿ”“ Released connection ${pooledConnection.id}`); } // Process waiting queue this.processWaitingQueue(); } this.updateStats(); } /** * Gets current pool statistics. * * @returns Current pool statistics */ getStats() { this.updateStats(); return { ...this.stats }; } /** * Closes all connections and shuts down the pool. * * @example * ```typescript * await pool.shutdown(); * ``` */ async shutdown() { if (this.cleanupTimer) { clearInterval(this.cleanupTimer); } // Reject all waiting requests this.waitingQueue.forEach(({ reject }) => { reject(new Error('Connection pool is shutting down')); }); this.waitingQueue = []; // Close all connections const closePromises = Array.from(this.connections.values()).map(async (pooledConnection) => { try { await pooledConnection.connection.disconnect(); this.stats.connectionsDestroyed++; } catch (error) { handleError(ErrorFactory.createConnectionError(`Failed to close connection during shutdown: ${error.message}`, { operation: 'shutdown', details: { connectionId: pooledConnection.id } })); } }); await Promise.all(closePromises); this.connections.clear(); this.updateStats(); if (this.options.verbose) { console.log('๐ŸŠ Connection pool shut down'); } } /** * Finds an available connection in the pool. */ findAvailableConnection() { for (const pooledConnection of this.connections.values()) { if (!pooledConnection.inUse) { return pooledConnection; } } return null; } /** * Finds a pooled connection by its browser connection instance. */ findPooledConnection(connection) { for (const pooledConnection of this.connections.values()) { if (pooledConnection.connection === connection) { return pooledConnection; } } return null; } /** * Creates a new connection and adds it to the pool. */ async createConnection() { try { const connection = createBrowserConnection(); const pooledConnection = { connection, createdAt: new Date(), lastUsedAt: new Date(), inUse: true, id: `conn-${Date.now()}-${Math.random().toString(36).substr(2, 9)}` }; this.connections.set(pooledConnection.id, pooledConnection); this.stats.connectionsCreated++; if (this.options.verbose) { console.log(`๐Ÿ†• Created new connection ${pooledConnection.id}`); } return connection; } catch (error) { const mcpError = ErrorFactory.createConnectionError(`Failed to create new connection: ${error.message}`, { operation: 'createConnection' }, error); handleError(mcpError); throw mcpError; } } /** * Waits for a connection to become available. */ async waitForConnection() { return new Promise((resolve, reject) => { const timeout = setTimeout(() => { const index = this.waitingQueue.findIndex(item => item.resolve === resolve); if (index !== -1) { this.waitingQueue.splice(index, 1); } reject(ErrorFactory.createConnectionError(`Timeout waiting for connection after ${this.options.acquireTimeout}ms`, { operation: 'waitForConnection', details: { timeout: this.options.acquireTimeout } })); }, this.options.acquireTimeout); this.waitingQueue.push({ resolve: (connection) => { clearTimeout(timeout); resolve(connection); }, reject: (error) => { clearTimeout(timeout); reject(error); }, timestamp: new Date() }); }); } /** * Processes the waiting queue when connections become available. */ processWaitingQueue() { if (this.waitingQueue.length === 0) return; const availableConnection = this.findAvailableConnection(); if (availableConnection) { const waiter = this.waitingQueue.shift(); if (waiter) { availableConnection.inUse = true; availableConnection.lastUsedAt = new Date(); waiter.resolve(availableConnection.connection); } } } /** * Starts the cleanup timer for idle connections. */ startCleanupTimer() { this.cleanupTimer = setInterval(() => { this.cleanupIdleConnections(); }, this.options.cleanupInterval); } /** * Cleans up idle connections that exceed the maximum idle time. */ cleanupIdleConnections() { const now = new Date(); const connectionsToRemove = []; for (const [id, pooledConnection] of this.connections) { if (!pooledConnection.inUse) { const idleTime = now.getTime() - pooledConnection.lastUsedAt.getTime(); if (idleTime > this.options.maxIdleTime && this.connections.size > this.options.minConnections) { connectionsToRemove.push(id); } } } connectionsToRemove.forEach(async (id) => { const pooledConnection = this.connections.get(id); if (pooledConnection) { try { await pooledConnection.connection.disconnect(); this.connections.delete(id); this.stats.connectionsDestroyed++; if (this.options.verbose) { console.log(`๐Ÿงน Cleaned up idle connection ${id}`); } } catch (error) { handleError(ErrorFactory.createConnectionError(`Failed to cleanup idle connection: ${error.message}`, { operation: 'cleanupIdleConnections', details: { connectionId: id } })); } } }); this.updateStats(); } /** * Updates the pool statistics. */ updateStats() { this.stats.totalConnections = this.connections.size; this.stats.activeConnections = Array.from(this.connections.values()).filter(c => c.inUse).length; this.stats.availableConnections = this.stats.totalConnections - this.stats.activeConnections; // Calculate average connection age if (this.connections.size > 0) { const now = new Date(); const totalAge = Array.from(this.connections.values()).reduce((sum, conn) => { return sum + (now.getTime() - conn.createdAt.getTime()); }, 0); this.stats.averageConnectionAge = totalAge / this.connections.size; } else { this.stats.averageConnectionAge = 0; } } } //# sourceMappingURL=ConnectionPool.js.map