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 +7 -0
- package/package.json +1 -1
- package/server/retention.js +34 -15
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.
|
|
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",
|
package/server/retention.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
222
|
-
|
|
223
|
-
//
|
|
224
|
-
//
|
|
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
|
}
|