@mulingai-npm/redis 3.35.0 → 3.35.3

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,65 @@
1
+ import { RedisClient } from '../redis-client';
2
+ /**
3
+ * Demo Room Pool Manager
4
+ *
5
+ * Manages a pool of pre-created demo rooms for unauthenticated visitors.
6
+ * Tracks room availability, active sessions, and rate limiting.
7
+ *
8
+ * Redis key structure:
9
+ * - demo:pool:all (Set) All demo room IDs
10
+ * - demo:pool:available (Set) Currently available room IDs
11
+ * - demo:pool:room:{id}:session (Hash) Active session info per room
12
+ * - demo:ratelimit:email:{hash} (Counter) Demos per email per 24h
13
+ */
14
+ export declare class DemoRoomPoolManager {
15
+ private redisClient;
16
+ constructor(redisClient: RedisClient);
17
+ /**
18
+ * Initialize the pool with demo room IDs.
19
+ * Called once at service startup. Idempotent — safe to call on restart.
20
+ */
21
+ initPool(roomIds: number[]): Promise<void>;
22
+ /**
23
+ * Assign an available room to a demo user.
24
+ * Returns null if no rooms available.
25
+ * Atomic: SPOP removes from available set in one operation.
26
+ */
27
+ assignRoom(emailHash: string, demoSessionId: string, durationMs: number): Promise<{
28
+ roomId: number;
29
+ expiresAt: number;
30
+ } | null>;
31
+ /**
32
+ * Release a room back to the available pool.
33
+ * Called when: user ends demo, session expires, or speaker disconnects.
34
+ */
35
+ releaseRoom(roomId: number): Promise<void>;
36
+ /**
37
+ * Get pool status (for landing page "all rooms busy" message).
38
+ */
39
+ getPoolStatus(): Promise<{
40
+ totalRooms: number;
41
+ availableRooms: number;
42
+ soonestAvailableMs: number | null;
43
+ }>;
44
+ /**
45
+ * Check if a room's session has expired (called by pipeline periodic check).
46
+ */
47
+ isSessionExpired(roomId: number): Promise<boolean>;
48
+ /**
49
+ * Get session info for a room.
50
+ */
51
+ getSession(roomId: number): Promise<{
52
+ emailHash: string;
53
+ demoSessionId: string;
54
+ startedAt: number;
55
+ expiresAt: number;
56
+ } | null>;
57
+ /**
58
+ * Check email rate limit. Returns true if under limit, false if exceeded.
59
+ */
60
+ checkEmailRateLimit(emailHash: string, max?: number): Promise<boolean>;
61
+ /**
62
+ * Increment email rate limit counter with 24h TTL.
63
+ */
64
+ incrementEmailRateLimit(emailHash: string): Promise<void>;
65
+ }
@@ -0,0 +1,140 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DemoRoomPoolManager = void 0;
4
+ /**
5
+ * Demo Room Pool Manager
6
+ *
7
+ * Manages a pool of pre-created demo rooms for unauthenticated visitors.
8
+ * Tracks room availability, active sessions, and rate limiting.
9
+ *
10
+ * Redis key structure:
11
+ * - demo:pool:all (Set) All demo room IDs
12
+ * - demo:pool:available (Set) Currently available room IDs
13
+ * - demo:pool:room:{id}:session (Hash) Active session info per room
14
+ * - demo:ratelimit:email:{hash} (Counter) Demos per email per 24h
15
+ */
16
+ class DemoRoomPoolManager {
17
+ constructor(redisClient) {
18
+ this.redisClient = redisClient;
19
+ }
20
+ /**
21
+ * Initialize the pool with demo room IDs.
22
+ * Called once at service startup. Idempotent — safe to call on restart.
23
+ */
24
+ async initPool(roomIds) {
25
+ if (roomIds.length === 0)
26
+ return;
27
+ const roomIdStrings = roomIds.map(String);
28
+ // Add all room IDs to the "all" set
29
+ await this.redisClient.sadd('demo:pool:all', ...roomIdStrings);
30
+ // For each room, if no active session exists, add to available set
31
+ for (const roomIdStr of roomIdStrings) {
32
+ const sessionData = await this.redisClient.hgetall(`demo:pool:room:${roomIdStr}:session`);
33
+ const hasActiveSession = sessionData && sessionData.expiresAt && Date.now() < parseInt(sessionData.expiresAt);
34
+ if (!hasActiveSession) {
35
+ // Clean up any stale session
36
+ if (sessionData && sessionData.expiresAt) {
37
+ await this.redisClient.del(`demo:pool:room:${roomIdStr}:session`);
38
+ }
39
+ await this.redisClient.sadd('demo:pool:available', roomIdStr);
40
+ }
41
+ }
42
+ }
43
+ /**
44
+ * Assign an available room to a demo user.
45
+ * Returns null if no rooms available.
46
+ * Atomic: SPOP removes from available set in one operation.
47
+ */
48
+ async assignRoom(emailHash, demoSessionId, durationMs) {
49
+ // Atomic pop from available set
50
+ const roomIdStr = await this.redisClient.spop('demo:pool:available');
51
+ if (!roomIdStr)
52
+ return null; // All rooms busy
53
+ const roomId = parseInt(roomIdStr);
54
+ const now = Date.now();
55
+ const expiresAt = now + durationMs;
56
+ // Store session info
57
+ await this.redisClient.hset(`demo:pool:room:${roomId}:session`, {
58
+ emailHash,
59
+ demoSessionId,
60
+ startedAt: now.toString(),
61
+ expiresAt: expiresAt.toString(),
62
+ durationMs: durationMs.toString()
63
+ });
64
+ // TTL slightly longer than session to auto-cleanup stale sessions
65
+ await this.redisClient.expire(`demo:pool:room:${roomId}:session`, Math.ceil(durationMs / 1000) + 300);
66
+ return { roomId, expiresAt };
67
+ }
68
+ /**
69
+ * Release a room back to the available pool.
70
+ * Called when: user ends demo, session expires, or speaker disconnects.
71
+ */
72
+ async releaseRoom(roomId) {
73
+ await this.redisClient.del(`demo:pool:room:${roomId}:session`);
74
+ await this.redisClient.sadd('demo:pool:available', roomId.toString());
75
+ }
76
+ /**
77
+ * Get pool status (for landing page "all rooms busy" message).
78
+ */
79
+ async getPoolStatus() {
80
+ const total = await this.redisClient.scard('demo:pool:all');
81
+ const available = await this.redisClient.scard('demo:pool:available');
82
+ let soonestAvailableMs = null;
83
+ if (available === 0 && total > 0) {
84
+ // Find the session that expires soonest
85
+ const allRoomIds = await this.redisClient.smembers('demo:pool:all');
86
+ let soonestExpiry = Infinity;
87
+ for (const rid of allRoomIds) {
88
+ const expiresAtStr = await this.redisClient.hget(`demo:pool:room:${rid}:session`, 'expiresAt');
89
+ if (expiresAtStr) {
90
+ const expiry = parseInt(expiresAtStr);
91
+ if (expiry < soonestExpiry)
92
+ soonestExpiry = expiry;
93
+ }
94
+ }
95
+ if (soonestExpiry < Infinity) {
96
+ soonestAvailableMs = Math.max(0, soonestExpiry - Date.now());
97
+ }
98
+ }
99
+ return { totalRooms: total, availableRooms: available, soonestAvailableMs };
100
+ }
101
+ /**
102
+ * Check if a room's session has expired (called by pipeline periodic check).
103
+ */
104
+ async isSessionExpired(roomId) {
105
+ const expiresAtStr = await this.redisClient.hget(`demo:pool:room:${roomId}:session`, 'expiresAt');
106
+ if (!expiresAtStr)
107
+ return true; // No session = expired
108
+ return Date.now() >= parseInt(expiresAtStr);
109
+ }
110
+ /**
111
+ * Get session info for a room.
112
+ */
113
+ async getSession(roomId) {
114
+ const data = await this.redisClient.hgetall(`demo:pool:room:${roomId}:session`);
115
+ if (!(data === null || data === void 0 ? void 0 : data.demoSessionId))
116
+ return null;
117
+ return {
118
+ emailHash: data.emailHash,
119
+ demoSessionId: data.demoSessionId,
120
+ startedAt: parseInt(data.startedAt),
121
+ expiresAt: parseInt(data.expiresAt)
122
+ };
123
+ }
124
+ /**
125
+ * Check email rate limit. Returns true if under limit, false if exceeded.
126
+ */
127
+ async checkEmailRateLimit(emailHash, max = 3) {
128
+ const count = await this.redisClient.get(`demo:ratelimit:email:${emailHash}`);
129
+ return !count || parseInt(count) < max;
130
+ }
131
+ /**
132
+ * Increment email rate limit counter with 24h TTL.
133
+ */
134
+ async incrementEmailRateLimit(emailHash) {
135
+ const key = `demo:ratelimit:email:${emailHash}`;
136
+ await this.redisClient.incr(key);
137
+ await this.redisClient.expire(key, 86400); // 24h
138
+ }
139
+ }
140
+ exports.DemoRoomPoolManager = DemoRoomPoolManager;
@@ -30,6 +30,7 @@ export type MulingstreamChunkData = {
30
30
  shortCodeTargetLanguages: string[];
31
31
  finalTranscription: string;
32
32
  createdAt: number;
33
+ creditUsageSessionId?: number | null;
33
34
  streamingChunk: StreamingChunkData;
34
35
  translation: {
35
36
  [language: string]: {
@@ -75,6 +76,7 @@ export declare class MulingstreamChunkManager {
75
76
  sttProvider: SttProvider;
76
77
  targetLanguages: string[];
77
78
  shortCodeTargetLanguages: string[];
79
+ creditUsageSessionId?: number | null;
78
80
  streamingData: {
79
81
  startMs: number;
80
82
  endMs: number;
@@ -34,6 +34,7 @@ class MulingstreamChunkManager {
34
34
  shortCodeTargetLanguages: this.deserialize(h.shortCodeTargetLanguages),
35
35
  finalTranscription: h.finalTranscription,
36
36
  createdAt: parseInt(h.createdAt, 10),
37
+ creditUsageSessionId: h.creditUsageSessionId ? parseInt(h.creditUsageSessionId, 10) : null,
37
38
  streamingChunk: this.deserialize(h.streamingChunk),
38
39
  translation: this.deserialize(h.translation),
39
40
  tts: this.deserialize(h.tts),
@@ -75,7 +76,7 @@ class MulingstreamChunkManager {
75
76
  */
76
77
  async addStreamingChunk(params) {
77
78
  var _a, _b;
78
- const { roomId, userId, chunkNumber, language, transcription, sttProvider, targetLanguages, shortCodeTargetLanguages, streamingData } = params;
79
+ const { roomId, userId, chunkNumber, language, transcription, sttProvider, targetLanguages, shortCodeTargetLanguages, creditUsageSessionId, streamingData } = params;
79
80
  // Clear room if this is first chunk (new session)
80
81
  if (chunkNumber === 1) {
81
82
  const old = await this.redisClient.zrange(this.roomZsetKey(roomId), 0, -1);
@@ -127,6 +128,7 @@ class MulingstreamChunkManager {
127
128
  shortCodeTargetLanguages,
128
129
  finalTranscription: transcription,
129
130
  createdAt,
131
+ creditUsageSessionId: creditUsageSessionId !== null && creditUsageSessionId !== void 0 ? creditUsageSessionId : null,
130
132
  streamingChunk,
131
133
  translation,
132
134
  tts,
@@ -143,6 +145,7 @@ class MulingstreamChunkManager {
143
145
  shortCodeTargetLanguages: this.serialize(shortCodeTargetLanguages),
144
146
  finalTranscription: transcription,
145
147
  createdAt: String(createdAt),
148
+ creditUsageSessionId: creditUsageSessionId != null ? String(creditUsageSessionId) : '',
146
149
  streamingChunk: this.serialize(streamingChunk),
147
150
  translation: this.serialize(translation),
148
151
  tts: this.serialize(tts),
@@ -11,6 +11,8 @@ export type MulingstreamSpeakerData = {
11
11
  recordingsDuration: number;
12
12
  targetLanguages: string[];
13
13
  timestamp: string;
14
+ isDemoSession?: boolean;
15
+ demoExpiresAt?: number;
14
16
  };
15
17
  export declare class MulingstreamSpeakerManager {
16
18
  private redisClient;
@@ -36,6 +38,8 @@ export declare class MulingstreamSpeakerManager {
36
38
  isRoomEmpty(roomId: string): Promise<boolean>;
37
39
  updateSourceLanguage(socketId: string, newLang: string): Promise<boolean>;
38
40
  updateTargetLanguages(socketId: string, languages: string[]): Promise<boolean>;
41
+ updateSourceLanguageByRoomId(roomId: string, newLang: string): Promise<number>;
42
+ updateTargetLanguagesByRoomId(roomId: string, languages: string[]): Promise<number>;
39
43
  increaseRecordingDuration(socketId: string, delta: number): Promise<number>;
40
44
  private cleanIndexes;
41
45
  }
@@ -32,7 +32,9 @@ class MulingstreamSpeakerManager {
32
32
  prompts: hash.prompts,
33
33
  recordingsDuration: parseInt(hash.recordingsDuration, 10) || 0,
34
34
  targetLanguages: JSON.parse(hash.targetLanguages || '[]'),
35
- timestamp: hash.timestamp
35
+ timestamp: hash.timestamp,
36
+ isDemoSession: hash.isDemoSession === 'true',
37
+ demoExpiresAt: parseInt(hash.demoExpiresAt, 10) || 0
36
38
  };
37
39
  }
38
40
  serialize(data) {
@@ -51,6 +53,12 @@ class MulingstreamSpeakerManager {
51
53
  if (data.roomUuid) {
52
54
  result.roomUuid = data.roomUuid;
53
55
  }
56
+ if (data.isDemoSession) {
57
+ result.isDemoSession = 'true';
58
+ }
59
+ if (data.demoExpiresAt) {
60
+ result.demoExpiresAt = data.demoExpiresAt.toString();
61
+ }
54
62
  return result;
55
63
  }
56
64
  buildId(roomId, userId, socketId) {
@@ -201,6 +209,24 @@ class MulingstreamSpeakerManager {
201
209
  await this.redisClient.hset(this.buildKey(speaker.speakerId), { targetLanguages: JSON.stringify(languages) });
202
210
  return true;
203
211
  }
212
+ async updateSourceLanguageByRoomId(roomId, newLang) {
213
+ const speakers = await this.getSpeakersByRoomId(roomId);
214
+ let updated = 0;
215
+ for (const speaker of speakers) {
216
+ await this.redisClient.hset(this.buildKey(speaker.speakerId), { sourceLanguage: newLang });
217
+ updated++;
218
+ }
219
+ return updated;
220
+ }
221
+ async updateTargetLanguagesByRoomId(roomId, languages) {
222
+ const speakers = await this.getSpeakersByRoomId(roomId);
223
+ let updated = 0;
224
+ for (const speaker of speakers) {
225
+ await this.redisClient.hset(this.buildKey(speaker.speakerId), { targetLanguages: JSON.stringify(languages) });
226
+ updated++;
227
+ }
228
+ return updated;
229
+ }
204
230
  async increaseRecordingDuration(socketId, delta) {
205
231
  const speaker = await this.getSpeakerBySocketId(socketId);
206
232
  if (speaker === null)
@@ -32,6 +32,9 @@ export declare class RedisClient {
32
32
  }>>;
33
33
  zcard(key: string): Promise<number>;
34
34
  zscore(key: string, member: string): Promise<number | null>;
35
+ scard(key: string): Promise<number>;
36
+ spop(key: string): Promise<string | null>;
37
+ hget(key: string, field: string): Promise<string | null>;
35
38
  incr(key: string): Promise<number>;
36
39
  scan(cursor: number, pattern: string, count?: number): Promise<[string, string[]]>;
37
40
  keys(pattern: string): Promise<string[]>;
@@ -91,6 +91,15 @@ class RedisClient {
91
91
  const score = await this.client.zscore(key, member);
92
92
  return score !== null ? parseFloat(score) : null;
93
93
  }
94
+ async scard(key) {
95
+ return this.client.scard(key);
96
+ }
97
+ async spop(key) {
98
+ return this.client.spop(key);
99
+ }
100
+ async hget(key, field) {
101
+ return this.client.hget(key, field);
102
+ }
94
103
  async incr(key) {
95
104
  return this.client.incr(key);
96
105
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mulingai-npm/redis",
3
- "version": "3.35.0",
3
+ "version": "3.35.3",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "repository": {