discovery-media-player 0.1.80 → 0.1.82

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/docs/RETENTION.md CHANGED
@@ -115,6 +115,13 @@ Parcours guidé par l'agent : **purge 13 mois** après `last_at`.
115
115
  contexte hôte fournit `storage.remove` (capacité optionnelle). Sans elle, l'URL devient
116
116
  introuvable depuis le produit mais l'objet survit dans le bucket — c'est dit ici plutôt que
117
117
  simulé.
118
+ - **Le plafond des présentations est GLOBAL** : messages et présences partagent chacun un budget
119
+ `plafond` réparti sur toutes les présentations d'une exécution — pas un plafond par présentation
120
+ (sinon 500 × 5000 = 2,5 M de lignes possibles). La boucle s'arrête quand les budgets sont
121
+ épuisés, sans supprimer les présentations restantes.
122
+ - **Le rapport dryRun est complet pour les présentations** : `messagesExaminees`,
123
+ `presencesExaminees` et `fichiersCandidats` disent ce que la VRAIE purge ferait — même parcours
124
+ de sélection, suppression no-op, `efface.* = 0`.
118
125
  - **La purge avance par LOTS bornés** (200 lignes, plafond 5000 par table et 500 présentations
119
126
  par exécution) : elle sélectionne un lot d'identifiants, les supprime par `id=in.(…)`, et
120
127
  recommence. Le rapport (`r.rapport`) porte, par table : `examinees`, `supprimees`, `tronque`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "discovery-media-player",
3
- "version": "0.1.80",
3
+ "version": "0.1.82",
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",
@@ -88,7 +88,11 @@ function optionsValidees(opts) {
88
88
  // Une valeur pour `id=in.(…)` : double-guillemets, guillemet et antislash internes échappés —
89
89
  // PostgREST exige le guillemetage dès qu'une valeur porte un caractère réservé (`:` d'une clé de
90
90
  // débit, par exemple). Guillemeter TOUJOURS est correct et évite d'avoir à deviner.
91
- const guill = (v) => '"' + String(v).replace(/\\/g, "\\\\").replace(/"/g, '\\"') + '"';
91
+ // ⚠️ DOUBLE encodage nécessaire (trouvé par le banc volumétrique, audit 10) : le guillemetage
92
+ // gère les délimiteurs de PostgREST (virgule, parenthèse) ; l'encodage d'URL gère ceux de l'URL
93
+ // (`&`, `#`, `+`, espace…) — un `&` non encodé dans une valeur coupe le filtre `in.(…)` en deux.
94
+ // PostgREST décode le percent-encoding avant de parser, donc `%22…%26…%22` redevient `"…&…"`.
95
+ const guill = (v) => encodeURIComponent('"' + String(v).replace(/\\/g, "\\\\").replace(/"/g, '\\"') + '"');
92
96
 
93
97
  // ⚠️ LES DEUX SEULES PORTES DE DESTRUCTION DU MODULE — dryRun court-circuité en PREMIÈRE ligne.
94
98
  // « Honoré » ne se prouve pas en lisant chaque DELETE (une fenêtre de lecture décide de la réponse
@@ -120,7 +124,8 @@ async function retirerFichier(chemin, opts) {
120
124
  // au RESTE du plafond (`min(taille, plafond - examinees)`) pour ne jamais le dépasser. Et on
121
125
  // compte les lignes RENDUES par le DELETE (`return=representation&select=id`), pas les ids
122
126
  // présélectionnés : deux exécutions concurrentes n'annoncent pas deux fois la même suppression.
123
- async function purgerParLots(table, filtre, colId, { dryRun = false, taille = LOT, plafond = PLAFOND } = {}) {
127
+ async function purgerParLots(table, filtre, colId, { dryRun = false, taille = LOT, plafond = PLAFOND } = {}, plafondForce) {
128
+ if (plafondForce !== undefined) plafond = plafondForce;
124
129
  let examinees = 0, supprimees = 0, tronque = false, curseur = null;
125
130
  for (;;) {
126
131
  const reste = plafond - examinees;
@@ -154,9 +159,9 @@ const { cheminPieceJointe: cheminSurSlug } = require("./presentations");
154
159
  // Purge des messages d'une présentation morte, par lots bornés qui lisent id+attachment ENSEMBLE :
155
160
  // on retire les fichiers du bucket du lot (si l'hôte sait), puis on supprime le lot. Rend `tronque`
156
161
  // pour que l'appelant décide de garder ou non la présentation. Compte les lignes RENDUES.
157
- async function purgerMessagesPresentation(slug, opts, base) {
158
- const { dryRun, taille, plafond } = opts;
159
- let supprimees = 0, fichiers = 0, fichiersErreur = 0, examinees = 0, tronque = false, curseur = null;
162
+ async function purgerMessagesPresentation(slug, opts, base, plafond) {
163
+ const { dryRun, taille } = opts;
164
+ let supprimees = 0, fichiers = 0, fichiersErreur = 0, fichiersCandidats = 0, examinees = 0, tronque = false, curseur = null;
160
165
  for (;;) {
161
166
  const reste = plafond - examinees;
162
167
  if (reste <= 0) { tronque = await resteEncore("doc_presentation_messages", `slug=eq.${enc(slug)}`, "id", curseur, dryRun); break; }
@@ -169,13 +174,14 @@ async function purgerMessagesPresentation(slug, opts, base) {
169
174
  for (const j of lot) {
170
175
  const url = j.attachment && (typeof j.attachment === "object" ? j.attachment.url : j.attachment);
171
176
  const chemin = cheminSurSlug(url, slug, base); // hors du dossier du slug → null → jamais retiré (barrière 2)
177
+ if (chemin) fichiersCandidats += 1; // compté même en dry-run (ce que la vraie purge tenterait)
172
178
  const issue = await retirerFichier(chemin, { dryRun });
173
179
  if (issue === true) fichiers += 1; else if (issue === false) fichiersErreur += 1; // false = échec compté
174
180
  }
175
181
  supprimees += await effacerParIds("doc_presentation_messages", "id", lot.map((r) => r.id).filter((v) => v != null), { dryRun });
176
182
  if (lot.length < limite) break;
177
183
  }
178
- return { supprimees, fichiers, fichiersErreur, examinees, tronque };
184
+ return { supprimees, fichiers, fichiersErreur, fichiersCandidats, examinees, tronque };
179
185
  }
180
186
 
181
187
  async function purgerRetention(now, optsBrutes = {}) {
@@ -206,22 +212,35 @@ async function purgerRetention(now, optsBrutes = {}) {
206
212
  // ses présences — et les fichiers du bucket si storage.remove est fourni (OPTIONNELLE).
207
213
  const bPres = borne(now, f.presentationsMois);
208
214
  const mortes = await PLAYER.db.request(`doc_presentations?active=eq.false&updated_at=lt.${enc(bPres)}&select=slug&order=slug.asc&limit=${PLAFOND_PRESENTATIONS}`);
209
- const presRapport = { examinees: 0, supprimees: 0, messages: 0, presences: 0, fichiers: 0, fichiersErreur: 0, tronque: Array.isArray(mortes) && mortes.length >= PLAFOND_PRESENTATIONS };
215
+ // ⚠️ BUDGET GLOBAL AUX PRÉSENTATIONS (P2 dixième audit). Le plafond n'était appliqué qu'À CHAQUE
216
+ // présentation : 500 présentations × 5 000 = 2,5 M de messages en une exécution, timeout et
217
+ // contention avec le chat. Deux budgets partagés — messages et présences — décrémentés au fil
218
+ // des présentations ; la boucle s'arrête quand ils sont épuisés (tronque), sans supprimer les
219
+ // parents restants. En dry-run, on parcourt quand même pour REMONTER ce que la vraie purge
220
+ // ferait (examinés), sans jamais détruire.
221
+ const presRapport = { examinees: 0, supprimees: 0, messages: 0, presences: 0, messagesExaminees: 0, presencesExaminees: 0, fichiers: 0, fichiersErreur: 0, fichiersCandidats: 0, tronque: Array.isArray(mortes) && mortes.length >= PLAFOND_PRESENTATIONS };
222
+ let budgetMessages = opts.plafond, budgetPresences = opts.plafond;
210
223
  for (const p of (Array.isArray(mortes) ? mortes : [])) {
211
224
  const slug = p && p.slug; if (!slug) continue;
225
+ if (budgetMessages <= 0 && budgetPresences <= 0) { presRapport.tronque = true; break; } // budgets épuisés
212
226
  presRapport.examinees += 1;
213
- // Messages : lot borné qui lit id ET attachment ENSEMBLE (la lecture des pièces jointes n'est
214
- // plus une requête globale non bornée — P2), retire les fichiers du lot, puis supprime le lot.
215
- const msgs = await purgerMessagesPresentation(slug, opts, base);
227
+ const msgs = await purgerMessagesPresentation(slug, opts, base, budgetMessages);
216
228
  presRapport.messages += msgs.supprimees;
229
+ presRapport.messagesExaminees += msgs.examinees;
217
230
  presRapport.fichiers += msgs.fichiers;
218
231
  presRapport.fichiersErreur += msgs.fichiersErreur;
219
- const pres = opts.dryRun ? { supprimees: 0, tronque: false } : await purgerParLots("doc_presentation_attendees", `slug=eq.${enc(slug)}`, "attendee_key", opts);
232
+ presRapport.fichiersCandidats += msgs.fichiersCandidats;
233
+ budgetMessages -= opts.dryRun ? msgs.examinees : msgs.supprimees;
234
+ // Présences : interrogées AUSSI en dry-run (pour remonter presencesExaminees), suppression
235
+ // no-op via effacerParIds. Budget global partagé.
236
+ const pres = await purgerParLots("doc_presentation_attendees", `slug=eq.${enc(slug)}`, "attendee_key", opts, Math.max(0, budgetPresences));
220
237
  presRapport.presences += pres.supprimees;
221
- // ⚠️ LA PRÉSENTATION N'EST SUPPRIMÉE QUE SI TOUS SES ENFANTS SONT PARTIS (P2 neuvième audit) :
222
- // sinon 5 000 messages partent, le parent aussi, et 1 000 orphelins restent que le balayage
223
- // suivant ne peut plus rattacher à leur présentation. Tronqués on garde le parent inactif
224
- // pour le passage suivant.
238
+ presRapport.presencesExaminees += pres.examinees;
239
+ budgetPresences -= opts.dryRun ? pres.examinees : pres.supprimees;
240
+ // ⚠️ LE RAPPORT DIT LA VÉRITÉ : si un enfant est tronqué, le rapport parent l'est aussi
241
+ // (P2 onzième audit — sinon la supervision croit la purge complète alors qu'un reste subsiste).
242
+ presRapport.tronque = presRapport.tronque || msgs.tronque || pres.tronque;
243
+ // La présentation n'est supprimée que si TOUS ses enfants sont partis (9e audit).
225
244
  if (!opts.dryRun && !msgs.tronque && !pres.tronque) {
226
245
  presRapport.supprimees += (await purgerParLots("doc_presentations", `slug=eq.${enc(slug)}`, "slug", opts)).supprimees;
227
246
  }