@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.
- package/dist/managers/chunk-sequencer-manager.d.ts +90 -0
- package/dist/managers/chunk-sequencer-manager.js +156 -0
- package/dist/managers/demo-state-manager.d.ts +6 -0
- package/dist/managers/demo-state-manager.js +25 -0
- package/dist/managers/mulingstream-chunk-manager.d.ts +12 -0
- package/dist/managers/mulingstream-chunk-manager.js +12 -0
- package/dist/redis-client.d.ts +7 -0
- package/dist/redis-client.js +21 -0
- package/package.json +1 -1
|
@@ -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 : [];
|
package/dist/redis-client.d.ts
CHANGED
|
@@ -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']>;
|
package/dist/redis-client.js
CHANGED
|
@@ -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
|
}
|