docs: dire partout que la lecture par lot n'existe plus
Le contrat publie et l'api-contract etaient traites avec le retrait lui-meme ; restaient les documents qui decrivaient readUnion comme la primitive de lecture du paquet, ou qui nommaient le module disparu. Ce que les lecteurs apprennent maintenant : lire un document, c'est docs.sparqlQuery ancre dessus, et lire plusieurs documents, c'est cet appel une fois par document -- avec la decision sur une lecture qui echoue ecrite au point d'appel. Deux consequences sont dites plutot que decouvertes : une lecture refusee leve au lieu de rendre une liste vide, et les sujets sous le prefixe reserve urn:ng-eventually: reviennent dans le resultat, a l'appelant de les ignorer -- sans aucun gain de droits, les deux registres qu'un document ordinaire porte etant l'adresse de sa boite et, pour un document public, la cle que son magasin sert deja a qui la demande. Les briefs dates ne sont pas touches : ils decrivent un etat de juillet, et le reecrire falsifierait ce qui avait ete decide alors.
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
|
||||
One entry point. Most of what it publishes has the same signature as the future SDK —
|
||||
`ng`, `useShape`, `useShapeQuery`, `getObjects`, `insertObject`, `docs`, `inbox`,
|
||||
`storeRegistry`, `readUnion` (+ types) -
|
||||
`storeRegistry` (+ types) -
|
||||
and is a drop-in for `@ng-org/web` / `@ng-org/orm`: as NextGraph matures it resolves to
|
||||
the real SDK (build alias removed) with no code change.
|
||||
|
||||
@@ -28,7 +28,7 @@ Per-symbol, with the target signature and an epistemic label on every claim:
|
||||
```ts
|
||||
import {
|
||||
// SDK-shaped — the real SDK replaces these in place.
|
||||
ensureIdentity, storeRegistry, inbox, readUnion, docs,
|
||||
ensureIdentity, storeRegistry, inbox, docs,
|
||||
// Polyfill-era — one call, and it is the whole of what goes away.
|
||||
configure,
|
||||
} from "@ng-eventually/polyfill";
|
||||
@@ -91,7 +91,7 @@ which the e2e suite drives.
|
||||
## How a document is reached — the three acts, and no others
|
||||
|
||||
```ts
|
||||
import { storeRegistry, inbox, readUnion } from "@ng-eventually/polyfill";
|
||||
import { storeRegistry, inbox, docs } from "@ng-eventually/polyfill";
|
||||
|
||||
// 1. CREATE — you hold its cap, with nothing to declare. No identity parameter: a
|
||||
// session belongs to one user, exactly as the target's own `doc_create` assumes.
|
||||
@@ -109,7 +109,11 @@ await inbox.share(doc, "bob");
|
||||
// it and opens nothing.
|
||||
const publicDoc = await storeRegistry.createEntityDoc("public");
|
||||
// …put `publicDoc` in a QR code, a message, another document. Nothing else to do.
|
||||
await readUnion([publicDoc]); // a stranger holding only this reads it
|
||||
// A stranger holding only that reference reads it with ONE anchored query — the anchor
|
||||
// scopes the read to that document's own graph, and the key is fetched from the public
|
||||
// store on the way in. `readUnion([publicDoc])` did this until `1.0.0-dev.8`; it was
|
||||
// this call in a loop, so the loop is yours now and the call is unchanged.
|
||||
await docs.sparqlQuery(sessionId, "SELECT ?s ?p ?o WHERE { ?s ?p ?o }", undefined, publicDoc);
|
||||
```
|
||||
|
||||
**The invariant behind all three: you never derive a cap from a bare reference.** You
|
||||
@@ -139,5 +143,6 @@ runtime checks stay regardless — a JavaScript caller never meets the compiler.
|
||||
|
||||
```ts
|
||||
const saved = localStorage.getItem("doc"); // string | null
|
||||
if (saved) await readUnion([saved]); // ✓ validated at the door
|
||||
// ✓ validated at the door — `anchor` is a `NuriLike`, so nothing to narrow here
|
||||
if (saved) await docs.sparqlQuery(sessionId, "SELECT ?s ?p ?o WHERE { ?s ?p ?o }", undefined, saved);
|
||||
```
|
||||
|
||||
@@ -173,14 +173,13 @@ In `@ng-eventually/polyfill` the one-shot read is exposed as:
|
||||
- **`docs.sparqlQuery(sid, query, base?, anchor?)`** — a raw anchored SPARQL query
|
||||
([`../src/surface/docs.ts`](../src/surface/docs.ts)). `anchor` = the document NURI to read; the
|
||||
anchor restricts the query to that one repo's graph.
|
||||
- **`readUnion(docs)`** — read a **bounded, by-need set** of document NURIs,
|
||||
each with its own anchored query, grouped per subject
|
||||
([`../src/surface/read-model.ts`](../src/surface/read-model.ts)). This is the polyfill's listing
|
||||
primitive (see [§ Current emulation status](#current-emulation-status) and
|
||||
[`read-model.md`](../../../docs/read-model.md)).
|
||||
`readUnion(docs)` sat above this until `1.0.0-dev.8` and is **gone**: it was this call
|
||||
once per document plus a batch policy nothing upstream answers. Reading a bounded set
|
||||
of documents is this call in a loop, and what one unreadable document does to the rest
|
||||
is decided at the call site (see [`read-model.md`](../../../docs/read-model.md)).
|
||||
|
||||
One-shot reads do not re-render on change. To stay live over a one-shot read you must
|
||||
re-run it on a change **signal** (e.g. re-call `readUnion` when a `doc_subscribe`
|
||||
re-run it on a change **signal** (e.g. re-run the anchored query when a `doc_subscribe`
|
||||
fires) — a manual assembly that exists only because of the emulation gap below; the
|
||||
finished contract is `useShape`.
|
||||
|
||||
@@ -303,11 +302,11 @@ from the reactive contract:
|
||||
`engine/verifier/src/request_processor.rs` `resolve_target` →
|
||||
`self.repos.get(...).ok_or(RepoNotFound)`; see
|
||||
[`nextgraph-current-state.md`](../../../docs/nextgraph-current-state.md) § *The ORM
|
||||
fan-out hang*). So the lib reads entity lists with **`readUnion`** — a
|
||||
bounded set of one-shot anchored `sparql_query`s
|
||||
([`read-model.md`](../../../docs/read-model.md)) — and reassembles reactivity by
|
||||
**re-querying on a change signal** (a lightweight `doc_subscribe` / single-store ORM
|
||||
used only as a *signal* source, then re-run `readUnion`). `useShape` remains valid
|
||||
fan-out hang*). So the lib reads entity lists as **one anchored `sparql_query` per
|
||||
document** ([`read-model.md`](../../../docs/read-model.md)) — and reassembles
|
||||
reactivity by **re-querying on a change signal** (a lightweight `doc_subscribe` /
|
||||
single-store ORM used only as a *signal* source, then re-run the reads). `useShape`
|
||||
remains valid
|
||||
for a **single already-opened document**; it is the per-entity **fan-out** that is
|
||||
unfit today.
|
||||
|
||||
@@ -334,7 +333,7 @@ from the reactive contract:
|
||||
`ng.doc_subscribe`), the subscription callback appears NOT to fire for its OWN
|
||||
local commit in the same session — so the reactive re-read chain
|
||||
([`../src/surface/use-shape-query.ts`](../src/surface/use-shape-query.ts) `useShapeQuery` → `reread` →
|
||||
[`../src/surface/read-model.ts`](../src/surface/read-model.ts) `readUnion`) never runs, and
|
||||
the anchored per-document read) never runs, and
|
||||
consumers keep the STALE value until the next connection delivers a fresh
|
||||
initial `State`. **Remote** commits DO push correctly (verified: cross-browser
|
||||
reactive update works). A code review verified the consumer wiring is correct,
|
||||
|
||||
@@ -232,7 +232,8 @@ export async function readStoreCaps(storeDoc: Nuri): Promise<ReadCap[]> {
|
||||
* store it was created in ({@link holdOwnCap}) — and until 2026-08-16 exactly one path
|
||||
* read it back: `listMyEntityDocs`. So a creator that reloaded the page and went straight
|
||||
* to the document it had just made, without listing that scope first, held nothing for it:
|
||||
* the read was refused, the batch read (`readUnion`) dropped it silently, and the write
|
||||
* the read was refused, the batch read of the day (`readUnion`, removed in `1.0.0-dev.8`)
|
||||
* dropped it silently, and the write
|
||||
* was refused. Its own document, denied to its own author, on the first refresh.
|
||||
*
|
||||
* Restoring what a user HOLDS is already what connecting means here — that is what the
|
||||
|
||||
@@ -288,7 +288,7 @@ async function applyLink(payload: unknown, ctx: Applying): Promise<boolean> {
|
||||
// reports the whole inbox, and everything not yet popped is preserved for the next
|
||||
// attempt. That is the rule stated beside {@link report}, and BOTH arms follow it — but
|
||||
// the reference arm only since its `unreadable` outcome was split out of `refused` on the
|
||||
// same day. It had been the opposite: `read-model.readDoc` swallows a failed read and
|
||||
// same day. It had been the opposite: the batch read of the day swallowed a failed read and
|
||||
// answers `[]`, so an outage arrived as an ordinary refusal and the drain walked the
|
||||
// whole queue destroying it. Correcting one arm and leaving the other is precisely how
|
||||
// that survived a round of review.
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
* nothing had to tell an absence from a failure. **That is no longer true, and it is the
|
||||
* sharpest consequence of consuming.** An absence is deterministic and costs its own deposit;
|
||||
* a silent broker is transient and must stop the drain, or one outage walks the queue and
|
||||
* destroys every reference in it. `read-model.readDoc` answers `[]` for both, so this module
|
||||
* destroys every reference in it. An anchored read answers `[]` for both, so this module
|
||||
* passes it an `onUnreadable` reporter and asks which one happened.
|
||||
*
|
||||
* What it no longer means, since deposits became consumed on 2026-08-26, is "and a later
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* open-repo — cold-start repo opening for the ANCHORED read path (polyfill-era).
|
||||
*
|
||||
* ── The cold-start defect this heals ──────────────────────────────────────
|
||||
* The anchored read path (`surface/read-model.ts` `readDoc`,
|
||||
* The anchored read path (`surface/read-document.ts` `readDocumentSubjects`,
|
||||
* `shared-wallet/account-registry.ts` `readUserStore`) assumes the target repo is
|
||||
* already usable by the verifier — true within the session that CREATED the doc
|
||||
* (every `doc_create` opens it), but FALSE on a FRESH session over the same
|
||||
@@ -19,9 +19,10 @@
|
||||
* the verifier: on a persistent profile `Verifier::load` repopulates `self.repos` from
|
||||
* user storage at construction (`engine/verifier/src/verifier.rs:535-560`), so the repo
|
||||
* is PRESENT but unsynced and the anchored query legitimately matches nothing; and this
|
||||
* library's own `readDoc` catches every error and returns `[]`
|
||||
* (`surface/read-model.ts:122`), so anything that did throw would reach the caller as
|
||||
* emptiness anyway. The fix below is right; the diagnosis written beside it was not.
|
||||
* library's own per-document read caught every error and returned `[]`
|
||||
* (`surface/read-model.ts`'s `readDoc`, until `1.0.0-dev.8` removed it along with the
|
||||
* batch it served), so anything that did throw reached the caller as emptiness anyway.
|
||||
* It no longer does: `surface/read-document.ts` lets the rejection through. The fix below is right; the diagnosis written beside it was not.
|
||||
*
|
||||
* The circularity that made this self-inflicted: `doc_subscribe` WOULD open the
|
||||
* repo, but the reactive layer only subscribes AFTER the listing produced NURIs —
|
||||
|
||||
@@ -165,7 +165,7 @@ export async function assertMayWrite(nuri: Nuri, op: string): Promise<void> {
|
||||
}
|
||||
|
||||
/**
|
||||
* **Rule 2 — do not even attempt**, at the CALLERS (`read-model`, `open-repo`,
|
||||
* **Rule 2 — do not even attempt**, at the CALLERS (`open-repo`,
|
||||
* `subscribe`'s callers…).
|
||||
*
|
||||
* A reader that does not hold a document's cap must not issue the operation at all.
|
||||
|
||||
@@ -17,7 +17,8 @@
|
||||
* It is SYNCHRONOUS and decides from what the holder holds at that instant. A document
|
||||
* 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.
|
||||
* The paths that DO ask are the ones this library still reads with (`readUnion`,
|
||||
* The paths that DO ask are the ones this library still reads with (the anchored
|
||||
* document read,
|
||||
* `docs.sparqlQuery`, `ensureRepoOpen`, `documentInboxAddress`). The two SHAPE reads
|
||||
* ask nothing: `useShape` because its signature is the real ORM's and has no await to
|
||||
* spend, and `useShapeQuery` since `1.0.0-dev.6`, because it reads through the SDK's
|
||||
|
||||
@@ -166,7 +166,7 @@ export const STORE_BRANCH_SUBJECT = `${SHIM}:storeBranch`;
|
||||
/**
|
||||
* Fixed subject of the **Header branch** emulation, inside an ENTITY document — the
|
||||
* first compartment we put in a document the consumer also reads, hence the filter in
|
||||
* `read-model.ts` (every `${SHIM}:` subject is machinery and never surfaces as data).
|
||||
* `read-document.ts` (every `${SHIM}:` subject is machinery, and the internal read drops it).
|
||||
*
|
||||
* It carries what must be readable by *whoever can read the document*, as opposed to
|
||||
* what belongs to its owner alone. Today that is one thing: the ADDRESS of the
|
||||
@@ -1165,7 +1165,7 @@ export async function createEntityDoc(id: string, scope: StoreType): Promise<Nur
|
||||
s.sessionId,
|
||||
// NO explicit `GRAPH <…>` wrapper: write the anchored DEFAULT graph (the
|
||||
// `indexDoc` anchor scopes it) — the CANONICAL, always-safe shape the anchored
|
||||
// default-graph read queries (readUserStore below, same as read-model.ts).
|
||||
// default-graph read queries (readUserStore below, same as read-document.ts).
|
||||
// entityNuri is a NURI stored as a literal → escapeLiteral.
|
||||
`INSERT DATA { <${MAIN_BRANCH_SUBJECT}> <${P.contains}> "${escapeLiteral(entityNuri)}" }`,
|
||||
indexDoc,
|
||||
|
||||
@@ -315,7 +315,7 @@ export function configureStoreRegistry(deps: StoreRegistryDeps): void {
|
||||
// Fire the outbox inspection (Volet 3 of the low-level data-path trace) once,
|
||||
// on the FIRST successful `getSession()` resolution — the most reliable
|
||||
// "a session is established" signal available: every low-level reader/writer
|
||||
// (store-registry, open-repo, read-model, subscribe, inbox) reaches its
|
||||
// (store-registry, open-repo, read-document, subscribe, inbox) reaches its
|
||||
// session through this SAME injected `getSession`, so wrapping it HERE catches
|
||||
// the first success from whichever caller happens to run first, instead of
|
||||
// tying the probe to one particular call site. Only on SUCCESS (an error
|
||||
@@ -424,7 +424,7 @@ export function getCurrentUser(): PrincipalId | null {
|
||||
}
|
||||
|
||||
/** The emulated cap registry — what the current identity holds, plus the emulated
|
||||
* public store. The read filter and the read-model consult it. */
|
||||
* public store. The read filter and the document read consult it. */
|
||||
export function getCaps(): CapRegistry {
|
||||
return caps;
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ import type { Nuri, NuriLike } from "../model/types";
|
||||
// through `sparqlQuery`, every write through `sparqlUpdate` (+ container creation
|
||||
// through `docCreate`) — each ultimately calling the real injected `ng` here. The
|
||||
// access log is therefore instrumented HERE so no access path escapes it. Callers
|
||||
// pass a semantic `label` (readDoc|readUnion|listMyEntityDocs|writeEntity|deposit
|
||||
// pass a semantic `label` (readDocument|listMyEntityDocs|writeEntity|deposit
|
||||
// |…); it is a lib-internal probe param, NOT forwarded to the real `ng` (the docs
|
||||
// primitives forward the exact SDK signature — see test/docs.test.ts). When the
|
||||
// log is OFF (default) the extra param is inert and costs one boolean read.
|
||||
|
||||
@@ -212,7 +212,7 @@ export async function post(targetInboxLike: NuriLike, opts: PostOptions): Promis
|
||||
|
||||
// NO explicit `GRAPH <…>` wrapper — write the anchored DEFAULT graph:
|
||||
// `sparqlUpdate(sid, update, targetInbox)` scopes the write to that repo's
|
||||
// default graph (same shape as read-model.ts readDoc/readUnion). This is the
|
||||
// default graph (same shape as `read-document.ts`'s anchored read). This is the
|
||||
// CANONICAL, always-safe shape and the one the anchored default-graph read
|
||||
// queries. (Not a round-trip necessity on the current broker: the e2e harness
|
||||
// `packages/polyfill/e2e/` verified that an anchored `GRAPH <plainNuri>` write
|
||||
|
||||
@@ -137,7 +137,7 @@ export async function createEntityDoc(store?: NuriLike): Promise<Nuri> {
|
||||
//
|
||||
// - `listMyEntityDocs` has no counterpart at any level: there is no `doc_list`, no
|
||||
// `list_docs`, nothing in the binding. Listing what one created is an ordinary query
|
||||
// over the store's `ldp:contains` — the same shape `readUnion` is, sugar over
|
||||
// over the store's `ldp:contains` — the same shape the anchored document read is, sugar over
|
||||
// something already expressible. It stays reachable at `../shared-wallet/account-registry`
|
||||
// for this library's own read path, which still calls it until that path queries the
|
||||
// store itself.
|
||||
|
||||
@@ -86,7 +86,7 @@ import type { Nuri, NuriLike } from "../model/types";
|
||||
* A push from the platform to a document subscriber. Loosely typed: the raw
|
||||
* serialized `AppResponse` (`{ V0: { State | Patch | TabInfo | ... } }`). The
|
||||
* consumer typically ignores the payload and uses the push purely as a
|
||||
* change SIGNAL (re-query on change — the read-model pattern), so this stays
|
||||
* change SIGNAL (re-query on change — the read model in `docs/read-model.md`), so this stays
|
||||
* permissive rather than modelling every AppResponse variant.
|
||||
*/
|
||||
// @provenance DocChange kind=aligned level=1 ref=engine/net/src/app_protocol.rs:AppResponseV0 — the raw serialized response, left `unknown` rather than modelling every variant
|
||||
|
||||
@@ -22,9 +22,9 @@
|
||||
* The SDK's own shape query - `RdfOrmSubscription.getOrCreate(shapeType, scope)` - and
|
||||
* nothing of this library's own. Four steps went with the change, and they were one
|
||||
* mechanism: a logical scope resolved to a document set (`listMyEntityDocs`), those
|
||||
* documents opened through `ensureReposOpen` for a sync barrier, `readUnion` over them,
|
||||
* and a filter of the union by the shape's `@type` IRI. All four existed because the
|
||||
* parameter had to be TRANSLATED; once the parameter is upstream's own `Scope`, the
|
||||
* documents opened through `ensureReposOpen` for a sync barrier, one anchored read over
|
||||
* each of them, and a filter of the result by the shape's `@type` IRI. All four existed
|
||||
* because the parameter had to be TRANSLATED; once the parameter is upstream's own `Scope`, the
|
||||
* question goes straight to the thing that answers it. `graphs`, `subjects` and the
|
||||
* whole-dataset marker `did:ng:i` are now read by the engine, literally, and this module
|
||||
* does not interpret them at all - so the three readings are upstream's, not ours.
|
||||
|
||||
@@ -82,7 +82,7 @@ function unescapeLiteral(s: string): string {
|
||||
}
|
||||
|
||||
/** A stateful fake `ng`: the shim SPARQL, the inbox SPARQL, and the anchored
|
||||
* per-doc `?s ?p ?o` read the read-model uses. */
|
||||
* per-doc `?s ?p ?o` read the anchored document read uses. */
|
||||
function makeFakeNg() {
|
||||
const quads: Quad[] = [];
|
||||
let docCounter = 0;
|
||||
|
||||
@@ -537,10 +537,11 @@ test("a broker that cannot read the referenced objects costs ONE deposit, not th
|
||||
fake.quads.filter((q) => q.p === `${INBOX}:payload` && q.o.includes("did:ng:o:")).length;
|
||||
expect(queued()).toBe(3);
|
||||
|
||||
// The broker goes quiet on the referenced objects. `read-model.readDoc` SWALLOWS a failed
|
||||
// read and answers `[]` — so the arm is TOLD, through the `onUnreadable` reporter, rather
|
||||
// than left to guess from an empty answer. Until 2026-08-26 this was classed as an
|
||||
// ordinary refusal: the drain reported it and carried on, popping and destroying all three
|
||||
// The broker goes quiet on the referenced objects. The read THROWS, so the arm is told a
|
||||
// failure happened rather than left to guess it from an empty answer — a distinction the
|
||||
// batch read of the day made with an `onUnreadable` reporter, because it swallowed the
|
||||
// failure into `[]`, and `1.0.0-dev.8` makes with the language. Until 2026-08-26 this was
|
||||
// classed as an ordinary refusal: the drain reported it and carried on, popping and destroying all three
|
||||
// while printing "will not be retried" for each.
|
||||
const realQuery = fake.sparql_query;
|
||||
fake.sparql_query = (async (...a: unknown[]) => {
|
||||
|
||||
@@ -208,7 +208,7 @@ function makeWallet(quads: Quad[] = []) {
|
||||
const g = wrapped ? wrapped[1]! : anchor;
|
||||
const inGraph = quads.filter((q) => q.g === g);
|
||||
|
||||
// The whole-document read (`read-model.readDoc`).
|
||||
// The whole-document read (`read-document.readDocumentSubjects`).
|
||||
if (/SELECT\s+\?s\s+\?p\s+\?o/.test(query)) {
|
||||
return {
|
||||
results: {
|
||||
|
||||
@@ -305,7 +305,7 @@ export function makeWallet(quads: Quad[], options: WalletOptions = {}): FakeWall
|
||||
if (cold && target !== undefined && !synced.has(target)) return { results: { bindings: [] } };
|
||||
const inGraph = quads.filter((q) => q.g === g);
|
||||
|
||||
// The whole-document read (`read-model.readDoc`).
|
||||
// The whole-document read (`read-document.readDocumentSubjects`).
|
||||
if (/SELECT\s+\?s\s+\?p\s+\?o/.test(query)) {
|
||||
return {
|
||||
results: {
|
||||
|
||||
Reference in New Issue
Block a user