polyv-live-cli
Version:
CLI tool for managing PolyV live streaming services.
474 lines • 17.8 kB
JavaScript
"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