docs: ce que l'ORM offre pour ecrire et supprimer, et la vraie cause du useShape mort
This commit is contained in:
@@ -0,0 +1,299 @@
|
||||
/**
|
||||
* The page for the LIVE ORM measurement — the one thing `e2e/polyfill-entry.ts` cannot do.
|
||||
*
|
||||
* That harness injects a hand-written Set-like into `configure({ useShape })`, because the
|
||||
* read filter is pure and does not need the real ORM to be exercised. What it therefore
|
||||
* never touches is the only hop that reaches the broker: `ng.orm_start_graph`, the streamed
|
||||
* method `OrmSubscription` opens and the one `bug_useshape-live-read-needs-a-newer-broker`
|
||||
* is about. So this page injects the REAL `@ng-org/orm` — nothing else about the bootstrap
|
||||
* differs — and the published `useShape` runs over a live subscription.
|
||||
*
|
||||
* ── The dependency, stated because it is not installed by default ──────────
|
||||
* `@ng-org/orm` is an OPTIONAL PEER of this package (`package.json`,
|
||||
* `peerDependenciesMeta`), so `bun install` does not fetch it and `bun.lock` records it
|
||||
* under `optionalPeers` with no resolution. This page is the only thing in the repository
|
||||
* that imports it: `e2e/run.ts` and its harness do not, so a checkout without it still
|
||||
* builds and runs the 42-check suite. Here the bundle simply will not build, and the
|
||||
* runner says so rather than reporting a broker verdict it did not measure.
|
||||
*
|
||||
* ── What the framework adapter contributes, and why it is not here ─────────
|
||||
* Upstream's `useShape` is React's (`sdk/js/orm/src/frontendAdapters/react/useShape.ts`):
|
||||
* it calls `OrmSubscription.getOrCreate(shape, scope)`, hands `signalObject` to
|
||||
* `useDeepSignal`, and closes the subscription on unmount. The broker sees the first of
|
||||
* those three and nothing else — `useDeepSignal` is re-render bookkeeping. So the hook
|
||||
* injected below IS `getOrCreate(...).signalObject`, which is what a React render reaches
|
||||
* the broker with, without a React tree to mount.
|
||||
*/
|
||||
|
||||
import { ng as realNg, init as realInit } from "@ng-org/web";
|
||||
import { initNg as ormInitNg, OrmSubscription, type Scope as OrmScope } from "@ng-org/orm";
|
||||
import { configure, docs, init, useShape as libUseShape } from "@ng-eventually/polyfill";
|
||||
import type { BaseType, Nuri, ShapeType } from "@ng-eventually/polyfill";
|
||||
import { adoptCurrentUser, configureStoreRegistry } from "../src/shared-wallet/bootstrap";
|
||||
|
||||
// ── the shape under measurement ─────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* The smallest shape that can be matched at all, written by hand rather than generated.
|
||||
*
|
||||
* `rdf-orm build` turns a `.shex` file into exactly this object — `{ schema, shape }`
|
||||
* (`sdk/js/shex-orm/src/types.ts:ShapeType`) — and the generator is not what is under
|
||||
* measurement here. Two predicates, both `minCardinality: 1`, because that is what
|
||||
* `engine/verifier/src/orm/graph/query.rs:schema_shape_to_sparql` turns into required
|
||||
* triple patterns: a shape with none of them would match every subject in the document.
|
||||
*/
|
||||
const LIVE_SHAPE = {
|
||||
shape: "did:ng:z:E2ELiveShape",
|
||||
schema: {
|
||||
"did:ng:z:E2ELiveShape": {
|
||||
iri: "did:ng:z:E2ELiveShape",
|
||||
predicates: [
|
||||
{
|
||||
iri: "http://www.w3.org/1999/02/22-rdf-syntax-ns#type",
|
||||
readablePredicate: "@type",
|
||||
dataTypes: [{ valType: "iri", literals: ["did:ng:z:E2ELive"] }],
|
||||
minCardinality: 1,
|
||||
maxCardinality: 1,
|
||||
},
|
||||
{
|
||||
iri: "did:ng:z:liveMark",
|
||||
readablePredicate: "liveMark",
|
||||
dataTypes: [{ valType: "string" }],
|
||||
minCardinality: 1,
|
||||
maxCardinality: 1,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
} satisfies ShapeType<BaseType>;
|
||||
|
||||
const TYPE_IRI = "http://www.w3.org/1999/02/22-rdf-syntax-ns#type";
|
||||
|
||||
// ── the broker session ──────────────────────────────────────────────────────
|
||||
|
||||
interface BrokerSession {
|
||||
session_id: string;
|
||||
private_store_id: string;
|
||||
protected_store_id: string;
|
||||
public_store_id: string;
|
||||
[k: string]: unknown;
|
||||
}
|
||||
|
||||
let session: BrokerSession | null = null;
|
||||
let sessionResolve!: (s: BrokerSession) => void;
|
||||
const sessionReady = new Promise<BrokerSession>((r) => (sessionResolve = r));
|
||||
|
||||
/**
|
||||
* The injected hook — the real ORM's broker-facing half.
|
||||
*
|
||||
* It keeps the subscription aside so the probe can await `readyPromise`, which is the ORM's
|
||||
* own answer to "did the initial payload arrive": it resolves on that path and on no other
|
||||
* (`sdk/js/orm/src/connector/GraphOrmSubscription.ts:handleInitialResponse`), so a start
|
||||
* the broker refused leaves it pending for ever — the silence this measurement is here to
|
||||
* break.
|
||||
*/
|
||||
let liveSubscription: OrmSubscription<BaseType> | null = null;
|
||||
function realOrmUseShape(shapeType: unknown, scope: unknown): unknown {
|
||||
const sub = OrmSubscription.getOrCreate(shapeType as ShapeType<BaseType>, scope as OrmScope);
|
||||
liveSubscription = sub;
|
||||
return sub.signalObject;
|
||||
}
|
||||
|
||||
configure({
|
||||
ng: realNg,
|
||||
useShape: realOrmUseShape,
|
||||
init: realInit,
|
||||
sharedWallet: { fileUrl: "/harness-not-used.ngw", password: "harness" },
|
||||
});
|
||||
adoptCurrentUser("e2e-orm-live");
|
||||
configureStoreRegistry({
|
||||
getSession: async () => {
|
||||
const s = session ?? (await sessionReady);
|
||||
return {
|
||||
sessionId: s.session_id,
|
||||
privateStoreId: s.private_store_id,
|
||||
protectedStoreId: s.protected_store_id,
|
||||
publicStoreId: s.public_store_id,
|
||||
};
|
||||
},
|
||||
normalizeId: (id: string) => id.trim().replace(/^@/, "").toLowerCase(),
|
||||
pointerGuard: { attempts: 8, baseMs: 150, maxStepMs: 2000 },
|
||||
});
|
||||
|
||||
// ── the probe ───────────────────────────────────────────────────────────────
|
||||
|
||||
const state: { status: string; error?: string } = { status: "connecting" };
|
||||
|
||||
interface LiveProbe {
|
||||
doc: Nuri;
|
||||
subject: string;
|
||||
/** What the published `useShape` returned — a Set-like of the matched objects. */
|
||||
view: Set<BaseType>;
|
||||
startError: string | null;
|
||||
readyMs: number | null;
|
||||
deleteError: string | null;
|
||||
}
|
||||
let probe: LiveProbe | null = null;
|
||||
|
||||
/** Count the triples the document still holds for the seeded subject. */
|
||||
async function seededTripleCount(doc: Nuri, subject: string): Promise<number> {
|
||||
const s = await sessionReady;
|
||||
const res = await docs.sparqlQuery(
|
||||
s.session_id,
|
||||
`SELECT ?p ?o WHERE { <${subject}> ?p ?o }`,
|
||||
undefined,
|
||||
doc,
|
||||
);
|
||||
const rows = Array.isArray(res)
|
||||
? res
|
||||
: ((res as { results?: { bindings?: unknown[] } })?.results?.bindings ?? []);
|
||||
return rows.length;
|
||||
}
|
||||
|
||||
(window as unknown as { __sdk: Record<string, unknown> }).__sdk = {
|
||||
status: () => state.status,
|
||||
error: () => state.error ?? null,
|
||||
|
||||
/**
|
||||
* Seed a document with one shape-matching object, then open the live read through the
|
||||
* PUBLISHED `useShape`.
|
||||
*
|
||||
* The subject IRI follows the ORM's own generator
|
||||
* (`GraphOrmSubscription.ts:signalObjectPropGenerator`): the document NURI's first 53
|
||||
* characters, then `:q:`, then a name. Nothing requires that form of a hand-written
|
||||
* SPARQL insert, and using it removes one way for this measurement to differ from what
|
||||
* the ORM itself would have written.
|
||||
*/
|
||||
async liveStart() {
|
||||
const s = await sessionReady;
|
||||
const doc = (await docs.docCreate(
|
||||
s.session_id,
|
||||
"Graph",
|
||||
"data:graph",
|
||||
"store",
|
||||
undefined,
|
||||
)) as Nuri;
|
||||
const subject = `${doc.substring(0, 53)}:q:e2eLiveRead`;
|
||||
await docs.sparqlUpdate(
|
||||
s.session_id,
|
||||
`INSERT DATA { <${subject}> <${TYPE_IRI}> <did:ng:z:E2ELive> ; <did:ng:z:liveMark> "measured" }`,
|
||||
doc,
|
||||
);
|
||||
const seeded = await seededTripleCount(doc, subject);
|
||||
|
||||
// The ORM reaches the SDK through its own module-level promise, resolved once.
|
||||
ormInitNg(realNg, s as unknown as Parameters<typeof ormInitNg>[1]);
|
||||
|
||||
let startError: string | null = null;
|
||||
let view: Set<BaseType>;
|
||||
const t0 = Date.now();
|
||||
try {
|
||||
view = libUseShape(LIVE_SHAPE, { graphs: [doc] }) as Set<BaseType>;
|
||||
} catch (e) {
|
||||
startError = e instanceof Error ? e.message : String(e);
|
||||
probe = { doc, subject, view: new Set(), startError, readyMs: null, deleteError: null };
|
||||
return { doc, subject, seeded, startError };
|
||||
}
|
||||
probe = { doc, subject, view, startError: null, readyMs: null, deleteError: null };
|
||||
// Un-awaited on purpose: a refused start never resolves this, and the runner's own
|
||||
// bound is what turns that silence into a verdict.
|
||||
void liveSubscription?.readyPromise.then(() => {
|
||||
if (probe) probe.readyMs = Date.now() - t0;
|
||||
});
|
||||
return { doc, subject, seeded, startError: null };
|
||||
},
|
||||
|
||||
/** How many objects the published `useShape` view holds right now. */
|
||||
liveCount: () => (probe ? probe.view.size : -1),
|
||||
/** Milliseconds from the `useShape` call to `readyPromise`, or null while it is pending. */
|
||||
liveReadyMs: () => probe?.readyMs ?? null,
|
||||
liveItems: () =>
|
||||
probe
|
||||
? [...probe.view].map((i) => ({
|
||||
id: i["@id"] as string,
|
||||
graph: i["@graph"] as string,
|
||||
mark: i.liveMark as string,
|
||||
}))
|
||||
: [],
|
||||
|
||||
/**
|
||||
* Delete the seeded object through the set the hook returned — the gesture the consuming
|
||||
* application reports as the shape for removal.
|
||||
*/
|
||||
liveDelete() {
|
||||
if (!probe) return { deleted: false, error: "no probe" };
|
||||
const item = [...probe.view][0];
|
||||
if (item === undefined) return { deleted: false, error: "view is empty" };
|
||||
try {
|
||||
const deleted = probe.view.delete(item);
|
||||
return { deleted, error: null };
|
||||
} catch (e) {
|
||||
const error = e instanceof Error ? e.message : String(e);
|
||||
probe.deleteError = error;
|
||||
return { deleted: false, error };
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* CHARACTERISATION of the deployed broker's `orm_start_graph` ARITY.
|
||||
*
|
||||
* The failure the runner reports names a parameter — `config` — that the ORM does not
|
||||
* send and that `nextgraph-rs` at the read commit does not declare. Two upstream
|
||||
* arrangements could put one there, and they differ by POSITION:
|
||||
* - config LAST, after the callback (upstream 2026-04-12 to 2026-04-21) — an
|
||||
* unsupplied `config`, never a Function;
|
||||
* - config at index 4, BEFORE the callback — the 5-argument call from `@ng-org/orm`
|
||||
* then lands its callback exactly there, which is what the error describes.
|
||||
*
|
||||
* So this calls the SDK member directly with an empty config in position 4. It does not
|
||||
* go through the ORM, which is fixed at five arguments — this is the one question the
|
||||
* ORM cannot ask. The app-side proxy will mistake the `{}` for the stream callback, so
|
||||
* nothing usable comes back; what is being read is only whether the engine still
|
||||
* refuses on `config`.
|
||||
*/
|
||||
async liveStartWithConfig() {
|
||||
const s = await sessionReady;
|
||||
const doc = (await docs.docCreate(
|
||||
s.session_id,
|
||||
"Graph",
|
||||
"data:graph",
|
||||
"store",
|
||||
undefined,
|
||||
)) as Nuri;
|
||||
// Six arguments against a declaration that has five: the declaration describes the
|
||||
// arity of the PUBLISHED client, and the arity of the DEPLOYED engine is the question.
|
||||
// Widened through `unknown`, never `any` — the call is deliberate, not unchecked.
|
||||
const startGraph = realNg.orm_start_graph as unknown as (
|
||||
...a: unknown[]
|
||||
) => Promise<unknown>;
|
||||
try {
|
||||
await startGraph([doc], [], LIVE_SHAPE, s.session_id, {}, () => {});
|
||||
return { doc, error: null };
|
||||
} catch (e) {
|
||||
return { doc, error: e instanceof Error ? e.message : String(e) };
|
||||
}
|
||||
},
|
||||
|
||||
/** The document's own answer: how many triples the seeded subject still has. */
|
||||
async liveSeededTriples() {
|
||||
if (!probe) return -1;
|
||||
return seededTripleCount(probe.doc, probe.subject);
|
||||
},
|
||||
};
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
await init(
|
||||
(event: { session: BrokerSession }) => {
|
||||
session = event.session;
|
||||
state.status = "connected";
|
||||
sessionResolve(session);
|
||||
},
|
||||
true,
|
||||
[],
|
||||
);
|
||||
} catch (e) {
|
||||
state.status = "error";
|
||||
state.error = e instanceof Error ? e.message : String(e);
|
||||
}
|
||||
})();
|
||||
@@ -0,0 +1,305 @@
|
||||
/**
|
||||
* Does a live `useShape` read reach the DEPLOYED broker, and does deleting from the set it
|
||||
* returns reach the document?
|
||||
*
|
||||
* Standalone, and deliberately outside `e2e/run.ts`: this is the one measurement that needs
|
||||
* `@ng-org/orm`, an OPTIONAL PEER this package does not install (`bun.lock`,
|
||||
* `optionalPeers`). Folding it into the 42-check suite would make that suite unbuildable on
|
||||
* a checkout without it. Here, a missing ORM fails the build of this runner alone, with the
|
||||
* reason said out loud — which is the honest outcome, because a measurement that did not
|
||||
* run must not be reported as a verdict either way.
|
||||
*
|
||||
* Run: `bun run e2e/orm-live-read.ts` from `packages/polyfill`.
|
||||
*
|
||||
* ── What it decides ────────────────────────────────────────────────────────
|
||||
* The verdict is readable in one number: does `readyPromise` settle. It does on one path
|
||||
* only — the arrival of an initial payload from the engine
|
||||
* (`GraphOrmSubscription.ts:handleInitialResponse`) — so a refused start leaves it pending
|
||||
* for ever, which is exactly why the failure is silent and why a bound, not an await, is
|
||||
* what turns it into a report. Nothing here infers a verdict from a version string.
|
||||
*
|
||||
* ── What it found, 2026-09-16, and why the last row exists ─────────────────
|
||||
* The read does NOT start, and the reason is not the one
|
||||
* `bug_useshape-live-read-needs-a-newer-broker` names. That leaf's mechanism — a broker
|
||||
* built before `orm_start` was renamed `orm_start_graph` — is REFUTED here: the method
|
||||
* resolves, and the call fails INSIDE it, on an argument. The engine answers
|
||||
* `Deserialization error of config Error: invalid type: JsValue(Function), expected any
|
||||
* valid JSON value`.
|
||||
*
|
||||
* The last row establishes where that `config` sits, because the position is the whole
|
||||
* diagnosis: calling the SDK member directly with `{}` in position 4 is ACCEPTED, and the
|
||||
* engine then streams back. So the deployed `orm_start_graph` is
|
||||
* `(graph_scope, subject_scope, shapeType, session_id, config, callback)`, and the
|
||||
* five-argument call the published `@ng-org/orm` makes lands its callback in `config`.
|
||||
* That arity is in no commit of `nextgraph-rs` at the read revision — the one window that
|
||||
* had a `config` at all (2026-04-12 to 2026-04-21) put it LAST, after the callback. The
|
||||
* deployment is therefore AHEAD of the client packages this repository pins, not behind
|
||||
* them, which inverts what the leaf says to do about it.
|
||||
*
|
||||
* The run is EXPECTED TO EXIT NON-ZERO while that holds: it is a measurement of a
|
||||
* deployment, not a gate on this package. Green again means the two ends agree, and the
|
||||
* bug leaf can close.
|
||||
*/
|
||||
|
||||
import * as fs from "node:fs";
|
||||
import * as path from "node:path";
|
||||
import { execSync } from "node:child_process";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import {
|
||||
armSuiteDeadline,
|
||||
closeContext,
|
||||
closeQuietly,
|
||||
launchWatchedContext,
|
||||
newPage,
|
||||
serveOnEphemeralPort,
|
||||
setupBrokerPage,
|
||||
within,
|
||||
type BrowserContext,
|
||||
type Frame,
|
||||
type Page,
|
||||
} from "ng-e2e-helpers";
|
||||
import { WALLET, mintBatchWallet } from "./harness-page";
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const ENTRY = path.resolve(__dirname, "orm-live-entry.ts");
|
||||
const BUNDLE_OUT = path.resolve(__dirname, ".dist", "orm-live-entry.js");
|
||||
|
||||
// ── bounds ──────────────────────────────────────────────────────────────────
|
||||
|
||||
/** `bun build` is a local bundle; a minute is already ten times what it takes. */
|
||||
const BUILD_MS = 60_000;
|
||||
/**
|
||||
* The whole measurement. Five checks against one fresh wallet, where the expensive parts
|
||||
* are the broker login and one document creation — the 42-check suite pays both and
|
||||
* measures 3.4 min in total, so eight minutes here is the point past which this has
|
||||
* stopped being a measurement of anything.
|
||||
*/
|
||||
const SUITE_DEADLINE_MS = 8 * 60 * 1000;
|
||||
/** One `__sdk` bridge call. `frame.evaluate()` has no deadline of its own. */
|
||||
const BRIDGE_MS = 2 * 60 * 1000;
|
||||
/**
|
||||
* How long a live read may take to deliver its initial payload.
|
||||
*
|
||||
* NOT yet computed from a measured latency of this path, and it cannot be while the start
|
||||
* is refused: nothing has ever settled here to measure. It is sized against the duration
|
||||
* the step before it is allowed — the broker login, bounded at 45s in `ng-e2e-helpers` and
|
||||
* measured at a few seconds — so a read that has not answered in 60s has not answered
|
||||
* because it was slow. The first run that settles prints `ready in Nms`; set this to a
|
||||
* generous multiple of that figure then, and say so here.
|
||||
*/
|
||||
const READY_MS = 60_000;
|
||||
/**
|
||||
* How long the delete may take to reach the document. Same status, and the write it waits
|
||||
* on is the one `e2e/run.ts` measures elsewhere at a second or two against this broker.
|
||||
*/
|
||||
const DELETE_MS = 30_000;
|
||||
|
||||
// ── what this run reports ───────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Every check, declared here so the run's total is known BEFORE the browser launches — the
|
||||
* convention `e2e/notebook.ts` states at length: a run that dies in its setup still has a
|
||||
* total to be compared against, so a difference between two runs is always a real one.
|
||||
*/
|
||||
const CHECKS: readonly string[] = [
|
||||
"the document accepted the seed (two triples for one subject)",
|
||||
"the live read STARTS: `useShape` returns a set and the subscription reports ready",
|
||||
"the initial payload carries the seeded object, with its `@id` and `@graph`",
|
||||
"`.delete()` on that set is accepted",
|
||||
"the deletion reached the DOCUMENT: the seeded subject's triples are gone",
|
||||
"[finding] the ARITY the deployed `orm_start_graph` expects",
|
||||
];
|
||||
|
||||
type Check = { name: string; ok: boolean; detail?: string };
|
||||
const results: Check[] = [];
|
||||
function check(name: string, ok: boolean, detail?: string): void {
|
||||
results.push({ name, ok, detail });
|
||||
console.log(` [${ok ? "PASS" : "FAIL"}] ${name}${detail ? " — " + detail : ""}`);
|
||||
}
|
||||
/**
|
||||
* A row that REPORTS rather than judges — `e2e/run.ts`'s convention for characterising the
|
||||
* broker rather than asserting about this package. The last row is one: whichever arity the
|
||||
* deployment expects, it is upstream's to change, so failing on it would only teach whoever
|
||||
* reads the report to ignore this file.
|
||||
*/
|
||||
function record(name: string, detail: string): void {
|
||||
results.push({ name, ok: true, detail });
|
||||
console.log(` [INFO] ${name} — ${detail}`);
|
||||
}
|
||||
|
||||
/** The bridge as the browser side of `frame.evaluate` sees it — no `any`, and no import
|
||||
* the serialized callback could not carry across. */
|
||||
type Bridge = Record<string, (...a: unknown[]) => unknown>;
|
||||
declare global {
|
||||
interface Window {
|
||||
__sdk?: Bridge;
|
||||
}
|
||||
}
|
||||
|
||||
function sdk<T>(frame: Frame, method: string, ...args: unknown[]): Promise<T> {
|
||||
return within(`__sdk.${method}() in the broker iframe`, BRIDGE_MS, () =>
|
||||
frame.evaluate(
|
||||
([m, a]) => window.__sdk![m as string]!(...(a as unknown[])),
|
||||
[method, args] as const,
|
||||
),
|
||||
) as Promise<T>;
|
||||
}
|
||||
|
||||
function buildBundle(): void {
|
||||
fs.mkdirSync(path.dirname(BUNDLE_OUT), { recursive: true });
|
||||
execSync(`bun build ${ENTRY} --outfile ${BUNDLE_OUT} --bundle --format=esm`, {
|
||||
stdio: "pipe",
|
||||
cwd: path.resolve(__dirname, ".."),
|
||||
timeout: BUILD_MS,
|
||||
});
|
||||
}
|
||||
|
||||
function serve(): Promise<{ url: string; close: () => void }> {
|
||||
const bundle = fs.readFileSync(BUNDLE_OUT, "utf-8");
|
||||
const html = `<!DOCTYPE html><html><head><meta charset="utf-8"><title>ng-eventually ORM live read</title></head><body><div id="root"></div><script type="module" src="/orm-live-entry.js"></script></body></html>`;
|
||||
return serveOnEphemeralPort((req, res) => {
|
||||
if (req.url === "/orm-live-entry.js") {
|
||||
res.writeHead(200, { "Content-Type": "application/javascript; charset=utf-8" });
|
||||
res.end(bundle);
|
||||
} else {
|
||||
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
||||
res.end(html);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function main(): Promise<void> {
|
||||
armSuiteDeadline("the ORM live-read measurement", SUITE_DEADLINE_MS);
|
||||
const started = Date.now();
|
||||
|
||||
console.log("[orm-live] building the page bundle...");
|
||||
try {
|
||||
buildBundle();
|
||||
} catch (e) {
|
||||
console.error(
|
||||
"[orm-live] the bundle did not build. `@ng-org/orm` is an OPTIONAL PEER of this " +
|
||||
"package and `bun install` does not fetch it — install it in packages/polyfill " +
|
||||
"before running this measurement. NOTHING was measured, so nothing is concluded.",
|
||||
);
|
||||
console.error(String((e as { stderr?: Buffer })?.stderr ?? e));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log("[orm-live] minting this run's wallet...");
|
||||
const wallet = await mintBatchWallet("the ORM live-read measurement (e2e/orm-live-read.ts)");
|
||||
const { url, close: closeServer } = await serve();
|
||||
console.log(`[orm-live] page served at ${url}`);
|
||||
|
||||
let ctx: BrowserContext | null = null;
|
||||
let page: Page | null = null;
|
||||
try {
|
||||
ctx = await launchWatchedContext("orm-live", wallet.dir);
|
||||
page = await newPage("the ORM live-read page", ctx);
|
||||
page.on("pageerror", (e) => console.error("[iframe error]", e.message));
|
||||
page.on("console", (m) => {
|
||||
if (m.type() === "error") console.error("[iframe console]", m.text());
|
||||
});
|
||||
|
||||
const frame = await setupBrokerPage(page, url, WALLET.password);
|
||||
await frame.waitForFunction(() => window.__sdk !== undefined, { timeout: 30_000 });
|
||||
await frame.waitForFunction(() => window.__sdk!.status!() === "connected", {
|
||||
timeout: 60_000,
|
||||
});
|
||||
|
||||
const start = await sdk<{
|
||||
doc: string;
|
||||
subject: string;
|
||||
seeded: number;
|
||||
startError: string | null;
|
||||
}>(frame, "liveStart");
|
||||
check(CHECKS[0]!, start.seeded === 2, `doc=${start.doc} triples=${start.seeded}`);
|
||||
|
||||
// The verdict. A refused start never settles `readyPromise`, so this wait IS the
|
||||
// measurement — and the bound is what turns "for ever" into a reported failure.
|
||||
let readyMs: number | null = null;
|
||||
try {
|
||||
await frame.waitForFunction(() => window.__sdk!.liveReadyMs!() !== null, {
|
||||
timeout: READY_MS,
|
||||
});
|
||||
readyMs = await sdk<number | null>(frame, "liveReadyMs");
|
||||
} catch {
|
||||
readyMs = null;
|
||||
}
|
||||
check(
|
||||
CHECKS[1]!,
|
||||
start.startError === null && readyMs !== null,
|
||||
start.startError !== null
|
||||
? `useShape threw: ${start.startError}`
|
||||
: readyMs === null
|
||||
? `readyPromise still pending after ${READY_MS / 1000}s — the engine refused the ` +
|
||||
"start; the next row says what it expected instead"
|
||||
: `ready in ${readyMs}ms`,
|
||||
);
|
||||
|
||||
const items = await sdk<Array<{ id: string; graph: string; mark: string }>>(frame, "liveItems");
|
||||
const seen = items[0];
|
||||
check(
|
||||
CHECKS[2]!,
|
||||
items.length === 1 && seen?.id === start.subject && seen?.graph === start.doc,
|
||||
`items=${JSON.stringify(items)}`,
|
||||
);
|
||||
|
||||
const del = await sdk<{ deleted: boolean; error: string | null }>(frame, "liveDelete");
|
||||
check(CHECKS[3]!, del.deleted === true && del.error === null, `deleted=${del.deleted} error=${del.error}`);
|
||||
|
||||
let remaining = await sdk<number>(frame, "liveSeededTriples");
|
||||
try {
|
||||
await within("the deletion reaching the document", DELETE_MS, async () => {
|
||||
const deadline = Date.now() + DELETE_MS;
|
||||
while (remaining > 0 && Date.now() < deadline) {
|
||||
await page!.waitForTimeout(1000);
|
||||
remaining = await sdk<number>(frame, "liveSeededTriples");
|
||||
}
|
||||
});
|
||||
} catch {
|
||||
/* the count below is the verdict either way */
|
||||
}
|
||||
check(CHECKS[4]!, remaining === 0, `triples left for the seeded subject: ${remaining}`);
|
||||
|
||||
// Only worth asking when the five-argument call did NOT start: it is the diagnosis of
|
||||
// that failure, and a deployment that answers the published arity has nothing to
|
||||
// characterise.
|
||||
if (readyMs === null) {
|
||||
const withConfig = await sdk<{ doc: string; error: string | null }>(
|
||||
frame,
|
||||
"liveStartWithConfig",
|
||||
);
|
||||
record(
|
||||
CHECKS[5]!,
|
||||
withConfig.error === null
|
||||
? "six arguments with `{}` in position 4 are ACCEPTED — the deployed " +
|
||||
"`orm_start_graph` takes a `config` BEFORE the callback, so the published " +
|
||||
"client's five-argument call puts its callback there"
|
||||
: `six arguments were refused too: ${withConfig.error}`,
|
||||
);
|
||||
} else {
|
||||
record(CHECKS[5]!, "not asked — the published five-argument call started");
|
||||
}
|
||||
} finally {
|
||||
if (page) await closeQuietly("the ORM live-read page", () => page!.close());
|
||||
if (ctx) await closeContext("orm-live", ctx);
|
||||
closeServer();
|
||||
wallet.discard();
|
||||
}
|
||||
|
||||
const passed = results.filter((r) => r.ok).length;
|
||||
const failed = results.length - passed;
|
||||
console.log(
|
||||
`\n══ ORM live-read summary: ${passed} passed, ${failed} failed, ${CHECKS.length} declared ` +
|
||||
`— run ${((Date.now() - started) / 60000).toFixed(1)} min ══`,
|
||||
);
|
||||
if (failed > 0 || results.length !== CHECKS.length) {
|
||||
for (const r of results.filter((x) => !x.ok)) console.log(` - ${r.name}: ${r.detail ?? ""}`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((e) => {
|
||||
console.error("[orm-live] fatal:", e);
|
||||
process.exit(1);
|
||||
});
|
||||
Reference in New Issue
Block a user