297 lines
		
	
	
		
			6.6 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
		
		
			
		
	
	
			297 lines
		
	
	
		
			6.6 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
| 
								 | 
							
								import * as plugins from '../plugins.js';
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								/**
							 | 
						||
| 
								 | 
							
								 * Base provider entry for tracking provider state
							 | 
						||
| 
								 | 
							
								 */
							 | 
						||
| 
								 | 
							
								export interface IBaseProviderEntry<TProvider> {
							 | 
						||
| 
								 | 
							
								  provider: TProvider;
							 | 
						||
| 
								 | 
							
								  config: IBaseProviderConfig;
							 | 
						||
| 
								 | 
							
								  lastError?: Error;
							 | 
						||
| 
								 | 
							
								  lastErrorTime?: Date;
							 | 
						||
| 
								 | 
							
								  successCount: number;
							 | 
						||
| 
								 | 
							
								  errorCount: number;
							 | 
						||
| 
								 | 
							
								}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								/**
							 | 
						||
| 
								 | 
							
								 * Base provider configuration
							 | 
						||
| 
								 | 
							
								 */
							 | 
						||
| 
								 | 
							
								export interface IBaseProviderConfig {
							 | 
						||
| 
								 | 
							
								  enabled: boolean;
							 | 
						||
| 
								 | 
							
								  priority: number;
							 | 
						||
| 
								 | 
							
								  timeout?: number;
							 | 
						||
| 
								 | 
							
								  retryAttempts?: number;
							 | 
						||
| 
								 | 
							
								  retryDelay?: number;
							 | 
						||
| 
								 | 
							
								  cacheTTL?: number;
							 | 
						||
| 
								 | 
							
								}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								/**
							 | 
						||
| 
								 | 
							
								 * Base provider interface
							 | 
						||
| 
								 | 
							
								 */
							 | 
						||
| 
								 | 
							
								export interface IBaseProvider {
							 | 
						||
| 
								 | 
							
								  name: string;
							 | 
						||
| 
								 | 
							
								  priority: number;
							 | 
						||
| 
								 | 
							
								  isAvailable(): Promise<boolean>;
							 | 
						||
| 
								 | 
							
								  readonly requiresAuth: boolean;
							 | 
						||
| 
								 | 
							
								  readonly rateLimit?: {
							 | 
						||
| 
								 | 
							
								    requestsPerMinute: number;
							 | 
						||
| 
								 | 
							
								    requestsPerDay?: number;
							 | 
						||
| 
								 | 
							
								  };
							 | 
						||
| 
								 | 
							
								}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								/**
							 | 
						||
| 
								 | 
							
								 * Cache entry for any data type
							 | 
						||
| 
								 | 
							
								 */
							 | 
						||
| 
								 | 
							
								export interface IBaseCacheEntry<TData> {
							 | 
						||
| 
								 | 
							
								  data: TData;
							 | 
						||
| 
								 | 
							
								  timestamp: Date;
							 | 
						||
| 
								 | 
							
								  ttl: number;
							 | 
						||
| 
								 | 
							
								}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								/**
							 | 
						||
| 
								 | 
							
								 * Base service for managing data providers with caching
							 | 
						||
| 
								 | 
							
								 * Shared logic extracted from StockPriceService and FundamentalsService
							 | 
						||
| 
								 | 
							
								 */
							 | 
						||
| 
								 | 
							
								export abstract class BaseProviderService<TProvider extends IBaseProvider, TData> {
							 | 
						||
| 
								 | 
							
								  protected providers = new Map<string, IBaseProviderEntry<TProvider>>();
							 | 
						||
| 
								 | 
							
								  protected cache = new Map<string, IBaseCacheEntry<TData>>();
							 | 
						||
| 
								 | 
							
								  protected logger = console;
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  protected cacheConfig = {
							 | 
						||
| 
								 | 
							
								    ttl: 60000, // Default 60 seconds
							 | 
						||
| 
								 | 
							
								    maxEntries: 10000
							 | 
						||
| 
								 | 
							
								  };
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  constructor(cacheConfig?: { ttl?: number; maxEntries?: number }) {
							 | 
						||
| 
								 | 
							
								    if (cacheConfig) {
							 | 
						||
| 
								 | 
							
								      this.cacheConfig = { ...this.cacheConfig, ...cacheConfig };
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  /**
							 | 
						||
| 
								 | 
							
								   * Register a provider
							 | 
						||
| 
								 | 
							
								   */
							 | 
						||
| 
								 | 
							
								  public register(provider: TProvider, config?: Partial<IBaseProviderConfig>): void {
							 | 
						||
| 
								 | 
							
								    const defaultConfig: IBaseProviderConfig = {
							 | 
						||
| 
								 | 
							
								      enabled: true,
							 | 
						||
| 
								 | 
							
								      priority: provider.priority,
							 | 
						||
| 
								 | 
							
								      timeout: 30000,
							 | 
						||
| 
								 | 
							
								      retryAttempts: 2,
							 | 
						||
| 
								 | 
							
								      retryDelay: 1000,
							 | 
						||
| 
								 | 
							
								      cacheTTL: this.cacheConfig.ttl
							 | 
						||
| 
								 | 
							
								    };
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    const mergedConfig = { ...defaultConfig, ...config };
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    this.providers.set(provider.name, {
							 | 
						||
| 
								 | 
							
								      provider,
							 | 
						||
| 
								 | 
							
								      config: mergedConfig,
							 | 
						||
| 
								 | 
							
								      successCount: 0,
							 | 
						||
| 
								 | 
							
								      errorCount: 0
							 | 
						||
| 
								 | 
							
								    });
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    console.log(`Registered provider: ${provider.name}`);
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  /**
							 | 
						||
| 
								 | 
							
								   * Unregister a provider
							 | 
						||
| 
								 | 
							
								   */
							 | 
						||
| 
								 | 
							
								  public unregister(providerName: string): void {
							 | 
						||
| 
								 | 
							
								    this.providers.delete(providerName);
							 | 
						||
| 
								 | 
							
								    console.log(`Unregistered provider: ${providerName}`);
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  /**
							 | 
						||
| 
								 | 
							
								   * Get a specific provider by name
							 | 
						||
| 
								 | 
							
								   */
							 | 
						||
| 
								 | 
							
								  public getProvider(name: string): TProvider | undefined {
							 | 
						||
| 
								 | 
							
								    return this.providers.get(name)?.provider;
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  /**
							 | 
						||
| 
								 | 
							
								   * Get all registered providers
							 | 
						||
| 
								 | 
							
								   */
							 | 
						||
| 
								 | 
							
								  public getAllProviders(): TProvider[] {
							 | 
						||
| 
								 | 
							
								    return Array.from(this.providers.values()).map(entry => entry.provider);
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  /**
							 | 
						||
| 
								 | 
							
								   * Get enabled providers sorted by priority
							 | 
						||
| 
								 | 
							
								   */
							 | 
						||
| 
								 | 
							
								  public getEnabledProviders(): TProvider[] {
							 | 
						||
| 
								 | 
							
								    return Array.from(this.providers.values())
							 | 
						||
| 
								 | 
							
								      .filter(entry => entry.config.enabled)
							 | 
						||
| 
								 | 
							
								      .sort((a, b) => (b.config.priority || 0) - (a.config.priority || 0))
							 | 
						||
| 
								 | 
							
								      .map(entry => entry.provider);
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  /**
							 | 
						||
| 
								 | 
							
								   * Check health of all providers
							 | 
						||
| 
								 | 
							
								   */
							 | 
						||
| 
								 | 
							
								  public async checkProvidersHealth(): Promise<Map<string, boolean>> {
							 | 
						||
| 
								 | 
							
								    const health = new Map<string, boolean>();
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    for (const [name, entry] of this.providers) {
							 | 
						||
| 
								 | 
							
								      if (!entry.config.enabled) {
							 | 
						||
| 
								 | 
							
								        health.set(name, false);
							 | 
						||
| 
								 | 
							
								        continue;
							 | 
						||
| 
								 | 
							
								      }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								      try {
							 | 
						||
| 
								 | 
							
								        const isAvailable = await entry.provider.isAvailable();
							 | 
						||
| 
								 | 
							
								        health.set(name, isAvailable);
							 | 
						||
| 
								 | 
							
								      } catch (error) {
							 | 
						||
| 
								 | 
							
								        health.set(name, false);
							 | 
						||
| 
								 | 
							
								        console.error(`Health check failed for ${name}:`, error);
							 | 
						||
| 
								 | 
							
								      }
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    return health;
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  /**
							 | 
						||
| 
								 | 
							
								   * Get provider statistics
							 | 
						||
| 
								 | 
							
								   */
							 | 
						||
| 
								 | 
							
								  public getProviderStats(): Map<
							 | 
						||
| 
								 | 
							
								    string,
							 | 
						||
| 
								 | 
							
								    {
							 | 
						||
| 
								 | 
							
								      successCount: number;
							 | 
						||
| 
								 | 
							
								      errorCount: number;
							 | 
						||
| 
								 | 
							
								      lastError?: string;
							 | 
						||
| 
								 | 
							
								      lastErrorTime?: Date;
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								  > {
							 | 
						||
| 
								 | 
							
								    const stats = new Map();
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    for (const [name, entry] of this.providers) {
							 | 
						||
| 
								 | 
							
								      stats.set(name, {
							 | 
						||
| 
								 | 
							
								        successCount: entry.successCount,
							 | 
						||
| 
								 | 
							
								        errorCount: entry.errorCount,
							 | 
						||
| 
								 | 
							
								        lastError: entry.lastError?.message,
							 | 
						||
| 
								 | 
							
								        lastErrorTime: entry.lastErrorTime
							 | 
						||
| 
								 | 
							
								      });
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    return stats;
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  /**
							 | 
						||
| 
								 | 
							
								   * Clear all cached data
							 | 
						||
| 
								 | 
							
								   */
							 | 
						||
| 
								 | 
							
								  public clearCache(): void {
							 | 
						||
| 
								 | 
							
								    this.cache.clear();
							 | 
						||
| 
								 | 
							
								    console.log('Cache cleared');
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  /**
							 | 
						||
| 
								 | 
							
								   * Set cache TTL
							 | 
						||
| 
								 | 
							
								   */
							 | 
						||
| 
								 | 
							
								  public setCacheTTL(ttl: number): void {
							 | 
						||
| 
								 | 
							
								    this.cacheConfig.ttl = ttl;
							 | 
						||
| 
								 | 
							
								    console.log(`Cache TTL set to ${ttl}ms`);
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  /**
							 | 
						||
| 
								 | 
							
								   * Get cache statistics
							 | 
						||
| 
								 | 
							
								   */
							 | 
						||
| 
								 | 
							
								  public getCacheStats(): {
							 | 
						||
| 
								 | 
							
								    size: number;
							 | 
						||
| 
								 | 
							
								    maxEntries: number;
							 | 
						||
| 
								 | 
							
								    ttl: number;
							 | 
						||
| 
								 | 
							
								  } {
							 | 
						||
| 
								 | 
							
								    return {
							 | 
						||
| 
								 | 
							
								      size: this.cache.size,
							 | 
						||
| 
								 | 
							
								      maxEntries: this.cacheConfig.maxEntries,
							 | 
						||
| 
								 | 
							
								      ttl: this.cacheConfig.ttl
							 | 
						||
| 
								 | 
							
								    };
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  /**
							 | 
						||
| 
								 | 
							
								   * Fetch with retry logic
							 | 
						||
| 
								 | 
							
								   */
							 | 
						||
| 
								 | 
							
								  protected async fetchWithRetry<T>(
							 | 
						||
| 
								 | 
							
								    fetchFn: () => Promise<T>,
							 | 
						||
| 
								 | 
							
								    config: IBaseProviderConfig
							 | 
						||
| 
								 | 
							
								  ): Promise<T> {
							 | 
						||
| 
								 | 
							
								    const maxAttempts = config.retryAttempts || 1;
							 | 
						||
| 
								 | 
							
								    let lastError: Error | undefined;
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    for (let attempt = 1; attempt <= maxAttempts; attempt++) {
							 | 
						||
| 
								 | 
							
								      try {
							 | 
						||
| 
								 | 
							
								        return await fetchFn();
							 | 
						||
| 
								 | 
							
								      } catch (error) {
							 | 
						||
| 
								 | 
							
								        lastError = error as Error;
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        if (attempt < maxAttempts) {
							 | 
						||
| 
								 | 
							
								          const delay = (config.retryDelay || 1000) * attempt;
							 | 
						||
| 
								 | 
							
								          console.log(`Retry attempt ${attempt} after ${delay}ms`);
							 | 
						||
| 
								 | 
							
								          await plugins.smartdelay.delayFor(delay);
							 | 
						||
| 
								 | 
							
								        }
							 | 
						||
| 
								 | 
							
								      }
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    throw lastError || new Error('Unknown error during fetch');
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  /**
							 | 
						||
| 
								 | 
							
								   * Get from cache if not expired
							 | 
						||
| 
								 | 
							
								   */
							 | 
						||
| 
								 | 
							
								  protected getFromCache(key: string): TData | null {
							 | 
						||
| 
								 | 
							
								    const entry = this.cache.get(key);
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    if (!entry) {
							 | 
						||
| 
								 | 
							
								      return null;
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    // Check if cache entry has expired
							 | 
						||
| 
								 | 
							
								    const age = Date.now() - entry.timestamp.getTime();
							 | 
						||
| 
								 | 
							
								    if (entry.ttl !== Infinity && age > entry.ttl) {
							 | 
						||
| 
								 | 
							
								      this.cache.delete(key);
							 | 
						||
| 
								 | 
							
								      return null;
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    return entry.data;
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  /**
							 | 
						||
| 
								 | 
							
								   * Add to cache with TTL
							 | 
						||
| 
								 | 
							
								   */
							 | 
						||
| 
								 | 
							
								  protected addToCache(key: string, data: TData, ttl?: number): void {
							 | 
						||
| 
								 | 
							
								    // Enforce max entries limit
							 | 
						||
| 
								 | 
							
								    if (this.cache.size >= this.cacheConfig.maxEntries) {
							 | 
						||
| 
								 | 
							
								      // Remove oldest entry
							 | 
						||
| 
								 | 
							
								      const oldestKey = this.cache.keys().next().value;
							 | 
						||
| 
								 | 
							
								      if (oldestKey) {
							 | 
						||
| 
								 | 
							
								        this.cache.delete(oldestKey);
							 | 
						||
| 
								 | 
							
								      }
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    this.cache.set(key, {
							 | 
						||
| 
								 | 
							
								      data,
							 | 
						||
| 
								 | 
							
								      timestamp: new Date(),
							 | 
						||
| 
								 | 
							
								      ttl: ttl || this.cacheConfig.ttl
							 | 
						||
| 
								 | 
							
								    });
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  /**
							 | 
						||
| 
								 | 
							
								   * Track successful fetch for provider
							 | 
						||
| 
								 | 
							
								   */
							 | 
						||
| 
								 | 
							
								  protected trackSuccess(providerName: string): void {
							 | 
						||
| 
								 | 
							
								    const entry = this.providers.get(providerName);
							 | 
						||
| 
								 | 
							
								    if (entry) {
							 | 
						||
| 
								 | 
							
								      entry.successCount++;
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  /**
							 | 
						||
| 
								 | 
							
								   * Track failed fetch for provider
							 | 
						||
| 
								 | 
							
								   */
							 | 
						||
| 
								 | 
							
								  protected trackError(providerName: string, error: Error): void {
							 | 
						||
| 
								 | 
							
								    const entry = this.providers.get(providerName);
							 | 
						||
| 
								 | 
							
								    if (entry) {
							 | 
						||
| 
								 | 
							
								      entry.errorCount++;
							 | 
						||
| 
								 | 
							
								      entry.lastError = error;
							 | 
						||
| 
								 | 
							
								      entry.lastErrorTime = new Date();
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								}
							 |