import * as plugins from '../plugins.js'; import type { IStockProvider, IProviderConfig, IProviderRegistry } from './interfaces/provider.js'; import type { IStockPrice, IStockQuoteRequest, IStockBatchQuoteRequest, IStockPriceError } from './interfaces/stockprice.js'; interface IProviderEntry { provider: IStockProvider; config: IProviderConfig; lastError?: Error; lastErrorTime?: Date; successCount: number; errorCount: number; } interface ICacheEntry { price: IStockPrice; timestamp: Date; } export class StockPriceService implements IProviderRegistry { private providers = new Map(); private cache = new Map(); private logger = console; private cacheConfig = { ttl: 60000, // 60 seconds default maxEntries: 1000 }; constructor(cacheConfig?: { ttl?: number; maxEntries?: number }) { if (cacheConfig) { this.cacheConfig = { ...this.cacheConfig, ...cacheConfig }; } } public register(provider: IStockProvider, config?: IProviderConfig): void { const defaultConfig: IProviderConfig = { enabled: true, priority: provider.priority, timeout: 10000, retryAttempts: 2, retryDelay: 1000 }; const mergedConfig = { ...defaultConfig, ...config }; this.providers.set(provider.name, { provider, config: mergedConfig, successCount: 0, errorCount: 0 }); console.log(`Registered provider: ${provider.name}`); } public unregister(providerName: string): void { this.providers.delete(providerName); console.log(`Unregistered provider: ${providerName}`); } public getProvider(name: string): IStockProvider | undefined { return this.providers.get(name)?.provider; } public getAllProviders(): IStockProvider[] { return Array.from(this.providers.values()).map(entry => entry.provider); } public getEnabledProviders(): IStockProvider[] { 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); } public async getPrice(request: IStockQuoteRequest): Promise { const cacheKey = this.getCacheKey(request); const cached = this.getFromCache(cacheKey); if (cached) { console.log(`Cache hit for ${request.ticker}`); return cached; } const providers = this.getEnabledProviders(); if (providers.length === 0) { throw new Error('No stock price providers available'); } let lastError: Error | undefined; for (const provider of providers) { const entry = this.providers.get(provider.name)!; try { const price = await this.fetchWithRetry( () => provider.fetchPrice(request), entry.config ); entry.successCount++; this.addToCache(cacheKey, price); console.log(`Successfully fetched ${request.ticker} from ${provider.name}`); return price; } catch (error) { entry.errorCount++; entry.lastError = error as Error; entry.lastErrorTime = new Date(); lastError = error as Error; console.warn( `Provider ${provider.name} failed for ${request.ticker}: ${error.message}` ); } } throw new Error( `Failed to fetch price for ${request.ticker} from all providers. Last error: ${lastError?.message}` ); } public async getPrices(request: IStockBatchQuoteRequest): Promise { const cachedPrices: IStockPrice[] = []; const tickersToFetch: string[] = []; // Check cache for each ticker for (const ticker of request.tickers) { const cacheKey = this.getCacheKey({ ticker, includeExtendedHours: request.includeExtendedHours }); const cached = this.getFromCache(cacheKey); if (cached) { cachedPrices.push(cached); } else { tickersToFetch.push(ticker); } } if (tickersToFetch.length === 0) { console.log(`All ${request.tickers.length} tickers served from cache`); return cachedPrices; } const providers = this.getEnabledProviders(); if (providers.length === 0) { throw new Error('No stock price providers available'); } let lastError: Error | undefined; let fetchedPrices: IStockPrice[] = []; for (const provider of providers) { const entry = this.providers.get(provider.name)!; try { fetchedPrices = await this.fetchWithRetry( () => provider.fetchPrices({ tickers: tickersToFetch, includeExtendedHours: request.includeExtendedHours }), entry.config ); entry.successCount++; // Cache the fetched prices for (const price of fetchedPrices) { const cacheKey = this.getCacheKey({ ticker: price.ticker, includeExtendedHours: request.includeExtendedHours }); this.addToCache(cacheKey, price); } console.log( `Successfully fetched ${fetchedPrices.length} prices from ${provider.name}` ); break; } catch (error) { entry.errorCount++; entry.lastError = error as Error; entry.lastErrorTime = new Date(); lastError = error as Error; console.warn( `Provider ${provider.name} failed for batch request: ${error.message}` ); } } if (fetchedPrices.length === 0 && lastError) { throw new Error( `Failed to fetch prices from all providers. Last error: ${lastError.message}` ); } return [...cachedPrices, ...fetchedPrices]; } 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; } public getProviderStats(): Map { 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; } public clearCache(): void { this.cache.clear(); console.log('Cache cleared'); } public setCacheTTL(ttl: number): void { this.cacheConfig.ttl = ttl; console.log(`Cache TTL set to ${ttl}ms`); } private async fetchWithRetry( fetchFn: () => Promise, config: IProviderConfig ): 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'); } private getCacheKey(request: IStockQuoteRequest): string { return `${request.ticker}:${request.includeExtendedHours || false}`; } private getFromCache(key: string): IStockPrice | null { const entry = this.cache.get(key); if (!entry) { return null; } const age = Date.now() - entry.timestamp.getTime(); if (age > this.cacheConfig.ttl) { this.cache.delete(key); return null; } return entry.price; } private addToCache(key: string, price: IStockPrice): 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, { price, timestamp: new Date() }); } }