feat!: le paquet crée un index et lui ajoute une référence, rien de plus

This commit is contained in:
Sylvain Duchesne
2026-08-21 19:56:11 +02:00
parent c2f9ff4674
commit ff78a70f14
26 changed files with 735 additions and 2651 deletions
@@ -1,6 +1,6 @@
---
type: contract
summary: What @ng-helpers/indexing engages to do — create an index, take a reference anyone deposits, and make it an entry once the index's creator connects; the index itself is an ordinary document anyone queries
summary: What @ng-helpers/indexing engages to do — create an index, and hand one a reference anyone may deposit; the index itself is an ordinary document anyone queries, and what it receives becomes entries through the layer below
---
# contract_indexing-layer — `@ng-helpers/indexing`
@@ -9,68 +9,58 @@ summary: What @ng-helpers/indexing engages to do — create an index, take a ref
This package builds an **index** on top of NextGraph: an ordinary public document that holds one entry per indexed object, keyed by that object's NURI and carrying its value for a single declared field.
It covers creating an index and depositing a reference to an object into one (open to anyone). What becomes of that reference is covered too, but never as a call — see `## Guarantees`. Reading is not covered: the document is ordinary, and `## Surface` has the shape to query it.
It covers two acts: creating an index, and handing one a reference to an object (open to anyone). It is a thin helper, and its whole job is to spare you the address an index receives on — you name documents, never an inbox.
It does not cover NextGraph itself documents, identity, sharing, transport, all of which reach it through a port you supply — nor search, filtering, pagination, or querying by anything but the index's field.
It does not cover what becomes of a reference you hand over: making entries of what an index receives belongs to the layer below, and there is no call here that does it, aims it or asks about it. Nor reading — the document is ordinary, and `## Surface` has the shape to query it. Nor NextGraph itself: documents, identity, sharing, transport, all reached through the session you hand it. Nor search, filtering, pagination, or querying by anything but the index's field.
### Deployment requirements
An application using this package must:
- have a NextGraph session open under the identity it wants to act as, and build the port from it — `polyfillPort({ sessionId })`, where `sessionId` is what `@ng-eventually/polyfill`'s own `init(…)` hands its callback;
- **await `indexing(port)` once at startup and keep what it produces.** That handle is one identity's — no call takes an identifier, so two users mean two handles — and it is also that identity's connection: awaiting it is what makes entries appear, dropping it is what stops them;
- reach a broker — nothing here is answered locally;
- **supply `@ng-eventually/polyfill` itself**, as a *peer*: the application names it among its own dependencies, and that copy must be the one its own code calls — that package requires exactly one instance of itself;
- **connect as an index's creator if that index is ever to fill.** Unconditional, whoever holds its reference: an index whose creator never returns stays as it was, however many references it is handed.
- have a NextGraph session open under the identity it wants to act as, and hand this package its **session id** what `@ng-eventually/polyfill`'s own `init(…)` hands its callback, relayed and never converted;
- **supply `@ng-eventually/polyfill` itself**, as a *peer*: the application names it among its own dependencies, and that copy must be the one its own code calls — that package requires exactly one instance of itself. It is also what turns what an index receives into entries, so there is nothing extra to install or configure for that;
- reach a broker — nothing here is answered locally.
**Holding an index's reference.** An index is reached by its NURI, held however the application holds any other reference — per user, per context, or read out of a document it opens anyway. That reference is the only way anyone reaches it, and losing it loses the index. **Hardcoding it is what a single GLOBAL index needs, and only that case**; anything narrower is discovered.
A handle is one identity's and holds nothing: build one at startup and keep it, or build one where you need it.
**Obtaining it.** Not published to npm and not built output: the entry point is TypeScript source, so whatever builds the application compiles it — and `@ng-eventually/polyfill` arrives the same way.
**Holding an index's reference.** An index is reached by its NURI, held however the application holds any other reference — per user, per context, or read out of a document it opens anyway. It is the only way anyone reaches the index, and losing it loses the index. **Hardcoding it is what a single GLOBAL index needs, and only that case**; anything narrower is discovered.
**Obtaining it.** Not published to npm and not built output: the entry point is TypeScript source, so whatever builds the application compiles it — as does `@ng-eventually/polyfill`.
## Surface
Full typed shape: the package's `types` entry. The load-bearing signatures:
Full typed shape: the package's `types` entry. All of it, and it is deliberately this small:
```ts
// wiring
export function polyfillPort(options: PolyfillPortOptions): NextGraphPort;
export interface PolyfillPortOptions { readonly sessionId: string | number }
/** This identity's handle, and its connection: see `## Guarantees`. */
export function indexing(port: NextGraphPort): Promise<Indexing>;
/** This identity's handle. It reaches nothing on its own — only the two acts do.
* `sessionId` is what
* `@ng-eventually/polyfill`'s own `init(…)` hands its callback, relayed, never converted. */
export function indexing(sessionId: string | number): Indexing;
// addressing (import these from here)
export type Nuri = `did:ng:${string}`;
export type NuriLike = Nuri | string;
export type { UnionSubject, NextGraphPort, IncomingDeposit, ObjectResolution };
// the three acts
export interface Indexing {
/** A new index in THIS identity's public store, and its NURI. Any user may. `field`
* is the predicate an indexed object must carry; an empty or blank one throws. */
createIndex(field: string): Promise<Nuri>;
/** Deposits a bare reference to an object into the index. Open to ANYONE. Throws
* when the document cannot take one, rather than losing it. */
refer(index: NuriLike, object: NuriLike): Promise<void>;
/** Convenience: the entries, ordered by value, without the index's own subject.
* Refuses a document declaring no field. NOT an engagement — see below. */
read(index: NuriLike): Promise<IndexEntry[]>;
create(field: string): Promise<Nuri>;
/** Hands the index a bare reference to an object. Open to ANYONE. Throws when the
* document cannot take one, rather than losing it. Added now, visible later. */
add(index: NuriLike, object: NuriLike): Promise<void>;
}
// what an index holds, and what a depositor sends
export interface IndexEntry { readonly object: Nuri; readonly value: string }
export interface IndexDescriptor { readonly field: string }
export type IndexDeposit = Nuri; // the reference IS the whole payload
export function decodeReference(payload: unknown): Nuri | null; // untrusted input
// addressing, as the two acts above speak it
export type Nuri = `did:ng:${string}`;
export type NuriLike = Nuri | string;
// the IRIs it is written with
// the IRIs an index is written with — how you recognise what you read
export const INDEX_FIELD: string; // "urn:ng-helpers:index:field"
export const ENTRY_VALUE: string; // "urn:ng-helpers:index:value"
```
**What an index document holds** — two shapes, and this layer writes nothing else:
**There is no call here that reads an index, and that absence is the engagement.** An index is an ordinary document: its contents come back the way any other document's do, and the two IRIs above are the whole of what you need to make sense of them. A helper of ours would only teach a shape you would have to unlearn.
- on the **index's own subject**, `INDEX_FIELD` carries the predicate an indexed object must carry, as a **literal**, not a URI;
- on **each entry**, whose subject is the indexed object's own `did:ng:` NURI, `ENTRY_VALUE` carries that object's value for the field, as a literal. One subject may carry more than one, which two sessions racing each other produce.
**What an index document holds** — two shapes:
- on the **index's own subject**, `INDEX_FIELD` carries the predicate an indexed object must carry, as a **literal**, not a URI. `create` writes it, and it is the only thing this package ever writes;
- on **each entry**, whose subject is the indexed object's own `did:ng:` NURI, `ENTRY_VALUE` carries that object's value for the field, as a literal. One subject may carry more than one.
That is everything reading takes: an anchored `SELECT ?object ?value WHERE { ?object <urn:ng-helpers:index:value> ?value }` returns the entries, for a stranger owning neither the index nor the objects exactly as for its creator; `readUnion([index])` returns the same subjects plus the index's own.
@@ -78,43 +68,31 @@ That is everything reading takes: an anchored `SELECT ?object ?value WHERE { ?ob
**An index is an ordinary public document, and nothing marks it as one.** It lives in its creator's public store, so any reader opens it from the reference alone; its creator owns it, and any user may create one.
**A reference deposited into an index becomes an entry once the index's creator is connected.** Awaiting `indexing(port)` is that connection: across every index that identity owns, what was deposited while it was away becomes an entry then, and what arrives from that moment on becomes one as it lands. Nothing to call, schedule or configure, and no way to aim it at one index — that session serves all of them.
**A reference you hand an index becomes one of its entries.** Not by anything on this surface: the layer below processes what an index receives, and that is what an entry is made by. Nothing to call, schedule or configure and nothing to wait on, which is why `add` returns as soon as the reference is lodged rather than when it can be read back.
**The field is declared once, inside the document, and cannot be changed.** `createIndex` refuses an empty or blank one at the door; an index created on a useless field is useless for good. A document that ends up declaring several — which nothing on this surface can do, only a direct write to it — stops gaining entries, loudly and permanently.
**The field is declared once, at creation, and this package never changes it.** `create` refuses an empty or blank one at the door; an index created on a useless field is useless for good.
**A new index is ready the moment `createIndex` produces it**deposit into it straight away; nothing to open or register.
**A new index is ready the moment `create` produces it**hand it a reference straight away; nothing to open or register.
**Depositing is open to anyone; writing is the creator's alone.** `refer` is a deposit, so a stranger contributes to an index they could not write. What is deposited is a **bare reference** — no operation, no claim, no copy of the value: what the object itself says, when its entry is made, is what goes in.
**Handing a reference over is open to anyone; writing an index is its creator's alone.** `add` is not a write, so a stranger contributes to an index they could not write a triple into. What travels is a **bare reference** — no operation, no claim, no copy of the value, and no index either, since the one it is handed to identifies it. What the object itself says, when its entry is made, is what goes in. And it is a refusal rather than a silent loss: `add` throws when the document cannot take a reference, and refuses anything that is not a NURI before it goes anywhere.
**An index ONLY EVER GROWS.** No call removes an entry, for anyone including the creator: this package cannot express a removal at all. The answer to "this entry must go" is a fresh index.
**Handing the same reference over twice is safe, and so is doing it a hundred times.** A reference is an invitation to look, never an instruction. Do it when the object is created, and again whenever anyone notices the index may not have it yet — including a third party who only read the index.
**The same references produce the same index, whatever order they arrived in.** One reference deposited a hundred times leaves one entry, and an object already indexed is passed over rather than read again. The cost: no deposit is ever retired, so the work behind an index is linear in its history.
**None of this can deny you anything.** A session that could not find its indexes, catch one up, or stay posted about one still hands you a working handle, and nothing deposited is lost: the next connection makes its entry. Such failures, and every reference that could not be resolved, are on this package's log stream.
**Everything deposited is untrusted.** Anyone may deposit anything; `decodeReference` returns `null` for whatever is not a reference, and such a payload is passed over rather than stopping the rest.
**Nothing on this surface removes anything.** No call takes an entry out of an index, for anyone including its creator: this package cannot express a removal at all. The answer to "this entry must go" is a fresh index.
## Non-guarantees
**Reading is not an engagement.** `read` is sugar over the shape above; its ordering, its tie-breaks and what it makes of a subject carrying several values are free to change. The order an index comes out in is the caller's decision — if it matters, query the document and order the result.
**Nothing tells you what became of a reference, or when.** No report, no callback, nothing to wait on, no ordering between handing one over and reading the index. Whether an entry is made at all, what value it ends up holding, and how long that takes are not engaged here — reading the index is how you find out. `add` checks that a reference is a NURI and nothing more: not that the object exists, that it is readable, or that it carries the index's field.
**Nothing tells you what became of a reference, or when.** No report, no callback, nothing to wait on, no ordering between a deposit and a read; while an index's creator stays away, a deposit waits as long as that lasts. A reference that could not be resolved is warned about on the log stream; an object carrying nothing for the field, one carrying several, a self-reference and a payload that is no reference are not reported at all. Reading the index is how you find out.
**No order, and nobody to ask for one.** Nothing here hands you an index's contents, so the order they come out in is whatever your own query says — and a subject carrying several values is yours to make something of.
**No refresh.** An already-indexed object is never read again, so one whose value changes later keeps its original indefinitely.
**No private data.** Only objects the index's creator can open are indexed; one it cannot read is not added.
**A handle is one identity for its whole life, and nothing releases what it holds.** An application that changes identity within one page must build a new handle and drop the old one, which goes on listening under a session that holds nothing.
**Connecting reads this identity's whole public store** — a store read plus one read per document, every time a handle is built. An index whose read did not answer then is not found, silently; its entries are made at the next connection.
**The narrow behaviours are open questions.** An object carrying nothing for the field is not added; one carrying several values is not added. Each may change.
**A handle holds nothing, and there is nothing to release.** It is one identity's for its whole life — an application changing identity within one page builds another, which is free.
**No cross-broker reach.** A NURI resolves for users of one broker.
**No depositor authentication and no rate limit.** Anyone may deposit any number of payloads into any index.
**No depositor authentication and no rate limit.** Anyone may hand any index any number of references, and what an index receives is untrusted: telling a reference from the rest is the business of whoever processes it.
**A BET, named as one — this layer's, not yours.** That a document can receive deposits at all is aligned with NextGraph; **what a deposit carries, and what receiving one does, are not** — upstream has defined no such shape and offers no hook to extend the one it has. When it does, this layer moves with it, under a `major`.
**A BET, named as one — this layer's, not yours.** That a document can receive at all is aligned with NextGraph; **what is handed over, and what receiving it does, are not** — upstream defines no such shape and offers no hook to extend the one it has. When it does, this layer moves with it, under a `major`.
## Change policy
@@ -124,12 +102,18 @@ That is everything reading takes: an anchored `SELECT ?object ?value WHERE { ?ob
- **minor** — a symbol is added and nothing existing moves: a new read helper, a new optional option.
- **patch** — a fix that changes neither the exported surface nor anything under `## Guarantees`, throw text included.
**A tag says where it comes from.** A release cut on `main` carries a **full version** (`3.0.0`); work on a branch carries a **pre-release** of the version it heads for (`3.1.0-dev.3`), which sorts below it, and between two pre-releases nothing is promised. Nothing you pinned is ever withdrawn. The tag is bare — `v3.0.0`.
**A tag says where it comes from.** A release cut on `main` carries a **full version** (`4.0.0`); work on a branch carries a **pre-release** of the version it heads for (`4.1.0-dev.3`), which sorts below it, and between two pre-releases nothing is promised. Nothing you pinned is ever withdrawn. The tag is bare — `v4.0.0`.
**`3.0.0` stops engaging on reading, and drops a re-export that never existed.** The guarantees describing what `read` does — ordering, the tie-break on NURI, a subject with several values, a leniently-read declaration — are gone, replaced by the storage shape under `## Surface`. `PrincipalId` leaves it too: listed as re-exported, never exported by `src/index.ts`. **Nothing to migrate and no call behaves differently**`read` still orders exactly as before. A **major** under the rule above: the surface loses a symbol, and `## Guarantees` loses statements you may no longer rely on.
**`4.0.0` cuts this package to two acts.** Everything that made entries of what an index received has left it: that work is done by the layer below, on the inbox an index already has, so an index goes on filling without any of the calls listed here.
**`2.0.0` removed `Indexing.curate(index)` — and `CurationReport`, `CurationOutcome`, `SkipReason` with it — and made `indexing(port)` a promise.** **Migrating from `1.x`**: delete every call to `curate`, `await` the `indexing(port)` you already make, and where you read a `CurationReport` read the index instead. `2.0.1` was prose and one relaxed deployment requirement; nothing to migrate. `1.0.1` and `1.0.0` keep resolving.
- **`indexing(port)``indexing(sessionId)`, and it no longer returns a promise.** Pass the session id you were passing to `polyfillPort`, and delete the `polyfillPort({ sessionId })` call: this package does that wiring itself now, so `polyfillPort` and `PolyfillPortOptions` go with it. `await indexing(…)` still compiles — there is simply nothing left for it to wait for.
- **`createIndex(field)``create(field)`**, and **`refer(index, object)``add(index, object)`.** Same behaviour, same throws; `add` is the verb every index carries, and it already says added-now-visible-later.
- **`read(index)` is gone — read the document.** `readUnion([index])` gives you its subjects: skip the one whose subject is the index itself, take `ENTRY_VALUE` off the rest, or run the anchored `SELECT` under `## Surface`. Ordering is yours now; the call you are replacing ordered by value, broke ties on the object's NURI, and refused a document declaring no field.
- **`decodeReference` and `IndexDeposit` are gone**, with the types that served only the calls above: `NextGraphPort`, `IndexEntry`, `IndexDescriptor`, `ObjectResolution`, `IncomingDeposit`, `UnionSubject`. `Nuri`, `NuriLike` and `Indexing` stay.
- **Nothing replaces the guarantees about processing, and nothing needs to.** `3.0.0` engaged that awaiting a handle applied what an identity's indexes had received, kept applying it, caught up a backlog, resolved each reference, passed over an object carrying nothing or too much, and refused an index declaring two fields. None of that is engaged here any more. If your application relied on a handle being what made an index fill, it no longer is — and no longer has to be: the index fills whether that handle exists or not.
Cut on `main`: pin `3.0.0`, and anchor your `usage_` leaf on `against: @ng-helpers/indexing@3.0.0`.
**`3.0.0` stopped engaging on what `read` returned** — its ordering, its tie-breaks, what it made of a subject carrying several values — replacing those guarantees with the storage shape under `## Surface`, and dropped `PrincipalId`, listed as re-exported and never exported. **`2.0.0` removed `Indexing.curate(index)`**, with `CurationReport`, `CurationOutcome` and `SkipReason`. `2.0.1`, `1.0.1` and `1.0.0` keep resolving.
Cut on `main`: pin `4.0.0`, and anchor your `usage_` leaf on `against: @ng-helpers/indexing@4.0.0`.
**No changelog file and no deprecation window: the sections above are the release note.** Diff `## Guarantees`, `## Non-guarantees` and `## Surface` between two pulls.