refactoring continuing

This commit is contained in:
Boki 2025-06-22 08:27:54 -04:00
parent 742e590382
commit a0a3b26177
20 changed files with 394 additions and 798 deletions

View file

@ -5,7 +5,6 @@
import type { IServiceContainer } from '@stock-bot/handlers';
import type { IDataIngestionServices } from '../service-interfaces';
import { ProxyManager } from '@stock-bot/proxy';
/**
* Adapter that converts IDataIngestionServices to IServiceContainer
@ -23,9 +22,10 @@ export class DataIngestionServiceAdapter implements IServiceContainer {
// HTTP client not in current data services - will be added when needed
return null;
}
get proxy() {
// Return singleton proxy manager instance
return ProxyManager.getInstance();
get proxy(): any {
// Proxy manager should be injected via Awilix container
// This adapter is for legacy compatibility
throw new Error('ProxyManager must be provided through Awilix container');
}
// Database clients

View file

@ -0,0 +1,174 @@
/**
* Awilix DI Container Setup
* Creates a decoupled, reusable dependency injection container
*/
import { createContainer, asFunction, asValue, InjectionMode, type AwilixContainer } from 'awilix';
import { createCache, type CacheProvider } from '@stock-bot/cache';
import { ProxyManager } from '@stock-bot/proxy';
import { getLogger } from '@stock-bot/logger';
import type { IServiceContainer } from '@stock-bot/handlers';
// Configuration types
export interface AppConfig {
redis: {
host: string;
port: number;
password?: string;
username?: string;
db?: number;
};
mongodb: {
uri: string;
database: string;
};
postgres: {
host: string;
port: number;
database: string;
user: string;
password: string;
};
questdb?: {
host: string;
port: number;
};
proxy?: {
cachePrefix?: string;
ttl?: number;
};
}
/**
* Create and configure the DI container
*/
export function createServiceContainer(config: AppConfig): AwilixContainer {
const container = createContainer({
injectionMode: InjectionMode.PROXY,
});
// Register configuration values
container.register({
// Configuration
config: asValue(config),
redisConfig: asValue(config.redis),
mongoConfig: asValue(config.mongodb),
postgresConfig: asValue(config.postgres),
questdbConfig: asValue(config.questdb || { host: 'localhost', port: 9009 }),
// Core services with dependency injection
logger: asFunction(() => getLogger('app')).singleton(),
// Cache with injected config and logger
cache: asFunction(({ redisConfig, logger }) =>
createCache({
redisConfig,
logger,
keyPrefix: 'cache:',
ttl: 3600,
enableMetrics: true,
})
).singleton(),
// Proxy manager with injected cache and logger
proxyManager: asFunction(({ cache, config, logger }) => {
const manager = new ProxyManager(
cache,
config.proxy || {},
logger
);
// Note: initialization happens in initializeServices function
return manager;
}).singleton(),
// HTTP client can be added here when decoupled
httpClient: asFunction(() => {
// TODO: Import and create HTTP client when decoupled
return null;
}).singleton(),
// Database clients - placeholders for now
mongoClient: asFunction(() => {
// TODO: Create MongoDB client
return null;
}).singleton(),
postgresClient: asFunction(() => {
// TODO: Create PostgreSQL client
return null;
}).singleton(),
questdbClient: asFunction(() => {
// TODO: Create QuestDB client
return null;
}).singleton(),
// Queue manager - placeholder
queueManager: asFunction(() => {
// TODO: Create queue manager when decoupled
return null;
}).singleton(),
// Build the IServiceContainer for handlers
serviceContainer: asFunction((cradle) => ({
logger: cradle.logger,
cache: cradle.cache,
proxy: cradle.proxyManager,
http: cradle.httpClient,
mongodb: cradle.mongoClient,
postgres: cradle.postgresClient,
questdb: cradle.questdbClient,
queue: cradle.queueManager,
} as IServiceContainer)).singleton(),
});
return container;
}
/**
* Initialize async services after container creation
*/
export async function initializeServices(container: AwilixContainer): Promise<void> {
const logger = container.resolve('logger');
try {
// Wait for cache to be ready first
const cache = container.resolve('cache');
if (cache && typeof cache.waitForReady === 'function') {
await cache.waitForReady(10000);
logger.info('Cache is ready');
}
// Initialize proxy manager
const proxyManager = container.resolve('proxyManager');
if (proxyManager && typeof proxyManager.initialize === 'function') {
await proxyManager.initialize();
logger.info('Proxy manager initialized');
}
// Initialize other async services as needed
// ...
logger.info('All services initialized successfully');
} catch (error) {
logger.error('Failed to initialize services', { error });
throw error;
}
}
// Type definitions for container resolution
export interface ServiceCradle {
config: AppConfig;
logger: any;
cache: CacheProvider;
proxyManager: ProxyManager;
httpClient: any;
mongoClient: any;
postgresClient: any;
questdbClient: any;
queueManager: any;
serviceContainer: IServiceContainer;
}
// Export typed container
export type ServiceContainer = AwilixContainer<ServiceCradle>;

View file

@ -6,4 +6,13 @@ export * from './pool-size-calculator';
export * from './types';
export * from './service-interfaces';
export * from './service-factory';
export * from './adapters/service-adapter';
export * from './adapters/service-adapter';
// Awilix container exports
export {
createServiceContainer,
initializeServices,
type AppConfig,
type ServiceCradle,
type ServiceContainer
} from './awilix-container';

View file

@ -5,7 +5,6 @@
import { getLogger } from '@stock-bot/logger';
import { ConnectionFactory } from './connection-factory';
import { PoolSizeCalculator } from './pool-size-calculator';
import { ProxyManager } from '@stock-bot/proxy';
import type {
IDataIngestionServices,
IServiceFactory,
@ -45,9 +44,7 @@ export class DataIngestionServiceFactory implements IServiceFactory {
this.createQueueConnection(connectionFactory, config)
]);
// Initialize proxy manager
logger.info('Initializing proxy manager...');
await ProxyManager.initialize();
// Note: Proxy manager initialization moved to Awilix container
const services: IDataIngestionServices = {
mongodb: mongoPool.client,

View file

@ -1,5 +1,4 @@
import Redis from 'ioredis';
import { getLogger } from '@stock-bot/logger';
import type { RedisConfig } from './types';
interface ConnectionConfig {
@ -7,6 +6,7 @@ interface ConnectionConfig {
singleton?: boolean;
db?: number;
redisConfig: RedisConfig;
logger?: any;
}
/**
@ -16,7 +16,7 @@ export class RedisConnectionManager {
private connections = new Map<string, Redis>();
private static sharedConnections = new Map<string, Redis>();
private static instance: RedisConnectionManager;
private logger = getLogger('redis-connection-manager');
private logger: any = console;
private static readyConnections = new Set<string>();
// Singleton pattern for the manager itself
@ -33,7 +33,10 @@ export class RedisConnectionManager {
* @returns Redis connection instance
*/
getConnection(config: ConnectionConfig): Redis {
const { name, singleton = false, db, redisConfig } = config;
const { name, singleton = false, db, redisConfig, logger } = config;
if (logger) {
this.logger = logger;
}
if (singleton) {
// Use shared connection across all instances

View file

@ -1,5 +1,4 @@
import Redis from 'ioredis';
import { getLogger } from '@stock-bot/logger';
import { RedisConnectionManager } from './connection-manager';
import { CacheOptions, CacheProvider, CacheStats } from './types';
@ -8,7 +7,7 @@ import { CacheOptions, CacheProvider, CacheStats } from './types';
*/
export class RedisCache implements CacheProvider {
private redis: Redis;
private logger = getLogger('redis-cache');
private logger: any;
private defaultTTL: number;
private keyPrefix: string;
private enableMetrics: boolean;
@ -29,6 +28,7 @@ export class RedisCache implements CacheProvider {
this.defaultTTL = options.ttl ?? 3600; // 1 hour default
this.keyPrefix = options.keyPrefix ?? 'cache:';
this.enableMetrics = options.enableMetrics ?? true;
this.logger = options.logger || console; // Use provided logger or console as fallback
// Get connection manager instance
this.connectionManager = RedisConnectionManager.getInstance();
@ -47,6 +47,7 @@ export class RedisCache implements CacheProvider {
name: `${baseName}-SERVICE`,
singleton: options.shared ?? true, // Default to shared connection for cache
redisConfig: options.redisConfig,
logger: this.logger,
});
// Only setup event handlers for non-shared connections to avoid memory leaks

View file

@ -85,6 +85,7 @@ export interface CacheOptions {
name?: string; // Name for connection identification
shared?: boolean; // Whether to use shared connection
redisConfig: RedisConfig;
logger?: any; // Optional logger instance
}
export interface CacheStats {

View file

@ -5,32 +5,15 @@
// Main classes
export { ProxyManager } from './proxy-manager';
export { ProxySyncService } from './proxy-sync';
// Types
export type {
ProxyInfo,
ProxyManagerConfig,
ProxySyncConfig,
ProxyStats
export type {
ProxyInfo,
ProxyManagerConfig, ProxyStats, ProxySyncConfig
} from './types';
// Convenience functions
export {
getProxy,
getRandomProxy,
getAllProxies,
getWorkingProxies,
updateProxies,
getProxyStats
} from './proxy-manager';
export {
getProxySyncService,
startProxySync,
stopProxySync,
syncProxiesOnce
} from './proxy-sync';
// Note: Convenience functions removed as ProxyManager is no longer a singleton
// Create an instance and use its methods directly
// Default export
export { ProxyManager as default } from './proxy-manager';
export { ProxyManager as default } from './proxy-manager';

View file

@ -1,37 +1,21 @@
/**
* Centralized Proxy Manager - Handles proxy storage, retrieval, and caching
*/
import { createCache, type CacheProvider } from '@stock-bot/cache';
import { getDatabaseConfig } from '@stock-bot/config';
import { getLogger } from '@stock-bot/logger';
import type { CacheProvider } from '@stock-bot/cache';
import type { ProxyInfo, ProxyManagerConfig, ProxyStats } from './types';
const logger = getLogger('proxy-manager');
export class ProxyManager {
private static instance: ProxyManager | null = null;
private cache: CacheProvider;
private proxies: ProxyInfo[] = [];
private proxyIndex: number = 0;
private lastUpdate: Date | null = null;
private isInitialized = false;
private config: ProxyManagerConfig;
private logger: any;
private constructor(config: ProxyManagerConfig = {}) {
this.config = {
cachePrefix: 'proxies:',
ttl: 86400, // 24 hours
enableMetrics: true,
...config
};
const databaseConfig = getDatabaseConfig();
this.cache = createCache({
redisConfig: databaseConfig.dragonfly,
keyPrefix: this.config.cachePrefix,
ttl: this.config.ttl,
enableMetrics: this.config.enableMetrics,
});
constructor(cache: CacheProvider, _config: ProxyManagerConfig = {}, logger?: any) {
this.cache = cache;
this.logger = logger || console;
// Config can be used in the future for customization
}
/**
@ -43,27 +27,27 @@ export class ProxyManager {
}
try {
logger.info('Initializing proxy manager...');
this.logger.info('Initializing proxy manager...');
// Wait for cache to be ready
await this.cache.waitForReady(10000); // Wait up to 10 seconds
logger.debug('Cache is ready');
this.logger.debug('Cache is ready');
await this.loadFromCache();
this.isInitialized = true;
logger.info('Proxy manager initialized', {
this.logger.info('Proxy manager initialized', {
proxiesLoaded: this.proxies.length,
lastUpdate: this.lastUpdate,
});
} catch (error) {
logger.error('Failed to initialize proxy manager', { 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) {
logger.warn('No proxies available in memory');
this.logger.warn('No proxies available in memory');
return null;
}
@ -97,7 +81,7 @@ export class ProxyManager {
// Return null if no proxies available
if (this.proxies.length === 0) {
logger.warn('No proxies available in memory');
this.logger.warn('No proxies available in memory');
return null;
}
@ -105,7 +89,7 @@ export class ProxyManager {
const workingProxies = this.proxies.filter(proxy => proxy.isWorking !== false);
if (workingProxies.length === 0) {
logger.warn('No working proxies available');
this.logger.warn('No working proxies available');
return null;
}
@ -122,11 +106,11 @@ export class ProxyManager {
const selectedProxy = topProxies[Math.floor(Math.random() * topProxies.length)];
if (!selectedProxy) {
logger.warn('No proxy selected from available pool');
this.logger.warn('No proxy selected from available pool');
return null;
}
logger.debug('Selected proxy', {
this.logger.debug('Selected proxy', {
host: selectedProxy.host,
port: selectedProxy.port,
successRate: selectedProxy.successRate,
@ -178,8 +162,13 @@ export class ProxyManager {
* 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 {
logger.info('Updating proxy pool', { newCount: proxies.length, existingCount: this.proxies.length });
this.logger.info('Updating proxy pool', { newCount: proxies.length, existingCount: this.proxies.length });
this.proxies = proxies;
this.lastUpdate = new Date();
@ -189,13 +178,13 @@ export class ProxyManager {
await this.cache.set('last-update', this.lastUpdate.toISOString());
const workingCount = proxies.filter(p => p.isWorking !== false).length;
logger.info('Proxy pool updated successfully', {
this.logger.info('Proxy pool updated successfully', {
totalProxies: proxies.length,
workingProxies: workingCount,
lastUpdate: this.lastUpdate,
});
} catch (error) {
logger.error('Failed to update proxy pool', { error });
this.logger.error('Failed to update proxy pool', { error });
throw error;
}
}
@ -210,10 +199,10 @@ export class ProxyManager {
if (existingIndex >= 0) {
this.proxies[existingIndex] = { ...this.proxies[existingIndex], ...proxy };
logger.debug('Updated existing proxy', { host: proxy.host, port: proxy.port });
this.logger.debug('Updated existing proxy', { host: proxy.host, port: proxy.port });
} else {
this.proxies.push(proxy);
logger.debug('Added new proxy', { host: proxy.host, port: proxy.port });
this.logger.debug('Added new proxy', { host: proxy.host, port: proxy.port });
}
// Update cache
@ -231,7 +220,7 @@ export class ProxyManager {
if (this.proxies.length < initialLength) {
await this.updateProxies(this.proxies);
logger.debug('Removed proxy', { host, port, protocol });
this.logger.debug('Removed proxy', { host, port, protocol });
}
}
@ -245,7 +234,7 @@ export class ProxyManager {
await this.cache.del('active-proxies');
await this.cache.del('last-update');
logger.info('Cleared all proxies');
this.logger.info('Cleared all proxies');
}
/**
@ -267,79 +256,29 @@ export class ProxyManager {
this.proxies = cachedProxies;
this.lastUpdate = lastUpdateStr ? new Date(lastUpdateStr) : null;
logger.debug('Loaded proxies from cache', {
this.logger.debug('Loaded proxies from cache', {
count: this.proxies.length,
lastUpdate: this.lastUpdate,
});
} else {
logger.debug('No cached proxies found');
this.logger.debug('No cached proxies found');
}
} catch (error) {
logger.error('Failed to load proxies from cache', { error });
this.logger.error('Failed to load proxies from cache', { error });
}
}
/**
* Initialize the singleton instance
* Initialize the proxy manager
*/
static async initialize(config?: ProxyManagerConfig): Promise<void> {
if (!ProxyManager.instance) {
ProxyManager.instance = new ProxyManager(config);
await ProxyManager.instance.initializeInternal();
// Perform initial sync with proxy:active:* storage
try {
const { syncProxiesOnce } = await import('./proxy-sync');
await syncProxiesOnce();
logger.info('Initial proxy sync completed');
} catch (error) {
logger.error('Failed to perform initial proxy sync', { error });
}
}
}
/**
* Get the singleton instance (must be initialized first)
*/
static getInstance(): ProxyManager {
if (!ProxyManager.instance) {
throw new Error('ProxyManager not initialized. Call ProxyManager.initialize() first.');
}
return ProxyManager.instance;
}
/**
* Reset the singleton instance (for testing)
*/
static reset(): void {
ProxyManager.instance = null;
async initialize(): Promise<void> {
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;
// Convenience functions for easier imports
export function getProxy(): string | null {
return ProxyManager.getInstance().getProxy();
}
export function getRandomProxy(): ProxyInfo | null {
return ProxyManager.getInstance().getRandomProxy();
}
export function getAllProxies(): ProxyInfo[] {
return ProxyManager.getInstance().getAllProxies();
}
export function getWorkingProxies(): ProxyInfo[] {
return ProxyManager.getInstance().getWorkingProxies();
}
export async function updateProxies(proxies: ProxyInfo[]): Promise<void> {
return ProxyManager.getInstance().updateProxies(proxies);
}
export function getProxyStats(): ProxyStats {
return ProxyManager.getInstance().getStats();
}
export default ProxyManager;

View file

@ -1,170 +0,0 @@
/**
* Proxy Storage Synchronization Service
*
* This service bridges the gap between two proxy storage systems:
* 1. proxy:active:* keys (used by proxy tasks for individual proxy storage)
* 2. proxies:active-proxies (used by ProxyManager for centralized storage)
*/
import { createCache, type CacheProvider } from '@stock-bot/cache';
import { getDatabaseConfig } from '@stock-bot/config';
import { getLogger } from '@stock-bot/logger';
import type { ProxyInfo, ProxySyncConfig } from './types';
import { ProxyManager } from './proxy-manager';
const logger = getLogger('proxy-sync');
export class ProxySyncService {
private cache: CacheProvider;
private syncInterval: Timer | null = null;
private isRunning = false;
private config: ProxySyncConfig;
constructor(config: ProxySyncConfig = {}) {
this.config = {
intervalMs: 300000, // 5 minutes
enableAutoSync: true,
...config
};
const databaseConfig = getDatabaseConfig();
this.cache = createCache({
redisConfig: databaseConfig.dragonfly,
keyPrefix: '', // No prefix to access all keys
ttl: 86400,
});
}
/**
* Start the synchronization service
* @param intervalMs - Sync interval in milliseconds (default: 5 minutes)
*/
async start(intervalMs?: number): Promise<void> {
const interval = intervalMs || this.config.intervalMs!;
if (this.isRunning) {
logger.warn('Proxy sync service is already running');
return;
}
this.isRunning = true;
logger.info('Starting proxy sync service', { intervalMs: interval });
// Wait for cache to be ready before initial sync
await this.cache.waitForReady(10000);
// Initial sync
await this.syncProxies();
// Set up periodic sync if enabled
if (this.config.enableAutoSync) {
this.syncInterval = setInterval(async () => {
try {
await this.syncProxies();
} catch (error) {
logger.error('Error during periodic sync', { error });
}
}, interval);
}
}
/**
* Stop the synchronization service
*/
stop(): void {
if (this.syncInterval) {
clearInterval(this.syncInterval);
this.syncInterval = null;
}
this.isRunning = false;
logger.info('Stopped proxy sync service');
}
/**
* Perform a one-time synchronization
*/
async syncProxies(): Promise<void> {
try {
logger.debug('Starting proxy synchronization');
// Wait for cache to be ready
await this.cache.waitForReady(5000);
// Collect all proxies from proxy:active:* storage
const proxyKeys = await this.cache.keys('proxy:active:*');
if (proxyKeys.length === 0) {
logger.debug('No proxies found in proxy:active:* storage');
return;
}
const allProxies: ProxyInfo[] = [];
// Fetch all proxies in parallel for better performance
const proxyPromises = proxyKeys.map(key => this.cache.get<ProxyInfo>(key));
const proxyResults = await Promise.all(proxyPromises);
for (const proxy of proxyResults) {
if (proxy) {
allProxies.push(proxy);
}
}
const workingCount = allProxies.filter(p => p.isWorking).length;
logger.info('Collected proxies from storage', {
total: allProxies.length,
working: workingCount,
});
// Update ProxyManager with all proxies
const manager = ProxyManager.getInstance();
await manager.updateProxies(allProxies);
logger.info('Proxy synchronization completed', {
synchronized: allProxies.length,
working: workingCount,
});
} catch (error) {
logger.error('Failed to sync proxies', { error });
throw error;
}
}
/**
* Get synchronization status
*/
getStatus(): { isRunning: boolean; config: ProxySyncConfig } {
return {
isRunning: this.isRunning,
config: this.config
};
}
}
// Export singleton instance
let syncServiceInstance: ProxySyncService | null = null;
export function getProxySyncService(config?: ProxySyncConfig): ProxySyncService {
if (!syncServiceInstance) {
syncServiceInstance = new ProxySyncService(config);
}
return syncServiceInstance;
}
// Convenience functions
export async function startProxySync(intervalMs?: number, config?: ProxySyncConfig): Promise<void> {
const service = getProxySyncService(config);
await service.start(intervalMs);
}
export function stopProxySync(): void {
if (syncServiceInstance) {
syncServiceInstance.stop();
}
}
export async function syncProxiesOnce(): Promise<void> {
const service = getProxySyncService();
await service.syncProxies();
}

View file

@ -2,4 +2,3 @@ export * from './calculations/index';
export * from './common';
export * from './dateUtils';
export * from './generic-functions';
export * from './proxy';

View file

@ -1,21 +0,0 @@
/**
* Proxy management utilities
*/
export {
default as ProxyManager,
getProxy,
getRandomProxy,
getAllProxies,
getWorkingProxies,
updateProxies
} from './proxy-manager';
export {
ProxySyncService,
getProxySyncService,
startProxySync,
stopProxySync,
syncProxiesOnce
} from './proxy-sync';
export type { ProxyInfo } from '@stock-bot/http'; // Re-export for convenience

View file

@ -1,291 +0,0 @@
/**
* Centralized Proxy Manager - Handles proxy storage, retrieval, and caching
*/
import { createCache, type CacheProvider } from '@stock-bot/cache';
import { getDatabaseConfig } from '@stock-bot/config';
import { getLogger } from '@stock-bot/logger';
import type { ProxyInfo } from '@stock-bot/http';
const logger = getLogger('proxy-manager');
export class ProxyManager {
private static instance: ProxyManager | null = null;
private cache: CacheProvider;
private proxies: ProxyInfo[] = [];
private lastUpdate: Date | null = null;
private isInitialized = false;
private constructor() {
const databaseConfig = getDatabaseConfig();
this.cache = createCache({
redisConfig: databaseConfig.dragonfly,
keyPrefix: 'proxies:',
ttl: 86400, // 24 hours
enableMetrics: true,
});
}
/**
* Internal initialization - loads existing proxies from cache
*/
private async initializeInternal(): Promise<void> {
if (this.isInitialized) {
return;
}
try {
logger.info('Initializing proxy manager...');
// Wait for cache to be ready
await this.cache.waitForReady(10000); // Wait up to 10 seconds
logger.debug('Cache is ready');
await this.loadFromCache();
this.isInitialized = true;
logger.info('Proxy manager initialized', {
proxiesLoaded: this.proxies.length,
lastUpdate: this.lastUpdate,
});
} catch (error) {
logger.error('Failed to initialize proxy manager', { error });
this.isInitialized = true; // Set to true anyway to avoid infinite retries
}
}
/**
* 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) {
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) {
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) {
logger.warn('No proxy selected from available pool');
return null;
}
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];
}
/**
* Update the proxy pool with new proxies
*/
async updateProxies(proxies: ProxyInfo[]): Promise<void> {
try {
logger.info('Updating proxy pool', { newCount: proxies.length, existingCount: this.proxies.length });
this.proxies = proxies;
this.lastUpdate = new Date();
// Store to cache
await this.cache.set('active-proxies', proxies);
await this.cache.set('last-update', this.lastUpdate.toISOString());
const workingCount = proxies.filter(p => p.isWorking !== false).length;
logger.info('Proxy pool updated successfully', {
totalProxies: proxies.length,
workingProxies: workingCount,
lastUpdate: this.lastUpdate,
});
} catch (error) {
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 };
logger.debug('Updated existing proxy', { host: proxy.host, port: proxy.port });
} else {
this.proxies.push(proxy);
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);
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-proxies');
await this.cache.del('last-update');
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-proxies');
const lastUpdateStr = await this.cache.get<string>('last-update');
if (cachedProxies && Array.isArray(cachedProxies)) {
this.proxies = cachedProxies;
this.lastUpdate = lastUpdateStr ? new Date(lastUpdateStr) : null;
logger.debug('Loaded proxies from cache', {
count: this.proxies.length,
lastUpdate: this.lastUpdate,
});
} else {
logger.debug('No cached proxies found');
}
} catch (error) {
logger.error('Failed to load proxies from cache', { error });
}
}
/**
* Initialize the singleton instance
*/
static async initialize(): Promise<void> {
if (!ProxyManager.instance) {
ProxyManager.instance = new ProxyManager();
await ProxyManager.instance.initializeInternal();
// Perform initial sync with proxy:active:* storage
try {
const { syncProxiesOnce } = await import('./proxy-sync');
await syncProxiesOnce();
logger.info('Initial proxy sync completed');
} catch (error) {
logger.error('Failed to perform initial proxy sync', { error });
}
}
}
/**
* Get the singleton instance (must be initialized first)
*/
static getInstance(): ProxyManager {
if (!ProxyManager.instance) {
throw new Error('ProxyManager not initialized. Call ProxyManager.initialize() first.');
}
return ProxyManager.instance;
}
/**
* Reset the singleton instance (for testing)
*/
static reset(): void {
ProxyManager.instance = null;
}
}
// Export the class as default
export default ProxyManager;
// Convenience functions for easier imports
export function getProxy(): ProxyInfo | null {
return ProxyManager.getInstance().getRandomProxy();
}
export function getRandomProxy(): ProxyInfo | null {
return ProxyManager.getInstance().getRandomProxy();
}
export function getAllProxies(): ProxyInfo[] {
return ProxyManager.getInstance().getAllProxies();
}
export function getWorkingProxies(): ProxyInfo[] {
return ProxyManager.getInstance().getWorkingProxies();
}
export async function updateProxies(proxies: ProxyInfo[]): Promise<void> {
return ProxyManager.getInstance().updateProxies(proxies);
}

View file

@ -1,157 +0,0 @@
/**
* Proxy Storage Synchronization Service
*
* This service bridges the gap between two proxy storage systems:
* 1. proxy:active:* keys (used by proxy tasks for individual proxy storage)
* 2. proxies:active-proxies (used by ProxyManager for centralized storage)
*/
import { createCache, type CacheProvider } from '@stock-bot/cache';
import { getDatabaseConfig } from '@stock-bot/config';
import { getLogger } from '@stock-bot/logger';
import type { ProxyInfo } from '@stock-bot/http';
import { ProxyManager } from './proxy-manager';
const logger = getLogger('proxy-sync');
export class ProxySyncService {
private cache: CacheProvider;
private syncInterval: Timer | null = null;
private isRunning = false;
constructor() {
const databaseConfig = getDatabaseConfig();
this.cache = createCache({
redisConfig: databaseConfig.dragonfly,
keyPrefix: '', // No prefix to access all keys
ttl: 86400,
});
}
/**
* Start the synchronization service
* @param intervalMs - Sync interval in milliseconds (default: 5 minutes)
*/
async start(intervalMs: number = 300000): Promise<void> {
if (this.isRunning) {
logger.warn('Proxy sync service is already running');
return;
}
this.isRunning = true;
logger.info('Starting proxy sync service', { intervalMs });
// Wait for cache to be ready before initial sync
await this.cache.waitForReady(10000);
// Initial sync
await this.syncProxies();
// Set up periodic sync
this.syncInterval = setInterval(async () => {
try {
await this.syncProxies();
} catch (error) {
logger.error('Error during periodic sync', { error });
}
}, intervalMs);
}
/**
* Stop the synchronization service
*/
stop(): void {
if (this.syncInterval) {
clearInterval(this.syncInterval);
this.syncInterval = null;
}
this.isRunning = false;
logger.info('Stopped proxy sync service');
}
/**
* Perform a one-time synchronization
*/
async syncProxies(): Promise<void> {
try {
logger.debug('Starting proxy synchronization');
// Wait for cache to be ready
await this.cache.waitForReady(5000);
// Collect all proxies from proxy:active:* storage
const proxyKeys = await this.cache.keys('proxy:active:*');
if (proxyKeys.length === 0) {
logger.debug('No proxies found in proxy:active:* storage');
return;
}
const allProxies: ProxyInfo[] = [];
// Fetch all proxies in parallel for better performance
const proxyPromises = proxyKeys.map(key => this.cache.get<ProxyInfo>(key));
const proxyResults = await Promise.all(proxyPromises);
for (const proxy of proxyResults) {
if (proxy) {
allProxies.push(proxy);
}
}
const workingCount = allProxies.filter(p => p.isWorking).length;
logger.info('Collected proxies from storage', {
total: allProxies.length,
working: workingCount,
});
// Update ProxyManager with all proxies
const manager = ProxyManager.getInstance();
await manager.updateProxies(allProxies);
logger.info('Proxy synchronization completed', {
synchronized: allProxies.length,
working: workingCount,
});
} catch (error) {
logger.error('Failed to sync proxies', { error });
throw error;
}
}
/**
* Get synchronization status
*/
getStatus(): { isRunning: boolean; lastSync?: Date } {
return {
isRunning: this.isRunning,
};
}
}
// Export singleton instance
let syncServiceInstance: ProxySyncService | null = null;
export function getProxySyncService(): ProxySyncService {
if (!syncServiceInstance) {
syncServiceInstance = new ProxySyncService();
}
return syncServiceInstance;
}
// Convenience functions
export async function startProxySync(intervalMs?: number): Promise<void> {
const service = getProxySyncService();
await service.start(intervalMs);
}
export function stopProxySync(): void {
const service = getProxySyncService();
service.stop();
}
export async function syncProxiesOnce(): Promise<void> {
const service = getProxySyncService();
await service.syncProxies();
}