discovery-media-player 0.1.148 → 0.1.150
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/HOST-CONTRACT.md +66 -4
- package/docs/RETENTION.md +35 -11
- package/package.json +1 -1
- package/server/retention.js +140 -15
package/docs/HOST-CONTRACT.md
CHANGED
|
@@ -486,7 +486,8 @@ above: an unheard-of action answered *no* narrows this view rather than breaking
|
|
|
486
486
|
`retentionSweep` says the instance *can* purge; it says nothing about what has piled up. The card
|
|
487
487
|
gains a `purge` block counting the rows that still carry a reader IP or a raw User-Agent:
|
|
488
488
|
|
|
489
|
-
"purge": { "borne":
|
|
489
|
+
"purge": { "borne": 5000, "tronque": false, "lignes": { "sessions": 1908, "vues": 3200 },
|
|
490
|
+
"sessionsIp": 0, "sessionsUa": 0, "vuesUa": 0, "vide": true }
|
|
490
491
|
|
|
491
492
|
`vide` is the reading that matters: `true` means nothing of that legacy is left **on this
|
|
492
493
|
instance's live rows** — the condition under which those columns can eventually be dropped —
|
|
@@ -494,9 +495,70 @@ instance's live rows** — the condition under which those columns can eventuall
|
|
|
494
495
|
`null` for the same reason: a failed probe must never read as a zero, because zero is the answer
|
|
495
496
|
that authorises a deletion.
|
|
496
497
|
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
the
|
|
498
|
+
`lignes` is what the counter **looked at**, per table. A bare `0` cannot tell "purged" from "never
|
|
499
|
+
written" from "the probe is aimed wrong"; the denominator separates them — *0 of 1908* means there
|
|
500
|
+
was something to look at, *0 of 0* means the table is empty or out of reach and the zero proves
|
|
501
|
+
nothing. It is `null` on the same terms as the counts.
|
|
502
|
+
|
|
503
|
+
The counts are **bounded** at `borne` rows and read one small column. ⚠️ **`tronque` says whether
|
|
504
|
+
anything was cut off**: when it is `true`, every number in the block is a *lower bound*, not a
|
|
505
|
+
count. Without it a saturated `5000` would be indistinguishable from an exact five thousand — a
|
|
506
|
+
wrong number that reads as right, which is worse than an absent one, because an absence makes you
|
|
507
|
+
look and a number makes you conclude. `vide` stays correct either way: saturation can only make it
|
|
508
|
+
`false`, never wrongly `true`.
|
|
509
|
+
|
|
510
|
+
⚠️ **And `tronque` does not assume our bound is the only ceiling** — it did, for one release, and a
|
|
511
|
+
host measured what that cost. PostgREST has a ceiling of its own, `db-max-rows`, set to **1000** by
|
|
512
|
+
default on Supabase: the server returns 1000 rows however many you ask for. Comparing the received
|
|
513
|
+
length against `borne` then compares against the wrong number, and a table of 1651 rows was
|
|
514
|
+
published as `1000` **with `tronque: false`** — asserting an exactness it did not have.
|
|
515
|
+
|
|
516
|
+
So the question asked is not *did I hit my bound* but **is there anything after what I received**:
|
|
517
|
+
one row is requested past the last one received, by keyset cursor (`col=gt.<last>`, never by
|
|
518
|
+
offset — a cursor is stable under concurrent writes, and it is this repository's pagination rule). A row returned proves more remain; none proves the lot was
|
|
519
|
+
the whole — whichever ceiling produced it, without having to know it. **What this does not cover,
|
|
520
|
+
stated rather than glossed:** a server ceiling of *zero* stays indistinguishable from an empty table
|
|
521
|
+
by the response body alone. Reading the count from `Content-Range` under `Prefer: count=exact` has
|
|
522
|
+
no ceiling to guess and transports nothing; it is strictly better, and it needs the `db` capability
|
|
523
|
+
to expose response headers, which today it does not.
|
|
524
|
+
|
|
525
|
+
⚠️ **And the same ceiling applies to every read you make through your own client, not just to
|
|
526
|
+
ours.** `limit=20000` does not return twenty thousand rows: PostgREST caps the response at
|
|
527
|
+
`db-max-rows` — **1000** on a default Supabase project — and says so nowhere in the body. A read
|
|
528
|
+
that asks for more than that ceiling is not a large read, it is a **false belief**, and it stays
|
|
529
|
+
invisible while your tables are small. So the question is worth asking of your own code as well as
|
|
530
|
+
of ours: *does my client paginate, or do I believe that `limit=20000` returns 20 000 rows?*
|
|
531
|
+
|
|
532
|
+
One host asked it of itself the day it found this in our counter, and the answer was not
|
|
533
|
+
hypothetical: a statistics read ordered `created_at.asc` with no `limit` was seeing the **1000
|
|
534
|
+
oldest** rows of 6424, so a "last opened" date read months stale for a link opened the day before,
|
|
535
|
+
and every breakdown described the beginning of the history. They also count **32** reads asking for
|
|
536
|
+
more than the ceiling — all latent on their volumes today, all live on an older installation.
|
|
537
|
+
|
|
538
|
+
⚠️ **The sort direction decides how bad it gets.** A read that saturates while ordered `desc` loses
|
|
539
|
+
the oldest rows; ordered `asc` it loses the newest — that is, the ones anyone is looking at. Same
|
|
540
|
+
ceiling, same silence, opposite severity. Counting is indifferent to it, but anything that reads
|
|
541
|
+
*content* under a ceiling should prefer `desc`.
|
|
542
|
+
|
|
543
|
+
They run only under `&schema=1`, the mode where you have asked for the database.
|
|
544
|
+
|
|
545
|
+
⚠️ **The purge attestation is a commitment, not a convenience.** Every column this player empties
|
|
546
|
+
carries a `comment on column` whose text **begins with the exact marker**:
|
|
547
|
+
|
|
548
|
+
VIDE ET PLUS JAMAIS ECRITE depuis la <migration number>.
|
|
549
|
+
|
|
550
|
+
Read it through `col_description()`. It is what *proves* a purge was applied — a count of zero does
|
|
551
|
+
not, since it cannot tell "purged" from "never written". **We commit to two things**: to post it on
|
|
552
|
+
every column a future migration empties, and not to reword that prefix. It is deliberately plain
|
|
553
|
+
ASCII, without accent or apostrophe, so it survives encodings and needs no escaping.
|
|
554
|
+
|
|
555
|
+
This used to be a convenience, designed for a person proving a purge. A host told us its inventory
|
|
556
|
+
now reads it **mechanically**, crossing it with the residual counts to raise an alarm when values
|
|
557
|
+
reappear beside an attestation. That is the moment an artefact becomes an interface — and the reason
|
|
558
|
+
to commit is the failure mode: if we quietly stopped posting it, that alarm would go **silent
|
|
559
|
+
without saying so**, a failure caused here and invisible there. A guard in this repository refuses
|
|
560
|
+
any migration that empties a column without the marker, so undoing the commitment turns something
|
|
561
|
+
red rather than turning something quiet.
|
|
500
562
|
|
|
501
563
|
⚠️ **Why this exists at all:** our tables live in *your* database, and your audit enumerates *your*
|
|
502
564
|
tables — a dependency's schema occupies a zone nobody's inventory visits. Two integrating hosts
|
package/docs/RETENTION.md
CHANGED
|
@@ -242,23 +242,47 @@ is a number.
|
|
|
242
242
|
**1. The rows.** Reading logs are deleted **13 months** after `at` / `last_at` by default. A host
|
|
243
243
|
changes that through `config.retention` — whole months in `[1, 120]`.
|
|
244
244
|
|
|
245
|
-
⚠️ **But the automatic sweep is strictly opt-in
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
245
|
+
⚠️ **But the automatic sweep is strictly opt-in**, and there are **three** states, not two — an
|
|
246
|
+
integrating host measured the one we had left out:
|
|
247
|
+
|
|
248
|
+
| state | how to tell | what you may claim |
|
|
249
|
+
|---|---|---|
|
|
250
|
+
| **off** | `retentionSweep` false, and no `retention.run` in your logs | nothing has ever been deleted; the window is a policy you have not applied |
|
|
251
|
+
| **armed, never exercised** | armed, but no row has yet reached the window — check the age of your oldest row against it | nothing has ever been deleted **either**, and not for want of configuration |
|
|
252
|
+
| **armed, and has deleted** | armed, and a run reported non-zero counts | the window is an *event*, and only here |
|
|
253
|
+
|
|
254
|
+
⚠️ **The middle state is the misleading one**, because it has every appearance of the good one: armed,
|
|
255
|
+
correct, and indistinguishable in its effects from being off — no deletion, no log, no evidence it
|
|
256
|
+
works. A host reported exactly this: sweep armed, oldest row 63 days old, **zero rows past 13 months
|
|
257
|
+
out of 1908**. Its first real execution will be roughly **eighteen months after it was armed**, on
|
|
258
|
+
data nobody will have looked at, never having run in anger. Treat it as what it is — a guard that has
|
|
259
|
+
never been exercised, with a deadline — and exercise it deliberately before then, on a copy or with
|
|
260
|
+
`retention.run` and a short window, rather than discovering its behaviour the day it matters.
|
|
261
|
+
|
|
262
|
+
Anyone attesting a retention period should establish which of the **three** is true of the
|
|
263
|
+
installation in front of them, rather than quoting the default.
|
|
250
264
|
|
|
251
265
|
**2. The values inside surviving rows.** Erased by 0026 and 0027 as soon as they are applied, and
|
|
252
266
|
physically gone from the table once routine autovacuum has passed — no operator action, typically
|
|
253
267
|
minutes to hours on an active table. This part does not wait for the 13 months.
|
|
254
268
|
|
|
255
269
|
**3. Backups, write-ahead logs, exports and migration dumps.** **Outside this player's reach, and we
|
|
256
|
-
neither set nor observe them.** They follow the hosting platform's own settings —
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
270
|
+
neither set nor observe them.** They follow the hosting platform's own settings — typically a
|
|
271
|
+
point-in-time-recovery window plus a snapshot schedule, each with its own retention. Ask the platform
|
|
272
|
+
for **the PITR window and the age of the oldest retained snapshot**; until they have rolled past the
|
|
273
|
+
day 0026/0027 were applied, earlier copies still hold the erased values.
|
|
274
|
+
|
|
275
|
+
⚠️ **Neither number is exposed by any API, and that matters more than it looks.** Two integrating
|
|
276
|
+
hosts checked independently, on two different toolsets: the provider's API and its MCP tools return
|
|
277
|
+
region, status and engine version — nothing about backups. **A human has to read them from the
|
|
278
|
+
dashboard.** This is written here because the instruction above is *executable in appearance*: an
|
|
279
|
+
agent following it will look for a tool, find none, and then either stop — or, the real risk, report
|
|
280
|
+
the purge complete having skipped the one step it could not measure. If you cannot produce these two
|
|
281
|
+
numbers, say so; do not round the sentence.
|
|
282
|
+
|
|
283
|
+
⚠️ **And it is not always "the later of two".** An option that is not subscribed retains nothing, so
|
|
284
|
+
it defers nothing. A host with no PITR and eight daily snapshots has **one** deadline, not two: the
|
|
285
|
+
age of its oldest snapshot. Take the later of the deadlines that *exist*.
|
|
262
286
|
|
|
263
287
|
## Limits stated rather than left unsaid
|
|
264
288
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "discovery-media-player",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.150",
|
|
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
|
@@ -410,9 +410,33 @@ function tick() {
|
|
|
410
410
|
*
|
|
411
411
|
* ⚠️ ON COMPTE DES LIGNES, PAS UN `count=exact`. La capacité `db` de l'hôte rend le corps de la
|
|
412
412
|
* réponse, pas ses en-têtes : le compte de PostgREST voyage dans `Content-Range`, donc il serait
|
|
413
|
-
* illisible sans élargir le contrat d'hôte —
|
|
414
|
-
* D'où un comptage BORNÉ : au plus `BORNE_RESTE` identifiants, une seule petite colonne.
|
|
415
|
-
*
|
|
413
|
+
* illisible sans élargir le contrat d'hôte — que des hôtes tiers implémentent eux-mêmes.
|
|
414
|
+
* D'où un comptage BORNÉ : au plus `BORNE_RESTE` identifiants, une seule petite colonne.
|
|
415
|
+
*
|
|
416
|
+
* ⚠️ CE CHOIX A UN COÛT, ET IL EST NOMMÉ ICI PLUTÔT QUE SUBI : lire des LIGNES, c'est dépendre des
|
|
417
|
+
* plafonds de qui les rend, et un hôte a mesuré que ce plafond peut être SOUS notre borne. Le
|
|
418
|
+
* compte d'en-tête n'a pas de plafond à deviner et ne transporte rien ; il est strictement
|
|
419
|
+
* supérieur, et le seul obstacle est le contrat. Tant que le contrat ne le rend pas, `resteApres`
|
|
420
|
+
* rattrape la seule chose qui rendait le nombre MENSONGER — l'affirmation d'exactitude.
|
|
421
|
+
*
|
|
422
|
+
* ⚠️ ET LA SATURATION SE DIT, ELLE NE SE DEVINE PAS — deux hôtes ont trouvé ce défaut dans la
|
|
423
|
+
* première version, le même jour, indépendamment. Elle demandait `limit=BORNE` et publiait
|
|
424
|
+
* `lignes.length` : sur une base portant cinq mille adresses, elle rendait `1000`, que rien ne
|
|
425
|
+
* distinguait d'un compte exact de mille. Un nombre faux qui se lit comme juste — pire qu'un
|
|
426
|
+
* nombre absent, parce que l'absence fait chercher et que le nombre fait conclure.
|
|
427
|
+
*
|
|
428
|
+
* Le remède vivait à trois cents lignes d'ici : `purgerRetention` rend `tronque` depuis toujours,
|
|
429
|
+
* pour exactement cette raison. On demande donc `BORNE + 1` : en recevoir autant prouve qu'il en
|
|
430
|
+
* reste, sans coûter une ligne de plus. `n` reste plafonné à la borne, et `tronque` dit qu'il faut
|
|
431
|
+
* le lire « au moins ».
|
|
432
|
+
*
|
|
433
|
+
* ⚠️ ET CE CORRECTIF ÉTAIT LUI-MÊME FAUX, D'UN CRAN PLUS LOIN — trouvé par un hôte réel QUATRE
|
|
434
|
+
* HEURES après sa publication. Il comparait le nombre de lignes reçues à NOTRE borne, donc il
|
|
435
|
+
* supposait que le seul plafond fût le nôtre. PostgREST en a un autre, `db-max-rows`, réglé à 1000
|
|
436
|
+
* par défaut chez Supabase : le serveur tronque EN AMONT, et la comparaison porte alors sur le
|
|
437
|
+
* mauvais nombre. Une table de 1651 lignes se lisait `1000` avec `tronque: false` — pire que la
|
|
438
|
+
* version d'avant, qui ne prétendait rien là où celle-ci AFFIRMAIT l'exactitude. `resteApres`
|
|
439
|
+
* ci-dessous pose désormais la seule question dont la réponse ne dépend d'aucun plafond.
|
|
416
440
|
*
|
|
417
441
|
* ⚠️ ET LE COÛT EST INVERSE DE L'INTUITION, donc il est dit plutôt que caché : quand il reste
|
|
418
442
|
* beaucoup de lignes, la base s'arrête à la borne et c'est rapide ; quand il n'en reste AUCUNE,
|
|
@@ -423,7 +447,13 @@ function tick() {
|
|
|
423
447
|
* ⚠️ UN ÉCHEC REND `null`, JAMAIS ZÉRO. Zéro est la réponse qui autorise à supprimer une colonne :
|
|
424
448
|
* la fabriquer à partir d'une sonde en panne serait le pire mensonge que cette carte puisse faire.
|
|
425
449
|
*/
|
|
426
|
-
|
|
450
|
+
// ⚠️ CINQ MILLE, ET LE NOMBRE VIENT D'UNE MESURE. Il valait mille, et le banc écrit avec les
|
|
451
|
+
// volumes RÉELS d'un hôte l'a fait rougir : sa table de vues en portait 1651. La borne saturait
|
|
452
|
+
// donc dès le premier jour chez lui, et un compteur qui plafonne sous les volumes qu'il est censé
|
|
453
|
+
// décrire ne décrit rien. Cinq mille couvre les deux hôtes connus avec de la marge, reste une
|
|
454
|
+
// seule petite colonne à transférer, et `tronque` dit le reste. La borne est un plafond de COÛT,
|
|
455
|
+
// pas une opinion sur ce qu'un hôte peut avoir.
|
|
456
|
+
const BORNE_RESTE = 5000;
|
|
427
457
|
|
|
428
458
|
const SONDES_RESTE = [
|
|
429
459
|
["sessionsIp", "commercial_doc_sessions", "session_id", "ip"],
|
|
@@ -431,6 +461,10 @@ const SONDES_RESTE = [
|
|
|
431
461
|
["vuesUa", "commercial_doc_views", "id", "ua"],
|
|
432
462
|
];
|
|
433
463
|
|
|
464
|
+
/** Les tables regardées, pour le dénominateur — une par table, pas une par sonde. */
|
|
465
|
+
const TABLES_RESTE = [["sessions", "commercial_doc_sessions", "session_id"],
|
|
466
|
+
["vues", "commercial_doc_views", "id"]];
|
|
467
|
+
|
|
434
468
|
/**
|
|
435
469
|
* ⚠️ ET LA COLONNE DISPARUE EST UN ÉTAT CONNU, PAS UNE PANNE. Le jour où un exploitant supprime ces
|
|
436
470
|
* colonnes — le geste que ce compteur sert à autoriser — la requête échoue avec le
|
|
@@ -443,27 +477,118 @@ const SONDES_RESTE = [
|
|
|
443
477
|
*/
|
|
444
478
|
const COLONNE_ABSENTE = "42703";
|
|
445
479
|
|
|
446
|
-
|
|
480
|
+
/** `{ n, tronque }` — `n` nul veut dire indéterminé, jamais zéro. */
|
|
481
|
+
const compte = (n, tronque) => ({ n, tronque });
|
|
482
|
+
|
|
483
|
+
/**
|
|
484
|
+
* ⚠️ « MOINS QUE DEMANDÉ » NE PROUVE PAS LA FIN — ET C'EST UN HÔTE RÉEL QUI L'A MONTRÉ.
|
|
485
|
+
*
|
|
486
|
+
* La version précédente comparait le nombre de lignes reçues à NOTRE borne, et concluait « pas
|
|
487
|
+
* tronqué » dès qu'il était plus petit. Elle supposait que le seul plafond fût le nôtre. PostgREST
|
|
488
|
+
* en a un autre, `db-max-rows`, que Supabase règle à 1000 : le serveur rend 1000 lignes quoi qu'on
|
|
489
|
+
* demande. Sur une table de 1651 lignes, la carte a donc publié `1000` AVEC `tronque: false` —
|
|
490
|
+
* c'est-à-dire le défaut qu'on venait de corriger, déplacé d'un cran et AGGRAVÉ : la version d'avant
|
|
491
|
+
* ne prétendait rien, celle-là AFFIRMAIT que le nombre était exact.
|
|
492
|
+
*
|
|
493
|
+
* Le contrôle honnête ne porte donc pas sur une borne connue, mais sur la seule question dont la
|
|
494
|
+
* réponse ne dépend d'aucun plafond : « y a-t-il quelque chose APRÈS ce que j'ai reçu ? » On la
|
|
495
|
+
* pose en demandant UNE ligne au-delà de la dernière reçue. Une ligne rendue prouve qu'il en
|
|
496
|
+
* reste ; aucune prouve que le lot reçu était le tout — quel que soit le plafond qui l'a produit,
|
|
497
|
+
* et sans avoir à le connaître.
|
|
498
|
+
*
|
|
499
|
+
* ⚠️ PAR CURSEUR KEYSET (`cle=gt.<dernier>`), PAS PAR `offset` — et cette phrase est déjà écrite
|
|
500
|
+
* trois cent quatre-vingts lignes plus haut, au-dessus de `purgerParLots`, où elle dit la même
|
|
501
|
+
* chose depuis toujours : la garde de portabilité de la forge interdit `offset=`, et un curseur
|
|
502
|
+
* est de toute façon stable sous écriture concurrente. Première rédaction de cette sonde : par
|
|
503
|
+
* `offset`. La forge l'a refusée. C'est la SECONDE fois dans ce fichier qu'un remède déjà présent
|
|
504
|
+
* n'a pas été vu — après le drapeau `tronque` de `purgerParLots`. Un fichier dont on vient
|
|
505
|
+
* d'écrire la partie difficile se relit mal, et c'est un fait à traiter, pas une excuse.
|
|
506
|
+
*
|
|
507
|
+
* ⚠️ ET CE QU'ELLE NE COUVRE PAS EST DIT, PARCE QU'UNE GARDE MUETTE VAUT MOINS QUE PAS DE GARDE :
|
|
508
|
+
* un plafond serveur à ZÉRO reste indiscernable d'une table vide par le corps seul — les deux
|
|
509
|
+
* requêtes rendent zéro ligne. C'est la limite de la lecture par lignes, et la raison pour laquelle
|
|
510
|
+
* le compte d'en-tête (`Content-Range` sous `Prefer: count=exact`) lui est strictement supérieur :
|
|
511
|
+
* il ne dépend d'aucun plafond. Il demanderait d'élargir la capacité `db` du contrat d'hôte, qui ne
|
|
512
|
+
* rend aujourd'hui que le corps analysé.
|
|
513
|
+
*/
|
|
514
|
+
async function resteApres(chemin, cle, dernier) {
|
|
515
|
+
// Sans curseur lisible, la fin ne se prouve pas : « au moins » est le seul côté sûr.
|
|
516
|
+
if (dernier == null) return true;
|
|
517
|
+
try {
|
|
518
|
+
const suite = await PLAYER.db.request(
|
|
519
|
+
`${chemin}&${cle}=gt.${enc(String(dernier))}&order=${cle}.asc&limit=1`, { timeoutMs: 8000 });
|
|
520
|
+
// Pas de réponse analysable ⇒ on ne sait pas ⇒ « au moins ». Se tromper vers le minorant ne
|
|
521
|
+
// fait que sous-estimer ; se tromper vers l'exactitude fait conclure.
|
|
522
|
+
return !Array.isArray(suite) || suite.length > 0;
|
|
523
|
+
} catch { return true; }
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
async function compterBorne(chemin, cle) {
|
|
447
527
|
try {
|
|
528
|
+
// ⚠️ BORNE + 1 : la ligne excédentaire ne sert qu'à PROUVER qu'il en reste. On ne la publie pas.
|
|
529
|
+
// ⚠️ ET L'ORDRE N'EST PAS DÉCORATIF : sans lui, « la dernière ligne reçue » ne désigne aucune
|
|
530
|
+
// frontière, et le curseur de la sonde ne voudrait rien dire.
|
|
448
531
|
const lignes = await PLAYER.db.request(
|
|
449
|
-
`${
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
532
|
+
`${chemin}&order=${cle}.asc&limit=${BORNE_RESTE + 1}`, { timeoutMs: 8000 });
|
|
533
|
+
if (!Array.isArray(lignes)) return compte(null, false);
|
|
534
|
+
// Notre propre borne atteinte : la preuve est dans la ligne excédentaire, rien à demander.
|
|
535
|
+
if (lignes.length > BORNE_RESTE) return compte(BORNE_RESTE, true);
|
|
536
|
+
// Zéro ligne : la sonde au-delà rendrait zéro elle aussi et n'apprendrait rien — y compris sous
|
|
537
|
+
// un plafond à zéro, que ni l'une ni l'autre ne distingue d'une table vide.
|
|
538
|
+
if (!lignes.length) return compte(0, false);
|
|
539
|
+
return compte(lignes.length, await resteApres(chemin, cle, lignes[lignes.length - 1][cle]));
|
|
453
540
|
} catch (e) {
|
|
454
|
-
if (e && e.details && e.details.code === COLONNE_ABSENTE) return 0;
|
|
455
|
-
return null; // indéterminé — surtout pas zéro
|
|
541
|
+
if (e && e.details && e.details.code === COLONNE_ABSENTE) return compte(0, false);
|
|
542
|
+
return compte(null, false); // indéterminé — surtout pas zéro
|
|
456
543
|
}
|
|
457
544
|
}
|
|
458
545
|
|
|
546
|
+
const compterReste = (table, cle, colonne) =>
|
|
547
|
+
compterBorne(`${table}?select=${cle}&${colonne}=not.is.null`, cle);
|
|
548
|
+
|
|
549
|
+
/**
|
|
550
|
+
* ⚠️ ET LE COMPTEUR PORTE CE QU'IL A REGARDÉ — un hôte nous l'a demandé, et il avait raison.
|
|
551
|
+
*
|
|
552
|
+
* `sessionsIp: 0` ne distingue pas trois choses : « purgé », « jamais écrit », et « la sonde vise à
|
|
553
|
+
* côté ». Les deux premières se valent pour qui veut supprimer une colonne ; la troisième est un
|
|
554
|
+
* mensonge. Le dénominateur les sépare : « 0 sur 1908 lignes examinées » dit qu'il y avait quelque
|
|
555
|
+
* chose à regarder, « 0 sur 0 » dit que la table est vide ou hors d'atteinte et que le zéro ne
|
|
556
|
+
* prouve rien.
|
|
557
|
+
*
|
|
558
|
+
* C'est notre propre règle anti-vacuité — un plancher compte la FORME RECONNUE, pas les choses
|
|
559
|
+
* comptées — appliquée partout dans `tools/` et absente d'ici jusqu'à ce qu'un lecteur la réclame.
|
|
560
|
+
*
|
|
561
|
+
* ⚠️ ET IL NE COÛTE PRESQUE RIEN, à l'inverse du compte filtré : sans filtre, la base s'arrête à la
|
|
562
|
+
* borne dès les premières lignes. Une par TABLE, pas une par sonde — deux des trois colonnes vivent
|
|
563
|
+
* dans la même.
|
|
564
|
+
*/
|
|
565
|
+
const compterLignes = (table, cle) => compterBorne(`${table}?select=${cle}`, cle);
|
|
566
|
+
|
|
459
567
|
async function resteDeLaPurge() {
|
|
460
|
-
const comptes = await Promise.all(
|
|
461
|
-
|
|
462
|
-
|
|
568
|
+
const [comptes, totaux] = await Promise.all([
|
|
569
|
+
Promise.all(SONDES_RESTE.map(([, t, c, col]) => compterReste(t, c, col))),
|
|
570
|
+
Promise.all(TABLES_RESTE.map(([, t, c]) => compterLignes(t, c))),
|
|
571
|
+
]);
|
|
572
|
+
// ⚠️ ACCUMULATEURS NUS, comme celui de `fenetresValidees` plus haut et pour la même raison : la
|
|
573
|
+
// garde de forme reconnaît `Object.create(null)`, et une écriture indexée par autre chose qu'un
|
|
574
|
+
// littéral n'a alors aucun prototype à polluer. Les clés viennent ici de constantes du fichier,
|
|
575
|
+
// mais un objet nu ne coûte rien et la propriété se lit sans avoir à remonter leur provenance.
|
|
576
|
+
const parTable = Object.create(null);
|
|
577
|
+
TABLES_RESTE.forEach(([nom], i) => { parTable[nom] = totaux[i].n; });
|
|
578
|
+
const out = Object.create(null);
|
|
579
|
+
out.borne = BORNE_RESTE;
|
|
580
|
+
// ⚠️ UN SEUL DRAPEAU POUR TOUT LE BLOC, parce qu'il ne sert qu'à une chose : dire au lecteur que
|
|
581
|
+
// les nombres qu'il voit sont des minorants. Un drapeau par compte suggérerait qu'on peut faire
|
|
582
|
+
// confiance aux autres, alors que la borne est commune et que la question ne l'est pas.
|
|
583
|
+
out.tronque = [...comptes, ...totaux].some((c) => c.tronque);
|
|
584
|
+
out.lignes = parTable;
|
|
585
|
+
SONDES_RESTE.forEach(([nom], i) => { out[nom] = comptes[i].n; });
|
|
463
586
|
// ⚠️ TROIS ÉTATS, PAS DEUX. `true` : plus rien, le retrait des colonnes est permis ICI. `false` :
|
|
464
587
|
// il reste des lignes. `null` : au moins une sonde n'a pas répondu — on ne sait pas, et « on ne
|
|
465
588
|
// sait pas » ne doit jamais se lire comme « c'est bon ».
|
|
466
|
-
|
|
589
|
+
// ⚠️ `vide` RESTE JUSTE MÊME SATURÉ, et c'est ce qui compte : c'est le champ qui autorise le
|
|
590
|
+
// retrait d'une colonne, et la saturation ne peut le rendre que FAUX — jamais vrai à tort.
|
|
591
|
+
out.vide = comptes.some((c) => c.n === null) ? null : comptes.every((c) => c.n === 0);
|
|
467
592
|
return out;
|
|
468
593
|
}
|
|
469
594
|
|