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();
 | 
						|
    }
 | 
						|
  }
 | 
						|
}
 |