feat!: le paquet crée un index et lui ajoute une référence, rien de plus

This commit is contained in:
Sylvain Duchesne
2026-08-21 19:56:11 +02:00
parent c2f9ff4674
commit ff78a70f14
26 changed files with 735 additions and 2651 deletions
+33 -14
View File
@@ -14,13 +14,27 @@
* ends up typed as a reference.
*/
import type { IndexEntry, UnionSubject } from "../src/index";
import type { UnionSubject } from "@ng-eventually/polyfill";
/**
* One entry, as THIS APPLICATION decodes one out of the index document.
*
* It is declared here and not imported, because `@ng-helpers/indexing` publishes no
* such type and no call that produces one: an index is an ordinary document, so its
* contents are whatever `readUnion` hands back, read with the two published IRIs.
* A subject carrying several values shows up as several rows — the document's own
* truth, and the application's business to make something of.
*/
export interface IndexRow {
readonly object: string;
readonly value: string;
}
/** What the leak probe observed — see `run.ts`'s last journey. */
export interface BrokenInboxOutcome {
/** The message `createIndex` rejected with, or `null` if it did not reject. */
/** The message `create` rejected with, or `null` if it did not reject. */
readonly rejected: string | null;
/** What `createIndex` returned, on the impossible branch where it did not reject. */
/** What `create` returned, on the impossible branch where it did not reject. */
readonly returned: string | null;
/** The documents that appeared in this identity's public store despite the failure. */
readonly appeared: readonly string[];
@@ -75,20 +89,25 @@ export interface IndexingBridge {
/** Publish a public document carrying one value for one predicate. */
publishObject(predicate: string, value: string): Promise<string>;
/** Hand the CONFIGURED index a reference to an object. Anyone may. */
referConfigured(object: string): Promise<void>;
addToConfigured(object: string): Promise<void>;
/** Hand a NAMED index a reference — used where no identity boundary is crossed. */
referTo(index: string, object: string): Promise<void>;
addTo(index: string, object: string): Promise<void>;
/**
* Connect again — obtain a fresh `Indexing` handle, which is what a page load does.
* 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.
* It reaches NOTHING — a handle is a session id and two acts, and building one talks
* to nobody. That is precisely what one journey asserts: taking a handle again changes
* nothing an index holds. It is NOT a settle point and cannot be used as one; what a
* journey waits on after a deposit is the INDEX, read like any other document (`run.ts`,
* `settled`).
*/
reconnect(): Promise<void>;
/** The index's entries, ordered by value. */
read(index: string): Promise<IndexEntry[]>;
rebuildHandle(): Promise<void>;
/**
* The index's entries, ordered by value — decoded BY THIS APPLICATION, out of an
* ordinary `readUnion` of the index document, with nothing from the library but the
* two IRIs it publishes. That the suite can do this at all is the claim under test.
*/
read(index: string): Promise<IndexRow[]>;
/** What a document literally holds, straight off `readUnion` — the write-form probe. */
readRaw(doc: string): Promise<UnionSubject[]>;
@@ -105,7 +124,7 @@ export interface IndexingBridge {
listPublicDocs(): Promise<string[]>;
/**
* `createIndex` with its inbox step made to fail — everything else real, against the
* `create` with its inbox step made to fail — everything else real, against the
* real broker. Answers whether a half-created index is left behind.
*/
createIndexWithBrokenInbox(field: string): Promise<BrokenInboxOutcome>;
+79 -41
View File
@@ -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);
}
+157 -41
View File
@@ -14,7 +14,7 @@
* makes it the control: if one round-trips and the other does not, the difference is
* the foreign subject and nothing else.
*
* 2. **A half-created index.** `createIndex` creates a document, writes its descriptor,
* 2. **A half-created index.** `create` creates a document, writes its descriptor,
* then opens its inbox. If the last step fails the caller gets an exception and no
* reference — but the document exists. The last journey injects that failure and
* asks the broker what was left behind.
@@ -28,6 +28,23 @@
* never happen — and does not happen here — is an inbox address crossing the identity
* boundary through a channel no deployment has.
*
* Waiting, because nothing here says when processing is done
* The two acts are `create` and `add`; what becomes of what is added is the business of
* the layer below, which applies a deposit when the owner's session is PUSHED one. There
* is no call that forces it, no receipt, and no `await` that covers it — by design. So a
* journey that has deposited WAITS for the index to hold the entry, reading it the way an
* application reads one (`settled`), and only then asserts. Before that wait existed the
* checks read the instant after the deposit and seven of them reported an index that was
* merely not written YET.
*
* The one claim that wait cannot carry: "an object carrying nothing for the field is not
* indexed". A deposit that is deliberately not indexed writes nothing, so no reading tells
* "examined and skipped" from "not examined yet", and there is nothing to wait for that is
* not a sleep. That journey reads straight away and its check is therefore weaker than its
* name — recorded here rather than hidden. What partly holds it up is order: that deposit
* precedes the hostile one, and the hostile one IS waited for, so by the end of the run the
* queue holding it has demonstrably been applied at least once.
*
* Reading a failure
* 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
@@ -77,7 +94,7 @@ type Frame = Awaited<ReturnType<typeof setupBrokerPage>>;
// 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.
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 is NOT built on — for the object that carries nothing usable. */
const UNRELATED = "urn:ng-helpers-e2e:unrelated";
// bounds
@@ -93,9 +110,48 @@ const BRIDGE_UP_MS = 60_000;
const READY_MS = 180_000;
/** One sign-in: a page, the broker round trip, and the application booting behind it. */
const SIGN_IN_MS = NEW_PAGE_MS + BROKER_ROUND_TRIP_MS + READY_MS;
/** One call across the bridge. The slowest here are curations, which round-trip per deposit. */
/** One call across the bridge. The slowest cross the broker once per deposit. */
const BRIDGE_MS = 4 * 60_000;
/** One journey. The longest holds two sign-ins' worth of work behind it. */
/**
* How often the index is READ while waiting for a deposit to have become an entry.
*
* Not a sleep standing in for the wait: it is the interval at which the CONDITION is asked,
* and the wait ends on the first reading that holds. `indexing-app.ts` polls its own store
* at the same interval, for the same reason.
*/
const SETTLE_POLL_MS = 500;
/**
* How long a deposit has to become an entry before the wait gives up and says what the
* index held instead.
*
* MEASURED on a green run (`E2E_TIMINGS=1`, 2026-08-21, one sample each): 0.6s for Bob's
* first deposit becoming an entry, 0.7s for the hostile one, and 0.0s for the entry
* reaching a stranger's own session — that last one had already converged by the time it
* was asked. The layer below is push-driven, so what is waited on is one push and one small
* write, not a broker crossing; that is why these are sub-second while a deposit or a
* publish measures 0.10.9s.
*
* Bounded at 60s ≈ 85x the slowest of them, generous on purpose. It must never fire on a
* slow-but-healthy broker, and it is what turns "the entry never came" into a named failure
* carrying the last reading — instead of a check that merely read too early, which is what
* the seven failures it was written for looked like.
*/
const SETTLE_MS = 60_000;
/**
* One journey. The longest holds two sign-ins' worth of work behind it.
*
* NOT the sum of the steps it encloses, and that is deliberate — the same arbitration
* `packages/polyfill/e2e/notebook.ts` records for its own: the longest journey here sums to
* 16 min of step bounds (four bridge calls), and a journey bounded above that would outlast
* the SUITE's own clock, so one hung journey would take the summary down with it. Every step
* inside a journey already carries a bound and names itself, so this catches only a hang in
* code no step wraps. What DOES have to hold is that this enclosure cannot fire BEFORE the
* settle point it now encloses, or a settle that gave up would report as an anonymous
* journey timeout: measured on a green run, the longest journey holding a settle takes 1.6s
* and the longest of all (a sign-in) 9.8s, so a journey that spends SETTLE_MS (60s) waiting
* still has more than eight minutes of this bound left — the settle is always the one that
* reports.
*/
const JOURNEY_MS = 10 * 60_000;
/** The whole run. A budget that cannot interrupt anything is not a budget. */
const SUITE_MS = 30 * 60_000;
@@ -126,7 +182,7 @@ const { check, journey, finish } = declareSuite({
],
},
{
name: "Bob hands the index a reference, and Alice's next connection curates it",
name: "Bob hands the index a reference, and it becomes an entry of Alice's index",
checks: [
"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",
@@ -204,6 +260,54 @@ function step<T>(what: string, ms: number, task: () => Promise<T>): Promise<T> {
return measured(what, ms, (bound) => within(what, bound, task));
}
/**
* Read the index until it HOLDS what the checks about to run expect, and hand that reading
* back to them.
*
* ── Why the harness needs this at all ───────────────────────────────────────
* Nothing on this package's surface says "processing is done", deliberately: the two acts
* are `create` and `add`, and what becomes of what is added belongs to the layer below,
* which applies a deposit when the owner's session is PUSHED one. There is no call that
* forces it and no receipt to hold. So a check that reads the instant after a deposit reads
* too early — which is exactly what seven of these checks were reporting.
*
* ── A wait is not a retry of the assertion ──────────────────────────────────
* `holds` asks one thing only: has the deposit ARRIVED. The checks then assert their own
* claims ONCE, on the reading this returns. A loop that re-ran the checks until one of them
* passed would turn a flapping result into a green one and report the reading that happened
* to suit it.
*
* ── It reads the way an application reads ───────────────────────────────────
* `read` is an ordinary `readUnion` of an ordinary document, or an anchored SELECT — the
* only two ways anyone has of reading an index, the harness included. Nothing here reaches
* for a way to hurry the layer below, because a consumer has none either.
*
* On expiry it throws, naming what never arrived and WHAT THE INDEX HELD INSTEAD: the
* journey's remaining checks are then reported unreached with that reason, so the run still
* reports its thirty rows and the reason is the observation rather than a bare "timeout".
*/
async function settled<T>(
what: string,
read: () => Promise<T>,
holds: (seen: T) => boolean,
describe: (seen: T) => string,
): Promise<T> {
return measured(what, SETTLE_MS, async (bound) => {
const deadline = Date.now() + bound;
for (;;) {
const seen = await within(what, bound, read);
if (holds(seen)) return seen;
if (Date.now() >= deadline) {
throw new Error(
`[e2e] ${what}: nothing arrived within ${(bound / 1000).toFixed(0)}s — ` +
`the last reading was ${describe(seen)}`,
);
}
await new Promise((r) => setTimeout(r, SETTLE_POLL_MS));
}
});
}
// an actor
interface Actor {
@@ -397,7 +501,7 @@ async function main(): Promise<void> {
});
await journey({
name: "Bob hands the index a reference, and Alice's next connection curates it",
name: "Bob hands the index a reference, and it becomes an entry of Alice's index",
needs: [
aliceIsUp,
bobIsUp,
@@ -408,21 +512,23 @@ async function main(): Promise<void> {
// Bob names his OWN object, and the index he was configured with. Nothing about
// the value travels: the deposit is the reference and nothing else.
await step("Bob depositing a reference", BRIDGE_MS, () =>
bob!.frame.evaluate((o) => window.__indexing.referConfigured(o), bobsObject!),
bob!.frame.evaluate((o) => window.__indexing.addToConfigured(o), bobsObject!),
);
// 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
// document, written into this one's anchored default graph. "The write did not
// throw" is not the same claim as "oxigraph stored it": this reads it back.
const raw = await step("Alice reading the index document back", BRIDGE_MS, () =>
alice!.frame.evaluate((d) => window.__indexing.readRaw(d), index!),
// NOBODY PROCESSES ANYTHING HERE, and nothing says when it is done: there is no
// such call on the surface, and no receipt. The reference becomes an entry because
// the layer below applies the inbox of the index Alice owns, when her session is
// pushed it — so the suite WAITS for the index to hold it, reading the document the
// way any application reads one.
//
// THE WRITE FORM, answered by the same reading. An entry is a triple whose subject
// is another document, written into this one's anchored default graph. "The write
// did not throw" is not the same claim as "oxigraph stored it": this reads it back.
const raw = await settled(
"Bob's deposit becoming an entry of Alice's index",
() => alice!.frame.evaluate((d) => window.__indexing.readRaw(d), index!),
(subjects) => subjects.some((s) => s.subject === bobsObject),
(subjects) => `subjects=${JSON.stringify(subjects.map((s) => s.subject))}`,
);
const entry = raw.find((s) => s.subject === bobsObject);
// The deposit is proven ARRIVED by its only possible effect: nobody but Alice
@@ -440,7 +546,7 @@ async function main(): Promise<void> {
`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.
// presence here means whatever processed that inbox 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"),
@@ -464,8 +570,15 @@ async function main(): Promise<void> {
// A public index is read by whoever holds its reference — including someone who
// owns neither it nor anything in it. This is the act an application performs.
const theirs = await step("Bob reading the index he does not own", BRIDGE_MS, () =>
bob!.frame.evaluate((i) => window.__indexing.read(i), index!),
//
// Waited for on its OWN account: the journey above settled ALICE's session, and
// Bob's is a different one reading a document he does not own. "The owner sees it"
// and "a stranger sees it" are two arrivals, and only one of them has happened.
const theirs = await settled(
"the entry reaching a stranger's session",
() => bob!.frame.evaluate((i) => window.__indexing.read(i), index!),
(rows) => rows.some((r) => r.object === bobsObject),
(rows) => `entries=${JSON.stringify(rows)}`,
);
check(
"Bob, who does not own the index, reads the same entry",
@@ -475,8 +588,8 @@ async function main(): Promise<void> {
// Deposits are never retired, so every run sees every deposit again. Convergence
// is what makes that affordable.
await step("Alice connecting a second time", BRIDGE_MS, () =>
alice!.frame.evaluate(() => window.__indexing.reconnect()),
await step("Alice loading her page a second time", BRIDGE_MS, () =>
alice!.frame.evaluate(() => window.__indexing.rebuildHandle()),
);
const still = await step("Alice reading the index again", BRIDGE_MS, () =>
alice!.frame.evaluate((i) => window.__indexing.read(i), index!),
@@ -504,13 +617,17 @@ async function main(): Promise<void> {
),
);
await step("Bob depositing the unrelated reference", BRIDGE_MS, () =>
bob!.frame.evaluate((o) => window.__indexing.referConfigured(o), other),
);
await step("Alice connecting after the unrelated reference", BRIDGE_MS, () =>
alice!.frame.evaluate(() => window.__indexing.reconnect()),
bob!.frame.evaluate((o) => window.__indexing.addToConfigured(o), other),
);
// NOTHING TO WAIT FOR, and it is worth saying rather than dressing up. A deposit
// that is deliberately not indexed writes nothing, so the index holds afterwards
// exactly what it held before and no reading can tell "examined and skipped" from
// "not examined yet". A wait for the state the check expects would return on its
// first reading and prove nothing; a wait for a duration would be a sleep. So this
// reads straight away, and the check is a weaker claim than it reads as — see the
// suite's header. What DOES hold it: the deposit is applied before the hostile one
// below, and that one is waited for.
const entries = await step("Alice reading the index once more", BRIDGE_MS, () =>
alice!.frame.evaluate((i) => window.__indexing.read(i), index!),
);
@@ -564,7 +681,7 @@ async function main(): Promise<void> {
const refused = await step("Alice trying to deposit into it", BRIDGE_MS, async () => {
try {
await alice!.frame.evaluate(
([i, o]) => window.__indexing.referTo(i!, o!),
([i, o]) => window.__indexing.addTo(i!, o!),
[leaked, bobsObject ?? leaked],
);
return null;
@@ -614,18 +731,17 @@ async function main(): Promise<void> {
);
await step("Bob depositing the hostile reference", BRIDGE_MS, () =>
bob!.frame.evaluate((o) => window.__indexing.referConfigured(o), object),
bob!.frame.evaluate((o) => window.__indexing.addToConfigured(o), object),
);
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
// `DROP ALL` that had taken effect would show up exactly here, as a descriptor
// that is no longer there — and `read()` alone could not tell that apart from an
// ordinary failure.
const after = await step("Alice reading the index after the hostile entry", BRIDGE_MS, () =>
alice!.frame.evaluate((d) => window.__indexing.readRaw(d), index!),
// Waited for as the first deposit was, and read RAW: the index must still declare
// its own field. An injected `DROP ALL` that had taken effect would show up exactly
// here, as a descriptor that is no longer there — and `read()` alone could not tell
// that apart from an ordinary failure.
const after = await settled(
"the hostile deposit becoming an entry",
() => alice!.frame.evaluate((d) => window.__indexing.readRaw(d), index!),
(subjects) => subjects.some((s) => s.subject === object),
(subjects) => `subjects=${JSON.stringify(subjects.map((s) => s.subject))}`,
);
const entry = after.find((s) => s.subject === object)?.props[ENTRY_VALUE] ?? [];
const descriptor = after.find((s) => s.subject === index)?.props[INDEX_FIELD] ?? [];