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:
@@ -26,6 +26,7 @@ import { useShapeQuery } from "../src/surface/use-shape-query";
|
||||
import { configure } from "../src/index";
|
||||
import { configureStoreRegistry, setCurrentUser, getCaps } from "../src/shared-wallet/bootstrap";
|
||||
import { resetCaps, resetConfig, resetStoreRegistry } from "../src/shared-wallet/bootstrap";
|
||||
import { resetOpenedRepos as resetRegistry } from "../src/emulated-verifier/open-repo";
|
||||
import { resetRegistryCache, createEntityDoc } from "../src/shared-wallet/account-registry";
|
||||
import { getSyncState, resetOpenedRepos, setOpenTimeoutForTests } from "../src/emulated-verifier/open-repo";
|
||||
import { mintCap } from "../src/emulated-verifier/caps";
|
||||
@@ -59,6 +60,16 @@ interface SubRec { nuri: string; cb: (r: unknown) => void }
|
||||
* The stateful fake `ng` - the shim account, the per-scope index and the document
|
||||
* creation the page does around the read. It is NOT what answers the read any more.
|
||||
*/
|
||||
/**
|
||||
* The repo ids this file's fake broker has handed out - MONOTONIC across every `makeFake`,
|
||||
* for the reason `wallet-fake.ts` records at its own scale: a broker never mints a repo id
|
||||
* twice, because an id is a public key. A counter INSIDE the builder restarted at each
|
||||
* test, so two tests in this file minted the same nuri and aliased each other's entries in
|
||||
* the process-global open registry - invisible while a reset cleared every document's sync
|
||||
* state, and immediately visible once a reset began downgrading it instead.
|
||||
*/
|
||||
let mintedHere = 0;
|
||||
|
||||
function makeFake(options: { pushState?: boolean; subscribe?: boolean } = {}) {
|
||||
/** Anchors whose anchored read throws. Mutable after boot, so a suite builds a healthy
|
||||
* world first and breaks only the one call it is about. */
|
||||
@@ -69,7 +80,6 @@ function makeFake(options: { pushState?: boolean; subscribe?: boolean } = {}) {
|
||||
// without waiting for it.
|
||||
const pushState = options.pushState !== false;
|
||||
const quads: Quad[] = [];
|
||||
let docCounter = 0;
|
||||
const subs: SubRec[] = [];
|
||||
|
||||
// A NAMESPACE OF ITS OWN, and it is not cosmetic. `wallet-fake.ts` mints into
|
||||
@@ -79,7 +89,7 @@ function makeFake(options: { pushState?: boolean; subscribe?: boolean } = {}) {
|
||||
// answers, hanging it until the 8s fallback. `wallet-fake` carries the same lesson at its
|
||||
// own scale ("a broker never mints a repo id twice - an id is a public key - so neither
|
||||
// does this"); this is that lesson one scope wider.
|
||||
const doc_create = mock(async () => `did:ng:o:usq${++docCounter}`);
|
||||
const doc_create = mock(async () => `did:ng:o:usq${++mintedHere}`);
|
||||
|
||||
const sparql_update = mock(async (...a: unknown[]) => {
|
||||
const query = a[1] as string;
|
||||
@@ -1168,6 +1178,69 @@ describe("useShapeQuery", () => {
|
||||
unsub();
|
||||
});
|
||||
|
||||
// -- what the REGISTRY says, and whether it is true -----------------------
|
||||
//
|
||||
// Both of these are the same fault seen from the caller: state recorded as authoritative
|
||||
// that is not, with no way to correct itself. The repair is in the bookkeeper, not here.
|
||||
|
||||
it("(ee) a session change does not turn a withheld read into a success", async () => {
|
||||
const ng = makeFake({ pushState: false });
|
||||
const orm = makeOrm();
|
||||
inject(ng, orm);
|
||||
setOpenTimeoutForTests(30);
|
||||
setCurrentUser("vic");
|
||||
const mine = await createEntityDoc("vic", "protected");
|
||||
|
||||
const obs = useShapeQuery(EventShape, { graphs: [mine] });
|
||||
const unsub = obs.subscribe(() => {});
|
||||
for (let i = 0; i < 40 && getSyncState(mine) !== "timed-out"; i++) await tick();
|
||||
expect(obs.getSnapshot().isSuccess).toBe(false); // correctly withheld
|
||||
|
||||
// An in-page re-login: `syncSession` resets the registry when the session id changes,
|
||||
// and `restoreOwnCaps` / `readUserStore` reach it at connect. The registry used to
|
||||
// clear every document's sync state, and a cleared state reads `"unknown"` - which is
|
||||
// deliberately treated as authoritative, because on an `ng` with no `doc_subscribe`
|
||||
// there is no barrier to have missed. So a document that was KNOWN not to have synced
|
||||
// came back claiming authority it never had, against a query this session never
|
||||
// re-subscribed.
|
||||
resetRegistry();
|
||||
getCaps().learn(mintCap("did:ng:o:some-arrival"));
|
||||
for (let i = 0; i < 20; i++) await tick();
|
||||
|
||||
expect(obs.getSnapshot().isSuccess).toBe(false);
|
||||
unsub();
|
||||
});
|
||||
|
||||
it("(ff) a refusal that has been lifted is never published again as the answer", async () => {
|
||||
const ng = makeFake({ pushState: false });
|
||||
const orm = makeOrm();
|
||||
inject(ng, orm);
|
||||
setOpenTimeoutForTests(30);
|
||||
setCurrentUser("alice");
|
||||
const hers = await createEntityDoc("alice", "protected");
|
||||
|
||||
setCurrentUser("walt");
|
||||
const obs = useShapeQuery(EventShape, { graphs: [hers] });
|
||||
const unsub = obs.subscribe(() => {});
|
||||
for (let i = 0; i < 20 && !obs.getSnapshot().isError; i++) await tick();
|
||||
expect(String((obs.getSnapshot().error as Error).message)).toContain("does not hold");
|
||||
|
||||
// The cap arrives, so the refusal is LIFTED - and the document then fails to sync.
|
||||
// Two things must follow, and neither did: the answer is `isPending` (this round
|
||||
// decided that a barrier not crossed is not a failure), and the application must not
|
||||
// be handed a reason that is no longer true. It was still reading "does not hold this
|
||||
// document's cap" about a document Walt now holds.
|
||||
getCaps().learn(mintCap(hers));
|
||||
for (let i = 0; i < 40 && obs.getSnapshot().isError; i++) await tick();
|
||||
|
||||
expect(getSyncState(hers)).toBe("timed-out");
|
||||
const snap = obs.getSnapshot();
|
||||
expect(snap.isError).toBe(false);
|
||||
expect(snap.isSuccess).toBe(false);
|
||||
expect(snap.isPending).toBe(true);
|
||||
unsub();
|
||||
});
|
||||
|
||||
// -- teardown ------------------------------------------------------------
|
||||
|
||||
it("(k) the last unsubscribe closes the shape query", async () => {
|
||||
|
||||
Reference in New Issue
Block a user