@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.
- 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 +121 -0
- package/dist/managers/demo-state-manager.js +307 -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;
|
|
@@ -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 : [];
|
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
|
}
|