@fin.cx/opendata 3.1.0 → 3.2.0
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.
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/stocks/classes.baseproviderservice.d.ts +129 -0
- package/dist_ts/stocks/classes.baseproviderservice.js +204 -0
- package/dist_ts/stocks/classes.stockdataservice.d.ts +121 -0
- package/dist_ts/stocks/classes.stockdataservice.js +495 -0
- package/dist_ts/stocks/index.d.ts +3 -0
- package/dist_ts/stocks/index.js +5 -1
- package/dist_ts/stocks/interfaces/stockdata.d.ts +57 -0
- package/dist_ts/stocks/interfaces/stockdata.js +2 -0
- package/package.json +1 -1
- package/readme.md +133 -61
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/stocks/classes.baseproviderservice.ts +296 -0
- package/ts/stocks/classes.stockdataservice.ts +647 -0
- package/ts/stocks/index.ts +5 -0
- package/ts/stocks/interfaces/stockdata.ts +65 -0
package/readme.md
CHANGED
|
@@ -14,64 +14,110 @@ pnpm add @fin.cx/opendata
|
|
|
14
14
|
|
|
15
15
|
## Quick Start
|
|
16
16
|
|
|
17
|
-
###
|
|
17
|
+
### ✨ Unified Stock Data API (Recommended)
|
|
18
18
|
|
|
19
|
-
Get
|
|
19
|
+
Get complete stock data with automatic enrichment - the elegant way:
|
|
20
20
|
|
|
21
21
|
```typescript
|
|
22
|
-
import {
|
|
22
|
+
import { StockDataService, YahooFinanceProvider, SecEdgarProvider } from '@fin.cx/opendata';
|
|
23
23
|
|
|
24
|
-
// Initialize service
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
24
|
+
// Initialize unified service
|
|
25
|
+
const stockData = new StockDataService();
|
|
26
|
+
|
|
27
|
+
// Register providers
|
|
28
|
+
stockData.registerPriceProvider(new YahooFinanceProvider());
|
|
29
|
+
stockData.registerFundamentalsProvider(new SecEdgarProvider({
|
|
30
|
+
userAgent: 'YourCompany youremail@example.com'
|
|
31
|
+
}));
|
|
32
|
+
|
|
33
|
+
// ✨ Get complete stock data with ONE method call
|
|
34
|
+
const apple = await stockData.getStockData('AAPL');
|
|
35
|
+
|
|
36
|
+
console.log({
|
|
37
|
+
company: apple.fundamentals.companyName, // "Apple Inc."
|
|
38
|
+
price: apple.price.price, // $270.37
|
|
39
|
+
marketCap: apple.fundamentals.marketCap, // $4.13T (auto-calculated!)
|
|
40
|
+
peRatio: apple.fundamentals.priceToEarnings, // 28.42 (auto-calculated!)
|
|
41
|
+
pbRatio: apple.fundamentals.priceToBook, // 45.12 (auto-calculated!)
|
|
42
|
+
eps: apple.fundamentals.earningsPerShareDiluted, // $6.13
|
|
43
|
+
revenue: apple.fundamentals.revenue // $385.6B
|
|
28
44
|
});
|
|
29
45
|
|
|
30
|
-
//
|
|
31
|
-
|
|
46
|
+
// Batch fetch with automatic enrichment
|
|
47
|
+
const stocks = await stockData.getBatchStockData(['AAPL', 'MSFT', 'GOOGL']);
|
|
32
48
|
|
|
33
|
-
|
|
34
|
-
|
|
49
|
+
stocks.forEach(stock => {
|
|
50
|
+
console.log(`${stock.ticker}: $${stock.price.price.toFixed(2)}, ` +
|
|
51
|
+
`P/E ${stock.fundamentals?.priceToEarnings?.toFixed(2)}`);
|
|
52
|
+
});
|
|
53
|
+
// Output:
|
|
54
|
+
// AAPL: $270.37, P/E 28.42
|
|
55
|
+
// MSFT: $425.50, P/E 34.21
|
|
56
|
+
// GOOGL: $142.15, P/E 25.63
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
**Why use the unified API?**
|
|
60
|
+
- ✅ **Single service** for both prices and fundamentals
|
|
61
|
+
- ✅ **Automatic enrichment** - Market cap, P/E, P/B calculated automatically
|
|
62
|
+
- ✅ **One method call** - No manual `enrichWithPrice()` calls
|
|
63
|
+
- ✅ **Simplified code** - Less boilerplate, more readable
|
|
64
|
+
- ✅ **Type-safe** - Full TypeScript support
|
|
65
|
+
|
|
66
|
+
### 📈 Price-Only Data (Alternative)
|
|
35
67
|
|
|
68
|
+
If you only need prices without fundamentals:
|
|
69
|
+
|
|
70
|
+
```typescript
|
|
71
|
+
import { StockDataService, YahooFinanceProvider } from '@fin.cx/opendata';
|
|
72
|
+
|
|
73
|
+
const stockData = new StockDataService();
|
|
74
|
+
stockData.registerPriceProvider(new YahooFinanceProvider());
|
|
75
|
+
|
|
76
|
+
// Get just the price
|
|
77
|
+
const price = await stockData.getPrice('AAPL');
|
|
78
|
+
console.log(`${price.ticker}: $${price.price}`);
|
|
79
|
+
|
|
80
|
+
// Or use StockPriceService directly for more control
|
|
81
|
+
import { StockPriceService } from '@fin.cx/opendata';
|
|
82
|
+
|
|
83
|
+
const stockService = new StockPriceService({ ttl: 60000 });
|
|
84
|
+
stockService.register(new YahooFinanceProvider());
|
|
85
|
+
|
|
86
|
+
const apple = await stockService.getData({ type: 'current', ticker: 'AAPL' });
|
|
36
87
|
console.log(`${apple.companyFullName}: $${apple.price}`);
|
|
37
|
-
// Output: "Apple Inc (NASDAQ:AAPL): $270.37"
|
|
38
88
|
```
|
|
39
89
|
|
|
40
|
-
### 💰
|
|
90
|
+
### 💰 Fundamentals-Only Data (Alternative)
|
|
41
91
|
|
|
42
|
-
|
|
92
|
+
If you only need fundamentals without prices:
|
|
43
93
|
|
|
44
94
|
```typescript
|
|
45
|
-
import {
|
|
95
|
+
import { StockDataService, SecEdgarProvider } from '@fin.cx/opendata';
|
|
46
96
|
|
|
47
|
-
|
|
48
|
-
|
|
97
|
+
const stockData = new StockDataService();
|
|
98
|
+
stockData.registerFundamentalsProvider(new SecEdgarProvider({
|
|
49
99
|
userAgent: 'YourCompany youremail@example.com'
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
const fundamentalsService = new FundamentalsService();
|
|
53
|
-
fundamentalsService.register(secEdgar);
|
|
100
|
+
}));
|
|
54
101
|
|
|
55
|
-
//
|
|
56
|
-
const fundamentals = await
|
|
102
|
+
// Get just fundamentals
|
|
103
|
+
const fundamentals = await stockData.getFundamentals('AAPL');
|
|
57
104
|
|
|
58
105
|
console.log({
|
|
59
|
-
company: fundamentals.companyName,
|
|
60
|
-
eps: fundamentals.earningsPerShareDiluted,
|
|
61
|
-
revenue: fundamentals.revenue,
|
|
62
|
-
sharesOutstanding: fundamentals.sharesOutstanding
|
|
106
|
+
company: fundamentals.companyName,
|
|
107
|
+
eps: fundamentals.earningsPerShareDiluted,
|
|
108
|
+
revenue: fundamentals.revenue,
|
|
109
|
+
sharesOutstanding: fundamentals.sharesOutstanding
|
|
63
110
|
});
|
|
64
111
|
|
|
65
|
-
//
|
|
66
|
-
|
|
67
|
-
const enriched = await fundamentalsService.enrichWithPrice(fundamentals, price.price);
|
|
112
|
+
// Or use FundamentalsService directly
|
|
113
|
+
import { FundamentalsService } from '@fin.cx/opendata';
|
|
68
114
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
115
|
+
const fundamentalsService = new FundamentalsService();
|
|
116
|
+
fundamentalsService.register(new SecEdgarProvider({
|
|
117
|
+
userAgent: 'YourCompany youremail@example.com'
|
|
118
|
+
}));
|
|
119
|
+
|
|
120
|
+
const data = await fundamentalsService.getFundamentals('AAPL');
|
|
75
121
|
```
|
|
76
122
|
|
|
77
123
|
### 🏢 German Business Intelligence
|
|
@@ -136,44 +182,70 @@ const details = await openData.handelsregister.getSpecificCompany({
|
|
|
136
182
|
|
|
137
183
|
## Advanced Examples
|
|
138
184
|
|
|
139
|
-
### Combined Market Analysis
|
|
185
|
+
### Combined Market Analysis (Unified API)
|
|
140
186
|
|
|
141
|
-
|
|
187
|
+
Analyze multiple companies with automatic enrichment:
|
|
142
188
|
|
|
143
189
|
```typescript
|
|
144
|
-
import {
|
|
145
|
-
|
|
146
|
-
// Setup services
|
|
147
|
-
const stockService = new StockPriceService({ ttl: 60000 });
|
|
148
|
-
stockService.register(new MarketstackProvider('YOUR_API_KEY'));
|
|
190
|
+
import { StockDataService, YahooFinanceProvider, SecEdgarProvider } from '@fin.cx/opendata';
|
|
149
191
|
|
|
150
|
-
|
|
151
|
-
|
|
192
|
+
// Setup unified service
|
|
193
|
+
const stockData = new StockDataService();
|
|
194
|
+
stockData.registerPriceProvider(new YahooFinanceProvider());
|
|
195
|
+
stockData.registerFundamentalsProvider(new SecEdgarProvider({
|
|
152
196
|
userAgent: 'YourCompany youremail@example.com'
|
|
153
197
|
}));
|
|
154
198
|
|
|
155
|
-
// Analyze multiple companies
|
|
199
|
+
// Analyze multiple companies with ONE call
|
|
156
200
|
const tickers = ['AAPL', 'MSFT', 'GOOGL', 'AMZN'];
|
|
201
|
+
const stocks = await stockData.getBatchStockData(tickers);
|
|
202
|
+
|
|
203
|
+
// All metrics are automatically calculated!
|
|
204
|
+
stocks.forEach(stock => {
|
|
205
|
+
if (stock.fundamentals) {
|
|
206
|
+
console.log(`\n${stock.fundamentals.companyName} (${stock.ticker})`);
|
|
207
|
+
console.log(` Price: $${stock.price.price.toFixed(2)}`);
|
|
208
|
+
console.log(` Market Cap: $${(stock.fundamentals.marketCap! / 1e9).toFixed(2)}B`);
|
|
209
|
+
console.log(` P/E Ratio: ${stock.fundamentals.priceToEarnings!.toFixed(2)}`);
|
|
210
|
+
console.log(` Revenue: $${(stock.fundamentals.revenue! / 1e9).toFixed(2)}B`);
|
|
211
|
+
console.log(` EPS: $${stock.fundamentals.earningsPerShareDiluted!.toFixed(2)}`);
|
|
212
|
+
}
|
|
213
|
+
});
|
|
157
214
|
|
|
215
|
+
// Or analyze one-by-one with automatic enrichment
|
|
158
216
|
for (const ticker of tickers) {
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
]);
|
|
164
|
-
|
|
165
|
-
// Calculate metrics
|
|
166
|
-
const enriched = await fundamentalsService.enrichWithPrice(fundamentals, price.price);
|
|
167
|
-
|
|
168
|
-
console.log(`\n${fundamentals.companyName} (${ticker})`);
|
|
169
|
-
console.log(` Price: $${price.price.toFixed(2)}`);
|
|
170
|
-
console.log(` Market Cap: $${(enriched.marketCap! / 1e9).toFixed(2)}B`);
|
|
171
|
-
console.log(` P/E Ratio: ${enriched.priceToEarnings!.toFixed(2)}`);
|
|
172
|
-
console.log(` Revenue: $${(fundamentals.revenue! / 1e9).toFixed(2)}B`);
|
|
173
|
-
console.log(` EPS: $${fundamentals.earningsPerShareDiluted!.toFixed(2)}`);
|
|
217
|
+
const stock = await stockData.getStockData(ticker);
|
|
218
|
+
|
|
219
|
+
// Everything is already enriched - no manual calculations needed!
|
|
220
|
+
console.log(`${ticker}: P/E ${stock.fundamentals?.priceToEarnings?.toFixed(2)}`);
|
|
174
221
|
}
|
|
175
222
|
```
|
|
176
223
|
|
|
224
|
+
### Fundamental Data Screening
|
|
225
|
+
|
|
226
|
+
Screen stocks by financial metrics:
|
|
227
|
+
|
|
228
|
+
```typescript
|
|
229
|
+
// Fetch data for multiple tickers
|
|
230
|
+
const tickers = ['AAPL', 'MSFT', 'GOOGL', 'AMZN', 'META', 'NVDA', 'TSLA'];
|
|
231
|
+
const stocks = await stockData.getBatchStockData(tickers);
|
|
232
|
+
|
|
233
|
+
// Filter by criteria (all metrics auto-calculated!)
|
|
234
|
+
const valueStocks = stocks.filter(stock => {
|
|
235
|
+
const f = stock.fundamentals;
|
|
236
|
+
return f &&
|
|
237
|
+
f.priceToEarnings! < 30 && // P/E under 30
|
|
238
|
+
f.priceToBook! < 10 && // P/B under 10
|
|
239
|
+
f.revenue! > 100_000_000_000; // Revenue > $100B
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
console.log('\n💎 Value Stocks:');
|
|
243
|
+
valueStocks.forEach(stock => {
|
|
244
|
+
console.log(`${stock.ticker}: P/E ${stock.fundamentals!.priceToEarnings!.toFixed(2)}, ` +
|
|
245
|
+
`P/B ${stock.fundamentals!.priceToBook!.toFixed(2)}`);
|
|
246
|
+
});
|
|
247
|
+
```
|
|
248
|
+
|
|
177
249
|
### Historical Data Analysis
|
|
178
250
|
|
|
179
251
|
Fetch and analyze historical price trends:
|
package/ts/00_commitinfo_data.ts
CHANGED
|
@@ -3,6 +3,6 @@
|
|
|
3
3
|
*/
|
|
4
4
|
export const commitinfo = {
|
|
5
5
|
name: '@fin.cx/opendata',
|
|
6
|
-
version: '3.
|
|
6
|
+
version: '3.2.0',
|
|
7
7
|
description: 'A comprehensive TypeScript library for accessing business data and real-time financial information. Features include German company data management with MongoDB integration, JSONL bulk processing, automated Handelsregister interactions, and real-time stock market data from multiple providers.'
|
|
8
8
|
}
|
|
@@ -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
|
+
}
|