discovery-media-player 0.1.110 → 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/context/standalone.js +10 -1
- package/package.json +1 -1
- package/server/cache.js +61 -19
- package/server/handler.js +9 -1
- package/server/presentations.js +53 -4
package/context/standalone.js
CHANGED
|
@@ -69,7 +69,16 @@ function creerDb(env) {
|
|
|
69
69
|
// début. Tronqué : c'est un message d'erreur, pas un relais de données.
|
|
70
70
|
let detail = "";
|
|
71
71
|
try { detail = (await r.text()).slice(0, 300); } catch { /* le statut restera seul */ }
|
|
72
|
-
|
|
72
|
+
// ⚠️ LE MESSAGE NE CHANGE PAS — du code appelant y cherche « 409 ». On AJOUTE seulement de quoi
|
|
73
|
+
// décider sans lire une phrase : le statut, et le corps PostgREST analysé quand il l'est. Sans
|
|
74
|
+
// ça, distinguer « cette fonction n'existe pas » (PGRST202) d'un hoquet réseau obligeait à
|
|
75
|
+
// deviner sur du texte — et l'appelant concluait « migration absente » par défaut, ce qui lui
|
|
76
|
+
// faisait retirer une protection sur une simple panne. L'autre contexte (studio) expose déjà
|
|
77
|
+
// `statusCode`/`details` : les deux formes convergent enfin.
|
|
78
|
+
const erreur = new Error(`Supabase ${methode} ${chemin} → ${r.status}${detail ? ` — ${detail}` : ""}`);
|
|
79
|
+
erreur.statusCode = r.status;
|
|
80
|
+
try { erreur.details = JSON.parse(detail); } catch { /* corps non JSON : le message suffit */ }
|
|
81
|
+
throw erreur;
|
|
73
82
|
}
|
|
74
83
|
const texte = await r.text();
|
|
75
84
|
return texte ? JSON.parse(texte) : null;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "discovery-media-player",
|
|
3
|
-
"version": "0.1.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
58
|
-
|
|
59
|
-
//
|
|
60
|
-
//
|
|
61
|
-
|
|
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
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
-
|
|
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,
|
package/server/presentations.js
CHANGED
|
@@ -6,7 +6,7 @@ const crypto = require("crypto");
|
|
|
6
6
|
// ⚠️ Le contexte est REÇU, pas construit. Ce module ne doit pas savoir d'où il vient : c'est ce
|
|
7
7
|
// qui lui permettra de partir dans le dépôt du player sans emporter le studio avec lui.
|
|
8
8
|
let PLAYER = null;
|
|
9
|
-
function init(ctx) { PLAYER = ctx; }
|
|
9
|
+
function init(ctx) { PLAYER = ctx; _bumpSansDurcissementJusqua = 0; _avertRpcPresence = false; }
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
const enc = encodeURIComponent;
|
|
@@ -779,10 +779,41 @@ let _avertRpcPresence = false;
|
|
|
779
779
|
* aller-retour perdu ; le chemin de la présence est chaud (un battement toutes les 25 s par
|
|
780
780
|
* participant). On retient donc l'ensemble d'arguments qui a marché, pour le processus.
|
|
781
781
|
*/
|
|
782
|
-
|
|
782
|
+
/**
|
|
783
|
+
* Cette erreur dit-elle « CETTE SIGNATURE N'EXISTE PAS », et rien d'autre ?
|
|
784
|
+
*
|
|
785
|
+
* ⚠️ C'EST LA QUESTION QUI MANQUAIT, ET SON ABSENCE RETIRAIT UNE PROTECTION. Le repli vers l'ancien
|
|
786
|
+
* contrat se déclenchait sur N'IMPORTE QUELLE exception : un `ECONNRESET`, un 500, un délai dépassé
|
|
787
|
+
* valaient « migration 0018 absente », et le processus restait dégradé — sans contrôle anti-usurpation
|
|
788
|
+
* — jusqu'à son redémarrage. Une panne réseau d'une seconde désarmait une garde de sécurité sur une
|
|
789
|
+
* base pourtant entièrement migrée.
|
|
790
|
+
*
|
|
791
|
+
* C'est la règle du jour appliquée au code de production : **un mécanisme qui ne peut pas mesurer doit
|
|
792
|
+
* refuser de conclure, pas conclure par défaut.** Ici, ne pas savoir distinguer PGRST202 d'un timeout
|
|
793
|
+
* ne rendait pas le repli prudent — il le rendait automatique.
|
|
794
|
+
*
|
|
795
|
+
* PostgREST rend `PGRST202` quand aucune fonction ne correspond au jeu d'arguments nommés. On accepte
|
|
796
|
+
* les DEUX formes que nos contextes produisent (code analysé, ou message contenant le code / la phrase
|
|
797
|
+
* de PostgREST) — et RIEN d'autre : un statut 404 seul ne suffit pas, il peut venir d'ailleurs.
|
|
798
|
+
*/
|
|
799
|
+
function signatureAbsente(erreur) {
|
|
800
|
+
if (!erreur) return false;
|
|
801
|
+
const code = erreur.details && (erreur.details.code || (erreur.details.error && erreur.details.error.code));
|
|
802
|
+
if (code === "PGRST202") return true;
|
|
803
|
+
const texte = String((erreur && erreur.message) || "");
|
|
804
|
+
return texte.includes("PGRST202") || /Could not find the function/i.test(texte);
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
// ⚠️ LE MÉMO EXPIRE — UN « NON » N'A PAS LA DURÉE DE VIE D'UN « OUI ». C'est la doctrine que schema.js
|
|
808
|
+
// applique déjà à ses sondes, et elle vaut ici pour la même raison : « 0018 absente » est un état que
|
|
809
|
+
// l'exploitant peut RÉPARER pendant que le processus tourne. Mémorisé pour toujours, il aurait exigé un
|
|
810
|
+
// redémarrage pour que le durcissement reprenne — une migration appliquée serait restée sans effet, en
|
|
811
|
+
// silence. Un « oui » (la signature existe) n'a pas besoin d'expirer : une fonction ne disparaît pas.
|
|
812
|
+
let _bumpSansDurcissementJusqua = 0;
|
|
813
|
+
const MEMO_SANS_DURCISSEMENT_MS = 60 * 1000;
|
|
783
814
|
async function appelerBump(corps, durcissementVoulu) {
|
|
784
815
|
const appel = (b) => PLAYER.db.request("rpc/player_attendance_bump", { method: "POST", body: b });
|
|
785
|
-
if (durcissementVoulu &&
|
|
816
|
+
if (durcissementVoulu && Date.now() < _bumpSansDurcissementJusqua) {
|
|
786
817
|
const { p_only_if_unclaimed: _retire, ...sansDurcissement } = corps;
|
|
787
818
|
return appel(sansDurcissement);
|
|
788
819
|
}
|
|
@@ -790,9 +821,12 @@ async function appelerBump(corps, durcissementVoulu) {
|
|
|
790
821
|
return await appel(corps);
|
|
791
822
|
} catch (erreur) {
|
|
792
823
|
if (!durcissementVoulu) throw erreur; // rien à retirer : l'échec est réel
|
|
824
|
+
// ⚠️ ON NE SE REPLIE QUE SUR LA PREUVE. Toute autre erreur remonte telle quelle : l'appelant
|
|
825
|
+
// refusera le battement plutôt que de l'écrire sans le contrôle qu'il avait demandé.
|
|
826
|
+
if (!signatureAbsente(erreur)) throw erreur;
|
|
793
827
|
// La signature à 12 arguments n'existe pas (0018 non appliquée) : on retire l'argument et on
|
|
794
828
|
// réessaie. Le durcissement n'est alors PAS appliqué — et ça se dit, une fois, plus bas.
|
|
795
|
-
|
|
829
|
+
_bumpSansDurcissementJusqua = Date.now() + MEMO_SANS_DURCISSEMENT_MS;
|
|
796
830
|
// ⚠️ CE QUI DISPARAÎT SE DIT. Le durcissement demandé n'est pas appliqué : sous
|
|
797
831
|
// PLAYER_PRESENCE_STRICT, la porte se fermerait sur les battements legacy tout en laissant un
|
|
798
832
|
// bootstrap auto-déclaré s'emparer d'une présence réclamée — c'est-à-dire une fermeture qui
|
|
@@ -871,6 +905,21 @@ async function recordAttendance(slug, participant, { presentation = null, ipHash
|
|
|
871
905
|
}
|
|
872
906
|
// Forme inattendue : on n'invente pas de verdict, on laisse la boucle de repli faire l'écriture.
|
|
873
907
|
} catch (erreur) {
|
|
908
|
+
// ⚠️ UN DURCISSEMENT DEMANDÉ QUI N'A PAS PU S'EXÉCUTER NE SE CONTOURNE PAS. Le repli en boucle
|
|
909
|
+
// lire-modifier-réécrire écrit SANS le contrôle anti-usurpation : l'emprunter ici reviendrait à
|
|
910
|
+
// faire par la porte de service ce que le chemin principal vient de refuser. On rend 503 — « je
|
|
911
|
+
// n'ai pas pu vérifier », pas « c'est refusé » ni « c'est écrit » — et le battement suivant
|
|
912
|
+
// réessaiera dans quelques secondes. Fail-closed sur le contrôle, jamais sur la disponibilité des
|
|
913
|
+
// battements ORDINAIRES, qui continuent de se replier normalement.
|
|
914
|
+
if (onlyIfUnclaimed) {
|
|
915
|
+
try {
|
|
916
|
+
PLAYER.errors.capture(new Error(
|
|
917
|
+
"bootstrap de présence refusé : le contrôle anti-usurpation n'a pas pu s'exécuter — "
|
|
918
|
+
+ ((erreur && erreur.message) || erreur),
|
|
919
|
+
), { route: "present-attend" });
|
|
920
|
+
} catch { /* jamais bloquant */ }
|
|
921
|
+
return { ok: false, status: 503 };
|
|
922
|
+
}
|
|
874
923
|
if (!_avertRpcPresence) {
|
|
875
924
|
_avertRpcPresence = true;
|
|
876
925
|
try {
|