discovery-media-player 0.1.62 → 0.1.63
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 +1 -1
- package/server/handler.js +15 -3
- package/server/presentations.js +55 -18
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "discovery-media-player",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.63",
|
|
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/handler.js
CHANGED
|
@@ -337,9 +337,21 @@ var Live=(function(){
|
|
|
337
337
|
// conversations de TOUTES les présentations, pas seulement la sienne. C'était le dernier
|
|
338
338
|
// obstacle avant de pouvoir fermer cette lecture.
|
|
339
339
|
// L'émetteur diffuse la ligne que le serveur vient d'écrire ; les autres l'ajoutent chez eux.
|
|
340
|
-
// ⚠️ La diffusion ne revient pas à son émetteur : il ajoute donc SA copie lui-même
|
|
341
|
-
|
|
342
|
-
|
|
340
|
+
// ⚠️ La diffusion ne revient pas à son émetteur : il ajoute donc SA copie lui-même — depuis la
|
|
341
|
+
// RÉPONSE du serveur, jamais depuis le canal.
|
|
342
|
+
//
|
|
343
|
+
// ⚠️ LA CHARGE EST VIDE, ET C'EST CE QUI REND LA PROPRIÉTÉ STRUCTURELLE. Elle transportait la
|
|
344
|
+
// ligne projetée — que le récepteur IGNORAIT (il relit par HTTP, cf. le commentaire du
|
|
345
|
+
// récepteur : sans relecture, une notification pourrait afficher un texte forgé). Un contenu
|
|
346
|
+
// que personne ne consomme n'est pas neutre : la description « un signal, jamais un contenu »
|
|
347
|
+
// était plus forte que le code, et tenait lieu de garde sans en être une — le jour où un
|
|
348
|
+
// récepteur nouveau aurait lu la charge « puisqu'elle est là », la projection serait devenue
|
|
349
|
+
// optionnelle sans que rien ne le dise. Le second hôte a levé l'écart ; sendState et
|
|
350
|
+
// sendMap avaient déjà la bonne forme. Un commentaire ici même affirmait l'ancien monde
|
|
351
|
+
// (« les autres l'ajoutent chez eux ») : il a fait dériver sa lecture, pas la nôtre — un
|
|
352
|
+
// commentaire n'est pas du code, et il vieillit sans essai pour le contredire.
|
|
353
|
+
function sendMsg(m){if(!m)return;try{if(ch)ch.send({type:'broadcast',event:'msg',payload:{}});}catch(e){}}
|
|
354
|
+
function sendMsgUpd(m){if(!m)return;try{if(ch)ch.send({type:'broadcast',event:'msg-upd',payload:{}});}catch(e){}}
|
|
343
355
|
// Édition, suppression, réaction : le serveur renvoie la ligne à jour, on l'applique chez soi
|
|
344
356
|
// puis on la diffuse. Même chemin pour les trois — une seule façon de se tromper.
|
|
345
357
|
function majDiffusee(r){return r.json().then(function(d){if(d&&d.ok&&d.message){updateMsg(d.message);sendMsgUpd(d.message);}}).catch(function(){});}
|
package/server/presentations.js
CHANGED
|
@@ -671,25 +671,62 @@ async function recordAttendance(slug, { key, name, email, avatar, isMember, isPr
|
|
|
671
671
|
// resté ouvert, longtemps après la fin.
|
|
672
672
|
if (pres.active === false && pres.control_hash == null) return REFUS_ARCHIVE;
|
|
673
673
|
const page = Math.max(1, Math.trunc(Number(pres.current_page) || 1));
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
674
|
+
// ⚠️ LE DERNIER LIRE-MODIFIER-RÉÉCRIRE DU DÉPÔT, fermé comme les autres — mais SANS migration :
|
|
675
|
+
// `last_seen` change à chaque battement accepté, c'est un verrou optimiste gratuit. L'écriture
|
|
676
|
+
// est conditionnée à la valeur LUE ; zéro ligne = quelqu'un d'autre a battu entre-temps (l'autre
|
|
677
|
+
// onglet du même participant) — on relit et on rejoue. Sans ça, deux onglets qui battent dans la
|
|
678
|
+
// même seconde perdaient une page vue : la seconde réécriture emportait la première.
|
|
679
|
+
let cur = null;
|
|
680
|
+
for (let essai = 0; essai < 4; essai += 1) {
|
|
681
|
+
if (cur === null) {
|
|
682
|
+
const rows = await PLAYER.db.request(`doc_presentation_attendees?slug=eq.${enc(slug)}&attendee_key=eq.${enc(String(key))}&select=*&limit=1`);
|
|
683
|
+
cur = (Array.isArray(rows) && rows[0]) || false;
|
|
684
|
+
}
|
|
685
|
+
if (!cur) {
|
|
686
|
+
const now = Date.now();
|
|
687
|
+
const row = { slug: String(slug), attendee_key: String(key).slice(0, 200), name: (name || "").slice(0, 120) || null, email: lc(email) || null, avatar: (avatar || "").slice(0, 600) || null, is_member: !!isMember, is_presenter: !!isPresenter, first_seen: new Date(now).toISOString(), last_seen: new Date(now).toISOString(), total_ms: 0, pages: [page] };
|
|
688
|
+
try {
|
|
689
|
+
await PLAYER.db.request("doc_presentation_attendees", { method: "POST", headers: { Prefer: "return=minimal" }, body: [row] });
|
|
690
|
+
return { ok: true };
|
|
691
|
+
} catch (erreur) {
|
|
692
|
+
// ⚠️ La clé primaire (slug, attendee_key) fait son travail : deux PREMIERS battements
|
|
693
|
+
// simultanés (deux onglets ouverts ensemble), et le second recevait un 409 que personne ne
|
|
694
|
+
// rattrapait — un 500 pour un battement, bénin mais faux. Le conflit dit « la ligne existe
|
|
695
|
+
// maintenant » : on la relit et on continue en mise à jour. Tout autre échec remonte.
|
|
696
|
+
if (!String((erreur && erreur.message) || "").includes("409")) throw erreur;
|
|
697
|
+
// Journalisé comme bénin : deux onglets qui arrivent ensemble sont une information, pas
|
|
698
|
+
// une panne — et la garde des écritures muettes exige que tout rattrapage parle.
|
|
699
|
+
try { PLAYER.errors.capture(new Error("présence déjà ouverte (second onglet) : " + String(slug)), { route: "present-attend", benin: true }); } catch { /* jamais bloquant */ }
|
|
700
|
+
cur = null;
|
|
701
|
+
continue;
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
// ⚠️ UN `last_seen` ACCEPTÉ EST STRICTEMENT CROISSANT — sans ça, la serrure est aveugle dans
|
|
705
|
+
// la milliseconde : deux battements dans la même ms écrivent la MÊME valeur, la condition du
|
|
706
|
+
// suivant matche encore, et l'écrasement revient par la fenêtre qu'on venait de fermer. Vu au
|
|
707
|
+
// banc (trois écritures, trois conditions vraies) avant d'être vu nulle part ailleurs. Le prix :
|
|
708
|
+
// une milliseconde d'avance sur l'horloge dans les rafales, invisible pour des statistiques.
|
|
709
|
+
const lu = new Date(cur.last_seen || 0).getTime();
|
|
710
|
+
const now = Math.max(Date.now(), lu + 1);
|
|
711
|
+
const gap = now - lu;
|
|
712
|
+
const addMs = gap > 0 && gap <= ATTEND_MAX_GAP_MS ? gap : 0;
|
|
713
|
+
const pages = Array.isArray(cur.pages) ? cur.pages.slice() : [];
|
|
714
|
+
if (!pages.includes(page)) pages.push(page);
|
|
715
|
+
const ecrit = await ecrireSiEncoreVrai(
|
|
716
|
+
`doc_presentation_attendees?slug=eq.${enc(slug)}&attendee_key=eq.${enc(String(key))}&last_seen=eq.${enc(String(cur.last_seen))}`,
|
|
717
|
+
{ last_seen: new Date(now).toISOString(), total_ms: Number(cur.total_ms || 0) + addMs, pages, name: (name || cur.name || "").slice(0, 120) || null, avatar: (avatar || cur.avatar || "").slice(0, 600) || null,
|
|
718
|
+
// ⚠️ Les deux drapeaux se remettent à jour à chaque battement, ils ne sont plus figés à la
|
|
719
|
+
// première ligne : un transfert de présentation change qui porte le titre, et une session qui
|
|
720
|
+
// s'authentifie en cours de route devient un membre. Figés, ils décriraient l'instant de
|
|
721
|
+
// l'arrivée et non la réalité — et le premier arrivé aurait raison pour toujours.
|
|
722
|
+
is_member: !!isMember, is_presenter: !!isPresenter },
|
|
723
|
+
);
|
|
724
|
+
if (ecrit) return { ok: true };
|
|
725
|
+
cur = null; // battu en vol : on relira l'état frais au tour suivant
|
|
681
726
|
}
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
if (!pages.includes(page)) pages.push(page);
|
|
686
|
-
await PLAYER.db.request(`doc_presentation_attendees?slug=eq.${enc(slug)}&attendee_key=eq.${enc(String(key))}`, { method: "PATCH", headers: { Prefer: "return=minimal" }, body: { last_seen: new Date(now).toISOString(), total_ms: Number(cur.total_ms || 0) + addMs, pages, name: (name || cur.name || "").slice(0, 120) || null, avatar: (avatar || cur.avatar || "").slice(0, 600) || null,
|
|
687
|
-
// ⚠️ Les deux drapeaux se remettent à jour à chaque battement, ils ne sont plus figés à la
|
|
688
|
-
// première ligne : un transfert de présentation change qui porte le titre, et une session qui
|
|
689
|
-
// s'authentifie en cours de route devient un membre. Figés, ils décriraient l'instant de
|
|
690
|
-
// l'arrivée et non la réalité — et le premier arrivé aurait raison pour toujours.
|
|
691
|
-
is_member: !!isMember, is_presenter: !!isPresenter } });
|
|
692
|
-
return { ok: true };
|
|
727
|
+
// Quatre tours perdus : un battement de présence se reperd sans conséquence — le suivant arrive
|
|
728
|
+
// dans quelques secondes. Refuser net plutôt qu'écraser ce que les gagnants viennent d'écrire.
|
|
729
|
+
return { ok: false, status: 409 };
|
|
693
730
|
}
|
|
694
731
|
|
|
695
732
|
// Détail d'une présentation : entête + participants + nombre de messages par participant.
|