@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
JavaScript
/**
* 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