Files
opendata/ts/stocks/classes.baseproviderservice.ts

297 lines
6.6 KiB
TypeScript
Raw Normal View History

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