Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c2f9ff4674 |
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
type: contract
|
type: contract
|
||||||
summary: The API @ng-helpers/indexing exposes to an application — creating an index, depositing references into it, reading it back; what a reference becomes is not a call: it becomes an entry once the index's creator is connected
|
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
|
||||||
---
|
---
|
||||||
|
|
||||||
# contract_indexing-layer — `@ng-helpers/indexing`
|
# contract_indexing-layer — `@ng-helpers/indexing`
|
||||||
@@ -9,101 +9,96 @@ 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.
|
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, depositing a reference to an object into one (open to anyone), and reading the entries back in order. What becomes of them is covered too, but never as a call — see `## Guarantees`.
|
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 does not cover NextGraph itself — documents, identity, sharing, 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 field. It **never removes anything**, anywhere — an engagement, not a missing feature.
|
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.
|
||||||
|
|
||||||
### Deployment requirements
|
### Deployment requirements
|
||||||
|
|
||||||
An application using this package must:
|
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;
|
- 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 and is also that identity's connection: awaiting it is what makes entries appear, dropping it is what stops them;
|
- **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: every operation here reaches NextGraph, and nothing is answered locally;
|
- reach a broker — nothing here is answered locally;
|
||||||
- **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, for reasons its own contract states;
|
- **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: entries are made while the creator is connected and by nothing else, so an index whose creator never returns stays as it was, however many references it is handed.
|
- **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.
|
||||||
|
|
||||||
One handle is one identity: the port carries a session, no call takes an identifier, and two users mean two handles.
|
**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.
|
||||||
|
|
||||||
**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. Nothing marks a document as an index, so that reference is the only way anyone reaches it, and losing it loses the index. **Hardcoding it in the source is what a single GLOBAL index needs, and only that case**: one index serving the whole application has nothing else to be discovered by. Anything narrower is discovered, not hardcoded.
|
**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.
|
||||||
|
|
||||||
**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, and `@ng-eventually/polyfill` arrives the same way. What this contract fixes is the version you pin and what you must provide alongside it.
|
|
||||||
|
|
||||||
## Surface
|
## Surface
|
||||||
|
|
||||||
Full typed shape: the package's `types` entry, `@ng-helpers/indexing`. The load-bearing signatures:
|
Full typed shape: the package's `types` entry. The load-bearing signatures:
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
// ── wiring: one handle, one identity, and that identity's connection ─────────
|
// wiring
|
||||||
export function polyfillPort(options: PolyfillPortOptions): NextGraphPort;
|
export function polyfillPort(options: PolyfillPortOptions): NextGraphPort;
|
||||||
export interface PolyfillPortOptions { readonly sessionId: string | number }
|
export interface PolyfillPortOptions { readonly sessionId: string | number }
|
||||||
/** This identity's handle, and its connection: see `## Guarantees`. */
|
/** This identity's handle, and its connection: see `## Guarantees`. */
|
||||||
export function indexing(port: NextGraphPort): Promise<Indexing>;
|
export function indexing(port: NextGraphPort): Promise<Indexing>;
|
||||||
|
|
||||||
// ── addressing (re-exported so you import them from here) ────────────────────
|
// addressing (import these from here)
|
||||||
export type Nuri = `did:ng:${string}`;
|
export type Nuri = `did:ng:${string}`;
|
||||||
export type NuriLike = Nuri | string;
|
export type NuriLike = Nuri | string;
|
||||||
export type { PrincipalId, UnionSubject, NextGraphPort, IncomingDeposit, ObjectResolution };
|
export type { UnionSubject, NextGraphPort, IncomingDeposit, ObjectResolution };
|
||||||
|
|
||||||
// ── the three acts an application performs ───────────────────────────────────
|
// the three acts
|
||||||
export interface Indexing {
|
export interface Indexing {
|
||||||
/** A new index in THIS identity's public store, and its NURI. Any user may. `field`
|
/** 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. */
|
* is the predicate an indexed object must carry; an empty or blank one throws. */
|
||||||
createIndex(field: string): Promise<Nuri>;
|
createIndex(field: string): Promise<Nuri>;
|
||||||
/** Deposits a bare reference to an object into the index. Open to ANYONE. Produces
|
/** Deposits a bare reference to an object into the index. Open to ANYONE. Throws
|
||||||
* nothing. Throws when the document cannot take one, rather than losing it. */
|
* when the document cannot take one, rather than losing it. */
|
||||||
refer(index: NuriLike, object: NuriLike): Promise<void>;
|
refer(index: NuriLike, object: NuriLike): Promise<void>;
|
||||||
/** The entries, ordered by value. Refuses a document that declares no index field
|
/** Convenience: the entries, ordered by value, without the index's own subject.
|
||||||
* rather than producing an empty list. Sugar over `readUnion([index])`. */
|
* Refuses a document declaring no field. NOT an engagement — see below. */
|
||||||
read(index: NuriLike): Promise<IndexEntry[]>;
|
read(index: NuriLike): Promise<IndexEntry[]>;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── what an index holds, and what travels from a depositor to an index ───────
|
// what an index holds, and what a depositor sends
|
||||||
export interface IndexEntry { readonly object: Nuri; readonly value: string }
|
export interface IndexEntry { readonly object: Nuri; readonly value: string }
|
||||||
export interface IndexDescriptor { readonly field: string }
|
export interface IndexDescriptor { readonly field: string }
|
||||||
export type IndexDeposit = Nuri; // the reference IS the whole payload
|
export type IndexDeposit = Nuri; // the reference IS the whole payload
|
||||||
export function decodeReference(payload: unknown): Nuri | null; // untrusted input
|
export function decodeReference(payload: unknown): Nuri | null; // untrusted input
|
||||||
|
|
||||||
// ── the IRIs, for a reader going straight to `readUnion` ─────────────────────
|
// the IRIs it is written with
|
||||||
export const INDEX_FIELD: string; // on the index's own subject: the field it indexes by
|
export const INDEX_FIELD: string; // "urn:ng-helpers:index:field"
|
||||||
export const ENTRY_VALUE: string; // on an entry: that object's value for the field
|
export const ENTRY_VALUE: string; // "urn:ng-helpers:index:value"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**What an index document holds** — two shapes, and this layer writes nothing else:
|
||||||
|
|
||||||
|
- 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.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
## Guarantees
|
## Guarantees
|
||||||
|
|
||||||
**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.
|
**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. There is nothing to call, schedule or configure, and no way to aim it at one index — the creator's session is the only thing that ever adds an entry, and it serves all of them at once.
|
**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.
|
||||||
|
|
||||||
**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, not in an application's source, stops two applications indexing one on two fields.
|
**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.
|
||||||
|
|
||||||
**A new index is ready the moment `createIndex` produces it.** It can be deposited into straight away, and the session that created it is already the one making its entries — nothing to open, register or reconnect.
|
**A new index is ready the moment `createIndex` produces it** — deposit into it straight away; nothing to open or register.
|
||||||
|
|
||||||
**Depositing is open to anyone; writing is the creator's alone.** `refer` is a deposit, not a write, 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.
|
**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.
|
||||||
|
|
||||||
**An index ONLY EVER GROWS.** No call removes an entry, for anyone including the creator, 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 answer to "this entry must go" is a fresh index.
|
**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.
|
||||||
|
|
||||||
**The same references produce the same index, whatever order they arrived in.** One reference deposited a hundred times leaves one entry; an object already indexed is passed over rather than read again; a burst settles exactly where those references would one at a time. The cost: no deposit is ever retired, so the work behind an index is linear in its whole history.
|
**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: reading an index and depositing into one never depended on that work. Every such failure is on this package's log stream, as is every reference that could not be resolved — harmless is not the same as invisible. Nothing is lost either way: a reference already deposited is still waiting, and the next connection makes its entry.
|
**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.
|
||||||
|
|
||||||
**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**, so 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 entry carrying several values keeps the smallest, deterministically** — which two sessions racing each other can produce, and which keeps the entry visible with every reader agreeing on it.
|
|
||||||
|
|
||||||
**The document's own declaration is read strictly for writing 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 gaining entries, loudly and permanently, and stays readable: picking one would order a single list by two properties, since entries already made are never revisited. That cannot be undone — start a fresh index.
|
|
||||||
|
|
||||||
**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, plus the index's own subject declaring its field, which `read` drops. `INDEX_FIELD` and `ENTRY_VALUE` are published for that reader.
|
|
||||||
|
|
||||||
**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.
|
**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.
|
||||||
|
|
||||||
**A BET, named as one — this layer's, not yours.** Nothing here is an application's to do or handle; it is what this layer stands on. That a document can receive deposits at all is aligned with NextGraph — a repository takes an inbox capability, at most one, on a real commit upstream. **What a deposit carries, and what receiving one does, are ours**: upstream's own set of deposit kinds is closed, carries no payload this could travel in, and offers no hook to extend it, so an index deposit has a shape NextGraph has not defined. When upstream defines it, this layer moves with it, and a `major` is how you hear about that.
|
|
||||||
|
|
||||||
## Non-guarantees
|
## Non-guarantees
|
||||||
|
|
||||||
**Nothing tells you what became of a reference.** 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, a self-reference and a payload that is no reference are not reported at all. Reading the index is how you find out.
|
**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.
|
||||||
|
|
||||||
**No timing.** Nothing says how long a reference takes to become an entry or lets you wait, and while its creator stays away it waits for as long as that lasts. There is no depth to inspect and no ordering between a deposit and a read.
|
**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 refresh.** An already-indexed object is never read again, so one whose value changes later keeps its original indefinitely.
|
**No refresh.** An already-indexed object is never read again, so one whose value changes later keeps its original indefinitely.
|
||||||
|
|
||||||
@@ -111,30 +106,30 @@ export const ENTRY_VALUE: string; // on an entry: that object's value for the
|
|||||||
|
|
||||||
**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.
|
**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, because nothing marks one as an index. An index whose read did not answer then is not found, silently, and its entries are made at the next connection.
|
**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, 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 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.
|
||||||
|
|
||||||
**No stable error text.** What a throw or a log line reads is for a human. Do not parse or branch on it.
|
|
||||||
|
|
||||||
**No cross-broker reach.** A NURI resolves for users of one broker.
|
**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 deposit any number of payloads into any index.
|
||||||
|
|
||||||
|
**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`.
|
||||||
|
|
||||||
## Change policy
|
## Change policy
|
||||||
|
|
||||||
**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.
|
**Semver, and majors are the normal case.** This layer sits on a polyfill still converging on a NextGraph that does not ship yet; several behaviours above are open questions and one part is a bet, and settling any narrows this surface.
|
||||||
|
|
||||||
- **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 you must react to counts; one that only accepts more 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 you must react to counts; one that only accepts more does not.
|
||||||
- **minor** — a symbol is added and nothing existing moves: a new read helper, a new optional option.
|
- **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.
|
- **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** (`2.0.1`); 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 one 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.1` — because this repository publishes exactly one engagement; should a second ever ship here, tags take the package name (`indexing/v…`).
|
**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`.
|
||||||
|
|
||||||
**`2.0.1` is prose, and one requirement corrected.** This surface was described throughout as an *inbox* you deposit into and a thing that gets *curated*; neither is yours to know — you deposit a reference and you read entries, and how that travels is this layer's business and free to change under you — so the same facts are stated as effects now. And "hardcode the index's NURI in your source" was stated unconditionally, fused with the creator-must-connect requirement; it is neither. A reference to an index is held however you hold any other, and hardcoding is what a single GLOBAL index needs. Creator-must-connect is unchanged and stays unconditional. **Nothing to migrate** — no symbol moved, no call behaves differently, and the requirement that changed asks less than before. A **patch** under the rule above, worth spelling out because the wording moved so much: no exported symbol changed, nothing under `## Guarantees` promises anything it did not promise in `2.0.0` — only how it is said — and the requirement that did change is a deployment one, asking less.
|
**`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.
|
||||||
|
|
||||||
**`2.0.0` removed `Indexing.curate(index)` — and `CurationReport`, `CurationOutcome`, `SkipReason` with it — and made `indexing(port)` a promise.** Removals, hence the major: that call asked an application to decide who owns an index and when its entries are made, neither of which is an application's decision. **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 — the log carries what did not resolve. `1.0.1` and `1.0.0` keep resolving and neither is forced to upgrade.
|
**`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.
|
||||||
|
|
||||||
Cut on `main`, so `2.0.1` is what you pin, and your `usage_` leaf anchors `against:` on that exact string — `against: @ng-helpers/indexing@2.0.1`.
|
Cut on `main`: pin `3.0.0`, and anchor your `usage_` leaf on `against: @ng-helpers/indexing@3.0.0`.
|
||||||
|
|
||||||
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.
|
**No changelog file and no deprecation window: the sections above are the release note.** Diff `## Guarantees`, `## Non-guarantees` and `## Surface` between two pulls.
|
||||||
|
|||||||
@@ -26,6 +26,24 @@ export interface BrokenInboxOutcome {
|
|||||||
readonly appeared: readonly string[];
|
readonly appeared: readonly string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* What one anchored SPARQL SELECT answered — or how it failed.
|
||||||
|
*
|
||||||
|
* The three fields are kept apart deliberately. "Nothing came back" and "the call failed"
|
||||||
|
* are different answers, and a shape that folded them together would let a failure read as
|
||||||
|
* an empty index — the defect class this repository keeps finding. `raw` carries the answer
|
||||||
|
* BEFORE anything here decodes it, so a decoder that is wrong about the result's shape
|
||||||
|
* cannot pass its own blindness off as a query that returned nothing.
|
||||||
|
*/
|
||||||
|
export interface SelectOutcome {
|
||||||
|
/** The message the query rejected with, or `null` when it returned. */
|
||||||
|
readonly failed: string | null;
|
||||||
|
/** Whatever came back, rendered as JSON — the answer before any decoding of it. */
|
||||||
|
readonly raw: string;
|
||||||
|
/** The SELECT's bindings, decoded to plain `variable → value` rows. */
|
||||||
|
readonly rows: ReadonlyArray<Readonly<Record<string, string>>>;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The acts this application can perform — and ONLY acts an application can perform.
|
* The acts this application can perform — and ONLY acts an application can perform.
|
||||||
*
|
*
|
||||||
@@ -74,6 +92,15 @@ export interface IndexingBridge {
|
|||||||
|
|
||||||
/** What a document literally holds, straight off `readUnion` — the write-form probe. */
|
/** What a document literally holds, straight off `readUnion` — the write-form probe. */
|
||||||
readRaw(doc: string): Promise<UnionSubject[]>;
|
readRaw(doc: string): Promise<UnionSubject[]>;
|
||||||
|
/**
|
||||||
|
* Run a SPARQL SELECT anchored on a document, through `docs.sparqlQuery`.
|
||||||
|
*
|
||||||
|
* An index is claimed to be an ORDINARY document, which means an ordinary query must
|
||||||
|
* reach it. Nothing in `src/` ever issues one — this layer composes SPARQL only to
|
||||||
|
* write — so this is the one act here that no code of this package performs, and it is
|
||||||
|
* on the bridge because the claim had never been measured against a broker.
|
||||||
|
*/
|
||||||
|
select(anchor: string, query: string): Promise<SelectOutcome>;
|
||||||
/** This identity's public documents. How an owner discovers a document it did not keep. */
|
/** This identity's public documents. How an owner discovers a document it did not keep. */
|
||||||
listPublicDocs(): Promise<string[]>;
|
listPublicDocs(): Promise<string[]>;
|
||||||
|
|
||||||
|
|||||||
+64
-6
@@ -2,7 +2,7 @@
|
|||||||
* The application the end-to-end suite drives — written the way a consumer of
|
* The application the end-to-end suite drives — written the way a consumer of
|
||||||
* `@ng-helpers/indexing` writes one, and nothing more.
|
* `@ng-helpers/indexing` writes one, and nothing more.
|
||||||
*
|
*
|
||||||
* ── Why an application and not a bag of library calls ──────────────────────
|
* Why an application and not a bag of library calls
|
||||||
* The 80 unit tests in `test/` run against a fake this repository wrote. They prove the
|
* The 80 unit tests in `test/` run against a fake this repository wrote. They prove the
|
||||||
* indexing RULES are consistent; they cannot prove that NextGraph does what the fake
|
* indexing RULES are consistent; they cannot prove that NextGraph does what the fake
|
||||||
* pretends, because the fake is the thing being asked. This page closes that gap by
|
* pretends, because the fake is the thing being asked. This page closes that gap by
|
||||||
@@ -15,7 +15,7 @@
|
|||||||
* package (`indexing`, `polyfillPort`). If something here is awkward, it is awkward for
|
* package (`indexing`, `polyfillPort`). If something here is awkward, it is awkward for
|
||||||
* every consumer, which is the second reason to write it this way.
|
* every consumer, which is the second reason to write it this way.
|
||||||
*
|
*
|
||||||
* ── The one thing here no application does ─────────────────────────────────
|
* The one thing here no application does
|
||||||
* `createIndexWithBrokenInbox` injects a failure into the inbox step of `createIndex`.
|
* `createIndexWithBrokenInbox` injects a failure into the inbox step of `createIndex`.
|
||||||
* That is a probe, it is named for what it is, and it exists because the question it
|
* That is a probe, it is named for what it is, and it exists because the question it
|
||||||
* answers — does a failed `openInbox` leave a document behind? — cannot be reached from
|
* answers — does a failed `openInbox` leave a document behind? — cannot be reached from
|
||||||
@@ -25,6 +25,7 @@
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
configure,
|
configure,
|
||||||
|
docs,
|
||||||
ensureIdentity,
|
ensureIdentity,
|
||||||
init,
|
init,
|
||||||
readUnion,
|
readUnion,
|
||||||
@@ -36,9 +37,9 @@ import { ng as realNg, init as realInit } from "@ng-org/web";
|
|||||||
|
|
||||||
import { indexing, polyfillPort } from "../src/index";
|
import { indexing, polyfillPort } from "../src/index";
|
||||||
import type { IndexEntry, Indexing, NextGraphPort } from "../src/index";
|
import type { IndexEntry, Indexing, NextGraphPort } from "../src/index";
|
||||||
import type { BrokenInboxOutcome, IndexingBridge } from "./bridge";
|
import type { BrokenInboxOutcome, IndexingBridge, SelectOutcome } from "./bridge";
|
||||||
|
|
||||||
// ── bootstrap: the one polyfill-era call, then the SDK-shaped ones ──────────
|
// bootstrap: the one polyfill-era call, then the SDK-shaped ones
|
||||||
//
|
//
|
||||||
// `sharedWallet` is declared because the access gate wants somewhere to point when it
|
// `sharedWallet` is declared because the access gate wants somewhere to point when it
|
||||||
// has to render, and never used: this suite always enters through the broker's redirect,
|
// has to render, and never used: this suite always enters through the broker's redirect,
|
||||||
@@ -64,7 +65,7 @@ const sessionReady = new Promise<{ session_id: string }>((resolve) => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
// ── this application's state ───────────────────────────────────────────────
|
// this application's state
|
||||||
|
|
||||||
const state: { status: string; error: string | null; who: string } = {
|
const state: { status: string; error: string | null; who: string } = {
|
||||||
status: "connecting",
|
status: "connecting",
|
||||||
@@ -134,7 +135,46 @@ async function publicDocsAfter(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── the acts ───────────────────────────────────────────────────────────────
|
/**
|
||||||
|
* How much of an answer a report carries. Bounded so a report stays one, generous enough
|
||||||
|
* that the answer is readable rather than merely counted.
|
||||||
|
*/
|
||||||
|
const RAW_LIMIT = 2000;
|
||||||
|
|
||||||
|
/** Whatever came back, as JSON — `undefined` and a value that will not render included,
|
||||||
|
* because both of those are answers too and a report that hides them is worth nothing. */
|
||||||
|
function render(result: unknown): string {
|
||||||
|
let text: string;
|
||||||
|
try {
|
||||||
|
text = JSON.stringify(result) ?? String(result);
|
||||||
|
} catch (e: unknown) {
|
||||||
|
text = `(did not render: ${String((e as Error)?.message ?? e)})`;
|
||||||
|
}
|
||||||
|
return text.length <= RAW_LIMIT ? text : `${text.slice(0, RAW_LIMIT)}…(${text.length} chars)`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The SELECT's bindings, in the shape the SPARQL results JSON specifies — the same
|
||||||
|
* `results.bindings` the polyfill itself reads out of this very call (`surface/inbox.ts`,
|
||||||
|
* `surface/read-model.ts`). A binding whose term carries no string `value` is dropped
|
||||||
|
* rather than guessed at; `raw` beside it is what keeps that honest.
|
||||||
|
*/
|
||||||
|
function rowsOf(result: unknown): Array<Record<string, string>> {
|
||||||
|
if (result === null || typeof result !== "object") return [];
|
||||||
|
const answered = result as {
|
||||||
|
results?: { bindings?: ReadonlyArray<Record<string, { value?: unknown } | undefined>> };
|
||||||
|
};
|
||||||
|
const bindings = answered.results?.bindings ?? [];
|
||||||
|
return bindings.map((binding) => {
|
||||||
|
const row: Record<string, string> = {};
|
||||||
|
for (const [variable, term] of Object.entries(binding)) {
|
||||||
|
if (term !== undefined && typeof term.value === "string") row[variable] = term.value;
|
||||||
|
}
|
||||||
|
return row;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// the acts
|
||||||
|
|
||||||
const bridge: IndexingBridge = {
|
const bridge: IndexingBridge = {
|
||||||
status: () => state.status,
|
status: () => state.status,
|
||||||
@@ -185,6 +225,24 @@ const bridge: IndexingBridge = {
|
|||||||
return readUnion([doc]);
|
return readUnion([doc]);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A SPARQL SELECT anchored on a document, through the polyfill's published `docs`.
|
||||||
|
*
|
||||||
|
* The session id is the one this application already holds — the same one every write
|
||||||
|
* of this layer is made with. Nothing is caught and rethrown: a rejection is REPORTED,
|
||||||
|
* because "the query failed" is a different answer from "the query found nothing" and
|
||||||
|
* the whole point of this probe is to tell them apart.
|
||||||
|
*/
|
||||||
|
async select(anchor: string, query: string): Promise<SelectOutcome> {
|
||||||
|
const session = await sessionReady;
|
||||||
|
try {
|
||||||
|
const result = await docs.sparqlQuery(session.session_id, query, undefined, anchor);
|
||||||
|
return { failed: null, raw: render(result), rows: rowsOf(result) };
|
||||||
|
} catch (e: unknown) {
|
||||||
|
return { failed: String((e as Error)?.message ?? e), raw: "(the query rejected)", rows: [] };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
async listPublicDocs(): Promise<string[]> {
|
async listPublicDocs(): Promise<string[]> {
|
||||||
const docs: Nuri[] = await storeRegistry.listMyEntityDocs("public");
|
const docs: Nuri[] = await storeRegistry.listMyEntityDocs("public");
|
||||||
return [...docs];
|
return [...docs];
|
||||||
|
|||||||
+129
-9
@@ -1,7 +1,7 @@
|
|||||||
/**
|
/**
|
||||||
* `@ng-helpers/indexing` against the REAL broker.
|
* `@ng-helpers/indexing` against the REAL broker.
|
||||||
*
|
*
|
||||||
* ── What this suite is for ─────────────────────────────────────────────────
|
* What this suite is for
|
||||||
* The unit suite proves the indexing rules are consistent with a fake this repository
|
* The unit suite proves the indexing rules are consistent with a fake this repository
|
||||||
* wrote. It cannot prove NextGraph behaves the way that fake pretends, because the fake
|
* wrote. It cannot prove NextGraph behaves the way that fake pretends, because the fake
|
||||||
* is the very thing in question. Two claims in particular had never met a broker:
|
* is the very thing in question. Two claims in particular had never met a broker:
|
||||||
@@ -19,7 +19,7 @@
|
|||||||
* reference — but the document exists. The last journey injects that failure and
|
* reference — but the document exists. The last journey injects that failure and
|
||||||
* asks the broker what was left behind.
|
* asks the broker what was left behind.
|
||||||
*
|
*
|
||||||
* ── Two identities, and how the index reference reaches the second ─────────
|
* Two identities, and how the index reference reaches the second
|
||||||
* The whole point of an index is that STRANGERS contribute to it. So Bob must reach
|
* The whole point of an index is that STRANGERS contribute to it. So Bob must reach
|
||||||
* Alice's index — and he must reach it the way an application would, not through a
|
* Alice's index — and he must reach it the way an application would, not through a
|
||||||
* variable in this file. An index is an ordinary document whose NURI an application
|
* variable in this file. An index is an ordinary document whose NURI an application
|
||||||
@@ -28,7 +28,7 @@
|
|||||||
* never happen — and does not happen here — is an inbox address crossing the identity
|
* never happen — and does not happen here — is an inbox address crossing the identity
|
||||||
* boundary through a channel no deployment has.
|
* boundary through a channel no deployment has.
|
||||||
*
|
*
|
||||||
* ── Reading a failure ──────────────────────────────────────────────────────
|
* Reading a failure
|
||||||
* A named deadline, or a message `ng-e2e-helpers` recognises as a browser or frame
|
* A named deadline, or a message `ng-e2e-helpers` recognises as a browser or frame
|
||||||
* failure, is the HOST. A failed check carrying an unexpected value is this code. The
|
* failure, is the HOST. A failed check carrying an unexpected value is this code. The
|
||||||
* report says which, and the run is repeated rather than anything being loosened.
|
* report says which, and the run is repeated rather than anything being loosened.
|
||||||
@@ -72,7 +72,7 @@ type BrowserContext = Awaited<ReturnType<typeof launchWatchedContext>>;
|
|||||||
type Page = Awaited<ReturnType<typeof newPage>>;
|
type Page = Awaited<ReturnType<typeof newPage>>;
|
||||||
type Frame = Awaited<ReturnType<typeof setupBrokerPage>>;
|
type Frame = Awaited<ReturnType<typeof setupBrokerPage>>;
|
||||||
|
|
||||||
// ── the domain this suite indexes by ───────────────────────────────────────
|
// the domain this suite indexes by
|
||||||
//
|
//
|
||||||
// A date, so the suite exercises the case the package is built around: an index "by a
|
// A date, so the suite exercises the case the package is built around: an index "by a
|
||||||
// date" is just an index whose field is a date predicate, and ISO-8601 sorts as a string.
|
// date" is just an index whose field is a date predicate, and ISO-8601 sorts as a string.
|
||||||
@@ -80,7 +80,7 @@ const PUBLISHED_AT = "urn:ng-helpers-e2e:published-at";
|
|||||||
/** A predicate an index does NOT curate on — for the object that carries nothing usable. */
|
/** A predicate an index does NOT curate on — for the object that carries nothing usable. */
|
||||||
const UNRELATED = "urn:ng-helpers-e2e:unrelated";
|
const UNRELATED = "urn:ng-helpers-e2e:unrelated";
|
||||||
|
|
||||||
// ── bounds ─────────────────────────────────────────────────────────────────
|
// bounds
|
||||||
//
|
//
|
||||||
// Sized to be generous rather than tight. A bound exists to turn a hang into a named
|
// Sized to be generous rather than tight. A bound exists to turn a hang into a named
|
||||||
// failure; sized to the median it would instead fail on a slow-but-healthy broker, which
|
// failure; sized to the median it would instead fail on a slow-but-healthy broker, which
|
||||||
@@ -100,7 +100,7 @@ const JOURNEY_MS = 10 * 60_000;
|
|||||||
/** The whole run. A budget that cannot interrupt anything is not a budget. */
|
/** The whole run. A budget that cannot interrupt anything is not a budget. */
|
||||||
const SUITE_MS = 30 * 60_000;
|
const SUITE_MS = 30 * 60_000;
|
||||||
|
|
||||||
// ── the report ─────────────────────────────────────────────────────────────
|
// the report
|
||||||
|
|
||||||
let actors: BrowserContext | null = null;
|
let actors: BrowserContext | null = null;
|
||||||
|
|
||||||
@@ -138,7 +138,7 @@ const { check, journey, finish } = declareSuite({
|
|||||||
checks: [
|
checks: [
|
||||||
"Alice reads exactly one entry, and it is Bob's object",
|
"Alice reads exactly one entry, and it is Bob's object",
|
||||||
"Bob, who does not own the index, reads the same entry",
|
"Bob, who does not own the index, reads the same entry",
|
||||||
"curating a second time changes nothing, and the index still holds one entry",
|
"connecting a second time changes nothing, and the index still holds one entry",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -162,15 +162,49 @@ const { check, journey, finish } = declareSuite({
|
|||||||
"the index holds it as one entry, and its own descriptor is untouched",
|
"the index holds it as one entry, and its own descriptor is untouched",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "The index answers an ordinary SPARQL query",
|
||||||
|
checks: [
|
||||||
|
"readUnion returns both entries and the index's own declaration",
|
||||||
|
"a SELECT for the entry predicate returns both entries, with their values",
|
||||||
|
"a SELECT of the index's own subject returns the field it declares",
|
||||||
|
"a stranger's SELECT returns the same entries",
|
||||||
|
],
|
||||||
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Two readings of the same entries — the same objects, carrying the same values.
|
||||||
|
*
|
||||||
|
* Compared as a WHOLE: a query that answered with a subset, or with a value that changed
|
||||||
|
* shape crossing the round trip, is not the same answer as the document's own content.
|
||||||
|
*/
|
||||||
|
function sameEntries(a: ReadonlyMap<string, string>, b: ReadonlyMap<string, string>): boolean {
|
||||||
|
if (a.size !== b.size) return false;
|
||||||
|
for (const [object, value] of a) {
|
||||||
|
if (b.get(object) !== value) return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The `?object`/`?value` rows of an entries SELECT, as the entries they claim to be. */
|
||||||
|
function entriesOf(rows: ReadonlyArray<Readonly<Record<string, string>>>): Map<string, string> {
|
||||||
|
const found = new Map<string, string>();
|
||||||
|
for (const row of rows) {
|
||||||
|
const object = row["object"];
|
||||||
|
const value = row["value"];
|
||||||
|
if (object !== undefined && value !== undefined) found.set(object, value);
|
||||||
|
}
|
||||||
|
return found;
|
||||||
|
}
|
||||||
|
|
||||||
/** A named step that is both measured and bounded — `evaluate` carries no timeout of its own. */
|
/** A named step that is both measured and bounded — `evaluate` carries no timeout of its own. */
|
||||||
function step<T>(what: string, ms: number, task: () => Promise<T>): Promise<T> {
|
function step<T>(what: string, ms: number, task: () => Promise<T>): Promise<T> {
|
||||||
return measured(what, ms, (bound) => within(what, bound, task));
|
return measured(what, ms, (bound) => within(what, bound, task));
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── an actor ───────────────────────────────────────────────────────────────
|
// an actor
|
||||||
|
|
||||||
interface Actor {
|
interface Actor {
|
||||||
readonly id: string;
|
readonly id: string;
|
||||||
@@ -250,7 +284,7 @@ function actorIsUp(id: string, actor: () => Actor | null): Prerequisite {
|
|||||||
return () => (actor() === null ? `${id} never signed in` : null);
|
return () => (actor() === null ? `${id} never signed in` : null);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── the run ────────────────────────────────────────────────────────────────
|
// the run
|
||||||
|
|
||||||
async function main(): Promise<void> {
|
async function main(): Promise<void> {
|
||||||
armSuiteDeadline("ng-helpers indexing e2e", SUITE_MS, () =>
|
armSuiteDeadline("ng-helpers indexing e2e", SUITE_MS, () =>
|
||||||
@@ -602,6 +636,92 @@ async function main(): Promise<void> {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// AFTER the hostile journey, and read-only: by here the index holds SEVERAL entries,
|
||||||
|
// which is the state the question is about — one entry cannot tell a query that
|
||||||
|
// returns everything from one that returns the first thing it finds.
|
||||||
|
//
|
||||||
|
// WHAT IS BEING ASKED. This package documents one way of reading an index
|
||||||
|
// (`readUnion`) and issues no query of its own, so "an index is an ordinary document
|
||||||
|
// anyone queries normally" has never been anything but plausible. These four checks
|
||||||
|
// are a measurement of that sentence, not a feature: an empty answer is a RESULT and
|
||||||
|
// is reported as one, and a rejection is reported apart from it, because "the query
|
||||||
|
// found nothing" and "the query failed" are the two answers this repository keeps
|
||||||
|
// finding folded into one.
|
||||||
|
await journey({
|
||||||
|
name: "The index answers an ordinary SPARQL query",
|
||||||
|
needs: [aliceIsUp, bobIsUp, indexExists],
|
||||||
|
run: async () => {
|
||||||
|
// The REFERENCE the queries below are judged against. "The SELECT came back with
|
||||||
|
// the entries" is only a claim if something independent says what the entries
|
||||||
|
// are — and it is what makes an empty answer below mean something instead of
|
||||||
|
// being indistinguishable from an index that holds nothing.
|
||||||
|
const raw = await step("Alice reading the index before querying it", BRIDGE_MS, () =>
|
||||||
|
alice!.frame.evaluate((d) => window.__indexing.readRaw(d), index!),
|
||||||
|
);
|
||||||
|
const held = new Map<string, string>();
|
||||||
|
for (const subject of raw) {
|
||||||
|
const value = subject.props[ENTRY_VALUE] ?? [];
|
||||||
|
if (value.length === 1 && value[0] !== undefined) held.set(subject.subject, value[0]);
|
||||||
|
}
|
||||||
|
const declares = (raw.find((s) => s.subject === index)?.props[INDEX_FIELD] ?? []).includes(
|
||||||
|
PUBLISHED_AT,
|
||||||
|
);
|
||||||
|
check(
|
||||||
|
"readUnion returns both entries and the index's own declaration",
|
||||||
|
held.size === 2 && declares,
|
||||||
|
`entries=${JSON.stringify([...held.keys()])} declares=${declares}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
// The shape a reader would write, with nothing of this package in it: the entry
|
||||||
|
// predicate, and the anchored default graph the index document is.
|
||||||
|
const entriesQuery = `SELECT ?object ?value WHERE { ?object <${ENTRY_VALUE}> ?value }`;
|
||||||
|
const mine = await step("Alice querying the index for its entries", BRIDGE_MS, () =>
|
||||||
|
alice!.frame.evaluate(
|
||||||
|
([a, q]) => window.__indexing.select(a!, q!),
|
||||||
|
[index!, entriesQuery],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
const answered = entriesOf(mine.rows);
|
||||||
|
check(
|
||||||
|
"a SELECT for the entry predicate returns both entries, with their values",
|
||||||
|
mine.failed === null && sameEntries(held, answered),
|
||||||
|
`failed=${mine.failed} rows=${mine.rows.length} ` +
|
||||||
|
`objects=${JSON.stringify([...answered.keys()])} raw=${mine.raw}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
// The index document's OWN subject, which is the other half of what an index
|
||||||
|
// holds — and the half a reader needs to know what the values mean.
|
||||||
|
const fieldQuery = `SELECT ?field WHERE { <${index!}> <${INDEX_FIELD}> ?field }`;
|
||||||
|
const declared = await step("Alice querying the index's declaration", BRIDGE_MS, () =>
|
||||||
|
alice!.frame.evaluate(([a, q]) => window.__indexing.select(a!, q!), [index!, fieldQuery]),
|
||||||
|
);
|
||||||
|
check(
|
||||||
|
"a SELECT of the index's own subject returns the field it declares",
|
||||||
|
declared.failed === null &&
|
||||||
|
declared.rows.length === 1 &&
|
||||||
|
declared.rows[0]?.["field"] === PUBLISHED_AT,
|
||||||
|
`failed=${declared.failed} rows=${declared.rows.length} raw=${declared.raw}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
// The reader who matters: an index exists to be read by people who own neither it
|
||||||
|
// nor anything in it. `readUnion` already answers him (the journey above); whether
|
||||||
|
// a query does is a separate question, and it is the one an application asks.
|
||||||
|
const theirs = await step("Bob querying the index he does not own", BRIDGE_MS, () =>
|
||||||
|
bob!.frame.evaluate(
|
||||||
|
([a, q]) => window.__indexing.select(a!, q!),
|
||||||
|
[index!, entriesQuery],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
const strangers = entriesOf(theirs.rows);
|
||||||
|
check(
|
||||||
|
"a stranger's SELECT returns the same entries",
|
||||||
|
theirs.failed === null && sameEntries(held, strangers),
|
||||||
|
`failed=${theirs.failed} rows=${theirs.rows.length} ` +
|
||||||
|
`objects=${JSON.stringify([...strangers.keys()])} raw=${theirs.raw}`,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
} finally {
|
} finally {
|
||||||
if (ctx !== null) await closeContext("actors", ctx);
|
if (ctx !== null) await closeContext("actors", ctx);
|
||||||
if (closeServer !== null) {
|
if (closeServer !== null) {
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@ng-helpers/indexing",
|
"name": "@ng-helpers/indexing",
|
||||||
"version": "2.0.1",
|
"version": "3.0.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "An indexing layer built on top of NextGraph, via @ng-eventually/polyfill. An index is an ordinary public document; anyone may hand it a reference to an object, and that reference becomes an entry once the index's creator is connected.",
|
"description": "An indexing layer built on top of NextGraph, via @ng-eventually/polyfill. An index is an ordinary public document; anyone may hand it a reference to an object, and that reference becomes an entry once the index's creator is connected.",
|
||||||
|
|||||||
Reference in New Issue
Block a user