@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.
@@ -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) {
@@ -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.2",
3
+ "version": "3.35.3",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "repository": {