feat!: la lecture quitte le contrat, un index se lit comme un document
This commit is contained in:
+64
-6
@@ -2,7 +2,7 @@
|
||||
* The application the end-to-end suite drives — written the way a consumer of
|
||||
* `@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
|
||||
* 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
|
||||
@@ -15,7 +15,7 @@
|
||||
* package (`indexing`, `polyfillPort`). If something here is awkward, it is awkward for
|
||||
* 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`.
|
||||
* 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
|
||||
@@ -25,6 +25,7 @@
|
||||
|
||||
import {
|
||||
configure,
|
||||
docs,
|
||||
ensureIdentity,
|
||||
init,
|
||||
readUnion,
|
||||
@@ -36,9 +37,9 @@ import { ng as realNg, init as realInit } from "@ng-org/web";
|
||||
|
||||
import { indexing, polyfillPort } 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
|
||||
// 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 } = {
|
||||
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 = {
|
||||
status: () => state.status,
|
||||
@@ -185,6 +225,24 @@ const bridge: IndexingBridge = {
|
||||
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[]> {
|
||||
const docs: Nuri[] = await storeRegistry.listMyEntityDocs("public");
|
||||
return [...docs];
|
||||
|
||||
Reference in New Issue
Block a user