stock-bot/libs/services/proxy/src/proxy-manager.ts

397 lines
12 KiB
TypeScript

/**
* 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<void> {
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<void> {
// 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<void> {
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<void> {
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<void> {
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<void> {
try {
const cachedProxies = await this.cache.get<ProxyInfo[]>('active');
const lastUpdateStr = await this.cache.get<string>('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<ProxyInfo[]> {
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<void> {
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<void> {
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;