discovery-media-player 0.1.149 → 0.1.151
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 +46 -3
- package/docs/HOST-CONTRACT.md +68 -1
- package/package.json +2 -2
- package/server/mesures.js +6 -0
- package/server/retention.js +125 -6
package/context/standalone.js
CHANGED
|
@@ -45,7 +45,13 @@ function creerDb(env) {
|
|
|
45
45
|
const url = sansBarreFinale(env.SUPABASE_URL);
|
|
46
46
|
const cle = String(env.SUPABASE_SERVICE_ROLE_KEY || "");
|
|
47
47
|
|
|
48
|
-
|
|
48
|
+
// ⚠️ UN SEUL ENDROIT QUI APPELLE ET QUI REJETTE. `count` a besoin d'un EN-TÊTE de la réponse,
|
|
49
|
+
// pas de son corps ; le tenter avec son propre `fetch` aurait recopié la construction des
|
|
50
|
+
// en-têtes, l'abandon, et surtout la forme de l'erreur (`statusCode`/`details`) dont six sites
|
|
51
|
+
// appelants dépendent. Une seconde orthographe de « appeler PostgREST et rejeter correctement »
|
|
52
|
+
// est exactement la recopie que ce dépôt a déjà payée trois fois. `request` et `count` se
|
|
53
|
+
// partagent donc l'appel ; ils ne se partagent que ce qu'ils lisent de la réponse.
|
|
54
|
+
async function appel(chemin, options = {}) {
|
|
49
55
|
if (!url || !cle) {
|
|
50
56
|
// Message explicite plutôt que `undefined` plus loin : sans base, ce sont les liens tracés
|
|
51
57
|
// et les présentations qui sont indisponibles — pas l'affichage d'un document.
|
|
@@ -92,10 +98,47 @@ function creerDb(env) {
|
|
|
92
98
|
try { erreur.details = JSON.parse(detail); } catch { /* corps non JSON : le message suffit */ }
|
|
93
99
|
throw erreur;
|
|
94
100
|
}
|
|
101
|
+
return r;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async function request(chemin, options = {}) {
|
|
105
|
+
const r = await appel(chemin, options);
|
|
95
106
|
const texte = await r.text();
|
|
96
107
|
return texte ? JSON.parse(texte) : null;
|
|
97
108
|
}
|
|
98
109
|
|
|
110
|
+
/**
|
|
111
|
+
* ⚠️ LE COMPTE EXACT, ET C'EST UNE QUESTION — PAS UN MÉCANISME. Le contrat demande « combien de
|
|
112
|
+
* lignes ce chemin sélectionne-t-il ? » ; il ne demande pas de lire un en-tête. Un hôte sur une
|
|
113
|
+
* autre base répond par un `count(*)`, celui-ci par PostgREST. Nommer le mécanisme dans le
|
|
114
|
+
* contrat l'aurait rendu PostgREST-seulement, ce que la règle de portabilité refuse.
|
|
115
|
+
*
|
|
116
|
+
* ⚠️ POURQUOI IL EXISTE : le comptage par LIGNES dépend des plafonds de qui les rend. PostgREST a
|
|
117
|
+
* `db-max-rows`, réglé à 1000 par défaut chez Supabase, et un hôte a mesuré une table de 1651
|
|
118
|
+
* lignes rendue « 1000 ». Le compte d'en-tête, lui, n'a AUCUN plafond à deviner et ne transporte
|
|
119
|
+
* rien. Mesuré chez un hôte le 02/09 : `Prefer: count=exact` + `Range: 0-0` rend bien le compte
|
|
120
|
+
* exact ; l'autre voie envisagée, `?select=count()`, est morte — `db-aggregates-enabled` vaut
|
|
121
|
+
* `false` par défaut, vérifié sur deux projets distincts.
|
|
122
|
+
*
|
|
123
|
+
* ⚠️ ET UN GET PLUTÔT QU'UN HEAD, DÉLIBÉRÉMENT. Un HEAD ne rend aucun corps, donc aucune erreur
|
|
124
|
+
* ANALYSÉE : l'appelant ne pourrait plus distinguer « colonne supprimée » (42703, un état connu
|
|
125
|
+
* qui vaut zéro) d'une panne. `Range: 0-0` ne coûte qu'une ligne et garde l'erreur lisible.
|
|
126
|
+
*
|
|
127
|
+
* ⚠️ RENDRE `null` PLUTÔT QUE ZÉRO QUAND LE COMPTE MANQUE. PostgREST écrit `…/*` quand il ne
|
|
128
|
+
* compte pas. Zéro est la réponse qui autorise à supprimer une colonne : la fabriquer depuis une
|
|
129
|
+
* réponse qui ne compte pas serait le pire mensonge que cette capacité puisse faire.
|
|
130
|
+
*/
|
|
131
|
+
async function count(chemin, options = {}) {
|
|
132
|
+
const r = await appel(chemin, {
|
|
133
|
+
...options,
|
|
134
|
+
method: "GET",
|
|
135
|
+
headers: { ...(options.headers || {}), Prefer: "count=exact", Range: "0-0" },
|
|
136
|
+
});
|
|
137
|
+
// `Content-Range: 0-0/1651` — le total suit la barre. `…/*` veut dire « je n'ai pas compté ».
|
|
138
|
+
const trouve = /\/(\d+)\s*$/.exec(String(r.headers.get("content-range") || ""));
|
|
139
|
+
return trouve ? Number(trouve[1]) : null;
|
|
140
|
+
}
|
|
141
|
+
|
|
99
142
|
/** Lecture paginée complète : un document très partagé dépasse la pagination par défaut. */
|
|
100
143
|
async function selectAll(chemin, taille = 1000) {
|
|
101
144
|
const tout = [];
|
|
@@ -107,7 +150,7 @@ function creerDb(env) {
|
|
|
107
150
|
}
|
|
108
151
|
}
|
|
109
152
|
|
|
110
|
-
return { request, selectAll, configuree: !!(url && cle) };
|
|
153
|
+
return { request, selectAll, count, configuree: !!(url && cle) };
|
|
111
154
|
}
|
|
112
155
|
|
|
113
156
|
/**
|
|
@@ -356,7 +399,7 @@ function createStandaloneContext(env = process.env) {
|
|
|
356
399
|
},
|
|
357
400
|
},
|
|
358
401
|
|
|
359
|
-
db: { request: db.request, selectAll: db.selectAll },
|
|
402
|
+
db: { request: db.request, selectAll: db.selectAll, count: db.count },
|
|
360
403
|
|
|
361
404
|
// Sans expéditeur configuré, le re-partage et le code du mur d'accès sont indisponibles — et
|
|
362
405
|
// le disent. Ils ne prétendent pas avoir envoyé.
|
package/docs/HOST-CONTRACT.md
CHANGED
|
@@ -501,12 +501,79 @@ 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
|
+
⚠️ **`db.count(path)` is the seam that closes this, and it is optional.** ⚠️ **The standalone
|
|
526
|
+
context shipped in this package already implements it** — if you build your context from
|
|
527
|
+
`discovery-media-player/context/standalone`, you get it on your next upgrade and there is nothing
|
|
528
|
+
to decide or write. This section is for a host that implements the `db` capability itself. A host
|
|
529
|
+
asked which of the two it was, and the answer was missing from this page: *"the two look alike in
|
|
530
|
+
your code and not at all alike at your hosts."* If your `db` capability
|
|
531
|
+
exposes it, the player asks it first and publishes an **exact** count — no bound, no `tronque`, and
|
|
532
|
+
no rows transported at all. If it is absent, everything above still applies unchanged: the bounded
|
|
533
|
+
read with its cursor probe. **That fallback is the whole design.** Third-party hosts implement this
|
|
534
|
+
capability themselves, and requiring a new method would break every one of them; the only kind of
|
|
535
|
+
contract addition this repository allows is the kind whose absence is the previous behaviour.
|
|
536
|
+
|
|
537
|
+
db.count(path) → number | null
|
|
538
|
+
|
|
539
|
+
**It asks the question, not the mechanism.** *How many rows does this path select?* — not *read this
|
|
540
|
+
header*. A PostgREST host answers with `Prefer: count=exact`; a host on another database answers
|
|
541
|
+
with a `count(*)`. Naming the header in the contract would have made it PostgREST-only, which the
|
|
542
|
+
portability rule refuses.
|
|
543
|
+
|
|
544
|
+
⚠️ **Answer `null` when you cannot say — never `0`.** Zero is the answer that authorises dropping a
|
|
545
|
+
column. Anything that is not a non-negative integer (a string, a float, `undefined`, `NaN`) is read
|
|
546
|
+
as "no answer" and the player falls back rather than believing it.
|
|
547
|
+
|
|
548
|
+
⚠️ **The two alternatives were measured at a host, not assumed here** — recorded so nobody proposes
|
|
549
|
+
them again in six months believing they were never tried. **`?select=count()` is dead**:
|
|
550
|
+
`db-aggregates-enabled` is `false` by default, verified on two distinct Supabase projects, and the
|
|
551
|
+
measurement is solid for a reason worth stating — the `PGRST123` error arrives *before* the
|
|
552
|
+
permission check, where the same table queried without an aggregate answers `42501 permission
|
|
553
|
+
denied`. The answer therefore depends on neither grants nor any `revoke`: it is a property of the
|
|
554
|
+
**configuration**, not of authorization. That was the route we would have preferred, since it bound
|
|
555
|
+
no one to a contract. **`Prefer: count=exact` with `Range: 0-0` works**: the exact count travels in
|
|
556
|
+
the header and the body carries nothing. It is the only one of the two that exists, and its only
|
|
557
|
+
obstacle is this contract.
|
|
558
|
+
|
|
559
|
+
⚠️ **And the same ceiling applies to every read you make through your own client, not just to
|
|
560
|
+
ours.** `limit=20000` does not return twenty thousand rows: PostgREST caps the response at
|
|
561
|
+
`db-max-rows` — **1000** on a default Supabase project — and says so nowhere in the body. A read
|
|
562
|
+
that asks for more than that ceiling is not a large read, it is a **false belief**, and it stays
|
|
563
|
+
invisible while your tables are small. So the question is worth asking of your own code as well as
|
|
564
|
+
of ours: *does my client paginate, or do I believe that `limit=20000` returns 20 000 rows?*
|
|
565
|
+
|
|
566
|
+
One host asked it of itself the day it found this in our counter, and the answer was not
|
|
567
|
+
hypothetical: a statistics read ordered `created_at.asc` with no `limit` was seeing the **1000
|
|
568
|
+
oldest** rows of 6424, so a "last opened" date read months stale for a link opened the day before,
|
|
569
|
+
and every breakdown described the beginning of the history. They also count **32** reads asking for
|
|
570
|
+
more than the ceiling — all latent on their volumes today, all live on an older installation.
|
|
571
|
+
|
|
572
|
+
⚠️ **The sort direction decides how bad it gets.** A read that saturates while ordered `desc` loses
|
|
573
|
+
the oldest rows; ordered `asc` it loses the newest — that is, the ones anyone is looking at. Same
|
|
574
|
+
ceiling, same silence, opposite severity. Counting is indifferent to it, but anything that reads
|
|
575
|
+
*content* under a ceiling should prefer `desc`.
|
|
576
|
+
|
|
510
577
|
They run only under `&schema=1`, the mode where you have asked for the database.
|
|
511
578
|
|
|
512
579
|
⚠️ **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.151",
|
|
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",
|
|
@@ -67,7 +67,7 @@
|
|
|
67
67
|
"build": "node -e \"require('fs').existsSync('build/bundle.mjs')||(console.error('Ce script ne vit pas dans le paquet publié - le champ scripts annonce plus que le tarball ne contient. Clonez le depot puis npm ci : https://github.com/Juli1artha/discovery-media-player'),process.exit(1))\" && node build/bundle.mjs && tsc -p tsconfig.build.json && node -e \"import('./build/bundle.mjs').then(m=>m.marquerDistEsm())\"",
|
|
68
68
|
"test": "node -e \"require('fs').existsSync('server/__tests__')||(console.error('Ce banc ne vit pas dans le paquet publié - le champ scripts annonce plus que le tarball ne contient. Clonez le depot puis npm ci : https://github.com/Juli1artha/discovery-media-player'),process.exit(1))\" && vitest run",
|
|
69
69
|
"test:watch": "vitest",
|
|
70
|
-
"lint": "node -e \"require('fs').existsSync('eslint.config.mjs')||(console.error('Ce script ne vit pas dans le paquet publié - le champ scripts annonce plus que le tarball ne contient. Clonez le depot puis npm ci : https://github.com/Juli1artha/discovery-media-player'),process.exit(1))\" && eslint bin context server src build tools charge --max-warnings 0",
|
|
70
|
+
"lint": "node -e \"require('fs').existsSync('eslint.config.mjs')||(console.error('Ce script ne vit pas dans le paquet publié - le champ scripts annonce plus que le tarball ne contient. Clonez le depot puis npm ci : https://github.com/Juli1artha/discovery-media-player'),process.exit(1))\" && eslint bin context server src build tools charge base --max-warnings 0",
|
|
71
71
|
"lint:fix": "eslint bin context server src build tools charge --fix",
|
|
72
72
|
"typecheck": "node -e \"require('fs').existsSync('tsconfig.json')||(console.error('Ce script ne vit pas dans le paquet publié - le champ scripts annonce plus que le tarball ne contient. Clonez le depot puis npm ci : https://github.com/Juli1artha/discovery-media-player'),process.exit(1))\" && tsc --noEmit",
|
|
73
73
|
"prepublishOnly": "npm run build && npm test",
|
package/server/mesures.js
CHANGED
|
@@ -130,6 +130,12 @@ function observerBase(db) {
|
|
|
130
130
|
vu.__mesuree = true;
|
|
131
131
|
vu.request = mesurer("request");
|
|
132
132
|
if (typeof db.selectAll === "function") vu.selectAll = mesurer("selectAll");
|
|
133
|
+
// ⚠️ CHAQUE MÉTHODE AJOUTÉE À LA CAPACITÉ DOIT ÊTRE AJOUTÉE ICI, et l'héritage rend cet oubli
|
|
134
|
+
// SILENCIEUX : `Object.create` laisse passer une méthode nouvelle, vivante et non mesurée — donc
|
|
135
|
+
// le paragraphe ci-dessus, qui promet de couvrir « y compris ce que personne n'a encore écrit »,
|
|
136
|
+
// deviendrait faux sans que rien ne rougisse. `count` est optionnelle chez l'hôte ; quand elle
|
|
137
|
+
// existe, elle interroge la base et son temps compte comme le reste.
|
|
138
|
+
if (typeof db.count === "function") vu.count = mesurer("count");
|
|
133
139
|
return vu;
|
|
134
140
|
}
|
|
135
141
|
|
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,117 @@ 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
|
+
* ⚠️ LES DEUX AUTRES VOIES ONT ÉTÉ MESURÉES CHEZ UN HÔTE, PAS SUPPOSÉES ICI. On les note pour que
|
|
515
|
+
* personne ne les repropose dans six mois en croyant qu'elles n'ont jamais été essayées :
|
|
516
|
+
*
|
|
517
|
+
* `?select=count()` — MORT. `db-aggregates-enabled` vaut `false` par défaut, vérifié sur DEUX
|
|
518
|
+
* projets Supabase distincts. Et la mesure est solide pour une raison qui vaut d'être dite :
|
|
519
|
+
* l'erreur `PGRST123` arrive AVANT le contrôle de droits — la même table, interrogée sans
|
|
520
|
+
* agrégat, rend `42501 permission denied`. La réponse ne dépend donc ni des droits ni d'un
|
|
521
|
+
* `revoke` : c'est une propriété de la CONFIGURATION, pas de l'autorisation. C'était la voie
|
|
522
|
+
* qu'on aurait préférée, puisqu'elle n'engageait aucun contrat.
|
|
523
|
+
*
|
|
524
|
+
* `Prefer: count=exact` + `Range: 0-0` — MARCHE. Le compte exact voyage dans l'en-tête, le corps
|
|
525
|
+
* ne transporte rien. C'est donc la SEULE des deux qui existe, et son seul obstacle est le
|
|
526
|
+
* contrat d'hôte.
|
|
527
|
+
*/
|
|
528
|
+
async function resteApres(chemin, cle, dernier) {
|
|
529
|
+
// Sans curseur lisible, la fin ne se prouve pas : « au moins » est le seul côté sûr.
|
|
530
|
+
if (dernier == null) return true;
|
|
531
|
+
try {
|
|
532
|
+
const suite = await PLAYER.db.request(
|
|
533
|
+
`${chemin}&${cle}=gt.${enc(String(dernier))}&order=${cle}.asc&limit=1`, { timeoutMs: 8000 });
|
|
534
|
+
// Pas de réponse analysable ⇒ on ne sait pas ⇒ « au moins ». Se tromper vers le minorant ne
|
|
535
|
+
// fait que sous-estimer ; se tromper vers l'exactitude fait conclure.
|
|
536
|
+
return !Array.isArray(suite) || suite.length > 0;
|
|
537
|
+
} catch { return true; }
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
/**
|
|
541
|
+
* ⚠️ LA VOIE EXACTE, QUAND L'HÔTE LA FOURNIT — ET LE CONTRAT DEMANDE LA QUESTION, PAS LE MÉCANISME.
|
|
542
|
+
* `db.count(chemin)` rend « combien de lignes ce chemin sélectionne-t-il ». Un hôte PostgREST y
|
|
543
|
+
* répond par `Prefer: count=exact` ; un hôte sur une autre base par un `count(*)`. Nommer l'en-tête
|
|
544
|
+
* dans le contrat l'aurait rendu PostgREST-seulement, ce que la règle de portabilité refuse.
|
|
545
|
+
*
|
|
546
|
+
* ⚠️ ELLE EST OPTIONNELLE, ET SON ABSENCE N'EST PAS UNE PANNE. Des hôtes tiers implémentent la
|
|
547
|
+
* capacité `db` eux-mêmes ; exiger une méthode nouvelle les casserait tous. Absente, on retombe sur
|
|
548
|
+
* le comptage borné ci-dessous, qui reste juste — seulement moins précis. C'est la seule forme
|
|
549
|
+
* d'ajout au contrat que ce dépôt s'autorise : celle dont le repli est le comportement d'avant.
|
|
550
|
+
*
|
|
551
|
+
* ⚠️ ET TOUT CE QUI N'EST PAS UN ENTIER POSITIF RETOMBE, plutôt que d'être cru. Un hôte qui rend
|
|
552
|
+
* `undefined`, une chaîne, ou un négatif n'a pas répondu à la question — le lire comme un compte
|
|
553
|
+
* fabriquerait le chiffre que ce fichier existe pour ne pas fabriquer.
|
|
554
|
+
*/
|
|
555
|
+
async function compteExact(chemin) {
|
|
556
|
+
// ⚠️ SORTIE ANTICIPÉE, PAS GARDE — ET LA DISTINCTION EST MESURÉE. Le `catch` ci-dessous suffirait
|
|
557
|
+
// à la correction : appeler une méthode absente lève, on retombe, le résultat est le même. Muté
|
|
558
|
+
// en `if (!PLAYER.db)`, AUCUN banc ne rougit — c'est dit ici plutôt que laissé croire à une
|
|
559
|
+
// protection. Ce que cette ligne achète est un COÛT : sans elle, tout hôte qui n'implémente pas
|
|
560
|
+
// `count` construirait cinq exceptions à chaque lecture de carte, pour rien.
|
|
561
|
+
if (!PLAYER.db || typeof PLAYER.db.count !== "function") return null;
|
|
562
|
+
try {
|
|
563
|
+
const n = await PLAYER.db.count(chemin);
|
|
564
|
+
return Number.isInteger(n) && n >= 0 ? n : null;
|
|
565
|
+
} catch {
|
|
566
|
+
// ⚠️ ON NE RECOPIE PAS ICI LA RÈGLE DE LA COLONNE ABSENTE. Une première rédaction traitait le
|
|
567
|
+
// `42703` sur cette voie aussi, pour rendre zéro « comme l'autre ». Muté, ce branchement n'a
|
|
568
|
+
// fait rougir aucun banc — et pour une raison de fond, pas par manque de cas : une colonne
|
|
569
|
+
// supprimée fait échouer LES DEUX voies de la même façon, donc le repli rend déjà ce zéro. Le
|
|
570
|
+
// branchement n'ajoutait rien d'observable et créait un SECOND endroit où tenir la même règle.
|
|
571
|
+
return null; // on ne sait pas ⇒ on essaie l'autre voie, qui elle sait lire le 42703
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
async function compterBorne(chemin, cle) {
|
|
576
|
+
// ⚠️ UN COMPTE EXACT N'EST NI BORNÉ NI TRONQUÉ, quelle que soit sa taille : `borne` décrit la
|
|
577
|
+
// méthode par lignes, pas celle-ci. `tronque: false` garde donc le sens qu'il a partout —
|
|
578
|
+
// « lisez ce nombre comme exact » — au lieu d'en prendre un second selon la voie employée.
|
|
579
|
+
const exact = await compteExact(chemin);
|
|
580
|
+
if (exact !== null) return compte(exact, false);
|
|
470
581
|
try {
|
|
471
582
|
// ⚠️ BORNE + 1 : la ligne excédentaire ne sert qu'à PROUVER qu'il en reste. On ne la publie pas.
|
|
472
|
-
|
|
583
|
+
// ⚠️ ET L'ORDRE N'EST PAS DÉCORATIF : sans lui, « la dernière ligne reçue » ne désigne aucune
|
|
584
|
+
// frontière, et le curseur de la sonde ne voudrait rien dire.
|
|
585
|
+
const lignes = await PLAYER.db.request(
|
|
586
|
+
`${chemin}&order=${cle}.asc&limit=${BORNE_RESTE + 1}`, { timeoutMs: 8000 });
|
|
473
587
|
if (!Array.isArray(lignes)) return compte(null, false);
|
|
474
|
-
|
|
588
|
+
// Notre propre borne atteinte : la preuve est dans la ligne excédentaire, rien à demander.
|
|
589
|
+
if (lignes.length > BORNE_RESTE) return compte(BORNE_RESTE, true);
|
|
590
|
+
// Zéro ligne : la sonde au-delà rendrait zéro elle aussi et n'apprendrait rien — y compris sous
|
|
591
|
+
// un plafond à zéro, que ni l'une ni l'autre ne distingue d'une table vide.
|
|
592
|
+
if (!lignes.length) return compte(0, false);
|
|
593
|
+
return compte(lignes.length, await resteApres(chemin, cle, lignes[lignes.length - 1][cle]));
|
|
475
594
|
} catch (e) {
|
|
476
595
|
if (e && e.details && e.details.code === COLONNE_ABSENTE) return compte(0, false);
|
|
477
596
|
return compte(null, false); // indéterminé — surtout pas zéro
|
|
@@ -479,7 +598,7 @@ async function compterBorne(chemin) {
|
|
|
479
598
|
}
|
|
480
599
|
|
|
481
600
|
const compterReste = (table, cle, colonne) =>
|
|
482
|
-
compterBorne(`${table}?select=${cle}&${colonne}=not.is.null
|
|
601
|
+
compterBorne(`${table}?select=${cle}&${colonne}=not.is.null`, cle);
|
|
483
602
|
|
|
484
603
|
/**
|
|
485
604
|
* ⚠️ ET LE COMPTEUR PORTE CE QU'IL A REGARDÉ — un hôte nous l'a demandé, et il avait raison.
|
|
@@ -497,7 +616,7 @@ const compterReste = (table, cle, colonne) =>
|
|
|
497
616
|
* borne dès les premières lignes. Une par TABLE, pas une par sonde — deux des trois colonnes vivent
|
|
498
617
|
* dans la même.
|
|
499
618
|
*/
|
|
500
|
-
const compterLignes = (table, cle) => compterBorne(`${table}?select=${cle}
|
|
619
|
+
const compterLignes = (table, cle) => compterBorne(`${table}?select=${cle}`, cle);
|
|
501
620
|
|
|
502
621
|
async function resteDeLaPurge() {
|
|
503
622
|
const [comptes, totaux] = await Promise.all([
|