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.
This commit is contained in:
@@ -73,6 +73,38 @@
|
||||
* bounded by what it exposes today; the day the ORM surfaces a failed start, this is
|
||||
* where it lands.
|
||||
*
|
||||
* -- A named document is REACHED before the query opens, since `1.0.0-dev.9` --------
|
||||
* `scope.graphs` does not only narrow a search: each entry NAMES a document, and naming
|
||||
* one is the same act `docs.sparqlQuery`'s anchor is. So each named document is put
|
||||
* through the same two steps that call makes - the (emulated) public store is asked for
|
||||
* its cap, and the document is refused out loud if the cap is not held - plus the
|
||||
* cold-start open every other read path in this library crosses (`ensureRepoOpen`).
|
||||
*
|
||||
* What it repairs is this surface's own version of the failure it exists against. A named
|
||||
* document whose cap the holder does not hold used to reach the engine, come back, and be
|
||||
* emptied by the capability filter below, publishing `isSuccess` with `data: []` - "this
|
||||
* document holds nothing of that shape" for "you cannot read this document", while the
|
||||
* anchored read of the SAME document refuses. A named document this session has never
|
||||
* synced published the same snapshot for a document that is not empty. Both are now
|
||||
* `isError`, which is what `isError` has always meant here: the current answer is UNKNOWN.
|
||||
*
|
||||
* **What is deliberately NOT guarded, and each is an answer rather than a failure:**
|
||||
* - `graphs: "did:ng:i"` or `""` - the whole local dataset. The application named no
|
||||
* document, so there is no cap question to ask; the capability filter is the right
|
||||
* and only answer, exactly as the broker delivering only what your keys open is.
|
||||
* - `graphs: []` - no objects, by the SDK's own definition. `isSuccess` with `data: []`.
|
||||
* - a named document held and synced that holds nothing of the shape - `isSuccess` with
|
||||
* `data: []`, which is the distinction this whole surface exists to publish.
|
||||
* - the INERT case: before the first cap is issued nothing is enforcing, the public
|
||||
* store is not asked at all, and `assertMayReach` passes for everything.
|
||||
*
|
||||
* A scope naming SEVERAL documents fails as a whole when one of them cannot be reached,
|
||||
* rather than answering the others. A short list is the same lie as an empty one, and it
|
||||
* is the tolerance `readUnion` was removed for in `1.0.0-dev.8`.
|
||||
*
|
||||
* The barrier is why the open below is asynchronous: it prolongs `isPending`, and nothing
|
||||
* else about the states moves.
|
||||
*
|
||||
* -- An observable cannot un-emit: `data` survives an error -----------------
|
||||
* A one-shot call rejects and is done. An observable has already handed a list to a
|
||||
* subscriber that rendered it, so its failure snapshot has to say something about
|
||||
@@ -84,7 +116,10 @@
|
||||
|
||||
import { getCaps, getConfig } from "../shared-wallet/bootstrap";
|
||||
import { filterReadable } from "../emulated-verifier/read-filter";
|
||||
import type { OrmSubscriptionHandle, OrmSubscriptionLike } from "../model/types";
|
||||
import { ensureRepoOpen } from "../emulated-verifier/open-repo";
|
||||
import { assertMayReach } from "../emulated-verifier/reach";
|
||||
import { isNuri } from "../model/nuri";
|
||||
import type { Nuri, OrmSubscriptionHandle, OrmSubscriptionLike } from "../model/types";
|
||||
// The SDK's own types, imported as TYPES (erased at build, so no runtime `@ng-org`
|
||||
// import is added here). The provenance of what is PUBLISHED is recorded in `index.ts`.
|
||||
import type { Scope } from "@ng-org/orm";
|
||||
@@ -135,6 +170,32 @@ export interface ShapeObservable<T extends BaseType = BaseType> {
|
||||
refetch(): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* The whole local dataset, as the SDK spells it. `normalizeScope` maps `""` onto this
|
||||
* very string (`@ng-org/orm`'s `types.ts`), so both forms are the same marker and neither
|
||||
* names a document.
|
||||
*/
|
||||
const WHOLE_DATASET = "did:ng:i";
|
||||
|
||||
/**
|
||||
* The DOCUMENTS a scope NAMES - the only entries a capability question can be asked
|
||||
* about.
|
||||
*
|
||||
* Flattened the way the SDK flattens `graphs` (a bare string is one entry), then reduced
|
||||
* to what identifies a document: the whole-dataset marker is dropped because it names
|
||||
* none, and so is anything that is not a NextGraph reference - an ordinary IRI names no
|
||||
* repo this library could hold a cap for, and the engine is left to match it or not.
|
||||
* Deduplicated, so a scope repeating a document opens it once.
|
||||
*/
|
||||
function namedDocuments(scope: Scope): Nuri[] {
|
||||
const out: Nuri[] = [];
|
||||
for (const g of [scope.graphs].flat()) {
|
||||
if (typeof g !== "string" || g === "" || g === WHOLE_DATASET) continue;
|
||||
if (isNuri(g) && !out.includes(g)) out.push(g);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a reactive, `useQuery`-shaped observable over one SHEX `shapeType` in one
|
||||
* `scope` - the SDK's own `{ graphs, subjects }`, passed through untouched. See the
|
||||
@@ -287,40 +348,77 @@ export function useShapeQuery<T extends BaseType = BaseType>(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cross the barrier on every document the scope NAMES, before the query opens - the
|
||||
* same two steps `docs.sparqlQuery` takes on its anchor, in the same order.
|
||||
*
|
||||
* `ensureRepoOpen` asks the (emulated) public store for the document's cap, and opens
|
||||
* and syncs the repo when it may be touched; it declines SILENTLY when it may not,
|
||||
* which is right for a caller that has another way to find out. This one has none, so
|
||||
* `assertMayReach` says it out loud immediately after - without that, an unheld
|
||||
* document would simply produce no objects and read as a document holding none.
|
||||
*
|
||||
* Sequential rather than parallel, deliberately: the first document that cannot be
|
||||
* reached is the one reported, and there is no batch policy to own (`ensureReposOpen`,
|
||||
* the parallel form, was removed in `1.0.0-dev.8`/`.9` for having none worth keeping).
|
||||
* Each open is memoised per session, so a re-subscribe costs a map hit.
|
||||
*/
|
||||
async function reachNamedDocuments(): Promise<void> {
|
||||
for (const doc of namedDocuments(scope)) {
|
||||
await ensureRepoOpen(doc);
|
||||
assertMayReach(doc, "useShapeQuery");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Open a shape query for `(shapeType, scope)` and wire its two signals.
|
||||
*
|
||||
* Supersedes whatever was open: the previous generation's callbacks are dropped on
|
||||
* arrival rather than raced, so a slow answer can never clobber a newer question.
|
||||
* arrival rather than raced, so a slow answer can never clobber a newer question. The
|
||||
* barrier above runs first, which is why the body is asynchronous - every step past an
|
||||
* `await` re-checks that it is still the current generation before touching anything,
|
||||
* so a `refetch` or a teardown during the barrier leaves no query behind.
|
||||
*/
|
||||
function open(): void {
|
||||
closeQuery();
|
||||
const gen = ++generation;
|
||||
let handle: OrmSubscriptionHandle;
|
||||
try {
|
||||
handle = shapeQuery()(shapeType, scope);
|
||||
} catch (error) {
|
||||
setUnknown(error);
|
||||
return;
|
||||
}
|
||||
query = handle;
|
||||
const onChange = (): void => publish(handle, gen);
|
||||
onQueryChange = onChange;
|
||||
handle.addChangeListener(onChange);
|
||||
handle.readyPromise.then(
|
||||
() => {
|
||||
if (gen !== generation) return;
|
||||
ready = true;
|
||||
publish(handle, gen);
|
||||
},
|
||||
(error: unknown) => {
|
||||
// Nothing upstream rejects this promise today (see the module header); the
|
||||
// rejection path is wired all the same, because a state nobody handles is how
|
||||
// a failure becomes silence the day the ORM starts reporting one.
|
||||
void (async () => {
|
||||
try {
|
||||
await reachNamedDocuments();
|
||||
} catch (error) {
|
||||
if (gen !== generation) return;
|
||||
setUnknown(error);
|
||||
},
|
||||
);
|
||||
return;
|
||||
}
|
||||
// Superseded while the barrier was crossing: drop this open rather than leaving a
|
||||
// subscription nothing will ever close.
|
||||
if (gen !== generation) return;
|
||||
let handle: OrmSubscriptionHandle;
|
||||
try {
|
||||
handle = shapeQuery()(shapeType, scope);
|
||||
} catch (error) {
|
||||
setUnknown(error);
|
||||
return;
|
||||
}
|
||||
query = handle;
|
||||
const onChange = (): void => publish(handle, gen);
|
||||
onQueryChange = onChange;
|
||||
handle.addChangeListener(onChange);
|
||||
handle.readyPromise.then(
|
||||
() => {
|
||||
if (gen !== generation) return;
|
||||
ready = true;
|
||||
publish(handle, gen);
|
||||
},
|
||||
(error: unknown) => {
|
||||
// Nothing upstream rejects this promise today (see the module header); the
|
||||
// rejection path is wired all the same, because a state nobody handles is how
|
||||
// a failure becomes silence the day the ORM starts reporting one.
|
||||
if (gen !== generation) return;
|
||||
setUnknown(error);
|
||||
},
|
||||
);
|
||||
})();
|
||||
}
|
||||
|
||||
function start(): void {
|
||||
|
||||
Reference in New Issue
Block a user