Files
ng-helpers/.project/concepts/indexing/polyfill-surface/usage_ng-helpers.md
T
Sylvain Duchesne 8bcea7c5be fix: lire un document par la requête ancrée, readUnion ayant quitté le polyfill
Le contrat polyfill-surface passe de 1.0.0-dev.6 à 1.0.0-dev.15, épinglé
désormais sur le tag polyfill/v1.0.0-dev.15. readUnion et UnionSubject ont
été retirés en dev.8 : l'application e2e lit un document par un SELECT ancré
regroupé par sujet, en ignorant les registres urn:ng-eventually: que la
requête renvoie désormais.

La déclaration usage_ng-helpers retire ce que l'engagement a absorbé
(exemple de forme, initNg, orm_start_graph corrigé en dev.7, comportement
d'un document illisible, lecture d'un document public par sa seule
référence, pin par tag) et garde les demandes encore ouvertes.

contract_indexing-layer 4.0.1 (patch) : la lecture d'un index ne cite plus
readUnion.
2026-10-01 16:22:02 +02:00

13 KiB

type, summary, against
type summary against
usage The polyfill entries the indexing layer and its end-to-end application stand on - placement by store reference, one anchored write, the deposit that becomes an entry, the anchored read, and one shape query @ng-eventually/polyfill@1.0.0-dev.15 (tag polyfill/v1.0.0-dev.15, commit ec3900077bfd090f5388e52b47a8132b2fc68d16)

usage_ng-helpers - @ng-helpers/indexing on polyfill-surface

The consumer is the indexing layer: an index is an ordinary public document that declares a field, anyone may hand it a reference by depositing into it, and its entries appear because the layer below applies that deposit. NextGraph has no indexing concept, so nothing of what a deposit MEANS belongs upstream - the polyfill's inbox stays generic and carries an opaque payload.

Two callers here, and they consume different things. The library (src/) reaches the polyfill from exactly ONE file, src/polyfill-adapter.ts: everything else is written against an internal port, so a change to the engagement breaks that one file and nothing else. The end-to-end application (e2e/indexing-app.ts, e2e/bridge.ts) is written the way a consumer of @ng-helpers/indexing writes one, and consumes the polyfill directly for its bootstrap and for reading - which is the point: if something is awkward there, it is awkward for every application.

Consumed surface

Library - placement, write, inbox (src/polyfill-adapter.ts)

  • storeRegistry.publicStore() then storeRegistry.createEntityDoc(store) - the index document, in its creator's public store. A store is named by the REFERENCE the accessor hands back, and the argument is never omitted: omitting it places the document in the private store, and an index nobody but its owner can open is not an index. This is the call that was still passing the withdrawn string "public" against the previous version.
  • storeRegistry.openDocumentInbox(doc) - called once, at creation, under the owner. The address it returns is deliberately dropped.
  • docs.sparqlUpdate(sessionId, update, anchor) - the only write this package makes, always an anchored INSERT DATA of one triple.
  • inbox.postToDocument(doc, { payload }) - the deposit, where the payload IS a bare did:ng: reference.
  • Types Nuri and NuriLike, type-only, so they are literally the published ones rather than a copy that can drift.

End-to-end application - bootstrap, reading, and one observation (e2e/indexing-app.ts, e2e/bridge.ts)

  • configure({ ng, useShape, ormSubscription, init, initNg, sharedWallet }), this package's own init(callback, true, []) for the session_id its callback delivers, this package's initNg(ng, session) once that session has arrived, and ensureIdentity(). ormSubscription is RdfOrmSubscription.getOrCreate and the injected initNg is the SDK's, both from @ng-org/orm@0.1.2-alpha.21, passed as they stand; the ng and init handed to configure are @ng-org/web@0.1.2-alpha.14's.
  • The package's own published ng, as the argument initNg is given.
  • docs.sparqlQuery(sessionId, query, undefined, anchor) - the anchored SELECT, and the ONLY way anything here reads a document by name: an index, a referenced object, and the stranger's case where the reader holds no key and the public store is asked for one. Two forms: the narrow SELECT ?object ?value a consumer of @ng-helpers/indexing is told to write, and SELECT ?s ?p ?o, whose rows the application groups by subject itself, skipping the polyfill's urn:ng-eventually: registers.
  • useShapeQuery(shape, { graphs: "did:ng:i" }), ShapeObservable's three members (getSnapshot, subscribe, refetch), the three states of ShapeQuery and its data, and the types Scope and ShapeType. ONE use, and it is an observation rather than a business read: after a failure is injected into the inbox step of create, this is how the suite finds the document that was left behind. The shape is hand-written and names one predicate - the descriptor create writes - which is what makes an index document matchable at all.

Not consumed, and safely evolvable as far as this repository is concerned: useShape (a function that REFUSES is handed to configure, and nothing calls it - see ## Frictions), subscribeDoc / subscribeDocs, docs.docCreate, storeRegistry.privateStore / protectedStore, inbox.share / post / read / readForDocument / readSynced / readSyncedForDocument / processInbox / watch, and the types Deposit and PrincipalId.

Constraints

A bare reference deposited into a document's inbox becomes an entry on that document. This is the single guarantee the whole layer rests on: add deposits a did:ng: reference and nothing else, precisely because that shape is this engagement's own and it acts on it. Nothing of indexing's meaning travels in the payload, and there is no second shape to fall back on.

An index is created in its creator's PUBLIC store, named by that store's reference. The public store is what serves an index's read key to whoever asks, and that is the whole reason a stranger can reach one from its reference alone. The unit suite pins both halves - where the document landed, and that the store travelled as the reference an accessor produced - so the private placement that omitting the argument would give is red rather than silent.

One handle is one identity. The session is one user's and no call takes an identifier, so an Indexing handle is one person's. Two users mean two handles - which is also what keeps the multi-actor end-to-end journeys honest: a depositor obtains the index reference the way an application does.

sessionId is relayed, never converted. It is carried at the engagement's own string | number and handed back untouched.

The ORM is the APPLICATION's to wire, and the library never touches it. src/ performs no shape read of any kind, so a consumer of @ng-helpers/indexing that wants none wires nothing and declares no @ng-org/* dependency of its own. Only the end-to-end application here wires it, and only because one observation has no other door.

A reactive read's load state is read before its data, and a pending read is bounded. An empty data means "this scope holds nothing" only under isSuccess; under anything else it means nobody answered, and the two never reach a check as one value - the state travels out to the report beside the result. The bound exists because a shape query the engine refuses never finishes loading, so without one that failure would be a hang rather than an observation.

The write is add-only, and structurally so. There is no delete builder anywhere in this package and the only statement it can compose is an anchored INSERT DATA; the unit suite executes that SPARQL against an engine that refuses anything else, so a removal is unrunnable rather than merely absent. What this asks of the engagement is exactly one write primitive and no more.

The document is named once, as the anchor. The statement carries no GRAPH <...> wrapper, because the anchor already scopes the write to that repo's default graph. Naming it twice would be two chances to disagree.

A document's inbox is opened from ONE place - create, under the owner, at the moment the document is created - and no address is held anywhere above that call. The non-guarantee about openDocumentInbox not coalescing across pages is therefore respected by construction rather than by care.

This package declares the polyfill a PEER dependency, so the application above remains the one place it is named. The single-copy rule is the one packaging mistake the surface cannot protect anyone from, and a library that declared it a direct dependency would be manufacturing exactly that.

Every NURI is validated before it reaches a query or a deposit, because no type guard is published (see ## Frictions).

This layer stays out of urn:ng-eventually: entirely. Its predicates are urn:ng-helpers: and the subjects it writes are always did:ng: references, so nothing it writes falls under the reserved prefix - which is what lets a reader skip every subject under that prefix without losing anything of an index.

A document that cannot be read is refused, not answered empty. Every read here rests on the anchored query rejecting for a document the reader cannot reach, rather than returning no rows; the end-to-end suite lets that rejection surface as a failure and never reads it as an index holding nothing.

An empty read is never concluded from. Where the end-to-end suite reads an index it polls to a bound and reports what the index held instead, rather than reading once and calling an empty answer an answer.

The identity ensureIdentity() returns is opaque here - checked for non-emptiness, carried, printed in a run report, never parsed or rendered as a name.

Frictions

configure requires useShape, and a frameworkless application has nothing to give it. @ng-org/orm publishes useShape only through its react / svelte / vue adapters; a page with no framework can produce none of them. The field is not optional in EventuallyConfig, so this application hands over a function that THROWS if anything ever calls it - a value invented to satisfy a signature. Making the field optional removes it.

No published NURI type guard. Every entry validates at its own door, but this layer composes SPARQL before it reaches one, so it carries its own guard. It is a guess at what the engagement considers a valid Nuri, and a wrong guess either rejects a legitimate reference or lets one through into a query.

No published escaping helpers. The engagement lists no escapeIri / escapeLiteral, and this package composes SPARQL against docs.sparqlUpdate - so it carries its own escaping rather than reach into the provider. That is a security-relevant duplication: two implementations of one rule, and the one running here is not the one the provider tests.

No published way to write triples above the raw SPARQL primitive that a library without the ORM can reach. insertObject is the write above docs.sparqlUpdate, but it is injected through configure and this library wires no @ng-org/* by design (see ## Constraints), so docs.sparqlUpdate remains the level it aligns on - which is where the two frictions above come from. An "add these triples to this document" entry, taking no shape, would delete the query composition, the escaping and the NURI validation from this package in a single move.

Needs

A way to tell which store holds a document whose reference is already held. The withdrawn listMyEntityDocs had one use here, and the shape read now covers most of it: after a failure is injected into the inbox step of create, the suite finds the document left behind by asking for every index document this identity HOLDS and diffing. What that cannot say is WHERE it sits, so the check it feeds now establishes that a document survived, and no longer that it survived in the public store. This is not the question the Change policy declined: it enumerates nothing, it names a document the caller already holds, and a store's graph is said to hold exactly the containment triples that would answer it. As it stands an application cannot verify its own placement at all - a document written to the wrong store is undetectable until some reader fails to open it.

A way to tell a shape the ENGINE refused from a shape the application wrote wrong. ## Non-guarantees says a refused shape query is caught upstream, written to the console and nothing more: the set stays empty and its readiness never settles. From an application both faults look identical - an isPending that does not end - and the console line is not something a page can act on. That is the whole diagnostic surface for the one thing an application must hand-write itself, so the first shape anybody writes has no way of telling "the engine would not take this" from "the ORM was never started". Anything that separates them - a refusal that reaches isError, or a state that says "never opened" - turns a silent hang into something an interface can report.

A way to tell "examined and discarded" from "not examined yet". A deposit this engagement applies is consumed whether or not it produced an entry, and a refusal goes to the provider's own log stream - nothing an application can observe, and by the engagement's own words there is no way to ask what was lost. The cost here is concrete: the journey that checks that an object carrying nothing for an index's field is NOT indexed cannot be waited for, because no reading distinguishes "the deposit was examined and skipped" from "the deposit has not been examined yet" - and this repository's suite says so in its own header rather than hiding it, declaring that check weaker than its name. Anything observable from the application - a per-deposit outcome, or merely a count that moves when a queue has been drained - would make it a real one. This layer holds nothing of its own on purpose: an index only ever grows, and the reference is the whole of what is deposited.