@mulingai-npm/redis 3.40.54 → 3.40.55

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.
@@ -89,13 +89,6 @@ export declare class MulingstreamListenerManager {
89
89
  * for why that single threshold could not be lowered and had to be split.
90
90
  */
91
91
  getUniqueLanguagesByRoom(roomId: string): Promise<string[]>;
92
- private guestLanguagesKey;
93
- /** Record that a guest, not the host, put this language in the room. */
94
- markGuestLanguage(roomId: string, language: string): Promise<void>;
95
- /** Forget a guest language, once it has been released from the room. */
96
- unmarkGuestLanguage(roomId: string, language: string): Promise<void>;
97
- /** Every language in this room that a guest added. */
98
- getGuestLanguages(roomId: string): Promise<string[]>;
99
92
  /**
100
93
  * How many PRESENT listeners in this room are on a language.
101
94
  *
@@ -140,14 +133,35 @@ export declare class MulingstreamListenerManager {
140
133
  *
141
134
  * Church passes turn on this number rather than on how many people joined. A
142
135
  * pass is spent only when translation reached a human being, so somebody
143
- * sitting on the join screen, or with the tab open and audio stopped, must
144
- * not burn a church's pass.
145
- *
146
- * `isListening` is already maintained by setListeningState, so this is the
147
- * honest count rather than a proxy for it. Falls back to counting a listener
148
- * whose flag has never been written, because the flag arrived after some
149
- * clients shipped and an older client that is genuinely playing audio should
150
- * still count.
136
+ * sitting on the join screen must not burn a church's pass.
137
+ *
138
+ * ─── RECEIVING MEANS AUDIO **OR** TEXT ──────────────────────────────────
139
+ *
140
+ * Corrected 2026-08-19. This used to require `isListening`, the audio
141
+ * playback flag, so a guest who joined, chose their language and sat reading
142
+ * the translated text counted for nothing: the pass clock stayed at zero
143
+ * until they pressed Listening. David found it on a phone, and the counting
144
+ * spec had said otherwise all along, in as many words: "the listener session
145
+ * is open AND we are delivering translated output to it, audio or text".
146
+ *
147
+ * The reading is the product. A deaf guest, a guest in a quiet room, a guest
148
+ * with no headphones and a guest who simply prefers to read are all being
149
+ * served, all costing us translation on every chunk, and none of them are
150
+ * pressing an audio button. Charging only for audio would have meant a
151
+ * church whose congregation reads never spends a pass, which sounds generous
152
+ * until you notice we are paying for every word of it.
153
+ *
154
+ * So the test is PRESENCE, and nothing else. Presence already means joined,
155
+ * in a language, and with a heartbeat inside the last ninety seconds, which
156
+ * is exactly the population `consumersFor` is translating for. Whether they
157
+ * also chose to hear it is not our business, and the spec is explicit that we
158
+ * do not police attention: a phone in a pocket with audio playing is
159
+ * listening, and a phone in a hand with text on it is reading.
160
+ *
161
+ * `isListening` keeps its real job, which is a different question: whether to
162
+ * SYNTHESISE SPEECH. See getLanguagesWithActiveListeners. Speech is the most
163
+ * expensive thing we produce and nobody should pay to generate audio no one
164
+ * is playing. Text is already produced for the room either way.
151
165
  */
152
166
  getReceivingCount(roomId: string): Promise<number>;
153
167
  private capKey;
@@ -263,48 +263,6 @@ class MulingstreamListenerManager {
263
263
  // 4) Map back to just the language strings in order
264
264
  return sortedEntries.map(([language]) => language);
265
265
  }
266
- /*
267
- * GUEST ADDED LANGUAGES
268
- *
269
- * Which of a room's target languages a guest put there, as opposed to the
270
- * host. The distinction is the whole reason this exists: a guest-added
271
- * language is released the moment nobody is on it, and the host's own
272
- * language never is, however empty the room gets. Without the distinction a
273
- * church that pinned Danish and had every guest wander off would find its room
274
- * translating nothing.
275
- *
276
- * It also stops a single guest eating the room. Changing language seven times
277
- * would otherwise leave seven languages standing, every one of them translated
278
- * and synthesised for the rest of the service on a counted pass, and no slot
279
- * left for the person who actually needed one.
280
- *
281
- * A set rather than a column: it is per-session bookkeeping, not a property of
282
- * the room, and it should disappear on its own if a process dies mid-service.
283
- * The TTL is the listener expiry for exactly that reason.
284
- */
285
- guestLanguagesKey(roomId) {
286
- return `room:${roomId}:guest-languages`;
287
- }
288
- /** Record that a guest, not the host, put this language in the room. */
289
- async markGuestLanguage(roomId, language) {
290
- if (!roomId || !language)
291
- return;
292
- const key = this.guestLanguagesKey(roomId);
293
- await this.redisClient.sadd(key, language);
294
- await this.redisClient.expire(key, EXPIRATION);
295
- }
296
- /** Forget a guest language, once it has been released from the room. */
297
- async unmarkGuestLanguage(roomId, language) {
298
- if (!roomId || !language)
299
- return;
300
- await this.redisClient.srem(this.guestLanguagesKey(roomId), language);
301
- }
302
- /** Every language in this room that a guest added. */
303
- async getGuestLanguages(roomId) {
304
- if (!roomId)
305
- return [];
306
- return (await this.redisClient.smembers(this.guestLanguagesKey(roomId))) || [];
307
- }
308
266
  /**
309
267
  * How many PRESENT listeners in this room are on a language.
310
268
  *
@@ -396,25 +354,40 @@ class MulingstreamListenerManager {
396
354
  *
397
355
  * Church passes turn on this number rather than on how many people joined. A
398
356
  * pass is spent only when translation reached a human being, so somebody
399
- * sitting on the join screen, or with the tab open and audio stopped, must
400
- * not burn a church's pass.
357
+ * sitting on the join screen must not burn a church's pass.
358
+ *
359
+ * ─── RECEIVING MEANS AUDIO **OR** TEXT ──────────────────────────────────
360
+ *
361
+ * Corrected 2026-08-19. This used to require `isListening`, the audio
362
+ * playback flag, so a guest who joined, chose their language and sat reading
363
+ * the translated text counted for nothing: the pass clock stayed at zero
364
+ * until they pressed Listening. David found it on a phone, and the counting
365
+ * spec had said otherwise all along, in as many words: "the listener session
366
+ * is open AND we are delivering translated output to it, audio or text".
367
+ *
368
+ * The reading is the product. A deaf guest, a guest in a quiet room, a guest
369
+ * with no headphones and a guest who simply prefers to read are all being
370
+ * served, all costing us translation on every chunk, and none of them are
371
+ * pressing an audio button. Charging only for audio would have meant a
372
+ * church whose congregation reads never spends a pass, which sounds generous
373
+ * until you notice we are paying for every word of it.
374
+ *
375
+ * So the test is PRESENCE, and nothing else. Presence already means joined,
376
+ * in a language, and with a heartbeat inside the last ninety seconds, which
377
+ * is exactly the population `consumersFor` is translating for. Whether they
378
+ * also chose to hear it is not our business, and the spec is explicit that we
379
+ * do not police attention: a phone in a pocket with audio playing is
380
+ * listening, and a phone in a hand with text on it is reading.
401
381
  *
402
- * `isListening` is already maintained by setListeningState, so this is the
403
- * honest count rather than a proxy for it. Falls back to counting a listener
404
- * whose flag has never been written, because the flag arrived after some
405
- * clients shipped and an older client that is genuinely playing audio should
406
- * still count.
382
+ * `isListening` keeps its real job, which is a different question: whether to
383
+ * SYNTHESISE SPEECH. See getLanguagesWithActiveListeners. Speech is the most
384
+ * expensive thing we produce and nobody should pay to generate audio no one
385
+ * is playing. Text is already produced for the room either way.
407
386
  */
408
387
  async getReceivingCount(roomId) {
409
388
  const now = Date.now();
410
389
  const listeners = await this.getListenersByRoom(roomId);
411
- return listeners.filter(
412
- // Present AND playing audio. Presence is the half that was missing:
413
- // a listener who closed their phone kept the isListening flag they had
414
- // when they left, so they went on being counted as receiving until the
415
- // sweep removed them, and went on pushing a church's pass toward being
416
- // spent. Nobody ever pauses audio on the way out of a building.
417
- (l) => (0, listener_presence_1.isListenerPresent)(l.lastHeartbeat, l.isAway === true, now) && l.isListening !== false).length;
390
+ return listeners.filter((l) => (0, listener_presence_1.isListenerPresent)(l.lastHeartbeat, l.isAway === true, now)).length;
418
391
  }
419
392
  // ─── Per-room listener capacity (design B, 2026-07-09) ────────────────────
420
393
  // The cap value is the room owner's plan `max_audience`, resolved ONCE by the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mulingai-npm/redis",
3
- "version": "3.40.54",
3
+ "version": "3.40.55",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "repository": {