@pioneer-platform/pioneer-cache 1.0.1 → 1.0.3

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,189 @@
1
+ "use strict";
2
+ /*
3
+ PortfolioCache - Portfolio/Charts cache implementation
4
+
5
+ Extends BaseCache with portfolio-specific logic.
6
+ Designed for NON-BLOCKING, instant returns.
7
+ */
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.PortfolioCache = void 0;
10
+ const base_cache_1 = require("../core/base-cache");
11
+ const log = require('@pioneer-platform/loggerdog')();
12
+ /**
13
+ * PortfolioCache - Caches portfolio/chart data
14
+ *
15
+ * CRITICAL: This cache is NON-BLOCKING by design
16
+ * - Returns empty arrays immediately on cache miss
17
+ * - Never blocks waiting for blockchain APIs
18
+ * - Background jobs populate cache for next request
19
+ */
20
+ class PortfolioCache extends base_cache_1.BaseCache {
21
+ constructor(redis, balanceModule, marketsModule, config) {
22
+ const defaultConfig = {
23
+ name: 'portfolio',
24
+ keyPrefix: 'portfolio_v2:',
25
+ ttl: 0, // Ignored when enableTTL: false
26
+ staleThreshold: 5 * 60 * 1000, // 5 minutes - triggers background refresh
27
+ enableTTL: false, // NEVER EXPIRE - data persists forever, show stale data instantly
28
+ queueName: 'cache-refresh',
29
+ enableQueue: true,
30
+ maxRetries: 3,
31
+ retryDelay: 5000,
32
+ blockOnMiss: false, // CRITICAL: NEVER WAIT! Return empty arrays instantly
33
+ enableLegacyFallback: false, // No legacy portfolio cache format
34
+ defaultValue: {
35
+ pubkeys: [],
36
+ charts: [],
37
+ totalValueUsd: 0,
38
+ timestamp: Date.now()
39
+ },
40
+ useSyncFallback: false, // CRITICAL: NEVER use synchronous fallback - always return instantly
41
+ maxConcurrentJobs: 3, // Limit concurrent portfolio refreshes
42
+ apiTimeout: 30000, // 30s timeout for full portfolio fetch
43
+ logCacheHits: true,
44
+ logCacheMisses: true,
45
+ logRefreshJobs: true
46
+ };
47
+ super(redis, { ...defaultConfig, ...config });
48
+ this.balanceModule = balanceModule;
49
+ this.marketsModule = marketsModule;
50
+ }
51
+ /**
52
+ * Build Redis key for portfolio data
53
+ *
54
+ * Key strategy: Hash all pubkeys+caips to create a stable identifier
55
+ * Format: portfolio_v2:hash(pubkeys)
56
+ *
57
+ * This allows caching the same portfolio regardless of pubkey order
58
+ */
59
+ buildKey(params) {
60
+ const { pubkeys } = params;
61
+ if (!pubkeys || !Array.isArray(pubkeys) || pubkeys.length === 0) {
62
+ throw new Error('PortfolioCache.buildKey: pubkeys array required');
63
+ }
64
+ // Sort pubkeys to create stable hash regardless of order
65
+ const sorted = [...pubkeys].sort((a, b) => {
66
+ const aKey = `${a.caip}:${a.pubkey}`;
67
+ const bKey = `${b.caip}:${b.pubkey}`;
68
+ return aKey.localeCompare(bKey);
69
+ });
70
+ // Create a simple hash from sorted pubkeys
71
+ const keyString = sorted.map(p => `${p.caip}:${p.pubkey}`).join('|');
72
+ const hash = this.simpleHash(keyString);
73
+ return `${this.config.keyPrefix}${hash}`;
74
+ }
75
+ /**
76
+ * Simple hash function for cache keys
77
+ * Not cryptographic - just needs to be stable and collision-resistant
78
+ */
79
+ simpleHash(str) {
80
+ let hash = 0;
81
+ for (let i = 0; i < str.length; i++) {
82
+ const char = str.charCodeAt(i);
83
+ hash = ((hash << 5) - hash) + char;
84
+ hash = hash & hash; // Convert to 32-bit integer
85
+ }
86
+ return Math.abs(hash).toString(36);
87
+ }
88
+ /**
89
+ * Fetch portfolio from blockchain APIs
90
+ *
91
+ * This is the SLOW operation that happens in the background
92
+ * It fetches balances for all pubkeys and enriches with pricing
93
+ */
94
+ async fetchFromSource(params) {
95
+ const tag = this.TAG + 'fetchFromSource | ';
96
+ const startTime = Date.now();
97
+ try {
98
+ const { pubkeys } = params;
99
+ log.info(tag, `Fetching portfolio for ${pubkeys.length} pubkeys`);
100
+ const charts = [];
101
+ // Fetch balances for all pubkeys in parallel
102
+ const balancePromises = pubkeys.map(async (item) => {
103
+ try {
104
+ // Extract networkId from CAIP
105
+ const networkId = item.caip.split('/')[0];
106
+ // Fetch balance
107
+ const asset = { caip: item.caip };
108
+ const owner = { pubkey: item.pubkey };
109
+ const balanceInfo = await this.balanceModule.getBalance(asset, owner);
110
+ if (!balanceInfo || !balanceInfo.balance) {
111
+ log.debug(tag, `No balance for ${item.caip}/${item.pubkey.substring(0, 10)}...`);
112
+ return null;
113
+ }
114
+ // Skip zero balances
115
+ const balanceNum = parseFloat(balanceInfo.balance);
116
+ if (isNaN(balanceNum) || balanceNum === 0) {
117
+ return null;
118
+ }
119
+ // Get asset metadata
120
+ const assetData = require('@pioneer-platform/pioneer-discovery').assetData;
121
+ const assetInfo = assetData[item.caip.toUpperCase()] || assetData[item.caip.toLowerCase()] || {};
122
+ // Get price
123
+ let priceUsd = 0;
124
+ try {
125
+ priceUsd = await this.marketsModule.getAssetPriceByCaip(item.caip);
126
+ if (isNaN(priceUsd) || priceUsd < 0) {
127
+ priceUsd = 0;
128
+ }
129
+ }
130
+ catch (priceError) {
131
+ log.warn(tag, `Error fetching price for ${item.caip}:`, priceError);
132
+ }
133
+ const valueUsd = balanceNum * priceUsd;
134
+ const chartData = {
135
+ caip: item.caip,
136
+ pubkey: item.pubkey,
137
+ networkId,
138
+ symbol: assetInfo.symbol || 'UNKNOWN',
139
+ name: assetInfo.name || 'Unknown Asset',
140
+ balance: balanceInfo.balance,
141
+ priceUsd,
142
+ valueUsd,
143
+ icon: assetInfo.icon || '',
144
+ type: assetInfo.type || 'native',
145
+ decimal: assetInfo.decimal
146
+ };
147
+ return chartData;
148
+ }
149
+ catch (error) {
150
+ log.error(tag, `Error fetching balance for ${item.caip}/${item.pubkey}:`, error);
151
+ return null;
152
+ }
153
+ });
154
+ const results = await Promise.all(balancePromises);
155
+ // Filter out nulls and calculate total
156
+ const validCharts = results.filter((c) => c !== null);
157
+ const totalValueUsd = validCharts.reduce((sum, c) => sum + c.valueUsd, 0);
158
+ const fetchTime = Date.now() - startTime;
159
+ log.info(tag, `✅ Fetched portfolio: ${validCharts.length} assets, $${totalValueUsd.toFixed(2)} in ${fetchTime}ms`);
160
+ return {
161
+ pubkeys,
162
+ charts: validCharts,
163
+ totalValueUsd,
164
+ timestamp: Date.now()
165
+ };
166
+ }
167
+ catch (error) {
168
+ log.error(tag, 'Error fetching portfolio:', error);
169
+ throw error;
170
+ }
171
+ }
172
+ /**
173
+ * No legacy cache format for portfolios
174
+ */
175
+ async getLegacyCached(params) {
176
+ return null;
177
+ }
178
+ /**
179
+ * Get portfolio for a set of pubkeys
180
+ * Convenience method that wraps base get()
181
+ *
182
+ * RETURNS INSTANTLY - either cached data or empty arrays
183
+ */
184
+ async getPortfolio(pubkeys, waitForFresh) {
185
+ const result = await this.get({ pubkeys }, waitForFresh);
186
+ return result.value || this.config.defaultValue;
187
+ }
188
+ }
189
+ exports.PortfolioCache = PortfolioCache;
@@ -21,7 +21,7 @@ export declare class PriceCache extends BaseCache<PriceData> {
21
21
  protected buildKey(params: Record<string, any>): string;
22
22
  /**
23
23
  * Fetch price from markets API using CAIP-first approach
24
- * FIX #7: Throws error on zero price to prevent caching invalid data
24
+ * FIX #7: Graceful handling of zero prices to prevent cache disruption
25
25
  */
26
26
  protected fetchFromSource(params: Record<string, any>): Promise<PriceData>;
27
27
  /**
@@ -17,21 +17,22 @@ class PriceCache extends base_cache_1.BaseCache {
17
17
  const defaultConfig = {
18
18
  name: 'price',
19
19
  keyPrefix: 'price_v2:',
20
- ttl: 60 * 60 * 1000, // 1 hour
21
- staleThreshold: 30 * 60 * 1000, // 30 minutes (refresh after 30min)
22
- enableTTL: true,
23
- queueName: 'price-refresh-v2',
20
+ ttl: 0, // Ignored when enableTTL: false
21
+ staleThreshold: 30 * 60 * 1000, // 30 minutes - triggers background refresh
22
+ enableTTL: false, // NEVER EXPIRE - data persists forever, show stale prices instantly
23
+ queueName: 'cache-refresh',
24
24
  enableQueue: true,
25
25
  maxRetries: 3,
26
26
  retryDelay: 5000,
27
- blockOnMiss: true, // MUST wait for price on first request (changed from false)
27
+ blockOnMiss: false, // CRITICAL: NEVER WAIT! Return $0 instantly on cache miss, refresh async in background
28
28
  enableLegacyFallback: true,
29
29
  defaultValue: {
30
30
  caip: '',
31
31
  price: 0
32
32
  },
33
+ useSyncFallback: false, // CRITICAL: NEVER use synchronous fallback - always return instantly with $0
33
34
  maxConcurrentJobs: 5,
34
- apiTimeout: 5000,
35
+ apiTimeout: 2000, // Reduced from 5000ms for faster failures
35
36
  logCacheHits: false,
36
37
  logCacheMisses: true,
37
38
  logRefreshJobs: true
@@ -53,7 +54,7 @@ class PriceCache extends base_cache_1.BaseCache {
53
54
  }
54
55
  /**
55
56
  * Fetch price from markets API using CAIP-first approach
56
- * FIX #7: Throws error on zero price to prevent caching invalid data
57
+ * FIX #7: Graceful handling of zero prices to prevent cache disruption
57
58
  */
58
59
  async fetchFromSource(params) {
59
60
  const tag = this.TAG + 'fetchFromSource | ';
@@ -62,12 +63,26 @@ class PriceCache extends base_cache_1.BaseCache {
62
63
  // Use CAIP-first API (no symbol conversion needed!)
63
64
  // This directly queries the markets module with CAIP identifiers
64
65
  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
66
+ // FIX #7: Gracefully handle zero prices without throwing
67
+ // This prevents disrupting batch operations during API rate limits
67
68
  if (isNaN(price) || price <= 0) {
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);
69
+ log.warn(tag, `Price fetch returned $${price} for ${caip} (likely API timeout or rate limit) - returning stale cache if available`);
70
+ // Try to get stale cached value instead of failing
71
+ const key = this.buildKey(params);
72
+ const cachedValue = await this.getCached(key);
73
+ if (cachedValue && cachedValue.value.price > 0) {
74
+ log.info(tag, `Returning stale cached price for ${caip}: $${cachedValue.value.price}`);
75
+ return cachedValue.value;
76
+ }
77
+ // Try legacy cache as fallback
78
+ const legacyValue = await this.getLegacyCached(params);
79
+ if (legacyValue && legacyValue.price > 0) {
80
+ log.info(tag, `Returning legacy cached price for ${caip}: $${legacyValue.price}`);
81
+ return legacyValue;
82
+ }
83
+ // Last resort: return zero price but don't cache it
84
+ log.warn(tag, `No cached price available for ${caip}, returning zero`);
85
+ throw new Error(`No valid price available for ${caip}`);
71
86
  }
72
87
  log.debug(tag, `Fetched price for ${caip}: $${price}`);
73
88
  return {
@@ -77,9 +92,14 @@ class PriceCache extends base_cache_1.BaseCache {
77
92
  };
78
93
  }
79
94
  catch (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
95
+ // Log as warning instead of error for expected API issues
96
+ const errorMsg = error instanceof Error ? error.message : String(error);
97
+ if (errorMsg.includes('rate limit') || errorMsg.includes('timeout') || errorMsg.includes('No valid price')) {
98
+ log.warn(tag, `Expected API issue: ${errorMsg}`);
99
+ }
100
+ else {
101
+ log.error(tag, `Unexpected error fetching price:`, error);
102
+ }
83
103
  throw error;
84
104
  }
85
105
  }
@@ -14,6 +14,7 @@ export interface CacheConfig {
14
14
  blockOnMiss: boolean;
15
15
  enableLegacyFallback: boolean;
16
16
  defaultValue: any;
17
+ useSyncFallback?: boolean;
17
18
  maxConcurrentJobs: number;
18
19
  apiTimeout: number;
19
20
  logCacheHits: boolean;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pioneer-platform/pioneer-cache",
3
- "version": "1.0.1",
3
+ "version": "1.0.3",
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": "^8.0.0",
24
- "@pioneer-platform/redis-queue": "^8.0.0",
25
- "@pioneer-platform/default-redis": "^8.0.0"
23
+ "@pioneer-platform/loggerdog": "^8.11.0",
24
+ "@pioneer-platform/redis-queue": "^8.11.1",
25
+ "@pioneer-platform/default-redis": "^8.11.7"
26
26
  },
27
27
  "devDependencies": {
28
28
  "@types/node": "^20.0.0",
@@ -77,10 +77,14 @@ export abstract class BaseCache<T> {
77
77
  const startTime = Date.now();
78
78
 
79
79
  try {
80
+ const t1 = Date.now();
80
81
  const key = this.buildKey(params);
82
+ log.info(tag, `⏱️ buildKey took ${Date.now() - t1}ms`);
81
83
 
82
84
  // Step 1: Try new cache format
85
+ const t2 = Date.now();
83
86
  const cachedValue = await this.getCached(key);
87
+ log.info(tag, `⏱️ getCached took ${Date.now() - t2}ms`);
84
88
 
85
89
  if (cachedValue) {
86
90
  const age = Date.now() - cachedValue.timestamp;
@@ -106,7 +110,9 @@ export abstract class BaseCache<T> {
106
110
 
107
111
  // Step 2: Try legacy cache fallback
108
112
  if (this.config.enableLegacyFallback) {
113
+ const t3 = Date.now();
109
114
  const legacyValue = await this.getLegacyCached(params);
115
+ log.info(tag, `⏱️ getLegacyCached took ${Date.now() - t3}ms`);
110
116
  if (legacyValue) {
111
117
  const responseTime = Date.now() - startTime;
112
118
  log.info(tag, `Legacy cache hit: ${key} (${responseTime}ms)`);
@@ -145,7 +151,10 @@ export abstract class BaseCache<T> {
145
151
  }
146
152
 
147
153
  // Non-blocking: trigger async refresh and return default
154
+ const t4 = Date.now();
148
155
  this.triggerAsyncRefresh(params, 'high');
156
+ log.info(tag, `⏱️ triggerAsyncRefresh took ${Date.now() - t4}ms`);
157
+ log.info(tag, `⏱️ Returning default value after ${Date.now() - startTime}ms TOTAL`);
149
158
 
150
159
  return {
151
160
  success: true,
@@ -172,22 +181,38 @@ export abstract class BaseCache<T> {
172
181
  */
173
182
  protected async getCached(key: string): Promise<CachedValue<T> | null> {
174
183
  const tag = this.TAG + 'getCached | ';
184
+ const t0 = Date.now();
175
185
 
176
186
  try {
177
- const cached = await this.redis.get(key);
187
+ // Redis timeout for cache reads
188
+ // PERFORMANCE FIX: Reduced from 10000ms to 100ms
189
+ // Local Redis should respond in <10ms. 100ms timeout prevents cascading failures
190
+ // when Redis is overloaded while still catching actual connection issues.
191
+ const timeoutMs = 100;
192
+ const cached = await Promise.race([
193
+ this.redis.get(key),
194
+ new Promise<null>((resolve) => setTimeout(() => {
195
+ log.warn(tag, `⏱️ Redis timeout after ${timeoutMs}ms, returning cache miss`);
196
+ resolve(null);
197
+ }, timeoutMs))
198
+ ]);
199
+
178
200
  if (!cached) {
201
+ log.debug(tag, `Cache miss: ${key}`);
179
202
  return null;
180
203
  }
181
204
 
182
205
  const parsed: CachedValue<T> = JSON.parse(cached);
183
206
 
184
- // Validate structure
185
- if (!parsed.value || typeof parsed.timestamp !== 'number') {
207
+ // Validate structure - Check for undefined/null, NOT falsy values!
208
+ // CRITICAL: Balance "0", empty arrays [], and empty objects {} are VALID!
209
+ if (parsed.value === undefined || parsed.value === null || typeof parsed.timestamp !== 'number') {
186
210
  log.warn(tag, `Invalid cache structure for ${key}, removing`);
187
211
  await this.redis.del(key);
188
212
  return null;
189
213
  }
190
214
 
215
+ log.debug(tag, `Cache hit: ${key}`);
191
216
  return parsed;
192
217
 
193
218
  } catch (error) {
@@ -212,19 +237,33 @@ export abstract class BaseCache<T> {
212
237
  metadata
213
238
  };
214
239
 
240
+ // DIAGNOSTIC: Log Redis connection details
241
+ const redisConstructor = this.redis.constructor?.name || 'unknown';
242
+ const redisOptions = this.redis.options || {};
243
+
215
244
  // FIX #2: Always set TTL (unless explicitly disabled)
216
245
  if (this.config.enableTTL) {
217
246
  const ttlSeconds = Math.floor(this.config.ttl / 1000);
247
+
248
+ // PERF: Reduced logging for production performance
249
+ log.debug(tag, `📝 Writing to Redis: ${key} [${JSON.stringify(cachedValue).length} bytes, TTL: ${ttlSeconds}s]`);
250
+
251
+ // Write
218
252
  await this.redis.set(key, JSON.stringify(cachedValue), 'EX', ttlSeconds);
219
- log.info(tag, `Updated cache: ${key} [TTL: ${ttlSeconds}s]`);
253
+
254
+ // PERF: Verification disabled for production performance
255
+ // Only enable for debugging cache issues
256
+ log.debug(tag, `✅ Updated cache: ${key} [TTL: ${ttlSeconds}s]`);
220
257
  } else {
221
258
  // Permanent caching (for transactions)
222
259
  await this.redis.set(key, JSON.stringify(cachedValue));
223
- log.info(tag, `Updated cache: ${key} [PERMANENT]`);
260
+
261
+ // PERF: Verification disabled for production performance
262
+ log.debug(tag, `✅ Updated cache: ${key} [PERMANENT]`);
224
263
  }
225
264
 
226
265
  } catch (error) {
227
- log.error(tag, `Error updating cache for ${key}:`, error);
266
+ log.error(tag, `❌ Error updating cache for ${key}:`, error);
228
267
  throw error;
229
268
  }
230
269
  }
@@ -249,8 +288,13 @@ export abstract class BaseCache<T> {
249
288
  log.error(tag, `❌ QUEUE NOT INITIALIZED! Cannot refresh ${key}`);
250
289
  log.error(tag, `Background refresh is BROKEN - cache will NOT update!`);
251
290
 
252
- // FIX #4: Synchronous fallback for high-priority
253
- if (priority === 'high') {
291
+ // FIX #4: Synchronous fallback for high-priority (only if useSyncFallback is enabled)
292
+ // Default: use sync fallback only for blocking caches (blockOnMiss=true)
293
+ const shouldUseSyncFallback = this.config.useSyncFallback !== undefined
294
+ ? this.config.useSyncFallback
295
+ : this.config.blockOnMiss;
296
+
297
+ if (priority === 'high' && shouldUseSyncFallback) {
254
298
  log.warn(tag, `Using synchronous fallback for high-priority refresh`);
255
299
  setImmediate(async () => {
256
300
  try {
@@ -295,6 +339,7 @@ export abstract class BaseCache<T> {
295
339
  * Fetch fresh data and update cache
296
340
  * FIX #1 & #4: Used for blocking requests and fallback
297
341
  * FIX #6: Request deduplication to prevent thundering herd
342
+ * FIX #8: Return stale cache on fetch failures
298
343
  */
299
344
  async fetchFresh(params: Record<string, any>): Promise<T> {
300
345
  const tag = this.TAG + 'fetchFresh | ';
@@ -304,6 +349,13 @@ export abstract class BaseCache<T> {
304
349
  // FIX #6: Check if there's already a pending fetch for this key
305
350
  const existingFetch = this.pendingFetches.get(key);
306
351
  if (existingFetch) {
352
+ // For non-blocking caches, return default value immediately instead of waiting
353
+ if (!this.config.blockOnMiss) {
354
+ log.debug(tag, `Non-blocking cache: returning default value while fetch in progress: ${key}`);
355
+ return this.config.defaultValue;
356
+ }
357
+
358
+ // For blocking caches, coalesce to prevent thundering herd
307
359
  log.debug(tag, `Coalescing request for: ${key} (fetch already in progress)`);
308
360
  return existingFetch;
309
361
  }
@@ -326,7 +378,31 @@ export abstract class BaseCache<T> {
326
378
 
327
379
  } catch (error) {
328
380
  const fetchTime = Date.now() - startTime;
329
- log.error(tag, `Failed to fetch fresh data after ${fetchTime}ms:`, error);
381
+ const errorMsg = error instanceof Error ? error.message : String(error);
382
+
383
+ // FIX #8: Try to return stale cache on fetch failures
384
+ const cachedValue = await this.getCached(key);
385
+ if (cachedValue) {
386
+ log.warn(tag, `Fetch failed after ${fetchTime}ms, returning stale cache: ${key}`);
387
+ return cachedValue.value;
388
+ }
389
+
390
+ // Try legacy cache as last resort
391
+ if (this.config.enableLegacyFallback) {
392
+ const legacyValue = await this.getLegacyCached(params);
393
+ if (legacyValue) {
394
+ log.warn(tag, `Fetch failed after ${fetchTime}ms, returning legacy cache: ${key}`);
395
+ return legacyValue;
396
+ }
397
+ }
398
+
399
+ // Log as warning for expected issues, error for unexpected
400
+ if (errorMsg.includes('rate limit') || errorMsg.includes('timeout') || errorMsg.includes('No valid')) {
401
+ log.warn(tag, `Expected fetch failure after ${fetchTime}ms: ${errorMsg}`);
402
+ } else {
403
+ log.error(tag, `Unexpected fetch failure after ${fetchTime}ms:`, error);
404
+ }
405
+
330
406
  return this.config.defaultValue;
331
407
  } finally {
332
408
  // Clean up pending fetch
@@ -7,6 +7,7 @@
7
7
 
8
8
  import { BalanceCache } from '../stores/balance-cache';
9
9
  import { PriceCache } from '../stores/price-cache';
10
+ import { PortfolioCache } from '../stores/portfolio-cache';
10
11
  import { TransactionCache } from '../stores/transaction-cache';
11
12
  import { RefreshWorker, startUnifiedWorker } from '../workers/refresh-worker';
12
13
  import type { BaseCache } from './base-cache';
@@ -24,6 +25,7 @@ export interface CacheManagerConfig {
24
25
  markets?: any; // Optional: if not provided, price cache won't be initialized
25
26
  enableBalanceCache?: boolean;
26
27
  enablePriceCache?: boolean;
28
+ enablePortfolioCache?: boolean;
27
29
  enableTransactionCache?: boolean;
28
30
  startWorkers?: boolean; // Auto-start workers on initialization
29
31
  }
@@ -35,6 +37,7 @@ export class CacheManager {
35
37
  private redis: any;
36
38
  private balanceCache?: BalanceCache;
37
39
  private priceCache?: PriceCache;
40
+ private portfolioCache?: PortfolioCache;
38
41
  private transactionCache?: TransactionCache;
39
42
  private workers: RefreshWorker[] = [];
40
43
 
@@ -53,6 +56,12 @@ export class CacheManager {
53
56
  log.info(TAG, '✅ Price cache initialized');
54
57
  }
55
58
 
59
+ // Initialize Portfolio Cache
60
+ if (config.enablePortfolioCache !== false && config.balanceModule && config.markets) {
61
+ this.portfolioCache = new PortfolioCache(this.redis, config.balanceModule, config.markets);
62
+ log.info(TAG, '✅ Portfolio cache initialized');
63
+ }
64
+
56
65
  // Initialize Transaction Cache
57
66
  if (config.enableTransactionCache !== false) {
58
67
  this.transactionCache = new TransactionCache(this.redis);
@@ -89,6 +98,10 @@ export class CacheManager {
89
98
  cacheRegistry.set('price', this.priceCache);
90
99
  }
91
100
 
101
+ if (this.portfolioCache) {
102
+ cacheRegistry.set('portfolio', this.portfolioCache);
103
+ }
104
+
92
105
  // Start unified worker if we have any caches with queues
93
106
  if (cacheRegistry.size > 0) {
94
107
  const worker = await startUnifiedWorker(
@@ -170,6 +183,18 @@ export class CacheManager {
170
183
  }
171
184
  }
172
185
 
186
+ // Check portfolio cache
187
+ if (this.portfolioCache) {
188
+ const portfolioHealth = await this.portfolioCache.getHealth(forceRefresh);
189
+ checks.portfolio = portfolioHealth;
190
+
191
+ if (portfolioHealth.status === 'unhealthy') {
192
+ issues.push(...portfolioHealth.issues.map(i => `Portfolio: ${i}`));
193
+ } else if (portfolioHealth.status === 'degraded') {
194
+ warnings.push(...portfolioHealth.warnings.map(w => `Portfolio: ${w}`));
195
+ }
196
+ }
197
+
173
198
  // Check transaction cache (simple stats check)
174
199
  if (this.transactionCache) {
175
200
  const txStats = await this.transactionCache.getStats();
@@ -241,6 +266,7 @@ export class CacheManager {
241
266
  return {
242
267
  balance: this.balanceCache,
243
268
  price: this.priceCache,
269
+ portfolio: this.portfolioCache,
244
270
  transaction: this.transactionCache
245
271
  };
246
272
  }
@@ -248,12 +274,14 @@ export class CacheManager {
248
274
  /**
249
275
  * Get specific cache by name
250
276
  */
251
- getCache(name: 'balance' | 'price' | 'transaction') {
277
+ getCache(name: 'balance' | 'price' | 'portfolio' | 'transaction') {
252
278
  switch (name) {
253
279
  case 'balance':
254
280
  return this.balanceCache;
255
281
  case 'price':
256
282
  return this.priceCache;
283
+ case 'portfolio':
284
+ return this.portfolioCache;
257
285
  case 'transaction':
258
286
  return this.transactionCache;
259
287
  default:
@@ -264,7 +292,7 @@ export class CacheManager {
264
292
  /**
265
293
  * Clear all caches (use with caution!)
266
294
  */
267
- async clearAll(): Promise<{ balance?: number; price?: number; transaction?: number }> {
295
+ async clearAll(): Promise<{ balance?: number; price?: number; portfolio?: number; transaction?: number }> {
268
296
  const tag = TAG + 'clearAll | ';
269
297
 
270
298
  try {
@@ -278,6 +306,10 @@ export class CacheManager {
278
306
  result.price = await this.priceCache.clearAll();
279
307
  }
280
308
 
309
+ if (this.portfolioCache) {
310
+ result.portfolio = await this.portfolioCache.clearAll();
311
+ }
312
+
281
313
  if (this.transactionCache) {
282
314
  result.transaction = await this.transactionCache.clearAll();
283
315
  }
package/src/index.ts CHANGED
@@ -12,6 +12,7 @@ export { CacheManager } from './core/cache-manager';
12
12
  // Cache implementations
13
13
  export { BalanceCache } from './stores/balance-cache';
14
14
  export { PriceCache } from './stores/price-cache';
15
+ export { PortfolioCache } from './stores/portfolio-cache';
15
16
  export { TransactionCache } from './stores/transaction-cache';
16
17
 
17
18
  // Worker exports
@@ -30,6 +31,7 @@ export type {
30
31
  // Data type exports
31
32
  export type { BalanceData } from './stores/balance-cache';
32
33
  export type { PriceData } from './stores/price-cache';
34
+ export type { PortfolioData, ChartData } from './stores/portfolio-cache';
33
35
 
34
36
  // Config type export
35
37
  export type { CacheManagerConfig } from './core/cache-manager';