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.
This commit is contained in:
@@ -109,6 +109,17 @@ const syncState = new Map<Nuri, SyncState>();
|
||||
/** The session id the current `opened`/`held` entries belong to. A change means a
|
||||
* new verifier session (fresh `self.repos`) → the registry must be invalidated. */
|
||||
let boundSessionId: string | number | null = null;
|
||||
/**
|
||||
* Which GENERATION of the registry an open belongs to, bumped by {@link resetOpenedRepos}.
|
||||
*
|
||||
* An open's continuation records the repo as opened, and it runs one microtask after the
|
||||
* open settles - a window a reset can land in. Without this it wrote that record into the
|
||||
* registry that had just replaced its own, and every later open of the same nuri
|
||||
* short-circuited on it: the repo was never subscribed in the new session, and a read over
|
||||
* it answered as though it had been. A reset must INVALIDATE what it abandons, not only
|
||||
* end it.
|
||||
*/
|
||||
let registryEpoch = 0;
|
||||
|
||||
/**
|
||||
* Max wait (ms) for the initial-state push before proceeding with the read anyway.
|
||||
@@ -151,6 +162,9 @@ export function resetOpenedRepos(): void {
|
||||
// nothing an absence. The abandoned open's own fallback fires later and sets the same
|
||||
// value, so the two cannot disagree.
|
||||
const abandoned = [...settleInFlight.entries()];
|
||||
// Everything in flight now belongs to a registry that no longer exists, so its
|
||||
// continuation must write nothing - see {@link registryEpoch}.
|
||||
registryEpoch += 1;
|
||||
opened.clear();
|
||||
inFlight.clear();
|
||||
settleInFlight.clear();
|
||||
@@ -272,6 +286,7 @@ export async function openRepoUnguarded(nuri: Nuri): Promise<void> {
|
||||
return;
|
||||
}
|
||||
|
||||
const myEpoch = registryEpoch;
|
||||
// Subscribed, first `State` not yet seen.
|
||||
syncState.set(nuri, "syncing");
|
||||
// Barrier clock: how long the subscribe→first-State (or fallback) round-trip
|
||||
@@ -321,6 +336,10 @@ export async function openRepoUnguarded(nuri: Nuri): Promise<void> {
|
||||
held.set(nuri, unsub);
|
||||
setTimeout(onTimeout, OPEN_TIMEOUT_MS);
|
||||
});
|
||||
// Only if this open still belongs to the registry it started in. After a reset the
|
||||
// three maps below are somebody else's, and `opened` in particular would become a
|
||||
// permanent lie: nothing re-subscribes a repo that is already recorded as open.
|
||||
if (myEpoch !== registryEpoch) return;
|
||||
opened.add(nuri);
|
||||
inFlight.delete(nuri);
|
||||
settleInFlight.delete(nuri);
|
||||
|
||||
@@ -176,7 +176,7 @@
|
||||
|
||||
import { getCaps, getConfig } from "../shared-wallet/bootstrap";
|
||||
import { filterReadable } from "../emulated-verifier/read-filter";
|
||||
import { ensureRepoOpen } from "../emulated-verifier/open-repo";
|
||||
import { ensureRepoOpen, getSyncState } from "../emulated-verifier/open-repo";
|
||||
import { assertMayReach } from "../emulated-verifier/reach";
|
||||
import { isNuri } from "../model/nuri";
|
||||
import type { Nuri, OrmSubscriptionHandle, OrmSubscriptionLike } from "../model/types";
|
||||
@@ -417,6 +417,35 @@ export function useShapeQuery<T extends BaseType = BaseType>(
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Has every document the scope NAMES actually ARRIVED - not merely "may this holder
|
||||
* reach it", which is a different question and the only one this module used to ask?
|
||||
*
|
||||
* `emulated-verifier/open-repo.ts` keeps three values apart for exactly this reader, and
|
||||
* which of them counts as an answer is the whole of the decision:
|
||||
*
|
||||
* - `"synced"` - the first `State` came. THE truth signal: zero rows means zero
|
||||
* objects, and that is the success this surface exists to publish.
|
||||
* - `"timed-out"` - the bounded fallback fired with no `State`. Opened best-effort,
|
||||
* sync unconfirmed. The document may hold anything and this read has no standing to
|
||||
* call it empty. This is the case the barrier was added for and did not close.
|
||||
* - `"syncing"` - unreachable once the open has resolved (it resolves on the `State`
|
||||
* or on the fallback, and each sets its own value); refused anyway, because if it
|
||||
* ever IS reached then the barrier demonstrably was not.
|
||||
* - `"unknown"` - "never requested, or the fake-ng no-op path: no `State` semantics".
|
||||
* **Passes, deliberately.** There is no barrier to have missed where there are no
|
||||
* barrier semantics, and an `ng` without `doc_subscribe` is the whole unit suite and
|
||||
* every early path. Treating it as not-synced is the inert regression this line of
|
||||
* work has already shipped twice.
|
||||
*/
|
||||
function namedDocumentsArrived(): boolean {
|
||||
for (const doc of namedDocuments(scope)) {
|
||||
const state = getSyncState(doc);
|
||||
if (state === "timed-out" || state === "syncing") return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Publish what the subscription holds right now. Only ever called on a READY
|
||||
* generation, so this snapshot is always an answer.
|
||||
@@ -441,6 +470,14 @@ 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;
|
||||
setSnapshot({
|
||||
data: readable(handle),
|
||||
isPending: false,
|
||||
|
||||
Reference in New Issue
Block a user