@pioneer-platform/pioneer-cache 1.0.0 → 1.0.1
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 +1 -2
- package/CHANGELOG.md +7 -0
- package/dist/core/base-cache.d.ts +2 -0
- package/dist/core/base-cache.js +35 -15
- package/dist/stores/price-cache.d.ts +2 -1
- package/dist/stores/price-cache.js +16 -31
- package/package.json +4 -4
- package/src/core/base-cache.ts +38 -15
- package/src/stores/price-cache.ts +16 -33
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,2 +1 @@
|
|
|
1
|
-
|
|
2
|
-
[0m[2m[35m$[0m [2m[1mtsc[0m
|
|
1
|
+
$ tsc
|
package/CHANGELOG.md
ADDED
|
@@ -8,6 +8,7 @@ export declare abstract class BaseCache<T> {
|
|
|
8
8
|
private cachedStats;
|
|
9
9
|
private cachedStatsTimestamp;
|
|
10
10
|
private readonly STATS_CACHE_TTL;
|
|
11
|
+
private pendingFetches;
|
|
11
12
|
constructor(redis: any, config: CacheConfig);
|
|
12
13
|
/**
|
|
13
14
|
* Initialize Redis queue for background refresh
|
|
@@ -36,6 +37,7 @@ export declare abstract class BaseCache<T> {
|
|
|
36
37
|
/**
|
|
37
38
|
* Fetch fresh data and update cache
|
|
38
39
|
* FIX #1 & #4: Used for blocking requests and fallback
|
|
40
|
+
* FIX #6: Request deduplication to prevent thundering herd
|
|
39
41
|
*/
|
|
40
42
|
fetchFresh(params: Record<string, any>): Promise<T>;
|
|
41
43
|
/**
|
package/dist/core/base-cache.js
CHANGED
|
@@ -15,6 +15,9 @@ class BaseCache {
|
|
|
15
15
|
this.cachedStats = null;
|
|
16
16
|
this.cachedStatsTimestamp = 0;
|
|
17
17
|
this.STATS_CACHE_TTL = 30000; // 30 seconds
|
|
18
|
+
// FIX #6: Request deduplication to prevent thundering herd
|
|
19
|
+
// Tracks in-flight network requests to prevent duplicate API calls
|
|
20
|
+
this.pendingFetches = new Map();
|
|
18
21
|
this.redis = redis;
|
|
19
22
|
this.config = config;
|
|
20
23
|
this.TAG = ` | ${config.name}Cache | `;
|
|
@@ -245,26 +248,43 @@ class BaseCache {
|
|
|
245
248
|
/**
|
|
246
249
|
* Fetch fresh data and update cache
|
|
247
250
|
* FIX #1 & #4: Used for blocking requests and fallback
|
|
251
|
+
* FIX #6: Request deduplication to prevent thundering herd
|
|
248
252
|
*/
|
|
249
253
|
async fetchFresh(params) {
|
|
250
254
|
const tag = this.TAG + 'fetchFresh | ';
|
|
251
255
|
const startTime = Date.now();
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
await this.updateCache(key, value);
|
|
259
|
-
const fetchTime = Date.now() - startTime;
|
|
260
|
-
log.info(tag, `✅ Fetched fresh data in ${fetchTime}ms: ${key}`);
|
|
261
|
-
return value;
|
|
262
|
-
}
|
|
263
|
-
catch (error) {
|
|
264
|
-
const fetchTime = Date.now() - startTime;
|
|
265
|
-
log.error(tag, `Failed to fetch fresh data after ${fetchTime}ms:`, error);
|
|
266
|
-
return this.config.defaultValue;
|
|
256
|
+
const key = this.buildKey(params);
|
|
257
|
+
// FIX #6: Check if there's already a pending fetch for this key
|
|
258
|
+
const existingFetch = this.pendingFetches.get(key);
|
|
259
|
+
if (existingFetch) {
|
|
260
|
+
log.debug(tag, `Coalescing request for: ${key} (fetch already in progress)`);
|
|
261
|
+
return existingFetch;
|
|
267
262
|
}
|
|
263
|
+
// Create the fetch promise
|
|
264
|
+
const fetchPromise = (async () => {
|
|
265
|
+
try {
|
|
266
|
+
log.info(tag, `Fetching fresh data: ${key}`);
|
|
267
|
+
// Call subclass-specific fetch implementation
|
|
268
|
+
const value = await this.fetchFromSource(params);
|
|
269
|
+
// Update cache
|
|
270
|
+
await this.updateCache(key, value);
|
|
271
|
+
const fetchTime = Date.now() - startTime;
|
|
272
|
+
log.info(tag, `✅ Fetched fresh data in ${fetchTime}ms: ${key}`);
|
|
273
|
+
return value;
|
|
274
|
+
}
|
|
275
|
+
catch (error) {
|
|
276
|
+
const fetchTime = Date.now() - startTime;
|
|
277
|
+
log.error(tag, `Failed to fetch fresh data after ${fetchTime}ms:`, error);
|
|
278
|
+
return this.config.defaultValue;
|
|
279
|
+
}
|
|
280
|
+
finally {
|
|
281
|
+
// Clean up pending fetch
|
|
282
|
+
this.pendingFetches.delete(key);
|
|
283
|
+
}
|
|
284
|
+
})();
|
|
285
|
+
// Store the promise so concurrent requests can reuse it
|
|
286
|
+
this.pendingFetches.set(key, fetchPromise);
|
|
287
|
+
return fetchPromise;
|
|
268
288
|
}
|
|
269
289
|
/**
|
|
270
290
|
* Migrate legacy cache value to new format
|
|
@@ -20,7 +20,8 @@ export declare class PriceCache extends BaseCache<PriceData> {
|
|
|
20
20
|
*/
|
|
21
21
|
protected buildKey(params: Record<string, any>): string;
|
|
22
22
|
/**
|
|
23
|
-
* Fetch price from markets API
|
|
23
|
+
* Fetch price from markets API using CAIP-first approach
|
|
24
|
+
* FIX #7: Throws error on zero price to prevent caching invalid data
|
|
24
25
|
*/
|
|
25
26
|
protected fetchFromSource(params: Record<string, any>): Promise<PriceData>;
|
|
26
27
|
/**
|
|
@@ -24,7 +24,7 @@ class PriceCache extends base_cache_1.BaseCache {
|
|
|
24
24
|
enableQueue: true,
|
|
25
25
|
maxRetries: 3,
|
|
26
26
|
retryDelay: 5000,
|
|
27
|
-
blockOnMiss:
|
|
27
|
+
blockOnMiss: true, // MUST wait for price on first request (changed from false)
|
|
28
28
|
enableLegacyFallback: true,
|
|
29
29
|
defaultValue: {
|
|
30
30
|
caip: '',
|
|
@@ -52,49 +52,34 @@ class PriceCache extends base_cache_1.BaseCache {
|
|
|
52
52
|
return `${this.config.keyPrefix}${normalizedCaip}`;
|
|
53
53
|
}
|
|
54
54
|
/**
|
|
55
|
-
* Fetch price from markets API
|
|
55
|
+
* Fetch price from markets API using CAIP-first approach
|
|
56
|
+
* FIX #7: Throws error on zero price to prevent caching invalid data
|
|
56
57
|
*/
|
|
57
58
|
async fetchFromSource(params) {
|
|
58
59
|
const tag = this.TAG + 'fetchFromSource | ';
|
|
59
60
|
try {
|
|
60
61
|
const { caip } = params;
|
|
61
|
-
//
|
|
62
|
-
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
return {
|
|
67
|
-
caip,
|
|
68
|
-
price: 0
|
|
69
|
-
};
|
|
70
|
-
}
|
|
71
|
-
// Get price from markets module
|
|
72
|
-
const symbol = asset.symbol.toLowerCase();
|
|
73
|
-
const priceResult = await this.markets.getPrice(symbol);
|
|
74
|
-
// Handle different response formats
|
|
75
|
-
let price = 0;
|
|
76
|
-
if (typeof priceResult === 'object' && priceResult.price) {
|
|
77
|
-
price = parseFloat(priceResult.price);
|
|
78
|
-
}
|
|
79
|
-
else if (typeof priceResult === 'number') {
|
|
80
|
-
price = priceResult;
|
|
81
|
-
}
|
|
62
|
+
// Use CAIP-first API (no symbol conversion needed!)
|
|
63
|
+
// This directly queries the markets module with CAIP identifiers
|
|
64
|
+
const price = await this.markets.getAssetPriceByCaip(caip);
|
|
65
|
+
// FIX #7: Throw error instead of returning 0 price
|
|
66
|
+
// This prevents caching invalid $0 prices from rate limits or API failures
|
|
82
67
|
if (isNaN(price) || price <= 0) {
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
price: 0
|
|
87
|
-
};
|
|
68
|
+
const errorMsg = `Price fetch failed for ${caip}: got $${price} (likely API timeout or rate limit)`;
|
|
69
|
+
log.warn(tag, errorMsg);
|
|
70
|
+
throw new Error(errorMsg);
|
|
88
71
|
}
|
|
89
|
-
log.debug(tag, `Fetched price for ${caip}
|
|
72
|
+
log.debug(tag, `Fetched price for ${caip}: $${price}`);
|
|
90
73
|
return {
|
|
91
74
|
caip,
|
|
92
75
|
price,
|
|
93
|
-
source: 'markets'
|
|
76
|
+
source: 'markets-caip'
|
|
94
77
|
};
|
|
95
78
|
}
|
|
96
79
|
catch (error) {
|
|
97
|
-
|
|
80
|
+
// Re-throw to prevent caching zero prices
|
|
81
|
+
// BaseCache.fetchFresh() will catch this and return defaultValue
|
|
82
|
+
// but won't cache the error
|
|
98
83
|
throw error;
|
|
99
84
|
}
|
|
100
85
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pioneer-platform/pioneer-cache",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"description": "Unified caching system for Pioneer platform with Redis backend",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
|
@@ -20,9 +20,9 @@
|
|
|
20
20
|
"author": "Pioneer Platform",
|
|
21
21
|
"license": "MIT",
|
|
22
22
|
"dependencies": {
|
|
23
|
-
"@pioneer-platform/loggerdog": "
|
|
24
|
-
"@pioneer-platform/redis-queue": "
|
|
25
|
-
"@pioneer-platform/default-redis": "
|
|
23
|
+
"@pioneer-platform/loggerdog": "^8.0.0",
|
|
24
|
+
"@pioneer-platform/redis-queue": "^8.0.0",
|
|
25
|
+
"@pioneer-platform/default-redis": "^8.0.0"
|
|
26
26
|
},
|
|
27
27
|
"devDependencies": {
|
|
28
28
|
"@types/node": "^20.0.0",
|
package/src/core/base-cache.ts
CHANGED
|
@@ -31,6 +31,10 @@ export abstract class BaseCache<T> {
|
|
|
31
31
|
private cachedStatsTimestamp: number = 0;
|
|
32
32
|
private readonly STATS_CACHE_TTL = 30000; // 30 seconds
|
|
33
33
|
|
|
34
|
+
// FIX #6: Request deduplication to prevent thundering herd
|
|
35
|
+
// Tracks in-flight network requests to prevent duplicate API calls
|
|
36
|
+
private pendingFetches: Map<string, Promise<T>> = new Map();
|
|
37
|
+
|
|
34
38
|
constructor(redis: any, config: CacheConfig) {
|
|
35
39
|
this.redis = redis;
|
|
36
40
|
this.config = config;
|
|
@@ -290,31 +294,50 @@ export abstract class BaseCache<T> {
|
|
|
290
294
|
/**
|
|
291
295
|
* Fetch fresh data and update cache
|
|
292
296
|
* FIX #1 & #4: Used for blocking requests and fallback
|
|
297
|
+
* FIX #6: Request deduplication to prevent thundering herd
|
|
293
298
|
*/
|
|
294
299
|
async fetchFresh(params: Record<string, any>): Promise<T> {
|
|
295
300
|
const tag = this.TAG + 'fetchFresh | ';
|
|
296
301
|
const startTime = Date.now();
|
|
302
|
+
const key = this.buildKey(params);
|
|
297
303
|
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
304
|
+
// FIX #6: Check if there's already a pending fetch for this key
|
|
305
|
+
const existingFetch = this.pendingFetches.get(key);
|
|
306
|
+
if (existingFetch) {
|
|
307
|
+
log.debug(tag, `Coalescing request for: ${key} (fetch already in progress)`);
|
|
308
|
+
return existingFetch;
|
|
309
|
+
}
|
|
301
310
|
|
|
302
|
-
|
|
303
|
-
|
|
311
|
+
// Create the fetch promise
|
|
312
|
+
const fetchPromise = (async () => {
|
|
313
|
+
try {
|
|
314
|
+
log.info(tag, `Fetching fresh data: ${key}`);
|
|
304
315
|
|
|
305
|
-
|
|
306
|
-
|
|
316
|
+
// Call subclass-specific fetch implementation
|
|
317
|
+
const value = await this.fetchFromSource(params);
|
|
307
318
|
|
|
308
|
-
|
|
309
|
-
|
|
319
|
+
// Update cache
|
|
320
|
+
await this.updateCache(key, value);
|
|
310
321
|
|
|
311
|
-
|
|
322
|
+
const fetchTime = Date.now() - startTime;
|
|
323
|
+
log.info(tag, `✅ Fetched fresh data in ${fetchTime}ms: ${key}`);
|
|
312
324
|
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
325
|
+
return value;
|
|
326
|
+
|
|
327
|
+
} catch (error) {
|
|
328
|
+
const fetchTime = Date.now() - startTime;
|
|
329
|
+
log.error(tag, `Failed to fetch fresh data after ${fetchTime}ms:`, error);
|
|
330
|
+
return this.config.defaultValue;
|
|
331
|
+
} finally {
|
|
332
|
+
// Clean up pending fetch
|
|
333
|
+
this.pendingFetches.delete(key);
|
|
334
|
+
}
|
|
335
|
+
})();
|
|
336
|
+
|
|
337
|
+
// Store the promise so concurrent requests can reuse it
|
|
338
|
+
this.pendingFetches.set(key, fetchPromise);
|
|
339
|
+
|
|
340
|
+
return fetchPromise;
|
|
318
341
|
}
|
|
319
342
|
|
|
320
343
|
/**
|
|
@@ -36,7 +36,7 @@ export class PriceCache extends BaseCache<PriceData> {
|
|
|
36
36
|
enableQueue: true,
|
|
37
37
|
maxRetries: 3,
|
|
38
38
|
retryDelay: 5000,
|
|
39
|
-
blockOnMiss:
|
|
39
|
+
blockOnMiss: true, // MUST wait for price on first request (changed from false)
|
|
40
40
|
enableLegacyFallback: true,
|
|
41
41
|
defaultValue: {
|
|
42
42
|
caip: '',
|
|
@@ -68,7 +68,8 @@ export class PriceCache extends BaseCache<PriceData> {
|
|
|
68
68
|
}
|
|
69
69
|
|
|
70
70
|
/**
|
|
71
|
-
* Fetch price from markets API
|
|
71
|
+
* Fetch price from markets API using CAIP-first approach
|
|
72
|
+
* FIX #7: Throws error on zero price to prevent caching invalid data
|
|
72
73
|
*/
|
|
73
74
|
protected async fetchFromSource(params: Record<string, any>): Promise<PriceData> {
|
|
74
75
|
const tag = this.TAG + 'fetchFromSource | ';
|
|
@@ -76,48 +77,30 @@ export class PriceCache extends BaseCache<PriceData> {
|
|
|
76
77
|
try {
|
|
77
78
|
const { caip } = params;
|
|
78
79
|
|
|
79
|
-
//
|
|
80
|
-
|
|
81
|
-
const
|
|
82
|
-
|
|
83
|
-
if (!asset || !asset.symbol) {
|
|
84
|
-
log.warn(tag, `No asset mapping found for ${caip}`);
|
|
85
|
-
return {
|
|
86
|
-
caip,
|
|
87
|
-
price: 0
|
|
88
|
-
};
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
// Get price from markets module
|
|
92
|
-
const symbol = asset.symbol.toLowerCase();
|
|
93
|
-
const priceResult = await this.markets.getPrice(symbol);
|
|
94
|
-
|
|
95
|
-
// Handle different response formats
|
|
96
|
-
let price = 0;
|
|
97
|
-
if (typeof priceResult === 'object' && priceResult.price) {
|
|
98
|
-
price = parseFloat(priceResult.price);
|
|
99
|
-
} else if (typeof priceResult === 'number') {
|
|
100
|
-
price = priceResult;
|
|
101
|
-
}
|
|
80
|
+
// Use CAIP-first API (no symbol conversion needed!)
|
|
81
|
+
// This directly queries the markets module with CAIP identifiers
|
|
82
|
+
const price = await this.markets.getAssetPriceByCaip(caip);
|
|
102
83
|
|
|
84
|
+
// FIX #7: Throw error instead of returning 0 price
|
|
85
|
+
// This prevents caching invalid $0 prices from rate limits or API failures
|
|
103
86
|
if (isNaN(price) || price <= 0) {
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
price: 0
|
|
108
|
-
};
|
|
87
|
+
const errorMsg = `Price fetch failed for ${caip}: got $${price} (likely API timeout or rate limit)`;
|
|
88
|
+
log.warn(tag, errorMsg);
|
|
89
|
+
throw new Error(errorMsg);
|
|
109
90
|
}
|
|
110
91
|
|
|
111
|
-
log.debug(tag, `Fetched price for ${caip}
|
|
92
|
+
log.debug(tag, `Fetched price for ${caip}: $${price}`);
|
|
112
93
|
|
|
113
94
|
return {
|
|
114
95
|
caip,
|
|
115
96
|
price,
|
|
116
|
-
source: 'markets'
|
|
97
|
+
source: 'markets-caip'
|
|
117
98
|
};
|
|
118
99
|
|
|
119
100
|
} catch (error) {
|
|
120
|
-
|
|
101
|
+
// Re-throw to prevent caching zero prices
|
|
102
|
+
// BaseCache.fetchFresh() will catch this and return defaultValue
|
|
103
|
+
// but won't cache the error
|
|
121
104
|
throw error;
|
|
122
105
|
}
|
|
123
106
|
}
|