|
|
|
@@ -0,0 +1,94 @@
|
|
|
|
|
---
|
|
|
|
|
type: usage
|
|
|
|
|
summary: 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 one-shot read, and one shape query
|
|
|
|
|
against: "@ng-eventually/polyfill@1.0.0-dev.6 (pulled 77e78ec81de81959e90e40da8d302e274b025c51)"
|
|
|
|
|
pulled_from: git@gitea.reconnexion.apps.gueraud.net:Sylvain/ng-helpers.git/.project/concepts/indexing/polyfill-surface/usage_ng-helpers.md
|
|
|
|
|
pulled_version: 71667ca14eae86bf8a327fae09383a4d4e7541dd
|
|
|
|
|
pulled_at: 2026-10-01
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
# 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.
|
|
|
|
|
- `readUnion([doc])` and the type `UnionSubject` - how an index is read, and how a referenced document is read. See `## Needs`.
|
|
|
|
|
- `docs.sparqlQuery(sessionId, query, undefined, anchor)` - an anchored SELECT, as the second of the two ways anyone reads an index.
|
|
|
|
|
- `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 and nothing it writes is dropped on read.
|
|
|
|
|
|
|
|
|
|
**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
|
|
|
|
|
|
|
|
|
|
**The acquisition channel carries no version, so the Change policy has nothing to bite on.** The engagement asks for a pinned version and says the channel by which the source reaches an application is agreed with that application; nothing has been agreed here. This repository reaches the package as a filesystem link and declares the peer range `*`, so what it compiles against is whatever is checked out at the other end - uncommitted edits included. That is not theoretical: while this declaration was being rewritten the linked tree moved from `1.0.0-dev.6` to work heading for `1.0.0-dev.7`, and the end-to-end suite's result moved with it, mid-session, with no pin able to prevent it. The `against:` line above pins the last COMMIT, which is the only pin that exists on this side. A tag-addressable, fetchable source would turn the whole semver discipline from a description into a mechanism.
|
|
|
|
|
|
|
|
|
|
**At the pinned version the one shape read here never starts, and the engagement says it cannot be told apart from a shape written wrong.** `useShapeQuery` is wired exactly as the Deployment requirements describe - `ormSubscription: RdfOrmSubscription.getOrCreate`, the SDK's `initNg` injected, this package's `initNg(ng, session)` called once the session has arrived - and against the broker the ORM's call into `ng` fails with `orm_start_graph is not a function` on the console. Per `## Non-guarantees` NextGraph swallows that and the read stays `isPending` for ever, so the observation it feeds reports `observed=pending` and the two end-to-end checks that rest on it fail. Reproduced on three consecutive runs, 2026-09-21. Measured through the published surface: the SDK's own `ng` answers that member, and the `ng` an application hands `initNg` makes no difference because the call substitutes one of its own. Nothing on this side can work around it - the withdrawn `listMyEntityDocs` had no other replacement - so this is carried as a failing check rather than hidden behind a looser one.
|
|
|
|
|
|
|
|
|
|
**`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 - better than the `() => undefined` it carried before, which a caller could not tell from an empty read-filtered view, but still a value invented to satisfy a signature. Making the field optional, or saying in the engagement what a frameworkless application should pass, removes it.
|
|
|
|
|
|
|
|
|
|
**A shape has to be hand-written, and the engagement carries no example.** `ShapeType` demands `readablePredicate`, `minCardinality` and `maxCardinality` on every predicate, plus a `schema` keyed by the shape's own IRI and a type carrying `@id` and `@graph`. The Change policy names three of those requirements in one clause and shows none of the shape. What was written here was assembled from the SDK's own type declarations - a read this contract exists to make unnecessary. Three lines of example in `## Surface` would cover every application that has no generated shape to hand.
|
|
|
|
|
|
|
|
|
|
**`initNg`'s first argument has no effect.** The Deployment requirements say to call `initNg(ng, session)`, and `## Surface` says of the published `ng` that it is the one to call instead of the one `configure` was given - so an application has a choice to make. Measured through the published surface: the call substitutes an `ng` of its own and the argument is discarded, so both readings behave identically and neither can be told apart from the outside. Saying so - or dropping the parameter - saves the next application the experiment.
|
|
|
|
|
|
|
|
|
|
**No published NURI type guard.** `## Guarantees` says so plainly, so this layer carries its own. 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.** `docs.sparqlUpdate` is the level this layer had to align on, which is where the two frictions above come from. An "add these triples to this document" entry would delete the query composition, the escaping and the NURI validation from this package in a single move.
|
|
|
|
|
|
|
|
|
|
**`readUnion` has no stated behaviour for a document it cannot read.** `## Guarantees` says what it returns for a document it can, and it says elsewhere that a rejection means "unknown, never absent" - but not whether an unreadable document inside the list comes back as a rejection or is swallowed into the result. The worst is assumed (swallowed, therefore indistinguishable from empty) and every read path here is written defensively around that guess. One sentence in `## Guarantees` would replace it.
|
|
|
|
|
|
|
|
|
|
## Needs
|
|
|
|
|
|
|
|
|
|
**A one-shot read of a named document's subjects - `readUnion` as it stands, or something that answers the same question for a document whose key the reader does not yet hold.** The ORM is wired here now, so `useShapeQuery` is reachable and the earlier objection - that this application wires none - has fallen. It still does not replace this read, and the reason is the engagement's own: **neither shape read fetches a public store's key**, so a public document read nowhere else appears in neither, and the way to make it appear is to read it once by naming it, with `readUnion` or an anchored SELECT. An index IS a public document and its central case is a STRANGER reaching it from its reference alone - precisely the case no shape read serves. Three smaller costs sit on top: a shape read is reactive and three-stated where a page-load read is one-shot; it projects only the predicates a shape declares, so an index's entries and the index's own declaration take two shapes where `readUnion([index])` takes one call; and it pushes the ORM, a hand-written shape and the `useShape` friction above onto every consumer of `@ng-helpers/indexing`, which today needs none of them. This package publishes to its own consumers that an index is an ordinary document, read with `readUnion([index])` and recognised by two IRIs; withdrawing it leaves the anchored SELECT as the only fallback, which puts every reader of an index back on raw SPARQL - where three of the frictions above come from.
|
|
|
|
|
|
|
|
|
|
**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 (see `## Frictions`), 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.
|