@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.
- package/dist/managers/demo-room-pool-manager.d.ts +65 -0
- package/dist/managers/demo-room-pool-manager.js +140 -0
- package/dist/managers/mulingstream-chunk-manager.d.ts +2 -0
- package/dist/managers/mulingstream-chunk-manager.js +4 -1
- package/dist/managers/mulingstream-speaker-manager.d.ts +4 -0
- package/dist/managers/mulingstream-speaker-manager.js +27 -1
- package/dist/redis-client.d.ts +3 -0
- package/dist/redis-client.js +9 -0
- package/package.json +1 -1
|
@@ -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)
|
package/dist/redis-client.d.ts
CHANGED
|
@@ -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[]>;
|
package/dist/redis-client.js
CHANGED
|
@@ -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
|
}
|