@mulingai-npm/redis 2.3.1 → 2.5.0
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/enums/redis-database.d.ts +5 -4
- package/dist/enums/redis-database.js +9 -8
- package/dist/managers/mulingstream-chunk-manager.d.ts +107 -107
- package/dist/managers/mulingstream-chunk-manager.js +242 -242
- package/dist/managers/mulingstream-listener-manager.d.ts +29 -29
- package/dist/managers/mulingstream-listener-manager.js +169 -169
- package/dist/managers/mulingstream-speaker-manager.d.ts +36 -0
- package/dist/managers/mulingstream-speaker-manager.js +224 -0
- package/dist/redis-client.d.ts +28 -28
- package/dist/redis-client.js +96 -95
- package/package.json +34 -34
|
@@ -1,169 +1,169 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.MulingstreamListenerManager = void 0;
|
|
4
|
-
const uuid_1 = require("uuid");
|
|
5
|
-
const EXPIRATION = 24 * 60 * 60; // 24 hours in seconds
|
|
6
|
-
class MulingstreamListenerManager {
|
|
7
|
-
constructor(redisClient) {
|
|
8
|
-
this.redisClient = redisClient;
|
|
9
|
-
}
|
|
10
|
-
parseHashData(data) {
|
|
11
|
-
return {
|
|
12
|
-
listenerId: data.listenerId,
|
|
13
|
-
roomId: data.roomId,
|
|
14
|
-
socketId: data.socketId || '',
|
|
15
|
-
token: data.token,
|
|
16
|
-
name: data.name || '',
|
|
17
|
-
firstJoined: parseInt(data.firstJoined, 10),
|
|
18
|
-
language: data.language || '',
|
|
19
|
-
isActive: data.isActive === 'true'
|
|
20
|
-
};
|
|
21
|
-
}
|
|
22
|
-
// Creates a new listener.
|
|
23
|
-
// -Generates a unique listenerId and adds it to the room's set.
|
|
24
|
-
// -Stores the listener data in a Redis hash under `listener:{listenerId}`.
|
|
25
|
-
async addListener(listenerData) {
|
|
26
|
-
var _a, _b, _c;
|
|
27
|
-
// You can combine roomId + uuid, or just a uuid, up to you:
|
|
28
|
-
const listenerId = `[${listenerData.roomId}]-[${(0, uuid_1.v4)()}]`;
|
|
29
|
-
// Add listenerId to the set for that room
|
|
30
|
-
await this.redisClient.sadd(`room:${listenerData.roomId}:listeners`, listenerId);
|
|
31
|
-
// Store listener data in a hash
|
|
32
|
-
await this.redisClient.hset(`listener:${listenerId}`, {
|
|
33
|
-
listenerId,
|
|
34
|
-
roomId: listenerData.roomId,
|
|
35
|
-
socketId: (_a = listenerData.socketId) !== null && _a !== void 0 ? _a : '',
|
|
36
|
-
token: listenerData.token,
|
|
37
|
-
name: (_b = listenerData.name) !== null && _b !== void 0 ? _b : '',
|
|
38
|
-
firstJoined: listenerData.firstJoined.toString(),
|
|
39
|
-
language: (_c = listenerData.language) !== null && _c !== void 0 ? _c : '',
|
|
40
|
-
isActive: 'true'
|
|
41
|
-
});
|
|
42
|
-
// expire listener
|
|
43
|
-
await this.redisClient.expire(`listener:${listenerId}`, EXPIRATION);
|
|
44
|
-
return listenerId;
|
|
45
|
-
}
|
|
46
|
-
async getAllListeners() {
|
|
47
|
-
// get all keys that match 'listener:*'
|
|
48
|
-
// TODO: if we have many keys, consider using SCAN instead of KEYS to avoid performance issues.
|
|
49
|
-
const keys = await this.redisClient.keys('listener:*');
|
|
50
|
-
if (!keys || keys.length === 0) {
|
|
51
|
-
return [];
|
|
52
|
-
}
|
|
53
|
-
// fetch each hash with HGETALL
|
|
54
|
-
const listeners = [];
|
|
55
|
-
for (const key of keys) {
|
|
56
|
-
const data = await this.redisClient.hgetall(key);
|
|
57
|
-
listeners.push(this.parseHashData(data));
|
|
58
|
-
}
|
|
59
|
-
return listeners;
|
|
60
|
-
}
|
|
61
|
-
async getListenersByRoom(roomId) {
|
|
62
|
-
// get the set of listener IDs
|
|
63
|
-
const listenerIds = await this.redisClient.smembers(`room:${roomId}:listeners`);
|
|
64
|
-
if (!listenerIds || listenerIds.length === 0) {
|
|
65
|
-
return [];
|
|
66
|
-
}
|
|
67
|
-
const listeners = [];
|
|
68
|
-
for (const id of listenerIds) {
|
|
69
|
-
// get hash data
|
|
70
|
-
const data = await this.redisClient.hgetall(`listener:${id}`);
|
|
71
|
-
// if the hash doesn't exist, it may have expired, so remove from the set
|
|
72
|
-
if (!data || Object.keys(data).length === 0) {
|
|
73
|
-
await this.redisClient.srem(`room:${roomId}:listeners`, id);
|
|
74
|
-
continue;
|
|
75
|
-
}
|
|
76
|
-
// otherwise, parse and keep it
|
|
77
|
-
listeners.push(this.parseHashData(data));
|
|
78
|
-
}
|
|
79
|
-
return listeners;
|
|
80
|
-
}
|
|
81
|
-
async getListener(listenerIdOrToken) {
|
|
82
|
-
// try direct lookup by listenerId:
|
|
83
|
-
const directData = await this.redisClient.hgetall(`listener:${listenerIdOrToken}`);
|
|
84
|
-
if (directData && directData.listenerId) {
|
|
85
|
-
return this.parseHashData(directData);
|
|
86
|
-
}
|
|
87
|
-
// if not found by listenerId, treat 'listenerIdOrToken' as a token and scan all listeners
|
|
88
|
-
const keys = await this.redisClient.keys('listener:*');
|
|
89
|
-
if (!keys || keys.length === 0) {
|
|
90
|
-
return null;
|
|
91
|
-
}
|
|
92
|
-
for (const key of keys) {
|
|
93
|
-
const data = await this.redisClient.hgetall(key);
|
|
94
|
-
if (data && data.token === listenerIdOrToken) {
|
|
95
|
-
return this.parseHashData(data);
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
return null;
|
|
99
|
-
}
|
|
100
|
-
async removeListenerByToken(token) {
|
|
101
|
-
// find the listener by token
|
|
102
|
-
const listener = await this.getListener(token);
|
|
103
|
-
if (!listener) {
|
|
104
|
-
return false;
|
|
105
|
-
}
|
|
106
|
-
// remove the listener ID from the room's set
|
|
107
|
-
await this.redisClient.srem(`room:${listener.roomId}:listeners`, listener.listenerId);
|
|
108
|
-
// remove the listener data (the hash)
|
|
109
|
-
await this.redisClient.del(`listener:${listener.listenerId}`);
|
|
110
|
-
return true;
|
|
111
|
-
}
|
|
112
|
-
async updateNameLanguage(listenerIdOrToken, name, language) {
|
|
113
|
-
const listener = await this.getListener(listenerIdOrToken);
|
|
114
|
-
if (!listener) {
|
|
115
|
-
return false;
|
|
116
|
-
}
|
|
117
|
-
// Update the name/language on the listener’s hash key
|
|
118
|
-
await this.redisClient.hset(`listener:${listener.listenerId}`, {
|
|
119
|
-
name,
|
|
120
|
-
language
|
|
121
|
-
});
|
|
122
|
-
return true;
|
|
123
|
-
}
|
|
124
|
-
async updateSocketId(listenerIdOrToken, socketId) {
|
|
125
|
-
const listener = await this.getListener(listenerIdOrToken);
|
|
126
|
-
if (!listener) {
|
|
127
|
-
return false;
|
|
128
|
-
}
|
|
129
|
-
// Update the name/language on the listener’s hash key
|
|
130
|
-
await this.redisClient.hset(`listener:${listener.listenerId}`, {
|
|
131
|
-
socketId
|
|
132
|
-
});
|
|
133
|
-
return true;
|
|
134
|
-
}
|
|
135
|
-
async getTargetSocketIdsByRoomLanguage(roomId, language) {
|
|
136
|
-
const listeners = await this.getListenersByRoom(roomId);
|
|
137
|
-
const filteredListeners = listeners.filter((listener) => {
|
|
138
|
-
return listener.isActive === true && listener.language === language && listener.socketId && listener.socketId.trim() !== '';
|
|
139
|
-
});
|
|
140
|
-
return filteredListeners.map((listener) => listener.socketId);
|
|
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
|
-
}
|
|
168
|
-
}
|
|
169
|
-
exports.MulingstreamListenerManager = MulingstreamListenerManager;
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.MulingstreamListenerManager = void 0;
|
|
4
|
+
const uuid_1 = require("uuid");
|
|
5
|
+
const EXPIRATION = 24 * 60 * 60; // 24 hours in seconds
|
|
6
|
+
class MulingstreamListenerManager {
|
|
7
|
+
constructor(redisClient) {
|
|
8
|
+
this.redisClient = redisClient;
|
|
9
|
+
}
|
|
10
|
+
parseHashData(data) {
|
|
11
|
+
return {
|
|
12
|
+
listenerId: data.listenerId,
|
|
13
|
+
roomId: data.roomId,
|
|
14
|
+
socketId: data.socketId || '',
|
|
15
|
+
token: data.token,
|
|
16
|
+
name: data.name || '',
|
|
17
|
+
firstJoined: parseInt(data.firstJoined, 10),
|
|
18
|
+
language: data.language || '',
|
|
19
|
+
isActive: data.isActive === 'true'
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
// Creates a new listener.
|
|
23
|
+
// -Generates a unique listenerId and adds it to the room's set.
|
|
24
|
+
// -Stores the listener data in a Redis hash under `listener:{listenerId}`.
|
|
25
|
+
async addListener(listenerData) {
|
|
26
|
+
var _a, _b, _c;
|
|
27
|
+
// You can combine roomId + uuid, or just a uuid, up to you:
|
|
28
|
+
const listenerId = `[${listenerData.roomId}]-[${(0, uuid_1.v4)()}]`;
|
|
29
|
+
// Add listenerId to the set for that room
|
|
30
|
+
await this.redisClient.sadd(`room:${listenerData.roomId}:listeners`, listenerId);
|
|
31
|
+
// Store listener data in a hash
|
|
32
|
+
await this.redisClient.hset(`listener:${listenerId}`, {
|
|
33
|
+
listenerId,
|
|
34
|
+
roomId: listenerData.roomId,
|
|
35
|
+
socketId: (_a = listenerData.socketId) !== null && _a !== void 0 ? _a : '',
|
|
36
|
+
token: listenerData.token,
|
|
37
|
+
name: (_b = listenerData.name) !== null && _b !== void 0 ? _b : '',
|
|
38
|
+
firstJoined: listenerData.firstJoined.toString(),
|
|
39
|
+
language: (_c = listenerData.language) !== null && _c !== void 0 ? _c : '',
|
|
40
|
+
isActive: 'true'
|
|
41
|
+
});
|
|
42
|
+
// expire listener
|
|
43
|
+
await this.redisClient.expire(`listener:${listenerId}`, EXPIRATION);
|
|
44
|
+
return listenerId;
|
|
45
|
+
}
|
|
46
|
+
async getAllListeners() {
|
|
47
|
+
// get all keys that match 'listener:*'
|
|
48
|
+
// TODO: if we have many keys, consider using SCAN instead of KEYS to avoid performance issues.
|
|
49
|
+
const keys = await this.redisClient.keys('listener:*');
|
|
50
|
+
if (!keys || keys.length === 0) {
|
|
51
|
+
return [];
|
|
52
|
+
}
|
|
53
|
+
// fetch each hash with HGETALL
|
|
54
|
+
const listeners = [];
|
|
55
|
+
for (const key of keys) {
|
|
56
|
+
const data = await this.redisClient.hgetall(key);
|
|
57
|
+
listeners.push(this.parseHashData(data));
|
|
58
|
+
}
|
|
59
|
+
return listeners;
|
|
60
|
+
}
|
|
61
|
+
async getListenersByRoom(roomId) {
|
|
62
|
+
// get the set of listener IDs
|
|
63
|
+
const listenerIds = await this.redisClient.smembers(`room:${roomId}:listeners`);
|
|
64
|
+
if (!listenerIds || listenerIds.length === 0) {
|
|
65
|
+
return [];
|
|
66
|
+
}
|
|
67
|
+
const listeners = [];
|
|
68
|
+
for (const id of listenerIds) {
|
|
69
|
+
// get hash data
|
|
70
|
+
const data = await this.redisClient.hgetall(`listener:${id}`);
|
|
71
|
+
// if the hash doesn't exist, it may have expired, so remove from the set
|
|
72
|
+
if (!data || Object.keys(data).length === 0) {
|
|
73
|
+
await this.redisClient.srem(`room:${roomId}:listeners`, id);
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
// otherwise, parse and keep it
|
|
77
|
+
listeners.push(this.parseHashData(data));
|
|
78
|
+
}
|
|
79
|
+
return listeners;
|
|
80
|
+
}
|
|
81
|
+
async getListener(listenerIdOrToken) {
|
|
82
|
+
// try direct lookup by listenerId:
|
|
83
|
+
const directData = await this.redisClient.hgetall(`listener:${listenerIdOrToken}`);
|
|
84
|
+
if (directData && directData.listenerId) {
|
|
85
|
+
return this.parseHashData(directData);
|
|
86
|
+
}
|
|
87
|
+
// if not found by listenerId, treat 'listenerIdOrToken' as a token and scan all listeners
|
|
88
|
+
const keys = await this.redisClient.keys('listener:*');
|
|
89
|
+
if (!keys || keys.length === 0) {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
for (const key of keys) {
|
|
93
|
+
const data = await this.redisClient.hgetall(key);
|
|
94
|
+
if (data && data.token === listenerIdOrToken) {
|
|
95
|
+
return this.parseHashData(data);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
async removeListenerByToken(token) {
|
|
101
|
+
// find the listener by token
|
|
102
|
+
const listener = await this.getListener(token);
|
|
103
|
+
if (!listener) {
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
// remove the listener ID from the room's set
|
|
107
|
+
await this.redisClient.srem(`room:${listener.roomId}:listeners`, listener.listenerId);
|
|
108
|
+
// remove the listener data (the hash)
|
|
109
|
+
await this.redisClient.del(`listener:${listener.listenerId}`);
|
|
110
|
+
return true;
|
|
111
|
+
}
|
|
112
|
+
async updateNameLanguage(listenerIdOrToken, name, language) {
|
|
113
|
+
const listener = await this.getListener(listenerIdOrToken);
|
|
114
|
+
if (!listener) {
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
// Update the name/language on the listener’s hash key
|
|
118
|
+
await this.redisClient.hset(`listener:${listener.listenerId}`, {
|
|
119
|
+
name,
|
|
120
|
+
language
|
|
121
|
+
});
|
|
122
|
+
return true;
|
|
123
|
+
}
|
|
124
|
+
async updateSocketId(listenerIdOrToken, socketId) {
|
|
125
|
+
const listener = await this.getListener(listenerIdOrToken);
|
|
126
|
+
if (!listener) {
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
129
|
+
// Update the name/language on the listener’s hash key
|
|
130
|
+
await this.redisClient.hset(`listener:${listener.listenerId}`, {
|
|
131
|
+
socketId
|
|
132
|
+
});
|
|
133
|
+
return true;
|
|
134
|
+
}
|
|
135
|
+
async getTargetSocketIdsByRoomLanguage(roomId, language) {
|
|
136
|
+
const listeners = await this.getListenersByRoom(roomId);
|
|
137
|
+
const filteredListeners = listeners.filter((listener) => {
|
|
138
|
+
return listener.isActive === true && listener.language === language && listener.socketId && listener.socketId.trim() !== '';
|
|
139
|
+
});
|
|
140
|
+
return filteredListeners.map((listener) => listener.socketId);
|
|
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
|
+
}
|
|
168
|
+
}
|
|
169
|
+
exports.MulingstreamListenerManager = MulingstreamListenerManager;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { RedisClient } from '../redis-client';
|
|
2
|
+
export type MulingstreamSpeakerData = {
|
|
3
|
+
speakerId: string;
|
|
4
|
+
userId: string;
|
|
5
|
+
roomId: string;
|
|
6
|
+
socketId: string;
|
|
7
|
+
sourceLanguage: string;
|
|
8
|
+
theme: string;
|
|
9
|
+
prompts: string;
|
|
10
|
+
recordingsDuration: number;
|
|
11
|
+
targetLanguages: string[];
|
|
12
|
+
timestamp: string;
|
|
13
|
+
};
|
|
14
|
+
export declare class MulingstreamSpeakerManager {
|
|
15
|
+
private redisClient;
|
|
16
|
+
constructor(redisClient: RedisClient);
|
|
17
|
+
private parseHash;
|
|
18
|
+
private serialize;
|
|
19
|
+
private buildId;
|
|
20
|
+
private buildKey;
|
|
21
|
+
addSpeaker(payload: Omit<MulingstreamSpeakerData, 'speakerId' | 'timestamp'>): Promise<string>;
|
|
22
|
+
removeSpeakerBySocketId(socketId: string): Promise<boolean>;
|
|
23
|
+
removeSpeakersByUserId(userId: string): Promise<number>;
|
|
24
|
+
removeSpeakersByRoomId(roomId: string): Promise<number>;
|
|
25
|
+
/** internal: remove hash + all index references */
|
|
26
|
+
private removeSpeakerById;
|
|
27
|
+
getSpeakerBySpeakerId(speakerId: string): Promise<MulingstreamSpeakerData | null>;
|
|
28
|
+
getSpeakerBySocketId(socketId: string): Promise<MulingstreamSpeakerData | null>;
|
|
29
|
+
getSpeakersByRoomId(roomId: string): Promise<MulingstreamSpeakerData[]>;
|
|
30
|
+
getSpeakersByUserId(userId: string): Promise<MulingstreamSpeakerData[]>;
|
|
31
|
+
getSpeakerByUserRoom(roomId: string, userId: string): Promise<MulingstreamSpeakerData | null>;
|
|
32
|
+
updateSourceLanguage(socketId: string, newLang: string): Promise<boolean>;
|
|
33
|
+
updateTargetLanguages(socketId: string, languages: string[]): Promise<boolean>;
|
|
34
|
+
increaseRecordingDuration(socketId: string, delta: number): Promise<number>;
|
|
35
|
+
private cleanIndexes;
|
|
36
|
+
}
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.MulingstreamSpeakerManager = void 0;
|
|
4
|
+
const EXPIRATION = 24 * 60 * 60; // 24h
|
|
5
|
+
class MulingstreamSpeakerManager {
|
|
6
|
+
constructor(redisClient) {
|
|
7
|
+
this.redisClient = redisClient;
|
|
8
|
+
}
|
|
9
|
+
/* ------------------------------------------------ helpers */
|
|
10
|
+
parseHash(hash) {
|
|
11
|
+
return {
|
|
12
|
+
speakerId: hash.speakerId,
|
|
13
|
+
userId: hash.userId,
|
|
14
|
+
roomId: hash.roomId,
|
|
15
|
+
socketId: hash.socketId,
|
|
16
|
+
sourceLanguage: hash.sourceLanguage,
|
|
17
|
+
theme: hash.theme,
|
|
18
|
+
prompts: hash.prompts,
|
|
19
|
+
recordingsDuration: parseInt(hash.recordingsDuration, 10) || 0,
|
|
20
|
+
targetLanguages: JSON.parse(hash.targetLanguages || '[]'),
|
|
21
|
+
timestamp: hash.timestamp
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
serialize(data) {
|
|
25
|
+
return {
|
|
26
|
+
speakerId: data.speakerId,
|
|
27
|
+
userId: data.userId,
|
|
28
|
+
roomId: data.roomId,
|
|
29
|
+
socketId: data.socketId,
|
|
30
|
+
sourceLanguage: data.sourceLanguage,
|
|
31
|
+
theme: data.theme,
|
|
32
|
+
prompts: data.prompts,
|
|
33
|
+
recordingsDuration: data.recordingsDuration.toString(),
|
|
34
|
+
targetLanguages: JSON.stringify(data.targetLanguages),
|
|
35
|
+
timestamp: data.timestamp
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
buildId(roomId, userId, socketId) {
|
|
39
|
+
return `[${roomId}]-[${userId}]-[${socketId}]`;
|
|
40
|
+
}
|
|
41
|
+
buildKey(speakerId) {
|
|
42
|
+
return `speaker:${speakerId}`;
|
|
43
|
+
}
|
|
44
|
+
/* ------------------------------------------------ insert */
|
|
45
|
+
async addSpeaker(payload) {
|
|
46
|
+
const speakerId = this.buildId(payload.roomId, payload.userId, payload.socketId);
|
|
47
|
+
const key = this.buildKey(speakerId);
|
|
48
|
+
const speakerData = {
|
|
49
|
+
...payload,
|
|
50
|
+
speakerId,
|
|
51
|
+
timestamp: new Date(Date.now()).toISOString()
|
|
52
|
+
};
|
|
53
|
+
await this.redisClient.hset(key, this.serialize(speakerData));
|
|
54
|
+
await this.redisClient.expire(key, EXPIRATION);
|
|
55
|
+
// index sets / mapping
|
|
56
|
+
await this.redisClient.sadd(`room:${payload.roomId}:speakers`, speakerId);
|
|
57
|
+
await this.redisClient.sadd(`user:${payload.userId}:speakers`, speakerId);
|
|
58
|
+
await this.redisClient.set(`socket:${payload.socketId}:speaker`, speakerId);
|
|
59
|
+
await this.redisClient.expire(`socket:${payload.socketId}:speaker`, EXPIRATION);
|
|
60
|
+
return speakerId;
|
|
61
|
+
}
|
|
62
|
+
/* ------------------------------------------------ remove helpers */
|
|
63
|
+
async removeSpeakerBySocketId(socketId) {
|
|
64
|
+
const speakerId = await this.redisClient.get(`socket:${socketId}:speaker`);
|
|
65
|
+
if (speakerId === null) {
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
const removed = await this.removeSpeakerById(speakerId);
|
|
69
|
+
return removed;
|
|
70
|
+
}
|
|
71
|
+
async removeSpeakersByUserId(userId) {
|
|
72
|
+
const ids = await this.redisClient.smembers(`user:${userId}:speakers`);
|
|
73
|
+
if (ids.length === 0) {
|
|
74
|
+
return 0;
|
|
75
|
+
}
|
|
76
|
+
let deletedCount = 0;
|
|
77
|
+
for (const id of ids) {
|
|
78
|
+
if (await this.removeSpeakerById(id)) {
|
|
79
|
+
deletedCount += 1;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return deletedCount;
|
|
83
|
+
}
|
|
84
|
+
async removeSpeakersByRoomId(roomId) {
|
|
85
|
+
const ids = await this.redisClient.smembers(`room:${roomId}:speakers`);
|
|
86
|
+
if (ids.length === 0) {
|
|
87
|
+
return 0;
|
|
88
|
+
}
|
|
89
|
+
let deletedCount = 0;
|
|
90
|
+
for (const id of ids) {
|
|
91
|
+
if (await this.removeSpeakerById(id)) {
|
|
92
|
+
deletedCount += 1;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return deletedCount;
|
|
96
|
+
}
|
|
97
|
+
/** internal: remove hash + all index references */
|
|
98
|
+
async removeSpeakerById(speakerId) {
|
|
99
|
+
const key = this.buildKey(speakerId);
|
|
100
|
+
const data = await this.redisClient.hgetall(key);
|
|
101
|
+
if (data === null || Object.keys(data).length === 0) {
|
|
102
|
+
// already gone; clean indexes anyway
|
|
103
|
+
await this.cleanIndexes(speakerId);
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
await this.redisClient.del(key);
|
|
107
|
+
await this.cleanIndexes(speakerId);
|
|
108
|
+
return true;
|
|
109
|
+
}
|
|
110
|
+
/* ------------------------------------------------ lookup helpers */
|
|
111
|
+
async getSpeakerBySpeakerId(speakerId) {
|
|
112
|
+
const key = this.buildKey(speakerId);
|
|
113
|
+
const hash = await this.redisClient.hgetall(key);
|
|
114
|
+
if (hash === null || Object.keys(hash).length === 0) {
|
|
115
|
+
/* the hash has expired or was never there – remove any stray index refs */
|
|
116
|
+
await this.cleanIndexes(speakerId);
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
return this.parseHash(hash);
|
|
120
|
+
}
|
|
121
|
+
async getSpeakerBySocketId(socketId) {
|
|
122
|
+
const speakerId = await this.redisClient.get(`socket:${socketId}:speaker`);
|
|
123
|
+
if (speakerId === null) {
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
const hash = await this.redisClient.hgetall(this.buildKey(speakerId));
|
|
127
|
+
if (hash !== null && Object.keys(hash).length > 0) {
|
|
128
|
+
return this.parseHash(hash);
|
|
129
|
+
}
|
|
130
|
+
// hash expired: tidy indexes
|
|
131
|
+
await this.cleanIndexes(speakerId);
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
async getSpeakersByRoomId(roomId) {
|
|
135
|
+
const ids = await this.redisClient.smembers(`room:${roomId}:speakers`);
|
|
136
|
+
const result = [];
|
|
137
|
+
for (const id of ids) {
|
|
138
|
+
const hash = await this.redisClient.hgetall(this.buildKey(id));
|
|
139
|
+
if (hash !== null && Object.keys(hash).length > 0) {
|
|
140
|
+
result.push(this.parseHash(hash));
|
|
141
|
+
}
|
|
142
|
+
else {
|
|
143
|
+
await this.cleanIndexes(id);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return result;
|
|
147
|
+
}
|
|
148
|
+
async getSpeakersByUserId(userId) {
|
|
149
|
+
const ids = await this.redisClient.smembers(`user:${userId}:speakers`);
|
|
150
|
+
const result = [];
|
|
151
|
+
for (const id of ids) {
|
|
152
|
+
const hash = await this.redisClient.hgetall(this.buildKey(id));
|
|
153
|
+
if (hash !== null && Object.keys(hash).length > 0) {
|
|
154
|
+
result.push(this.parseHash(hash));
|
|
155
|
+
}
|
|
156
|
+
else {
|
|
157
|
+
await this.cleanIndexes(id);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return result;
|
|
161
|
+
}
|
|
162
|
+
async getSpeakerByUserRoom(roomId, userId) {
|
|
163
|
+
const ids = await this.redisClient.smembers(`room:${roomId}:speakers`);
|
|
164
|
+
for (const id of ids) {
|
|
165
|
+
if (id.includes(`]-[${userId}]-[`)) {
|
|
166
|
+
const hash = await this.redisClient.hgetall(this.buildKey(id));
|
|
167
|
+
if (hash !== null && Object.keys(hash).length > 0) {
|
|
168
|
+
return this.parseHash(hash);
|
|
169
|
+
}
|
|
170
|
+
await this.cleanIndexes(id);
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
/* ------------------------------------------------ update helpers */
|
|
177
|
+
async updateSourceLanguage(socketId, newLang) {
|
|
178
|
+
const speaker = await this.getSpeakerBySocketId(socketId);
|
|
179
|
+
if (speaker === null) {
|
|
180
|
+
return false;
|
|
181
|
+
}
|
|
182
|
+
await this.redisClient.hset(this.buildKey(speaker.speakerId), {
|
|
183
|
+
sourceLanguage: newLang
|
|
184
|
+
});
|
|
185
|
+
return true;
|
|
186
|
+
}
|
|
187
|
+
async updateTargetLanguages(socketId, languages) {
|
|
188
|
+
const speaker = await this.getSpeakerBySocketId(socketId);
|
|
189
|
+
if (speaker === null) {
|
|
190
|
+
return false;
|
|
191
|
+
}
|
|
192
|
+
await this.redisClient.hset(this.buildKey(speaker.speakerId), {
|
|
193
|
+
targetLanguages: JSON.stringify(languages)
|
|
194
|
+
});
|
|
195
|
+
return true;
|
|
196
|
+
}
|
|
197
|
+
async increaseRecordingDuration(socketId, delta) {
|
|
198
|
+
const speaker = await this.getSpeakerBySocketId(socketId);
|
|
199
|
+
if (speaker === null) {
|
|
200
|
+
return -1;
|
|
201
|
+
}
|
|
202
|
+
speaker.recordingsDuration += delta;
|
|
203
|
+
await this.redisClient.hset(this.buildKey(speaker.speakerId), {
|
|
204
|
+
recordingsDuration: speaker.recordingsDuration.toString()
|
|
205
|
+
});
|
|
206
|
+
return speaker.recordingsDuration;
|
|
207
|
+
}
|
|
208
|
+
/* ------------------------------------------------ index cleanup */
|
|
209
|
+
async cleanIndexes(speakerId) {
|
|
210
|
+
const parts = speakerId
|
|
211
|
+
.substring(1, speakerId.length - 1) // remove outer [...]
|
|
212
|
+
.split(']-['); // ['roomId','userId','socketId']
|
|
213
|
+
if (parts.length !== 3) {
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
const roomId = parts[0];
|
|
217
|
+
const userId = parts[1];
|
|
218
|
+
const socketId = parts[2];
|
|
219
|
+
await this.redisClient.srem(`room:${roomId}:speakers`, speakerId);
|
|
220
|
+
await this.redisClient.srem(`user:${userId}:speakers`, speakerId);
|
|
221
|
+
await this.redisClient.del(`socket:${socketId}:speaker`);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
exports.MulingstreamSpeakerManager = MulingstreamSpeakerManager;
|
package/dist/redis-client.d.ts
CHANGED
|
@@ -1,28 +1,28 @@
|
|
|
1
|
-
export interface RedisConfig {
|
|
2
|
-
host: string;
|
|
3
|
-
port: number;
|
|
4
|
-
password?: string;
|
|
5
|
-
db: number;
|
|
6
|
-
}
|
|
7
|
-
export declare class RedisClient {
|
|
8
|
-
private client;
|
|
9
|
-
constructor(config: RedisConfig);
|
|
10
|
-
set(key: string, value: string): Promise<void>;
|
|
11
|
-
get(key: string): Promise<string | null>;
|
|
12
|
-
del(key: string): Promise<number>;
|
|
13
|
-
sadd(key: string, ...values: string[]): Promise<number>;
|
|
14
|
-
srem(key: string, ...values: string[]): Promise<number>;
|
|
15
|
-
smembers(key: string): Promise<string[]>;
|
|
16
|
-
keys(pattern: string): Promise<string[]>;
|
|
17
|
-
hset(key: string, data: Record<string, string>): Promise<number>;
|
|
18
|
-
hgetall(key: string): Promise<Record<string, string>>;
|
|
19
|
-
flushAll(): Promise<string>;
|
|
20
|
-
flushDb(): Promise<string>;
|
|
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
|
-
jsonArrLen(key: string, path: string): Promise<number | null>;
|
|
27
|
-
jsonArrPop(key: string, path: string, index?: number): Promise<any>;
|
|
28
|
-
}
|
|
1
|
+
export interface RedisConfig {
|
|
2
|
+
host: string;
|
|
3
|
+
port: number;
|
|
4
|
+
password?: string;
|
|
5
|
+
db: number;
|
|
6
|
+
}
|
|
7
|
+
export declare class RedisClient {
|
|
8
|
+
private client;
|
|
9
|
+
constructor(config: RedisConfig);
|
|
10
|
+
set(key: string, value: string): Promise<void>;
|
|
11
|
+
get(key: string): Promise<string | null>;
|
|
12
|
+
del(key: string): Promise<number>;
|
|
13
|
+
sadd(key: string, ...values: string[]): Promise<number>;
|
|
14
|
+
srem(key: string, ...values: string[]): Promise<number>;
|
|
15
|
+
smembers(key: string): Promise<string[]>;
|
|
16
|
+
keys(pattern: string): Promise<string[]>;
|
|
17
|
+
hset(key: string, data: Record<string, string>): Promise<number>;
|
|
18
|
+
hgetall(key: string): Promise<Record<string, string>>;
|
|
19
|
+
flushAll(): Promise<string>;
|
|
20
|
+
flushDb(): Promise<string>;
|
|
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
|
+
jsonArrLen(key: string, path: string): Promise<number | null>;
|
|
27
|
+
jsonArrPop(key: string, path: string, index?: number): Promise<any>;
|
|
28
|
+
}
|