@mulingai-npm/redis 3.40.35 → 3.40.36
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.
|
@@ -36,6 +36,28 @@ export declare class MulingstreamListenerManager {
|
|
|
36
36
|
totalListeners: number;
|
|
37
37
|
languageBreakdown: Record<string, number>;
|
|
38
38
|
}>;
|
|
39
|
+
private capKey;
|
|
40
|
+
/**
|
|
41
|
+
* Cache the room's listener cap. Called by the speaker service at go-live and
|
|
42
|
+
* on reconnect; `ttlSeconds` should comfortably exceed a session so the key
|
|
43
|
+
* stays warm for the whole stream (it is re-set on every speaker (re)join).
|
|
44
|
+
* No-ops on a non-positive cap so a misconfigured plan never blocks everyone.
|
|
45
|
+
*/
|
|
46
|
+
setRoomListenerCap(roomId: string, cap: number, ttlSeconds: number): Promise<void>;
|
|
47
|
+
/**
|
|
48
|
+
* Read the cached listener cap for a room. Returns null when no cap has been
|
|
49
|
+
* cached yet (e.g. the speaker isn't live), so the caller can apply its own
|
|
50
|
+
* default.
|
|
51
|
+
*/
|
|
52
|
+
getRoomListenerCap(roomId: string): Promise<number | null>;
|
|
53
|
+
/**
|
|
54
|
+
* True when the room has reached (or exceeded) its listener cap. Only NEW
|
|
55
|
+
* joins should be gated on this — a rejoin already holds a seat.
|
|
56
|
+
* `fallbackCap` is used when no cap is cached yet (speaker not live); pass a
|
|
57
|
+
* generous value since listeners with no live speaker generate no pipeline
|
|
58
|
+
* load. Counting reuses the self-healing room set (stale members are pruned).
|
|
59
|
+
*/
|
|
60
|
+
isRoomAtListenerCapacity(roomId: string, fallbackCap: number): Promise<boolean>;
|
|
39
61
|
getListenerBySocketId(socketId: string): Promise<MulingstreamListenerData | null>;
|
|
40
62
|
/**
|
|
41
63
|
* Update the isListening state for a listener (audio play/pause).
|
|
@@ -239,6 +239,56 @@ class MulingstreamListenerManager {
|
|
|
239
239
|
languageBreakdown
|
|
240
240
|
};
|
|
241
241
|
}
|
|
242
|
+
// ─── Per-room listener capacity (design B, 2026-07-09) ────────────────────
|
|
243
|
+
// The cap value is the room owner's plan `max_audience`, resolved ONCE by the
|
|
244
|
+
// speaker service when it goes live (it already fetches the plan entitlements)
|
|
245
|
+
// and cached here, so the listener join path enforces it with a pure Redis
|
|
246
|
+
// read — no billing gRPC on the hot path. Lives in the same
|
|
247
|
+
// MULINGSTREAM_LISTENER DB as the listener set, so one manager owns both the
|
|
248
|
+
// cap and the live count. NOT persisted in Postgres: a TTL'd cache means a
|
|
249
|
+
// plan change is picked up on the next speaker (re)join, nothing goes stale.
|
|
250
|
+
capKey(roomId) {
|
|
251
|
+
return `room:${roomId}:listener-cap`;
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* Cache the room's listener cap. Called by the speaker service at go-live and
|
|
255
|
+
* on reconnect; `ttlSeconds` should comfortably exceed a session so the key
|
|
256
|
+
* stays warm for the whole stream (it is re-set on every speaker (re)join).
|
|
257
|
+
* No-ops on a non-positive cap so a misconfigured plan never blocks everyone.
|
|
258
|
+
*/
|
|
259
|
+
async setRoomListenerCap(roomId, cap, ttlSeconds) {
|
|
260
|
+
if (!Number.isFinite(cap) || cap <= 0)
|
|
261
|
+
return;
|
|
262
|
+
await this.redisClient.set(this.capKey(roomId), Math.floor(cap).toString());
|
|
263
|
+
await this.redisClient.expire(this.capKey(roomId), ttlSeconds);
|
|
264
|
+
}
|
|
265
|
+
/**
|
|
266
|
+
* Read the cached listener cap for a room. Returns null when no cap has been
|
|
267
|
+
* cached yet (e.g. the speaker isn't live), so the caller can apply its own
|
|
268
|
+
* default.
|
|
269
|
+
*/
|
|
270
|
+
async getRoomListenerCap(roomId) {
|
|
271
|
+
const raw = await this.redisClient.get(this.capKey(roomId));
|
|
272
|
+
if (raw == null)
|
|
273
|
+
return null;
|
|
274
|
+
const cap = parseInt(raw, 10);
|
|
275
|
+
return Number.isFinite(cap) && cap > 0 ? cap : null;
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* True when the room has reached (or exceeded) its listener cap. Only NEW
|
|
279
|
+
* joins should be gated on this — a rejoin already holds a seat.
|
|
280
|
+
* `fallbackCap` is used when no cap is cached yet (speaker not live); pass a
|
|
281
|
+
* generous value since listeners with no live speaker generate no pipeline
|
|
282
|
+
* load. Counting reuses the self-healing room set (stale members are pruned).
|
|
283
|
+
*/
|
|
284
|
+
async isRoomAtListenerCapacity(roomId, fallbackCap) {
|
|
285
|
+
var _a;
|
|
286
|
+
const cap = (_a = (await this.getRoomListenerCap(roomId))) !== null && _a !== void 0 ? _a : fallbackCap;
|
|
287
|
+
if (!Number.isFinite(cap) || cap <= 0)
|
|
288
|
+
return false;
|
|
289
|
+
const listeners = await this.getListenersByRoom(roomId);
|
|
290
|
+
return listeners.length >= cap;
|
|
291
|
+
}
|
|
242
292
|
async getListenerBySocketId(socketId) {
|
|
243
293
|
const keys = await this.redisClient.keys('listener:*');
|
|
244
294
|
if (!keys || keys.length === 0) {
|