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:
@@ -302,6 +302,14 @@ export async function restoreOwnCaps(holderKey: string): Promise<void> {
|
||||
*
|
||||
* **Never creates.** Asking where to deposit must not bring an inbox into existence —
|
||||
* only its owner opens one, and only on its own document.
|
||||
*
|
||||
* **`undefined` is an ANSWER, and only an answer** (`1.0.0-dev.9`). It means the document
|
||||
* carries no address — nobody opened an inbox on it — or that it is not this holder's to
|
||||
* see one on. It no longer means "the read did not happen": both routes by which a
|
||||
* breakdown used to arrive here as `undefined` now reject. That mattered because two
|
||||
* published calls turn this value into a statement an application acts on:
|
||||
* `inbox.postToDocument` says "its owner never opened one", and `inbox.readForDocument`
|
||||
* says zero deposits — for a document whose inbox is full.
|
||||
*/
|
||||
// @provenance documentInboxAddress kind=divergent level=1 ref=engine/net/src/types.rs:ContactDetails — the deposit-side half of the same divergence: upstream you are TOLD an address, you never read one off a document
|
||||
export async function documentInboxAddress(doc: Nuri): Promise<Nuri | undefined> {
|
||||
@@ -313,7 +321,9 @@ export async function documentInboxAddress(doc: Nuri): Promise<Nuri | undefined>
|
||||
// …but ask the (emulated) network first: a document in a public store serves its cap
|
||||
// to whoever asks (public-store.ts), and "where do I deposit for this public
|
||||
// document" is exactly the question a third party arrives with, holding nothing but
|
||||
// the reference.
|
||||
// the reference. Its rejection is NOT caught: "the store could not be asked" is not
|
||||
// "this document has no inbox", and the whole value of the answer below is that the
|
||||
// two are told apart.
|
||||
await fetchReadCap(doc);
|
||||
if (mustNotAttempt(doc)) return undefined;
|
||||
const s = await session();
|
||||
@@ -330,10 +340,17 @@ export async function documentInboxAddress(doc: Nuri): Promise<Nuri | undefined>
|
||||
if (a && isNuri(a)) return a;
|
||||
}
|
||||
} catch (error) {
|
||||
// Unreadable document (no cap) or not synced → no address to give. Refusing to
|
||||
// read is the boundary doing its job, not an error to propagate here.
|
||||
// **Propagates a failed read** (`1.0.0-dev.9`). This used to answer `undefined`, on
|
||||
// the ground that "refusing to read is the boundary doing its job" — but the boundary
|
||||
// has already had its say at `mustNotAttempt` above, which returned before reaching
|
||||
// here. What this catch could still meet is a breakdown: a repo that would not open,
|
||||
// a broker that did not answer, a query that rejected. Answering `undefined` for that
|
||||
// told the caller the document has no inbox, which is the failure-as-absence this
|
||||
// path has paid for before. A caller that genuinely prefers to carry on catches it.
|
||||
console.error(accessLogPrefix() + " documentInboxAddress failed:", error);
|
||||
throw error;
|
||||
}
|
||||
// No address on the Header branch: nobody opened an inbox on this document. An answer.
|
||||
return undefined;
|
||||
}
|
||||
|
||||
|
||||
@@ -188,8 +188,16 @@ async function syncSession(): Promise<void> {
|
||||
* `"synced"`). Idempotent: a repo already opened (or in flight) is not re-opened.
|
||||
*
|
||||
* Tolerant by construction: if the injected `ng` exposes no `doc_subscribe` (e.g.
|
||||
* the fake `ng` in the unit suite), this is a no-op — the read proceeds unchanged.
|
||||
* Never throws; a failed open just leaves the read to behave as it did before.
|
||||
* the fake `ng` in the unit suite), this is a no-op — the read proceeds unchanged. The
|
||||
* open itself never throws: a `doc_subscribe` that never pushes a `State` is bounded by
|
||||
* the fallback timeout and leaves the read to behave as it did before.
|
||||
*
|
||||
* **It DOES throw when the public store could not be asked** (`1.0.0-dev.9`). That is the
|
||||
* one step here that can fail to ANSWER, and until then it answered `false` — "this
|
||||
* document exposes no cap" — so the open declined silently and the anchored read that
|
||||
* followed refused the reader for holding no cap, which was true but was not the reason.
|
||||
* A caller that means to carry on regardless says so with a `catch` of its own; none does
|
||||
* today, because every one of them is about to read the document.
|
||||
*
|
||||
* `holderKey` names WHOSE possession decides, when it is not the connected identity's —
|
||||
* the emulated inbox processor opening an inbox repo for its owner during someone else's
|
||||
@@ -205,7 +213,10 @@ export async function ensureRepoOpen(nuri: Nuri, holderKey?: string): Promise<vo
|
||||
// the outer overlay (`PublicRepoLinkV0`, `engine/net/src/types.rs:5098`). So ASK
|
||||
// before deciding whether we may touch it, or the answer would be "no" purely for
|
||||
// want of asking, and a bare reference to a public document would never suffice.
|
||||
// Memoised and inert once the cap is held (see public-store.ts).
|
||||
// Memoised and inert once the cap is held (see public-store.ts). REJECTS when the
|
||||
// store could not be asked at all, which is deliberately not the same event as "this
|
||||
// document exposes no cap": the open has not happened and the caller must not read as
|
||||
// though it had.
|
||||
if (holderKey === undefined) await fetchReadCap(nuri);
|
||||
// RULE 2 — do not even attempt. Opening a repo IS an access: it subscribes and
|
||||
// pulls its state. A user that holds no cap for it has no business asking.
|
||||
|
||||
@@ -181,10 +181,21 @@ export async function exposeReadCap(doc: Nuri, cap: ReadCap): Promise<void> {
|
||||
* Nothing is asked when the cap is already held: a document you can read needs no
|
||||
* fetching, and skipping it keeps the ordinary path free of physical reads.
|
||||
*
|
||||
* Never throws. Two different negatives reach the caller as the same `false`, and the
|
||||
* caller is right not to tell them apart — it obtained no cap either way. They are kept
|
||||
* apart where it matters, in the memo: an absence is remembered, a failure is not (see
|
||||
* the note on {@link attempted}).
|
||||
* **Two outcomes only, and a THIRD that is not an outcome.** `false` means the network
|
||||
* answered and the document exposes no cap — an absence, and the normal case. `true` means
|
||||
* a cap was obtained and filed. A download that REJECTED is neither: it REJECTS here too.
|
||||
*
|
||||
* It used to answer `false` for that as well, which is the defect this function was the
|
||||
* head of. `false` then meant "no cap" to three call sites at once, and each of them
|
||||
* turned a breakdown into a statement about the document: {@link ensureRepoOpen} declined
|
||||
* to open and let the read that followed blame the reader for holding no cap, and
|
||||
* `documentInboxAddress` answered "this document has no inbox", which an application reads
|
||||
* as zero deposits. `downloadReadCap` below produces the distinction deliberately and says
|
||||
* why; collapsing it one line later destroyed it. A failure must not read as an absence.
|
||||
*
|
||||
* The memo keeps the same asymmetry it always did, and it is now the same asymmetry: an
|
||||
* absence is remembered, a failure is not (see the note on {@link attempted}), so the next
|
||||
* caller asks the network again instead of replaying a rejection.
|
||||
*/
|
||||
// @provenance fetchReadCap kind=declared-not-wired level=1 ref=engine/net/src/types.rs:PublicRepoLinkV0 — the type DECLARES the cap is downloaded from the outer overlay if the brokers allow; it is constructed nowhere, both `PinRepo` constructors hard-code `expose_outer: false`, and `ExtTopicSyncReq` is `unimplemented!()`
|
||||
export async function fetchReadCap(docLike: Nuri): Promise<boolean> {
|
||||
@@ -200,7 +211,10 @@ export async function fetchReadCap(docLike: Nuri): Promise<boolean> {
|
||||
pending = downloadReadCap(doc);
|
||||
attempted.set(doc, pending);
|
||||
// A failure is not an answer, so it must not be filed as one: drop the memo the
|
||||
// moment the download rejects, and the next caller reaches the broker again.
|
||||
// moment the download rejects, and the next caller reaches the broker again. The
|
||||
// report stays even though the rejection now reaches the caller: this handler runs
|
||||
// once per download however many callers shared it, and it runs at all when every one
|
||||
// of them has given up, which is the only trace left in that case.
|
||||
// Attached HERE — once per download, before anybody awaits — so the eviction and the
|
||||
// report each happen exactly once however many callers share this one in-flight
|
||||
// fetch, and happen at all even if every one of them has already given up. The
|
||||
@@ -215,12 +229,24 @@ export async function fetchReadCap(docLike: Nuri): Promise<boolean> {
|
||||
let cap: ReadCap | null;
|
||||
try {
|
||||
cap = await pending;
|
||||
} catch {
|
||||
// Reported and forgotten by the handler attached where the download started, just
|
||||
// above — not swallowed here. The answer to the caller is the same as for an absence
|
||||
// (no cap was obtained), but the memo now holds nothing for this document, so the
|
||||
// next call asks the network instead of replaying this failure.
|
||||
return false;
|
||||
} catch (error) {
|
||||
// The download REJECTED: the repo would not open, the broker did not answer, the read
|
||||
// threw. Whether this document may be read is UNKNOWN, and that is not what `false`
|
||||
// says. Rethrown rather than returned so the caller's read fails instead of continuing
|
||||
// on a document it never obtained anything for — upstream's observable is that a read
|
||||
// rejects or it does not, and this is the side of it that was missing.
|
||||
//
|
||||
// Wrapped so the reason survives the trip: the message a caller would otherwise meet
|
||||
// is `assertMayReach`'s "you do not hold this document's cap", which is a different
|
||||
// event calling for a different action (give up, rather than retry). The original is
|
||||
// on `cause`, and the handler attached where the download started has already logged
|
||||
// it and dropped the memo, so the next caller asks the network again.
|
||||
throw new Error(
|
||||
"[ng-eventually] fetchReadCap: the public store could not be asked for this " +
|
||||
"document's read cap, so whether it may be read is UNKNOWN — this is not " +
|
||||
`"the document exposes no cap". ${JSON.stringify(doc)}`,
|
||||
{ cause: error },
|
||||
);
|
||||
}
|
||||
if (cap === null) return false;
|
||||
// Filed for whoever is connected NOW, on every call — the memo spares the round-trip,
|
||||
@@ -266,13 +292,10 @@ async function downloadReadCap(doc: Nuri): Promise<ReadCap | null> {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ask for a SET of documents' caps, in parallel — what a batch read does before it
|
||||
* decides which documents it may touch. Each fetch is independent and tolerant.
|
||||
*/
|
||||
// @provenance fetchReadCaps kind=declared-not-wired level=1 ref=engine/net/src/types.rs:PublicRepoLinkV0 — the batch form of the same unwired mechanism
|
||||
export async function fetchReadCaps(docs: Nuri[]): Promise<void> {
|
||||
const unique = [...new Set(docs.filter(Boolean))];
|
||||
if (unique.length === 0) return;
|
||||
await Promise.all(unique.map((d) => fetchReadCap(d)));
|
||||
}
|
||||
// `fetchReadCaps(docs)` was REMOVED in `1.0.0-dev.9`. It was the batch form — a
|
||||
// `Promise.all` over the singular, described as "independent and tolerant" — and it had
|
||||
// ZERO callers, the last one having gone with `readUnion` in `1.0.0-dev.8`. Keeping it
|
||||
// would have left an unowned tolerance policy standing over a call that now rejects:
|
||||
// "each fetch is independent" is exactly the skip-and-carry-on this line of work has
|
||||
// removed twice. Whoever needs several caps asks for each, at the call site that knows
|
||||
// what a missing one costs it. Internal throughout, so nothing published moves.
|
||||
|
||||
Reference in New Issue
Block a user