discovery-media-player 0.1.149 → 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 +34 -1
- package/package.json +1 -1
- package/server/retention.js +71 -6
package/docs/HOST-CONTRACT.md
CHANGED
|
@@ -501,12 +501,45 @@ was something to look at, *0 of 0* means the table is empty or out of reach and
|
|
|
501
501
|
nothing. It is `null` on the same terms as the counts.
|
|
502
502
|
|
|
503
503
|
The counts are **bounded** at `borne` rows and read one small column. ⚠️ **`tronque` says whether
|
|
504
|
-
|
|
504
|
+
anything was cut off**: when it is `true`, every number in the block is a *lower bound*, not a
|
|
505
505
|
count. Without it a saturated `5000` would be indistinguishable from an exact five thousand — a
|
|
506
506
|
wrong number that reads as right, which is worse than an absent one, because an absence makes you
|
|
507
507
|
look and a number makes you conclude. `vide` stays correct either way: saturation can only make it
|
|
508
508
|
`false`, never wrongly `true`.
|
|
509
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
|
+
|
|
510
543
|
They run only under `&schema=1`, the mode where you have asked for the database.
|
|
511
544
|
|
|
512
545
|
⚠️ **The purge attestation is a commitment, not a convenience.** Every column this player empties
|
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,15 @@ 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 —
|
|
413
|
+
* illisible sans élargir le contrat d'hôte — que des hôtes tiers implémentent eux-mêmes.
|
|
414
414
|
* D'où un comptage BORNÉ : au plus `BORNE_RESTE` identifiants, une seule petite colonne.
|
|
415
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
|
+
*
|
|
416
422
|
* ⚠️ ET LA SATURATION SE DIT, ELLE NE SE DEVINE PAS — deux hôtes ont trouvé ce défaut dans la
|
|
417
423
|
* première version, le même jour, indépendamment. Elle demandait `limit=BORNE` et publiait
|
|
418
424
|
* `lignes.length` : sur une base portant cinq mille adresses, elle rendait `1000`, que rien ne
|
|
@@ -424,6 +430,14 @@ function tick() {
|
|
|
424
430
|
* reste, sans coûter une ligne de plus. `n` reste plafonné à la borne, et `tronque` dit qu'il faut
|
|
425
431
|
* le lire « au moins ».
|
|
426
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.
|
|
440
|
+
*
|
|
427
441
|
* ⚠️ ET LE COÛT EST INVERSE DE L'INTUITION, donc il est dit plutôt que caché : quand il reste
|
|
428
442
|
* beaucoup de lignes, la base s'arrête à la borne et c'est rapide ; quand il n'en reste AUCUNE,
|
|
429
443
|
* elle parcourt la table pour ne rien trouver. Le cas cher est le cas terminal — celui où ce
|
|
@@ -466,12 +480,63 @@ const COLONNE_ABSENTE = "42703";
|
|
|
466
480
|
/** `{ n, tronque }` — `n` nul veut dire indéterminé, jamais zéro. */
|
|
467
481
|
const compte = (n, tronque) => ({ n, tronque });
|
|
468
482
|
|
|
469
|
-
|
|
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) {
|
|
470
527
|
try {
|
|
471
528
|
// ⚠️ BORNE + 1 : la ligne excédentaire ne sert qu'à PROUVER qu'il en reste. On ne la publie pas.
|
|
472
|
-
|
|
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.
|
|
531
|
+
const lignes = await PLAYER.db.request(
|
|
532
|
+
`${chemin}&order=${cle}.asc&limit=${BORNE_RESTE + 1}`, { timeoutMs: 8000 });
|
|
473
533
|
if (!Array.isArray(lignes)) return compte(null, false);
|
|
474
|
-
|
|
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]));
|
|
475
540
|
} catch (e) {
|
|
476
541
|
if (e && e.details && e.details.code === COLONNE_ABSENTE) return compte(0, false);
|
|
477
542
|
return compte(null, false); // indéterminé — surtout pas zéro
|
|
@@ -479,7 +544,7 @@ async function compterBorne(chemin) {
|
|
|
479
544
|
}
|
|
480
545
|
|
|
481
546
|
const compterReste = (table, cle, colonne) =>
|
|
482
|
-
compterBorne(`${table}?select=${cle}&${colonne}=not.is.null
|
|
547
|
+
compterBorne(`${table}?select=${cle}&${colonne}=not.is.null`, cle);
|
|
483
548
|
|
|
484
549
|
/**
|
|
485
550
|
* ⚠️ ET LE COMPTEUR PORTE CE QU'IL A REGARDÉ — un hôte nous l'a demandé, et il avait raison.
|
|
@@ -497,7 +562,7 @@ const compterReste = (table, cle, colonne) =>
|
|
|
497
562
|
* borne dès les premières lignes. Une par TABLE, pas une par sonde — deux des trois colonnes vivent
|
|
498
563
|
* dans la même.
|
|
499
564
|
*/
|
|
500
|
-
const compterLignes = (table, cle) => compterBorne(`${table}?select=${cle}
|
|
565
|
+
const compterLignes = (table, cle) => compterBorne(`${table}?select=${cle}`, cle);
|
|
501
566
|
|
|
502
567
|
async function resteDeLaPurge() {
|
|
503
568
|
const [comptes, totaux] = await Promise.all([
|