discovery-media-player 0.1.2 → 0.1.3

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.2",
3
+ "version": "0.1.3",
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",
@@ -0,0 +1,90 @@
1
+ // @vitest-environment jsdom
2
+ //
3
+ // L'AUDIENCE DOIT VRAIMENT ÊTRE BRANCHÉE SUR L'ÉTAT DU PRÉSENTATEUR.
4
+ //
5
+ // ⚠️ Ce test existe à cause d'une panne en production. `appliquerEtat` était défini dans un bloc
6
+ // de script, et l'abonnement à la diffusion écrit dans le SUIVANT — autre portée. La référence
7
+ // n'existait pas, la ReferenceError partait dans un try/catch muet, et l'audience n'avait aucun
8
+ // écouteur d'état. Invisible pendant tout le temps où une seconde voie (lecture de table) portait
9
+ // la page ; « les pages ne tournent plus » le jour où on l'a retirée pour fermer une fuite.
10
+ //
11
+ // La leçon : deux voies qui font la même chose ne se valident pas l'une l'autre. Tant que la
12
+ // seconde existait, aucun test ne pouvait dire laquelle marchait. On exécute donc la page.
13
+
14
+ // ⚠️ AVANT de requérir le gestionnaire : il déstructure ses dépendances au chargement, donc une
15
+ // substitution plus tardive n'aurait aucun effet — il partirait chercher la vraie base.
16
+ const PRESENTATION_ID = require.resolve("../presentations.js");
17
+ const vraiesPresentations = require("../presentations.js");
18
+ const PRESENTATION = {
19
+ slug: "Ab3-_xYz9012", doc_title: "Démo", file_name: "demo.pdf",
20
+ file_url: "https://exemple.supabase.co/storage/v1/object/public/resources/demo.pdf",
21
+ current_page: 1, active: true, updated_at: "2026-08-13T00:00:00.000Z",
22
+ };
23
+ require.cache[PRESENTATION_ID] = {
24
+ id: PRESENTATION_ID, filename: PRESENTATION_ID, loaded: true,
25
+ exports: { ...vraiesPresentations, getPresentation: async () => ({ ...PRESENTATION }), listMessages: async () => [] },
26
+ };
27
+
28
+ const player = require("../handler.js");
29
+
30
+ function contexteMinimal() {
31
+ return {
32
+ plugins: {}, has: () => false,
33
+ storage: { isAllowedUrl: () => true, async fetchFile() { return null; }, async put() {} },
34
+ db: { async request() { return []; }, async selectAll() { return []; } },
35
+ mail: { async send() {} },
36
+ identity: { async verifyToken() { return null; }, roleOf: () => "", isAdmin: () => false, async canManageShares() { return false; } },
37
+ limits: { async allow() { return true; } },
38
+ branding: { async logo() { return ""; }, name: "", poweredBy: "", loaderName: "", async forKey() { return null; }, title: (b) => b },
39
+ errors: { async capture() {} },
40
+ legal: { sourceUrl: "", legalUrl: "", privacyUrl: "", trackingNotice: "" },
41
+ config: { supabaseUrl: "https://exemple.supabase.co", supabasePublishableKey: "cle", mapsKey: "", extraFrameAncestors: [] },
42
+ };
43
+ }
44
+
45
+ async function pageAudience() {
46
+ player.init(contexteMinimal());
47
+ const res = {
48
+ statusCode: 0, headers: {}, body: "",
49
+ setHeader(k, v) { this.headers[k.toLowerCase()] = v; },
50
+ end(b) { this.body = String(b == null ? "" : b); },
51
+ };
52
+ await player.handler({ method: "GET", headers: {}, socket: {}, query: { present: PRESENTATION.slug } }, res);
53
+ return res.body;
54
+ }
55
+
56
+ describe("page audience", () => {
57
+ it("expose vraiment le gestionnaire que la couche live doit brancher", async () => {
58
+ const html = await pageAudience();
59
+
60
+ // ⚠️ On EXÉCUTE la page. Le test statique ne pouvait pas voir le défaut : le code fautif
61
+ // contenait bien l'appel à onState — il référençait simplement un nom absent de cette
62
+ // portée-là. Seule l'exécution distingue « écrit » de « branché ».
63
+ window.supabase = { createClient: () => ({ channel: () => { const c = { on: () => c, subscribe: () => c }; return c; } }) };
64
+ for (const m of html.matchAll(/<script[^>]*>([\s\S]*?)<\/script>/g)) {
65
+ // `window.eval` et pas `new Function` : les scripts déclarent des `var` au niveau global,
66
+ // qu'une portée de fonction rendrait invisibles au bloc suivant — c'est exactement le
67
+ // genre de nuance de portée qui a produit le bug.
68
+ try { window.eval(m[1]); } catch { /* les scripts qui dépendent de pdf.js ne nous intéressent pas */ }
69
+ }
70
+
71
+ expect(typeof window.__presAppliquerEtat,
72
+ "le gestionnaire d'état n'est pas exposé : la couche live, définie dans un autre bloc, ne peut pas le brancher")
73
+ .toBe("function");
74
+ });
75
+
76
+ it("branche ce gestionnaire-là, et pas un nom hors de portée", async () => {
77
+ const html = await pageAudience();
78
+ expect(html).toContain("Live.onState(window.__presAppliquerEtat)");
79
+ });
80
+
81
+ // Il ne doit plus rester une seule voie qui exige une table lisible publiquement.
82
+ it("n'ouvre aucun abonnement à une table", async () => {
83
+ expect(await pageAudience()).not.toContain("postgres_changes");
84
+ });
85
+
86
+ // Un câblage raté doit se dire. C'est le silence, pas le bug, qui a coûté la journée.
87
+ it("crie si la couche live manque, au lieu d'avaler l'erreur", async () => {
88
+ expect(await pageAudience()).toContain("console.error('[present]");
89
+ });
90
+ });
package/server/handler.js CHANGED
@@ -397,8 +397,8 @@ var Live=(function(){
397
397
  try{sb=window.supabase.createClient(LIVECFG.supaUrl,LIVECFG.supaKey,{realtime:{params:{eventsPerSecond:10}}});
398
398
  ch=sb.channel('plive-'+slug,{config:{presence:{key:(me.email||me.name||'x')+':'+MYID}}});
399
399
  ch.on('presence',{event:'sync'},function(){renderPres(ch.presenceState());});
400
- ch.on('postgres_changes',{event:'INSERT',schema:'public',table:'doc_presentation_messages',filter:'slug=eq.'+slug},function(p){if(p&&p.new&&addMsg(p.new))notifyMsg(p.new);});
401
- ch.on('postgres_changes',{event:'UPDATE',schema:'public',table:'doc_presentation_messages',filter:'slug=eq.'+slug},function(p){if(p&&p.new)updateMsg(p.new);});
400
+ // Plus d'abonnement à la table des messages : elle n'est plus publiée ni lisible
401
+ // publiquement. Tout passe par la diffusion, et l'historique par la route de chat.
402
402
  ch.on('broadcast',{event:'msg'},function(p){if(p&&p.payload&&addMsg(p.payload))notifyMsg(p.payload);});
403
403
  ch.on('broadcast',{event:'msg-upd'},function(p){if(p&&p.payload)updateMsg(p.payload);});
404
404
  ch.on('broadcast',{event:'typing'},function(p){onTyping(p&&p.payload);});
@@ -2016,6 +2016,12 @@ function presentHtml(pres, nonce, logoUrl, supaUrl, supaKey) {
2016
2016
  // diffusion temps réel. La règle d'ordre (terminée > carte > changement de doc > page) et la
2017
2017
  // re-validation du contenu vivent dans le module, pas ici.
2018
2018
  var _etatVu='';
2019
+ // ⚠️ EXPOSÉ HORS DE CETTE FERMETURE, ET C'EST INDISPENSABLE. La couche Live est définie dans
2020
+ // le bloc de script SUIVANT : y appeler onState avec ce nom-ci référence une fonction qui
2021
+ // n'existe pas dans cette portée-là. L'exception partait dans un try/catch muet, donc
2022
+ // l'audience n'avait aucun écouteur de diffusion — invisible tant que la lecture de table
2023
+ // portait encore la page, puis « les pages ne tournent plus » le jour où on l'a retirée.
2024
+ window.__presAppliquerEtat = appliquerEtat;
2019
2025
  function appliquerEtat(row){
2020
2026
  if(!row) return;
2021
2027
  // La table et la diffusion portent la même vérité : sans cette garde, chaque changement de
@@ -2042,16 +2048,13 @@ function presentHtml(pres, nonce, logoUrl, supaUrl, supaKey) {
2042
2048
  document.addEventListener('visibilitychange',function(){ if(document.visibilityState==='visible') relireEtat(); });
2043
2049
  window.addEventListener('online', relireEtat);
2044
2050
 
2045
- // Realtime : suivre la page courante + la fin de session (UPDATE de notre ligne).
2046
- try{
2047
- if(window.supabase && CFG.supaUrl && CFG.supaKey){
2048
- var sb=window.supabase.createClient(CFG.supaUrl, CFG.supaKey, {realtime:{params:{eventsPerSecond:5}}});
2049
- sb.channel('present-'+CFG.slug)
2050
- .on('postgres_changes',{event:'UPDATE',schema:'public',table:'doc_presentations',filter:'slug=eq.'+CFG.slug},function(payload){
2051
- appliquerEtat(payload && payload.new);
2052
- }).subscribe();
2053
- }
2054
- }catch(e){}
2051
+ // ⚠️ PLUS D'ABONNEMENT À LA TABLE ICI. Il exigeait une lecture publique de
2052
+ // de la table des présentations — donc, avec la clé publiable, TOUTES les présentations de
2053
+ // l'instance. L'état arrive par diffusion du présentateur, et la relecture d'état ci-dessus
2054
+ // rattrape les arrivées tardives et les retours d'onglet en interrogeant le serveur.
2055
+ //
2056
+ // Le laisser en place n'était pas neutre : il ouvrait un abonnement qui ne recevra jamais
2057
+ // rien, ce qui donne l'impression que le suivi est branché alors qu'il ne l'est pas.
2055
2058
  // Si on rejoint alors que le présentateur est DÉJÀ sur une carte / Street View → l'afficher dès que Map3DD est prêt.
2056
2059
  if(CFG.content && (CFG.content.kind==='map'||CFG.content.kind==='streetview')){ var _mi=setInterval(function(){ if(window.Map3DD){ clearInterval(_mi); if(CFG.content.kind==='streetview')Map3DD.enterSV(CFG.content,false); else Map3DD.enter(CFG.content,false); } },100); setTimeout(function(){ clearInterval(_mi); },8000); }
2057
2060
  })();
@@ -2067,7 +2070,16 @@ function presentHtml(pres, nonce, logoUrl, supaUrl, supaKey) {
2067
2070
  // L'état arrive maintenant par DEUX voies : la table (historique) et la diffusion du
2068
2071
  // présentateur (nouvelle). Les deux passent par le même filtre, qui ignore un état déjà
2069
2072
  // appliqué — recevoir deux fois la même chose ne doit pas re-rendre la page.
2070
- try{ if(window.Live) Live.onState(appliquerEtat); }catch(e){}
2073
+ // Par la référence exposée plus haut : le nom local n'existe pas dans cette portée-ci.
2074
+ try{
2075
+ if(!window.Live || !window.__presAppliquerEtat) throw new Error('couche live absente');
2076
+ Live.onState(window.__presAppliquerEtat);
2077
+ }catch(e){
2078
+ // PLUS DE SILENCE ICI. Ce try/catch a avalé une ReferenceError : l'audience n'avait aucun
2079
+ // ecouteur d'etat, ce qui ne se voyait pas tant qu'une seconde voie portait la page. Un
2080
+ // cablage rate doit se dire, meme si la page continue de vivre.
2081
+ console.error('[present] suivi de l etat non branche :', e && e.message);
2082
+ }
2071
2083
  // « Reprendre la main » : si le membre connecté (même origine) devient propriétaire de CETTE présentation
2072
2084
  // (après un transfert), on affiche un bouton pour ouvrir la visionneuse en pilotage — sinon on ne peut pas
2073
2085
  // piloter depuis la page audience.