@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,179 @@
1
+ "use strict";
2
+ /*
3
+ PriceCache - Price-specific cache implementation
4
+
5
+ Extends BaseCache with price-specific logic.
6
+ All common logic is inherited from BaseCache (including all 5 fixes!)
7
+ */
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.PriceCache = void 0;
10
+ const base_cache_1 = require("../core/base-cache");
11
+ const log = require('@pioneer-platform/loggerdog')();
12
+ /**
13
+ * PriceCache - Caches USD prices for assets
14
+ */
15
+ class PriceCache extends base_cache_1.BaseCache {
16
+ constructor(redis, markets, config) {
17
+ const defaultConfig = {
18
+ name: 'price',
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',
24
+ enableQueue: true,
25
+ maxRetries: 3,
26
+ retryDelay: 5000,
27
+ blockOnMiss: false, // Return immediately with $0 (prices less critical)
28
+ enableLegacyFallback: true,
29
+ defaultValue: {
30
+ caip: '',
31
+ price: 0
32
+ },
33
+ maxConcurrentJobs: 5,
34
+ apiTimeout: 5000,
35
+ logCacheHits: false,
36
+ logCacheMisses: true,
37
+ logRefreshJobs: true
38
+ };
39
+ super(redis, { ...defaultConfig, ...config });
40
+ this.markets = markets;
41
+ }
42
+ /**
43
+ * Build Redis key for price data
44
+ * Format: price_v2:caip
45
+ */
46
+ buildKey(params) {
47
+ const { caip } = params;
48
+ if (!caip) {
49
+ throw new Error('PriceCache.buildKey: caip required');
50
+ }
51
+ const normalizedCaip = caip.toLowerCase();
52
+ return `${this.config.keyPrefix}${normalizedCaip}`;
53
+ }
54
+ /**
55
+ * Fetch price from markets API
56
+ */
57
+ async fetchFromSource(params) {
58
+ const tag = this.TAG + 'fetchFromSource | ';
59
+ try {
60
+ 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
+ }
82
+ if (isNaN(price) || price <= 0) {
83
+ log.warn(tag, `Invalid price for ${caip} (${symbol}): ${priceResult}`);
84
+ return {
85
+ caip,
86
+ price: 0
87
+ };
88
+ }
89
+ log.debug(tag, `Fetched price for ${caip} (${symbol}): $${price}`);
90
+ return {
91
+ caip,
92
+ price,
93
+ source: 'markets'
94
+ };
95
+ }
96
+ catch (error) {
97
+ log.error(tag, 'Error fetching price:', error);
98
+ throw error;
99
+ }
100
+ }
101
+ /**
102
+ * Try to get price from legacy cache formats
103
+ */
104
+ async getLegacyCached(params) {
105
+ const tag = this.TAG + 'getLegacyCached | ';
106
+ try {
107
+ const { caip } = params;
108
+ // Try CoinGecko format first (more accurate)
109
+ const coingeckoKey = `coingecko:${caip}`;
110
+ const coingeckoData = await this.redis.get(coingeckoKey);
111
+ if (coingeckoData) {
112
+ const parsed = JSON.parse(coingeckoData);
113
+ if (parsed.current_price && typeof parsed.current_price === 'number') {
114
+ return {
115
+ caip,
116
+ price: parsed.current_price,
117
+ source: 'coingecko-legacy'
118
+ };
119
+ }
120
+ }
121
+ // Try CoinCap format
122
+ const coincapKey = `coincap:${caip}`;
123
+ const coincapData = await this.redis.get(coincapKey);
124
+ if (coincapData) {
125
+ const parsed = JSON.parse(coincapData);
126
+ if (parsed.priceUsd && typeof parsed.priceUsd === 'string') {
127
+ const price = parseFloat(parsed.priceUsd);
128
+ if (!isNaN(price)) {
129
+ return {
130
+ caip,
131
+ price,
132
+ source: 'coincap-legacy'
133
+ };
134
+ }
135
+ }
136
+ }
137
+ return null;
138
+ }
139
+ catch (error) {
140
+ log.error(tag, 'Error getting legacy cached price:', error);
141
+ return null;
142
+ }
143
+ }
144
+ /**
145
+ * Get price for a specific asset
146
+ * Convenience method that wraps base get()
147
+ */
148
+ async getPrice(caip, waitForFresh) {
149
+ const result = await this.get({ caip }, waitForFresh);
150
+ return result.value?.price || 0;
151
+ }
152
+ /**
153
+ * Get prices for multiple assets (batch operation)
154
+ */
155
+ async getBatchPrices(caips, waitForFresh) {
156
+ const tag = this.TAG + 'getBatchPrices | ';
157
+ const startTime = Date.now();
158
+ try {
159
+ log.info(tag, `Batch request for ${caips.length} prices`);
160
+ // Get all prices in parallel
161
+ const promises = caips.map(caip => this.getPrice(caip, waitForFresh));
162
+ const results = await Promise.all(promises);
163
+ // Build map of caip -> price
164
+ const priceMap = new Map();
165
+ caips.forEach((caip, index) => {
166
+ priceMap.set(caip, results[index]);
167
+ });
168
+ const responseTime = Date.now() - startTime;
169
+ log.info(tag, `Batch completed: ${results.length} prices in ${responseTime}ms (${(responseTime / results.length).toFixed(1)}ms avg)`);
170
+ return priceMap;
171
+ }
172
+ catch (error) {
173
+ log.error(tag, 'Error in batch price request:', error);
174
+ // Return empty map
175
+ return new Map();
176
+ }
177
+ }
178
+ }
179
+ exports.PriceCache = PriceCache;
@@ -0,0 +1,42 @@
1
+ /**
2
+ * TransactionCache - Caches immutable blockchain transaction data
3
+ */
4
+ export declare class TransactionCache {
5
+ private redis;
6
+ private readonly keyPrefix;
7
+ private readonly TAG;
8
+ constructor(redis: any);
9
+ /**
10
+ * Get cached transaction data by txid
11
+ */
12
+ get(txid: string): Promise<any | null>;
13
+ /**
14
+ * Cache transaction data PERMANENTLY (no expiry)
15
+ * Transactions are immutable - once on the blockchain, they never change
16
+ */
17
+ set(txid: string, txData: any): Promise<void>;
18
+ /**
19
+ * Get transaction from cache or fetch from source and cache it
20
+ * Classic cache-aside pattern
21
+ */
22
+ getOrFetch(txid: string, fetchFn: () => Promise<any>): Promise<any>;
23
+ /**
24
+ * Invalidate cached transaction (useful for unconfirmed transactions)
25
+ */
26
+ invalidate(txid: string): Promise<void>;
27
+ /**
28
+ * Get cache statistics
29
+ */
30
+ getStats(): Promise<{
31
+ totalKeys: number;
32
+ memoryUsage: string;
33
+ }>;
34
+ /**
35
+ * Clear all cached transactions (use with caution)
36
+ */
37
+ clearAll(): Promise<number>;
38
+ /**
39
+ * Build cache key for a transaction
40
+ */
41
+ private buildKey;
42
+ }
@@ -0,0 +1,148 @@
1
+ "use strict";
2
+ /*
3
+ TransactionCache - Transaction-specific cache implementation
4
+
5
+ Different pattern from Balance/Price caches:
6
+ - Caches transaction data PERMANENTLY (no TTL)
7
+ - No background refresh (blockchain data is immutable)
8
+ - Simple cache-aside pattern with getOrFetch()
9
+ - No workers needed
10
+
11
+ Does NOT extend BaseCache - intentionally different architecture.
12
+ */
13
+ Object.defineProperty(exports, "__esModule", { value: true });
14
+ exports.TransactionCache = void 0;
15
+ const log = require('@pioneer-platform/loggerdog')();
16
+ /**
17
+ * TransactionCache - Caches immutable blockchain transaction data
18
+ */
19
+ class TransactionCache {
20
+ constructor(redis) {
21
+ this.keyPrefix = 'tx:';
22
+ this.TAG = ' | TransactionCache | ';
23
+ this.redis = redis;
24
+ log.info(this.TAG, 'TransactionCache initialized (permanent caching, no TTL)');
25
+ }
26
+ /**
27
+ * Get cached transaction data by txid
28
+ */
29
+ async get(txid) {
30
+ const tag = this.TAG + 'get | ';
31
+ try {
32
+ const key = this.buildKey(txid);
33
+ const cached = await this.redis.get(key);
34
+ if (cached) {
35
+ log.info(tag, `Cache HIT for txid: ${txid.substring(0, 16)}...`);
36
+ return JSON.parse(cached);
37
+ }
38
+ log.info(tag, `Cache MISS for txid: ${txid.substring(0, 16)}...`);
39
+ return null;
40
+ }
41
+ catch (error) {
42
+ log.error(tag, `Error getting cached tx ${txid}:`, error);
43
+ return null; // Fail gracefully
44
+ }
45
+ }
46
+ /**
47
+ * Cache transaction data PERMANENTLY (no expiry)
48
+ * Transactions are immutable - once on the blockchain, they never change
49
+ */
50
+ async set(txid, txData) {
51
+ const tag = this.TAG + 'set | ';
52
+ try {
53
+ const key = this.buildKey(txid);
54
+ // NO TTL - cache permanently
55
+ await this.redis.set(key, JSON.stringify(txData));
56
+ log.info(tag, `Cached txid: ${txid.substring(0, 16)}... (PERMANENT - no expiry)`);
57
+ }
58
+ catch (error) {
59
+ log.error(tag, `Error caching tx ${txid}:`, error);
60
+ // Don't throw - caching is optional
61
+ }
62
+ }
63
+ /**
64
+ * Get transaction from cache or fetch from source and cache it
65
+ * Classic cache-aside pattern
66
+ */
67
+ async getOrFetch(txid, fetchFn) {
68
+ const tag = this.TAG + 'getOrFetch | ';
69
+ // Try cache first
70
+ const cached = await this.get(txid);
71
+ if (cached) {
72
+ return cached;
73
+ }
74
+ // Not in cache - fetch from source
75
+ log.info(tag, `Fetching from source: ${txid.substring(0, 16)}...`);
76
+ const txData = await fetchFn();
77
+ // Cache the result
78
+ await this.set(txid, txData);
79
+ return txData;
80
+ }
81
+ /**
82
+ * Invalidate cached transaction (useful for unconfirmed transactions)
83
+ */
84
+ async invalidate(txid) {
85
+ const tag = this.TAG + 'invalidate | ';
86
+ try {
87
+ const key = this.buildKey(txid);
88
+ await this.redis.del(key);
89
+ log.info(tag, `Invalidated cache for txid: ${txid.substring(0, 16)}...`);
90
+ }
91
+ catch (error) {
92
+ log.error(tag, `Error invalidating cached tx ${txid}:`, error);
93
+ }
94
+ }
95
+ /**
96
+ * Get cache statistics
97
+ */
98
+ async getStats() {
99
+ const tag = this.TAG + 'getStats | ';
100
+ try {
101
+ const keys = await this.redis.keys(`${this.keyPrefix}*`);
102
+ const totalKeys = keys.length;
103
+ // Get Redis memory info
104
+ const info = await this.redis.info('memory');
105
+ const memoryMatch = info.match(/used_memory_human:(.+)/);
106
+ const memoryUsage = memoryMatch ? memoryMatch[1].trim() : 'unknown';
107
+ log.info(tag, `Cache stats: ${totalKeys} keys, ${memoryUsage} memory`);
108
+ return {
109
+ totalKeys,
110
+ memoryUsage
111
+ };
112
+ }
113
+ catch (error) {
114
+ log.error(tag, 'Error getting cache stats:', error);
115
+ return {
116
+ totalKeys: 0,
117
+ memoryUsage: 'unknown'
118
+ };
119
+ }
120
+ }
121
+ /**
122
+ * Clear all cached transactions (use with caution)
123
+ */
124
+ async clearAll() {
125
+ const tag = this.TAG + 'clearAll | ';
126
+ try {
127
+ const keys = await this.redis.keys(`${this.keyPrefix}*`);
128
+ if (keys.length === 0) {
129
+ log.info(tag, 'No cached transactions to clear');
130
+ return 0;
131
+ }
132
+ await this.redis.del(...keys);
133
+ log.info(tag, `Cleared ${keys.length} cached transactions`);
134
+ return keys.length;
135
+ }
136
+ catch (error) {
137
+ log.error(tag, 'Error clearing cache:', error);
138
+ return 0;
139
+ }
140
+ }
141
+ /**
142
+ * Build cache key for a transaction
143
+ */
144
+ buildKey(txid) {
145
+ return `${this.keyPrefix}${txid}`;
146
+ }
147
+ }
148
+ exports.TransactionCache = TransactionCache;
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Cache configuration for a specific cache store
3
+ */
4
+ export interface CacheConfig {
5
+ name: string;
6
+ keyPrefix: string;
7
+ ttl: number;
8
+ staleThreshold?: number;
9
+ enableTTL: boolean;
10
+ queueName: string;
11
+ enableQueue: boolean;
12
+ maxRetries: number;
13
+ retryDelay: number;
14
+ blockOnMiss: boolean;
15
+ enableLegacyFallback: boolean;
16
+ defaultValue: any;
17
+ maxConcurrentJobs: number;
18
+ apiTimeout: number;
19
+ logCacheHits: boolean;
20
+ logCacheMisses: boolean;
21
+ logRefreshJobs: boolean;
22
+ }
23
+ /**
24
+ * Generic cached value wrapper
25
+ */
26
+ export interface CachedValue<T> {
27
+ value: T;
28
+ timestamp: number;
29
+ source: string;
30
+ lastUpdated: string;
31
+ metadata?: Record<string, any>;
32
+ }
33
+ /**
34
+ * Cache operation result
35
+ */
36
+ export interface CacheResult<T> {
37
+ success: boolean;
38
+ value?: T;
39
+ cached: boolean;
40
+ fresh: boolean;
41
+ age?: number;
42
+ error?: string;
43
+ }
44
+ /**
45
+ * Health check result
46
+ */
47
+ export interface HealthCheckResult {
48
+ status: 'healthy' | 'degraded' | 'unhealthy';
49
+ queueInitialized: boolean;
50
+ redisConnected: boolean;
51
+ stats: CacheStats;
52
+ issues: string[];
53
+ warnings: string[];
54
+ timestamp: number;
55
+ timestampISO: string;
56
+ }
57
+ /**
58
+ * Cache statistics
59
+ */
60
+ export interface CacheStats {
61
+ totalEntries: number;
62
+ staleEntries: number;
63
+ freshEntries: number;
64
+ stalenessRate: string;
65
+ sources?: Record<string, number>;
66
+ byNetwork?: Record<string, number>;
67
+ entriesWithoutTTL?: number;
68
+ ttl?: number;
69
+ staleThreshold?: number;
70
+ }
71
+ /**
72
+ * Refresh job for background workers
73
+ */
74
+ export interface RefreshJob {
75
+ type: string;
76
+ key: string;
77
+ params: Record<string, any>;
78
+ priority?: 'high' | 'normal' | 'low';
79
+ retryCount?: number;
80
+ timestamp?: number;
81
+ }
82
+ /**
83
+ * Source fetcher function signature
84
+ * Returns the data to be cached
85
+ */
86
+ export type SourceFetcher<T> = (params: Record<string, any>) => Promise<T>;
87
+ /**
88
+ * Key builder function signature
89
+ * Builds Redis key from parameters
90
+ */
91
+ export type KeyBuilder = (...args: any[]) => string;
92
+ /**
93
+ * Legacy key pattern for migration
94
+ */
95
+ export interface LegacyKeyPattern {
96
+ prefix: string;
97
+ keyBuilder: KeyBuilder;
98
+ }
@@ -0,0 +1,5 @@
1
+ "use strict";
2
+ /*
3
+ Pioneer Cache - Core Types and Interfaces
4
+ */
5
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,57 @@
1
+ import type { BaseCache } from '../core/base-cache';
2
+ /**
3
+ * Worker configuration
4
+ */
5
+ export interface WorkerConfig {
6
+ queueName: string;
7
+ maxRetries: number;
8
+ retryDelay: number;
9
+ pollInterval?: number;
10
+ }
11
+ /**
12
+ * Unified refresh worker that processes jobs for any cache type
13
+ */
14
+ export declare class RefreshWorker {
15
+ private redis;
16
+ private redisQueue;
17
+ private cacheRegistry;
18
+ private config;
19
+ private isRunning;
20
+ private isProcessing;
21
+ private pollTimeoutId;
22
+ constructor(redis: any, config: WorkerConfig);
23
+ /**
24
+ * Register a cache instance with this worker
25
+ * The worker will route jobs to the appropriate cache based on job type
26
+ */
27
+ registerCache(cacheName: string, cache: BaseCache<any>): void;
28
+ /**
29
+ * Start processing jobs from the queue
30
+ */
31
+ start(): Promise<void>;
32
+ /**
33
+ * Stop the worker gracefully
34
+ */
35
+ stop(): Promise<void>;
36
+ /**
37
+ * Poll for next job from the queue
38
+ */
39
+ private poll;
40
+ /**
41
+ * Schedule next poll
42
+ */
43
+ private schedulePoll;
44
+ /**
45
+ * Process a single refresh job
46
+ */
47
+ private processJob;
48
+ /**
49
+ * Get worker statistics
50
+ */
51
+ getStats(): Promise<any>;
52
+ }
53
+ /**
54
+ * Start a unified refresh worker for multiple cache types
55
+ * Convenience function for common usage
56
+ */
57
+ export declare function startUnifiedWorker(redis: any, caches: Map<string, BaseCache<any>>, queueName: string, config?: Partial<WorkerConfig>): Promise<RefreshWorker>;