@mulingai-npm/redis 3.17.2 → 3.18.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,90 @@
1
+ import { RedisClient } from '../redis-client';
2
+ /**
3
+ * Chunk waiting room entry
4
+ */
5
+ export interface WaitingChunk {
6
+ chunkNumber: number;
7
+ arrivedAt: number;
8
+ language: string;
9
+ }
10
+ /**
11
+ * Sequencer configuration (passed from settings)
12
+ */
13
+ export interface SequencerConfig {
14
+ timeoutMs: number;
15
+ maxWaitingRoom: number;
16
+ maxGapSize: number;
17
+ }
18
+ /**
19
+ * Chunk Sequencer Manager
20
+ * Maintains strict chunk ordering per room per language
21
+ * Holds out-of-order chunks in a waiting room with timeout
22
+ */
23
+ export declare class ChunkSequencerManager {
24
+ private redisClient;
25
+ constructor(redisClient: RedisClient);
26
+ /**
27
+ * Redis key for expected chunk number
28
+ * Format: sequencer:{roomId}:{language}:expectedChunk
29
+ */
30
+ private expectedChunkKey;
31
+ /**
32
+ * Redis key for waiting room (ZSET)
33
+ * Format: sequencer:{roomId}:{language}:waiting
34
+ * Score: arrivalTimestamp
35
+ * Member: chunkNumber
36
+ */
37
+ private waitingRoomKey;
38
+ /**
39
+ * Get the next expected chunk number for a room/language
40
+ * Returns 1 if not initialized yet (first chunk)
41
+ */
42
+ getExpectedChunk(roomId: string, language: string): Promise<number>;
43
+ /**
44
+ * Set the expected chunk number
45
+ */
46
+ setExpectedChunk(roomId: string, language: string, chunkNumber: number): Promise<void>;
47
+ /**
48
+ * Increment the expected chunk number
49
+ */
50
+ incrementExpectedChunk(roomId: string, language: string): Promise<number>;
51
+ /**
52
+ * Add a chunk to the waiting room
53
+ * Uses ZSET with timestamp as score for timeout checking
54
+ */
55
+ addToWaitingRoom(roomId: string, chunkNumber: number, language: string): Promise<void>;
56
+ /**
57
+ * Get all chunks in waiting room sorted by arrival time (oldest first)
58
+ */
59
+ getWaitingChunks(roomId: string, language: string): Promise<WaitingChunk[]>;
60
+ /**
61
+ * Remove a chunk from the waiting room
62
+ */
63
+ removeFromWaitingRoom(roomId: string, chunkNumber: number, language: string): Promise<void>;
64
+ /**
65
+ * Clear the entire waiting room for a room/language
66
+ * Used when skipping ahead (Option C)
67
+ */
68
+ clearWaitingRoom(roomId: string, language: string): Promise<number>;
69
+ /**
70
+ * Get the size of the waiting room
71
+ */
72
+ getWaitingRoomSize(roomId: string, language: string): Promise<number>;
73
+ /**
74
+ * Check if a specific chunk is in the waiting room
75
+ */
76
+ isChunkInWaitingRoom(roomId: string, chunkNumber: number, language: string): Promise<boolean>;
77
+ /**
78
+ * Get chunks that have exceeded the timeout
79
+ */
80
+ getTimedOutChunks(roomId: string, language: string, timeoutMs: number): Promise<WaitingChunk[]>;
81
+ /**
82
+ * Clean up all sequencer state for a room/language
83
+ * Called when room ends
84
+ */
85
+ cleanupSequencerState(roomId: string, language: string): Promise<void>;
86
+ /**
87
+ * Clean up all sequencer state for an entire room (all languages)
88
+ */
89
+ cleanupRoomSequencerState(roomId: string): Promise<void>;
90
+ }
@@ -0,0 +1,156 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ChunkSequencerManager = void 0;
4
+ /**
5
+ * Chunk Sequencer Manager
6
+ * Maintains strict chunk ordering per room per language
7
+ * Holds out-of-order chunks in a waiting room with timeout
8
+ */
9
+ class ChunkSequencerManager {
10
+ constructor(redisClient) {
11
+ this.redisClient = redisClient;
12
+ }
13
+ /**
14
+ * Redis key for expected chunk number
15
+ * Format: sequencer:{roomId}:{language}:expectedChunk
16
+ */
17
+ expectedChunkKey(roomId, language) {
18
+ return `sequencer:${roomId}:${language}:expectedChunk`;
19
+ }
20
+ /**
21
+ * Redis key for waiting room (ZSET)
22
+ * Format: sequencer:{roomId}:{language}:waiting
23
+ * Score: arrivalTimestamp
24
+ * Member: chunkNumber
25
+ */
26
+ waitingRoomKey(roomId, language) {
27
+ return `sequencer:${roomId}:${language}:waiting`;
28
+ }
29
+ /**
30
+ * Get the next expected chunk number for a room/language
31
+ * Returns 1 if not initialized yet (first chunk)
32
+ */
33
+ async getExpectedChunk(roomId, language) {
34
+ const key = this.expectedChunkKey(roomId, language);
35
+ const value = await this.redisClient.get(key);
36
+ if (!value) {
37
+ // Initialize to 1 for first chunk
38
+ await this.redisClient.set(key, '1');
39
+ await this.redisClient.expire(key, 3600); // 1 hour TTL
40
+ return 1;
41
+ }
42
+ return parseInt(value, 10);
43
+ }
44
+ /**
45
+ * Set the expected chunk number
46
+ */
47
+ async setExpectedChunk(roomId, language, chunkNumber) {
48
+ const key = this.expectedChunkKey(roomId, language);
49
+ await this.redisClient.set(key, chunkNumber.toString());
50
+ await this.redisClient.expire(key, 3600); // 1 hour TTL
51
+ }
52
+ /**
53
+ * Increment the expected chunk number
54
+ */
55
+ async incrementExpectedChunk(roomId, language) {
56
+ const key = this.expectedChunkKey(roomId, language);
57
+ const newValue = await this.redisClient.incr(key);
58
+ await this.redisClient.expire(key, 3600); // 1 hour TTL
59
+ return newValue;
60
+ }
61
+ /**
62
+ * Add a chunk to the waiting room
63
+ * Uses ZSET with timestamp as score for timeout checking
64
+ */
65
+ async addToWaitingRoom(roomId, chunkNumber, language) {
66
+ const key = this.waitingRoomKey(roomId, language);
67
+ const now = Date.now();
68
+ await this.redisClient.zadd(key, now, chunkNumber.toString());
69
+ await this.redisClient.expire(key, 3600); // 1 hour TTL
70
+ }
71
+ /**
72
+ * Get all chunks in waiting room sorted by arrival time (oldest first)
73
+ */
74
+ async getWaitingChunks(roomId, language) {
75
+ const key = this.waitingRoomKey(roomId, language);
76
+ // Get all members with scores (ZRANGE with WITHSCORES)
77
+ const results = await this.redisClient.zrangeWithScores(key, 0, -1);
78
+ if (!results || results.length === 0) {
79
+ return [];
80
+ }
81
+ const chunks = [];
82
+ for (const { value, score } of results) {
83
+ chunks.push({
84
+ chunkNumber: parseInt(value, 10),
85
+ arrivedAt: score,
86
+ language
87
+ });
88
+ }
89
+ return chunks;
90
+ }
91
+ /**
92
+ * Remove a chunk from the waiting room
93
+ */
94
+ async removeFromWaitingRoom(roomId, chunkNumber, language) {
95
+ const key = this.waitingRoomKey(roomId, language);
96
+ await this.redisClient.zrem(key, chunkNumber.toString());
97
+ }
98
+ /**
99
+ * Clear the entire waiting room for a room/language
100
+ * Used when skipping ahead (Option C)
101
+ */
102
+ async clearWaitingRoom(roomId, language) {
103
+ const key = this.waitingRoomKey(roomId, language);
104
+ const count = await this.redisClient.zcard(key);
105
+ await this.redisClient.del(key);
106
+ return count;
107
+ }
108
+ /**
109
+ * Get the size of the waiting room
110
+ */
111
+ async getWaitingRoomSize(roomId, language) {
112
+ const key = this.waitingRoomKey(roomId, language);
113
+ return await this.redisClient.zcard(key);
114
+ }
115
+ /**
116
+ * Check if a specific chunk is in the waiting room
117
+ */
118
+ async isChunkInWaitingRoom(roomId, chunkNumber, language) {
119
+ const key = this.waitingRoomKey(roomId, language);
120
+ const score = await this.redisClient.zscore(key, chunkNumber.toString());
121
+ return score !== null;
122
+ }
123
+ /**
124
+ * Get chunks that have exceeded the timeout
125
+ */
126
+ async getTimedOutChunks(roomId, language, timeoutMs) {
127
+ const allWaiting = await this.getWaitingChunks(roomId, language);
128
+ const now = Date.now();
129
+ return allWaiting.filter(chunk => {
130
+ const age = now - chunk.arrivedAt;
131
+ return age > timeoutMs;
132
+ });
133
+ }
134
+ /**
135
+ * Clean up all sequencer state for a room/language
136
+ * Called when room ends
137
+ */
138
+ async cleanupSequencerState(roomId, language) {
139
+ const expectedKey = this.expectedChunkKey(roomId, language);
140
+ const waitingKey = this.waitingRoomKey(roomId, language);
141
+ await this.redisClient.del(expectedKey);
142
+ await this.redisClient.del(waitingKey);
143
+ }
144
+ /**
145
+ * Clean up all sequencer state for an entire room (all languages)
146
+ */
147
+ async cleanupRoomSequencerState(roomId) {
148
+ // Find all sequencer keys for this room
149
+ const pattern = `sequencer:${roomId}:*`;
150
+ const keys = await this.redisClient.keys(pattern);
151
+ if (keys && keys.length > 0) {
152
+ await Promise.all(keys.map(key => this.redisClient.del(key)));
153
+ }
154
+ }
155
+ }
156
+ exports.ChunkSequencerManager = ChunkSequencerManager;
@@ -0,0 +1,121 @@
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
+ /**
116
+ * Cleanup ALL demo states on startup
117
+ * This prevents stale states from service restarts
118
+ * Call this when the service initializes
119
+ */
120
+ cleanupAllDemosOnStartup(): Promise<number>;
121
+ }
@@ -0,0 +1,307 @@
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
+ * Cleanup ALL demo states on startup
283
+ * This prevents stale states from service restarts
284
+ * Call this when the service initializes
285
+ */
286
+ async cleanupAllDemosOnStartup() {
287
+ console.log('[Demo State Manager] Cleaning up all demo states from previous run...');
288
+ const activeDemoIds = await this.getActiveDemoRooms();
289
+ if (activeDemoIds.length === 0) {
290
+ console.log('[Demo State Manager] No stale demo states found');
291
+ return 0;
292
+ }
293
+ console.log(`[Demo State Manager] Found ${activeDemoIds.length} stale demo state(s): ${activeDemoIds.join(', ')}`);
294
+ // Delete all demo states
295
+ const pipeline = this.redisClient.pipeline();
296
+ for (const roomId of activeDemoIds) {
297
+ const key = this.getStateKey(roomId);
298
+ pipeline.del(key);
299
+ }
300
+ // Clear the active set
301
+ pipeline.del(this.ACTIVE_DEMOS_SET);
302
+ await pipeline.exec();
303
+ console.log(`[Demo State Manager] ✅ Cleaned up ${activeDemoIds.length} stale demo state(s)`);
304
+ return activeDemoIds.length;
305
+ }
306
+ }
307
+ exports.DemoStateManager = DemoStateManager;
@@ -108,5 +108,17 @@ export declare class MulingstreamChunkManager {
108
108
  isEmitted?: boolean;
109
109
  }): Promise<MulingstreamChunkData | null>;
110
110
  areTranslationsProcessed(roomId: string, n: number): Promise<boolean>;
111
+ /**
112
+ * @deprecated This function is replaced by ChunkSequencer (5-sequencer-handler.ts)
113
+ *
114
+ * This function tried to sort and emit chunks, but it discarded late-arriving chunks
115
+ * instead of waiting for them with a timeout. The new ChunkSequencer provides:
116
+ * - Waiting room for out-of-order chunks
117
+ * - Configurable timeout (20 seconds)
118
+ * - Option C overflow handling (gap skip)
119
+ * - Per-room-per-language isolation
120
+ *
121
+ * DO NOT USE THIS FUNCTION - use sequenceAndPublish() from 5-sequencer-handler.ts instead
122
+ */
111
123
  getAllReadyTts(roomId: string, lang: string): Promise<MulingstreamChunkData[]>;
112
124
  }
@@ -298,6 +298,18 @@ class MulingstreamChunkManager {
298
298
  const c = await this.getMulingstreamChunkById(roomId, n);
299
299
  return !!c && Object.values(c.translation).every((t) => t.status !== 'INIT');
300
300
  }
301
+ /**
302
+ * @deprecated This function is replaced by ChunkSequencer (5-sequencer-handler.ts)
303
+ *
304
+ * This function tried to sort and emit chunks, but it discarded late-arriving chunks
305
+ * instead of waiting for them with a timeout. The new ChunkSequencer provides:
306
+ * - Waiting room for out-of-order chunks
307
+ * - Configurable timeout (20 seconds)
308
+ * - Option C overflow handling (gap skip)
309
+ * - Per-room-per-language isolation
310
+ *
311
+ * DO NOT USE THIS FUNCTION - use sequenceAndPublish() from 5-sequencer-handler.ts instead
312
+ */
301
313
  async getAllReadyTts(roomId, lang) {
302
314
  var _a;
303
315
  const chunks = (_a = (await this.getMulingstreamChunksByRoom(roomId))) !== null && _a !== void 0 ? _a : [];
@@ -24,6 +24,13 @@ export declare class RedisClient {
24
24
  zremrangebyrank(key: string, start: number, stop: number): Promise<number>;
25
25
  zrangebyscore(key: string, min: number | string, max: number | string): Promise<string[]>;
26
26
  zrem(key: string, ...members: string[]): Promise<number>;
27
+ zrangeWithScores(key: string, start: number, stop: number): Promise<Array<{
28
+ value: string;
29
+ score: number;
30
+ }>>;
31
+ zcard(key: string): Promise<number>;
32
+ zscore(key: string, member: string): Promise<number | null>;
33
+ incr(key: string): Promise<number>;
27
34
  scan(cursor: number, pattern: string, count?: number): Promise<[string, string[]]>;
28
35
  keys(pattern: string): Promise<string[]>;
29
36
  pipeline(): ReturnType<IORedis['pipeline']>;
@@ -67,6 +67,27 @@ class RedisClient {
67
67
  async zrem(key, ...members) {
68
68
  return this.client.zrem(key, members);
69
69
  }
70
+ async zrangeWithScores(key, start, stop) {
71
+ const results = await this.client.zrange(key, start, stop, 'WITHSCORES');
72
+ const parsed = [];
73
+ for (let i = 0; i < results.length; i += 2) {
74
+ parsed.push({
75
+ value: results[i],
76
+ score: parseFloat(results[i + 1])
77
+ });
78
+ }
79
+ return parsed;
80
+ }
81
+ async zcard(key) {
82
+ return this.client.zcard(key);
83
+ }
84
+ async zscore(key, member) {
85
+ const score = await this.client.zscore(key, member);
86
+ return score !== null ? parseFloat(score) : null;
87
+ }
88
+ async incr(key) {
89
+ return this.client.incr(key);
90
+ }
70
91
  async scan(cursor, pattern, count = 1000) {
71
92
  return this.client.scan(cursor, 'MATCH', pattern, 'COUNT', count);
72
93
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mulingai-npm/redis",
3
- "version": "3.17.2",
3
+ "version": "3.18.3",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "repository": {