@mulingai-npm/redis 3.18.0 → 3.18.4

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;
@@ -112,4 +112,10 @@ export declare class DemoStateManager {
112
112
  * Call this periodically to remove demos that should have ended
113
113
  */
114
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>;
115
121
  }
@@ -278,5 +278,30 @@ class DemoStateManager {
278
278
  }
279
279
  return cleaned;
280
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
+ }
281
306
  }
282
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.18.0",
3
+ "version": "3.18.4",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "repository": {