UNPKG

polyv-live-cli

Version:

CLI tool for managing PolyV live streaming services.

474 lines 17.8 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.ConnectionPoolManager = void 0; const events_1 = require("events"); const http_1 = __importDefault(require("http")); const https_1 = __importDefault(require("https")); class ConnectionPoolManager extends events_1.EventEmitter { constructor(config = {}) { super(); this.connectionPool = new Map(); this.activeConnections = new Map(); this.pendingRequests = new Map(); this.requestIdCounter = 0; this.connectionIdCounter = 0; this.isRunning = false; this.config = { maxConnectionsPerHost: 10, maxTotalConnections: 100, connectionTimeout: 5000, keepAliveTimeout: 60000, maxIdleTime: 30000, enablePooling: true, enableKeepAlive: true, socketTimeout: 30000, maxRequestsPerConnection: 100, ...config, }; this.stats = { activeConnections: 0, idleConnections: 0, totalRequests: 0, reusedConnections: 0, reuseRatio: 0, averageConnectionDuration: 0, poolUtilization: 0, hostStats: new Map(), }; this.initializeAgents(); this.setupEventHandlers(); } start() { if (this.isRunning) { return; } this.isRunning = true; this.startCleanupTimer(); this.emit('poolStarted', { timestamp: Date.now(), config: this.config, }); } stop() { if (!this.isRunning) { return; } this.isRunning = false; if (this.cleanupTimer) { clearInterval(this.cleanupTimer); delete this.cleanupTimer; } this.closeAllConnections(); if (this.httpAgent) { this.httpAgent.destroy(); } if (this.httpsAgent) { this.httpsAgent.destroy(); } this.emit('poolStopped', { timestamp: Date.now(), finalStats: this.getStats(), }); } async getConnection(host, port, protocol = 'https') { if (!this.isRunning) { throw new Error('Connection pool is not running'); } const hostKey = `${protocol}://${host}:${port}`; this.stats.totalRequests++; this.updateHostStats(hostKey, 'request'); const idleConnection = this.getIdleConnection(hostKey); if (idleConnection) { this.activateConnection(idleConnection); this.stats.reusedConnections++; this.updateReuseRatio(); this.emit('connectionReused', { connectionId: idleConnection.id, host: hostKey, reuseCount: idleConnection.requestCount, timestamp: Date.now(), }); return idleConnection; } if (this.canCreateNewConnection(hostKey)) { return this.createNewConnection(host, port, protocol); } return this.queueConnectionRequest(host, port, protocol); } returnConnection(connection) { const hostKey = `${connection.protocol}://${connection.host}:${connection.port}`; connection.isActive = false; connection.lastUsed = Date.now(); connection.state = 'idle'; this.activeConnections.delete(connection.id); if (this.shouldReuseConnection(connection)) { const hostConnections = this.connectionPool.get(hostKey) || []; hostConnections.push(connection); this.connectionPool.set(hostKey, hostConnections); this.stats.idleConnections++; this.stats.activeConnections--; this.emit('connectionReturned', { connectionId: connection.id, host: hostKey, requestCount: connection.requestCount, timestamp: Date.now(), }); this.processPendingRequests(hostKey); } else { this.closeConnection(connection); } this.updatePoolStats(); } getStats() { this.updatePoolStats(); return { ...this.stats }; } getPoolInfo() { const connectionsByHost = new Map(); const idleConnectionsByHost = new Map(); const activeConnectionsByHost = new Map(); const pendingRequestsByHost = new Map(); let oldestConnection = null; let newestConnection = null; for (const [hostKey, connections] of this.connectionPool) { connectionsByHost.set(hostKey, connections.length); idleConnectionsByHost.set(hostKey, connections.length); for (const conn of connections) { if (!oldestConnection || conn.createdAt < oldestConnection.createdAt) { oldestConnection = conn; } if (!newestConnection || conn.createdAt > newestConnection.createdAt) { newestConnection = conn; } } } for (const [, connection] of this.activeConnections) { const hostKey = `${connection.protocol}://${connection.host}:${connection.port}`; const current = activeConnectionsByHost.get(hostKey) || 0; activeConnectionsByHost.set(hostKey, current + 1); const total = connectionsByHost.get(hostKey) || 0; connectionsByHost.set(hostKey, total + 1); } for (const [hostKey, requests] of this.pendingRequests) { pendingRequestsByHost.set(hostKey, requests.length); } return { totalConnections: this.stats.activeConnections + this.stats.idleConnections, connectionsByHost, idleConnectionsByHost, activeConnectionsByHost, pendingRequestsByHost, oldestConnection, newestConnection, }; } async forceCleanup() { const cleanedUp = this.cleanupIdleConnections(); this.emit('forcedCleanup', { connectionsCleanedUp: cleanedUp, timestamp: Date.now(), }); this.updatePoolStats(); } updateConfig(newConfig) { this.config = { ...this.config, ...newConfig }; this.initializeAgents(); this.emit('configUpdated', { config: this.config, timestamp: Date.now(), }); } getAgents() { return { ...(this.httpAgent && { httpAgent: this.httpAgent }), ...(this.httpsAgent && { httpsAgent: this.httpsAgent }), }; } initializeAgents() { if (this.config.enablePooling) { this.httpAgent = new http_1.default.Agent({ keepAlive: this.config.enableKeepAlive, keepAliveMsecs: this.config.keepAliveTimeout, maxSockets: this.config.maxConnectionsPerHost, maxTotalSockets: this.config.maxTotalConnections, timeout: this.config.connectionTimeout, }); this.httpsAgent = new https_1.default.Agent({ keepAlive: this.config.enableKeepAlive, keepAliveMsecs: this.config.keepAliveTimeout, maxSockets: this.config.maxConnectionsPerHost, maxTotalSockets: this.config.maxTotalConnections, timeout: this.config.connectionTimeout, }); } } setupEventHandlers() { this.on('connectionError', (error) => { this.emit('error', error); }); this.on('connectionTimeout', (connectionId) => { const connection = this.activeConnections.get(connectionId); if (connection) { this.closeConnection(connection); } }); } getIdleConnection(hostKey) { const hostConnections = this.connectionPool.get(hostKey); if (!hostConnections || hostConnections.length === 0) { return null; } const connection = hostConnections.pop(); if (connection) { this.connectionPool.set(hostKey, hostConnections); this.stats.idleConnections--; } return connection || null; } canCreateNewConnection(hostKey) { const hostConnections = this.getHostConnectionCount(hostKey); const totalConnections = this.stats.activeConnections + this.stats.idleConnections; return (hostConnections < this.config.maxConnectionsPerHost && totalConnections < this.config.maxTotalConnections); } async createNewConnection(host, port, protocol) { const connectionId = `conn_${++this.connectionIdCounter}`; const connection = { id: connectionId, host, port, protocol, socket: null, createdAt: Date.now(), lastUsed: Date.now(), requestCount: 0, isActive: true, state: 'connecting', }; try { connection.socket = await this.createSocket(host, port, protocol); connection.state = 'connected'; this.activateConnection(connection); this.emit('connectionCreated', { connectionId, host: `${protocol}://${host}:${port}`, timestamp: Date.now(), }); return connection; } catch (error) { connection.state = 'closed'; this.emit('connectionError', { connectionId, host: `${protocol}://${host}:${port}`, error, timestamp: Date.now(), }); throw error; } } createSocket(host, port, protocol) { return new Promise((resolve, reject) => { const options = { host, port, timeout: this.config.connectionTimeout, }; const socket = protocol === 'https' ? require('tls').connect(options) : require('net').createConnection(options); const timeout = setTimeout(() => { socket.destroy(); reject(new Error(`Connection timeout after ${this.config.connectionTimeout}ms`)); }, this.config.connectionTimeout); socket.on('connect', () => { clearTimeout(timeout); resolve(socket); }); socket.on('error', (error) => { clearTimeout(timeout); reject(error); }); }); } queueConnectionRequest(host, port, protocol) { const hostKey = `${protocol}://${host}:${port}`; const requestId = `req_${++this.requestIdCounter}`; return new Promise((resolve, reject) => { const request = { id: requestId, host, port, protocol, timestamp: Date.now(), callback: (error, connection) => { if (error) { reject(error); } else if (connection) { resolve(connection); } }, timeout: this.config.connectionTimeout, }; const hostRequests = this.pendingRequests.get(hostKey) || []; hostRequests.push(request); this.pendingRequests.set(hostKey, hostRequests); this.emit('requestQueued', { requestId, host: hostKey, queueLength: hostRequests.length, timestamp: Date.now(), }); setTimeout(() => { this.removePendingRequest(hostKey, requestId); request.callback(new Error(`Connection request timeout after ${this.config.connectionTimeout}ms`)); }, this.config.connectionTimeout); }); } activateConnection(connection) { connection.isActive = true; connection.lastUsed = Date.now(); connection.requestCount++; connection.state = 'connected'; this.activeConnections.set(connection.id, connection); this.stats.activeConnections++; } shouldReuseConnection(connection) { const age = Date.now() - connection.createdAt; return (connection.requestCount < this.config.maxRequestsPerConnection && age < this.config.keepAliveTimeout && connection.state === 'idle'); } processPendingRequests(hostKey) { const pendingRequests = this.pendingRequests.get(hostKey); if (!pendingRequests || pendingRequests.length === 0) { return; } while (pendingRequests.length > 0 && this.canCreateNewConnection(hostKey)) { const request = pendingRequests.shift(); if (request) { this.createNewConnection(request.host, request.port, request.protocol) .then(connection => request.callback(null, connection)) .catch(error => request.callback(error)); } } this.pendingRequests.set(hostKey, pendingRequests); } removePendingRequest(hostKey, requestId) { const requests = this.pendingRequests.get(hostKey) || []; const filtered = requests.filter(req => req.id !== requestId); this.pendingRequests.set(hostKey, filtered); } getHostConnectionCount(hostKey) { const idleCount = (this.connectionPool.get(hostKey) || []).length; let activeCount = 0; for (const [, connection] of this.activeConnections) { const connHostKey = `${connection.protocol}://${connection.host}:${connection.port}`; if (connHostKey === hostKey) { activeCount++; } } return idleCount + activeCount; } startCleanupTimer() { this.cleanupTimer = setInterval(() => { this.cleanupIdleConnections(); this.updatePoolStats(); }, this.config.maxIdleTime); } cleanupIdleConnections() { let cleanedUp = 0; const now = Date.now(); for (const [hostKey, connections] of this.connectionPool) { const validConnections = connections.filter(connection => { const idleTime = now - connection.lastUsed; if (idleTime > this.config.maxIdleTime) { this.closeConnection(connection); cleanedUp++; return false; } return true; }); this.connectionPool.set(hostKey, validConnections); } this.stats.idleConnections -= cleanedUp; return cleanedUp; } closeConnection(connection) { if (connection.socket) { connection.socket.destroy(); } connection.state = 'closed'; this.activeConnections.delete(connection.id); this.emit('connectionClosed', { connectionId: connection.id, host: `${connection.protocol}://${connection.host}:${connection.port}`, duration: Date.now() - connection.createdAt, requestCount: connection.requestCount, timestamp: Date.now(), }); } closeAllConnections() { for (const [, connection] of this.activeConnections) { this.closeConnection(connection); } for (const [, connections] of this.connectionPool) { for (const connection of connections) { this.closeConnection(connection); } } this.connectionPool.clear(); this.activeConnections.clear(); this.pendingRequests.clear(); } updateHostStats(hostKey, event) { let hostStats = this.stats.hostStats.get(hostKey); if (!hostStats) { hostStats = { host: hostKey, activeConnections: 0, idleConnections: 0, totalRequests: 0, reuseCount: 0, averageResponseTime: 0, }; this.stats.hostStats.set(hostKey, hostStats); } switch (event) { case 'request': hostStats.totalRequests++; break; case 'reuse': hostStats.reuseCount++; break; } } updateReuseRatio() { this.stats.reuseRatio = this.stats.totalRequests > 0 ? this.stats.reusedConnections / this.stats.totalRequests : 0; } updatePoolStats() { const totalConnections = this.stats.activeConnections + this.stats.idleConnections; this.stats.poolUtilization = this.config.maxTotalConnections > 0 ? totalConnections / this.config.maxTotalConnections : 0; for (const [hostKey, hostStats] of this.stats.hostStats) { hostStats.activeConnections = 0; hostStats.idleConnections = (this.connectionPool.get(hostKey) || []).length; for (const [, connection] of this.activeConnections) { const connHostKey = `${connection.protocol}://${connection.host}:${connection.port}`; if (connHostKey === hostKey) { hostStats.activeConnections++; } } } } } exports.ConnectionPoolManager = ConnectionPoolManager; //# sourceMappingURL=connection-pool-manager.js.map