discovery-media-player 0.1.150 → 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 +34 -0
- package/package.json +2 -2
- package/server/mesures.js +6 -0
- package/server/retention.js +54 -0
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
|
@@ -522,6 +522,40 @@ 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
|
+
⚠️ **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
|
+
|
|
525
559
|
⚠️ **And the same ceiling applies to every read you make through your own client, not just to
|
|
526
560
|
ours.** `limit=20000` does not return twenty thousand rows: PostgREST caps the response at
|
|
527
561
|
`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.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
|
@@ -510,6 +510,20 @@ const compte = (n, tronque) => ({ n, tronque });
|
|
|
510
510
|
* le compte d'en-tête (`Content-Range` sous `Prefer: count=exact`) lui est strictement supérieur :
|
|
511
511
|
* il ne dépend d'aucun plafond. Il demanderait d'élargir la capacité `db` du contrat d'hôte, qui ne
|
|
512
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.
|
|
513
527
|
*/
|
|
514
528
|
async function resteApres(chemin, cle, dernier) {
|
|
515
529
|
// Sans curseur lisible, la fin ne se prouve pas : « au moins » est le seul côté sûr.
|
|
@@ -523,7 +537,47 @@ async function resteApres(chemin, cle, dernier) {
|
|
|
523
537
|
} catch { return true; }
|
|
524
538
|
}
|
|
525
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
|
+
|
|
526
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);
|
|
527
581
|
try {
|
|
528
582
|
// ⚠️ BORNE + 1 : la ligne excédentaire ne sert qu'à PROUVER qu'il en reste. On ne la publie pas.
|
|
529
583
|
// ⚠️ ET L'ORDRE N'EST PAS DÉCORATIF : sans lui, « la dernière ligne reçue » ne désigne aucune
|