/** * Centralized Proxy Manager - Handles proxy storage, retrieval, and caching */ import type { CacheProvider } from '@stock-bot/cache'; import type { ProxyInfo, ProxyManagerConfig, ProxyStats } from './types'; export class ProxyManager { private cache: CacheProvider; private proxies: ProxyInfo[] = []; private proxyIndex: number = 0; private lastUpdate: Date | null = null; private isInitialized = false; private logger: any; constructor(cache: CacheProvider, _config: ProxyManagerConfig = {}, logger?: any) { this.cache = cache; this.logger = logger || console; // Config can be used in the future for customization } /** * Internal initialization - loads existing proxies from cache */ private async initializeInternal(): Promise { if (this.isInitialized) { return; } try { this.logger.info('Initializing proxy manager...'); // Wait for cache to be ready await this.cache.waitForReady(10000); // Wait up to 10 seconds this.logger.debug('Cache is ready'); await this.loadFromCache(); this.isInitialized = true; this.logger.info('Proxy manager initialized', { proxiesLoaded: this.proxies.length, lastUpdate: this.lastUpdate, }); } catch (error) { this.logger.error('Failed to initialize proxy manager', { error }); this.isInitialized = true; // Set to true anyway to avoid infinite retries } } getProxy(): string | null { if (this.proxies.length === 0) { this.logger.warn('No proxies available in memory'); return null; } // Cycle through proxies if (this.proxyIndex >= this.proxies.length) { this.proxyIndex = 0; } const proxyInfo = this.proxies[this.proxyIndex++]; if (!proxyInfo) { return null; } // Build proxy URL with optional auth let proxyUrl = `${proxyInfo.protocol}://`; if (proxyInfo.username && proxyInfo.password) { proxyUrl += `${proxyInfo.username}:${proxyInfo.password}@`; } proxyUrl += `${proxyInfo.host}:${proxyInfo.port}`; return proxyUrl; } /** * Get a random working proxy from the available pool (synchronous) */ getRandomProxy(): ProxyInfo | null { // Ensure initialized if (!this.isInitialized) { throw new Error('ProxyManager not initialized'); } // Return null if no proxies available if (this.proxies.length === 0) { this.logger.warn('No proxies available in memory'); return null; } // Filter for working proxies (not explicitly marked as non-working) const workingProxies = this.proxies.filter(proxy => proxy.isWorking !== false); if (workingProxies.length === 0) { this.logger.warn('No working proxies available'); return null; } // Return random proxy with preference for recently successful ones const sortedProxies = workingProxies.sort((a, b) => { // Prefer proxies with better success rates const aRate = a.successRate || 0; const bRate = b.successRate || 0; return bRate - aRate; }); // Take from top 50% of best performing proxies const topProxies = sortedProxies.slice(0, Math.max(1, Math.floor(sortedProxies.length * 0.5))); const selectedProxy = topProxies[Math.floor(Math.random() * topProxies.length)]; if (!selectedProxy) { this.logger.warn('No proxy selected from available pool'); return null; } this.logger.debug('Selected proxy', { host: selectedProxy.host, port: selectedProxy.port, successRate: selectedProxy.successRate, totalAvailable: workingProxies.length, }); return selectedProxy; } /** * Get all working proxies (synchronous) */ getWorkingProxies(): ProxyInfo[] { if (!this.isInitialized) { throw new Error('ProxyManager not initialized'); } return this.proxies.filter(proxy => proxy.isWorking !== false); } /** * Get all proxies (working and non-working) */ getAllProxies(): ProxyInfo[] { if (!this.isInitialized) { throw new Error('ProxyManager not initialized'); } return [...this.proxies]; } /** * Get proxy statistics */ getStats(): ProxyStats { if (!this.isInitialized) { throw new Error('ProxyManager not initialized'); } return { total: this.proxies.length, working: this.proxies.filter(p => p.isWorking !== false).length, failed: this.proxies.filter(p => p.isWorking === false).length, lastUpdate: this.lastUpdate, }; } /** * Update the proxy pool with new proxies */ async updateProxies(proxies: ProxyInfo[]): Promise { // Ensure manager is initialized before updating if (!this.isInitialized) { await this.initializeInternal(); } try { this.logger.info('Updating proxy pool', { newCount: proxies.length, existingCount: this.proxies.length, }); this.proxies = proxies; this.lastUpdate = new Date(); // Store to cache (keys will be prefixed with cache:proxy: automatically) await this.cache.set('active', proxies); await this.cache.set('last-update', this.lastUpdate.toISOString()); const workingCount = proxies.filter(p => p.isWorking !== false).length; this.logger.info('Proxy pool updated successfully', { totalProxies: proxies.length, workingProxies: workingCount, lastUpdate: this.lastUpdate, }); } catch (error) { this.logger.error('Failed to update proxy pool', { error }); throw error; } } /** * Add or update a single proxy in the pool */ async updateProxy(proxy: ProxyInfo): Promise { const existingIndex = this.proxies.findIndex( p => p.host === proxy.host && p.port === proxy.port && p.protocol === proxy.protocol ); if (existingIndex >= 0) { this.proxies[existingIndex] = { ...this.proxies[existingIndex], ...proxy }; this.logger.debug('Updated existing proxy', { host: proxy.host, port: proxy.port }); } else { this.proxies.push(proxy); this.logger.debug('Added new proxy', { host: proxy.host, port: proxy.port }); } // Update cache await this.updateProxies(this.proxies); } /** * Remove a proxy from the pool */ async removeProxy(host: string, port: number, protocol: string): Promise { const initialLength = this.proxies.length; this.proxies = this.proxies.filter( p => !(p.host === host && p.port === port && p.protocol === protocol) ); if (this.proxies.length < initialLength) { await this.updateProxies(this.proxies); this.logger.debug('Removed proxy', { host, port, protocol }); } } /** * Clear all proxies from memory and cache */ async clearProxies(): Promise { this.proxies = []; this.lastUpdate = null; await this.cache.del('active'); await this.cache.del('last-update'); this.logger.info('Cleared all proxies'); } /** * Check if proxy manager is ready */ isReady(): boolean { return this.isInitialized; } /** * Load proxies from cache storage */ private async loadFromCache(): Promise { try { const cachedProxies = await this.cache.get('active'); const lastUpdateStr = await this.cache.get('last-update'); if (cachedProxies && Array.isArray(cachedProxies)) { this.proxies = cachedProxies; this.lastUpdate = lastUpdateStr ? new Date(lastUpdateStr) : null; this.logger.debug('Loaded proxies from cache', { count: this.proxies.length, lastUpdate: this.lastUpdate, }); } else { this.logger.debug('No cached proxies found'); } } catch (error) { this.logger.error('Failed to load proxies from cache', { error }); } } /** * Initialize the proxy manager */ async initialize(): Promise { await this.initializeInternal(); // Note: Initial proxy sync should be handled by the container or application // that creates ProxyManager instance this.logger.info('ProxyManager initialized - proxy sync should be handled externally'); } } // Export the class as default export default ProxyManager;