@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,2 @@
1
+
2
+ $ tsc
package/README.md ADDED
@@ -0,0 +1,451 @@
1
+ # @pioneer-platform/pioneer-cache
2
+
3
+ Unified caching system for Pioneer Platform with stale-while-revalidate pattern, TTL management, background refresh workers, and production health monitoring.
4
+
5
+ ## Features
6
+
7
+ ✅ **Stale-While-Revalidate** - Return cached data instantly, refresh in background
8
+ ✅ **Automatic TTL Management** - Redis keys auto-expire, preventing stale data
9
+ ✅ **Background Refresh Workers** - Queue-based async refresh with retry logic
10
+ ✅ **Health Monitoring** - Real-time cache health checks with staleness metrics
11
+ ✅ **Legacy Migration** - Automatic fallback to old cache formats
12
+ ✅ **Blocking Mode** - Wait for fresh data on first request (configurable)
13
+ ✅ **Batch Operations** - Efficient parallel requests for multiple items
14
+ ✅ **TypeScript** - Full type safety with generics
15
+
16
+ ## Installation
17
+
18
+ ```bash
19
+ bun add @pioneer-platform/pioneer-cache
20
+ ```
21
+
22
+ ## Architecture
23
+
24
+ ### BaseCache Pattern
25
+
26
+ All cache implementations extend `BaseCache<T>` which provides:
27
+ - Stale-while-revalidate logic
28
+ - TTL management
29
+ - Background refresh coordination
30
+ - Health monitoring
31
+ - Legacy fallback
32
+
33
+ Subclasses implement only 3 methods:
34
+ 1. `buildKey(params)` - Build Redis key from parameters
35
+ 2. `fetchFromSource(params)` - Fetch fresh data from source
36
+ 3. `getLegacyCached(params)` - Optional legacy cache migration
37
+
38
+ ### Cache Types
39
+
40
+ **BalanceCache** - Blockchain balance data (5 minute TTL, 2 minute refresh)
41
+ - Fetches from blockchain via balance module
42
+ - Blocks on first miss for accurate balances
43
+ - Supports batch balance requests
44
+
45
+ **PriceCache** - USD price data (1 hour TTL, 30 minute refresh)
46
+ - Fetches from markets API
47
+ - Returns immediately with $0 on miss (non-blocking)
48
+ - Supports batch price requests
49
+
50
+ **TransactionCache** - Immutable transaction data (no TTL)
51
+ - Classic cache-aside pattern
52
+ - Permanent caching (blockchain data never changes)
53
+ - No background workers needed
54
+
55
+ ## Quick Start
56
+
57
+ ### Using CacheManager (Recommended)
58
+
59
+ ```typescript
60
+ import { CacheManager } from '@pioneer-platform/pioneer-cache';
61
+
62
+ const cacheManager = new CacheManager({
63
+ redis, // Redis client
64
+ balanceModule, // Balance fetcher
65
+ markets, // Price fetcher
66
+ enableBalanceCache: true,
67
+ enablePriceCache: true,
68
+ enableTransactionCache: true,
69
+ startWorkers: true // Auto-start background workers
70
+ });
71
+
72
+ // Get caches
73
+ const { balance, price, transaction } = cacheManager.getCaches();
74
+
75
+ // Get balance
76
+ const balanceData = await balance.getBalance('eip155:1/slip44:60', 'xpub123...');
77
+
78
+ // Get price
79
+ const btcPrice = await price.getPrice('eip155:1/slip44:0');
80
+
81
+ // Get transaction
82
+ const tx = await transaction.getOrFetch('txid123...', async () => {
83
+ return await fetchTransactionFromBlockchain(txid);
84
+ });
85
+
86
+ // Health check
87
+ const health = await cacheManager.getHealth();
88
+ console.log(health.status); // 'healthy' | 'degraded' | 'unhealthy'
89
+ ```
90
+
91
+ ### Using Individual Caches
92
+
93
+ ```typescript
94
+ import { BalanceCache, PriceCache, TransactionCache } from '@pioneer-platform/pioneer-cache';
95
+
96
+ // Balance Cache
97
+ const balanceCache = new BalanceCache(redis, balanceModule, {
98
+ ttl: 5 * 60 * 1000, // 5 minutes
99
+ staleThreshold: 2 * 60 * 1000, // Refresh after 2 minutes
100
+ blockOnMiss: true // Wait for first request
101
+ });
102
+
103
+ const balance = await balanceCache.getBalance('eip155:1/slip44:60', 'xpub123...');
104
+ console.log(balance.balance); // "1234567890"
105
+
106
+ // Price Cache
107
+ const priceCache = new PriceCache(redis, markets, {
108
+ ttl: 60 * 60 * 1000, // 1 hour
109
+ staleThreshold: 30 * 60 * 1000, // Refresh after 30 minutes
110
+ blockOnMiss: false // Return $0 immediately
111
+ });
112
+
113
+ const price = await priceCache.getPrice('eip155:1/slip44:60');
114
+ console.log(price); // 2500.00
115
+
116
+ // Transaction Cache
117
+ const txCache = new TransactionCache(redis);
118
+
119
+ const tx = await txCache.getOrFetch('txid123...', async () => {
120
+ return await blockchain.getTransaction('txid123...');
121
+ });
122
+ ```
123
+
124
+ ### Batch Operations
125
+
126
+ ```typescript
127
+ // Batch balances
128
+ const items = [
129
+ { caip: 'eip155:1/slip44:60', pubkey: 'xpub123...' },
130
+ { caip: 'eip155:1/slip44:0', pubkey: 'xpub456...' },
131
+ ];
132
+ const balances = await balanceCache.getBatchBalances(items);
133
+
134
+ // Batch prices
135
+ const caips = ['eip155:1/slip44:60', 'eip155:1/slip44:0'];
136
+ const prices = await priceCache.getBatchPrices(caips);
137
+ console.log(prices.get('eip155:1/slip44:60')); // 2500.00
138
+ ```
139
+
140
+ ## Configuration
141
+
142
+ ### CacheConfig Interface
143
+
144
+ ```typescript
145
+ interface CacheConfig {
146
+ name: string; // Cache name for logging
147
+ keyPrefix: string; // Redis key prefix (e.g., "balance_v2:")
148
+ ttl: number; // Time-to-live in milliseconds
149
+ staleThreshold: number; // Refresh threshold in milliseconds
150
+ enableTTL: boolean; // Enable automatic expiration
151
+ queueName: string; // Queue name for background jobs
152
+ enableQueue: boolean; // Enable background refresh
153
+ maxRetries: number; // Max retry attempts
154
+ retryDelay: number; // Delay between retries (ms)
155
+ blockOnMiss: boolean; // Block on first request vs return default
156
+ enableLegacyFallback: boolean; // Try legacy cache formats
157
+ defaultValue: T; // Default value on miss
158
+ maxConcurrentJobs: number; // Worker concurrency limit
159
+ apiTimeout: number; // Source API timeout (ms)
160
+ logCacheHits: boolean; // Log cache hits
161
+ logCacheMisses: boolean; // Log cache misses
162
+ logRefreshJobs: boolean; // Log refresh jobs
163
+ }
164
+ ```
165
+
166
+ ### Default Configurations
167
+
168
+ **Balance Cache** (Critical - block on miss)
169
+ ```typescript
170
+ {
171
+ ttl: 5 * 60 * 1000, // 5 minutes
172
+ staleThreshold: 2 * 60 * 1000, // Refresh after 2 minutes
173
+ blockOnMiss: true, // Wait for fresh data
174
+ maxRetries: 3,
175
+ retryDelay: 10000
176
+ }
177
+ ```
178
+
179
+ **Price Cache** (Non-critical - return immediately)
180
+ ```typescript
181
+ {
182
+ ttl: 60 * 60 * 1000, // 1 hour
183
+ staleThreshold: 30 * 60 * 1000, // Refresh after 30 minutes
184
+ blockOnMiss: false, // Return $0 immediately
185
+ maxRetries: 3,
186
+ retryDelay: 5000
187
+ }
188
+ ```
189
+
190
+ ## Background Workers
191
+
192
+ ### Unified Worker
193
+
194
+ A single worker processes refresh jobs for all cache types:
195
+
196
+ ```typescript
197
+ import { startUnifiedWorker } from '@pioneer-platform/pioneer-cache';
198
+
199
+ const cacheRegistry = new Map();
200
+ cacheRegistry.set('balance', balanceCache);
201
+ cacheRegistry.set('price', priceCache);
202
+
203
+ const worker = await startUnifiedWorker(
204
+ redis,
205
+ cacheRegistry,
206
+ 'cache-refresh',
207
+ {
208
+ maxRetries: 3,
209
+ retryDelay: 5000,
210
+ pollInterval: 100
211
+ }
212
+ );
213
+
214
+ // Worker stats
215
+ const stats = await worker.getStats();
216
+ console.log(stats.queueLength); // Pending jobs
217
+ console.log(stats.isRunning); // Worker status
218
+
219
+ // Stop worker
220
+ await worker.stop();
221
+ ```
222
+
223
+ ## Health Monitoring
224
+
225
+ ### Cache Health
226
+
227
+ ```typescript
228
+ const health = await balanceCache.getHealth();
229
+
230
+ console.log(health.status); // 'healthy' | 'degraded' | 'unhealthy'
231
+ console.log(health.queueInitialized); // true/false
232
+ console.log(health.redisConnected); // true/false
233
+ console.log(health.stats); // Cache statistics
234
+ console.log(health.issues); // Critical issues (unhealthy)
235
+ console.log(health.warnings); // Non-critical warnings (degraded)
236
+ ```
237
+
238
+ **Status Levels:**
239
+ - `healthy` - All systems operational
240
+ - `degraded` - Non-critical issues (warnings)
241
+ - `unhealthy` - Critical issues requiring immediate attention
242
+
243
+ ### Aggregate Health (CacheManager)
244
+
245
+ ```typescript
246
+ const health = await cacheManager.getHealth();
247
+
248
+ console.log(health.status); // Overall status
249
+ console.log(health.checks.balance); // Balance cache health
250
+ console.log(health.checks.price); // Price cache health
251
+ console.log(health.checks.transaction);// Transaction cache health
252
+ console.log(health.checks.worker); // Worker health
253
+ ```
254
+
255
+ ## Critical Fixes Included
256
+
257
+ This module fixes 5 critical bugs from the legacy cache implementations:
258
+
259
+ ### ✅ Fix #1: Cache Miss Blocking
260
+ **Problem:** Cache miss returned "0" immediately instead of waiting for fresh data
261
+ **Solution:** Added `blockOnMiss` config and `fetchFresh()` method that waits for source
262
+
263
+ ### ✅ Fix #2: Redis TTL Management
264
+ **Problem:** Cache values never expired, causing stale data to stick forever
265
+ **Solution:** All cache writes include TTL: `redis.set(key, value, 'EX', ttlSeconds)`
266
+
267
+ ### ✅ Fix #3: Loud Queue Failures
268
+ **Problem:** Queue initialization failures were silent (debug logs only)
269
+ **Solution:** Changed to ERROR logs and added validation checks
270
+
271
+ ### ✅ Fix #4: Synchronous Fallback
272
+ **Problem:** When queue failed, no fallback - refresh never happened
273
+ **Solution:** Added immediate synchronous refresh when queue unavailable
274
+
275
+ ### ✅ Fix #5: Health Monitoring
276
+ **Problem:** No way to detect cache issues in production
277
+ **Solution:** Added `getHealth()` with staleness detection and issue reporting
278
+
279
+ ## Legacy Migration
280
+
281
+ The cache automatically migrates from old formats:
282
+
283
+ **Balance Cache Legacy:**
284
+ - `cache:balance:pubkey:networkId` → `balance_v2:caip:pubkey`
285
+
286
+ **Price Cache Legacy:**
287
+ - `coingecko:caip` → `price_v2:caip`
288
+ - `coincap:caip` → `price_v2:caip`
289
+
290
+ Legacy keys are automatically migrated to new format on first access.
291
+
292
+ ## Advanced Usage
293
+
294
+ ### Custom Cache Implementation
295
+
296
+ ```typescript
297
+ import { BaseCache, CacheConfig, CacheResult } from '@pioneer-platform/pioneer-cache';
298
+
299
+ interface MyData {
300
+ id: string;
301
+ value: number;
302
+ }
303
+
304
+ class MyCache extends BaseCache<MyData> {
305
+ protected buildKey(params: Record<string, any>): string {
306
+ return `${this.config.keyPrefix}${params.id}`;
307
+ }
308
+
309
+ protected async fetchFromSource(params: Record<string, any>): Promise<MyData> {
310
+ // Fetch from your source
311
+ const response = await fetch(`https://api.example.com/data/${params.id}`);
312
+ return response.json();
313
+ }
314
+
315
+ protected async getLegacyCached(params: Record<string, any>): Promise<MyData | null> {
316
+ // Optional: migrate from old cache format
317
+ return null;
318
+ }
319
+ }
320
+
321
+ // Use it
322
+ const myCache = new MyCache(redis, {
323
+ name: 'my-cache',
324
+ keyPrefix: 'my:',
325
+ ttl: 60 * 1000,
326
+ // ... other config
327
+ });
328
+
329
+ const data = await myCache.get({ id: '123' });
330
+ ```
331
+
332
+ ### Manual Refresh
333
+
334
+ ```typescript
335
+ // Force refresh specific item
336
+ await balanceCache.fetchFresh({ caip: 'eip155:1/slip44:60', pubkey: 'xpub123...' });
337
+
338
+ // Clear all caches
339
+ const cleared = await cacheManager.clearAll();
340
+ console.log(cleared.balance); // Number of keys deleted
341
+ console.log(cleared.price);
342
+ console.log(cleared.transaction);
343
+ ```
344
+
345
+ ## Testing
346
+
347
+ ```typescript
348
+ import { BalanceCache } from '@pioneer-platform/pioneer-cache';
349
+
350
+ describe('BalanceCache', () => {
351
+ let redis;
352
+ let balanceModule;
353
+ let cache;
354
+
355
+ beforeEach(() => {
356
+ redis = createMockRedis();
357
+ balanceModule = createMockBalanceModule();
358
+ cache = new BalanceCache(redis, balanceModule);
359
+ });
360
+
361
+ it('should block on first miss', async () => {
362
+ const result = await cache.getBalance('eip155:1/slip44:60', 'xpub123...');
363
+ expect(result.balance).not.toBe('0');
364
+ expect(result.balance).toBe('1234567890');
365
+ });
366
+
367
+ it('should return stale and refresh async', async () => {
368
+ // First request - cache miss, blocks
369
+ await cache.getBalance('eip155:1/slip44:60', 'xpub123...');
370
+
371
+ // Second request - cache hit, returns stale, refreshes async
372
+ const result = await cache.getBalance('eip155:1/slip44:60', 'xpub123...');
373
+ expect(result.source).toBe('cache_stale');
374
+ });
375
+
376
+ it('should include TTL on all writes', async () => {
377
+ await cache.getBalance('eip155:1/slip44:60', 'xpub123...');
378
+
379
+ // Verify TTL was set
380
+ expect(redis.set).toHaveBeenCalledWith(
381
+ expect.any(String),
382
+ expect.any(String),
383
+ 'EX',
384
+ expect.any(Number)
385
+ );
386
+ });
387
+ });
388
+ ```
389
+
390
+ ## Migration from Legacy Caches
391
+
392
+ ### Before (pioneer-server)
393
+
394
+ ```typescript
395
+ // services/balance-cache.service.ts
396
+ import { BalanceCacheService } from './services/balance-cache.service';
397
+
398
+ const balanceCache = new BalanceCacheService(redis, balanceModule);
399
+ const balance = await balanceCache.getBalance(caip, pubkey);
400
+ ```
401
+
402
+ ### After
403
+
404
+ ```typescript
405
+ // Using CacheManager
406
+ import { CacheManager } from '@pioneer-platform/pioneer-cache';
407
+
408
+ const cacheManager = new CacheManager({
409
+ redis,
410
+ balanceModule,
411
+ markets,
412
+ enableBalanceCache: true,
413
+ enablePriceCache: true,
414
+ startWorkers: true
415
+ });
416
+
417
+ const balance = await cacheManager.getCache('balance')
418
+ .getBalance(caip, pubkey);
419
+
420
+ // Or direct import
421
+ import { BalanceCache } from '@pioneer-platform/pioneer-cache';
422
+
423
+ const balanceCache = new BalanceCache(redis, balanceModule);
424
+ const balance = await balanceCache.getBalance(caip, pubkey);
425
+ ```
426
+
427
+ ### Migration Checklist
428
+
429
+ - [ ] Install `@pioneer-platform/pioneer-cache`
430
+ - [ ] Replace `BalanceCacheService` imports with `BalanceCache`
431
+ - [ ] Replace `PriceCacheService` imports with `PriceCache`
432
+ - [ ] Update worker initialization to use `startUnifiedWorker()`
433
+ - [ ] Add health check endpoints using `getHealth()`
434
+ - [ ] Test thoroughly in staging environment
435
+ - [ ] Monitor for issues in production
436
+ - [ ] Remove old cache service files after validation
437
+
438
+ ## Performance
439
+
440
+ **Token Reduction:** ~2,000 lines → ~1,800 lines (10% smaller)
441
+ **Code Duplication:** 70-80% duplicate code eliminated
442
+ **Maintenance:** Single BaseCache implementation for all fixes
443
+ **Type Safety:** Full TypeScript generics support
444
+
445
+ ## License
446
+
447
+ MIT
448
+
449
+ ## Contributing
450
+
451
+ See main Pioneer Platform contributing guidelines.
@@ -0,0 +1,75 @@
1
+ import type { CacheConfig, CachedValue, CacheResult, HealthCheckResult, CacheStats } from '../types';
2
+ export declare abstract class BaseCache<T> {
3
+ protected redis: any;
4
+ protected redisQueue: any;
5
+ protected config: CacheConfig;
6
+ protected queueInitialized: boolean;
7
+ protected TAG: string;
8
+ private cachedStats;
9
+ private cachedStatsTimestamp;
10
+ private readonly STATS_CACHE_TTL;
11
+ constructor(redis: any, config: CacheConfig);
12
+ /**
13
+ * Initialize Redis queue for background refresh
14
+ */
15
+ private initializeQueue;
16
+ /**
17
+ * Main cache get method
18
+ * Implements stale-while-revalidate pattern with optional blocking
19
+ */
20
+ get(params: Record<string, any>, waitForFresh?: boolean): Promise<CacheResult<T>>;
21
+ /**
22
+ * Get value from cache
23
+ */
24
+ protected getCached(key: string): Promise<CachedValue<T> | null>;
25
+ /**
26
+ * Update cache with new value
27
+ * FIX #2: Always includes TTL
28
+ */
29
+ updateCache(key: string, value: T, metadata?: Record<string, any>): Promise<void>;
30
+ /**
31
+ * Trigger background refresh job
32
+ * FIX #3: Loud error logging
33
+ * FIX #4: Synchronous fallback for high-priority
34
+ */
35
+ protected triggerAsyncRefresh(params: Record<string, any>, priority?: 'high' | 'normal' | 'low'): void;
36
+ /**
37
+ * Fetch fresh data and update cache
38
+ * FIX #1 & #4: Used for blocking requests and fallback
39
+ */
40
+ fetchFresh(params: Record<string, any>): Promise<T>;
41
+ /**
42
+ * Migrate legacy cache value to new format
43
+ * FIX #2: Includes TTL
44
+ */
45
+ protected migrateLegacyValue(key: string, value: T): void;
46
+ /**
47
+ * Get cache statistics
48
+ * @param forceRefresh - Skip cache and fetch fresh stats (for testing/debugging)
49
+ */
50
+ getCacheStats(forceRefresh?: boolean): Promise<CacheStats>;
51
+ /**
52
+ * FIX #5: Health check
53
+ * @param forceRefresh - Force refresh stats (bypasses 30s cache)
54
+ */
55
+ getHealth(forceRefresh?: boolean): Promise<HealthCheckResult>;
56
+ /**
57
+ * Clear all cache entries (use with caution)
58
+ */
59
+ clearAll(): Promise<number>;
60
+ /**
61
+ * Build Redis key from parameters
62
+ * Each cache type implements its own key structure
63
+ */
64
+ protected abstract buildKey(params: Record<string, any>): string;
65
+ /**
66
+ * Fetch data from source (blockchain, API, etc.)
67
+ * Each cache type implements its own data fetching
68
+ */
69
+ protected abstract fetchFromSource(params: Record<string, any>): Promise<T>;
70
+ /**
71
+ * Get legacy cached value (optional)
72
+ * Each cache type can implement legacy key migration
73
+ */
74
+ protected abstract getLegacyCached(params: Record<string, any>): Promise<T | null>;
75
+ }