@fin.cx/opendata 3.1.0 → 3.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,296 @@
1
+ import * as plugins from '../plugins.js';
2
+
3
+ /**
4
+ * Base provider entry for tracking provider state
5
+ */
6
+ export interface IBaseProviderEntry<TProvider> {
7
+ provider: TProvider;
8
+ config: IBaseProviderConfig;
9
+ lastError?: Error;
10
+ lastErrorTime?: Date;
11
+ successCount: number;
12
+ errorCount: number;
13
+ }
14
+
15
+ /**
16
+ * Base provider configuration
17
+ */
18
+ export interface IBaseProviderConfig {
19
+ enabled: boolean;
20
+ priority: number;
21
+ timeout?: number;
22
+ retryAttempts?: number;
23
+ retryDelay?: number;
24
+ cacheTTL?: number;
25
+ }
26
+
27
+ /**
28
+ * Base provider interface
29
+ */
30
+ export interface IBaseProvider {
31
+ name: string;
32
+ priority: number;
33
+ isAvailable(): Promise<boolean>;
34
+ readonly requiresAuth: boolean;
35
+ readonly rateLimit?: {
36
+ requestsPerMinute: number;
37
+ requestsPerDay?: number;
38
+ };
39
+ }
40
+
41
+ /**
42
+ * Cache entry for any data type
43
+ */
44
+ export interface IBaseCacheEntry<TData> {
45
+ data: TData;
46
+ timestamp: Date;
47
+ ttl: number;
48
+ }
49
+
50
+ /**
51
+ * Base service for managing data providers with caching
52
+ * Shared logic extracted from StockPriceService and FundamentalsService
53
+ */
54
+ export abstract class BaseProviderService<TProvider extends IBaseProvider, TData> {
55
+ protected providers = new Map<string, IBaseProviderEntry<TProvider>>();
56
+ protected cache = new Map<string, IBaseCacheEntry<TData>>();
57
+ protected logger = console;
58
+
59
+ protected cacheConfig = {
60
+ ttl: 60000, // Default 60 seconds
61
+ maxEntries: 10000
62
+ };
63
+
64
+ constructor(cacheConfig?: { ttl?: number; maxEntries?: number }) {
65
+ if (cacheConfig) {
66
+ this.cacheConfig = { ...this.cacheConfig, ...cacheConfig };
67
+ }
68
+ }
69
+
70
+ /**
71
+ * Register a provider
72
+ */
73
+ public register(provider: TProvider, config?: Partial<IBaseProviderConfig>): void {
74
+ const defaultConfig: IBaseProviderConfig = {
75
+ enabled: true,
76
+ priority: provider.priority,
77
+ timeout: 30000,
78
+ retryAttempts: 2,
79
+ retryDelay: 1000,
80
+ cacheTTL: this.cacheConfig.ttl
81
+ };
82
+
83
+ const mergedConfig = { ...defaultConfig, ...config };
84
+
85
+ this.providers.set(provider.name, {
86
+ provider,
87
+ config: mergedConfig,
88
+ successCount: 0,
89
+ errorCount: 0
90
+ });
91
+
92
+ console.log(`Registered provider: ${provider.name}`);
93
+ }
94
+
95
+ /**
96
+ * Unregister a provider
97
+ */
98
+ public unregister(providerName: string): void {
99
+ this.providers.delete(providerName);
100
+ console.log(`Unregistered provider: ${providerName}`);
101
+ }
102
+
103
+ /**
104
+ * Get a specific provider by name
105
+ */
106
+ public getProvider(name: string): TProvider | undefined {
107
+ return this.providers.get(name)?.provider;
108
+ }
109
+
110
+ /**
111
+ * Get all registered providers
112
+ */
113
+ public getAllProviders(): TProvider[] {
114
+ return Array.from(this.providers.values()).map(entry => entry.provider);
115
+ }
116
+
117
+ /**
118
+ * Get enabled providers sorted by priority
119
+ */
120
+ public getEnabledProviders(): TProvider[] {
121
+ return Array.from(this.providers.values())
122
+ .filter(entry => entry.config.enabled)
123
+ .sort((a, b) => (b.config.priority || 0) - (a.config.priority || 0))
124
+ .map(entry => entry.provider);
125
+ }
126
+
127
+ /**
128
+ * Check health of all providers
129
+ */
130
+ public async checkProvidersHealth(): Promise<Map<string, boolean>> {
131
+ const health = new Map<string, boolean>();
132
+
133
+ for (const [name, entry] of this.providers) {
134
+ if (!entry.config.enabled) {
135
+ health.set(name, false);
136
+ continue;
137
+ }
138
+
139
+ try {
140
+ const isAvailable = await entry.provider.isAvailable();
141
+ health.set(name, isAvailable);
142
+ } catch (error) {
143
+ health.set(name, false);
144
+ console.error(`Health check failed for ${name}:`, error);
145
+ }
146
+ }
147
+
148
+ return health;
149
+ }
150
+
151
+ /**
152
+ * Get provider statistics
153
+ */
154
+ public getProviderStats(): Map<
155
+ string,
156
+ {
157
+ successCount: number;
158
+ errorCount: number;
159
+ lastError?: string;
160
+ lastErrorTime?: Date;
161
+ }
162
+ > {
163
+ const stats = new Map();
164
+
165
+ for (const [name, entry] of this.providers) {
166
+ stats.set(name, {
167
+ successCount: entry.successCount,
168
+ errorCount: entry.errorCount,
169
+ lastError: entry.lastError?.message,
170
+ lastErrorTime: entry.lastErrorTime
171
+ });
172
+ }
173
+
174
+ return stats;
175
+ }
176
+
177
+ /**
178
+ * Clear all cached data
179
+ */
180
+ public clearCache(): void {
181
+ this.cache.clear();
182
+ console.log('Cache cleared');
183
+ }
184
+
185
+ /**
186
+ * Set cache TTL
187
+ */
188
+ public setCacheTTL(ttl: number): void {
189
+ this.cacheConfig.ttl = ttl;
190
+ console.log(`Cache TTL set to ${ttl}ms`);
191
+ }
192
+
193
+ /**
194
+ * Get cache statistics
195
+ */
196
+ public getCacheStats(): {
197
+ size: number;
198
+ maxEntries: number;
199
+ ttl: number;
200
+ } {
201
+ return {
202
+ size: this.cache.size,
203
+ maxEntries: this.cacheConfig.maxEntries,
204
+ ttl: this.cacheConfig.ttl
205
+ };
206
+ }
207
+
208
+ /**
209
+ * Fetch with retry logic
210
+ */
211
+ protected async fetchWithRetry<T>(
212
+ fetchFn: () => Promise<T>,
213
+ config: IBaseProviderConfig
214
+ ): Promise<T> {
215
+ const maxAttempts = config.retryAttempts || 1;
216
+ let lastError: Error | undefined;
217
+
218
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
219
+ try {
220
+ return await fetchFn();
221
+ } catch (error) {
222
+ lastError = error as Error;
223
+
224
+ if (attempt < maxAttempts) {
225
+ const delay = (config.retryDelay || 1000) * attempt;
226
+ console.log(`Retry attempt ${attempt} after ${delay}ms`);
227
+ await plugins.smartdelay.delayFor(delay);
228
+ }
229
+ }
230
+ }
231
+
232
+ throw lastError || new Error('Unknown error during fetch');
233
+ }
234
+
235
+ /**
236
+ * Get from cache if not expired
237
+ */
238
+ protected getFromCache(key: string): TData | null {
239
+ const entry = this.cache.get(key);
240
+
241
+ if (!entry) {
242
+ return null;
243
+ }
244
+
245
+ // Check if cache entry has expired
246
+ const age = Date.now() - entry.timestamp.getTime();
247
+ if (entry.ttl !== Infinity && age > entry.ttl) {
248
+ this.cache.delete(key);
249
+ return null;
250
+ }
251
+
252
+ return entry.data;
253
+ }
254
+
255
+ /**
256
+ * Add to cache with TTL
257
+ */
258
+ protected addToCache(key: string, data: TData, ttl?: number): void {
259
+ // Enforce max entries limit
260
+ if (this.cache.size >= this.cacheConfig.maxEntries) {
261
+ // Remove oldest entry
262
+ const oldestKey = this.cache.keys().next().value;
263
+ if (oldestKey) {
264
+ this.cache.delete(oldestKey);
265
+ }
266
+ }
267
+
268
+ this.cache.set(key, {
269
+ data,
270
+ timestamp: new Date(),
271
+ ttl: ttl || this.cacheConfig.ttl
272
+ });
273
+ }
274
+
275
+ /**
276
+ * Track successful fetch for provider
277
+ */
278
+ protected trackSuccess(providerName: string): void {
279
+ const entry = this.providers.get(providerName);
280
+ if (entry) {
281
+ entry.successCount++;
282
+ }
283
+ }
284
+
285
+ /**
286
+ * Track failed fetch for provider
287
+ */
288
+ protected trackError(providerName: string, error: Error): void {
289
+ const entry = this.providers.get(providerName);
290
+ if (entry) {
291
+ entry.errorCount++;
292
+ entry.lastError = error;
293
+ entry.lastErrorTime = new Date();
294
+ }
295
+ }
296
+ }