Commit Graph

10 Commits

Author SHA1 Message Date
Sylvain Duchesne 2e5bc100b9 fix: un State tardif EST la barriere, donc il est enregistre quand il arrive
En relisant ce que je venais d'ecrire dans le contrat - « si le document se
synchronise plus tard et apporte des donnees, la lecture repond d'elle-meme » -
j'ai constate que c'etait faux. `onState` sortait aussitot des lors que le repli
borne avait deja termine l'ouverture, donc un `State` tardif etait jete :
`"timed-out"` restait indefiniment sur un depot desormais synchronise, et le
lecteur qui consulte cet etat ne pouvait jamais l'apprendre.

Le repli veut dire « cette ouverture a cesse d'attendre », jamais « la barriere ne
sera pas franchie » : l'abonnement n'est pas ferme, donc le `State` arrive encore.
`onState` enregistre desormais `"synced"` avant le controle. `"timed-out"` ->
`"synced"` est la seule transition ajoutee, et elle va de moins d'autorite vers
plus ; les deux lecteurs de cette valeur la veulent ainsi.

(dd) l'epingle : elle tombe si l'enregistrement repasse sous le controle.
2026-09-24 11:09:02 +02:00
Sylvain Duchesne 4af0be433d fix!: joindre un document n-est pas en avoir des nouvelles, et une reinit invalide ce qu-elle abandonne
Deux defauts, aucun des deux sur la reprise - elle a passe cette revue intacte et
n'a pas ete rouverte.

1. La barriere verifiait qu'un document nomme peut etre JOINT, jamais qu'il a
   REPONDU. Elle attendait `ensureRepoOpen` puis demandait `assertMayReach`, qui
   est la question de la capacite, et ne consultait pas l'etat de synchro que
   l'ouverture venait de produire. Un document dont l'abonnement a reussi mais dont
   le premier `State` n'est jamais venu publiait donc `isSuccess` avec `data: []` :
   le defaut d'origine, dans le cas precis pour lequel la barriere a ete ajoutee.

   La verification rejoint le reste de la decision, dans `publish`, donc rejouee a
   chaque publication au lieu d'etre memorisee. Les trois valeurs, enumerees avant
   d'ecrire la condition : `"synced"` fait autorite - zero ligne VEUT DIRE zero
   objet, la ligne tenue depuis cinq revues -, `"timed-out"` et `"syncing"` non, et
   `"unknown"` PASSE deliberement, parce qu'il veut dire « pas de semantique de
   `State` sur ce `ng` » et que le traiter autrement est exactement la regression
   du cas inerte livree aux tours 1 et 3.

   `isPending` plutot que `isError`, des deux que le contrat autorise : rien n'a
   echoue, il manque l'autorite de dire que le document est vide.

   Le test (t) EPINGLAIT le defaut - il attendait `isSuccess` apres trois
   barrieres expirees. Corrige. Ce n'est pas un test affaibli : l'ancienne
   assertion etait fausse.

   Et un trou dans ma propre suite, trouve par mutation : inverser la condition
   pour refuser `"unknown"` passait tous les tests, faute d'un `ng` SANS
   `doc_subscribe` nulle part. (cc) le couvre.

2. Regression de mon correctif du tour 4. Terminer une ouverture abandonnee
   faisait tourner sa continuation juste apres que la reinit avait vide les
   tables, donc elle ecrivait une entree `opened` perimee dans le registre neuf -
   et toute ouverture ulterieure de ce nuri court-circuitait dessus. Le depot
   n'etait jamais abonne dans la nouvelle session. Une epoque de registre corrige
   cela : une reinit l'incremente, une continuation dont l'epoque ne correspond
   plus n'ecrit rien. Une reinit INVALIDE ce qu'elle abandonne, en plus de le
   terminer.

`getSyncState` reste interne, et la doctrine cesse de l'offrir : elle pointait sur
un symbole qu'aucun appelant ne peut importer.
2026-09-24 11:03:07 +02:00
Sylvain Duchesne 8c890411bb fix!: la reprise cesse de dependre d-un front, et une ouverture abandonnee se termine
Quatrieme tour. Ce n'est pas un drapeau de plus : c'est la forme qui change.

Les tours 2 et 3 construisaient tous deux la reprise ainsi - noter qu'on a
refuse, puis reagir a un evenement ulterieur - et c'est la note qui etait
fautive les deux fois : une fois conditionnee a quelque chose qui ne pouvait pas
devenir vrai, une fois ecrite apres la fenetre ou arrive l'evenement decisif. Un
front manque n'est pas rattrapable par construction. Donc :

- LA DECISION EST UN NIVEAU. Les documents nommes sont reinterroges a chaque
  publication, jamais reportes depuis la barriere. Une capacite qui arrive change
  `isError` en document par le canal qui existait deja, et rien n'etant memorise,
  il n'y a aucun evenement a manquer. C'est aussi ce qui rend sans objet la
  question du porteur : une requete ouverte pendant qu'un autre etait connecte ne
  peut pas revenir a cet abonne en succes auquel il n'a pas droit.
- UNE TENTATIVE QUI A COURU CONTRE UN CHANGEMENT S'EN APERCOIT au lieu
  d'attendre. `open()` echantillonne `CapRegistry.version()` avant la barriere et
  compare apres son echec ; un ecart signifie qu'elle a decide sur un terrain
  perime, et elle retente d'elle-meme. Cela termine : une tentative ne se repete
  que parce que le trousseau a bouge, les mouvements sont finis, et ceux qu'une
  tentative provoque elle-meme sont les capacites qu'elle depose - au plus une par
  document nomme.

La comparaison de porteur disparait : `holderKey()` vaut "" tant que personne
n'est connecte, donc un refus pris avant que l'identite se fixe - la ou les
composants se montent - ne correspondait plus jamais ensuite.

Un instantane d'echec inchange n'est plus republie : la surface documente sa
stabilite, et huit arrivees sans rapport produisaient huit objets porteurs de huit
`Error` distinctes pour un etat identique.

Enfin, un defaut a moi, trouve en mesurant : le changement rendait la suite
intermittente. `resetOpenedRepos()` abandonnait les ouvertures en vol sans les
terminer, donc un appelant attendait les 8s du repli sur un abonnement demonte,
puis repartait avec son etat de synchro efface - et « unknown » est la lecture
LAXE, celle qui accepte qu'une lecture a zero ligne soit une absence averee. Une
absence-qui-cache-un-echec latente, de la famille meme de cette tache. Une
reinitialisation termine desormais ce qu'elle abandonne, en « timed-out ».

Deux fixtures qui se couraient apres sont corrigees, raison ecrite a cote.
2026-09-21 17:10:20 +02:00
Sylvain Duchesne f875047099 fix!: la garde circulaire disparait, et une nouvelle tentative se fait au nom de qui a ete refuse
Troisieme tour de revue adversariale, trois constats, tous reproduits avant
correction.

1. La garde etait CIRCULAIRE, et c'etait structurel. `namedDocumentsReachable`
   demandait si la capacite est DETENUE. Pour un document en store public, le seul
   canal qui puisse jamais la deposer est `fetchReadCap` - l'appel meme que la
   garde retenait. Elle attendait une reponse que seule l'action qu'elle bloquait
   pouvait produire, et toute cette classe restait irrecuperable.

   La bonne question n'est pas une meilleure garde, c'est de RETENTER. Les deux
   facons d'etre refuse se reparent par des choses opposees, et le memo les
   distingue deja : un echec n'est deliberement pas memoise, donc une nouvelle
   tentative redemande vraiment au reseau - c'est la reparation de cette classe,
   pas un cout a eviter ; une absence est memoise, donc une nouvelle tentative ne
   demande a personne et coute un instantane, jusqu'au jour ou la capacite arrive
   d'ailleurs, seule chose qui repare celle-la. La garde est supprimee : le module
   est plus petit qu'au tour precedent.

2. La reouverture faisait un ACCES sous la mauvaise identite. Republier REFILTRE
   une requete existante ; rouvrir EMET un abonnement et une requete au store
   public pour un document que cet abonne a nomme. C'est un acte neuf, pas un
   ancien qui ressort, et l'etat final etait `isSuccess` avec `data: []` pour un
   document illisible - le mensonge que cette tache supprime, par la porte du
   correctif. `refusedFor` retient QUI a ete refuse ; on ne retente que tant que
   c'est encore lui le connecte.

   Etabli plutot qu'affirme : une application ne peut pas changer d'identite en
   cours de session. `ensureIdentity` est le seul appel publie qui en etablit une,
   et `resolveIdentity` commence par `getCurrentUser()`. Corrige quand meme.

3. `Promise.all` court-circuite a la premiere rejection ARRIVEE, et un fetch qui
   rejette n'attend pas la barriere de synchronisation : il double un refus portant
   sur un document nomme avant lui. Le comportement est corrige plutot que le
   commentaire - `allSettled` puis parcours dans l'ordre du scope.

Et le quatrieme controle e2e est retire, pas repare : il passait avec toute la
machinerie de reprise supprimee. Un controle qui ne peut echouer pour aucune
regression de ce qu'il garde se lit comme de la couverture et vaut moins que rien.
2026-09-21 16:28:24 +02:00
Sylvain Duchesne f36ed5c164 fix!: un scope nomme refuse se rouvre quand la cle arrive, et la barriere cesse d-etre serie
Revue adversariale : la barriere livree au tour precedent rendait le refus
TERMINAL. Le signal du trousseau s'ecrit `if (query !== null) publish(...)`, parce
qu'avant la barriere il n'y avait qu'une seule raison de n'avoir pas de requete -
rien n'avait encore ete tente - et qu'une cle qui arrive ne pouvait changer que ce
qu'une requete OUVERTE avait le droit de montrer. La barriere en a ajoute une
seconde : elle abandonne AVANT que la requete existe, donc le seul canal qui fait
converger une cle tardive passait a cote de l'etat qui en avait le plus besoin.
Une page qui s'abonne pendant que son trousseau se restaure, ou un document
partage sous les yeux de son destinataire, publiait `isError` et y restait. Ce
n'est pas une surface plus stricte, c'est une pire : echanger un vide mensonger
contre une erreur bloquee ne repare rien, et cela casse une garantie que ce paquet
publie.

Le signal rouvre donc un scope refuse, et seulement quand le refus est leve
(`namedDocumentsReachable`). Le garde-fou porte autant que la reouverture :
rouvrir a chaque changement de trousseau rejouerait la barriere pour chaque cle
qu'une connexion depose, redemanderait au reseau tout document dont le
telechargement avait ECHOUE - un echec n'est deliberement pas memoise - et
republierait la meme erreur a chaque abonne. La variante indiscriminee casse deux
tests, dont celui de demontage qui preexistait.

La barriere passe en parallele. Elle etait serie, donc N documents qui ne poussent
jamais de `State` coutaient N replis bornes, 8s chacun en production. Les refus
restent lus dans l'ordre du scope apres coup.

Et ce qui a laisse passer les deux : l'e2e ne parcourait pas ce chemin. Les deux
journeys `useShapeQuery` lisent `{graphs: "did:ng:i"}`, donc 43/43 n'a jamais rien
prouve sur la barriere. Quatre controles ajoutes sur un scope NOMME, contre le
vrai broker : la proprietaire lit son document, un porteur de rien recoit
`isError` et jamais un succes vide, un document PUBLIC s'ouvre pour lui depuis la
seule reference via la requete de la barriere, et une cle qui tombe pour un AUTRE
document laisse le scope refuse en place.
2026-09-21 15:57:56 +02:00
Sylvain Duchesne 534d8f91b7 test: les deux cas qui ne doivent pas bouger, et la course que la barriere asynchrone ouvre
(p) `graphs: ""` est l'autre orthographe du dataset entier, jamais un nom de
document : le filtre reste seul a decider. C'est une garde, pas un test
discriminant - `isNuri` ecarte deja la chaine vide -, et elle epingle le
comportement plutot que le chemin.

(q) se desabonner pendant que la barriere est en train d'etre franchie ne doit
laisser aucune requete ouverte. La barriere rend l'ouverture asynchrone, donc il
existe desormais une fenetre ou l'observable a des abonnes, pas de requete, et une
ouverture en chemin ; verifie en retirant le controle de generation, ou la
requete est creee apres le dernier auditeur et plus rien ne la ferme.
2026-09-21 15:12:09 +02:00
Sylvain Duchesne 75313f75a2 fix!: une lecture qui n'atteint pas son document echoue, elle ne repond plus vide
`fetchReadCap` rendait `false` pour trois situations sans rapport : l'emulation
inerte, le document qui n'expose aucune capacite, et le reseau qu'on n'a pas pu
interroger. Les trois appelants vivants lisaient ce `false` comme « pas de
capacite » et transformaient une panne en affirmation sur le document :
`ensureRepoOpen` renoncait en silence et laissait la lecture suivante accuser le
lecteur de ne rien detenir, et `documentInboxAddress` repondait « ce document n'a
pas d'inbox », ce qu'une application lit comme zero depot.

Le telechargement rejette desormais jusqu'a l'appelant, avec le document nomme et
la cause d'origine conservee. `false` ne veut plus dire qu'une chose : le document
n'expose rien, et c'est une reponse. Le memo garde la meme asymetrie qu'avant.

`documentInboxAddress` propage aussi l'echec de sa propre lecture d'adresse : le
`catch` qui rendait `undefined` se justifiait par un refus que `mustNotAttempt` a
deja ecarte une ligne plus haut, donc il n'avalait que des pannes.

`useShapeQuery` franchit la barriere sur chaque document NOMME par `scope.graphs`
avant d'ouvrir la requete : capacite demandee au store public, refus a voix haute
si elle n'est pas detenue, ouverture et synchronisation. Un document nomme
qu'on ne peut pas lire donnait `isSuccess` avec `data: []` apres que le filtre de
capacites l'eut vide - la seule reponse que cette surface existe pour reserver a
« ce scope ne contient rien ». Restent intacts et succes : `did:ng:i`, `graphs:
[]`, un document joignable et vide, et tout le cas inerte.

`fetchReadCaps` supprime : aucun appelant, et une politique de tolerance par lot
sans proprietaire au-dessus d'un appel qui rejette desormais.
2026-09-21 15:00:01 +02:00
Sylvain Duchesne 77e78ec81d feat!: useShapeQuery lit par la requete de forme du SDK 2026-09-21 11:41:31 +02:00
Sylvain Duchesne 4a4e6af337 chore: ng-e2e-helpers suit alpha.14, et la mise a jour des paquets devient une tache 2026-09-21 10:52:07 +02:00
Sylvain Duchesne d0ca5c5d41 feat!: useShapeQuery prend le Scope du SDK, et un store se nomme par sa reference 2026-09-16 16:47:09 +02:00