@mulingai-npm/redis 3.40.48 → 3.40.50

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.
@@ -52,6 +52,36 @@ export declare class MulingstreamListenerManager {
52
52
  */
53
53
  getReceivingCount(roomId: string): Promise<number>;
54
54
  private capKey;
55
+ private previewKey;
56
+ private previewCountKey;
57
+ /** Open a preview window on one language. Re-pressing simply extends it. */
58
+ openPreview(roomId: string, language: string, windowSeconds: number): Promise<void>;
59
+ /** Seconds left on a language's preview window, or 0 when none is open. */
60
+ getPreviewRemaining(roomId: string, language: string): Promise<number>;
61
+ /**
62
+ * Which of `candidates` currently have a preview open.
63
+ *
64
+ * Takes the candidate list rather than scanning for a pattern on purpose:
65
+ * KEYS and SCAN against a live Redis to answer a question asked on every
66
+ * audio chunk is the kind of thing that is fine until it is not.
67
+ */
68
+ getPreviewLanguages(roomId: string, candidates: string[]): Promise<string[]>;
69
+ /**
70
+ * Count one press against the room's allowance and say whether it is allowed.
71
+ *
72
+ * The cap exists because a preview translates for real. Without it, holding
73
+ * the button open is a way to run a translation service for nothing, and the
74
+ * obvious abuse is someone using a speaker preview as a free manual
75
+ * translation tool. The counter's own TTL resets the allowance, so a church
76
+ * running a genuine service every week is never permanently locked out.
77
+ */
78
+ consumePreviewAllowance(roomId: string, maxPresses: number, resetSeconds: number): Promise<{
79
+ allowed: boolean;
80
+ used: number;
81
+ remaining: number;
82
+ }>;
83
+ /** Presses already spent in the current window. Read only, never increments. */
84
+ getPreviewUsage(roomId: string): Promise<number>;
55
85
  /**
56
86
  * Cache the room's listener cap. Called by the speaker service at go-live and
57
87
  * on reconnect; `ttlSeconds` should comfortably exceed a session so the key
@@ -268,6 +268,82 @@ class MulingstreamListenerManager {
268
268
  capKey(roomId) {
269
269
  return `room:${roomId}:listener-cap`;
270
270
  }
271
+ /*
272
+ * SPEAKER PREVIEW
273
+ *
274
+ * A speaker looking at their own translation feed is a consumer of
275
+ * translation but must never be a payer for it: nobody should be able to
276
+ * spend a church's pass, or their own credits, by watching their own screen.
277
+ * So a preview is a short window they open deliberately, not a state they
278
+ * fall into by leaving a tab open.
279
+ *
280
+ * In Redis rather than in the pipeline's memory because the request arrives
281
+ * at the room service and the decision is read by the pipeline service, and
282
+ * because a pipeline replica restarting mid window must not silently start
283
+ * translating for nobody. The TTL IS the expiry: nothing has to remember to
284
+ * close it, and a crash fails in the safe direction.
285
+ */
286
+ previewKey(roomId, language) {
287
+ return `room:${roomId}:preview:${language}`;
288
+ }
289
+ previewCountKey(roomId) {
290
+ return `room:${roomId}:preview-count`;
291
+ }
292
+ /** Open a preview window on one language. Re-pressing simply extends it. */
293
+ async openPreview(roomId, language, windowSeconds) {
294
+ if (!roomId || !language || windowSeconds <= 0)
295
+ return;
296
+ const key = this.previewKey(roomId, language);
297
+ await this.redisClient.set(key, '1');
298
+ await this.redisClient.expire(key, Math.floor(windowSeconds));
299
+ }
300
+ /** Seconds left on a language's preview window, or 0 when none is open. */
301
+ async getPreviewRemaining(roomId, language) {
302
+ const ttl = await this.redisClient.ttl(this.previewKey(roomId, language));
303
+ return ttl > 0 ? ttl : 0;
304
+ }
305
+ /**
306
+ * Which of `candidates` currently have a preview open.
307
+ *
308
+ * Takes the candidate list rather than scanning for a pattern on purpose:
309
+ * KEYS and SCAN against a live Redis to answer a question asked on every
310
+ * audio chunk is the kind of thing that is fine until it is not.
311
+ */
312
+ async getPreviewLanguages(roomId, candidates) {
313
+ if (!roomId || candidates.length === 0)
314
+ return [];
315
+ const open = [];
316
+ for (const language of candidates) {
317
+ if (await this.redisClient.get(this.previewKey(roomId, language)))
318
+ open.push(language);
319
+ }
320
+ return open;
321
+ }
322
+ /**
323
+ * Count one press against the room's allowance and say whether it is allowed.
324
+ *
325
+ * The cap exists because a preview translates for real. Without it, holding
326
+ * the button open is a way to run a translation service for nothing, and the
327
+ * obvious abuse is someone using a speaker preview as a free manual
328
+ * translation tool. The counter's own TTL resets the allowance, so a church
329
+ * running a genuine service every week is never permanently locked out.
330
+ */
331
+ async consumePreviewAllowance(roomId, maxPresses, resetSeconds) {
332
+ const key = this.previewCountKey(roomId);
333
+ const used = await this.redisClient.incr(key);
334
+ // Only the first press starts the clock, so the window is a fixed period
335
+ // from first use rather than one that slides forward on every press.
336
+ if (used === 1)
337
+ await this.redisClient.expire(key, Math.floor(resetSeconds));
338
+ const allowed = used <= maxPresses;
339
+ return { allowed, used, remaining: Math.max(0, maxPresses - used) };
340
+ }
341
+ /** Presses already spent in the current window. Read only, never increments. */
342
+ async getPreviewUsage(roomId) {
343
+ const raw = await this.redisClient.get(this.previewCountKey(roomId));
344
+ const used = raw ? parseInt(raw, 10) : 0;
345
+ return Number.isFinite(used) && used > 0 ? used : 0;
346
+ }
271
347
  /**
272
348
  * Cache the room's listener cap. Called by the speaker service at go-live and
273
349
  * on reconnect; `ttlSeconds` should comfortably exceed a session so the key
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mulingai-npm/redis",
3
- "version": "3.40.48",
3
+ "version": "3.40.50",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "repository": {