@mulingai-npm/redis 3.4.2 → 3.5.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.
|
@@ -14,6 +14,13 @@ export type MulingstreamSpeakerData = {
|
|
|
14
14
|
export declare class MulingstreamSpeakerManager {
|
|
15
15
|
private redisClient;
|
|
16
16
|
constructor(redisClient: RedisClient);
|
|
17
|
+
private buildLockKey;
|
|
18
|
+
/** Try to claim the speaker slot for this room.
|
|
19
|
+
* @returns true when the lock was acquired, false if someone else holds it.
|
|
20
|
+
*/
|
|
21
|
+
acquireRoomLock(roomId: string, socketId: string, ttl?: number): Promise<boolean>;
|
|
22
|
+
/** Release the lock if we still own it. */
|
|
23
|
+
releaseRoomLock(roomId: string, socketId: string): Promise<void>;
|
|
17
24
|
private parseHash;
|
|
18
25
|
private serialize;
|
|
19
26
|
private buildId;
|
|
@@ -2,10 +2,34 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.MulingstreamSpeakerManager = void 0;
|
|
4
4
|
const EXPIRATION = 24 * 60 * 60; // 24h
|
|
5
|
+
const LOCK_TTL = 24 * 60 * 60;
|
|
5
6
|
class MulingstreamSpeakerManager {
|
|
6
7
|
constructor(redisClient) {
|
|
7
8
|
this.redisClient = redisClient;
|
|
8
9
|
}
|
|
10
|
+
/* ------------------------------------------------------------------ */
|
|
11
|
+
/* One-speaker-per-room locking helpers */
|
|
12
|
+
/* ------------------------------------------------------------------ */
|
|
13
|
+
buildLockKey(roomId) {
|
|
14
|
+
return `room:${roomId}:speaker_lock`;
|
|
15
|
+
}
|
|
16
|
+
/** Try to claim the speaker slot for this room.
|
|
17
|
+
* @returns true when the lock was acquired, false if someone else holds it.
|
|
18
|
+
*/
|
|
19
|
+
async acquireRoomLock(roomId, socketId, ttl = LOCK_TTL) {
|
|
20
|
+
return this.redisClient.setNxEx(this.buildLockKey(roomId), socketId, ttl);
|
|
21
|
+
}
|
|
22
|
+
/** Release the lock if we still own it. */
|
|
23
|
+
async releaseRoomLock(roomId, socketId) {
|
|
24
|
+
const lockKey = this.buildLockKey(roomId);
|
|
25
|
+
const current = await this.redisClient.get(lockKey);
|
|
26
|
+
if (current === socketId) {
|
|
27
|
+
await this.redisClient.del(lockKey);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
/* ------------------------------------------------------------------ */
|
|
31
|
+
/* Speaker CRUD + queries */
|
|
32
|
+
/* ------------------------------------------------------------------ */
|
|
9
33
|
parseHash(hash) {
|
|
10
34
|
return {
|
|
11
35
|
speakerId: hash.speakerId,
|
|
@@ -50,7 +74,6 @@ class MulingstreamSpeakerManager {
|
|
|
50
74
|
};
|
|
51
75
|
await this.redisClient.hset(key, this.serialize(speakerData));
|
|
52
76
|
await this.redisClient.expire(key, EXPIRATION);
|
|
53
|
-
// index sets / mapping
|
|
54
77
|
await this.redisClient.sadd(`room:${payload.roomId}:speakers`, speakerId);
|
|
55
78
|
await this.redisClient.sadd(`user:${payload.userId}:speakers`, speakerId);
|
|
56
79
|
await this.redisClient.set(`socket:${payload.socketId}:speaker`, speakerId);
|
|
@@ -59,43 +82,30 @@ class MulingstreamSpeakerManager {
|
|
|
59
82
|
}
|
|
60
83
|
async removeSpeakerBySocketId(socketId) {
|
|
61
84
|
const speakerId = await this.redisClient.get(`socket:${socketId}:speaker`);
|
|
62
|
-
if (speakerId === null)
|
|
85
|
+
if (speakerId === null)
|
|
63
86
|
return false;
|
|
64
|
-
|
|
65
|
-
const removed = await this.removeSpeakerById(speakerId);
|
|
66
|
-
return removed;
|
|
87
|
+
return this.removeSpeakerById(speakerId);
|
|
67
88
|
}
|
|
68
89
|
async removeSpeakersByUserId(userId) {
|
|
69
90
|
const ids = await this.redisClient.smembers(`user:${userId}:speakers`);
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
if (await this.removeSpeakerById(id)) {
|
|
76
|
-
deletedCount += 1;
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
return deletedCount;
|
|
91
|
+
let deleted = 0;
|
|
92
|
+
for (const id of ids)
|
|
93
|
+
if (await this.removeSpeakerById(id))
|
|
94
|
+
deleted += 1;
|
|
95
|
+
return deleted;
|
|
80
96
|
}
|
|
81
97
|
async removeSpeakersByRoomId(roomId) {
|
|
82
98
|
const ids = await this.redisClient.smembers(`room:${roomId}:speakers`);
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
if (await this.removeSpeakerById(id)) {
|
|
89
|
-
deletedCount += 1;
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
return deletedCount;
|
|
99
|
+
let deleted = 0;
|
|
100
|
+
for (const id of ids)
|
|
101
|
+
if (await this.removeSpeakerById(id))
|
|
102
|
+
deleted += 1;
|
|
103
|
+
return deleted;
|
|
93
104
|
}
|
|
94
105
|
async removeSpeakerById(speakerId) {
|
|
95
106
|
const key = this.buildKey(speakerId);
|
|
96
107
|
const data = await this.redisClient.hgetall(key);
|
|
97
108
|
if (data === null || Object.keys(data).length === 0) {
|
|
98
|
-
// already gone; clean indexes anyway
|
|
99
109
|
await this.cleanIndexes(speakerId);
|
|
100
110
|
return false;
|
|
101
111
|
}
|
|
@@ -107,7 +117,6 @@ class MulingstreamSpeakerManager {
|
|
|
107
117
|
const key = this.buildKey(speakerId);
|
|
108
118
|
const hash = await this.redisClient.hgetall(key);
|
|
109
119
|
if (hash === null || Object.keys(hash).length === 0) {
|
|
110
|
-
// the hash has expired or was never there – remove any stray index refs
|
|
111
120
|
await this.cleanIndexes(speakerId);
|
|
112
121
|
return null;
|
|
113
122
|
}
|
|
@@ -115,14 +124,11 @@ class MulingstreamSpeakerManager {
|
|
|
115
124
|
}
|
|
116
125
|
async getSpeakerBySocketId(socketId) {
|
|
117
126
|
const speakerId = await this.redisClient.get(`socket:${socketId}:speaker`);
|
|
118
|
-
if (speakerId === null)
|
|
127
|
+
if (speakerId === null)
|
|
119
128
|
return null;
|
|
120
|
-
}
|
|
121
129
|
const hash = await this.redisClient.hgetall(this.buildKey(speakerId));
|
|
122
|
-
if (hash !== null && Object.keys(hash).length > 0)
|
|
130
|
+
if (hash !== null && Object.keys(hash).length > 0)
|
|
123
131
|
return this.parseHash(hash);
|
|
124
|
-
}
|
|
125
|
-
// hash expired: tidy indexes
|
|
126
132
|
await this.cleanIndexes(speakerId);
|
|
127
133
|
return null;
|
|
128
134
|
}
|
|
@@ -159,9 +165,8 @@ class MulingstreamSpeakerManager {
|
|
|
159
165
|
for (const id of ids) {
|
|
160
166
|
if (id.includes(`]-[${userId}]-[`)) {
|
|
161
167
|
const hash = await this.redisClient.hgetall(this.buildKey(id));
|
|
162
|
-
if (hash !== null && Object.keys(hash).length > 0)
|
|
168
|
+
if (hash !== null && Object.keys(hash).length > 0)
|
|
163
169
|
return this.parseHash(hash);
|
|
164
|
-
}
|
|
165
170
|
await this.cleanIndexes(id);
|
|
166
171
|
return null;
|
|
167
172
|
}
|
|
@@ -169,57 +174,44 @@ class MulingstreamSpeakerManager {
|
|
|
169
174
|
return null;
|
|
170
175
|
}
|
|
171
176
|
async getSpeakersLength(roomId) {
|
|
172
|
-
|
|
173
|
-
return speakers.length;
|
|
177
|
+
return (await this.getSpeakersByRoomId(roomId)).length;
|
|
174
178
|
}
|
|
175
179
|
async isRoomEmpty(roomId) {
|
|
176
|
-
|
|
177
|
-
return totalSpeakers === 0;
|
|
180
|
+
return (await this.getSpeakersLength(roomId)) === 0;
|
|
178
181
|
}
|
|
179
182
|
async updateSourceLanguage(socketId, newLang) {
|
|
180
183
|
const speaker = await this.getSpeakerBySocketId(socketId);
|
|
181
|
-
if (speaker === null)
|
|
184
|
+
if (speaker === null)
|
|
182
185
|
return false;
|
|
183
|
-
}
|
|
184
|
-
await this.redisClient.hset(this.buildKey(speaker.speakerId), {
|
|
185
|
-
sourceLanguage: newLang
|
|
186
|
-
});
|
|
186
|
+
await this.redisClient.hset(this.buildKey(speaker.speakerId), { sourceLanguage: newLang });
|
|
187
187
|
return true;
|
|
188
188
|
}
|
|
189
189
|
async updateTargetLanguages(socketId, languages) {
|
|
190
190
|
const speaker = await this.getSpeakerBySocketId(socketId);
|
|
191
|
-
if (speaker === null)
|
|
191
|
+
if (speaker === null)
|
|
192
192
|
return false;
|
|
193
|
-
}
|
|
194
|
-
await this.redisClient.hset(this.buildKey(speaker.speakerId), {
|
|
195
|
-
targetLanguages: JSON.stringify(languages)
|
|
196
|
-
});
|
|
193
|
+
await this.redisClient.hset(this.buildKey(speaker.speakerId), { targetLanguages: JSON.stringify(languages) });
|
|
197
194
|
return true;
|
|
198
195
|
}
|
|
199
196
|
async increaseRecordingDuration(socketId, delta) {
|
|
200
197
|
const speaker = await this.getSpeakerBySocketId(socketId);
|
|
201
|
-
if (speaker === null)
|
|
198
|
+
if (speaker === null)
|
|
202
199
|
return -1;
|
|
203
|
-
}
|
|
204
200
|
speaker.recordingsDuration += delta;
|
|
205
|
-
await this.redisClient.hset(this.buildKey(speaker.speakerId), {
|
|
206
|
-
recordingsDuration: speaker.recordingsDuration.toString()
|
|
207
|
-
});
|
|
201
|
+
await this.redisClient.hset(this.buildKey(speaker.speakerId), { recordingsDuration: speaker.recordingsDuration.toString() });
|
|
208
202
|
return speaker.recordingsDuration;
|
|
209
203
|
}
|
|
210
204
|
async cleanIndexes(speakerId) {
|
|
211
|
-
const parts = speakerId
|
|
212
|
-
|
|
213
|
-
.split(']-['); // ['roomId','userId','socketId']
|
|
214
|
-
if (parts.length !== 3) {
|
|
205
|
+
const parts = speakerId.substring(1, speakerId.length - 1).split(']-['); // ['roomId','userId','socketId']
|
|
206
|
+
if (parts.length !== 3)
|
|
215
207
|
return;
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
208
|
+
const [roomId, userId, socketId] = parts;
|
|
209
|
+
await Promise.all([
|
|
210
|
+
this.redisClient.srem(`room:${roomId}:speakers`, speakerId),
|
|
211
|
+
this.redisClient.srem(`user:${userId}:speakers`, speakerId),
|
|
212
|
+
this.redisClient.del(`socket:${socketId}:speaker`),
|
|
213
|
+
this.redisClient.del(this.buildLockKey(roomId)) // ensure lock clears
|
|
214
|
+
]);
|
|
223
215
|
}
|
|
224
216
|
}
|
|
225
217
|
exports.MulingstreamSpeakerManager = MulingstreamSpeakerManager;
|
package/dist/redis-client.d.ts
CHANGED
|
@@ -7,7 +7,7 @@ export interface RedisConfig {
|
|
|
7
7
|
}
|
|
8
8
|
/**
|
|
9
9
|
* Thin wrapper around ioredis that exposes exactly the helpers used by the two
|
|
10
|
-
* managers. All helpers are 1
|
|
10
|
+
* managers. All helpers are 1-to-1 pass-through so there is no behavioural
|
|
11
11
|
* change – this merely centralises typing and keeps the rest of the codebase
|
|
12
12
|
* clean.
|
|
13
13
|
*/
|
|
@@ -15,6 +15,9 @@ export declare class RedisClient {
|
|
|
15
15
|
private client;
|
|
16
16
|
constructor(config: RedisConfig);
|
|
17
17
|
set(key: string, value: string): Promise<void>;
|
|
18
|
+
/** Atomic SET with NX + EX.
|
|
19
|
+
* @returns true if the key was set, false when it already existed */
|
|
20
|
+
setNxEx(key: string, value: string, expireSeconds: number): Promise<boolean>;
|
|
18
21
|
get(key: string): Promise<string | null>;
|
|
19
22
|
del(key: string): Promise<number>;
|
|
20
23
|
exists(key: string): Promise<number>;
|
package/dist/redis-client.js
CHANGED
|
@@ -7,7 +7,7 @@ exports.RedisClient = void 0;
|
|
|
7
7
|
const ioredis_1 = __importDefault(require("ioredis"));
|
|
8
8
|
/**
|
|
9
9
|
* Thin wrapper around ioredis that exposes exactly the helpers used by the two
|
|
10
|
-
* managers. All helpers are 1
|
|
10
|
+
* managers. All helpers are 1-to-1 pass-through so there is no behavioural
|
|
11
11
|
* change – this merely centralises typing and keeps the rest of the codebase
|
|
12
12
|
* clean.
|
|
13
13
|
*/
|
|
@@ -29,6 +29,13 @@ class RedisClient {
|
|
|
29
29
|
async set(key, value) {
|
|
30
30
|
await this.client.set(key, value);
|
|
31
31
|
}
|
|
32
|
+
/** Atomic SET with NX + EX.
|
|
33
|
+
* @returns true if the key was set, false when it already existed */
|
|
34
|
+
async setNxEx(key, value, expireSeconds) {
|
|
35
|
+
// correct order: 'EX', ttl, 'NX'
|
|
36
|
+
const res = (await this.client.set(key, value, 'EX', expireSeconds, 'NX'));
|
|
37
|
+
return res === 'OK';
|
|
38
|
+
}
|
|
32
39
|
async get(key) {
|
|
33
40
|
return this.client.get(key);
|
|
34
41
|
}
|
|
@@ -63,7 +70,7 @@ class RedisClient {
|
|
|
63
70
|
return this.client.hgetall(key);
|
|
64
71
|
}
|
|
65
72
|
/* ----------------------------------------------------------------------- */
|
|
66
|
-
/* Sorted
|
|
73
|
+
/* Sorted-set helpers */
|
|
67
74
|
/* ----------------------------------------------------------------------- */
|
|
68
75
|
async zadd(key, score, member) {
|
|
69
76
|
return this.client.zadd(key, score, member);
|