@mulingai-npm/redis 1.14.2 → 2.0.1

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.
@@ -1,18 +1,13 @@
1
1
  import { RedisClient } from '../redis-client';
2
2
  export type StepStatus = 'INIT' | 'DISCARDED' | 'READY' | 'USED';
3
- export type MulingstreamChunkStatus = 'INIT' | 'STARTED' | 'FINISHED' | 'CANCELED' | 'USED';
4
- export type MulingstreamChunkStep = 'RECEIVED' | 'STT' | 'TRANSLATION' | 'TTS' | 'EMITTED';
5
3
  export type SttProvider = 'azure' | 'whisper' | 'google' | 'aws';
6
4
  export type MulingstreamChunkData = {
7
- mulingstreamChunkId: string;
8
- roomId: string;
9
5
  chunkNumber: number;
10
6
  language: string;
11
7
  sttProviders: SttProvider[];
12
8
  targetLanguages: string[];
13
9
  finalTranscription: string;
14
- mulingstreamChunkStatus: MulingstreamChunkStatus;
15
- mulingstreamChunkStep: MulingstreamChunkStep;
10
+ sttStatus: StepStatus;
16
11
  audioChunk: {
17
12
  start: number;
18
13
  end: number;
@@ -45,7 +40,31 @@ export type MulingstreamChunkData = {
45
40
  export declare class MulingstreamChunkManager {
46
41
  private redisClient;
47
42
  constructor(redisClient: RedisClient);
48
- private parseHashData;
43
+ /**
44
+ * Initializes a room in Redis as an empty JSON array.
45
+ * If the key [roomId] already exists, we do NOT overwrite it.
46
+ * Returns true if room was created, false if room already existed.
47
+ */
48
+ initRoom(roomId: string): Promise<boolean>;
49
+ /**
50
+ * Returns all rooms. This is naive if you store many other keys in Redis,
51
+ * because we search keys for pattern "[*]".
52
+ * Adjust as needed if you have a separate naming prefix.
53
+ */
54
+ getRooms(): Promise<string[]>;
55
+ /**
56
+ * Returns the entire array of chunks for the given roomId,
57
+ * or null if the room doesn't exist in Redis.
58
+ */
59
+ getMulingstreamChunksByRoom(roomId: string): Promise<MulingstreamChunkData[] | null>;
60
+ /**
61
+ * Returns the room array "as is" for debugging
62
+ * (same as getMulingstreamChunksByRoom in this example).
63
+ */
64
+ getRoomById(roomId: string): Promise<MulingstreamChunkData[] | null>;
65
+ /**
66
+ * Adds a new Mulingstream chunk to the array stored at [${roomId}].
67
+ */
49
68
  addMulingstreamChunk(params: {
50
69
  roomId: string;
51
70
  chunkNumber: number;
@@ -58,28 +77,31 @@ export declare class MulingstreamChunkManager {
58
77
  theme: string;
59
78
  sttProviders: SttProvider[];
60
79
  targetLanguages: string[];
61
- }): Promise<string>;
62
- getMulingstreamChunks(): Promise<MulingstreamChunkData[]>;
63
- getMulingstreamChunk(mulingstreamChunkId: string): Promise<MulingstreamChunkData | null>;
64
- getMulingstreamChunksByRoom(roomId: string): Promise<MulingstreamChunkData[]>;
65
- getByChunkNumber(chunkNumber: number, roomId: string): Promise<MulingstreamChunkData | null>;
66
- updateMulingstreamChunkStatus(mulingstreamChunkId: string, mulingstreamChunkStep: MulingstreamChunkStep, mulingstreamChunkStatus: MulingstreamChunkStatus): Promise<boolean>;
67
- updateFinalTranscription(mulingstreamChunkId: string, finalTranscription: string): Promise<boolean>;
68
- updateSttStatus(mulingstreamChunkId: string, service: SttProvider, status: StepStatus): Promise<boolean>;
69
- updateTranscription(mulingstreamChunkId: string, service: SttProvider, transcription: string, sttStatus: StepStatus): Promise<boolean>;
70
- /****
71
- * Discards all post-STT steps for a given chunk, i.e. sets
72
- * all translation[].status and all tts[].status to "DISCARDED".
73
- ****/
74
- discardAllPostStt(mulingstreamChunkId: string): Promise<boolean>;
75
- /****
76
- * Discards all post-translation steps for a given chunk, i.e. sets
77
- * all tts[].status to "DISCARDED" (leaves translation alone).
78
- ****/
79
- discardAllPostTranslation(mulingstreamChunkId: string): Promise<boolean>;
80
- /****
81
- * Discards a specific language in both the translation and tts objects,
82
- * setting status to DISCARDED for just that one language.
83
- ****/
84
- discardLanguage(mulingstreamChunkId: string, language: string): Promise<boolean>;
80
+ }): Promise<void>;
81
+ /**
82
+ * Given roomId and chunkNumber, return the single chunk from the array
83
+ * or null if not found.
84
+ */
85
+ getMulingstreamChunkById(roomId: string, chunkNumber: number): Promise<MulingstreamChunkData | null>;
86
+ /**
87
+ * Update STT fields for a given chunk.
88
+ * If transcription or sttStatus is null, skip that field.
89
+ */
90
+ updateStt(roomId: string, chunkNumber: number, sttProvider: SttProvider, options: {
91
+ transcription?: string;
92
+ sttStatus?: StepStatus;
93
+ }): Promise<boolean>;
94
+ /**
95
+ * Update the final transcription field for a chunk.
96
+ */
97
+ updateFinalTranscription(roomId: string, chunkNumber: number, transcription: string, sttStatus: StepStatus): Promise<boolean>;
98
+ /**
99
+ * Discards all post-STT steps for a given chunk:
100
+ * sets all translation[].status & tts[].status to "DISCARDED".
101
+ */
102
+ discardPostStt(roomId: string, chunkNumber: number): Promise<boolean>;
103
+ /**
104
+ * Discards a specific language in both translation and tts for a chunk.
105
+ */
106
+ discardLanguage(roomId: string, chunkNumber: number, language: string): Promise<boolean>;
85
107
  }
@@ -1,44 +1,70 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.MulingstreamChunkManager = void 0;
4
- const uuid_1 = require("uuid");
5
4
  const EXPIRATION = 12 * 60 * 60; // 12 hours in seconds
5
+ const ROOM_ARRAY_LENGTH = 5; // keep only the last 5 elements
6
6
  class MulingstreamChunkManager {
7
7
  constructor(redisClient) {
8
8
  this.redisClient = redisClient;
9
9
  }
10
- parseHashData(data) {
11
- // Parse the arrays and nested JSON fields
12
- const sttProviders = JSON.parse(data.sttProviders);
13
- const targetLanguages = JSON.parse(data.targetLanguages);
14
- const audioChunk = JSON.parse(data.audioChunk);
15
- const stt = JSON.parse(data.stt);
16
- const translation = JSON.parse(data.translation);
17
- const tts = JSON.parse(data.tts);
18
- return {
19
- mulingstreamChunkId: data.mulingstreamChunkId,
20
- roomId: data.roomId,
21
- chunkNumber: parseInt(data.chunkNumber, 10),
22
- language: data.language,
23
- sttProviders,
24
- targetLanguages,
25
- finalTranscription: data.finalTranscription,
26
- mulingstreamChunkStatus: data.mulingstreamChunkStatus,
27
- mulingstreamChunkStep: data.mulingstreamChunkStep,
28
- audioChunk,
29
- stt,
30
- translation,
31
- tts
32
- };
10
+ /**
11
+ * Initializes a room in Redis as an empty JSON array.
12
+ * If the key [roomId] already exists, we do NOT overwrite it.
13
+ * Returns true if room was created, false if room already existed.
14
+ */
15
+ async initRoom(roomId) {
16
+ // Check if the key already exists (JSON.GET)
17
+ const isRoomExisting = await this.redisClient.jsonGet(`[${roomId}]`, '.');
18
+ if (isRoomExisting !== null) {
19
+ return false;
20
+ }
21
+ // Create an empty array at the root
22
+ await this.redisClient.jsonSet(`[${roomId}]`, '.', []);
23
+ await this.redisClient.expire(`[${roomId}]`, EXPIRATION);
24
+ return true;
25
+ }
26
+ /**
27
+ * Returns all rooms. This is naive if you store many other keys in Redis,
28
+ * because we search keys for pattern "[*]".
29
+ * Adjust as needed if you have a separate naming prefix.
30
+ */
31
+ async getRooms() {
32
+ // e.g., if we store everything as [foo], [bar], etc., we do:
33
+ const rooms = await this.redisClient.keys('\\[*\\]');
34
+ // This returns the raw keys, e.g. ['[myRoom]', '[anotherRoom]']
35
+ // We might want to strip off the brackets:
36
+ return rooms.map((k) => k.replace(/^\[|\]$/g, ''));
37
+ }
38
+ /**
39
+ * Returns the entire array of chunks for the given roomId,
40
+ * or null if the room doesn't exist in Redis.
41
+ */
42
+ async getMulingstreamChunksByRoom(roomId) {
43
+ // JSON.GET [roomId] .
44
+ const chunks = await this.redisClient.jsonGet(`[${roomId}]`, '.');
45
+ if (chunks === null)
46
+ return null; // room not found
47
+ return chunks;
48
+ }
49
+ /**
50
+ * Returns the room array "as is" for debugging
51
+ * (same as getMulingstreamChunksByRoom in this example).
52
+ */
53
+ async getRoomById(roomId) {
54
+ return this.getMulingstreamChunksByRoom(roomId);
33
55
  }
34
- // Adds a new mulingstream chunk.
35
- // - Creates a unique mulingstreamChunkId.
36
- // - Stores the chunk data in a Redis hash under `mulingstreamChunk:{mulingstreamChunkId}`.
37
- // - Applies an expiration (12h).
56
+ /**
57
+ * Adds a new Mulingstream chunk to the array stored at [${roomId}].
58
+ */
38
59
  async addMulingstreamChunk(params) {
39
60
  const { roomId, chunkNumber, language, start, end, duration, isFirst, isLast, theme, sttProviders, targetLanguages } = params;
40
- const mulingstreamChunkId = `[${roomId}]-[${chunkNumber}]-[${(0, uuid_1.v4)()}]`;
41
- // Prepare the nested objects
61
+ // ensure we only keep the last 5 elements
62
+ const currentLength = await this.redisClient.jsonArrPop(`[${roomId}]`, '.', 0);
63
+ if (typeof currentLength === 'number' && currentLength >= ROOM_ARRAY_LENGTH) {
64
+ // remove the oldest (front of the array)
65
+ await this.redisClient.jsonArrPop(`[${roomId}]`, '.', 0);
66
+ }
67
+ // Build the chunk
42
68
  const audioChunk = {
43
69
  start,
44
70
  end,
@@ -48,16 +74,15 @@ class MulingstreamChunkManager {
48
74
  theme,
49
75
  processingStart: Date.now()
50
76
  };
51
- // Build the STT object
52
- // For each service in `services`, create { transcription: '', status: 'INIT' }
77
+ // Build stt object
53
78
  const stt = {};
54
- for (const service of sttProviders) {
55
- stt[service] = {
79
+ for (const svc of sttProviders) {
80
+ stt[svc] = {
56
81
  transcription: '',
57
82
  status: 'INIT'
58
83
  };
59
84
  }
60
- // Build translation & tts objects keyed by each target language
85
+ // Build translation and tts objects
61
86
  const translation = {};
62
87
  const tts = {};
63
88
  for (const lang of targetLanguages) {
@@ -71,217 +96,123 @@ class MulingstreamChunkManager {
71
96
  isEmitted: false
72
97
  };
73
98
  }
74
- // Root-level finalTranscription is empty initially
75
- const finalTranscription = '';
76
- // Store the data in a Redis hash
77
- await this.redisClient.hset(`mulingstreamChunk:${mulingstreamChunkId}`, {
78
- mulingstreamChunkId,
79
- roomId,
80
- chunkNumber: chunkNumber.toString(),
99
+ const newChunk = {
100
+ chunkNumber,
81
101
  language,
82
- sttProviders: JSON.stringify(sttProviders),
83
- targetLanguages: JSON.stringify(targetLanguages),
84
- finalTranscription,
85
- mulingstreamChunkStatus: 'INIT',
86
- mulingstreamChunkStep: 'RECEIVED',
87
- audioChunk: JSON.stringify(audioChunk),
88
- stt: JSON.stringify(stt),
89
- translation: JSON.stringify(translation),
90
- tts: JSON.stringify(tts)
91
- });
92
- // set expiration
93
- await this.redisClient.expire(`mulingstreamChunk:${mulingstreamChunkId}`, EXPIRATION);
94
- return mulingstreamChunkId;
95
- }
96
- // Retrieves all mulingstream chunks by scanning keys `mulingstreamChunk:*`.
97
- async getMulingstreamChunks() {
98
- const keys = await this.redisClient.keys('mulingstreamChunk:*');
99
- if (!keys || keys.length === 0) {
100
- return [];
101
- }
102
- const results = [];
103
- for (const key of keys) {
104
- const data = await this.redisClient.hgetall(key);
105
- if (data && data.mulingstreamChunkId) {
106
- results.push(this.parseHashData(data));
107
- }
108
- }
109
- return results;
110
- }
111
- // Retrieves a single mulingstream chunk by ID.
112
- async getMulingstreamChunk(mulingstreamChunkId) {
113
- const data = await this.redisClient.hgetall(`mulingstreamChunk:${mulingstreamChunkId}`);
114
- if (!data || !data.mulingstreamChunkId) {
115
- return null;
116
- }
117
- return this.parseHashData(data);
118
- }
119
- // Retrieves all mulingstream chunks for a specific room by pattern matching keys `mulingstreamChunk:[roomId]-*`.
120
- async getMulingstreamChunksByRoom(roomId) {
121
- const pattern = `mulingstreamChunk:[${roomId}]-*`;
122
- const keys = await this.redisClient.keys(pattern);
123
- if (!keys || keys.length === 0) {
124
- return [];
125
- }
126
- const results = [];
127
- for (const key of keys) {
128
- const data = await this.redisClient.hgetall(key);
129
- if (!data || !data.mulingstreamChunkId) {
130
- continue;
131
- }
132
- results.push(this.parseHashData(data));
133
- }
134
- return results;
102
+ sttProviders,
103
+ targetLanguages,
104
+ finalTranscription: '',
105
+ sttStatus: 'INIT',
106
+ audioChunk,
107
+ stt,
108
+ translation,
109
+ tts
110
+ };
111
+ // Append it to the array
112
+ await this.redisClient.jsonArrAppend(`[${roomId}]`, '.', // the root of the JSON document (the array)
113
+ newChunk);
135
114
  }
136
- // Retrieves a mulingstream chunk by (roomId, chunkNumber).
137
- // Since mulingstreamChunkId = `[${roomId}]-[${chunkNumber}]-[uuid]`, we can pattern-match on keys.
138
- async getByChunkNumber(chunkNumber, roomId) {
139
- const pattern = `mulingstreamChunk:[${roomId}]-[${chunkNumber}]-*`;
140
- const keys = await this.redisClient.keys(pattern);
141
- if (!keys || keys.length === 0) {
115
+ /**
116
+ * Given roomId and chunkNumber, return the single chunk from the array
117
+ * or null if not found.
118
+ */
119
+ async getMulingstreamChunkById(roomId, chunkNumber) {
120
+ // Retrieve the entire array
121
+ const chunks = await this.getMulingstreamChunksByRoom(roomId);
122
+ if (!chunks)
142
123
  return null;
143
- }
144
- // If multiple match, return the first
145
- const data = await this.redisClient.hgetall(keys[0]);
146
- if (!data || !data.mulingstreamChunkId) {
147
- return null;
148
- }
149
- return this.parseHashData(data);
124
+ // Find by chunkNumber
125
+ const chunk = chunks.find((c) => c.chunkNumber === chunkNumber);
126
+ return chunk || null;
150
127
  }
151
- async updateMulingstreamChunkStatus(mulingstreamChunkId, mulingstreamChunkStep, mulingstreamChunkStatus) {
152
- // Fetch the chunk data first
153
- const data = await this.redisClient.hgetall(`mulingstreamChunk:${mulingstreamChunkId}`);
154
- if (!data || !data.mulingstreamChunkId) {
155
- return false; // The chunk may not exist or has expired
156
- }
157
- // Update only the chunk status and step
158
- await this.redisClient.hset(`mulingstreamChunk:${mulingstreamChunkId}`, {
159
- mulingstreamChunkStatus,
160
- mulingstreamChunkStep
161
- });
162
- return true;
163
- }
164
- async updateFinalTranscription(mulingstreamChunkId, finalTranscription) {
165
- // Fetch the chunk data first
166
- const data = await this.redisClient.hgetall(`mulingstreamChunk:${mulingstreamChunkId}`);
167
- if (!data || !data.mulingstreamChunkId) {
168
- return false; // The chunk may not exist or has expired
169
- }
170
- // Update the finalTranscription field
171
- await this.redisClient.hset(`mulingstreamChunk:${mulingstreamChunkId}`, {
172
- finalTranscription
173
- });
174
- return true;
175
- }
176
- // Updates the STT status for a given service in the chunk's STT data.
177
- async updateSttStatus(mulingstreamChunkId, service, status) {
178
- const data = await this.redisClient.hgetall(`mulingstreamChunk:${mulingstreamChunkId}`);
179
- if (!data || !data.mulingstreamChunkId) {
128
+ /**
129
+ * Update STT fields for a given chunk.
130
+ * If transcription or sttStatus is null, skip that field.
131
+ */
132
+ async updateStt(roomId, chunkNumber, sttProvider, options) {
133
+ const chunks = await this.getMulingstreamChunksByRoom(roomId);
134
+ if (!chunks)
180
135
  return false;
181
- }
182
- const stt = JSON.parse(data.stt);
183
- if (!stt[service]) {
184
- return false; // service not in stt object
185
- }
186
- stt[service].status = status;
187
- await this.redisClient.hset(`mulingstreamChunk:${mulingstreamChunkId}`, {
188
- stt: JSON.stringify(stt)
189
- });
190
- return true;
191
- }
192
- // Updates the transcription + status for a given service in the chunk's STT data.
193
- async updateTranscription(mulingstreamChunkId, service, transcription, sttStatus) {
194
- const data = await this.redisClient.hgetall(`mulingstreamChunk:${mulingstreamChunkId}`);
195
- if (!data || !data.mulingstreamChunkId) {
136
+ // Find the chunk
137
+ const chunkIndex = chunks.findIndex((c) => c.chunkNumber === chunkNumber);
138
+ if (chunkIndex === -1)
139
+ return false;
140
+ const chunk = chunks[chunkIndex];
141
+ // If this stt provider doesn't exist, ignore
142
+ if (!chunk.stt[sttProvider])
196
143
  return false;
144
+ // Update
145
+ if (options.transcription != null) {
146
+ chunk.stt[sttProvider].transcription = options.transcription;
197
147
  }
198
- const stt = JSON.parse(data.stt);
199
- if (!stt[service]) {
200
- return false; // service not in stt object
148
+ if (options.sttStatus != null) {
149
+ chunk.stt[sttProvider].status = options.sttStatus;
201
150
  }
202
- stt[service].transcription = transcription;
203
- stt[service].status = sttStatus;
204
- await this.redisClient.hset(`mulingstreamChunk:${mulingstreamChunkId}`, {
205
- stt: JSON.stringify(stt)
206
- });
151
+ // Write back
152
+ chunks[chunkIndex] = chunk;
153
+ await this.redisClient.jsonSet(`[${roomId}]`, '.', chunks);
207
154
  return true;
208
155
  }
209
- /****
210
- * Discards all post-STT steps for a given chunk, i.e. sets
211
- * all translation[].status and all tts[].status to "DISCARDED".
212
- ****/
213
- async discardAllPostStt(mulingstreamChunkId) {
214
- // Fetch the chunk
215
- const data = await this.redisClient.hgetall(`mulingstreamChunk:${mulingstreamChunkId}`);
216
- if (!data || !data.mulingstreamChunkId) {
156
+ /**
157
+ * Update the final transcription field for a chunk.
158
+ */
159
+ async updateFinalTranscription(roomId, chunkNumber, transcription, sttStatus) {
160
+ const chunks = await this.getMulingstreamChunksByRoom(roomId);
161
+ if (!chunks)
217
162
  return false;
218
- }
219
- // Parse the translation and tts objects
220
- const translation = JSON.parse(data.translation);
221
- const tts = JSON.parse(data.tts);
222
- // Set every language status to DISCARDED in translation
223
- for (const lang of Object.keys(translation)) {
224
- translation[lang].status = 'DISCARDED';
225
- }
226
- // Set every language status to DISCARDED in tts
227
- for (const lang of Object.keys(tts)) {
228
- tts[lang].status = 'DISCARDED';
229
- }
163
+ // Find the chunk
164
+ const chunkIndex = chunks.findIndex((c) => c.chunkNumber === chunkNumber);
165
+ if (chunkIndex === -1)
166
+ return false;
167
+ chunks[chunkIndex].finalTranscription = transcription;
168
+ chunks[chunkIndex].sttStatus = sttStatus;
230
169
  // Write back
231
- await this.redisClient.hset(`mulingstreamChunk:${mulingstreamChunkId}`, {
232
- translation: JSON.stringify(translation),
233
- tts: JSON.stringify(tts)
234
- });
170
+ await this.redisClient.jsonSet(`[${roomId}]`, '.', chunks);
235
171
  return true;
236
172
  }
237
- /****
238
- * Discards all post-translation steps for a given chunk, i.e. sets
239
- * all tts[].status to "DISCARDED" (leaves translation alone).
240
- ****/
241
- async discardAllPostTranslation(mulingstreamChunkId) {
242
- // Fetch the chunk
243
- const data = await this.redisClient.hgetall(`mulingstreamChunk:${mulingstreamChunkId}`);
244
- if (!data || !data.mulingstreamChunkId) {
173
+ /**
174
+ * Discards all post-STT steps for a given chunk:
175
+ * sets all translation[].status & tts[].status to "DISCARDED".
176
+ */
177
+ async discardPostStt(roomId, chunkNumber) {
178
+ const chunks = await this.getMulingstreamChunksByRoom(roomId);
179
+ if (!chunks)
245
180
  return false;
181
+ // Find chunk
182
+ const chunkIndex = chunks.findIndex((c) => c.chunkNumber === chunkNumber);
183
+ if (chunkIndex === -1)
184
+ return false;
185
+ // Discard translation & TTS statuses
186
+ const chunk = chunks[chunkIndex];
187
+ for (const lang of Object.keys(chunk.translation)) {
188
+ chunk.translation[lang].status = 'DISCARDED';
246
189
  }
247
- // Parse the tts object
248
- const tts = JSON.parse(data.tts);
249
- // Set every language status to DISCARDED in tts
250
- for (const lang of Object.keys(tts)) {
251
- tts[lang].status = 'DISCARDED';
190
+ for (const lang of Object.keys(chunk.tts)) {
191
+ chunk.tts[lang].status = 'DISCARDED';
252
192
  }
253
- // Write back
254
- await this.redisClient.hset(`mulingstreamChunk:${mulingstreamChunkId}`, {
255
- tts: JSON.stringify(tts)
256
- });
193
+ chunks[chunkIndex] = chunk;
194
+ await this.redisClient.jsonSet(`[${roomId}]`, '.', chunks);
257
195
  return true;
258
196
  }
259
- /****
260
- * Discards a specific language in both the translation and tts objects,
261
- * setting status to DISCARDED for just that one language.
262
- ****/
263
- async discardLanguage(mulingstreamChunkId, language) {
264
- // Fetch the chunk
265
- const data = await this.redisClient.hgetall(`mulingstreamChunk:${mulingstreamChunkId}`);
266
- if (!data || !data.mulingstreamChunkId) {
197
+ /**
198
+ * Discards a specific language in both translation and tts for a chunk.
199
+ */
200
+ async discardLanguage(roomId, chunkNumber, language) {
201
+ const chunks = await this.getMulingstreamChunksByRoom(roomId);
202
+ if (!chunks)
267
203
  return false;
204
+ const chunkIndex = chunks.findIndex((c) => c.chunkNumber === chunkNumber);
205
+ if (chunkIndex === -1)
206
+ return false;
207
+ const chunk = chunks[chunkIndex];
208
+ if (chunk.translation[language]) {
209
+ chunk.translation[language].status = 'DISCARDED';
268
210
  }
269
- // Parse the translation and tts objects
270
- const translation = JSON.parse(data.translation);
271
- const tts = JSON.parse(data.tts);
272
- // If the language exists in translation, discard
273
- if (translation[language]) {
274
- translation[language].status = 'DISCARDED';
275
- }
276
- // If the language exists in tts, discard
277
- if (tts[language]) {
278
- tts[language].status = 'DISCARDED';
211
+ if (chunk.tts[language]) {
212
+ chunk.tts[language].status = 'DISCARDED';
279
213
  }
280
- // Write back
281
- await this.redisClient.hset(`mulingstreamChunk:${mulingstreamChunkId}`, {
282
- translation: JSON.stringify(translation),
283
- tts: JSON.stringify(tts)
284
- });
214
+ chunks[chunkIndex] = chunk;
215
+ await this.redisClient.jsonSet(`[${roomId}]`, '.', chunks);
285
216
  return true;
286
217
  }
287
218
  }
@@ -21,4 +21,9 @@ export declare class MulingstreamListenerManager {
21
21
  updateNameLanguage(listenerIdOrToken: string, name: string, language: string): Promise<boolean>;
22
22
  updateSocketId(listenerIdOrToken: string, socketId: string): Promise<boolean>;
23
23
  getTargetSocketIdsByRoomLanguage(roomId: string, language: string): Promise<string[]>;
24
+ /**
25
+ * Returns an array of unique languages for the given room, from highest frequency to lowest.
26
+ * If a listener has no language (empty string / undefined), it is ignored.
27
+ */
28
+ getUniqueLanguagesByRoom(roomId: string): Promise<string[]>;
24
29
  }
@@ -139,5 +139,31 @@ class MulingstreamListenerManager {
139
139
  });
140
140
  return filteredListeners.map((listener) => listener.socketId);
141
141
  }
142
+ /**
143
+ * Returns an array of unique languages for the given room, from highest frequency to lowest.
144
+ * If a listener has no language (empty string / undefined), it is ignored.
145
+ */
146
+ async getUniqueLanguagesByRoom(roomId) {
147
+ // 1) Fetch all listeners for the room.
148
+ // (This includes inactive ones, but feel free to filter out isActive === false if you only want active ones.)
149
+ const listeners = await this.getListenersByRoom(roomId);
150
+ // if I wanted to get only active listeners
151
+ // const listeners = (await this.getListenersByRoom(roomId)).filter(l => l.isActive);
152
+ // 2) Count how many times each language appears.
153
+ const languageCountMap = {};
154
+ for (const listener of listeners) {
155
+ // skip blank/unset language
156
+ if (!listener.language || !listener.language.trim()) {
157
+ continue;
158
+ }
159
+ // increment count
160
+ const lang = listener.language;
161
+ languageCountMap[lang] = (languageCountMap[lang] || 0) + 1;
162
+ }
163
+ // 3) Sort by frequency descending: highest count first
164
+ const sortedEntries = Object.entries(languageCountMap).sort(([, countA], [, countB]) => countB - countA);
165
+ // 4) Map back to just the language strings in order
166
+ return sortedEntries.map(([language]) => language);
167
+ }
142
168
  }
143
169
  exports.MulingstreamListenerManager = MulingstreamListenerManager;
@@ -19,4 +19,9 @@ export declare class RedisClient {
19
19
  flushAll(): Promise<string>;
20
20
  flushDb(): Promise<string>;
21
21
  expire(key: string, seconds: number): Promise<number>;
22
+ jsonSet<T>(key: string, path: string, value: T): Promise<string>;
23
+ jsonGet<T>(key: string, path: string): Promise<T | null>;
24
+ jsonDel(key: string, path: string): Promise<number>;
25
+ jsonArrAppend<T>(key: string, path: string, ...values: T[]): Promise<number>;
26
+ jsonArrPop(key: string, path: string, index?: number): Promise<any>;
22
27
  }
@@ -61,5 +61,31 @@ class RedisClient {
61
61
  async expire(key, seconds) {
62
62
  return this.client.expire(key, seconds);
63
63
  }
64
+ async jsonSet(key, path, value) {
65
+ const result = await this.client.call('JSON.SET', key, path, JSON.stringify(value));
66
+ return result;
67
+ }
68
+ async jsonGet(key, path) {
69
+ const result = (await this.client.call('JSON.GET', key, path));
70
+ if (!result) {
71
+ return null;
72
+ }
73
+ return JSON.parse(result);
74
+ }
75
+ async jsonDel(key, path) {
76
+ const result = await this.client.call('JSON.DEL', key, path);
77
+ return Number(result);
78
+ }
79
+ async jsonArrAppend(key, path, ...values) {
80
+ const stringifiedValues = values.map((v) => JSON.stringify(v));
81
+ const result = await this.client.call('JSON.ARRAPPEND', key, path, ...stringifiedValues);
82
+ return Number(result);
83
+ }
84
+ async jsonArrPop(key, path, index) {
85
+ if (index !== undefined) {
86
+ return this.client.call('JSON.ARRPOP', key, path, index);
87
+ }
88
+ return this.client.call('JSON.ARRPOP', key, path);
89
+ }
64
90
  }
65
91
  exports.RedisClient = RedisClient;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mulingai-npm/redis",
3
- "version": "1.14.2",
3
+ "version": "2.0.1",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "repository": {