feat!: curer n'est plus un appel, c'est ce que fait le traitement de l'inbox
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
---
|
||||
type: contract
|
||||
summary: The API @ng-helpers/indexing exposes to an application — creating an index, depositing references into it, curating it, and reading it back
|
||||
summary: The API @ng-helpers/indexing exposes to an application — creating an index, depositing references into it, reading it back; curating is not on it: it is what an index's inbox being processed does
|
||||
---
|
||||
|
||||
# contract_indexing-layer — `@ng-helpers/indexing`
|
||||
@@ -9,71 +9,58 @@ summary: The API @ng-helpers/indexing exposes to an application — creating an
|
||||
|
||||
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, handing an index a reference to an object (open to anyone), the owner resolving those references and adding what it can, and reading the entries back in order.
|
||||
It covers creating an index, handing one a reference to an object (open to anyone), and reading the entries back in order. Resolving those references and adding what can be added is covered too, but never as a call: it is what happens when the index's inbox is processed.
|
||||
|
||||
It does not cover NextGraph itself — documents, identity, sharing, inboxes, transport — all of which reach it through a port you supply. It does not cover search, filtering, pagination, or querying by anything but the index's own field. It **never removes anything**, from anywhere, and that is a property of the engagement rather than a missing feature.
|
||||
It does not cover NextGraph itself — documents, identity, sharing, inboxes, transport — all of which reach it through a port you supply. It does not cover search, filtering, pagination, or querying by anything but the index's own field. It **never removes anything**, anywhere — an engagement, not a missing feature.
|
||||
|
||||
### Deployment requirements
|
||||
|
||||
An application using this package must:
|
||||
|
||||
- have a NextGraph session already 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;
|
||||
- reach a broker, since every operation here is a document read, a document write, or an inbox deposit;
|
||||
- **supply `@ng-eventually/polyfill` itself.** This package declares it a *peer*, not a dependency: the application names it among its own dependencies and decides which copy it gets. That copy must be the very one the application's own code calls, because everything this package does passes through it — and that package requires exactly one instance of itself in an application, for reasons its own contract states.
|
||||
- **hardcode the index's NURI in its own source.** Nothing marks a document as an index; the reference is what makes it one, and it is the only way anyone reaches it.
|
||||
- **await `indexing(port)` once at startup and keep what it produces.** That handle is one identity's and is also that identity's connection: awaiting it is what curates, dropping it is what stops;
|
||||
- reach a broker, since every operation here is a document read, a document write or an inbox deposit;
|
||||
- **supply `@ng-eventually/polyfill` itself.** This package declares it 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 in an application, for reasons its own contract states;
|
||||
- **hardcode the index's NURI in its own source, and be the creator's own application if the index is ever to fill.** One requirement, not two: nothing marks a document as an index, so the reference an application carries is the only way anyone reaches it, and its creator's connections are the only thing that curates it. An index nobody hardcodes is unreachable; one whose creator never comes back stays as it was, however many references it is handed.
|
||||
|
||||
One handle is one identity: the port carries a session and no call takes an identifier. Two users mean two handles.
|
||||
One handle is one identity: the port carries a session, no call takes an identifier, and two users mean two handles.
|
||||
|
||||
**Obtaining it.** This package is not published to npm, nor to any other package host, and it is not distributed as built output: its published entry point is TypeScript source, so whatever builds the application is what compiles it, and a toolchain that accepts only JavaScript cannot consume it as it stands. `@ng-eventually/polyfill` is distributed the same way. By which channel the source reaches a given application is agreed with that application rather than fixed here; what this contract fixes is the version you pin and what you must provide alongside it.
|
||||
**Obtaining it.** Not published to npm or any other host, and not built output: the entry point is TypeScript source, so whatever builds the application compiles it. `@ng-eventually/polyfill` arrives the same way. What this contract fixes is the version you pin and what you must provide alongside it.
|
||||
|
||||
## Surface
|
||||
|
||||
Full typed shape: the package's `types` entry, `@ng-helpers/indexing`. The load-bearing signatures:
|
||||
|
||||
```ts
|
||||
// ── wiring: one handle, one identity ─────────────────────────────────────────
|
||||
// ── wiring: one handle, one identity, and that identity's connection ─────────
|
||||
export function polyfillPort(options: PolyfillPortOptions): NextGraphPort;
|
||||
export interface PolyfillPortOptions { readonly sessionId: string | number }
|
||||
export function indexing(port: NextGraphPort): Indexing;
|
||||
/** Produces this identity's handle — and before resolving, goes through the inbox of
|
||||
* every index it owns, leaving each watched for as long as the handle lives. */
|
||||
export function indexing(port: NextGraphPort): Promise<Indexing>;
|
||||
|
||||
// ── addressing (re-exported so you import them from here) ────────────────────
|
||||
export type Nuri = `did:ng:${string}`;
|
||||
export type NuriLike = Nuri | string;
|
||||
export type { PrincipalId, UnionSubject, NextGraphPort, IncomingDeposit, ObjectResolution };
|
||||
|
||||
// ── everything this package does ─────────────────────────────────────────────
|
||||
// ── the three acts an application performs ───────────────────────────────────
|
||||
export interface Indexing {
|
||||
/** Creates an index in THIS identity's public store and opens its inbox. Any user may.
|
||||
* `field` is the predicate an indexed object must carry, declared once and for good;
|
||||
* an empty or blank one throws. Returns the NURI to hardcode. */
|
||||
/** Produces a new index in THIS identity's public store, its inbox open, and the NURI
|
||||
* to hardcode. Any user may. `field` is the predicate an indexed object must carry,
|
||||
* declared once and for good; an empty or blank one throws. */
|
||||
createIndex(field: string): Promise<Nuri>;
|
||||
/** Deposits a bare reference into the index's inbox. Open to ANYONE. Nothing lands in
|
||||
* the index until its owner curates. Throws if the index has no inbox. */
|
||||
/** Deposits a bare reference into the index's inbox. Open to ANYONE. Produces nothing:
|
||||
* no receipt, and no inbox address is ever handed out. Throws if there is no inbox. */
|
||||
refer(index: NuriLike, object: NuriLike): Promise<void>;
|
||||
/** OWNER only — resolves the references received and adds what it can. */
|
||||
curate(index: NuriLike): Promise<CurationReport>;
|
||||
/** The entries, ordered by value. Sugar over `readUnion([index])`. */
|
||||
/** Produces the entries, ordered by value. Refuses a document that declares no index
|
||||
* field rather than producing an empty list. Sugar over `readUnion([index])`. */
|
||||
read(index: NuriLike): Promise<IndexEntry[]>;
|
||||
}
|
||||
|
||||
// ── what an index holds ──────────────────────────────────────────────────────
|
||||
// ── what an index holds, and what travels from a depositor to a curator ──────
|
||||
export interface IndexEntry { readonly object: Nuri; readonly value: string }
|
||||
export interface IndexDescriptor { readonly field: string }
|
||||
|
||||
// ── what curating reports ────────────────────────────────────────────────────
|
||||
export type CurationOutcome =
|
||||
| { readonly result: "indexed"; readonly object: Nuri; readonly value: string }
|
||||
| { readonly result: "unchanged"; readonly object: Nuri }
|
||||
| { readonly result: "skipped"; readonly object: Nuri; readonly reason: SkipReason }
|
||||
| { readonly result: "unresolved"; readonly object: Nuri; readonly reason: string }
|
||||
| { readonly result: "foreign"; readonly reason: string };
|
||||
export type SkipReason = "no-field" | "several-values" | "self-reference";
|
||||
export interface CurationReport {
|
||||
readonly index: Nuri;
|
||||
readonly outcomes: readonly CurationOutcome[]; // one per deposit, in deposit order
|
||||
}
|
||||
|
||||
// ── what travels from a depositor to a curator ───────────────────────────────
|
||||
export type IndexDeposit = Nuri; // the reference IS the whole payload
|
||||
export function decodeReference(payload: unknown): Nuri | null; // untrusted input
|
||||
|
||||
@@ -86,70 +73,66 @@ export const ENTRY_VALUE: string; // on an entry: that object's value for the
|
||||
|
||||
**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.
|
||||
|
||||
**The field is declared once, inside the document, and cannot be changed.** `createIndex` refuses an empty or blank field at the door, because nothing here deletes and an index created on a useless field is useless for good. Declaring it in the document rather than in an application's source is what stops two applications curating the same index on two different fields.
|
||||
**What becomes of what was created: the index is curated at its creator's next connection, and on each deposit while the creator is connected.** Awaiting `indexing(port)` is that connection — it goes through the inbox of every index the identity owns, backlog and all, and leaves each watched, so a deposit made from then on is applied as it lands. There is nothing to call, schedule or configure, and no way to aim curating at one index. Only the owner could anyway: nobody else reads that inbox, and nobody else writes that document.
|
||||
|
||||
**`createIndex` opens the index's inbox itself.** Only the owner can, and creation is the one moment the owner is present, so it is not left to a later call to remember.
|
||||
**The field is declared once, inside the document, and cannot be changed.** `createIndex` refuses an empty or blank one at the door: nothing here deletes, so an index created on a useless field is useless for good. Declaring it in the document rather than in an application's source stops two applications curating one index on two fields.
|
||||
|
||||
**Depositing is open to anyone; writing is the owner's alone.** `refer` is a deposit into the index document's inbox — not a write — so a stranger can contribute to an index they do not own. `curate` reads that inbox and writes the document, and both are refused to anyone but the owner. The deposit is a **bare reference**: it carries no operation, no index reference (the inbox address already identifies the index), and no copy of the indexed value. What the object itself says is what goes in.
|
||||
**`createIndex` opens the index's inbox itself, and brings the new index under observation.** Only the owner can open one, and creation is the one moment the owner is present; and since the search at connection ran before this document existed, what was just created is added to what the session watches.
|
||||
|
||||
**An index ONLY EVER GROWS.** There is no call that removes an entry, for anyone including the owner, and none is planned. This package cannot express a removal at all. The only answer to "this entry must go" is a fresh index.
|
||||
**Depositing is open to anyone; writing is the owner's alone.** `refer` is a deposit into the index document's inbox — not a write — so a stranger can contribute to an index they do not own. The deposit is a **bare reference**: no operation, no index reference (the inbox address identifies the index), no copy of the indexed value. What the object itself says is what goes in.
|
||||
|
||||
**Curation is convergent and order-independent.** Deposits are never consumed, so every run sees every deposit again; re-applying one re-resolves the reference and lands on the same result. An already-indexed object is skipped outright as `unchanged`. Nothing depends on the order references arrived in.
|
||||
**A BET, named as one.** That a document can have an inbox is aligned with NextGraph: a repository takes an inbox capability, at most one, driven by a real commit upstream. **What a deposit carries, and what processing one does, are ours.** Upstream's inbox content type declares `Link`, `Patch` and four others as bare names with no payload at all — reserved words, not shapes — and the only two kinds carrying data are unrelated to indexing; that set is closed, with no trait, table of handlers or hook. An index deposit therefore has a shape NextGraph has not defined. When upstream defines those variants, this layer moves with them, and a `major` is how you hear about it.
|
||||
|
||||
**A reference that does not resolve costs nothing and is reported.** It comes back as `unresolved`, nothing is written for it, and nothing already in the index is touched — a later deposit adds it. Every unresolved reference appears in `CurationReport.outcomes`: harmless is not the same as invisible.
|
||||
**An index ONLY EVER GROWS.** No call removes an entry, for anyone including the owner, and none is planned: this package cannot express a removal at all, and it was deliberately never built rather than left for later. Do not design around a future delete — the only answer to "this entry must go" is a fresh index.
|
||||
|
||||
**Reading is per-entry tolerant.** `read` returns entries ordered by value, ties broken on the object NURI, so two readers of the same index always see the same order. Values are compared **as strings** — an index whose field holds ISO-8601 dates therefore comes out in chronological order. A subject that is not a NURI is skipped, never thrown on, and only own properties are read: one stray triple cannot make every real entry unreadable.
|
||||
**Curating is convergent and order-independent.** Deposits are never retired, so every run reads every deposit ever made to that index — linear in its history — and re-applying one lands on the same result; an already-indexed object is passed over. Neither the order references arrived in nor the number of notifications a burst produced changes anything: runs on one index never overlap, and an arrival during a run earns exactly one more run after it.
|
||||
|
||||
**An entry carrying several values keeps the smallest, deterministically** — which two curation runs racing each other can produce. The entry stays visible and every reader agrees on it.
|
||||
**None of this can deny you anything.** A session that could not read its public store, watch an index, or go through one still hands you a working handle: reading an index and depositing into one never depended on that work. Every such failure is on this package's log stream, and so is every reference that could not be resolved — harmless is not the same as invisible. Nothing is lost either way: the deposits stay in their inbox for the next notification or connection.
|
||||
|
||||
**`read` refuses a document that declares no field at all**, rather than answering "an empty index". An unreadable document and an empty one arrive as the same empty result, so an empty answer would be a failure wearing the shape of a fact. Retry before concluding the document is malformed.
|
||||
**Reading is per-entry tolerant.** `read` returns entries ordered by value, ties broken on the object NURI, so two readers always see the same order. Values are compared **as strings** — an index whose field holds ISO-8601 dates comes out in chronological order. A subject that is not a NURI is passed over rather than thrown on, and only own properties are read: one stray triple cannot make every real entry unreadable.
|
||||
|
||||
**An index declaring SEVERAL fields refuses to CURATE, loudly and permanently — and stays readable.** Picking one would leave a single list ordered by two different properties, because entries already written are never re-read. Existing entries stay visible and correct; nothing new is added. The refusal cannot be undone, and it says so instead of suggesting a retry.
|
||||
**An entry carrying several values keeps the smallest, deterministically** — which two runs racing each other can produce, and which keeps the entry visible with every reader agreeing on it.
|
||||
|
||||
**Reading needs nothing from this package.** An application that knows the NURI can call the polyfill's `readUnion([index])` and get the entries as subjects — one per indexed object, keyed by its NURI — plus the index's own subject declaring its field, which `read` drops. `INDEX_FIELD` and `ENTRY_VALUE` are published for exactly that reader.
|
||||
**The document's own declaration is read strictly for curating and leniently for reading.** `read` refuses a document that declares no field at all rather than answering "an empty index": an unreadable document and an empty one arrive as the same empty result, so an empty answer would be a failure wearing the shape of a fact — retry before concluding it is malformed. An index declaring SEVERAL fields stops being curated, loudly and permanently, and stays readable: picking one would leave a single list ordered by two properties, since entries already written are never re-read. That cannot be undone — curate into a fresh index.
|
||||
|
||||
**Every inbox payload is untrusted.** Anyone may deposit anything; `decodeReference` returns `null` for everything that is not a reference, and such a payload is reported as `foreign` rather than crashing curation.
|
||||
**Reading needs nothing from this package.** An application that knows the NURI can call the polyfill's `readUnion([index])` and get one subject per indexed object, keyed by its NURI, plus the index's own subject declaring its field, which `read` drops. `INDEX_FIELD` and `ENTRY_VALUE` are published for that reader.
|
||||
|
||||
**Every inbox payload is untrusted.** Anyone may deposit anything; `decodeReference` returns `null` for whatever is not a reference, and such a payload is passed over rather than crashing the run.
|
||||
|
||||
## Non-guarantees
|
||||
|
||||
**No removal, at any level, ever.** Not an oversight and not "not yet": it was deliberately never built. Do not design around a future delete.
|
||||
**Nothing reports curating to you.** No report, no outcome list, no callback: an application that cannot ask for it has nowhere to receive the result. A reference that could not be resolved is warned about on the log stream; an object carrying nothing for the field, one carrying several values, a self-reference and a payload that is not a reference are not reported at all. Reading the index is how you find out.
|
||||
|
||||
**No refresh.** An already-indexed object is never re-read, so an object whose field value changes later keeps its original value in the index, indefinitely.
|
||||
**No timing.** A deposit is in the index once its creator's session has been through that inbox; nothing says how long that takes or lets you wait, and if the creator is not connected it waits for as long as that lasts. There is no queue depth and no ordering between a deposit and a read.
|
||||
|
||||
**No private data.** Indexing is limited to objects the curator can open itself. An object the index's owner cannot read is simply `unresolved`.
|
||||
**No refresh.** An already-indexed object is never re-read, so one whose value changes later keeps its original indefinitely.
|
||||
|
||||
**`unresolved` does not tell you why.** Gone, unreadable, and "the read failed" arrive identically and are deliberately not distinguished. Never read it as "the object does not exist".
|
||||
**No private data.** Only objects the curator can open itself are indexed; one the owner cannot read is not added.
|
||||
|
||||
**The narrow behaviours are open questions, not promises.** An object carrying nothing for the field is `skipped: "no-field"`; one carrying several values is `skipped: "several-values"`; a raced entry keeps the smallest value. Each is implemented in its narrowest form and reported rather than generalised, and each may change.
|
||||
**A handle is one identity for its whole life, and nothing detaches the inboxes it watches.** An application that changes identity within one page must build a new handle and drop the old one, which goes on watching under a session that holds nothing.
|
||||
|
||||
**No stable error text.** What a throw or an `unresolved` reason reads is for a human reading a report. Do not parse it or branch on it.
|
||||
**Connecting reads this identity's whole public store** — a store read plus one read per document, every time a handle is built, because nothing marks a document as an index. An index whose read did not answer in that moment is not found, silently, and is curated at the next connection instead.
|
||||
|
||||
**No timing and no delivery promise.** A deposit is not in the index until the owner curates, and nothing here schedules curation. There is no notification, no queue depth, and no ordering between a deposit and a read.
|
||||
**The narrow behaviours are open questions, not promises.** An object carrying nothing for the field is not added; one carrying several values is not added; a raced entry keeps the smallest value. Each is implemented in its narrowest form rather than generalised, and each may change.
|
||||
|
||||
**The report grows with the inbox.** Since deposits are never retired, `CurationReport.outcomes` has one entry per deposit ever made, not per change.
|
||||
**No stable error text.** What a throw or a log line reads is for a human. Do not parse it or branch on it.
|
||||
|
||||
**No cross-broker reach.** A NURI resolves for users of the same broker.
|
||||
**No cross-broker reach.** A NURI resolves for users of one broker.
|
||||
|
||||
**No depositor authentication or rate limit.** Anyone may deposit any number of payloads into any index's inbox.
|
||||
**No depositor authentication or rate limit.** Anyone may deposit any number of payloads into any inbox.
|
||||
|
||||
## Change policy
|
||||
|
||||
**Semver, and majors are the normal case.** This layer sits on a polyfill that is itself converging on a NextGraph that does not ship yet, and several of its own behaviours are declared above as open questions. Settling one of them narrows this surface — the major number will move often, and that frequency is the honest signal about this package, not an apology. Refusing to version would not slow the churn down; it would only take away the one tool you have for managing it. Pin a version, upgrade deliberately, and re-pull this contract each time.
|
||||
**Semver, and majors are the normal case.** This layer sits on a polyfill itself converging on a NextGraph that does not ship yet, several of its behaviours are open questions above, and one part of it is a bet. Settling any of those narrows this surface, so the major number moves often — that frequency is the honest signal about this package, not an apology.
|
||||
|
||||
What each level means here, in this package's own terms:
|
||||
|
||||
- **major** — an exported symbol is removed or renamed, **or** an existing call narrows: it now throws where it returned, or reports a state you did not have to handle before. Settling an open question counts, and so does adding a `CurationOutcome` variant or a `SkipReason` — an exhaustive `switch` in your code stops being exhaustive. A signature change a caller must react to counts; one that only accepts more than before does not.
|
||||
- **major** — an exported symbol is removed or renamed, **or** an existing call narrows: it throws where it returned, or reports a state you did not have to handle before. Settling an open question counts, and so does anything the bet forces. A signature change a caller must react to counts; one that only accepts more than before does not.
|
||||
- **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 above under `## Guarantees`, including the text of a throw, which is explicitly disclaimed above.
|
||||
- **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** (`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.3`), 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** (`2.0.0`); work on a branch carries a **pre-release** of the version it heads for (`2.1.0-dev.3`), which sorts below it by construction, and between two pre-releases of the same version nothing is promised. Nothing you pinned is ever withdrawn: a pre-release keeps resolving once the full version appears alongside it. The tag is bare — `v2.0.0` — because this repository publishes exactly one engagement; should a second ever ship here, tags take the package name from then on (`indexing/v…`).
|
||||
|
||||
**The tag is bare — `v1.0.1` — because this repository publishes exactly one engagement**, so there is nothing for a prefix to disambiguate. Should a second one ever ship here, tags take the package name from that point on (`indexing/v…`), because a bare tag stops saying which surface it froze the day two versions move independently. Bare tags already laid stay valid as history.
|
||||
**`2.0.0` took the curating call off this surface, and made obtaining a handle asynchronous.** `Indexing.curate(index)` is gone, and with it `CurationReport`, `CurationOutcome` and `SkipReason`, which nothing published produces any more; `indexing(port)` now returns a promise, because obtaining a handle is what goes through this identity's inboxes and a caller has to be able to await it. Both are removals under the rule above, hence the major. The reason is not tidiness: a published `curate(index)` asked every application to decide who owns an index and when curating runs, and neither is an application's decision — the owner is the only one who can, and "when" is "whenever a deposit arrives, or has been waiting". **Migrating**: delete every call to `curate`, and `await` the `indexing(port)` you already make. If you read `CurationReport` for what happened, read the index instead, and the log for what did not resolve.
|
||||
|
||||
`1.0.0` was a baseline, not a claim of maturity: it was the number that made your pin mean something. Nothing was released before it. **It could not be installed, however**, and `1.0.1` supersedes it. `1.0.0` declared `@ng-eventually/polyfill` as a dependency resolved through a path that existed only in one working copy, so every attempt to install it from anywhere else failed outright — not on some operations but at the install itself, which is why no application ever ran it. `1.0.1` declares that package a peer, which the application supplies. Nothing exported moved, which is what makes this a patch and not a major: the only thing that changed for a caller is a requirement it could never have satisfied before, so there is no working arrangement for it to break.
|
||||
`1.0.1` and `1.0.0` keep resolving and neither is forced to upgrade, `1.0.0` having been uninstallable from anywhere but one working copy. This engagement is cut on `main`, so `2.0.0` is what you pin, and your `usage_` leaf anchors `against:` on that exact string — `against: @ng-helpers/indexing@2.0.0`.
|
||||
|
||||
**`1.0.0` is superseded, not withdrawn.** The tag stays where it is and keeps resolving, because no pinned reference is ever taken away from under you — this contract's policy holds even for a version that never worked. Nothing forces an upgrade; it is simply that an installation pinned there cannot have succeeded, so there is nothing to migrate.
|
||||
|
||||
This engagement is cut on `main`, so `1.0.1` is what you pin, and your `usage_` leaf anchors `against:` on that exact string — `against: @ng-helpers/indexing@1.0.1`. Had you pinned a pre-release, `against:` would carry that string, pre-release suffix included.
|
||||
|
||||
There is no changelog file and no deprecation window: **the sections above are the release note.** A removal or a narrowing lands in `## Surface` and `## Guarantees` in the same version that ships it. Diff this leaf between two pulls — `## Guarantees` and `## Non-guarantees` before `## Surface`, because that is where a narrowing shows up first.
|
||||
There is no changelog file and no deprecation window: **the sections above are the release note.** Diff this leaf between two pulls, `## Guarantees` and `## Non-guarantees` before `## Surface`, because that is where a narrowing shows up first.
|
||||
|
||||
@@ -1,30 +1,30 @@
|
||||
---
|
||||
type: usage
|
||||
summary: The seven polyfill entries the indexing layer stands on, the constraints it holds itself to, and what it had to build for want of a published helper
|
||||
summary: The nine polyfill entries the indexing layer stands on, the constraints it holds itself to, and what it had to build for want of a published helper
|
||||
against: "@ng-eventually/polyfill@1.0.0-dev.1"
|
||||
---
|
||||
|
||||
# usage_ng-helpers — `@ng-helpers/indexing` on `polyfill-surface`
|
||||
|
||||
The consumer is the indexing layer: an index is an ordinary public document, contributions reach it through its inbox, and its owner curates it. NextGraph has no indexing concept, so nothing of what a deposit *means* belongs upstream — the polyfill's inbox stays generic and carries opaque payloads, and this layer decides what they say.
|
||||
The consumer is the indexing layer: an index is an ordinary public document, contributions reach it through its inbox, and its owner's session curates it by going through that inbox. NextGraph has no indexing concept, so nothing of what a deposit *means* belongs upstream — the polyfill's inbox stays generic and carries opaque payloads, and this layer decides what they say.
|
||||
|
||||
The whole runtime dependency passes through **one file**, the adapter that builds our `NextGraphPort`. Everything else in the package is written against that port, so a change to the engagement breaks exactly one file and nothing else. The types are imported type-only, so they are literally the published ones rather than a copy that can drift.
|
||||
|
||||
## Consumed surface
|
||||
|
||||
**Placement** — `storeRegistry.createEntityDoc("public")`, for the index document itself; `storeRegistry.openDocumentInbox(doc)`, called once at creation.
|
||||
**Placement** — `storeRegistry.createEntityDoc("public")`, for the index document itself; `storeRegistry.listMyEntityDocs("public")`, to find again, at each connection, the indexes this identity owns; `storeRegistry.openDocumentInbox(doc)`, at creation to open one and afterwards to resolve its address.
|
||||
|
||||
**Reading** — `readUnion([doc])` and the type `UnionSubject`. Used both for the index document and for resolving a deposited reference.
|
||||
|
||||
**Writing** — `docs.sparqlUpdate(sessionId, update, anchor)`, with the document named ONCE as the anchor so the statement carries no `GRAPH <…>` wrapper. It is the only write this package makes, and it is always an `INSERT DATA` of literal triples.
|
||||
|
||||
**Inbox** — `inbox.postToDocument(doc, { payload })` for depositing, `inbox.readForDocument(doc)` for the owner draining it, and the shape of `Deposit` (`from` / `payload` / `ts`), which our `IncomingDeposit` mirrors.
|
||||
**Inbox** — `inbox.postToDocument(doc, { payload })` for depositing, `inbox.readForDocument(doc)` for the owner going through it, `inbox.watch(address, onDeposits)` so that a deposit made while the owner is connected is applied as it lands, and the shape of `Deposit` (`from` / `payload` / `ts`), which our `IncomingDeposit` mirrors.
|
||||
|
||||
**Types** — `Nuri`, `NuriLike`, `PrincipalId`, `UnionSubject`.
|
||||
|
||||
**Bootstrap, in the end-to-end application only** — `configure`, this package's `init` (for the `sessionId` its callback delivers), and `ensureIdentity`.
|
||||
|
||||
Everything else on the engagement is offered and NOT consumed: `watchShape`, `useShape`, `subscribeDoc`/`subscribeDocs`, `docs.docCreate`, `docs.sparqlQuery`, `storeRegistry.listMyEntityDocs`/`resolveScopeGraph`/`resolveWriteGraph`, `inbox.share`/`post`/`read`/`readSynced`/`readSyncedForDocument`/`processInbox`/`watch`, `ng`, `initNg`. It is safely evolvable as far as this layer is concerned.
|
||||
Everything else on the engagement is offered and NOT consumed: `watchShape`, `useShape`, `subscribeDoc`/`subscribeDocs`, `docs.docCreate`, `docs.sparqlQuery`, `storeRegistry.resolveScopeGraph`/`resolveWriteGraph`, `inbox.share`/`post`/`read`/`readSynced`/`readSyncedForDocument`/`processInbox`, `ng`, `initNg`. It is safely evolvable as far as this layer is concerned.
|
||||
|
||||
## Constraints
|
||||
|
||||
@@ -34,7 +34,11 @@ Everything else on the engagement is offered and NOT consumed: `watchShape`, `us
|
||||
|
||||
**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`. Our own tests execute that SPARQL against an engine that refuses anything else, so a removal is unrunnable rather than merely undetected. This constrains what we ask of the engagement: we need exactly one write primitive and no more.
|
||||
|
||||
**The inbox is opened at creation, from one place.** The engagement disclaims coalescing `openDocumentInbox` across pages, so we never open an index's inbox anywhere but in `createIndex`, under its owner, at the moment the document is created.
|
||||
**The inbox is opened at creation, from one place — and resolved, never opened, afterwards.** The engagement disclaims coalescing `openDocumentInbox` across pages, so the only call that can CREATE one is in `createIndex`, under its owner, at the moment the document is created. Every later call is on a document whose inbox already exists, where the same entry resolves the address instead; the engagement guarantees that idempotence within a page, which is the level our watching lives at.
|
||||
|
||||
**One session watches its own indexes, and nothing stops it.** `inbox.watch`'s unsubscribe is deliberately dropped: our watching lasts exactly as long as the identity is connected, which is what the engagement does with the inboxes it watches on its own account. It also means the inbox address never leaves the one adapter function that resolves it — everything above names a document.
|
||||
|
||||
**Our watching relies on subscriptions coexisting on one document.** `inbox.watch` opens a `subscribeDoc` on the inbox document, and the engagement already watches every inbox this identity may read. Before subscriptions coexisted, one of those two would have silenced the other with nothing raised anywhere. Verified in the resolved copy: the fan-out holds a set of listeners.
|
||||
|
||||
**Every payload out of an inbox is untrusted input.** Anyone may deposit anything, so nothing read from a deposit reaches a query before being checked; a non-reference is reported, never thrown on.
|
||||
|
||||
@@ -48,4 +52,8 @@ Everything else on the engagement is offered and NOT consumed: `watchShape`, `us
|
||||
|
||||
**No published escaping helpers.** The engagement lists no `escapeIri`/`escapeLiteral`, and we compose SPARQL against `docs.sparqlUpdate` — so this package carries its own escaping rather than reach into the provider's internals. That is a security-relevant duplication of something the provider certainly already has: two implementations of the same rule, one of which is not the one the provider tests.
|
||||
|
||||
**Watching a document's inbox needs an address, and the only call that hands one out is the one that creates one.** The engagement is explicit that an application never resolves an inbox address, and it publishes `inbox.watch(address, …)` with no `watchForDocument(doc, …)` beside `readForDocument`. So the one thing an owner cannot avoid — being told about deposits on its own document — is also the one place this layer must hold an address, obtained from `openDocumentInbox`, whose other job is to create. A `watchForDocument(doc, onDeposits)` would close that gap and keep the "never resolve an address" rule whole.
|
||||
|
||||
**The version declared and the version the guarantees describe disagree.** The resolved copy's `package.json` says `1.0.0-dev.1`, while the engagement dates the continuous inbox observation and the coexisting subscriptions to `1.0.0-dev.2`. The code has them, so nothing is broken; but a pin cannot express what we actually depend on, and `against:` above names the string we resolve rather than the one the guarantees belong to.
|
||||
|
||||
**No published way to write triples above the raw SPARQL primitive.** `docs.sparqlUpdate` is the level we had to align on, which is why the two frictions above exist at all. A published "add these triples to this document" would remove the query composition, the escaping and the NURI validation from this layer in one move.
|
||||
|
||||
Reference in New Issue
Block a user