@pioneer-platform/pioneer-cache 1.7.0 → 1.10.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.
- package/ACTUAL_CACHE_CONFIG_AUDIT.md +319 -0
- package/CHANGELOG.md +24 -0
- package/dist/core/base-cache.js +11 -0
- package/dist/core/cache-manager.d.ts +14 -1
- package/dist/core/cache-manager.js +74 -7
- package/dist/index.d.ts +3 -1
- package/dist/index.js +3 -1
- package/dist/stores/balance-cache.d.ts +2 -0
- package/dist/stores/balance-cache.js +4 -1
- package/dist/stores/zapper-cache.d.ts +163 -0
- package/dist/stores/zapper-cache.js +239 -0
- package/dist/types/index.d.ts +10 -0
- package/package.json +1 -1
- package/src/core/base-cache.ts +12 -0
- package/src/core/cache-manager.ts +88 -9
- package/src/index.ts +4 -1
- package/src/stores/balance-cache.ts +6 -1
- package/src/stores/zapper-cache.ts +368 -0
- package/src/types/index.ts +13 -0
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
# Actual Cache Configuration Audit
|
|
2
|
+
**Date**: 2025-12-27
|
|
3
|
+
**Location**: `/modules/pioneer/pioneer-cache/src/stores/`
|
|
4
|
+
|
|
5
|
+
## Cache Configuration Reality Check
|
|
6
|
+
|
|
7
|
+
### Balance Cache (CRITICAL - Your Issue)
|
|
8
|
+
**File**: `balance-cache.ts:77-79`
|
|
9
|
+
|
|
10
|
+
```typescript
|
|
11
|
+
ttl: 0, // Ignored when enableTTL: false
|
|
12
|
+
staleThreshold: 5 * 60 * 1000, // 5 minutes - triggers background refresh
|
|
13
|
+
enableTTL: false, // NEVER EXPIRE - data persists forever
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
**Reality**:
|
|
17
|
+
- ✅ **Stale threshold**: 5 minutes (triggers background refresh)
|
|
18
|
+
- ❌ **TTL**: DISABLED (no hard expiry)
|
|
19
|
+
- ⚠️ **Risk**: If background refresh fails, data becomes **infinitely stale**
|
|
20
|
+
|
|
21
|
+
**Your Case**:
|
|
22
|
+
- Balance is 22.5 hours old (1,350 minutes)
|
|
23
|
+
- Should have refreshed 270 times already (every 5 minutes)
|
|
24
|
+
- Background refresh **completely broken**
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
### Price Cache
|
|
29
|
+
**File**: `price-cache.ts:32-34`
|
|
30
|
+
|
|
31
|
+
```typescript
|
|
32
|
+
ttl: 0, // Ignored when enableTTL: false
|
|
33
|
+
staleThreshold: 30 * 60 * 1000, // 30 minutes - triggers background refresh
|
|
34
|
+
enableTTL: false, // NEVER EXPIRE - data persists forever
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
**Reality**:
|
|
38
|
+
- ✅ **Stale threshold**: 30 minutes
|
|
39
|
+
- ❌ **TTL**: DISABLED
|
|
40
|
+
- ⚠️ **Risk**: Stale prices can persist indefinitely
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
### Portfolio Cache
|
|
45
|
+
**File**: `portfolio-cache.ts:57-59`
|
|
46
|
+
|
|
47
|
+
```typescript
|
|
48
|
+
ttl: 0, // Ignored when enableTTL: false
|
|
49
|
+
staleThreshold: 5 * 60 * 1000, // 5 minutes - triggers background refresh
|
|
50
|
+
enableTTL: false, // NEVER EXPIRE - data persists forever
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
**Reality**:
|
|
54
|
+
- ✅ **Stale threshold**: 5 minutes
|
|
55
|
+
- ❌ **TTL**: DISABLED
|
|
56
|
+
- ⚠️ **Risk**: Same as balance cache
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
### Staking Cache (ONLY ONE WITH TTL!)
|
|
61
|
+
**File**: `staking-cache.ts:59-61`
|
|
62
|
+
|
|
63
|
+
```typescript
|
|
64
|
+
ttl: 5 * 60 * 1000, // 5 minutes - staking changes slowly
|
|
65
|
+
staleThreshold: 2 * 60 * 1000, // Refresh after 2 minutes
|
|
66
|
+
enableTTL: true, // Enable expiration ✅
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
**Reality**:
|
|
70
|
+
- ✅ **Stale threshold**: 2 minutes (soft refresh)
|
|
71
|
+
- ✅ **TTL**: 5 minutes (hard expiry) ✅
|
|
72
|
+
- ✅ **Safety net**: Even if background refresh fails, data expires in 5 minutes
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
### Zapper Cache (Most Expensive)
|
|
77
|
+
**File**: `zapper-cache.ts:123-125`
|
|
78
|
+
|
|
79
|
+
```typescript
|
|
80
|
+
ttl: 0, // Ignored when enableTTL: false
|
|
81
|
+
staleThreshold: 24 * 60 * 60 * 1000, // 24 hours - ONLY refresh after this
|
|
82
|
+
enableTTL: false, // NEVER EXPIRE - data persists forever
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
**Reality**:
|
|
86
|
+
- ✅ **Stale threshold**: 24 hours (expensive API)
|
|
87
|
+
- ❌ **TTL**: DISABLED
|
|
88
|
+
- ⚠️ **Risk**: If refresh fails, could be stale for days/weeks
|
|
89
|
+
|
|
90
|
+
---
|
|
91
|
+
|
|
92
|
+
## The Pattern
|
|
93
|
+
|
|
94
|
+
**All caches except Staking**:
|
|
95
|
+
```
|
|
96
|
+
enableTTL: false → No hard expiry, relies 100% on background refresh
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
**Design Intent**: "Stale-while-revalidate" pattern
|
|
100
|
+
1. Return stale data instantly (fast UX)
|
|
101
|
+
2. Trigger background refresh if older than threshold
|
|
102
|
+
3. Cache persists forever for instant loads
|
|
103
|
+
|
|
104
|
+
**Critical Flaw**: **NO SAFETY NET** if background refresh breaks!
|
|
105
|
+
|
|
106
|
+
---
|
|
107
|
+
|
|
108
|
+
## Why Background Refresh is Broken
|
|
109
|
+
|
|
110
|
+
### The Queue System
|
|
111
|
+
**File**: `base-cache.ts:334-347`
|
|
112
|
+
|
|
113
|
+
```typescript
|
|
114
|
+
protected triggerAsyncRefresh(params, priority = 'normal'): void {
|
|
115
|
+
if (!this.config.enableQueue) {
|
|
116
|
+
return; // Queue disabled
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (!this.queueInitialized || !this.redisQueue) {
|
|
120
|
+
log.error(tag, `❌ QUEUE NOT INITIALIZED! Cannot refresh`);
|
|
121
|
+
log.error(tag, `Background refresh is BROKEN - cache will NOT update!`);
|
|
122
|
+
return; // ← SILENT FAILURE (logs only, no alerts)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Queue the refresh job...
|
|
126
|
+
}
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
**Possible Failures**:
|
|
130
|
+
1. ❌ Queue never initialized at startup
|
|
131
|
+
2. ❌ Workers never started
|
|
132
|
+
3. ❌ Workers started but crashed
|
|
133
|
+
4. ❌ Jobs queued but never processed
|
|
134
|
+
5. ❌ Redis connection issues
|
|
135
|
+
|
|
136
|
+
**Current Visibility**: ZERO (only logs, no monitoring)
|
|
137
|
+
|
|
138
|
+
---
|
|
139
|
+
|
|
140
|
+
## Recommended TTL Values (Safety Net)
|
|
141
|
+
|
|
142
|
+
### Balance Cache
|
|
143
|
+
```typescript
|
|
144
|
+
ttl: 15 * 60 * 1000, // 15 minutes - reasonable balance freshness
|
|
145
|
+
staleThreshold: 5 * 60 * 1000, // 5 minutes - trigger soft refresh
|
|
146
|
+
enableTTL: true, // Enable safety net ✅
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
**Reasoning**:
|
|
150
|
+
- Background refresh should hit at 5 minutes
|
|
151
|
+
- TTL expires at 15 minutes if refresh fails
|
|
152
|
+
- Max staleness: 15 minutes (vs current: infinite)
|
|
153
|
+
|
|
154
|
+
### Price Cache
|
|
155
|
+
```typescript
|
|
156
|
+
ttl: 60 * 60 * 1000, // 60 minutes - prices don't change that fast
|
|
157
|
+
staleThreshold: 30 * 60 * 1000, // 30 minutes - trigger soft refresh
|
|
158
|
+
enableTTL: true, // Enable safety net ✅
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
**Reasoning**:
|
|
162
|
+
- Crypto prices don't change drastically in 1 hour
|
|
163
|
+
- Background refresh at 30 minutes
|
|
164
|
+
- Max staleness: 60 minutes
|
|
165
|
+
|
|
166
|
+
### Portfolio Cache
|
|
167
|
+
```typescript
|
|
168
|
+
ttl: 15 * 60 * 1000, // 15 minutes - same as balance
|
|
169
|
+
staleThreshold: 5 * 60 * 1000, // 5 minutes - trigger soft refresh
|
|
170
|
+
enableTTL: true, // Enable safety net ✅
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
### Zapper Cache
|
|
174
|
+
```typescript
|
|
175
|
+
ttl: 48 * 60 * 60 * 1000, // 48 hours - expensive API
|
|
176
|
+
staleThreshold: 24 * 60 * 60 * 1000, // 24 hours - trigger soft refresh
|
|
177
|
+
enableTTL: true, // Enable safety net ✅
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
**Reasoning**:
|
|
181
|
+
- Expensive API, minimize calls
|
|
182
|
+
- Background refresh at 24 hours
|
|
183
|
+
- Max staleness: 48 hours (vs current: infinite)
|
|
184
|
+
|
|
185
|
+
---
|
|
186
|
+
|
|
187
|
+
## Critical Discord Alerts Needed
|
|
188
|
+
|
|
189
|
+
### 1. Queue Initialization Failure (CRITICAL)
|
|
190
|
+
```typescript
|
|
191
|
+
if (!this.queueInitialized) {
|
|
192
|
+
await discord.alert({
|
|
193
|
+
severity: 'critical',
|
|
194
|
+
title: '🚨 Cache Queue Failed to Initialize',
|
|
195
|
+
message: `Cache: ${this.config.name}\nBackground refresh BROKEN!`
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
### 2. High Staleness Rate (WARNING)
|
|
201
|
+
```typescript
|
|
202
|
+
// Monitor every 5 minutes
|
|
203
|
+
if (stats.stalenessRate > 50) {
|
|
204
|
+
await discord.alert({
|
|
205
|
+
severity: 'warning',
|
|
206
|
+
title: '⚠️ High Cache Staleness',
|
|
207
|
+
message: `${this.config.name}: ${stats.stalenessRate}% stale\nWorkers may be failing`
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
### 3. Excessive Cache Age (CRITICAL)
|
|
213
|
+
```typescript
|
|
214
|
+
// Check max age in cache
|
|
215
|
+
const maxAge = Math.max(...Object.values(stats.ages));
|
|
216
|
+
const maxAgeHours = maxAge / (60 * 60 * 1000);
|
|
217
|
+
|
|
218
|
+
if (maxAgeHours > 1) { // More than 1 hour old
|
|
219
|
+
await discord.alert({
|
|
220
|
+
severity: 'critical',
|
|
221
|
+
title: '🚨 Extremely Stale Cache Data',
|
|
222
|
+
message: `${this.config.name}: Data ${maxAgeHours.toFixed(1)}h old!\nRefresh completely broken`
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
### 4. Worker Crash Detection
|
|
228
|
+
```typescript
|
|
229
|
+
// On worker exit
|
|
230
|
+
process.on('exit', async () => {
|
|
231
|
+
await discord.alert({
|
|
232
|
+
severity: 'critical',
|
|
233
|
+
title: '🚨 Cache Worker Crashed',
|
|
234
|
+
message: 'Background refresh workers stopped unexpectedly'
|
|
235
|
+
});
|
|
236
|
+
});
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
---
|
|
240
|
+
|
|
241
|
+
## Action Items
|
|
242
|
+
|
|
243
|
+
### Immediate (Today)
|
|
244
|
+
1. ✅ Add TTL to balance cache (15 min)
|
|
245
|
+
2. ✅ Add TTL to portfolio cache (15 min)
|
|
246
|
+
3. ✅ Add Discord alerts to queue failures
|
|
247
|
+
4. ✅ Clear your stale Bitcoin balance
|
|
248
|
+
|
|
249
|
+
### Short-term (This Week)
|
|
250
|
+
1. Diagnose why workers aren't running
|
|
251
|
+
2. Add health monitoring (check staleness every 5 min)
|
|
252
|
+
3. Add startup validation (fail fast if queue doesn't init)
|
|
253
|
+
4. Test with cleared cache to verify refresh works
|
|
254
|
+
|
|
255
|
+
### Long-term (Next Sprint)
|
|
256
|
+
1. Implement comprehensive cache monitoring dashboard
|
|
257
|
+
2. Add Prometheus metrics for cache health
|
|
258
|
+
3. Document cache incident runbook
|
|
259
|
+
4. Consider Redis TTL fallback for all caches
|
|
260
|
+
|
|
261
|
+
---
|
|
262
|
+
|
|
263
|
+
## Testing the Fix
|
|
264
|
+
|
|
265
|
+
### Step 1: Clear Stale Cache
|
|
266
|
+
```bash
|
|
267
|
+
redis-cli DEL "balance_v2:bip122:000000000019d6689c085ae165831e93/slip44:0:d66c81ddb395633630fbd6949e5cb0b4"
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
### Step 2: Fetch Fresh Balance
|
|
271
|
+
```bash
|
|
272
|
+
curl -X POST http://localhost:9001/api/balances \
|
|
273
|
+
-H "Content-Type: application/json" \
|
|
274
|
+
-d '{
|
|
275
|
+
"pubkeys": [{
|
|
276
|
+
"caip": "bip122:000000000019d6689c085ae165831e93/slip44:0",
|
|
277
|
+
"pubkey": "zpub6rPBGeYJtMkALyZgM9AQ8cYQnFprAfetXQeDrSjWpCEPnhfC8tpTDoVzdTrLKg7box2zncYFptaWPCaR8wW4FCiULBU6y77RtqFaGjs9mN6"
|
|
278
|
+
}]
|
|
279
|
+
}'
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
### Step 3: Verify Balance Correct
|
|
283
|
+
```json
|
|
284
|
+
{
|
|
285
|
+
"balance": "0.00002285", // ✅ Should be correct now
|
|
286
|
+
"valueUsd": "2.00",
|
|
287
|
+
"fetchedAt": 1735346400000 // ✅ Should be recent
|
|
288
|
+
}
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
### Step 4: Wait 16 Minutes
|
|
292
|
+
- At 5min: Background refresh should trigger
|
|
293
|
+
- At 15min: If refresh failed, TTL expires and forces refetch
|
|
294
|
+
|
|
295
|
+
### Step 5: Check Cache Age
|
|
296
|
+
```bash
|
|
297
|
+
redis-cli GET "balance_v2:bip122:000000000019d6689c085ae165831e93/slip44:0:d66c81ddb395633630fbd6949e5cb0b4"
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
Should be <15 minutes old, or not exist (expired via TTL).
|
|
301
|
+
|
|
302
|
+
---
|
|
303
|
+
|
|
304
|
+
## Summary
|
|
305
|
+
|
|
306
|
+
**Current State**:
|
|
307
|
+
- Balance cache: NO TTL, relies 100% on background refresh
|
|
308
|
+
- Background refresh: COMPLETELY BROKEN (22.5 hours stale)
|
|
309
|
+
- Alerts: NONE (silent failures)
|
|
310
|
+
|
|
311
|
+
**Proposed State**:
|
|
312
|
+
- Balance cache: 15-minute TTL as safety net
|
|
313
|
+
- Background refresh: 5-minute soft refresh
|
|
314
|
+
- Alerts: Discord alerts on all failures
|
|
315
|
+
- Monitoring: Health checks every 5 minutes
|
|
316
|
+
|
|
317
|
+
**Max Staleness**:
|
|
318
|
+
- Current: **INFINITE** (if refresh fails)
|
|
319
|
+
- Proposed: **15 minutes** (TTL safety net)
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,29 @@
|
|
|
1
1
|
# @pioneer-platform/pioneer-cache
|
|
2
2
|
|
|
3
|
+
## 1.10.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- chore: Merge pull request #10 from coinmastersguild/feature/fix-blockbook-websocket-subscriptions
|
|
8
|
+
|
|
9
|
+
## 1.9.0
|
|
10
|
+
|
|
11
|
+
### Minor Changes
|
|
12
|
+
|
|
13
|
+
- Merge pull request #10 from coinmastersguild/feature/fix-blockbook-websocket-subscriptions
|
|
14
|
+
|
|
15
|
+
## 1.8.1
|
|
16
|
+
|
|
17
|
+
### Patch Changes
|
|
18
|
+
|
|
19
|
+
- fix: Filter xpubs from Blockbook WebSocket subscriptions
|
|
20
|
+
|
|
21
|
+
## 1.8.0
|
|
22
|
+
|
|
23
|
+
### Minor Changes
|
|
24
|
+
|
|
25
|
+
- chore: chore: chore: chore: chore: chore: feat(pioneer): implement end-to-end Solana transaction signing
|
|
26
|
+
|
|
3
27
|
## 1.7.0
|
|
4
28
|
|
|
5
29
|
### Minor Changes
|
package/dist/core/base-cache.js
CHANGED
|
@@ -293,6 +293,17 @@ class BaseCache {
|
|
|
293
293
|
const key = this.buildKey(params);
|
|
294
294
|
log.error(tag, `❌ QUEUE NOT INITIALIZED! Cannot refresh ${key}`);
|
|
295
295
|
log.error(tag, `Background refresh is BROKEN - cache will NOT update!`);
|
|
296
|
+
// Send Discord alert for queue failure (if handler configured)
|
|
297
|
+
if (this.config.alertHandler?.onQueueFailure) {
|
|
298
|
+
this.config.alertHandler.onQueueFailure(this.config.name, {
|
|
299
|
+
key,
|
|
300
|
+
queueInitialized: this.queueInitialized,
|
|
301
|
+
redisQueue: !!this.redisQueue,
|
|
302
|
+
enableQueue: this.config.enableQueue
|
|
303
|
+
}).catch(err => {
|
|
304
|
+
log.error(tag, 'Failed to send queue failure alert:', err);
|
|
305
|
+
});
|
|
306
|
+
}
|
|
296
307
|
// FIX #4: Synchronous fallback for high-priority (only if useSyncFallback is enabled)
|
|
297
308
|
// Default: use sync fallback only for blocking caches (blockOnMiss=true)
|
|
298
309
|
const shouldUseSyncFallback = this.config.useSyncFallback !== undefined
|
|
@@ -3,7 +3,9 @@ import { PriceCache } from '../stores/price-cache';
|
|
|
3
3
|
import { PortfolioCache } from '../stores/portfolio-cache';
|
|
4
4
|
import { TransactionCache } from '../stores/transaction-cache';
|
|
5
5
|
import { StakingCache } from '../stores/staking-cache';
|
|
6
|
+
import { ZapperCache } from '../stores/zapper-cache';
|
|
6
7
|
import type { HealthCheckResult } from '../types';
|
|
8
|
+
import type { CacheAlertHandler } from '../types';
|
|
7
9
|
/**
|
|
8
10
|
* Configuration for CacheManager
|
|
9
11
|
*/
|
|
@@ -13,11 +15,14 @@ export interface CacheManagerConfig {
|
|
|
13
15
|
balanceModule?: any;
|
|
14
16
|
markets?: any;
|
|
15
17
|
networkModules?: Map<string, any>;
|
|
18
|
+
zapperClient?: any;
|
|
19
|
+
alertHandler?: CacheAlertHandler;
|
|
16
20
|
enableBalanceCache?: boolean;
|
|
17
21
|
enablePriceCache?: boolean;
|
|
18
22
|
enablePortfolioCache?: boolean;
|
|
19
23
|
enableTransactionCache?: boolean;
|
|
20
24
|
enableStakingCache?: boolean;
|
|
25
|
+
enableZapperCache?: boolean;
|
|
21
26
|
startWorkers?: boolean;
|
|
22
27
|
}
|
|
23
28
|
/**
|
|
@@ -31,8 +36,14 @@ export declare class CacheManager {
|
|
|
31
36
|
private portfolioCache?;
|
|
32
37
|
private transactionCache?;
|
|
33
38
|
private stakingCache?;
|
|
39
|
+
private zapperCache?;
|
|
34
40
|
private workers;
|
|
35
41
|
constructor(config: CacheManagerConfig);
|
|
42
|
+
/**
|
|
43
|
+
* Purge all pending jobs from the cache refresh queue
|
|
44
|
+
* Used in local dev to clear stale jobs on startup
|
|
45
|
+
*/
|
|
46
|
+
purgeQueue(queueName?: string): Promise<void>;
|
|
36
47
|
/**
|
|
37
48
|
* Start background refresh workers for all caches
|
|
38
49
|
*/
|
|
@@ -57,11 +68,12 @@ export declare class CacheManager {
|
|
|
57
68
|
portfolio: PortfolioCache | undefined;
|
|
58
69
|
transaction: TransactionCache | undefined;
|
|
59
70
|
staking: StakingCache | undefined;
|
|
71
|
+
zapper: ZapperCache | undefined;
|
|
60
72
|
};
|
|
61
73
|
/**
|
|
62
74
|
* Get specific cache by name
|
|
63
75
|
*/
|
|
64
|
-
getCache(name: 'balance' | 'price' | 'portfolio' | 'transaction' | 'staking'): BalanceCache | PriceCache | PortfolioCache | TransactionCache | StakingCache | undefined;
|
|
76
|
+
getCache(name: 'balance' | 'price' | 'portfolio' | 'transaction' | 'staking' | 'zapper'): BalanceCache | PriceCache | PortfolioCache | TransactionCache | StakingCache | ZapperCache | undefined;
|
|
65
77
|
/**
|
|
66
78
|
* Clear all caches (use with caution!)
|
|
67
79
|
*/
|
|
@@ -71,5 +83,6 @@ export declare class CacheManager {
|
|
|
71
83
|
portfolio?: number;
|
|
72
84
|
transaction?: number;
|
|
73
85
|
staking?: number;
|
|
86
|
+
zapper?: number;
|
|
74
87
|
}>;
|
|
75
88
|
}
|
|
@@ -12,6 +12,7 @@ const price_cache_1 = require("../stores/price-cache");
|
|
|
12
12
|
const portfolio_cache_1 = require("../stores/portfolio-cache");
|
|
13
13
|
const transaction_cache_1 = require("../stores/transaction-cache");
|
|
14
14
|
const staking_cache_1 = require("../stores/staking-cache");
|
|
15
|
+
const zapper_cache_1 = require("../stores/zapper-cache");
|
|
15
16
|
const refresh_worker_1 = require("../workers/refresh-worker");
|
|
16
17
|
const log = require('@pioneer-platform/loggerdog')();
|
|
17
18
|
const TAG = ' | CacheManager | ';
|
|
@@ -25,17 +26,23 @@ class CacheManager {
|
|
|
25
26
|
this.redisQueue = config.redisQueue || config.redis; // Fallback to main redis if not provided
|
|
26
27
|
// Initialize Balance Cache
|
|
27
28
|
if (config.enableBalanceCache !== false && config.balanceModule) {
|
|
28
|
-
this.balanceCache = new balance_cache_1.BalanceCache(this.redis, config.balanceModule
|
|
29
|
+
this.balanceCache = new balance_cache_1.BalanceCache(this.redis, config.balanceModule, {
|
|
30
|
+
alertHandler: config.alertHandler
|
|
31
|
+
});
|
|
29
32
|
log.info(TAG, '✅ Balance cache initialized');
|
|
30
33
|
}
|
|
31
34
|
// Initialize Price Cache
|
|
32
35
|
if (config.enablePriceCache !== false && config.markets) {
|
|
33
|
-
this.priceCache = new price_cache_1.PriceCache(this.redis, config.markets
|
|
36
|
+
this.priceCache = new price_cache_1.PriceCache(this.redis, config.markets, {
|
|
37
|
+
alertHandler: config.alertHandler
|
|
38
|
+
});
|
|
34
39
|
log.info(TAG, '✅ Price cache initialized');
|
|
35
40
|
}
|
|
36
41
|
// Initialize Portfolio Cache
|
|
37
42
|
if (config.enablePortfolioCache !== false && config.balanceModule && config.markets) {
|
|
38
|
-
this.portfolioCache = new portfolio_cache_1.PortfolioCache(this.redis, config.balanceModule, config.markets
|
|
43
|
+
this.portfolioCache = new portfolio_cache_1.PortfolioCache(this.redis, config.balanceModule, config.markets, {
|
|
44
|
+
alertHandler: config.alertHandler
|
|
45
|
+
});
|
|
39
46
|
log.info(TAG, '✅ Portfolio cache initialized');
|
|
40
47
|
}
|
|
41
48
|
// Initialize Transaction Cache
|
|
@@ -45,17 +52,57 @@ class CacheManager {
|
|
|
45
52
|
}
|
|
46
53
|
// Initialize Staking Cache (with markets module for price enrichment)
|
|
47
54
|
if (config.enableStakingCache !== false && config.networkModules && config.networkModules.size > 0) {
|
|
48
|
-
this.stakingCache = new staking_cache_1.StakingCache(this.redis, config.networkModules, config.markets
|
|
55
|
+
this.stakingCache = new staking_cache_1.StakingCache(this.redis, config.networkModules, config.markets, {
|
|
56
|
+
alertHandler: config.alertHandler
|
|
57
|
+
});
|
|
49
58
|
log.info(TAG, '✅ Staking cache initialized');
|
|
50
59
|
}
|
|
60
|
+
// Initialize Zapper Cache
|
|
61
|
+
if (config.enableZapperCache !== false && config.zapperClient) {
|
|
62
|
+
this.zapperCache = new zapper_cache_1.ZapperCache(this.redis, config.zapperClient, {
|
|
63
|
+
alertHandler: config.alertHandler
|
|
64
|
+
});
|
|
65
|
+
log.info(TAG, '✅ Zapper cache initialized');
|
|
66
|
+
}
|
|
51
67
|
// Auto-start workers if requested
|
|
52
68
|
if (config.startWorkers) {
|
|
53
|
-
setImmediate(() => {
|
|
54
|
-
|
|
69
|
+
setImmediate(async () => {
|
|
70
|
+
try {
|
|
71
|
+
await this.startWorkers();
|
|
72
|
+
}
|
|
73
|
+
catch (error) {
|
|
74
|
+
log.error(TAG, '❌ CRITICAL: Failed to start cache workers:', error);
|
|
75
|
+
// Send alert if handler configured
|
|
76
|
+
if (config.alertHandler?.onQueueFailure) {
|
|
77
|
+
await config.alertHandler.onQueueFailure('CacheManager', {
|
|
78
|
+
error: error instanceof Error ? error.message : String(error),
|
|
79
|
+
stage: 'worker_startup'
|
|
80
|
+
}).catch(alertErr => {
|
|
81
|
+
log.error(TAG, 'Failed to send worker startup failure alert:', alertErr);
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
}
|
|
55
85
|
});
|
|
56
86
|
}
|
|
57
87
|
log.info(TAG, '✅ CacheManager initialized');
|
|
58
88
|
}
|
|
89
|
+
/**
|
|
90
|
+
* Purge all pending jobs from the cache refresh queue
|
|
91
|
+
* Used in local dev to clear stale jobs on startup
|
|
92
|
+
*/
|
|
93
|
+
async purgeQueue(queueName = 'cache-refresh') {
|
|
94
|
+
const tag = TAG + 'purgeQueue | ';
|
|
95
|
+
try {
|
|
96
|
+
log.info(tag, `Purging queue: ${queueName}...`);
|
|
97
|
+
// Delete the entire queue (Redis list)
|
|
98
|
+
const deleted = await this.redis.del(queueName);
|
|
99
|
+
log.info(tag, `✅ Queue purged: ${queueName} (${deleted} keys deleted)`);
|
|
100
|
+
}
|
|
101
|
+
catch (error) {
|
|
102
|
+
log.error(tag, `❌ Failed to purge queue ${queueName}:`, error);
|
|
103
|
+
throw error;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
59
106
|
/**
|
|
60
107
|
* Start background refresh workers for all caches
|
|
61
108
|
*/
|
|
@@ -77,6 +124,9 @@ class CacheManager {
|
|
|
77
124
|
if (this.stakingCache) {
|
|
78
125
|
cacheRegistry.set('staking', this.stakingCache);
|
|
79
126
|
}
|
|
127
|
+
if (this.zapperCache) {
|
|
128
|
+
cacheRegistry.set('zapper', this.zapperCache);
|
|
129
|
+
}
|
|
80
130
|
// Start unified worker if we have any caches with queues
|
|
81
131
|
if (cacheRegistry.size > 0) {
|
|
82
132
|
const worker = await (0, refresh_worker_1.startUnifiedWorker)(this.redisQueue, // Use dedicated queue client for blocking operations
|
|
@@ -175,6 +225,17 @@ class CacheManager {
|
|
|
175
225
|
warnings.push(...stakingHealth.warnings.map(w => `Staking: ${w}`));
|
|
176
226
|
}
|
|
177
227
|
}
|
|
228
|
+
// Check zapper cache
|
|
229
|
+
if (this.zapperCache) {
|
|
230
|
+
const zapperHealth = await this.zapperCache.getHealth(forceRefresh);
|
|
231
|
+
checks.zapper = zapperHealth;
|
|
232
|
+
if (zapperHealth.status === 'unhealthy') {
|
|
233
|
+
issues.push(...zapperHealth.issues.map(i => `Zapper: ${i}`));
|
|
234
|
+
}
|
|
235
|
+
else if (zapperHealth.status === 'degraded') {
|
|
236
|
+
warnings.push(...zapperHealth.warnings.map(w => `Zapper: ${w}`));
|
|
237
|
+
}
|
|
238
|
+
}
|
|
178
239
|
// Check workers
|
|
179
240
|
for (const worker of this.workers) {
|
|
180
241
|
const workerStats = await worker.getStats();
|
|
@@ -236,7 +297,8 @@ class CacheManager {
|
|
|
236
297
|
price: this.priceCache,
|
|
237
298
|
portfolio: this.portfolioCache,
|
|
238
299
|
transaction: this.transactionCache,
|
|
239
|
-
staking: this.stakingCache
|
|
300
|
+
staking: this.stakingCache,
|
|
301
|
+
zapper: this.zapperCache
|
|
240
302
|
};
|
|
241
303
|
}
|
|
242
304
|
/**
|
|
@@ -254,6 +316,8 @@ class CacheManager {
|
|
|
254
316
|
return this.transactionCache;
|
|
255
317
|
case 'staking':
|
|
256
318
|
return this.stakingCache;
|
|
319
|
+
case 'zapper':
|
|
320
|
+
return this.zapperCache;
|
|
257
321
|
default:
|
|
258
322
|
return undefined;
|
|
259
323
|
}
|
|
@@ -280,6 +344,9 @@ class CacheManager {
|
|
|
280
344
|
if (this.stakingCache) {
|
|
281
345
|
result.staking = await this.stakingCache.clearAll();
|
|
282
346
|
}
|
|
347
|
+
if (this.zapperCache) {
|
|
348
|
+
result.zapper = await this.zapperCache.clearAll();
|
|
349
|
+
}
|
|
283
350
|
log.info(tag, 'Cleared all caches:', result);
|
|
284
351
|
return result;
|
|
285
352
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -5,11 +5,13 @@ export { PriceCache } from './stores/price-cache';
|
|
|
5
5
|
export { PortfolioCache } from './stores/portfolio-cache';
|
|
6
6
|
export { TransactionCache } from './stores/transaction-cache';
|
|
7
7
|
export { StakingCache } from './stores/staking-cache';
|
|
8
|
+
export { ZapperCache } from './stores/zapper-cache';
|
|
8
9
|
export { RefreshWorker, startUnifiedWorker } from './workers/refresh-worker';
|
|
9
|
-
export type { CacheConfig, CachedValue, CacheResult, RefreshJob, HealthCheckResult, CacheStats } from './types';
|
|
10
|
+
export type { CacheConfig, CachedValue, CacheResult, RefreshJob, HealthCheckResult, CacheStats, CacheAlertHandler } from './types';
|
|
10
11
|
export type { BalanceData } from './stores/balance-cache';
|
|
11
12
|
export type { PriceData } from './stores/price-cache';
|
|
12
13
|
export type { PortfolioData, ChartData } from './stores/portfolio-cache';
|
|
13
14
|
export type { StakingPosition } from './stores/staking-cache';
|
|
15
|
+
export type { ZapperPortfolioData, ZapperNFTData, ZapperAppsData, ZapperTotalsData } from './stores/zapper-cache';
|
|
14
16
|
export type { CacheManagerConfig } from './core/cache-manager';
|
|
15
17
|
export type { WorkerConfig } from './workers/refresh-worker';
|
package/dist/index.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
Provides stale-while-revalidate caching with TTL, background refresh, and health monitoring.
|
|
7
7
|
*/
|
|
8
8
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
-
exports.startUnifiedWorker = exports.RefreshWorker = exports.StakingCache = exports.TransactionCache = exports.PortfolioCache = exports.PriceCache = exports.BalanceCache = exports.CacheManager = exports.BaseCache = void 0;
|
|
9
|
+
exports.startUnifiedWorker = exports.RefreshWorker = exports.ZapperCache = exports.StakingCache = exports.TransactionCache = exports.PortfolioCache = exports.PriceCache = exports.BalanceCache = exports.CacheManager = exports.BaseCache = void 0;
|
|
10
10
|
// Core exports
|
|
11
11
|
var base_cache_1 = require("./core/base-cache");
|
|
12
12
|
Object.defineProperty(exports, "BaseCache", { enumerable: true, get: function () { return base_cache_1.BaseCache; } });
|
|
@@ -23,6 +23,8 @@ var transaction_cache_1 = require("./stores/transaction-cache");
|
|
|
23
23
|
Object.defineProperty(exports, "TransactionCache", { enumerable: true, get: function () { return transaction_cache_1.TransactionCache; } });
|
|
24
24
|
var staking_cache_1 = require("./stores/staking-cache");
|
|
25
25
|
Object.defineProperty(exports, "StakingCache", { enumerable: true, get: function () { return staking_cache_1.StakingCache; } });
|
|
26
|
+
var zapper_cache_1 = require("./stores/zapper-cache");
|
|
27
|
+
Object.defineProperty(exports, "ZapperCache", { enumerable: true, get: function () { return zapper_cache_1.ZapperCache; } });
|
|
26
28
|
// Worker exports
|
|
27
29
|
var refresh_worker_1 = require("./workers/refresh-worker");
|
|
28
30
|
Object.defineProperty(exports, "RefreshWorker", { enumerable: true, get: function () { return refresh_worker_1.RefreshWorker; } });
|
|
@@ -169,7 +169,10 @@ class BalanceCache extends base_cache_1.BaseCache {
|
|
|
169
169
|
precision: assetInfo.decimals, // Set precision to decimals for compatibility
|
|
170
170
|
fetchedAt: now,
|
|
171
171
|
fetchedAtISO: new Date(now).toISOString(),
|
|
172
|
-
dataSource
|
|
172
|
+
dataSource,
|
|
173
|
+
// CRITICAL: Pass through derived addresses and tokens from balance module for xpub subscription
|
|
174
|
+
derivedAddresses: balanceInfo.derivedAddresses,
|
|
175
|
+
tokens: balanceInfo.tokens
|
|
173
176
|
};
|
|
174
177
|
}
|
|
175
178
|
catch (error) {
|