feat!: une seule reponse a « ce document est-il ouvert dans cette session »

Trois reponses coexistaient et pouvaient se contredire : un ensemble de
souscriptions bootstrap et une carte d'etat de barriere dans `open-repo`, et les
auditeurs du fan-out dans `surface/subscribe`. Les deux premieres survivaient a
la disparition de la troisieme, parce que `resetDocSubscriptions()` - ce que fait
le `configure()` publie - largue ses auditeurs sans rien dire a personne.

Il n'en reste qu'une, l'etat de l'unique souscription reelle du document, exposee
par `docChannel(nuri)`. `open-repo` n'enregistre plus que l'auditeur qu'il a pose
- « est-ce que j'ecoute encore », rien de plus - et derive `getSyncState` a chaque
lecture, si bien qu'aucune reponse ne peut survivre au canal qu'elle decrit.
Disparaissent : `held`, `syncState`, `inFlight`, `settleInFlight` et le compteur
de generation, remplace par l'identite de l'enregistrement.

Les deux echecs prouves deviennent des tests qui echouent sans le changement :
un second `configure()` apres une synchro reussie (`"synced"` au lieu de
`"timed-out"`, zero re-souscription, et `isSuccess` cote application), et un
etablissement encore en cours quand la couche de souscription est demontee - cas
que le canal `onSetupFailed` ne peut pas rattraper, puisqu'il ne rapporte que la
tentative encore attendue.

La ligne qui ne casse pas : un document synchronise qui ne contient rien repond
toujours un succes a zero objet. `1.0.0-dev.10`, les deux non-garanties
nommees sont retirees et la garantie livree porte le test qui l'epingle.
This commit is contained in:
Sylvain Duchesne
2026-09-24 12:57:43 +02:00
parent 2da26c5c4d
commit de9b8a0d67
10 changed files with 574 additions and 191 deletions
+9 -7
View File
@@ -95,17 +95,17 @@ export function subscribePhysicalDoc(nuri: Nuri, onChange: (r: DocChange, type:
## 6. Bootstrap repo opening — `emulated-verifier/open-repo.ts`
```ts
// open-repo.ts:90
// open-repo.ts:98
export type SyncState = "syncing" | "synced" | "timed-out";
// open-repo.ts:120 - TEST-ONLY
// open-repo.ts:197 - TEST-ONLY
export function setOpenTimeoutForTests(ms: number): void;
// open-repo.ts:127
// open-repo.ts:204
export function resetOpenedRepos(): void;
// open-repo.ts:162
// open-repo.ts:252 - DERIVED, nothing stores it
export function getSyncState(nuri: Nuri): SyncState | "unknown";
// open-repo.ts:210
// open-repo.ts:315
export async function ensureRepoOpen(nuri: Nuri, holderKey?: string): Promise<void>;
// open-repo.ts:234 - the unguarded core, for ONE importer (`shared-wallet/physical.ts`)
// open-repo.ts:339 - the unguarded core, for ONE importer (`shared-wallet/physical.ts`)
export async function openRepoUnguarded(nuri: Nuri): Promise<void>;
```
@@ -116,7 +116,9 @@ export async function openRepoUnguarded(nuri: Nuri): Promise<void>;
> `ensureRepoOpen`'s second parameter. The rest of this document carries drift of the same
> age and was deliberately left alone.
Heals the cold-start defect of the anchored read path: on a fresh session a not-yet-open repo reads empty, so before an anchored read the repo is opened by subscribing (`subscribePhysicalDoc`) and awaiting the first `State` push — the sync barrier — with a bounded timeout. The subscription is held for the session; per-nuri `SyncState` keeps `synced` and `timed-out` apart.
Heals the cold-start defect of the anchored read path: on a fresh session a not-yet-open repo reads empty, so before an anchored read the repo is opened by subscribing (`subscribePhysicalDoc`) and awaiting the first `State` push — the sync barrier — with a bounded timeout. The subscription is held for the session; `SyncState` keeps `synced` and `timed-out` apart.
> Corrected 2026-09-24 (`1.0.0-dev.10`), for this module only: the line numbers above, and the fact that nothing here STORES openness or the barrier any more. This module kept a set of live bootstrap subscriptions and a per-nuri sync-state map, and both outlived the subscription they described - `surface/subscribe.ts` drops its listeners without a word, which `configure()` makes it do. There is one answer now, `surface/subscribe.ts`'s `docChannel(nuri)`, and `getSyncState` derives from it on every call; what this module records is the bootstrap listener it took out, which answers "am I still listening" and nothing more.
- The opening mechanism is **level 2, VERIFIED as a composition**: `doc_subscribe` exists (`sdk/js/lib-wasm/src/lib.rs:1908`), and the push variants `TabInfo`/`State`/`Patch` are the engine's `AppResponseV0` (`engine/net/src/app_protocol.rs:1354-1358`). The ORDER (TabInfo first, then the initial State) and "first State = presence guaranteed, absence definitive" are **empirical, pinned by the in-repo e2e CONTRACT-3 probe — an ASSUMPTION about ordering as far as upstream is concerned**: no upstream statement fixes the push order, so a future reordering upstream would silently break the barrier. Bound: the e2e probe fails loudly if the order changes.
- "Hold a live subscription to keep the repo open" — **ASSUMPTION** (nothing upstream documents subscription lifetime as what retains a repo in `self.repos`); observed to work, bounded by the same probe.