@mulingai-npm/redis 3.17.2 → 3.18.0
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,115 @@
|
|
|
1
|
+
import { RedisClient } from '../redis-client';
|
|
2
|
+
/**
|
|
3
|
+
* Demo Room State
|
|
4
|
+
* Tracks the runtime state of an active demo room
|
|
5
|
+
*/
|
|
6
|
+
export interface DemoRoomState {
|
|
7
|
+
roomId: string;
|
|
8
|
+
userId: string;
|
|
9
|
+
demoType: 'preTranscribed' | 'alwaysLive';
|
|
10
|
+
sourceLanguage: string;
|
|
11
|
+
targetLanguages: string[];
|
|
12
|
+
currentChunkIndex: number;
|
|
13
|
+
totalChunks: number;
|
|
14
|
+
loopCount: number;
|
|
15
|
+
startedAt: number;
|
|
16
|
+
demoStartAt: number | null;
|
|
17
|
+
demoDurationMinutes: number;
|
|
18
|
+
isActive: boolean;
|
|
19
|
+
isPaused: boolean;
|
|
20
|
+
lastChunkEmittedAt: number | null;
|
|
21
|
+
createdAt: number;
|
|
22
|
+
updatedAt: number;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Demo State Manager
|
|
26
|
+
* Manages Redis-backed state for active demo rooms
|
|
27
|
+
*/
|
|
28
|
+
export declare class DemoStateManager {
|
|
29
|
+
private redisClient;
|
|
30
|
+
private readonly EXPIRATION;
|
|
31
|
+
private readonly KEY_PREFIX;
|
|
32
|
+
private readonly ACTIVE_DEMOS_SET;
|
|
33
|
+
constructor(redisClient: RedisClient);
|
|
34
|
+
/**
|
|
35
|
+
* Generate Redis key for demo room state
|
|
36
|
+
*/
|
|
37
|
+
private getStateKey;
|
|
38
|
+
/**
|
|
39
|
+
* Serialize state to Redis format
|
|
40
|
+
*/
|
|
41
|
+
private serializeState;
|
|
42
|
+
/**
|
|
43
|
+
* Deserialize state from Redis format
|
|
44
|
+
*/
|
|
45
|
+
private deserializeState;
|
|
46
|
+
/**
|
|
47
|
+
* Initialize a new demo room state
|
|
48
|
+
*/
|
|
49
|
+
createDemoState(params: {
|
|
50
|
+
roomId: string;
|
|
51
|
+
userId: string;
|
|
52
|
+
demoType: 'preTranscribed' | 'alwaysLive';
|
|
53
|
+
sourceLanguage: string;
|
|
54
|
+
targetLanguages: string[];
|
|
55
|
+
totalChunks: number;
|
|
56
|
+
demoStartAt: number | null;
|
|
57
|
+
demoDurationMinutes: number;
|
|
58
|
+
}): Promise<DemoRoomState>;
|
|
59
|
+
/**
|
|
60
|
+
* Get demo room state
|
|
61
|
+
*/
|
|
62
|
+
getDemoState(roomId: string): Promise<DemoRoomState | null>;
|
|
63
|
+
/**
|
|
64
|
+
* Update demo state (incremental update)
|
|
65
|
+
*/
|
|
66
|
+
updateDemoState(roomId: string, updates: Partial<Omit<DemoRoomState, 'roomId' | 'userId' | 'createdAt'>>): Promise<DemoRoomState | null>;
|
|
67
|
+
/**
|
|
68
|
+
* Advance to next chunk
|
|
69
|
+
* Handles looping when reaching end of transcriptions
|
|
70
|
+
*/
|
|
71
|
+
advanceToNextChunk(roomId: string): Promise<DemoRoomState | null>;
|
|
72
|
+
/**
|
|
73
|
+
* Check if demo should still be running
|
|
74
|
+
* Returns true if demo should continue, false if it should stop
|
|
75
|
+
*/
|
|
76
|
+
isDemoStillActive(state: DemoRoomState): boolean;
|
|
77
|
+
/**
|
|
78
|
+
* Stop a demo room
|
|
79
|
+
*/
|
|
80
|
+
stopDemo(roomId: string): Promise<boolean>;
|
|
81
|
+
/**
|
|
82
|
+
* Pause a demo room
|
|
83
|
+
*/
|
|
84
|
+
pauseDemo(roomId: string): Promise<boolean>;
|
|
85
|
+
/**
|
|
86
|
+
* Resume a paused demo room
|
|
87
|
+
*/
|
|
88
|
+
resumeDemo(roomId: string): Promise<boolean>;
|
|
89
|
+
/**
|
|
90
|
+
* Get all active demo room IDs
|
|
91
|
+
*/
|
|
92
|
+
getActiveDemoRooms(): Promise<string[]>;
|
|
93
|
+
/**
|
|
94
|
+
* Delete demo state completely
|
|
95
|
+
*/
|
|
96
|
+
deleteDemoState(roomId: string): Promise<void>;
|
|
97
|
+
/**
|
|
98
|
+
* Get statistics about a demo
|
|
99
|
+
*/
|
|
100
|
+
getDemoStats(roomId: string): Promise<{
|
|
101
|
+
totalChunksProcessed: number;
|
|
102
|
+
loopCount: number;
|
|
103
|
+
uptimeMinutes: number;
|
|
104
|
+
isActive: boolean;
|
|
105
|
+
} | null>;
|
|
106
|
+
/**
|
|
107
|
+
* Check if a demo room is currently active
|
|
108
|
+
*/
|
|
109
|
+
isDemoActive(roomId: string): Promise<boolean>;
|
|
110
|
+
/**
|
|
111
|
+
* Cleanup stale demos (safety mechanism)
|
|
112
|
+
* Call this periodically to remove demos that should have ended
|
|
113
|
+
*/
|
|
114
|
+
cleanupStaleDemos(): Promise<number>;
|
|
115
|
+
}
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DemoStateManager = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* Demo State Manager
|
|
6
|
+
* Manages Redis-backed state for active demo rooms
|
|
7
|
+
*/
|
|
8
|
+
class DemoStateManager {
|
|
9
|
+
constructor(redisClient) {
|
|
10
|
+
this.redisClient = redisClient;
|
|
11
|
+
this.EXPIRATION = 24 * 60 * 60; // 24 hours
|
|
12
|
+
this.KEY_PREFIX = 'demo:room:';
|
|
13
|
+
this.ACTIVE_DEMOS_SET = 'demo:active';
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Generate Redis key for demo room state
|
|
17
|
+
*/
|
|
18
|
+
getStateKey(roomId) {
|
|
19
|
+
return `${this.KEY_PREFIX}${roomId}`;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Serialize state to Redis format
|
|
23
|
+
*/
|
|
24
|
+
serializeState(state) {
|
|
25
|
+
return {
|
|
26
|
+
roomId: state.roomId,
|
|
27
|
+
userId: state.userId,
|
|
28
|
+
demoType: state.demoType,
|
|
29
|
+
sourceLanguage: state.sourceLanguage,
|
|
30
|
+
targetLanguages: JSON.stringify(state.targetLanguages),
|
|
31
|
+
currentChunkIndex: String(state.currentChunkIndex),
|
|
32
|
+
totalChunks: String(state.totalChunks),
|
|
33
|
+
loopCount: String(state.loopCount),
|
|
34
|
+
startedAt: String(state.startedAt),
|
|
35
|
+
demoStartAt: state.demoStartAt ? String(state.demoStartAt) : '',
|
|
36
|
+
demoDurationMinutes: String(state.demoDurationMinutes),
|
|
37
|
+
isActive: String(state.isActive),
|
|
38
|
+
isPaused: String(state.isPaused),
|
|
39
|
+
lastChunkEmittedAt: state.lastChunkEmittedAt ? String(state.lastChunkEmittedAt) : '',
|
|
40
|
+
createdAt: String(state.createdAt),
|
|
41
|
+
updatedAt: String(state.updatedAt),
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Deserialize state from Redis format
|
|
46
|
+
*/
|
|
47
|
+
deserializeState(data) {
|
|
48
|
+
return {
|
|
49
|
+
roomId: data.roomId,
|
|
50
|
+
userId: data.userId,
|
|
51
|
+
demoType: data.demoType,
|
|
52
|
+
sourceLanguage: data.sourceLanguage,
|
|
53
|
+
targetLanguages: JSON.parse(data.targetLanguages),
|
|
54
|
+
currentChunkIndex: parseInt(data.currentChunkIndex, 10),
|
|
55
|
+
totalChunks: parseInt(data.totalChunks, 10),
|
|
56
|
+
loopCount: parseInt(data.loopCount, 10),
|
|
57
|
+
startedAt: parseInt(data.startedAt, 10),
|
|
58
|
+
demoStartAt: data.demoStartAt ? parseInt(data.demoStartAt, 10) : null,
|
|
59
|
+
demoDurationMinutes: parseInt(data.demoDurationMinutes, 10),
|
|
60
|
+
isActive: data.isActive === 'true',
|
|
61
|
+
isPaused: data.isPaused === 'true',
|
|
62
|
+
lastChunkEmittedAt: data.lastChunkEmittedAt ? parseInt(data.lastChunkEmittedAt, 10) : null,
|
|
63
|
+
createdAt: parseInt(data.createdAt, 10),
|
|
64
|
+
updatedAt: parseInt(data.updatedAt, 10),
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Initialize a new demo room state
|
|
69
|
+
*/
|
|
70
|
+
async createDemoState(params) {
|
|
71
|
+
const now = Date.now();
|
|
72
|
+
const state = {
|
|
73
|
+
roomId: params.roomId,
|
|
74
|
+
userId: params.userId,
|
|
75
|
+
demoType: params.demoType,
|
|
76
|
+
sourceLanguage: params.sourceLanguage,
|
|
77
|
+
targetLanguages: params.targetLanguages,
|
|
78
|
+
currentChunkIndex: 0,
|
|
79
|
+
totalChunks: params.totalChunks,
|
|
80
|
+
loopCount: 0,
|
|
81
|
+
startedAt: now,
|
|
82
|
+
demoStartAt: params.demoStartAt,
|
|
83
|
+
demoDurationMinutes: params.demoDurationMinutes,
|
|
84
|
+
isActive: true,
|
|
85
|
+
isPaused: false,
|
|
86
|
+
lastChunkEmittedAt: null,
|
|
87
|
+
createdAt: now,
|
|
88
|
+
updatedAt: now,
|
|
89
|
+
};
|
|
90
|
+
const key = this.getStateKey(params.roomId);
|
|
91
|
+
const serialized = this.serializeState(state);
|
|
92
|
+
// Store state and add to active demos set
|
|
93
|
+
const pipeline = this.redisClient.pipeline();
|
|
94
|
+
pipeline.hset(key, serialized);
|
|
95
|
+
pipeline.expire(key, this.EXPIRATION);
|
|
96
|
+
pipeline.sadd(this.ACTIVE_DEMOS_SET, params.roomId);
|
|
97
|
+
await pipeline.exec();
|
|
98
|
+
return state;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Get demo room state
|
|
102
|
+
*/
|
|
103
|
+
async getDemoState(roomId) {
|
|
104
|
+
const key = this.getStateKey(roomId);
|
|
105
|
+
const data = await this.redisClient.hgetall(key);
|
|
106
|
+
if (!data || Object.keys(data).length === 0) {
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
return this.deserializeState(data);
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Update demo state (incremental update)
|
|
113
|
+
*/
|
|
114
|
+
async updateDemoState(roomId, updates) {
|
|
115
|
+
const current = await this.getDemoState(roomId);
|
|
116
|
+
if (!current) {
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
const updated = {
|
|
120
|
+
...current,
|
|
121
|
+
...updates,
|
|
122
|
+
updatedAt: Date.now(),
|
|
123
|
+
};
|
|
124
|
+
const key = this.getStateKey(roomId);
|
|
125
|
+
const serialized = this.serializeState(updated);
|
|
126
|
+
const pipeline = this.redisClient.pipeline();
|
|
127
|
+
pipeline.hset(key, serialized);
|
|
128
|
+
pipeline.expire(key, this.EXPIRATION);
|
|
129
|
+
await pipeline.exec();
|
|
130
|
+
return updated;
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Advance to next chunk
|
|
134
|
+
* Handles looping when reaching end of transcriptions
|
|
135
|
+
*/
|
|
136
|
+
async advanceToNextChunk(roomId) {
|
|
137
|
+
const state = await this.getDemoState(roomId);
|
|
138
|
+
if (!state) {
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
const nextIndex = state.currentChunkIndex + 1;
|
|
142
|
+
let newLoopCount = state.loopCount;
|
|
143
|
+
// Check if we've reached the end
|
|
144
|
+
if (nextIndex >= state.totalChunks) {
|
|
145
|
+
// Loop back to beginning
|
|
146
|
+
return await this.updateDemoState(roomId, {
|
|
147
|
+
currentChunkIndex: 0,
|
|
148
|
+
loopCount: state.loopCount + 1,
|
|
149
|
+
lastChunkEmittedAt: Date.now(),
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
// Normal advancement
|
|
153
|
+
return await this.updateDemoState(roomId, {
|
|
154
|
+
currentChunkIndex: nextIndex,
|
|
155
|
+
lastChunkEmittedAt: Date.now(),
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Check if demo should still be running
|
|
160
|
+
* Returns true if demo should continue, false if it should stop
|
|
161
|
+
*/
|
|
162
|
+
isDemoStillActive(state) {
|
|
163
|
+
// Always-live demos run forever
|
|
164
|
+
if (state.demoType === 'alwaysLive') {
|
|
165
|
+
return true;
|
|
166
|
+
}
|
|
167
|
+
// Check if scheduled demo duration has expired
|
|
168
|
+
if (state.demoDurationMinutes === 0) {
|
|
169
|
+
return true; // Duration 0 = infinite
|
|
170
|
+
}
|
|
171
|
+
const now = Date.now();
|
|
172
|
+
const elapsedMs = now - state.startedAt;
|
|
173
|
+
const durationMs = state.demoDurationMinutes * 60 * 1000;
|
|
174
|
+
return elapsedMs < durationMs;
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Stop a demo room
|
|
178
|
+
*/
|
|
179
|
+
async stopDemo(roomId) {
|
|
180
|
+
const state = await this.getDemoState(roomId);
|
|
181
|
+
if (!state) {
|
|
182
|
+
return false;
|
|
183
|
+
}
|
|
184
|
+
await this.updateDemoState(roomId, {
|
|
185
|
+
isActive: false,
|
|
186
|
+
isPaused: false,
|
|
187
|
+
});
|
|
188
|
+
// Remove from active demos set
|
|
189
|
+
await this.redisClient.srem(this.ACTIVE_DEMOS_SET, roomId);
|
|
190
|
+
return true;
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Pause a demo room
|
|
194
|
+
*/
|
|
195
|
+
async pauseDemo(roomId) {
|
|
196
|
+
const state = await this.getDemoState(roomId);
|
|
197
|
+
if (!state) {
|
|
198
|
+
return false;
|
|
199
|
+
}
|
|
200
|
+
await this.updateDemoState(roomId, {
|
|
201
|
+
isPaused: true,
|
|
202
|
+
});
|
|
203
|
+
return true;
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Resume a paused demo room
|
|
207
|
+
*/
|
|
208
|
+
async resumeDemo(roomId) {
|
|
209
|
+
const state = await this.getDemoState(roomId);
|
|
210
|
+
if (!state) {
|
|
211
|
+
return false;
|
|
212
|
+
}
|
|
213
|
+
await this.updateDemoState(roomId, {
|
|
214
|
+
isPaused: false,
|
|
215
|
+
});
|
|
216
|
+
return true;
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Get all active demo room IDs
|
|
220
|
+
*/
|
|
221
|
+
async getActiveDemoRooms() {
|
|
222
|
+
return await this.redisClient.smembers(this.ACTIVE_DEMOS_SET);
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Delete demo state completely
|
|
226
|
+
*/
|
|
227
|
+
async deleteDemoState(roomId) {
|
|
228
|
+
const key = this.getStateKey(roomId);
|
|
229
|
+
const pipeline = this.redisClient.pipeline();
|
|
230
|
+
pipeline.del(key);
|
|
231
|
+
pipeline.srem(this.ACTIVE_DEMOS_SET, roomId);
|
|
232
|
+
await pipeline.exec();
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Get statistics about a demo
|
|
236
|
+
*/
|
|
237
|
+
async getDemoStats(roomId) {
|
|
238
|
+
const state = await this.getDemoState(roomId);
|
|
239
|
+
if (!state) {
|
|
240
|
+
return null;
|
|
241
|
+
}
|
|
242
|
+
const now = Date.now();
|
|
243
|
+
const uptimeMs = now - state.startedAt;
|
|
244
|
+
return {
|
|
245
|
+
totalChunksProcessed: state.loopCount * state.totalChunks + state.currentChunkIndex,
|
|
246
|
+
loopCount: state.loopCount,
|
|
247
|
+
uptimeMinutes: Math.floor(uptimeMs / 1000 / 60),
|
|
248
|
+
isActive: state.isActive,
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* Check if a demo room is currently active
|
|
253
|
+
*/
|
|
254
|
+
async isDemoActive(roomId) {
|
|
255
|
+
const state = await this.getDemoState(roomId);
|
|
256
|
+
return state !== null && state.isActive;
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* Cleanup stale demos (safety mechanism)
|
|
260
|
+
* Call this periodically to remove demos that should have ended
|
|
261
|
+
*/
|
|
262
|
+
async cleanupStaleDemos() {
|
|
263
|
+
const activeDemoIds = await this.getActiveDemoRooms();
|
|
264
|
+
let cleaned = 0;
|
|
265
|
+
for (const roomId of activeDemoIds) {
|
|
266
|
+
const state = await this.getDemoState(roomId);
|
|
267
|
+
if (!state) {
|
|
268
|
+
// State missing but in active set - cleanup
|
|
269
|
+
await this.redisClient.srem(this.ACTIVE_DEMOS_SET, roomId);
|
|
270
|
+
cleaned++;
|
|
271
|
+
continue;
|
|
272
|
+
}
|
|
273
|
+
// Check if demo should still be running
|
|
274
|
+
if (!this.isDemoStillActive(state)) {
|
|
275
|
+
await this.stopDemo(roomId);
|
|
276
|
+
cleaned++;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
return cleaned;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
exports.DemoStateManager = DemoStateManager;
|