/** * 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 ────────────────────── * The 69 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 * putting the real broker underneath: it imports `@ng-eventually/polyfill` for real, * crosses the real broker, and calls `indexing(polyfillPort(...))` exactly as an * application would. * * It reaches nothing private. Every import below is a published entry — of the polyfill * (`configure`, `ensureIdentity`, `init`, `readUnion`, `storeRegistry`) or of this * 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 ───────────────────────────────── * `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 * outside: nothing a caller controls makes a real `openDocumentInbox` fail on demand. * Everything around the injection is real, including the broker and the document. */ import { configure, ensureIdentity, init, readUnion, storeRegistry, type Nuri, type UnionSubject, } from "@ng-eventually/polyfill"; import { ng as realNg, init as realInit } from "@ng-org/web"; import { indexing, polyfillPort } from "../src/index"; import type { CurationReport, IndexEntry, Indexing, NextGraphPort, } from "../src/index"; import type { BrokenInboxOutcome, IndexingBridge } from "./bridge"; // ── 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, // where the wallet is already open in the run's profile. Nothing is served at that path. configure({ ng: realNg, useShape: () => undefined, // this application reads through `readUnion`, not the ORM init: realInit, sharedWallet: { fileUrl: "/wallet-never-served.ngw", password: "" }, }); // The library's `init`, not the injected one: it settles the identity BEFORE handing the // page to the broker, so the round-trip leaves with `?ng-id=` in the address it carries. // The callback is this application's own business — it keeps the session because // `polyfillPort` takes a session id, exactly as the real SDK's primitives do. const sessionReady = new Promise<{ session_id: string }>((resolve) => { init( (event: { status: string; session?: { session_id: string } }) => { if (event.status === "loggedin" && event.session) resolve(event.session); }, true, [], ); }); // ── this application's state ─────────────────────────────────────────────── const state: { status: string; error: string | null; who: string } = { status: "connecting", error: null, who: "", }; let api: Indexing | null = null; let port: NextGraphPort | null = null; /** The index this deployment contributes to, read off its own configuration. */ function configuredIndex(): string | null { return new URLSearchParams(window.location.search).get("index"); } async function boot(): Promise { // One await, and it covers everything: the identity settles, the connection work runs, // and the identity comes back. The application keeps it only to show it. state.who = await ensureIdentity(); const session = await sessionReady; port = polyfillPort({ sessionId: session.session_id }); api = indexing(port); state.status = "ready"; } void boot().catch((e: unknown) => { state.status = "failed"; state.error = String((e as Error)?.message ?? e); }); /** The library, once the page is up. Throws with the boot's own reason if it is not. */ function ready(): Indexing { if (api === null) { throw new Error(`[e2e] the application is not ready (${state.status}): ${state.error ?? "still connecting"}`); } return api; } function readyPort(): NextGraphPort { if (port === null) { throw new Error(`[e2e] the application is not ready (${state.status}): ${state.error ?? "still connecting"}`); } return port; } /** * Wait for a document to appear in this identity's public store. * * A store listing is a read like any other, and a document written a moment ago is not * owed to be in it instantly. Polling is therefore what an owner would actually do, and * it is bounded: an empty answer at the end is evidence, not a hang. */ async function publicDocsAfter( before: ReadonlySet, budgetMs: number, ): Promise { const deadline = Date.now() + budgetMs; let appeared: readonly string[] = []; for (;;) { const now = await storeRegistry.listMyEntityDocs("public"); appeared = now.filter((d) => !before.has(d)); if (appeared.length > 0 || Date.now() >= deadline) return appeared; await new Promise((r) => setTimeout(r, 500)); } } // ── the acts ─────────────────────────────────────────────────────────────── const bridge: IndexingBridge = { status: () => state.status, error: () => state.error, whoami: () => state.who, configuredIndex, async createIndex(field: string): Promise { return ready().createIndex(field); }, /** * Publish a public document carrying one value for one predicate. * * It goes through the SAME primitive the curator writes an entry with * (`addLiteralProperty`), with the document as its own subject. That makes it the * CONTROL for the write-form question: if this round-trips and an index entry does * not, the difference is the foreign subject and nothing else. */ async publishObject(predicate: string, value: string): Promise { const p = readyPort(); const doc = await p.createPublicDocument(); await p.addLiteralProperty(doc, doc, predicate, value); return doc; }, async referConfigured(object: string): Promise { const index = configuredIndex(); if (index === null) { throw new Error("[e2e] this application was not configured with an index reference"); } await ready().refer(index, object); }, async referTo(index: string, object: string): Promise { await ready().refer(index, object); }, async curate(index: string): Promise { return ready().curate(index); }, async read(index: string): Promise { return ready().read(index); }, async readRaw(doc: string): Promise { return readUnion([doc]); }, async listPublicDocs(): Promise { const docs: Nuri[] = await storeRegistry.listMyEntityDocs("public"); return [...docs]; }, async createIndexWithBrokenInbox(field: string): Promise { const p = readyPort(); const before = new Set(await storeRegistry.listMyEntityDocs("public")); // Everything real except the inbox step. The failure is injected at the exact moment // the question is about: after the document exists and carries its descriptor, before // anyone can deposit into it. const broken = indexing({ ...p, openInbox: async (): Promise => { throw new Error("[e2e] injected: the inbox could not be opened"); }, }); let rejected: string | null = null; let returned: string | null = null; try { returned = await broken.createIndex(field); } catch (e: unknown) { rejected = String((e as Error)?.message ?? e); } return { rejected, returned, appeared: await publicDocsAfter(before, 15_000) }; }, }; window.__indexing = bridge;