discovery-media-player 0.1.152 → 0.1.154

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.
@@ -25,7 +25,7 @@ need.
25
25
  it the same day, and remove it when the release lands.
26
26
  6. **Describe what you do — including what you think is trivial.** Not *"report deviations"*: you
27
27
  cannot know what one is, because that would mean knowing this page better than we do. ⚠️ The
28
- over-specified sentence in [the `tts-cache` section](#four-things-that-will-bite) stood for
28
+ over-specified sentence in [the `tts-cache` section](#what-will-bite) stood for
29
29
  weeks, and it took a host mentioning its own naming **as a curiosity** for anyone to look. Its
30
30
  own account of it, on 27/08: *"je ne l'ai décrite que parce que je citais `preview-fr-v2` comme
31
31
  une curiosité, sans savoir que c'était un écart. Si j'avais su que votre page l'interdisait, je
@@ -50,7 +50,7 @@ need.
50
50
  "presenceDurcissement": "inconnu",
51
51
  "presenceFusion": "inconnu",
52
52
  "lectureSaturee": { "total": 0, "fenetreS": 0, "derniereIlYaS": null },
53
- "mesures": { "fenetreS": 0, "seauxMs": [1, 2, 5, 10, 25, 50, 100, 250, 500, 1000, 2500, 5000, 10000], "routes": {}, "base": { "n": 0 }, "statuts": { "ok": 0, "refus4xx": 0, "debit429": 0, "occupe503": 0, "erreur5xx": 0 }, "memoireMio": { "rss": 0, "heap": 0, "tampons": 0 }, "boucleMs": { "n": 0, "moyen": null, "p99": null, "resolutionMs": 20 } },
53
+ "mesures": { "fenetreS": 0, "seauxMs": [1, 2, 5, 10, 25, 50, 100, 250, 500, 1000, 2500, 5000, 10000], "familles": ["document", "presentation", "action", "fichier", "carte", "autre"], "routes": {}, "base": { "n": 0 }, "statuts": { "ok": 0, "refus4xx": 0, "debit429": 0, "occupe503": 0, "erreur5xx": 0 }, "memoireMio": { "rss": 0, "heap": 0, "tampons": 0 }, "boucleMs": { "n": 0, "moyen": null, "p99": null, "resolutionMs": 20 } },
54
54
  "retentionSweep": false,
55
55
  "hostShare": true,
56
56
  "hostMail": true,
@@ -180,6 +180,7 @@ these numbers from their side.
180
180
  |---|---|
181
181
  | `fenetreS` | seconds this process has been running — **the window every total below was counted over** |
182
182
  | `seauxMs` | the bucket ladder the percentiles are read off, published **with** the numbers |
183
+ | `familles` | **the denominator of `routes`** — every family this build measures, whether or not it was exercised. It does not move with traffic; that is what makes it a denominator |
183
184
  | `routes` | one entry per family of work — `document`, `presentation`, `action`, `fichier`, `carte`, `autre`. Families absent from the object were never exercised in this process |
184
185
  | `base` | the same shape, for calls through the `db` capability **you** supply — measured at the seam, so it covers every call, including ones nobody has written yet |
185
186
  | `statuts` | responses by class: `ok` (<400), `refus4xx`, `debit429`, `occupe503`, `erreur5xx` |
@@ -194,6 +195,17 @@ reading is. `null` means *past the top of the ladder* (over 10 s), which is itse
194
195
  ⚠️ **`n: 0` is not `0 ms`.** A family that was never exercised reports `{ "n": 0 }` and nothing
195
196
  else, and `boucleMs` with no samples reports `moyen: null` — not a zero that would read as *healthy*.
196
197
 
198
+ ⚠️ **`routes: {}` used to be indistinguishable from broken instrumentation, and `familles` is why
199
+ it no longer is.** Omitting an unexercised family is correct — a `0 ms` would read as *instantaneous*
200
+ — but the omission left a reader unable to tell *no traffic in this window* from *measurement is not
201
+ running*. A loaded host never meets the question: its entries are always there, so their presence
202
+ witnesses itself.
203
+
204
+ The rule came from a host measuring their own smallness: **a field whose value is its own witness at
205
+ scale needs an explicit witness at small scale.** At 99 sessions nothing witnesses anything, and a
206
+ busier host is better instrumented without having instrumented anything. `statuts` and `boucleMs`
207
+ already carried their denominators; `routes` had not.
208
+
197
209
  ⚠️ **`boucleMs` is the delay, not the interval.** The sampler observes how long its own timer
198
210
  actually took, which at rest equals its resolution; the resolution is subtracted, so an idle
199
211
  instance reports about `0` rather than a permanent `20` that would send you hunting a fault that
@@ -733,7 +745,7 @@ The rule underneath, safer than the list: **never fall back on a refusal of *acc
733
745
  back on an inability to *reach*.** And "do not fall back" applies to what you **offer** — an
734
746
  "Open ↗" button left in place is falling back one second later.
735
747
 
736
- ## Four things that will bite
748
+ ## What will bite
737
749
 
738
750
  **Your document-opening doors reappear.** A host has more than one place that opens a file, and new
739
751
  ones get written. Keep the list and hunt it periodically — and note that **your search criteria
@@ -881,6 +893,90 @@ cannot. Setting the number spares you that reasoning entirely.
881
893
  but *did you measure exactly what fails*. Two true statements about the same instance can describe
882
894
  different responses.
883
895
 
896
+ **⚠️ The ceiling on your requests is set by the role that OPENS the connection, not the one they run
897
+ as.** A host measured this and it is invisible from our side. Their `authenticator` role carries a
898
+ `statement_timeout`; PostgREST's `SET ROLE` does **not** reset it, so code running as `service_role`
899
+ inherits it — while `service_role` itself shows no setting at all, so nothing our code can read
900
+ suggests a limit exists. At that host every request we issue is capped at 8 seconds, and a lock
901
+ waited on for longer than that fails. Your paginated `selectAll`, a batched retention sweep, a
902
+ `count` on a large table: each is one statement, so each gets the whole budget and no more,
903
+ whatever the batch size.
904
+
905
+ Our own client abort no longer sits at that same value, deliberately. Two timers set to the same
906
+ number do not produce a wrong answer here — both routes fall back to `null`, and a bench proves it —
907
+ but they make the *cause* undecidable: when our abort wins the race, the server's `57014` never
908
+ reaches us and "too slow" becomes indistinguishable from "the network died".
909
+
910
+ **⚠️ And `safeupdate` refuses an unrestricted write even under `service_role` — read that backwards.**
911
+ The same host has it preloaded on `authenticator`: a `DELETE` or `UPDATE` with no restricting clause
912
+ is rejected outright, and the error message does not name `safeupdate`, so the refusal arrives
913
+ without its reason. They paid for that once, on one of our functions.
914
+
915
+ The trap is not the refusal. **`safeupdate` is the net.** At a host that has it, an unfiltered write
916
+ fails loudly; at a host that does not — and nothing in this contract requires it — the very same
917
+ line succeeds and empties the table. The defect is therefore silent exactly where it is severe. A
918
+ guard in this repository now refuses any `DELETE`, `PATCH` or `PUT` we write without a restricting
919
+ predicate, and it counts `?select=…` as a projection rather than a filter, because that is the shape
920
+ that survives a review.
921
+
922
+ ## What you can see and we cannot
923
+
924
+ **Read this only if you run this player somewhere.** If you are evaluating it, skip to Versioning —
925
+ this section asks for nothing from you.
926
+
927
+ ⚠️ **Everything below exists because three defects in one week were found by hosts, not by us**, and
928
+ none of the three was findable from here. Every guard here measures this repository. They
929
+ cannot measure an installation they have never seen, and that is not a gap we can close by adding
930
+ one more. So this is not a request for feedback in general — it is four specific questions we cannot
931
+ answer ourselves, each printed with what it cost us not to have asked it earlier.
932
+
933
+ You owe us none of this. But you are the only one who can answer any of it.
934
+
935
+ **1. A ceiling in your installation that we assume away.** PostgREST's `db-max-rows` is set to 1000
936
+ by default on Supabase: the server returns 1000 rows however many you ask for. We compared what we
937
+ received against *our* bound, concluded "not truncated", and published `1000` rows of a 1651-row
938
+ table **asserting the number was exact** — which is worse than the defect it replaced, because the
939
+ previous version claimed nothing. Four hours after the release, a host measured it. If your
940
+ deployment caps, times out, paginates, or rewrites anything between us and your database, that
941
+ limit is invisible from here and our arithmetic is probably wrong about it.
942
+
943
+ **2. Something this card asserts that you can check against your own database.** Not "does it look
944
+ right" — *does this number match what a query returns right now*. The purge card is the obvious one:
945
+ `vide: true` is what authorises dropping a column, so a wrong `true` is expensive and a wrong `0`
946
+ authorises a deletion. A host who compared the card against their own tables is the reason the
947
+ counter stopped being able to lie.
948
+
949
+ **3. What your volumes hide from you — and what they reveal for free.** This one is the least
950
+ obvious and cost us a field. At 1655 rows an exact count **is its own proof**: it exceeds a ceiling
951
+ the bounded route structurally cannot cross, so the number itself witnesses which mechanism ran. At
952
+ 99 sessions no value can ever separate the two, and a broken seam is indistinguishable from a
953
+ working one. Same code, same card — one installation detects a failure for free that the other
954
+ cannot see at all. So: **tell us your orders of magnitude**, and tell us when a field of ours only
955
+ makes sense at your scale. A small host is not merely short of occasions; it has lost discriminating
956
+ power, and that is our problem to fix with an explicit witness, not theirs to live with.
957
+
958
+ **4. A rule you drew from your own defect.** The most useful things we received this week were not
959
+ bug reports. They were sentences: *a control that separates two mechanisms must rest on what one can
960
+ do and the other cannot, never on a value both could return* — and *is there a state of the world
961
+ where this value is false?*, which is the question separating a limit of observation from a silent
962
+ failure mode. Both were written by hosts about their own mistakes. Both, applied to this repository
963
+ within the hour, found something. A defect tells us about one line; a rule tells us where to look.
964
+
965
+ ⚠️ **And tell us what you did not do.** A host closed a report with thirty-two reads still
966
+ unaudited, said so plainly, and that sentence is the only reason we know those reads exist. We
967
+ cannot measure your code. We can only know what someone wrote down. *"Not measured"* and *"nothing
968
+ found"* are different sentences, and only one of them is honest when you have not looked.
969
+
970
+ **What not to send.** Shapes and counts, never contents. No row data, no reader IPs or User-Agents —
971
+ those are the columns half this contract exists to get rid of — no keys, tokens, connection strings,
972
+ or private hostnames. *"A table of ~1600 rows returned 1000"* is the whole of what we needed to fix
973
+ the ceiling defect; the rows themselves would have added nothing and created a problem.
974
+
975
+ **What we do with it.** Every item above became code, a bench that dies if the fix is removed, and a
976
+ dated entry naming the case. That last part is deliberate and a host asked for it: an abstract
977
+ justification rots, a dated incident does not. In six months someone will read a field and ask why
978
+ it exists, and the answer will name you.
979
+
884
980
  ## Versioning
885
981
 
886
982
  Semantic versioning on the package, independent of the `contract` number. Pin an **exact** version:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "discovery-media-player",
3
- "version": "0.1.152",
3
+ "version": "0.1.154",
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/mesures.js CHANGED
@@ -180,6 +180,22 @@ function relever() {
180
180
  // ⚠️ L'ÉCHELLE EST PUBLIÉE AVEC LES CHIFFRES. Sans elle, `p95sousMs: 250` ne dit pas si la
181
181
  // mesure suivante aurait pu être 251 ou 999 — un lecteur ne peut pas juger de sa précision.
182
182
  seauxMs: SEAUX_MS,
183
+ // ⚠️ LE DÉNOMINATEUR DE `routes`, ET IL MANQUAIT DEPUIS LE DÉBUT. Une famille sans échantillon
184
+ // est OMISE de `routes` — donc `routes: {}` ne distingue pas « aucun trafic sur la fenêtre »
185
+ // de « la mesure ne tourne pas ». Chez un hôte chargé la question ne se pose jamais : il y a
186
+ // toujours des entrées, et leur présence témoigne d'elle-même. Chez un hôte à 99 sessions,
187
+ // rien ne témoigne de rien.
188
+ //
189
+ // La règle vient d'un hôte, sur ses propres volumes : « les champs dont la valeur est son
190
+ // propre témoin à grande échelle ont besoin d'un témoin explicite à petite échelle ». Une
191
+ // petite installation n'est pas seulement privée d'occasions — elle perd du POUVOIR
192
+ // DISCRIMINANT, et un hôte plus chargé se trouve mieux instrumenté sans avoir rien instrumenté.
193
+ //
194
+ // ⚠️ ET LES DEUX CHAMPS VOISINS AVAIENT DÉJÀ RAISON, ce qui rend l'omission mesurable plutôt
195
+ // qu'opinable : `statuts` publie ses cinq clés à zéro, `boucleMs` publie `n: 0` avec des
196
+ // `null` explicites « plutôt que de publier un zéro qui se lirait la boucle est saine ». Trois
197
+ // champs frères du même objet, deux qui portent leur dénominateur et un qui l'oubliait.
198
+ familles: [...FAMILLES],
183
199
  routes,
184
200
  base: centiles(histoBase),
185
201
  statuts: { ...statuts },
@@ -455,6 +455,16 @@ function tick() {
455
455
  // pas une opinion sur ce qu'un hôte peut avoir.
456
456
  const BORNE_RESTE = 5000;
457
457
 
458
+ // ⚠️ DOUZE SECONDES, ET LE NOMBRE VIENT D'ÉVITER UNE ÉGALITÉ, PAS D'UN GOÛT. Il valait 8000 — très
459
+ // exactement le `statement_timeout` qu'un hôte a mesuré sur son rôle `authenticator`. Deux minuteries
460
+ // réglées sur la même valeur ne rendent pas un résultat faux ici (les deux voies retombent sur
461
+ // `null`, un banc l'éprouve), mais elles rendent la CAUSE indécidable : quand notre abandon gagne la
462
+ // course, le `57014` du serveur ne nous parvient jamais, et « la requête était trop lente » devient
463
+ // indistinguable de « le réseau est tombé ». Un plafond client strictement AU-DESSUS des plafonds
464
+ // serveur courants laisse le serveur expliquer son refus. C'est la règle de la marge sur le SUJET,
465
+ // appliquée à du code de production plutôt qu'à un banc.
466
+ const DELAI_LECTURE = 12000;
467
+
458
468
  const SONDES_RESTE = [
459
469
  ["sessionsIp", "commercial_doc_sessions", "session_id", "ip"],
460
470
  ["sessionsUa", "commercial_doc_sessions", "session_id", "ua"],
@@ -543,7 +553,7 @@ async function resteApres(chemin, cle, dernier) {
543
553
  if (dernier == null) return true;
544
554
  try {
545
555
  const suite = await PLAYER.db.request(
546
- `${chemin}&${cle}=gt.${enc(String(dernier))}&order=${cle}.asc&limit=1`, { timeoutMs: 8000 });
556
+ `${chemin}&${cle}=gt.${enc(String(dernier))}&order=${cle}.asc&limit=1`, { timeoutMs: DELAI_LECTURE });
547
557
  // Pas de réponse analysable ⇒ on ne sait pas ⇒ « au moins ». Se tromper vers le minorant ne
548
558
  // fait que sous-estimer ; se tromper vers l'exactitude fait conclure.
549
559
  return !Array.isArray(suite) || suite.length > 0;
@@ -596,7 +606,7 @@ async function compterBorne(chemin, cle) {
596
606
  // ⚠️ ET L'ORDRE N'EST PAS DÉCORATIF : sans lui, « la dernière ligne reçue » ne désigne aucune
597
607
  // frontière, et le curseur de la sonde ne voudrait rien dire.
598
608
  const lignes = await PLAYER.db.request(
599
- `${chemin}&order=${cle}.asc&limit=${BORNE_RESTE + 1}`, { timeoutMs: 8000 });
609
+ `${chemin}&order=${cle}.asc&limit=${BORNE_RESTE + 1}`, { timeoutMs: DELAI_LECTURE });
600
610
  if (!Array.isArray(lignes)) return compte(null, false, VOIE_BORNEE);
601
611
  // Notre propre borne atteinte : la preuve est dans la ligne excédentaire, rien à demander.
602
612
  if (lignes.length > BORNE_RESTE) return compte(BORNE_RESTE, true, VOIE_BORNEE);