feat!: le paquet crée un index et lui ajoute une référence, rien de plus
This commit is contained in:
+79
-41
@@ -7,20 +7,27 @@
|
||||
* 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.
|
||||
* crosses the real broker, and calls `indexing(sessionId)` 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.
|
||||
* What this application takes from `@ng-helpers/indexing` is its WHOLE published
|
||||
* surface: `indexing(sessionId)`, the two acts on what it hands back, and the two IRIs.
|
||||
* Nothing here makes an entry of a deposit, and nothing here can ask for one: that is
|
||||
* the business of the layer below, and this page is where that shows or does not.
|
||||
* Everything else here is the polyfill (`configure`, `ensureIdentity`, `init`,
|
||||
* `readUnion`, `docs`, `storeRegistry`) — including reading the index, which is an
|
||||
* ordinary `readUnion` of an ordinary document. 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.
|
||||
* The two things here no application does, and why they reach past the surface
|
||||
* `createIndexWithBrokenInbox` injects a failure into the inbox step of `create`: the
|
||||
* question it answers — does a failed `openInbox` leave a document behind? — cannot be
|
||||
* reached from outside, because nothing a caller controls makes a real
|
||||
* `openDocumentInbox` fail on demand. And `publishObject` writes through the very
|
||||
* primitive `create` declares an index's field with, which is what makes it the CONTROL
|
||||
* for the write-form question. Both therefore build the package's INTERNAL port, and
|
||||
* both are named for what they are. Everything around them is real, including the
|
||||
* broker.
|
||||
*/
|
||||
|
||||
import {
|
||||
@@ -35,9 +42,13 @@ import {
|
||||
} from "@ng-eventually/polyfill";
|
||||
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, SelectOutcome } from "./bridge";
|
||||
// The whole published surface of the package under test.
|
||||
import { ENTRY_VALUE, indexing, type Indexing } from "../src/index";
|
||||
// NOT published, and reached only by the two probes above — see the header.
|
||||
import { indexingOn } from "../src/indexing";
|
||||
import type { NextGraphPort } from "../src/port";
|
||||
import { polyfillPort } from "../src/polyfill-adapter";
|
||||
import type { BrokenInboxOutcome, IndexRow, IndexingBridge, SelectOutcome } from "./bridge";
|
||||
|
||||
// bootstrap: the one polyfill-era call, then the SDK-shaped ones
|
||||
//
|
||||
@@ -54,7 +65,7 @@ configure({
|
||||
// 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.
|
||||
// `indexing(sessionId)` takes one, exactly as the real SDK's primitives do.
|
||||
const sessionReady = new Promise<{ session_id: string }>((resolve) => {
|
||||
init(
|
||||
(event: { status: string; session?: { session_id: string } }) => {
|
||||
@@ -74,7 +85,6 @@ const state: { status: string; error: string | null; who: string } = {
|
||||
};
|
||||
|
||||
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 {
|
||||
@@ -86,11 +96,11 @@ async function boot(): Promise<void> {
|
||||
// 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 });
|
||||
// One await, and curation is part of it: obtaining the handle processes the inboxes
|
||||
// of the indexes this identity owns and leaves them watched. This application never
|
||||
// curates anything, and has nothing to call if it wanted to.
|
||||
api = await indexing(port);
|
||||
// The session id, and nothing else — this application never names a port, and never
|
||||
// awaits anything here: a handle reaches nothing. A reference this application hands
|
||||
// an index becomes an entry because the layer below processes that index's inbox,
|
||||
// and there is nothing to call, schedule or configure for it.
|
||||
api = indexing(session.session_id);
|
||||
state.status = "ready";
|
||||
}
|
||||
|
||||
@@ -107,11 +117,13 @@ function ready(): Indexing {
|
||||
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;
|
||||
/**
|
||||
* The package's INTERNAL port, for the two probes that need one. Never used by an act
|
||||
* this application performs as an application — see the header for why each needs it.
|
||||
*/
|
||||
async function probePort(): Promise<NextGraphPort> {
|
||||
const session = await sessionReady;
|
||||
return polyfillPort({ sessionId: session.session_id });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -159,6 +171,10 @@ function render(result: unknown): string {
|
||||
* `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 compare(a: string, b: string): number {
|
||||
return a < b ? -1 : a > b ? 1 : 0;
|
||||
}
|
||||
|
||||
function rowsOf(result: unknown): Array<Record<string, string>> {
|
||||
if (result === null || typeof result !== "object") return [];
|
||||
const answered = result as {
|
||||
@@ -183,42 +199,64 @@ const bridge: IndexingBridge = {
|
||||
configuredIndex,
|
||||
|
||||
async createIndex(field: string): Promise<string> {
|
||||
return ready().createIndex(field);
|
||||
return ready().create(field);
|
||||
},
|
||||
|
||||
/**
|
||||
* Publish a public document carrying one value for one predicate.
|
||||
*
|
||||
* It goes through the SAME primitive the curator writes an entry with
|
||||
* It goes through the SAME primitive `create` declares an index's field 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<string> {
|
||||
const p = readyPort();
|
||||
const p = await probePort();
|
||||
const doc = await p.createPublicDocument();
|
||||
await p.addLiteralProperty(doc, doc, predicate, value);
|
||||
return doc;
|
||||
},
|
||||
|
||||
async referConfigured(object: string): Promise<void> {
|
||||
async addToConfigured(object: string): Promise<void> {
|
||||
const index = configuredIndex();
|
||||
if (index === null) {
|
||||
throw new Error("[e2e] this application was not configured with an index reference");
|
||||
}
|
||||
await ready().refer(index, object);
|
||||
await ready().add(index, object);
|
||||
},
|
||||
|
||||
async referTo(index: string, object: string): Promise<void> {
|
||||
await ready().refer(index, object);
|
||||
async addTo(index: string, object: string): Promise<void> {
|
||||
await ready().add(index, object);
|
||||
},
|
||||
|
||||
async reconnect(): Promise<void> {
|
||||
api = await indexing(readyPort());
|
||||
async rebuildHandle(): Promise<void> {
|
||||
const session = await sessionReady;
|
||||
api = indexing(session.session_id);
|
||||
},
|
||||
|
||||
async read(index: string): Promise<IndexEntry[]> {
|
||||
return ready().read(index);
|
||||
/**
|
||||
* The index read BY THIS APPLICATION, with nothing the package publishes but
|
||||
* `ENTRY_VALUE` — the claim "an index is an ordinary document" performed rather than
|
||||
* repeated. `readUnion([index])` is the same call this page makes on any other
|
||||
* document; the index's own subject is the one that is not an entry, told apart by
|
||||
* being the document itself.
|
||||
*/
|
||||
async read(index: string): Promise<IndexRow[]> {
|
||||
const subjects = await readUnion([index]);
|
||||
const rows: IndexRow[] = [];
|
||||
for (const subject of subjects) {
|
||||
if (subject.subject === index) continue;
|
||||
for (const value of subject.props[ENTRY_VALUE] ?? []) {
|
||||
rows.push({ object: subject.subject, value });
|
||||
}
|
||||
}
|
||||
// Ordered by value, ties broken on the object, so two readers of the same document
|
||||
// see the same order. The package used to do this; an application does it in four
|
||||
// lines, and gets to choose differently.
|
||||
rows.sort((a, b) =>
|
||||
a.value === b.value ? compare(a.object, b.object) : compare(a.value, b.value),
|
||||
);
|
||||
return rows;
|
||||
},
|
||||
|
||||
async readRaw(doc: string): Promise<UnionSubject[]> {
|
||||
@@ -249,13 +287,13 @@ const bridge: IndexingBridge = {
|
||||
},
|
||||
|
||||
async createIndexWithBrokenInbox(field: string): Promise<BrokenInboxOutcome> {
|
||||
const p = readyPort();
|
||||
const p = await probePort();
|
||||
const before = new Set<string>(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 = await indexing({
|
||||
const broken = indexingOn({
|
||||
...p,
|
||||
openInbox: async (): Promise<void> => {
|
||||
throw new Error("[e2e] injected: the inbox could not be opened");
|
||||
@@ -265,7 +303,7 @@ const bridge: IndexingBridge = {
|
||||
let rejected: string | null = null;
|
||||
let returned: string | null = null;
|
||||
try {
|
||||
returned = await broken.createIndex(field);
|
||||
returned = await broken.create(field);
|
||||
} catch (e: unknown) {
|
||||
rejected = String((e as Error)?.message ?? e);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user