@mulingai-npm/redis 3.4.2 → 3.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.
@@ -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 = 30; // seconds
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
- if (ids.length === 0) {
71
- return 0;
72
- }
73
- let deletedCount = 0;
74
- for (const id of ids) {
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
- if (ids.length === 0) {
84
- return 0;
85
- }
86
- let deletedCount = 0;
87
- for (const id of ids) {
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
- const speakers = await this.getSpeakersByRoomId(roomId);
173
- return speakers.length;
177
+ return (await this.getSpeakersByRoomId(roomId)).length;
174
178
  }
175
179
  async isRoomEmpty(roomId) {
176
- const totalSpeakers = await this.getSpeakersLength(roomId);
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
- .substring(1, speakerId.length - 1) // remove outer [...]
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
- const roomId = parts[0];
218
- const userId = parts[1];
219
- const socketId = parts[2];
220
- await this.redisClient.srem(`room:${roomId}:speakers`, speakerId);
221
- await this.redisClient.srem(`user:${userId}:speakers`, speakerId);
222
- await this.redisClient.del(`socket:${socketId}:speaker`);
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;
@@ -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 1to1 passthrough so there is no behavioural
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>;
@@ -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 1to1 passthrough so there is no behavioural
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
- /* Sortedset helpers */
73
+ /* Sorted-set helpers */
67
74
  /* ----------------------------------------------------------------------- */
68
75
  async zadd(key, score, member) {
69
76
  return this.client.zadd(key, score, member);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mulingai-npm/redis",
3
- "version": "3.4.2",
3
+ "version": "3.5.0",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "repository": {