feat!: le paquet crée un index et lui ajoute une référence, rien de plus
This commit is contained in:
+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