docs: le contrat passe en 1.0.0-dev.9, et ce qu'une lecture echouee repond est consigne
Majeur : trois appels existants se resserrent. `useShapeQuery` publie `isError` pour un document NOMME qu'il ne peut pas lire, la ou il rendait `isSuccess` avec `data: []` - la seule image que cette surface reserve a « ce scope ne contient rien ». `inbox.postToDocument`, `readForDocument` et `readSyncedForDocument` rejettent la ou elles annoncaient « pas d'inbox ». Rien de ce qui etait deja une reponse ne bouge : un document vide, `graphs: []`, un document sans inbox, un document public qui n'expose aucune cle. Deux non-garanties corrigees parce que le changement les rend fausses : « aucune des deux lectures par forme ne va chercher la cle d'un store public » ne vaut plus que pour `useShape` et pour `did:ng:i`, et le pending eternel d'un demarrage refuse se referme sur le cas nomme. Nouveau `knowledge_what-a-failed-read-answers-upstream` : ce que la cible repond pour un document injoignable, non synchronise ou vide, et les deux collisions que l'appelant ne peut pas demeler - « n'existe pas » contre « pas connu de moi » d'un cote, « non synchronise » contre « vraiment vide » de l'autre. C'est le fait durable qui justifie la direction prise, et il vaut quel que soit le remede. `docs/internal-contract.md`, section open-repo seulement : numeros de ligne, `ensurePhysicalRepoOpen` qui n'a jamais ete un export de ce module, `ensureReposOpen` supprime, `openRepoUnguarded` absent, et le second parametre de `ensureRepoOpen`. Le reste du document porte une derive du meme age, laissee.
This commit is contained in:
@@ -47,6 +47,7 @@ This repo is the **provider** of `polyfill-surface`; consuming applications live
|
|||||||
- `knowledge_what-an-app-deletes-at-migration` — the two fates a published symbol can have.
|
- `knowledge_what-an-app-deletes-at-migration` — the two fates a published symbol can have.
|
||||||
- `knowledge_what-an-iri-says-upstream` — the same two fates for a written *triple*: NextGraph's own vocabulary, ours, and which of our IRIs must outlive the migration.
|
- `knowledge_what-an-iri-says-upstream` — the same two fates for a written *triple*: NextGraph's own vocabulary, ours, and which of our IRIs must outlive the migration.
|
||||||
- `knowledge_what-a-read-scope-is-upstream` — what the target offers for reading many objects at once, and the one question it cannot express.
|
- `knowledge_what-a-read-scope-is-upstream` — what the target offers for reading many objects at once, and the one question it cannot express.
|
||||||
|
- `knowledge_what-a-failed-read-answers-upstream` - the other half of the same question: what an unreachable, an unsynced and an empty document each answer, and which of them a caller can tell apart.
|
||||||
- `knowledge_what-the-orm-writes-and-deletes-upstream` — the writing half: removal is a set mutation wired end to end, and an ORM write never passes through `sparql_update`.
|
- `knowledge_what-the-orm-writes-and-deletes-upstream` — the writing half: removal is a set mutation wired end to end, and an ORM write never passes through `sparql_update`.
|
||||||
- `knowledge_what-an-inbox-is-upstream` — what the target actually offers around inboxes, and where a choice of ours begins.
|
- `knowledge_what-an-inbox-is-upstream` — what the target actually offers around inboxes, and where a choice of ours begins.
|
||||||
- `knowledge_who-processes-an-inbox` — the division of labour along a deposit's path: what this package applies, what it merely delivers, and what a layer above owes itself.
|
- `knowledge_who-processes-an-inbox` — the division of labour along a deposit's path: what this package applies, what it merely delivers, and what a layer above owes itself.
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
---
|
||||||
|
type: knowledge
|
||||||
|
summary: What the target answers for an unreachable, an unsynced and an empty document - a read rejects or it does not, so "not known to me" and "does not exist" are one answer while "unsynced" and "genuinely empty" are another
|
||||||
|
last_checked: 2026-09-21
|
||||||
|
---
|
||||||
|
|
||||||
|
# What a failed read answers upstream, and what a caller can tell apart
|
||||||
|
|
||||||
|
The question a caller actually asks: **when a read comes back with nothing, what can I conclude?** This leaf records the target's answer, because every version of the failure-as-absence defect in this package has come from getting it wrong in one direction or the other - either swallowing a breakdown into an empty result, or turning a real emptiness into an error.
|
||||||
|
|
||||||
|
## The engine's observable: a read REJECTS, or it does not
|
||||||
|
|
||||||
|
An anchored SPARQL query resolves its target through `resolve_target_for_sparql`, whose first act is `self.repos.get(repo_id).ok_or(NgError::RepoNotFound)?` (`engine/verifier/src/request_processor.rs:269`, at `213338f6`). A repo the verifier does not hold is an **error**, not an empty result. It crosses into JavaScript as a **rejected promise carrying the string `RepoNotFound`** (`sdk/js/lib-wasm/src/lib.rs:403` for nodejs, `:606` for the browser).
|
||||||
|
|
||||||
|
So the target offers exactly **two** observable outcomes for a read, and no third: it rejects, or it resolves with rows - possibly zero of them. There is no "partially answered", no "could not find out but here is what I have", and no status object. Any richer vocabulary in this package would be something a caller learns here and unlearns at migration.
|
||||||
|
|
||||||
|
## The three states a caller wants to distinguish, and the two it gets
|
||||||
|
|
||||||
|
| the document is... | what the engine does | what a caller can conclude |
|
||||||
|
|---|---|---|
|
||||||
|
| **not in `self.repos`** - never existed, or exists but this session does not know it | REJECTS with `RepoNotFound` | "I could not read it." **Not** *why*. |
|
||||||
|
| **in `self.repos` but unsynced** - a persistent profile reloads every known repo at `Verifier::load` (`engine/verifier/src/verifier.rs:535-560`), so a fresh session over the same wallet has the repo without its remote commits | resolves, **zero rows** | nothing distinguishes this from the row below |
|
||||||
|
| **in `self.repos`, synced, and genuinely empty** | resolves, **zero rows** | nothing distinguishes this from the row above |
|
||||||
|
|
||||||
|
Two collapses, and they run in opposite directions. Above the line, **"does not exist" and "exists but I do not know it" are indistinguishable** - the partition the engine exposes is membership of `self.repos`, and nothing finer. Below it, **"known but unsynced" collides with "genuinely empty"** - both are a legitimate zero-row answer, and the error channel is not involved at all.
|
||||||
|
|
||||||
|
That second collapse is the one a caller gets bitten by, because zero rows looks like an answer. The only thing that separates the two is a **sync barrier**: `doc_subscribe` pushes `TabInfo` and then the first `State`, and past that first `State` presence is guaranteed and absence definitive (pinned empirically by CONTRACT 3 in `packages/polyfill/e2e/`). Crossing it is what turns "zero rows" into a statement.
|
||||||
|
|
||||||
|
## There is no capability check on the read path at all
|
||||||
|
|
||||||
|
`resolve_target_for_sparql` reads `nuri.target` and nothing else. Upstream a document you cannot read is one whose `ReadCap` does not open it - the refusal is cryptographic and happens further down, never as a "may I?" question answered on the way in. So **a read never fails for want of a right granted to you**; it fails because the repo is not there, or it succeeds.
|
||||||
|
|
||||||
|
This is why this package's `fetchReadCap` (the emulated *"downloaded from the outer overlay"*, `PublicRepoLinkV0`) has **no upstream counterpart to mirror**: there is no signature at any level to copy, and the alignment falls to the engine's model instead. That is the reasoning behind `1.0.0-dev.9`: the emulation's own step was collapsing "the store could not be asked" into "the document exposes no key", so the honest projection of a failed emulation step is the one outcome the engine has for a read that cannot happen - a rejection.
|
||||||
|
|
||||||
|
## What this package does with it, and the line it must not cross
|
||||||
|
|
||||||
|
Three rules follow, and all three are load-bearing:
|
||||||
|
|
||||||
|
1. **A breakdown rejects.** Anything that stops a read from happening - a repo that will not open, a broker that does not answer, an emulation step that could not be performed - reaches the caller as a failure. `A rejection means "unknown", never "absent"` is the contract's own wording.
|
||||||
|
2. **An empty document is a SUCCESS holding zero objects.** The repair for (1) must never be bought by making a real emptiness into an error; that only moves the lie. An empty document, an empty scope, a document nobody opened an inbox on, a document in a public store that exposes no key - each is an answer, and each stays one.
|
||||||
|
3. **The unsynced/empty collision is closed by the barrier, not by an error.** Where this package can cross the sync barrier before answering, it does (`emulated-verifier/open-repo.ts`), which is what makes a zero-row answer mean something. Where it cannot, the answer is still a success, and `getSyncState` is what says whether it was authoritative. Turning an unsynced read into a rejection would be inventing a third outcome the engine does not have.
|
||||||
|
|
||||||
|
Related: `knowledge_what-a-read-scope-is-upstream` (what a read request can SAY, including the `[]` that means the empty set and the `[]` that means no filtering), and `polyfill-surface/contract_polyfill-surface` `## Guarantees` for the caller-facing form of all of this.
|
||||||
@@ -161,6 +161,8 @@ Only a document's owner writes to it. Holding its read key never grants a write.
|
|||||||
|
|
||||||
**`insertObject(shapeType, object)` needs the object's `@graph`**, and refuses a document you do not own. The `@graph` is the only thing that says which document the write lands in - upstream the insert subscribes to no document at all - and it is also what decides the refusal, so there is nothing extra to pass. An object with no `@graph` is forwarded and NextGraph refuses it in its own words.
|
**`insertObject(shapeType, object)` needs the object's `@graph`**, and refuses a document you do not own. The `@graph` is the only thing that says which document the write lands in - upstream the insert subscribes to no document at all - and it is also what decides the refusal, so there is nothing extra to pass. An object with no `@graph` is forwarded and NextGraph refuses it in its own words.
|
||||||
|
|
||||||
|
**"This document has no inbox" is an answer, and only an answer, since `1.0.0-dev.9`.** `inbox.postToDocument` refuses a document whose owner never opened an inbox, and `inbox.readForDocument` / `readSyncedForDocument` answer `[]` for one - both unchanged, both still the ordinary case. What changed is that a read which could not HAPPEN no longer arrives as one of those: a broker that cannot answer for the document, or a public store that cannot be asked for its key, now rejects. Until this version a depositor was told the owner had opened nothing, and a reader was handed an empty list, for a document whose inbox is full.
|
||||||
|
|
||||||
`inbox.share(doc, toUser)` names the document and the person; the recipient calls nothing. It refuses a recipient nobody has signed in as, rather than creating them.
|
`inbox.share(doc, toUser)` names the document and the person; the recipient calls nothing. It refuses a recipient nobody has signed in as, rather than creating them.
|
||||||
|
|
||||||
`inbox.post` refuses a target that is not an inbox; to reach a document's owner, use `inbox.postToDocument(doc, …)`. Anyone may deposit into an inbox; only its owner reads it.
|
`inbox.post` refuses a target that is not an inbox; to reach a document's owner, use `inbox.postToDocument(doc, …)`. Anyone may deposit into an inbox; only its owner reads it.
|
||||||
@@ -191,6 +193,8 @@ Where a call must first find out whether something already exists — a document
|
|||||||
|
|
||||||
**A reactive read says "nothing" and "I could not find out" differently.** `useShapeQuery` answers in three states and only two of them are answers about your data: `isPending` while the question is still open, `isSuccess` once it has been answered, `isError` when it could not be. An empty `data` under `isSuccess` means this scope holds nothing of that shape — the distinction the surface exists for, and the one thing it will never do is publish that snapshot for a read that did not happen. And because an observable cannot take back a list a subscriber has already rendered, `data` under `isError` keeps the **last read that answered** rather than emptying — so an empty `data` is never handed to you as a failure's answer. Read the load state before `data`: **a rejection means "unknown", never "absent"** here too.
|
**A reactive read says "nothing" and "I could not find out" differently.** `useShapeQuery` answers in three states and only two of them are answers about your data: `isPending` while the question is still open, `isSuccess` once it has been answered, `isError` when it could not be. An empty `data` under `isSuccess` means this scope holds nothing of that shape — the distinction the surface exists for, and the one thing it will never do is publish that snapshot for a read that did not happen. And because an observable cannot take back a list a subscriber has already rendered, `data` under `isError` keeps the **last read that answered** rather than emptying — so an empty `data` is never handed to you as a failure's answer. Read the load state before `data`: **a rejection means "unknown", never "absent"** here too.
|
||||||
|
|
||||||
|
**A document you NAME in `graphs` is reached before the read opens, since `1.0.0-dev.9`.** Naming a document in a scope is the same act as anchoring `docs.sparqlQuery` on it, and it is now answered the same way: the key is asked of a public store if you do not already hold it, the document is refused out loud if you hold no key for it, and its repo is synced before the question is put. A named document you cannot read publishes `isError`, where until this version it reached the engine, came back, and was emptied by the per-document filter into `isSuccess` with `data: []` - the one snapshot this surface reserves for "this scope holds nothing". A named document that has never synced published that same snapshot for a document that is not empty; it too is now `isError` or a longer `isPending`, never a false emptiness. **Naming SEVERAL documents fails as a whole** when one of them cannot be reached: a short list is the same lie as an empty one, so nothing is answered rather than half of it. What is NOT touched, and each stays a success: `graphs: "did:ng:i"` or `""` (you named no document - the per-document filter is the whole of the answer there, and an empty result means you hold nothing of that shape), `graphs: []` (no objects, by NextGraph's own definition), and a named document you can read that holds nothing of the shape. The practical cost is that this read now spends round-trips a named scope did not spend before, once per document per session.
|
||||||
|
|
||||||
**Which failure lands in `isError` changed in `1.0.0-dev.6`, and one of them now stays PENDING.** The read goes through the SDK's own shape query, so what `isError` carries is the query failing to open: a shape the ORM cannot subscribe, or a `configure` that was never given `ormSubscription`. A start the underlying engine REFUSES does not reach it: NextGraph catches that failure and logs it, and its readiness signal is only ever resolved, never rejected, so such a read stays `isPending` indefinitely. The guarantee above is unaffected — you are still never told "there is nothing" for "nobody answered" — but an interface that handles only `isError` will show its loading state where it used to show an error. Handle a `isPending` that does not end.
|
**Which failure lands in `isError` changed in `1.0.0-dev.6`, and one of them now stays PENDING.** The read goes through the SDK's own shape query, so what `isError` carries is the query failing to open: a shape the ORM cannot subscribe, or a `configure` that was never given `ormSubscription`. A start the underlying engine REFUSES does not reach it: NextGraph catches that failure and logs it, and its readiness signal is only ever resolved, never rejected, so such a read stays `isPending` indefinitely. The guarantee above is unaffected — you are still never told "there is nothing" for "nobody answered" — but an interface that handles only `isError` will show its loading state where it used to show an error. Handle a `isPending` that does not end.
|
||||||
|
|
||||||
**A read is scoped by document and by subject, never by store.** `Scope` has two axes and there is no third: `graphs` names one document's graph per entry — `did:ng:i` or `""` for the whole local dataset, `[]` or `undefined` for no objects at all — and `subjects` filters subject IRIs literally, with no prefix or hierarchy matching. There is no call that lists the documents in a store, and naming a store in `graphs` is not the question you meant: a store's graph holds its containment triples and nothing of the shape you asked for, so it answers empty rather than raising. That is the target's own shape, not a narrowing this package chose — and since `1.0.0-dev.6` nothing is translated on the way: the `Scope` object you write is the object the SDK receives.
|
**A read is scoped by document and by subject, never by store.** `Scope` has two axes and there is no third: `graphs` names one document's graph per entry — `did:ng:i` or `""` for the whole local dataset, `[]` or `undefined` for no objects at all — and `subjects` filters subject IRIs literally, with no prefix or hierarchy matching. There is no call that lists the documents in a store, and naming a store in `graphs` is not the question you meant: a store's graph holds its containment triples and nothing of the shape you asked for, so it answers empty rather than raising. That is the target's own shape, not a narrowing this package chose — and since `1.0.0-dev.6` nothing is translated on the way: the `Scope` object you write is the object the SDK receives.
|
||||||
@@ -219,7 +223,7 @@ Where a call must first find out whether something already exists — a document
|
|||||||
|
|
||||||
**The wrapped `ng` is a wrapper, and a wrapper can be wrong.** It forwards all 88 members to the object you gave `configure`, adding two refusals of its own and nothing else. Until `1.0.0-dev.7` it did not forward them at all - every member came back as a promise, and calling one failed with NextGraph's own `<member> is not a function`. The lesson to keep is that a member of `ng` that misbehaves is worth reporting here rather than worked around: nothing in this package's own code goes through that wrapper, so this surface is the only place such a defect shows.
|
**The wrapped `ng` is a wrapper, and a wrapper can be wrong.** It forwards all 88 members to the object you gave `configure`, adding two refusals of its own and nothing else. Until `1.0.0-dev.7` it did not forward them at all - every member came back as a promise, and calling one failed with NextGraph's own `<member> is not a function`. The lesson to keep is that a member of `ng` that misbehaves is worth reporting here rather than worked around: nothing in this package's own code goes through that wrapper, so this surface is the only place such a defect shows.
|
||||||
|
|
||||||
**A shape query the engine refuses never finishes loading.** Both shape reads — `useShape` and `useShapeQuery` — open a subscription through NextGraph's ORM, and a start the engine refuses is caught there, written to the console and nothing more: the set stays empty, its readiness never settles, and the only trace is that one console line. `useShapeQuery` reports it as a `isPending` that does not end (see the guarantee above); `useShape` returns a set you cannot tell from an empty one. The cause is upstream and nothing in this package can work around it. This was a total outage until the client packages caught up with the deployed broker on 2026-09-21; it is the residual failure mode, not the ordinary case.
|
**A shape query the engine refuses never finishes loading.** Both shape reads — `useShape` and `useShapeQuery` — open a subscription through NextGraph's ORM, and a start the engine refuses is caught there, written to the console and nothing more: the set stays empty, its readiness never settles, and the only trace is that one console line. `useShapeQuery` reports it as a `isPending` that does not end (see the guarantee above); `useShape` returns a set you cannot tell from an empty one. The cause is upstream and nothing in this package can work around it. This was a total outage until the client packages caught up with the deployed broker on 2026-09-21; it is the residual failure mode, not the ordinary case. Since `1.0.0-dev.9` the commonest way in is closed for `useShapeQuery`: a document NAMED in `graphs` is opened and refused before the query is handed over, so an unreachable named document reads `isError` rather than pending for ever. What remains is the start the engine refuses for a document you can perfectly well reach, and everything under `graphs: "did:ng:i"`, where there was no name to check.
|
||||||
|
|
||||||
**No object removal call, and no document deletion at all.** There is no `deleteObject` and no `removeDocument` at any level of NextGraph, so there is none here. You remove an object by deleting it from the LIVE set a shape query handed you - the item the set is holding, reached by iterating it, by `.first()` or by `.getBy(graph, subject)`. What that removes is the predicates your SHAPE declares on that subject: triples under a predicate the shape does not name survive, the subject survives, and the document survives. Deleting an item of a `getObjects` result removes nothing, because that result is a clone. `@ng-org/orm@0.1.2-alpha.21` ships a `removeObject(graphNuri, subjectIri)`; it is not published here, because it exists in the package and not in the source this package is verified against, and a symbol whose only evidence is what an npm build happens to export is not something to build a guarantee on.
|
**No object removal call, and no document deletion at all.** There is no `deleteObject` and no `removeDocument` at any level of NextGraph, so there is none here. You remove an object by deleting it from the LIVE set a shape query handed you - the item the set is holding, reached by iterating it, by `.first()` or by `.getBy(graph, subject)`. What that removes is the predicates your SHAPE declares on that subject: triples under a predicate the shape does not name survive, the subject survives, and the document survives. Deleting an item of a `getObjects` result removes nothing, because that result is a clone. `@ng-org/orm@0.1.2-alpha.21` ships a `removeObject(graphNuri, subjectIri)`; it is not published here, because it exists in the package and not in the source this package is verified against, and a symbol whose only evidence is what an npm build happens to export is not something to build a guarantee on.
|
||||||
|
|
||||||
@@ -245,7 +249,7 @@ Where a call must first find out whether something already exists — a document
|
|||||||
|
|
||||||
**`getObjects` does not fetch a public store's key for you either**, for the same reason as the two shape reads below: it goes through the same ORM query and spends no round-trip asking. Read the document once by naming it — `docs.sparqlQuery` anchored on it — and it appears.
|
**`getObjects` does not fetch a public store's key for you either**, for the same reason as the two shape reads below: it goes through the same ORM query and spends no round-trip asking. Read the document once by naming it — `docs.sparqlQuery` anchored on it — and it appears.
|
||||||
|
|
||||||
**Neither shape read fetches a public store's key for you.** A document in a public store gives up its key to whoever asks, but asking is a round-trip and neither `useShape` nor `useShapeQuery` spends one — since `1.0.0-dev.6` the second reads through the SDK and no longer opens a document of its own. So a public document you have read nowhere else does not appear in either. Read it once by naming it — `docs.sparqlQuery` anchored on it, which is the whole of what `readUnion` did for this before it was removed in `1.0.0-dev.8` — and both pick it up: the key lands in what you hold, and that is a change both of them re-read on.
|
**`useShape` does not fetch a public store's key for you, and `useShapeQuery` does so only for what you NAME.** A document in a public store gives up its key to whoever asks, but asking is a round-trip: `useShape`'s signature is NextGraph's own and has no await to spend, so a public document you have read nowhere else never appears in it. `useShapeQuery` spent none either until `1.0.0-dev.9`; it now asks once per document named in `graphs`, which is what lets a bare reference to a public document be read through that call. It still asks nothing under `graphs: "did:ng:i"` - the whole local dataset names no document, so there is nobody to ask about. For those two cases, read the document once by naming it - `docs.sparqlQuery` anchored on it, which is the whole of what `readUnion` did for this before it was removed in `1.0.0-dev.8` - and both reads pick it up: the key lands in what you hold, and that is a change both of them re-read on.
|
||||||
|
|
||||||
**A watch on one inbox that could not be opened does not come back on its own.** Opening it can fail — a broker that does not answer in that moment — and the failure is reported rather than passed over, but what follows is event-driven and this package deliberately never polls. The watch is opened again at the next moment this identity comes to hold something it did not: it creates a document, it opens an inbox on a document, a `ReadCap` reaches it through an inbox still being watched, it reads a public-store document for the first time. A session that does none of those goes on without that inbox — deposits made into it are not applied, nothing raises, and they wait unconsumed for the next connection. The exposed case is the identity that only ever READS, since it produces none of those events, where an identity that goes on creating recovers as a by-product of its own work. Awaiting `ensureIdentity()` again applies what is waiting — it drains every one of this identity's queues before it resolves, and shows no barrier a second time — but the watching itself comes back only on a fresh page.
|
**A watch on one inbox that could not be opened does not come back on its own.** Opening it can fail — a broker that does not answer in that moment — and the failure is reported rather than passed over, but what follows is event-driven and this package deliberately never polls. The watch is opened again at the next moment this identity comes to hold something it did not: it creates a document, it opens an inbox on a document, a `ReadCap` reaches it through an inbox still being watched, it reads a public-store document for the first time. A session that does none of those goes on without that inbox — deposits made into it are not applied, nothing raises, and they wait unconsumed for the next connection. The exposed case is the identity that only ever READS, since it produces none of those events, where an identity that goes on creating recovers as a by-product of its own work. Awaiting `ensureIdentity()` again applies what is waiting — it drains every one of this identity's queues before it resolves, and shows no barrier a second time — but the watching itself comes back only on a fresh page.
|
||||||
|
|
||||||
@@ -259,11 +263,19 @@ What each level means here, in this package's own terms:
|
|||||||
- **minor** — a symbol is added and nothing existing moves (`inbox.readSyncedForDocument` arrived this way).
|
- **minor** — a symbol is added and nothing existing moves (`inbox.readSyncedForDocument` arrived this way).
|
||||||
- **patch** — a fix that changes neither the published surface nor anything above under `## Guarantees`.
|
- **patch** — a fix that changes neither the published surface nor anything above under `## Guarantees`.
|
||||||
|
|
||||||
**A tag says where it comes from.** A release cut on `main` carries a **full version** (`1.0.0`), and the three rules above govern what changes between two full versions. Work still on a branch carries a **pre-release** of the version it is heading for (`1.0.0-dev.8`), which sorts *below* that version by construction - so you can pin what exists today while the tag itself tells you the surface has not been released and may still move before it is. Between two pre-releases of the same version nothing is promised: re-pull and read this leaf again. When the branch lands, the full version appears alongside; the pre-release keeps resolving, so no reference you pinned is ever withdrawn from under you.
|
**A tag says where it comes from.** A release cut on `main` carries a **full version** (`1.0.0`), and the three rules above govern what changes between two full versions. Work still on a branch carries a **pre-release** of the version it is heading for (`1.0.0-dev.9`), which sorts *below* that version by construction - so you can pin what exists today while the tag itself tells you the surface has not been released and may still move before it is. Between two pre-releases of the same version nothing is promised: re-pull and read this leaf again. When the branch lands, the full version appears alongside; the pre-release keeps resolving, so no reference you pinned is ever withdrawn from under you.
|
||||||
|
|
||||||
**Tags carry the package name**, because this repository publishes more than one engagement and their versions move independently: `polyfill/v1.0.0-dev.8` is this package, `ng-e2e-helpers/v...` is the other one. A bare `v...` tag would say nothing about which surface it froze the day the two diverge - which is the day one of them takes a major and the other does not.
|
**Tags carry the package name**, because this repository publishes more than one engagement and their versions move independently: `polyfill/v1.0.0-dev.9` is this package, `ng-e2e-helpers/v...` is the other one. A bare `v...` tag would say nothing about which surface it froze the day the two diverge - which is the day one of them takes a major and the other does not.
|
||||||
|
|
||||||
`1.0.0` is a baseline, not a claim of maturity: it is the number that makes your pin mean something. Nothing was released before it, so none of the changes named above is a bump from anything - but the next release very likely is a major. What exists today is `1.0.0-dev.8`, on a branch: pin that string exactly, and anchor your `usage_` leaf's `against:` on it - `against: @ng-eventually/polyfill@1.0.0-dev.8`, the string you pinned, never the version it is heading for.
|
`1.0.0` is a baseline, not a claim of maturity: it is the number that makes your pin mean something. Nothing was released before it, so none of the changes named above is a bump from anything - but the next release very likely is a major. What exists today is `1.0.0-dev.9`, on a branch: pin that string exactly, and anchor your `usage_` leaf's `against:` on it - `against: @ng-eventually/polyfill@1.0.0-dev.9`, the string you pinned, never the version it is heading for.
|
||||||
|
|
||||||
|
`1.0.0-dev.9` is a **major**, and all of it is one rule: **a read that could not reach its document now FAILS, where it used to answer as though the document were empty.** Nothing is added, nothing is renamed, nothing is removed from the entry - three existing calls narrow.
|
||||||
|
|
||||||
|
- **`useShapeQuery` publishes `isError` for a document you NAME and cannot read.** Until this version a document named in `scope.graphs` was handed straight to the engine; whatever came back was filtered per document by what you hold, and a document you hold no key for was emptied out of the result. The snapshot was `isSuccess` with `data: []` - indistinguishable, byte for byte, from the scope genuinely holding nothing. It now asks the public store for the key, refuses out loud if there is none to be had, and crosses the sync barrier first, so the same read answers `isError`. **Handle `isError` on a named scope**; an interface that only branched on `isPending` / `isSuccess` will show its success state with nothing in it. Naming several documents fails as a whole when one of them fails.
|
||||||
|
- **A named scope now costs round-trips it did not cost before**, once per named document per session: the key fetch, and the subscribe that syncs the repo. `graphs: "did:ng:i"` is unchanged and spends nothing.
|
||||||
|
- **`inbox.postToDocument`, `inbox.readForDocument` and `inbox.readSyncedForDocument` reject where they used to report "no inbox".** Resolving a document's inbox address reads that document, and when the read could not happen - the broker could not answer for it, or its public store could not be asked for the key - all three used to render the breakdown as the document's owner having opened nothing: a refusal on the deposit side, an empty list on the read side. They now reject. A document that genuinely has no inbox is unchanged, and still answers exactly as before.
|
||||||
|
- **Nothing that was already an ANSWER moves.** An empty document is a success holding zero objects, `graphs: []` is a success holding none, a document in a public store that exposes no key is a plain "no", and a document with no inbox answers `[]`. The whole change is that a *failure* stopped borrowing those shapes.
|
||||||
|
- **Why this direction, and not a richer set of outcomes.** The step that collapsed the two was this package's own emulation of a public store serving its key; NextGraph has no counterpart for it, because upstream nothing consults a capability on the read path at all. So there is no signature to mirror, and the alignment falls to the engine's own observable: an anchored query on a repo the verifier does not hold answers `NgError::RepoNotFound`, a rejected promise - **a read rejects, or it does not**. Inventing a third outcome for a caller to branch on would have been a vocabulary the target will never grow.
|
||||||
|
|
||||||
`1.0.0-dev.8` is a **major**, and it is one removal: **`readUnion` and `UnionSubject` are gone from this surface, and nothing replaces them.**
|
`1.0.0-dev.8` is a **major**, and it is one removal: **`readUnion` and `UnionSubject` are gone from this surface, and nothing replaces them.**
|
||||||
|
|
||||||
|
|||||||
@@ -259,7 +259,7 @@ export function useShapeQuery<T extends BaseType = BaseType>(
|
|||||||
|
|
||||||
**What it reads THROUGH, since `1.0.0-dev.6`: the SDK's own shape query.** `RdfOrmSubscription.getOrCreate(shapeType, scope)`, injected at `configure` as `ormSubscription` (§ 1). Four steps went with the change and they were one mechanism: resolving the logical scope to a document set (`listMyEntityDocs`), opening those documents through `ensureReposOpen` for a sync barrier, an anchored read over each, and filtering the result by the shape's `@type` IRI. All four existed because the parameter had to be TRANSLATED; once it is upstream's own `Scope`, the question goes to the thing that answers it, and `graphs` / `subjects` / `did:ng:i` are read by the engine literally. It does NOT delegate to the SDK's `useShape` — that is React's hook and its `DeepSignalSet` cannot carry the request state, which is the whole reason this symbol exists. What is taken from the SDK is the QUERY; the observable, the snapshot and the state tracking stay here, and so does the capability filter (`emulated-verifier/read-filter.ts`, applied per `@graph` on what comes back, exactly where § 4 applies it).
|
**What it reads THROUGH, since `1.0.0-dev.6`: the SDK's own shape query.** `RdfOrmSubscription.getOrCreate(shapeType, scope)`, injected at `configure` as `ormSubscription` (§ 1). Four steps went with the change and they were one mechanism: resolving the logical scope to a document set (`listMyEntityDocs`), opening those documents through `ensureReposOpen` for a sync barrier, an anchored read over each, and filtering the result by the shape's `@type` IRI. All four existed because the parameter had to be TRANSLATED; once it is upstream's own `Scope`, the question goes to the thing that answers it, and `graphs` / `subjects` / `did:ng:i` are read by the engine literally. It does NOT delegate to the SDK's `useShape` — that is React's hook and its `DeepSignalSet` cannot carry the request state, which is the whole reason this symbol exists. What is taken from the SDK is the QUERY; the observable, the snapshot and the state tracking stay here, and so does the capability filter (`emulated-verifier/read-filter.ts`, applied per `@graph` on what comes back, exactly where § 4 applies it).
|
||||||
|
|
||||||
**Consequences a caller sees.** `data` items are now the SHAPE's own objects — `@id`, `@graph` and the shape's readable predicates — where they were the property bags the then-published `readUnion` returned; `shapeType` is typed `ShapeType<T>` instead of `unknown`, so `T` follows the shape and the snapshot is typed without a cast; and the read no longer opens a document of its own, so a public-store document whose cap has not been fetched by some other read does not appear until it has been (the same limit § 4 carries, and the same `CapRegistry.onChange` recovery).
|
**Consequences a caller sees.** `data` items are now the SHAPE's own objects — `@id`, `@graph` and the shape's readable predicates — where they were the property bags the then-published `readUnion` returned; `shapeType` is typed `ShapeType<T>` instead of `unknown`, so `T` follows the shape and the snapshot is typed without a cast; and the read no longer opens a document of its own, so a public-store document whose cap has not been fetched by some other read does not appear until it has been (the same limit § 4 carries, and the same `CapRegistry.onChange` recovery). *(Corrected 2026-09-21, `1.0.0-dev.9`: that last clause now holds only for `graphs: "did:ng:i"`. A document NAMED in `scope.graphs` is put through the same public-store fetch and sync barrier `docs.sparqlQuery` applies to its anchor, before the query opens - and is refused out loud rather than silently emptied when its cap cannot be obtained.)*
|
||||||
|
|
||||||
**The name and the scope, since `1.0.0-dev.5`.** It was `watchShape(shapeType, "public" | "protected" | "private")`. `watchShape` has ZERO occurrences in `nextgraph-rs` and spends `watch`, a verb the ORM already spends on signal observation; the target's own name is `useShapeQuery`, announced by NextGraph's developers and on record in `.project/rules/nextgraph/surface-mirrors-the-sdk.md`, which is what the symbol's `announced-not-written` annotation cites. The three literals were this package's own and named the scope of a READ, which the SDK answers on two axes instead: `graphs` (one document's graph per entry, `did:ng:i` or `""` for the whole dataset, `[]` or `undefined` for no objects at all) and `subjects`. Nothing is translated any more: the `Scope` object a caller writes is the object the SDK receives.
|
**The name and the scope, since `1.0.0-dev.5`.** It was `watchShape(shapeType, "public" | "protected" | "private")`. `watchShape` has ZERO occurrences in `nextgraph-rs` and spends `watch`, a verb the ORM already spends on signal observation; the target's own name is `useShapeQuery`, announced by NextGraph's developers and on record in `.project/rules/nextgraph/surface-mirrors-the-sdk.md`, which is what the symbol's `announced-not-written` annotation cites. The three literals were this package's own and named the scope of a READ, which the SDK answers on two axes instead: `graphs` (one document's graph per entry, `did:ng:i` or `""` for the whole dataset, `[]` or `undefined` for no objects at all) and `subjects`. Nothing is translated any more: the `Scope` object a caller writes is the object the SDK receives.
|
||||||
|
|
||||||
|
|||||||
+17
-11
@@ -95,27 +95,33 @@ export function subscribePhysicalDoc(nuri: Nuri, onChange: (r: DocChange, type:
|
|||||||
## 6. Bootstrap repo opening — `emulated-verifier/open-repo.ts`
|
## 6. Bootstrap repo opening — `emulated-verifier/open-repo.ts`
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
// open-repo.ts:75
|
// open-repo.ts:90
|
||||||
export type SyncState = "syncing" | "synced" | "timed-out";
|
export type SyncState = "syncing" | "synced" | "timed-out";
|
||||||
// open-repo.ts:104 — TEST-ONLY
|
// open-repo.ts:120 - TEST-ONLY
|
||||||
export function setOpenTimeoutForTests(ms: number): void;
|
export function setOpenTimeoutForTests(ms: number): void;
|
||||||
// open-repo.ts:110
|
// open-repo.ts:127
|
||||||
export function resetOpenedRepos(): void;
|
export function resetOpenedRepos(): void;
|
||||||
// open-repo.ts:135
|
// open-repo.ts:162
|
||||||
export function getSyncState(nuri: Nuri): SyncState | "unknown";
|
export function getSyncState(nuri: Nuri): SyncState | "unknown";
|
||||||
// open-repo.ts:167
|
// open-repo.ts:210
|
||||||
export async function ensureRepoOpen(nuri: Nuri): Promise<void>;
|
export async function ensureRepoOpen(nuri: Nuri, holderKey?: string): Promise<void>;
|
||||||
// open-repo.ts:184
|
// open-repo.ts:234 - the unguarded core, for ONE importer (`shared-wallet/physical.ts`)
|
||||||
export async function ensurePhysicalRepoOpen(nuri: Nuri): Promise<void>;
|
export async function openRepoUnguarded(nuri: Nuri): Promise<void>;
|
||||||
// open-repo.ts:259
|
|
||||||
export async function ensureReposOpen(nuris: Nuri[]): Promise<void>;
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
> Corrected 2026-09-21 (`1.0.0-dev.9`), for this module only. The listing above used to name
|
||||||
|
> an `ensurePhysicalRepoOpen` export that this module has never had - it lives in
|
||||||
|
> `shared-wallet/physical.ts:134`, the machinery's door - and an `ensureReposOpen(nuris)`
|
||||||
|
> batch that was deleted as dead code. It also missed `openRepoUnguarded` and
|
||||||
|
> `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; per-nuri `SyncState` keeps `synced` and `timed-out` apart.
|
||||||
|
|
||||||
- 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.
|
- 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.
|
- "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.
|
||||||
- **Defect — the header's mechanism claim is contradicted at the source (see Findings F2).** `open-repo.ts:10-12` says an anchored `sparql_query` on a repo absent from `self.repos` "silently returns 0 rows (never a `RepoNotFound`)". Verified upstream: absence from `self.repos` yields `Err(NgError::RepoNotFound)` (`request_processor.rs:264,269`), the ReadQuery arm converts it into `AppResponse::error` (`:1293-1296`), and the web binding REJECTS the JS promise with it (`sdk/js/lib-wasm/src/lib.rs:606`). The observed 0-rows-no-error behaviour has two candidate explanations that the source does support: a persistent verifier reloads every known repo into `self.repos` at `Verifier::load` (`engine/verifier/src/verifier.rs:535-560`) so the repo is present-but-unsynced (a genuine 0-row read), and/or the lib's own per-doc tolerance (`readUserStore`, and the `surface/read-model.ts` per-doc catch until `1.0.0-dev.8` removed it) converts a rejection into an empty result. The healed symptom is real and the fix correct; the stated mechanism is not established, and a maintainer reasoning from it would mispredict behaviour whenever the verifier is not persistent.
|
- **`ensureRepoOpen` THROWS when the public store could not be asked** (`1.0.0-dev.9`). It calls `emulated-verifier/public-store.ts`'s `fetchReadCap` first - a document in a public store hands its key to whoever asks, so the question has to be put before the boundary decides - and that call used to answer `false` for a download that had REJECTED, which is the same value a document exposing no key answers with. The open then declined silently and the anchored read that followed refused the reader for holding no key: a true statement, made out of an outage, and the wrong one to act on. The rejection now reaches the caller. The open itself still never throws (a `doc_subscribe` that never pushes is bounded by the fallback timeout), and `holderKey` skips the fetch entirely, so the processor's door is unaffected.
|
||||||
|
- **Defect, SINCE REPAIRED in the code — the header's mechanism claim was contradicted at the source (see Findings F2).** The header was corrected on 2026-08-03 and now states the mechanism below itself; what follows is kept as the record of what was wrong and how it was established. `open-repo.ts:10-12` says an anchored `sparql_query` on a repo absent from `self.repos` "silently returns 0 rows (never a `RepoNotFound`)". Verified upstream: absence from `self.repos` yields `Err(NgError::RepoNotFound)` (`request_processor.rs:264,269`), the ReadQuery arm converts it into `AppResponse::error` (`:1293-1296`), and the web binding REJECTS the JS promise with it (`sdk/js/lib-wasm/src/lib.rs:606`). The observed 0-rows-no-error behaviour has two candidate explanations that the source does support: a persistent verifier reloads every known repo into `self.repos` at `Verifier::load` (`engine/verifier/src/verifier.rs:535-560`) so the repo is present-but-unsynced (a genuine 0-row read), and/or the lib's own per-doc tolerance (`readUserStore`, and the `surface/read-model.ts` per-doc catch until `1.0.0-dev.8` removed it) converts a rejection into an empty result. The healed symptom is real and the fix correct; the stated mechanism is not established, and a maintainer reasoning from it would mispredict behaviour whenever the verifier is not persistent.
|
||||||
- `SyncState` and `getSyncState` are lib-invented vocabulary — **NO COUNTERPART** (upstream has no consumer-facing "sync state of a repo" API at any level; `OrmSubscription.readyPromise`, `sdk/js/orm/src/connector/GraphOrmSubscription.ts:260`, is the closest level-3 signal, per-subscription not per-repo).
|
- `SyncState` and `getSyncState` are lib-invented vocabulary — **NO COUNTERPART** (upstream has no consumer-facing "sync state of a repo" API at any level; `OrmSubscription.readyPromise`, `sdk/js/orm/src/connector/GraphOrmSubscription.ts:260`, is the closest level-3 signal, per-subscription not per-repo).
|
||||||
- At migration the whole module becomes "open the store by cap at bootstrap" (native) and is removed with the shim.
|
- At migration the whole module becomes "open the store by cap at bootstrap" (native) and is removed with the shim.
|
||||||
|
|
||||||
|
|||||||
@@ -364,7 +364,7 @@ The consequence for anything this library exposes: **a call either hands over th
|
|||||||
|
|
||||||
**And the property that makes the first act worth anything — a public store SERVES its documents' caps.** `PublicRepoLinkV0` (`engine/net/src/types.rs:5098-5124`) carries `repo`, `public_store` and `peers` and no `read_cap`, and its own comment says why: *"The latest ReadCap of the branch (or main branch) will be **downloaded from the outerOverlay**, if the peer brokers listed below allow it. […] This link is durable, because the public site are **served differently by brokers**."* The key is not something a sender hands over; it is something the network gives to whoever asks, because the broker pinned the outer overlay (`expose_outer`, `engine/broker/src/server_storage/core/overlay.rs:103-133`).
|
**And the property that makes the first act worth anything — a public store SERVES its documents' caps.** `PublicRepoLinkV0` (`engine/net/src/types.rs:5098-5124`) carries `repo`, `public_store` and `peers` and no `read_cap`, and its own comment says why: *"The latest ReadCap of the branch (or main branch) will be **downloaded from the outerOverlay**, if the peer brokers listed below allow it. […] This link is durable, because the public site are **served differently by brokers**."* The key is not something a sender hands over; it is something the network gives to whoever asks, because the broker pinned the outer overlay (`expose_outer`, `engine/broker/src/server_storage/core/overlay.rs:103-133`).
|
||||||
|
|
||||||
That is emulated, since 2026-08-06, in `emulated-verifier/public-store.ts` — and emulated **without touching the guard**. Possession remains the one criterion: a public document is readable not because `mayReach` makes an exception, but because its cap is *obtainable* — the library asks, files what it gets, and from there the ordinary path applies. Every read door asks first (`docs.sparqlQuery`, `ensureRepoOpen`, `documentInboxAddress`).
|
That is emulated, since 2026-08-06, in `emulated-verifier/public-store.ts` — and emulated **without touching the guard**. Possession remains the one criterion: a public document is readable not because `mayReach` makes an exception, but because its cap is *obtainable* — the library asks, files what it gets, and from there the ordinary path applies. Every read door asks first (`docs.sparqlQuery`, `ensureRepoOpen`, `documentInboxAddress`, and since `1.0.0-dev.9` `useShapeQuery` for every document its scope NAMES). A door that could not ASK says so: the fetch rejects rather than answering "this document exposes no cap", because a reader told the second would stop looking.
|
||||||
|
|
||||||
Where the emulation shows its seams, stated rather than hidden:
|
Where the emulation shows its seams, stated rather than hidden:
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@ng-eventually/polyfill",
|
"name": "@ng-eventually/polyfill",
|
||||||
"version": "1.0.0-dev.8",
|
"version": "1.0.0-dev.9",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "Polyfill of the NextGraph JS SDK over @ng-org/web + @ng-org/orm, with emulated capabilities and inbox. Drop-in; remove at migration.",
|
"description": "Polyfill of the NextGraph JS SDK over @ng-org/web + @ng-org/orm, with emulated capabilities and inbox. Drop-in; remove at migration.",
|
||||||
"main": "./src/index.ts",
|
"main": "./src/index.ts",
|
||||||
|
|||||||
@@ -304,12 +304,12 @@ export async function restoreOwnCaps(holderKey: string): Promise<void> {
|
|||||||
* only its owner opens one, and only on its own document.
|
* 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
|
* **`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
|
* 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
|
* 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
|
* 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:
|
* published calls turn this value into a statement an application acts on:
|
||||||
* `inbox.postToDocument` says "its owner never opened one", and `inbox.readForDocument`
|
* `inbox.postToDocument` says "its owner never opened one", and `inbox.readForDocument`
|
||||||
* says zero deposits — for a document whose inbox is full.
|
* 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
|
// @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> {
|
export async function documentInboxAddress(doc: Nuri): Promise<Nuri | undefined> {
|
||||||
@@ -341,7 +341,7 @@ export async function documentInboxAddress(doc: Nuri): Promise<Nuri | undefined>
|
|||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// **Propagates a failed read** (`1.0.0-dev.9`). This used to answer `undefined`, on
|
// **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
|
// 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
|
// 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,
|
// 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
|
// a broker that did not answer, a query that rejected. Answering `undefined` for that
|
||||||
|
|||||||
@@ -188,13 +188,13 @@ async function syncSession(): Promise<void> {
|
|||||||
* `"synced"`). Idempotent: a repo already opened (or in flight) is not re-opened.
|
* `"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.
|
* 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. The
|
* 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
|
* 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.
|
* 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
|
* **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
|
* 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
|
* 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.
|
* 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
|
* 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.
|
* today, because every one of them is about to read the document.
|
||||||
|
|||||||
@@ -182,7 +182,7 @@ export async function exposeReadCap(doc: Nuri, cap: ReadCap): Promise<void> {
|
|||||||
* fetching, and skipping it keeps the ordinary path free of physical reads.
|
* fetching, and skipping it keeps the ordinary path free of physical reads.
|
||||||
*
|
*
|
||||||
* **Two outcomes only, and a THIRD that is not an outcome.** `false` means the network
|
* **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
|
* 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.
|
* 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
|
* It used to answer `false` for that as well, which is the defect this function was the
|
||||||
@@ -233,7 +233,7 @@ export async function fetchReadCap(docLike: Nuri): Promise<boolean> {
|
|||||||
// The download REJECTED: the repo would not open, the broker did not answer, the read
|
// 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`
|
// 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
|
// 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
|
// 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.
|
// 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
|
// Wrapped so the reason survives the trip: the message a caller would otherwise meet
|
||||||
@@ -243,7 +243,7 @@ export async function fetchReadCap(docLike: Nuri): Promise<boolean> {
|
|||||||
// it and dropped the memo, so the next caller asks the network again.
|
// it and dropped the memo, so the next caller asks the network again.
|
||||||
throw new Error(
|
throw new Error(
|
||||||
"[ng-eventually] fetchReadCap: the public store could not be asked for this " +
|
"[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 " +
|
"document's read cap, so whether it may be read is UNKNOWN - this is not " +
|
||||||
`"the document exposes no cap". ${JSON.stringify(doc)}`,
|
`"the document exposes no cap". ${JSON.stringify(doc)}`,
|
||||||
{ cause: error },
|
{ cause: error },
|
||||||
);
|
);
|
||||||
@@ -292,8 +292,8 @@ async function downloadReadCap(doc: Nuri): Promise<ReadCap | null> {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// `fetchReadCaps(docs)` was REMOVED in `1.0.0-dev.9`. It was the batch form — a
|
// `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
|
// `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
|
// 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:
|
// 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
|
// "each fetch is independent" is exactly the skip-and-carry-on this line of work has
|
||||||
|
|||||||
@@ -18,14 +18,22 @@
|
|||||||
* in a PUBLIC store hands its cap to whoever asks (`public-store.ts`), but asking is a
|
* in a PUBLIC store hands its cap to whoever asks (`public-store.ts`), but asking is a
|
||||||
* round-trip - so this filter drops such a document until some read path has asked.
|
* round-trip - so this filter drops such a document until some read path has asked.
|
||||||
* The paths that DO ask are the ones this library still reads with (the anchored
|
* The paths that DO ask are the ones this library still reads with (the anchored
|
||||||
* document read,
|
* document read, `docs.sparqlQuery`, `ensureRepoOpen`, `documentInboxAddress`) and,
|
||||||
* `docs.sparqlQuery`, `ensureRepoOpen`, `documentInboxAddress`). The two SHAPE reads
|
* since `1.0.0-dev.9`, `useShapeQuery` for every document its scope NAMES - it crosses
|
||||||
* ask nothing: `useShape` because its signature is the real ORM's and has no await to
|
* the same barrier on each before the query opens, so a named public document is held
|
||||||
* spend, and `useShapeQuery` since `1.0.0-dev.6`, because it reads through the SDK's
|
* by the time this filter sees its objects.
|
||||||
* own shape query and no longer opens a document of its own. An application reaching a
|
*
|
||||||
* public document through either of them alone, having read it nowhere first, sees
|
* `useShape` still asks nothing - its signature is the real ORM's and has no await to
|
||||||
* nothing until it does - and a cap that lands later makes both re-publish, since each
|
* spend - and neither read asks anything under `did:ng:i`, where the scope names no
|
||||||
* follows `CapRegistry.onChange`. A polyfill-era limit, removed with the module.
|
* document to ask about and this filter is the whole of the answer. An application
|
||||||
|
* reaching a public document through one of those alone, having read it nowhere first,
|
||||||
|
* sees nothing until it does - and a cap that lands later makes both re-publish, since
|
||||||
|
* each follows `CapRegistry.onChange`. A polyfill-era limit, removed with the module.
|
||||||
|
*
|
||||||
|
* Note what the barrier does NOT make redundant here. Under `did:ng:i` this filter is
|
||||||
|
* the only thing standing between the caller and every identity's objects, and even on
|
||||||
|
* a named scope it keeps the answer honest per `@graph` for whatever else the engine
|
||||||
|
* returned. Refusing at the ASK and filtering the RESULT are two different questions.
|
||||||
*
|
*
|
||||||
* Note there is no `user` parameter anywhere below, and that is the point: reading
|
* Note there is no `user` parameter anywhere below, and that is the point: reading
|
||||||
* is key possession, so the only question askable is "do I hold this document's
|
* is key possession, so the only question askable is "do I hold this document's
|
||||||
|
|||||||
Reference in New Issue
Block a user