@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.
@@ -1,2 +1 @@
1
-
2
- $ tsc
1
+ $ tsc
package/CHANGELOG.md ADDED
@@ -0,0 +1,7 @@
1
+ # @pioneer-platform/pioneer-cache
2
+
3
+ ## 1.0.1
4
+
5
+ ### Patch Changes
6
+
7
+ - Fix workspace dependencies to use published versions for Docker build compatibility
@@ -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
  /**
@@ -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
- try {
253
- const key = this.buildKey(params);
254
- log.info(tag, `Fetching fresh data: ${key}`);
255
- // Call subclass-specific fetch implementation
256
- const value = await this.fetchFromSource(params);
257
- // Update cache
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: false, // Return immediately with $0 (prices less critical)
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
- // Map CAIP to symbol for markets API
62
- const assetData = require('@pioneer-platform/pioneer-discovery').assetData;
63
- const asset = assetData[caip.toUpperCase()] || assetData[caip.toLowerCase()];
64
- if (!asset || !asset.symbol) {
65
- log.warn(tag, `No asset mapping found for ${caip}`);
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
- log.warn(tag, `Invalid price for ${caip} (${symbol}): ${priceResult}`);
84
- return {
85
- caip,
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} (${symbol}): $${price}`);
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
- log.error(tag, 'Error fetching price:', error);
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.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": "workspace:*",
24
- "@pioneer-platform/redis-queue": "workspace:*",
25
- "@pioneer-platform/default-redis": "workspace:*"
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",
@@ -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
- try {
299
- const key = this.buildKey(params);
300
- log.info(tag, `Fetching fresh data: ${key}`);
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
- // Call subclass-specific fetch implementation
303
- const value = await this.fetchFromSource(params);
311
+ // Create the fetch promise
312
+ const fetchPromise = (async () => {
313
+ try {
314
+ log.info(tag, `Fetching fresh data: ${key}`);
304
315
 
305
- // Update cache
306
- await this.updateCache(key, value);
316
+ // Call subclass-specific fetch implementation
317
+ const value = await this.fetchFromSource(params);
307
318
 
308
- const fetchTime = Date.now() - startTime;
309
- log.info(tag, `✅ Fetched fresh data in ${fetchTime}ms: ${key}`);
319
+ // Update cache
320
+ await this.updateCache(key, value);
310
321
 
311
- return value;
322
+ const fetchTime = Date.now() - startTime;
323
+ log.info(tag, `✅ Fetched fresh data in ${fetchTime}ms: ${key}`);
312
324
 
313
- } catch (error) {
314
- const fetchTime = Date.now() - startTime;
315
- log.error(tag, `Failed to fetch fresh data after ${fetchTime}ms:`, error);
316
- return this.config.defaultValue;
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: false, // Return immediately with $0 (prices less critical)
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
- // Map CAIP to symbol for markets API
80
- const assetData = require('@pioneer-platform/pioneer-discovery').assetData;
81
- const asset = assetData[caip.toUpperCase()] || assetData[caip.toLowerCase()];
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
- log.warn(tag, `Invalid price for ${caip} (${symbol}): ${priceResult}`);
105
- return {
106
- caip,
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} (${symbol}): $${price}`);
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
- log.error(tag, 'Error fetching price:', error);
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
  }