forked from Nova-reward/Nova-Rewards
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathcacheService.js
More file actions
164 lines (150 loc) · 5.59 KB
/
Copy pathcacheService.js
File metadata and controls
164 lines (150 loc) · 5.59 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
const logger = require('../lib/logger');
const { client } = require('../lib/redis');
const { getUserByWallet } = require('../db/userRepository');
/**
* Service to manage Redis caching layer.
* Requirements: #358 Caching Layer
*
* ---------------------------------------------------------------------------
* BALANCE MUTATION → CACHE INVALIDATION MAP
* ---------------------------------------------------------------------------
* Every service/method that mutates a user's NOVA balance or off-chain points
* MUST call the corresponding cache invalidation below after a successful mutation.
*
* | # | Mutation Path | Service / Route | Method | Cache Key(s) Invalidated | Invalidation Call |
*|---|---------------|-----------------|--------|---------------------------|-------------------|
* | 1 | Direct Stellar tx submission | walletService | submitTransaction() | tokenBalance:${userId}, balance:${userId} | invalidateBalanceCache(sourceWallet, destWallet) |
* | 2 | Direct reward distribution | routes/rewards.js | POST /distribute | tokenBalance:${userId}, balance:${userId} | invalidateBalanceCache(recipientWallet) |
* | 3 | Reward issuance (BullMQ) | rewardIssuanceService | processRewardIssuance() | tokenBalance:${userId}, balance:${userId} | invalidateBalanceCache(walletAddress) |
* | 4 | Campaign batch distribution | campaignDistributionService | processCampaignDistribution() | tokenBalance:${userId}, balance:${userId} | invalidateBalanceCache(walletAddress) per recipient |
* | 5 | Referral bonus | referralService | processReferralBonus() | balance:${userId} | invalidateUserBalance(referrerId) |
* | 6 | Refund transaction | transactionService | refundTransaction() | balance:${userId} | invalidateUserBalance(userId) for both wallets |
* ---------------------------------------------------------------------------
*/
class CacheService {
constructor() {
this.client = client;
this.DEFAULT_TTL = 3600; // 1 hour default
}
/**
* Get a cached value by key.
*/
async get(key) {
try {
const value = await this.client.get(key);
return value ? JSON.parse(value) : null;
} catch (err) {
logger.error(`[Cache] Error getting key=${key}`, err);
return null;
}
}
/**
* Set a cached value with TTL.
*/
async set(key, value, ttl = this.DEFAULT_TTL) {
try {
await this.client.setEx(key, ttl, JSON.stringify(value));
return true;
} catch (err) {
logger.error(`[Cache] Error setting key=${key}`, err);
return false;
}
}
/**
* Invalidate a specific key.
*/
async del(key) {
try {
await this.client.del(key);
return true;
} catch (err) {
logger.error(`[Cache] Error deleting key=${key}`, err);
return false;
}
}
/**
* Force invalidate by pattern (e.g., 'user:*').
* Warning: keys() can be slow on large datasets, use with caution.
*/
async invalidatePattern(pattern) {
try {
const keys = await this.client.keys(pattern);
if (keys.length > 0) {
await this.client.del(keys);
logger.info(`[Cache] Invalidated ${keys.length} keys matching ${pattern}`);
}
return true;
} catch (err) {
logger.error(`[Cache] Error invalidating pattern=${pattern}`, err);
return false;
}
}
/**
* Invalidate all balance-related cache entries for a wallet address.
* Looks up the userId from the wallet address and deletes both the
* user-based keys (balance:${userId}, tokenBalance:${userId}) and
* the wallet-based keys (wallet:balance:${walletAddress}, wallet:tokenBalance:${walletAddress}).
*
* @param {string} walletAddress - Stellar public key
* @param {number} [userId] - Optional user ID to skip DB lookup
* @returns {Promise<boolean>}
*/
async invalidateBalanceCache(walletAddress, userId) {
if (!walletAddress) return false;
try {
const resolvedUserId = userId || (await getUserByWallet(walletAddress))?.id;
const keysToDelete = [
`wallet:balance:${walletAddress}`,
`wallet:tokenBalance:${walletAddress}`,
];
if (resolvedUserId) {
keysToDelete.push(`balance:${resolvedUserId}`, `tokenBalance:${resolvedUserId}`);
}
await this.client.del(keysToDelete);
logger.info(`[Cache] Invalidated balance cache for wallet=${walletAddress} userId=${resolvedUserId || 'unknown'}`);
return true;
} catch (err) {
logger.error(`[Cache] Error invalidating balance cache for wallet=${walletAddress}`, err);
return false;
}
}
/**
* Invalidate balance cache for a user ID only (no wallet address known).
* @param {number} userId
* @returns {Promise<boolean>}
*/
async invalidateUserBalance(userId) {
if (!userId) return false;
try {
await this.client.del([`balance:${userId}`, `tokenBalance:${userId}`]);
logger.info(`[Cache] Invalidated user balance cache for userId=${userId}`);
return true;
} catch (err) {
logger.error(`[Cache] Error invalidating user balance cache for userId=${userId}`, err);
return false;
}
}
/**
* Track / monitor cache health.
*/
async getHealth() {
try {
const startTime = Date.now();
await this.client.ping();
const latency = Date.now() - startTime;
const info = await this.client.info('memory');
const usedMemory = info.match(/used_memory_human:(.*)/)?.[1] || 'unknown';
return {
status: 'healthy',
latencyMs: latency,
usedMemory,
};
} catch (err) {
return {
status: 'unhealthy',
error: err.message,
};
}
}
}
module.exports = new CacheService();