@mulingai-npm/redis 3.30.0 → 3.30.2
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/dist/managers/credit-session-manager.d.ts +112 -0
- package/dist/managers/credit-session-manager.js +211 -0
- package/dist/managers/plan-entitlements-manager.d.ts +66 -0
- package/dist/managers/plan-entitlements-manager.js +98 -0
- package/dist/redis-client.d.ts +1 -0
- package/dist/redis-client.js +3 -0
- package/package.json +1 -1
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { RedisClient } from '../redis-client';
|
|
2
|
+
/**
|
|
3
|
+
* Credit session data stored in Redis during streaming
|
|
4
|
+
*/
|
|
5
|
+
export interface CreditSessionData {
|
|
6
|
+
sessionId: string;
|
|
7
|
+
userId: string;
|
|
8
|
+
roomId: number;
|
|
9
|
+
serviceCode: string;
|
|
10
|
+
sourceLanguage: string;
|
|
11
|
+
targetLanguages: string[];
|
|
12
|
+
keywordsEnabled: boolean;
|
|
13
|
+
speculativeEnabled: boolean;
|
|
14
|
+
creditsPerMinute: number;
|
|
15
|
+
initialBalance: number;
|
|
16
|
+
creditsUsed: number;
|
|
17
|
+
totalDurationMs: number;
|
|
18
|
+
multiplierExtraLanguage: number;
|
|
19
|
+
multiplierKeywords: number;
|
|
20
|
+
multiplierSpeculative: number;
|
|
21
|
+
sessionStart: number;
|
|
22
|
+
lastUpdated: number;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Credit Session Manager
|
|
26
|
+
*
|
|
27
|
+
* Tracks credit usage during streaming sessions in Redis (USAGE_DATA db).
|
|
28
|
+
* Similar to UsageDataManager but tracks credits instead of milliseconds.
|
|
29
|
+
*
|
|
30
|
+
* Key structure:
|
|
31
|
+
* - credit:session:{sessionId} - Hash with session credit data
|
|
32
|
+
* - credit:session:user:{userId} - String with current sessionId (for lookup)
|
|
33
|
+
* - credit:pending:users - Set of user IDs with active credit sessions
|
|
34
|
+
*
|
|
35
|
+
* Flow:
|
|
36
|
+
* 1. Session starts: credit-service creates session in Redis
|
|
37
|
+
* 2. Every 5 seconds: pipeline-service increments creditsUsed
|
|
38
|
+
* 3. Session ends: credit-service flushes to DB, clears Redis
|
|
39
|
+
*
|
|
40
|
+
* Crash recovery:
|
|
41
|
+
* - If session not closed properly, orphan cleanup job will flush to DB
|
|
42
|
+
* - TTL: 24 hours (safety net)
|
|
43
|
+
*
|
|
44
|
+
* @example
|
|
45
|
+
* ```typescript
|
|
46
|
+
* const manager = new CreditSessionManager(UsageDataRedisClient);
|
|
47
|
+
* await manager.startSession(sessionData);
|
|
48
|
+
* await manager.recordUsage(sessionId, creditsDelta, durationDeltaMs);
|
|
49
|
+
* const session = await manager.getSession(sessionId);
|
|
50
|
+
* await manager.endSession(sessionId);
|
|
51
|
+
* ```
|
|
52
|
+
*/
|
|
53
|
+
export declare class CreditSessionManager {
|
|
54
|
+
private redisClient;
|
|
55
|
+
private readonly EXPIRATION;
|
|
56
|
+
constructor(redisClient: RedisClient);
|
|
57
|
+
/**
|
|
58
|
+
* Key for a session's credit data
|
|
59
|
+
*/
|
|
60
|
+
private sessionKey;
|
|
61
|
+
/**
|
|
62
|
+
* Key to look up user's current session
|
|
63
|
+
*/
|
|
64
|
+
private userSessionKey;
|
|
65
|
+
/**
|
|
66
|
+
* Key for the set of users with active credit sessions
|
|
67
|
+
*/
|
|
68
|
+
private pendingUsersKey;
|
|
69
|
+
/**
|
|
70
|
+
* Start a new credit session
|
|
71
|
+
*/
|
|
72
|
+
startSession(data: Omit<CreditSessionData, 'creditsUsed' | 'totalDurationMs' | 'lastUpdated'>): Promise<void>;
|
|
73
|
+
/**
|
|
74
|
+
* Record credit usage during a session
|
|
75
|
+
* Called by pipeline-service every 5 seconds
|
|
76
|
+
*/
|
|
77
|
+
recordUsage(sessionId: string, creditsDelta: number, durationDeltaMs: number): Promise<{
|
|
78
|
+
success: boolean;
|
|
79
|
+
creditsUsed: number;
|
|
80
|
+
totalDurationMs: number;
|
|
81
|
+
}>;
|
|
82
|
+
/**
|
|
83
|
+
* Get session data
|
|
84
|
+
*/
|
|
85
|
+
getSession(sessionId: string): Promise<CreditSessionData | null>;
|
|
86
|
+
/**
|
|
87
|
+
* Get session by user ID
|
|
88
|
+
*/
|
|
89
|
+
getSessionByUserId(userId: string): Promise<CreditSessionData | null>;
|
|
90
|
+
/**
|
|
91
|
+
* End a session and get final data (for DB flush)
|
|
92
|
+
*/
|
|
93
|
+
endSession(sessionId: string): Promise<CreditSessionData | null>;
|
|
94
|
+
/**
|
|
95
|
+
* Get all users with active credit sessions (for orphan cleanup)
|
|
96
|
+
*/
|
|
97
|
+
getUsersWithActiveSessions(): Promise<string[]>;
|
|
98
|
+
/**
|
|
99
|
+
* Get all stale sessions (lastUpdated > staleThresholdMs ago)
|
|
100
|
+
* Used by orphan cleanup job
|
|
101
|
+
*/
|
|
102
|
+
getStaleSessions(staleThresholdMs: number): Promise<CreditSessionData[]>;
|
|
103
|
+
/**
|
|
104
|
+
* Check remaining credits for a session
|
|
105
|
+
*/
|
|
106
|
+
checkBalance(sessionId: string): Promise<{
|
|
107
|
+
creditsUsed: number;
|
|
108
|
+
creditsRemaining: number;
|
|
109
|
+
limitReached: boolean;
|
|
110
|
+
lowBalance: boolean;
|
|
111
|
+
} | null>;
|
|
112
|
+
}
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.CreditSessionManager = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* Credit Session Manager
|
|
6
|
+
*
|
|
7
|
+
* Tracks credit usage during streaming sessions in Redis (USAGE_DATA db).
|
|
8
|
+
* Similar to UsageDataManager but tracks credits instead of milliseconds.
|
|
9
|
+
*
|
|
10
|
+
* Key structure:
|
|
11
|
+
* - credit:session:{sessionId} - Hash with session credit data
|
|
12
|
+
* - credit:session:user:{userId} - String with current sessionId (for lookup)
|
|
13
|
+
* - credit:pending:users - Set of user IDs with active credit sessions
|
|
14
|
+
*
|
|
15
|
+
* Flow:
|
|
16
|
+
* 1. Session starts: credit-service creates session in Redis
|
|
17
|
+
* 2. Every 5 seconds: pipeline-service increments creditsUsed
|
|
18
|
+
* 3. Session ends: credit-service flushes to DB, clears Redis
|
|
19
|
+
*
|
|
20
|
+
* Crash recovery:
|
|
21
|
+
* - If session not closed properly, orphan cleanup job will flush to DB
|
|
22
|
+
* - TTL: 24 hours (safety net)
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* ```typescript
|
|
26
|
+
* const manager = new CreditSessionManager(UsageDataRedisClient);
|
|
27
|
+
* await manager.startSession(sessionData);
|
|
28
|
+
* await manager.recordUsage(sessionId, creditsDelta, durationDeltaMs);
|
|
29
|
+
* const session = await manager.getSession(sessionId);
|
|
30
|
+
* await manager.endSession(sessionId);
|
|
31
|
+
* ```
|
|
32
|
+
*/
|
|
33
|
+
class CreditSessionManager {
|
|
34
|
+
constructor(redisClient) {
|
|
35
|
+
this.EXPIRATION = 24 * 60 * 60; // 24 hours TTL
|
|
36
|
+
this.redisClient = redisClient;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Key for a session's credit data
|
|
40
|
+
*/
|
|
41
|
+
sessionKey(sessionId) {
|
|
42
|
+
return `credit:session:${sessionId}`;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Key to look up user's current session
|
|
46
|
+
*/
|
|
47
|
+
userSessionKey(userId) {
|
|
48
|
+
return `credit:session:user:${userId}`;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Key for the set of users with active credit sessions
|
|
52
|
+
*/
|
|
53
|
+
pendingUsersKey() {
|
|
54
|
+
return 'credit:pending:users';
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Start a new credit session
|
|
58
|
+
*/
|
|
59
|
+
async startSession(data) {
|
|
60
|
+
const sessionKey = this.sessionKey(data.sessionId);
|
|
61
|
+
const userSessionKey = this.userSessionKey(data.userId);
|
|
62
|
+
await this.redisClient.hset(sessionKey, {
|
|
63
|
+
sessionId: data.sessionId,
|
|
64
|
+
userId: data.userId,
|
|
65
|
+
roomId: data.roomId.toString(),
|
|
66
|
+
serviceCode: data.serviceCode,
|
|
67
|
+
sourceLanguage: data.sourceLanguage,
|
|
68
|
+
targetLanguages: JSON.stringify(data.targetLanguages),
|
|
69
|
+
keywordsEnabled: data.keywordsEnabled ? '1' : '0',
|
|
70
|
+
speculativeEnabled: data.speculativeEnabled ? '1' : '0',
|
|
71
|
+
creditsPerMinute: data.creditsPerMinute.toString(),
|
|
72
|
+
initialBalance: data.initialBalance.toString(),
|
|
73
|
+
creditsUsed: '0',
|
|
74
|
+
totalDurationMs: '0',
|
|
75
|
+
multiplierExtraLanguage: data.multiplierExtraLanguage.toString(),
|
|
76
|
+
multiplierKeywords: data.multiplierKeywords.toString(),
|
|
77
|
+
multiplierSpeculative: data.multiplierSpeculative.toString(),
|
|
78
|
+
sessionStart: data.sessionStart.toString(),
|
|
79
|
+
lastUpdated: Date.now().toString()
|
|
80
|
+
});
|
|
81
|
+
await this.redisClient.expire(sessionKey, this.EXPIRATION);
|
|
82
|
+
// Store user's current session ID for lookup
|
|
83
|
+
await this.redisClient.set(userSessionKey, data.sessionId);
|
|
84
|
+
await this.redisClient.expire(userSessionKey, this.EXPIRATION);
|
|
85
|
+
// Add user to the set of users with active sessions
|
|
86
|
+
await this.redisClient.sadd(this.pendingUsersKey(), data.userId);
|
|
87
|
+
await this.redisClient.expire(this.pendingUsersKey(), this.EXPIRATION);
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Record credit usage during a session
|
|
91
|
+
* Called by pipeline-service every 5 seconds
|
|
92
|
+
*/
|
|
93
|
+
async recordUsage(sessionId, creditsDelta, durationDeltaMs) {
|
|
94
|
+
const sessionKey = this.sessionKey(sessionId);
|
|
95
|
+
// Check if session exists
|
|
96
|
+
const exists = await this.redisClient.exists(sessionKey);
|
|
97
|
+
if (exists !== 1) {
|
|
98
|
+
return { success: false, creditsUsed: 0, totalDurationMs: 0 };
|
|
99
|
+
}
|
|
100
|
+
// Increment credits used
|
|
101
|
+
await this.redisClient.hincrbyfloat(sessionKey, 'creditsUsed', creditsDelta);
|
|
102
|
+
// Increment duration
|
|
103
|
+
await this.redisClient.hincrby(sessionKey, 'totalDurationMs', durationDeltaMs);
|
|
104
|
+
// Update timestamp
|
|
105
|
+
await this.redisClient.hset(sessionKey, { lastUpdated: Date.now().toString() });
|
|
106
|
+
// Refresh TTL
|
|
107
|
+
await this.redisClient.expire(sessionKey, this.EXPIRATION);
|
|
108
|
+
// Get updated values
|
|
109
|
+
const data = await this.redisClient.hgetall(sessionKey);
|
|
110
|
+
return {
|
|
111
|
+
success: true,
|
|
112
|
+
creditsUsed: parseFloat((data === null || data === void 0 ? void 0 : data.creditsUsed) || '0'),
|
|
113
|
+
totalDurationMs: parseInt((data === null || data === void 0 ? void 0 : data.totalDurationMs) || '0', 10)
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Get session data
|
|
118
|
+
*/
|
|
119
|
+
async getSession(sessionId) {
|
|
120
|
+
const sessionKey = this.sessionKey(sessionId);
|
|
121
|
+
const data = await this.redisClient.hgetall(sessionKey);
|
|
122
|
+
if (!data || Object.keys(data).length === 0) {
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
return {
|
|
126
|
+
sessionId: data.sessionId,
|
|
127
|
+
userId: data.userId,
|
|
128
|
+
roomId: parseInt(data.roomId, 10),
|
|
129
|
+
serviceCode: data.serviceCode,
|
|
130
|
+
sourceLanguage: data.sourceLanguage,
|
|
131
|
+
targetLanguages: JSON.parse(data.targetLanguages || '[]'),
|
|
132
|
+
keywordsEnabled: data.keywordsEnabled === '1',
|
|
133
|
+
speculativeEnabled: data.speculativeEnabled === '1',
|
|
134
|
+
creditsPerMinute: parseFloat(data.creditsPerMinute || '0'),
|
|
135
|
+
initialBalance: parseFloat(data.initialBalance || '0'),
|
|
136
|
+
creditsUsed: parseFloat(data.creditsUsed || '0'),
|
|
137
|
+
totalDurationMs: parseInt(data.totalDurationMs || '0', 10),
|
|
138
|
+
multiplierExtraLanguage: parseFloat(data.multiplierExtraLanguage || '1'),
|
|
139
|
+
multiplierKeywords: parseFloat(data.multiplierKeywords || '1'),
|
|
140
|
+
multiplierSpeculative: parseFloat(data.multiplierSpeculative || '1'),
|
|
141
|
+
sessionStart: parseInt(data.sessionStart || '0', 10),
|
|
142
|
+
lastUpdated: parseInt(data.lastUpdated || '0', 10)
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Get session by user ID
|
|
147
|
+
*/
|
|
148
|
+
async getSessionByUserId(userId) {
|
|
149
|
+
const userSessionKey = this.userSessionKey(userId);
|
|
150
|
+
const sessionId = await this.redisClient.get(userSessionKey);
|
|
151
|
+
if (!sessionId) {
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
return this.getSession(sessionId);
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* End a session and get final data (for DB flush)
|
|
158
|
+
*/
|
|
159
|
+
async endSession(sessionId) {
|
|
160
|
+
const session = await this.getSession(sessionId);
|
|
161
|
+
if (session) {
|
|
162
|
+
const sessionKey = this.sessionKey(sessionId);
|
|
163
|
+
const userSessionKey = this.userSessionKey(session.userId);
|
|
164
|
+
await this.redisClient.del(sessionKey);
|
|
165
|
+
await this.redisClient.del(userSessionKey);
|
|
166
|
+
await this.redisClient.srem(this.pendingUsersKey(), session.userId);
|
|
167
|
+
}
|
|
168
|
+
return session;
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Get all users with active credit sessions (for orphan cleanup)
|
|
172
|
+
*/
|
|
173
|
+
async getUsersWithActiveSessions() {
|
|
174
|
+
const members = await this.redisClient.smembers(this.pendingUsersKey());
|
|
175
|
+
return members || [];
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Get all stale sessions (lastUpdated > staleThresholdMs ago)
|
|
179
|
+
* Used by orphan cleanup job
|
|
180
|
+
*/
|
|
181
|
+
async getStaleSessions(staleThresholdMs) {
|
|
182
|
+
const now = Date.now();
|
|
183
|
+
const userIds = await this.getUsersWithActiveSessions();
|
|
184
|
+
const staleSessions = [];
|
|
185
|
+
for (const userId of userIds) {
|
|
186
|
+
const session = await this.getSessionByUserId(userId);
|
|
187
|
+
if (session && (now - session.lastUpdated) > staleThresholdMs) {
|
|
188
|
+
staleSessions.push(session);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
return staleSessions;
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Check remaining credits for a session
|
|
195
|
+
*/
|
|
196
|
+
async checkBalance(sessionId) {
|
|
197
|
+
const session = await this.getSession(sessionId);
|
|
198
|
+
if (!session) {
|
|
199
|
+
return null;
|
|
200
|
+
}
|
|
201
|
+
const creditsRemaining = session.initialBalance - session.creditsUsed;
|
|
202
|
+
const percentUsed = session.creditsUsed / session.initialBalance;
|
|
203
|
+
return {
|
|
204
|
+
creditsUsed: session.creditsUsed,
|
|
205
|
+
creditsRemaining: Math.max(0, creditsRemaining),
|
|
206
|
+
limitReached: creditsRemaining <= 0,
|
|
207
|
+
lowBalance: percentUsed >= 0.75
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
exports.CreditSessionManager = CreditSessionManager;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { RedisClient } from '../redis-client';
|
|
2
|
+
/**
|
|
3
|
+
* Plan multipliers cached in Redis
|
|
4
|
+
*/
|
|
5
|
+
export interface CachedPlanMultipliers {
|
|
6
|
+
userId: string;
|
|
7
|
+
planId: string | null;
|
|
8
|
+
multiplierExtraLanguage: number;
|
|
9
|
+
multiplierKeywords: number;
|
|
10
|
+
multiplierSpeculative: number;
|
|
11
|
+
featureKeywords: boolean;
|
|
12
|
+
featureSpeculative: boolean;
|
|
13
|
+
quotaCredits: number;
|
|
14
|
+
cachedAt: number;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Plan Entitlements Manager
|
|
18
|
+
*
|
|
19
|
+
* Caches user plan multipliers in Redis (USAGE_DATA db) for fast access
|
|
20
|
+
* during streaming sessions. Avoids repeated DB queries for plan entitlements.
|
|
21
|
+
*
|
|
22
|
+
* Key structure:
|
|
23
|
+
* - plan:multipliers:{userId} - Hash with all multiplier values
|
|
24
|
+
*
|
|
25
|
+
* Cache invalidation:
|
|
26
|
+
* - On plan change: billing-service publishes event → credit-service clears cache
|
|
27
|
+
* - TTL: 24 hours (safety net if event missed)
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* ```typescript
|
|
31
|
+
* const manager = new PlanEntitlementsManager(UsageDataRedisClient);
|
|
32
|
+
* await manager.setMultipliers(userId, multipliers);
|
|
33
|
+
* const cached = await manager.getMultipliers(userId);
|
|
34
|
+
* await manager.clearMultipliers(userId); // On plan change
|
|
35
|
+
* ```
|
|
36
|
+
*/
|
|
37
|
+
export declare class PlanEntitlementsManager {
|
|
38
|
+
private redisClient;
|
|
39
|
+
private readonly EXPIRATION;
|
|
40
|
+
constructor(redisClient: RedisClient);
|
|
41
|
+
/**
|
|
42
|
+
* Key for a user's plan multipliers
|
|
43
|
+
*/
|
|
44
|
+
private userKey;
|
|
45
|
+
/**
|
|
46
|
+
* Cache plan multipliers for a user
|
|
47
|
+
*/
|
|
48
|
+
setMultipliers(userId: string, data: Omit<CachedPlanMultipliers, 'userId' | 'cachedAt'>): Promise<void>;
|
|
49
|
+
/**
|
|
50
|
+
* Get cached plan multipliers for a user
|
|
51
|
+
* Returns null if not cached (caller should fetch from DB and cache)
|
|
52
|
+
*/
|
|
53
|
+
getMultipliers(userId: string): Promise<CachedPlanMultipliers | null>;
|
|
54
|
+
/**
|
|
55
|
+
* Clear cached multipliers for a user (call on plan change)
|
|
56
|
+
*/
|
|
57
|
+
clearMultipliers(userId: string): Promise<void>;
|
|
58
|
+
/**
|
|
59
|
+
* Check if user has cached multipliers
|
|
60
|
+
*/
|
|
61
|
+
hasMultipliers(userId: string): Promise<boolean>;
|
|
62
|
+
/**
|
|
63
|
+
* Refresh TTL for a user's cached multipliers (call during active session)
|
|
64
|
+
*/
|
|
65
|
+
refreshTTL(userId: string): Promise<void>;
|
|
66
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.PlanEntitlementsManager = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* Plan Entitlements Manager
|
|
6
|
+
*
|
|
7
|
+
* Caches user plan multipliers in Redis (USAGE_DATA db) for fast access
|
|
8
|
+
* during streaming sessions. Avoids repeated DB queries for plan entitlements.
|
|
9
|
+
*
|
|
10
|
+
* Key structure:
|
|
11
|
+
* - plan:multipliers:{userId} - Hash with all multiplier values
|
|
12
|
+
*
|
|
13
|
+
* Cache invalidation:
|
|
14
|
+
* - On plan change: billing-service publishes event → credit-service clears cache
|
|
15
|
+
* - TTL: 24 hours (safety net if event missed)
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* ```typescript
|
|
19
|
+
* const manager = new PlanEntitlementsManager(UsageDataRedisClient);
|
|
20
|
+
* await manager.setMultipliers(userId, multipliers);
|
|
21
|
+
* const cached = await manager.getMultipliers(userId);
|
|
22
|
+
* await manager.clearMultipliers(userId); // On plan change
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
class PlanEntitlementsManager {
|
|
26
|
+
constructor(redisClient) {
|
|
27
|
+
this.EXPIRATION = 24 * 60 * 60; // 24 hours TTL (safety net)
|
|
28
|
+
this.redisClient = redisClient;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Key for a user's plan multipliers
|
|
32
|
+
*/
|
|
33
|
+
userKey(userId) {
|
|
34
|
+
return `plan:multipliers:${userId}`;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Cache plan multipliers for a user
|
|
38
|
+
*/
|
|
39
|
+
async setMultipliers(userId, data) {
|
|
40
|
+
const key = this.userKey(userId);
|
|
41
|
+
await this.redisClient.hset(key, {
|
|
42
|
+
planId: data.planId || '',
|
|
43
|
+
multiplierExtraLanguage: data.multiplierExtraLanguage.toString(),
|
|
44
|
+
multiplierKeywords: data.multiplierKeywords.toString(),
|
|
45
|
+
multiplierSpeculative: data.multiplierSpeculative.toString(),
|
|
46
|
+
featureKeywords: data.featureKeywords ? '1' : '0',
|
|
47
|
+
featureSpeculative: data.featureSpeculative ? '1' : '0',
|
|
48
|
+
quotaCredits: data.quotaCredits.toString(),
|
|
49
|
+
cachedAt: Date.now().toString()
|
|
50
|
+
});
|
|
51
|
+
await this.redisClient.expire(key, this.EXPIRATION);
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Get cached plan multipliers for a user
|
|
55
|
+
* Returns null if not cached (caller should fetch from DB and cache)
|
|
56
|
+
*/
|
|
57
|
+
async getMultipliers(userId) {
|
|
58
|
+
const key = this.userKey(userId);
|
|
59
|
+
const data = await this.redisClient.hgetall(key);
|
|
60
|
+
if (!data || Object.keys(data).length === 0) {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
return {
|
|
64
|
+
userId,
|
|
65
|
+
planId: data.planId || null,
|
|
66
|
+
multiplierExtraLanguage: parseFloat(data.multiplierExtraLanguage || '1.0'),
|
|
67
|
+
multiplierKeywords: parseFloat(data.multiplierKeywords || '1.5'),
|
|
68
|
+
multiplierSpeculative: parseFloat(data.multiplierSpeculative || '1.4'),
|
|
69
|
+
featureKeywords: data.featureKeywords === '1',
|
|
70
|
+
featureSpeculative: data.featureSpeculative === '1',
|
|
71
|
+
quotaCredits: parseInt(data.quotaCredits || '60', 10),
|
|
72
|
+
cachedAt: parseInt(data.cachedAt || '0', 10)
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Clear cached multipliers for a user (call on plan change)
|
|
77
|
+
*/
|
|
78
|
+
async clearMultipliers(userId) {
|
|
79
|
+
const key = this.userKey(userId);
|
|
80
|
+
await this.redisClient.del(key);
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Check if user has cached multipliers
|
|
84
|
+
*/
|
|
85
|
+
async hasMultipliers(userId) {
|
|
86
|
+
const key = this.userKey(userId);
|
|
87
|
+
const exists = await this.redisClient.exists(key);
|
|
88
|
+
return exists === 1;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Refresh TTL for a user's cached multipliers (call during active session)
|
|
92
|
+
*/
|
|
93
|
+
async refreshTTL(userId) {
|
|
94
|
+
const key = this.userKey(userId);
|
|
95
|
+
await this.redisClient.expire(key, this.EXPIRATION);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
exports.PlanEntitlementsManager = PlanEntitlementsManager;
|
package/dist/redis-client.d.ts
CHANGED
|
@@ -20,6 +20,7 @@ export declare class RedisClient {
|
|
|
20
20
|
hset(key: string, data: Record<string, string>): Promise<number>;
|
|
21
21
|
hgetall(key: string): Promise<Record<string, string>>;
|
|
22
22
|
hincrby(key: string, field: string, increment: number): Promise<number>;
|
|
23
|
+
hincrbyfloat(key: string, field: string, increment: number): Promise<string>;
|
|
23
24
|
zadd(key: string, score: number, member: string): Promise<number>;
|
|
24
25
|
zrange(key: string, start: number, stop: number): Promise<string[]>;
|
|
25
26
|
zremrangebyrank(key: string, start: number, stop: number): Promise<number>;
|
package/dist/redis-client.js
CHANGED
|
@@ -55,6 +55,9 @@ class RedisClient {
|
|
|
55
55
|
async hincrby(key, field, increment) {
|
|
56
56
|
return this.client.hincrby(key, field, increment);
|
|
57
57
|
}
|
|
58
|
+
async hincrbyfloat(key, field, increment) {
|
|
59
|
+
return this.client.hincrbyfloat(key, field, increment);
|
|
60
|
+
}
|
|
58
61
|
async zadd(key, score, member) {
|
|
59
62
|
return this.client.zadd(key, score, member);
|
|
60
63
|
}
|