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.
@@ -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
- that bound was reached**: when it is `true`, every number in the block is a *lower bound*, not a
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.149",
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",
@@ -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 — ce qu'un compteur de diagnostic ne justifie pas.
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
- async function compterBorne(chemin) {
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
- const lignes = await PLAYER.db.request(`${chemin}&limit=${BORNE_RESTE + 1}`, { timeoutMs: 8000 });
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
- return compte(Math.min(lignes.length, BORNE_RESTE), lignes.length > BORNE_RESTE);
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([