@mulingai-npm/redis 3.35.2 → 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-speaker-manager.d.ts +2 -0
- package/dist/managers/mulingstream-speaker-manager.js +9 -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;
|
|
@@ -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;
|
|
@@ -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) {
|
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
|
}
|