@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) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mulingai-npm/redis",
3
- "version": "3.40.35",
3
+ "version": "3.40.36",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "repository": {