feat!: le paquet crée un index et lui ajoute une référence, rien de plus
This commit is contained in:
+33
-14
@@ -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
@@ -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
@@ -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.1–0.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] ?? [];
|
||||
|
||||
Reference in New Issue
Block a user