discovery-media-player 0.1.150 → 0.1.152
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 +53 -1
- package/package.json +2 -2
- package/server/mesures.js +6 -0
- package/server/retention.js +88 -8
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
|
@@ -487,7 +487,7 @@ above: an unheard-of action answered *no* narrows this view rather than breaking
|
|
|
487
487
|
gains a `purge` block counting the rows that still carry a reader IP or a raw User-Agent:
|
|
488
488
|
|
|
489
489
|
"purge": { "borne": 5000, "tronque": false, "lignes": { "sessions": 1908, "vues": 3200 },
|
|
490
|
-
"sessionsIp": 0, "sessionsUa": 0, "vuesUa": 0, "vide": true }
|
|
490
|
+
"sessionsIp": 0, "sessionsUa": 0, "vuesUa": 0, "vide": true, "voie": "bornee" }
|
|
491
491
|
|
|
492
492
|
`vide` is the reading that matters: `true` means nothing of that legacy is left **on this
|
|
493
493
|
instance's live rows** — the condition under which those columns can eventually be dropped —
|
|
@@ -522,6 +522,58 @@ by the response body alone. Reading the count from `Content-Range` under `Prefer
|
|
|
522
522
|
no ceiling to guess and transports nothing; it is strictly better, and it needs the `db` capability
|
|
523
523
|
to expose response headers, which today it does not.
|
|
524
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
|
+
⚠️ **And `voie` tells you which route produced the numbers, because the numbers cannot.** `"exact"`,
|
|
549
|
+
`"bornee"`, or `"mixte"` when both served the same read — which happens for real: a host mid-purge
|
|
550
|
+
whose column is already dropped makes `count` throw on the filtered paths and answer on the same
|
|
551
|
+
table's totals.
|
|
552
|
+
|
|
553
|
+
This field exists because two hosts, independently and on the same day, found they could not verify
|
|
554
|
+
their own seam. An exact count and an untruncated bounded count render an **identical** block —
|
|
555
|
+
same numbers, same `tronque`, same `vide`. So a `db.count` that returns a string, or a float, falls
|
|
556
|
+
back **silently** and the card looks exactly like a working one: you believe your seam is wired
|
|
557
|
+
when it is not. One host caught it only by luck of volume — their table held 1655 rows, and the
|
|
558
|
+
bounded route is structurally incapable of exceeding the 1000-row ceiling, so the number itself
|
|
559
|
+
happened to be proof. Under a thousand rows there is no such luck, and the other host, whose
|
|
560
|
+
volumes are small, could not tell at all.
|
|
561
|
+
|
|
562
|
+
**Check `voie`, not a value.** A control that distinguishes two *mechanisms* must rest on something
|
|
563
|
+
one can do and the other cannot; a value both could return proves nothing, and a value written down
|
|
564
|
+
from yesterday's measurement rots without anyone touching anything.
|
|
565
|
+
|
|
566
|
+
⚠️ **The two alternatives were measured at a host, not assumed here** — recorded so nobody proposes
|
|
567
|
+
them again in six months believing they were never tried. **`?select=count()` is dead**:
|
|
568
|
+
`db-aggregates-enabled` is `false` by default, verified on two distinct Supabase projects, and the
|
|
569
|
+
measurement is solid for a reason worth stating — the `PGRST123` error arrives *before* the
|
|
570
|
+
permission check, where the same table queried without an aggregate answers `42501 permission
|
|
571
|
+
denied`. The answer therefore depends on neither grants nor any `revoke`: it is a property of the
|
|
572
|
+
**configuration**, not of authorization. That was the route we would have preferred, since it bound
|
|
573
|
+
no one to a contract. **`Prefer: count=exact` with `Range: 0-0` works**: the exact count travels in
|
|
574
|
+
the header and the body carries nothing. It is the only one of the two that exists, and its only
|
|
575
|
+
obstacle is this contract.
|
|
576
|
+
|
|
525
577
|
⚠️ **And the same ceiling applies to every read you make through your own client, not just to
|
|
526
578
|
ours.** `limit=20000` does not return twenty thousand rows: PostgREST caps the response at
|
|
527
579
|
`db-max-rows` — **1000** on a default Supabase project — and says so nowhere in the body. A read
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "discovery-media-player",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.152",
|
|
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
|
@@ -477,8 +477,21 @@ const TABLES_RESTE = [["sessions", "commercial_doc_sessions", "session_id"],
|
|
|
477
477
|
*/
|
|
478
478
|
const COLONNE_ABSENTE = "42703";
|
|
479
479
|
|
|
480
|
-
/**
|
|
481
|
-
|
|
480
|
+
/**
|
|
481
|
+
* `{ n, tronque, voie }` — `n` nul veut dire indéterminé, jamais zéro.
|
|
482
|
+
*
|
|
483
|
+
* ⚠️ ET `voie` NOMME LE MÉCANISME QUI A PRODUIT LE NOMBRE, parce que le nombre seul ne le dit pas.
|
|
484
|
+
* Un compte exact et un compte borné NON tronqué rendent le même JSON : deux hôtes l'ont relevé le
|
|
485
|
+
* même jour, l'un en constatant qu'il ne pouvait pas vérifier sa propre couture, l'autre en
|
|
486
|
+
* écrivant un contrôle qui n'a marché que par chance de volume — sa table dépassait mille, donc la
|
|
487
|
+
* voie par lignes était structurellement incapable de rendre son chiffre. Sous mille, personne ne
|
|
488
|
+
* peut trancher, et un `db.count` qui rend une chaîne retombe SILENCIEUSEMENT sur la voie bornée :
|
|
489
|
+
* l'hôte croit sa couture branchée alors qu'elle ne sert pas.
|
|
490
|
+
*/
|
|
491
|
+
const compte = (n, tronque, voie) => ({ n, tronque, voie });
|
|
492
|
+
|
|
493
|
+
const VOIE_EXACTE = "exact";
|
|
494
|
+
const VOIE_BORNEE = "bornee";
|
|
482
495
|
|
|
483
496
|
/**
|
|
484
497
|
* ⚠️ « MOINS QUE DEMANDÉ » NE PROUVE PAS LA FIN — ET C'EST UN HÔTE RÉEL QUI L'A MONTRÉ.
|
|
@@ -510,6 +523,20 @@ const compte = (n, tronque) => ({ n, tronque });
|
|
|
510
523
|
* le compte d'en-tête (`Content-Range` sous `Prefer: count=exact`) lui est strictement supérieur :
|
|
511
524
|
* il ne dépend d'aucun plafond. Il demanderait d'élargir la capacité `db` du contrat d'hôte, qui ne
|
|
512
525
|
* rend aujourd'hui que le corps analysé.
|
|
526
|
+
*
|
|
527
|
+
* ⚠️ LES DEUX AUTRES VOIES ONT ÉTÉ MESURÉES CHEZ UN HÔTE, PAS SUPPOSÉES ICI. On les note pour que
|
|
528
|
+
* personne ne les repropose dans six mois en croyant qu'elles n'ont jamais été essayées :
|
|
529
|
+
*
|
|
530
|
+
* `?select=count()` — MORT. `db-aggregates-enabled` vaut `false` par défaut, vérifié sur DEUX
|
|
531
|
+
* projets Supabase distincts. Et la mesure est solide pour une raison qui vaut d'être dite :
|
|
532
|
+
* l'erreur `PGRST123` arrive AVANT le contrôle de droits — la même table, interrogée sans
|
|
533
|
+
* agrégat, rend `42501 permission denied`. La réponse ne dépend donc ni des droits ni d'un
|
|
534
|
+
* `revoke` : c'est une propriété de la CONFIGURATION, pas de l'autorisation. C'était la voie
|
|
535
|
+
* qu'on aurait préférée, puisqu'elle n'engageait aucun contrat.
|
|
536
|
+
*
|
|
537
|
+
* `Prefer: count=exact` + `Range: 0-0` — MARCHE. Le compte exact voyage dans l'en-tête, le corps
|
|
538
|
+
* ne transporte rien. C'est donc la SEULE des deux qui existe, et son seul obstacle est le
|
|
539
|
+
* contrat d'hôte.
|
|
513
540
|
*/
|
|
514
541
|
async function resteApres(chemin, cle, dernier) {
|
|
515
542
|
// Sans curseur lisible, la fin ne se prouve pas : « au moins » est le seul côté sûr.
|
|
@@ -523,23 +550,64 @@ async function resteApres(chemin, cle, dernier) {
|
|
|
523
550
|
} catch { return true; }
|
|
524
551
|
}
|
|
525
552
|
|
|
553
|
+
/**
|
|
554
|
+
* ⚠️ LA VOIE EXACTE, QUAND L'HÔTE LA FOURNIT — ET LE CONTRAT DEMANDE LA QUESTION, PAS LE MÉCANISME.
|
|
555
|
+
* `db.count(chemin)` rend « combien de lignes ce chemin sélectionne-t-il ». Un hôte PostgREST y
|
|
556
|
+
* répond par `Prefer: count=exact` ; un hôte sur une autre base par un `count(*)`. Nommer l'en-tête
|
|
557
|
+
* dans le contrat l'aurait rendu PostgREST-seulement, ce que la règle de portabilité refuse.
|
|
558
|
+
*
|
|
559
|
+
* ⚠️ ELLE EST OPTIONNELLE, ET SON ABSENCE N'EST PAS UNE PANNE. Des hôtes tiers implémentent la
|
|
560
|
+
* capacité `db` eux-mêmes ; exiger une méthode nouvelle les casserait tous. Absente, on retombe sur
|
|
561
|
+
* le comptage borné ci-dessous, qui reste juste — seulement moins précis. C'est la seule forme
|
|
562
|
+
* d'ajout au contrat que ce dépôt s'autorise : celle dont le repli est le comportement d'avant.
|
|
563
|
+
*
|
|
564
|
+
* ⚠️ ET TOUT CE QUI N'EST PAS UN ENTIER POSITIF RETOMBE, plutôt que d'être cru. Un hôte qui rend
|
|
565
|
+
* `undefined`, une chaîne, ou un négatif n'a pas répondu à la question — le lire comme un compte
|
|
566
|
+
* fabriquerait le chiffre que ce fichier existe pour ne pas fabriquer.
|
|
567
|
+
*/
|
|
568
|
+
async function compteExact(chemin) {
|
|
569
|
+
// ⚠️ SORTIE ANTICIPÉE, PAS GARDE — ET LA DISTINCTION EST MESURÉE. Le `catch` ci-dessous suffirait
|
|
570
|
+
// à la correction : appeler une méthode absente lève, on retombe, le résultat est le même. Muté
|
|
571
|
+
// en `if (!PLAYER.db)`, AUCUN banc ne rougit — c'est dit ici plutôt que laissé croire à une
|
|
572
|
+
// protection. Ce que cette ligne achète est un COÛT : sans elle, tout hôte qui n'implémente pas
|
|
573
|
+
// `count` construirait cinq exceptions à chaque lecture de carte, pour rien.
|
|
574
|
+
if (!PLAYER.db || typeof PLAYER.db.count !== "function") return null;
|
|
575
|
+
try {
|
|
576
|
+
const n = await PLAYER.db.count(chemin);
|
|
577
|
+
return Number.isInteger(n) && n >= 0 ? n : null;
|
|
578
|
+
} catch {
|
|
579
|
+
// ⚠️ ON NE RECOPIE PAS ICI LA RÈGLE DE LA COLONNE ABSENTE. Une première rédaction traitait le
|
|
580
|
+
// `42703` sur cette voie aussi, pour rendre zéro « comme l'autre ». Muté, ce branchement n'a
|
|
581
|
+
// fait rougir aucun banc — et pour une raison de fond, pas par manque de cas : une colonne
|
|
582
|
+
// supprimée fait échouer LES DEUX voies de la même façon, donc le repli rend déjà ce zéro. Le
|
|
583
|
+
// branchement n'ajoutait rien d'observable et créait un SECOND endroit où tenir la même règle.
|
|
584
|
+
return null; // on ne sait pas ⇒ on essaie l'autre voie, qui elle sait lire le 42703
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
|
|
526
588
|
async function compterBorne(chemin, cle) {
|
|
589
|
+
// ⚠️ UN COMPTE EXACT N'EST NI BORNÉ NI TRONQUÉ, quelle que soit sa taille : `borne` décrit la
|
|
590
|
+
// méthode par lignes, pas celle-ci. `tronque: false` garde donc le sens qu'il a partout —
|
|
591
|
+
// « lisez ce nombre comme exact » — au lieu d'en prendre un second selon la voie employée.
|
|
592
|
+
const exact = await compteExact(chemin);
|
|
593
|
+
if (exact !== null) return compte(exact, false, VOIE_EXACTE);
|
|
527
594
|
try {
|
|
528
595
|
// ⚠️ BORNE + 1 : la ligne excédentaire ne sert qu'à PROUVER qu'il en reste. On ne la publie pas.
|
|
529
596
|
// ⚠️ ET L'ORDRE N'EST PAS DÉCORATIF : sans lui, « la dernière ligne reçue » ne désigne aucune
|
|
530
597
|
// frontière, et le curseur de la sonde ne voudrait rien dire.
|
|
531
598
|
const lignes = await PLAYER.db.request(
|
|
532
599
|
`${chemin}&order=${cle}.asc&limit=${BORNE_RESTE + 1}`, { timeoutMs: 8000 });
|
|
533
|
-
if (!Array.isArray(lignes)) return compte(null, false);
|
|
600
|
+
if (!Array.isArray(lignes)) return compte(null, false, VOIE_BORNEE);
|
|
534
601
|
// 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);
|
|
602
|
+
if (lignes.length > BORNE_RESTE) return compte(BORNE_RESTE, true, VOIE_BORNEE);
|
|
536
603
|
// Zéro ligne : la sonde au-delà rendrait zéro elle aussi et n'apprendrait rien — y compris sous
|
|
537
604
|
// 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])
|
|
605
|
+
if (!lignes.length) return compte(0, false, VOIE_BORNEE);
|
|
606
|
+
return compte(lignes.length, await resteApres(chemin, cle, lignes[lignes.length - 1][cle]),
|
|
607
|
+
VOIE_BORNEE);
|
|
540
608
|
} catch (e) {
|
|
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
|
|
609
|
+
if (e && e.details && e.details.code === COLONNE_ABSENTE) return compte(0, false, VOIE_BORNEE);
|
|
610
|
+
return compte(null, false, VOIE_BORNEE); // indéterminé — surtout pas zéro
|
|
543
611
|
}
|
|
544
612
|
}
|
|
545
613
|
|
|
@@ -582,6 +650,18 @@ async function resteDeLaPurge() {
|
|
|
582
650
|
// confiance aux autres, alors que la borne est commune et que la question ne l'est pas.
|
|
583
651
|
out.tronque = [...comptes, ...totaux].some((c) => c.tronque);
|
|
584
652
|
out.lignes = parTable;
|
|
653
|
+
// ⚠️ UNE SEULE RÉPONSE POUR LES CINQ COMPTES, ET TROIS ÉTATS PLUTÔT QUE DEUX. La question qu'un
|
|
654
|
+
// hôte se pose est « ma couture sert-elle ? », pas « laquelle des cinq ». `"mixte"` n'est pas une
|
|
655
|
+
// commodité : il arrive vraiment — un `count` qui lève sur le chemin d'une colonne supprimée et
|
|
656
|
+
// répond sur le total de la même table — et c'est précisément le cas qu'un drapeau binaire
|
|
657
|
+
// aurait dû arrondir dans un sens ou dans l'autre, donc mentir.
|
|
658
|
+
//
|
|
659
|
+
// ⚠️ CE CHAMP NE DIT RIEN SUR LA JUSTESSE DES NOMBRES, seulement sur leur provenance. Il ne
|
|
660
|
+
// double aucun autre : `tronque` vaut `false` sur les DEUX voies, c'est même toute la raison
|
|
661
|
+
// d'être de cette ligne.
|
|
662
|
+
const voies = [...comptes, ...totaux].map((c) => c.voie);
|
|
663
|
+
out.voie = voies.every((v) => v === VOIE_EXACTE) ? VOIE_EXACTE
|
|
664
|
+
: voies.every((v) => v === VOIE_BORNEE) ? VOIE_BORNEE : "mixte";
|
|
585
665
|
SONDES_RESTE.forEach(([nom], i) => { out[nom] = comptes[i].n; });
|
|
586
666
|
// ⚠️ TROIS ÉTATS, PAS DEUX. `true` : plus rien, le retrait des colonnes est permis ICI. `false` :
|
|
587
667
|
// il reste des lignes. `null` : au moins une sonde n'a pas répondu — on ne sait pas, et « on ne
|