import * as plugins from '../plugins.js'; /** * Base provider entry for tracking provider state */ export interface IBaseProviderEntry { 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; readonly requiresAuth: boolean; readonly rateLimit?: { requestsPerMinute: number; requestsPerDay?: number; }; } /** * Cache entry for any data type */ export interface IBaseCacheEntry { 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 { protected providers = new Map>(); protected cache = new Map>(); 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): 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> { const health = new Map(); 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( fetchFn: () => Promise, config: IBaseProviderConfig ): Promise { 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(); } } }