@pioneer-platform/pioneer-cache 1.0.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.
@@ -0,0 +1,238 @@
1
+ "use strict";
2
+ /*
3
+ CacheManager - Orchestrates all cache instances
4
+
5
+ Central coordinator for all cache types (balance, price, transaction).
6
+ Manages cache lifecycle, health monitoring, and worker coordination.
7
+ */
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.CacheManager = void 0;
10
+ const balance_cache_1 = require("../stores/balance-cache");
11
+ const price_cache_1 = require("../stores/price-cache");
12
+ const transaction_cache_1 = require("../stores/transaction-cache");
13
+ const refresh_worker_1 = require("../workers/refresh-worker");
14
+ const log = require('@pioneer-platform/loggerdog')();
15
+ const TAG = ' | CacheManager | ';
16
+ /**
17
+ * CacheManager - Central coordinator for all caches
18
+ */
19
+ class CacheManager {
20
+ constructor(config) {
21
+ this.workers = [];
22
+ this.redis = config.redis;
23
+ // Initialize Balance Cache
24
+ if (config.enableBalanceCache !== false && config.balanceModule) {
25
+ this.balanceCache = new balance_cache_1.BalanceCache(this.redis, config.balanceModule);
26
+ log.info(TAG, '✅ Balance cache initialized');
27
+ }
28
+ // Initialize Price Cache
29
+ if (config.enablePriceCache !== false && config.markets) {
30
+ this.priceCache = new price_cache_1.PriceCache(this.redis, config.markets);
31
+ log.info(TAG, '✅ Price cache initialized');
32
+ }
33
+ // Initialize Transaction Cache
34
+ if (config.enableTransactionCache !== false) {
35
+ this.transactionCache = new transaction_cache_1.TransactionCache(this.redis);
36
+ log.info(TAG, '✅ Transaction cache initialized');
37
+ }
38
+ // Auto-start workers if requested
39
+ if (config.startWorkers) {
40
+ setImmediate(() => {
41
+ this.startWorkers();
42
+ });
43
+ }
44
+ log.info(TAG, '✅ CacheManager initialized');
45
+ }
46
+ /**
47
+ * Start background refresh workers for all caches
48
+ */
49
+ async startWorkers() {
50
+ const tag = TAG + 'startWorkers | ';
51
+ try {
52
+ log.info(tag, 'Starting refresh workers...');
53
+ // Create cache registry for workers
54
+ const cacheRegistry = new Map();
55
+ if (this.balanceCache) {
56
+ cacheRegistry.set('balance', this.balanceCache);
57
+ }
58
+ if (this.priceCache) {
59
+ cacheRegistry.set('price', this.priceCache);
60
+ }
61
+ // Start unified worker if we have any caches with queues
62
+ if (cacheRegistry.size > 0) {
63
+ const worker = await (0, refresh_worker_1.startUnifiedWorker)(this.redis, cacheRegistry, 'cache-refresh', // Unified queue name
64
+ {
65
+ maxRetries: 3,
66
+ retryDelay: 5000,
67
+ pollInterval: 100
68
+ });
69
+ this.workers.push(worker);
70
+ log.info(tag, '✅ Refresh worker started for all caches');
71
+ }
72
+ }
73
+ catch (error) {
74
+ log.error(tag, '❌ Failed to start workers:', error);
75
+ throw error;
76
+ }
77
+ }
78
+ /**
79
+ * Stop all workers gracefully
80
+ */
81
+ async stopWorkers() {
82
+ const tag = TAG + 'stopWorkers | ';
83
+ try {
84
+ log.info(tag, 'Stopping all workers...');
85
+ for (const worker of this.workers) {
86
+ await worker.stop();
87
+ }
88
+ this.workers = [];
89
+ log.info(tag, '✅ All workers stopped');
90
+ }
91
+ catch (error) {
92
+ log.error(tag, 'Error stopping workers:', error);
93
+ throw error;
94
+ }
95
+ }
96
+ /**
97
+ * Get aggregate health status for all caches
98
+ * @param forceRefresh - Force refresh stats (bypasses 30s cache)
99
+ */
100
+ async getHealth(forceRefresh = false) {
101
+ const tag = TAG + 'getHealth | ';
102
+ try {
103
+ const checks = {};
104
+ const issues = [];
105
+ const warnings = [];
106
+ // Check balance cache
107
+ if (this.balanceCache) {
108
+ const balanceHealth = await this.balanceCache.getHealth(forceRefresh);
109
+ checks.balance = balanceHealth;
110
+ if (balanceHealth.status === 'unhealthy') {
111
+ issues.push(...balanceHealth.issues.map(i => `Balance: ${i}`));
112
+ }
113
+ else if (balanceHealth.status === 'degraded') {
114
+ warnings.push(...balanceHealth.warnings.map(w => `Balance: ${w}`));
115
+ }
116
+ }
117
+ // Check price cache
118
+ if (this.priceCache) {
119
+ const priceHealth = await this.priceCache.getHealth(forceRefresh);
120
+ checks.price = priceHealth;
121
+ if (priceHealth.status === 'unhealthy') {
122
+ issues.push(...priceHealth.issues.map(i => `Price: ${i}`));
123
+ }
124
+ else if (priceHealth.status === 'degraded') {
125
+ warnings.push(...priceHealth.warnings.map(w => `Price: ${w}`));
126
+ }
127
+ }
128
+ // Check transaction cache (simple stats check)
129
+ if (this.transactionCache) {
130
+ const txStats = await this.transactionCache.getStats();
131
+ checks.transaction = {
132
+ status: 'healthy',
133
+ stats: txStats
134
+ };
135
+ }
136
+ // Check workers
137
+ for (const worker of this.workers) {
138
+ const workerStats = await worker.getStats();
139
+ checks.worker = workerStats;
140
+ if (!workerStats.isRunning) {
141
+ issues.push('Refresh worker not running');
142
+ }
143
+ }
144
+ // Determine overall status
145
+ let status = 'healthy';
146
+ if (issues.length > 0) {
147
+ status = 'unhealthy';
148
+ }
149
+ else if (warnings.length > 0) {
150
+ status = 'degraded';
151
+ }
152
+ return {
153
+ status,
154
+ queueInitialized: this.workers.length > 0,
155
+ redisConnected: true,
156
+ stats: {
157
+ totalEntries: 0,
158
+ staleEntries: 0,
159
+ freshEntries: 0,
160
+ stalenessRate: '0%'
161
+ },
162
+ issues,
163
+ warnings,
164
+ timestamp: Date.now(),
165
+ timestampISO: new Date().toISOString(),
166
+ checks
167
+ };
168
+ }
169
+ catch (error) {
170
+ log.error(tag, 'Health check failed:', error);
171
+ return {
172
+ status: 'unhealthy',
173
+ queueInitialized: false,
174
+ redisConnected: false,
175
+ stats: {
176
+ totalEntries: 0,
177
+ staleEntries: 0,
178
+ freshEntries: 0,
179
+ stalenessRate: '0%'
180
+ },
181
+ issues: [`Health check error: ${error instanceof Error ? error.message : String(error)}`],
182
+ warnings: [],
183
+ timestamp: Date.now(),
184
+ timestampISO: new Date().toISOString()
185
+ };
186
+ }
187
+ }
188
+ /**
189
+ * Get all cache instances
190
+ */
191
+ getCaches() {
192
+ return {
193
+ balance: this.balanceCache,
194
+ price: this.priceCache,
195
+ transaction: this.transactionCache
196
+ };
197
+ }
198
+ /**
199
+ * Get specific cache by name
200
+ */
201
+ getCache(name) {
202
+ switch (name) {
203
+ case 'balance':
204
+ return this.balanceCache;
205
+ case 'price':
206
+ return this.priceCache;
207
+ case 'transaction':
208
+ return this.transactionCache;
209
+ default:
210
+ return undefined;
211
+ }
212
+ }
213
+ /**
214
+ * Clear all caches (use with caution!)
215
+ */
216
+ async clearAll() {
217
+ const tag = TAG + 'clearAll | ';
218
+ try {
219
+ const result = {};
220
+ if (this.balanceCache) {
221
+ result.balance = await this.balanceCache.clearAll();
222
+ }
223
+ if (this.priceCache) {
224
+ result.price = await this.priceCache.clearAll();
225
+ }
226
+ if (this.transactionCache) {
227
+ result.transaction = await this.transactionCache.clearAll();
228
+ }
229
+ log.info(tag, 'Cleared all caches:', result);
230
+ return result;
231
+ }
232
+ catch (error) {
233
+ log.error(tag, 'Error clearing caches:', error);
234
+ throw error;
235
+ }
236
+ }
237
+ }
238
+ exports.CacheManager = CacheManager;
@@ -0,0 +1,11 @@
1
+ export { BaseCache } from './core/base-cache';
2
+ export { CacheManager } from './core/cache-manager';
3
+ export { BalanceCache } from './stores/balance-cache';
4
+ export { PriceCache } from './stores/price-cache';
5
+ export { TransactionCache } from './stores/transaction-cache';
6
+ export { RefreshWorker, startUnifiedWorker } from './workers/refresh-worker';
7
+ export type { CacheConfig, CachedValue, CacheResult, RefreshJob, HealthCheckResult, CacheStats } from './types';
8
+ export type { BalanceData } from './stores/balance-cache';
9
+ export type { PriceData } from './stores/price-cache';
10
+ export type { CacheManagerConfig } from './core/cache-manager';
11
+ export type { WorkerConfig } from './workers/refresh-worker';
package/dist/index.js ADDED
@@ -0,0 +1,25 @@
1
+ "use strict";
2
+ /*
3
+ @pioneer-platform/pioneer-cache
4
+
5
+ Unified caching system for Pioneer Platform.
6
+ Provides stale-while-revalidate caching with TTL, background refresh, and health monitoring.
7
+ */
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.startUnifiedWorker = exports.RefreshWorker = exports.TransactionCache = exports.PriceCache = exports.BalanceCache = exports.CacheManager = exports.BaseCache = void 0;
10
+ // Core exports
11
+ var base_cache_1 = require("./core/base-cache");
12
+ Object.defineProperty(exports, "BaseCache", { enumerable: true, get: function () { return base_cache_1.BaseCache; } });
13
+ var cache_manager_1 = require("./core/cache-manager");
14
+ Object.defineProperty(exports, "CacheManager", { enumerable: true, get: function () { return cache_manager_1.CacheManager; } });
15
+ // Cache implementations
16
+ var balance_cache_1 = require("./stores/balance-cache");
17
+ Object.defineProperty(exports, "BalanceCache", { enumerable: true, get: function () { return balance_cache_1.BalanceCache; } });
18
+ var price_cache_1 = require("./stores/price-cache");
19
+ Object.defineProperty(exports, "PriceCache", { enumerable: true, get: function () { return price_cache_1.PriceCache; } });
20
+ var transaction_cache_1 = require("./stores/transaction-cache");
21
+ Object.defineProperty(exports, "TransactionCache", { enumerable: true, get: function () { return transaction_cache_1.TransactionCache; } });
22
+ // Worker exports
23
+ var refresh_worker_1 = require("./workers/refresh-worker");
24
+ Object.defineProperty(exports, "RefreshWorker", { enumerable: true, get: function () { return refresh_worker_1.RefreshWorker; } });
25
+ Object.defineProperty(exports, "startUnifiedWorker", { enumerable: true, get: function () { return refresh_worker_1.startUnifiedWorker; } });
@@ -0,0 +1,47 @@
1
+ import { BaseCache } from '../core/base-cache';
2
+ import type { CacheConfig } from '../types';
3
+ /**
4
+ * Balance data structure
5
+ */
6
+ export interface BalanceData {
7
+ caip: string;
8
+ pubkey: string;
9
+ balance: string;
10
+ priceUsd?: string;
11
+ valueUsd?: string;
12
+ symbol?: string;
13
+ name?: string;
14
+ networkId?: string;
15
+ }
16
+ /**
17
+ * BalanceCache - Caches blockchain balance data
18
+ */
19
+ export declare class BalanceCache extends BaseCache<BalanceData> {
20
+ private balanceModule;
21
+ constructor(redis: any, balanceModule: any, config?: Partial<CacheConfig>);
22
+ /**
23
+ * Build Redis key for balance data
24
+ * Format: balance_v2:caip:pubkey
25
+ */
26
+ protected buildKey(params: Record<string, any>): string;
27
+ /**
28
+ * Fetch balance from blockchain via balance module
29
+ */
30
+ protected fetchFromSource(params: Record<string, any>): Promise<BalanceData>;
31
+ /**
32
+ * Try to get balance from legacy cache format
33
+ */
34
+ protected getLegacyCached(params: Record<string, any>): Promise<BalanceData | null>;
35
+ /**
36
+ * Get balance for a specific asset and pubkey
37
+ * Convenience method that wraps base get()
38
+ */
39
+ getBalance(caip: string, pubkey: string, waitForFresh?: boolean): Promise<BalanceData>;
40
+ /**
41
+ * Get balances for multiple assets (batch operation)
42
+ */
43
+ getBatchBalances(items: Array<{
44
+ caip: string;
45
+ pubkey: string;
46
+ }>, waitForFresh?: boolean): Promise<BalanceData[]>;
47
+ }
@@ -0,0 +1,158 @@
1
+ "use strict";
2
+ /*
3
+ BalanceCache - Balance-specific cache implementation
4
+
5
+ Extends BaseCache with balance-specific logic.
6
+ All common logic is inherited from BaseCache.
7
+ */
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.BalanceCache = void 0;
10
+ const base_cache_1 = require("../core/base-cache");
11
+ const log = require('@pioneer-platform/loggerdog')();
12
+ /**
13
+ * BalanceCache - Caches blockchain balance data
14
+ */
15
+ class BalanceCache extends base_cache_1.BaseCache {
16
+ constructor(redis, balanceModule, config) {
17
+ const defaultConfig = {
18
+ name: 'balance',
19
+ keyPrefix: 'balance_v2:',
20
+ ttl: 5 * 60 * 1000, // 5 minutes
21
+ staleThreshold: 2 * 60 * 1000, // 2 minutes
22
+ enableTTL: true,
23
+ queueName: 'balance-refresh-v2',
24
+ enableQueue: true,
25
+ maxRetries: 3,
26
+ retryDelay: 10000,
27
+ blockOnMiss: true, // Wait for fresh data on first request
28
+ enableLegacyFallback: true,
29
+ defaultValue: {
30
+ caip: '',
31
+ pubkey: '',
32
+ balance: '0',
33
+ },
34
+ maxConcurrentJobs: 10,
35
+ apiTimeout: 15000,
36
+ logCacheHits: false,
37
+ logCacheMisses: true,
38
+ logRefreshJobs: true
39
+ };
40
+ super(redis, { ...defaultConfig, ...config });
41
+ this.balanceModule = balanceModule;
42
+ }
43
+ /**
44
+ * Build Redis key for balance data
45
+ * Format: balance_v2:caip:pubkey
46
+ */
47
+ buildKey(params) {
48
+ const { caip, pubkey } = params;
49
+ if (!caip || !pubkey) {
50
+ throw new Error('BalanceCache.buildKey: caip and pubkey required');
51
+ }
52
+ const normalizedCaip = caip.toLowerCase();
53
+ // Don't lowercase pubkeys - xpub/ypub/zpub are case-sensitive base58
54
+ const normalizedPubkey = pubkey;
55
+ return `${this.config.keyPrefix}${normalizedCaip}:${normalizedPubkey}`;
56
+ }
57
+ /**
58
+ * Fetch balance from blockchain via balance module
59
+ */
60
+ async fetchFromSource(params) {
61
+ const tag = this.TAG + 'fetchFromSource | ';
62
+ try {
63
+ const { caip, pubkey } = params;
64
+ // Fetch balance using balance module
65
+ const asset = { caip };
66
+ const owner = { pubkey };
67
+ const balanceInfo = await this.balanceModule.getBalance(asset, owner);
68
+ if (!balanceInfo || !balanceInfo.balance) {
69
+ log.warn(tag, `No balance returned for ${caip}/${pubkey.substring(0, 10)}...`);
70
+ return {
71
+ caip,
72
+ pubkey,
73
+ balance: '0'
74
+ };
75
+ }
76
+ // Get asset metadata
77
+ const assetData = require('@pioneer-platform/pioneer-discovery').assetData;
78
+ const { caipToNetworkId } = require('@pioneer-platform/pioneer-caip');
79
+ const assetInfo = assetData[caip.toUpperCase()] || assetData[caip.toLowerCase()] || {};
80
+ const networkId = caipToNetworkId(caip);
81
+ return {
82
+ caip,
83
+ pubkey,
84
+ balance: balanceInfo.balance,
85
+ symbol: assetInfo.symbol,
86
+ name: assetInfo.name,
87
+ networkId
88
+ };
89
+ }
90
+ catch (error) {
91
+ log.error(tag, 'Error fetching balance:', error);
92
+ throw error;
93
+ }
94
+ }
95
+ /**
96
+ * Try to get balance from legacy cache format
97
+ */
98
+ async getLegacyCached(params) {
99
+ const tag = this.TAG + 'getLegacyCached | ';
100
+ try {
101
+ const { caip, pubkey } = params;
102
+ const { caipToNetworkId } = require('@pioneer-platform/pioneer-caip');
103
+ const networkId = caipToNetworkId(caip);
104
+ // Try legacy format: cache:balance:pubkey:asset
105
+ const legacyKey = `cache:balance:${pubkey}:${networkId}`;
106
+ const legacyData = await this.redis.get(legacyKey);
107
+ if (legacyData) {
108
+ const parsed = JSON.parse(legacyData);
109
+ if (parsed.balance !== undefined) {
110
+ return {
111
+ caip,
112
+ pubkey,
113
+ balance: typeof parsed.balance === 'string' ? parsed.balance : String(parsed.balance)
114
+ };
115
+ }
116
+ }
117
+ return null;
118
+ }
119
+ catch (error) {
120
+ log.error(tag, 'Error getting legacy cached balance:', error);
121
+ return null;
122
+ }
123
+ }
124
+ /**
125
+ * Get balance for a specific asset and pubkey
126
+ * Convenience method that wraps base get()
127
+ */
128
+ async getBalance(caip, pubkey, waitForFresh) {
129
+ const result = await this.get({ caip, pubkey }, waitForFresh);
130
+ return result.value || this.config.defaultValue;
131
+ }
132
+ /**
133
+ * Get balances for multiple assets (batch operation)
134
+ */
135
+ async getBatchBalances(items, waitForFresh) {
136
+ const tag = this.TAG + 'getBatchBalances | ';
137
+ const startTime = Date.now();
138
+ try {
139
+ log.info(tag, `Batch request for ${items.length} balances`);
140
+ // Get all balances in parallel
141
+ const promises = items.map(item => this.getBalance(item.caip, item.pubkey, waitForFresh));
142
+ const results = await Promise.all(promises);
143
+ const responseTime = Date.now() - startTime;
144
+ log.info(tag, `Batch completed: ${results.length} balances in ${responseTime}ms (${(responseTime / results.length).toFixed(1)}ms avg)`);
145
+ return results;
146
+ }
147
+ catch (error) {
148
+ log.error(tag, 'Error in batch balance request:', error);
149
+ // Return defaults for all items
150
+ return items.map(item => ({
151
+ caip: item.caip,
152
+ pubkey: item.pubkey,
153
+ balance: '0'
154
+ }));
155
+ }
156
+ }
157
+ }
158
+ exports.BalanceCache = BalanceCache;
@@ -0,0 +1,39 @@
1
+ import { BaseCache } from '../core/base-cache';
2
+ import type { CacheConfig } from '../types';
3
+ /**
4
+ * Price data structure
5
+ */
6
+ export interface PriceData {
7
+ caip: string;
8
+ price: number;
9
+ source?: string;
10
+ }
11
+ /**
12
+ * PriceCache - Caches USD prices for assets
13
+ */
14
+ export declare class PriceCache extends BaseCache<PriceData> {
15
+ private markets;
16
+ constructor(redis: any, markets: any, config?: Partial<CacheConfig>);
17
+ /**
18
+ * Build Redis key for price data
19
+ * Format: price_v2:caip
20
+ */
21
+ protected buildKey(params: Record<string, any>): string;
22
+ /**
23
+ * Fetch price from markets API
24
+ */
25
+ protected fetchFromSource(params: Record<string, any>): Promise<PriceData>;
26
+ /**
27
+ * Try to get price from legacy cache formats
28
+ */
29
+ protected getLegacyCached(params: Record<string, any>): Promise<PriceData | null>;
30
+ /**
31
+ * Get price for a specific asset
32
+ * Convenience method that wraps base get()
33
+ */
34
+ getPrice(caip: string, waitForFresh?: boolean): Promise<number>;
35
+ /**
36
+ * Get prices for multiple assets (batch operation)
37
+ */
38
+ getBatchPrices(caips: string[], waitForFresh?: boolean): Promise<Map<string, number>>;
39
+ }