fix!: le registre d-ouverture n-a plus qu-un seul sens, et il cesse de mentir

Trois constats, un seul defaut sous trois visages : un etat enregistre comme vrai
qui ne l'est pas, sans moyen de se corriger. Deux des trois vivaient dans
`open-repo.ts`. Cinq tours sont alles dans l'appelant ; c'est le teneur de
registre qui mentait.

CE QUE LE REGISTRE VEUT DIRE, ecrit dans l'en-tete. `held` est le SEUL registre
d'ouverture : cette session detient un abonnement vivant a ce document. Il y avait
a cote un `opened` voulant dire « on a appele `doc_subscribe` une fois et on a
cesse d'attendre », et ce sens plus lache mentait de trois facons : il survivait a
un abonnement REFUSE par le courtier (donc la reprise que `subscribe.ts` offre
etait bloquee a jamais), il survivait a une reinitialisation (donc une nouvelle
session court-circuitait sur l'enregistrement de la precedente), et il laissait un
document annoncer une barriere jamais franchie. `opened` est supprime.

Les trois consequences, desormais vraies par construction :
- un abonnement qui a ECHOUE n'enregistre rien - `onSetupFailed`, le canal que
  `subscribe.ts` offre exactement pour cela, est enfin branche - donc l'appelant
  suivant s'abonne a nouveau ;
- une reinitialisation efface l'enregistrement ET DECLASSE l'etat de synchro :
  une barriere franchie dans la session qui vient de disparaitre n'en est pas une
  que celle-ci a franchie ;
- `"unknown"` ne veut plus dire qu'une chose : aucune barriere n'etait attendue ici
  (jamais demande, ou un `ng` sans `doc_subscribe`). Jamais « on a oublie ».

Cote appelant, un seul changement, et c'est le troisieme constat : quand un
document nomme n'a pas repondu, `publish` PUBLIE « la question reste ouverte » au
lieu de sortir en laissant en place l'instantane precedent. Sortir laissait un
refus DEJA LEVE a l'affichage : l'application lisait « vous ne detenez pas la
capacite de ce document » a propos d'un document qu'elle detient desormais.

Deux tests portaient l'ancien sens et sont corriges avec leur raison : celui du
`ng` sans `doc_subscribe` (le compteur d'ids du faux courtier redemarrait a chaque
test, donc deux tests aliasaient leurs depots - la lecon de `wallet-fake` a
l'echelle du fichier), et celui de la lecture froide, ou « timed-out » est
desormais ce que dit « cette page ne l'a pas confirme ».
This commit is contained in:
Sylvain Duchesne
2026-09-24 11:34:10 +02:00
parent 2571f8e738
commit 6b2b8ab42a
6 changed files with 218 additions and 32 deletions
@@ -380,6 +380,26 @@ export function useShapeQuery<T extends BaseType = BaseType>(
});
}
/**
* Publish "the question is still open" - no authority yet, and nothing to say about the
* data either way.
*
* `data` is carried over for the same reason {@link setUnknown} carries it: an observable
* cannot un-emit a list a subscriber has already rendered. Before the first answer there
* is nothing to carry and it stays `[]`. Idempotent, so a document that keeps failing to
* sync does not wake every bound view on each attempt.
*/
function setNotYetAnswered(): void {
if (snapshot.isPending && !snapshot.isSuccess && !snapshot.isError) return;
setSnapshot({
data: snapshot.data,
isPending: true,
isSuccess: false,
isError: false,
error: undefined,
});
}
/**
* The readable items of the live subscription, as a plain array.
*
@@ -470,14 +490,20 @@ export function useShapeQuery<T extends BaseType = BaseType>(
setUnknown(refusal);
return;
}
// NOT AUTHORITATIVE YET, so say nothing rather than say "empty". A named document
// whose sync barrier was never crossed may hold anything, and publishing the ORM's
// view of it as `isSuccess` is the original defect in the one case the barrier was
// added to close. Returning here leaves the snapshot where it was - the initial
// pending one on a first publication - which is the contract's "a longer `isPending`",
// and never a successful empty. Re-read on every publication, so a `State` that lands
// later and brings data republishes through the ORM's own change signal and answers.
if (!namedDocumentsArrived()) return;
// NOT AUTHORITATIVE YET, so say the question is still open rather than say "empty". A
// named document whose sync barrier was never crossed may hold anything, and
// publishing the ORM's view of it as `isSuccess` is the original defect in the one case
// the barrier was added to close.
//
// It PUBLISHES pending rather than returning. Returning left whatever snapshot was
// standing, and that was wrong in the case that matters: a refusal that has since been
// LIFTED stayed on display, so an application was handed `isError` reading "you do not
// hold this document's cap" about a document it now holds. Whatever the state was, it
// is not the answer to the question just asked.
if (!namedDocumentsArrived()) {
setNotYetAnswered();
return;
}
setSnapshot({
data: readable(handle),
isPending: false,