@mulingai-npm/redis 3.40.48 → 3.40.51
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
|