@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.
@@ -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;
@@ -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>;
@@ -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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mulingai-npm/redis",
3
- "version": "3.30.0",
3
+ "version": "3.30.2",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "repository": {