discovery-media-player 0.1.111 → 0.1.112

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "discovery-media-player",
3
- "version": "0.1.111",
3
+ "version": "0.1.112",
4
4
  "description": "Self-hosted document viewer: per-recipient tracked links, reading analytics, live presentation. The core knows nothing about the application hosting it — everything it borrows arrives through an injected context.",
5
5
  "keywords": [
6
6
  "pdf-viewer",
package/server/cache.js CHANGED
@@ -37,10 +37,38 @@
37
37
  */
38
38
  function creerCache(options) {
39
39
  const ttl = Math.max(0, Number(options && options.ttlMs) || 0);
40
- const max = Math.max(1, Number(options && options.max) || 500);
40
+ // ⚠️ DEUX PLAFONDS, ET LE SECOND EST CELUI QUI COMPTE. Un plafond en NOMBRE d'entrées ne borne rien
41
+ // quand la taille d'une entrée est choisie par l'appelant : 500 réponses de chat à 600 Ko font
42
+ // ~286 Mo, et la clé porte un curseur (`chatAfter`) que le visiteur fait varier à volonté — chaque
43
+ // valeur crée une entrée. On borne donc aussi le POIDS, et le poids est MESURÉ, pas estimé : les
44
+ // valeurs mises en cache ici sont des chaînes JSON. (Relevé par un audit externe.)
45
+ const max = Math.max(1, Number(options && options.max) || 100);
46
+ const maxOctets = Math.max(1, Number(options && options.maxOctets) || 8 * 1024 * 1024);
41
47
  const now = (options && options.now) || (() => Date.now());
42
- /** @type {Map<string, { echeance: number, promesse: Promise<unknown> }>} */
48
+ /** @type {Map<string, { echeance: number, promesse: Promise<unknown>, poids: number, enVol: boolean }>} */
43
49
  const entrees = new Map();
50
+ let poidsTotal = 0;
51
+
52
+ const oublier = (k) => {
53
+ const e = entrees.get(k);
54
+ if (!e) return;
55
+ poidsTotal -= e.poids;
56
+ entrees.delete(k);
57
+ };
58
+
59
+ // ⚠️ ON N'ÉVINCE JAMAIS UNE PROMESSE EN VOL. L'éviction sert à borner la mémoire des RÉSULTATS ;
60
+ // retirer une demande en cours ne libère rien (elle est déjà lancée) et casse le regroupement : les
61
+ // appelants suivants relanceraient la même production, ce que ce cache existe pour éviter.
62
+ const borner = () => {
63
+ for (const [k, e] of entrees) {
64
+ if (entrees.size <= max && poidsTotal <= maxOctets) break;
65
+ if (!e.enVol) oublier(k);
66
+ }
67
+ };
68
+
69
+ const purger = (t) => {
70
+ for (const [k, e] of entrees) if (!e.enVol && e.echeance <= t) oublier(k);
71
+ };
44
72
 
45
73
  return {
46
74
  /**
@@ -50,32 +78,46 @@ function creerCache(options) {
50
78
  async lire(cle, produire) {
51
79
  const k = String(cle);
52
80
  const t = now();
81
+ // ⚠️ PURGE À CHAQUE ACCÈS. Sans elle, une entrée périmée restait en mémoire jusqu'à ce qu'une
82
+ // AUTRE la pousse dehors : le cache gardait indéfiniment ce qu'il ne servait plus.
83
+ purger(t);
53
84
  const vue = entrees.get(k);
54
- if (vue && vue.echeance > t) return vue.promesse;
85
+ // Une entrée en vol se partage toujours : c'est tout l'objet du regroupement.
86
+ if (vue && (vue.enVol || vue.echeance > t)) return vue.promesse;
55
87
 
56
88
  const promesse = Promise.resolve().then(produire);
57
- entrees.set(k, { echeance: t + ttl, promesse });
58
-
59
- // ⚠️ On oublie une promesse ROMPUE, jamais une promesse tenue : sinon un hoquet de base
60
- // resterait servi pendant toute la fenêtre, et le rétablissement attendrait pour rien.
61
- promesse.catch(() => {
62
- const courante = entrees.get(k);
63
- if (courante && courante.promesse === promesse) entrees.delete(k);
64
- });
89
+ // ⚠️ L'ÉCHÉANCE PART DE LA RÉSOLUTION, PAS DE LA DEMANDE. Posée à la demande, elle expirait
90
+ // AVANT que la production ne réponde dès que celle-ci dépassait le TTL — et le regroupement ne
91
+ // servait alors plus à rien pour exactement les producteurs lents, les seuls qu'il valait la
92
+ // peine de mutualiser. Tant qu'elle est en vol, l'entrée ne périme pas.
93
+ entrees.set(k, { echeance: 0, promesse, poids: 0, enVol: true });
65
94
 
66
- // Éviction : la plus ancienne d'abord (Map conserve l'ordre d'insertion). La clé venant de
67
- // l'appelant, une table sans borne serait une fuite mémoire commandée depuis l'extérieur.
68
- if (entrees.size > max) {
69
- for (const vieille of entrees.keys()) {
70
- entrees.delete(vieille);
71
- if (entrees.size <= max) break;
72
- }
73
- }
95
+ promesse.then(
96
+ (valeur) => {
97
+ const courante = entrees.get(k);
98
+ if (!courante || courante.promesse !== promesse) return;
99
+ courante.enVol = false;
100
+ courante.echeance = now() + ttl;
101
+ // Le poids réel : ces valeurs sont des chaînes JSON. Une valeur d'une autre forme compte
102
+ // pour un poids nominal — mieux vaut sous-estimer que prétendre mesurer ce qu'on ignore.
103
+ courante.poids = typeof valeur === "string" ? valeur.length : 1;
104
+ poidsTotal += courante.poids;
105
+ borner();
106
+ },
107
+ () => {
108
+ // ⚠️ On oublie une promesse ROMPUE, jamais une promesse tenue : sinon un hoquet de base
109
+ // resterait servi pendant toute la fenêtre, et le rétablissement attendrait pour rien.
110
+ const courante = entrees.get(k);
111
+ if (courante && courante.promesse === promesse) oublier(k);
112
+ },
113
+ );
74
114
  return promesse;
75
115
  },
76
116
 
77
117
  /** Pour les tests et l'exploitation : ce que la table contient réellement. */
78
118
  taille: () => entrees.size,
119
+ /** Poids cumulé des résultats retenus, en caractères. */
120
+ poids: () => poidsTotal,
79
121
  };
80
122
  }
81
123
 
package/server/handler.js CHANGED
@@ -728,7 +728,15 @@ async function handler(req, res) {
728
728
  // requête base). Il entre dans la CLÉ DE CACHE : deux clients au même curseur — le cas normal,
729
729
  // toute une salle à jour du même message — partagent la même lecture ; seuls ceux qui ont pris
730
730
  // du retard divergent. Absent → relecture complète (client ancien, ou hôte non migré).
731
- const chatAfter = (Number.isFinite(+q.chatAfter) && +q.chatAfter > 0) ? String(Math.trunc(+q.chatAfter)) : null;
731
+ // ⚠️ ENTIER DÉCIMAL SÛR, ET RIEN D'AUTRE. Cette valeur entre dans une CLÉ DE CACHE : tout ce qui
732
+ // élargit l'espace des valeurs élargit l'espace des clés, et le cache est une ressource
733
+ // partagée. `+"1e20"` est fini et passait, produisant une clé qu'aucun message n'atteindra
734
+ // jamais ; `Number.isSafeInteger` ferme la plage, et le motif décimal ferme les écritures
735
+ // équivalentes (« 0x10 », « 1e3 », espaces). La normalisation par `String(...)` reste ce qui
736
+ // fait que « 05 » et « 5 » ne créent pas deux entrées pour la même question.
737
+ const brutAfter = String(q.chatAfter == null ? "" : q.chatAfter).trim();
738
+ const nAfter = /^[0-9]{1,15}$/.test(brutAfter) ? Number(brutAfter) : NaN;
739
+ const chatAfter = (Number.isSafeInteger(nAfter) && nAfter > 0) ? String(nAfter) : null;
732
740
  const LECTURES_PARTAGEES = [
733
741
  { param: "state", produire: async (p) => ({ ok: true, state: {
734
742
  active: p.active !== false,