feat!: curer n'est plus un appel, c'est ce que fait le traitement de l'inbox

This commit is contained in:
Sylvain Duchesne
2026-08-20 11:04:31 +02:00
parent 2ce2113157
commit e2ed970cbd
21 changed files with 956 additions and 273 deletions
+10 -3
View File
@@ -14,7 +14,7 @@
* ends up typed as a reference.
*/
import type { CurationReport, IndexEntry, UnionSubject } from "../src/index";
import type { IndexEntry, UnionSubject } from "../src/index";
/** What the leak probe observed — see `run.ts`'s last journey. */
export interface BrokenInboxOutcome {
@@ -60,8 +60,15 @@ export interface IndexingBridge {
referConfigured(object: string): Promise<void>;
/** Hand a NAMED index a reference — used where no identity boundary is crossed. */
referTo(index: string, object: string): Promise<void>;
/** Resolve the references this index received and add what can be added. Owner only. */
curate(index: string): Promise<CurationReport>;
/**
* Connect again — obtain a fresh `Indexing` handle, which is what a page load does.
*
* There is no curating act to drive: an index is curated at its creator's next
* connection and on each deposit while the creator is connected. This is the first
* of the two, driven deliberately so a journey has a point at which the catching
* up is over; the second needs nothing from anyone.
*/
reconnect(): Promise<void>;
/** The index's entries, ordered by value. */
read(index: string): Promise<IndexEntry[]>;
+9 -11
View File
@@ -3,7 +3,7 @@
* `@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
* 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
* putting the real broker underneath: it imports `@ng-eventually/polyfill` for real,
@@ -35,12 +35,7 @@ import {
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 { IndexEntry, Indexing, NextGraphPort } from "../src/index";
import type { BrokenInboxOutcome, IndexingBridge } from "./bridge";
// ── bootstrap: the one polyfill-era call, then the SDK-shaped ones ──────────
@@ -91,7 +86,10 @@ async function boot(): Promise<void> {
state.who = await ensureIdentity();
const session = await sessionReady;
port = polyfillPort({ sessionId: session.session_id });
api = indexing(port);
// 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);
state.status = "ready";
}
@@ -175,8 +173,8 @@ const bridge: IndexingBridge = {
await ready().refer(index, object);
},
async curate(index: string): Promise<CurationReport> {
return ready().curate(index);
async reconnect(): Promise<void> {
api = await indexing(readyPort());
},
async read(index: string): Promise<IndexEntry[]> {
@@ -199,7 +197,7 @@ const bridge: IndexingBridge = {
// 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({
const broken = await indexing({
...p,
openInbox: async (): Promise<void> => {
throw new Error("[e2e] injected: the inbox could not be opened");
+38 -49
View File
@@ -126,12 +126,11 @@ const { check, journey, finish } = declareSuite({
],
},
{
name: "Bob hands the index a reference, and Alice curates it",
name: "Bob hands the index a reference, and Alice's next connection curates it",
checks: [
"a stranger's deposit into the index's inbox is accepted",
"curation reports Bob's object as indexed",
"the indexed value was read off Bob's object, and never travelled in his deposit",
"a stranger's deposit into the index's inbox reached its owner and became an entry",
"the entry is stored under Bob's object's own reference as its subject",
"the indexed value was read off Bob's object, and never travelled in his deposit",
],
},
{
@@ -145,8 +144,7 @@ const { check, journey, finish } = declareSuite({
{
name: "An object carrying nothing for the field is not indexed",
checks: [
"curation reports it skipped for want of the field, rather than indexed",
"the index still holds exactly one entry",
"the unrelated object is not indexed, and the index still holds exactly one entry",
],
},
{
@@ -365,7 +363,7 @@ async function main(): Promise<void> {
});
await journey({
name: "Bob hands the index a reference, and Alice curates it",
name: "Bob hands the index a reference, and Alice's next connection curates it",
needs: [
aliceIsUp,
bobIsUp,
@@ -379,30 +377,11 @@ async function main(): Promise<void> {
bob!.frame.evaluate((o) => window.__indexing.referConfigured(o), bobsObject!),
);
const report = await step("Alice curating", BRIDGE_MS, () =>
alice!.frame.evaluate((i) => window.__indexing.curate(i), index!),
);
// The deposit is proven ARRIVED, by the only person who can see it. That the post
// did not throw is a weaker claim entirely — it says the call returned, not that
// anything crossed the identity boundary — and asserting it would be asserting a
// constant. Alice reads her own inbox; one outcome means one deposit reached it.
check(
"a stranger's deposit into the index's inbox is accepted",
report.outcomes.length === 1,
`from=${BOB} outcomes=${report.outcomes.length}`,
);
const forBob = report.outcomes.find(
(o) => "object" in o && o.object === bobsObject,
);
check(
"curation reports Bob's object as indexed",
forBob?.result === "indexed",
`outcomes=${JSON.stringify(report.outcomes)}`,
);
check(
"the indexed value was read off Bob's object, and never travelled in his deposit",
forBob?.result === "indexed" && forBob.value === "2026-08-17T09:00:00Z",
`value=${forBob !== undefined && "value" in forBob ? forBob.value : "(none)"}`,
// NOBODY CURATES: there is nothing on the surface to call. Alice's page connects
// again — what a page load does — and her session processes the inboxes of the
// indexes she owns, this one among them.
await step("Alice connecting again", BRIDGE_MS, () =>
alice!.frame.evaluate(() => window.__indexing.reconnect()),
);
// THE WRITE FORM, answered. An entry is a triple whose subject is another
@@ -412,11 +391,27 @@ async function main(): Promise<void> {
alice!.frame.evaluate((d) => window.__indexing.readRaw(d), index!),
);
const entry = raw.find((s) => s.subject === bobsObject);
// The deposit is proven ARRIVED by its only possible effect: nobody but Alice
// reads that inbox, so an entry for Bob's object means his deposit crossed the
// identity boundary and her session found it. That the post did not throw is a
// weaker claim entirely — it says the call returned, and nothing more.
check(
"a stranger's deposit into the index's inbox reached its owner and became an entry",
entry !== undefined,
`from=${BOB} subjects=${JSON.stringify(raw.map((s) => s.subject))}`,
);
check(
"the entry is stored under Bob's object's own reference as its subject",
(entry?.props[ENTRY_VALUE] ?? []).includes("2026-08-17T09:00:00Z"),
entry !== undefined && Object.hasOwn(entry.props, ENTRY_VALUE),
`subjects=${JSON.stringify(raw.map((s) => s.subject))}`,
);
// The value never travelled: a deposit is the reference and nothing else, so its
// presence here means the curation read it off Bob's object itself.
check(
"the indexed value was read off Bob's object, and never travelled in his deposit",
(entry?.props[ENTRY_VALUE] ?? []).includes("2026-08-17T09:00:00Z"),
`entry=${JSON.stringify(entry?.props ?? {})}`,
);
},
});
@@ -446,16 +441,16 @@ async function main(): Promise<void> {
// Deposits are never retired, so every run sees every deposit again. Convergence
// is what makes that affordable.
const again = await step("Alice curating a second time", BRIDGE_MS, () =>
alice!.frame.evaluate((i) => window.__indexing.curate(i), index!),
await step("Alice connecting a second time", BRIDGE_MS, () =>
alice!.frame.evaluate(() => window.__indexing.reconnect()),
);
const still = await step("Alice reading the index again", BRIDGE_MS, () =>
alice!.frame.evaluate((i) => window.__indexing.read(i), index!),
);
check(
"curating a second time changes nothing, and the index still holds one entry",
again.outcomes.every((o) => o.result === "unchanged") && still.length === 1,
`outcomes=${JSON.stringify(again.outcomes)} entries=${still.length}`,
"connecting a second time changes nothing, and the index still holds one entry",
still.length === 1 && still[0]?.object === bobsObject,
`entries=${JSON.stringify(still)}`,
);
},
});
@@ -478,22 +473,16 @@ async function main(): Promise<void> {
bob!.frame.evaluate((o) => window.__indexing.referConfigured(o), other),
);
const report = await step("Alice curating the unrelated reference", BRIDGE_MS, () =>
alice!.frame.evaluate((i) => window.__indexing.curate(i), index!),
);
const forOther = report.outcomes.find((o) => "object" in o && o.object === other);
check(
"curation reports it skipped for want of the field, rather than indexed",
forOther?.result === "skipped" && forOther.reason === "no-field",
`outcome=${JSON.stringify(forOther)}`,
await step("Alice connecting after the unrelated reference", BRIDGE_MS, () =>
alice!.frame.evaluate(() => window.__indexing.reconnect()),
);
const entries = await step("Alice reading the index once more", BRIDGE_MS, () =>
alice!.frame.evaluate((i) => window.__indexing.read(i), index!),
);
check(
"the index still holds exactly one entry",
entries.length === 1,
"the unrelated object is not indexed, and the index still holds exactly one entry",
entries.length === 1 && !entries.some((e) => e.object === other),
`entries=${JSON.stringify(entries)}`,
);
},
@@ -593,8 +582,8 @@ async function main(): Promise<void> {
await step("Bob depositing the hostile reference", BRIDGE_MS, () =>
bob!.frame.evaluate((o) => window.__indexing.referConfigured(o), object),
);
await step("Alice curating the hostile reference", BRIDGE_MS, () =>
alice!.frame.evaluate((i) => window.__indexing.curate(i), index!),
await step("Alice connecting after the hostile reference", BRIDGE_MS, () =>
alice!.frame.evaluate(() => window.__indexing.reconnect()),
);
// Read the index document RAW: it must still declare its own field. An injected