discovery-media-player 0.1.162 → 0.1.163
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 +55 -17
- package/docs/HOST-CONTRACT.md +23 -5
- package/docs/RETENTION.md +5 -2
- package/package.json +1 -1
- package/server/constantes-presentation.js +27 -0
- package/server/mesures.js +26 -0
- package/server/presentations.js +4 -1
- package/server/retention.js +5 -2
- package/server/schema.js +5 -2
package/context/standalone.js
CHANGED
|
@@ -41,6 +41,41 @@ function sansBarreFinale(valeur) {
|
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
/** Client REST minimal (PostgREST). Absent de configuration ⇒ chaque appel échoue franchement. */
|
|
44
|
+
/**
|
|
45
|
+
* ⚠️ UN SEUL ENDROIT QUI BORNE, PARCE QU'IL Y EN AVAIT UN SUR QUATRE.
|
|
46
|
+
*
|
|
47
|
+
* `db.request` abandonnait déjà après un délai, avec le raisonnement écrit à côté : sans signal, un
|
|
48
|
+
* service qui accepte la connexion et ne répond plus immobilise la requête, sa socket ET la place
|
|
49
|
+
* d'admission jusqu'à ce que la plateforme tue la fonction. Trois autres appels — suppression
|
|
50
|
+
* Storage, signature d'envoi, vérification de jeton — partaient nus. Un audit externe l'a mesuré le
|
|
51
|
+
* 11/09 en remplaçant `fetch` : `REST hasSignal true`, les trois autres `false`.
|
|
52
|
+
*
|
|
53
|
+
* ⚠️ CE N'EST PAS UN CONTOURNEMENT D'AUTORISATION : ces chemins refusent en cas d'échec, ils rendent
|
|
54
|
+
* `null` ou `false`. Le risque est de DISPONIBILITÉ — sous concurrence, des sockets, de la mémoire
|
|
55
|
+
* et des exécutions serverless retenues par un tiers lent, y compris pour les purges et les
|
|
56
|
+
* consultations protégées.
|
|
57
|
+
*
|
|
58
|
+
* ⚠️ ET UNE COURSE DE PROMESSES NE SUFFIRAIT PAS : elle rendrait la main sans ANNULER le `fetch`,
|
|
59
|
+
* donc sans rien libérer. C'est `AbortSignal` ou rien. Un signal fourni par l'appelant a priorité —
|
|
60
|
+
* un hôte qui borne lui-même une opération longue n'est pas écrasé.
|
|
61
|
+
*/
|
|
62
|
+
function fetchBorne(cible, options = {}, delaiMs) {
|
|
63
|
+
const signal = options.signal
|
|
64
|
+
|| (typeof AbortSignal !== "undefined" && AbortSignal.timeout ? AbortSignal.timeout(delaiMs) : undefined);
|
|
65
|
+
return fetch(cible, { ...options, ...(signal ? { signal } : {}) });
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Les délais, nommés plutôt qu'écrits en clair sur l'appel. ⚠️ ILS NE SONT PAS ÉGAUX, ET C'EST LE
|
|
70
|
+
* SUJET : une vérification de jeton est sur le chemin d'une réponse qu'un visiteur attend, un
|
|
71
|
+
* transfert vers Storage ne l'est pas. Un délai unique ferait patienter le visiteur au rythme du
|
|
72
|
+
* service le plus lent.
|
|
73
|
+
*/
|
|
74
|
+
const DELAI_AUTH_MS = 5000;
|
|
75
|
+
const DELAI_ROUTE_HOTE_MS = 4000;
|
|
76
|
+
const DELAI_STOCKAGE_MS = 15000;
|
|
77
|
+
const DELAI_BASE_MS = 15000;
|
|
78
|
+
|
|
44
79
|
function creerDb(env) {
|
|
45
80
|
const url = sansBarreFinale(env.SUPABASE_URL);
|
|
46
81
|
const cle = String(env.SUPABASE_SERVICE_ROLE_KEY || "");
|
|
@@ -67,12 +102,10 @@ function creerDb(env) {
|
|
|
67
102
|
// transforme un ralentissement en refus général. Une course `Promise.race` ne suffirait pas —
|
|
68
103
|
// elle rendrait la main sans ANNULER le fetch, donc sans libérer quoi que ce soit. L'hôte peut
|
|
69
104
|
// fournir son propre `signal` (opérations longues : purges, transferts). (Audit externe.)
|
|
70
|
-
const delai = Number(options.timeoutMs) > 0 ? Number(options.timeoutMs) :
|
|
71
|
-
const
|
|
72
|
-
? AbortSignal.timeout(delai) : undefined);
|
|
73
|
-
const r = await fetch(`${url}/rest/v1/${chemin}`, {
|
|
105
|
+
const delai = Number(options.timeoutMs) > 0 ? Number(options.timeoutMs) : DELAI_BASE_MS;
|
|
106
|
+
const r = await fetchBorne(`${url}/rest/v1/${chemin}`, {
|
|
74
107
|
method: methode,
|
|
75
|
-
...(signal ? { signal } : {}),
|
|
108
|
+
...(options.signal ? { signal: options.signal } : {}),
|
|
76
109
|
headers: {
|
|
77
110
|
apikey: cle,
|
|
78
111
|
Authorization: `Bearer ${cle}`,
|
|
@@ -80,7 +113,7 @@ function creerDb(env) {
|
|
|
80
113
|
...(options.headers || {}),
|
|
81
114
|
},
|
|
82
115
|
body: options.body ? JSON.stringify(options.body) : undefined,
|
|
83
|
-
});
|
|
116
|
+
}, delai);
|
|
84
117
|
if (!r.ok) {
|
|
85
118
|
// ⚠️ LE CORPS DIT POURQUOI, LE CODE NE DIT QUE COMBIEN. Un « 400 » nu a coûté un aller-retour
|
|
86
119
|
// de forge complet pour apprendre ce que PostgREST avait écrit dans sa réponse depuis le
|
|
@@ -166,15 +199,14 @@ async function appelHote(url, secret, corps, errors) {
|
|
|
166
199
|
// perdu exactement ce temps-là.
|
|
167
200
|
const signaler = (quoi) => { try { errors && errors.capture(new Error(`route hôte : ${quoi}`), { url }); } catch { /* jamais bloquant */ } };
|
|
168
201
|
try {
|
|
169
|
-
const r = await
|
|
202
|
+
const r = await fetchBorne(url, {
|
|
170
203
|
method: "POST",
|
|
171
204
|
headers: {
|
|
172
205
|
"Content-Type": "application/json",
|
|
173
206
|
...(secret ? { "x-player-fetch-secret": secret } : {}),
|
|
174
207
|
},
|
|
175
208
|
body: JSON.stringify(corps),
|
|
176
|
-
|
|
177
|
-
});
|
|
209
|
+
}, DELAI_ROUTE_HOTE_MS); // une décision qui tarde est une décision absente
|
|
178
210
|
if (!r.ok) { signaler(`réponse ${r.status}`); return null; }
|
|
179
211
|
const d = await r.json().catch(() => null);
|
|
180
212
|
if (!d || typeof d !== "object") { signaler("réponse illisible (JSON attendu)"); return null; }
|
|
@@ -306,8 +338,14 @@ function creerLimites(db, journal) {
|
|
|
306
338
|
// passer ; dans les deux cas on le dit, en NOMMANT le fichier à appliquer. C'est la règle
|
|
307
339
|
// du chemin de migration : dégrader, jamais casser, et ne jamais dégrader en silence.
|
|
308
340
|
prevenirUneFois(
|
|
309
|
-
|
|
310
|
-
|
|
341
|
+
// ⚠️ CE MESSAGE NOMMAIT UN MODE QUI N'EXISTE PAS. Il disait « compteurs non atomiques »,
|
|
342
|
+
// ce qui décrit un comptage partagé plus faible. Or ici il n'y a PLUS de comptage partagé
|
|
343
|
+
// du tout : le `return true` ci-dessous laisse passer, et seul l'étage local subsiste.
|
|
344
|
+
// Dire « non atomique » laisse croire qu'un plafond d'instance tient encore, en moins
|
|
345
|
+
// précis. Trouvé par un audit externe le 11/09, avec la contradiction jumelle du contrat.
|
|
346
|
+
"compteur de débit PARTAGÉ INDISPONIBLE : appliquez supabase/migrations/0004-limites-atomiques.sql. "
|
|
347
|
+
+ "Sans elle il ne reste que le compteur LOCAL, par processus — une limite de 120/h en "
|
|
348
|
+
+ "autorise 120 PAR EXÉCUTION. Ce n'est pas un comptage partagé dégradé, c'est aucun. "
|
|
311
349
|
+ "(" + ((erreur && erreur.message) || erreur) + ")",
|
|
312
350
|
);
|
|
313
351
|
return true;
|
|
@@ -358,9 +396,9 @@ function createStandaloneContext(env = process.env) {
|
|
|
358
396
|
if (seg === "" || seg === "." || seg === ".." || !/^[A-Za-z0-9._-]+$/.test(seg)) return false;
|
|
359
397
|
}
|
|
360
398
|
try {
|
|
361
|
-
const r = await
|
|
399
|
+
const r = await fetchBorne(`${base}/storage/v1/object/${encodeURIComponent(bucket)}/${segs.map(encodeURIComponent).join("/")}`, {
|
|
362
400
|
method: "DELETE", headers: { apikey: cle, Authorization: `Bearer ${cle}` },
|
|
363
|
-
});
|
|
401
|
+
}, DELAI_STOCKAGE_MS);
|
|
364
402
|
return r.ok;
|
|
365
403
|
} catch { return false; }
|
|
366
404
|
},
|
|
@@ -382,11 +420,11 @@ function createStandaloneContext(env = process.env) {
|
|
|
382
420
|
const cle = String(env.SUPABASE_SERVICE_ROLE_KEY || "");
|
|
383
421
|
if (!base || !cle || !bucket || !chemin) return null;
|
|
384
422
|
try {
|
|
385
|
-
const r = await
|
|
423
|
+
const r = await fetchBorne(`${base}/storage/v1/object/upload/sign/${bucket}/${chemin}`, {
|
|
386
424
|
method: "POST",
|
|
387
425
|
headers: { apikey: cle, Authorization: `Bearer ${cle}`, "Content-Type": "application/json" },
|
|
388
426
|
body: "{}",
|
|
389
|
-
});
|
|
427
|
+
}, DELAI_STOCKAGE_MS);
|
|
390
428
|
if (!r.ok) return null;
|
|
391
429
|
const d = await r.json().catch(() => null);
|
|
392
430
|
const url = d && d.url;
|
|
@@ -474,9 +512,9 @@ function createStandaloneContext(env = process.env) {
|
|
|
474
512
|
}
|
|
475
513
|
if (!jeton || !url || !cle) return null;
|
|
476
514
|
try {
|
|
477
|
-
const r = await
|
|
515
|
+
const r = await fetchBorne(`${url}/auth/v1/user`, {
|
|
478
516
|
headers: { apikey: cle, Authorization: `Bearer ${jeton}` },
|
|
479
|
-
});
|
|
517
|
+
}, DELAI_AUTH_MS);
|
|
480
518
|
return r.ok ? await r.json() : null;
|
|
481
519
|
} catch { return null; }
|
|
482
520
|
},
|
package/docs/HOST-CONTRACT.md
CHANGED
|
@@ -441,10 +441,25 @@ Two deliberate exceptions, both written next to the code:
|
|
|
441
441
|
with a shared counter would make the guard pay the price we had just spared the thing it guards.
|
|
442
442
|
On that path the real protection is the cache, not the counter.
|
|
443
443
|
|
|
444
|
-
⚠️ **The
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
than
|
|
444
|
+
⚠️ **The paragraph that used to stand here said the shared count was not atomic. That has been
|
|
445
|
+
false since `0004`, and it contradicted the paragraph above it in this same document.** It was
|
|
446
|
+
written before the database function existed and outlived what it described. An external audit found
|
|
447
|
+
it on 2026-09-11; it is corrected rather than quietly deleted, because a host who read it may have
|
|
448
|
+
built a compensating control they do not need.
|
|
449
|
+
|
|
450
|
+
Here is the matrix, stated once, so nothing has to be inferred:
|
|
451
|
+
|
|
452
|
+
| Installed | What actually protects you |
|
|
453
|
+
|---|---|
|
|
454
|
+
| `0003` **and** `0004` | fast local refusal **+ shared atomic counter** — the limit means what it says for the instance |
|
|
455
|
+
| `0003` without `0004` | **local only.** The function is absent, PostgREST answers 404, and the shared stage lets through after warning by name |
|
|
456
|
+
| neither | **local only**, in memory, warned by name |
|
|
457
|
+
| keys prefixed `pread:` | local only, **deliberately** — see the exception above |
|
|
458
|
+
|
|
459
|
+
⚠️ **Read the second row carefully: without `0004` the shared stage does not count less well, it
|
|
460
|
+
does not count at all.** The degradation is to the local counter, not to a weaker shared one. The
|
|
461
|
+
warning the player emits used to say *"non-atomic rate counters"*, which named a mode that does not
|
|
462
|
+
exist; it now says the shared counter is unavailable.
|
|
448
463
|
|
|
449
464
|
## The three things a host implements
|
|
450
465
|
|
|
@@ -793,7 +808,10 @@ claimed the opposite. Two consequences you must act on:
|
|
|
793
808
|
- **`bot-tts` now requires a `sessionId`**, bound to the requested `slug`, and the text must match
|
|
794
809
|
something the assistant said in that session. The player reads `listMessages(sessionId)` and
|
|
795
810
|
treats a message as the assistant's when its `role` is `bot`, `assistant` or `ai`, taking the text
|
|
796
|
-
from `text` or `
|
|
811
|
+
from `text`, `content` or `body` — the same three fields, in the same order, as everywhere else in
|
|
812
|
+
this document. ⚠️ **This sentence named only the first two**, and an external audit found the
|
|
813
|
+
mismatch on 2026-09-11: a host whose messages carry `body` and nothing else would have read here
|
|
814
|
+
that its assistant never speaks, while the code reads it perfectly well. **Anything it cannot read counts as "not said"** — an unrecognised shape
|
|
797
815
|
yields an empty set and every request is refused. On the one route that spends money, *"I could
|
|
798
816
|
not verify"* must read as **no**, never as *go ahead*.
|
|
799
817
|
|
package/docs/RETENTION.md
CHANGED
|
@@ -131,8 +131,11 @@ it is the trace. The row records a fingerprint and a date and nothing else: writ
|
|
|
131
131
|
would recreate, inside the database, whatever personal data the bucket may already hold — and make
|
|
132
132
|
it queryable, which is strictly worse than not having it.
|
|
133
133
|
|
|
134
|
-
⚠️ **
|
|
135
|
-
|
|
134
|
+
⚠️ **This paragraph used to say a visitor chooses what goes in. That stopped being true** when
|
|
135
|
+
`bot-tts` began confronting the text with what the assistant actually said in that session — an
|
|
136
|
+
external audit found the stale claim on 2026-09-11. The caller **proposes** a text; only a text the
|
|
137
|
+
assistant already spoke is accepted. What still holds is the consequence: each *distinct accepted*
|
|
138
|
+
text leaves an MP3 and a JSON in a public bucket. The grouping and ceilings added in 0.1.140 bound the cost per
|
|
136
139
|
hour; only this window bounds the **duration**.
|
|
137
140
|
|
|
138
141
|
## Purging the reader IP and User-Agent (migrations 0026 and 0027)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "discovery-media-player",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.163",
|
|
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,27 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
// Copyright © 2026 3D Discovery
|
|
3
|
+
// UNE FEUILLE, ET C'EST TOUT CE QU'ELLE EST — ELLE N'IMPORTE RIEN, DONC ELLE NE PEUT FERMER AUCUN CYCLE.
|
|
4
|
+
//
|
|
5
|
+
// ⚠️ CE FICHIER NAÎT D'UN CYCLE MESURÉ, PAS D'UN GOÛT POUR LE RANGEMENT. Un audit externe a relevé
|
|
6
|
+
// le 11/09 le seul cycle du graphe serveur : `schema.js` ↔ `presentations.js`, par cinq `require()`
|
|
7
|
+
// DYNAMIQUES croisés — écrits dans le corps des fonctions précisément parce qu'un `require` en tête
|
|
8
|
+
// aurait rendu le cycle visible à l'initialisation. Un cycle qu'on contourne par le placement de
|
|
9
|
+
// l'import est un cycle qu'on a caché, pas retiré.
|
|
10
|
+
//
|
|
11
|
+
// ⚠️ ET LA MOITIÉ DU CYCLE ÉTAIT UN PUR DÉTOUR. `schema.js` allait chercher `signatureAbsente` DANS
|
|
12
|
+
// `presentations.js`, qui l'importe lui-même de `erreurs-base.js` : trois modules pour une fonction
|
|
13
|
+
// qui en habite un. Corrigé en important à la source. L'autre moitié est ce fichier : un seuil de
|
|
14
|
+
// domaine que deux modules lisent, et qui n'appartenait qu'à l'un d'eux par accident d'écriture.
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Au-delà de ce silence, une présentation est ABANDONNÉE — pas muette.
|
|
18
|
+
*
|
|
19
|
+
* ⚠️ UN SEUL NOMBRE, PARCE QUE DEUX AURAIENT DIVERGÉ. Le présentateur bat toutes les 30 s
|
|
20
|
+
* (`present-touch`). Ce seuil définit « vivante » pour le balayage des présentations orphelines ET
|
|
21
|
+
* pour le comptage des présentations actives de la carte d'identité. Les deux doivent répondre la
|
|
22
|
+
* même chose : une carte qui compte « actives » selon un autre seuil que celui qui les clôt
|
|
23
|
+
* afficherait des présentations que le balayage vient de retirer.
|
|
24
|
+
*/
|
|
25
|
+
const STALE_MS = 3 * 60 * 1000;
|
|
26
|
+
|
|
27
|
+
module.exports = { STALE_MS };
|
package/server/mesures.js
CHANGED
|
@@ -58,6 +58,16 @@ function creerHistogramme() {
|
|
|
58
58
|
},
|
|
59
59
|
compte: () => n,
|
|
60
60
|
max: () => Math.round(maxMs),
|
|
61
|
+
/**
|
|
62
|
+
* ⚠️ REMET À ZÉRO SANS CHANGER D'IDENTITÉ, ET C'EST LA RAISON DE CETTE MÉTHODE. `histoBase` est
|
|
63
|
+
* un `const` exporté par identité (`__histoBase`) : le réassigner périmerait l'export et les
|
|
64
|
+
* bancs mesureraient un objet que le module n'utilise plus. On vide l'état en place.
|
|
65
|
+
*/
|
|
66
|
+
reset() {
|
|
67
|
+
seaux.fill(0);
|
|
68
|
+
n = 0;
|
|
69
|
+
maxMs = 0;
|
|
70
|
+
},
|
|
61
71
|
};
|
|
62
72
|
}
|
|
63
73
|
|
|
@@ -218,8 +228,24 @@ function relever() {
|
|
|
218
228
|
}
|
|
219
229
|
|
|
220
230
|
/** Pour les bancs : repartir d'une instance vierge sans recharger le module. */
|
|
231
|
+
/**
|
|
232
|
+
* ⚠️ CETTE FONCTION PROMETTAIT PLUS QUE CE QU'ELLE FAISAIT, ET C'EST UN INSTRUMENT QUI MENTAIT.
|
|
233
|
+
*
|
|
234
|
+
* Elle annonçait « repartir d'une instance vierge sans recharger le module » et ne remettait à zéro
|
|
235
|
+
* que les histogrammes de routes et les compteurs de statut. `histoBase` et le retard de boucle
|
|
236
|
+
* survivaient. La télémétrie de production n'en souffrait pas — `vider()` n'y est jamais appelée —
|
|
237
|
+
* mais les BANCS D'ENDURANCE l'appellent entre l'échauffement et la mesure, puis entre scénarios.
|
|
238
|
+
* Ils pouvaient donc attribuer au scénario courant les appels base de l'échauffement et les
|
|
239
|
+
* ralentissements de boucle du scénario précédent.
|
|
240
|
+
*
|
|
241
|
+
* ⚠️ CE N'EST PAS UN DÉFAUT DE PRODUIT, C'EST PIRE POUR CE DÉPÔT : un instrument affaibli mesure
|
|
242
|
+
* moins bien le code qu'il surveille, et rien ne le dit. Trouvé par un audit externe le 11/09, qui
|
|
243
|
+
* l'a REPRODUIT plutôt que lu — `avant base n=1 boucle n=0` puis `apresVider base n=1 boucle n=2`.
|
|
244
|
+
*/
|
|
221
245
|
function vider() {
|
|
222
246
|
for (const nom of FAMILLES) histos.set(nom, creerHistogramme());
|
|
247
|
+
histoBase.reset();
|
|
248
|
+
boucle.reset();
|
|
223
249
|
// Écrits un par un, pour la même raison que les deux enveloppes de `observerBase` : une clé
|
|
224
250
|
// calculée sur un objet ordinaire est la forme que `proprieteEcrite.test.js` refuse.
|
|
225
251
|
statuts.ok = 0; statuts.refus4xx = 0; statuts.debit429 = 0; statuts.occupe503 = 0; statuts.erreur5xx = 0;
|
package/server/presentations.js
CHANGED
|
@@ -171,7 +171,10 @@ async function touchPresentation(slug, control) {
|
|
|
171
171
|
|
|
172
172
|
// Liste des présentations en cours (membre authentifié). Auto-purge : une présentation active dont le
|
|
173
173
|
// dernier heartbeat remonte à > STALE_MS (présentateur parti sans clôturer) est marquée inactive.
|
|
174
|
-
|
|
174
|
+
// ⚠️ LE SEUIL VIT DÉSORMAIS DANS UNE FEUILLE, et `schema.js` le lit de là plutôt que d'ici. Il
|
|
175
|
+
// était défini ici et emprunté par un `require()` dynamique croisé, ce qui fermait le seul cycle du
|
|
176
|
+
// graphe serveur. Le commentaire ci-dessus reste : il explique POURQUOI trois minutes.
|
|
177
|
+
const { STALE_MS } = require("./constantes-presentation.js");
|
|
175
178
|
|
|
176
179
|
/**
|
|
177
180
|
* ÉCRIRE SEULEMENT SI LA CONDITION TIENT ENCORE — au moment de l'écriture, pas au moment du contrôle.
|
package/server/retention.js
CHANGED
|
@@ -231,8 +231,11 @@ async function purgerMessagesPresentation(slug, opts, base, plafond) {
|
|
|
231
231
|
* `list` : il n'y avait littéralement rien à parcourir. `doc_tts_objects` (migration 0021) est la
|
|
232
232
|
* trace, et c'est elle qui rend cette purge possible.
|
|
233
233
|
*
|
|
234
|
-
* ⚠️
|
|
235
|
-
*
|
|
234
|
+
* ⚠️ CE COMMENTAIRE DISAIT « C'EST UN VISITEUR QUI DÉCIDE DE CE QUI Y ENTRE ». CE N'EST PLUS VRAI
|
|
235
|
+
* depuis que `bot-tts` confronte le texte à ce que l'assistant a réellement dit dans cette session :
|
|
236
|
+
* l'appelant PROPOSE, il ne choisit pas. Trouvé par un audit externe le 11/09, en même temps que la
|
|
237
|
+
* phrase jumelle de `docs/RETENTION.md`. Ce qui reste vrai est la conséquence : chaque texte DISTINCT
|
|
238
|
+
* ACCEPTÉ laisse un MP3 et un JSON dans un bucket PUBLIC. Les plafonds de la 0.1.140
|
|
236
239
|
* bornent le coût par heure ; seule cette fenêtre borne la DURÉE.
|
|
237
240
|
*/
|
|
238
241
|
async function purgerCacheDeVoix(opts, borneDate) {
|
package/server/schema.js
CHANGED
|
@@ -471,7 +471,10 @@ async function ajouterMigrationsDePresence(etat) {
|
|
|
471
471
|
p_max_gap_ms: 0, p_anon_cap: 0, p_has_token: null, p_only_if_unclaimed: true,
|
|
472
472
|
};
|
|
473
473
|
const estSignatureAbsente = (erreur) => {
|
|
474
|
-
|
|
474
|
+
// ⚠️ À LA SOURCE, PAS PAR `presentations.js`. Ce détour fermait le seul cycle du graphe serveur
|
|
475
|
+
// (audit externe du 11/09) alors que `presentations.js` importe lui-même cette fonction de
|
|
476
|
+
// `erreurs-base.js` : trois modules pour une fonction qui en habite un.
|
|
477
|
+
try { return require("./erreurs-base.js").signatureAbsente(erreur); } catch { return false; }
|
|
475
478
|
};
|
|
476
479
|
|
|
477
480
|
etat.fusionBaseCouvre = PORTEE_FUSION;
|
|
@@ -612,7 +615,7 @@ async function ajouterPresence(etat) {
|
|
|
612
615
|
// même que celui du balayage des présentations orphelines — plutôt que d'inventer un second
|
|
613
616
|
// nombre qui divergerait. Le présentateur bat toutes les 30 s (`present-touch`) : une présentation
|
|
614
617
|
// sans battement depuis trois minutes est abandonnée, pas silencieuse. (Relevé du second hôte.)
|
|
615
|
-
const vivantDepuis = new Date(Date.now() - require("./
|
|
618
|
+
const vivantDepuis = new Date(Date.now() - require("./constantes-presentation.js").STALE_MS).toISOString();
|
|
616
619
|
const actives = await PLAYER.db.request(`doc_presentations?active=eq.true&last_seen=gt.${encodeURIComponent(vivantDepuis)}&select=slug${bornee}`);
|
|
617
620
|
const nActives = Array.isArray(actives) ? actives.length : 0;
|
|
618
621
|
// ⚠️ `couvre` VOYAGE AVEC LES NOMBRES, ET C'EST LE POINT. Le commentaire ci-dessus protège celui
|