/** * 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 lastFetchTime: Date | null = null; private isInitialized = false; private logger: any; private config: ProxyManagerConfig; constructor(cache: CacheProvider, config: ProxyManagerConfig = {}, logger?: any) { this.cache = cache; this.config = config; this.logger = logger || console; } /** * 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 }); } } /** * Fetch proxies from WebShare API */ private async fetchWebShareProxies(): Promise { if (!this.config.webshare) { throw new Error('WebShare configuration not provided'); } const { apiKey, apiUrl } = this.config.webshare; this.logger.info('Fetching proxies from WebShare API', { apiUrl }); try { const response = await fetch( `${apiUrl}proxy/list/?mode=direct&page=1&page_size=100`, { method: 'GET', headers: { Authorization: `Token ${apiKey}`, 'Content-Type': 'application/json', }, signal: AbortSignal.timeout(10000), // 10 second timeout } ); if (!response.ok) { throw new Error(`WebShare API request failed: ${response.status} ${response.statusText}`); } const data = await response.json(); if (!data.results || !Array.isArray(data.results)) { throw new Error('Invalid response format from WebShare API'); } // Transform proxy data to ProxyInfo format const proxies: ProxyInfo[] = data.results.map( (proxy: { username: string; password: string; proxy_address: string; port: number }) => ({ source: 'webshare', protocol: 'http' as const, host: proxy.proxy_address, port: proxy.port, username: proxy.username, password: proxy.password, isWorking: true, // WebShare provides working proxies firstSeen: new Date(), lastChecked: new Date(), }) ); this.logger.info('Successfully fetched proxies from WebShare', { count: proxies.length, total: data.count || proxies.length, }); this.lastFetchTime = new Date(); return proxies; } catch (error) { this.logger.error('Failed to fetch proxies from WebShare', { error }); throw error; } } /** * Refresh proxies from WebShare (public method for manual refresh) */ async refreshProxies(): Promise { if (!this.config.enabled || !this.config.webshare) { this.logger.warn('Proxy refresh called but WebShare is not configured'); return; } try { const proxies = await this.fetchWebShareProxies(); await this.updateProxies(proxies); } catch (error) { this.logger.error('Failed to refresh proxies', { error }); throw error; } } /** * Get the last time proxies were fetched from WebShare */ getLastFetchTime(): Date | null { return this.lastFetchTime; } /** * Initialize the proxy manager */ async initialize(): Promise { await this.initializeInternal(); // Fetch proxies on startup if enabled if (this.config.enabled && this.config.webshare) { this.logger.info('Proxy fetching is enabled, fetching proxies from WebShare...'); try { const proxies = await this.fetchWebShareProxies(); if (proxies.length === 0) { throw new Error('No proxies fetched from WebShare API'); } await this.updateProxies(proxies); this.logger.info('ProxyManager initialized with fresh proxies', { count: proxies.length, lastFetchTime: this.lastFetchTime, }); } catch (error) { // If proxy fetching is enabled but fails, the service should not start this.logger.error('Failed to fetch proxies during initialization', { error }); throw new Error(`ProxyManager initialization failed: ${error instanceof Error ? error.message : 'Unknown error'}`); } } else { this.logger.info('ProxyManager initialized without fetching proxies (disabled or not configured)'); } } } // Export the class as default export default ProxyManager;