@pioneer-platform/pioneer-cache 1.0.1 → 1.0.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.
- package/.turbo/turbo-build.log +2 -1
- package/CHANGELOG.md +6 -0
- package/dist/core/base-cache.d.ts +1 -0
- package/dist/core/base-cache.js +73 -9
- package/dist/core/cache-manager.d.ts +6 -1
- package/dist/core/cache-manager.js +26 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +3 -1
- package/dist/stores/balance-cache.d.ts +1 -0
- package/dist/stores/balance-cache.js +63 -10
- package/dist/stores/portfolio-cache.d.ts +79 -0
- package/dist/stores/portfolio-cache.js +189 -0
- package/dist/stores/price-cache.d.ts +1 -1
- package/dist/stores/price-cache.js +35 -15
- package/dist/types/index.d.ts +1 -0
- package/package.json +1 -1
- package/src/core/base-cache.ts +84 -9
- package/src/core/cache-manager.ts +34 -2
- package/src/index.ts +2 -0
- package/src/stores/balance-cache.ts +69 -10
- package/src/stores/portfolio-cache.ts +244 -0
- package/src/stores/price-cache.ts +38 -15
- package/src/types/index.ts +1 -0
- package/test/redis-persistence.test.ts +265 -0
|
@@ -34,14 +34,14 @@ export class BalanceCache extends BaseCache<BalanceData> {
|
|
|
34
34
|
const defaultConfig: CacheConfig = {
|
|
35
35
|
name: 'balance',
|
|
36
36
|
keyPrefix: 'balance_v2:',
|
|
37
|
-
ttl:
|
|
38
|
-
staleThreshold:
|
|
39
|
-
enableTTL:
|
|
40
|
-
queueName: '
|
|
37
|
+
ttl: 0, // Ignored when enableTTL: false
|
|
38
|
+
staleThreshold: 5 * 60 * 1000, // 5 minutes - triggers background refresh
|
|
39
|
+
enableTTL: false, // NEVER EXPIRE - data persists forever
|
|
40
|
+
queueName: 'cache-refresh',
|
|
41
41
|
enableQueue: true,
|
|
42
42
|
maxRetries: 3,
|
|
43
43
|
retryDelay: 10000,
|
|
44
|
-
blockOnMiss: true, // Wait for fresh data on first request
|
|
44
|
+
blockOnMiss: true, // Wait for fresh data on first request - users need real balances!
|
|
45
45
|
enableLegacyFallback: true,
|
|
46
46
|
defaultValue: {
|
|
47
47
|
caip: '',
|
|
@@ -166,20 +166,79 @@ export class BalanceCache extends BaseCache<BalanceData> {
|
|
|
166
166
|
|
|
167
167
|
/**
|
|
168
168
|
* Get balances for multiple assets (batch operation)
|
|
169
|
+
* OPTIMIZED: Uses Redis MGET for single round-trip instead of N individual GETs
|
|
169
170
|
*/
|
|
170
171
|
async getBatchBalances(items: Array<{ caip: string; pubkey: string }>, waitForFresh?: boolean): Promise<BalanceData[]> {
|
|
171
172
|
const tag = this.TAG + 'getBatchBalances | ';
|
|
172
173
|
const startTime = Date.now();
|
|
173
174
|
|
|
174
175
|
try {
|
|
175
|
-
log.info(tag, `Batch request for ${items.length} balances`);
|
|
176
|
+
log.info(tag, `Batch request for ${items.length} balances using Redis MGET`);
|
|
177
|
+
|
|
178
|
+
// Build all Redis keys
|
|
179
|
+
const keys = items.map(item => this.buildKey({ caip: item.caip, pubkey: item.pubkey }));
|
|
180
|
+
|
|
181
|
+
// PERF: Use MGET to fetch all keys in ONE Redis round-trip
|
|
182
|
+
const cachedValues = await this.redis.mget(...keys);
|
|
183
|
+
|
|
184
|
+
// Process results
|
|
185
|
+
const results: BalanceData[] = [];
|
|
186
|
+
const missedItems: Array<{ caip: string; pubkey: string; index: number }> = [];
|
|
187
|
+
|
|
188
|
+
for (let i = 0; i < items.length; i++) {
|
|
189
|
+
const item = items[i];
|
|
190
|
+
const cached = cachedValues[i];
|
|
191
|
+
|
|
192
|
+
if (cached) {
|
|
193
|
+
try {
|
|
194
|
+
const parsed = JSON.parse(cached);
|
|
195
|
+
if (parsed.value && parsed.value.caip && parsed.value.pubkey) {
|
|
196
|
+
results[i] = parsed.value;
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
} catch (e) {
|
|
200
|
+
log.warn(tag, `Failed to parse cached value for ${keys[i]}`);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
176
203
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
204
|
+
// Cache miss - record for fetching
|
|
205
|
+
missedItems.push({ ...item, index: i });
|
|
206
|
+
results[i] = this.config.defaultValue; // Placeholder
|
|
207
|
+
}
|
|
180
208
|
|
|
181
209
|
const responseTime = Date.now() - startTime;
|
|
182
|
-
|
|
210
|
+
const hitRate = ((items.length - missedItems.length) / items.length * 100).toFixed(1);
|
|
211
|
+
log.info(tag, `MGET completed: ${items.length} keys in ${responseTime}ms (${hitRate}% hit rate)`);
|
|
212
|
+
|
|
213
|
+
// If we have cache misses and blocking is enabled, fetch them
|
|
214
|
+
if (missedItems.length > 0) {
|
|
215
|
+
const shouldBlock = waitForFresh !== undefined ? waitForFresh : this.config.blockOnMiss;
|
|
216
|
+
|
|
217
|
+
if (shouldBlock) {
|
|
218
|
+
log.info(tag, `Fetching ${missedItems.length} cache misses...`);
|
|
219
|
+
const fetchStart = Date.now();
|
|
220
|
+
|
|
221
|
+
// Fetch all misses in parallel
|
|
222
|
+
const fetchPromises = missedItems.map(async (item) => {
|
|
223
|
+
try {
|
|
224
|
+
// Use fetchFresh to ensure Redis is updated and requests are deduplicated
|
|
225
|
+
const freshData = await this.fetchFresh({ caip: item.caip, pubkey: item.pubkey });
|
|
226
|
+
results[item.index] = freshData;
|
|
227
|
+
} catch (error) {
|
|
228
|
+
log.error(tag, `Failed to fetch ${item.caip}/${item.pubkey}:`, error);
|
|
229
|
+
results[item.index] = { caip: item.caip, pubkey: item.pubkey, balance: '0' };
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
await Promise.all(fetchPromises);
|
|
234
|
+
log.info(tag, `Fetched ${missedItems.length} misses in ${Date.now() - fetchStart}ms`);
|
|
235
|
+
} else {
|
|
236
|
+
// Non-blocking: trigger background refresh for misses
|
|
237
|
+
missedItems.forEach(item => {
|
|
238
|
+
this.triggerAsyncRefresh({ caip: item.caip, pubkey: item.pubkey }, 'high');
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
}
|
|
183
242
|
|
|
184
243
|
return results;
|
|
185
244
|
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
/*
|
|
2
|
+
PortfolioCache - Portfolio/Charts cache implementation
|
|
3
|
+
|
|
4
|
+
Extends BaseCache with portfolio-specific logic.
|
|
5
|
+
Designed for NON-BLOCKING, instant returns.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { BaseCache } from '../core/base-cache';
|
|
9
|
+
import type { CacheConfig } from '../types';
|
|
10
|
+
|
|
11
|
+
const log = require('@pioneer-platform/loggerdog')();
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Portfolio chart data structure
|
|
15
|
+
* Represents a single asset balance with pricing for charts
|
|
16
|
+
*/
|
|
17
|
+
export interface ChartData {
|
|
18
|
+
caip: string;
|
|
19
|
+
pubkey: string;
|
|
20
|
+
networkId: string;
|
|
21
|
+
symbol: string;
|
|
22
|
+
name: string;
|
|
23
|
+
balance: string;
|
|
24
|
+
priceUsd: number;
|
|
25
|
+
valueUsd: number;
|
|
26
|
+
icon?: string;
|
|
27
|
+
type?: string; // 'native', 'token', etc.
|
|
28
|
+
decimal?: number;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Full portfolio data for a pubkey set
|
|
33
|
+
*/
|
|
34
|
+
export interface PortfolioData {
|
|
35
|
+
pubkeys: Array<{ pubkey: string; caip: string }>;
|
|
36
|
+
charts: ChartData[];
|
|
37
|
+
totalValueUsd: number;
|
|
38
|
+
timestamp: number;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* PortfolioCache - Caches portfolio/chart data
|
|
43
|
+
*
|
|
44
|
+
* CRITICAL: This cache is NON-BLOCKING by design
|
|
45
|
+
* - Returns empty arrays immediately on cache miss
|
|
46
|
+
* - Never blocks waiting for blockchain APIs
|
|
47
|
+
* - Background jobs populate cache for next request
|
|
48
|
+
*/
|
|
49
|
+
export class PortfolioCache extends BaseCache<PortfolioData> {
|
|
50
|
+
private balanceModule: any;
|
|
51
|
+
private marketsModule: any;
|
|
52
|
+
|
|
53
|
+
constructor(redis: any, balanceModule: any, marketsModule: any, config?: Partial<CacheConfig>) {
|
|
54
|
+
const defaultConfig: CacheConfig = {
|
|
55
|
+
name: 'portfolio',
|
|
56
|
+
keyPrefix: 'portfolio_v2:',
|
|
57
|
+
ttl: 0, // Ignored when enableTTL: false
|
|
58
|
+
staleThreshold: 5 * 60 * 1000, // 5 minutes - triggers background refresh
|
|
59
|
+
enableTTL: false, // NEVER EXPIRE - data persists forever, show stale data instantly
|
|
60
|
+
queueName: 'cache-refresh',
|
|
61
|
+
enableQueue: true,
|
|
62
|
+
maxRetries: 3,
|
|
63
|
+
retryDelay: 5000,
|
|
64
|
+
blockOnMiss: false, // CRITICAL: NEVER WAIT! Return empty arrays instantly
|
|
65
|
+
enableLegacyFallback: false, // No legacy portfolio cache format
|
|
66
|
+
defaultValue: {
|
|
67
|
+
pubkeys: [],
|
|
68
|
+
charts: [],
|
|
69
|
+
totalValueUsd: 0,
|
|
70
|
+
timestamp: Date.now()
|
|
71
|
+
},
|
|
72
|
+
useSyncFallback: false, // CRITICAL: NEVER use synchronous fallback - always return instantly
|
|
73
|
+
maxConcurrentJobs: 3, // Limit concurrent portfolio refreshes
|
|
74
|
+
apiTimeout: 30000, // 30s timeout for full portfolio fetch
|
|
75
|
+
logCacheHits: true,
|
|
76
|
+
logCacheMisses: true,
|
|
77
|
+
logRefreshJobs: true
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
super(redis, { ...defaultConfig, ...config });
|
|
81
|
+
this.balanceModule = balanceModule;
|
|
82
|
+
this.marketsModule = marketsModule;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Build Redis key for portfolio data
|
|
87
|
+
*
|
|
88
|
+
* Key strategy: Hash all pubkeys+caips to create a stable identifier
|
|
89
|
+
* Format: portfolio_v2:hash(pubkeys)
|
|
90
|
+
*
|
|
91
|
+
* This allows caching the same portfolio regardless of pubkey order
|
|
92
|
+
*/
|
|
93
|
+
protected buildKey(params: Record<string, any>): string {
|
|
94
|
+
const { pubkeys } = params;
|
|
95
|
+
if (!pubkeys || !Array.isArray(pubkeys) || pubkeys.length === 0) {
|
|
96
|
+
throw new Error('PortfolioCache.buildKey: pubkeys array required');
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Sort pubkeys to create stable hash regardless of order
|
|
100
|
+
const sorted = [...pubkeys].sort((a, b) => {
|
|
101
|
+
const aKey = `${a.caip}:${a.pubkey}`;
|
|
102
|
+
const bKey = `${b.caip}:${b.pubkey}`;
|
|
103
|
+
return aKey.localeCompare(bKey);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
// Create a simple hash from sorted pubkeys
|
|
107
|
+
const keyString = sorted.map(p => `${p.caip}:${p.pubkey}`).join('|');
|
|
108
|
+
const hash = this.simpleHash(keyString);
|
|
109
|
+
|
|
110
|
+
return `${this.config.keyPrefix}${hash}`;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Simple hash function for cache keys
|
|
115
|
+
* Not cryptographic - just needs to be stable and collision-resistant
|
|
116
|
+
*/
|
|
117
|
+
private simpleHash(str: string): string {
|
|
118
|
+
let hash = 0;
|
|
119
|
+
for (let i = 0; i < str.length; i++) {
|
|
120
|
+
const char = str.charCodeAt(i);
|
|
121
|
+
hash = ((hash << 5) - hash) + char;
|
|
122
|
+
hash = hash & hash; // Convert to 32-bit integer
|
|
123
|
+
}
|
|
124
|
+
return Math.abs(hash).toString(36);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Fetch portfolio from blockchain APIs
|
|
129
|
+
*
|
|
130
|
+
* This is the SLOW operation that happens in the background
|
|
131
|
+
* It fetches balances for all pubkeys and enriches with pricing
|
|
132
|
+
*/
|
|
133
|
+
protected async fetchFromSource(params: Record<string, any>): Promise<PortfolioData> {
|
|
134
|
+
const tag = this.TAG + 'fetchFromSource | ';
|
|
135
|
+
const startTime = Date.now();
|
|
136
|
+
|
|
137
|
+
try {
|
|
138
|
+
const { pubkeys } = params;
|
|
139
|
+
log.info(tag, `Fetching portfolio for ${pubkeys.length} pubkeys`);
|
|
140
|
+
|
|
141
|
+
const charts: ChartData[] = [];
|
|
142
|
+
|
|
143
|
+
// Fetch balances for all pubkeys in parallel
|
|
144
|
+
const balancePromises = pubkeys.map(async (item: { pubkey: string; caip: string }) => {
|
|
145
|
+
try {
|
|
146
|
+
// Extract networkId from CAIP
|
|
147
|
+
const networkId = item.caip.split('/')[0];
|
|
148
|
+
|
|
149
|
+
// Fetch balance
|
|
150
|
+
const asset = { caip: item.caip };
|
|
151
|
+
const owner = { pubkey: item.pubkey };
|
|
152
|
+
const balanceInfo = await this.balanceModule.getBalance(asset, owner);
|
|
153
|
+
|
|
154
|
+
if (!balanceInfo || !balanceInfo.balance) {
|
|
155
|
+
log.debug(tag, `No balance for ${item.caip}/${item.pubkey.substring(0, 10)}...`);
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Skip zero balances
|
|
160
|
+
const balanceNum = parseFloat(balanceInfo.balance);
|
|
161
|
+
if (isNaN(balanceNum) || balanceNum === 0) {
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Get asset metadata
|
|
166
|
+
const assetData = require('@pioneer-platform/pioneer-discovery').assetData;
|
|
167
|
+
const assetInfo = assetData[item.caip.toUpperCase()] || assetData[item.caip.toLowerCase()] || {};
|
|
168
|
+
|
|
169
|
+
// Get price
|
|
170
|
+
let priceUsd = 0;
|
|
171
|
+
try {
|
|
172
|
+
priceUsd = await this.marketsModule.getAssetPriceByCaip(item.caip);
|
|
173
|
+
if (isNaN(priceUsd) || priceUsd < 0) {
|
|
174
|
+
priceUsd = 0;
|
|
175
|
+
}
|
|
176
|
+
} catch (priceError) {
|
|
177
|
+
log.warn(tag, `Error fetching price for ${item.caip}:`, priceError);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const valueUsd = balanceNum * priceUsd;
|
|
181
|
+
|
|
182
|
+
const chartData: ChartData = {
|
|
183
|
+
caip: item.caip,
|
|
184
|
+
pubkey: item.pubkey,
|
|
185
|
+
networkId,
|
|
186
|
+
symbol: assetInfo.symbol || 'UNKNOWN',
|
|
187
|
+
name: assetInfo.name || 'Unknown Asset',
|
|
188
|
+
balance: balanceInfo.balance,
|
|
189
|
+
priceUsd,
|
|
190
|
+
valueUsd,
|
|
191
|
+
icon: assetInfo.icon || '',
|
|
192
|
+
type: assetInfo.type || 'native',
|
|
193
|
+
decimal: assetInfo.decimal
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
return chartData;
|
|
197
|
+
|
|
198
|
+
} catch (error) {
|
|
199
|
+
log.error(tag, `Error fetching balance for ${item.caip}/${item.pubkey}:`, error);
|
|
200
|
+
return null;
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
const results = await Promise.all(balancePromises);
|
|
205
|
+
|
|
206
|
+
// Filter out nulls and calculate total
|
|
207
|
+
const validCharts = results.filter((c): c is ChartData => c !== null);
|
|
208
|
+
const totalValueUsd = validCharts.reduce((sum, c) => sum + c.valueUsd, 0);
|
|
209
|
+
|
|
210
|
+
const fetchTime = Date.now() - startTime;
|
|
211
|
+
log.info(tag, `✅ Fetched portfolio: ${validCharts.length} assets, $${totalValueUsd.toFixed(2)} in ${fetchTime}ms`);
|
|
212
|
+
|
|
213
|
+
return {
|
|
214
|
+
pubkeys,
|
|
215
|
+
charts: validCharts,
|
|
216
|
+
totalValueUsd,
|
|
217
|
+
timestamp: Date.now()
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
} catch (error) {
|
|
221
|
+
log.error(tag, 'Error fetching portfolio:', error);
|
|
222
|
+
throw error;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* No legacy cache format for portfolios
|
|
228
|
+
*/
|
|
229
|
+
protected async getLegacyCached(params: Record<string, any>): Promise<PortfolioData | null> {
|
|
230
|
+
return null;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Get portfolio for a set of pubkeys
|
|
235
|
+
* Convenience method that wraps base get()
|
|
236
|
+
*
|
|
237
|
+
* RETURNS INSTANTLY - either cached data or empty arrays
|
|
238
|
+
*/
|
|
239
|
+
async getPortfolio(pubkeys: Array<{ pubkey: string; caip: string }>, waitForFresh?: boolean): Promise<PortfolioData> {
|
|
240
|
+
const result = await this.get({ pubkeys }, waitForFresh);
|
|
241
|
+
return result.value || this.config.defaultValue;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
@@ -29,21 +29,22 @@ export class PriceCache extends BaseCache<PriceData> {
|
|
|
29
29
|
const defaultConfig: CacheConfig = {
|
|
30
30
|
name: 'price',
|
|
31
31
|
keyPrefix: 'price_v2:',
|
|
32
|
-
ttl:
|
|
33
|
-
staleThreshold: 30 * 60 * 1000, // 30 minutes
|
|
34
|
-
enableTTL:
|
|
35
|
-
queueName: '
|
|
32
|
+
ttl: 0, // Ignored when enableTTL: false
|
|
33
|
+
staleThreshold: 30 * 60 * 1000, // 30 minutes - triggers background refresh
|
|
34
|
+
enableTTL: false, // NEVER EXPIRE - data persists forever, show stale prices instantly
|
|
35
|
+
queueName: 'cache-refresh',
|
|
36
36
|
enableQueue: true,
|
|
37
37
|
maxRetries: 3,
|
|
38
38
|
retryDelay: 5000,
|
|
39
|
-
blockOnMiss:
|
|
39
|
+
blockOnMiss: false, // CRITICAL: NEVER WAIT! Return $0 instantly on cache miss, refresh async in background
|
|
40
40
|
enableLegacyFallback: true,
|
|
41
41
|
defaultValue: {
|
|
42
42
|
caip: '',
|
|
43
43
|
price: 0
|
|
44
44
|
},
|
|
45
|
+
useSyncFallback: false, // CRITICAL: NEVER use synchronous fallback - always return instantly with $0
|
|
45
46
|
maxConcurrentJobs: 5,
|
|
46
|
-
apiTimeout:
|
|
47
|
+
apiTimeout: 2000, // Reduced from 5000ms for faster failures
|
|
47
48
|
logCacheHits: false,
|
|
48
49
|
logCacheMisses: true,
|
|
49
50
|
logRefreshJobs: true
|
|
@@ -69,7 +70,7 @@ export class PriceCache extends BaseCache<PriceData> {
|
|
|
69
70
|
|
|
70
71
|
/**
|
|
71
72
|
* Fetch price from markets API using CAIP-first approach
|
|
72
|
-
* FIX #7:
|
|
73
|
+
* FIX #7: Graceful handling of zero prices to prevent cache disruption
|
|
73
74
|
*/
|
|
74
75
|
protected async fetchFromSource(params: Record<string, any>): Promise<PriceData> {
|
|
75
76
|
const tag = this.TAG + 'fetchFromSource | ';
|
|
@@ -81,12 +82,30 @@ export class PriceCache extends BaseCache<PriceData> {
|
|
|
81
82
|
// This directly queries the markets module with CAIP identifiers
|
|
82
83
|
const price = await this.markets.getAssetPriceByCaip(caip);
|
|
83
84
|
|
|
84
|
-
// FIX #7:
|
|
85
|
-
// This prevents
|
|
85
|
+
// FIX #7: Gracefully handle zero prices without throwing
|
|
86
|
+
// This prevents disrupting batch operations during API rate limits
|
|
86
87
|
if (isNaN(price) || price <= 0) {
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
88
|
+
log.warn(tag, `Price fetch returned $${price} for ${caip} (likely API timeout or rate limit) - returning stale cache if available`);
|
|
89
|
+
|
|
90
|
+
// Try to get stale cached value instead of failing
|
|
91
|
+
const key = this.buildKey(params);
|
|
92
|
+
const cachedValue = await this.getCached(key);
|
|
93
|
+
|
|
94
|
+
if (cachedValue && cachedValue.value.price > 0) {
|
|
95
|
+
log.info(tag, `Returning stale cached price for ${caip}: $${cachedValue.value.price}`);
|
|
96
|
+
return cachedValue.value;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Try legacy cache as fallback
|
|
100
|
+
const legacyValue = await this.getLegacyCached(params);
|
|
101
|
+
if (legacyValue && legacyValue.price > 0) {
|
|
102
|
+
log.info(tag, `Returning legacy cached price for ${caip}: $${legacyValue.price}`);
|
|
103
|
+
return legacyValue;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Last resort: return zero price but don't cache it
|
|
107
|
+
log.warn(tag, `No cached price available for ${caip}, returning zero`);
|
|
108
|
+
throw new Error(`No valid price available for ${caip}`);
|
|
90
109
|
}
|
|
91
110
|
|
|
92
111
|
log.debug(tag, `Fetched price for ${caip}: $${price}`);
|
|
@@ -98,9 +117,13 @@ export class PriceCache extends BaseCache<PriceData> {
|
|
|
98
117
|
};
|
|
99
118
|
|
|
100
119
|
} catch (error) {
|
|
101
|
-
//
|
|
102
|
-
|
|
103
|
-
|
|
120
|
+
// Log as warning instead of error for expected API issues
|
|
121
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
122
|
+
if (errorMsg.includes('rate limit') || errorMsg.includes('timeout') || errorMsg.includes('No valid price')) {
|
|
123
|
+
log.warn(tag, `Expected API issue: ${errorMsg}`);
|
|
124
|
+
} else {
|
|
125
|
+
log.error(tag, `Unexpected error fetching price:`, error);
|
|
126
|
+
}
|
|
104
127
|
throw error;
|
|
105
128
|
}
|
|
106
129
|
}
|
package/src/types/index.ts
CHANGED
|
@@ -25,6 +25,7 @@ export interface CacheConfig {
|
|
|
25
25
|
blockOnMiss: boolean; // Wait for fresh data on cache miss
|
|
26
26
|
enableLegacyFallback: boolean; // Try legacy cache keys on miss
|
|
27
27
|
defaultValue: any; // Default value to return on error
|
|
28
|
+
useSyncFallback?: boolean; // Use synchronous fallback when queue fails (default: true for blockOnMiss, false otherwise)
|
|
28
29
|
|
|
29
30
|
// Performance
|
|
30
31
|
maxConcurrentJobs: number; // Max jobs processed concurrently
|